@bleedingdev/modern-js-plugin-i18n 3.9.0-ultramodern.8 → 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/cli/index.js +11 -0
- package/dist/cjs/cli/localisedRoutes.js +47 -0
- package/dist/cjs/runtime/context.js +6 -14
- package/dist/cjs/runtime/contexts.js +49 -0
- package/dist/cjs/runtime/core.js +12 -2
- package/dist/cjs/runtime/i18n/detection/middleware.js +1 -1
- package/dist/cjs/runtime/i18n/detection/middleware.node.js +10 -1
- package/dist/cjs/runtime/i18n/detection/priority.js +1 -1
- package/dist/cjs/runtime/navigation.js +48 -0
- package/dist/cjs/runtime/routerAdapter.js +3 -5
- package/dist/cjs/runtime/sharedContexts.js +43 -0
- package/dist/cjs/runtime/utils.js +6 -2
- package/dist/cjs/server/defaultUrlStrategy.js +48 -0
- package/dist/cjs/server/index.js +3 -2
- package/dist/cjs/server/mappedUrlStrategy.js +43 -0
- package/dist/cjs/server/redirectPolicy.js +13 -9
- package/dist/cjs/shared/detection.js +118 -2
- package/dist/cjs/shared/mappedUrlStrategy.js +63 -0
- package/dist/cjs/shared/urlStrategy.js +31 -1
- package/dist/esm/cli/index.mjs +11 -0
- package/dist/esm/cli/localisedRoutes.mjs +9 -0
- package/dist/esm/runtime/context.mjs +2 -10
- package/dist/esm/runtime/contexts.mjs +5 -0
- package/dist/esm/runtime/core.mjs +7 -3
- package/dist/esm/runtime/i18n/detection/middleware.mjs +1 -1
- package/dist/esm/runtime/i18n/detection/middleware.node.mjs +10 -1
- package/dist/esm/runtime/i18n/detection/priority.mjs +1 -1
- package/dist/esm/runtime/navigation.mjs +7 -0
- package/dist/esm/runtime/routerAdapter.mjs +2 -4
- package/dist/esm/runtime/sharedContexts.mjs +1 -0
- package/dist/esm/runtime/utils.mjs +6 -2
- package/dist/esm/server/defaultUrlStrategy.mjs +7 -0
- package/dist/esm/server/index.mjs +3 -2
- package/dist/esm/server/mappedUrlStrategy.mjs +1 -0
- package/dist/esm/server/redirectPolicy.mjs +6 -2
- package/dist/esm/shared/detection.mjs +85 -1
- package/dist/esm/shared/mappedUrlStrategy.mjs +19 -0
- package/dist/esm/shared/urlStrategy.mjs +7 -0
- package/dist/esm-node/cli/index.mjs +11 -0
- package/dist/esm-node/cli/localisedRoutes.mjs +10 -0
- package/dist/esm-node/runtime/context.mjs +2 -10
- package/dist/esm-node/runtime/contexts.mjs +6 -0
- package/dist/esm-node/runtime/core.mjs +7 -3
- package/dist/esm-node/runtime/i18n/detection/middleware.mjs +1 -1
- package/dist/esm-node/runtime/i18n/detection/middleware.node.mjs +10 -1
- package/dist/esm-node/runtime/i18n/detection/priority.mjs +1 -1
- package/dist/esm-node/runtime/navigation.mjs +8 -0
- package/dist/esm-node/runtime/routerAdapter.mjs +2 -4
- package/dist/esm-node/runtime/sharedContexts.mjs +2 -0
- package/dist/esm-node/runtime/utils.mjs +6 -2
- package/dist/esm-node/server/defaultUrlStrategy.mjs +8 -0
- package/dist/esm-node/server/index.mjs +3 -2
- package/dist/esm-node/server/mappedUrlStrategy.mjs +2 -0
- package/dist/esm-node/server/redirectPolicy.mjs +6 -2
- package/dist/esm-node/shared/detection.mjs +85 -1
- package/dist/esm-node/shared/mappedUrlStrategy.mjs +20 -0
- package/dist/esm-node/shared/urlStrategy.mjs +7 -0
- package/dist/types/cli/localisedRoutes.d.ts +19 -0
- package/dist/types/runtime/context.d.ts +2 -2
- package/dist/types/runtime/contexts.d.ts +6 -0
- package/dist/types/runtime/core.d.ts +2 -1
- package/dist/types/runtime/i18n/detection/middleware.d.ts +1 -1
- package/dist/types/runtime/i18n/detection/middleware.node.d.ts +1 -5
- package/dist/types/runtime/navigation.d.ts +10 -0
- package/dist/types/runtime/sharedContexts.d.ts +1 -0
- package/dist/types/runtime/utils.d.ts +1 -1
- package/dist/types/server/defaultUrlStrategy.d.ts +11 -0
- package/dist/types/server/index.d.ts +1 -1
- package/dist/types/server/mappedUrlStrategy.d.ts +7 -0
- package/dist/types/server/redirectPolicy.d.ts +1 -1
- package/dist/types/shared/detection.d.ts +11 -1
- package/dist/types/shared/mappedUrlStrategy.d.ts +27 -0
- package/dist/types/shared/type.d.ts +10 -0
- package/dist/types/shared/urlStrategy.d.ts +11 -0
- package/package.json +43 -24
- package/src/cli/index.ts +43 -1
- package/src/cli/localisedRoutes.ts +53 -0
- package/src/runtime/context.tsx +6 -31
- package/src/runtime/contexts.ts +16 -0
- package/src/runtime/core.tsx +30 -3
- package/src/runtime/i18n/detection/middleware.node.ts +25 -7
- package/src/runtime/i18n/detection/middleware.ts +2 -0
- package/src/runtime/i18n/detection/priority.ts +2 -0
- package/src/runtime/navigation.tsx +20 -0
- package/src/runtime/routerAdapter.tsx +2 -11
- package/src/runtime/sharedContexts.ts +8 -0
- package/src/runtime/utils.ts +13 -4
- package/src/server/defaultUrlStrategy.ts +27 -0
- package/src/server/index.ts +7 -12
- package/src/server/mappedUrlStrategy.ts +10 -0
- package/src/server/redirectPolicy.ts +19 -4
- package/src/shared/detection.ts +34 -10
- package/src/shared/mappedUrlStrategy.ts +59 -0
- package/src/shared/type.ts +10 -0
- package/src/shared/urlStrategy.ts +27 -0
- package/rslib.config.mts +0 -4
- package/rstest.config.mts +0 -46
- package/tests/backendDefaults.test.ts +0 -30
- package/tests/federatedI18nBoundaryResolver.test.tsx +0 -65
- package/tests/i18nUtils.test.ts +0 -60
- package/tests/link.test.tsx +0 -139
- package/tests/localisedUrls.test.ts +0 -176
- package/tests/reactI18nextRuntimeBoundary.test.ts +0 -117
- package/tests/redirectPolicy.test.ts +0 -54
- package/tests/routerAdapter.test.tsx +0 -221
- package/tsconfig.json +0 -15
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bleedingdev/modern-js-plugin-i18n",
|
|
3
3
|
"description": "A Progressive React Framework for modern web development.",
|
|
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/plugin-i18n"
|
|
12
12
|
},
|
|
13
13
|
"license": "MIT",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"modern",
|
|
18
18
|
"modern.js"
|
|
19
19
|
],
|
|
20
|
-
"version": "3.9.
|
|
20
|
+
"version": "3.9.3-ultramodern.1",
|
|
21
21
|
"engines": {
|
|
22
22
|
"node": ">=20"
|
|
23
23
|
},
|
|
@@ -55,6 +55,13 @@
|
|
|
55
55
|
},
|
|
56
56
|
"default": "./dist/esm/runtime/context.mjs"
|
|
57
57
|
},
|
|
58
|
+
"./runtime/contexts": {
|
|
59
|
+
"types": "./dist/types/runtime/contexts.d.ts",
|
|
60
|
+
"node": {
|
|
61
|
+
"module": "./dist/esm/runtime/contexts.mjs"
|
|
62
|
+
},
|
|
63
|
+
"default": "./dist/esm/runtime/contexts.mjs"
|
|
64
|
+
},
|
|
58
65
|
"./runtime/no-react-i18next": {
|
|
59
66
|
"types": "./dist/types/runtime/no-react-i18next.d.ts",
|
|
60
67
|
"node": {
|
|
@@ -104,6 +111,9 @@
|
|
|
104
111
|
"runtime/consumer": [
|
|
105
112
|
"./dist/types/runtime/context.d.ts"
|
|
106
113
|
],
|
|
114
|
+
"runtime/contexts": [
|
|
115
|
+
"./dist/types/runtime/contexts.d.ts"
|
|
116
|
+
],
|
|
107
117
|
"runtime/no-react-i18next": [
|
|
108
118
|
"./dist/types/runtime/no-react-i18next.d.ts"
|
|
109
119
|
],
|
|
@@ -122,22 +132,23 @@
|
|
|
122
132
|
}
|
|
123
133
|
},
|
|
124
134
|
"dependencies": {
|
|
125
|
-
"@modern-js/
|
|
126
|
-
"@modern-js/
|
|
127
|
-
"@modern-js/
|
|
128
|
-
"@modern-js/server-
|
|
129
|
-
"@modern-js/
|
|
130
|
-
"@modern-js/
|
|
135
|
+
"@modern-js/i18n-runtime-extensions": "npm:@bleedingdev/modern-js-i18n-runtime-extensions@3.9.3-ultramodern.1",
|
|
136
|
+
"@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.9.3-ultramodern.1",
|
|
137
|
+
"@modern-js/runtime-utils": "npm:@bleedingdev/modern-js-runtime-utils@3.9.3-ultramodern.1",
|
|
138
|
+
"@modern-js/server-core": "npm:@bleedingdev/modern-js-server-core@3.9.3-ultramodern.1",
|
|
139
|
+
"@modern-js/server-runtime": "npm:@bleedingdev/modern-js-server-runtime@3.9.3-ultramodern.1",
|
|
140
|
+
"@modern-js/types": "npm:@bleedingdev/modern-js-types@3.9.3-ultramodern.1",
|
|
141
|
+
"@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.9.3-ultramodern.1",
|
|
131
142
|
"@swc/helpers": "^0.5.23",
|
|
132
143
|
"i18next-browser-languagedetector": "^8.2.1",
|
|
133
144
|
"i18next-chained-backend": "^5.0.6",
|
|
134
|
-
"i18next-fs-backend": "^2.6.
|
|
145
|
+
"i18next-fs-backend": "^2.6.8",
|
|
135
146
|
"i18next-http-backend": "^4.0.2",
|
|
136
|
-
"i18next-http-middleware": "^3.9.
|
|
147
|
+
"i18next-http-middleware": "^3.9.9"
|
|
137
148
|
},
|
|
138
149
|
"peerDependencies": {
|
|
139
|
-
"@modern-js/runtime": "3.9.
|
|
140
|
-
"i18next": ">=25.
|
|
150
|
+
"@modern-js/runtime": "3.9.3-ultramodern.1",
|
|
151
|
+
"i18next": ">=25.10.10",
|
|
141
152
|
"react": "^19.2.8",
|
|
142
153
|
"react-dom": "^19.2.8",
|
|
143
154
|
"react-i18next": ">=15.7.4"
|
|
@@ -151,26 +162,34 @@
|
|
|
151
162
|
}
|
|
152
163
|
},
|
|
153
164
|
"devDependencies": {
|
|
154
|
-
"@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.9.
|
|
155
|
-
"@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.9.
|
|
156
|
-
"@
|
|
157
|
-
"@
|
|
158
|
-
"@
|
|
165
|
+
"@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.9.3-ultramodern.1",
|
|
166
|
+
"@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.9.3-ultramodern.1",
|
|
167
|
+
"@modern-js/runtime-extensions": "npm:@bleedingdev/modern-js-runtime-extensions@3.9.3-ultramodern.1",
|
|
168
|
+
"@rslib/core": "1.0.3",
|
|
169
|
+
"@scripts/rstest-config": "2.66.0",
|
|
170
|
+
"@types/node": "^26.6.3",
|
|
159
171
|
"esbuild": "^0.28.2",
|
|
160
172
|
"i18next": "26.4.2",
|
|
161
|
-
"react": "^19.
|
|
162
|
-
"react-dom": "^19.
|
|
163
|
-
"react-i18next": "17.0.
|
|
173
|
+
"react": "^19.3.0",
|
|
174
|
+
"react-dom": "^19.3.0",
|
|
175
|
+
"react-i18next": "17.0.15",
|
|
164
176
|
"ts-node": "^10.9.2",
|
|
165
|
-
"typescript": "
|
|
177
|
+
"typescript": "7.0.2"
|
|
166
178
|
},
|
|
167
179
|
"sideEffects": false,
|
|
168
180
|
"publishConfig": {
|
|
169
181
|
"access": "public"
|
|
170
182
|
},
|
|
183
|
+
"files": [
|
|
184
|
+
"dist",
|
|
185
|
+
"src"
|
|
186
|
+
],
|
|
171
187
|
"scripts": {
|
|
172
188
|
"dev": "rslib build --watch",
|
|
173
189
|
"build": "rslib build",
|
|
174
|
-
"test": "rstest
|
|
190
|
+
"test": "rstest"
|
|
191
|
+
},
|
|
192
|
+
"ultramodern": {
|
|
193
|
+
"sourceName": "@modern-js/plugin-i18n"
|
|
175
194
|
}
|
|
176
195
|
}
|
package/src/cli/index.ts
CHANGED
|
@@ -3,9 +3,12 @@ import path from 'node:path';
|
|
|
3
3
|
import type { AppTools, CliPlugin } from '@modern-js/app-tools';
|
|
4
4
|
import { getPublicDirRoutePrefixes } from '@modern-js/server-core';
|
|
5
5
|
import type { Entrypoint } from '@modern-js/types';
|
|
6
|
+
import { logger } from '@modern-js/utils';
|
|
6
7
|
import type { BackendOptions, LocaleDetectionOptions } from '../shared/type';
|
|
8
|
+
import { isUnusableI18nUrlStrategy } from '../shared/urlStrategy';
|
|
7
9
|
import { getBackendOptions, getLocaleDetectionOptions } from '../shared/utils';
|
|
8
10
|
import { applyDetectedBackendPaths, detectLocalesDirectory } from './locales';
|
|
11
|
+
import { applyLocalisedRoutes } from './localisedRoutes';
|
|
9
12
|
import '../runtime/types';
|
|
10
13
|
|
|
11
14
|
export type TransformRuntimeConfigFn = (
|
|
@@ -112,12 +115,28 @@ export const i18nPlugin = (
|
|
|
112
115
|
}
|
|
113
116
|
|
|
114
117
|
// Build final config with base config and transformed extended config
|
|
115
|
-
const config = {
|
|
118
|
+
const config: Record<string, unknown> = {
|
|
116
119
|
entryName: entrypoint.entryName,
|
|
117
120
|
localeDetection: localeDetectionOptions,
|
|
118
121
|
backend: backendOptions,
|
|
119
122
|
...extendedConfig,
|
|
120
123
|
};
|
|
124
|
+
|
|
125
|
+
// This config is written into the generated runtime registration as
|
|
126
|
+
// JSON, so an object carrying functions arrives at the runtime with none
|
|
127
|
+
// of them. Drop it rather than shipping a broken `{}` that every call
|
|
128
|
+
// site would then have to survive, and say what to configure instead.
|
|
129
|
+
if (isUnusableI18nUrlStrategy(config.urlStrategy)) {
|
|
130
|
+
delete config.urlStrategy;
|
|
131
|
+
logger.warn(
|
|
132
|
+
'[i18n] `urlStrategy` cannot be passed through `modern.config.ts`: ' +
|
|
133
|
+
'the generated runtime registration carries plugin options as ' +
|
|
134
|
+
'JSON, which drops its methods. Configure ' +
|
|
135
|
+
'`localeDetection.localisedUrls` instead — the runtime and the ' +
|
|
136
|
+
'server both derive the URL policy from it — or register a ' +
|
|
137
|
+
'runtime plugin that supplies the strategy directly.',
|
|
138
|
+
);
|
|
139
|
+
}
|
|
121
140
|
const { reactI18next } = extendedConfig as { reactI18next?: boolean };
|
|
122
141
|
const runtimePluginPath =
|
|
123
142
|
customPlugin?.runtime?.path ||
|
|
@@ -136,6 +155,29 @@ export const i18nPlugin = (
|
|
|
136
155
|
};
|
|
137
156
|
});
|
|
138
157
|
|
|
158
|
+
// Mapped locale URLs are a property of `localeDetection.localisedUrls`, not
|
|
159
|
+
// of any particular app-tools composition, so the route expansion lives
|
|
160
|
+
// here rather than in an opt-in fork plugin. A bare `appTools()` consumer
|
|
161
|
+
// that declares the map gets `/cs/obchodni-podminky` generated alongside
|
|
162
|
+
// `/cs/terms-of-service`; without this the localized path 404s under SSR.
|
|
163
|
+
//
|
|
164
|
+
// This is deliberately not registered from `@modern-js/i18n-integration`:
|
|
165
|
+
// that package peer-depends on `@modern-js/runtime`, and pulling it into
|
|
166
|
+
// the default app-tools path closes an Nx cycle
|
|
167
|
+
// (app-tools -> app-tools-extensions -> i18n-integration -> runtime -> app-tools).
|
|
168
|
+
// `@modern-js/i18n-runtime-extensions` is already a direct dependency here,
|
|
169
|
+
// so nothing new is added to the graph.
|
|
170
|
+
api.modifyFileSystemRoutes(({ entrypoint, routes }) => ({
|
|
171
|
+
entrypoint,
|
|
172
|
+
routes: applyLocalisedRoutes(
|
|
173
|
+
routes as any,
|
|
174
|
+
localeDetection
|
|
175
|
+
? getLocaleDetectionOptions(entrypoint.entryName, localeDetection)
|
|
176
|
+
: undefined,
|
|
177
|
+
entrypoint,
|
|
178
|
+
) as typeof routes,
|
|
179
|
+
}));
|
|
180
|
+
|
|
139
181
|
api._internalServerPlugins(({ plugins }) => {
|
|
140
182
|
const { serverRoutes, metaName } = api.getAppContext();
|
|
141
183
|
const normalizedConfig = api.getNormalizedConfig();
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applyLocalisedUrlsToRoutes,
|
|
3
|
+
resolveLocalisedUrlsConfig,
|
|
4
|
+
} from '@modern-js/i18n-runtime-extensions';
|
|
5
|
+
import { getEntrypointRoutesOwner } from '@modern-js/runtime/cli';
|
|
6
|
+
import type {
|
|
7
|
+
Entrypoint,
|
|
8
|
+
NestedRouteForCli,
|
|
9
|
+
PageRoute,
|
|
10
|
+
} from '@modern-js/types';
|
|
11
|
+
import type { BaseLocaleDetectionOptions } from '../shared/type';
|
|
12
|
+
|
|
13
|
+
type FileSystemRoutes = (NestedRouteForCli | PageRoute)[];
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Expand file-system routes from `localeDetection.localisedUrls`.
|
|
17
|
+
*
|
|
18
|
+
* Fork-owned: the localised-URL engine lives in
|
|
19
|
+
* `@modern-js/i18n-runtime-extensions`, which upstream-owned files must not
|
|
20
|
+
* import. `cli/index.ts` calls this through a relative import so the seam, not
|
|
21
|
+
* the edge, is what appears in the upstream-shaped file.
|
|
22
|
+
*
|
|
23
|
+
* Returns the routes unchanged unless locale-path redirects are on, languages
|
|
24
|
+
* are declared, and a non-empty map is configured — an upstream-style config
|
|
25
|
+
* with `localePathRedirect` and `languages` but no map keeps plain
|
|
26
|
+
* locale-prefix behaviour.
|
|
27
|
+
*/
|
|
28
|
+
export const applyLocalisedRoutes = (
|
|
29
|
+
routes: FileSystemRoutes,
|
|
30
|
+
localeDetection: BaseLocaleDetectionOptions | undefined,
|
|
31
|
+
entrypoint?: Entrypoint & Parameters<typeof getEntrypointRoutesOwner>[0],
|
|
32
|
+
): FileSystemRoutes => {
|
|
33
|
+
const {
|
|
34
|
+
localePathRedirect,
|
|
35
|
+
languages = [],
|
|
36
|
+
localisedUrls,
|
|
37
|
+
} = localeDetection ?? {};
|
|
38
|
+
const resolved = resolveLocalisedUrlsConfig(localisedUrls);
|
|
39
|
+
|
|
40
|
+
if (!localePathRedirect || !languages.length || !resolved.enabled) {
|
|
41
|
+
return routes;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return applyLocalisedUrlsToRoutes(
|
|
45
|
+
routes,
|
|
46
|
+
languages,
|
|
47
|
+
resolved.map,
|
|
48
|
+
entrypoint &&
|
|
49
|
+
getEntrypointRoutesOwner(entrypoint) === '@modern-js/plugin-tanstack'
|
|
50
|
+
? 'canonical'
|
|
51
|
+
: 'physical',
|
|
52
|
+
) as FileSystemRoutes;
|
|
53
|
+
};
|
package/src/runtime/context.tsx
CHANGED
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
import { RuntimeComponentResolverContext } from '@modern-js/runtime/context';
|
|
2
2
|
import type { ComponentType, FC, ReactNode } from 'react';
|
|
3
|
-
import {
|
|
4
|
-
createContext,
|
|
5
|
-
useCallback,
|
|
6
|
-
useContext,
|
|
7
|
-
useEffect,
|
|
8
|
-
useMemo,
|
|
9
|
-
} from 'react';
|
|
3
|
+
import { useCallback, useContext, useEffect, useMemo } from 'react';
|
|
10
4
|
import type { I18nUrlStrategy } from '../shared/urlStrategy';
|
|
11
5
|
import {
|
|
12
6
|
changeModernI18nLanguage,
|
|
@@ -19,6 +13,10 @@ import type { I18nInstance } from './i18n';
|
|
|
19
13
|
import type { Resources } from './i18n/instance';
|
|
20
14
|
|
|
21
15
|
import { useI18nRouterAdapter } from './routerAdapter';
|
|
16
|
+
import {
|
|
17
|
+
ModernI18nContext,
|
|
18
|
+
ReactI18nextProviderContext,
|
|
19
|
+
} from './sharedContexts';
|
|
22
20
|
|
|
23
21
|
export { getActualI18nextInstance } from './i18n/instance';
|
|
24
22
|
|
|
@@ -36,30 +34,7 @@ export interface ModernI18nContextValue {
|
|
|
36
34
|
synchronizeLanguage?: (newLang: string) => void;
|
|
37
35
|
}
|
|
38
36
|
|
|
39
|
-
|
|
40
|
-
'@modern-js/plugin-i18n/runtime/ModernI18nContext',
|
|
41
|
-
);
|
|
42
|
-
const reactI18nextProviderContextKey = Symbol.for(
|
|
43
|
-
'@modern-js/plugin-i18n/runtime/ReactI18nextProviderContext',
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
type GlobalContextStore<T> = typeof globalThis & {
|
|
47
|
-
[key: symbol]: ReturnType<typeof createContext<T>> | undefined;
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
const getGlobalContext = <T,>(key: symbol, defaultValue: T) => {
|
|
51
|
-
const globalStore = globalThis as GlobalContextStore<T>;
|
|
52
|
-
globalStore[key] ??= createContext<T>(defaultValue);
|
|
53
|
-
return globalStore[key];
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
export const ModernI18nContext =
|
|
57
|
-
getGlobalContext<ModernI18nContextValue | null>(modernI18nContextKey, null);
|
|
58
|
-
export const ReactI18nextProviderContext =
|
|
59
|
-
getGlobalContext<ComponentType<any> | null>(
|
|
60
|
-
reactI18nextProviderContextKey,
|
|
61
|
-
null,
|
|
62
|
-
);
|
|
37
|
+
export { ModernI18nContext, ReactI18nextProviderContext };
|
|
63
38
|
|
|
64
39
|
export interface ModernI18nProviderProps {
|
|
65
40
|
children: ReactNode;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ComponentType } from 'react';
|
|
2
|
+
import { createContext } from 'react';
|
|
3
|
+
import type { ModernI18nContextValue } from './context';
|
|
4
|
+
import type { I18nRouterAdapter } from './routerAdapter';
|
|
5
|
+
|
|
6
|
+
// Every other i18n runtime module imports these through
|
|
7
|
+
// `@modern-js/plugin-i18n/runtime/contexts` so Module Federation shares one
|
|
8
|
+
// copy of them. Their values stay provider-owned.
|
|
9
|
+
export const ModernI18nContext = createContext<ModernI18nContextValue | null>(
|
|
10
|
+
null,
|
|
11
|
+
);
|
|
12
|
+
export const ReactI18nextProviderContext =
|
|
13
|
+
createContext<ComponentType<any> | null>(null);
|
|
14
|
+
export const I18nNavigationContext = createContext<I18nRouterAdapter | null>(
|
|
15
|
+
null,
|
|
16
|
+
);
|
package/src/runtime/core.tsx
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { RuntimePlugin } from '@modern-js/runtime';
|
|
2
2
|
import type React from 'react';
|
|
3
|
+
import { resolveMappedUrlStrategy } from '../shared/mappedUrlStrategy';
|
|
3
4
|
import type {
|
|
4
5
|
BaseBackendOptions,
|
|
5
6
|
BaseLocaleDetectionOptions,
|
|
6
7
|
} from '../shared/type';
|
|
7
|
-
import type
|
|
8
|
+
import { asI18nUrlStrategy, type I18nUrlStrategy } from '../shared/urlStrategy';
|
|
8
9
|
import type { I18nInitOptions, I18nInstance } from './i18n';
|
|
10
|
+
import { I18nRouterNavigationProvider } from './navigation';
|
|
9
11
|
import {
|
|
10
12
|
type RuntimeContextWithI18n,
|
|
11
13
|
setupI18nBeforeRender,
|
|
@@ -31,6 +33,10 @@ export type {
|
|
|
31
33
|
Resources,
|
|
32
34
|
TranslateFn,
|
|
33
35
|
} from './i18n/instance';
|
|
36
|
+
export {
|
|
37
|
+
I18nRouterNavigationProvider,
|
|
38
|
+
useIntegratedRouterAdapter,
|
|
39
|
+
} from './navigation';
|
|
34
40
|
export type {
|
|
35
41
|
LoadReactI18nextIntegration,
|
|
36
42
|
ReactI18nextIntegration,
|
|
@@ -86,6 +92,21 @@ export const createI18nPlugin =
|
|
|
86
92
|
ignoreRedirectRoutes,
|
|
87
93
|
} = localeDetection || {};
|
|
88
94
|
const { enabled: backendEnabled = false } = backend || {};
|
|
95
|
+
// A strategy configured in `modern.config.ts` reaches here through JSON,
|
|
96
|
+
// which drops its methods. Anything unusable is treated as absent so the
|
|
97
|
+
// built-in language-prefix behaviour applies instead of throwing.
|
|
98
|
+
//
|
|
99
|
+
// Falling back to `localeDetection.localisedUrls` makes mapped locale
|
|
100
|
+
// URLs work for a bare `appTools()` consumer: the map is plain data, so
|
|
101
|
+
// it survives the JSON boundary that a `urlStrategy` object does not.
|
|
102
|
+
// The server already derives its policy from the same field, and the two
|
|
103
|
+
// must agree — otherwise SSR renders `/cs/obchodni-podminky` and the
|
|
104
|
+
// hydrated `<Link>` navigates to `/cs/terms-of-service`, which 404s.
|
|
105
|
+
// An explicit, usable `urlStrategy` (supplied by a composing runtime
|
|
106
|
+
// plugin) still wins.
|
|
107
|
+
const resolvedUrlStrategy =
|
|
108
|
+
asI18nUrlStrategy(urlStrategy) ??
|
|
109
|
+
resolveMappedUrlStrategy(localeDetection);
|
|
89
110
|
let latestI18nInstance: I18nInstance | undefined;
|
|
90
111
|
let I18nextProvider: React.ComponentType<any> | null;
|
|
91
112
|
|
|
@@ -120,8 +141,14 @@ export const createI18nPlugin =
|
|
|
120
141
|
languages,
|
|
121
142
|
fallbackLanguage,
|
|
122
143
|
ignoreRedirectRoutes,
|
|
123
|
-
urlStrategy,
|
|
124
|
-
|
|
144
|
+
urlStrategy: resolvedUrlStrategy,
|
|
145
|
+
// The selected router, not react-router, is what a localized `<Link>`
|
|
146
|
+
// has to navigate through, and the i18n language has to follow a
|
|
147
|
+
// client-side navigation to a mapped URL. Both need the actual router
|
|
148
|
+
// instance, so a bare `appTools()` consumer gets the integrated
|
|
149
|
+
// adapter by default; a composing runtime plugin can still replace it.
|
|
150
|
+
NavigationProvider:
|
|
151
|
+
NavigationProvider ?? I18nRouterNavigationProvider,
|
|
125
152
|
LanguageSynchronization,
|
|
126
153
|
getLatestI18nInstance: () => latestI18nInstance,
|
|
127
154
|
getI18nextProvider: () => I18nextProvider,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { LanguageDetector } from 'i18next-http-middleware';
|
|
2
|
+
import { detectLanguageFromRequest } from '../../../shared/detection.js';
|
|
2
3
|
import type { I18nInstance, LanguageDetectorOptions } from '../instance';
|
|
3
4
|
|
|
4
5
|
type HttpDetectorInit = (
|
|
@@ -13,6 +14,20 @@ type HttpDetectorDetect = (
|
|
|
13
14
|
detectionOrder?: unknown,
|
|
14
15
|
) => string | string[] | undefined;
|
|
15
16
|
|
|
17
|
+
type DetectionRequest = Parameters<typeof detectLanguageFromRequest>[0];
|
|
18
|
+
|
|
19
|
+
const isDetectionRequest = (request: unknown): request is DetectionRequest => {
|
|
20
|
+
if (!request || typeof request !== 'object') {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const { url, headers } = request as {
|
|
25
|
+
url?: unknown;
|
|
26
|
+
headers?: unknown;
|
|
27
|
+
};
|
|
28
|
+
return typeof url === 'string' && !!headers && typeof headers === 'object';
|
|
29
|
+
};
|
|
30
|
+
|
|
16
31
|
export const cacheUserLanguage = (
|
|
17
32
|
_i18nInstance: I18nInstance,
|
|
18
33
|
_language: string,
|
|
@@ -21,15 +36,18 @@ export const cacheUserLanguage = (
|
|
|
21
36
|
return;
|
|
22
37
|
};
|
|
23
38
|
|
|
24
|
-
/**
|
|
25
|
-
* Read language directly from storage (localStorage/cookie)
|
|
26
|
-
* Not available in Node.js environment, returns undefined
|
|
27
|
-
*/
|
|
28
39
|
export const readLanguageFromStorage = (
|
|
29
|
-
|
|
40
|
+
detectionOptions?: LanguageDetectorOptions,
|
|
41
|
+
request?: unknown,
|
|
42
|
+
languages: string[] = [],
|
|
30
43
|
): string | undefined => {
|
|
31
|
-
|
|
32
|
-
|
|
44
|
+
if (!isDetectionRequest(request)) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
detectLanguageFromRequest(request, languages, detectionOptions) ?? undefined
|
|
50
|
+
);
|
|
33
51
|
};
|
|
34
52
|
/**
|
|
35
53
|
* Register LanguageDetector plugin to i18n instance
|
|
@@ -30,6 +30,8 @@ export const useI18nextLanguageDetector = (i18nInstance: I18nInstance) => {
|
|
|
30
30
|
*/
|
|
31
31
|
export const readLanguageFromStorage = (
|
|
32
32
|
detectionOptions?: LanguageDetectorOptions,
|
|
33
|
+
_request?: unknown,
|
|
34
|
+
_languages?: string[],
|
|
33
35
|
): string | undefined => {
|
|
34
36
|
try {
|
|
35
37
|
const options = detectionOptions || {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createI18nRouterNavigation } from '@modern-js/i18n-runtime-extensions/router-navigation';
|
|
2
|
+
import {
|
|
3
|
+
I18nNavigationProvider,
|
|
4
|
+
type I18nRouterAdapter,
|
|
5
|
+
useNativeI18nRouterAdapter,
|
|
6
|
+
} from './routerAdapter';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The seam, and only the seam. The selected-router navigation adapter is a
|
|
10
|
+
* fork subsystem, so it lives in the fork-owned
|
|
11
|
+
* `@modern-js/i18n-runtime-extensions` and is attached here through the
|
|
12
|
+
* `NavigationProvider` extension point this package already exposes. The
|
|
13
|
+
* factory takes this package's own primitives as arguments, so the fork
|
|
14
|
+
* package never imports back into it and the dependency stays one-way.
|
|
15
|
+
*/
|
|
16
|
+
export const { I18nRouterNavigationProvider, useIntegratedRouterAdapter } =
|
|
17
|
+
createI18nRouterNavigation<I18nRouterAdapter>({
|
|
18
|
+
I18nNavigationProvider,
|
|
19
|
+
useNativeI18nRouterAdapter,
|
|
20
|
+
});
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { I18nNavigationContext } from '@modern-js/plugin-i18n/runtime/contexts';
|
|
1
2
|
import { isBrowser, RuntimeContext } from '@modern-js/runtime';
|
|
2
3
|
import { InternalRuntimeContext } from '@modern-js/runtime/context';
|
|
3
4
|
import type React from 'react';
|
|
4
|
-
import {
|
|
5
|
+
import { useContext } from 'react';
|
|
5
6
|
|
|
6
7
|
interface I18nRouterLocation {
|
|
7
8
|
pathname: string;
|
|
@@ -44,16 +45,6 @@ export interface I18nRouterAdapter {
|
|
|
44
45
|
};
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
const navigationContextKey = Symbol.for(
|
|
48
|
-
'@modern-js/plugin-i18n/runtime/I18nNavigationContext',
|
|
49
|
-
);
|
|
50
|
-
// Only the React context identity is shared. Its values remain provider-owned.
|
|
51
|
-
const contextStore = globalThis as typeof globalThis & {
|
|
52
|
-
[navigationContextKey]?: React.Context<I18nRouterAdapter | null>;
|
|
53
|
-
};
|
|
54
|
-
const I18nNavigationContext = (contextStore[navigationContextKey] ??=
|
|
55
|
-
createContext<I18nRouterAdapter | null>(null));
|
|
56
|
-
|
|
57
48
|
export const I18nNavigationProvider = ({
|
|
58
49
|
children,
|
|
59
50
|
value,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Upstream-owned runtime modules take the i18n contexts from this fork-owned
|
|
2
|
+
// module, which reaches them through the shared
|
|
3
|
+
// `@modern-js/plugin-i18n/runtime/contexts` request. That keeps the fork
|
|
4
|
+
// import edge out of the upstream-owned files.
|
|
5
|
+
export {
|
|
6
|
+
ModernI18nContext,
|
|
7
|
+
ReactI18nextProviderContext,
|
|
8
|
+
} from '@modern-js/plugin-i18n/runtime/contexts';
|
package/src/runtime/utils.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { isBrowser } from '@modern-js/runtime';
|
|
2
2
|
import { getGlobalBasename } from '@modern-js/runtime/context';
|
|
3
3
|
import { splitUrlTarget } from '@modern-js/runtime-utils/url';
|
|
4
|
-
import
|
|
4
|
+
import { isDefaultLocaleRedirectSkipPath } from '../shared/mappedUrlStrategy';
|
|
5
|
+
import { asI18nUrlStrategy, type I18nUrlStrategy } from '../shared/urlStrategy';
|
|
5
6
|
|
|
6
7
|
export { splitUrlTarget } from '@modern-js/runtime-utils/url';
|
|
7
8
|
|
|
@@ -60,8 +61,9 @@ export const buildLocalizedUrl = (
|
|
|
60
61
|
urlStrategy?: I18nUrlStrategy,
|
|
61
62
|
): string => {
|
|
62
63
|
const { pathname, search, hash } = splitUrlTarget(target);
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
const strategy = asI18nUrlStrategy(urlStrategy);
|
|
65
|
+
if (strategy) {
|
|
66
|
+
return `${strategy.localizePathname(pathname, language, languages)}${search}${hash}`;
|
|
65
67
|
}
|
|
66
68
|
const segments = pathname.split('/').filter(Boolean);
|
|
67
69
|
if (
|
|
@@ -121,7 +123,14 @@ export const shouldIgnoreRedirect = (
|
|
|
121
123
|
ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean),
|
|
122
124
|
urlStrategy?: I18nUrlStrategy,
|
|
123
125
|
): boolean => {
|
|
124
|
-
|
|
126
|
+
// Same native exclusions as the server: federation artifacts are never
|
|
127
|
+
// locale-redirected, with or without a configured strategy.
|
|
128
|
+
if (isDefaultLocaleRedirectSkipPath(pathname, languages)) {
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
if (
|
|
132
|
+
asI18nUrlStrategy(urlStrategy)?.shouldSkipRedirect?.(pathname, languages)
|
|
133
|
+
) {
|
|
125
134
|
return true;
|
|
126
135
|
}
|
|
127
136
|
if (!ignoreRedirectRoutes) {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { LocalisedUrlsOption } from '@modern-js/i18n-runtime-extensions';
|
|
2
|
+
import type { I18nUrlStrategy } from '../shared/urlStrategy';
|
|
3
|
+
import { createMappedUrlStrategy } from './mappedUrlStrategy';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Mapped locale URLs work without any extra plugin. `localeDetection.localisedUrls`
|
|
7
|
+
* alone is enough: the server derives the URL policy from the map it is given.
|
|
8
|
+
* An explicit `resolveUrlStrategy` still wins, so an integration can replace the
|
|
9
|
+
* policy wholesale.
|
|
10
|
+
*/
|
|
11
|
+
export const createDefaultUrlStrategy = (
|
|
12
|
+
localisedUrls?: LocalisedUrlsOption,
|
|
13
|
+
): I18nUrlStrategy => createMappedUrlStrategy(localisedUrls);
|
|
14
|
+
|
|
15
|
+
/** A non-empty map is the only configuration that needs a derived policy. */
|
|
16
|
+
export const resolveDefaultUrlStrategy = (
|
|
17
|
+
localisedUrls: unknown,
|
|
18
|
+
): I18nUrlStrategy | undefined => {
|
|
19
|
+
if (
|
|
20
|
+
!localisedUrls ||
|
|
21
|
+
typeof localisedUrls !== 'object' ||
|
|
22
|
+
Object.keys(localisedUrls).length === 0
|
|
23
|
+
) {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
return createDefaultUrlStrategy(localisedUrls as LocalisedUrlsOption);
|
|
27
|
+
};
|
package/src/server/index.ts
CHANGED
|
@@ -4,9 +4,10 @@ const { languageDetector } = honoPkg;
|
|
|
4
4
|
|
|
5
5
|
import type { Context, Next, ServerPlugin } from '@modern-js/server-runtime';
|
|
6
6
|
import type { LocaleDetectionOptions } from '../shared/type';
|
|
7
|
-
import type
|
|
7
|
+
import { asI18nUrlStrategy, type I18nUrlStrategy } from '../shared/urlStrategy';
|
|
8
8
|
import { getLocaleDetectionOptions } from '../shared/utils.js';
|
|
9
9
|
import { collectApiPrefixes, matchesApiPrefix } from './apiPrefix.js';
|
|
10
|
+
import { resolveDefaultUrlStrategy } from './defaultUrlStrategy.js';
|
|
10
11
|
import { convertToHonoLanguageDetectorOptions } from './detectorOptions.js';
|
|
11
12
|
import {
|
|
12
13
|
buildLocalizedUrl,
|
|
@@ -70,17 +71,11 @@ export const i18nServerPlugin = (options: I18nPluginOptions): ServerPlugin => ({
|
|
|
70
71
|
entryLocaleDetection,
|
|
71
72
|
'localisedUrls',
|
|
72
73
|
);
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
!urlStrategy
|
|
79
|
-
) {
|
|
80
|
-
throw new Error(
|
|
81
|
-
'Mapped locale URLs require a URL strategy. Use the i18n integration server plugin or supply resolveUrlStrategy.',
|
|
82
|
-
);
|
|
83
|
-
}
|
|
74
|
+
// `localeDetection.localisedUrls` is self-sufficient: when no explicit
|
|
75
|
+
// strategy is supplied the plugin derives one from the map itself.
|
|
76
|
+
const urlStrategy =
|
|
77
|
+
asI18nUrlStrategy(options.resolveUrlStrategy?.(entryName)) ??
|
|
78
|
+
resolveDefaultUrlStrategy(legacyLocalisedUrls);
|
|
84
79
|
const staticRoutePrefixes = options.staticRoutePrefixes;
|
|
85
80
|
const originUrlPath = route.urlPath;
|
|
86
81
|
const urlPath = originUrlPath.endsWith('/')
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The mapped-URL policy moved to `../shared` so the client runtime can derive
|
|
3
|
+
* the same policy the server does — SSR and a hydrated `<Link>` have to agree
|
|
4
|
+
* on the mapping or client navigation targets a path the server will not serve.
|
|
5
|
+
* Re-exported here to keep the server-side import path stable.
|
|
6
|
+
*/
|
|
7
|
+
export {
|
|
8
|
+
createMappedUrlStrategy,
|
|
9
|
+
resolveMappedUrlStrategy,
|
|
10
|
+
} from '../shared/mappedUrlStrategy';
|