@bleedingdev/modern-js-plugin-i18n 3.2.0-ultramodern.99 → 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.
Files changed (98) hide show
  1. package/README.md +221 -11
  2. package/dist/cjs/cli/index.js +17 -64
  3. package/dist/cjs/cli/locales.js +132 -0
  4. package/dist/cjs/runtime/I18nLink.js +17 -20
  5. package/dist/cjs/runtime/Link.js +264 -0
  6. package/dist/cjs/runtime/canonicalRoutes.js +18 -0
  7. package/dist/cjs/runtime/context.js +9 -5
  8. package/dist/cjs/runtime/hooks.js +9 -5
  9. package/dist/cjs/runtime/i18n/backend/config.js +9 -5
  10. package/dist/cjs/runtime/i18n/backend/defaults.js +20 -11
  11. package/dist/cjs/runtime/i18n/backend/defaults.node.js +79 -10
  12. package/dist/cjs/runtime/i18n/backend/index.js +9 -5
  13. package/dist/cjs/runtime/i18n/backend/middleware.common.js +9 -5
  14. package/dist/cjs/runtime/i18n/backend/middleware.js +9 -5
  15. package/dist/cjs/runtime/i18n/backend/middleware.node.js +9 -5
  16. package/dist/cjs/runtime/i18n/backend/sdk-backend.js +9 -5
  17. package/dist/cjs/runtime/i18n/backend/sdk-event.js +16 -11
  18. package/dist/cjs/runtime/i18n/detection/config.js +9 -5
  19. package/dist/cjs/runtime/i18n/detection/index.js +9 -5
  20. package/dist/cjs/runtime/i18n/detection/middleware.js +9 -5
  21. package/dist/cjs/runtime/i18n/detection/middleware.node.js +9 -5
  22. package/dist/cjs/runtime/i18n/index.js +9 -5
  23. package/dist/cjs/runtime/i18n/instance.js +17 -13
  24. package/dist/cjs/runtime/i18n/react-i18next.js +12 -8
  25. package/dist/cjs/runtime/i18n/utils.js +9 -5
  26. package/dist/cjs/runtime/index.js +32 -5
  27. package/dist/cjs/runtime/localizedPaths.js +102 -0
  28. package/dist/cjs/runtime/routerAdapter.js +11 -7
  29. package/dist/cjs/runtime/utils.js +31 -17
  30. package/dist/cjs/server/index.js +10 -14
  31. package/dist/cjs/shared/deepMerge.js +12 -8
  32. package/dist/cjs/shared/detection.js +9 -5
  33. package/dist/cjs/shared/localisedUrls.js +148 -34
  34. package/dist/cjs/shared/utils.js +15 -11
  35. package/dist/esm/cli/index.mjs +8 -48
  36. package/dist/esm/cli/locales.mjs +80 -0
  37. package/dist/esm/runtime/I18nLink.mjs +7 -14
  38. package/dist/esm/runtime/Link.mjs +221 -0
  39. package/dist/esm/runtime/canonicalRoutes.mjs +0 -0
  40. package/dist/esm/runtime/i18n/backend/defaults.mjs +6 -2
  41. package/dist/esm/runtime/i18n/backend/defaults.node.mjs +56 -5
  42. package/dist/esm/runtime/index.mjs +4 -2
  43. package/dist/esm/runtime/localizedPaths.mjs +55 -0
  44. package/dist/esm/runtime/routerAdapter.mjs +3 -3
  45. package/dist/esm/runtime/utils.mjs +19 -12
  46. package/dist/esm/server/index.mjs +2 -10
  47. package/dist/esm/shared/localisedUrls.mjs +115 -23
  48. package/dist/esm-node/cli/index.mjs +8 -48
  49. package/dist/esm-node/cli/locales.mjs +81 -0
  50. package/dist/esm-node/runtime/I18nLink.mjs +7 -14
  51. package/dist/esm-node/runtime/Link.mjs +222 -0
  52. package/dist/esm-node/runtime/canonicalRoutes.mjs +1 -0
  53. package/dist/esm-node/runtime/i18n/backend/defaults.mjs +6 -2
  54. package/dist/esm-node/runtime/i18n/backend/defaults.node.mjs +56 -5
  55. package/dist/esm-node/runtime/index.mjs +4 -2
  56. package/dist/esm-node/runtime/localizedPaths.mjs +56 -0
  57. package/dist/esm-node/runtime/routerAdapter.mjs +3 -3
  58. package/dist/esm-node/runtime/utils.mjs +19 -12
  59. package/dist/esm-node/server/index.mjs +2 -10
  60. package/dist/esm-node/shared/localisedUrls.mjs +115 -23
  61. package/dist/types/cli/index.d.ts +1 -0
  62. package/dist/types/cli/locales.d.ts +17 -0
  63. package/dist/types/runtime/I18nLink.d.ts +4 -13
  64. package/dist/types/runtime/Link.d.ts +66 -0
  65. package/dist/types/runtime/canonicalRoutes.d.ts +60 -0
  66. package/dist/types/runtime/i18n/backend/defaults.d.ts +10 -7
  67. package/dist/types/runtime/i18n/backend/defaults.node.d.ts +13 -4
  68. package/dist/types/runtime/index.d.ts +5 -1
  69. package/dist/types/runtime/localizedPaths.d.ts +39 -0
  70. package/dist/types/runtime/types.d.ts +1 -1
  71. package/dist/types/runtime/utils.d.ts +13 -4
  72. package/dist/types/shared/localisedUrls.d.ts +23 -0
  73. package/dist/types/shared/type.d.ts +27 -5
  74. package/package.json +28 -25
  75. package/rstest.config.mts +7 -2
  76. package/src/cli/index.ts +25 -98
  77. package/src/cli/locales.ts +186 -0
  78. package/src/runtime/I18nLink.tsx +13 -44
  79. package/src/runtime/Link.tsx +430 -0
  80. package/src/runtime/canonicalRoutes.ts +93 -0
  81. package/src/runtime/i18n/backend/defaults.node.ts +112 -7
  82. package/src/runtime/i18n/backend/defaults.ts +20 -18
  83. package/src/runtime/index.tsx +24 -2
  84. package/src/runtime/localizedPaths.ts +107 -0
  85. package/src/runtime/routerAdapter.tsx +4 -5
  86. package/src/runtime/types.ts +1 -1
  87. package/src/runtime/utils.ts +33 -26
  88. package/src/server/index.ts +7 -17
  89. package/src/shared/localisedUrls.ts +256 -26
  90. package/src/shared/type.ts +27 -5
  91. package/tests/backendDefaults.test.ts +51 -0
  92. package/tests/i18nUtils.test.ts +10 -3
  93. package/tests/link.test.tsx +525 -0
  94. package/tests/linkTypes.test.ts +28 -0
  95. package/tests/localisedUrls.test.ts +224 -0
  96. package/tests/routerAdapter.test.tsx +86 -12
  97. package/tests/type-fixture/linkTypes.fixture.tsx +51 -0
  98. 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 normalisePathPattern = (path)=>{
6
+ const normaliseSlashes = (path)=>{
7
7
  const withoutDuplicateSlashes = path.replace(/\/+/g, '/');
8
8
  const withLeadingSlash = withoutDuplicateSlashes.startsWith('/') ? withoutDuplicateSlashes : `/${withoutDuplicateSlashes}`;
9
- const withoutTrailingSlash = withLeadingSlash.length > 1 ? withLeadingSlash.replace(/\/+$/, '') : withLeadingSlash;
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 (false === option) return {
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: true,
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 = normalisePathPattern(pattern).split('/').filter(Boolean);
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
- return {
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(normalisePathPattern(pathname));
194
+ const match = regexp.exec(normalisePathname(pathname));
160
195
  if (!match) return null;
161
- return names.reduce((params, name, index)=>{
162
- params[name] = decodeURIComponent(match[index + 1] || '');
163
- return params;
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 = normalisePathPattern(pathname);
180
- for (const localisedUrlEntry of Object.values(localisedUrls)){
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) for (const language of languages){
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(targetPattern, params);
258
+ if (params) return buildPathFromPattern(canonicalPattern, params);
187
259
  }
188
260
  }
189
261
  return normalizedPathname;
190
262
  };
191
- export { applyLocalisedUrlsToRoutes, normalisePathPattern, resolveLocalisedPath, resolveLocalisedUrlsConfig, validateLocalisedUrls };
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
- function hasJsonFiles(dirPath) {
8
- try {
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
- if (entryBackendOptions?.enabled === false) backendOptions = entryBackendOptions;
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
- else if (entryBackendOptions?.enabled !== true) {
54
- const hasLocales = detectLocalesDirectory(appDirectory, normalizedConfig);
55
- backendOptions = hasLocales ? {
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 { useModernI18n } from "./context.mjs";
4
- import { useI18nRouterAdapter } from "./routerAdapter.mjs";
5
- import { buildLocalizedUrl } from "./utils.mjs";
3
+ import { Link } from "./Link.mjs";
4
+ let warnedDeprecation = false;
6
5
  const I18nLink = ({ to, children, ...props })=>{
7
- const { Link, params, hasRouter } = useI18nRouterAdapter();
8
- const { language, supportedLanguages, localisedUrls } = useModernI18n();
9
- const currentLang = language;
10
- const localizedTo = buildLocalizedUrl(to, currentLang, supportedLanguages, localisedUrls);
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: localizedTo,
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;