next-i18next 16.0.1 → 16.0.3
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.
- package/README.md +29 -18
- package/dist/appRouter/index.cjs +1 -0
- package/dist/appRouter/index.cjs.map +1 -1
- package/dist/appRouter/index.d.cts +5 -0
- package/dist/appRouter/index.d.cts.map +1 -1
- package/dist/appRouter/index.d.mts +5 -0
- package/dist/appRouter/index.d.mts.map +1 -1
- package/dist/appRouter/index.mjs +1 -0
- package/dist/appRouter/index.mjs.map +1 -1
- package/dist/appRouter/proxy/index.cjs +25 -2
- package/dist/appRouter/proxy/index.cjs.map +1 -1
- package/dist/appRouter/proxy/index.d.cts +5 -0
- package/dist/appRouter/proxy/index.d.cts.map +1 -1
- package/dist/appRouter/proxy/index.d.mts +5 -0
- package/dist/appRouter/proxy/index.d.mts.map +1 -1
- package/dist/appRouter/proxy/index.mjs +25 -2
- package/dist/appRouter/proxy/index.mjs.map +1 -1
- package/dist/appRouter/server.cjs +10 -2
- package/dist/appRouter/server.cjs.map +1 -1
- package/dist/appRouter/server.d.cts +4 -0
- package/dist/appRouter/server.d.cts.map +1 -1
- package/dist/appRouter/server.d.mts +4 -0
- package/dist/appRouter/server.d.mts.map +1 -1
- package/dist/appRouter/server.mjs +10 -2
- package/dist/appRouter/server.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -45,7 +45,7 @@ npm install next-i18next i18next react-i18next
|
|
|
45
45
|
|
|
46
46
|
Place JSON translation files in your project. There are two common patterns:
|
|
47
47
|
|
|
48
|
-
**In `public/locales/`** (served statically, works with default config):
|
|
48
|
+
**In `public/locales/`** (served statically, works with default config — **local/traditional hosting only**):
|
|
49
49
|
|
|
50
50
|
```
|
|
51
51
|
public/locales/en/common.json
|
|
@@ -54,7 +54,9 @@ public/locales/de/common.json
|
|
|
54
54
|
public/locales/de/home.json
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
**
|
|
57
|
+
> **Serverless platforms (Vercel, AWS Lambda, etc.)**: Files in `public/` are served via CDN but are **not** available on the filesystem at runtime. Use `resourceLoader` with dynamic imports instead (see below).
|
|
58
|
+
|
|
59
|
+
**In `app/i18n/locales/`** (bundled via dynamic imports, requires `resourceLoader` — **works everywhere including serverless**):
|
|
58
60
|
|
|
59
61
|
```
|
|
60
62
|
app/i18n/locales/en/common.json
|
|
@@ -73,21 +75,7 @@ const i18nConfig: I18nConfig = {
|
|
|
73
75
|
fallbackLng: 'en',
|
|
74
76
|
defaultNS: 'common',
|
|
75
77
|
ns: ['common', 'home'],
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
export default i18nConfig
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
Or with a custom `resourceLoader` for non-public locale files:
|
|
82
|
-
|
|
83
|
-
```ts
|
|
84
|
-
import type { I18nConfig } from 'next-i18next/proxy'
|
|
85
|
-
|
|
86
|
-
const i18nConfig: I18nConfig = {
|
|
87
|
-
supportedLngs: ['en', 'de'],
|
|
88
|
-
fallbackLng: 'en',
|
|
89
|
-
defaultNS: 'common',
|
|
90
|
-
ns: ['common', 'home'],
|
|
78
|
+
// Recommended: works on all platforms including Vercel/serverless
|
|
91
79
|
resourceLoader: (language, namespace) =>
|
|
92
80
|
import(`./app/i18n/locales/${language}/${namespace}.json`),
|
|
93
81
|
}
|
|
@@ -95,6 +83,8 @@ const i18nConfig: I18nConfig = {
|
|
|
95
83
|
export default i18nConfig
|
|
96
84
|
```
|
|
97
85
|
|
|
86
|
+
The `resourceLoader` uses dynamic `import()` which the bundler can trace, ensuring translation files are included in the serverless function bundle. If you prefer to keep translations in `public/locales/` and are **not** deploying to a serverless platform, you can omit `resourceLoader` — next-i18next will read from the filesystem at runtime.
|
|
87
|
+
|
|
98
88
|
> **Tip**: Import `I18nConfig` from `next-i18next/proxy` (not from `next-i18next`) to keep the config file Edge-safe.
|
|
99
89
|
|
|
100
90
|
### 4. Proxy
|
|
@@ -252,9 +242,29 @@ For the no-locale-path mode (cookie-based), see `useChangeLanguage` [below](#no-
|
|
|
252
242
|
|
|
253
243
|
---
|
|
254
244
|
|
|
245
|
+
## Hide Default Locale
|
|
246
|
+
|
|
247
|
+
If you want clean URLs for the default language while keeping locale prefixes for other languages, set `hideDefaultLocale: true`:
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
const i18nConfig: I18nConfig = {
|
|
251
|
+
supportedLngs: ['en', 'de'],
|
|
252
|
+
fallbackLng: 'en',
|
|
253
|
+
hideDefaultLocale: true,
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
In this mode:
|
|
258
|
+
- `/about` serves the default language (English) — no prefix needed
|
|
259
|
+
- `/de/about` serves German — non-default locales keep their prefix
|
|
260
|
+
- `/en/about` automatically redirects to `/about` (canonical clean URL)
|
|
261
|
+
- The `[lng]` folder structure stays the same — the proxy rewrites internally
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
255
265
|
## No-Locale-Path Mode
|
|
256
266
|
|
|
257
|
-
If you prefer clean URLs without a locale prefix (e.g., `/about` instead of `/en/about`), set `localeInPath: false`:
|
|
267
|
+
If you prefer clean URLs without a locale prefix for **all** languages (e.g., `/about` instead of `/en/about`), set `localeInPath: false`:
|
|
258
268
|
|
|
259
269
|
```ts
|
|
260
270
|
const i18nConfig: I18nConfig = {
|
|
@@ -619,6 +629,7 @@ In **serverless environments** (Lambda, Vercel Serverless, etc.), the cache only
|
|
|
619
629
|
| `defaultNS` | `'common'` | Default namespace |
|
|
620
630
|
| `ns` | `[defaultNS]` | All known namespaces |
|
|
621
631
|
| `localeInPath` | `true` | Include locale in URL path |
|
|
632
|
+
| `hideDefaultLocale` | `false` | When `true` (with `localeInPath: true`), the default language has no URL prefix |
|
|
622
633
|
| `localePath` | `'/locales'` | Path to locale files relative to `/public` |
|
|
623
634
|
| `localeStructure` | `'{{lng}}/{{ns}}'` | Locale file directory structure |
|
|
624
635
|
| `localeExtension` | `'json'` | Locale file extension |
|
package/dist/appRouter/index.cjs
CHANGED
|
@@ -15,6 +15,7 @@ function normalizeConfig(userConfig) {
|
|
|
15
15
|
defaultNS,
|
|
16
16
|
ns: userConfig.ns ?? [defaultNS],
|
|
17
17
|
localeInPath: userConfig.localeInPath ?? true,
|
|
18
|
+
hideDefaultLocale: userConfig.hideDefaultLocale ?? false,
|
|
18
19
|
localePath: userConfig.localePath ?? "/locales",
|
|
19
20
|
localeStructure: userConfig.localeStructure ?? "{{lng}}/{{ns}}",
|
|
20
21
|
localeExtension: userConfig.localeExtension ?? "json",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":[],"sources":["../../src/appRouter/config.ts"],"sourcesContent":["import type { I18nConfig, NormalizedConfig } from './types'\n\nexport function defineConfig(config: I18nConfig): I18nConfig {\n return config\n}\n\nexport function normalizeConfig(userConfig: I18nConfig): NormalizedConfig {\n // Support legacy format: { i18n: { defaultLocale, locales } }\n const supportedLngs = userConfig.supportedLngs ??\n userConfig.i18n?.locales?.filter((l: string) => l !== 'default') ??\n ['en']\n const fallbackLng = userConfig.fallbackLng ??\n userConfig.i18n?.defaultLocale ??\n supportedLngs[0]\n\n if (!fallbackLng) {\n throw new Error('next-i18next: fallbackLng (or i18n.defaultLocale) is required')\n }\n if (supportedLngs.length === 0) {\n throw new Error('next-i18next: supportedLngs (or i18n.locales) must contain at least one language')\n }\n\n const defaultNS = userConfig.defaultNS ?? 'common'\n\n return {\n supportedLngs,\n fallbackLng,\n defaultNS,\n ns: userConfig.ns ?? [defaultNS],\n localeInPath: userConfig.localeInPath ?? true,\n localePath: userConfig.localePath ?? '/locales',\n localeStructure: userConfig.localeStructure ?? '{{lng}}/{{ns}}',\n localeExtension: userConfig.localeExtension ?? 'json',\n cookieName: userConfig.cookieName ?? 'i18next',\n headerName: userConfig.headerName ?? 'x-i18next-current-language',\n cookieMaxAge: userConfig.cookieMaxAge ?? 365 * 24 * 60 * 60,\n ignoredPaths: userConfig.ignoredPaths ?? ['/api', '/_next', '/static'],\n basePath: userConfig.basePath,\n resources: userConfig.resources,\n resourceLoader: userConfig.resourceLoader,\n use: userConfig.use ?? [],\n i18nextOptions: (userConfig.i18nextOptions ?? {}) as Record<string, any>,\n nonExplicitSupportedLngs: userConfig.nonExplicitSupportedLngs ?? false,\n // Preserve legacy fields\n i18n: userConfig.i18n,\n serializeConfig: userConfig.serializeConfig,\n reloadOnPrerender: userConfig.reloadOnPrerender,\n }\n}\n"],"mappings":";;AAEA,SAAgB,aAAa,QAAgC;AAC3D,QAAO;;AAGT,SAAgB,gBAAgB,YAA0C;CAExE,MAAM,gBAAgB,WAAW,iBAC/B,WAAW,MAAM,SAAS,QAAQ,MAAc,MAAM,UAAU,IAChE,CAAC,KAAK;CACR,MAAM,cAAc,WAAW,eAC7B,WAAW,MAAM,iBACjB,cAAc;AAEhB,KAAI,CAAC,YACH,OAAM,IAAI,MAAM,gEAAgE;AAElF,KAAI,cAAc,WAAW,EAC3B,OAAM,IAAI,MAAM,mFAAmF;CAGrG,MAAM,YAAY,WAAW,aAAa;AAE1C,QAAO;EACL;EACA;EACA;EACA,IAAI,WAAW,MAAM,CAAC,UAAU;EAChC,cAAc,WAAW,gBAAgB;EACzC,YAAY,WAAW,cAAc;EACrC,iBAAiB,WAAW,mBAAmB;EAC/C,iBAAiB,WAAW,mBAAmB;EAC/C,YAAY,WAAW,cAAc;EACrC,YAAY,WAAW,cAAc;EACrC,cAAc,WAAW,gBAAgB,MAAM,KAAK,KAAK;EACzD,cAAc,WAAW,gBAAgB;GAAC;GAAQ;GAAU;GAAU;EACtE,UAAU,WAAW;EACrB,WAAW,WAAW;EACtB,gBAAgB,WAAW;EAC3B,KAAK,WAAW,OAAO,EAAE;EACzB,gBAAiB,WAAW,kBAAkB,EAAE;EAChD,0BAA0B,WAAW,4BAA4B;EAEjE,MAAM,WAAW;EACjB,iBAAiB,WAAW;EAC5B,mBAAmB,WAAW;EAC/B"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../../src/appRouter/config.ts"],"sourcesContent":["import type { I18nConfig, NormalizedConfig } from './types'\n\nexport function defineConfig(config: I18nConfig): I18nConfig {\n return config\n}\n\nexport function normalizeConfig(userConfig: I18nConfig): NormalizedConfig {\n // Support legacy format: { i18n: { defaultLocale, locales } }\n const supportedLngs = userConfig.supportedLngs ??\n userConfig.i18n?.locales?.filter((l: string) => l !== 'default') ??\n ['en']\n const fallbackLng = userConfig.fallbackLng ??\n userConfig.i18n?.defaultLocale ??\n supportedLngs[0]\n\n if (!fallbackLng) {\n throw new Error('next-i18next: fallbackLng (or i18n.defaultLocale) is required')\n }\n if (supportedLngs.length === 0) {\n throw new Error('next-i18next: supportedLngs (or i18n.locales) must contain at least one language')\n }\n\n const defaultNS = userConfig.defaultNS ?? 'common'\n\n return {\n supportedLngs,\n fallbackLng,\n defaultNS,\n ns: userConfig.ns ?? [defaultNS],\n localeInPath: userConfig.localeInPath ?? true,\n hideDefaultLocale: userConfig.hideDefaultLocale ?? false,\n localePath: userConfig.localePath ?? '/locales',\n localeStructure: userConfig.localeStructure ?? '{{lng}}/{{ns}}',\n localeExtension: userConfig.localeExtension ?? 'json',\n cookieName: userConfig.cookieName ?? 'i18next',\n headerName: userConfig.headerName ?? 'x-i18next-current-language',\n cookieMaxAge: userConfig.cookieMaxAge ?? 365 * 24 * 60 * 60,\n ignoredPaths: userConfig.ignoredPaths ?? ['/api', '/_next', '/static'],\n basePath: userConfig.basePath,\n resources: userConfig.resources,\n resourceLoader: userConfig.resourceLoader,\n use: userConfig.use ?? [],\n i18nextOptions: (userConfig.i18nextOptions ?? {}) as Record<string, any>,\n nonExplicitSupportedLngs: userConfig.nonExplicitSupportedLngs ?? false,\n // Preserve legacy fields\n i18n: userConfig.i18n,\n serializeConfig: userConfig.serializeConfig,\n reloadOnPrerender: userConfig.reloadOnPrerender,\n }\n}\n"],"mappings":";;AAEA,SAAgB,aAAa,QAAgC;AAC3D,QAAO;;AAGT,SAAgB,gBAAgB,YAA0C;CAExE,MAAM,gBAAgB,WAAW,iBAC/B,WAAW,MAAM,SAAS,QAAQ,MAAc,MAAM,UAAU,IAChE,CAAC,KAAK;CACR,MAAM,cAAc,WAAW,eAC7B,WAAW,MAAM,iBACjB,cAAc;AAEhB,KAAI,CAAC,YACH,OAAM,IAAI,MAAM,gEAAgE;AAElF,KAAI,cAAc,WAAW,EAC3B,OAAM,IAAI,MAAM,mFAAmF;CAGrG,MAAM,YAAY,WAAW,aAAa;AAE1C,QAAO;EACL;EACA;EACA;EACA,IAAI,WAAW,MAAM,CAAC,UAAU;EAChC,cAAc,WAAW,gBAAgB;EACzC,mBAAmB,WAAW,qBAAqB;EACnD,YAAY,WAAW,cAAc;EACrC,iBAAiB,WAAW,mBAAmB;EAC/C,iBAAiB,WAAW,mBAAmB;EAC/C,YAAY,WAAW,cAAc;EACrC,YAAY,WAAW,cAAc;EACrC,cAAc,WAAW,gBAAgB,MAAM,KAAK,KAAK;EACzD,cAAc,WAAW,gBAAgB;GAAC;GAAQ;GAAU;GAAU;EACtE,UAAU,WAAW;EACrB,WAAW,WAAW;EACtB,gBAAgB,WAAW;EAC3B,KAAK,WAAW,OAAO,EAAE;EACzB,gBAAiB,WAAW,kBAAkB,EAAE;EAChD,0BAA0B,WAAW,4BAA4B;EAEjE,MAAM,WAAW;EACjB,iBAAiB,WAAW;EAC5B,mBAAmB,WAAW;EAC/B"}
|
|
@@ -23,6 +23,10 @@ interface I18nConfig {
|
|
|
23
23
|
resourceLoader?: ResourceLoader;
|
|
24
24
|
/** Whether to include locale in URL path (defaults to true) */
|
|
25
25
|
localeInPath?: boolean;
|
|
26
|
+
/** When true (and localeInPath is true), the default language has no URL prefix.
|
|
27
|
+
* e.g. `/about` serves the default language, `/de/about` serves German.
|
|
28
|
+
* Requests to the explicit default prefix (`/en/about`) are redirected to `/about`. */
|
|
29
|
+
hideDefaultLocale?: boolean;
|
|
26
30
|
/** Cookie name for storing selected language (defaults to 'i18next') */
|
|
27
31
|
cookieName?: string;
|
|
28
32
|
/** Custom header name for passing language to server components (defaults to 'x-i18next-current-language') */
|
|
@@ -62,6 +66,7 @@ interface NormalizedConfig {
|
|
|
62
66
|
defaultNS: string;
|
|
63
67
|
ns: string[];
|
|
64
68
|
localeInPath: boolean;
|
|
69
|
+
hideDefaultLocale: boolean;
|
|
65
70
|
localePath: string;
|
|
66
71
|
localeStructure: string;
|
|
67
72
|
localeExtension: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":[],"sources":["../../src/appRouter/types.ts","../../src/appRouter/config.ts"],"mappings":";;;KAEY,cAAA,IAAkB,QAAA,UAAkB,SAAA,aAAsB,OAAA;AAAA,UAErD,UAAA;EAFL;EAIV,aAAA;;EAEA,WAAA;EAN4B;EAQ5B,SAAA;EARoE;EAUpE,EAAA;EAV2E;EAc3E,UAAA;EAZyB;EAczB,eAAA;EAIY;EAFZ,eAAA;
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../../src/appRouter/types.ts","../../src/appRouter/config.ts"],"mappings":";;;KAEY,cAAA,IAAkB,QAAA,UAAkB,SAAA,aAAsB,OAAA;AAAA,UAErD,UAAA;EAFL;EAIV,aAAA;;EAEA,WAAA;EAN4B;EAQ5B,SAAA;EARoE;EAUpE,EAAA;EAV2E;EAc3E,UAAA;EAZyB;EAczB,eAAA;EAIY;EAFZ,eAAA;EA8BsB;EA5BtB,SAAA,GAAY,QAAA;EA4BS;EA1BrB,cAAA,GAAiB,cAAA;EAlBjB;EAsBA,YAAA;EAlBA;;;EAsBA,iBAAA;EAZA;EAgBA,UAAA;EAdY;EAgBZ,UAAA;EAdiB;EAgBjB,YAAA;EARA;EAUA,YAAA;EAJA;EAMA,QAAA;EAFA;EAMA,GAAA;EAAA;EAEA,cAAA,GAAiB,IAAA,CAAK,WAAA;EAAL;EAIjB,IAAA;IACE,aAAA;IACA,OAAA;IACA,OAAA;MACE,aAAA;MACA,MAAA;MACA,IAAA;MACA,OAAA;IAAA;IAEF,eAAA;EAAA;EAKF;EAFA,eAAA;EAIwB;EAFxB,iBAAA;EAKe;EAHf,wBAAA;AAAA;AAAA,UAGe,gBAAA;EACf,aAAA;EACA,WAAA;EACA,SAAA;EACA,EAAA;EACA,YAAA;EACA,iBAAA;EACA,UAAA;EACA,eAAA;EACA,eAAA;EACA,UAAA;EACA,UAAA;EACA,YAAA;EACA,YAAA;EACA,QAAA;EACA,SAAA,GAAY,QAAA;EACZ,cAAA,GAAiB,cAAA;EACjB,GAAA;EACA,cAAA,EAAgB,MAAA;EAChB,wBAAA;EAEA,IAAA,GAAO,UAAA;EACP,eAAA;EACA,iBAAA;AAAA;AAAA,KAGU,UAAA,YAAsB,aAAA,GAAgB,aAAA;EAChD,CAAA,EAAG,SAAA,CAAU,EAAA,EAAI,OAAA;EACjB,IAAA,EAAM,IAAA,EATN;EAWA,GAAA;AAAA;;;iBCrGc,YAAA,CAAa,MAAA,EAAQ,UAAA,GAAa,UAAA;AAAA,iBAIlC,eAAA,CAAgB,UAAA,EAAY,UAAA,GAAa,gBAAA"}
|
|
@@ -23,6 +23,10 @@ interface I18nConfig {
|
|
|
23
23
|
resourceLoader?: ResourceLoader;
|
|
24
24
|
/** Whether to include locale in URL path (defaults to true) */
|
|
25
25
|
localeInPath?: boolean;
|
|
26
|
+
/** When true (and localeInPath is true), the default language has no URL prefix.
|
|
27
|
+
* e.g. `/about` serves the default language, `/de/about` serves German.
|
|
28
|
+
* Requests to the explicit default prefix (`/en/about`) are redirected to `/about`. */
|
|
29
|
+
hideDefaultLocale?: boolean;
|
|
26
30
|
/** Cookie name for storing selected language (defaults to 'i18next') */
|
|
27
31
|
cookieName?: string;
|
|
28
32
|
/** Custom header name for passing language to server components (defaults to 'x-i18next-current-language') */
|
|
@@ -62,6 +66,7 @@ interface NormalizedConfig {
|
|
|
62
66
|
defaultNS: string;
|
|
63
67
|
ns: string[];
|
|
64
68
|
localeInPath: boolean;
|
|
69
|
+
hideDefaultLocale: boolean;
|
|
65
70
|
localePath: string;
|
|
66
71
|
localeStructure: string;
|
|
67
72
|
localeExtension: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/appRouter/types.ts","../../src/appRouter/config.ts"],"mappings":";;;KAEY,cAAA,IAAkB,QAAA,UAAkB,SAAA,aAAsB,OAAA;AAAA,UAErD,UAAA;EAFL;EAIV,aAAA;;EAEA,WAAA;EAN4B;EAQ5B,SAAA;EARoE;EAUpE,EAAA;EAV2E;EAc3E,UAAA;EAZyB;EAczB,eAAA;EAIY;EAFZ,eAAA;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/appRouter/types.ts","../../src/appRouter/config.ts"],"mappings":";;;KAEY,cAAA,IAAkB,QAAA,UAAkB,SAAA,aAAsB,OAAA;AAAA,UAErD,UAAA;EAFL;EAIV,aAAA;;EAEA,WAAA;EAN4B;EAQ5B,SAAA;EARoE;EAUpE,EAAA;EAV2E;EAc3E,UAAA;EAZyB;EAczB,eAAA;EAIY;EAFZ,eAAA;EA8BsB;EA5BtB,SAAA,GAAY,QAAA;EA4BS;EA1BrB,cAAA,GAAiB,cAAA;EAlBjB;EAsBA,YAAA;EAlBA;;;EAsBA,iBAAA;EAZA;EAgBA,UAAA;EAdY;EAgBZ,UAAA;EAdiB;EAgBjB,YAAA;EARA;EAUA,YAAA;EAJA;EAMA,QAAA;EAFA;EAMA,GAAA;EAAA;EAEA,cAAA,GAAiB,IAAA,CAAK,WAAA;EAAL;EAIjB,IAAA;IACE,aAAA;IACA,OAAA;IACA,OAAA;MACE,aAAA;MACA,MAAA;MACA,IAAA;MACA,OAAA;IAAA;IAEF,eAAA;EAAA;EAKF;EAFA,eAAA;EAIwB;EAFxB,iBAAA;EAKe;EAHf,wBAAA;AAAA;AAAA,UAGe,gBAAA;EACf,aAAA;EACA,WAAA;EACA,SAAA;EACA,EAAA;EACA,YAAA;EACA,iBAAA;EACA,UAAA;EACA,eAAA;EACA,eAAA;EACA,UAAA;EACA,UAAA;EACA,YAAA;EACA,YAAA;EACA,QAAA;EACA,SAAA,GAAY,QAAA;EACZ,cAAA,GAAiB,cAAA;EACjB,GAAA;EACA,cAAA,EAAgB,MAAA;EAChB,wBAAA;EAEA,IAAA,GAAO,UAAA;EACP,eAAA;EACA,iBAAA;AAAA;AAAA,KAGU,UAAA,YAAsB,aAAA,GAAgB,aAAA;EAChD,CAAA,EAAG,SAAA,CAAU,EAAA,EAAI,OAAA;EACjB,IAAA,EAAM,IAAA,EATN;EAWA,GAAA;AAAA;;;iBCrGc,YAAA,CAAa,MAAA,EAAQ,UAAA,GAAa,UAAA;AAAA,iBAIlC,eAAA,CAAgB,UAAA,EAAY,UAAA,GAAa,gBAAA"}
|
package/dist/appRouter/index.mjs
CHANGED
|
@@ -14,6 +14,7 @@ function normalizeConfig(userConfig) {
|
|
|
14
14
|
defaultNS,
|
|
15
15
|
ns: userConfig.ns ?? [defaultNS],
|
|
16
16
|
localeInPath: userConfig.localeInPath ?? true,
|
|
17
|
+
hideDefaultLocale: userConfig.hideDefaultLocale ?? false,
|
|
17
18
|
localePath: userConfig.localePath ?? "/locales",
|
|
18
19
|
localeStructure: userConfig.localeStructure ?? "{{lng}}/{{ns}}",
|
|
19
20
|
localeExtension: userConfig.localeExtension ?? "json",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/appRouter/config.ts"],"sourcesContent":["import type { I18nConfig, NormalizedConfig } from './types'\n\nexport function defineConfig(config: I18nConfig): I18nConfig {\n return config\n}\n\nexport function normalizeConfig(userConfig: I18nConfig): NormalizedConfig {\n // Support legacy format: { i18n: { defaultLocale, locales } }\n const supportedLngs = userConfig.supportedLngs ??\n userConfig.i18n?.locales?.filter((l: string) => l !== 'default') ??\n ['en']\n const fallbackLng = userConfig.fallbackLng ??\n userConfig.i18n?.defaultLocale ??\n supportedLngs[0]\n\n if (!fallbackLng) {\n throw new Error('next-i18next: fallbackLng (or i18n.defaultLocale) is required')\n }\n if (supportedLngs.length === 0) {\n throw new Error('next-i18next: supportedLngs (or i18n.locales) must contain at least one language')\n }\n\n const defaultNS = userConfig.defaultNS ?? 'common'\n\n return {\n supportedLngs,\n fallbackLng,\n defaultNS,\n ns: userConfig.ns ?? [defaultNS],\n localeInPath: userConfig.localeInPath ?? true,\n localePath: userConfig.localePath ?? '/locales',\n localeStructure: userConfig.localeStructure ?? '{{lng}}/{{ns}}',\n localeExtension: userConfig.localeExtension ?? 'json',\n cookieName: userConfig.cookieName ?? 'i18next',\n headerName: userConfig.headerName ?? 'x-i18next-current-language',\n cookieMaxAge: userConfig.cookieMaxAge ?? 365 * 24 * 60 * 60,\n ignoredPaths: userConfig.ignoredPaths ?? ['/api', '/_next', '/static'],\n basePath: userConfig.basePath,\n resources: userConfig.resources,\n resourceLoader: userConfig.resourceLoader,\n use: userConfig.use ?? [],\n i18nextOptions: (userConfig.i18nextOptions ?? {}) as Record<string, any>,\n nonExplicitSupportedLngs: userConfig.nonExplicitSupportedLngs ?? false,\n // Preserve legacy fields\n i18n: userConfig.i18n,\n serializeConfig: userConfig.serializeConfig,\n reloadOnPrerender: userConfig.reloadOnPrerender,\n }\n}\n"],"mappings":";AAEA,SAAgB,aAAa,QAAgC;AAC3D,QAAO;;AAGT,SAAgB,gBAAgB,YAA0C;CAExE,MAAM,gBAAgB,WAAW,iBAC/B,WAAW,MAAM,SAAS,QAAQ,MAAc,MAAM,UAAU,IAChE,CAAC,KAAK;CACR,MAAM,cAAc,WAAW,eAC7B,WAAW,MAAM,iBACjB,cAAc;AAEhB,KAAI,CAAC,YACH,OAAM,IAAI,MAAM,gEAAgE;AAElF,KAAI,cAAc,WAAW,EAC3B,OAAM,IAAI,MAAM,mFAAmF;CAGrG,MAAM,YAAY,WAAW,aAAa;AAE1C,QAAO;EACL;EACA;EACA;EACA,IAAI,WAAW,MAAM,CAAC,UAAU;EAChC,cAAc,WAAW,gBAAgB;EACzC,YAAY,WAAW,cAAc;EACrC,iBAAiB,WAAW,mBAAmB;EAC/C,iBAAiB,WAAW,mBAAmB;EAC/C,YAAY,WAAW,cAAc;EACrC,YAAY,WAAW,cAAc;EACrC,cAAc,WAAW,gBAAgB,MAAM,KAAK,KAAK;EACzD,cAAc,WAAW,gBAAgB;GAAC;GAAQ;GAAU;GAAU;EACtE,UAAU,WAAW;EACrB,WAAW,WAAW;EACtB,gBAAgB,WAAW;EAC3B,KAAK,WAAW,OAAO,EAAE;EACzB,gBAAiB,WAAW,kBAAkB,EAAE;EAChD,0BAA0B,WAAW,4BAA4B;EAEjE,MAAM,WAAW;EACjB,iBAAiB,WAAW;EAC5B,mBAAmB,WAAW;EAC/B"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/appRouter/config.ts"],"sourcesContent":["import type { I18nConfig, NormalizedConfig } from './types'\n\nexport function defineConfig(config: I18nConfig): I18nConfig {\n return config\n}\n\nexport function normalizeConfig(userConfig: I18nConfig): NormalizedConfig {\n // Support legacy format: { i18n: { defaultLocale, locales } }\n const supportedLngs = userConfig.supportedLngs ??\n userConfig.i18n?.locales?.filter((l: string) => l !== 'default') ??\n ['en']\n const fallbackLng = userConfig.fallbackLng ??\n userConfig.i18n?.defaultLocale ??\n supportedLngs[0]\n\n if (!fallbackLng) {\n throw new Error('next-i18next: fallbackLng (or i18n.defaultLocale) is required')\n }\n if (supportedLngs.length === 0) {\n throw new Error('next-i18next: supportedLngs (or i18n.locales) must contain at least one language')\n }\n\n const defaultNS = userConfig.defaultNS ?? 'common'\n\n return {\n supportedLngs,\n fallbackLng,\n defaultNS,\n ns: userConfig.ns ?? [defaultNS],\n localeInPath: userConfig.localeInPath ?? true,\n hideDefaultLocale: userConfig.hideDefaultLocale ?? false,\n localePath: userConfig.localePath ?? '/locales',\n localeStructure: userConfig.localeStructure ?? '{{lng}}/{{ns}}',\n localeExtension: userConfig.localeExtension ?? 'json',\n cookieName: userConfig.cookieName ?? 'i18next',\n headerName: userConfig.headerName ?? 'x-i18next-current-language',\n cookieMaxAge: userConfig.cookieMaxAge ?? 365 * 24 * 60 * 60,\n ignoredPaths: userConfig.ignoredPaths ?? ['/api', '/_next', '/static'],\n basePath: userConfig.basePath,\n resources: userConfig.resources,\n resourceLoader: userConfig.resourceLoader,\n use: userConfig.use ?? [],\n i18nextOptions: (userConfig.i18nextOptions ?? {}) as Record<string, any>,\n nonExplicitSupportedLngs: userConfig.nonExplicitSupportedLngs ?? false,\n // Preserve legacy fields\n i18n: userConfig.i18n,\n serializeConfig: userConfig.serializeConfig,\n reloadOnPrerender: userConfig.reloadOnPrerender,\n }\n}\n"],"mappings":";AAEA,SAAgB,aAAa,QAAgC;AAC3D,QAAO;;AAGT,SAAgB,gBAAgB,YAA0C;CAExE,MAAM,gBAAgB,WAAW,iBAC/B,WAAW,MAAM,SAAS,QAAQ,MAAc,MAAM,UAAU,IAChE,CAAC,KAAK;CACR,MAAM,cAAc,WAAW,eAC7B,WAAW,MAAM,iBACjB,cAAc;AAEhB,KAAI,CAAC,YACH,OAAM,IAAI,MAAM,gEAAgE;AAElF,KAAI,cAAc,WAAW,EAC3B,OAAM,IAAI,MAAM,mFAAmF;CAGrG,MAAM,YAAY,WAAW,aAAa;AAE1C,QAAO;EACL;EACA;EACA;EACA,IAAI,WAAW,MAAM,CAAC,UAAU;EAChC,cAAc,WAAW,gBAAgB;EACzC,mBAAmB,WAAW,qBAAqB;EACnD,YAAY,WAAW,cAAc;EACrC,iBAAiB,WAAW,mBAAmB;EAC/C,iBAAiB,WAAW,mBAAmB;EAC/C,YAAY,WAAW,cAAc;EACrC,YAAY,WAAW,cAAc;EACrC,cAAc,WAAW,gBAAgB,MAAM,KAAK,KAAK;EACzD,cAAc,WAAW,gBAAgB;GAAC;GAAQ;GAAU;GAAU;EACtE,UAAU,WAAW;EACrB,WAAW,WAAW;EACtB,gBAAgB,WAAW;EAC3B,KAAK,WAAW,OAAO,EAAE;EACzB,gBAAiB,WAAW,kBAAkB,EAAE;EAChD,0BAA0B,WAAW,4BAA4B;EAEjE,MAAM,WAAW;EACjB,iBAAiB,WAAW;EAC5B,mBAAmB,WAAW;EAC/B"}
|
|
@@ -16,6 +16,7 @@ function normalizeConfig(userConfig) {
|
|
|
16
16
|
defaultNS,
|
|
17
17
|
ns: userConfig.ns ?? [defaultNS],
|
|
18
18
|
localeInPath: userConfig.localeInPath ?? true,
|
|
19
|
+
hideDefaultLocale: userConfig.hideDefaultLocale ?? false,
|
|
19
20
|
localePath: userConfig.localePath ?? "/locales",
|
|
20
21
|
localeStructure: userConfig.localeStructure ?? "{{lng}}/{{ns}}",
|
|
21
22
|
localeExtension: userConfig.localeExtension ?? "json",
|
|
@@ -108,11 +109,33 @@ function createProxy(userConfig) {
|
|
|
108
109
|
if (!lng) lng = config.fallbackLng;
|
|
109
110
|
const lngInPath = findLocaleInPath(basePath ? pathname.slice(basePath.length) || "/" : pathname, config.supportedLngs, nonExplicit);
|
|
110
111
|
if (config.localeInPath) {
|
|
112
|
+
const prefix = basePath ?? "";
|
|
113
|
+
const pathAfterBase = basePath ? pathname.slice(basePath.length) : pathname;
|
|
114
|
+
if (config.hideDefaultLocale && lngInPath === config.fallbackLng) {
|
|
115
|
+
const pathWithoutLocale = pathAfterBase.replace(/^\/[^/]+/, "") || "/";
|
|
116
|
+
const redirectUrl = new URL(`${prefix}${pathWithoutLocale}${search}`, req.url);
|
|
117
|
+
const response = next_server.NextResponse.redirect(redirectUrl);
|
|
118
|
+
response.cookies.set(config.cookieName, config.fallbackLng, {
|
|
119
|
+
path: "/",
|
|
120
|
+
maxAge: config.cookieMaxAge,
|
|
121
|
+
sameSite: "lax"
|
|
122
|
+
});
|
|
123
|
+
return response;
|
|
124
|
+
}
|
|
111
125
|
const headers = new Headers(req.headers);
|
|
112
126
|
headers.set(config.headerName, lngInPath || lng);
|
|
113
127
|
if (!lngInPath) {
|
|
114
|
-
|
|
115
|
-
|
|
128
|
+
if (config.hideDefaultLocale) {
|
|
129
|
+
const rewriteUrl = new URL(`${prefix}/${config.fallbackLng}${pathAfterBase}${search}`, req.url);
|
|
130
|
+
headers.set(config.headerName, config.fallbackLng);
|
|
131
|
+
const response = next_server.NextResponse.rewrite(rewriteUrl, { request: { headers } });
|
|
132
|
+
response.cookies.set(config.cookieName, config.fallbackLng, {
|
|
133
|
+
path: "/",
|
|
134
|
+
maxAge: config.cookieMaxAge,
|
|
135
|
+
sameSite: "lax"
|
|
136
|
+
});
|
|
137
|
+
return response;
|
|
138
|
+
}
|
|
116
139
|
const redirectUrl = new URL(`${prefix}/${lng}${pathAfterBase}${search}`, req.url);
|
|
117
140
|
const response = next_server.NextResponse.redirect(redirectUrl);
|
|
118
141
|
response.cookies.set(config.cookieName, lng, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["NextResponse"],"sources":["../../../src/appRouter/config.ts","../../../src/appRouter/proxy/languageDetector.ts","../../../src/appRouter/proxy/index.ts"],"sourcesContent":["import type { I18nConfig, NormalizedConfig } from './types'\n\nexport function defineConfig(config: I18nConfig): I18nConfig {\n return config\n}\n\nexport function normalizeConfig(userConfig: I18nConfig): NormalizedConfig {\n // Support legacy format: { i18n: { defaultLocale, locales } }\n const supportedLngs = userConfig.supportedLngs ??\n userConfig.i18n?.locales?.filter((l: string) => l !== 'default') ??\n ['en']\n const fallbackLng = userConfig.fallbackLng ??\n userConfig.i18n?.defaultLocale ??\n supportedLngs[0]\n\n if (!fallbackLng) {\n throw new Error('next-i18next: fallbackLng (or i18n.defaultLocale) is required')\n }\n if (supportedLngs.length === 0) {\n throw new Error('next-i18next: supportedLngs (or i18n.locales) must contain at least one language')\n }\n\n const defaultNS = userConfig.defaultNS ?? 'common'\n\n return {\n supportedLngs,\n fallbackLng,\n defaultNS,\n ns: userConfig.ns ?? [defaultNS],\n localeInPath: userConfig.localeInPath ?? true,\n localePath: userConfig.localePath ?? '/locales',\n localeStructure: userConfig.localeStructure ?? '{{lng}}/{{ns}}',\n localeExtension: userConfig.localeExtension ?? 'json',\n cookieName: userConfig.cookieName ?? 'i18next',\n headerName: userConfig.headerName ?? 'x-i18next-current-language',\n cookieMaxAge: userConfig.cookieMaxAge ?? 365 * 24 * 60 * 60,\n ignoredPaths: userConfig.ignoredPaths ?? ['/api', '/_next', '/static'],\n basePath: userConfig.basePath,\n resources: userConfig.resources,\n resourceLoader: userConfig.resourceLoader,\n use: userConfig.use ?? [],\n i18nextOptions: (userConfig.i18nextOptions ?? {}) as Record<string, any>,\n nonExplicitSupportedLngs: userConfig.nonExplicitSupportedLngs ?? false,\n // Preserve legacy fields\n i18n: userConfig.i18n,\n serializeConfig: userConfig.serializeConfig,\n reloadOnPrerender: userConfig.reloadOnPrerender,\n }\n}\n","/**\n * Edge-safe Accept-Language parser and language matcher.\n * No external dependencies — runs in Edge Runtime, Node.js, and browser.\n */\n\nexport function parseAcceptLanguage(header: string | null | undefined): string[] {\n if (!header) return []\n return header\n .split(',')\n .map(part => {\n const [lang, qPart] = part.trim().split(';')\n const q = qPart?.trim().startsWith('q=')\n ? parseFloat(qPart.trim().slice(2))\n : 1.0\n return { lang: lang.trim(), q: isNaN(q) ? 0 : q }\n })\n .filter(item => item.lang && item.q > 0)\n .sort((a, b) => b.q - a.q)\n .map(item => item.lang)\n}\n\n/**\n * Find a matching supported language for a given code, respecting nonExplicitSupportedLngs.\n *\n * Matching order (mirrors i18next's LanguageUtils.getBestMatchFromCodes):\n * 1. Exact match (case-insensitive)\n * 2. Preferred prefix → supported base (e.g. preferred 'en-US' matches supported 'en')\n * 3. If nonExplicitSupportedLngs: preferred base → supported region\n * (e.g. preferred 'en' matches supported 'en-US')\n */\nexport function findSupportedMatch(\n code: string,\n supportedLanguages: readonly string[],\n nonExplicitSupportedLngs: boolean,\n): string | undefined {\n const lower = code.toLowerCase()\n\n // 1. Exact match (case-insensitive)\n const exact = supportedLanguages.find(l => l.toLowerCase() === lower)\n if (exact) return exact\n\n // 2. Preferred prefix → supported base (e.g. 'en-US' → 'en')\n const prefix = lower.split('-')[0]\n const partial = supportedLanguages.find(l => l.toLowerCase() === prefix)\n if (partial) return partial\n\n // 3. Reverse match: preferred base → supported region (e.g. 'en' → 'en-US')\n if (nonExplicitSupportedLngs) {\n const reverse = supportedLanguages.find(\n l => l.toLowerCase().split('-')[0] === prefix\n )\n if (reverse) return reverse\n }\n\n return undefined\n}\n\nexport function matchLanguage(\n acceptLanguages: string[],\n supportedLanguages: readonly string[],\n defaultLanguage: string,\n nonExplicitSupportedLngs = false,\n): string {\n for (const preferred of acceptLanguages) {\n const match = findSupportedMatch(preferred, supportedLanguages, nonExplicitSupportedLngs)\n if (match) return match\n }\n return defaultLanguage\n}\n","import { NextRequest, NextResponse } from 'next/server'\nimport type { I18nConfig } from '../types'\nimport { normalizeConfig } from '../config'\nimport { parseAcceptLanguage, matchLanguage, findSupportedMatch } from './languageDetector'\n\n// Re-export config utilities for Edge-safe usage (no react-i18next dependency)\nexport { defineConfig, normalizeConfig } from '../config'\nexport type { I18nConfig, NormalizedConfig, ResourceLoader } from '../types'\n\nfunction findLocaleInPath(\n pathname: string,\n supportedLngs: readonly string[],\n nonExplicitSupportedLngs: boolean,\n): string | undefined {\n // Extract the first path segment\n const match = pathname.match(/^\\/([^/]+)/)\n if (!match) return undefined\n return findSupportedMatch(match[1], supportedLngs, nonExplicitSupportedLngs)\n}\n\nexport function createProxy(userConfig: I18nConfig) {\n const config = normalizeConfig(userConfig)\n const nonExplicit = config.nonExplicitSupportedLngs\n // Normalize basePath: ensure leading slash, strip trailing slash\n const basePath = config.basePath\n ? ('/' + config.basePath.replace(/^\\/+/, '').replace(/\\/+$/, ''))\n : undefined\n\n return function middleware(req: NextRequest): NextResponse {\n const { pathname, search } = req.nextUrl\n\n // When basePath is set, only handle requests under that prefix\n if (basePath) {\n if (pathname !== basePath && !pathname.startsWith(basePath + '/')) {\n return NextResponse.next()\n }\n }\n\n // Skip ignored paths\n for (const ignored of config.ignoredPaths) {\n if (pathname.startsWith(ignored)) {\n return NextResponse.next()\n }\n }\n\n // Skip common static file extensions\n if (/\\.(ico|png|jpg|jpeg|svg|gif|webp|css|js|map|woff2?|ttf|eot)$/.test(pathname)) {\n return NextResponse.next()\n }\n\n // Detect language from cookie, then Accept-Language header, then default\n let lng: string | undefined\n const cookieValue = req.cookies.get(config.cookieName)?.value\n if (cookieValue) {\n lng = matchLanguage([cookieValue], config.supportedLngs, config.fallbackLng, nonExplicit)\n }\n if (!lng) {\n lng = matchLanguage(\n parseAcceptLanguage(req.headers.get('Accept-Language')),\n config.supportedLngs,\n config.fallbackLng,\n nonExplicit,\n )\n }\n if (!lng) {\n lng = config.fallbackLng\n }\n\n // For locale-in-path detection, strip basePath prefix so we look at the right segment\n const pathForLocale = basePath ? pathname.slice(basePath.length) || '/' : pathname\n const lngInPath = findLocaleInPath(pathForLocale, config.supportedLngs, nonExplicit)\n\n if (config.localeInPath) {\n // Set custom header for server components to read\n const headers = new Headers(req.headers)\n headers.set(config.headerName, lngInPath || lng)\n\n // Redirect if no locale in path\n if (!lngInPath) {\n const prefix = basePath ?? ''\n const pathAfterBase = basePath ? pathname.slice(basePath.length) : pathname\n const redirectUrl = new URL(`${prefix}/${lng}${pathAfterBase}${search}`, req.url)\n const response = NextResponse.redirect(redirectUrl)\n response.cookies.set(config.cookieName, lng, {\n path: '/',\n maxAge: config.cookieMaxAge,\n sameSite: 'lax',\n })\n return response\n }\n\n // Persist language from referer URL into cookie\n const response = NextResponse.next({ request: { headers } })\n if (req.headers.has('referer')) {\n const refererUrl = new URL(req.headers.get('referer')!)\n const refererPathForLocale = basePath\n ? refererUrl.pathname.slice(basePath.length) || '/'\n : refererUrl.pathname\n const lngInReferer = findLocaleInPath(refererPathForLocale, config.supportedLngs, nonExplicit)\n if (lngInReferer) {\n response.cookies.set(config.cookieName, lngInReferer, {\n path: '/',\n maxAge: config.cookieMaxAge,\n sameSite: 'lax',\n })\n }\n }\n\n return response\n } else {\n // No-locale-path mode: don't redirect, just set the header\n const headers = new Headers(req.headers)\n headers.set(config.headerName, lng)\n\n const response = NextResponse.next({ request: { headers } })\n return response\n }\n }\n}\n\n/**\n * Backwards-compatible alias for createProxy.\n * Use `createProxy` for new projects with Next.js 16+ `proxy.ts`.\n */\nexport const createMiddleware = createProxy\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["NextResponse"],"sources":["../../../src/appRouter/config.ts","../../../src/appRouter/proxy/languageDetector.ts","../../../src/appRouter/proxy/index.ts"],"sourcesContent":["import type { I18nConfig, NormalizedConfig } from './types'\n\nexport function defineConfig(config: I18nConfig): I18nConfig {\n return config\n}\n\nexport function normalizeConfig(userConfig: I18nConfig): NormalizedConfig {\n // Support legacy format: { i18n: { defaultLocale, locales } }\n const supportedLngs = userConfig.supportedLngs ??\n userConfig.i18n?.locales?.filter((l: string) => l !== 'default') ??\n ['en']\n const fallbackLng = userConfig.fallbackLng ??\n userConfig.i18n?.defaultLocale ??\n supportedLngs[0]\n\n if (!fallbackLng) {\n throw new Error('next-i18next: fallbackLng (or i18n.defaultLocale) is required')\n }\n if (supportedLngs.length === 0) {\n throw new Error('next-i18next: supportedLngs (or i18n.locales) must contain at least one language')\n }\n\n const defaultNS = userConfig.defaultNS ?? 'common'\n\n return {\n supportedLngs,\n fallbackLng,\n defaultNS,\n ns: userConfig.ns ?? [defaultNS],\n localeInPath: userConfig.localeInPath ?? true,\n hideDefaultLocale: userConfig.hideDefaultLocale ?? false,\n localePath: userConfig.localePath ?? '/locales',\n localeStructure: userConfig.localeStructure ?? '{{lng}}/{{ns}}',\n localeExtension: userConfig.localeExtension ?? 'json',\n cookieName: userConfig.cookieName ?? 'i18next',\n headerName: userConfig.headerName ?? 'x-i18next-current-language',\n cookieMaxAge: userConfig.cookieMaxAge ?? 365 * 24 * 60 * 60,\n ignoredPaths: userConfig.ignoredPaths ?? ['/api', '/_next', '/static'],\n basePath: userConfig.basePath,\n resources: userConfig.resources,\n resourceLoader: userConfig.resourceLoader,\n use: userConfig.use ?? [],\n i18nextOptions: (userConfig.i18nextOptions ?? {}) as Record<string, any>,\n nonExplicitSupportedLngs: userConfig.nonExplicitSupportedLngs ?? false,\n // Preserve legacy fields\n i18n: userConfig.i18n,\n serializeConfig: userConfig.serializeConfig,\n reloadOnPrerender: userConfig.reloadOnPrerender,\n }\n}\n","/**\n * Edge-safe Accept-Language parser and language matcher.\n * No external dependencies — runs in Edge Runtime, Node.js, and browser.\n */\n\nexport function parseAcceptLanguage(header: string | null | undefined): string[] {\n if (!header) return []\n return header\n .split(',')\n .map(part => {\n const [lang, qPart] = part.trim().split(';')\n const q = qPart?.trim().startsWith('q=')\n ? parseFloat(qPart.trim().slice(2))\n : 1.0\n return { lang: lang.trim(), q: isNaN(q) ? 0 : q }\n })\n .filter(item => item.lang && item.q > 0)\n .sort((a, b) => b.q - a.q)\n .map(item => item.lang)\n}\n\n/**\n * Find a matching supported language for a given code, respecting nonExplicitSupportedLngs.\n *\n * Matching order (mirrors i18next's LanguageUtils.getBestMatchFromCodes):\n * 1. Exact match (case-insensitive)\n * 2. Preferred prefix → supported base (e.g. preferred 'en-US' matches supported 'en')\n * 3. If nonExplicitSupportedLngs: preferred base → supported region\n * (e.g. preferred 'en' matches supported 'en-US')\n */\nexport function findSupportedMatch(\n code: string,\n supportedLanguages: readonly string[],\n nonExplicitSupportedLngs: boolean,\n): string | undefined {\n const lower = code.toLowerCase()\n\n // 1. Exact match (case-insensitive)\n const exact = supportedLanguages.find(l => l.toLowerCase() === lower)\n if (exact) return exact\n\n // 2. Preferred prefix → supported base (e.g. 'en-US' → 'en')\n const prefix = lower.split('-')[0]\n const partial = supportedLanguages.find(l => l.toLowerCase() === prefix)\n if (partial) return partial\n\n // 3. Reverse match: preferred base → supported region (e.g. 'en' → 'en-US')\n if (nonExplicitSupportedLngs) {\n const reverse = supportedLanguages.find(\n l => l.toLowerCase().split('-')[0] === prefix\n )\n if (reverse) return reverse\n }\n\n return undefined\n}\n\nexport function matchLanguage(\n acceptLanguages: string[],\n supportedLanguages: readonly string[],\n defaultLanguage: string,\n nonExplicitSupportedLngs = false,\n): string {\n for (const preferred of acceptLanguages) {\n const match = findSupportedMatch(preferred, supportedLanguages, nonExplicitSupportedLngs)\n if (match) return match\n }\n return defaultLanguage\n}\n","import { NextRequest, NextResponse } from 'next/server'\nimport type { I18nConfig } from '../types'\nimport { normalizeConfig } from '../config'\nimport { parseAcceptLanguage, matchLanguage, findSupportedMatch } from './languageDetector'\n\n// Re-export config utilities for Edge-safe usage (no react-i18next dependency)\nexport { defineConfig, normalizeConfig } from '../config'\nexport type { I18nConfig, NormalizedConfig, ResourceLoader } from '../types'\n\nfunction findLocaleInPath(\n pathname: string,\n supportedLngs: readonly string[],\n nonExplicitSupportedLngs: boolean,\n): string | undefined {\n // Extract the first path segment\n const match = pathname.match(/^\\/([^/]+)/)\n if (!match) return undefined\n return findSupportedMatch(match[1], supportedLngs, nonExplicitSupportedLngs)\n}\n\nexport function createProxy(userConfig: I18nConfig) {\n const config = normalizeConfig(userConfig)\n const nonExplicit = config.nonExplicitSupportedLngs\n // Normalize basePath: ensure leading slash, strip trailing slash\n const basePath = config.basePath\n ? ('/' + config.basePath.replace(/^\\/+/, '').replace(/\\/+$/, ''))\n : undefined\n\n return function middleware(req: NextRequest): NextResponse {\n const { pathname, search } = req.nextUrl\n\n // When basePath is set, only handle requests under that prefix\n if (basePath) {\n if (pathname !== basePath && !pathname.startsWith(basePath + '/')) {\n return NextResponse.next()\n }\n }\n\n // Skip ignored paths\n for (const ignored of config.ignoredPaths) {\n if (pathname.startsWith(ignored)) {\n return NextResponse.next()\n }\n }\n\n // Skip common static file extensions\n if (/\\.(ico|png|jpg|jpeg|svg|gif|webp|css|js|map|woff2?|ttf|eot)$/.test(pathname)) {\n return NextResponse.next()\n }\n\n // Detect language from cookie, then Accept-Language header, then default\n let lng: string | undefined\n const cookieValue = req.cookies.get(config.cookieName)?.value\n if (cookieValue) {\n lng = matchLanguage([cookieValue], config.supportedLngs, config.fallbackLng, nonExplicit)\n }\n if (!lng) {\n lng = matchLanguage(\n parseAcceptLanguage(req.headers.get('Accept-Language')),\n config.supportedLngs,\n config.fallbackLng,\n nonExplicit,\n )\n }\n if (!lng) {\n lng = config.fallbackLng\n }\n\n // For locale-in-path detection, strip basePath prefix so we look at the right segment\n const pathForLocale = basePath ? pathname.slice(basePath.length) || '/' : pathname\n const lngInPath = findLocaleInPath(pathForLocale, config.supportedLngs, nonExplicit)\n\n if (config.localeInPath) {\n const prefix = basePath ?? ''\n const pathAfterBase = basePath ? pathname.slice(basePath.length) : pathname\n\n // hideDefaultLocale: redirect explicit default-locale paths to the clean URL\n if (config.hideDefaultLocale && lngInPath === config.fallbackLng) {\n const pathWithoutLocale = pathAfterBase.replace(/^\\/[^/]+/, '') || '/'\n const redirectUrl = new URL(`${prefix}${pathWithoutLocale}${search}`, req.url)\n const response = NextResponse.redirect(redirectUrl)\n response.cookies.set(config.cookieName, config.fallbackLng, {\n path: '/',\n maxAge: config.cookieMaxAge,\n sameSite: 'lax',\n })\n return response\n }\n\n // Set custom header for server components to read\n const headers = new Headers(req.headers)\n headers.set(config.headerName, lngInPath || lng)\n\n // Redirect if no locale in path\n if (!lngInPath) {\n if (config.hideDefaultLocale) {\n // Rewrite internally to the default-locale path, keeping the clean URL\n const rewriteUrl = new URL(`${prefix}/${config.fallbackLng}${pathAfterBase}${search}`, req.url)\n headers.set(config.headerName, config.fallbackLng)\n const response = NextResponse.rewrite(rewriteUrl, { request: { headers } })\n response.cookies.set(config.cookieName, config.fallbackLng, {\n path: '/',\n maxAge: config.cookieMaxAge,\n sameSite: 'lax',\n })\n return response\n }\n\n const redirectUrl = new URL(`${prefix}/${lng}${pathAfterBase}${search}`, req.url)\n const response = NextResponse.redirect(redirectUrl)\n response.cookies.set(config.cookieName, lng, {\n path: '/',\n maxAge: config.cookieMaxAge,\n sameSite: 'lax',\n })\n return response\n }\n\n // Persist language from referer URL into cookie\n const response = NextResponse.next({ request: { headers } })\n if (req.headers.has('referer')) {\n const refererUrl = new URL(req.headers.get('referer')!)\n const refererPathForLocale = basePath\n ? refererUrl.pathname.slice(basePath.length) || '/'\n : refererUrl.pathname\n const lngInReferer = findLocaleInPath(refererPathForLocale, config.supportedLngs, nonExplicit)\n if (lngInReferer) {\n response.cookies.set(config.cookieName, lngInReferer, {\n path: '/',\n maxAge: config.cookieMaxAge,\n sameSite: 'lax',\n })\n }\n }\n\n return response\n } else {\n // No-locale-path mode: don't redirect, just set the header\n const headers = new Headers(req.headers)\n headers.set(config.headerName, lng)\n\n const response = NextResponse.next({ request: { headers } })\n return response\n }\n }\n}\n\n/**\n * Backwards-compatible alias for createProxy.\n * Use `createProxy` for new projects with Next.js 16+ `proxy.ts`.\n */\nexport const createMiddleware = createProxy\n"],"mappings":";;;AAEA,SAAgB,aAAa,QAAgC;AAC3D,QAAO;;AAGT,SAAgB,gBAAgB,YAA0C;CAExE,MAAM,gBAAgB,WAAW,iBAC/B,WAAW,MAAM,SAAS,QAAQ,MAAc,MAAM,UAAU,IAChE,CAAC,KAAK;CACR,MAAM,cAAc,WAAW,eAC7B,WAAW,MAAM,iBACjB,cAAc;AAEhB,KAAI,CAAC,YACH,OAAM,IAAI,MAAM,gEAAgE;AAElF,KAAI,cAAc,WAAW,EAC3B,OAAM,IAAI,MAAM,mFAAmF;CAGrG,MAAM,YAAY,WAAW,aAAa;AAE1C,QAAO;EACL;EACA;EACA;EACA,IAAI,WAAW,MAAM,CAAC,UAAU;EAChC,cAAc,WAAW,gBAAgB;EACzC,mBAAmB,WAAW,qBAAqB;EACnD,YAAY,WAAW,cAAc;EACrC,iBAAiB,WAAW,mBAAmB;EAC/C,iBAAiB,WAAW,mBAAmB;EAC/C,YAAY,WAAW,cAAc;EACrC,YAAY,WAAW,cAAc;EACrC,cAAc,WAAW,gBAAgB,MAAM,KAAK,KAAK;EACzD,cAAc,WAAW,gBAAgB;GAAC;GAAQ;GAAU;GAAU;EACtE,UAAU,WAAW;EACrB,WAAW,WAAW;EACtB,gBAAgB,WAAW;EAC3B,KAAK,WAAW,OAAO,EAAE;EACzB,gBAAiB,WAAW,kBAAkB,EAAE;EAChD,0BAA0B,WAAW,4BAA4B;EAEjE,MAAM,WAAW;EACjB,iBAAiB,WAAW;EAC5B,mBAAmB,WAAW;EAC/B;;;;;;;;AC3CH,SAAgB,oBAAoB,QAA6C;AAC/E,KAAI,CAAC,OAAQ,QAAO,EAAE;AACtB,QAAO,OACJ,MAAM,IAAI,CACV,KAAI,SAAQ;EACX,MAAM,CAAC,MAAM,SAAS,KAAK,MAAM,CAAC,MAAM,IAAI;EAC5C,MAAM,IAAI,OAAO,MAAM,CAAC,WAAW,KAAK,GACpC,WAAW,MAAM,MAAM,CAAC,MAAM,EAAE,CAAC,GACjC;AACJ,SAAO;GAAE,MAAM,KAAK,MAAM;GAAE,GAAG,MAAM,EAAE,GAAG,IAAI;GAAG;GACjD,CACD,QAAO,SAAQ,KAAK,QAAQ,KAAK,IAAI,EAAE,CACvC,MAAM,GAAG,MAAM,EAAE,IAAI,EAAE,EAAE,CACzB,KAAI,SAAQ,KAAK,KAAK;;;;;;;;;;;AAY3B,SAAgB,mBACd,MACA,oBACA,0BACoB;CACpB,MAAM,QAAQ,KAAK,aAAa;CAGhC,MAAM,QAAQ,mBAAmB,MAAK,MAAK,EAAE,aAAa,KAAK,MAAM;AACrE,KAAI,MAAO,QAAO;CAGlB,MAAM,SAAS,MAAM,MAAM,IAAI,CAAC;CAChC,MAAM,UAAU,mBAAmB,MAAK,MAAK,EAAE,aAAa,KAAK,OAAO;AACxE,KAAI,QAAS,QAAO;AAGpB,KAAI,0BAA0B;EAC5B,MAAM,UAAU,mBAAmB,MACjC,MAAK,EAAE,aAAa,CAAC,MAAM,IAAI,CAAC,OAAO,OACxC;AACD,MAAI,QAAS,QAAO;;;AAMxB,SAAgB,cACd,iBACA,oBACA,iBACA,2BAA2B,OACnB;AACR,MAAK,MAAM,aAAa,iBAAiB;EACvC,MAAM,QAAQ,mBAAmB,WAAW,oBAAoB,yBAAyB;AACzF,MAAI,MAAO,QAAO;;AAEpB,QAAO;;;;AC1DT,SAAS,iBACP,UACA,eACA,0BACoB;CAEpB,MAAM,QAAQ,SAAS,MAAM,aAAa;AAC1C,KAAI,CAAC,MAAO,QAAO,KAAA;AACnB,QAAO,mBAAmB,MAAM,IAAI,eAAe,yBAAyB;;AAG9E,SAAgB,YAAY,YAAwB;CAClD,MAAM,SAAS,gBAAgB,WAAW;CAC1C,MAAM,cAAc,OAAO;CAE3B,MAAM,WAAW,OAAO,WACnB,MAAM,OAAO,SAAS,QAAQ,QAAQ,GAAG,CAAC,QAAQ,QAAQ,GAAG,GAC9D,KAAA;AAEJ,QAAO,SAAS,WAAW,KAAgC;EACzD,MAAM,EAAE,UAAU,WAAW,IAAI;AAGjC,MAAI;OACE,aAAa,YAAY,CAAC,SAAS,WAAW,WAAW,IAAI,CAC/D,QAAOA,YAAAA,aAAa,MAAM;;AAK9B,OAAK,MAAM,WAAW,OAAO,aAC3B,KAAI,SAAS,WAAW,QAAQ,CAC9B,QAAOA,YAAAA,aAAa,MAAM;AAK9B,MAAI,+DAA+D,KAAK,SAAS,CAC/E,QAAOA,YAAAA,aAAa,MAAM;EAI5B,IAAI;EACJ,MAAM,cAAc,IAAI,QAAQ,IAAI,OAAO,WAAW,EAAE;AACxD,MAAI,YACF,OAAM,cAAc,CAAC,YAAY,EAAE,OAAO,eAAe,OAAO,aAAa,YAAY;AAE3F,MAAI,CAAC,IACH,OAAM,cACJ,oBAAoB,IAAI,QAAQ,IAAI,kBAAkB,CAAC,EACvD,OAAO,eACP,OAAO,aACP,YACD;AAEH,MAAI,CAAC,IACH,OAAM,OAAO;EAKf,MAAM,YAAY,iBADI,WAAW,SAAS,MAAM,SAAS,OAAO,IAAI,MAAM,UACxB,OAAO,eAAe,YAAY;AAEpF,MAAI,OAAO,cAAc;GACvB,MAAM,SAAS,YAAY;GAC3B,MAAM,gBAAgB,WAAW,SAAS,MAAM,SAAS,OAAO,GAAG;AAGnE,OAAI,OAAO,qBAAqB,cAAc,OAAO,aAAa;IAChE,MAAM,oBAAoB,cAAc,QAAQ,YAAY,GAAG,IAAI;IACnE,MAAM,cAAc,IAAI,IAAI,GAAG,SAAS,oBAAoB,UAAU,IAAI,IAAI;IAC9E,MAAM,WAAWA,YAAAA,aAAa,SAAS,YAAY;AACnD,aAAS,QAAQ,IAAI,OAAO,YAAY,OAAO,aAAa;KAC1D,MAAM;KACN,QAAQ,OAAO;KACf,UAAU;KACX,CAAC;AACF,WAAO;;GAIT,MAAM,UAAU,IAAI,QAAQ,IAAI,QAAQ;AACxC,WAAQ,IAAI,OAAO,YAAY,aAAa,IAAI;AAGhD,OAAI,CAAC,WAAW;AACd,QAAI,OAAO,mBAAmB;KAE5B,MAAM,aAAa,IAAI,IAAI,GAAG,OAAO,GAAG,OAAO,cAAc,gBAAgB,UAAU,IAAI,IAAI;AAC/F,aAAQ,IAAI,OAAO,YAAY,OAAO,YAAY;KAClD,MAAM,WAAWA,YAAAA,aAAa,QAAQ,YAAY,EAAE,SAAS,EAAE,SAAS,EAAE,CAAC;AAC3E,cAAS,QAAQ,IAAI,OAAO,YAAY,OAAO,aAAa;MAC1D,MAAM;MACN,QAAQ,OAAO;MACf,UAAU;MACX,CAAC;AACF,YAAO;;IAGT,MAAM,cAAc,IAAI,IAAI,GAAG,OAAO,GAAG,MAAM,gBAAgB,UAAU,IAAI,IAAI;IACjF,MAAM,WAAWA,YAAAA,aAAa,SAAS,YAAY;AACnD,aAAS,QAAQ,IAAI,OAAO,YAAY,KAAK;KAC3C,MAAM;KACN,QAAQ,OAAO;KACf,UAAU;KACX,CAAC;AACF,WAAO;;GAIT,MAAM,WAAWA,YAAAA,aAAa,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,CAAC;AAC5D,OAAI,IAAI,QAAQ,IAAI,UAAU,EAAE;IAC9B,MAAM,aAAa,IAAI,IAAI,IAAI,QAAQ,IAAI,UAAU,CAAE;IAIvD,MAAM,eAAe,iBAHQ,WACzB,WAAW,SAAS,MAAM,SAAS,OAAO,IAAI,MAC9C,WAAW,UAC6C,OAAO,eAAe,YAAY;AAC9F,QAAI,aACF,UAAS,QAAQ,IAAI,OAAO,YAAY,cAAc;KACpD,MAAM;KACN,QAAQ,OAAO;KACf,UAAU;KACX,CAAC;;AAIN,UAAO;SACF;GAEL,MAAM,UAAU,IAAI,QAAQ,IAAI,QAAQ;AACxC,WAAQ,IAAI,OAAO,YAAY,IAAI;AAGnC,UADiBA,YAAAA,aAAa,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,CAAC;;;;;;;;AAUlE,MAAa,mBAAmB"}
|
|
@@ -24,6 +24,10 @@ interface I18nConfig {
|
|
|
24
24
|
resourceLoader?: ResourceLoader;
|
|
25
25
|
/** Whether to include locale in URL path (defaults to true) */
|
|
26
26
|
localeInPath?: boolean;
|
|
27
|
+
/** When true (and localeInPath is true), the default language has no URL prefix.
|
|
28
|
+
* e.g. `/about` serves the default language, `/de/about` serves German.
|
|
29
|
+
* Requests to the explicit default prefix (`/en/about`) are redirected to `/about`. */
|
|
30
|
+
hideDefaultLocale?: boolean;
|
|
27
31
|
/** Cookie name for storing selected language (defaults to 'i18next') */
|
|
28
32
|
cookieName?: string;
|
|
29
33
|
/** Custom header name for passing language to server components (defaults to 'x-i18next-current-language') */
|
|
@@ -63,6 +67,7 @@ interface NormalizedConfig {
|
|
|
63
67
|
defaultNS: string;
|
|
64
68
|
ns: string[];
|
|
65
69
|
localeInPath: boolean;
|
|
70
|
+
hideDefaultLocale: boolean;
|
|
66
71
|
localePath: string;
|
|
67
72
|
localeStructure: string;
|
|
68
73
|
localeExtension: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":[],"sources":["../../../src/appRouter/types.ts","../../../src/appRouter/config.ts","../../../src/appRouter/proxy/index.ts"],"mappings":";;;;KAEY,cAAA,IAAkB,QAAA,UAAkB,SAAA,aAAsB,OAAA;AAAA,UAErD,UAAA;;EAEf,aAAA;EAJwB;EAMxB,WAAA;EAN2E;EAQ3E,SAAA;EAR8C;EAU9C,EAAA;EAV2E;EAc3E,UAAA;EAZe;EAcf,eAAA;;EAEA,eAAA;EAIiB;EAFjB,SAAA,GAAY,QAAA;
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../../../src/appRouter/types.ts","../../../src/appRouter/config.ts","../../../src/appRouter/proxy/index.ts"],"mappings":";;;;KAEY,cAAA,IAAkB,QAAA,UAAkB,SAAA,aAAsB,OAAA;AAAA,UAErD,UAAA;;EAEf,aAAA;EAJwB;EAMxB,WAAA;EAN2E;EAQ3E,SAAA;EAR8C;EAU9C,EAAA;EAV2E;EAc3E,UAAA;EAZe;EAcf,eAAA;;EAEA,eAAA;EAIiB;EAFjB,SAAA,GAAY,QAAA;EA4BK;EA1BjB,cAAA,GAAiB,cAAA;EA0BI;EAtBrB,YAAA;EApBA;;;EAwBA,iBAAA;EAdA;EAkBA,UAAA;EAdA;EAgBA,UAAA;EAdA;EAgBA,YAAA;EAZA;EAcA,YAAA;EANA;EAQA,QAAA;EAJA;EAQA,GAAA;EAJA;EAMA,cAAA,GAAiB,IAAA,CAAK,WAAA;EAAtB;EAIA,IAAA;IACE,aAAA;IACA,OAAA;IACA,OAAA;MACE,aAAA;MACA,MAAA;MACA,IAAA;MACA,OAAA;IAAA;IAEF,eAAA;EAAA;EAGF;EAAA,eAAA;EAIA;EAFA,iBAAA;EAEwB;EAAxB,wBAAA;AAAA;AAAA,UAGe,gBAAA;EACf,aAAA;EACA,WAAA;EACA,SAAA;EACA,EAAA;EACA,YAAA;EACA,iBAAA;EACA,UAAA;EACA,eAAA;EACA,eAAA;EACA,UAAA;EACA,UAAA;EACA,YAAA;EACA,YAAA;EACA,QAAA;EACA,SAAA,GAAY,QAAA;EACZ,cAAA,GAAiB,cAAA;EACjB,GAAA;EACA,cAAA,EAAgB,MAAA;EAChB,wBAAA;EAEA,IAAA,GAAO,UAAA;EACP,eAAA;EACA,iBAAA;AAAA;;;iBC9Fc,YAAA,CAAa,MAAA,EAAQ,UAAA,GAAa,UAAA;AAAA,iBAIlC,eAAA,CAAgB,UAAA,EAAY,UAAA,GAAa,gBAAA;;;iBCczC,WAAA,CAAY,UAAA,EAAY,UAAA,IAQX,GAAA,EAAK,WAAA,KAAc,YAAA;;;;;cA2HnC,gBAAA,SAAgB,WAAA"}
|
|
@@ -24,6 +24,10 @@ interface I18nConfig {
|
|
|
24
24
|
resourceLoader?: ResourceLoader;
|
|
25
25
|
/** Whether to include locale in URL path (defaults to true) */
|
|
26
26
|
localeInPath?: boolean;
|
|
27
|
+
/** When true (and localeInPath is true), the default language has no URL prefix.
|
|
28
|
+
* e.g. `/about` serves the default language, `/de/about` serves German.
|
|
29
|
+
* Requests to the explicit default prefix (`/en/about`) are redirected to `/about`. */
|
|
30
|
+
hideDefaultLocale?: boolean;
|
|
27
31
|
/** Cookie name for storing selected language (defaults to 'i18next') */
|
|
28
32
|
cookieName?: string;
|
|
29
33
|
/** Custom header name for passing language to server components (defaults to 'x-i18next-current-language') */
|
|
@@ -63,6 +67,7 @@ interface NormalizedConfig {
|
|
|
63
67
|
defaultNS: string;
|
|
64
68
|
ns: string[];
|
|
65
69
|
localeInPath: boolean;
|
|
70
|
+
hideDefaultLocale: boolean;
|
|
66
71
|
localePath: string;
|
|
67
72
|
localeStructure: string;
|
|
68
73
|
localeExtension: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../../src/appRouter/types.ts","../../../src/appRouter/config.ts","../../../src/appRouter/proxy/index.ts"],"mappings":";;;;KAEY,cAAA,IAAkB,QAAA,UAAkB,SAAA,aAAsB,OAAA;AAAA,UAErD,UAAA;;EAEf,aAAA;EAJwB;EAMxB,WAAA;EAN2E;EAQ3E,SAAA;EAR8C;EAU9C,EAAA;EAV2E;EAc3E,UAAA;EAZe;EAcf,eAAA;;EAEA,eAAA;EAIiB;EAFjB,SAAA,GAAY,QAAA;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../../src/appRouter/types.ts","../../../src/appRouter/config.ts","../../../src/appRouter/proxy/index.ts"],"mappings":";;;;KAEY,cAAA,IAAkB,QAAA,UAAkB,SAAA,aAAsB,OAAA;AAAA,UAErD,UAAA;;EAEf,aAAA;EAJwB;EAMxB,WAAA;EAN2E;EAQ3E,SAAA;EAR8C;EAU9C,EAAA;EAV2E;EAc3E,UAAA;EAZe;EAcf,eAAA;;EAEA,eAAA;EAIiB;EAFjB,SAAA,GAAY,QAAA;EA4BK;EA1BjB,cAAA,GAAiB,cAAA;EA0BI;EAtBrB,YAAA;EApBA;;;EAwBA,iBAAA;EAdA;EAkBA,UAAA;EAdA;EAgBA,UAAA;EAdA;EAgBA,YAAA;EAZA;EAcA,YAAA;EANA;EAQA,QAAA;EAJA;EAQA,GAAA;EAJA;EAMA,cAAA,GAAiB,IAAA,CAAK,WAAA;EAAtB;EAIA,IAAA;IACE,aAAA;IACA,OAAA;IACA,OAAA;MACE,aAAA;MACA,MAAA;MACA,IAAA;MACA,OAAA;IAAA;IAEF,eAAA;EAAA;EAGF;EAAA,eAAA;EAIA;EAFA,iBAAA;EAEwB;EAAxB,wBAAA;AAAA;AAAA,UAGe,gBAAA;EACf,aAAA;EACA,WAAA;EACA,SAAA;EACA,EAAA;EACA,YAAA;EACA,iBAAA;EACA,UAAA;EACA,eAAA;EACA,eAAA;EACA,UAAA;EACA,UAAA;EACA,YAAA;EACA,YAAA;EACA,QAAA;EACA,SAAA,GAAY,QAAA;EACZ,cAAA,GAAiB,cAAA;EACjB,GAAA;EACA,cAAA,EAAgB,MAAA;EAChB,wBAAA;EAEA,IAAA,GAAO,UAAA;EACP,eAAA;EACA,iBAAA;AAAA;;;iBC9Fc,YAAA,CAAa,MAAA,EAAQ,UAAA,GAAa,UAAA;AAAA,iBAIlC,eAAA,CAAgB,UAAA,EAAY,UAAA,GAAa,gBAAA;;;iBCczC,WAAA,CAAY,UAAA,EAAY,UAAA,IAQX,GAAA,EAAK,WAAA,KAAc,YAAA;;;;;cA2HnC,gBAAA,SAAgB,WAAA"}
|
|
@@ -15,6 +15,7 @@ function normalizeConfig(userConfig) {
|
|
|
15
15
|
defaultNS,
|
|
16
16
|
ns: userConfig.ns ?? [defaultNS],
|
|
17
17
|
localeInPath: userConfig.localeInPath ?? true,
|
|
18
|
+
hideDefaultLocale: userConfig.hideDefaultLocale ?? false,
|
|
18
19
|
localePath: userConfig.localePath ?? "/locales",
|
|
19
20
|
localeStructure: userConfig.localeStructure ?? "{{lng}}/{{ns}}",
|
|
20
21
|
localeExtension: userConfig.localeExtension ?? "json",
|
|
@@ -107,11 +108,33 @@ function createProxy(userConfig) {
|
|
|
107
108
|
if (!lng) lng = config.fallbackLng;
|
|
108
109
|
const lngInPath = findLocaleInPath(basePath ? pathname.slice(basePath.length) || "/" : pathname, config.supportedLngs, nonExplicit);
|
|
109
110
|
if (config.localeInPath) {
|
|
111
|
+
const prefix = basePath ?? "";
|
|
112
|
+
const pathAfterBase = basePath ? pathname.slice(basePath.length) : pathname;
|
|
113
|
+
if (config.hideDefaultLocale && lngInPath === config.fallbackLng) {
|
|
114
|
+
const pathWithoutLocale = pathAfterBase.replace(/^\/[^/]+/, "") || "/";
|
|
115
|
+
const redirectUrl = new URL(`${prefix}${pathWithoutLocale}${search}`, req.url);
|
|
116
|
+
const response = NextResponse.redirect(redirectUrl);
|
|
117
|
+
response.cookies.set(config.cookieName, config.fallbackLng, {
|
|
118
|
+
path: "/",
|
|
119
|
+
maxAge: config.cookieMaxAge,
|
|
120
|
+
sameSite: "lax"
|
|
121
|
+
});
|
|
122
|
+
return response;
|
|
123
|
+
}
|
|
110
124
|
const headers = new Headers(req.headers);
|
|
111
125
|
headers.set(config.headerName, lngInPath || lng);
|
|
112
126
|
if (!lngInPath) {
|
|
113
|
-
|
|
114
|
-
|
|
127
|
+
if (config.hideDefaultLocale) {
|
|
128
|
+
const rewriteUrl = new URL(`${prefix}/${config.fallbackLng}${pathAfterBase}${search}`, req.url);
|
|
129
|
+
headers.set(config.headerName, config.fallbackLng);
|
|
130
|
+
const response = NextResponse.rewrite(rewriteUrl, { request: { headers } });
|
|
131
|
+
response.cookies.set(config.cookieName, config.fallbackLng, {
|
|
132
|
+
path: "/",
|
|
133
|
+
maxAge: config.cookieMaxAge,
|
|
134
|
+
sameSite: "lax"
|
|
135
|
+
});
|
|
136
|
+
return response;
|
|
137
|
+
}
|
|
115
138
|
const redirectUrl = new URL(`${prefix}/${lng}${pathAfterBase}${search}`, req.url);
|
|
116
139
|
const response = NextResponse.redirect(redirectUrl);
|
|
117
140
|
response.cookies.set(config.cookieName, lng, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../../src/appRouter/config.ts","../../../src/appRouter/proxy/languageDetector.ts","../../../src/appRouter/proxy/index.ts"],"sourcesContent":["import type { I18nConfig, NormalizedConfig } from './types'\n\nexport function defineConfig(config: I18nConfig): I18nConfig {\n return config\n}\n\nexport function normalizeConfig(userConfig: I18nConfig): NormalizedConfig {\n // Support legacy format: { i18n: { defaultLocale, locales } }\n const supportedLngs = userConfig.supportedLngs ??\n userConfig.i18n?.locales?.filter((l: string) => l !== 'default') ??\n ['en']\n const fallbackLng = userConfig.fallbackLng ??\n userConfig.i18n?.defaultLocale ??\n supportedLngs[0]\n\n if (!fallbackLng) {\n throw new Error('next-i18next: fallbackLng (or i18n.defaultLocale) is required')\n }\n if (supportedLngs.length === 0) {\n throw new Error('next-i18next: supportedLngs (or i18n.locales) must contain at least one language')\n }\n\n const defaultNS = userConfig.defaultNS ?? 'common'\n\n return {\n supportedLngs,\n fallbackLng,\n defaultNS,\n ns: userConfig.ns ?? [defaultNS],\n localeInPath: userConfig.localeInPath ?? true,\n localePath: userConfig.localePath ?? '/locales',\n localeStructure: userConfig.localeStructure ?? '{{lng}}/{{ns}}',\n localeExtension: userConfig.localeExtension ?? 'json',\n cookieName: userConfig.cookieName ?? 'i18next',\n headerName: userConfig.headerName ?? 'x-i18next-current-language',\n cookieMaxAge: userConfig.cookieMaxAge ?? 365 * 24 * 60 * 60,\n ignoredPaths: userConfig.ignoredPaths ?? ['/api', '/_next', '/static'],\n basePath: userConfig.basePath,\n resources: userConfig.resources,\n resourceLoader: userConfig.resourceLoader,\n use: userConfig.use ?? [],\n i18nextOptions: (userConfig.i18nextOptions ?? {}) as Record<string, any>,\n nonExplicitSupportedLngs: userConfig.nonExplicitSupportedLngs ?? false,\n // Preserve legacy fields\n i18n: userConfig.i18n,\n serializeConfig: userConfig.serializeConfig,\n reloadOnPrerender: userConfig.reloadOnPrerender,\n }\n}\n","/**\n * Edge-safe Accept-Language parser and language matcher.\n * No external dependencies — runs in Edge Runtime, Node.js, and browser.\n */\n\nexport function parseAcceptLanguage(header: string | null | undefined): string[] {\n if (!header) return []\n return header\n .split(',')\n .map(part => {\n const [lang, qPart] = part.trim().split(';')\n const q = qPart?.trim().startsWith('q=')\n ? parseFloat(qPart.trim().slice(2))\n : 1.0\n return { lang: lang.trim(), q: isNaN(q) ? 0 : q }\n })\n .filter(item => item.lang && item.q > 0)\n .sort((a, b) => b.q - a.q)\n .map(item => item.lang)\n}\n\n/**\n * Find a matching supported language for a given code, respecting nonExplicitSupportedLngs.\n *\n * Matching order (mirrors i18next's LanguageUtils.getBestMatchFromCodes):\n * 1. Exact match (case-insensitive)\n * 2. Preferred prefix → supported base (e.g. preferred 'en-US' matches supported 'en')\n * 3. If nonExplicitSupportedLngs: preferred base → supported region\n * (e.g. preferred 'en' matches supported 'en-US')\n */\nexport function findSupportedMatch(\n code: string,\n supportedLanguages: readonly string[],\n nonExplicitSupportedLngs: boolean,\n): string | undefined {\n const lower = code.toLowerCase()\n\n // 1. Exact match (case-insensitive)\n const exact = supportedLanguages.find(l => l.toLowerCase() === lower)\n if (exact) return exact\n\n // 2. Preferred prefix → supported base (e.g. 'en-US' → 'en')\n const prefix = lower.split('-')[0]\n const partial = supportedLanguages.find(l => l.toLowerCase() === prefix)\n if (partial) return partial\n\n // 3. Reverse match: preferred base → supported region (e.g. 'en' → 'en-US')\n if (nonExplicitSupportedLngs) {\n const reverse = supportedLanguages.find(\n l => l.toLowerCase().split('-')[0] === prefix\n )\n if (reverse) return reverse\n }\n\n return undefined\n}\n\nexport function matchLanguage(\n acceptLanguages: string[],\n supportedLanguages: readonly string[],\n defaultLanguage: string,\n nonExplicitSupportedLngs = false,\n): string {\n for (const preferred of acceptLanguages) {\n const match = findSupportedMatch(preferred, supportedLanguages, nonExplicitSupportedLngs)\n if (match) return match\n }\n return defaultLanguage\n}\n","import { NextRequest, NextResponse } from 'next/server'\nimport type { I18nConfig } from '../types'\nimport { normalizeConfig } from '../config'\nimport { parseAcceptLanguage, matchLanguage, findSupportedMatch } from './languageDetector'\n\n// Re-export config utilities for Edge-safe usage (no react-i18next dependency)\nexport { defineConfig, normalizeConfig } from '../config'\nexport type { I18nConfig, NormalizedConfig, ResourceLoader } from '../types'\n\nfunction findLocaleInPath(\n pathname: string,\n supportedLngs: readonly string[],\n nonExplicitSupportedLngs: boolean,\n): string | undefined {\n // Extract the first path segment\n const match = pathname.match(/^\\/([^/]+)/)\n if (!match) return undefined\n return findSupportedMatch(match[1], supportedLngs, nonExplicitSupportedLngs)\n}\n\nexport function createProxy(userConfig: I18nConfig) {\n const config = normalizeConfig(userConfig)\n const nonExplicit = config.nonExplicitSupportedLngs\n // Normalize basePath: ensure leading slash, strip trailing slash\n const basePath = config.basePath\n ? ('/' + config.basePath.replace(/^\\/+/, '').replace(/\\/+$/, ''))\n : undefined\n\n return function middleware(req: NextRequest): NextResponse {\n const { pathname, search } = req.nextUrl\n\n // When basePath is set, only handle requests under that prefix\n if (basePath) {\n if (pathname !== basePath && !pathname.startsWith(basePath + '/')) {\n return NextResponse.next()\n }\n }\n\n // Skip ignored paths\n for (const ignored of config.ignoredPaths) {\n if (pathname.startsWith(ignored)) {\n return NextResponse.next()\n }\n }\n\n // Skip common static file extensions\n if (/\\.(ico|png|jpg|jpeg|svg|gif|webp|css|js|map|woff2?|ttf|eot)$/.test(pathname)) {\n return NextResponse.next()\n }\n\n // Detect language from cookie, then Accept-Language header, then default\n let lng: string | undefined\n const cookieValue = req.cookies.get(config.cookieName)?.value\n if (cookieValue) {\n lng = matchLanguage([cookieValue], config.supportedLngs, config.fallbackLng, nonExplicit)\n }\n if (!lng) {\n lng = matchLanguage(\n parseAcceptLanguage(req.headers.get('Accept-Language')),\n config.supportedLngs,\n config.fallbackLng,\n nonExplicit,\n )\n }\n if (!lng) {\n lng = config.fallbackLng\n }\n\n // For locale-in-path detection, strip basePath prefix so we look at the right segment\n const pathForLocale = basePath ? pathname.slice(basePath.length) || '/' : pathname\n const lngInPath = findLocaleInPath(pathForLocale, config.supportedLngs, nonExplicit)\n\n if (config.localeInPath) {\n // Set custom header for server components to read\n const headers = new Headers(req.headers)\n headers.set(config.headerName, lngInPath || lng)\n\n // Redirect if no locale in path\n if (!lngInPath) {\n const prefix = basePath ?? ''\n const pathAfterBase = basePath ? pathname.slice(basePath.length) : pathname\n const redirectUrl = new URL(`${prefix}/${lng}${pathAfterBase}${search}`, req.url)\n const response = NextResponse.redirect(redirectUrl)\n response.cookies.set(config.cookieName, lng, {\n path: '/',\n maxAge: config.cookieMaxAge,\n sameSite: 'lax',\n })\n return response\n }\n\n // Persist language from referer URL into cookie\n const response = NextResponse.next({ request: { headers } })\n if (req.headers.has('referer')) {\n const refererUrl = new URL(req.headers.get('referer')!)\n const refererPathForLocale = basePath\n ? refererUrl.pathname.slice(basePath.length) || '/'\n : refererUrl.pathname\n const lngInReferer = findLocaleInPath(refererPathForLocale, config.supportedLngs, nonExplicit)\n if (lngInReferer) {\n response.cookies.set(config.cookieName, lngInReferer, {\n path: '/',\n maxAge: config.cookieMaxAge,\n sameSite: 'lax',\n })\n }\n }\n\n return response\n } else {\n // No-locale-path mode: don't redirect, just set the header\n const headers = new Headers(req.headers)\n headers.set(config.headerName, lng)\n\n const response = NextResponse.next({ request: { headers } })\n return response\n }\n }\n}\n\n/**\n * Backwards-compatible alias for createProxy.\n * Use `createProxy` for new projects with Next.js 16+ `proxy.ts`.\n */\nexport const createMiddleware = createProxy\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../../src/appRouter/config.ts","../../../src/appRouter/proxy/languageDetector.ts","../../../src/appRouter/proxy/index.ts"],"sourcesContent":["import type { I18nConfig, NormalizedConfig } from './types'\n\nexport function defineConfig(config: I18nConfig): I18nConfig {\n return config\n}\n\nexport function normalizeConfig(userConfig: I18nConfig): NormalizedConfig {\n // Support legacy format: { i18n: { defaultLocale, locales } }\n const supportedLngs = userConfig.supportedLngs ??\n userConfig.i18n?.locales?.filter((l: string) => l !== 'default') ??\n ['en']\n const fallbackLng = userConfig.fallbackLng ??\n userConfig.i18n?.defaultLocale ??\n supportedLngs[0]\n\n if (!fallbackLng) {\n throw new Error('next-i18next: fallbackLng (or i18n.defaultLocale) is required')\n }\n if (supportedLngs.length === 0) {\n throw new Error('next-i18next: supportedLngs (or i18n.locales) must contain at least one language')\n }\n\n const defaultNS = userConfig.defaultNS ?? 'common'\n\n return {\n supportedLngs,\n fallbackLng,\n defaultNS,\n ns: userConfig.ns ?? [defaultNS],\n localeInPath: userConfig.localeInPath ?? true,\n hideDefaultLocale: userConfig.hideDefaultLocale ?? false,\n localePath: userConfig.localePath ?? '/locales',\n localeStructure: userConfig.localeStructure ?? '{{lng}}/{{ns}}',\n localeExtension: userConfig.localeExtension ?? 'json',\n cookieName: userConfig.cookieName ?? 'i18next',\n headerName: userConfig.headerName ?? 'x-i18next-current-language',\n cookieMaxAge: userConfig.cookieMaxAge ?? 365 * 24 * 60 * 60,\n ignoredPaths: userConfig.ignoredPaths ?? ['/api', '/_next', '/static'],\n basePath: userConfig.basePath,\n resources: userConfig.resources,\n resourceLoader: userConfig.resourceLoader,\n use: userConfig.use ?? [],\n i18nextOptions: (userConfig.i18nextOptions ?? {}) as Record<string, any>,\n nonExplicitSupportedLngs: userConfig.nonExplicitSupportedLngs ?? false,\n // Preserve legacy fields\n i18n: userConfig.i18n,\n serializeConfig: userConfig.serializeConfig,\n reloadOnPrerender: userConfig.reloadOnPrerender,\n }\n}\n","/**\n * Edge-safe Accept-Language parser and language matcher.\n * No external dependencies — runs in Edge Runtime, Node.js, and browser.\n */\n\nexport function parseAcceptLanguage(header: string | null | undefined): string[] {\n if (!header) return []\n return header\n .split(',')\n .map(part => {\n const [lang, qPart] = part.trim().split(';')\n const q = qPart?.trim().startsWith('q=')\n ? parseFloat(qPart.trim().slice(2))\n : 1.0\n return { lang: lang.trim(), q: isNaN(q) ? 0 : q }\n })\n .filter(item => item.lang && item.q > 0)\n .sort((a, b) => b.q - a.q)\n .map(item => item.lang)\n}\n\n/**\n * Find a matching supported language for a given code, respecting nonExplicitSupportedLngs.\n *\n * Matching order (mirrors i18next's LanguageUtils.getBestMatchFromCodes):\n * 1. Exact match (case-insensitive)\n * 2. Preferred prefix → supported base (e.g. preferred 'en-US' matches supported 'en')\n * 3. If nonExplicitSupportedLngs: preferred base → supported region\n * (e.g. preferred 'en' matches supported 'en-US')\n */\nexport function findSupportedMatch(\n code: string,\n supportedLanguages: readonly string[],\n nonExplicitSupportedLngs: boolean,\n): string | undefined {\n const lower = code.toLowerCase()\n\n // 1. Exact match (case-insensitive)\n const exact = supportedLanguages.find(l => l.toLowerCase() === lower)\n if (exact) return exact\n\n // 2. Preferred prefix → supported base (e.g. 'en-US' → 'en')\n const prefix = lower.split('-')[0]\n const partial = supportedLanguages.find(l => l.toLowerCase() === prefix)\n if (partial) return partial\n\n // 3. Reverse match: preferred base → supported region (e.g. 'en' → 'en-US')\n if (nonExplicitSupportedLngs) {\n const reverse = supportedLanguages.find(\n l => l.toLowerCase().split('-')[0] === prefix\n )\n if (reverse) return reverse\n }\n\n return undefined\n}\n\nexport function matchLanguage(\n acceptLanguages: string[],\n supportedLanguages: readonly string[],\n defaultLanguage: string,\n nonExplicitSupportedLngs = false,\n): string {\n for (const preferred of acceptLanguages) {\n const match = findSupportedMatch(preferred, supportedLanguages, nonExplicitSupportedLngs)\n if (match) return match\n }\n return defaultLanguage\n}\n","import { NextRequest, NextResponse } from 'next/server'\nimport type { I18nConfig } from '../types'\nimport { normalizeConfig } from '../config'\nimport { parseAcceptLanguage, matchLanguage, findSupportedMatch } from './languageDetector'\n\n// Re-export config utilities for Edge-safe usage (no react-i18next dependency)\nexport { defineConfig, normalizeConfig } from '../config'\nexport type { I18nConfig, NormalizedConfig, ResourceLoader } from '../types'\n\nfunction findLocaleInPath(\n pathname: string,\n supportedLngs: readonly string[],\n nonExplicitSupportedLngs: boolean,\n): string | undefined {\n // Extract the first path segment\n const match = pathname.match(/^\\/([^/]+)/)\n if (!match) return undefined\n return findSupportedMatch(match[1], supportedLngs, nonExplicitSupportedLngs)\n}\n\nexport function createProxy(userConfig: I18nConfig) {\n const config = normalizeConfig(userConfig)\n const nonExplicit = config.nonExplicitSupportedLngs\n // Normalize basePath: ensure leading slash, strip trailing slash\n const basePath = config.basePath\n ? ('/' + config.basePath.replace(/^\\/+/, '').replace(/\\/+$/, ''))\n : undefined\n\n return function middleware(req: NextRequest): NextResponse {\n const { pathname, search } = req.nextUrl\n\n // When basePath is set, only handle requests under that prefix\n if (basePath) {\n if (pathname !== basePath && !pathname.startsWith(basePath + '/')) {\n return NextResponse.next()\n }\n }\n\n // Skip ignored paths\n for (const ignored of config.ignoredPaths) {\n if (pathname.startsWith(ignored)) {\n return NextResponse.next()\n }\n }\n\n // Skip common static file extensions\n if (/\\.(ico|png|jpg|jpeg|svg|gif|webp|css|js|map|woff2?|ttf|eot)$/.test(pathname)) {\n return NextResponse.next()\n }\n\n // Detect language from cookie, then Accept-Language header, then default\n let lng: string | undefined\n const cookieValue = req.cookies.get(config.cookieName)?.value\n if (cookieValue) {\n lng = matchLanguage([cookieValue], config.supportedLngs, config.fallbackLng, nonExplicit)\n }\n if (!lng) {\n lng = matchLanguage(\n parseAcceptLanguage(req.headers.get('Accept-Language')),\n config.supportedLngs,\n config.fallbackLng,\n nonExplicit,\n )\n }\n if (!lng) {\n lng = config.fallbackLng\n }\n\n // For locale-in-path detection, strip basePath prefix so we look at the right segment\n const pathForLocale = basePath ? pathname.slice(basePath.length) || '/' : pathname\n const lngInPath = findLocaleInPath(pathForLocale, config.supportedLngs, nonExplicit)\n\n if (config.localeInPath) {\n const prefix = basePath ?? ''\n const pathAfterBase = basePath ? pathname.slice(basePath.length) : pathname\n\n // hideDefaultLocale: redirect explicit default-locale paths to the clean URL\n if (config.hideDefaultLocale && lngInPath === config.fallbackLng) {\n const pathWithoutLocale = pathAfterBase.replace(/^\\/[^/]+/, '') || '/'\n const redirectUrl = new URL(`${prefix}${pathWithoutLocale}${search}`, req.url)\n const response = NextResponse.redirect(redirectUrl)\n response.cookies.set(config.cookieName, config.fallbackLng, {\n path: '/',\n maxAge: config.cookieMaxAge,\n sameSite: 'lax',\n })\n return response\n }\n\n // Set custom header for server components to read\n const headers = new Headers(req.headers)\n headers.set(config.headerName, lngInPath || lng)\n\n // Redirect if no locale in path\n if (!lngInPath) {\n if (config.hideDefaultLocale) {\n // Rewrite internally to the default-locale path, keeping the clean URL\n const rewriteUrl = new URL(`${prefix}/${config.fallbackLng}${pathAfterBase}${search}`, req.url)\n headers.set(config.headerName, config.fallbackLng)\n const response = NextResponse.rewrite(rewriteUrl, { request: { headers } })\n response.cookies.set(config.cookieName, config.fallbackLng, {\n path: '/',\n maxAge: config.cookieMaxAge,\n sameSite: 'lax',\n })\n return response\n }\n\n const redirectUrl = new URL(`${prefix}/${lng}${pathAfterBase}${search}`, req.url)\n const response = NextResponse.redirect(redirectUrl)\n response.cookies.set(config.cookieName, lng, {\n path: '/',\n maxAge: config.cookieMaxAge,\n sameSite: 'lax',\n })\n return response\n }\n\n // Persist language from referer URL into cookie\n const response = NextResponse.next({ request: { headers } })\n if (req.headers.has('referer')) {\n const refererUrl = new URL(req.headers.get('referer')!)\n const refererPathForLocale = basePath\n ? refererUrl.pathname.slice(basePath.length) || '/'\n : refererUrl.pathname\n const lngInReferer = findLocaleInPath(refererPathForLocale, config.supportedLngs, nonExplicit)\n if (lngInReferer) {\n response.cookies.set(config.cookieName, lngInReferer, {\n path: '/',\n maxAge: config.cookieMaxAge,\n sameSite: 'lax',\n })\n }\n }\n\n return response\n } else {\n // No-locale-path mode: don't redirect, just set the header\n const headers = new Headers(req.headers)\n headers.set(config.headerName, lng)\n\n const response = NextResponse.next({ request: { headers } })\n return response\n }\n }\n}\n\n/**\n * Backwards-compatible alias for createProxy.\n * Use `createProxy` for new projects with Next.js 16+ `proxy.ts`.\n */\nexport const createMiddleware = createProxy\n"],"mappings":"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"}
|
|
@@ -39,6 +39,7 @@ function normalizeConfig(userConfig) {
|
|
|
39
39
|
defaultNS,
|
|
40
40
|
ns: userConfig.ns ?? [defaultNS],
|
|
41
41
|
localeInPath: userConfig.localeInPath ?? true,
|
|
42
|
+
hideDefaultLocale: userConfig.hideDefaultLocale ?? false,
|
|
42
43
|
localePath: userConfig.localePath ?? "/locales",
|
|
43
44
|
localeStructure: userConfig.localeStructure ?? "{{lng}}/{{ns}}",
|
|
44
45
|
localeExtension: userConfig.localeExtension ?? "json",
|
|
@@ -84,11 +85,17 @@ function createResourceBackend(config) {
|
|
|
84
85
|
if (config.resourceLoader) return (0, i18next_resources_to_backend.default)(config.resourceLoader);
|
|
85
86
|
return (0, i18next_resources_to_backend.default)(async (language, namespace) => {
|
|
86
87
|
const filePath = `${config.localePath}/${config.localeStructure.replace("{{lng}}", language).replace("{{ns}}", namespace)}.${config.localeExtension}`;
|
|
87
|
-
if (typeof process !== "undefined" && process.versions?.node) {
|
|
88
|
+
if (typeof process !== "undefined" && process.versions?.node) try {
|
|
88
89
|
const fs = await import("fs/promises");
|
|
89
90
|
const resolved = (await import("path")).resolve(process.cwd(), `public${filePath}`);
|
|
90
91
|
const content = await fs.readFile(resolved, "utf-8");
|
|
91
92
|
return JSON.parse(content);
|
|
93
|
+
} catch {
|
|
94
|
+
throw new Error(`next-i18next: Could not read locale file "public${filePath}". On serverless platforms (Vercel, AWS Lambda, etc.), files in public/ are served via CDN but are NOT available on the filesystem at runtime. Use the \`resourceLoader\` option with dynamic imports instead:
|
|
95
|
+
|
|
96
|
+
resourceLoader: (language, namespace) =>
|
|
97
|
+
import(\`./public/locales/\${language}/\${namespace}.json\`)
|
|
98
|
+
`);
|
|
92
99
|
}
|
|
93
100
|
throw new Error(`next-i18next: Cannot load locale file "${filePath}" in Edge Runtime. Provide pre-bundled \`resources\`, a custom \`resourceLoader\`, or use a custom backend (e.g. i18next-http-backend) via the \`use\` option.`);
|
|
94
101
|
});
|
|
@@ -104,7 +111,8 @@ async function getSharedInstance(config) {
|
|
|
104
111
|
if (_sharedInstancePromise) return _sharedInstancePromise;
|
|
105
112
|
_sharedInstancePromise = (async () => {
|
|
106
113
|
const i18nInstance = (0, i18next.createInstance)();
|
|
107
|
-
|
|
114
|
+
const partialBundled = config.i18nextOptions?.partialBundledLanguages;
|
|
115
|
+
if ((!config.resources || partialBundled) && !hasCustomBackend(config.use)) i18nInstance.use(createResourceBackend(config));
|
|
108
116
|
config.use.forEach((plugin) => i18nInstance.use(plugin));
|
|
109
117
|
await i18nInstance.init({
|
|
110
118
|
lng: config.fallbackLng,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.cjs","names":[],"sources":["../../src/appRouter/config.ts","../../src/appRouter/server.ts"],"sourcesContent":["import type { I18nConfig, NormalizedConfig } from './types'\n\nexport function defineConfig(config: I18nConfig): I18nConfig {\n return config\n}\n\nexport function normalizeConfig(userConfig: I18nConfig): NormalizedConfig {\n // Support legacy format: { i18n: { defaultLocale, locales } }\n const supportedLngs = userConfig.supportedLngs ??\n userConfig.i18n?.locales?.filter((l: string) => l !== 'default') ??\n ['en']\n const fallbackLng = userConfig.fallbackLng ??\n userConfig.i18n?.defaultLocale ??\n supportedLngs[0]\n\n if (!fallbackLng) {\n throw new Error('next-i18next: fallbackLng (or i18n.defaultLocale) is required')\n }\n if (supportedLngs.length === 0) {\n throw new Error('next-i18next: supportedLngs (or i18n.locales) must contain at least one language')\n }\n\n const defaultNS = userConfig.defaultNS ?? 'common'\n\n return {\n supportedLngs,\n fallbackLng,\n defaultNS,\n ns: userConfig.ns ?? [defaultNS],\n localeInPath: userConfig.localeInPath ?? true,\n localePath: userConfig.localePath ?? '/locales',\n localeStructure: userConfig.localeStructure ?? '{{lng}}/{{ns}}',\n localeExtension: userConfig.localeExtension ?? 'json',\n cookieName: userConfig.cookieName ?? 'i18next',\n headerName: userConfig.headerName ?? 'x-i18next-current-language',\n cookieMaxAge: userConfig.cookieMaxAge ?? 365 * 24 * 60 * 60,\n ignoredPaths: userConfig.ignoredPaths ?? ['/api', '/_next', '/static'],\n basePath: userConfig.basePath,\n resources: userConfig.resources,\n resourceLoader: userConfig.resourceLoader,\n use: userConfig.use ?? [],\n i18nextOptions: (userConfig.i18nextOptions ?? {}) as Record<string, any>,\n nonExplicitSupportedLngs: userConfig.nonExplicitSupportedLngs ?? false,\n // Preserve legacy fields\n i18n: userConfig.i18n,\n serializeConfig: userConfig.serializeConfig,\n reloadOnPrerender: userConfig.reloadOnPrerender,\n }\n}\n","import { createInstance } from 'i18next'\nimport type { i18n as I18NextClient, Resource, Module, FlatNamespace, KeyPrefix } from 'i18next'\nimport resourcesToBackend from 'i18next-resources-to-backend'\nimport { cache } from 'react'\nimport { headers, cookies } from 'next/headers'\n\nimport type { I18nConfig, NormalizedConfig, GetTResult } from './types'\nimport { normalizeConfig } from './config'\n\nlet _config: NormalizedConfig | null = null\n\n// Module-level singleton: persists across requests within the same server process.\n// This is critical for custom backends (i18next-http-backend, i18next-locize-backend)\n// to avoid re-fetching translations on every request.\n// In serverless environments (Lambda, Cloud Functions, etc.), this lives as long as\n// the warm function instance — backends with reloadInterval will refresh automatically.\nlet _sharedInstance: I18NextClient | null = null\nlet _sharedInstancePromise: Promise<I18NextClient> | null = null\n\nfunction getConfig(): NormalizedConfig {\n if (!_config) {\n throw new Error(\n 'next-i18next: Server module not initialized. Call initServerI18next(config) in your root layout.'\n )\n }\n return _config\n}\n\n/**\n * Initialize the server-side i18next configuration.\n * Call this once in your root layout or a shared setup file.\n */\nexport function initServerI18next(userConfig: I18nConfig): void {\n _config = normalizeConfig(userConfig)\n}\n\nfunction hasCustomBackend(plugins: any[]): boolean {\n return plugins.some((b: Module) => b.type === 'backend')\n}\n\nfunction createResourceBackend(config: NormalizedConfig) {\n if (config.resourceLoader) {\n return resourcesToBackend(config.resourceLoader)\n }\n return resourcesToBackend(async (language: string, namespace: string) => {\n const filePath = `${config.localePath}/${config.localeStructure\n .replace('{{lng}}', language)\n .replace('{{ns}}', namespace)}.${config.localeExtension}`\n\n // Node.js runtime: read from filesystem\n if (typeof process !== 'undefined' && process.versions?.node) {\n const fs = await import('fs/promises')\n const pathMod = await import('path')\n const resolved = pathMod.resolve(process.cwd(), `public${filePath}`)\n const content = await fs.readFile(resolved, 'utf-8')\n return JSON.parse(content)\n }\n\n // Edge runtime: filesystem not available\n throw new Error(\n `next-i18next: Cannot load locale file \"${filePath}\" in Edge Runtime. ` +\n 'Provide pre-bundled `resources`, a custom `resourceLoader`, or use a custom backend (e.g. i18next-http-backend) via the `use` option.'\n )\n })\n}\n\n/**\n * Get or create the shared i18next instance.\n * The instance is created once and reused across all requests.\n * All languages are preloaded so that getFixedT(lng) works for any supported language.\n * Additional namespaces are loaded on demand and cached in the instance store.\n */\nasync function getSharedInstance(config: NormalizedConfig): Promise<I18NextClient> {\n if (_sharedInstance?.isInitialized) return _sharedInstance\n\n // Deduplicate concurrent init calls (multiple requests arriving while first init is in flight)\n if (_sharedInstancePromise) return _sharedInstancePromise\n\n _sharedInstancePromise = (async () => {\n const i18nInstance = createInstance()\n\n // Only add a backend if resources are not pre-loaded and no custom backend is provided\n if (!config.resources && !hasCustomBackend(config.use)) {\n i18nInstance.use(createResourceBackend(config))\n }\n\n config.use.forEach((plugin: any) => i18nInstance.use(plugin))\n\n await i18nInstance.init({\n // No `lng` — the shared instance is language-neutral.\n // We use getFixedT(lng, ns) to get language-specific translators.\n lng: config.fallbackLng,\n ns: config.ns,\n defaultNS: config.defaultNS,\n fallbackLng: config.fallbackLng,\n supportedLngs: config.supportedLngs,\n nonExplicitSupportedLngs: config.nonExplicitSupportedLngs,\n fallbackNS: config.defaultNS,\n preload: config.supportedLngs, // preload ALL languages upfront\n interpolation: { escapeValue: false },\n ...(config.resources ? { resources: config.resources } : {}),\n ...config.i18nextOptions,\n })\n\n _sharedInstance = i18nInstance\n return i18nInstance\n })()\n\n return _sharedInstancePromise\n}\n\n// Per-request language detection, deduplicated within a single React render\nconst detectLanguage = cache(async (config: NormalizedConfig): Promise<string> => {\n const headerList = await headers()\n const fromHeader = headerList.get(config.headerName)\n if (fromHeader) return fromHeader\n\n const cookieStore = await cookies()\n const cookieValue = cookieStore.get(config.cookieName)?.value\n if (cookieValue) {\n if (config.supportedLngs.includes(cookieValue)) {\n return cookieValue\n }\n // nonExplicitSupportedLngs: e.g. cookie 'en' matches supported 'en-US'\n if (config.nonExplicitSupportedLngs) {\n const prefix = cookieValue.toLowerCase().split('-')[0]\n const match = config.supportedLngs.find(\n l => l.toLowerCase() === prefix || l.toLowerCase().split('-')[0] === prefix\n )\n if (match) return match\n }\n }\n\n return config.fallbackLng\n})\n\n/**\n * Get a translation function for use in Server Components, layouts, and generateMetadata.\n *\n * The underlying i18next instance is a **module-level singleton** that persists across\n * requests. This means custom backends (i18next-http-backend, i18next-locize-backend, etc.)\n * only fetch translations once (or according to their own reloadInterval), not on every request.\n *\n * @example\n * ```tsx\n * import { getT } from 'next-i18next/server'\n *\n * export default async function Page() {\n * const { t, i18n } = await getT('home')\n * return <h1>{t('heading')}</h1>\n * }\n * ```\n */\nexport async function getT<\n Ns extends FlatNamespace = FlatNamespace,\n KPrefix extends KeyPrefix<Ns> = undefined,\n>(\n ns?: Ns | Ns[],\n options: { keyPrefix?: KPrefix; lng?: string } = {},\n): Promise<GetTResult<Ns, KPrefix>> {\n const config = getConfig()\n\n const lng = options.lng || await detectLanguage(config)\n const i18nInstance = await getSharedInstance(config)\n\n // Load additional namespaces on demand if not already loaded\n const nsArray: string[] = ns\n ? (Array.isArray(ns) ? ns as string[] : [ns as string])\n : config.ns\n const missingNs = nsArray.filter(n => !i18nInstance.hasLoadedNamespace(n))\n if (missingNs.length > 0) {\n await i18nInstance.loadNamespaces(missingNs)\n }\n\n const resolvedNs = ns\n ? (Array.isArray(ns) ? ns[0] : ns) as string\n : config.defaultNS\n\n return {\n t: i18nInstance.getFixedT(lng, resolvedNs, options.keyPrefix as string | undefined),\n i18n: i18nInstance,\n lng,\n } as any\n}\n\n/**\n * Extract loaded resources from the server i18next instance for passing to I18nProvider.\n *\n * @example\n * ```tsx\n * const { i18n } = await getT()\n * const resources = getResources(i18n, ['common', 'footer'])\n * return <I18nProvider language={i18n.language} resources={resources}>{children}</I18nProvider>\n * ```\n */\nexport function getResources(\n i18n: I18NextClient,\n namespaces?: string[],\n): Resource {\n const resources: Resource = {}\n const store = i18n.store?.data || {}\n const nsFilter = namespaces ? new Set(namespaces) : null\n\n for (const lng of Object.keys(store)) {\n resources[lng] = {}\n for (const ns of Object.keys(store[lng])) {\n if (!nsFilter || nsFilter.has(ns)) {\n resources[lng][ns] = store[lng][ns]\n }\n }\n }\n\n return resources\n}\n\n/**\n * Helper for generateStaticParams — returns params for all supported languages.\n *\n * @example\n * ```tsx\n * import { generateI18nStaticParams } from 'next-i18next/server'\n *\n * export async function generateStaticParams() {\n * return generateI18nStaticParams()\n * }\n * ```\n */\nexport function generateI18nStaticParams(): { lng: string }[] {\n const config = getConfig()\n return config.supportedLngs.map(lng => ({ lng }))\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAMA,SAAgB,gBAAgB,YAA0C;CAExE,MAAM,gBAAgB,WAAW,iBAC/B,WAAW,MAAM,SAAS,QAAQ,MAAc,MAAM,UAAU,IAChE,CAAC,KAAK;CACR,MAAM,cAAc,WAAW,eAC7B,WAAW,MAAM,iBACjB,cAAc;AAEhB,KAAI,CAAC,YACH,OAAM,IAAI,MAAM,gEAAgE;AAElF,KAAI,cAAc,WAAW,EAC3B,OAAM,IAAI,MAAM,mFAAmF;CAGrG,MAAM,YAAY,WAAW,aAAa;AAE1C,QAAO;EACL;EACA;EACA;EACA,IAAI,WAAW,MAAM,CAAC,UAAU;EAChC,cAAc,WAAW,gBAAgB;EACzC,YAAY,WAAW,cAAc;EACrC,iBAAiB,WAAW,mBAAmB;EAC/C,iBAAiB,WAAW,mBAAmB;EAC/C,YAAY,WAAW,cAAc;EACrC,YAAY,WAAW,cAAc;EACrC,cAAc,WAAW,gBAAgB,MAAM,KAAK,KAAK;EACzD,cAAc,WAAW,gBAAgB;GAAC;GAAQ;GAAU;GAAU;EACtE,UAAU,WAAW;EACrB,WAAW,WAAW;EACtB,gBAAgB,WAAW;EAC3B,KAAK,WAAW,OAAO,EAAE;EACzB,gBAAiB,WAAW,kBAAkB,EAAE;EAChD,0BAA0B,WAAW,4BAA4B;EAEjE,MAAM,WAAW;EACjB,iBAAiB,WAAW;EAC5B,mBAAmB,WAAW;EAC/B;;;;ACtCH,IAAI,UAAmC;AAOvC,IAAI,kBAAwC;AAC5C,IAAI,yBAAwD;AAE5D,SAAS,YAA8B;AACrC,KAAI,CAAC,QACH,OAAM,IAAI,MACR,mGACD;AAEH,QAAO;;;;;;AAOT,SAAgB,kBAAkB,YAA8B;AAC9D,WAAU,gBAAgB,WAAW;;AAGvC,SAAS,iBAAiB,SAAyB;AACjD,QAAO,QAAQ,MAAM,MAAc,EAAE,SAAS,UAAU;;AAG1D,SAAS,sBAAsB,QAA0B;AACvD,KAAI,OAAO,eACT,SAAA,GAAA,6BAAA,SAA0B,OAAO,eAAe;AAElD,SAAA,GAAA,6BAAA,SAA0B,OAAO,UAAkB,cAAsB;EACvE,MAAM,WAAW,GAAG,OAAO,WAAW,GAAG,OAAO,gBAC7C,QAAQ,WAAW,SAAS,CAC5B,QAAQ,UAAU,UAAU,CAAC,GAAG,OAAO;AAG1C,MAAI,OAAO,YAAY,eAAe,QAAQ,UAAU,MAAM;GAC5D,MAAM,KAAK,MAAM,OAAO;GAExB,MAAM,YADU,MAAM,OAAO,SACJ,QAAQ,QAAQ,KAAK,EAAE,SAAS,WAAW;GACpE,MAAM,UAAU,MAAM,GAAG,SAAS,UAAU,QAAQ;AACpD,UAAO,KAAK,MAAM,QAAQ;;AAI5B,QAAM,IAAI,MACR,0CAA0C,SAAS,gKAEpD;GACD;;;;;;;;AASJ,eAAe,kBAAkB,QAAkD;AACjF,KAAI,iBAAiB,cAAe,QAAO;AAG3C,KAAI,uBAAwB,QAAO;AAEnC,2BAA0B,YAAY;EACpC,MAAM,gBAAA,GAAA,QAAA,iBAA+B;AAGrC,MAAI,CAAC,OAAO,aAAa,CAAC,iBAAiB,OAAO,IAAI,CACpD,cAAa,IAAI,sBAAsB,OAAO,CAAC;AAGjD,SAAO,IAAI,SAAS,WAAgB,aAAa,IAAI,OAAO,CAAC;AAE7D,QAAM,aAAa,KAAK;GAGtB,KAAK,OAAO;GACZ,IAAI,OAAO;GACX,WAAW,OAAO;GAClB,aAAa,OAAO;GACpB,eAAe,OAAO;GACtB,0BAA0B,OAAO;GACjC,YAAY,OAAO;GACnB,SAAS,OAAO;GAChB,eAAe,EAAE,aAAa,OAAO;GACrC,GAAI,OAAO,YAAY,EAAE,WAAW,OAAO,WAAW,GAAG,EAAE;GAC3D,GAAG,OAAO;GACX,CAAC;AAEF,oBAAkB;AAClB,SAAO;KACL;AAEJ,QAAO;;AAIT,MAAM,kBAAA,GAAA,MAAA,OAAuB,OAAO,WAA8C;CAEhF,MAAM,cADa,OAAA,GAAA,aAAA,UAAe,EACJ,IAAI,OAAO,WAAW;AACpD,KAAI,WAAY,QAAO;CAGvB,MAAM,eADc,OAAA,GAAA,aAAA,UAAe,EACH,IAAI,OAAO,WAAW,EAAE;AACxD,KAAI,aAAa;AACf,MAAI,OAAO,cAAc,SAAS,YAAY,CAC5C,QAAO;AAGT,MAAI,OAAO,0BAA0B;GACnC,MAAM,SAAS,YAAY,aAAa,CAAC,MAAM,IAAI,CAAC;GACpD,MAAM,QAAQ,OAAO,cAAc,MACjC,MAAK,EAAE,aAAa,KAAK,UAAU,EAAE,aAAa,CAAC,MAAM,IAAI,CAAC,OAAO,OACtE;AACD,OAAI,MAAO,QAAO;;;AAItB,QAAO,OAAO;EACd;;;;;;;;;;;;;;;;;;AAmBF,eAAsB,KAIpB,IACA,UAAiD,EAAE,EACjB;CAClC,MAAM,SAAS,WAAW;CAE1B,MAAM,MAAM,QAAQ,OAAO,MAAM,eAAe,OAAO;CACvD,MAAM,eAAe,MAAM,kBAAkB,OAAO;CAMpD,MAAM,aAHoB,KACrB,MAAM,QAAQ,GAAG,GAAG,KAAiB,CAAC,GAAa,GACpD,OAAO,IACe,QAAO,MAAK,CAAC,aAAa,mBAAmB,EAAE,CAAC;AAC1E,KAAI,UAAU,SAAS,EACrB,OAAM,aAAa,eAAe,UAAU;CAG9C,MAAM,aAAa,KACd,MAAM,QAAQ,GAAG,GAAG,GAAG,KAAK,KAC7B,OAAO;AAEX,QAAO;EACL,GAAG,aAAa,UAAU,KAAK,YAAY,QAAQ,UAAgC;EACnF,MAAM;EACN;EACD;;;;;;;;;;;;AAaH,SAAgB,aACd,MACA,YACU;CACV,MAAM,YAAsB,EAAE;CAC9B,MAAM,QAAQ,KAAK,OAAO,QAAQ,EAAE;CACpC,MAAM,WAAW,aAAa,IAAI,IAAI,WAAW,GAAG;AAEpD,MAAK,MAAM,OAAO,OAAO,KAAK,MAAM,EAAE;AACpC,YAAU,OAAO,EAAE;AACnB,OAAK,MAAM,MAAM,OAAO,KAAK,MAAM,KAAK,CACtC,KAAI,CAAC,YAAY,SAAS,IAAI,GAAG,CAC/B,WAAU,KAAK,MAAM,MAAM,KAAK;;AAKtC,QAAO;;;;;;;;;;;;;;AAeT,SAAgB,2BAA8C;AAE5D,QADe,WAAW,CACZ,cAAc,KAAI,SAAQ,EAAE,KAAK,EAAE"}
|
|
1
|
+
{"version":3,"file":"server.cjs","names":[],"sources":["../../src/appRouter/config.ts","../../src/appRouter/server.ts"],"sourcesContent":["import type { I18nConfig, NormalizedConfig } from './types'\n\nexport function defineConfig(config: I18nConfig): I18nConfig {\n return config\n}\n\nexport function normalizeConfig(userConfig: I18nConfig): NormalizedConfig {\n // Support legacy format: { i18n: { defaultLocale, locales } }\n const supportedLngs = userConfig.supportedLngs ??\n userConfig.i18n?.locales?.filter((l: string) => l !== 'default') ??\n ['en']\n const fallbackLng = userConfig.fallbackLng ??\n userConfig.i18n?.defaultLocale ??\n supportedLngs[0]\n\n if (!fallbackLng) {\n throw new Error('next-i18next: fallbackLng (or i18n.defaultLocale) is required')\n }\n if (supportedLngs.length === 0) {\n throw new Error('next-i18next: supportedLngs (or i18n.locales) must contain at least one language')\n }\n\n const defaultNS = userConfig.defaultNS ?? 'common'\n\n return {\n supportedLngs,\n fallbackLng,\n defaultNS,\n ns: userConfig.ns ?? [defaultNS],\n localeInPath: userConfig.localeInPath ?? true,\n hideDefaultLocale: userConfig.hideDefaultLocale ?? false,\n localePath: userConfig.localePath ?? '/locales',\n localeStructure: userConfig.localeStructure ?? '{{lng}}/{{ns}}',\n localeExtension: userConfig.localeExtension ?? 'json',\n cookieName: userConfig.cookieName ?? 'i18next',\n headerName: userConfig.headerName ?? 'x-i18next-current-language',\n cookieMaxAge: userConfig.cookieMaxAge ?? 365 * 24 * 60 * 60,\n ignoredPaths: userConfig.ignoredPaths ?? ['/api', '/_next', '/static'],\n basePath: userConfig.basePath,\n resources: userConfig.resources,\n resourceLoader: userConfig.resourceLoader,\n use: userConfig.use ?? [],\n i18nextOptions: (userConfig.i18nextOptions ?? {}) as Record<string, any>,\n nonExplicitSupportedLngs: userConfig.nonExplicitSupportedLngs ?? false,\n // Preserve legacy fields\n i18n: userConfig.i18n,\n serializeConfig: userConfig.serializeConfig,\n reloadOnPrerender: userConfig.reloadOnPrerender,\n }\n}\n","import { createInstance } from 'i18next'\nimport type { i18n as I18NextClient, Resource, Module, FlatNamespace, KeyPrefix } from 'i18next'\nimport resourcesToBackend from 'i18next-resources-to-backend'\nimport { cache } from 'react'\nimport { headers, cookies } from 'next/headers'\n\nimport type { I18nConfig, NormalizedConfig, GetTResult } from './types'\nimport { normalizeConfig } from './config'\n\nlet _config: NormalizedConfig | null = null\n\n// Module-level singleton: persists across requests within the same server process.\n// This is critical for custom backends (i18next-http-backend, i18next-locize-backend)\n// to avoid re-fetching translations on every request.\n// In serverless environments (Lambda, Cloud Functions, etc.), this lives as long as\n// the warm function instance — backends with reloadInterval will refresh automatically.\nlet _sharedInstance: I18NextClient | null = null\nlet _sharedInstancePromise: Promise<I18NextClient> | null = null\n\nfunction getConfig(): NormalizedConfig {\n if (!_config) {\n throw new Error(\n 'next-i18next: Server module not initialized. Call initServerI18next(config) in your root layout.'\n )\n }\n return _config\n}\n\n/**\n * Initialize the server-side i18next configuration.\n * Call this once in your root layout or a shared setup file.\n */\nexport function initServerI18next(userConfig: I18nConfig): void {\n _config = normalizeConfig(userConfig)\n}\n\nfunction hasCustomBackend(plugins: any[]): boolean {\n return plugins.some((b: Module) => b.type === 'backend')\n}\n\nfunction createResourceBackend(config: NormalizedConfig) {\n if (config.resourceLoader) {\n return resourcesToBackend(config.resourceLoader)\n }\n return resourcesToBackend(async (language: string, namespace: string) => {\n const filePath = `${config.localePath}/${config.localeStructure\n .replace('{{lng}}', language)\n .replace('{{ns}}', namespace)}.${config.localeExtension}`\n\n // Node.js runtime: read from filesystem\n if (typeof process !== 'undefined' && process.versions?.node) {\n try {\n const fs = await import('fs/promises')\n const pathMod = await import('path')\n const resolved = pathMod.resolve(process.cwd(), `public${filePath}`)\n const content = await fs.readFile(resolved, 'utf-8')\n return JSON.parse(content)\n } catch {\n throw new Error(\n `next-i18next: Could not read locale file \"public${filePath}\". ` +\n 'On serverless platforms (Vercel, AWS Lambda, etc.), files in public/ are served via CDN ' +\n 'but are NOT available on the filesystem at runtime. Use the `resourceLoader` option with ' +\n 'dynamic imports instead:\\n\\n' +\n ' resourceLoader: (language, namespace) =>\\n' +\n // eslint-disable-next-line no-template-curly-in-string\n ' import(`./public/locales/${language}/${namespace}.json`)\\n'\n )\n }\n }\n\n // Edge runtime: filesystem not available\n throw new Error(\n `next-i18next: Cannot load locale file \"${filePath}\" in Edge Runtime. ` +\n 'Provide pre-bundled `resources`, a custom `resourceLoader`, or use a custom backend (e.g. i18next-http-backend) via the `use` option.'\n )\n })\n}\n\n/**\n * Get or create the shared i18next instance.\n * The instance is created once and reused across all requests.\n * All languages are preloaded so that getFixedT(lng) works for any supported language.\n * Additional namespaces are loaded on demand and cached in the instance store.\n */\nasync function getSharedInstance(config: NormalizedConfig): Promise<I18NextClient> {\n if (_sharedInstance?.isInitialized) return _sharedInstance\n\n // Deduplicate concurrent init calls (multiple requests arriving while first init is in flight)\n if (_sharedInstancePromise) return _sharedInstancePromise\n\n _sharedInstancePromise = (async () => {\n const i18nInstance = createInstance()\n\n // Add a backend when needed:\n // - No resources provided → backend loads everything\n // - Resources provided with partialBundledLanguages → backend loads the rest\n // - Custom backend in config.use → user handles it, skip default backend\n const partialBundled = config.i18nextOptions?.partialBundledLanguages\n if ((!config.resources || partialBundled) && !hasCustomBackend(config.use)) {\n i18nInstance.use(createResourceBackend(config))\n }\n\n config.use.forEach((plugin: any) => i18nInstance.use(plugin))\n\n await i18nInstance.init({\n // No `lng` — the shared instance is language-neutral.\n // We use getFixedT(lng, ns) to get language-specific translators.\n lng: config.fallbackLng,\n ns: config.ns,\n defaultNS: config.defaultNS,\n fallbackLng: config.fallbackLng,\n supportedLngs: config.supportedLngs,\n nonExplicitSupportedLngs: config.nonExplicitSupportedLngs,\n fallbackNS: config.defaultNS,\n preload: config.supportedLngs, // preload ALL languages upfront\n interpolation: { escapeValue: false },\n ...(config.resources ? { resources: config.resources } : {}),\n ...config.i18nextOptions,\n })\n\n _sharedInstance = i18nInstance\n return i18nInstance\n })()\n\n return _sharedInstancePromise\n}\n\n// Per-request language detection, deduplicated within a single React render\nconst detectLanguage = cache(async (config: NormalizedConfig): Promise<string> => {\n const headerList = await headers()\n const fromHeader = headerList.get(config.headerName)\n if (fromHeader) return fromHeader\n\n const cookieStore = await cookies()\n const cookieValue = cookieStore.get(config.cookieName)?.value\n if (cookieValue) {\n if (config.supportedLngs.includes(cookieValue)) {\n return cookieValue\n }\n // nonExplicitSupportedLngs: e.g. cookie 'en' matches supported 'en-US'\n if (config.nonExplicitSupportedLngs) {\n const prefix = cookieValue.toLowerCase().split('-')[0]\n const match = config.supportedLngs.find(\n l => l.toLowerCase() === prefix || l.toLowerCase().split('-')[0] === prefix\n )\n if (match) return match\n }\n }\n\n return config.fallbackLng\n})\n\n/**\n * Get a translation function for use in Server Components, layouts, and generateMetadata.\n *\n * The underlying i18next instance is a **module-level singleton** that persists across\n * requests. This means custom backends (i18next-http-backend, i18next-locize-backend, etc.)\n * only fetch translations once (or according to their own reloadInterval), not on every request.\n *\n * @example\n * ```tsx\n * import { getT } from 'next-i18next/server'\n *\n * export default async function Page() {\n * const { t, i18n } = await getT('home')\n * return <h1>{t('heading')}</h1>\n * }\n * ```\n */\nexport async function getT<\n Ns extends FlatNamespace = FlatNamespace,\n KPrefix extends KeyPrefix<Ns> = undefined,\n>(\n ns?: Ns | Ns[],\n options: { keyPrefix?: KPrefix; lng?: string } = {},\n): Promise<GetTResult<Ns, KPrefix>> {\n const config = getConfig()\n\n const lng = options.lng || await detectLanguage(config)\n const i18nInstance = await getSharedInstance(config)\n\n // Load additional namespaces on demand if not already loaded\n const nsArray: string[] = ns\n ? (Array.isArray(ns) ? ns as string[] : [ns as string])\n : config.ns\n const missingNs = nsArray.filter(n => !i18nInstance.hasLoadedNamespace(n))\n if (missingNs.length > 0) {\n await i18nInstance.loadNamespaces(missingNs)\n }\n\n const resolvedNs = ns\n ? (Array.isArray(ns) ? ns[0] : ns) as string\n : config.defaultNS\n\n return {\n t: i18nInstance.getFixedT(lng, resolvedNs, options.keyPrefix as string | undefined),\n i18n: i18nInstance,\n lng,\n } as any\n}\n\n/**\n * Extract loaded resources from the server i18next instance for passing to I18nProvider.\n *\n * @example\n * ```tsx\n * const { i18n } = await getT()\n * const resources = getResources(i18n, ['common', 'footer'])\n * return <I18nProvider language={i18n.language} resources={resources}>{children}</I18nProvider>\n * ```\n */\nexport function getResources(\n i18n: I18NextClient,\n namespaces?: string[],\n): Resource {\n const resources: Resource = {}\n const store = i18n.store?.data || {}\n const nsFilter = namespaces ? new Set(namespaces) : null\n\n for (const lng of Object.keys(store)) {\n resources[lng] = {}\n for (const ns of Object.keys(store[lng])) {\n if (!nsFilter || nsFilter.has(ns)) {\n resources[lng][ns] = store[lng][ns]\n }\n }\n }\n\n return resources\n}\n\n/**\n * Helper for generateStaticParams — returns params for all supported languages.\n *\n * @example\n * ```tsx\n * import { generateI18nStaticParams } from 'next-i18next/server'\n *\n * export async function generateStaticParams() {\n * return generateI18nStaticParams()\n * }\n * ```\n */\nexport function generateI18nStaticParams(): { lng: string }[] {\n const config = getConfig()\n return config.supportedLngs.map(lng => ({ lng }))\n}\n"],"mappings":"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"}
|
|
@@ -23,6 +23,10 @@ interface I18nConfig {
|
|
|
23
23
|
resourceLoader?: ResourceLoader;
|
|
24
24
|
/** Whether to include locale in URL path (defaults to true) */
|
|
25
25
|
localeInPath?: boolean;
|
|
26
|
+
/** When true (and localeInPath is true), the default language has no URL prefix.
|
|
27
|
+
* e.g. `/about` serves the default language, `/de/about` serves German.
|
|
28
|
+
* Requests to the explicit default prefix (`/en/about`) are redirected to `/about`. */
|
|
29
|
+
hideDefaultLocale?: boolean;
|
|
26
30
|
/** Cookie name for storing selected language (defaults to 'i18next') */
|
|
27
31
|
cookieName?: string;
|
|
28
32
|
/** Custom header name for passing language to server components (defaults to 'x-i18next-current-language') */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.cts","names":[],"sources":["../../src/appRouter/types.ts","../../src/appRouter/server.ts"],"mappings":";;;KAEY,cAAA,IAAkB,QAAA,UAAkB,SAAA,aAAsB,OAAA;AAAA,UAErD,UAAA;EAFL;EAIV,aAAA;;EAEA,WAAA;EAN4B;EAQ5B,SAAA;EARoE;EAUpE,EAAA;EAV2E;EAc3E,UAAA;EAZyB;EAczB,eAAA;EAIY;EAFZ,eAAA;
|
|
1
|
+
{"version":3,"file":"server.d.cts","names":[],"sources":["../../src/appRouter/types.ts","../../src/appRouter/server.ts"],"mappings":";;;KAEY,cAAA,IAAkB,QAAA,UAAkB,SAAA,aAAsB,OAAA;AAAA,UAErD,UAAA;EAFL;EAIV,aAAA;;EAEA,WAAA;EAN4B;EAQ5B,SAAA;EARoE;EAUpE,EAAA;EAV2E;EAc3E,UAAA;EAZyB;EAczB,eAAA;EAIY;EAFZ,eAAA;EA8BsB;EA5BtB,SAAA,GAAY,QAAA;EA4BS;EA1BrB,cAAA,GAAiB,cAAA;EAlBjB;EAsBA,YAAA;EAlBA;;;EAsBA,iBAAA;EAZA;EAgBA,UAAA;EAdY;EAgBZ,UAAA;EAdiB;EAgBjB,YAAA;EARA;EAUA,YAAA;EAJA;EAMA,QAAA;EAFA;EAMA,GAAA;EAAA;EAEA,cAAA,GAAiB,IAAA,CAAK,WAAA;EAAL;EAIjB,IAAA;IACE,aAAA;IACA,OAAA;IACA,OAAA;MACE,aAAA;MACA,MAAA;MACA,IAAA;MACA,OAAA;IAAA;IAEF,eAAA;EAAA;EAKF;EAFA,eAAA;EAIwB;EAFxB,iBAAA;EA+BU;EA7BV,wBAAA;AAAA;AAAA,KA6BU,UAAA,YAAsB,aAAA,GAAgB,aAAA;EAChD,CAAA,EAAG,SAAA,CAAU,EAAA,EAAI,OAAA;EACjB,IAAA,EAAM,IAAA,ECrEsC;EDuE5C,GAAA;AAAA;;;;AArGF;;;iBC8BgB,iBAAA,CAAkB,UAAA,EAAY,UAAA;;;;;;AD5B9C;;;;;;;;;;;;iBCqKsB,IAAA,YACT,aAAA,GAAgB,aAAA,kBACX,SAAA,CAAU,EAAA,cAAA,CAE1B,EAAA,GAAK,EAAA,GAAK,EAAA,IACV,OAAA;EAAW,SAAA,GAAY,OAAA;EAAS,GAAA;AAAA,IAC/B,OAAA,CAAQ,UAAA,CAAW,EAAA,EAAI,OAAA;;;;;;;;;;;iBAoCV,YAAA,CACd,IAAA,EAAM,IAAA,EACN,UAAA,cACC,QAAA;;;;;;;;;;;;;iBA6Ba,wBAAA,CAAA;EAA8B,GAAA;AAAA"}
|
|
@@ -23,6 +23,10 @@ interface I18nConfig {
|
|
|
23
23
|
resourceLoader?: ResourceLoader;
|
|
24
24
|
/** Whether to include locale in URL path (defaults to true) */
|
|
25
25
|
localeInPath?: boolean;
|
|
26
|
+
/** When true (and localeInPath is true), the default language has no URL prefix.
|
|
27
|
+
* e.g. `/about` serves the default language, `/de/about` serves German.
|
|
28
|
+
* Requests to the explicit default prefix (`/en/about`) are redirected to `/about`. */
|
|
29
|
+
hideDefaultLocale?: boolean;
|
|
26
30
|
/** Cookie name for storing selected language (defaults to 'i18next') */
|
|
27
31
|
cookieName?: string;
|
|
28
32
|
/** Custom header name for passing language to server components (defaults to 'x-i18next-current-language') */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.mts","names":[],"sources":["../../src/appRouter/types.ts","../../src/appRouter/server.ts"],"mappings":";;;KAEY,cAAA,IAAkB,QAAA,UAAkB,SAAA,aAAsB,OAAA;AAAA,UAErD,UAAA;EAFL;EAIV,aAAA;;EAEA,WAAA;EAN4B;EAQ5B,SAAA;EARoE;EAUpE,EAAA;EAV2E;EAc3E,UAAA;EAZyB;EAczB,eAAA;EAIY;EAFZ,eAAA;
|
|
1
|
+
{"version":3,"file":"server.d.mts","names":[],"sources":["../../src/appRouter/types.ts","../../src/appRouter/server.ts"],"mappings":";;;KAEY,cAAA,IAAkB,QAAA,UAAkB,SAAA,aAAsB,OAAA;AAAA,UAErD,UAAA;EAFL;EAIV,aAAA;;EAEA,WAAA;EAN4B;EAQ5B,SAAA;EARoE;EAUpE,EAAA;EAV2E;EAc3E,UAAA;EAZyB;EAczB,eAAA;EAIY;EAFZ,eAAA;EA8BsB;EA5BtB,SAAA,GAAY,QAAA;EA4BS;EA1BrB,cAAA,GAAiB,cAAA;EAlBjB;EAsBA,YAAA;EAlBA;;;EAsBA,iBAAA;EAZA;EAgBA,UAAA;EAdY;EAgBZ,UAAA;EAdiB;EAgBjB,YAAA;EARA;EAUA,YAAA;EAJA;EAMA,QAAA;EAFA;EAMA,GAAA;EAAA;EAEA,cAAA,GAAiB,IAAA,CAAK,WAAA;EAAL;EAIjB,IAAA;IACE,aAAA;IACA,OAAA;IACA,OAAA;MACE,aAAA;MACA,MAAA;MACA,IAAA;MACA,OAAA;IAAA;IAEF,eAAA;EAAA;EAKF;EAFA,eAAA;EAIwB;EAFxB,iBAAA;EA+BU;EA7BV,wBAAA;AAAA;AAAA,KA6BU,UAAA,YAAsB,aAAA,GAAgB,aAAA;EAChD,CAAA,EAAG,SAAA,CAAU,EAAA,EAAI,OAAA;EACjB,IAAA,EAAM,IAAA,ECrEsC;EDuE5C,GAAA;AAAA;;;;AArGF;;;iBC8BgB,iBAAA,CAAkB,UAAA,EAAY,UAAA;;;;;;AD5B9C;;;;;;;;;;;;iBCqKsB,IAAA,YACT,aAAA,GAAgB,aAAA,kBACX,SAAA,CAAU,EAAA,cAAA,CAE1B,EAAA,GAAK,EAAA,GAAK,EAAA,IACV,OAAA;EAAW,SAAA,GAAY,OAAA;EAAS,GAAA;AAAA,IAC/B,OAAA,CAAQ,UAAA,CAAW,EAAA,EAAI,OAAA;;;;;;;;;;;iBAoCV,YAAA,CACd,IAAA,EAAM,IAAA,EACN,UAAA,cACC,QAAA;;;;;;;;;;;;;iBA6Ba,wBAAA,CAAA;EAA8B,GAAA;AAAA"}
|
|
@@ -15,6 +15,7 @@ function normalizeConfig(userConfig) {
|
|
|
15
15
|
defaultNS,
|
|
16
16
|
ns: userConfig.ns ?? [defaultNS],
|
|
17
17
|
localeInPath: userConfig.localeInPath ?? true,
|
|
18
|
+
hideDefaultLocale: userConfig.hideDefaultLocale ?? false,
|
|
18
19
|
localePath: userConfig.localePath ?? "/locales",
|
|
19
20
|
localeStructure: userConfig.localeStructure ?? "{{lng}}/{{ns}}",
|
|
20
21
|
localeExtension: userConfig.localeExtension ?? "json",
|
|
@@ -60,11 +61,17 @@ function createResourceBackend(config) {
|
|
|
60
61
|
if (config.resourceLoader) return resourcesToBackend(config.resourceLoader);
|
|
61
62
|
return resourcesToBackend(async (language, namespace) => {
|
|
62
63
|
const filePath = `${config.localePath}/${config.localeStructure.replace("{{lng}}", language).replace("{{ns}}", namespace)}.${config.localeExtension}`;
|
|
63
|
-
if (typeof process !== "undefined" && process.versions?.node) {
|
|
64
|
+
if (typeof process !== "undefined" && process.versions?.node) try {
|
|
64
65
|
const fs = await import("fs/promises");
|
|
65
66
|
const resolved = (await import("path")).resolve(process.cwd(), `public${filePath}`);
|
|
66
67
|
const content = await fs.readFile(resolved, "utf-8");
|
|
67
68
|
return JSON.parse(content);
|
|
69
|
+
} catch {
|
|
70
|
+
throw new Error(`next-i18next: Could not read locale file "public${filePath}". On serverless platforms (Vercel, AWS Lambda, etc.), files in public/ are served via CDN but are NOT available on the filesystem at runtime. Use the \`resourceLoader\` option with dynamic imports instead:
|
|
71
|
+
|
|
72
|
+
resourceLoader: (language, namespace) =>
|
|
73
|
+
import(\`./public/locales/\${language}/\${namespace}.json\`)
|
|
74
|
+
`);
|
|
68
75
|
}
|
|
69
76
|
throw new Error(`next-i18next: Cannot load locale file "${filePath}" in Edge Runtime. Provide pre-bundled \`resources\`, a custom \`resourceLoader\`, or use a custom backend (e.g. i18next-http-backend) via the \`use\` option.`);
|
|
70
77
|
});
|
|
@@ -80,7 +87,8 @@ async function getSharedInstance(config) {
|
|
|
80
87
|
if (_sharedInstancePromise) return _sharedInstancePromise;
|
|
81
88
|
_sharedInstancePromise = (async () => {
|
|
82
89
|
const i18nInstance = createInstance();
|
|
83
|
-
|
|
90
|
+
const partialBundled = config.i18nextOptions?.partialBundledLanguages;
|
|
91
|
+
if ((!config.resources || partialBundled) && !hasCustomBackend(config.use)) i18nInstance.use(createResourceBackend(config));
|
|
84
92
|
config.use.forEach((plugin) => i18nInstance.use(plugin));
|
|
85
93
|
await i18nInstance.init({
|
|
86
94
|
lng: config.fallbackLng,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.mjs","names":[],"sources":["../../src/appRouter/config.ts","../../src/appRouter/server.ts"],"sourcesContent":["import type { I18nConfig, NormalizedConfig } from './types'\n\nexport function defineConfig(config: I18nConfig): I18nConfig {\n return config\n}\n\nexport function normalizeConfig(userConfig: I18nConfig): NormalizedConfig {\n // Support legacy format: { i18n: { defaultLocale, locales } }\n const supportedLngs = userConfig.supportedLngs ??\n userConfig.i18n?.locales?.filter((l: string) => l !== 'default') ??\n ['en']\n const fallbackLng = userConfig.fallbackLng ??\n userConfig.i18n?.defaultLocale ??\n supportedLngs[0]\n\n if (!fallbackLng) {\n throw new Error('next-i18next: fallbackLng (or i18n.defaultLocale) is required')\n }\n if (supportedLngs.length === 0) {\n throw new Error('next-i18next: supportedLngs (or i18n.locales) must contain at least one language')\n }\n\n const defaultNS = userConfig.defaultNS ?? 'common'\n\n return {\n supportedLngs,\n fallbackLng,\n defaultNS,\n ns: userConfig.ns ?? [defaultNS],\n localeInPath: userConfig.localeInPath ?? true,\n localePath: userConfig.localePath ?? '/locales',\n localeStructure: userConfig.localeStructure ?? '{{lng}}/{{ns}}',\n localeExtension: userConfig.localeExtension ?? 'json',\n cookieName: userConfig.cookieName ?? 'i18next',\n headerName: userConfig.headerName ?? 'x-i18next-current-language',\n cookieMaxAge: userConfig.cookieMaxAge ?? 365 * 24 * 60 * 60,\n ignoredPaths: userConfig.ignoredPaths ?? ['/api', '/_next', '/static'],\n basePath: userConfig.basePath,\n resources: userConfig.resources,\n resourceLoader: userConfig.resourceLoader,\n use: userConfig.use ?? [],\n i18nextOptions: (userConfig.i18nextOptions ?? {}) as Record<string, any>,\n nonExplicitSupportedLngs: userConfig.nonExplicitSupportedLngs ?? false,\n // Preserve legacy fields\n i18n: userConfig.i18n,\n serializeConfig: userConfig.serializeConfig,\n reloadOnPrerender: userConfig.reloadOnPrerender,\n }\n}\n","import { createInstance } from 'i18next'\nimport type { i18n as I18NextClient, Resource, Module, FlatNamespace, KeyPrefix } from 'i18next'\nimport resourcesToBackend from 'i18next-resources-to-backend'\nimport { cache } from 'react'\nimport { headers, cookies } from 'next/headers'\n\nimport type { I18nConfig, NormalizedConfig, GetTResult } from './types'\nimport { normalizeConfig } from './config'\n\nlet _config: NormalizedConfig | null = null\n\n// Module-level singleton: persists across requests within the same server process.\n// This is critical for custom backends (i18next-http-backend, i18next-locize-backend)\n// to avoid re-fetching translations on every request.\n// In serverless environments (Lambda, Cloud Functions, etc.), this lives as long as\n// the warm function instance — backends with reloadInterval will refresh automatically.\nlet _sharedInstance: I18NextClient | null = null\nlet _sharedInstancePromise: Promise<I18NextClient> | null = null\n\nfunction getConfig(): NormalizedConfig {\n if (!_config) {\n throw new Error(\n 'next-i18next: Server module not initialized. Call initServerI18next(config) in your root layout.'\n )\n }\n return _config\n}\n\n/**\n * Initialize the server-side i18next configuration.\n * Call this once in your root layout or a shared setup file.\n */\nexport function initServerI18next(userConfig: I18nConfig): void {\n _config = normalizeConfig(userConfig)\n}\n\nfunction hasCustomBackend(plugins: any[]): boolean {\n return plugins.some((b: Module) => b.type === 'backend')\n}\n\nfunction createResourceBackend(config: NormalizedConfig) {\n if (config.resourceLoader) {\n return resourcesToBackend(config.resourceLoader)\n }\n return resourcesToBackend(async (language: string, namespace: string) => {\n const filePath = `${config.localePath}/${config.localeStructure\n .replace('{{lng}}', language)\n .replace('{{ns}}', namespace)}.${config.localeExtension}`\n\n // Node.js runtime: read from filesystem\n if (typeof process !== 'undefined' && process.versions?.node) {\n const fs = await import('fs/promises')\n const pathMod = await import('path')\n const resolved = pathMod.resolve(process.cwd(), `public${filePath}`)\n const content = await fs.readFile(resolved, 'utf-8')\n return JSON.parse(content)\n }\n\n // Edge runtime: filesystem not available\n throw new Error(\n `next-i18next: Cannot load locale file \"${filePath}\" in Edge Runtime. ` +\n 'Provide pre-bundled `resources`, a custom `resourceLoader`, or use a custom backend (e.g. i18next-http-backend) via the `use` option.'\n )\n })\n}\n\n/**\n * Get or create the shared i18next instance.\n * The instance is created once and reused across all requests.\n * All languages are preloaded so that getFixedT(lng) works for any supported language.\n * Additional namespaces are loaded on demand and cached in the instance store.\n */\nasync function getSharedInstance(config: NormalizedConfig): Promise<I18NextClient> {\n if (_sharedInstance?.isInitialized) return _sharedInstance\n\n // Deduplicate concurrent init calls (multiple requests arriving while first init is in flight)\n if (_sharedInstancePromise) return _sharedInstancePromise\n\n _sharedInstancePromise = (async () => {\n const i18nInstance = createInstance()\n\n // Only add a backend if resources are not pre-loaded and no custom backend is provided\n if (!config.resources && !hasCustomBackend(config.use)) {\n i18nInstance.use(createResourceBackend(config))\n }\n\n config.use.forEach((plugin: any) => i18nInstance.use(plugin))\n\n await i18nInstance.init({\n // No `lng` — the shared instance is language-neutral.\n // We use getFixedT(lng, ns) to get language-specific translators.\n lng: config.fallbackLng,\n ns: config.ns,\n defaultNS: config.defaultNS,\n fallbackLng: config.fallbackLng,\n supportedLngs: config.supportedLngs,\n nonExplicitSupportedLngs: config.nonExplicitSupportedLngs,\n fallbackNS: config.defaultNS,\n preload: config.supportedLngs, // preload ALL languages upfront\n interpolation: { escapeValue: false },\n ...(config.resources ? { resources: config.resources } : {}),\n ...config.i18nextOptions,\n })\n\n _sharedInstance = i18nInstance\n return i18nInstance\n })()\n\n return _sharedInstancePromise\n}\n\n// Per-request language detection, deduplicated within a single React render\nconst detectLanguage = cache(async (config: NormalizedConfig): Promise<string> => {\n const headerList = await headers()\n const fromHeader = headerList.get(config.headerName)\n if (fromHeader) return fromHeader\n\n const cookieStore = await cookies()\n const cookieValue = cookieStore.get(config.cookieName)?.value\n if (cookieValue) {\n if (config.supportedLngs.includes(cookieValue)) {\n return cookieValue\n }\n // nonExplicitSupportedLngs: e.g. cookie 'en' matches supported 'en-US'\n if (config.nonExplicitSupportedLngs) {\n const prefix = cookieValue.toLowerCase().split('-')[0]\n const match = config.supportedLngs.find(\n l => l.toLowerCase() === prefix || l.toLowerCase().split('-')[0] === prefix\n )\n if (match) return match\n }\n }\n\n return config.fallbackLng\n})\n\n/**\n * Get a translation function for use in Server Components, layouts, and generateMetadata.\n *\n * The underlying i18next instance is a **module-level singleton** that persists across\n * requests. This means custom backends (i18next-http-backend, i18next-locize-backend, etc.)\n * only fetch translations once (or according to their own reloadInterval), not on every request.\n *\n * @example\n * ```tsx\n * import { getT } from 'next-i18next/server'\n *\n * export default async function Page() {\n * const { t, i18n } = await getT('home')\n * return <h1>{t('heading')}</h1>\n * }\n * ```\n */\nexport async function getT<\n Ns extends FlatNamespace = FlatNamespace,\n KPrefix extends KeyPrefix<Ns> = undefined,\n>(\n ns?: Ns | Ns[],\n options: { keyPrefix?: KPrefix; lng?: string } = {},\n): Promise<GetTResult<Ns, KPrefix>> {\n const config = getConfig()\n\n const lng = options.lng || await detectLanguage(config)\n const i18nInstance = await getSharedInstance(config)\n\n // Load additional namespaces on demand if not already loaded\n const nsArray: string[] = ns\n ? (Array.isArray(ns) ? ns as string[] : [ns as string])\n : config.ns\n const missingNs = nsArray.filter(n => !i18nInstance.hasLoadedNamespace(n))\n if (missingNs.length > 0) {\n await i18nInstance.loadNamespaces(missingNs)\n }\n\n const resolvedNs = ns\n ? (Array.isArray(ns) ? ns[0] : ns) as string\n : config.defaultNS\n\n return {\n t: i18nInstance.getFixedT(lng, resolvedNs, options.keyPrefix as string | undefined),\n i18n: i18nInstance,\n lng,\n } as any\n}\n\n/**\n * Extract loaded resources from the server i18next instance for passing to I18nProvider.\n *\n * @example\n * ```tsx\n * const { i18n } = await getT()\n * const resources = getResources(i18n, ['common', 'footer'])\n * return <I18nProvider language={i18n.language} resources={resources}>{children}</I18nProvider>\n * ```\n */\nexport function getResources(\n i18n: I18NextClient,\n namespaces?: string[],\n): Resource {\n const resources: Resource = {}\n const store = i18n.store?.data || {}\n const nsFilter = namespaces ? new Set(namespaces) : null\n\n for (const lng of Object.keys(store)) {\n resources[lng] = {}\n for (const ns of Object.keys(store[lng])) {\n if (!nsFilter || nsFilter.has(ns)) {\n resources[lng][ns] = store[lng][ns]\n }\n }\n }\n\n return resources\n}\n\n/**\n * Helper for generateStaticParams — returns params for all supported languages.\n *\n * @example\n * ```tsx\n * import { generateI18nStaticParams } from 'next-i18next/server'\n *\n * export async function generateStaticParams() {\n * return generateI18nStaticParams()\n * }\n * ```\n */\nexport function generateI18nStaticParams(): { lng: string }[] {\n const config = getConfig()\n return config.supportedLngs.map(lng => ({ lng }))\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"server.mjs","names":[],"sources":["../../src/appRouter/config.ts","../../src/appRouter/server.ts"],"sourcesContent":["import type { I18nConfig, NormalizedConfig } from './types'\n\nexport function defineConfig(config: I18nConfig): I18nConfig {\n return config\n}\n\nexport function normalizeConfig(userConfig: I18nConfig): NormalizedConfig {\n // Support legacy format: { i18n: { defaultLocale, locales } }\n const supportedLngs = userConfig.supportedLngs ??\n userConfig.i18n?.locales?.filter((l: string) => l !== 'default') ??\n ['en']\n const fallbackLng = userConfig.fallbackLng ??\n userConfig.i18n?.defaultLocale ??\n supportedLngs[0]\n\n if (!fallbackLng) {\n throw new Error('next-i18next: fallbackLng (or i18n.defaultLocale) is required')\n }\n if (supportedLngs.length === 0) {\n throw new Error('next-i18next: supportedLngs (or i18n.locales) must contain at least one language')\n }\n\n const defaultNS = userConfig.defaultNS ?? 'common'\n\n return {\n supportedLngs,\n fallbackLng,\n defaultNS,\n ns: userConfig.ns ?? [defaultNS],\n localeInPath: userConfig.localeInPath ?? true,\n hideDefaultLocale: userConfig.hideDefaultLocale ?? false,\n localePath: userConfig.localePath ?? '/locales',\n localeStructure: userConfig.localeStructure ?? '{{lng}}/{{ns}}',\n localeExtension: userConfig.localeExtension ?? 'json',\n cookieName: userConfig.cookieName ?? 'i18next',\n headerName: userConfig.headerName ?? 'x-i18next-current-language',\n cookieMaxAge: userConfig.cookieMaxAge ?? 365 * 24 * 60 * 60,\n ignoredPaths: userConfig.ignoredPaths ?? ['/api', '/_next', '/static'],\n basePath: userConfig.basePath,\n resources: userConfig.resources,\n resourceLoader: userConfig.resourceLoader,\n use: userConfig.use ?? [],\n i18nextOptions: (userConfig.i18nextOptions ?? {}) as Record<string, any>,\n nonExplicitSupportedLngs: userConfig.nonExplicitSupportedLngs ?? false,\n // Preserve legacy fields\n i18n: userConfig.i18n,\n serializeConfig: userConfig.serializeConfig,\n reloadOnPrerender: userConfig.reloadOnPrerender,\n }\n}\n","import { createInstance } from 'i18next'\nimport type { i18n as I18NextClient, Resource, Module, FlatNamespace, KeyPrefix } from 'i18next'\nimport resourcesToBackend from 'i18next-resources-to-backend'\nimport { cache } from 'react'\nimport { headers, cookies } from 'next/headers'\n\nimport type { I18nConfig, NormalizedConfig, GetTResult } from './types'\nimport { normalizeConfig } from './config'\n\nlet _config: NormalizedConfig | null = null\n\n// Module-level singleton: persists across requests within the same server process.\n// This is critical for custom backends (i18next-http-backend, i18next-locize-backend)\n// to avoid re-fetching translations on every request.\n// In serverless environments (Lambda, Cloud Functions, etc.), this lives as long as\n// the warm function instance — backends with reloadInterval will refresh automatically.\nlet _sharedInstance: I18NextClient | null = null\nlet _sharedInstancePromise: Promise<I18NextClient> | null = null\n\nfunction getConfig(): NormalizedConfig {\n if (!_config) {\n throw new Error(\n 'next-i18next: Server module not initialized. Call initServerI18next(config) in your root layout.'\n )\n }\n return _config\n}\n\n/**\n * Initialize the server-side i18next configuration.\n * Call this once in your root layout or a shared setup file.\n */\nexport function initServerI18next(userConfig: I18nConfig): void {\n _config = normalizeConfig(userConfig)\n}\n\nfunction hasCustomBackend(plugins: any[]): boolean {\n return plugins.some((b: Module) => b.type === 'backend')\n}\n\nfunction createResourceBackend(config: NormalizedConfig) {\n if (config.resourceLoader) {\n return resourcesToBackend(config.resourceLoader)\n }\n return resourcesToBackend(async (language: string, namespace: string) => {\n const filePath = `${config.localePath}/${config.localeStructure\n .replace('{{lng}}', language)\n .replace('{{ns}}', namespace)}.${config.localeExtension}`\n\n // Node.js runtime: read from filesystem\n if (typeof process !== 'undefined' && process.versions?.node) {\n try {\n const fs = await import('fs/promises')\n const pathMod = await import('path')\n const resolved = pathMod.resolve(process.cwd(), `public${filePath}`)\n const content = await fs.readFile(resolved, 'utf-8')\n return JSON.parse(content)\n } catch {\n throw new Error(\n `next-i18next: Could not read locale file \"public${filePath}\". ` +\n 'On serverless platforms (Vercel, AWS Lambda, etc.), files in public/ are served via CDN ' +\n 'but are NOT available on the filesystem at runtime. Use the `resourceLoader` option with ' +\n 'dynamic imports instead:\\n\\n' +\n ' resourceLoader: (language, namespace) =>\\n' +\n // eslint-disable-next-line no-template-curly-in-string\n ' import(`./public/locales/${language}/${namespace}.json`)\\n'\n )\n }\n }\n\n // Edge runtime: filesystem not available\n throw new Error(\n `next-i18next: Cannot load locale file \"${filePath}\" in Edge Runtime. ` +\n 'Provide pre-bundled `resources`, a custom `resourceLoader`, or use a custom backend (e.g. i18next-http-backend) via the `use` option.'\n )\n })\n}\n\n/**\n * Get or create the shared i18next instance.\n * The instance is created once and reused across all requests.\n * All languages are preloaded so that getFixedT(lng) works for any supported language.\n * Additional namespaces are loaded on demand and cached in the instance store.\n */\nasync function getSharedInstance(config: NormalizedConfig): Promise<I18NextClient> {\n if (_sharedInstance?.isInitialized) return _sharedInstance\n\n // Deduplicate concurrent init calls (multiple requests arriving while first init is in flight)\n if (_sharedInstancePromise) return _sharedInstancePromise\n\n _sharedInstancePromise = (async () => {\n const i18nInstance = createInstance()\n\n // Add a backend when needed:\n // - No resources provided → backend loads everything\n // - Resources provided with partialBundledLanguages → backend loads the rest\n // - Custom backend in config.use → user handles it, skip default backend\n const partialBundled = config.i18nextOptions?.partialBundledLanguages\n if ((!config.resources || partialBundled) && !hasCustomBackend(config.use)) {\n i18nInstance.use(createResourceBackend(config))\n }\n\n config.use.forEach((plugin: any) => i18nInstance.use(plugin))\n\n await i18nInstance.init({\n // No `lng` — the shared instance is language-neutral.\n // We use getFixedT(lng, ns) to get language-specific translators.\n lng: config.fallbackLng,\n ns: config.ns,\n defaultNS: config.defaultNS,\n fallbackLng: config.fallbackLng,\n supportedLngs: config.supportedLngs,\n nonExplicitSupportedLngs: config.nonExplicitSupportedLngs,\n fallbackNS: config.defaultNS,\n preload: config.supportedLngs, // preload ALL languages upfront\n interpolation: { escapeValue: false },\n ...(config.resources ? { resources: config.resources } : {}),\n ...config.i18nextOptions,\n })\n\n _sharedInstance = i18nInstance\n return i18nInstance\n })()\n\n return _sharedInstancePromise\n}\n\n// Per-request language detection, deduplicated within a single React render\nconst detectLanguage = cache(async (config: NormalizedConfig): Promise<string> => {\n const headerList = await headers()\n const fromHeader = headerList.get(config.headerName)\n if (fromHeader) return fromHeader\n\n const cookieStore = await cookies()\n const cookieValue = cookieStore.get(config.cookieName)?.value\n if (cookieValue) {\n if (config.supportedLngs.includes(cookieValue)) {\n return cookieValue\n }\n // nonExplicitSupportedLngs: e.g. cookie 'en' matches supported 'en-US'\n if (config.nonExplicitSupportedLngs) {\n const prefix = cookieValue.toLowerCase().split('-')[0]\n const match = config.supportedLngs.find(\n l => l.toLowerCase() === prefix || l.toLowerCase().split('-')[0] === prefix\n )\n if (match) return match\n }\n }\n\n return config.fallbackLng\n})\n\n/**\n * Get a translation function for use in Server Components, layouts, and generateMetadata.\n *\n * The underlying i18next instance is a **module-level singleton** that persists across\n * requests. This means custom backends (i18next-http-backend, i18next-locize-backend, etc.)\n * only fetch translations once (or according to their own reloadInterval), not on every request.\n *\n * @example\n * ```tsx\n * import { getT } from 'next-i18next/server'\n *\n * export default async function Page() {\n * const { t, i18n } = await getT('home')\n * return <h1>{t('heading')}</h1>\n * }\n * ```\n */\nexport async function getT<\n Ns extends FlatNamespace = FlatNamespace,\n KPrefix extends KeyPrefix<Ns> = undefined,\n>(\n ns?: Ns | Ns[],\n options: { keyPrefix?: KPrefix; lng?: string } = {},\n): Promise<GetTResult<Ns, KPrefix>> {\n const config = getConfig()\n\n const lng = options.lng || await detectLanguage(config)\n const i18nInstance = await getSharedInstance(config)\n\n // Load additional namespaces on demand if not already loaded\n const nsArray: string[] = ns\n ? (Array.isArray(ns) ? ns as string[] : [ns as string])\n : config.ns\n const missingNs = nsArray.filter(n => !i18nInstance.hasLoadedNamespace(n))\n if (missingNs.length > 0) {\n await i18nInstance.loadNamespaces(missingNs)\n }\n\n const resolvedNs = ns\n ? (Array.isArray(ns) ? ns[0] : ns) as string\n : config.defaultNS\n\n return {\n t: i18nInstance.getFixedT(lng, resolvedNs, options.keyPrefix as string | undefined),\n i18n: i18nInstance,\n lng,\n } as any\n}\n\n/**\n * Extract loaded resources from the server i18next instance for passing to I18nProvider.\n *\n * @example\n * ```tsx\n * const { i18n } = await getT()\n * const resources = getResources(i18n, ['common', 'footer'])\n * return <I18nProvider language={i18n.language} resources={resources}>{children}</I18nProvider>\n * ```\n */\nexport function getResources(\n i18n: I18NextClient,\n namespaces?: string[],\n): Resource {\n const resources: Resource = {}\n const store = i18n.store?.data || {}\n const nsFilter = namespaces ? new Set(namespaces) : null\n\n for (const lng of Object.keys(store)) {\n resources[lng] = {}\n for (const ns of Object.keys(store[lng])) {\n if (!nsFilter || nsFilter.has(ns)) {\n resources[lng][ns] = store[lng][ns]\n }\n }\n }\n\n return resources\n}\n\n/**\n * Helper for generateStaticParams — returns params for all supported languages.\n *\n * @example\n * ```tsx\n * import { generateI18nStaticParams } from 'next-i18next/server'\n *\n * export async function generateStaticParams() {\n * return generateI18nStaticParams()\n * }\n * ```\n */\nexport function generateI18nStaticParams(): { lng: string }[] {\n const config = getConfig()\n return config.supportedLngs.map(lng => ({ lng }))\n}\n"],"mappings":"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"}
|