@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
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
const LOCALE_PARAM_NAMES = new Set([
|
|
2
|
+
'lang',
|
|
3
|
+
'locale',
|
|
4
|
+
'language'
|
|
5
|
+
]);
|
|
6
|
+
const normalisePathPattern = (path)=>{
|
|
7
|
+
const withoutDuplicateSlashes = path.replace(/\/+/g, '/');
|
|
8
|
+
const withLeadingSlash = withoutDuplicateSlashes.startsWith('/') ? withoutDuplicateSlashes : `/${withoutDuplicateSlashes}`;
|
|
9
|
+
const withoutTrailingSlash = withLeadingSlash.length > 1 ? withLeadingSlash.replace(/\/+$/, '') : withLeadingSlash;
|
|
10
|
+
return withoutTrailingSlash.replace(/\[(.+?)\]/g, ':$1');
|
|
11
|
+
};
|
|
12
|
+
const normaliseRoutePath = (path)=>{
|
|
13
|
+
const normalized = normalisePathPattern(path);
|
|
14
|
+
return '/' === normalized ? '' : normalized.slice(1);
|
|
15
|
+
};
|
|
16
|
+
const getLocaleParamSegment = (segment)=>{
|
|
17
|
+
if (!segment.startsWith(':')) return null;
|
|
18
|
+
const paramName = segment.slice(1).replace(/\?$/, '');
|
|
19
|
+
return LOCALE_PARAM_NAMES.has(paramName) ? segment : null;
|
|
20
|
+
};
|
|
21
|
+
const splitPathSegments = (path)=>{
|
|
22
|
+
if (!path) return [];
|
|
23
|
+
return normalisePathPattern(path).split('/').filter(Boolean);
|
|
24
|
+
};
|
|
25
|
+
const stripLeadingLocaleParam = (path)=>{
|
|
26
|
+
const segments = splitPathSegments(path);
|
|
27
|
+
const leadingLocaleParam = getLocaleParamSegment(segments[0] || '');
|
|
28
|
+
if (!leadingLocaleParam) return path;
|
|
29
|
+
const remainingPath = segments.slice(1).join('/');
|
|
30
|
+
return remainingPath ? `/${remainingPath}` : void 0;
|
|
31
|
+
};
|
|
32
|
+
const getLeadingLocaleParam = (path)=>{
|
|
33
|
+
const segments = splitPathSegments(path);
|
|
34
|
+
return getLocaleParamSegment(segments[0] || '');
|
|
35
|
+
};
|
|
36
|
+
const resolveLocalisedUrlsConfig = (option)=>{
|
|
37
|
+
if (false === option) return {
|
|
38
|
+
enabled: false,
|
|
39
|
+
map: {}
|
|
40
|
+
};
|
|
41
|
+
if (option && 'object' == typeof option) return {
|
|
42
|
+
enabled: true,
|
|
43
|
+
map: option
|
|
44
|
+
};
|
|
45
|
+
return {
|
|
46
|
+
enabled: true,
|
|
47
|
+
map: {}
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
const isLocalisableRoutePath = (path)=>{
|
|
51
|
+
const pathWithoutLocale = stripLeadingLocaleParam(path);
|
|
52
|
+
if (!pathWithoutLocale || '/' === pathWithoutLocale || '*' === pathWithoutLocale) return false;
|
|
53
|
+
return true;
|
|
54
|
+
};
|
|
55
|
+
const joinPath = (parentPath, routePath)=>{
|
|
56
|
+
if (!isLocalisableRoutePath(routePath)) return parentPath;
|
|
57
|
+
const segment = normaliseRoutePath(stripLeadingLocaleParam(routePath) || '');
|
|
58
|
+
return normalisePathPattern(`${parentPath}/${segment}`);
|
|
59
|
+
};
|
|
60
|
+
const ensureLocalisedUrlsForPath = (canonicalPath, languages, localisedUrls)=>{
|
|
61
|
+
const entry = localisedUrls[canonicalPath];
|
|
62
|
+
if (!entry) throw new Error(`localisedUrls is enabled, but route "${canonicalPath}" does not define localised URLs for languages: ${languages.join(', ')}. Add localisedUrls["${canonicalPath}"] or set localeDetection.localisedUrls to false.`);
|
|
63
|
+
const missingLanguages = languages.filter((language)=>!entry[language]);
|
|
64
|
+
if (missingLanguages.length > 0) throw new Error(`localisedUrls["${canonicalPath}"] is missing languages: ${missingLanguages.join(', ')}. Every configured language must have a localised URL.`);
|
|
65
|
+
return entry;
|
|
66
|
+
};
|
|
67
|
+
const validateLocalisedUrls = (routes, languages, localisedUrls)=>{
|
|
68
|
+
const visit = (route, parentPath)=>{
|
|
69
|
+
const canonicalPath = joinPath(parentPath, route.path);
|
|
70
|
+
if (isLocalisableRoutePath(route.path)) ensureLocalisedUrlsForPath(canonicalPath, languages, localisedUrls);
|
|
71
|
+
if ('children' in route && route.children) route.children.forEach((child)=>visit(child, canonicalPath));
|
|
72
|
+
};
|
|
73
|
+
routes.forEach((route)=>visit(route, ''));
|
|
74
|
+
};
|
|
75
|
+
const getLocalisedRoutePaths = (canonicalPath, parentLocalisedPaths, languages, entry)=>{
|
|
76
|
+
const paths = languages.map((language)=>{
|
|
77
|
+
const fullPath = normalisePathPattern(entry[language]);
|
|
78
|
+
const parentPath = normalisePathPattern(parentLocalisedPaths[language] || '/');
|
|
79
|
+
if ('/' === parentPath) return normaliseRoutePath(fullPath) || void 0;
|
|
80
|
+
const parentPrefix = `${parentPath}/`;
|
|
81
|
+
if (!fullPath.startsWith(parentPrefix)) throw new Error(`localisedUrls["${canonicalPath}"].${language} must be nested under "${parentPath}" because its parent route is localised there.`);
|
|
82
|
+
return normaliseRoutePath(fullPath.slice(parentPath.length));
|
|
83
|
+
});
|
|
84
|
+
return Array.from(new Set(paths.filter(Boolean)));
|
|
85
|
+
};
|
|
86
|
+
const transformLocalisedRoute = (route, parentCanonicalPath, parentLocalisedPaths, languages, localisedUrls)=>{
|
|
87
|
+
const canonicalPath = joinPath(parentCanonicalPath, route.path);
|
|
88
|
+
const localisedUrlEntry = isLocalisableRoutePath(route.path) ? ensureLocalisedUrlsForPath(canonicalPath, languages, localisedUrls) : void 0;
|
|
89
|
+
const routeLocalisedPaths = localisedUrlEntry ? languages.reduce((acc, language)=>{
|
|
90
|
+
acc[language] = normalisePathPattern(localisedUrlEntry[language]);
|
|
91
|
+
return acc;
|
|
92
|
+
}, {}) : parentLocalisedPaths;
|
|
93
|
+
const children = 'children' in route && route.children ? route.children.flatMap((child)=>transformLocalisedRoute(child, canonicalPath, routeLocalisedPaths, languages, localisedUrls)) : void 0;
|
|
94
|
+
const baseRoute = {
|
|
95
|
+
...route,
|
|
96
|
+
...children ? {
|
|
97
|
+
children
|
|
98
|
+
} : {}
|
|
99
|
+
};
|
|
100
|
+
if (!localisedUrlEntry) return [
|
|
101
|
+
baseRoute
|
|
102
|
+
];
|
|
103
|
+
return getLocalisedRoutePaths(canonicalPath, parentLocalisedPaths, languages, localisedUrlEntry).map((localisedPath, index)=>cloneRouteWithLocalisedPath(baseRoute, localisedPath, index));
|
|
104
|
+
};
|
|
105
|
+
const legalRouteIdPart = (value)=>value.replace(/[^a-zA-Z0-9_$-]+/g, '_').replace(/^_+|_+$/g, '') || 'index';
|
|
106
|
+
const suffixRouteIds = (route, suffix)=>{
|
|
107
|
+
const children = 'children' in route && route.children ? route.children.map((child)=>suffixRouteIds(child, suffix)) : void 0;
|
|
108
|
+
return {
|
|
109
|
+
...route,
|
|
110
|
+
...route.id ? {
|
|
111
|
+
id: `${route.id}__localised_${suffix}`
|
|
112
|
+
} : {},
|
|
113
|
+
...children ? {
|
|
114
|
+
children
|
|
115
|
+
} : {}
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
const cloneRouteWithLocalisedPath = (route, path, index)=>{
|
|
119
|
+
const leadingLocaleParam = getLeadingLocaleParam(route.path);
|
|
120
|
+
const localisedPath = leadingLocaleParam ? normaliseRoutePath(`${leadingLocaleParam}/${path}`) : path;
|
|
121
|
+
const routeWithPath = {
|
|
122
|
+
...route,
|
|
123
|
+
path: localisedPath
|
|
124
|
+
};
|
|
125
|
+
return 0 === index ? routeWithPath : suffixRouteIds(routeWithPath, legalRouteIdPart(localisedPath));
|
|
126
|
+
};
|
|
127
|
+
const applyLocalisedUrlsToRoutes = (routes, languages, localisedUrls)=>{
|
|
128
|
+
const rootLocalisedPaths = languages.reduce((acc, language)=>{
|
|
129
|
+
acc[language] = '/';
|
|
130
|
+
return acc;
|
|
131
|
+
}, {});
|
|
132
|
+
validateLocalisedUrls(routes, languages, localisedUrls);
|
|
133
|
+
return routes.flatMap((route)=>transformLocalisedRoute(route, '', rootLocalisedPaths, languages, localisedUrls));
|
|
134
|
+
};
|
|
135
|
+
const escapeRegExp = (value)=>value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
136
|
+
const getParamName = (segment)=>segment.slice(1).replace(/\?$/, '');
|
|
137
|
+
const compilePathPattern = (pattern)=>{
|
|
138
|
+
const names = [];
|
|
139
|
+
const segments = normalisePathPattern(pattern).split('/').filter(Boolean);
|
|
140
|
+
const source = segments.map((segment)=>{
|
|
141
|
+
if (segment.startsWith(':')) {
|
|
142
|
+
names.push(getParamName(segment));
|
|
143
|
+
const paramPattern = '([^/]+)';
|
|
144
|
+
return segment.endsWith('?') ? `(?:/${paramPattern})?` : `/${paramPattern}`;
|
|
145
|
+
}
|
|
146
|
+
if ('*' === segment) {
|
|
147
|
+
names.push('*');
|
|
148
|
+
return '/(.*)';
|
|
149
|
+
}
|
|
150
|
+
return `/${escapeRegExp(segment)}`;
|
|
151
|
+
}).join('');
|
|
152
|
+
return {
|
|
153
|
+
names,
|
|
154
|
+
regexp: new RegExp(`^${source || '/'}$`)
|
|
155
|
+
};
|
|
156
|
+
};
|
|
157
|
+
const matchPathPattern = (pathname, pattern)=>{
|
|
158
|
+
const { names, regexp } = compilePathPattern(pattern);
|
|
159
|
+
const match = regexp.exec(normalisePathPattern(pathname));
|
|
160
|
+
if (!match) return null;
|
|
161
|
+
return names.reduce((params, name, index)=>{
|
|
162
|
+
params[name] = decodeURIComponent(match[index + 1] || '');
|
|
163
|
+
return params;
|
|
164
|
+
}, {});
|
|
165
|
+
};
|
|
166
|
+
const buildPathFromPattern = (pattern, params)=>{
|
|
167
|
+
const segments = normalisePathPattern(pattern).split('/').filter(Boolean);
|
|
168
|
+
const path = segments.map((segment)=>{
|
|
169
|
+
if (segment.startsWith(':')) {
|
|
170
|
+
const param = params[getParamName(segment)];
|
|
171
|
+
return param ? encodeURIComponent(param) : '';
|
|
172
|
+
}
|
|
173
|
+
if ('*' === segment) return params['*'] || '';
|
|
174
|
+
return segment;
|
|
175
|
+
}).filter(Boolean).join('/');
|
|
176
|
+
return `/${path}`;
|
|
177
|
+
};
|
|
178
|
+
const resolveLocalisedPath = (pathname, targetLanguage, languages, localisedUrls)=>{
|
|
179
|
+
const normalizedPathname = normalisePathPattern(pathname);
|
|
180
|
+
for (const localisedUrlEntry of Object.values(localisedUrls)){
|
|
181
|
+
const targetPattern = localisedUrlEntry[targetLanguage];
|
|
182
|
+
if (targetPattern) for (const language of languages){
|
|
183
|
+
const sourcePattern = localisedUrlEntry[language];
|
|
184
|
+
if (!sourcePattern) continue;
|
|
185
|
+
const params = matchPathPattern(normalizedPathname, sourcePattern);
|
|
186
|
+
if (params) return buildPathFromPattern(targetPattern, params);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return normalizedPathname;
|
|
190
|
+
};
|
|
191
|
+
export { applyLocalisedUrlsToRoutes, normalisePathPattern, resolveLocalisedPath, resolveLocalisedUrlsConfig, validateLocalisedUrls };
|
|
@@ -2,6 +2,7 @@ import "node:module";
|
|
|
2
2
|
import { getPublicDirRoutePrefixes } from "@modern-js/server-core";
|
|
3
3
|
import fs from "fs";
|
|
4
4
|
import path from "path";
|
|
5
|
+
import { applyLocalisedUrlsToRoutes, resolveLocalisedUrlsConfig } from "../shared/localisedUrls.mjs";
|
|
5
6
|
import { getBackendOptions, getLocaleDetectionOptions } from "../shared/utils.mjs";
|
|
6
7
|
function hasJsonFiles(dirPath) {
|
|
7
8
|
try {
|
|
@@ -81,6 +82,27 @@ const i18nPlugin = (options = {})=>({
|
|
|
81
82
|
plugins
|
|
82
83
|
};
|
|
83
84
|
});
|
|
85
|
+
api.modifyFileSystemRoutes(({ entrypoint, routes })=>{
|
|
86
|
+
if (!localeDetection) return {
|
|
87
|
+
entrypoint,
|
|
88
|
+
routes
|
|
89
|
+
};
|
|
90
|
+
const localeDetectionOptions = getLocaleDetectionOptions(entrypoint.entryName, localeDetection);
|
|
91
|
+
const { localePathRedirect, languages = [], localisedUrls } = localeDetectionOptions;
|
|
92
|
+
if (!localePathRedirect || 0 === languages.length) return {
|
|
93
|
+
entrypoint,
|
|
94
|
+
routes
|
|
95
|
+
};
|
|
96
|
+
const localisedUrlsConfig = resolveLocalisedUrlsConfig(localisedUrls);
|
|
97
|
+
if (!localisedUrlsConfig.enabled) return {
|
|
98
|
+
entrypoint,
|
|
99
|
+
routes
|
|
100
|
+
};
|
|
101
|
+
return {
|
|
102
|
+
entrypoint,
|
|
103
|
+
routes: applyLocalisedUrlsToRoutes(routes, languages, localisedUrlsConfig.map)
|
|
104
|
+
};
|
|
105
|
+
});
|
|
84
106
|
api._internalServerPlugins(({ plugins })=>{
|
|
85
107
|
const { serverRoutes, metaName } = api.getAppContext();
|
|
86
108
|
const normalizedConfig = api.getNormalizedConfig();
|
|
@@ -1,21 +1,13 @@
|
|
|
1
1
|
import "node:module";
|
|
2
2
|
import { jsx } from "react/jsx-runtime";
|
|
3
|
-
import { Link as router_Link, useInRouterContext, useParams } from "@modern-js/runtime/router";
|
|
4
3
|
import { useModernI18n } from "./context.mjs";
|
|
4
|
+
import { useI18nRouterAdapter } from "./routerAdapter.mjs";
|
|
5
5
|
import { buildLocalizedUrl } from "./utils.mjs";
|
|
6
|
-
const useRouterHooks = ()=>{
|
|
7
|
-
const inRouter = useInRouterContext();
|
|
8
|
-
return {
|
|
9
|
-
Link: inRouter ? router_Link : null,
|
|
10
|
-
params: inRouter ? useParams() : {},
|
|
11
|
-
hasRouter: inRouter
|
|
12
|
-
};
|
|
13
|
-
};
|
|
14
6
|
const I18nLink = ({ to, children, ...props })=>{
|
|
15
|
-
const { Link, params, hasRouter } =
|
|
16
|
-
const { language, supportedLanguages } = useModernI18n();
|
|
7
|
+
const { Link, params, hasRouter } = useI18nRouterAdapter();
|
|
8
|
+
const { language, supportedLanguages, localisedUrls } = useModernI18n();
|
|
17
9
|
const currentLang = language;
|
|
18
|
-
const localizedTo = buildLocalizedUrl(to, currentLang, supportedLanguages);
|
|
10
|
+
const localizedTo = buildLocalizedUrl(to, currentLang, supportedLanguages, localisedUrls);
|
|
19
11
|
if ('development' === process.env.NODE_ENV && hasRouter && !params.lang) console.warn("I18nLink is being used outside of a :lang dynamic route context. This may cause unexpected behavior. Please ensure I18nLink is used within a route that has a :lang parameter.");
|
|
20
12
|
if (!hasRouter || !Link) return /*#__PURE__*/ jsx("a", {
|
|
21
13
|
href: localizedTo,
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import "node:module";
|
|
2
2
|
import { jsx } from "react/jsx-runtime";
|
|
3
3
|
import { isBrowser } from "@modern-js/runtime";
|
|
4
|
-
import { createContext, useCallback, useContext, useMemo } from "react";
|
|
4
|
+
import { createContext, useCallback, useContext, useEffect, useMemo } from "react";
|
|
5
5
|
import { cacheUserLanguage } from "./i18n/detection/index.mjs";
|
|
6
|
-
import {
|
|
6
|
+
import { useI18nRouterAdapter } from "./routerAdapter.mjs";
|
|
7
|
+
import { buildLocalizedUrl, detectLanguageFromPath, getEntryPath, shouldIgnoreRedirect } from "./utils.mjs";
|
|
7
8
|
const ModernI18nContext = /*#__PURE__*/ createContext(null);
|
|
8
9
|
const ModernI18nProvider = ({ children, value })=>/*#__PURE__*/ jsx(ModernI18nContext.Provider, {
|
|
9
10
|
value: value,
|
|
@@ -12,9 +13,33 @@ const ModernI18nProvider = ({ children, value })=>/*#__PURE__*/ jsx(ModernI18nCo
|
|
|
12
13
|
const useModernI18n = ()=>{
|
|
13
14
|
const context = useContext(ModernI18nContext);
|
|
14
15
|
if (!context) throw new Error('useModernI18n must be used within a ModernI18nProvider');
|
|
15
|
-
const { language: contextLanguage, i18nInstance, languages, localePathRedirect, ignoreRedirectRoutes, updateLanguage } = context;
|
|
16
|
-
const { navigate, location, hasRouter } =
|
|
17
|
-
const
|
|
16
|
+
const { language: contextLanguage, i18nInstance, languages, localePathRedirect, ignoreRedirectRoutes, localisedUrls, updateLanguage } = context;
|
|
17
|
+
const { navigate, location, hasRouter } = useI18nRouterAdapter();
|
|
18
|
+
const pathLanguage = useMemo(()=>{
|
|
19
|
+
if (!localePathRedirect || !location?.pathname) return;
|
|
20
|
+
const detected = detectLanguageFromPath(location.pathname, languages || [], localePathRedirect);
|
|
21
|
+
return detected.detected ? detected.language : void 0;
|
|
22
|
+
}, [
|
|
23
|
+
languages,
|
|
24
|
+
localePathRedirect,
|
|
25
|
+
location?.pathname
|
|
26
|
+
]);
|
|
27
|
+
const currentLanguage = pathLanguage || contextLanguage;
|
|
28
|
+
useEffect(()=>{
|
|
29
|
+
if (!pathLanguage || pathLanguage === contextLanguage) return;
|
|
30
|
+
updateLanguage?.(pathLanguage);
|
|
31
|
+
i18nInstance?.setLang?.(pathLanguage);
|
|
32
|
+
i18nInstance?.changeLanguage?.(pathLanguage);
|
|
33
|
+
if (isBrowser()) {
|
|
34
|
+
const detectionOptions = i18nInstance.options?.detection;
|
|
35
|
+
cacheUserLanguage(i18nInstance, pathLanguage, detectionOptions);
|
|
36
|
+
}
|
|
37
|
+
}, [
|
|
38
|
+
contextLanguage,
|
|
39
|
+
i18nInstance,
|
|
40
|
+
pathLanguage,
|
|
41
|
+
updateLanguage
|
|
42
|
+
]);
|
|
18
43
|
const changeLanguage = useCallback(async (newLang)=>{
|
|
19
44
|
try {
|
|
20
45
|
if (!newLang || 'string' != typeof newLang) throw new Error('Language must be a non-empty string');
|
|
@@ -31,7 +56,7 @@ const useModernI18n = ()=>{
|
|
|
31
56
|
const pathLanguage = detectLanguageFromPath(currentPath, languages || [], localePathRedirect);
|
|
32
57
|
if (pathLanguage.detected && pathLanguage.language === newLang) return;
|
|
33
58
|
if (!shouldIgnoreRedirect(relativePath, languages || [], ignoreRedirectRoutes)) {
|
|
34
|
-
const newPath = buildLocalizedUrl(relativePath, newLang, languages || []);
|
|
59
|
+
const newPath = buildLocalizedUrl(relativePath, newLang, languages || [], localisedUrls);
|
|
35
60
|
const newUrl = entryPath + newPath + location.search + location.hash;
|
|
36
61
|
await navigate(newUrl, {
|
|
37
62
|
replace: true
|
|
@@ -44,7 +69,7 @@ const useModernI18n = ()=>{
|
|
|
44
69
|
const pathLanguage = detectLanguageFromPath(currentPath, languages || [], localePathRedirect);
|
|
45
70
|
if (pathLanguage.detected && pathLanguage.language === newLang) return;
|
|
46
71
|
if (!shouldIgnoreRedirect(relativePath, languages || [], ignoreRedirectRoutes)) {
|
|
47
|
-
const newPath = buildLocalizedUrl(relativePath, newLang, languages || []);
|
|
72
|
+
const newPath = buildLocalizedUrl(relativePath, newLang, languages || [], localisedUrls);
|
|
48
73
|
const newUrl = entryPath + newPath + window.location.search + window.location.hash;
|
|
49
74
|
window.history.pushState(null, '', newUrl);
|
|
50
75
|
}
|
|
@@ -59,6 +84,7 @@ const useModernI18n = ()=>{
|
|
|
59
84
|
updateLanguage,
|
|
60
85
|
localePathRedirect,
|
|
61
86
|
ignoreRedirectRoutes,
|
|
87
|
+
localisedUrls,
|
|
62
88
|
languages,
|
|
63
89
|
hasRouter,
|
|
64
90
|
navigate,
|
|
@@ -99,6 +125,7 @@ const useModernI18n = ()=>{
|
|
|
99
125
|
changeLanguage,
|
|
100
126
|
i18nInstance,
|
|
101
127
|
supportedLanguages: languages || [],
|
|
128
|
+
localisedUrls,
|
|
102
129
|
isLanguageSupported,
|
|
103
130
|
isResourcesReady
|
|
104
131
|
};
|
|
@@ -3,7 +3,8 @@ import { isBrowser } from "@modern-js/runtime";
|
|
|
3
3
|
import { useEffect, useRef } from "react";
|
|
4
4
|
import { I18N_SDK_RESOURCES_LOADED_EVENT, getI18nSdkBackendId } from "./i18n/backend/sdk-event.mjs";
|
|
5
5
|
import { cacheUserLanguage } from "./i18n/detection/index.mjs";
|
|
6
|
-
import {
|
|
6
|
+
import { useI18nRouterAdapter } from "./routerAdapter.mjs";
|
|
7
|
+
import { buildLocalizedUrl, detectLanguageFromPath, getEntryPath, getPathname, shouldIgnoreRedirect } from "./utils.mjs";
|
|
7
8
|
function createMinimalI18nInstance(language) {
|
|
8
9
|
const minimalInstance = {
|
|
9
10
|
language,
|
|
@@ -15,7 +16,7 @@ function createMinimalI18nInstance(language) {
|
|
|
15
16
|
};
|
|
16
17
|
return minimalInstance;
|
|
17
18
|
}
|
|
18
|
-
function createContextValue(lang, i18nInstance, entryName, languages, localePathRedirect, ignoreRedirectRoutes, setLang) {
|
|
19
|
+
function createContextValue(lang, i18nInstance, entryName, languages, localePathRedirect, ignoreRedirectRoutes, localisedUrls, setLang) {
|
|
19
20
|
const instance = i18nInstance || createMinimalI18nInstance(lang);
|
|
20
21
|
return {
|
|
21
22
|
language: lang,
|
|
@@ -24,6 +25,7 @@ function createContextValue(lang, i18nInstance, entryName, languages, localePath
|
|
|
24
25
|
languages,
|
|
25
26
|
localePathRedirect,
|
|
26
27
|
ignoreRedirectRoutes,
|
|
28
|
+
localisedUrls,
|
|
27
29
|
updateLanguage: setLang
|
|
28
30
|
};
|
|
29
31
|
}
|
|
@@ -73,9 +75,9 @@ function useSdkResourcesLoader(i18nInstance, setForceUpdate) {
|
|
|
73
75
|
setForceUpdate
|
|
74
76
|
]);
|
|
75
77
|
}
|
|
76
|
-
function useClientSideRedirect(i18nInstance, localePathRedirect, languages, fallbackLanguage, ignoreRedirectRoutes) {
|
|
78
|
+
function useClientSideRedirect(i18nInstance, localePathRedirect, languages, fallbackLanguage, ignoreRedirectRoutes, localisedUrls) {
|
|
77
79
|
const hasRedirectedRef = useRef(false);
|
|
78
|
-
const { navigate, location, hasRouter } =
|
|
80
|
+
const { navigate, location, hasRouter } = useI18nRouterAdapter();
|
|
79
81
|
useEffect(()=>{
|
|
80
82
|
if ('browser' !== process.env.MODERN_TARGET) return;
|
|
81
83
|
if (!localePathRedirect || !i18nInstance) return;
|
|
@@ -94,7 +96,7 @@ function useClientSideRedirect(i18nInstance, localePathRedirect, languages, fall
|
|
|
94
96
|
const pathDetection = detectLanguageFromPath(currentPathname, languages, localePathRedirect);
|
|
95
97
|
if (pathDetection.detected) return;
|
|
96
98
|
const targetLanguage = i18nInstance.language || fallbackLanguage || languages[0] || 'en';
|
|
97
|
-
const newPath = buildLocalizedUrl(relativePath, targetLanguage, languages);
|
|
99
|
+
const newPath = buildLocalizedUrl(relativePath, targetLanguage, languages, localisedUrls);
|
|
98
100
|
const newUrl = entryPath + newPath + currentSearch + currentHash;
|
|
99
101
|
if (newUrl !== currentPathname + currentSearch + currentHash) {
|
|
100
102
|
hasRedirectedRef.current = true;
|
|
@@ -111,7 +113,8 @@ function useClientSideRedirect(i18nInstance, localePathRedirect, languages, fall
|
|
|
111
113
|
i18nInstance,
|
|
112
114
|
languages,
|
|
113
115
|
fallbackLanguage,
|
|
114
|
-
ignoreRedirectRoutes
|
|
116
|
+
ignoreRedirectRoutes,
|
|
117
|
+
localisedUrls
|
|
115
118
|
]);
|
|
116
119
|
}
|
|
117
120
|
function useLanguageSync(i18nInstance, localePathRedirect, languages, runtimeContextRef, prevLangRef, setLang) {
|
|
@@ -5,7 +5,7 @@ const DEFAULT_I18NEXT_BACKEND_OPTIONS = {
|
|
|
5
5
|
};
|
|
6
6
|
function convertPath(path) {
|
|
7
7
|
if (!path) return path;
|
|
8
|
-
if (path.startsWith('/')) return `${window.__assetPrefix__ || ''}${path}`;
|
|
8
|
+
if (path.startsWith('/')) return "u" < typeof window ? path : `${window.__assetPrefix__ || ''}${path}`;
|
|
9
9
|
return path;
|
|
10
10
|
}
|
|
11
11
|
function convertBackendOptions(options) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import "node:module";
|
|
2
2
|
const DEFAULT_I18NEXT_BACKEND_OPTIONS = {
|
|
3
|
-
loadPath: './locales/{{lng}}/{{ns}}.json',
|
|
4
|
-
addPath: './locales/{{lng}}/{{ns}}.json'
|
|
3
|
+
loadPath: './config/public/locales/{{lng}}/{{ns}}.json',
|
|
4
|
+
addPath: './config/public/locales/{{lng}}/{{ns}}.json'
|
|
5
5
|
};
|
|
6
6
|
function convertPath(path) {
|
|
7
7
|
if (!path) return path;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import "node:module";
|
|
2
|
-
import
|
|
2
|
+
import cjs from "i18next-fs-backend/cjs";
|
|
3
3
|
import { useI18nextBackendCommon } from "./middleware.common.mjs";
|
|
4
|
-
class FsBackendWithSave extends
|
|
4
|
+
class FsBackendWithSave extends cjs {
|
|
5
5
|
save(_language, _namespace, _data) {}
|
|
6
6
|
}
|
|
7
7
|
const HttpBackendWithSave = FsBackendWithSave;
|
|
8
|
-
const useI18nextBackend = (i18nInstance, backend)=>useI18nextBackendCommon(i18nInstance, FsBackendWithSave,
|
|
8
|
+
const useI18nextBackend = (i18nInstance, backend)=>useI18nextBackendCommon(i18nInstance, FsBackendWithSave, cjs, backend);
|
|
9
9
|
export { FsBackendWithSave, HttpBackendWithSave, useI18nextBackend };
|
|
@@ -41,14 +41,6 @@ async function createI18nextInstance() {
|
|
|
41
41
|
return null;
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
-
async function tryImportReactI18next() {
|
|
45
|
-
try {
|
|
46
|
-
const reactI18next = await import("react-i18next");
|
|
47
|
-
return reactI18next;
|
|
48
|
-
} catch (error) {
|
|
49
|
-
return null;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
44
|
function getI18nextInstanceForProvider(instance) {
|
|
53
45
|
if (isI18nWrapperInstance(instance)) {
|
|
54
46
|
const i18nextInstance = getI18nWrapperI18nextInstance(instance);
|
|
@@ -65,14 +57,4 @@ async function getI18nInstance(userInstance) {
|
|
|
65
57
|
if (i18nextInstance) return i18nextInstance;
|
|
66
58
|
throw new Error('No i18n instance found');
|
|
67
59
|
}
|
|
68
|
-
|
|
69
|
-
const reactI18nextModule = await tryImportReactI18next();
|
|
70
|
-
if (reactI18nextModule) return reactI18nextModule.initReactI18next;
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
73
|
-
async function getI18nextProvider() {
|
|
74
|
-
const reactI18nextModule = await tryImportReactI18next();
|
|
75
|
-
if (reactI18nextModule) return reactI18nextModule.I18nextProvider;
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
export { getActualI18nextInstance, getI18nInstance, getI18nWrapperI18nextInstance, getI18nextInstanceForProvider, getI18nextProvider, getInitReactI18next, isI18nInstance, isI18nWrapperInstance };
|
|
60
|
+
export { getActualI18nextInstance, getI18nInstance, getI18nWrapperI18nextInstance, getI18nextInstanceForProvider, isI18nInstance, isI18nWrapperInstance };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
async function tryImportReactI18next() {
|
|
3
|
+
try {
|
|
4
|
+
return await import("react-i18next");
|
|
5
|
+
} catch (error) {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
async function getReactI18nextIntegration() {
|
|
10
|
+
const reactI18nextModule = await tryImportReactI18next();
|
|
11
|
+
return {
|
|
12
|
+
I18nextProvider: reactI18nextModule?.I18nextProvider ?? null,
|
|
13
|
+
initReactI18next: reactI18nextModule?.initReactI18next ?? null
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export { getReactI18nextIntegration };
|
|
@@ -101,18 +101,6 @@ const initializeI18nInstance = async (i18nInstance, finalLanguage, fallbackLangu
|
|
|
101
101
|
};
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
|
-
if (mergedBackend && hasOptions(i18nInstance)) {
|
|
105
|
-
const defaultNS = initOptions.defaultNS || initOptions.ns || 'translation';
|
|
106
|
-
const ns = Array.isArray(defaultNS) ? defaultNS[0] : defaultNS;
|
|
107
|
-
let retries = 20;
|
|
108
|
-
while(retries > 0){
|
|
109
|
-
const actualInstance = getActualI18nextInstance(i18nInstance);
|
|
110
|
-
const store = actualInstance.store;
|
|
111
|
-
if (store?.data?.[finalLanguage]?.[ns]) break;
|
|
112
|
-
await new Promise((resolve)=>setTimeout(resolve, 100));
|
|
113
|
-
retries--;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
104
|
}
|
|
117
105
|
};
|
|
118
106
|
function hasOptions(instance) {
|
|
@@ -11,26 +11,31 @@ import { mergeBackendOptions } from "./i18n/backend/index.mjs";
|
|
|
11
11
|
import { useI18nextBackend } from "./i18n/backend/middleware.mjs";
|
|
12
12
|
import { detectLanguageWithPriority, exportServerLngToWindow, mergeDetectionOptions } from "./i18n/detection/index.mjs";
|
|
13
13
|
import { useI18nextLanguageDetector } from "./i18n/detection/middleware.mjs";
|
|
14
|
-
import { getI18nextInstanceForProvider
|
|
14
|
+
import { getI18nextInstanceForProvider } from "./i18n/instance.mjs";
|
|
15
15
|
import { changeI18nLanguage, ensureLanguageMatch, initializeI18nInstance, setupClonedInstance } from "./i18n/utils.mjs";
|
|
16
16
|
import { getPathname } from "./utils.mjs";
|
|
17
17
|
import "./types.mjs";
|
|
18
18
|
const i18nPlugin = (options)=>({
|
|
19
19
|
name: '@modern-js/plugin-i18n',
|
|
20
20
|
setup: (api)=>{
|
|
21
|
-
const { entryName, i18nInstance: userI18nInstance, initOptions, localeDetection, backend, htmlLangAttr = false } = options;
|
|
22
|
-
const { localePathRedirect = false, i18nextDetector = true, languages = [], fallbackLanguage = 'en', detection, ignoreRedirectRoutes } = localeDetection || {};
|
|
21
|
+
const { entryName, i18nInstance: userI18nInstance, initOptions, localeDetection, backend, htmlLangAttr = false, reactI18next = true } = options;
|
|
22
|
+
const { localePathRedirect = false, i18nextDetector = true, languages = [], fallbackLanguage = 'en', detection, ignoreRedirectRoutes, localisedUrls } = localeDetection || {};
|
|
23
23
|
const { enabled: backendEnabled = false } = backend || {};
|
|
24
24
|
let latestI18nInstance;
|
|
25
25
|
let I18nextProvider;
|
|
26
|
+
const loadReactI18nextIntegration = async ()=>{
|
|
27
|
+
if (!reactI18next) return null;
|
|
28
|
+
const { getReactI18nextIntegration } = await import("./i18n/react-i18next.mjs");
|
|
29
|
+
return getReactI18nextIntegration();
|
|
30
|
+
};
|
|
26
31
|
api.onBeforeRender(async (context)=>{
|
|
27
32
|
let i18nInstance = await getI18nInstance(userI18nInstance);
|
|
28
33
|
const { i18n: otherConfig } = api.getRuntimeConfig();
|
|
29
34
|
const { initOptions: otherInitOptions } = otherConfig || {};
|
|
30
35
|
const userInitOptions = merge(otherInitOptions || {}, initOptions || {});
|
|
31
|
-
const
|
|
32
|
-
I18nextProvider =
|
|
33
|
-
if (initReactI18next) i18nInstance.use(initReactI18next);
|
|
36
|
+
const reactI18nextIntegration = await loadReactI18nextIntegration();
|
|
37
|
+
I18nextProvider = reactI18nextIntegration?.I18nextProvider ?? null;
|
|
38
|
+
if (reactI18nextIntegration?.initReactI18next) i18nInstance.use(reactI18nextIntegration.initReactI18next);
|
|
34
39
|
const pathname = getPathname(context);
|
|
35
40
|
if (i18nextDetector) useI18nextLanguageDetector(i18nInstance);
|
|
36
41
|
const mergedDetection = mergeDetectionOptions(i18nextDetector, detection, localePathRedirect, userInitOptions);
|
|
@@ -90,16 +95,18 @@ const i18nPlugin = (options)=>({
|
|
|
90
95
|
]);
|
|
91
96
|
useSdkResourcesLoader(i18nInstance, setForceUpdate);
|
|
92
97
|
useLanguageSync(i18nInstance, localePathRedirect, languages, runtimeContextRef, prevLangRef, setLang);
|
|
93
|
-
useClientSideRedirect(i18nInstance, localePathRedirect, languages, fallbackLanguage, ignoreRedirectRoutes);
|
|
94
|
-
const contextValue = useMemo(()=>createContextValue(lang, i18nInstance, entryName, languages, localePathRedirect, ignoreRedirectRoutes, setLang), [
|
|
98
|
+
useClientSideRedirect(i18nInstance, localePathRedirect, languages, fallbackLanguage, ignoreRedirectRoutes, localisedUrls);
|
|
99
|
+
const contextValue = useMemo(()=>createContextValue(lang, i18nInstance, entryName, languages, localePathRedirect, ignoreRedirectRoutes, localisedUrls, setLang), [
|
|
95
100
|
lang,
|
|
96
101
|
i18nInstance,
|
|
97
102
|
entryName,
|
|
98
103
|
languages,
|
|
99
104
|
localePathRedirect,
|
|
100
105
|
ignoreRedirectRoutes,
|
|
106
|
+
localisedUrls,
|
|
101
107
|
forceUpdate
|
|
102
108
|
]);
|
|
109
|
+
const children = props.children;
|
|
103
110
|
const appContent = /*#__PURE__*/ jsxs(Fragment, {
|
|
104
111
|
children: [
|
|
105
112
|
Boolean(htmlLangAttr) && /*#__PURE__*/ jsx(Helmet, {
|
|
@@ -109,9 +116,10 @@ const i18nPlugin = (options)=>({
|
|
|
109
116
|
}),
|
|
110
117
|
/*#__PURE__*/ jsx(ModernI18nProvider, {
|
|
111
118
|
value: contextValue,
|
|
112
|
-
children: /*#__PURE__*/ jsx(App, {
|
|
113
|
-
...props
|
|
114
|
-
|
|
119
|
+
children: App ? /*#__PURE__*/ jsx(App, {
|
|
120
|
+
...props,
|
|
121
|
+
children: children
|
|
122
|
+
}) : children
|
|
115
123
|
})
|
|
116
124
|
]
|
|
117
125
|
});
|