nuxt-i18n-micro 1.98.0 → 1.100.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/dist/module.mjs CHANGED
@@ -149,26 +149,23 @@ function extractDefineI18nRouteData(content, filePath) {
149
149
  if (localesStrTrimmed.startsWith("[") && localesStrTrimmed.endsWith("]")) {
150
150
  const arrayMatch = localesStrTrimmed.match(/\[(.*?)\]/s);
151
151
  if (arrayMatch && arrayMatch[1]) {
152
- const elements = arrayMatch[1].split(",").map((el) => el.trim().replace(/['"]/g, "")).filter((el) => el.length > 0);
153
- locales = elements;
152
+ locales = arrayMatch[1].split(",").map((el) => el.trim().replace(/['"]/g, "")).filter((el) => el.length > 0);
154
153
  }
155
154
  }
156
155
  if (localesStrTrimmed.startsWith("{") && localesStrTrimmed.endsWith("}")) {
157
- const topLevelKeyMatches = localesStrTrimmed.match(/^\s*(\w+)\s*:\s*\{/gm);
156
+ const topLevelKeyMatches = localesStrTrimmed.match(/^\s*(\[?['"]?([\w-]+)['"]?\]?)\s*:\s*\{/gm);
158
157
  if (topLevelKeyMatches) {
159
- const keys = topLevelKeyMatches.map((match) => {
160
- const keyMatch = match.match(/^\s*(\w+)\s*:/);
158
+ locales = topLevelKeyMatches.map((match) => {
159
+ const keyMatch = match.match(/^\s*\[?['"]?([\w-]+)['"]?\]?\s*:/);
161
160
  return keyMatch ? keyMatch[1] : "";
162
161
  }).filter((key) => key.length > 0);
163
- locales = keys;
164
162
  } else {
165
- const fallbackMatches = localesStrTrimmed.match(/(\w+)\s*:\s*\{/g);
163
+ const fallbackMatches = localesStrTrimmed.match(/(\[?['"]?([\w-]+)['"]?\]?)\s*:\s*\{/g);
166
164
  if (fallbackMatches) {
167
- const keys = fallbackMatches.map((match) => {
168
- const keyMatch = match.match(/(\w+)\s*:/);
165
+ locales = fallbackMatches.map((match) => {
166
+ const keyMatch = match.match(/(\[?['"]?([\w-]+)['"]?\]?)\s*:/);
169
167
  return keyMatch ? keyMatch[1] : "";
170
168
  }).filter((key) => key.length > 0);
171
- locales = keys;
172
169
  }
173
170
  }
174
171
  }
@@ -240,9 +237,10 @@ class PageManager {
240
237
  activeLocaleCodes;
241
238
  globalLocaleRoutes;
242
239
  filesLocaleRoutes;
240
+ routeLocales;
243
241
  noPrefixRedirect;
244
242
  excludePatterns;
245
- constructor(locales, defaultLocaleCode, strategy, globalLocaleRoutes, filesLocaleRoutes, noPrefixRedirect, excludePatterns) {
243
+ constructor(locales, defaultLocaleCode, strategy, globalLocaleRoutes, filesLocaleRoutes, routeLocales, noPrefixRedirect, excludePatterns) {
246
244
  this.locales = locales;
247
245
  this.defaultLocale = this.findLocaleByCode(defaultLocaleCode) || { code: defaultLocaleCode };
248
246
  this.strategy = strategy;
@@ -251,6 +249,7 @@ class PageManager {
251
249
  this.activeLocaleCodes = this.computeActiveLocaleCodes();
252
250
  this.globalLocaleRoutes = globalLocaleRoutes || {};
253
251
  this.filesLocaleRoutes = filesLocaleRoutes || {};
252
+ this.routeLocales = routeLocales || {};
254
253
  }
255
254
  findLocaleByCode(code) {
256
255
  return this.locales.find((locale) => locale.code === code);
@@ -258,10 +257,22 @@ class PageManager {
258
257
  computeActiveLocaleCodes() {
259
258
  return this.locales.filter((locale) => locale.code !== this.defaultLocale.code || isPrefixAndDefaultStrategy(this.strategy) || isPrefixStrategy(this.strategy)).map((locale) => locale.code);
260
259
  }
260
+ getAllowedLocalesForPage(pagePath, pageName) {
261
+ const allowedLocales = this.routeLocales[pagePath] || this.routeLocales[pageName];
262
+ if (allowedLocales && allowedLocales.length > 0) {
263
+ return allowedLocales.filter(
264
+ (locale) => this.locales.some((l) => l.code === locale)
265
+ );
266
+ }
267
+ return this.locales.map((locale) => locale.code);
268
+ }
269
+ hasLocaleRestrictions(pagePath, pageName) {
270
+ return !!(this.routeLocales[pagePath] || this.routeLocales[pageName]);
271
+ }
261
272
  // private isAlreadyLocalized(p: string) {
262
273
  // const codes = this.locales.map(l => l.code).join('|') // en|de|ru…
263
- // return p.startsWith('/:locale(') // динамический префикс
264
- // || new RegExp(`^/(${codes})(/|$)`).test(p) // статический /de/…
274
+ // return p.startsWith('/:locale(') // dynamic prefix
275
+ // || new RegExp(`^/(${codes})(/|$)`).test(p) // static /de/…
265
276
  // }
266
277
  extendPages(pages, customRegex, isCloudflarePages) {
267
278
  this.localizedPaths = this.extractLocalizedPaths(pages);
@@ -302,15 +313,14 @@ class PageManager {
302
313
  const localizedPaths = {};
303
314
  pages.forEach((page) => {
304
315
  const pageName = buildRouteNameFromRoute(page.name, page.path);
305
- const globalLocalePath = this.globalLocaleRoutes[pageName];
316
+ const normalizedFullPath = normalizePath(path.posix.join(parentPath, page.path));
317
+ const globalLocalePath = this.globalLocaleRoutes[normalizedFullPath] || this.globalLocaleRoutes[pageName];
306
318
  if (!globalLocalePath) {
307
319
  const filesLocalePath = this.filesLocaleRoutes[pageName];
308
320
  if (filesLocalePath && typeof filesLocalePath === "object") {
309
- const normalizedFullPath = normalizePath(path.posix.join(parentPath, page.path));
310
321
  localizedPaths[normalizedFullPath] = filesLocalePath;
311
322
  }
312
323
  } else if (typeof globalLocalePath === "object") {
313
- const normalizedFullPath = normalizePath(path.posix.join(parentPath, page.path));
314
324
  localizedPaths[normalizedFullPath] = globalLocalePath;
315
325
  }
316
326
  if (page.children?.length) {
@@ -321,7 +331,12 @@ class PageManager {
321
331
  return localizedPaths;
322
332
  }
323
333
  addCustomGlobalLocalizedRoutes(page, customRoutePaths, additionalRoutes, customRegex) {
324
- this.locales.forEach((locale) => {
334
+ const normalizedFullPath = normalizePath(page.path);
335
+ const pageName = buildRouteNameFromRoute(page.name, page.path);
336
+ const allowedLocales = this.getAllowedLocalesForPage(normalizedFullPath, pageName);
337
+ const hasRestrictions = this.hasLocaleRestrictions(normalizedFullPath, pageName);
338
+ const localesToUse = hasRestrictions ? this.locales.filter((locale) => allowedLocales.includes(locale.code)) : this.locales;
339
+ localesToUse.forEach((locale) => {
325
340
  const customPath = customRoutePaths[locale.code];
326
341
  const isDefaultLocale = isLocaleDefault(locale, this.defaultLocale, isPrefixStrategy(this.strategy) || isPrefixAndDefaultStrategy(this.strategy));
327
342
  if (customPath) {
@@ -353,17 +368,44 @@ class PageManager {
353
368
  if (isPageRedirectOnly(page)) return;
354
369
  const originalChildren = cloneArray(page.children ?? []);
355
370
  const normalizedFullPath = normalizePath(page.path);
356
- const localeCodesWithoutCustomPaths = this.filterLocaleCodesWithoutCustomPaths(normalizedFullPath);
371
+ const pageName = buildRouteNameFromRoute(page.name, page.path);
372
+ const allowedLocales = this.getAllowedLocalesForPage(normalizedFullPath, pageName);
373
+ const hasRestrictions = this.hasLocaleRestrictions(normalizedFullPath, pageName);
374
+ const localeCodesWithoutCustomPaths = this.filterLocaleCodesWithoutCustomPaths(normalizedFullPath).filter((locale) => hasRestrictions ? allowedLocales.includes(locale) : true);
357
375
  if (localeCodesWithoutCustomPaths.length) {
358
376
  const newRoute = this.createLocalizedRoute(page, localeCodesWithoutCustomPaths, originalChildren, false, "", customRegex, false, true);
359
377
  if (newRoute) additionalRoutes.push(newRoute);
360
378
  }
361
- this.addCustomLocalizedRoutes(page, normalizedFullPath, originalChildren, additionalRoutes);
379
+ this.addCustomLocalizedRoutes(page, normalizedFullPath, originalChildren, additionalRoutes, hasRestrictions ? allowedLocales : void 0);
362
380
  this.adjustRouteForDefaultLocale(page, originalChildren);
381
+ this.handleAliasRoutes(page, additionalRoutes, customRegex, hasRestrictions ? allowedLocales : void 0);
363
382
  }
364
383
  filterLocaleCodesWithoutCustomPaths(fullPath) {
365
384
  return this.activeLocaleCodes.filter((code) => !this.localizedPaths[fullPath]?.[code]);
366
385
  }
386
+ handleAliasRoutes(page, additionalRoutes, customRegex, allowedLocales) {
387
+ const aliasRoutes = page.alias || page.meta?.alias;
388
+ if (!aliasRoutes || !Array.isArray(aliasRoutes)) {
389
+ return;
390
+ }
391
+ const localesToUse = allowedLocales || this.activeLocaleCodes;
392
+ aliasRoutes.forEach((aliasPath) => {
393
+ const localizedAliasPath = buildFullPath(localesToUse, aliasPath, customRegex);
394
+ const aliasRoute = {
395
+ ...page,
396
+ path: localizedAliasPath,
397
+ name: `localized-${page.name ?? ""}`,
398
+ meta: {
399
+ ...page.meta,
400
+ alias: void 0
401
+ // Remove alias to prevent infinite recursion
402
+ },
403
+ alias: void 0
404
+ // Remove alias from root to prevent infinite recursion
405
+ };
406
+ additionalRoutes.push(aliasRoute);
407
+ });
408
+ }
367
409
  adjustRouteForDefaultLocale(page, originalChildren) {
368
410
  if (isNoPrefixStrategy(this.strategy)) {
369
411
  return;
@@ -397,16 +439,20 @@ class PageManager {
397
439
  page.children = currentChildren;
398
440
  }
399
441
  }
400
- addCustomLocalizedRoutes(page, fullPath, originalChildren, additionalRoutes, customRegex) {
401
- this.locales.forEach((locale) => {
442
+ addCustomLocalizedRoutes(page, fullPath, originalChildren, additionalRoutes, allowedLocales, customRegex) {
443
+ const localesToUse = allowedLocales ? this.locales.filter((locale) => allowedLocales.includes(locale.code)) : this.locales;
444
+ localesToUse.forEach((locale) => {
402
445
  const customPath = this.localizedPaths[fullPath]?.[locale.code];
403
446
  if (!customPath) return;
404
447
  const isDefaultLocale = isLocaleDefault(locale, this.defaultLocale, isPrefixStrategy(this.strategy) || isNoPrefixStrategy(this.strategy));
405
- if (isDefaultLocale) {
406
- page.children = this.createLocalizedChildren(originalChildren, "", [locale.code], false);
448
+ if (isDefaultLocale && isPrefixExceptDefaultStrategy(this.strategy)) {
449
+ page.path = normalizePath(customPath);
450
+ page.children = this.createLocalizedChildren(originalChildren, "", [locale.code], false, false, false, { [locale.code]: customPath });
407
451
  } else {
408
452
  const newRoute = this.createLocalizedRoute(page, [locale.code], originalChildren, true, customPath, customRegex, false, locale.code);
409
- if (newRoute) additionalRoutes.push(newRoute);
453
+ if (newRoute) {
454
+ additionalRoutes.push(newRoute);
455
+ }
410
456
  }
411
457
  if (isPrefixAndDefaultStrategy(this.strategy) && locale === this.defaultLocale) {
412
458
  const newRoute = this.createLocalizedRoute(page, [locale.code], originalChildren, true, customPath, customRegex, true, locale.code);
@@ -430,7 +476,14 @@ class PageManager {
430
476
  createLocalizedVariants(route, parentPath, localeCodes, modifyName, addLocalePrefix, parentLocale = false, localizedParentPaths) {
431
477
  const routePath = normalizePath(route.path);
432
478
  const fullPath = normalizePath(path.posix.join(parentPath, routePath));
433
- const customLocalePaths = this.localizedPaths[fullPath] ?? this.localizedPaths[normalizePath(route.path)];
479
+ let customLocalePaths = this.localizedPaths[fullPath] ?? this.localizedPaths[normalizePath(route.path)];
480
+ if (!customLocalePaths && Object.keys(localizedParentPaths).length > 0) {
481
+ const hasLocalizedContext = Object.values(localizedParentPaths).some((path2) => path2 && path2 !== "");
482
+ if (hasLocalizedContext) {
483
+ const originalRoutePath = normalizePath(path.posix.join("/activity-locale", route.path));
484
+ customLocalePaths = this.localizedPaths[originalRoutePath];
485
+ }
486
+ }
434
487
  const isCustomLocalized = !!customLocalePaths;
435
488
  const result = [];
436
489
  if (!isCustomLocalized) {
@@ -457,15 +510,21 @@ class PageManager {
457
510
  const parentLocalizedPath = localizedParentPaths?.[locale];
458
511
  const hasParentLocalized = !!parentLocalizedPath;
459
512
  const customPath = customLocalePaths?.[locale];
460
- const basePath = customPath ? normalizePath(customPath) : normalizePath(route.path);
513
+ let basePath = customPath ? normalizePath(customPath) : normalizePath(route.path);
514
+ if (hasParentLocalized && parentLocalizedPath) {
515
+ if (customPath) {
516
+ basePath = normalizePath(customPath);
517
+ } else {
518
+ basePath = normalizePath(path.posix.join(parentLocalizedPath, route.path));
519
+ }
520
+ }
461
521
  const finalRoutePath = shouldAddLocalePrefix(
462
522
  locale,
463
523
  this.defaultLocale,
464
524
  addLocalePrefix,
465
525
  isPrefixStrategy(this.strategy)
466
526
  ) ? buildFullPath(locale, basePath) : basePath;
467
- const isChildRoute = parentPath !== "";
468
- const finalPathForRoute = isChildRoute && hasParentLocalized ? normalizePath(route.path) : removeLeadingSlash(finalRoutePath);
527
+ const finalPathForRoute = removeLeadingSlash(finalRoutePath);
469
528
  const nextParentPath = customPath ? normalizePath(customPath) : hasParentLocalized ? parentLocalizedPath : normalizePath(path.posix.join(parentPath, routePath));
470
529
  const localizedChildren = this.createLocalizedChildren(
471
530
  cloneArray(route.children ?? []),
@@ -512,7 +571,7 @@ class PageManager {
512
571
  const routeName = buildRouteName(buildRouteNameFromRoute(page.name ?? "", page.path ?? ""), firstLocale, isCustom);
513
572
  return {
514
573
  ...page,
515
- children: this.createLocalizedChildren(originalChildren, page.path, localeCodes, true, false, parentLocale),
574
+ children: this.createLocalizedChildren(originalChildren, page.path, localeCodes, true, false, parentLocale, { [firstLocale]: customPath }),
516
575
  path: routePath,
517
576
  name: routeName
518
577
  };
@@ -670,17 +729,17 @@ const module = defineNuxtModule({
670
729
  const fileContent = readFileSync(fullPath, "utf-8");
671
730
  const { locales: extractedLocales, localeRoutes } = extractDefineI18nRouteData(fileContent, fullPath);
672
731
  const routePath = pageFile.replace(/^pages\//, "/").replace(/\/index\.vue$/, "").replace(/\.vue$/, "").replace(/\/$/, "") || "/";
673
- const pageName = routePath.replace(/[^a-z0-9]/gi, "-").replace(/^-+|-+$/g, "");
674
732
  if (extractedLocales) {
675
733
  routeLocales[routePath] = extractedLocales;
676
734
  }
677
735
  if (localeRoutes) {
678
- globalLocaleRoutes[pageName] = localeRoutes;
736
+ globalLocaleRoutes[routePath] = localeRoutes;
679
737
  }
680
738
  } catch {
681
739
  }
682
740
  }
683
- const pageManager = new PageManager(localeManager.locales, defaultLocale, options.strategy, options.globalLocaleRoutes, globalLocaleRoutes, options.noPrefixRedirect, options.excludePatterns);
741
+ const mergedGlobalLocaleRoutes = { ...options.globalLocaleRoutes, ...globalLocaleRoutes };
742
+ const pageManager = new PageManager(localeManager.locales, defaultLocale, options.strategy, mergedGlobalLocaleRoutes, globalLocaleRoutes, routeLocales, options.noPrefixRedirect, options.excludePatterns);
684
743
  addTemplate({
685
744
  filename: "i18n.plural.mjs",
686
745
  write: true,
@@ -708,7 +767,10 @@ const module = defineNuxtModule({
708
767
  excludePatterns: options.excludePatterns ?? [],
709
768
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
710
769
  // @ts-ignore
711
- routeLocales
770
+ routeLocales,
771
+ experimental: {
772
+ i18nPreviousPageFallback: options.experimental?.i18nPreviousPageFallback ?? false
773
+ }
712
774
  };
713
775
  if (typeof options.customRegexMatcher !== "undefined") {
714
776
  const localeCodes = localeManager.locales.map((l) => l.code);
@@ -31,8 +31,13 @@ export default defineNuxtPlugin(async (nuxtApp) => {
31
31
  );
32
32
  const translationService = new FormatService();
33
33
  const i18nRouteParams = useState("i18n-route-params", () => ({}));
34
- nuxtApp.hook("page:start", () => {
35
- i18nRouteParams.value = null;
34
+ const previousPageInfo = useState("i18n-previous-page", () => null);
35
+ const enablePreviousPageFallback = i18nConfig.experimental?.i18nPreviousPageFallback ?? false;
36
+ nuxtApp.hook("page:finish", () => {
37
+ if (import.meta.client) {
38
+ i18nRouteParams.value = null;
39
+ previousPageInfo.value = null;
40
+ }
36
41
  });
37
42
  const loadTranslationsIfNeeded = async (locale, routeName, path) => {
38
43
  try {
@@ -90,6 +95,12 @@ export default defineNuxtPlugin(async (nuxtApp) => {
90
95
  }
91
96
  router.beforeEach(async (to, from, next) => {
92
97
  if (to.path !== from.path || isNoPrefixStrategy(i18nConfig.strategy)) {
98
+ if (import.meta.client && from.path !== to.path && enablePreviousPageFallback) {
99
+ const fromLocale = routeService.getCurrentLocale(from);
100
+ const fromRouteName = routeService.getRouteName(from, fromLocale);
101
+ previousPageInfo.value = { locale: fromLocale, routeName: fromRouteName };
102
+ console.log(`Saved previous page info for cleanup: ${fromRouteName}`);
103
+ }
93
104
  await loadGlobalTranslations(to);
94
105
  }
95
106
  if (next) {
@@ -115,9 +126,20 @@ export default defineNuxtPlugin(async (nuxtApp) => {
115
126
  const locale = routeService.getCurrentLocale();
116
127
  const routeName = routeService.getRouteName(route, locale);
117
128
  let value = i18nHelper.getTranslation(locale, routeName, key);
129
+ if (!value && previousPageInfo.value && enablePreviousPageFallback) {
130
+ const prev = previousPageInfo.value;
131
+ const prevValue = i18nHelper.getTranslation(prev.locale, prev.routeName, key);
132
+ if (prevValue) {
133
+ value = prevValue;
134
+ console.log(`Using fallback translation from previous route: ${prev.routeName} -> ${key}`);
135
+ }
136
+ }
137
+ if (!value) {
138
+ value = i18nHelper.getTranslation(locale, "", key);
139
+ }
118
140
  if (!value) {
119
141
  if (isDev && import.meta.client) {
120
- console.warn(`Not found '${key}' key in '${locale}' locale messages.`);
142
+ console.warn(`Not found '${key}' key in '${locale}' locale messages for route '${routeName}'.`);
121
143
  }
122
144
  value = defaultValue === void 0 ? key : defaultValue;
123
145
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-i18n-micro",
3
- "version": "1.98.0",
3
+ "version": "1.100.0",
4
4
  "description": "Nuxt I18n Micro is a lightweight, high-performance internationalization module for Nuxt, designed to handle multi-language support with minimal overhead, fast build times, and efficient runtime performance.",
5
5
  "repository": "s00d/nuxt-i18n-micro",
6
6
  "license": "MIT",
@@ -62,8 +62,8 @@
62
62
  "sirv": "^2.0.4",
63
63
  "ufo": "^1.5.4",
64
64
  "nuxt-i18n-micro-core": "1.0.18",
65
- "nuxt-i18n-micro-types": "1.0.8",
66
- "nuxt-i18n-micro-test-utils": "1.0.6"
65
+ "nuxt-i18n-micro-test-utils": "1.0.6",
66
+ "nuxt-i18n-micro-types": "1.0.9"
67
67
  },
68
68
  "devDependencies": {
69
69
  "@nuxt/devtools": "^2.6.3",
@@ -1 +0,0 @@
1
- {"id":"9bdb882b-a145-42be-bbc5-f7c9185266d5","timestamp":1759229326396,"matcher":{"static":{},"wildcard":{},"dynamic":{}},"prerendered":[]}