next-intl 4.1.0 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -62,12 +62,15 @@ function compileLocalizedPathname({
62
62
 
63
63
  // Clean up optional catch-all segments that were not replaced
64
64
  compiled = compiled.replace(/\[\[\.\.\..+\]\]/g, '');
65
- compiled = normalizeTrailingSlash(compiled);
66
65
  if (compiled.includes('[')) {
67
66
  // Next.js throws anyway, therefore better provide a more helpful error message
68
67
  throw new Error(`Insufficient params provided for localized pathname.\nTemplate: ${template}\nParams: ${JSON.stringify(params)}`);
69
68
  }
69
+ compiled = normalizeTrailingSlash(compiled);
70
+ compiled = encodePathname(compiled);
70
71
  if (query) {
72
+ // This also encodes non-ASCII characters by
73
+ // using `new URLSearchParams()` internally
71
74
  compiled += serializeSearchParams(query);
72
75
  }
73
76
  return compiled;
@@ -90,6 +93,29 @@ function compileLocalizedPathname({
90
93
  return result;
91
94
  }
92
95
  }
96
+ function encodePathname(pathname) {
97
+ // Generally, to comply with RFC 3986 and Google's best practices for URL structures
98
+ // (https://developers.google.com/search/docs/crawling-indexing/url-structure),
99
+ // we should always encode non-ASCII characters.
100
+ //
101
+ // There are two places where next-intl interacts with potentially non-ASCII URLs:
102
+ // 1. Middleware: When mapping a localized pathname to a non-localized pathname internally
103
+ // 2. Navigation APIs: When generating a URLs to be used for <Link /> & friends
104
+ //
105
+ // Next.js normalizes incoming pathnames to always be encoded, therefore we can safely
106
+ // decode them there (see middleware.tsx). On the other hand, Next.js doesn't consistently
107
+ // encode non-ASCII characters that are passed to navigation APIs:
108
+ // 1. <Link /> doesn't encode non-ASCII characters
109
+ // 2. useRouter() uses `new URL()` internally, which will encode—but only if necessary
110
+ // 3. redirect() uses useRouter() on the client, but on the server side only
111
+ // assigns the location header without encoding.
112
+ //
113
+ // In addition to this, for getPathname() we need to encode non-ASCII characters.
114
+ //
115
+ // Therefore, the bottom line is that next-intl should take care of encoding non-ASCII
116
+ // characters in all cases, but can rely on `new URL()` to not double-encode characters.
117
+ return pathname.split('/').map(segment => encodeURIComponent(segment)).join('/');
118
+ }
93
119
  function getRoute(locale, pathname, pathnames) {
94
120
  const sortedPathnames = getSortedPathnames(Object.keys(pathnames));
95
121
  const decoded = decodeURI(pathname);
@@ -1 +1 @@
1
- import{getSortedPathnames as e,matchesPathname as n,isLocalizableHref as t,prefixPathname as r,getLocalizedTemplate as o,normalizeTrailingSlash as a,getLocalePrefix as i}from"../../shared/utils.js";function c(e){return"string"==typeof e?{pathname:e}:e}function s(e){function n(e){return String(e)}const t=new URLSearchParams;for(const[r,o]of Object.entries(e))Array.isArray(o)?o.forEach((e=>{t.append(r,n(e))})):t.set(r,n(o));return"?"+t.toString()}function f({pathname:e,locale:n,params:t,pathnames:r,query:i}){function c(e){let n=r[e];return n||(n=e),n}function f(e,r){let c=o(e,n,r);return t&&Object.entries(t).forEach((([e,n])=>{let t,r;Array.isArray(n)?(t=`(\\[)?\\[...${e}\\](\\])?`,r=n.map((e=>String(e))).join("/")):(t=`\\[${e}\\]`,r=String(n)),c=c.replace(new RegExp(t,"g"),r)})),c=c.replace(/\[\[\.\.\..+\]\]/g,""),c=a(c),i&&(c+=s(i)),c}if("string"==typeof e){return f(c(e),e)}{const{pathname:n,...t}=e;return{...t,pathname:f(c(n),n)}}}function u(t,r,a){const i=e(Object.keys(a)),c=decodeURI(r);for(const e of i){const r=a[e];if("string"==typeof r){if(n(r,c))return e}else if(n(o(r,t,e),c))return e}return r}function l(e,n=window.location.pathname){return"/"===e?n:n.replace(e,"")}function p(e,n,o,a){const{mode:c}=o.localePrefix;let s;return void 0!==a?s=a:t(e)&&("always"===c?s=!0:"as-needed"===c&&(s=o.domains?!o.domains.some((e=>e.defaultLocale===n)):n!==o.defaultLocale)),s?r(i(n,o.localePrefix),e):e}export{p as applyPathnamePrefix,f as compileLocalizedPathname,l as getBasePath,u as getRoute,c as normalizeNameOrNameWithParams,s as serializeSearchParams};
1
+ import{getSortedPathnames as e,matchesPathname as n,isLocalizableHref as t,prefixPathname as r,getLocalizedTemplate as o,normalizeTrailingSlash as a,getLocalePrefix as i}from"../../shared/utils.js";function c(e){return"string"==typeof e?{pathname:e}:e}function s(e){function n(e){return String(e)}const t=new URLSearchParams;for(const[r,o]of Object.entries(e))Array.isArray(o)?o.forEach((e=>{t.append(r,n(e))})):t.set(r,n(o));return"?"+t.toString()}function f({pathname:e,locale:n,params:t,pathnames:r,query:i}){function c(e){let n=r[e];return n||(n=e),n}function f(e,r){let c=o(e,n,r);return t&&Object.entries(t).forEach((([e,n])=>{let t,r;Array.isArray(n)?(t=`(\\[)?\\[...${e}\\](\\])?`,r=n.map((e=>String(e))).join("/")):(t=`\\[${e}\\]`,r=String(n)),c=c.replace(new RegExp(t,"g"),r)})),c=c.replace(/\[\[\.\.\..+\]\]/g,""),c=a(c),c=function(e){return e.split("/").map((e=>encodeURIComponent(e))).join("/")}(c),i&&(c+=s(i)),c}if("string"==typeof e){return f(c(e),e)}{const{pathname:n,...t}=e;return{...t,pathname:f(c(n),n)}}}function u(t,r,a){const i=e(Object.keys(a)),c=decodeURI(r);for(const e of i){const r=a[e];if("string"==typeof r){if(n(r,c))return e}else if(n(o(r,t,e),c))return e}return r}function l(e,n=window.location.pathname){return"/"===e?n:n.replace(e,"")}function p(e,n,o,a){const{mode:c}=o.localePrefix;let s;return void 0!==a?s=a:t(e)&&("always"===c?s=!0:"as-needed"===c&&(s=o.domains?!o.domains.some((e=>e.defaultLocale===n)):n!==o.defaultLocale)),s?r(i(n,o.localePrefix),e):e}export{p as applyPathnamePrefix,f as compileLocalizedPathname,l as getBasePath,u as getRoute,c as normalizeNameOrNameWithParams,s as serializeSearchParams};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-intl",
3
- "version": "4.1.0",
3
+ "version": "4.3.0",
4
4
  "sideEffects": false,
5
5
  "author": "Jan Amann <jan@amann.work>",
6
6
  "funding": [
@@ -112,7 +112,7 @@
112
112
  "dependencies": {
113
113
  "@formatjs/intl-localematcher": "^0.5.4",
114
114
  "negotiator": "^1.0.0",
115
- "use-intl": "^4.1.0"
115
+ "use-intl": "^4.3.0"
116
116
  },
117
117
  "peerDependencies": {
118
118
  "next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0",
@@ -124,5 +124,5 @@
124
124
  "optional": true
125
125
  }
126
126
  },
127
- "gitHead": "9a70cef9f7d36cde5e8a69fd380f0747a050be56"
127
+ "gitHead": "56c7937e4a54d8265615ad28633f5294ea4fe4c5"
128
128
  }