@bleedingdev/modern-js-plugin-i18n 3.2.0-ultramodern.98 → 3.4.0-ultramodern.0
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/README.md +221 -11
- package/dist/cjs/cli/index.js +17 -64
- package/dist/cjs/cli/locales.js +132 -0
- package/dist/cjs/runtime/I18nLink.js +17 -20
- package/dist/cjs/runtime/Link.js +264 -0
- package/dist/cjs/runtime/canonicalRoutes.js +18 -0
- package/dist/cjs/runtime/context.js +9 -5
- package/dist/cjs/runtime/hooks.js +9 -5
- package/dist/cjs/runtime/i18n/backend/config.js +9 -5
- package/dist/cjs/runtime/i18n/backend/defaults.js +20 -11
- package/dist/cjs/runtime/i18n/backend/defaults.node.js +79 -10
- package/dist/cjs/runtime/i18n/backend/index.js +9 -5
- package/dist/cjs/runtime/i18n/backend/middleware.common.js +9 -5
- package/dist/cjs/runtime/i18n/backend/middleware.js +9 -5
- package/dist/cjs/runtime/i18n/backend/middleware.node.js +9 -5
- package/dist/cjs/runtime/i18n/backend/sdk-backend.js +9 -5
- package/dist/cjs/runtime/i18n/backend/sdk-event.js +16 -11
- package/dist/cjs/runtime/i18n/detection/config.js +9 -5
- package/dist/cjs/runtime/i18n/detection/index.js +9 -5
- package/dist/cjs/runtime/i18n/detection/middleware.js +9 -5
- package/dist/cjs/runtime/i18n/detection/middleware.node.js +9 -5
- package/dist/cjs/runtime/i18n/index.js +9 -5
- package/dist/cjs/runtime/i18n/instance.js +17 -13
- package/dist/cjs/runtime/i18n/react-i18next.js +12 -8
- package/dist/cjs/runtime/i18n/utils.js +9 -5
- package/dist/cjs/runtime/index.js +32 -5
- package/dist/cjs/runtime/localizedPaths.js +102 -0
- package/dist/cjs/runtime/routerAdapter.js +11 -7
- package/dist/cjs/runtime/utils.js +31 -17
- package/dist/cjs/server/index.js +10 -14
- package/dist/cjs/shared/deepMerge.js +12 -8
- package/dist/cjs/shared/detection.js +9 -5
- package/dist/cjs/shared/localisedUrls.js +148 -34
- package/dist/cjs/shared/utils.js +15 -11
- package/dist/esm/cli/index.mjs +8 -48
- package/dist/esm/cli/locales.mjs +80 -0
- package/dist/esm/runtime/I18nLink.mjs +7 -14
- package/dist/esm/runtime/Link.mjs +221 -0
- package/dist/esm/runtime/canonicalRoutes.mjs +0 -0
- package/dist/esm/runtime/i18n/backend/defaults.mjs +6 -2
- package/dist/esm/runtime/i18n/backend/defaults.node.mjs +56 -5
- package/dist/esm/runtime/index.mjs +4 -2
- package/dist/esm/runtime/localizedPaths.mjs +55 -0
- package/dist/esm/runtime/routerAdapter.mjs +3 -3
- package/dist/esm/runtime/utils.mjs +19 -12
- package/dist/esm/server/index.mjs +2 -10
- package/dist/esm/shared/localisedUrls.mjs +115 -23
- package/dist/esm-node/cli/index.mjs +8 -48
- package/dist/esm-node/cli/locales.mjs +81 -0
- package/dist/esm-node/runtime/I18nLink.mjs +7 -14
- package/dist/esm-node/runtime/Link.mjs +222 -0
- package/dist/esm-node/runtime/canonicalRoutes.mjs +1 -0
- package/dist/esm-node/runtime/i18n/backend/defaults.mjs +6 -2
- package/dist/esm-node/runtime/i18n/backend/defaults.node.mjs +56 -5
- package/dist/esm-node/runtime/index.mjs +4 -2
- package/dist/esm-node/runtime/localizedPaths.mjs +56 -0
- package/dist/esm-node/runtime/routerAdapter.mjs +3 -3
- package/dist/esm-node/runtime/utils.mjs +19 -12
- package/dist/esm-node/server/index.mjs +2 -10
- package/dist/esm-node/shared/localisedUrls.mjs +115 -23
- package/dist/types/cli/index.d.ts +1 -0
- package/dist/types/cli/locales.d.ts +17 -0
- package/dist/types/runtime/I18nLink.d.ts +4 -13
- package/dist/types/runtime/Link.d.ts +66 -0
- package/dist/types/runtime/canonicalRoutes.d.ts +60 -0
- package/dist/types/runtime/i18n/backend/defaults.d.ts +10 -7
- package/dist/types/runtime/i18n/backend/defaults.node.d.ts +13 -4
- package/dist/types/runtime/index.d.ts +5 -1
- package/dist/types/runtime/localizedPaths.d.ts +39 -0
- package/dist/types/runtime/types.d.ts +1 -1
- package/dist/types/runtime/utils.d.ts +13 -4
- package/dist/types/shared/localisedUrls.d.ts +23 -0
- package/dist/types/shared/type.d.ts +27 -5
- package/package.json +28 -25
- package/rstest.config.mts +7 -2
- package/src/cli/index.ts +25 -98
- package/src/cli/locales.ts +186 -0
- package/src/runtime/I18nLink.tsx +13 -44
- package/src/runtime/Link.tsx +430 -0
- package/src/runtime/canonicalRoutes.ts +93 -0
- package/src/runtime/i18n/backend/defaults.node.ts +112 -7
- package/src/runtime/i18n/backend/defaults.ts +20 -18
- package/src/runtime/index.tsx +24 -2
- package/src/runtime/localizedPaths.ts +107 -0
- package/src/runtime/routerAdapter.tsx +4 -5
- package/src/runtime/types.ts +1 -1
- package/src/runtime/utils.ts +33 -26
- package/src/server/index.ts +7 -17
- package/src/shared/localisedUrls.ts +256 -26
- package/src/shared/type.ts +27 -5
- package/tests/backendDefaults.test.ts +51 -0
- package/tests/i18nUtils.test.ts +10 -3
- package/tests/link.test.tsx +525 -0
- package/tests/linkTypes.test.ts +28 -0
- package/tests/localisedUrls.test.ts +224 -0
- package/tests/routerAdapter.test.tsx +86 -12
- package/tests/type-fixture/linkTypes.fixture.tsx +51 -0
- package/tests/type-fixture/tsconfig.json +15 -0
|
@@ -3,12 +3,13 @@ const LOCALE_PARAM_NAMES = new Set([
|
|
|
3
3
|
'locale',
|
|
4
4
|
'language'
|
|
5
5
|
]);
|
|
6
|
-
const
|
|
6
|
+
const normaliseSlashes = (path)=>{
|
|
7
7
|
const withoutDuplicateSlashes = path.replace(/\/+/g, '/');
|
|
8
8
|
const withLeadingSlash = withoutDuplicateSlashes.startsWith('/') ? withoutDuplicateSlashes : `/${withoutDuplicateSlashes}`;
|
|
9
|
-
|
|
10
|
-
return withoutTrailingSlash.replace(/\[(.+?)\]/g, ':$1');
|
|
9
|
+
return withLeadingSlash.length > 1 ? withLeadingSlash.replace(/\/+$/, '') : withLeadingSlash;
|
|
11
10
|
};
|
|
11
|
+
const normalisePathPattern = (path)=>normaliseSlashes(path).replace(/\[(.+?)\]/g, ':$1');
|
|
12
|
+
const normalisePathname = (pathname)=>normaliseSlashes(pathname);
|
|
12
13
|
const normaliseRoutePath = (path)=>{
|
|
13
14
|
const normalized = normalisePathPattern(path);
|
|
14
15
|
return '/' === normalized ? '' : normalized.slice(1);
|
|
@@ -34,16 +35,12 @@ const getLeadingLocaleParam = (path)=>{
|
|
|
34
35
|
return getLocaleParamSegment(segments[0] || '');
|
|
35
36
|
};
|
|
36
37
|
const resolveLocalisedUrlsConfig = (option)=>{
|
|
37
|
-
if (
|
|
38
|
-
enabled: false,
|
|
39
|
-
map: {}
|
|
40
|
-
};
|
|
41
|
-
if (option && 'object' == typeof option) return {
|
|
38
|
+
if (option && 'object' == typeof option && Object.keys(option).length > 0) return {
|
|
42
39
|
enabled: true,
|
|
43
40
|
map: option
|
|
44
41
|
};
|
|
45
42
|
return {
|
|
46
|
-
enabled:
|
|
43
|
+
enabled: false,
|
|
47
44
|
map: {}
|
|
48
45
|
};
|
|
49
46
|
};
|
|
@@ -100,7 +97,7 @@ const transformLocalisedRoute = (route, parentCanonicalPath, parentLocalisedPath
|
|
|
100
97
|
if (!localisedUrlEntry) return [
|
|
101
98
|
baseRoute
|
|
102
99
|
];
|
|
103
|
-
return getLocalisedRoutePaths(canonicalPath, parentLocalisedPaths, languages, localisedUrlEntry).map((localisedPath, index)=>cloneRouteWithLocalisedPath(baseRoute, localisedPath, index));
|
|
100
|
+
return getLocalisedRoutePaths(canonicalPath, parentLocalisedPaths, languages, localisedUrlEntry).map((localisedPath, index)=>cloneRouteWithLocalisedPath(baseRoute, localisedPath, index, canonicalPath));
|
|
104
101
|
};
|
|
105
102
|
const legalRouteIdPart = (value)=>value.replace(/[^a-zA-Z0-9_$-]+/g, '_').replace(/^_+|_+$/g, '') || 'index';
|
|
106
103
|
const suffixRouteIds = (route, suffix)=>{
|
|
@@ -115,13 +112,14 @@ const suffixRouteIds = (route, suffix)=>{
|
|
|
115
112
|
} : {}
|
|
116
113
|
};
|
|
117
114
|
};
|
|
118
|
-
const cloneRouteWithLocalisedPath = (route, path, index)=>{
|
|
115
|
+
const cloneRouteWithLocalisedPath = (route, path, index, canonicalPath)=>{
|
|
119
116
|
const leadingLocaleParam = getLeadingLocaleParam(route.path);
|
|
120
117
|
const localisedPath = leadingLocaleParam ? normaliseRoutePath(`${leadingLocaleParam}/${path}`) : path;
|
|
121
118
|
const routeWithPath = {
|
|
122
119
|
...route,
|
|
123
120
|
path: localisedPath
|
|
124
121
|
};
|
|
122
|
+
routeWithPath.modernCanonicalPath = canonicalPath;
|
|
125
123
|
return 0 === index ? routeWithPath : suffixRouteIds(routeWithPath, legalRouteIdPart(localisedPath));
|
|
126
124
|
};
|
|
127
125
|
const applyLocalisedUrlsToRoutes = (routes, languages, localisedUrls)=>{
|
|
@@ -134,9 +132,13 @@ const applyLocalisedUrlsToRoutes = (routes, languages, localisedUrls)=>{
|
|
|
134
132
|
};
|
|
135
133
|
const escapeRegExp = (value)=>value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
136
134
|
const getParamName = (segment)=>segment.slice(1).replace(/\?$/, '');
|
|
135
|
+
const compiledPathPatternCache = new Map();
|
|
137
136
|
const compilePathPattern = (pattern)=>{
|
|
137
|
+
const normalizedPattern = normalisePathPattern(pattern);
|
|
138
|
+
const cached = compiledPathPatternCache.get(normalizedPattern);
|
|
139
|
+
if (cached) return cached;
|
|
138
140
|
const names = [];
|
|
139
|
-
const segments =
|
|
141
|
+
const segments = normalizedPattern.split('/').filter(Boolean);
|
|
140
142
|
const source = segments.map((segment)=>{
|
|
141
143
|
if (segment.startsWith(':')) {
|
|
142
144
|
names.push(getParamName(segment));
|
|
@@ -149,19 +151,55 @@ const compilePathPattern = (pattern)=>{
|
|
|
149
151
|
}
|
|
150
152
|
return `/${escapeRegExp(segment)}`;
|
|
151
153
|
}).join('');
|
|
152
|
-
|
|
154
|
+
const compiled = {
|
|
153
155
|
names,
|
|
154
156
|
regexp: new RegExp(`^${source || '/'}$`)
|
|
155
157
|
};
|
|
158
|
+
compiledPathPatternCache.set(normalizedPattern, compiled);
|
|
159
|
+
return compiled;
|
|
160
|
+
};
|
|
161
|
+
const getPatternSpecificity = (pattern)=>{
|
|
162
|
+
const segments = normalisePathPattern(pattern).split('/').filter(Boolean);
|
|
163
|
+
let staticSegments = 0;
|
|
164
|
+
let dynamicSegments = 0;
|
|
165
|
+
let splatSegments = 0;
|
|
166
|
+
for (const segment of segments)if ('*' === segment) splatSegments++;
|
|
167
|
+
else if (segment.startsWith(':')) dynamicSegments++;
|
|
168
|
+
else staticSegments++;
|
|
169
|
+
return {
|
|
170
|
+
staticSegments,
|
|
171
|
+
dynamicSegments,
|
|
172
|
+
splatSegments,
|
|
173
|
+
totalSegments: segments.length
|
|
174
|
+
};
|
|
175
|
+
};
|
|
176
|
+
const comparePatternSpecificity = (left, right)=>{
|
|
177
|
+
const a = getPatternSpecificity(left);
|
|
178
|
+
const b = getPatternSpecificity(right);
|
|
179
|
+
return b.staticSegments - a.staticSegments || b.totalSegments - a.totalSegments || a.splatSegments - b.splatSegments || a.dynamicSegments - b.dynamicSegments;
|
|
180
|
+
};
|
|
181
|
+
const sortPatternsBySpecificity = (patterns)=>patterns.map((pattern, index)=>({
|
|
182
|
+
pattern,
|
|
183
|
+
index
|
|
184
|
+
})).sort((left, right)=>comparePatternSpecificity(left.pattern.pattern, right.pattern.pattern) || left.index - right.index).map(({ pattern })=>pattern);
|
|
185
|
+
const decodePathParam = (value)=>{
|
|
186
|
+
try {
|
|
187
|
+
return decodeURIComponent(value);
|
|
188
|
+
} catch {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
156
191
|
};
|
|
157
192
|
const matchPathPattern = (pathname, pattern)=>{
|
|
158
193
|
const { names, regexp } = compilePathPattern(pattern);
|
|
159
|
-
const match = regexp.exec(
|
|
194
|
+
const match = regexp.exec(normalisePathname(pathname));
|
|
160
195
|
if (!match) return null;
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
196
|
+
const params = {};
|
|
197
|
+
for(let index = 0; index < names.length; index++){
|
|
198
|
+
const decoded = decodePathParam(match[index + 1] || '');
|
|
199
|
+
if (null === decoded) return null;
|
|
200
|
+
params[names[index]] = decoded;
|
|
201
|
+
}
|
|
202
|
+
return params;
|
|
165
203
|
};
|
|
166
204
|
const buildPathFromPattern = (pattern, params)=>{
|
|
167
205
|
const segments = normalisePathPattern(pattern).split('/').filter(Boolean);
|
|
@@ -176,16 +214,70 @@ const buildPathFromPattern = (pattern, params)=>{
|
|
|
176
214
|
return `/${path}`;
|
|
177
215
|
};
|
|
178
216
|
const resolveLocalisedPath = (pathname, targetLanguage, languages, localisedUrls)=>{
|
|
179
|
-
const normalizedPathname =
|
|
180
|
-
|
|
217
|
+
const normalizedPathname = normalisePathname(pathname);
|
|
218
|
+
const canonicalCandidates = sortPatternsBySpecificity(Object.entries(localisedUrls).map(([canonicalPattern, localisedUrlEntry])=>({
|
|
219
|
+
pattern: canonicalPattern,
|
|
220
|
+
canonicalPattern,
|
|
221
|
+
localisedUrlEntry
|
|
222
|
+
})));
|
|
223
|
+
for (const { canonicalPattern, localisedUrlEntry } of canonicalCandidates){
|
|
224
|
+
const targetPattern = localisedUrlEntry[targetLanguage];
|
|
225
|
+
if (!targetPattern) continue;
|
|
226
|
+
const params = matchPathPattern(normalizedPathname, canonicalPattern);
|
|
227
|
+
if (params) return buildPathFromPattern(targetPattern, params);
|
|
228
|
+
}
|
|
229
|
+
const localisedCandidates = sortPatternsBySpecificity(Object.values(localisedUrls).flatMap((localisedUrlEntry)=>{
|
|
181
230
|
const targetPattern = localisedUrlEntry[targetLanguage];
|
|
182
|
-
if (targetPattern)
|
|
231
|
+
if (!targetPattern) return [];
|
|
232
|
+
return languages.map((language)=>localisedUrlEntry[language]).filter((sourcePattern)=>Boolean(sourcePattern)).map((sourcePattern)=>({
|
|
233
|
+
pattern: sourcePattern,
|
|
234
|
+
sourcePattern,
|
|
235
|
+
targetPattern
|
|
236
|
+
}));
|
|
237
|
+
}));
|
|
238
|
+
for (const { sourcePattern, targetPattern } of localisedCandidates){
|
|
239
|
+
const params = matchPathPattern(normalizedPathname, sourcePattern);
|
|
240
|
+
if (params) return buildPathFromPattern(targetPattern, params);
|
|
241
|
+
}
|
|
242
|
+
return normalizedPathname;
|
|
243
|
+
};
|
|
244
|
+
const resolveCanonicalLocalisedPath = (pathname, languages, localisedUrls)=>{
|
|
245
|
+
const normalizedPathname = normalisePathname(pathname);
|
|
246
|
+
const canonicalCandidates = sortPatternsBySpecificity(Object.entries(localisedUrls).map(([canonicalPattern, localisedUrlEntry])=>({
|
|
247
|
+
pattern: canonicalPattern,
|
|
248
|
+
canonicalPattern,
|
|
249
|
+
localisedUrlEntry
|
|
250
|
+
})));
|
|
251
|
+
for (const { canonicalPattern, localisedUrlEntry } of canonicalCandidates){
|
|
252
|
+
const canonicalParams = matchPathPattern(normalizedPathname, canonicalPattern);
|
|
253
|
+
if (canonicalParams) return buildPathFromPattern(canonicalPattern, canonicalParams);
|
|
254
|
+
for (const language of languages){
|
|
183
255
|
const sourcePattern = localisedUrlEntry[language];
|
|
184
256
|
if (!sourcePattern) continue;
|
|
185
257
|
const params = matchPathPattern(normalizedPathname, sourcePattern);
|
|
186
|
-
if (params) return buildPathFromPattern(
|
|
258
|
+
if (params) return buildPathFromPattern(canonicalPattern, params);
|
|
187
259
|
}
|
|
188
260
|
}
|
|
189
261
|
return normalizedPathname;
|
|
190
262
|
};
|
|
191
|
-
|
|
263
|
+
const stripLanguagePrefix = (pathname, languages)=>{
|
|
264
|
+
const segments = pathname.split('/').filter(Boolean);
|
|
265
|
+
if (segments.length > 0 && languages.includes(segments[0])) return `/${segments.slice(1).join('/')}`;
|
|
266
|
+
return pathname || '/';
|
|
267
|
+
};
|
|
268
|
+
const localiseTargetPathname = (pathname, language, languages, localisedUrls)=>{
|
|
269
|
+
const pathWithoutLanguage = stripLanguagePrefix(pathname, languages);
|
|
270
|
+
const localisedUrlsConfig = resolveLocalisedUrlsConfig(localisedUrls);
|
|
271
|
+
const resolvedPath = localisedUrlsConfig.enabled ? resolveLocalisedPath(pathWithoutLanguage, language, languages, localisedUrlsConfig.map) : pathWithoutLanguage;
|
|
272
|
+
const resolvedSegments = resolvedPath.split('/').filter(Boolean);
|
|
273
|
+
return `/${[
|
|
274
|
+
language,
|
|
275
|
+
...resolvedSegments
|
|
276
|
+
].join('/')}`;
|
|
277
|
+
};
|
|
278
|
+
const canonicalTargetPathname = (pathname, languages, localisedUrls)=>{
|
|
279
|
+
const pathWithoutLanguage = stripLanguagePrefix(pathname, languages);
|
|
280
|
+
const localisedUrlsConfig = resolveLocalisedUrlsConfig(localisedUrls);
|
|
281
|
+
return localisedUrlsConfig.enabled ? resolveCanonicalLocalisedPath(pathWithoutLanguage, languages, localisedUrlsConfig.map) : pathWithoutLanguage;
|
|
282
|
+
};
|
|
283
|
+
export { applyLocalisedUrlsToRoutes, buildPathFromPattern, canonicalTargetPathname, localiseTargetPathname, matchPathPattern, normalisePathPattern, normalisePathname, resolveCanonicalLocalisedPath, resolveLocalisedPath, resolveLocalisedUrlsConfig, validateLocalisedUrls };
|
|
@@ -1,39 +1,9 @@
|
|
|
1
1
|
import "node:module";
|
|
2
2
|
import { getPublicDirRoutePrefixes } from "@modern-js/server-core";
|
|
3
|
-
import fs from "fs";
|
|
4
|
-
import path from "path";
|
|
5
3
|
import { applyLocalisedUrlsToRoutes, resolveLocalisedUrlsConfig } from "../shared/localisedUrls.mjs";
|
|
6
4
|
import { getBackendOptions, getLocaleDetectionOptions } from "../shared/utils.mjs";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
if (!fs.existsSync(dirPath) || !fs.statSync(dirPath).isDirectory()) return false;
|
|
10
|
-
const entries = fs.readdirSync(dirPath);
|
|
11
|
-
for (const entry of entries){
|
|
12
|
-
const entryPath = path.join(dirPath, entry);
|
|
13
|
-
const stat = fs.statSync(entryPath);
|
|
14
|
-
if (stat.isFile() && entry.endsWith('.json')) return true;
|
|
15
|
-
if (stat.isDirectory()) {
|
|
16
|
-
if (hasJsonFiles(entryPath)) return true;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
return false;
|
|
20
|
-
} catch {
|
|
21
|
-
return false;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
function detectLocalesDirectory(appDirectory, normalizedConfig) {
|
|
25
|
-
const rootLocalesPath = path.join(appDirectory, 'locales');
|
|
26
|
-
if (hasJsonFiles(rootLocalesPath)) return true;
|
|
27
|
-
const configPublicPath = path.join(appDirectory, 'config', 'public', 'locales');
|
|
28
|
-
if (hasJsonFiles(configPublicPath)) return true;
|
|
29
|
-
const publicDir = normalizedConfig?.server?.publicDir;
|
|
30
|
-
if (publicDir) {
|
|
31
|
-
const publicDirPath = Array.isArray(publicDir) ? publicDir[0] : publicDir;
|
|
32
|
-
const localesPath = path.isAbsolute(publicDirPath) ? path.join(publicDirPath, 'locales') : path.join(appDirectory, publicDirPath, 'locales');
|
|
33
|
-
if (hasJsonFiles(localesPath)) return true;
|
|
34
|
-
}
|
|
35
|
-
return false;
|
|
36
|
-
}
|
|
5
|
+
import { applyDetectedBackendPaths, detectLocalesDirectory } from "./locales.mjs";
|
|
6
|
+
import "../runtime/types.mjs";
|
|
37
7
|
const i18nPlugin = (options = {})=>({
|
|
38
8
|
name: '@modern-js/plugin-i18n',
|
|
39
9
|
setup: (api)=>{
|
|
@@ -43,26 +13,16 @@ const i18nPlugin = (options = {})=>({
|
|
|
43
13
|
let backendOptions;
|
|
44
14
|
const { appDirectory } = api.getAppContext();
|
|
45
15
|
const normalizedConfig = api.getNormalizedConfig();
|
|
16
|
+
const detectedLocales = detectLocalesDirectory(appDirectory, normalizedConfig);
|
|
46
17
|
if (backend) {
|
|
47
18
|
const entryBackendOptions = getBackendOptions(entrypoint.entryName, backend);
|
|
48
|
-
|
|
49
|
-
else if (entryBackendOptions?.loadPath || entryBackendOptions?.addPath) backendOptions = {
|
|
19
|
+
backendOptions = entryBackendOptions?.enabled === false ? entryBackendOptions : detectedLocales ? applyDetectedBackendPaths(entryBackendOptions, detectedLocales) : entryBackendOptions?.loadPath || entryBackendOptions?.addPath ? {
|
|
50
20
|
...entryBackendOptions,
|
|
51
21
|
enabled: true
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
...entryBackendOptions,
|
|
57
|
-
enabled: true
|
|
58
|
-
} : entryBackendOptions;
|
|
59
|
-
} else backendOptions = entryBackendOptions;
|
|
60
|
-
} else {
|
|
61
|
-
const hasLocales = detectLocalesDirectory(appDirectory, normalizedConfig);
|
|
62
|
-
if (hasLocales) backendOptions = getBackendOptions(entrypoint.entryName, {
|
|
63
|
-
enabled: true
|
|
64
|
-
});
|
|
65
|
-
}
|
|
22
|
+
} : entryBackendOptions;
|
|
23
|
+
} else if (detectedLocales) backendOptions = applyDetectedBackendPaths(getBackendOptions(entrypoint.entryName, {
|
|
24
|
+
enabled: true
|
|
25
|
+
}), detectedLocales);
|
|
66
26
|
const { metaName } = api.getAppContext();
|
|
67
27
|
let extendedConfig = restOptions;
|
|
68
28
|
if (transformRuntimeConfig) extendedConfig = transformRuntimeConfig(restOptions, entrypoint);
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
import { getPublicDirRoutePrefixes, normalizePublicDir, normalizePublicDirPath } from "@modern-js/server-core";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
const LOCALES_RESOURCE_PATTERN = '{{lng}}/{{ns}}.json';
|
|
6
|
+
function hasJsonFiles(dirPath) {
|
|
7
|
+
try {
|
|
8
|
+
if (!fs.existsSync(dirPath) || !fs.statSync(dirPath).isDirectory()) return false;
|
|
9
|
+
const entries = fs.readdirSync(dirPath);
|
|
10
|
+
for (const entry of entries){
|
|
11
|
+
const entryPath = path.join(dirPath, entry);
|
|
12
|
+
const stat = fs.statSync(entryPath);
|
|
13
|
+
if (stat.isFile() && entry.endsWith('.json')) return true;
|
|
14
|
+
if (stat.isDirectory() && hasJsonFiles(entryPath)) return true;
|
|
15
|
+
}
|
|
16
|
+
return false;
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function toPosixPath(filePath) {
|
|
22
|
+
return filePath.split(path.sep).join(path.posix.sep);
|
|
23
|
+
}
|
|
24
|
+
function getPublicDirOutputPath(publicDir) {
|
|
25
|
+
return normalizePublicDirPath(publicDir);
|
|
26
|
+
}
|
|
27
|
+
function buildDetectedLocalesDirectory(clientBasePath, serverBasePath, serverBasePathCandidates = [
|
|
28
|
+
serverBasePath
|
|
29
|
+
]) {
|
|
30
|
+
const normalizedClientBasePath = clientBasePath.startsWith('/') ? clientBasePath : `/${clientBasePath}`;
|
|
31
|
+
const normalizedServerBasePath = toPosixPath(serverBasePath).replace(/\/$/, '');
|
|
32
|
+
const normalizedServerBasePathCandidates = Array.from(new Set(serverBasePathCandidates.map((candidate)=>toPosixPath(candidate).replace(/\/$/, ''))));
|
|
33
|
+
const serverLoadPaths = normalizedServerBasePathCandidates.map((candidate)=>`${candidate}/${LOCALES_RESOURCE_PATTERN}`);
|
|
34
|
+
const serverAddPaths = normalizedServerBasePathCandidates.map((candidate)=>`${candidate}/${LOCALES_RESOURCE_PATTERN}`);
|
|
35
|
+
return {
|
|
36
|
+
loadPath: `${normalizedClientBasePath}/${LOCALES_RESOURCE_PATTERN}`,
|
|
37
|
+
addPath: `${normalizedClientBasePath}/${LOCALES_RESOURCE_PATTERN}`,
|
|
38
|
+
serverLoadPath: `${normalizedServerBasePath}/${LOCALES_RESOURCE_PATTERN}`,
|
|
39
|
+
serverAddPath: `${normalizedServerBasePath}/${LOCALES_RESOURCE_PATTERN}`,
|
|
40
|
+
serverLoadPaths,
|
|
41
|
+
serverAddPaths
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function detectLocalesDirectory(appDirectory, normalizedConfig) {
|
|
45
|
+
const publicDirs = normalizePublicDir(normalizedConfig?.server?.publicDir);
|
|
46
|
+
const publicDirPrefixes = getPublicDirRoutePrefixes(normalizedConfig?.server?.publicDir);
|
|
47
|
+
for(let index = 0; index < publicDirs.length; index++){
|
|
48
|
+
const publicDir = publicDirs[index];
|
|
49
|
+
if (path.isAbsolute(publicDir)) continue;
|
|
50
|
+
const publicDirPath = path.join(appDirectory, publicDir);
|
|
51
|
+
const publicDirPrefix = publicDirPrefixes[index] || `/${normalizePublicDirPath(publicDir)}`;
|
|
52
|
+
const publicDirOutputPath = getPublicDirOutputPath(publicDir);
|
|
53
|
+
if ('locales' === path.basename(normalizePublicDirPath(publicDir)) && hasJsonFiles(publicDirPath)) return buildDetectedLocalesDirectory(publicDirPrefix, `./${publicDirOutputPath}`);
|
|
54
|
+
const localesPath = path.join(publicDirPath, 'locales');
|
|
55
|
+
if (hasJsonFiles(localesPath)) return buildDetectedLocalesDirectory(`${publicDirPrefix}/locales`, `./${publicDirOutputPath}/locales`);
|
|
56
|
+
}
|
|
57
|
+
const configPublicPath = path.join(appDirectory, 'config', 'public', 'locales');
|
|
58
|
+
if (hasJsonFiles(configPublicPath)) return buildDetectedLocalesDirectory('/locales', './public/locales', [
|
|
59
|
+
'./config/public/locales',
|
|
60
|
+
'./public/locales'
|
|
61
|
+
]);
|
|
62
|
+
const rootLocalesPath = path.join(appDirectory, 'locales');
|
|
63
|
+
if (hasJsonFiles(rootLocalesPath)) return buildDetectedLocalesDirectory('/locales', './locales');
|
|
64
|
+
}
|
|
65
|
+
function applyDetectedBackendPaths(backendOptions, detectedLocales) {
|
|
66
|
+
if (!detectedLocales) return backendOptions;
|
|
67
|
+
const backendWithDetectedPaths = {
|
|
68
|
+
...backendOptions,
|
|
69
|
+
enabled: true,
|
|
70
|
+
loadPath: backendOptions.loadPath ?? detectedLocales.loadPath,
|
|
71
|
+
addPath: backendOptions.addPath ?? detectedLocales.addPath,
|
|
72
|
+
serverLoadPath: backendOptions.serverLoadPath ?? detectedLocales.serverLoadPath,
|
|
73
|
+
serverLoadPaths: backendOptions.serverLoadPath || backendOptions.serverLoadPaths ? backendOptions.serverLoadPaths : detectedLocales.serverLoadPaths,
|
|
74
|
+
serverAddPath: backendOptions.serverAddPath ?? detectedLocales.serverAddPath,
|
|
75
|
+
serverAddPaths: backendOptions.serverAddPath || backendOptions.serverAddPaths ? backendOptions.serverAddPaths : detectedLocales.serverAddPaths,
|
|
76
|
+
_detectedLoadPath: detectedLocales.loadPath,
|
|
77
|
+
_detectedAddPath: detectedLocales.addPath
|
|
78
|
+
};
|
|
79
|
+
return backendWithDetectedPaths;
|
|
80
|
+
}
|
|
81
|
+
export { applyDetectedBackendPaths, detectLocalesDirectory };
|
|
@@ -1,21 +1,14 @@
|
|
|
1
1
|
import "node:module";
|
|
2
2
|
import { jsx } from "react/jsx-runtime";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
import { buildLocalizedUrl } from "./utils.mjs";
|
|
3
|
+
import { Link } from "./Link.mjs";
|
|
4
|
+
let warnedDeprecation = false;
|
|
6
5
|
const I18nLink = ({ to, children, ...props })=>{
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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.");
|
|
12
|
-
if (!hasRouter || !Link) return /*#__PURE__*/ jsx("a", {
|
|
13
|
-
href: localizedTo,
|
|
14
|
-
...props,
|
|
15
|
-
children: children
|
|
16
|
-
});
|
|
6
|
+
if ('development' === process.env.NODE_ENV && !warnedDeprecation) {
|
|
7
|
+
warnedDeprecation = true;
|
|
8
|
+
console.warn("[plugin-i18n] I18nLink is deprecated. Import { Link } from '@modern-js/plugin-i18n/runtime' instead — it accepts the same language-agnostic `to` values.");
|
|
9
|
+
}
|
|
17
10
|
return /*#__PURE__*/ jsx(Link, {
|
|
18
|
-
to:
|
|
11
|
+
to: to,
|
|
19
12
|
...props,
|
|
20
13
|
children: children
|
|
21
14
|
});
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import { useModernI18n } from "./context.mjs";
|
|
5
|
+
import { canonicalPath } from "./localizedPaths.mjs";
|
|
6
|
+
import { useI18nRouterAdapter } from "./routerAdapter.mjs";
|
|
7
|
+
import { buildLocalizedUrl, splitUrlTarget } from "./utils.mjs";
|
|
8
|
+
const EXTERNAL_TARGET_RE = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
|
|
9
|
+
const warnedTargets = new Set();
|
|
10
|
+
const warnOnce = (key, message)=>{
|
|
11
|
+
if ('development' !== process.env.NODE_ENV || warnedTargets.has(key)) return;
|
|
12
|
+
warnedTargets.add(key);
|
|
13
|
+
console.warn(message);
|
|
14
|
+
};
|
|
15
|
+
const interpolateRouteParams = (pathname, params)=>{
|
|
16
|
+
if (!/[$:*{]/.test(pathname)) return pathname;
|
|
17
|
+
const resolveParam = (name)=>{
|
|
18
|
+
const value = params?.[name];
|
|
19
|
+
return void 0 === value ? void 0 : String(value);
|
|
20
|
+
};
|
|
21
|
+
const segments = pathname.split('/').map((segment)=>{
|
|
22
|
+
if (!segment) return segment;
|
|
23
|
+
if (segment.startsWith('{-$') && segment.endsWith('}')) {
|
|
24
|
+
const value = resolveParam(segment.slice(3, -1));
|
|
25
|
+
return void 0 === value ? null : encodeURIComponent(value);
|
|
26
|
+
}
|
|
27
|
+
if ('$' === segment || '*' === segment) {
|
|
28
|
+
const value = resolveParam('_splat') ?? resolveParam('*');
|
|
29
|
+
return void 0 === value ? null : value.split('/').map(encodeURIComponent).join('/');
|
|
30
|
+
}
|
|
31
|
+
if (segment.startsWith('$')) {
|
|
32
|
+
const value = resolveParam(segment.slice(1));
|
|
33
|
+
if (void 0 === value) {
|
|
34
|
+
warnOnce(`missing-param:${pathname}:${segment}`, `[plugin-i18n] <Link to="${pathname}"> is missing required param "${segment.slice(1)}".`);
|
|
35
|
+
return segment;
|
|
36
|
+
}
|
|
37
|
+
return encodeURIComponent(value);
|
|
38
|
+
}
|
|
39
|
+
if (segment.startsWith(':')) {
|
|
40
|
+
const optional = segment.endsWith('?');
|
|
41
|
+
const name = segment.slice(1, optional ? -1 : void 0);
|
|
42
|
+
const value = resolveParam(name);
|
|
43
|
+
if (void 0 === value) {
|
|
44
|
+
if (optional) return null;
|
|
45
|
+
warnOnce(`missing-param:${pathname}:${segment}`, `[plugin-i18n] <Link to="${pathname}"> is missing required param "${name}".`);
|
|
46
|
+
return segment;
|
|
47
|
+
}
|
|
48
|
+
return encodeURIComponent(value);
|
|
49
|
+
}
|
|
50
|
+
return segment;
|
|
51
|
+
}).filter((segment)=>null !== segment);
|
|
52
|
+
return segments.join('/') || '/';
|
|
53
|
+
};
|
|
54
|
+
const normalizeSearch = (search, searchFromTo)=>{
|
|
55
|
+
if (search && 'object' == typeof search) {
|
|
56
|
+
const entries = Object.entries(search).filter(([, value])=>null != value);
|
|
57
|
+
const searchObject = Object.fromEntries(entries.map(([key, value])=>[
|
|
58
|
+
key,
|
|
59
|
+
String(value)
|
|
60
|
+
]));
|
|
61
|
+
const params = new URLSearchParams(searchObject);
|
|
62
|
+
const serialized = params.toString();
|
|
63
|
+
return {
|
|
64
|
+
searchString: serialized ? `?${serialized}` : '',
|
|
65
|
+
searchObject
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const raw = 'string' == typeof search && search ? search : searchFromTo;
|
|
69
|
+
if (!raw) return {
|
|
70
|
+
searchString: '',
|
|
71
|
+
searchObject: void 0
|
|
72
|
+
};
|
|
73
|
+
const searchString = raw.startsWith('?') ? raw : `?${raw}`;
|
|
74
|
+
const searchObject = {};
|
|
75
|
+
new URLSearchParams(searchString).forEach((value, key)=>{
|
|
76
|
+
searchObject[key] = value;
|
|
77
|
+
});
|
|
78
|
+
return {
|
|
79
|
+
searchString,
|
|
80
|
+
searchObject
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
const splitActiveProps = (active, activeProps)=>{
|
|
84
|
+
if (!active || !activeProps) return {};
|
|
85
|
+
return activeProps;
|
|
86
|
+
};
|
|
87
|
+
const mergeClassNames = (...values)=>{
|
|
88
|
+
const classNames = values.filter((value)=>'string' == typeof value && value.length > 0);
|
|
89
|
+
return classNames.length > 0 ? classNames.join(' ') : void 0;
|
|
90
|
+
};
|
|
91
|
+
const Link = (props)=>{
|
|
92
|
+
const { to, params, children, hash: hashProp, search: searchProp, hashScrollIntoView, activeOptions, activeProps, prefetch, preload, ...rest } = props;
|
|
93
|
+
const adapter = useI18nRouterAdapter();
|
|
94
|
+
const { language, supportedLanguages, localisedUrls } = useModernI18n();
|
|
95
|
+
const config = {
|
|
96
|
+
languages: supportedLanguages,
|
|
97
|
+
localisedUrls
|
|
98
|
+
};
|
|
99
|
+
const isExternal = EXTERNAL_TARGET_RE.test(to);
|
|
100
|
+
const isBareHash = to.startsWith('#');
|
|
101
|
+
const target = useMemo(()=>{
|
|
102
|
+
if (isExternal || isBareHash) return null;
|
|
103
|
+
const { pathname, search: searchFromTo, hash: hashFromTo } = splitUrlTarget(to);
|
|
104
|
+
const interpolated = interpolateRouteParams(pathname || '/', params);
|
|
105
|
+
const firstSegment = interpolated.split('/').filter(Boolean)[0];
|
|
106
|
+
if (firstSegment && supportedLanguages.includes(firstSegment)) warnOnce(`lang-prefix:${to}`, `[plugin-i18n] <Link to="${to}"> starts with a language prefix. Write language-agnostic canonical paths; the Link localizes them automatically.`);
|
|
107
|
+
const localizedPathname = buildLocalizedUrl(interpolated, language, supportedLanguages, localisedUrls);
|
|
108
|
+
const hash = hashProp ?? (hashFromTo ? hashFromTo.slice(1) : '');
|
|
109
|
+
const { searchString, searchObject } = normalizeSearch(searchProp, searchFromTo);
|
|
110
|
+
return {
|
|
111
|
+
canonicalPathname: interpolated,
|
|
112
|
+
localizedPathname,
|
|
113
|
+
hash,
|
|
114
|
+
searchString,
|
|
115
|
+
searchObject,
|
|
116
|
+
href: `${localizedPathname}${searchString}${hash ? `#${hash}` : ''}`
|
|
117
|
+
};
|
|
118
|
+
}, [
|
|
119
|
+
to,
|
|
120
|
+
params,
|
|
121
|
+
hashProp,
|
|
122
|
+
searchProp,
|
|
123
|
+
isExternal,
|
|
124
|
+
isBareHash,
|
|
125
|
+
language,
|
|
126
|
+
supportedLanguages,
|
|
127
|
+
localisedUrls
|
|
128
|
+
]);
|
|
129
|
+
const isActive = useMemo(()=>{
|
|
130
|
+
if (!target || !adapter.location) return false;
|
|
131
|
+
const current = canonicalPath(adapter.location.pathname, config);
|
|
132
|
+
const targetCanonical = canonicalPath(target.canonicalPathname, config);
|
|
133
|
+
const exact = activeOptions?.exact ?? '/' === targetCanonical;
|
|
134
|
+
if (current === targetCanonical) return true;
|
|
135
|
+
if (exact) return false;
|
|
136
|
+
return current.startsWith('/' === targetCanonical ? '/' : `${targetCanonical}/`);
|
|
137
|
+
}, [
|
|
138
|
+
target,
|
|
139
|
+
adapter.location,
|
|
140
|
+
activeOptions?.exact,
|
|
141
|
+
supportedLanguages,
|
|
142
|
+
localisedUrls
|
|
143
|
+
]);
|
|
144
|
+
const resolvedActiveProps = splitActiveProps(isActive, activeProps);
|
|
145
|
+
const activeAttributes = isActive ? {
|
|
146
|
+
'data-status': 'active',
|
|
147
|
+
'aria-current': rest['aria-current'] ?? resolvedActiveProps['aria-current'] ?? 'page'
|
|
148
|
+
} : {};
|
|
149
|
+
if (!target) {
|
|
150
|
+
const { replace: _replace, ...anchorProps } = rest;
|
|
151
|
+
return /*#__PURE__*/ jsx("a", {
|
|
152
|
+
href: to,
|
|
153
|
+
...anchorProps,
|
|
154
|
+
children: children
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
const { Link: RouterLink, hasRouter, framework } = adapter;
|
|
158
|
+
if (!hasRouter || !RouterLink) {
|
|
159
|
+
const { replace: _replace, ...anchorProps } = rest;
|
|
160
|
+
const { className: activeClassName, style: activeStyle, ...activeRest } = resolvedActiveProps;
|
|
161
|
+
return /*#__PURE__*/ jsx("a", {
|
|
162
|
+
href: target.href,
|
|
163
|
+
...anchorProps,
|
|
164
|
+
...activeRest,
|
|
165
|
+
...activeAttributes,
|
|
166
|
+
className: mergeClassNames(rest.className, activeClassName),
|
|
167
|
+
style: {
|
|
168
|
+
...rest.style,
|
|
169
|
+
...activeStyle
|
|
170
|
+
},
|
|
171
|
+
children: children
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
const { className: activeClassName, style: activeStyle, ...activeRest } = resolvedActiveProps;
|
|
175
|
+
const mergedClassName = mergeClassNames(rest.className, activeClassName);
|
|
176
|
+
const mergedStyle = {
|
|
177
|
+
...rest.style,
|
|
178
|
+
...activeStyle
|
|
179
|
+
};
|
|
180
|
+
if ('tanstack' === framework) {
|
|
181
|
+
const tanstackPreload = void 0 !== preload ? preload : void 0 === prefetch ? void 0 : 'none' === prefetch ? false : prefetch;
|
|
182
|
+
return /*#__PURE__*/ jsx(RouterLink, {
|
|
183
|
+
to: target.localizedPathname,
|
|
184
|
+
...target.searchObject ? {
|
|
185
|
+
search: target.searchObject
|
|
186
|
+
} : {},
|
|
187
|
+
...target.hash ? {
|
|
188
|
+
hash: target.hash
|
|
189
|
+
} : {},
|
|
190
|
+
...void 0 === hashScrollIntoView ? {} : {
|
|
191
|
+
hashScrollIntoView
|
|
192
|
+
},
|
|
193
|
+
...void 0 === tanstackPreload ? {} : {
|
|
194
|
+
preload: tanstackPreload
|
|
195
|
+
},
|
|
196
|
+
...rest,
|
|
197
|
+
...activeRest,
|
|
198
|
+
...activeAttributes,
|
|
199
|
+
className: mergedClassName,
|
|
200
|
+
style: mergedStyle,
|
|
201
|
+
children: children
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
return /*#__PURE__*/ jsx(RouterLink, {
|
|
205
|
+
to: target.href,
|
|
206
|
+
...void 0 === prefetch ? {} : {
|
|
207
|
+
prefetch
|
|
208
|
+
},
|
|
209
|
+
...void 0 === preload ? {} : {
|
|
210
|
+
preload
|
|
211
|
+
},
|
|
212
|
+
...rest,
|
|
213
|
+
...activeRest,
|
|
214
|
+
...activeAttributes,
|
|
215
|
+
className: mergedClassName,
|
|
216
|
+
style: mergedStyle,
|
|
217
|
+
children: children
|
|
218
|
+
});
|
|
219
|
+
};
|
|
220
|
+
const runtime_Link = Link;
|
|
221
|
+
export default runtime_Link;
|
|
222
|
+
export { Link, interpolateRouteParams };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "node:module";
|
|
@@ -4,8 +4,6 @@ const DEFAULT_I18NEXT_BACKEND_OPTIONS = {
|
|
|
4
4
|
addPath: '/locales/{{lng}}/{{ns}}.json'
|
|
5
5
|
};
|
|
6
6
|
function convertPath(path) {
|
|
7
|
-
if (!path) return path;
|
|
8
|
-
if (path.startsWith('/')) return "u" < typeof window ? path : `${window.__assetPrefix__ || ''}${path}`;
|
|
9
7
|
return path;
|
|
10
8
|
}
|
|
11
9
|
function convertBackendOptions(options) {
|
|
@@ -13,6 +11,12 @@ function convertBackendOptions(options) {
|
|
|
13
11
|
const converted = {
|
|
14
12
|
...options
|
|
15
13
|
};
|
|
14
|
+
delete converted.serverLoadPath;
|
|
15
|
+
delete converted.serverAddPath;
|
|
16
|
+
delete converted.serverLoadPaths;
|
|
17
|
+
delete converted.serverAddPaths;
|
|
18
|
+
delete converted._detectedLoadPath;
|
|
19
|
+
delete converted._detectedAddPath;
|
|
16
20
|
if (converted.loadPath) converted.loadPath = convertPath(converted.loadPath);
|
|
17
21
|
if (converted.addPath) converted.addPath = convertPath(converted.addPath);
|
|
18
22
|
return converted;
|