@bleedingdev/modern-js-plugin-i18n 3.9.0-ultramodern.4 → 3.9.0-ultramodern.6
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 +0 -22
- package/dist/cjs/runtime/Link.js +19 -32
- package/dist/cjs/runtime/context.js +26 -61
- package/dist/cjs/runtime/contextHelpers.js +5 -5
- package/dist/cjs/runtime/core.js +17 -14
- package/dist/cjs/runtime/hooks.js +7 -8
- package/dist/cjs/runtime/localizedPaths.js +11 -9
- package/dist/cjs/runtime/providerComposition.js +37 -13
- package/dist/cjs/runtime/routerAdapter.js +35 -140
- package/dist/cjs/runtime/utils.js +17 -19
- package/dist/cjs/server/index.js +10 -7
- package/dist/cjs/server/redirectPolicy.js +36 -31
- package/dist/cjs/shared/urlStrategy.js +18 -0
- package/dist/esm/cli/index.mjs +0 -22
- package/dist/esm/runtime/Link.mjs +19 -32
- package/dist/esm/runtime/context.mjs +18 -62
- package/dist/esm/runtime/contextHelpers.mjs +5 -5
- package/dist/esm/runtime/core.mjs +8 -5
- package/dist/esm/runtime/hooks.mjs +7 -8
- package/dist/esm/runtime/localizedPaths.mjs +11 -9
- package/dist/esm/runtime/providerComposition.mjs +38 -14
- package/dist/esm/runtime/routerAdapter.mjs +30 -141
- package/dist/esm/runtime/utils.mjs +15 -17
- package/dist/esm/server/index.mjs +10 -7
- package/dist/esm/server/redirectPolicy.mjs +29 -24
- package/dist/esm/shared/urlStrategy.mjs +0 -0
- package/dist/esm-node/cli/index.mjs +0 -22
- package/dist/esm-node/runtime/Link.mjs +19 -32
- package/dist/esm-node/runtime/context.mjs +18 -62
- package/dist/esm-node/runtime/contextHelpers.mjs +5 -5
- package/dist/esm-node/runtime/core.mjs +8 -5
- package/dist/esm-node/runtime/hooks.mjs +7 -8
- package/dist/esm-node/runtime/localizedPaths.mjs +11 -9
- package/dist/esm-node/runtime/providerComposition.mjs +38 -14
- package/dist/esm-node/runtime/routerAdapter.mjs +30 -141
- package/dist/esm-node/runtime/utils.mjs +15 -17
- package/dist/esm-node/server/index.mjs +10 -7
- package/dist/esm-node/server/redirectPolicy.mjs +29 -24
- package/dist/esm-node/shared/urlStrategy.mjs +1 -0
- package/dist/types/runtime/context.d.ts +8 -11
- package/dist/types/runtime/contextHelpers.d.ts +2 -2
- package/dist/types/runtime/core.d.ts +11 -2
- package/dist/types/runtime/hooks.d.ts +4 -5
- package/dist/types/runtime/localizedPaths.d.ts +4 -4
- package/dist/types/runtime/providerComposition.d.ts +13 -1
- package/dist/types/runtime/routerAdapter.d.ts +23 -6
- package/dist/types/runtime/utils.d.ts +4 -12
- package/dist/types/server/index.d.ts +2 -0
- package/dist/types/server/redirectPolicy.d.ts +5 -13
- package/dist/types/shared/type.d.ts +0 -14
- package/dist/types/shared/urlStrategy.d.ts +7 -0
- package/package.json +21 -11
- package/rstest.config.mts +2 -8
- package/src/cli/index.ts +1 -43
- package/src/runtime/Link.tsx +20 -40
- package/src/runtime/context.tsx +34 -133
- package/src/runtime/contextHelpers.ts +7 -5
- package/src/runtime/core.tsx +26 -6
- package/src/runtime/hooks.ts +14 -9
- package/src/runtime/localizedPaths.ts +24 -19
- package/src/runtime/providerComposition.tsx +71 -23
- package/src/runtime/routerAdapter.tsx +73 -338
- package/src/runtime/utils.ts +40 -31
- package/src/server/index.ts +44 -9
- package/src/server/redirectPolicy.ts +70 -70
- package/src/shared/type.ts +0 -14
- package/src/shared/urlStrategy.ts +11 -0
- package/tests/federatedI18nBoundaryResolver.test.tsx +71 -0
- package/tests/i18nUtils.test.ts +31 -65
- package/tests/link.test.tsx +13 -511
- package/tests/linkTypes.test.ts +12 -3
- package/tests/localisedUrlRewriteMatrix.fork.test.ts +4 -96
- package/tests/localisedUrls.test.ts +7 -216
- package/tests/reactI18nextRuntimeBoundary.test.ts +1 -82
- package/tests/redirectPolicy.test.ts +39 -30
- package/tests/routerAdapter.test.tsx +95 -1055
- package/tests/federatedI18nBoundary.client.test.tsx +0 -129
- package/tests/federatedI18nBoundary.test.tsx +0 -132
- package/tests/peerCohort.test.ts +0 -25
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { ServerPlugin } from '@modern-js/server-runtime';
|
|
2
2
|
import type { LocaleDetectionOptions } from '../shared/type.js';
|
|
3
|
+
import type { I18nUrlStrategy } from '../shared/urlStrategy.js';
|
|
3
4
|
export { collectApiPrefixes, matchesApiPrefix } from './apiPrefix.js';
|
|
4
5
|
export interface I18nPluginOptions {
|
|
5
6
|
localeDetection: LocaleDetectionOptions;
|
|
6
7
|
staticRoutePrefixes: string[];
|
|
8
|
+
resolveUrlStrategy?: (entryName: string) => I18nUrlStrategy | undefined;
|
|
7
9
|
}
|
|
8
10
|
export declare const i18nServerPlugin: (options: I18nPluginOptions) => ServerPlugin;
|
|
9
11
|
export default i18nServerPlugin;
|
|
@@ -1,22 +1,14 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { I18nUrlStrategy } from '../shared/urlStrategy.js';
|
|
2
2
|
interface LocaleRedirectRequest {
|
|
3
3
|
url: string;
|
|
4
|
-
header
|
|
4
|
+
header?: () => {
|
|
5
5
|
host?: string;
|
|
6
6
|
};
|
|
7
7
|
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
*/
|
|
11
|
-
export declare const shouldIgnoreRedirect: (pathname: string, urlPath: string, ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean)) => boolean;
|
|
12
|
-
/**
|
|
13
|
-
* Check if pathname is a static or federation resource request.
|
|
14
|
-
*
|
|
15
|
-
* This includes configured staticRoutePrefixes, the shared default skip policy
|
|
16
|
-
* from ADR-0002, and language-prefixed variants such as /en/static/app.js.
|
|
17
|
-
*/
|
|
8
|
+
export declare const shouldIgnoreRedirect: (pathname: string, urlPath: string, ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean), urlStrategy?: I18nUrlStrategy, languages?: string[]) => boolean;
|
|
9
|
+
/** Native configured/static exclusions remain in effect for every strategy. */
|
|
18
10
|
export declare const isStaticResourceRequest: (pathname: string, staticRoutePrefixes: string[], languages?: string[]) => boolean;
|
|
19
11
|
export declare const getLanguageFromPath: (req: LocaleRedirectRequest, urlPath: string, languages: string[]) => string | null;
|
|
20
|
-
export declare const buildLocalizedUrl: (req: LocaleRedirectRequest, urlPath: string, language: string, languages: string[],
|
|
12
|
+
export declare const buildLocalizedUrl: (req: LocaleRedirectRequest, urlPath: string, language: string, languages: string[], urlStrategy?: I18nUrlStrategy) => string;
|
|
21
13
|
export declare const createLocaleRedirectResponse: (location: string) => Response;
|
|
22
14
|
export {};
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { LocalisedUrlsOption } from '@modern-js/i18n-runtime-extensions';
|
|
2
1
|
import type { LanguageDetectorOptions, Resources } from '../runtime/i18n/instance.js';
|
|
3
2
|
export interface BaseLocaleDetectionOptions {
|
|
4
3
|
localePathRedirect?: boolean;
|
|
@@ -7,19 +6,6 @@ export interface BaseLocaleDetectionOptions {
|
|
|
7
6
|
fallbackLanguage?: string;
|
|
8
7
|
detection?: LanguageDetectorOptions;
|
|
9
8
|
ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean);
|
|
10
|
-
/**
|
|
11
|
-
* Enables localised pathnames in addition to the locale prefix.
|
|
12
|
-
*
|
|
13
|
-
* - non-empty object: map canonical route paths to every configured
|
|
14
|
-
* language; route generation then validates that every localisable route
|
|
15
|
-
* path has entries for all configured languages.
|
|
16
|
-
* - absent / `false` / `true` / empty object: keep only locale-prefix
|
|
17
|
-
* behavior (`/en/about`).
|
|
18
|
-
*
|
|
19
|
-
* Strictly opt-in: without a map, `localePathRedirect` + `languages` behave
|
|
20
|
-
* exactly like upstream Modern.js.
|
|
21
|
-
*/
|
|
22
|
-
localisedUrls?: LocalisedUrlsOption;
|
|
23
9
|
}
|
|
24
10
|
export interface LocaleDetectionOptions extends BaseLocaleDetectionOptions {
|
|
25
11
|
localeDetectionByEntry?: Record<string, BaseLocaleDetectionOptions>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Per-plugin URL policy. Search and hash remain owned by native URL handling. */
|
|
2
|
+
export interface I18nUrlStrategy {
|
|
3
|
+
localizePathname(pathname: string, language: string, languages: readonly string[]): string;
|
|
4
|
+
canonicalPathname(pathname: string, languages: readonly string[]): string;
|
|
5
|
+
/** Adds exclusions; it cannot enable redirects excluded by native rules. */
|
|
6
|
+
shouldSkipRedirect?(pathname: string, languages: readonly string[]): boolean;
|
|
7
|
+
}
|
package/package.json
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"modern",
|
|
18
18
|
"modern.js"
|
|
19
19
|
],
|
|
20
|
-
"version": "3.9.0-ultramodern.
|
|
20
|
+
"version": "3.9.0-ultramodern.6",
|
|
21
21
|
"engines": {
|
|
22
22
|
"node": ">=20"
|
|
23
23
|
},
|
|
@@ -80,6 +80,14 @@
|
|
|
80
80
|
"./i18n": {
|
|
81
81
|
"types": "./dist/types/runtime/i18n/index.d.ts",
|
|
82
82
|
"default": "./dist/esm/runtime/i18n/index.mjs"
|
|
83
|
+
},
|
|
84
|
+
"./config": {
|
|
85
|
+
"types": "./dist/types/shared/utils.d.ts",
|
|
86
|
+
"node": {
|
|
87
|
+
"import": "./dist/esm-node/shared/utils.mjs",
|
|
88
|
+
"require": "./dist/cjs/shared/utils.js"
|
|
89
|
+
},
|
|
90
|
+
"default": "./dist/esm/shared/utils.mjs"
|
|
83
91
|
}
|
|
84
92
|
},
|
|
85
93
|
"typesVersions": {
|
|
@@ -107,17 +115,19 @@
|
|
|
107
115
|
],
|
|
108
116
|
"i18n": [
|
|
109
117
|
"./dist/types/runtime/i18n/index.d.ts"
|
|
118
|
+
],
|
|
119
|
+
"config": [
|
|
120
|
+
"./dist/types/shared/utils.d.ts"
|
|
110
121
|
]
|
|
111
122
|
}
|
|
112
123
|
},
|
|
113
124
|
"dependencies": {
|
|
114
|
-
"@modern-js/
|
|
115
|
-
"@modern-js/
|
|
116
|
-
"@modern-js/
|
|
117
|
-
"@modern-js/server-
|
|
118
|
-
"@modern-js/
|
|
119
|
-
"@modern-js/
|
|
120
|
-
"@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.9.0-ultramodern.4",
|
|
125
|
+
"@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.9.0-ultramodern.6",
|
|
126
|
+
"@modern-js/runtime-utils": "npm:@bleedingdev/modern-js-runtime-utils@3.9.0-ultramodern.6",
|
|
127
|
+
"@modern-js/server-core": "npm:@bleedingdev/modern-js-server-core@3.9.0-ultramodern.6",
|
|
128
|
+
"@modern-js/server-runtime": "npm:@bleedingdev/modern-js-server-runtime@3.9.0-ultramodern.6",
|
|
129
|
+
"@modern-js/types": "npm:@bleedingdev/modern-js-types@3.9.0-ultramodern.6",
|
|
130
|
+
"@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.9.0-ultramodern.6",
|
|
121
131
|
"@swc/helpers": "^0.5.23",
|
|
122
132
|
"i18next-browser-languagedetector": "^8.2.1",
|
|
123
133
|
"i18next-chained-backend": "^5.0.6",
|
|
@@ -126,7 +136,7 @@
|
|
|
126
136
|
"i18next-http-middleware": "^3.9.8"
|
|
127
137
|
},
|
|
128
138
|
"peerDependencies": {
|
|
129
|
-
"@modern-js/runtime": "3.9.0-ultramodern.
|
|
139
|
+
"@modern-js/runtime": "3.9.0-ultramodern.6",
|
|
130
140
|
"i18next": ">=25.7.4",
|
|
131
141
|
"react": "^19.2.8",
|
|
132
142
|
"react-dom": "^19.2.8",
|
|
@@ -141,8 +151,8 @@
|
|
|
141
151
|
}
|
|
142
152
|
},
|
|
143
153
|
"devDependencies": {
|
|
144
|
-
"@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.9.0-ultramodern.
|
|
145
|
-
"@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.9.0-ultramodern.
|
|
154
|
+
"@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.9.0-ultramodern.6",
|
|
155
|
+
"@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.9.0-ultramodern.6",
|
|
146
156
|
"@rslib/core": "1.0.0",
|
|
147
157
|
"@types/node": "^26.4.1",
|
|
148
158
|
"@typescript/native-preview": "7.0.0-dev.20260707.2",
|
package/rstest.config.mts
CHANGED
|
@@ -29,24 +29,18 @@ export default {
|
|
|
29
29
|
include: [
|
|
30
30
|
'tests/i18nUtils.test.ts',
|
|
31
31
|
'tests/localisedUrls.test.ts',
|
|
32
|
-
'tests/localisedUrlRewriteMatrix.fork.test.ts',
|
|
33
32
|
'tests/linkTypes.test.ts',
|
|
34
33
|
'tests/backendDefaults.test.ts',
|
|
35
34
|
'tests/redirectPolicy.test.ts',
|
|
36
|
-
'tests/peerCohort.test.ts',
|
|
37
35
|
'tests/reactI18nextRuntimeBoundary.test.ts',
|
|
38
|
-
'tests/
|
|
36
|
+
'tests/federatedI18nBoundaryResolver.test.tsx',
|
|
39
37
|
],
|
|
40
38
|
extends: commonConfig,
|
|
41
39
|
}),
|
|
42
40
|
withTestPreset({
|
|
43
41
|
name: 'plugin-i18n-client',
|
|
44
42
|
testEnvironment: 'happy-dom',
|
|
45
|
-
include: [
|
|
46
|
-
'tests/routerAdapter.test.tsx',
|
|
47
|
-
'tests/link.test.tsx',
|
|
48
|
-
'tests/federatedI18nBoundary.client.test.tsx',
|
|
49
|
-
],
|
|
43
|
+
include: ['tests/routerAdapter.test.tsx', 'tests/link.test.tsx'],
|
|
50
44
|
extends: commonConfig,
|
|
51
45
|
}),
|
|
52
46
|
],
|
package/src/cli/index.ts
CHANGED
|
@@ -1,16 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import type { AppTools, CliPlugin } from '@modern-js/app-tools';
|
|
4
|
-
import {
|
|
5
|
-
applyLocalisedUrlsToRoutes,
|
|
6
|
-
resolveLocalisedUrlsConfig,
|
|
7
|
-
} from '@modern-js/i18n-runtime-extensions';
|
|
8
4
|
import { getPublicDirRoutePrefixes } from '@modern-js/server-core';
|
|
9
|
-
import type {
|
|
10
|
-
Entrypoint,
|
|
11
|
-
NestedRouteForCli,
|
|
12
|
-
PageRoute,
|
|
13
|
-
} from '@modern-js/types';
|
|
5
|
+
import type { Entrypoint } from '@modern-js/types';
|
|
14
6
|
import type { BackendOptions, LocaleDetectionOptions } from '../shared/type';
|
|
15
7
|
import { getBackendOptions, getLocaleDetectionOptions } from '../shared/utils';
|
|
16
8
|
import { applyDetectedBackendPaths, detectLocalesDirectory } from './locales';
|
|
@@ -144,40 +136,6 @@ export const i18nPlugin = (
|
|
|
144
136
|
};
|
|
145
137
|
});
|
|
146
138
|
|
|
147
|
-
api.modifyFileSystemRoutes(({ entrypoint, routes }) => {
|
|
148
|
-
if (!localeDetection) {
|
|
149
|
-
return { entrypoint, routes };
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const localeDetectionOptions = getLocaleDetectionOptions(
|
|
153
|
-
entrypoint.entryName,
|
|
154
|
-
localeDetection,
|
|
155
|
-
);
|
|
156
|
-
const {
|
|
157
|
-
localePathRedirect,
|
|
158
|
-
languages = [],
|
|
159
|
-
localisedUrls,
|
|
160
|
-
} = localeDetectionOptions;
|
|
161
|
-
|
|
162
|
-
if (!localePathRedirect || languages.length === 0) {
|
|
163
|
-
return { entrypoint, routes };
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const localisedUrlsConfig = resolveLocalisedUrlsConfig(localisedUrls);
|
|
167
|
-
if (!localisedUrlsConfig.enabled) {
|
|
168
|
-
return { entrypoint, routes };
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
return {
|
|
172
|
-
entrypoint,
|
|
173
|
-
routes: applyLocalisedUrlsToRoutes(
|
|
174
|
-
routes as (NestedRouteForCli | PageRoute)[],
|
|
175
|
-
languages,
|
|
176
|
-
localisedUrlsConfig.map,
|
|
177
|
-
) as (NestedRouteForCli | PageRoute)[],
|
|
178
|
-
};
|
|
179
|
-
});
|
|
180
|
-
|
|
181
139
|
api._internalServerPlugins(({ plugins }) => {
|
|
182
140
|
const { serverRoutes, metaName } = api.getAppContext();
|
|
183
141
|
const normalizedConfig = api.getNormalizedConfig();
|
package/src/runtime/Link.tsx
CHANGED
|
@@ -104,11 +104,11 @@ export const Link = <TTo extends string = string>(
|
|
|
104
104
|
} = props as LinkBaseProps & { to: string; params?: LinkParams };
|
|
105
105
|
|
|
106
106
|
const adapter = useI18nRouterAdapter();
|
|
107
|
-
const { language, supportedLanguages,
|
|
107
|
+
const { language, supportedLanguages, urlStrategy } = useModernI18n();
|
|
108
108
|
|
|
109
109
|
const config: LocalizedPathsConfig = {
|
|
110
110
|
languages: supportedLanguages,
|
|
111
|
-
|
|
111
|
+
urlStrategy,
|
|
112
112
|
};
|
|
113
113
|
|
|
114
114
|
const isExternal = EXTERNAL_TARGET_RE.test(to);
|
|
@@ -139,7 +139,7 @@ export const Link = <TTo extends string = string>(
|
|
|
139
139
|
interpolated,
|
|
140
140
|
language,
|
|
141
141
|
supportedLanguages,
|
|
142
|
-
|
|
142
|
+
urlStrategy,
|
|
143
143
|
);
|
|
144
144
|
const hash = hashProp ?? (hashFromTo ? hashFromTo.slice(1) : '');
|
|
145
145
|
const { searchString, searchObject } = normalizeSearch(
|
|
@@ -164,7 +164,7 @@ export const Link = <TTo extends string = string>(
|
|
|
164
164
|
isBareHash,
|
|
165
165
|
language,
|
|
166
166
|
supportedLanguages,
|
|
167
|
-
|
|
167
|
+
urlStrategy,
|
|
168
168
|
]);
|
|
169
169
|
|
|
170
170
|
const isActive = useMemo(() => {
|
|
@@ -190,7 +190,7 @@ export const Link = <TTo extends string = string>(
|
|
|
190
190
|
adapter.location,
|
|
191
191
|
activeOptions?.exact,
|
|
192
192
|
supportedLanguages,
|
|
193
|
-
|
|
193
|
+
urlStrategy,
|
|
194
194
|
]);
|
|
195
195
|
|
|
196
196
|
const resolvedActiveProps = splitActiveProps(isActive, activeProps);
|
|
@@ -214,7 +214,7 @@ export const Link = <TTo extends string = string>(
|
|
|
214
214
|
);
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
-
const { Link: RouterLink, hasRouter
|
|
217
|
+
const { Link: RouterLink, hasRouter } = adapter;
|
|
218
218
|
|
|
219
219
|
if (!hasRouter || !RouterLink) {
|
|
220
220
|
const { replace: _replace, ...anchorProps } = rest;
|
|
@@ -252,43 +252,23 @@ export const Link = <TTo extends string = string>(
|
|
|
252
252
|
...(activeStyle as React.CSSProperties | undefined),
|
|
253
253
|
};
|
|
254
254
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
// TanStack navigation.
|
|
269
|
-
return (
|
|
270
|
-
<RouterLink
|
|
271
|
-
to={target.localizedPathname}
|
|
272
|
-
{...(target.searchObject ? { search: target.searchObject } : {})}
|
|
273
|
-
{...(target.hash ? { hash: target.hash } : {})}
|
|
274
|
-
{...(hashScrollIntoView === undefined ? {} : { hashScrollIntoView })}
|
|
275
|
-
{...(tanstackPreload === undefined ? {} : { preload: tanstackPreload })}
|
|
276
|
-
{...rest}
|
|
277
|
-
{...activeRest}
|
|
278
|
-
{...activeAttributes}
|
|
279
|
-
className={mergedClassName}
|
|
280
|
-
style={mergedStyle}
|
|
281
|
-
>
|
|
282
|
-
{children}
|
|
283
|
-
</RouterLink>
|
|
284
|
-
);
|
|
285
|
-
}
|
|
255
|
+
const routerProps = adapter.createLinkProps?.({
|
|
256
|
+
pathname: target.localizedPathname,
|
|
257
|
+
href: target.href,
|
|
258
|
+
search: target.searchObject,
|
|
259
|
+
hash: target.hash,
|
|
260
|
+
hashScrollIntoView,
|
|
261
|
+
prefetch,
|
|
262
|
+
preload,
|
|
263
|
+
}) ?? {
|
|
264
|
+
to: target.href,
|
|
265
|
+
...(prefetch === undefined ? {} : { prefetch }),
|
|
266
|
+
...(preload === undefined ? {} : { preload }),
|
|
267
|
+
};
|
|
286
268
|
|
|
287
269
|
return (
|
|
288
270
|
<RouterLink
|
|
289
|
-
|
|
290
|
-
{...(prefetch === undefined ? {} : { prefetch })}
|
|
291
|
-
{...(preload === undefined ? {} : { preload })}
|
|
271
|
+
{...routerProps}
|
|
292
272
|
{...rest}
|
|
293
273
|
{...activeRest}
|
|
294
274
|
{...activeAttributes}
|
package/src/runtime/context.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { RuntimeComponentResolverContext } from '@modern-js/runtime/context';
|
|
2
2
|
import type { ComponentType, FC, ReactNode } from 'react';
|
|
3
3
|
import {
|
|
4
4
|
createContext,
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
useEffect,
|
|
8
8
|
useMemo,
|
|
9
9
|
} from 'react';
|
|
10
|
+
import type { I18nUrlStrategy } from '../shared/urlStrategy';
|
|
10
11
|
import {
|
|
11
12
|
changeModernI18nLanguage,
|
|
12
13
|
getPathLanguage,
|
|
@@ -16,10 +17,12 @@ import {
|
|
|
16
17
|
} from './contextHelpers';
|
|
17
18
|
import type { I18nInstance } from './i18n';
|
|
18
19
|
import type { Resources } from './i18n/instance';
|
|
19
|
-
|
|
20
|
+
|
|
20
21
|
import { useI18nRouterAdapter } from './routerAdapter';
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
export { getActualI18nextInstance } from './i18n/instance';
|
|
24
|
+
|
|
25
|
+
export interface ModernI18nContextValue {
|
|
23
26
|
language: string;
|
|
24
27
|
i18nInstance: I18nInstance;
|
|
25
28
|
// Plugin configuration for useModernI18n hook
|
|
@@ -27,7 +30,7 @@ interface ModernI18nContextValue {
|
|
|
27
30
|
languages?: string[];
|
|
28
31
|
localePathRedirect?: boolean;
|
|
29
32
|
ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean);
|
|
30
|
-
|
|
33
|
+
urlStrategy?: I18nUrlStrategy;
|
|
31
34
|
// Callback to update language in context
|
|
32
35
|
updateLanguage?: (newLang: string) => void;
|
|
33
36
|
synchronizeLanguage?: (newLang: string) => void;
|
|
@@ -50,16 +53,15 @@ const getGlobalContext = <T,>(key: symbol, defaultValue: T) => {
|
|
|
50
53
|
return globalStore[key];
|
|
51
54
|
};
|
|
52
55
|
|
|
53
|
-
const ModernI18nContext =
|
|
54
|
-
modernI18nContextKey,
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
);
|
|
56
|
+
export const ModernI18nContext =
|
|
57
|
+
getGlobalContext<ModernI18nContextValue | null>(modernI18nContextKey, null);
|
|
58
|
+
export const ReactI18nextProviderContext =
|
|
59
|
+
getGlobalContext<ComponentType<any> | null>(
|
|
60
|
+
reactI18nextProviderContextKey,
|
|
61
|
+
null,
|
|
62
|
+
);
|
|
61
63
|
|
|
62
|
-
interface ModernI18nProviderProps {
|
|
64
|
+
export interface ModernI18nProviderProps {
|
|
63
65
|
children: ReactNode;
|
|
64
66
|
i18nextProvider?: ComponentType<any> | null;
|
|
65
67
|
value: ModernI18nContextValue;
|
|
@@ -92,123 +94,22 @@ export interface FederatedI18nBoundaryProps {
|
|
|
92
94
|
supportedLanguages?: string[];
|
|
93
95
|
}
|
|
94
96
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
* resources used below this boundary.
|
|
99
|
-
*/
|
|
100
|
-
export const FederatedI18nBoundary: FC<FederatedI18nBoundaryProps> = ({
|
|
101
|
-
children,
|
|
102
|
-
defaultNamespace,
|
|
103
|
-
fallbackLanguage,
|
|
104
|
-
resources,
|
|
105
|
-
supportedLanguages,
|
|
106
|
-
}) => {
|
|
107
|
-
const parent = useContext(ModernI18nContext);
|
|
108
|
-
const I18nextProvider = useContext(ReactI18nextProviderContext);
|
|
109
|
-
if (!parent) {
|
|
110
|
-
throw new Error(
|
|
111
|
-
'FederatedI18nBoundary must be used within ModernI18nProvider',
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const languages =
|
|
116
|
-
supportedLanguages ?? parent.languages ?? Object.keys(resources);
|
|
117
|
-
const scopedInstance = useMemo(() => {
|
|
118
|
-
const parentInstance = getActualI18nextInstance(parent.i18nInstance);
|
|
119
|
-
const clone = parentInstance.cloneInstance?.({
|
|
120
|
-
defaultNS: defaultNamespace,
|
|
121
|
-
fallbackLng: fallbackLanguage ?? languages[0] ?? parent.language,
|
|
122
|
-
forkResourceStore: true,
|
|
123
|
-
initImmediate: false,
|
|
124
|
-
lng: parent.language,
|
|
125
|
-
ns: [defaultNamespace],
|
|
126
|
-
resources,
|
|
127
|
-
supportedLngs: languages,
|
|
128
|
-
});
|
|
129
|
-
if (!clone) {
|
|
130
|
-
throw new Error(
|
|
131
|
-
'FederatedI18nBoundary requires an i18n instance with cloneInstance support',
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
const resourceStore = clone.store;
|
|
135
|
-
const parentResourceStore = parentInstance.store;
|
|
136
|
-
const sharesNestedResourceState = Object.entries(
|
|
137
|
-
resourceStore?.data ?? {},
|
|
138
|
-
).some(([language, namespaces]) => {
|
|
139
|
-
const parentNamespaces = parentResourceStore?.data?.[language];
|
|
140
|
-
return (
|
|
141
|
-
namespaces === parentNamespaces ||
|
|
142
|
-
Object.entries(namespaces).some(
|
|
143
|
-
([namespace, resource]) =>
|
|
144
|
-
resource !== null &&
|
|
145
|
-
typeof resource === 'object' &&
|
|
146
|
-
resource === parentNamespaces?.[namespace],
|
|
147
|
-
)
|
|
148
|
-
);
|
|
149
|
-
});
|
|
150
|
-
if (
|
|
151
|
-
clone === parentInstance ||
|
|
152
|
-
resourceStore === parentResourceStore ||
|
|
153
|
-
resourceStore?.data === parentResourceStore?.data ||
|
|
154
|
-
sharesNestedResourceState
|
|
155
|
-
) {
|
|
156
|
-
throw new Error(
|
|
157
|
-
'FederatedI18nBoundary cloneInstance did not isolate the host resource store',
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
if (
|
|
161
|
-
!resourceStore?.addResourceBundle ||
|
|
162
|
-
!clone.removeResourceBundle ||
|
|
163
|
-
!resourceStore.data
|
|
164
|
-
) {
|
|
165
|
-
throw new Error(
|
|
166
|
-
'FederatedI18nBoundary requires an isolated mutable i18n resource store',
|
|
167
|
-
);
|
|
168
|
-
}
|
|
169
|
-
for (const [language, namespaces] of Object.entries(resourceStore.data)) {
|
|
170
|
-
for (const namespace of Object.keys(namespaces)) {
|
|
171
|
-
clone.removeResourceBundle(language, namespace);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
for (const [language, namespaces] of Object.entries(resources)) {
|
|
175
|
-
for (const [namespace, resource] of Object.entries(namespaces)) {
|
|
176
|
-
resourceStore.addResourceBundle(
|
|
177
|
-
language,
|
|
178
|
-
namespace,
|
|
179
|
-
resource as Record<string, string>,
|
|
180
|
-
true,
|
|
181
|
-
true,
|
|
182
|
-
);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
return clone;
|
|
186
|
-
}, [
|
|
187
|
-
defaultNamespace,
|
|
188
|
-
fallbackLanguage,
|
|
189
|
-
languages,
|
|
190
|
-
parent.i18nInstance,
|
|
191
|
-
parent.language,
|
|
192
|
-
resources,
|
|
193
|
-
]);
|
|
194
|
-
const value = useMemo(
|
|
195
|
-
() => ({
|
|
196
|
-
...parent,
|
|
197
|
-
i18nInstance: scopedInstance,
|
|
198
|
-
language: parent.language,
|
|
199
|
-
languages,
|
|
200
|
-
}),
|
|
201
|
-
[languages, parent, scopedInstance],
|
|
97
|
+
const MissingFederatedI18nBoundary: FC<FederatedI18nBoundaryProps> = () => {
|
|
98
|
+
throw new Error(
|
|
99
|
+
'FederatedI18nBoundary requires the @modern-js/i18n-integration runtime plugin',
|
|
202
100
|
);
|
|
101
|
+
};
|
|
203
102
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
103
|
+
export const FederatedI18nBoundary: FC<FederatedI18nBoundaryProps> = props => {
|
|
104
|
+
const resolveComponent = useContext(RuntimeComponentResolverContext);
|
|
105
|
+
const Boundary =
|
|
106
|
+
resolveComponent?.(MissingFederatedI18nBoundary, {
|
|
107
|
+
name: 'i18n.FederatedI18nBoundary',
|
|
108
|
+
}) ?? MissingFederatedI18nBoundary;
|
|
109
|
+
if (Boundary === FederatedI18nBoundary) {
|
|
110
|
+
throw new Error('FederatedI18nBoundary resolver returned its own wrapper');
|
|
111
|
+
}
|
|
112
|
+
return <Boundary {...props} />;
|
|
212
113
|
};
|
|
213
114
|
|
|
214
115
|
export interface UseModernI18nReturn<
|
|
@@ -219,7 +120,7 @@ export interface UseModernI18nReturn<
|
|
|
219
120
|
t: (key: string | string[], ...args: any[]) => string;
|
|
220
121
|
i18nInstance: TInstance;
|
|
221
122
|
supportedLanguages: string[];
|
|
222
|
-
|
|
123
|
+
urlStrategy?: I18nUrlStrategy;
|
|
223
124
|
isLanguageSupported: (lang: string) => boolean;
|
|
224
125
|
// Indicates whether translation resources for current language are ready
|
|
225
126
|
isResourcesReady: boolean;
|
|
@@ -258,7 +159,7 @@ export const useModernI18n = <
|
|
|
258
159
|
languages,
|
|
259
160
|
localePathRedirect,
|
|
260
161
|
ignoreRedirectRoutes,
|
|
261
|
-
|
|
162
|
+
urlStrategy,
|
|
262
163
|
updateLanguage,
|
|
263
164
|
synchronizeLanguage,
|
|
264
165
|
} = context;
|
|
@@ -295,7 +196,7 @@ export const useModernI18n = <
|
|
|
295
196
|
updateLanguage,
|
|
296
197
|
localePathRedirect,
|
|
297
198
|
ignoreRedirectRoutes,
|
|
298
|
-
|
|
199
|
+
urlStrategy,
|
|
299
200
|
languages,
|
|
300
201
|
hasRouter,
|
|
301
202
|
navigate,
|
|
@@ -306,7 +207,7 @@ export const useModernI18n = <
|
|
|
306
207
|
updateLanguage,
|
|
307
208
|
localePathRedirect,
|
|
308
209
|
ignoreRedirectRoutes,
|
|
309
|
-
|
|
210
|
+
urlStrategy,
|
|
310
211
|
languages,
|
|
311
212
|
hasRouter,
|
|
312
213
|
navigate,
|
|
@@ -341,7 +242,7 @@ export const useModernI18n = <
|
|
|
341
242
|
// narrows to the concrete instance type via the TInstance type argument.
|
|
342
243
|
i18nInstance: i18nInstance as TInstance,
|
|
343
244
|
supportedLanguages: languages || [],
|
|
344
|
-
|
|
245
|
+
urlStrategy,
|
|
345
246
|
isLanguageSupported,
|
|
346
247
|
isResourcesReady,
|
|
347
248
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { LocalisedUrlsOption } from '@modern-js/i18n-runtime-extensions';
|
|
2
1
|
import { isBrowser } from '@modern-js/runtime';
|
|
2
|
+
import type { I18nUrlStrategy } from '../shared/urlStrategy';
|
|
3
3
|
import type { I18nInstance } from './i18n';
|
|
4
4
|
import type { SdkBackend } from './i18n/backend/sdk-backend';
|
|
5
5
|
import { cacheUserLanguage } from './i18n/detection';
|
|
@@ -66,7 +66,7 @@ interface ChangeModernI18nLanguageOptions {
|
|
|
66
66
|
updateLanguage?: (newLang: string) => void;
|
|
67
67
|
localePathRedirect?: boolean;
|
|
68
68
|
ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean);
|
|
69
|
-
|
|
69
|
+
urlStrategy?: I18nUrlStrategy;
|
|
70
70
|
languages?: string[];
|
|
71
71
|
hasRouter: boolean;
|
|
72
72
|
navigate?: Navigate | null;
|
|
@@ -82,7 +82,7 @@ export async function changeModernI18nLanguage(
|
|
|
82
82
|
updateLanguage,
|
|
83
83
|
localePathRedirect,
|
|
84
84
|
ignoreRedirectRoutes,
|
|
85
|
-
|
|
85
|
+
urlStrategy,
|
|
86
86
|
languages,
|
|
87
87
|
hasRouter,
|
|
88
88
|
navigate,
|
|
@@ -123,13 +123,14 @@ export async function changeModernI18nLanguage(
|
|
|
123
123
|
relativePath,
|
|
124
124
|
languages || [],
|
|
125
125
|
ignoreRedirectRoutes,
|
|
126
|
+
urlStrategy,
|
|
126
127
|
)
|
|
127
128
|
) {
|
|
128
129
|
const newPath = buildLocalizedUrl(
|
|
129
130
|
relativePath,
|
|
130
131
|
newLang,
|
|
131
132
|
languages || [],
|
|
132
|
-
|
|
133
|
+
urlStrategy,
|
|
133
134
|
);
|
|
134
135
|
const newUrl = entryPath + newPath + location.search + location.hash;
|
|
135
136
|
|
|
@@ -155,13 +156,14 @@ export async function changeModernI18nLanguage(
|
|
|
155
156
|
relativePath,
|
|
156
157
|
languages || [],
|
|
157
158
|
ignoreRedirectRoutes,
|
|
159
|
+
urlStrategy,
|
|
158
160
|
)
|
|
159
161
|
) {
|
|
160
162
|
const newPath = buildLocalizedUrl(
|
|
161
163
|
relativePath,
|
|
162
164
|
newLang,
|
|
163
165
|
languages || [],
|
|
164
|
-
|
|
166
|
+
urlStrategy,
|
|
165
167
|
);
|
|
166
168
|
const newUrl =
|
|
167
169
|
entryPath + newPath + window.location.search + window.location.hash;
|