@bleedingdev/modern-js-i18n-runtime-extensions 3.9.0-ultramodern.9 → 3.9.3-ultramodern.1
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/dist/cjs/index.js +10 -10
- package/dist/cjs/localeRedirectPolicy.js +6 -6
- package/dist/cjs/native.js +160 -0
- package/dist/cjs/paths.js +11 -3
- package/dist/cjs/requestLanguage.js +195 -0
- package/dist/cjs/router-navigation/adapter.js +93 -0
- package/dist/cjs/{localisedUrls/config.js → router-navigation/index.js} +5 -14
- package/dist/cjs/ssrLanguageHandoff.js +94 -0
- package/dist/cjs/urlRewrite.js +64 -0
- package/dist/esm/index.mjs +1 -1
- package/dist/esm/localeRedirectPolicy.mjs +1 -1
- package/dist/esm/native.mjs +112 -0
- package/dist/esm/paths.mjs +7 -2
- package/dist/esm/requestLanguage.mjs +148 -0
- package/dist/esm/router-navigation/adapter.mjs +55 -0
- package/dist/esm/router-navigation/index.mjs +1 -0
- package/dist/esm/ssrLanguageHandoff.mjs +44 -0
- package/dist/esm/urlRewrite.mjs +26 -0
- package/dist/esm-node/index.mjs +1 -1
- package/dist/esm-node/localeRedirectPolicy.mjs +1 -1
- package/dist/esm-node/native.mjs +113 -0
- package/dist/esm-node/paths.mjs +7 -2
- package/dist/esm-node/requestLanguage.mjs +149 -0
- package/dist/esm-node/router-navigation/adapter.mjs +56 -0
- package/dist/esm-node/router-navigation/index.mjs +2 -0
- package/dist/esm-node/ssrLanguageHandoff.mjs +45 -0
- package/dist/esm-node/urlRewrite.mjs +27 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/localeRedirectPolicy.d.ts +1 -1
- package/dist/types/native.d.ts +21 -0
- package/dist/types/paths.d.ts +8 -1
- package/dist/types/requestLanguage.d.ts +60 -0
- package/dist/types/router-navigation/adapter.d.ts +23 -0
- package/dist/types/router-navigation/index.d.ts +1 -0
- package/dist/types/ssrLanguageHandoff.d.ts +60 -0
- package/dist/types/urlRewrite.d.ts +60 -0
- package/package.json +101 -14
- package/src/index.ts +1 -1
- package/src/localeRedirectPolicy.ts +2 -2
- package/src/native.ts +212 -0
- package/src/paths.ts +23 -2
- package/src/requestLanguage.ts +311 -0
- package/src/router-navigation/adapter.tsx +120 -0
- package/src/router-navigation/index.ts +6 -0
- package/src/ssrLanguageHandoff.ts +138 -0
- package/src/urlRewrite.ts +107 -0
- package/dist/cjs/localisedUrls/index.js +0 -98
- package/dist/cjs/localisedUrls/normalise.js +0 -87
- package/dist/cjs/localisedUrls/pathname.js +0 -68
- package/dist/cjs/localisedUrls/patterns.js +0 -214
- package/dist/cjs/localisedUrls/redirect.js +0 -100
- package/dist/cjs/localisedUrls/resolve.js +0 -78
- package/dist/cjs/localisedUrls/routeIdentity.js +0 -118
- package/dist/cjs/localisedUrls/routes.js +0 -210
- package/dist/cjs/localisedUrls/types.js +0 -18
- package/dist/esm/localisedUrls/config.mjs +0 -11
- package/dist/esm/localisedUrls/index.mjs +0 -8
- package/dist/esm/localisedUrls/normalise.mjs +0 -37
- package/dist/esm/localisedUrls/pathname.mjs +0 -24
- package/dist/esm/localisedUrls/patterns.mjs +0 -152
- package/dist/esm/localisedUrls/redirect.mjs +0 -53
- package/dist/esm/localisedUrls/resolve.mjs +0 -37
- package/dist/esm/localisedUrls/routeIdentity.mjs +0 -77
- package/dist/esm/localisedUrls/routes.mjs +0 -169
- package/dist/esm/localisedUrls/types.mjs +0 -0
- package/dist/esm-node/localisedUrls/config.mjs +0 -12
- package/dist/esm-node/localisedUrls/index.mjs +0 -9
- package/dist/esm-node/localisedUrls/normalise.mjs +0 -38
- package/dist/esm-node/localisedUrls/pathname.mjs +0 -25
- package/dist/esm-node/localisedUrls/patterns.mjs +0 -153
- package/dist/esm-node/localisedUrls/redirect.mjs +0 -54
- package/dist/esm-node/localisedUrls/resolve.mjs +0 -38
- package/dist/esm-node/localisedUrls/routeIdentity.mjs +0 -78
- package/dist/esm-node/localisedUrls/routes.mjs +0 -170
- package/dist/esm-node/localisedUrls/types.mjs +0 -1
- package/dist/types/localisedUrls/config.d.ts +0 -9
- package/dist/types/localisedUrls/index.d.ts +0 -10
- package/dist/types/localisedUrls/normalise.d.ts +0 -10
- package/dist/types/localisedUrls/pathname.d.ts +0 -4
- package/dist/types/localisedUrls/patterns.d.ts +0 -11
- package/dist/types/localisedUrls/redirect.d.ts +0 -19
- package/dist/types/localisedUrls/resolve.d.ts +0 -7
- package/dist/types/localisedUrls/routeIdentity.d.ts +0 -24
- package/dist/types/localisedUrls/routes.d.ts +0 -3
- package/dist/types/localisedUrls/types.d.ts +0 -14
- package/rslib.config.mts +0 -4
- package/rstest.config.mts +0 -36
- package/src/localisedUrls/config.ts +0 -18
- package/src/localisedUrls/index.ts +0 -28
- package/src/localisedUrls/normalise.ts +0 -62
- package/src/localisedUrls/pathname.ts +0 -60
- package/src/localisedUrls/patterns.ts +0 -283
- package/src/localisedUrls/redirect.ts +0 -107
- package/src/localisedUrls/resolve.ts +0 -106
- package/src/localisedUrls/routeIdentity.ts +0 -107
- package/src/localisedUrls/routes.ts +0 -432
- package/src/localisedUrls/types.ts +0 -18
- package/tests/languageSyncController.test.ts +0 -136
- package/tests/localeRedirectPolicy.test.ts +0 -173
- package/tests/localisedUrls.test.ts +0 -532
- package/tests/paths.test.ts +0 -24
- package/tests/reactLanguageSync.test.tsx +0 -136
- package/tests/tsconfig.json +0 -11
- package/tsconfig.json +0 -11
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type LocalisedUrlsOption } from '@modern-js/runtime-extensions/localised-urls';
|
|
2
|
+
/**
|
|
3
|
+
* Structurally identical to `@tanstack/router-core`'s `LocationRewriteFunction`.
|
|
4
|
+
* Defined locally so this module has no runtime dependency on router-core —
|
|
5
|
+
* any router built on that contract (TanStack Router, `@octanejs/tanstack-router`,
|
|
6
|
+
* which re-exports it verbatim) can consume the rewrite this module builds.
|
|
7
|
+
*/
|
|
8
|
+
export type I18nLocationRewriteFunction = (args: {
|
|
9
|
+
url: URL;
|
|
10
|
+
}) => undefined | string | URL;
|
|
11
|
+
/** Structurally identical to `@tanstack/router-core`'s `LocationRewrite`. */
|
|
12
|
+
export interface I18nLocationRewrite {
|
|
13
|
+
input?: I18nLocationRewriteFunction;
|
|
14
|
+
output?: I18nLocationRewriteFunction;
|
|
15
|
+
}
|
|
16
|
+
export interface CreateI18nUrlRewriteOptions {
|
|
17
|
+
/** Supported language codes, e.g. `['en', 'cs']`. */
|
|
18
|
+
languages: readonly string[];
|
|
19
|
+
/**
|
|
20
|
+
* The language an outgoing (internal -> public) location is localized to
|
|
21
|
+
* when its pathname does not already start with a supported language.
|
|
22
|
+
*/
|
|
23
|
+
getLanguage: () => string;
|
|
24
|
+
/**
|
|
25
|
+
* Canonical-path -> per-language path map; mirrors
|
|
26
|
+
* `localeDetection.localisedUrls`. Omit for the plain language-prefix policy.
|
|
27
|
+
*/
|
|
28
|
+
localisedUrls?: LocalisedUrlsOption;
|
|
29
|
+
/**
|
|
30
|
+
* Called with the language segment found in an incoming public pathname,
|
|
31
|
+
* before it is stripped to its canonical form. Not called when the
|
|
32
|
+
* pathname carries no recognizable language segment.
|
|
33
|
+
*/
|
|
34
|
+
onLanguageDetected?: (language: string) => void;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Build a TanStack router-core `LocationRewrite` from the fork's localized-URL
|
|
38
|
+
* pathname helpers: the router matches against canonical (language-agnostic)
|
|
39
|
+
* paths internally, while every public href it produces carries the current
|
|
40
|
+
* language prefix (and any mapped locale slug). An outgoing pathname that
|
|
41
|
+
* already starts with a supported language keeps that language, which is how
|
|
42
|
+
* a route mask such as `LocalizedLink`'s publishes another language's URL.
|
|
43
|
+
*
|
|
44
|
+
* Pass the result directly as a router's `rewrite` option:
|
|
45
|
+
*
|
|
46
|
+
* ```ts
|
|
47
|
+
* createRouter({ routeTree, rewrite: createI18nUrlRewrite({ languages, getLanguage }) })
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* If the router also configures a `basepath`, router-core composes the two
|
|
51
|
+
* for you — its constructor internally does
|
|
52
|
+
* `this.rewrite = rewriteBasepath(basepath, caseSensitive, options.rewrite)`
|
|
53
|
+
* (that helper is an internal, not part of router-core's public exports; the
|
|
54
|
+
* composition happens automatically, nothing extra to call here). It strips
|
|
55
|
+
* the basepath before this rewrite's `input` runs, and applies this
|
|
56
|
+
* rewrite's `output` before the basepath is re-added, so the public URL
|
|
57
|
+
* shape is `${basepath}${languagePrefix}${canonicalPath}` — e.g.
|
|
58
|
+
* `/app/en/dashboard` while the router itself only ever sees `/dashboard`.
|
|
59
|
+
*/
|
|
60
|
+
export declare const createI18nUrlRewrite: (options: CreateI18nUrlRewriteOptions) => I18nLocationRewrite;
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bleedingdev/modern-js-i18n-runtime-extensions",
|
|
3
3
|
"description": "Fork-owned UltraModern.js i18n runtime coordination and localized-route extensions.",
|
|
4
|
-
"homepage": "https://github.com/
|
|
4
|
+
"homepage": "https://github.com/ultramodernjs/ultramodern.js#readme",
|
|
5
5
|
"bugs": {
|
|
6
|
-
"url": "https://github.com/
|
|
6
|
+
"url": "https://github.com/ultramodernjs/ultramodern.js/issues"
|
|
7
7
|
},
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
|
-
"url": "git+https://github.com/
|
|
10
|
+
"url": "git+https://github.com/ultramodernjs/ultramodern.js.git",
|
|
11
11
|
"directory": "packages/runtime/i18n-extensions"
|
|
12
12
|
},
|
|
13
13
|
"license": "MIT",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"modern.js",
|
|
18
18
|
"i18n"
|
|
19
19
|
],
|
|
20
|
-
"version": "3.9.
|
|
20
|
+
"version": "3.9.3-ultramodern.1",
|
|
21
21
|
"types": "./dist/types/index.d.ts",
|
|
22
22
|
"main": "./dist/cjs/index.js",
|
|
23
23
|
"module": "./dist/esm/index.mjs",
|
|
@@ -30,6 +30,14 @@
|
|
|
30
30
|
},
|
|
31
31
|
"default": "./dist/esm/index.mjs"
|
|
32
32
|
},
|
|
33
|
+
"./router-navigation": {
|
|
34
|
+
"types": "./dist/types/router-navigation/index.d.ts",
|
|
35
|
+
"node": {
|
|
36
|
+
"import": "./dist/esm-node/router-navigation/index.mjs",
|
|
37
|
+
"require": "./dist/cjs/router-navigation/index.js"
|
|
38
|
+
},
|
|
39
|
+
"default": "./dist/esm/router-navigation/index.mjs"
|
|
40
|
+
},
|
|
33
41
|
"./package.json": "./package.json",
|
|
34
42
|
"./paths": {
|
|
35
43
|
"types": "./dist/types/paths.d.ts",
|
|
@@ -38,6 +46,46 @@
|
|
|
38
46
|
"require": "./dist/cjs/paths.js"
|
|
39
47
|
},
|
|
40
48
|
"default": "./dist/esm/paths.mjs"
|
|
49
|
+
},
|
|
50
|
+
"./urlRewrite": {
|
|
51
|
+
"types": "./dist/types/urlRewrite.d.ts",
|
|
52
|
+
"node": {
|
|
53
|
+
"import": "./dist/esm-node/urlRewrite.mjs",
|
|
54
|
+
"require": "./dist/cjs/urlRewrite.js"
|
|
55
|
+
},
|
|
56
|
+
"default": "./dist/esm/urlRewrite.mjs"
|
|
57
|
+
},
|
|
58
|
+
"./ssrLanguageHandoff": {
|
|
59
|
+
"types": "./dist/types/ssrLanguageHandoff.d.ts",
|
|
60
|
+
"node": {
|
|
61
|
+
"import": "./dist/esm-node/ssrLanguageHandoff.mjs",
|
|
62
|
+
"require": "./dist/cjs/ssrLanguageHandoff.js"
|
|
63
|
+
},
|
|
64
|
+
"default": "./dist/esm/ssrLanguageHandoff.mjs"
|
|
65
|
+
},
|
|
66
|
+
"./language-sync/controller": {
|
|
67
|
+
"types": "./dist/types/language-sync/controller.d.ts",
|
|
68
|
+
"node": {
|
|
69
|
+
"import": "./dist/esm-node/language-sync/controller.mjs",
|
|
70
|
+
"require": "./dist/cjs/language-sync/controller.js"
|
|
71
|
+
},
|
|
72
|
+
"default": "./dist/esm/language-sync/controller.mjs"
|
|
73
|
+
},
|
|
74
|
+
"./request-language": {
|
|
75
|
+
"types": "./dist/types/requestLanguage.d.ts",
|
|
76
|
+
"node": {
|
|
77
|
+
"import": "./dist/esm-node/requestLanguage.mjs",
|
|
78
|
+
"require": "./dist/cjs/requestLanguage.js"
|
|
79
|
+
},
|
|
80
|
+
"default": "./dist/esm/requestLanguage.mjs"
|
|
81
|
+
},
|
|
82
|
+
"./native": {
|
|
83
|
+
"types": "./dist/types/native.d.ts",
|
|
84
|
+
"node": {
|
|
85
|
+
"import": "./dist/esm-node/native.mjs",
|
|
86
|
+
"require": "./dist/cjs/native.js"
|
|
87
|
+
},
|
|
88
|
+
"default": "./dist/esm/native.mjs"
|
|
41
89
|
}
|
|
42
90
|
},
|
|
43
91
|
"typesVersions": {
|
|
@@ -48,36 +96,75 @@
|
|
|
48
96
|
],
|
|
49
97
|
"paths": [
|
|
50
98
|
"./dist/types/paths.d.ts"
|
|
99
|
+
],
|
|
100
|
+
"router-navigation": [
|
|
101
|
+
"./dist/types/router-navigation/index.d.ts"
|
|
102
|
+
],
|
|
103
|
+
"urlRewrite": [
|
|
104
|
+
"./dist/types/urlRewrite.d.ts"
|
|
105
|
+
],
|
|
106
|
+
"ssrLanguageHandoff": [
|
|
107
|
+
"./dist/types/ssrLanguageHandoff.d.ts"
|
|
108
|
+
],
|
|
109
|
+
"language-sync/controller": [
|
|
110
|
+
"./dist/types/language-sync/controller.d.ts"
|
|
111
|
+
],
|
|
112
|
+
"request-language": [
|
|
113
|
+
"./dist/types/requestLanguage.d.ts"
|
|
114
|
+
],
|
|
115
|
+
"native": [
|
|
116
|
+
"./dist/types/native.d.ts"
|
|
51
117
|
]
|
|
52
118
|
}
|
|
53
119
|
},
|
|
54
120
|
"engines": {
|
|
55
|
-
"node": ">=26.
|
|
121
|
+
"node": ">=26.10.0"
|
|
56
122
|
},
|
|
57
123
|
"dependencies": {
|
|
58
|
-
"@modern-js/
|
|
124
|
+
"@modern-js/renderer-core": "npm:@bleedingdev/modern-js-renderer-core@3.9.3-ultramodern.1",
|
|
125
|
+
"@modern-js/runtime-extensions": "npm:@bleedingdev/modern-js-runtime-extensions@3.9.3-ultramodern.1",
|
|
126
|
+
"@modern-js/runtime-utils": "npm:@bleedingdev/modern-js-runtime-utils@3.9.3-ultramodern.1",
|
|
59
127
|
"i18next-browser-languagedetector": "^8.2.1"
|
|
60
128
|
},
|
|
61
129
|
"peerDependencies": {
|
|
62
|
-
"@modern-js/runtime": "3.9.
|
|
130
|
+
"@modern-js/runtime": "3.9.3-ultramodern.1",
|
|
131
|
+
"i18next": ">=25.7.4",
|
|
63
132
|
"react": "^19.2.8"
|
|
64
133
|
},
|
|
134
|
+
"peerDependenciesMeta": {
|
|
135
|
+
"@modern-js/runtime": {
|
|
136
|
+
"optional": true
|
|
137
|
+
},
|
|
138
|
+
"i18next": {
|
|
139
|
+
"optional": true
|
|
140
|
+
},
|
|
141
|
+
"react": {
|
|
142
|
+
"optional": true
|
|
143
|
+
}
|
|
144
|
+
},
|
|
65
145
|
"devDependencies": {
|
|
66
|
-
"@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.9.
|
|
67
|
-
"@rslib/core": "1.0.
|
|
146
|
+
"@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.9.3-ultramodern.1",
|
|
147
|
+
"@rslib/core": "1.0.3",
|
|
68
148
|
"@scripts/rstest-config": "2.66.0",
|
|
69
|
-
"@types/node": "^26.
|
|
70
|
-
"
|
|
71
|
-
"react": "^19.
|
|
72
|
-
"react-dom": "^19.
|
|
149
|
+
"@types/node": "^26.6.3",
|
|
150
|
+
"i18next": "26.4.2",
|
|
151
|
+
"react": "^19.3.0",
|
|
152
|
+
"react-dom": "^19.3.0"
|
|
73
153
|
},
|
|
74
154
|
"sideEffects": false,
|
|
75
155
|
"publishConfig": {
|
|
76
156
|
"access": "public"
|
|
77
157
|
},
|
|
158
|
+
"files": [
|
|
159
|
+
"dist",
|
|
160
|
+
"src"
|
|
161
|
+
],
|
|
78
162
|
"scripts": {
|
|
79
163
|
"dev": "rslib build --watch",
|
|
80
164
|
"build": "rslib build",
|
|
81
|
-
"test": "rstest
|
|
165
|
+
"test": "rstest"
|
|
166
|
+
},
|
|
167
|
+
"ultramodern": {
|
|
168
|
+
"sourceName": "@modern-js/i18n-runtime-extensions"
|
|
82
169
|
}
|
|
83
170
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type { LocalisedUrlsOption } from '
|
|
1
|
+
import type { LocalisedUrlsOption } from '@modern-js/runtime-extensions/localised-urls';
|
|
2
2
|
import {
|
|
3
3
|
isDefaultLocaleRedirectSkipPath,
|
|
4
4
|
localiseTargetPathname,
|
|
5
5
|
matchesPathPrefix,
|
|
6
6
|
shouldSkipLocaleRedirect,
|
|
7
7
|
stripLanguagePrefix,
|
|
8
|
-
} from '
|
|
8
|
+
} from '@modern-js/runtime-extensions/localised-urls';
|
|
9
9
|
|
|
10
10
|
interface LocaleRedirectRequest {
|
|
11
11
|
url: string;
|
package/src/native.ts
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
NativeEntryI18n,
|
|
3
|
+
NativeI18nResources,
|
|
4
|
+
} from '@modern-js/renderer-core/entry-server';
|
|
5
|
+
import type { LocalisedUrlsOption } from '@modern-js/runtime-extensions/localised-urls';
|
|
6
|
+
import i18next, {
|
|
7
|
+
type BackendModule,
|
|
8
|
+
type i18n as I18nInstance,
|
|
9
|
+
type InitOptions,
|
|
10
|
+
} from 'i18next';
|
|
11
|
+
import { createLatestLanguageSyncBinding } from './language-sync/controller';
|
|
12
|
+
import {
|
|
13
|
+
createRequestLanguageRedirect,
|
|
14
|
+
languageFromPathname,
|
|
15
|
+
type ResolveRequestLanguageOptions,
|
|
16
|
+
resolveRequestLanguage,
|
|
17
|
+
} from './requestLanguage';
|
|
18
|
+
import {
|
|
19
|
+
createI18nSsrHandoffInlineData,
|
|
20
|
+
readI18nSsrHandoff,
|
|
21
|
+
} from './ssrLanguageHandoff';
|
|
22
|
+
import { createI18nUrlRewrite } from './urlRewrite';
|
|
23
|
+
|
|
24
|
+
export type { I18nInstance };
|
|
25
|
+
|
|
26
|
+
export interface NativeI18nOptions extends ResolveRequestLanguageOptions {
|
|
27
|
+
/**
|
|
28
|
+
* JSON-serializable i18next `init` options. `lng`, `fallbackLng`,
|
|
29
|
+
* `supportedLngs`, `resources` and `ns` are owned by the native entry.
|
|
30
|
+
*/
|
|
31
|
+
initOptions?: InitOptions;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** language -> namespace -> lazily imported translation bundle. */
|
|
35
|
+
export type NativeI18nLoaders = Readonly<
|
|
36
|
+
Record<string, Readonly<Record<string, () => Promise<unknown>>>>
|
|
37
|
+
>;
|
|
38
|
+
|
|
39
|
+
export type NativeI18n = NativeEntryI18n<I18nInstance, LocalisedUrlsOption>;
|
|
40
|
+
|
|
41
|
+
function unwrapResource(module: unknown): Record<string, unknown> {
|
|
42
|
+
const resource =
|
|
43
|
+
typeof module === 'object' && module !== null && 'default' in module
|
|
44
|
+
? module.default
|
|
45
|
+
: module;
|
|
46
|
+
return resource as Record<string, unknown>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The localized routing and per-request i18next instances of a native
|
|
51
|
+
* Solid or Octane entry. Each language's bundles load on demand; an instance
|
|
52
|
+
* is never a module singleton.
|
|
53
|
+
*/
|
|
54
|
+
export function createNativeI18n(
|
|
55
|
+
options: NativeI18nOptions,
|
|
56
|
+
loaders: NativeI18nLoaders,
|
|
57
|
+
): NativeI18n {
|
|
58
|
+
const { initOptions = {}, ...routing } = options;
|
|
59
|
+
const namespaces = [
|
|
60
|
+
...new Set(Object.values(loaders).flatMap(Object.keys)),
|
|
61
|
+
].sort();
|
|
62
|
+
const defaultNamespace =
|
|
63
|
+
typeof initOptions.defaultNS === 'string'
|
|
64
|
+
? initOptions.defaultNS
|
|
65
|
+
: namespaces.includes('translation')
|
|
66
|
+
? 'translation'
|
|
67
|
+
: (namespaces[0] ?? 'translation');
|
|
68
|
+
const cookie = routing.detection?.lookupCookie ?? 'i18next';
|
|
69
|
+
const backend: BackendModule = {
|
|
70
|
+
type: 'backend',
|
|
71
|
+
init() {},
|
|
72
|
+
read(language, namespace, callback) {
|
|
73
|
+
// Own keys only: `constructor` or `__proto__` never reaches Object.
|
|
74
|
+
const bundles = Object.hasOwn(loaders, language)
|
|
75
|
+
? loaders[language]
|
|
76
|
+
: undefined;
|
|
77
|
+
const load =
|
|
78
|
+
bundles && Object.hasOwn(bundles, namespace)
|
|
79
|
+
? bundles[namespace]
|
|
80
|
+
: undefined;
|
|
81
|
+
if (!load) {
|
|
82
|
+
callback(null, {});
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
load().then(
|
|
86
|
+
module => callback(null, unwrapResource(module)),
|
|
87
|
+
error => callback(error as Error, false),
|
|
88
|
+
);
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
const currentLanguage = () =>
|
|
92
|
+
languageFromPathname(
|
|
93
|
+
window.location.pathname,
|
|
94
|
+
routing.languages,
|
|
95
|
+
routing.basePath,
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
languages: routing.languages,
|
|
100
|
+
...(routing.localisedUrls === undefined
|
|
101
|
+
? {}
|
|
102
|
+
: { localisedUrls: routing.localisedUrls }),
|
|
103
|
+
resolveRequest: request => resolveRequestLanguage(request, routing),
|
|
104
|
+
redirect: createRequestLanguageRedirect,
|
|
105
|
+
async create(language, resources) {
|
|
106
|
+
// The default export resolves under both i18next module formats.
|
|
107
|
+
const instance = i18next.createInstance();
|
|
108
|
+
instance.use(backend);
|
|
109
|
+
await instance.init({
|
|
110
|
+
...initOptions,
|
|
111
|
+
lng: language,
|
|
112
|
+
fallbackLng: routing.fallbackLanguage,
|
|
113
|
+
supportedLngs: [...routing.languages],
|
|
114
|
+
ns: namespaces,
|
|
115
|
+
defaultNS: defaultNamespace,
|
|
116
|
+
partialBundledLanguages: true,
|
|
117
|
+
resources: resources ? { [language]: resources } : {},
|
|
118
|
+
interpolation: { escapeValue: false, ...initOptions.interpolation },
|
|
119
|
+
});
|
|
120
|
+
return instance;
|
|
121
|
+
},
|
|
122
|
+
rewrite: getLanguage =>
|
|
123
|
+
createI18nUrlRewrite({
|
|
124
|
+
languages: routing.languages,
|
|
125
|
+
getLanguage,
|
|
126
|
+
localisedUrls: routing.localisedUrls,
|
|
127
|
+
}),
|
|
128
|
+
handoff(language, instance) {
|
|
129
|
+
return createI18nSsrHandoffInlineData({
|
|
130
|
+
language,
|
|
131
|
+
...(instance
|
|
132
|
+
? {
|
|
133
|
+
resources: Object.fromEntries(
|
|
134
|
+
namespaces.map(namespace => [
|
|
135
|
+
namespace,
|
|
136
|
+
(instance.getResourceBundle(language, namespace) ??
|
|
137
|
+
{}) as Record<string, unknown>,
|
|
138
|
+
]),
|
|
139
|
+
) satisfies NativeI18nResources,
|
|
140
|
+
}
|
|
141
|
+
: {}),
|
|
142
|
+
});
|
|
143
|
+
},
|
|
144
|
+
clientHandoff() {
|
|
145
|
+
const handoff = readI18nSsrHandoff();
|
|
146
|
+
if (handoff && routing.languages.includes(handoff.language))
|
|
147
|
+
return handoff;
|
|
148
|
+
return { language: currentLanguage() ?? routing.fallbackLanguage };
|
|
149
|
+
},
|
|
150
|
+
syncWithRouter(router, instance) {
|
|
151
|
+
// Navigation to another language prefix switches the instance. A
|
|
152
|
+
// masked location (LocalizedLink to another language) publishes its
|
|
153
|
+
// language on the mask; the unmasked location stays canonical. Switches
|
|
154
|
+
// run through the shared sync policy: the latest URL language wins and
|
|
155
|
+
// rejected loads retry. When retries run out the router has already
|
|
156
|
+
// committed the URL, so a reload lets the server render that language.
|
|
157
|
+
const sync = createLatestLanguageSyncBinding<I18nInstance>();
|
|
158
|
+
sync.updateCallbacks({
|
|
159
|
+
changeLanguage: (target, language) => target.changeLanguage(language),
|
|
160
|
+
commitLanguage() {},
|
|
161
|
+
readLanguage: target => target.language,
|
|
162
|
+
reportFailure: () => window.location.reload(),
|
|
163
|
+
});
|
|
164
|
+
sync.activate(instance);
|
|
165
|
+
const unsubscribe = (router as LanguageRouter).subscribe(
|
|
166
|
+
'onBeforeLoad',
|
|
167
|
+
({ toLocation }) => {
|
|
168
|
+
const publicHref =
|
|
169
|
+
toLocation.maskedLocation?.publicHref ?? toLocation.publicHref;
|
|
170
|
+
const pathname = new URL(publicHref, window.location.origin).pathname;
|
|
171
|
+
const language = languageFromPathname(
|
|
172
|
+
pathname,
|
|
173
|
+
routing.languages,
|
|
174
|
+
routing.basePath,
|
|
175
|
+
);
|
|
176
|
+
if (language) sync.request(language);
|
|
177
|
+
},
|
|
178
|
+
);
|
|
179
|
+
// A retired entry stops listening and retrying.
|
|
180
|
+
return () => {
|
|
181
|
+
unsubscribe();
|
|
182
|
+
sync.deactivate();
|
|
183
|
+
};
|
|
184
|
+
},
|
|
185
|
+
persist(instance) {
|
|
186
|
+
// Every language change reaches the document and the server's
|
|
187
|
+
// language detector, with or without a router.
|
|
188
|
+
const persist = (language: string) => {
|
|
189
|
+
document.documentElement.lang = language;
|
|
190
|
+
// biome-ignore lint/suspicious/noDocumentCookie: the server's language detector reads this cookie; CookieStore is not in every browser.
|
|
191
|
+
document.cookie = `${cookie}=${encodeURIComponent(language)}; path=/; max-age=31536000; samesite=lax`;
|
|
192
|
+
};
|
|
193
|
+
// The client starts in its URL's language, which a stale cookie from
|
|
194
|
+
// an earlier visit may not match; persist it before any later change.
|
|
195
|
+
if (instance.language) persist(instance.language);
|
|
196
|
+
instance.on('languageChanged', persist);
|
|
197
|
+
return () => instance.off('languageChanged', persist);
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
interface LanguageRouter {
|
|
203
|
+
subscribe(
|
|
204
|
+
event: 'onBeforeLoad',
|
|
205
|
+
listener: (event: {
|
|
206
|
+
toLocation: {
|
|
207
|
+
publicHref: string;
|
|
208
|
+
maskedLocation?: { publicHref: string };
|
|
209
|
+
};
|
|
210
|
+
}) => void,
|
|
211
|
+
): () => void;
|
|
212
|
+
}
|
package/src/paths.ts
CHANGED
|
@@ -1,15 +1,36 @@
|
|
|
1
|
-
import { splitUrlTarget } from '@modern-js/runtime-utils/url';
|
|
2
1
|
import {
|
|
3
2
|
canonicalTargetPathname,
|
|
4
3
|
type LocalisedUrlsOption,
|
|
5
4
|
localiseTargetPathname,
|
|
6
|
-
} from '
|
|
5
|
+
} from '@modern-js/runtime-extensions/localised-urls';
|
|
6
|
+
import { splitUrlTarget } from '@modern-js/runtime-utils/url';
|
|
7
|
+
|
|
8
|
+
export type { LocalisedUrlsOption };
|
|
7
9
|
|
|
8
10
|
export interface LocalizedPathsConfig {
|
|
9
11
|
languages: string[];
|
|
10
12
|
localisedUrls?: LocalisedUrlsOption;
|
|
11
13
|
}
|
|
12
14
|
|
|
15
|
+
/**
|
|
16
|
+
* The configured spelling of a requested language, matched case-insensitively.
|
|
17
|
+
* Only a configured language has a locale prefix, so anything else throws
|
|
18
|
+
* rather than becoming an unknown path segment.
|
|
19
|
+
*/
|
|
20
|
+
export const configuredLanguage = (
|
|
21
|
+
requested: string,
|
|
22
|
+
languages: readonly string[],
|
|
23
|
+
): string => {
|
|
24
|
+
const language = languages.find(
|
|
25
|
+
candidate => candidate.toLowerCase() === String(requested).toLowerCase(),
|
|
26
|
+
);
|
|
27
|
+
if (language === undefined)
|
|
28
|
+
throw new RangeError(
|
|
29
|
+
`Unsupported language "${requested}"; expected one of: ${languages.join(', ')}`,
|
|
30
|
+
);
|
|
31
|
+
return language;
|
|
32
|
+
};
|
|
33
|
+
|
|
13
34
|
/** Synchronous mapped URL construction, independent of React/plugin setup. */
|
|
14
35
|
export const buildLocalizedUrl = (
|
|
15
36
|
target: string,
|