@bleedingdev/modern-js-plugin-i18n 3.5.0-ultramodern.9 → 3.5.0-ultramodern.90
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 +2 -1
- package/dist/cjs/cli/locales.js +15 -15
- package/dist/cjs/runtime/Link.js +8 -89
- package/dist/cjs/runtime/context.js +78 -91
- package/dist/cjs/runtime/contextHelpers.js +143 -0
- package/dist/cjs/runtime/core.js +32 -132
- package/dist/cjs/runtime/hooks.js +54 -21
- package/dist/cjs/runtime/i18n/backend/defaults.node.js +7 -7
- package/dist/cjs/runtime/i18n/detection/cache.js +150 -0
- package/dist/cjs/runtime/i18n/detection/detector.js +100 -0
- package/dist/cjs/runtime/i18n/detection/index.js +8 -263
- package/dist/cjs/runtime/i18n/detection/initOptions.js +69 -0
- package/dist/cjs/runtime/i18n/detection/language.js +62 -0
- package/dist/cjs/runtime/i18n/detection/path.js +49 -0
- package/dist/cjs/runtime/i18n/detection/priority.js +66 -0
- package/dist/cjs/runtime/i18n/detection/ssr.js +70 -0
- package/dist/cjs/runtime/i18n/detection/types.js +18 -0
- package/dist/cjs/runtime/i18n/instance.js +34 -10
- package/dist/cjs/runtime/linkHelpers.js +148 -0
- package/dist/cjs/runtime/pluginSetup.js +102 -0
- package/dist/cjs/runtime/providerComposition.js +115 -0
- package/dist/cjs/runtime/reactI18next.js +43 -0
- package/dist/cjs/runtime/routerAdapter.js +16 -8
- package/dist/cjs/runtime/utils.js +1 -9
- package/dist/cjs/server/apiPrefix.js +72 -0
- package/dist/cjs/server/detectorOptions.js +97 -0
- package/dist/cjs/server/index.js +17 -144
- package/dist/cjs/server/redirectPolicy.js +96 -0
- package/dist/cjs/shared/detection.js +2 -104
- package/dist/cjs/shared/localisedUrls/config.js +49 -0
- package/dist/cjs/shared/localisedUrls/index.js +91 -0
- package/dist/cjs/shared/localisedUrls/normalise.js +87 -0
- package/dist/cjs/shared/localisedUrls/pathname.js +68 -0
- package/dist/cjs/shared/localisedUrls/patterns.js +130 -0
- package/dist/cjs/shared/localisedUrls/redirect.js +103 -0
- package/dist/cjs/shared/localisedUrls/resolve.js +88 -0
- package/dist/cjs/shared/localisedUrls/routes.js +130 -0
- package/dist/cjs/shared/localisedUrls/types.js +18 -0
- package/dist/cjs/shared/localisedUrls.js +28 -294
- package/dist/cjs/shared/utils.js +2 -8
- package/dist/esm/cli/index.mjs +2 -1
- package/dist/esm/cli/locales.mjs +13 -13
- package/dist/esm/runtime/Link.mjs +1 -82
- package/dist/esm/runtime/context.mjs +76 -92
- package/dist/esm/runtime/contextHelpers.mjs +87 -0
- package/dist/esm/runtime/core.mjs +30 -133
- package/dist/esm/runtime/hooks.mjs +56 -23
- package/dist/esm/runtime/i18n/backend/defaults.node.mjs +5 -5
- package/dist/esm/runtime/i18n/detection/cache.mjs +103 -0
- package/dist/esm/runtime/i18n/detection/detector.mjs +62 -0
- package/dist/esm/runtime/i18n/detection/index.mjs +4 -260
- package/dist/esm/runtime/i18n/detection/initOptions.mjs +28 -0
- package/dist/esm/runtime/i18n/detection/language.mjs +21 -0
- package/dist/esm/runtime/i18n/detection/path.mjs +11 -0
- package/dist/esm/runtime/i18n/detection/priority.mjs +28 -0
- package/dist/esm/runtime/i18n/detection/ssr.mjs +26 -0
- package/dist/esm/runtime/i18n/detection/types.mjs +0 -0
- package/dist/esm/runtime/i18n/instance.mjs +35 -8
- package/dist/esm/runtime/linkHelpers.mjs +98 -0
- package/dist/esm/runtime/pluginSetup.mjs +64 -0
- package/dist/esm/runtime/providerComposition.mjs +77 -0
- package/dist/esm/runtime/reactI18next.mjs +5 -0
- package/dist/esm/runtime/routerAdapter.mjs +16 -8
- package/dist/esm/runtime/utils.mjs +2 -10
- package/dist/esm/server/apiPrefix.mjs +31 -0
- package/dist/esm/server/detectorOptions.mjs +59 -0
- package/dist/esm/server/index.mjs +8 -135
- package/dist/esm/server/redirectPolicy.mjs +46 -0
- package/dist/esm/shared/detection.mjs +1 -71
- package/dist/esm/shared/localisedUrls/config.mjs +11 -0
- package/dist/esm/shared/localisedUrls/index.mjs +7 -0
- package/dist/esm/shared/localisedUrls/normalise.mjs +37 -0
- package/dist/esm/shared/localisedUrls/pathname.mjs +24 -0
- package/dist/esm/shared/localisedUrls/patterns.mjs +86 -0
- package/dist/esm/shared/localisedUrls/redirect.mjs +56 -0
- package/dist/esm/shared/localisedUrls/resolve.mjs +47 -0
- package/dist/esm/shared/localisedUrls/routes.mjs +89 -0
- package/dist/esm/shared/localisedUrls/types.mjs +0 -0
- package/dist/esm/shared/localisedUrls.mjs +1 -283
- package/dist/esm/shared/utils.mjs +1 -1
- package/dist/esm-node/cli/index.mjs +2 -1
- package/dist/esm-node/cli/locales.mjs +13 -13
- package/dist/esm-node/runtime/Link.mjs +1 -82
- package/dist/esm-node/runtime/context.mjs +76 -92
- package/dist/esm-node/runtime/contextHelpers.mjs +88 -0
- package/dist/esm-node/runtime/core.mjs +30 -133
- package/dist/esm-node/runtime/hooks.mjs +56 -23
- package/dist/esm-node/runtime/i18n/backend/defaults.node.mjs +5 -5
- package/dist/esm-node/runtime/i18n/detection/cache.mjs +104 -0
- package/dist/esm-node/runtime/i18n/detection/detector.mjs +63 -0
- package/dist/esm-node/runtime/i18n/detection/index.mjs +4 -260
- package/dist/esm-node/runtime/i18n/detection/initOptions.mjs +29 -0
- package/dist/esm-node/runtime/i18n/detection/language.mjs +22 -0
- package/dist/esm-node/runtime/i18n/detection/path.mjs +12 -0
- package/dist/esm-node/runtime/i18n/detection/priority.mjs +29 -0
- package/dist/esm-node/runtime/i18n/detection/ssr.mjs +27 -0
- package/dist/esm-node/runtime/i18n/detection/types.mjs +1 -0
- package/dist/esm-node/runtime/i18n/instance.mjs +35 -8
- package/dist/esm-node/runtime/linkHelpers.mjs +99 -0
- package/dist/esm-node/runtime/pluginSetup.mjs +65 -0
- package/dist/esm-node/runtime/providerComposition.mjs +78 -0
- package/dist/esm-node/runtime/reactI18next.mjs +6 -0
- package/dist/esm-node/runtime/routerAdapter.mjs +16 -8
- package/dist/esm-node/runtime/utils.mjs +2 -10
- package/dist/esm-node/server/apiPrefix.mjs +32 -0
- package/dist/esm-node/server/detectorOptions.mjs +60 -0
- package/dist/esm-node/server/index.mjs +8 -135
- package/dist/esm-node/server/redirectPolicy.mjs +47 -0
- package/dist/esm-node/shared/detection.mjs +1 -71
- package/dist/esm-node/shared/localisedUrls/config.mjs +12 -0
- package/dist/esm-node/shared/localisedUrls/index.mjs +8 -0
- package/dist/esm-node/shared/localisedUrls/normalise.mjs +38 -0
- package/dist/esm-node/shared/localisedUrls/pathname.mjs +25 -0
- package/dist/esm-node/shared/localisedUrls/patterns.mjs +87 -0
- package/dist/esm-node/shared/localisedUrls/redirect.mjs +57 -0
- package/dist/esm-node/shared/localisedUrls/resolve.mjs +48 -0
- package/dist/esm-node/shared/localisedUrls/routes.mjs +90 -0
- package/dist/esm-node/shared/localisedUrls/types.mjs +1 -0
- package/dist/esm-node/shared/localisedUrls.mjs +1 -283
- package/dist/esm-node/shared/utils.mjs +1 -1
- package/dist/types/cli/locales.d.ts +1 -1
- package/dist/types/runtime/Link.d.ts +3 -7
- package/dist/types/runtime/context.d.ts +21 -5
- package/dist/types/runtime/contextHelpers.d.ts +29 -0
- package/dist/types/runtime/core.d.ts +4 -8
- package/dist/types/runtime/hooks.d.ts +3 -7
- package/dist/types/runtime/i18n/backend/middleware.common.d.ts +2 -1
- package/dist/types/runtime/i18n/detection/cache.d.ts +14 -0
- package/dist/types/runtime/i18n/detection/detector.d.ts +8 -0
- package/dist/types/runtime/i18n/detection/index.d.ts +5 -50
- package/dist/types/runtime/i18n/detection/initOptions.d.ts +10 -0
- package/dist/types/runtime/i18n/detection/language.d.ts +11 -0
- package/dist/types/runtime/i18n/detection/middleware.d.ts +4 -4
- package/dist/types/runtime/i18n/detection/middleware.node.d.ts +4 -4
- package/dist/types/runtime/i18n/detection/path.d.ts +5 -0
- package/dist/types/runtime/i18n/detection/priority.d.ts +10 -0
- package/dist/types/runtime/i18n/detection/ssr.d.ts +9 -0
- package/dist/types/runtime/i18n/detection/types.d.ts +32 -0
- package/dist/types/runtime/i18n/instance.d.ts +31 -27
- package/dist/types/runtime/i18n/react-i18next.d.ts +1 -6
- package/dist/types/runtime/i18n/utils.d.ts +14 -5
- package/dist/types/runtime/linkHelpers.d.ts +17 -0
- package/dist/types/runtime/pluginSetup.d.ts +32 -0
- package/dist/types/runtime/providerComposition.d.ts +16 -0
- package/dist/types/runtime/reactI18next.d.ts +7 -0
- package/dist/types/runtime/routerAdapter.d.ts +7 -6
- package/dist/types/runtime/utils.d.ts +7 -2
- package/dist/types/server/apiPrefix.d.ts +7 -0
- package/dist/types/server/detectorOptions.d.ts +21 -0
- package/dist/types/server/index.d.ts +1 -6
- package/dist/types/server/redirectPolicy.d.ts +22 -0
- package/dist/types/shared/detection.d.ts +1 -11
- package/dist/types/shared/localisedUrls/config.d.ts +9 -0
- package/dist/types/shared/localisedUrls/index.d.ts +9 -0
- package/dist/types/shared/localisedUrls/normalise.d.ts +10 -0
- package/dist/types/shared/localisedUrls/pathname.d.ts +4 -0
- package/dist/types/shared/localisedUrls/patterns.d.ts +5 -0
- package/dist/types/shared/localisedUrls/redirect.d.ts +11 -0
- package/dist/types/shared/localisedUrls/resolve.d.ts +7 -0
- package/dist/types/shared/localisedUrls/routes.d.ts +3 -0
- package/dist/types/shared/localisedUrls/types.d.ts +14 -0
- package/dist/types/shared/localisedUrls.d.ts +2 -36
- package/dist/types/shared/utils.d.ts +0 -2
- package/package.json +17 -20
- package/rstest.config.mts +8 -1
- package/src/cli/index.ts +5 -4
- package/src/cli/locales.ts +3 -3
- package/src/runtime/Link.tsx +11 -138
- package/src/runtime/context.tsx +170 -207
- package/src/runtime/contextHelpers.ts +242 -0
- package/src/runtime/core.tsx +48 -275
- package/src/runtime/hooks.ts +86 -24
- package/src/runtime/i18n/backend/defaults.node.ts +2 -2
- package/src/runtime/i18n/backend/middleware.common.ts +5 -4
- package/src/runtime/i18n/detection/cache.ts +189 -0
- package/src/runtime/i18n/detection/detector.ts +166 -0
- package/src/runtime/i18n/detection/index.ts +10 -641
- package/src/runtime/i18n/detection/initOptions.ts +74 -0
- package/src/runtime/i18n/detection/language.ts +64 -0
- package/src/runtime/i18n/detection/middleware.node.ts +26 -7
- package/src/runtime/i18n/detection/middleware.ts +15 -7
- package/src/runtime/i18n/detection/path.ts +41 -0
- package/src/runtime/i18n/detection/priority.ts +82 -0
- package/src/runtime/i18n/detection/ssr.ts +47 -0
- package/src/runtime/i18n/detection/types.ts +41 -0
- package/src/runtime/i18n/instance.ts +98 -34
- package/src/runtime/i18n/react-i18next.ts +1 -6
- package/src/runtime/i18n/utils.ts +18 -9
- package/src/runtime/linkHelpers.ts +174 -0
- package/src/runtime/pluginSetup.ts +189 -0
- package/src/runtime/providerComposition.tsx +148 -0
- package/src/runtime/reactI18next.ts +20 -0
- package/src/runtime/routerAdapter.tsx +36 -15
- package/src/runtime/utils.ts +13 -35
- package/src/server/apiPrefix.ts +67 -0
- package/src/server/detectorOptions.ts +105 -0
- package/src/server/index.ts +14 -319
- package/src/server/redirectPolicy.ts +127 -0
- package/src/shared/detection.ts +1 -1
- package/src/shared/localisedUrls/config.ts +18 -0
- package/src/shared/localisedUrls/index.ts +24 -0
- package/src/shared/localisedUrls/normalise.ts +67 -0
- package/src/shared/localisedUrls/pathname.ts +60 -0
- package/src/shared/localisedUrls/patterns.ts +160 -0
- package/src/shared/localisedUrls/redirect.ts +93 -0
- package/src/shared/localisedUrls/resolve.ts +135 -0
- package/src/shared/localisedUrls/routes.ts +234 -0
- package/src/shared/localisedUrls/types.ts +18 -0
- package/src/shared/localisedUrls.ts +26 -623
- package/src/shared/utils.ts +2 -2
- package/tests/federatedI18nBoundary.client.test.tsx +112 -0
- package/tests/federatedI18nBoundary.test.tsx +120 -0
- package/tests/fixtures/localised-routes.golden.json +156 -0
- package/tests/link.test.tsx +111 -0
- package/tests/localisedUrlRewriteMatrix.fork.test.ts +147 -0
- package/tests/localisedUrls.test.ts +148 -2
- package/tests/reactI18nextRuntimeBoundary.test.ts +6 -3
- package/tests/redirectPolicy.test.ts +45 -0
- package/tests/routerAdapter.test.tsx +603 -0
- package/tests/type-fixture/fixture-globals.d.ts +11 -0
- package/tests/type-fixture/tsconfig.json +1 -0
|
@@ -11,7 +11,16 @@ import {
|
|
|
11
11
|
isI18nWrapperInstance,
|
|
12
12
|
} from './instance';
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
type MergedBackendOptions = NonNullable<I18nInitOptions['backend']> & {
|
|
15
|
+
_useChainedBackend?: boolean;
|
|
16
|
+
_chainedBackendConfig?: {
|
|
17
|
+
backendOptions: Array<Record<string, unknown>>;
|
|
18
|
+
};
|
|
19
|
+
backends?: unknown[];
|
|
20
|
+
backendOptions?: unknown;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function assertI18nInstance(obj: unknown): asserts obj is I18nInstance {
|
|
15
24
|
if (!isI18nInstance(obj)) {
|
|
16
25
|
throw new Error('Object does not implement I18nInstance interface');
|
|
17
26
|
}
|
|
@@ -24,8 +33,8 @@ export const buildInitOptions = async (
|
|
|
24
33
|
finalLanguage: string,
|
|
25
34
|
fallbackLanguage: string,
|
|
26
35
|
languages: string[],
|
|
27
|
-
mergedDetection:
|
|
28
|
-
mergedBackend:
|
|
36
|
+
mergedDetection: I18nInitOptions['detection'],
|
|
37
|
+
mergedBackend: MergedBackendOptions | undefined,
|
|
29
38
|
userInitOptions?: I18nInitOptions,
|
|
30
39
|
useSuspense?: boolean,
|
|
31
40
|
i18nInstance?: I18nInstance,
|
|
@@ -84,8 +93,8 @@ export const buildInitOptions = async (
|
|
|
84
93
|
if (isChainedBackend && mergedBackend._chainedBackendConfig) {
|
|
85
94
|
// Try to get backend classes from i18nInstance.options.backend.backends first
|
|
86
95
|
// This avoids importing fs-backend in browser environment
|
|
87
|
-
let HttpBackend:
|
|
88
|
-
let SdkBackendClass:
|
|
96
|
+
let HttpBackend: unknown;
|
|
97
|
+
let SdkBackendClass: unknown;
|
|
89
98
|
|
|
90
99
|
if (
|
|
91
100
|
i18nInstance?.options?.backend?.backends &&
|
|
@@ -148,7 +157,7 @@ export const changeI18nLanguage = async (
|
|
|
148
157
|
i18nInstance: I18nInstance,
|
|
149
158
|
newLang: string,
|
|
150
159
|
options?: {
|
|
151
|
-
detectionOptions?:
|
|
160
|
+
detectionOptions?: I18nInitOptions['detection'];
|
|
152
161
|
},
|
|
153
162
|
): Promise<void> => {
|
|
154
163
|
if (!newLang || typeof newLang !== 'string') {
|
|
@@ -179,8 +188,8 @@ export const initializeI18nInstance = async (
|
|
|
179
188
|
finalLanguage: string,
|
|
180
189
|
fallbackLanguage: string,
|
|
181
190
|
languages: string[],
|
|
182
|
-
mergedDetection:
|
|
183
|
-
mergedBackend:
|
|
191
|
+
mergedDetection: I18nInitOptions['detection'],
|
|
192
|
+
mergedBackend: MergedBackendOptions | undefined,
|
|
184
193
|
userInitOptions?: I18nInitOptions,
|
|
185
194
|
useSuspense?: boolean,
|
|
186
195
|
): Promise<void> => {
|
|
@@ -265,7 +274,7 @@ export const setupClonedInstance = async (
|
|
|
265
274
|
backendEnabled: boolean,
|
|
266
275
|
backend: BaseBackendOptions | undefined,
|
|
267
276
|
i18nextDetector: boolean,
|
|
268
|
-
detection:
|
|
277
|
+
detection: I18nInitOptions['detection'],
|
|
269
278
|
localePathRedirect: boolean,
|
|
270
279
|
userInitOptions: I18nInitOptions | undefined,
|
|
271
280
|
): Promise<void> => {
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
const warnedTargets = new Set<string>();
|
|
2
|
+
|
|
3
|
+
export const warnOnce = (key: string, message: string) => {
|
|
4
|
+
if (process.env.NODE_ENV !== 'development' || warnedTargets.has(key)) {
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
warnedTargets.add(key);
|
|
8
|
+
console.warn(message);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type LinkParams = Record<string, string | number | undefined>;
|
|
12
|
+
type NormalizedSearchValue = string | string[];
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Interpolate `$param`, `:param`, optional (`{-$param}` / `:param?`) and splat
|
|
16
|
+
* (`$` / `*`) segments with concrete values before localization, so
|
|
17
|
+
* pattern-mapped slugs localize correctly.
|
|
18
|
+
*/
|
|
19
|
+
export const interpolateRouteParams = (
|
|
20
|
+
pathname: string,
|
|
21
|
+
params?: LinkParams,
|
|
22
|
+
): string => {
|
|
23
|
+
if (!/[$:*{]/.test(pathname)) {
|
|
24
|
+
return pathname;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const resolveParam = (name: string): string | undefined => {
|
|
28
|
+
const value = params?.[name];
|
|
29
|
+
return value === undefined ? undefined : String(value);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const segments = pathname
|
|
33
|
+
.split('/')
|
|
34
|
+
.map(segment => {
|
|
35
|
+
if (!segment) {
|
|
36
|
+
return segment;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (segment.startsWith('{-$') && segment.endsWith('}')) {
|
|
40
|
+
const value = resolveParam(segment.slice(3, -1));
|
|
41
|
+
return value === undefined ? null : encodeURIComponent(value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (segment === '$' || segment === '*') {
|
|
45
|
+
const value = resolveParam('_splat') ?? resolveParam('*');
|
|
46
|
+
return value === undefined
|
|
47
|
+
? null
|
|
48
|
+
: value.split('/').map(encodeURIComponent).join('/');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (segment.startsWith('$')) {
|
|
52
|
+
const value = resolveParam(segment.slice(1));
|
|
53
|
+
if (value === undefined) {
|
|
54
|
+
warnOnce(
|
|
55
|
+
`missing-param:${pathname}:${segment}`,
|
|
56
|
+
`[plugin-i18n] <Link to="${pathname}"> is missing required param "${segment.slice(1)}".`,
|
|
57
|
+
);
|
|
58
|
+
return segment;
|
|
59
|
+
}
|
|
60
|
+
return encodeURIComponent(value);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (segment.startsWith(':')) {
|
|
64
|
+
const optional = segment.endsWith('?');
|
|
65
|
+
const name = segment.slice(1, optional ? -1 : undefined);
|
|
66
|
+
const value = resolveParam(name);
|
|
67
|
+
if (value === undefined) {
|
|
68
|
+
if (optional) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
warnOnce(
|
|
72
|
+
`missing-param:${pathname}:${segment}`,
|
|
73
|
+
`[plugin-i18n] <Link to="${pathname}"> is missing required param "${name}".`,
|
|
74
|
+
);
|
|
75
|
+
return segment;
|
|
76
|
+
}
|
|
77
|
+
return encodeURIComponent(value);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return segment;
|
|
81
|
+
})
|
|
82
|
+
.filter(segment => segment !== null);
|
|
83
|
+
|
|
84
|
+
return segments.join('/') || '/';
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const normalizeSearch = (
|
|
88
|
+
search: string | Record<string, unknown> | undefined,
|
|
89
|
+
searchFromTo: string,
|
|
90
|
+
): {
|
|
91
|
+
searchString: string;
|
|
92
|
+
searchObject: Record<string, NormalizedSearchValue> | undefined;
|
|
93
|
+
} => {
|
|
94
|
+
if (search && typeof search === 'object') {
|
|
95
|
+
const searchObject: Record<string, NormalizedSearchValue> = {};
|
|
96
|
+
const params = new URLSearchParams();
|
|
97
|
+
|
|
98
|
+
for (const [key, value] of Object.entries(search)) {
|
|
99
|
+
if (value === undefined || value === null) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (Array.isArray(value)) {
|
|
104
|
+
const values = value
|
|
105
|
+
.filter(item => item !== undefined && item !== null)
|
|
106
|
+
.map(String);
|
|
107
|
+
if (values.length === 0) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
searchObject[key] = values;
|
|
111
|
+
for (const item of values) {
|
|
112
|
+
params.append(key, item);
|
|
113
|
+
}
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const stringValue = String(value);
|
|
118
|
+
searchObject[key] = stringValue;
|
|
119
|
+
params.append(key, stringValue);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const serialized = params.toString();
|
|
123
|
+
return {
|
|
124
|
+
searchString: serialized ? `?${serialized}` : '',
|
|
125
|
+
searchObject:
|
|
126
|
+
Object.keys(searchObject).length > 0 ? searchObject : undefined,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const raw = typeof search === 'string' ? search : searchFromTo;
|
|
131
|
+
if (!raw) {
|
|
132
|
+
return { searchString: '', searchObject: undefined };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const searchString = raw.startsWith('?') ? raw : `?${raw}`;
|
|
136
|
+
const searchObject: Record<string, NormalizedSearchValue> = {};
|
|
137
|
+
new URLSearchParams(searchString).forEach((value, key) => {
|
|
138
|
+
const existing = searchObject[key];
|
|
139
|
+
if (existing === undefined) {
|
|
140
|
+
searchObject[key] = value;
|
|
141
|
+
} else if (Array.isArray(existing)) {
|
|
142
|
+
existing.push(value);
|
|
143
|
+
} else {
|
|
144
|
+
searchObject[key] = [existing, value];
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
return { searchString, searchObject };
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
type ActivePropsResult<TActiveProps extends Record<string, unknown>> = Record<
|
|
152
|
+
string,
|
|
153
|
+
unknown
|
|
154
|
+
> &
|
|
155
|
+
Partial<TActiveProps>;
|
|
156
|
+
|
|
157
|
+
export const splitActiveProps = <TActiveProps extends Record<string, unknown>>(
|
|
158
|
+
active: boolean,
|
|
159
|
+
activeProps?: TActiveProps,
|
|
160
|
+
): ActivePropsResult<TActiveProps> => {
|
|
161
|
+
if (!active || !activeProps) {
|
|
162
|
+
return {};
|
|
163
|
+
}
|
|
164
|
+
return activeProps;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
export const mergeClassNames = (
|
|
168
|
+
...values: Array<unknown>
|
|
169
|
+
): string | undefined => {
|
|
170
|
+
const classNames = values.filter(
|
|
171
|
+
(value): value is string => typeof value === 'string' && value.length > 0,
|
|
172
|
+
);
|
|
173
|
+
return classNames.length > 0 ? classNames.join(' ') : undefined;
|
|
174
|
+
};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { isBrowser } from '@modern-js/runtime';
|
|
2
|
+
import type { TInternalRuntimeContext } from '@modern-js/runtime/internal';
|
|
3
|
+
import { merge } from '@modern-js/runtime-utils/merge';
|
|
4
|
+
import type {
|
|
5
|
+
BaseBackendOptions,
|
|
6
|
+
BaseLocaleDetectionOptions,
|
|
7
|
+
} from '../shared/type';
|
|
8
|
+
import type { I18nInitOptions, I18nInstance } from './i18n';
|
|
9
|
+
import { getI18nInstance } from './i18n';
|
|
10
|
+
import { mergeBackendOptions } from './i18n/backend';
|
|
11
|
+
import {
|
|
12
|
+
detectLanguageWithPriority,
|
|
13
|
+
exportServerLngToWindow,
|
|
14
|
+
mergeDetectionOptions,
|
|
15
|
+
} from './i18n/detection';
|
|
16
|
+
import { useI18nextLanguageDetector } from './i18n/detection/middleware';
|
|
17
|
+
import type { ReactI18nextIntegration } from './reactI18next';
|
|
18
|
+
import { getPathname } from './utils';
|
|
19
|
+
|
|
20
|
+
type I18nLifecycleHelpers = {
|
|
21
|
+
useI18nextBackend: typeof import('./i18n/backend/middleware')['useI18nextBackend'];
|
|
22
|
+
changeI18nLanguage: typeof import('./i18n/utils')['changeI18nLanguage'];
|
|
23
|
+
ensureLanguageMatch: typeof import('./i18n/utils')['ensureLanguageMatch'];
|
|
24
|
+
initializeI18nInstance: typeof import('./i18n/utils')['initializeI18nInstance'];
|
|
25
|
+
setupClonedInstance: typeof import('./i18n/utils')['setupClonedInstance'];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
let i18nLifecycleHelpersPromise: Promise<I18nLifecycleHelpers> | undefined;
|
|
29
|
+
|
|
30
|
+
function loadI18nLifecycleHelpers(): Promise<I18nLifecycleHelpers> {
|
|
31
|
+
i18nLifecycleHelpersPromise ??= Promise.all([
|
|
32
|
+
import('./i18n/backend/middleware'),
|
|
33
|
+
import('./i18n/utils'),
|
|
34
|
+
]).then(([backendMiddleware, utils]) => ({
|
|
35
|
+
useI18nextBackend: backendMiddleware.useI18nextBackend,
|
|
36
|
+
changeI18nLanguage: utils.changeI18nLanguage,
|
|
37
|
+
ensureLanguageMatch: utils.ensureLanguageMatch,
|
|
38
|
+
initializeI18nInstance: utils.initializeI18nInstance,
|
|
39
|
+
setupClonedInstance: utils.setupClonedInstance,
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
return i18nLifecycleHelpersPromise;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface RuntimeConfigWithI18n {
|
|
46
|
+
i18n?: {
|
|
47
|
+
initOptions?: I18nInitOptions;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface I18nRuntimeApi {
|
|
52
|
+
getRuntimeConfig: () => RuntimeConfigWithI18n;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface RuntimeContextWithI18n extends TInternalRuntimeContext {
|
|
56
|
+
i18nInstance?: I18nInstance;
|
|
57
|
+
changeLanguage?: (lang: string) => Promise<void>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface SetupI18nBeforeRenderOptions {
|
|
61
|
+
api: I18nRuntimeApi;
|
|
62
|
+
userI18nInstance?: I18nInstance;
|
|
63
|
+
initOptions?: I18nInitOptions;
|
|
64
|
+
backend?: BaseBackendOptions;
|
|
65
|
+
backendEnabled: boolean;
|
|
66
|
+
i18nextDetector: boolean;
|
|
67
|
+
detection?: BaseLocaleDetectionOptions['detection'];
|
|
68
|
+
localePathRedirect: boolean;
|
|
69
|
+
languages: string[];
|
|
70
|
+
fallbackLanguage: string;
|
|
71
|
+
resolveReactI18nextIntegration: () => Promise<ReactI18nextIntegration | null>;
|
|
72
|
+
setI18nextProvider: (
|
|
73
|
+
provider: ReactI18nextIntegration['I18nextProvider'],
|
|
74
|
+
) => void;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function setupI18nBeforeRender(
|
|
78
|
+
context: RuntimeContextWithI18n,
|
|
79
|
+
options: SetupI18nBeforeRenderOptions,
|
|
80
|
+
): Promise<I18nInstance> {
|
|
81
|
+
const {
|
|
82
|
+
api,
|
|
83
|
+
userI18nInstance,
|
|
84
|
+
initOptions,
|
|
85
|
+
backend,
|
|
86
|
+
backendEnabled,
|
|
87
|
+
i18nextDetector,
|
|
88
|
+
detection,
|
|
89
|
+
localePathRedirect,
|
|
90
|
+
languages,
|
|
91
|
+
fallbackLanguage,
|
|
92
|
+
resolveReactI18nextIntegration,
|
|
93
|
+
setI18nextProvider,
|
|
94
|
+
} = options;
|
|
95
|
+
const {
|
|
96
|
+
useI18nextBackend,
|
|
97
|
+
changeI18nLanguage,
|
|
98
|
+
ensureLanguageMatch,
|
|
99
|
+
initializeI18nInstance,
|
|
100
|
+
setupClonedInstance,
|
|
101
|
+
} = await loadI18nLifecycleHelpers();
|
|
102
|
+
let i18nInstance = await getI18nInstance(userI18nInstance);
|
|
103
|
+
const { i18n: otherConfig } = api.getRuntimeConfig();
|
|
104
|
+
const { initOptions: otherInitOptions } = otherConfig || {};
|
|
105
|
+
const userInitOptions = merge(otherInitOptions || {}, initOptions || {});
|
|
106
|
+
|
|
107
|
+
const reactI18nextIntegration = await resolveReactI18nextIntegration();
|
|
108
|
+
setI18nextProvider(reactI18nextIntegration?.I18nextProvider ?? null);
|
|
109
|
+
if (reactI18nextIntegration?.initReactI18next) {
|
|
110
|
+
i18nInstance.use(reactI18nextIntegration.initReactI18next);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const pathname = getPathname(context);
|
|
114
|
+
if (i18nextDetector) {
|
|
115
|
+
useI18nextLanguageDetector(i18nInstance);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const mergedDetection = mergeDetectionOptions(
|
|
119
|
+
i18nextDetector,
|
|
120
|
+
detection,
|
|
121
|
+
localePathRedirect,
|
|
122
|
+
userInitOptions,
|
|
123
|
+
);
|
|
124
|
+
const mergedBackend = mergeBackendOptions(backend, userInitOptions);
|
|
125
|
+
|
|
126
|
+
// Register Backend BEFORE detectLanguageWithPriority because detection may init.
|
|
127
|
+
const hasSdkConfig =
|
|
128
|
+
typeof userInitOptions?.backend?.sdk === 'function' ||
|
|
129
|
+
(mergedBackend?.sdk && typeof mergedBackend.sdk === 'function');
|
|
130
|
+
if (mergedBackend && (backendEnabled || hasSdkConfig)) {
|
|
131
|
+
useI18nextBackend(i18nInstance, mergedBackend);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const { finalLanguage } = await detectLanguageWithPriority(i18nInstance, {
|
|
135
|
+
languages,
|
|
136
|
+
fallbackLanguage,
|
|
137
|
+
localePathRedirect,
|
|
138
|
+
i18nextDetector,
|
|
139
|
+
detection,
|
|
140
|
+
userInitOptions,
|
|
141
|
+
mergedBackend,
|
|
142
|
+
pathname,
|
|
143
|
+
ssrContext: context.ssrContext,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
await initializeI18nInstance(
|
|
147
|
+
i18nInstance,
|
|
148
|
+
finalLanguage,
|
|
149
|
+
fallbackLanguage,
|
|
150
|
+
languages,
|
|
151
|
+
mergedDetection,
|
|
152
|
+
mergedBackend,
|
|
153
|
+
userInitOptions,
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
if (!isBrowser() && i18nInstance.cloneInstance) {
|
|
157
|
+
i18nInstance = i18nInstance.cloneInstance();
|
|
158
|
+
await setupClonedInstance(
|
|
159
|
+
i18nInstance,
|
|
160
|
+
finalLanguage,
|
|
161
|
+
fallbackLanguage,
|
|
162
|
+
languages,
|
|
163
|
+
backendEnabled,
|
|
164
|
+
backend,
|
|
165
|
+
i18nextDetector,
|
|
166
|
+
detection,
|
|
167
|
+
localePathRedirect,
|
|
168
|
+
userInitOptions,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (localePathRedirect) {
|
|
173
|
+
await ensureLanguageMatch(i18nInstance, finalLanguage);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!isBrowser()) {
|
|
177
|
+
exportServerLngToWindow(context, finalLanguage);
|
|
178
|
+
}
|
|
179
|
+
context.i18nInstance = i18nInstance;
|
|
180
|
+
|
|
181
|
+
// Add changeLanguage method to context for other runtime plugins to use.
|
|
182
|
+
context.changeLanguage = async (newLang: string) => {
|
|
183
|
+
await changeI18nLanguage(i18nInstance, newLang, {
|
|
184
|
+
detectionOptions: mergedDetection,
|
|
185
|
+
});
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
return i18nInstance;
|
|
189
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { RuntimeContext } from '@modern-js/runtime';
|
|
2
|
+
import { Helmet } from '@modern-js/runtime/head';
|
|
3
|
+
import type React from 'react';
|
|
4
|
+
import { useContext, useEffect, useMemo, useRef, useState } from 'react';
|
|
5
|
+
import type { BaseLocaleDetectionOptions } from '../shared/type';
|
|
6
|
+
import { ModernI18nProvider } from './context';
|
|
7
|
+
import {
|
|
8
|
+
createContextValue,
|
|
9
|
+
useClientSideRedirect,
|
|
10
|
+
useLanguageSync,
|
|
11
|
+
useSdkResourcesLoader,
|
|
12
|
+
} from './hooks';
|
|
13
|
+
import type { I18nInstance } from './i18n';
|
|
14
|
+
import { getI18nextInstanceForProvider } from './i18n/instance';
|
|
15
|
+
import type { RuntimeContextWithI18n } from './pluginSetup';
|
|
16
|
+
import { useI18nRouterAdapter } from './routerAdapter';
|
|
17
|
+
|
|
18
|
+
interface I18nRootWrapperOptions {
|
|
19
|
+
entryName?: string;
|
|
20
|
+
htmlLangAttr: boolean;
|
|
21
|
+
localePathRedirect: boolean;
|
|
22
|
+
languages: string[];
|
|
23
|
+
fallbackLanguage: string;
|
|
24
|
+
ignoreRedirectRoutes?: BaseLocaleDetectionOptions['ignoreRedirectRoutes'];
|
|
25
|
+
localisedUrls?: BaseLocaleDetectionOptions['localisedUrls'];
|
|
26
|
+
getLatestI18nInstance: () => I18nInstance | undefined;
|
|
27
|
+
getI18nextProvider: () => React.ComponentType<any> | null | undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const createI18nRootWrapper =
|
|
31
|
+
(options: I18nRootWrapperOptions) => (App: React.ComponentType<any>) => {
|
|
32
|
+
return (props: Record<string, unknown>) => {
|
|
33
|
+
const {
|
|
34
|
+
entryName,
|
|
35
|
+
htmlLangAttr,
|
|
36
|
+
localePathRedirect,
|
|
37
|
+
languages,
|
|
38
|
+
fallbackLanguage,
|
|
39
|
+
ignoreRedirectRoutes,
|
|
40
|
+
localisedUrls,
|
|
41
|
+
getLatestI18nInstance,
|
|
42
|
+
getI18nextProvider,
|
|
43
|
+
} = options;
|
|
44
|
+
const runtimeContext = useContext(
|
|
45
|
+
RuntimeContext,
|
|
46
|
+
) as RuntimeContextWithI18n;
|
|
47
|
+
const i18nInstance =
|
|
48
|
+
runtimeContext.i18nInstance || getLatestI18nInstance();
|
|
49
|
+
const initialLang = useMemo(
|
|
50
|
+
() => i18nInstance?.language || fallbackLanguage,
|
|
51
|
+
[i18nInstance?.language, fallbackLanguage],
|
|
52
|
+
);
|
|
53
|
+
const [lang, setLang] = useState(initialLang);
|
|
54
|
+
const [forceUpdate, setForceUpdate] = useState(0);
|
|
55
|
+
const prevLangRef = useRef(lang);
|
|
56
|
+
const { location } = useI18nRouterAdapter();
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (i18nInstance?.language) {
|
|
60
|
+
const translator = (i18nInstance as any).translator;
|
|
61
|
+
if (translator) {
|
|
62
|
+
translator.language = i18nInstance.language;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}, [i18nInstance?.language]);
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
prevLangRef.current = lang;
|
|
69
|
+
}, [lang]);
|
|
70
|
+
|
|
71
|
+
useSdkResourcesLoader(i18nInstance, setForceUpdate);
|
|
72
|
+
const synchronizeLanguage = useLanguageSync(
|
|
73
|
+
i18nInstance,
|
|
74
|
+
localePathRedirect,
|
|
75
|
+
languages,
|
|
76
|
+
location?.pathname,
|
|
77
|
+
prevLangRef,
|
|
78
|
+
setLang,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
// Handle client-side redirect for static deployments.
|
|
82
|
+
useClientSideRedirect(
|
|
83
|
+
i18nInstance,
|
|
84
|
+
localePathRedirect,
|
|
85
|
+
languages,
|
|
86
|
+
fallbackLanguage,
|
|
87
|
+
ignoreRedirectRoutes,
|
|
88
|
+
localisedUrls,
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const contextValue = useMemo(
|
|
92
|
+
() =>
|
|
93
|
+
createContextValue(
|
|
94
|
+
lang,
|
|
95
|
+
i18nInstance,
|
|
96
|
+
entryName,
|
|
97
|
+
languages,
|
|
98
|
+
localePathRedirect,
|
|
99
|
+
ignoreRedirectRoutes,
|
|
100
|
+
localisedUrls,
|
|
101
|
+
setLang,
|
|
102
|
+
synchronizeLanguage,
|
|
103
|
+
),
|
|
104
|
+
[
|
|
105
|
+
lang,
|
|
106
|
+
i18nInstance,
|
|
107
|
+
entryName,
|
|
108
|
+
languages,
|
|
109
|
+
localePathRedirect,
|
|
110
|
+
ignoreRedirectRoutes,
|
|
111
|
+
localisedUrls,
|
|
112
|
+
forceUpdate,
|
|
113
|
+
synchronizeLanguage,
|
|
114
|
+
],
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const children = (props as React.PropsWithChildren).children;
|
|
118
|
+
let appContent: React.ReactNode = App ? (
|
|
119
|
+
<App {...props}>{children}</App>
|
|
120
|
+
) : (
|
|
121
|
+
children
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
if (i18nInstance) {
|
|
125
|
+
const I18nextProvider = getI18nextProvider();
|
|
126
|
+
if (I18nextProvider) {
|
|
127
|
+
const i18nextInstanceForProvider = getI18nextInstanceForProvider(
|
|
128
|
+
i18nInstance,
|
|
129
|
+
lang,
|
|
130
|
+
);
|
|
131
|
+
appContent = (
|
|
132
|
+
<I18nextProvider i18n={i18nextInstanceForProvider}>
|
|
133
|
+
{appContent}
|
|
134
|
+
</I18nextProvider>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<>
|
|
141
|
+
{Boolean(htmlLangAttr) && <Helmet htmlAttributes={{ lang }} />}
|
|
142
|
+
<ModernI18nProvider value={contextValue}>
|
|
143
|
+
{appContent}
|
|
144
|
+
</ModernI18nProvider>
|
|
145
|
+
</>
|
|
146
|
+
);
|
|
147
|
+
};
|
|
148
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
|
|
3
|
+
export interface ReactI18nextIntegration {
|
|
4
|
+
I18nextProvider: React.ComponentType<any> | null;
|
|
5
|
+
initReactI18next: any | null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type LoadReactI18nextIntegration =
|
|
9
|
+
() => Promise<ReactI18nextIntegration | null>;
|
|
10
|
+
|
|
11
|
+
export const resolveReactI18nextIntegration = async (
|
|
12
|
+
reactI18next: boolean,
|
|
13
|
+
loadReactI18nextIntegration?: LoadReactI18nextIntegration,
|
|
14
|
+
): Promise<ReactI18nextIntegration | null> => {
|
|
15
|
+
if (!reactI18next) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return loadReactI18nextIntegration?.() ?? null;
|
|
20
|
+
};
|