@bleedingdev/modern-js-plugin-i18n 3.2.0-ultramodern.2 → 3.2.0-ultramodern.22
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/middleware.node.js +4 -4
- package/dist/cjs/runtime/i18n/instance.js +95 -80
- package/dist/cjs/runtime/index.js +7 -6
- package/dist/cjs/runtime/routerAdapter.js +164 -0
- package/dist/cjs/runtime/utils.js +63 -94
- package/dist/cjs/server/index.js +64 -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/middleware.node.mjs +3 -3
- package/dist/esm/runtime/i18n/instance.mjs +8 -2
- package/dist/esm/runtime/index.mjs +7 -6
- package/dist/esm/runtime/routerAdapter.mjs +130 -0
- package/dist/esm/runtime/utils.mjs +11 -30
- package/dist/esm/server/index.mjs +57 -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/middleware.node.mjs +3 -3
- package/dist/esm-node/runtime/i18n/instance.mjs +8 -2
- package/dist/esm-node/runtime/index.mjs +7 -6
- package/dist/esm-node/runtime/routerAdapter.mjs +131 -0
- package/dist/esm-node/runtime/utils.mjs +11 -30
- package/dist/esm-node/server/index.mjs +57 -7
- package/dist/esm-node/shared/localisedUrls.mjs +192 -0
- package/dist/types/cli/index.d.ts +21 -0
- package/dist/types/runtime/I18nLink.d.ts +23 -0
- package/dist/types/runtime/context.d.ts +41 -0
- package/dist/types/runtime/hooks.d.ts +30 -0
- package/dist/types/runtime/i18n/backend/config.d.ts +2 -0
- package/dist/types/runtime/i18n/backend/defaults.d.ts +13 -0
- package/dist/types/runtime/i18n/backend/defaults.node.d.ts +8 -0
- package/dist/types/runtime/i18n/backend/index.d.ts +3 -0
- package/dist/types/runtime/i18n/backend/middleware.common.d.ts +14 -0
- package/dist/types/runtime/i18n/backend/middleware.d.ts +12 -0
- package/dist/types/runtime/i18n/backend/middleware.node.d.ts +13 -0
- package/dist/types/runtime/i18n/backend/sdk-backend.d.ts +53 -0
- package/dist/types/runtime/i18n/backend/sdk-event.d.ts +9 -0
- package/dist/types/runtime/i18n/detection/config.d.ts +11 -0
- package/dist/types/runtime/i18n/detection/index.d.ts +50 -0
- package/dist/types/runtime/i18n/detection/middleware.d.ts +24 -0
- package/dist/types/runtime/i18n/detection/middleware.node.d.ts +17 -0
- package/dist/types/runtime/i18n/index.d.ts +3 -0
- package/dist/types/runtime/i18n/instance.d.ts +96 -0
- package/dist/types/runtime/i18n/utils.d.ts +29 -0
- package/dist/types/runtime/index.d.ts +21 -0
- package/dist/types/runtime/routerAdapter.d.ts +26 -0
- package/dist/types/runtime/types.d.ts +15 -0
- package/dist/types/runtime/utils.d.ts +28 -0
- package/dist/types/server/index.d.ts +14 -0
- package/dist/types/shared/deepMerge.d.ts +1 -0
- package/dist/types/shared/detection.d.ts +11 -0
- package/dist/types/shared/localisedUrls.d.ts +13 -0
- package/dist/types/shared/type.d.ts +168 -0
- package/dist/types/shared/utils.d.ts +5 -0
- package/package.json +15 -15
- 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.ts +3 -1
- package/src/runtime/i18n/backend/middleware.node.ts +1 -1
- package/src/runtime/i18n/instance.ts +8 -5
- package/src/runtime/index.tsx +10 -2
- package/src/runtime/routerAdapter.tsx +333 -0
- package/src/runtime/utils.ts +22 -34
- package/src/server/index.ts +135 -10
- package/src/shared/localisedUrls.ts +393 -0
- package/src/shared/type.ts +12 -0
- package/tests/localisedUrls.test.ts +278 -0
- package/tests/routerAdapter.test.tsx +278 -0
|
@@ -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,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 };
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import "node:module";
|
|
2
|
+
import { __webpack_require__ } from "../../rslib-runtime.mjs";
|
|
3
|
+
import * as __rspack_external_react_i18next_05dede28 from "react-i18next";
|
|
4
|
+
__webpack_require__.add({
|
|
5
|
+
"react-i18next" (module) {
|
|
6
|
+
module.exports = __rspack_external_react_i18next_05dede28;
|
|
7
|
+
}
|
|
8
|
+
});
|
|
2
9
|
function isI18nWrapperInstance(obj) {
|
|
3
10
|
if (!obj || 'object' != typeof obj) return false;
|
|
4
11
|
if (!obj.i18nInstance || 'object' != typeof obj.i18nInstance) return false;
|
|
@@ -43,8 +50,7 @@ async function createI18nextInstance() {
|
|
|
43
50
|
}
|
|
44
51
|
async function tryImportReactI18next() {
|
|
45
52
|
try {
|
|
46
|
-
|
|
47
|
-
return reactI18next;
|
|
53
|
+
return __webpack_require__("react-i18next");
|
|
48
54
|
} catch (error) {
|
|
49
55
|
return null;
|
|
50
56
|
}
|
|
@@ -18,8 +18,8 @@ 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;
|
|
@@ -28,8 +28,8 @@ const i18nPlugin = (options)=>({
|
|
|
28
28
|
const { i18n: otherConfig } = api.getRuntimeConfig();
|
|
29
29
|
const { initOptions: otherInitOptions } = otherConfig || {};
|
|
30
30
|
const userInitOptions = merge(otherInitOptions || {}, initOptions || {});
|
|
31
|
-
const initReactI18next = await getInitReactI18next();
|
|
32
|
-
I18nextProvider = await getI18nextProvider();
|
|
31
|
+
const initReactI18next = reactI18next ? await getInitReactI18next() : null;
|
|
32
|
+
I18nextProvider = reactI18next ? await getI18nextProvider() : null;
|
|
33
33
|
if (initReactI18next) i18nInstance.use(initReactI18next);
|
|
34
34
|
const pathname = getPathname(context);
|
|
35
35
|
if (i18nextDetector) useI18nextLanguageDetector(i18nInstance);
|
|
@@ -90,14 +90,15 @@ const i18nPlugin = (options)=>({
|
|
|
90
90
|
]);
|
|
91
91
|
useSdkResourcesLoader(i18nInstance, setForceUpdate);
|
|
92
92
|
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), [
|
|
93
|
+
useClientSideRedirect(i18nInstance, localePathRedirect, languages, fallbackLanguage, ignoreRedirectRoutes, localisedUrls);
|
|
94
|
+
const contextValue = useMemo(()=>createContextValue(lang, i18nInstance, entryName, languages, localePathRedirect, ignoreRedirectRoutes, localisedUrls, setLang), [
|
|
95
95
|
lang,
|
|
96
96
|
i18nInstance,
|
|
97
97
|
entryName,
|
|
98
98
|
languages,
|
|
99
99
|
localePathRedirect,
|
|
100
100
|
ignoreRedirectRoutes,
|
|
101
|
+
localisedUrls,
|
|
101
102
|
forceUpdate
|
|
102
103
|
]);
|
|
103
104
|
const appContent = /*#__PURE__*/ jsxs(Fragment, {
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
import { RuntimeContext, isBrowser } from "@modern-js/runtime";
|
|
3
|
+
import { InternalRuntimeContext } from "@modern-js/runtime/context";
|
|
4
|
+
import { useInRouterContext, useLocation, useNavigate, useParams } from "@modern-js/runtime/router";
|
|
5
|
+
import { Link as router_Link } from "@modern-js/runtime-utils/router";
|
|
6
|
+
import { useCallback, useContext, useEffect, useState } from "react";
|
|
7
|
+
const normalizeUrlPart = (value, prefix)=>{
|
|
8
|
+
if ('string' != typeof value || !value) return '';
|
|
9
|
+
return value.startsWith(prefix) ? value : `${prefix}${value}`;
|
|
10
|
+
};
|
|
11
|
+
const normalizeLocation = (location)=>{
|
|
12
|
+
if (!location || 'object' != typeof location) return null;
|
|
13
|
+
const locationValue = location;
|
|
14
|
+
if ('string' != typeof locationValue.pathname) return null;
|
|
15
|
+
return {
|
|
16
|
+
pathname: locationValue.pathname,
|
|
17
|
+
search: normalizeUrlPart('string' == typeof locationValue.search ? locationValue.search : locationValue.searchStr, '?'),
|
|
18
|
+
hash: normalizeUrlPart(locationValue.hash, '#')
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
const getWindowLocation = ()=>{
|
|
22
|
+
if (!isBrowser()) return null;
|
|
23
|
+
return {
|
|
24
|
+
pathname: window.location.pathname,
|
|
25
|
+
search: window.location.search,
|
|
26
|
+
hash: window.location.hash
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
const getRouterFramework = (runtimeContext, internalContext, inReactRouter)=>{
|
|
30
|
+
const framework = internalContext.routerFramework || internalContext.routerRuntime?.framework || runtimeContext.routerFramework;
|
|
31
|
+
if (framework) return framework;
|
|
32
|
+
if (internalContext.router?.useRouter || runtimeContext.router?.useRouter) return 'tanstack';
|
|
33
|
+
if (internalContext.router?.useLocation || internalContext.router?.useHref || runtimeContext.router?.useLocation || runtimeContext.router?.useHref) return 'react-router';
|
|
34
|
+
if (inReactRouter) return 'react-router';
|
|
35
|
+
};
|
|
36
|
+
const getRouterInstance = (internalContext, contextRouter)=>{
|
|
37
|
+
if (contextRouter) return contextRouter;
|
|
38
|
+
const router = internalContext.routerInstance || internalContext.routerRuntime?.instance;
|
|
39
|
+
if (!router || 'object' != typeof router) return null;
|
|
40
|
+
return router;
|
|
41
|
+
};
|
|
42
|
+
const getRouterStateLocation = (internalContext, contextRouter)=>{
|
|
43
|
+
const router = getRouterInstance(internalContext, contextRouter);
|
|
44
|
+
return normalizeLocation(router?.stores?.location?.get?.()) || normalizeLocation(router?.state?.location);
|
|
45
|
+
};
|
|
46
|
+
const getRouterParams = (internalContext, contextRouter)=>{
|
|
47
|
+
const router = getRouterInstance(internalContext, contextRouter);
|
|
48
|
+
const matches = router?.stores?.matches?.get?.() || router?.state?.matches;
|
|
49
|
+
if (!Array.isArray(matches)) return {};
|
|
50
|
+
return matches.reduce((params, match)=>{
|
|
51
|
+
if (match?.params) Object.assign(params, match.params);
|
|
52
|
+
return params;
|
|
53
|
+
}, {});
|
|
54
|
+
};
|
|
55
|
+
const useI18nRouterAdapter = ()=>{
|
|
56
|
+
const runtimeContext = useContext(RuntimeContext);
|
|
57
|
+
const internalContext = useContext(InternalRuntimeContext);
|
|
58
|
+
const inReactRouter = useInRouterContext();
|
|
59
|
+
const reactRouterNavigate = inReactRouter ? useNavigate() : null;
|
|
60
|
+
const reactRouterLocation = inReactRouter ? useLocation() : null;
|
|
61
|
+
const reactRouterParams = inReactRouter ? useParams() : {};
|
|
62
|
+
const framework = getRouterFramework(runtimeContext, internalContext, inReactRouter);
|
|
63
|
+
const contextUseRouter = inReactRouter || 'tanstack' !== framework ? void 0 : internalContext.router?.useRouter || runtimeContext.router?.useRouter;
|
|
64
|
+
const contextRouter = contextUseRouter ? contextUseRouter({
|
|
65
|
+
warn: false
|
|
66
|
+
}) : null;
|
|
67
|
+
const [, setRouterVersion] = useState(0);
|
|
68
|
+
const hasRouter = 'tanstack' === framework || 'react-router' === framework || Boolean(reactRouterNavigate);
|
|
69
|
+
useEffect(()=>{
|
|
70
|
+
if ('tanstack' !== framework) return;
|
|
71
|
+
const router = getRouterInstance(internalContext, contextRouter);
|
|
72
|
+
if (!router) return;
|
|
73
|
+
const update = ()=>setRouterVersion((version)=>version + 1);
|
|
74
|
+
const unsubscribers = [];
|
|
75
|
+
if ('function' == typeof router.stores?.location?.subscribe) {
|
|
76
|
+
const unsubscribe = router.stores.location.subscribe(update);
|
|
77
|
+
if ('function' == typeof unsubscribe) unsubscribers.push(unsubscribe);
|
|
78
|
+
}
|
|
79
|
+
if ('function' == typeof router.subscribe) for (const eventType of [
|
|
80
|
+
'onBeforeNavigate',
|
|
81
|
+
'onBeforeLoad'
|
|
82
|
+
]){
|
|
83
|
+
const unsubscribe = router.subscribe(eventType, update);
|
|
84
|
+
if ('function' == typeof unsubscribe) unsubscribers.push(unsubscribe);
|
|
85
|
+
}
|
|
86
|
+
return ()=>{
|
|
87
|
+
for (const unsubscribe of unsubscribers)unsubscribe();
|
|
88
|
+
};
|
|
89
|
+
}, [
|
|
90
|
+
contextRouter,
|
|
91
|
+
framework,
|
|
92
|
+
internalContext
|
|
93
|
+
]);
|
|
94
|
+
const navigate = useCallback((href, options)=>{
|
|
95
|
+
const router = getRouterInstance(internalContext, contextRouter);
|
|
96
|
+
const activeFramework = getRouterFramework(runtimeContext, internalContext, inReactRouter);
|
|
97
|
+
if ('tanstack' === activeFramework) {
|
|
98
|
+
if ('function' == typeof router?.navigate) return router.navigate({
|
|
99
|
+
to: href,
|
|
100
|
+
replace: options?.replace,
|
|
101
|
+
...options?.state === void 0 ? {} : {
|
|
102
|
+
state: options.state
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
throw new Error('TanStack router instance is not available.');
|
|
106
|
+
}
|
|
107
|
+
if (reactRouterNavigate) return reactRouterNavigate(href, options);
|
|
108
|
+
if ('react-router' === activeFramework) {
|
|
109
|
+
if ('function' == typeof router?.navigate) return router.navigate(href, options);
|
|
110
|
+
throw new Error('React Router instance is not available.');
|
|
111
|
+
}
|
|
112
|
+
}, [
|
|
113
|
+
contextRouter,
|
|
114
|
+
internalContext,
|
|
115
|
+
inReactRouter,
|
|
116
|
+
reactRouterNavigate,
|
|
117
|
+
runtimeContext
|
|
118
|
+
]);
|
|
119
|
+
const location = (reactRouterLocation ? normalizeLocation(reactRouterLocation) : getRouterStateLocation(internalContext, contextRouter)) || getWindowLocation();
|
|
120
|
+
const params = inReactRouter ? reactRouterParams : getRouterParams(internalContext, contextRouter);
|
|
121
|
+
const Link = 'tanstack' === framework ? internalContext.router?.Link || runtimeContext.router?.Link || null : 'react-router' === framework || inReactRouter ? router_Link : null;
|
|
122
|
+
return {
|
|
123
|
+
framework,
|
|
124
|
+
hasRouter,
|
|
125
|
+
location,
|
|
126
|
+
navigate: hasRouter ? navigate : null,
|
|
127
|
+
Link,
|
|
128
|
+
params
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
export { useI18nRouterAdapter };
|