@bleedingdev/modern-js-plugin-i18n 3.2.0-ultramodern.9 → 3.2.0-ultramodern.91
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 +22 -0
- package/dist/cjs/runtime/I18nLink.js +4 -12
- package/dist/cjs/runtime/context.js +32 -5
- package/dist/cjs/runtime/hooks.js +8 -5
- package/dist/cjs/runtime/i18n/backend/defaults.js +1 -1
- package/dist/cjs/runtime/i18n/backend/defaults.node.js +2 -2
- package/dist/cjs/runtime/i18n/backend/middleware.node.js +4 -4
- package/dist/cjs/runtime/i18n/instance.js +0 -24
- package/dist/cjs/runtime/i18n/react-i18next.js +49 -0
- package/dist/cjs/runtime/i18n/utils.js +0 -12
- package/dist/cjs/runtime/index.js +18 -10
- package/dist/cjs/runtime/routerAdapter.js +163 -0
- package/dist/cjs/runtime/utils.js +63 -94
- package/dist/cjs/server/index.js +60 -8
- package/dist/cjs/shared/localisedUrls.js +237 -0
- package/dist/esm/cli/index.mjs +22 -0
- package/dist/esm/runtime/I18nLink.mjs +4 -12
- package/dist/esm/runtime/context.mjs +34 -7
- package/dist/esm/runtime/hooks.mjs +9 -6
- package/dist/esm/runtime/i18n/backend/defaults.mjs +1 -1
- package/dist/esm/runtime/i18n/backend/defaults.node.mjs +2 -2
- package/dist/esm/runtime/i18n/backend/middleware.node.mjs +3 -3
- package/dist/esm/runtime/i18n/instance.mjs +1 -19
- package/dist/esm/runtime/i18n/react-i18next.mjs +15 -0
- package/dist/esm/runtime/i18n/utils.mjs +0 -12
- package/dist/esm/runtime/index.mjs +19 -11
- package/dist/esm/runtime/routerAdapter.mjs +129 -0
- package/dist/esm/runtime/utils.mjs +11 -30
- package/dist/esm/server/index.mjs +53 -7
- package/dist/esm/shared/localisedUrls.mjs +191 -0
- package/dist/esm-node/cli/index.mjs +22 -0
- package/dist/esm-node/runtime/I18nLink.mjs +4 -12
- package/dist/esm-node/runtime/context.mjs +34 -7
- package/dist/esm-node/runtime/hooks.mjs +9 -6
- package/dist/esm-node/runtime/i18n/backend/defaults.mjs +1 -1
- package/dist/esm-node/runtime/i18n/backend/defaults.node.mjs +2 -2
- package/dist/esm-node/runtime/i18n/backend/middleware.node.mjs +3 -3
- package/dist/esm-node/runtime/i18n/instance.mjs +1 -19
- package/dist/esm-node/runtime/i18n/react-i18next.mjs +16 -0
- package/dist/esm-node/runtime/i18n/utils.mjs +0 -12
- package/dist/esm-node/runtime/index.mjs +19 -11
- package/dist/esm-node/runtime/routerAdapter.mjs +130 -0
- package/dist/esm-node/runtime/utils.mjs +11 -30
- package/dist/esm-node/server/index.mjs +53 -7
- package/dist/esm-node/shared/localisedUrls.mjs +192 -0
- package/dist/types/runtime/I18nLink.d.ts +15 -0
- package/dist/types/runtime/context.d.ts +3 -0
- package/dist/types/runtime/hooks.d.ts +4 -2
- package/dist/types/runtime/i18n/backend/middleware.node.d.ts +1 -1
- package/dist/types/runtime/i18n/instance.d.ts +0 -5
- package/dist/types/runtime/i18n/react-i18next.d.ts +7 -0
- package/dist/types/runtime/index.d.ts +1 -0
- package/dist/types/runtime/routerAdapter.d.ts +26 -0
- package/dist/types/runtime/utils.d.ts +2 -7
- package/dist/types/server/index.d.ts +6 -0
- package/dist/types/shared/localisedUrls.d.ts +13 -0
- package/dist/types/shared/type.d.ts +12 -0
- package/package.json +18 -22
- package/rstest.config.mts +39 -0
- package/src/cli/index.ts +43 -1
- package/src/runtime/I18nLink.tsx +10 -16
- package/src/runtime/context.tsx +45 -7
- package/src/runtime/hooks.ts +13 -4
- package/src/runtime/i18n/backend/defaults.node.ts +2 -2
- package/src/runtime/i18n/backend/defaults.ts +3 -1
- package/src/runtime/i18n/backend/middleware.node.ts +1 -1
- package/src/runtime/i18n/instance.ts +0 -29
- package/src/runtime/i18n/react-i18next.ts +25 -0
- package/src/runtime/i18n/utils.ts +4 -26
- package/src/runtime/index.tsx +23 -10
- package/src/runtime/routerAdapter.tsx +333 -0
- package/src/runtime/utils.ts +22 -34
- package/src/server/index.ts +117 -10
- package/src/shared/localisedUrls.ts +393 -0
- package/src/shared/type.ts +12 -0
- package/tests/i18nUtils.test.ts +52 -0
- package/tests/localisedUrls.test.ts +312 -0
- package/tests/routerAdapter.test.tsx +382 -0
- package/dist/esm/rslib-runtime.mjs +0 -18
- package/dist/esm-node/rslib-runtime.mjs +0 -19
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type TInternalRuntimeContext } from '@modern-js/runtime/context';
|
|
2
|
+
import type { LocalisedUrlsMap } from '../shared/localisedUrls';
|
|
2
3
|
export declare const getPathname: (context: TInternalRuntimeContext) => string;
|
|
3
4
|
export declare const getEntryPath: () => string;
|
|
4
5
|
/**
|
|
@@ -16,7 +17,7 @@ export declare const getLanguageFromPath: (pathname: string, languages: string[]
|
|
|
16
17
|
* @param languages - Array of supported languages
|
|
17
18
|
* @returns The localized URL path
|
|
18
19
|
*/
|
|
19
|
-
export declare const buildLocalizedUrl: (pathname: string, language: string, languages: string[]) => string;
|
|
20
|
+
export declare const buildLocalizedUrl: (pathname: string, language: string, languages: string[], localisedUrls?: boolean | LocalisedUrlsMap) => string;
|
|
20
21
|
export declare const detectLanguageFromPath: (pathname: string, languages: string[], localePathRedirect: boolean) => {
|
|
21
22
|
detected: boolean;
|
|
22
23
|
language?: string;
|
|
@@ -25,9 +26,3 @@ export declare const detectLanguageFromPath: (pathname: string, languages: strin
|
|
|
25
26
|
* Check if the given pathname should ignore automatic locale redirect
|
|
26
27
|
*/
|
|
27
28
|
export declare const shouldIgnoreRedirect: (pathname: string, languages: string[], ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean)) => boolean;
|
|
28
|
-
export declare const useRouterHooks: () => {
|
|
29
|
-
navigate: any;
|
|
30
|
-
location: any;
|
|
31
|
-
params: any;
|
|
32
|
-
hasRouter: boolean;
|
|
33
|
-
};
|
|
@@ -4,5 +4,11 @@ export interface I18nPluginOptions {
|
|
|
4
4
|
localeDetection: LocaleDetectionOptions;
|
|
5
5
|
staticRoutePrefixes: string[];
|
|
6
6
|
}
|
|
7
|
+
type ApiPrefixInput = string | string[] | undefined;
|
|
8
|
+
export declare const collectApiPrefixes: (routes: Array<{
|
|
9
|
+
isApi?: boolean;
|
|
10
|
+
urlPath?: string;
|
|
11
|
+
}>, bffPrefix?: ApiPrefixInput) => string[];
|
|
12
|
+
export declare const matchesApiPrefix: (pathname: string, apiPrefixes: string[]) => boolean;
|
|
7
13
|
export declare const i18nServerPlugin: (options: I18nPluginOptions) => ServerPlugin;
|
|
8
14
|
export default i18nServerPlugin;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { NestedRouteForCli, PageRoute } from '@modern-js/types';
|
|
2
|
+
export type LocalisedUrlPathMap = Record<string, string>;
|
|
3
|
+
export type LocalisedUrlsMap = Record<string, LocalisedUrlPathMap>;
|
|
4
|
+
export type LocalisedUrlsOption = boolean | LocalisedUrlsMap;
|
|
5
|
+
export interface ResolvedLocalisedUrlsConfig {
|
|
6
|
+
enabled: boolean;
|
|
7
|
+
map: LocalisedUrlsMap;
|
|
8
|
+
}
|
|
9
|
+
export declare const normalisePathPattern: (path: string) => string;
|
|
10
|
+
export declare const resolveLocalisedUrlsConfig: (option: LocalisedUrlsOption | undefined) => ResolvedLocalisedUrlsConfig;
|
|
11
|
+
export declare const validateLocalisedUrls: (routes: (NestedRouteForCli | PageRoute)[], languages: string[], localisedUrls: LocalisedUrlsMap) => void;
|
|
12
|
+
export declare const applyLocalisedUrlsToRoutes: (routes: (NestedRouteForCli | PageRoute)[], languages: string[], localisedUrls: LocalisedUrlsMap) => (NestedRouteForCli | PageRoute)[];
|
|
13
|
+
export declare const resolveLocalisedPath: (pathname: string, targetLanguage: string, languages: string[], localisedUrls: LocalisedUrlsMap) => string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { LanguageDetectorOptions, Resources } from '../runtime/i18n/instance';
|
|
2
|
+
import type { LocalisedUrlsOption } from './localisedUrls';
|
|
2
3
|
export interface BaseLocaleDetectionOptions {
|
|
3
4
|
localePathRedirect?: boolean;
|
|
4
5
|
i18nextDetector?: boolean;
|
|
@@ -6,6 +7,17 @@ export interface BaseLocaleDetectionOptions {
|
|
|
6
7
|
fallbackLanguage?: string;
|
|
7
8
|
detection?: LanguageDetectorOptions;
|
|
8
9
|
ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean);
|
|
10
|
+
/**
|
|
11
|
+
* Enables localised pathnames in addition to the locale prefix.
|
|
12
|
+
*
|
|
13
|
+
* - `false`: keep only locale-prefix behavior (`/en/about`).
|
|
14
|
+
* - object: map canonical route paths to every configured language.
|
|
15
|
+
*
|
|
16
|
+
* Defaults to `true` when `localePathRedirect` is enabled, so route
|
|
17
|
+
* generation validates that every localisable route path has entries for all
|
|
18
|
+
* configured languages.
|
|
19
|
+
*/
|
|
20
|
+
localisedUrls?: LocalisedUrlsOption;
|
|
9
21
|
}
|
|
10
22
|
export interface LocaleDetectionOptions extends BaseLocaleDetectionOptions {
|
|
11
23
|
localeDetectionByEntry?: Record<string, BaseLocaleDetectionOptions>;
|
package/package.json
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"modern",
|
|
18
18
|
"modern.js"
|
|
19
19
|
],
|
|
20
|
-
"version": "3.2.0-ultramodern.
|
|
20
|
+
"version": "3.2.0-ultramodern.91",
|
|
21
21
|
"engines": {
|
|
22
22
|
"node": ">=20"
|
|
23
23
|
},
|
|
@@ -81,47 +81,43 @@
|
|
|
81
81
|
}
|
|
82
82
|
},
|
|
83
83
|
"dependencies": {
|
|
84
|
-
"@swc/helpers": "^0.5.
|
|
84
|
+
"@swc/helpers": "^0.5.23",
|
|
85
85
|
"i18next-browser-languagedetector": "^8.2.1",
|
|
86
86
|
"i18next-chained-backend": "^5.0.4",
|
|
87
|
-
"i18next-fs-backend": "^2.6.
|
|
87
|
+
"i18next-fs-backend": "^2.6.6",
|
|
88
88
|
"i18next-http-backend": "^4.0.0",
|
|
89
|
-
"i18next-http-middleware": "^3.9.
|
|
90
|
-
"
|
|
91
|
-
"@modern-js/
|
|
92
|
-
"@modern-js/
|
|
93
|
-
"@modern-js/
|
|
94
|
-
"@modern-js/
|
|
95
|
-
"@modern-js/
|
|
89
|
+
"i18next-http-middleware": "^3.9.7",
|
|
90
|
+
"react-i18next": "17.0.8",
|
|
91
|
+
"@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.2.0-ultramodern.91",
|
|
92
|
+
"@modern-js/runtime-utils": "npm:@bleedingdev/modern-js-runtime-utils@3.2.0-ultramodern.91",
|
|
93
|
+
"@modern-js/server-runtime": "npm:@bleedingdev/modern-js-server-runtime@3.2.0-ultramodern.91",
|
|
94
|
+
"@modern-js/server-core": "npm:@bleedingdev/modern-js-server-core@3.2.0-ultramodern.91",
|
|
95
|
+
"@modern-js/types": "npm:@bleedingdev/modern-js-types@3.2.0-ultramodern.91",
|
|
96
|
+
"@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.2.0-ultramodern.91"
|
|
96
97
|
},
|
|
97
98
|
"peerDependencies": {
|
|
98
|
-
"@modern-js/runtime": "3.2.0-ultramodern.
|
|
99
|
+
"@modern-js/runtime": "3.2.0-ultramodern.91",
|
|
99
100
|
"i18next": ">=25.7.4",
|
|
100
101
|
"react": "^19.2.6",
|
|
101
|
-
"react-dom": "^19.2.6"
|
|
102
|
-
"react-i18next": ">=15.7.4"
|
|
102
|
+
"react-dom": "^19.2.6"
|
|
103
103
|
},
|
|
104
104
|
"peerDependenciesMeta": {
|
|
105
105
|
"i18next": {
|
|
106
106
|
"optional": true
|
|
107
|
-
},
|
|
108
|
-
"react-i18next": {
|
|
109
|
-
"optional": true
|
|
110
107
|
}
|
|
111
108
|
},
|
|
112
109
|
"devDependencies": {
|
|
113
110
|
"@rslib/core": "0.21.5",
|
|
114
111
|
"@types/jest": "^30.0.0",
|
|
115
|
-
"@types/node": "^25.
|
|
116
|
-
"@typescript/native-preview": "7.0.0-dev.
|
|
112
|
+
"@types/node": "^25.9.1",
|
|
113
|
+
"@typescript/native-preview": "7.0.0-dev.20260527.2",
|
|
117
114
|
"i18next": "26.2.0",
|
|
118
115
|
"jest": "^30.4.2",
|
|
119
116
|
"react": "^19.2.6",
|
|
120
117
|
"react-dom": "^19.2.6",
|
|
121
|
-
"
|
|
122
|
-
"
|
|
123
|
-
"@modern-js/
|
|
124
|
-
"@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.2.0-ultramodern.9"
|
|
118
|
+
"ts-jest": "^29.4.11",
|
|
119
|
+
"@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.2.0-ultramodern.91",
|
|
120
|
+
"@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.2.0-ultramodern.91"
|
|
125
121
|
},
|
|
126
122
|
"sideEffects": false,
|
|
127
123
|
"publishConfig": {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { dirname, resolve } from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import type { ProjectConfig } from '@rstest/core';
|
|
4
|
+
import { withTestPreset } from '@scripts/rstest-config';
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
|
|
8
|
+
const commonConfig: ProjectConfig = {
|
|
9
|
+
setupFiles: [resolve(__dirname, '../../../scripts/rstest-config/setup.ts')],
|
|
10
|
+
globals: true,
|
|
11
|
+
tools: {
|
|
12
|
+
swc: {
|
|
13
|
+
jsc: {
|
|
14
|
+
transform: {
|
|
15
|
+
react: {
|
|
16
|
+
runtime: 'automatic',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default {
|
|
25
|
+
projects: [
|
|
26
|
+
withTestPreset({
|
|
27
|
+
name: 'plugin-i18n-node',
|
|
28
|
+
testEnvironment: 'node',
|
|
29
|
+
include: ['tests/localisedUrls.test.ts'],
|
|
30
|
+
extends: commonConfig,
|
|
31
|
+
}),
|
|
32
|
+
withTestPreset({
|
|
33
|
+
name: 'plugin-i18n-client',
|
|
34
|
+
testEnvironment: 'happy-dom',
|
|
35
|
+
include: ['tests/routerAdapter.test.tsx'],
|
|
36
|
+
extends: commonConfig,
|
|
37
|
+
}),
|
|
38
|
+
],
|
|
39
|
+
};
|
package/src/cli/index.ts
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import type { AppTools, CliPlugin } from '@modern-js/app-tools';
|
|
2
2
|
import { getPublicDirRoutePrefixes } from '@modern-js/server-core';
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
Entrypoint,
|
|
5
|
+
NestedRouteForCli,
|
|
6
|
+
PageRoute,
|
|
7
|
+
} from '@modern-js/types';
|
|
4
8
|
import fs from 'fs';
|
|
5
9
|
import path from 'path';
|
|
10
|
+
import {
|
|
11
|
+
applyLocalisedUrlsToRoutes,
|
|
12
|
+
resolveLocalisedUrlsConfig,
|
|
13
|
+
} from '../shared/localisedUrls';
|
|
6
14
|
import type { BackendOptions, LocaleDetectionOptions } from '../shared/type';
|
|
7
15
|
import { getBackendOptions, getLocaleDetectionOptions } from '../shared/utils';
|
|
8
16
|
|
|
@@ -203,6 +211,40 @@ export const i18nPlugin = (
|
|
|
203
211
|
};
|
|
204
212
|
});
|
|
205
213
|
|
|
214
|
+
api.modifyFileSystemRoutes(({ entrypoint, routes }) => {
|
|
215
|
+
if (!localeDetection) {
|
|
216
|
+
return { entrypoint, routes };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const localeDetectionOptions = getLocaleDetectionOptions(
|
|
220
|
+
entrypoint.entryName,
|
|
221
|
+
localeDetection,
|
|
222
|
+
);
|
|
223
|
+
const {
|
|
224
|
+
localePathRedirect,
|
|
225
|
+
languages = [],
|
|
226
|
+
localisedUrls,
|
|
227
|
+
} = localeDetectionOptions;
|
|
228
|
+
|
|
229
|
+
if (!localePathRedirect || languages.length === 0) {
|
|
230
|
+
return { entrypoint, routes };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const localisedUrlsConfig = resolveLocalisedUrlsConfig(localisedUrls);
|
|
234
|
+
if (!localisedUrlsConfig.enabled) {
|
|
235
|
+
return { entrypoint, routes };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
entrypoint,
|
|
240
|
+
routes: applyLocalisedUrlsToRoutes(
|
|
241
|
+
routes as (NestedRouteForCli | PageRoute)[],
|
|
242
|
+
languages,
|
|
243
|
+
localisedUrlsConfig.map,
|
|
244
|
+
),
|
|
245
|
+
};
|
|
246
|
+
});
|
|
247
|
+
|
|
206
248
|
api._internalServerPlugins(({ plugins }) => {
|
|
207
249
|
const { serverRoutes, metaName } = api.getAppContext();
|
|
208
250
|
const normalizedConfig = api.getNormalizedConfig();
|
package/src/runtime/I18nLink.tsx
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { Link, useInRouterContext, useParams } from '@modern-js/runtime/router';
|
|
2
1
|
import type React from 'react';
|
|
3
2
|
import { useModernI18n } from './context';
|
|
3
|
+
import { useI18nRouterAdapter } from './routerAdapter';
|
|
4
4
|
import { buildLocalizedUrl } from './utils';
|
|
5
5
|
|
|
6
6
|
export interface I18nLinkProps {
|
|
7
7
|
to: string;
|
|
8
8
|
children: React.ReactNode;
|
|
9
|
-
[key: string]: any;
|
|
9
|
+
[key: string]: any;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
/**
|
|
@@ -24,30 +24,25 @@ export interface I18nLinkProps {
|
|
|
24
24
|
* <I18nLink to="/">Home</I18nLink>
|
|
25
25
|
* ```
|
|
26
26
|
*/
|
|
27
|
-
// Use static imports to avoid breaking router tree-shaking. Detect router context via useInRouterContext.
|
|
28
|
-
const useRouterHooks = () => {
|
|
29
|
-
const inRouter = useInRouterContext();
|
|
30
|
-
return {
|
|
31
|
-
Link: inRouter ? Link : null,
|
|
32
|
-
params: inRouter ? useParams() : ({} as any),
|
|
33
|
-
hasRouter: inRouter,
|
|
34
|
-
};
|
|
35
|
-
};
|
|
36
|
-
|
|
37
27
|
export const I18nLink: React.FC<I18nLinkProps> = ({
|
|
38
28
|
to,
|
|
39
29
|
children,
|
|
40
30
|
...props
|
|
41
31
|
}) => {
|
|
42
|
-
const { Link, params, hasRouter } =
|
|
43
|
-
const { language, supportedLanguages } = useModernI18n();
|
|
32
|
+
const { Link, params, hasRouter } = useI18nRouterAdapter();
|
|
33
|
+
const { language, supportedLanguages, localisedUrls } = useModernI18n();
|
|
44
34
|
|
|
45
35
|
// Get the current language from context (which reflects the actual current language)
|
|
46
36
|
// URL params might be stale after language changes, so we prioritize the context language
|
|
47
37
|
const currentLang = language;
|
|
48
38
|
|
|
49
39
|
// Build the localized URL by adding language prefix
|
|
50
|
-
const localizedTo = buildLocalizedUrl(
|
|
40
|
+
const localizedTo = buildLocalizedUrl(
|
|
41
|
+
to,
|
|
42
|
+
currentLang,
|
|
43
|
+
supportedLanguages,
|
|
44
|
+
localisedUrls,
|
|
45
|
+
);
|
|
51
46
|
|
|
52
47
|
// In development mode, warn if used outside of :lang route context
|
|
53
48
|
if (process.env.NODE_ENV === 'development' && hasRouter && !params.lang) {
|
|
@@ -57,7 +52,6 @@ export const I18nLink: React.FC<I18nLinkProps> = ({
|
|
|
57
52
|
);
|
|
58
53
|
}
|
|
59
54
|
|
|
60
|
-
// If router is not available, render as a regular anchor tag
|
|
61
55
|
if (!hasRouter || !Link) {
|
|
62
56
|
return (
|
|
63
57
|
<a href={localizedTo} {...props}>
|
package/src/runtime/context.tsx
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
import { isBrowser } from '@modern-js/runtime';
|
|
2
2
|
import type { FC, ReactNode } from 'react';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
createContext,
|
|
5
|
+
useCallback,
|
|
6
|
+
useContext,
|
|
7
|
+
useEffect,
|
|
8
|
+
useMemo,
|
|
9
|
+
} from 'react';
|
|
10
|
+
import type { LocalisedUrlsOption } from '../shared/localisedUrls';
|
|
4
11
|
import type { I18nInstance } from './i18n';
|
|
5
12
|
import type { SdkBackend } from './i18n/backend/sdk-backend';
|
|
6
13
|
import { cacheUserLanguage } from './i18n/detection';
|
|
14
|
+
import { useI18nRouterAdapter } from './routerAdapter';
|
|
7
15
|
import {
|
|
8
16
|
buildLocalizedUrl,
|
|
9
17
|
detectLanguageFromPath,
|
|
10
18
|
getEntryPath,
|
|
11
19
|
shouldIgnoreRedirect,
|
|
12
|
-
useRouterHooks,
|
|
13
20
|
} from './utils';
|
|
14
21
|
|
|
15
22
|
export interface ModernI18nContextValue {
|
|
@@ -20,6 +27,7 @@ export interface ModernI18nContextValue {
|
|
|
20
27
|
languages?: string[];
|
|
21
28
|
localePathRedirect?: boolean;
|
|
22
29
|
ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean);
|
|
30
|
+
localisedUrls?: LocalisedUrlsOption;
|
|
23
31
|
// Callback to update language in context
|
|
24
32
|
updateLanguage?: (newLang: string) => void;
|
|
25
33
|
}
|
|
@@ -47,6 +55,7 @@ export interface UseModernI18nReturn {
|
|
|
47
55
|
changeLanguage: (newLang: string) => Promise<void>;
|
|
48
56
|
i18nInstance: I18nInstance;
|
|
49
57
|
supportedLanguages: string[];
|
|
58
|
+
localisedUrls?: LocalisedUrlsOption;
|
|
50
59
|
isLanguageSupported: (lang: string) => boolean;
|
|
51
60
|
// Indicates if translation resources for current language are ready to use
|
|
52
61
|
isResourcesReady: boolean;
|
|
@@ -77,15 +86,40 @@ export const useModernI18n = (): UseModernI18nReturn => {
|
|
|
77
86
|
languages,
|
|
78
87
|
localePathRedirect,
|
|
79
88
|
ignoreRedirectRoutes,
|
|
89
|
+
localisedUrls,
|
|
80
90
|
updateLanguage,
|
|
81
91
|
} = context;
|
|
82
92
|
|
|
83
|
-
|
|
84
|
-
|
|
93
|
+
const { navigate, location, hasRouter } = useI18nRouterAdapter();
|
|
94
|
+
|
|
95
|
+
const pathLanguage = useMemo(() => {
|
|
96
|
+
if (!localePathRedirect || !location?.pathname) {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
const detected = detectLanguageFromPath(
|
|
100
|
+
location.pathname,
|
|
101
|
+
languages || [],
|
|
102
|
+
localePathRedirect,
|
|
103
|
+
);
|
|
104
|
+
return detected.detected ? detected.language : undefined;
|
|
105
|
+
}, [languages, localePathRedirect, location?.pathname]);
|
|
106
|
+
|
|
107
|
+
const currentLanguage = pathLanguage || contextLanguage;
|
|
85
108
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
if (!pathLanguage || pathLanguage === contextLanguage) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
updateLanguage?.(pathLanguage);
|
|
115
|
+
i18nInstance?.setLang?.(pathLanguage);
|
|
116
|
+
void i18nInstance?.changeLanguage?.(pathLanguage);
|
|
117
|
+
|
|
118
|
+
if (isBrowser()) {
|
|
119
|
+
const detectionOptions = i18nInstance.options?.detection;
|
|
120
|
+
cacheUserLanguage(i18nInstance, pathLanguage, detectionOptions);
|
|
121
|
+
}
|
|
122
|
+
}, [contextLanguage, i18nInstance, pathLanguage, updateLanguage]);
|
|
89
123
|
|
|
90
124
|
/**
|
|
91
125
|
* Changes the current language and updates the URL accordingly.
|
|
@@ -147,6 +181,7 @@ export const useModernI18n = (): UseModernI18nReturn => {
|
|
|
147
181
|
relativePath,
|
|
148
182
|
newLang,
|
|
149
183
|
languages || [],
|
|
184
|
+
localisedUrls,
|
|
150
185
|
);
|
|
151
186
|
const newUrl =
|
|
152
187
|
entryPath + newPath + location.search + location.hash;
|
|
@@ -181,6 +216,7 @@ export const useModernI18n = (): UseModernI18nReturn => {
|
|
|
181
216
|
relativePath,
|
|
182
217
|
newLang,
|
|
183
218
|
languages || [],
|
|
219
|
+
localisedUrls,
|
|
184
220
|
);
|
|
185
221
|
const newUrl =
|
|
186
222
|
entryPath +
|
|
@@ -206,6 +242,7 @@ export const useModernI18n = (): UseModernI18nReturn => {
|
|
|
206
242
|
updateLanguage,
|
|
207
243
|
localePathRedirect,
|
|
208
244
|
ignoreRedirectRoutes,
|
|
245
|
+
localisedUrls,
|
|
209
246
|
languages,
|
|
210
247
|
hasRouter,
|
|
211
248
|
navigate,
|
|
@@ -275,6 +312,7 @@ export const useModernI18n = (): UseModernI18nReturn => {
|
|
|
275
312
|
changeLanguage,
|
|
276
313
|
i18nInstance,
|
|
277
314
|
supportedLanguages: languages || [],
|
|
315
|
+
localisedUrls,
|
|
278
316
|
isLanguageSupported,
|
|
279
317
|
isResourcesReady,
|
|
280
318
|
};
|
package/src/runtime/hooks.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { TRuntimeContext } from '@modern-js/runtime';
|
|
|
2
2
|
import { isBrowser } from '@modern-js/runtime';
|
|
3
3
|
import type React from 'react';
|
|
4
4
|
import { useEffect, useRef } from 'react';
|
|
5
|
+
import type { LocalisedUrlsOption } from '../shared/localisedUrls';
|
|
5
6
|
import type { I18nInstance } from './i18n';
|
|
6
7
|
import {
|
|
7
8
|
getI18nSdkBackendId,
|
|
@@ -9,13 +10,13 @@ import {
|
|
|
9
10
|
type I18nSdkResourcesLoadedEventDetail,
|
|
10
11
|
} from './i18n/backend/sdk-event';
|
|
11
12
|
import { cacheUserLanguage } from './i18n/detection';
|
|
13
|
+
import { useI18nRouterAdapter } from './routerAdapter';
|
|
12
14
|
import {
|
|
13
15
|
buildLocalizedUrl,
|
|
14
16
|
detectLanguageFromPath,
|
|
15
17
|
getEntryPath,
|
|
16
18
|
getPathname,
|
|
17
19
|
shouldIgnoreRedirect,
|
|
18
|
-
useRouterHooks,
|
|
19
20
|
} from './utils';
|
|
20
21
|
|
|
21
22
|
interface RuntimeContextWithI18n extends TRuntimeContext {
|
|
@@ -41,6 +42,7 @@ export function createContextValue(
|
|
|
41
42
|
languages: string[],
|
|
42
43
|
localePathRedirect: boolean,
|
|
43
44
|
ignoreRedirectRoutes: string[] | ((pathname: string) => boolean) | undefined,
|
|
45
|
+
localisedUrls: LocalisedUrlsOption | undefined,
|
|
44
46
|
setLang: (lang: string) => void,
|
|
45
47
|
) {
|
|
46
48
|
const instance = i18nInstance || createMinimalI18nInstance(lang);
|
|
@@ -51,6 +53,7 @@ export function createContextValue(
|
|
|
51
53
|
languages,
|
|
52
54
|
localePathRedirect,
|
|
53
55
|
ignoreRedirectRoutes,
|
|
56
|
+
localisedUrls,
|
|
54
57
|
updateLanguage: setLang,
|
|
55
58
|
};
|
|
56
59
|
}
|
|
@@ -162,10 +165,10 @@ export function useClientSideRedirect(
|
|
|
162
165
|
languages: string[],
|
|
163
166
|
fallbackLanguage: string,
|
|
164
167
|
ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean),
|
|
168
|
+
localisedUrls?: LocalisedUrlsOption,
|
|
165
169
|
) {
|
|
166
170
|
const hasRedirectedRef = useRef(false);
|
|
167
|
-
|
|
168
|
-
const { navigate, location, hasRouter } = useRouterHooks();
|
|
171
|
+
const { navigate, location, hasRouter } = useI18nRouterAdapter();
|
|
169
172
|
|
|
170
173
|
useEffect(() => {
|
|
171
174
|
if (process.env.MODERN_TARGET !== 'browser') {
|
|
@@ -220,7 +223,12 @@ export function useClientSideRedirect(
|
|
|
220
223
|
const targetLanguage =
|
|
221
224
|
i18nInstance.language || fallbackLanguage || languages[0] || 'en';
|
|
222
225
|
|
|
223
|
-
const newPath = buildLocalizedUrl(
|
|
226
|
+
const newPath = buildLocalizedUrl(
|
|
227
|
+
relativePath,
|
|
228
|
+
targetLanguage,
|
|
229
|
+
languages,
|
|
230
|
+
localisedUrls,
|
|
231
|
+
);
|
|
224
232
|
const newUrl = entryPath + newPath + currentSearch + currentHash;
|
|
225
233
|
|
|
226
234
|
if (newUrl !== currentPathname + currentSearch + currentHash) {
|
|
@@ -244,6 +252,7 @@ export function useClientSideRedirect(
|
|
|
244
252
|
languages,
|
|
245
253
|
fallbackLanguage,
|
|
246
254
|
ignoreRedirectRoutes,
|
|
255
|
+
localisedUrls,
|
|
247
256
|
]);
|
|
248
257
|
}
|
|
249
258
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export const DEFAULT_I18NEXT_BACKEND_OPTIONS = {
|
|
2
|
-
loadPath: './locales/{{lng}}/{{ns}}.json',
|
|
3
|
-
addPath: './locales/{{lng}}/{{ns}}.json',
|
|
2
|
+
loadPath: './config/public/locales/{{lng}}/{{ns}}.json',
|
|
3
|
+
addPath: './config/public/locales/{{lng}}/{{ns}}.json',
|
|
4
4
|
};
|
|
5
5
|
|
|
6
6
|
function convertPath(path: string | undefined): string | undefined {
|
|
@@ -15,7 +15,9 @@ function convertPath(path: string | undefined): string | undefined {
|
|
|
15
15
|
}
|
|
16
16
|
// If it's an absolute path (starts with /), convert to relative path
|
|
17
17
|
if (path.startsWith('/')) {
|
|
18
|
-
return
|
|
18
|
+
return typeof window === 'undefined'
|
|
19
|
+
? path
|
|
20
|
+
: `${window.__assetPrefix__ || ''}${path}`;
|
|
19
21
|
}
|
|
20
22
|
return path;
|
|
21
23
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import Backend from 'i18next-fs-backend';
|
|
1
|
+
import Backend from 'i18next-fs-backend/cjs';
|
|
2
2
|
import type { ExtendedBackendOptions } from '../../../shared/type';
|
|
3
3
|
import type { I18nInstance } from '../instance';
|
|
4
4
|
import { useI18nextBackendCommon } from './middleware.common';
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import type { BaseBackendOptions } from '../../shared/type';
|
|
2
2
|
|
|
3
|
-
type ReactI18nextModule = typeof import('react-i18next');
|
|
4
|
-
type InitReactI18next = ReactI18nextModule['initReactI18next'];
|
|
5
|
-
type I18nextProviderComponent = ReactI18nextModule['I18nextProvider'];
|
|
6
|
-
|
|
7
3
|
export interface I18nResourceStore {
|
|
8
4
|
data?: {
|
|
9
5
|
[language: string]: {
|
|
@@ -171,15 +167,6 @@ async function createI18nextInstance(): Promise<I18nInstance | null> {
|
|
|
171
167
|
}
|
|
172
168
|
}
|
|
173
169
|
|
|
174
|
-
async function tryImportReactI18next(): Promise<ReactI18nextModule | null> {
|
|
175
|
-
try {
|
|
176
|
-
const reactI18next = await import('react-i18next');
|
|
177
|
-
return reactI18next;
|
|
178
|
-
} catch (error) {
|
|
179
|
-
return null;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
170
|
export function getI18nextInstanceForProvider(
|
|
184
171
|
instance: I18nInstance | any,
|
|
185
172
|
): any {
|
|
@@ -213,19 +200,3 @@ export async function getI18nInstance(
|
|
|
213
200
|
|
|
214
201
|
throw new Error('No i18n instance found');
|
|
215
202
|
}
|
|
216
|
-
|
|
217
|
-
export async function getInitReactI18next(): Promise<InitReactI18next | null> {
|
|
218
|
-
const reactI18nextModule = await tryImportReactI18next();
|
|
219
|
-
if (reactI18nextModule) {
|
|
220
|
-
return reactI18nextModule.initReactI18next;
|
|
221
|
-
}
|
|
222
|
-
return null;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
export async function getI18nextProvider(): Promise<I18nextProviderComponent | null> {
|
|
226
|
-
const reactI18nextModule = await tryImportReactI18next();
|
|
227
|
-
if (reactI18nextModule) {
|
|
228
|
-
return reactI18nextModule.I18nextProvider;
|
|
229
|
-
}
|
|
230
|
-
return null;
|
|
231
|
-
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
|
|
3
|
+
type ReactI18nextModule = typeof import('react-i18next');
|
|
4
|
+
|
|
5
|
+
interface ReactI18nextIntegration {
|
|
6
|
+
I18nextProvider: React.ComponentType<any> | null;
|
|
7
|
+
initReactI18next: any | null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async function tryImportReactI18next(): Promise<ReactI18nextModule | null> {
|
|
11
|
+
try {
|
|
12
|
+
return (await import('react-i18next')) as ReactI18nextModule;
|
|
13
|
+
} catch (error) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function getReactI18nextIntegration(): Promise<ReactI18nextIntegration> {
|
|
19
|
+
const reactI18nextModule = await tryImportReactI18next();
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
I18nextProvider: reactI18nextModule?.I18nextProvider ?? null,
|
|
23
|
+
initReactI18next: reactI18nextModule?.initReactI18next ?? null,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -238,32 +238,10 @@ export const initializeI18nInstance = async (
|
|
|
238
238
|
}
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
// 2. Asynchronously loads from the second backend (SDK) and updates the store
|
|
246
|
-
// 3. Triggers 'loaded' event when SDK resources are loaded, which causes React to re-render
|
|
247
|
-
//
|
|
248
|
-
// Note: i18next.init() returns a Promise that resolves when the first backend loads.
|
|
249
|
-
// For chained backend, it does NOT wait for the second backend (SDK) to load.
|
|
250
|
-
// The SDK backend loads asynchronously and triggers 'loaded' event automatically.
|
|
251
|
-
const defaultNS =
|
|
252
|
-
initOptions.defaultNS || initOptions.ns || 'translation';
|
|
253
|
-
const ns = Array.isArray(defaultNS) ? defaultNS[0] : defaultNS;
|
|
254
|
-
|
|
255
|
-
let retries = 20;
|
|
256
|
-
while (retries > 0) {
|
|
257
|
-
// Get the actual i18next instance to access store property
|
|
258
|
-
const actualInstance = getActualI18nextInstance(i18nInstance);
|
|
259
|
-
const store = (actualInstance as any).store;
|
|
260
|
-
if (store?.data?.[finalLanguage]?.[ns]) {
|
|
261
|
-
break;
|
|
262
|
-
}
|
|
263
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
264
|
-
retries--;
|
|
265
|
-
}
|
|
266
|
-
}
|
|
241
|
+
// i18next.init() is the synchronization boundary for the primary backend.
|
|
242
|
+
// Chained SDK refreshes update the store through their own loaded events and
|
|
243
|
+
// must not block SSR HTML, otherwise missing/edge-only resources add fixed
|
|
244
|
+
// latency to every route render.
|
|
267
245
|
}
|
|
268
246
|
};
|
|
269
247
|
|