nuxt-i18n-micro 4.0.1 → 4.1.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 (34) hide show
  1. package/dist/client/200.html +1 -1
  2. package/dist/client/404.html +1 -1
  3. package/dist/client/_nuxt/builds/latest.json +1 -1
  4. package/dist/client/_nuxt/builds/meta/54fb212e-6673-4dcd-9032-ab7a5f971d8f.json +1 -0
  5. package/dist/client/index.html +1 -1
  6. package/dist/module.d.mts +4 -2
  7. package/dist/module.json +1 -1
  8. package/dist/module.mjs +74 -47
  9. package/dist/runtime/composables/useI18nLocale.js +1 -1
  10. package/dist/runtime/plugins/01.plugin.d.ts +11 -11
  11. package/dist/runtime/plugins/01.plugin.js +4 -2
  12. package/dist/runtime/plugins/06.redirect.js +4 -3
  13. package/dist/runtime/server/internal/dev-live-translations.js +4 -1
  14. package/dist/runtime/server/internal/translation-watcher.d.ts +2 -0
  15. package/dist/runtime/server/internal/translation-watcher.js +5 -9
  16. package/dist/runtime/server/utils/server-loader.js +4 -1
  17. package/dist/runtime/server/utils/translation-server-middleware.d.ts +7 -2
  18. package/dist/runtime/server/utils/translation-server-middleware.js +36 -3
  19. package/dist/runtime/server/utils/warn-once.d.ts +1 -0
  20. package/dist/runtime/server/utils/warn-once.js +6 -0
  21. package/dist/runtime/utils/formatter.d.ts +9 -0
  22. package/dist/runtime/utils/formatter.js +7 -0
  23. package/dist/runtime/utils/hooks.d.ts +8 -0
  24. package/dist/runtime/utils/hooks.js +10 -0
  25. package/dist/runtime/utils/i18n-api.d.ts +10 -7
  26. package/dist/runtime/utils/i18n-api.js +13 -4
  27. package/dist/runtime/utils/i18n-config.js +21 -4
  28. package/dist/runtime/utils/i18n-navigation.js +30 -9
  29. package/dist/runtime/utils/nuxt-i18n.d.ts +3 -9
  30. package/dist/runtime/utils/nuxt-i18n.js +0 -7
  31. package/dist/types.d.mts +1 -1
  32. package/internals.d.mts +6 -0
  33. package/package.json +6 -6
  34. package/dist/client/_nuxt/builds/meta/356b87e3-45dd-4f80-a40a-b649665c1844.json +0 -1
@@ -1 +1 @@
1
- <!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><script type="importmap">{"imports":{"#entry":"/__nuxt-i18n-micro/_nuxt/DKUOBR6F.js"}}</script><link rel="stylesheet" href="/__nuxt-i18n-micro/_nuxt/entry.PSWtHhay.css" crossorigin><link rel="modulepreload" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/DKUOBR6F.js"><link rel="modulepreload" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/BVna_KKx.js"><script type="module" src="/__nuxt-i18n-micro/_nuxt/DKUOBR6F.js" crossorigin></script><link rel="prefetch" as="style" crossorigin href="/__nuxt-i18n-micro/_nuxt/error-404.xs3fxhVJ.css"><link rel="prefetch" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/CmvObVGq.js"><link rel="prefetch" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/CPz_jhsD.js"><link rel="prefetch" as="style" crossorigin href="/__nuxt-i18n-micro/_nuxt/error-500.C2S-4iLg.css"><link rel="prefetch" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/Bw81RhAv.js"></head><body><div id="__nuxt"></div><div id="teleports"></div><script>window.__NUXT__={};window.__NUXT__.config={public:{},app:{baseURL:"/__nuxt-i18n-micro",buildId:"356b87e3-45dd-4f80-a40a-b649665c1844",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},1793785774795,false]</script></body></html>
1
+ <!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><script type="importmap">{"imports":{"#entry":"/__nuxt-i18n-micro/_nuxt/DKUOBR6F.js"}}</script><link rel="stylesheet" href="/__nuxt-i18n-micro/_nuxt/entry.PSWtHhay.css" crossorigin><link rel="modulepreload" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/DKUOBR6F.js"><link rel="modulepreload" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/BVna_KKx.js"><script type="module" src="/__nuxt-i18n-micro/_nuxt/DKUOBR6F.js" crossorigin></script><link rel="prefetch" as="style" crossorigin href="/__nuxt-i18n-micro/_nuxt/error-404.xs3fxhVJ.css"><link rel="prefetch" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/CmvObVGq.js"><link rel="prefetch" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/CPz_jhsD.js"><link rel="prefetch" as="style" crossorigin href="/__nuxt-i18n-micro/_nuxt/error-500.C2S-4iLg.css"><link rel="prefetch" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/Bw81RhAv.js"></head><body><div id="__nuxt"></div><div id="teleports"></div><script>window.__NUXT__={};window.__NUXT__.config={public:{},app:{baseURL:"/__nuxt-i18n-micro",buildId:"54fb212e-6673-4dcd-9032-ab7a5f971d8f",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},1793807641063,false]</script></body></html>
@@ -1 +1 @@
1
- <!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><script type="importmap">{"imports":{"#entry":"/__nuxt-i18n-micro/_nuxt/DKUOBR6F.js"}}</script><link rel="stylesheet" href="/__nuxt-i18n-micro/_nuxt/entry.PSWtHhay.css" crossorigin><link rel="modulepreload" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/DKUOBR6F.js"><link rel="modulepreload" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/BVna_KKx.js"><script type="module" src="/__nuxt-i18n-micro/_nuxt/DKUOBR6F.js" crossorigin></script><link rel="prefetch" as="style" crossorigin href="/__nuxt-i18n-micro/_nuxt/error-404.xs3fxhVJ.css"><link rel="prefetch" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/CmvObVGq.js"><link rel="prefetch" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/CPz_jhsD.js"><link rel="prefetch" as="style" crossorigin href="/__nuxt-i18n-micro/_nuxt/error-500.C2S-4iLg.css"><link rel="prefetch" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/Bw81RhAv.js"></head><body><div id="__nuxt"></div><div id="teleports"></div><script>window.__NUXT__={};window.__NUXT__.config={public:{},app:{baseURL:"/__nuxt-i18n-micro",buildId:"356b87e3-45dd-4f80-a40a-b649665c1844",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},1793785774795,false]</script></body></html>
1
+ <!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><script type="importmap">{"imports":{"#entry":"/__nuxt-i18n-micro/_nuxt/DKUOBR6F.js"}}</script><link rel="stylesheet" href="/__nuxt-i18n-micro/_nuxt/entry.PSWtHhay.css" crossorigin><link rel="modulepreload" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/DKUOBR6F.js"><link rel="modulepreload" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/BVna_KKx.js"><script type="module" src="/__nuxt-i18n-micro/_nuxt/DKUOBR6F.js" crossorigin></script><link rel="prefetch" as="style" crossorigin href="/__nuxt-i18n-micro/_nuxt/error-404.xs3fxhVJ.css"><link rel="prefetch" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/CmvObVGq.js"><link rel="prefetch" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/CPz_jhsD.js"><link rel="prefetch" as="style" crossorigin href="/__nuxt-i18n-micro/_nuxt/error-500.C2S-4iLg.css"><link rel="prefetch" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/Bw81RhAv.js"></head><body><div id="__nuxt"></div><div id="teleports"></div><script>window.__NUXT__={};window.__NUXT__.config={public:{},app:{baseURL:"/__nuxt-i18n-micro",buildId:"54fb212e-6673-4dcd-9032-ab7a5f971d8f",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},1793807641063,false]</script></body></html>
@@ -1 +1 @@
1
- {"id":"356b87e3-45dd-4f80-a40a-b649665c1844","timestamp":1793785771623}
1
+ {"id":"54fb212e-6673-4dcd-9032-ab7a5f971d8f","timestamp":1793807638616}
@@ -0,0 +1 @@
1
+ {"id":"54fb212e-6673-4dcd-9032-ab7a5f971d8f","timestamp":1793807638616,"prerendered":[]}
@@ -1 +1 @@
1
- <!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><script type="importmap">{"imports":{"#entry":"/__nuxt-i18n-micro/_nuxt/DKUOBR6F.js"}}</script><link rel="stylesheet" href="/__nuxt-i18n-micro/_nuxt/entry.PSWtHhay.css" crossorigin><link rel="modulepreload" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/DKUOBR6F.js"><link rel="modulepreload" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/BVna_KKx.js"><script type="module" src="/__nuxt-i18n-micro/_nuxt/DKUOBR6F.js" crossorigin></script><link rel="prefetch" as="style" crossorigin href="/__nuxt-i18n-micro/_nuxt/error-404.xs3fxhVJ.css"><link rel="prefetch" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/CmvObVGq.js"><link rel="prefetch" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/CPz_jhsD.js"><link rel="prefetch" as="style" crossorigin href="/__nuxt-i18n-micro/_nuxt/error-500.C2S-4iLg.css"><link rel="prefetch" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/Bw81RhAv.js"></head><body><div id="__nuxt"></div><div id="teleports"></div><script>window.__NUXT__={};window.__NUXT__.config={public:{},app:{baseURL:"/__nuxt-i18n-micro",buildId:"356b87e3-45dd-4f80-a40a-b649665c1844",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},1793785774795,false]</script></body></html>
1
+ <!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><script type="importmap">{"imports":{"#entry":"/__nuxt-i18n-micro/_nuxt/DKUOBR6F.js"}}</script><link rel="stylesheet" href="/__nuxt-i18n-micro/_nuxt/entry.PSWtHhay.css" crossorigin><link rel="modulepreload" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/DKUOBR6F.js"><link rel="modulepreload" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/BVna_KKx.js"><script type="module" src="/__nuxt-i18n-micro/_nuxt/DKUOBR6F.js" crossorigin></script><link rel="prefetch" as="style" crossorigin href="/__nuxt-i18n-micro/_nuxt/error-404.xs3fxhVJ.css"><link rel="prefetch" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/CmvObVGq.js"><link rel="prefetch" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/CPz_jhsD.js"><link rel="prefetch" as="style" crossorigin href="/__nuxt-i18n-micro/_nuxt/error-500.C2S-4iLg.css"><link rel="prefetch" as="script" crossorigin href="/__nuxt-i18n-micro/_nuxt/Bw81RhAv.js"></head><body><div id="__nuxt"></div><div id="teleports"></div><script>window.__NUXT__={};window.__NUXT__.config={public:{},app:{baseURL:"/__nuxt-i18n-micro",buildId:"54fb212e-6673-4dcd-9032-ab7a5f971d8f",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},1793807641063,false]</script></body></html>
package/dist/module.d.mts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as _nuxt_schema from '@nuxt/schema';
2
2
  import { HookResult } from '@nuxt/schema';
3
- import { ModuleOptions } from '@i18n-micro/types';
4
- export { Getter, GlobalLocaleRoutes, Locale, LocaleCode, ModuleOptions, PluralFunc, Strategies } from '@i18n-micro/types';
3
+ import { I18nBeforeLocaleSwitchContext, ModuleOptions } from '@i18n-micro/types';
4
+ export { Getter, GlobalLocaleRoutes, I18nBeforeLocaleSwitchContext, Locale, LocaleCode, ModuleOptions, PluralFunc, Strategies } from '@i18n-micro/types';
5
5
  import { PluginsInjections } from '../dist/runtime/plugins/01.plugin.js';
6
6
  export { PluginsInjections } from '../dist/runtime/plugins/01.plugin.js';
7
7
 
@@ -16,6 +16,8 @@ declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, fa
16
16
  */
17
17
  interface ModuleRuntimeHooks {
18
18
  'i18n:register': (registerModule: (translations: unknown, locale?: string) => void, locale: string) => HookResult;
19
+ /** Before the page switches to another locale: set `ctx.to` to switch to another one, `ctx.cancel = true` to stay. */
20
+ 'i18n:beforeLocaleSwitch': (ctx: I18nBeforeLocaleSwitchContext) => HookResult;
19
21
  }
20
22
  declare module '#app' {
21
23
  interface RuntimeNuxtHooks extends ModuleRuntimeHooks {
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=3.18.0"
6
6
  },
7
- "version": "4.0.1",
7
+ "version": "4.1.0",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.3",
10
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -1,22 +1,22 @@
1
1
  import * as fs from 'node:fs';
2
2
  import fs__default, { existsSync, mkdirSync, writeFileSync, readdirSync, statSync, readFileSync } from 'node:fs';
3
- import path, { join, dirname, relative, basename, resolve } from 'node:path';
3
+ import path, { resolve, dirname, join, relative, basename, isAbsolute } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { defaultPlural, isNoPrefixStrategy } from '@i18n-micro/core';
6
6
  import { forwardSlashes, trimLeadingSlashes, isInternalPath } from '@i18n-micro/utils/app-path';
7
- import { readTranslationFile, resolveLiveTranslationDirs, translationRelativePath } from '../dist/runtime/server/internal/translation-watcher.js';
7
+ import { readTranslationFile, translationRelativePath, resolveLiveTranslationDirs } from '../dist/runtime/server/internal/translation-watcher.js';
8
8
  import { isLocaleAllowedForUnlocalizedRoute, normalizePath, routeKey, nameOfPath, RouteGenerator, localesOutsideMatcher, pageNames } from '@i18n-micro/route-strategy';
9
9
  import { configForHost } from '@i18n-micro/utils/domains';
10
10
  import { createHash } from 'node:crypto';
11
11
  import { globby } from 'globby';
12
12
  import { deepMergeTranslationsRecursive } from '@i18n-micro/utils/deep-merge';
13
+ import { additionalTranslationRoots, isInsideDir, resolveTranslationFile } from '@i18n-micro/utils/load-dir';
13
14
  import { PREMERGED_ROOT_PAGE, mergeSourceTranslations, PREMERGED_ROOT_MARK, mergeOverRoot, buildFallbackLocaleChain } from '@i18n-micro/utils/merge-source';
14
15
  import { gzipSync, brotliCompressSync, constants } from 'node:zlib';
15
16
  import { resolveTranslationPayloadCachePolicy } from '@i18n-micro/utils/payload-url';
16
17
  import { useNuxt, addDevServerHandler, defineNuxtModule, useLogger, createResolver, addImportsDir, addPlugin, addRouteMiddleware, addPluginTemplate, addServerImports, addServerHandler, addComponentsDir, addVitePlugin, addTemplate, addTypeTemplate, resolvePath, addPrerenderRoutes } from '@nuxt/kit';
17
18
  import sirv from 'sirv';
18
19
  import { startSubprocess, addCustomTab, onDevToolsInitialized, extendServerRpc } from '@nuxt/devtools-kit';
19
- import { isInsideDir, resolveTranslationFile } from '@i18n-micro/utils/load-dir';
20
20
  import { fromNodeMiddleware, defineEventHandler, proxyRequest } from 'h3';
21
21
  import { joinURL } from 'ufo';
22
22
  import { DomainPathStrategy, createPathStrategy } from '@i18n-micro/path-strategy';
@@ -61,24 +61,14 @@ ${read}`;
61
61
  function mergeAdditionalRootLocaleFiles(rootDirs, additionalDirNames, locale, primaryContent = {}) {
62
62
  if (!additionalDirNames.length) return primaryContent;
63
63
  let content = {};
64
- for (const dirName of additionalDirNames) {
65
- for (const rootDir of rootDirs) {
66
- content = deepMergeTranslationsRecursive(content, readTranslationFile(join(rootDir, dirName, `${locale}.json`)));
67
- }
68
- }
64
+ for (const dir of additionalTranslationRoots(rootDirs, additionalDirNames))
65
+ content = deepMergeTranslationsRecursive(content, readTranslationFile(join(dir, `${locale}.json`)));
69
66
  return deepMergeTranslationsRecursive(content, primaryContent);
70
67
  }
71
68
  function listAdditionalRootLocales(rootDirs, additionalDirNames) {
72
69
  const locales = /* @__PURE__ */ new Set();
73
- for (const dirName of additionalDirNames) {
74
- for (const rootDir of rootDirs) {
75
- const dir = join(rootDir, dirName);
76
- if (!existsSync(dir)) continue;
77
- for (const name of readdirSync(dir)) {
78
- if (!name.endsWith(".json")) continue;
79
- locales.add(name.slice(0, -".json".length));
80
- }
81
- }
70
+ for (const dir of additionalTranslationRoots(rootDirs, additionalDirNames).filter((path) => existsSync(path))) {
71
+ for (const name of readdirSync(dir)) if (name.endsWith(".json")) locales.add(name.slice(0, -".json".length));
82
72
  }
83
73
  return [...locales];
84
74
  }
@@ -130,7 +120,7 @@ async function namingDeepFile(rootDirs, dirNames, step) {
130
120
  await step();
131
121
  } catch (error) {
132
122
  if (!(error instanceof RangeError) || !error.message.includes("call stack")) throw error;
133
- const dirs = rootDirs.flatMap((root) => dirNames.map((name) => join(root, name))).filter((dir) => existsSync(dir));
123
+ const dirs = rootDirs.flatMap((root) => dirNames.map((name) => resolve(root, name))).filter((dir) => existsSync(dir));
134
124
  const files = (await Promise.all(dirs.map((cwd) => globby("**/*.json", { cwd, absolute: true })))).flat();
135
125
  const [deepest] = files.map((file) => ({ file, depth: depthOf(readTranslationFile(file)) })).sort((a, b) => b.depth - a.depth);
136
126
  if (!deepest) throw error;
@@ -142,7 +132,13 @@ async function preMergeLocales(options) {
142
132
  }
143
133
  async function writeChunks(options) {
144
134
  const { rootDirs, translationDirName, outputDir, locales, globalFallbackLocale, disablePageLocales, additionalTranslationDirs } = options;
145
- const { routeNames = [], rootDelta = false } = options;
135
+ const { routeNames = [], rootDelta = false, linkedMessages = false } = options;
136
+ const warned = /* @__PURE__ */ new Set();
137
+ const warn = (message) => {
138
+ if (warned.has(message)) return;
139
+ warned.add(message);
140
+ options.warn?.(message);
141
+ };
146
142
  const merged = await mergedFiles(rootDirs, translationDirName, outputDir, additionalTranslationDirs);
147
143
  const pages = /* @__PURE__ */ new Set([...[...merged.keys()].filter((f) => f.startsWith("pages/")).map((f) => dirname(f).slice("pages/".length)), ...routeNames]);
148
144
  const targets = disablePageLocales || pages.size === 0 ? ["index"] : [...pages, ...pages.has("index") ? [] : ["index"]];
@@ -152,7 +148,10 @@ async function writeChunks(options) {
152
148
  locales,
153
149
  globalFallbackLocale,
154
150
  disablePageLocales,
155
- readLocaleFile: (relativePath) => merged.get(relativePath) ?? {}
151
+ readLocaleFile: (relativePath) => merged.get(relativePath) ?? {},
152
+ // the root and every page resolved alike: a delta is taken between the resolved trees
153
+ linkedMessages,
154
+ warn
156
155
  });
157
156
  const write = (page, locale, data) => {
158
157
  const targetPath = join(outputDir, page, locale, "data.json");
@@ -342,12 +341,14 @@ function hashTranslationSources(rootDirs, translationDirName, additionalTranslat
342
341
  }
343
342
  hashFiles(dir, files);
344
343
  };
344
+ const relativeExtras = additionalTranslationDirs.filter((extra) => !isAbsolute(extra));
345
345
  for (const rootDir of rootDirs) {
346
346
  hashDir(join(rootDir, translationDirName));
347
- for (const extra of additionalTranslationDirs) {
347
+ for (const extra of relativeExtras) {
348
348
  hashAdditionalRootLocales(join(rootDir, extra));
349
349
  }
350
350
  }
351
+ for (const extra of new Set(additionalTranslationDirs.filter((dir) => isAbsolute(dir)))) hashAdditionalRootLocales(extra);
351
352
  return seen > 0 ? hash.digest("hex").slice(0, 16) : null;
352
353
  }
353
354
  const NITRO_MIN_COMPRESS_BYTES = 1024;
@@ -451,7 +452,8 @@ function setupDevToolsUI(options, rootDirs) {
451
452
  const extraDir = file.startsWith("additional:") ? (options.additionalTranslationDirs ?? []).find((dir) => file.startsWith(`additional:${dir}/`)) : void 0;
452
453
  if (file.startsWith("additional:") && !extraDir) throw new Error(`Not a configured additional translation directory: ${file}`);
453
454
  const relativeFile = extraDir ? file.slice(`additional:${extraDir}/`.length) : file;
454
- const target = [...rootDirs].reverse().map((rootDir) => resolveTranslationFile(path.join(rootDir, extraDir ?? (options.translationDir || "locales")), relativeFile)).find((candidate) => candidate !== null);
455
+ const dirs = extraDir ? additionalTranslationRoots(rootDirs, [extraDir]) : rootDirs.map((rootDir) => path.join(rootDir, options.translationDir || "locales"));
456
+ const target = dirs.reverse().map((dir) => resolveTranslationFile(dir, relativeFile)).find((candidate) => candidate !== null);
455
457
  if (!target) throw new Error(`File not found in the translation directories: ${file}`);
456
458
  fs.writeFileSync(target, JSON.stringify(content, null, 2), "utf-8");
457
459
  },
@@ -484,14 +486,12 @@ function setupDevToolsUI(options, rootDirs) {
484
486
  const pagesDir = path.join(localesDir, "pages");
485
487
  processDirectory(localesDir, localesDir);
486
488
  processDirectory(pagesDir, localesDir);
487
- for (const extra of options.additionalTranslationDirs ?? []) {
488
- const extraDir = path.join(rootDir, extra);
489
- if (!fs.existsSync(extraDir)) continue;
489
+ }
490
+ for (const extra of options.additionalTranslationDirs ?? []) {
491
+ for (const extraDir of additionalTranslationRoots(rootDirs, [extra]).filter((dir) => fs.existsSync(dir))) {
490
492
  for (const name of fs.readdirSync(extraDir)) {
491
- if (!name.endsWith(".json")) continue;
492
493
  const filePath = path.join(extraDir, name);
493
- if (!fs.statSync(filePath).isFile()) continue;
494
- readFile(`additional:${extra}/${name}`, filePath);
494
+ if (name.endsWith(".json") && fs.statSync(filePath).isFile()) readFile(`additional:${extra}/${name}`, filePath);
495
495
  }
496
496
  }
497
497
  }
@@ -499,8 +499,10 @@ function setupDevToolsUI(options, rootDirs) {
499
499
  }
500
500
  };
501
501
  const rpc = extendServerRpc(DEVTOOLS_RPC_NAMESPACE, functions);
502
- const directories = [options.translationDir || "locales", ...options.additionalTranslationDirs ?? []];
503
- const roots = rootDirs.flatMap((root) => directories.map((dir) => resolve(root, dir)));
502
+ const roots = [
503
+ ...rootDirs.map((root) => resolve(root, options.translationDir || "locales")),
504
+ ...additionalTranslationRoots(rootDirs, options.additionalTranslationDirs)
505
+ ];
504
506
  let timer;
505
507
  nuxt.hook("builder:watch", (_event, watched) => {
506
508
  const file = resolve(nuxt.options.srcDir, watched);
@@ -881,6 +883,8 @@ function extractDefineI18nRouteData(content, filePath) {
881
883
  warnUnextractable(filePath, result.error);
882
884
  return null;
883
885
  }
886
+ if (result.config === false) return false;
887
+ if (!result.config || typeof result.config !== "object") return null;
884
888
  return normalizeDefineConfig(result.config);
885
889
  } catch {
886
890
  return null;
@@ -919,6 +923,10 @@ function mergeDefineI18nRouteMeta(entries, config) {
919
923
  const routeDisableMeta = { ...config.routeDisableMeta };
920
924
  const fromPages = {};
921
925
  for (const { routePath, config: page } of entries) {
926
+ if (page === false) {
927
+ fromPages[routePath] = false;
928
+ continue;
929
+ }
922
930
  if (Array.isArray(page.locales)) routeLocales[routePath] = page.locales;
923
931
  else if (page.locales) routeLocales[routePath] = Object.keys(page.locales);
924
932
  if (page.localeRoutes) fromPages[routePath] = page.localeRoutes;
@@ -936,7 +944,7 @@ function collectDefineI18nRouteMetaFromFiles(pageFiles, rootDirs) {
936
944
  if (!rootDir) continue;
937
945
  try {
938
946
  const config = extractDefineI18nRouteData(readFileSync(pageFile, "utf-8"), pageFile);
939
- if (!config) continue;
947
+ if (config === null) continue;
940
948
  entries.push({
941
949
  routePath: pageFilePathToRoutePath(pageFile, rootDir),
942
950
  config
@@ -967,12 +975,13 @@ declare module '#app' {
967
975
 
968
976
  export {}`;
969
977
  }
978
+ const REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
970
979
  const DEFAULT_CANONICAL_QUERY_WHITELIST = ["page", "sort", "filter", "search", "q", "query", "tag"];
971
980
  function isLiveDictionary(options, mode, serverHost) {
972
981
  return mode === "source" || Boolean(options.cacheTtl) || Boolean(serverHost);
973
982
  }
974
983
  function buildFullConfig(params) {
975
- const { options, nuxt, defaultLocale, apiConfig, translationPayloadMode, locales, translationsHash, publicPayloads, rootDelta } = params;
984
+ const { options, nuxt, defaultLocale, apiConfig, translationPayloadMode, locales, publicPayloads, rootDelta } = params;
976
985
  const noScripts = nuxt.options.features.noScripts;
977
986
  return {
978
987
  locales: locales ?? [],
@@ -985,12 +994,16 @@ function buildFullConfig(params) {
985
994
  defaultLocale,
986
995
  fallbackLocale: options.fallbackLocale ?? void 0,
987
996
  localeCookie: options.localeCookie ?? null,
997
+ // set only when it is not the default: 06.redirect reads it
998
+ redirectStatusCode: options.redirectStatusCode ?? void 0,
999
+ // only a build that writes a cookie ships its attributes
1000
+ localeCookieOptions: options.localeCookie || nuxt.options?.router?.options?.hashMode ? options.localeCookieOptions : void 0,
988
1001
  autoDetectLanguage: options.autoDetectLanguage,
989
1002
  autoDetectPath: options.autoDetectPath || "/",
990
1003
  strategy: options.strategy,
991
- // the `?v=` of payload URLs: a fingerprint of the translations, as a timestamp would make clients re-download
992
- // what never changed. `options.dateBuild` wins even when it is `0`/`''`: that opts out of busting.
993
- dateBuild: options.dateBuild ?? translationsHash ?? Date.now(),
1004
+ // the `?v=` of payload URLs: a fingerprint of the translations (set at modules:done), as a timestamp would make
1005
+ // clients re-download what never changed. `options.dateBuild` wins even when it is `0`/`''`: that opts out of busting.
1006
+ dateBuild: options.dateBuild ?? Date.now(),
994
1007
  hashMode: nuxt.options?.router?.options?.hashMode ?? false,
995
1008
  trailingSlash: options.trailingSlash ?? nuxt.options.experimental?.defaults?.nuxtLink?.trailingSlash,
996
1009
  apiBaseUrl: apiConfig.apiBaseUrl,
@@ -1027,6 +1040,8 @@ function buildPrivateConfig(options, nuxt, rootDirs, apiConfig, locales, payload
1027
1040
  additionalTranslationDirs: options.additionalTranslationDirs ?? [],
1028
1041
  apiBaseServerHost: apiConfig.apiBaseServerHost,
1029
1042
  serverTranslationPreload: options.serverTranslationPreload ?? false,
1043
+ // read where a server merges source files (mode 'source', dev): set only when on
1044
+ linkedMessages: options.linkedMessages || void 0,
1030
1045
  cacheStorage: options.cacheStorage,
1031
1046
  cacheStorageBuild: nuxt.options.buildId,
1032
1047
  payloadFsDir,
@@ -1159,6 +1174,13 @@ const module$1 = defineNuxtModule({
1159
1174
  options.localeCookie = "user-locale";
1160
1175
  logger.info("Strategy 'no_prefix': localeCookie automatically set to 'user-locale' for locale persistence.");
1161
1176
  }
1177
+ if (options.redirectStatusCode !== void 0 && !REDIRECT_STATUSES.has(options.redirectStatusCode)) {
1178
+ logger.warn(`redirectStatusCode ${options.redirectStatusCode} is none of 301, 302, 303, 307, 308; 302 is used.`);
1179
+ options.redirectStatusCode = void 0;
1180
+ }
1181
+ if (options.localeCookieOptions?.sameSite === "none" && !options.localeCookieOptions.secure) {
1182
+ logger.warn("localeCookieOptions: sameSite 'none' needs secure: true, or browsers reject the locale cookie.");
1183
+ }
1162
1184
  if (options.strategy !== "no_prefix" && options.redirects !== false && !options.localeCookie) {
1163
1185
  logger.info(
1164
1186
  "Redirects are enabled but localeCookie is not set. Locale-based redirects will not remember user's locale preference across page reloads. Set `localeCookie: 'user-locale'` to enable cookie-based locale persistence for redirects."
@@ -1169,10 +1191,11 @@ const module$1 = defineNuxtModule({
1169
1191
  const mergedLocalesDir = resolve(nuxt.options.buildDir, "i18n-merged");
1170
1192
  const sourceLocalesDir = resolve(nuxt.options.buildDir, "i18n-source");
1171
1193
  const translationDirName = options.translationDir || "locales";
1172
- const translationsHash = options.dateBuild === void 0 ? hashTranslationSources(
1194
+ const extraDirs = options.additionalTranslationDirs ??= [];
1195
+ const fingerprint = () => options.dateBuild === void 0 ? hashTranslationSources(
1173
1196
  rootDirs,
1174
1197
  translationDirName,
1175
- options.additionalTranslationDirs,
1198
+ extraDirs,
1176
1199
  JSON.stringify([options.fallbackLocale, options.disablePageLocales, (options.locales ?? []).map((l) => [l.code, l.fallbackLocale])])
1177
1200
  ) : null;
1178
1201
  const translationPayloads = resolveTranslationPayloadOptions(options);
@@ -1200,10 +1223,11 @@ const module$1 = defineNuxtModule({
1200
1223
  localeInfosByCode.set(locale.code, {
1201
1224
  code: locale.code,
1202
1225
  fallbackLocale: prev?.fallbackLocale ?? locale.fallbackLocale,
1226
+ iso: prev?.iso ?? locale.iso,
1203
1227
  disabled: prev?.disabled ?? locale.disabled
1204
1228
  });
1205
1229
  }
1206
- const localeInfos = [...localeInfosByCode.values()].filter((locale) => !locale.disabled).map(({ code, fallbackLocale }) => ({ code, fallbackLocale }));
1230
+ const localeInfos = [...localeInfosByCode.values()].filter((locale) => !locale.disabled).map(({ code, fallbackLocale, iso }) => ({ code, fallbackLocale, iso }));
1207
1231
  let routeNames = [];
1208
1232
  nuxt.hook("build:before", async () => {
1209
1233
  if (translationPayloads.mode === "source") {
@@ -1220,7 +1244,9 @@ const module$1 = defineNuxtModule({
1220
1244
  additionalTranslationDirs: options.additionalTranslationDirs,
1221
1245
  routeNames,
1222
1246
  // dev reads live files, and a prerender merges what it reads like any server
1223
- rootDelta: !nuxt.options.dev && translationPayloads.rootDelta
1247
+ rootDelta: !nuxt.options.dev && translationPayloads.rootDelta,
1248
+ linkedMessages: options.linkedMessages,
1249
+ warn: (message) => logger.warn(message)
1224
1250
  });
1225
1251
  logger.info(`Pre-merged translations from ${rootDirs.length} layer(s) into ${mergedLocalesDir}`);
1226
1252
  }
@@ -1271,8 +1297,12 @@ const module$1 = defineNuxtModule({
1271
1297
  translationPayloadMode: translationPayloads.mode,
1272
1298
  publicPayloads: translationPayloads.mode === "premerged" && translationPayloads.publicAssets,
1273
1299
  rootDelta: !nuxt.options.dev && translationPayloads.mode === "premerged" && translationPayloads.rootDelta && !options.disablePageLocales && (translationPayloads.publicAssets || Boolean(options.apiBaseClientHost)),
1274
- locales: routeGenerator.locales ?? [],
1275
- translationsHash
1300
+ locales: routeGenerator.locales ?? []
1301
+ });
1302
+ nuxt.hook("modules:done", () => {
1303
+ for (const dir of nuxt.options.i18n?.additionalTranslationDirs ?? [])
1304
+ if (!extraDirs.includes(dir)) extraDirs.push(dir);
1305
+ fullConfig.dateBuild = fingerprint() ?? fullConfig.dateBuild;
1276
1306
  });
1277
1307
  const generateDomain = process.env.NUXT_I18N_DOMAIN;
1278
1308
  const buildConfig = generateDomain ? configForHost(fullConfig, generateDomain) : fullConfig;
@@ -1366,16 +1396,13 @@ export default defineNuxtPlugin(() => ({ provide: { defineI18nRoute: async () =>
1366
1396
  });
1367
1397
  }
1368
1398
  if (nuxt.options.dev && (options.hmr ?? true)) {
1369
- const translationDirs = resolveLiveTranslationDirs({
1370
- rootDirs,
1371
- translationDir: translationDirName,
1372
- additionalTranslationDirs: options.additionalTranslationDirs
1373
- });
1399
+ let translationDirs;
1400
+ const liveDirs = () => translationDirs ??= resolveLiveTranslationDirs({ rootDirs, translationDir: translationDirName, additionalTranslationDirs: extraDirs });
1374
1401
  addVitePlugin(
1375
1402
  {
1376
1403
  name: "i18n-micro:translations-hmr",
1377
1404
  hotUpdate({ file }) {
1378
- if (this.environment.name !== "client" || !translationRelativePath(file, translationDirs)) return;
1405
+ if (this.environment.name !== "client" || !translationRelativePath(file, liveDirs())) return;
1379
1406
  this.environment.hot.send({ type: "custom", event: TRANSLATIONS_HMR_EVENT });
1380
1407
  return [];
1381
1408
  }
@@ -13,7 +13,7 @@ export function useI18nLocale() {
13
13
  const localeState = useState("i18n-locale", () => null);
14
14
  const localeCookieName = getLocaleCookieName(i18nConfig);
15
15
  const hashCookieName = getHashCookieName(i18nConfig);
16
- const cookieRef = (name) => cookies && name ? useCookie(name, getLocaleCookieOptions()) : { value: null };
16
+ const cookieRef = (name) => cookies && name ? useCookie(name, getLocaleCookieOptions(i18nConfig.localeCookieOptions)) : { value: null };
17
17
  const localeCookie = cookieRef(localeCookieName);
18
18
  const hashCookie = cookieRef(hashCookieName);
19
19
  const syncLocale = (locale) => {
@@ -17,8 +17,8 @@ declare const _default: import("nuxt/app").Plugin<{
17
17
  defaultLocale: () => string;
18
18
  getLocales: () => Locale[];
19
19
  getRouteName: (route?: RouteLocationNormalizedLoaded | RouteLocationResolvedGeneric, _locale?: string) => string;
20
- t: (key: TranslationKey, params?: Params, defaultValue?: string | null, routeContext?: unknown) => CleanTranslation;
21
- ts: (key: TranslationKey, params?: Params, defaultValue?: string, routeContext?: unknown) => string;
20
+ t: (key: TranslationKey, params?: import("@i18n-micro/types").TranslateParams, defaultValue?: string | null, routeContext?: unknown) => CleanTranslation;
21
+ ts: (key: TranslationKey, params?: import("@i18n-micro/types").TranslateParams, defaultValue?: string, routeContext?: unknown) => string;
22
22
  _t: (route: RouteLocationNormalizedLoaded) => TranslationFn<CleanTranslation>;
23
23
  _ts: (route: RouteLocationNormalizedLoaded) => TranslationFn<string>;
24
24
  tc: (key: TranslationKey, count: number | Params, defaultValue?: string) => string;
@@ -49,7 +49,7 @@ declare const _default: import("nuxt/app").Plugin<{
49
49
  loadPageTranslations: (locale: string, routeName: string, translations: Translations) => Promise<void>;
50
50
  setMissingHandler: (handler: MissingHandler | null) => void;
51
51
  };
52
- defineI18nRoute: ReturnType<({ getLocale, mergeTranslations }: Pick<NuxtI18nPluginProvide, "getLocale" | "mergeTranslations">) => ({ locales }: DefineI18nRouteConfig) => Promise<void>>;
52
+ defineI18nRoute: ReturnType<({ getLocale, mergeTranslations }: Pick<NuxtI18nPluginProvide, "getLocale" | "mergeTranslations">) => (config: DefineI18nRouteConfig | false) => Promise<void>>;
53
53
  helper: {
54
54
  mergeTranslation(locale: string, routeName: string, newTranslations: Translations): Promise<void>;
55
55
  };
@@ -61,8 +61,8 @@ declare const _default: import("nuxt/app").Plugin<{
61
61
  defaultLocale: () => string;
62
62
  getLocales: () => Locale[];
63
63
  getRouteName: (route?: RouteLocationNormalizedLoaded | RouteLocationResolvedGeneric, _locale?: string) => string;
64
- t: (key: TranslationKey, params?: Params, defaultValue?: string | null, routeContext?: unknown) => CleanTranslation;
65
- ts: (key: TranslationKey, params?: Params, defaultValue?: string, routeContext?: unknown) => string;
64
+ t: (key: TranslationKey, params?: import("@i18n-micro/types").TranslateParams, defaultValue?: string | null, routeContext?: unknown) => CleanTranslation;
65
+ ts: (key: TranslationKey, params?: import("@i18n-micro/types").TranslateParams, defaultValue?: string, routeContext?: unknown) => string;
66
66
  _t: (route: RouteLocationNormalizedLoaded) => TranslationFn<CleanTranslation>;
67
67
  _ts: (route: RouteLocationNormalizedLoaded) => TranslationFn<string>;
68
68
  tc: (key: TranslationKey, count: number | Params, defaultValue?: string) => string;
@@ -105,8 +105,8 @@ declare const _default: import("nuxt/app").Plugin<{
105
105
  defaultLocale: () => string;
106
106
  getLocales: () => Locale[];
107
107
  getRouteName: (route?: RouteLocationNormalizedLoaded | RouteLocationResolvedGeneric, _locale?: string) => string;
108
- t: (key: TranslationKey, params?: Params, defaultValue?: string | null, routeContext?: unknown) => CleanTranslation;
109
- ts: (key: TranslationKey, params?: Params, defaultValue?: string, routeContext?: unknown) => string;
108
+ t: (key: TranslationKey, params?: import("@i18n-micro/types").TranslateParams, defaultValue?: string | null, routeContext?: unknown) => CleanTranslation;
109
+ ts: (key: TranslationKey, params?: import("@i18n-micro/types").TranslateParams, defaultValue?: string, routeContext?: unknown) => string;
110
110
  _t: (route: RouteLocationNormalizedLoaded) => TranslationFn<CleanTranslation>;
111
111
  _ts: (route: RouteLocationNormalizedLoaded) => TranslationFn<string>;
112
112
  tc: (key: TranslationKey, count: number | Params, defaultValue?: string) => string;
@@ -137,7 +137,7 @@ declare const _default: import("nuxt/app").Plugin<{
137
137
  loadPageTranslations: (locale: string, routeName: string, translations: Translations) => Promise<void>;
138
138
  setMissingHandler: (handler: MissingHandler | null) => void;
139
139
  };
140
- defineI18nRoute: ReturnType<({ getLocale, mergeTranslations }: Pick<NuxtI18nPluginProvide, "getLocale" | "mergeTranslations">) => ({ locales }: DefineI18nRouteConfig) => Promise<void>>;
140
+ defineI18nRoute: ReturnType<({ getLocale, mergeTranslations }: Pick<NuxtI18nPluginProvide, "getLocale" | "mergeTranslations">) => (config: DefineI18nRouteConfig | false) => Promise<void>>;
141
141
  helper: {
142
142
  mergeTranslation(locale: string, routeName: string, newTranslations: Translations): Promise<void>;
143
143
  };
@@ -149,8 +149,8 @@ declare const _default: import("nuxt/app").Plugin<{
149
149
  defaultLocale: () => string;
150
150
  getLocales: () => Locale[];
151
151
  getRouteName: (route?: RouteLocationNormalizedLoaded | RouteLocationResolvedGeneric, _locale?: string) => string;
152
- t: (key: TranslationKey, params?: Params, defaultValue?: string | null, routeContext?: unknown) => CleanTranslation;
153
- ts: (key: TranslationKey, params?: Params, defaultValue?: string, routeContext?: unknown) => string;
152
+ t: (key: TranslationKey, params?: import("@i18n-micro/types").TranslateParams, defaultValue?: string | null, routeContext?: unknown) => CleanTranslation;
153
+ ts: (key: TranslationKey, params?: import("@i18n-micro/types").TranslateParams, defaultValue?: string, routeContext?: unknown) => string;
154
154
  _t: (route: RouteLocationNormalizedLoaded) => TranslationFn<CleanTranslation>;
155
155
  _ts: (route: RouteLocationNormalizedLoaded) => TranslationFn<string>;
156
156
  tc: (key: TranslationKey, count: number | Params, defaultValue?: string) => string;
@@ -323,5 +323,5 @@ export interface NuxtAppOnlyInjections {
323
323
  * Register per-page locale configuration and translations from inside a component.
324
324
  * Merges into the active locale and re-applies when the locale changes.
325
325
  */
326
- $defineI18nRoute: (routeDefinition: DefineI18nRouteConfig) => Promise<void>;
326
+ $defineI18nRoute: (routeDefinition: DefineI18nRouteConfig | false) => Promise<void>;
327
327
  }
@@ -12,7 +12,8 @@ import { useI18nLocale } from "../composables/useI18nLocale.js";
12
12
  import { createI18nProvide, createNuxtI18nPluginApi } from "../utils/i18n-api.js";
13
13
  import { useResolvedI18nConfig } from "../utils/i18n-config.js";
14
14
  import { setupI18nNavigation } from "../utils/i18n-navigation.js";
15
- import { formatterFor, NuxtI18n, NuxtTranslationLoader } from "../utils/nuxt-i18n.js";
15
+ import { formatterFor } from "../utils/formatter.js";
16
+ import { NuxtI18n, NuxtTranslationLoader } from "../utils/nuxt-i18n.js";
16
17
  import { ServerNuxtI18n } from "../utils/nuxt-i18n-server.js";
17
18
  import { setI18nDevtoolsBridge } from "../devtools/bridge.js";
18
19
  import { DEFAULT_SERVER_CACHE_SIZE } from "../utils/cache-keys.js";
@@ -174,7 +175,8 @@ export default defineNuxtPlugin({
174
175
  customMissingHandler.value = handler;
175
176
  },
176
177
  // a ref: a link rendered without the hash gets it back when hydration ends
177
- isHydrating: () => !hydrated.value && nuxtApp.isHydrating === true
178
+ isHydrating: () => !hydrated.value && nuxtApp.isHydrating === true,
179
+ nuxtApp
178
180
  });
179
181
  if (import.meta.server) rewriteSwitcherLinks(nuxtApp, api.provide, i18nRouteParams, (path) => router.resolve(path).href);
180
182
  if (import.meta.dev && import.meta.client) {
@@ -41,7 +41,7 @@ export default defineNuxtPlugin({
41
41
  const urlLocale = i18nStrategy.getLocaleFromPath(path);
42
42
  const writeCookie = (locale) => {
43
43
  if (!cookieName) return;
44
- const { watch: _watch, decode: _decode, ...options } = getLocaleCookieOptions();
44
+ const { watch: _watch, decode: _decode, ...options } = getLocaleCookieOptions(i18nConfig.localeCookieOptions);
45
45
  setCookie(event, cookieName, locale, options);
46
46
  };
47
47
  const redirectable = redirects && !urlless && !i18nConfig.hashMode && (urlLocale || i18nConfig.strategy !== "prefix_and_default");
@@ -49,8 +49,9 @@ export default defineNuxtPlugin({
49
49
  const router = useRouter();
50
50
  const served = i18nConfig.strategy !== "no_prefix" && (router.getRoutes().length === 0 || router.currentRoute.value.matched.length > 0);
51
51
  if (urlLocale && served && !(redirects && anyPath)) writeCookie(urlLocale);
52
+ const redirectCode = i18nConfig.redirectStatusCode ?? 302;
52
53
  if (i18nConfig.hashMode && alwaysPrefixed)
53
- return navigateTo(`/#${applyTrailingSlash(path, i18nConfig.trailingSlash)}${url.search}`, { redirectCode: 302 });
54
+ return navigateTo(`/#${applyTrailingSlash(path, i18nConfig.trailingSlash)}${url.search}`, { redirectCode });
54
55
  if (!redirects || i18nConfig.hashMode) return;
55
56
  const preferred = redirectLocale({
56
57
  path,
@@ -69,7 +70,7 @@ export default defineNuxtPlugin({
69
70
  if (!target) return;
70
71
  if (anyPath && urlLocale) writeCookie(preferred);
71
72
  if (DEBUG) console.error("[i18n-redirect] REDIRECT", { path, target, preferred });
72
- return navigateTo(applyTrailingSlash(target, i18nConfig.trailingSlash) + url.search + url.hash, { redirectCode: 302 });
73
+ return navigateTo(applyTrailingSlash(target, i18nConfig.trailingSlash) + url.search + url.hash, { redirectCode });
73
74
  }
74
75
  });
75
76
  function chosenState(state, urlLocale) {
@@ -1,6 +1,7 @@
1
1
  import { createTranslationDirReader, mergeDevTranslations, resolveLiveTranslationDirs } from "./translation-watcher.js";
2
2
  import { toTranslations } from "@i18n-micro/utils/normalize";
3
3
  import { getI18nPrivateConfig } from "#i18n-internal/config";
4
+ import { warnOnce } from "../utils/warn-once.js";
4
5
  export async function loadLiveDevTranslations(locale, pageName, config) {
5
6
  const privateConfig = getI18nPrivateConfig();
6
7
  const { translationRoots, additionalRoots } = resolveLiveTranslationDirs({
@@ -16,7 +17,9 @@ export async function loadLiveDevTranslations(locale, pageName, config) {
16
17
  locales: config.linkLocales ?? config.locales,
17
18
  fallbackLocale: config.fallbackLocale,
18
19
  disablePageLocales: config.disablePageLocales,
19
- readLocaleFile
20
+ readLocaleFile,
21
+ linkedMessages: privateConfig.linkedMessages,
22
+ warn: warnOnce
20
23
  });
21
24
  return toTranslations(merged);
22
25
  }
@@ -11,6 +11,8 @@ export interface DevTranslationMergeInput {
11
11
  fallbackLocale?: string;
12
12
  disablePageLocales?: boolean;
13
13
  readLocaleFile: (relativePath: string) => Record<string, unknown> | Promise<Record<string, unknown>>;
14
+ linkedMessages?: boolean;
15
+ warn?: (message: string) => void;
14
16
  }
15
17
  export interface HandleTranslationWatchChangeInput {
16
18
  relativePath: string;
@@ -3,7 +3,7 @@ import { existsSync, readFileSync } from "node:fs";
3
3
  import { isAbsolute, join, relative, resolve } from "node:path";
4
4
  import { forwardSlashes } from "@i18n-micro/utils/app-path";
5
5
  import { deepMergeTranslationsRecursive } from "@i18n-micro/utils/deep-merge";
6
- import { parseJsonFile } from "@i18n-micro/utils/load-dir";
6
+ import { additionalTranslationRoots, parseJsonFile } from "@i18n-micro/utils/load-dir";
7
7
  import { mergeSourceTranslations, normalizeConfiguredLocales } from "@i18n-micro/utils/merge-source";
8
8
  import { parseTranslationRelativePath } from "@i18n-micro/utils/parse-path";
9
9
  export function readTranslationFile(filePath) {
@@ -25,13 +25,7 @@ function skip(filePath, reason) {
25
25
  const WARNED = /* @__PURE__ */ new Set();
26
26
  export function resolveLiveTranslationDirs(input) {
27
27
  const translationRoots = input.rootDirs.map((root) => resolve(root, input.translationDir));
28
- const additionalRoots = [];
29
- for (const dir of input.additionalTranslationDirs ?? []) {
30
- for (const root of input.rootDirs) {
31
- additionalRoots.push(resolve(root, dir));
32
- }
33
- }
34
- return { translationRoots, additionalRoots };
28
+ return { translationRoots, additionalRoots: additionalTranslationRoots(input.rootDirs, input.additionalTranslationDirs) };
35
29
  }
36
30
  export function translationRelativePath(filePath, dirs) {
37
31
  const inside = (root) => {
@@ -73,7 +67,9 @@ export async function mergeDevTranslations(input) {
73
67
  locales: effectiveLocales,
74
68
  globalFallbackLocale: input.fallbackLocale,
75
69
  disablePageLocales: input.disablePageLocales,
76
- readLocaleFile: input.readLocaleFile
70
+ readLocaleFile: input.readLocaleFile,
71
+ linkedMessages: input.linkedMessages,
72
+ warn: input.warn
77
73
  });
78
74
  }
79
75
  export async function handleTranslationWatchChange(input) {
@@ -11,6 +11,7 @@ import { getI18nServerConfig } from "./locale-detector.js";
11
11
  import { getOrCreateServerTranslationCache } from "../internal/server-translation-cache.js";
12
12
  import { readSharedChunk, sharedChunkKey, writeSharedChunk } from "../internal/shared-cache.js";
13
13
  import { takeRootMark, toEntry } from "../../utils/translation-entry.js";
14
+ import { warnOnce } from "./warn-once.js";
14
15
  const EMPTY = { data: {}, json: "{}" };
15
16
  export const NO_FILE = { data: {}, json: "{}" };
16
17
  const pending = /* @__PURE__ */ new Map();
@@ -95,7 +96,9 @@ async function load(locale, page, config) {
95
96
  // a fallback may live on another domain: its file is still this build's
96
97
  locales: normalizeConfiguredLocales(config.linkLocales ?? config.locales),
97
98
  globalFallbackLocale: config.fallbackLocale,
98
- disablePageLocales: config.disablePageLocales
99
+ disablePageLocales: config.disablePageLocales,
100
+ linkedMessages: getI18nPrivateConfig().linkedMessages,
101
+ warn: warnOnce
99
102
  }
100
103
  );
101
104
  return { entry: toEntry(data) };
@@ -1,3 +1,8 @@
1
- import type { Params } from '@i18n-micro/types';
1
+ import { BaseI18n } from '@i18n-micro/core';
2
+ import type { TranslateParams } from '@i18n-micro/types';
2
3
  import type { H3Event } from 'h3';
3
- export declare const useTranslationServerMiddleware: (event: H3Event, defaultLocale?: string, currentLocale?: string) => Promise<(key: string, params?: Params, defaultValue?: string) => string>;
4
+ type Helpers = Pick<BaseI18n, 'tc' | 'tn' | 'td' | 'tdr' | 'has'>;
5
+ export declare const useTranslationServerMiddleware: (event: H3Event, defaultLocale?: string, currentLocale?: string) => Promise<((key: string, params?: TranslateParams, defaultValue?: string) => string) & Helpers>;
6
+ /** `t` of `useTranslationServerMiddleware`, with the page's `tc`, `tn`, `td`, `tdr` and `has`. */
7
+ export type ServerTranslate = Awaited<ReturnType<typeof useTranslationServerMiddleware>>;
8
+ export {};
@@ -1,4 +1,6 @@
1
- import { interpolate, resolveTranslation } from "@i18n-micro/core";
1
+ import { BaseI18n, interpolate, localeIsoMap, resolveTranslation, translationCacheKey } from "@i18n-micro/core";
2
+ import { plural } from "#i18n-internal/plural";
3
+ import { formatterFor } from "../../utils/formatter.js";
2
4
  import { getI18nServerConfig, getRequestLocale } from "./locale-detector.js";
3
5
  import { loadTranslationsFromServer } from "./server-loader.js";
4
6
  const I18N_CONTEXT_KEY = "__i18n_translations__";
@@ -16,8 +18,30 @@ async function dictionaryOf(event, locale) {
16
18
  }
17
19
  return loaded[locale] = chunk.data;
18
20
  }
21
+ class DictionaryI18n extends BaseI18n {
22
+ constructor(locale, translations, config) {
23
+ super({
24
+ storage: { translations: /* @__PURE__ */ new Map([[translationCacheKey(locale, "index"), translations]]) },
25
+ plural,
26
+ formatter: formatterFor(config),
27
+ missingWarn: false,
28
+ localeIso: localeIsoMap(config.linkLocales ?? config.locales)
29
+ });
30
+ this.locale = locale;
31
+ }
32
+ getLocale() {
33
+ return this.locale;
34
+ }
35
+ getFallbackLocale() {
36
+ return this.locale;
37
+ }
38
+ getRoute() {
39
+ return "index";
40
+ }
41
+ }
19
42
  export const useTranslationServerMiddleware = async (event, defaultLocale, currentLocale) => {
20
- const locale = currentLocale || getRequestLocale(event, getI18nServerConfig(event), defaultLocale);
43
+ const config = getI18nServerConfig(event);
44
+ const locale = currentLocale || getRequestLocale(event, config, defaultLocale);
21
45
  const translations = await dictionaryOf(event, locale);
22
46
  function t(key, params, defaultValue) {
23
47
  const value = resolveTranslation(translations, key);
@@ -25,5 +49,14 @@ export const useTranslationServerMiddleware = async (event, defaultLocale, curre
25
49
  const str = typeof value === "string" ? value : String(value);
26
50
  return params ? interpolate(str, params) : str;
27
51
  }
28
- return t;
52
+ let i18n;
53
+ const own = () => i18n ??= new DictionaryI18n(locale, translations, config);
54
+ const helpers = {
55
+ tc: (...args) => own().tc(...args),
56
+ tn: ((...args) => own().tn(...args)),
57
+ td: ((...args) => own().td(...args)),
58
+ tdr: (...args) => own().tdr(...args),
59
+ has: (...args) => own().has(...args)
60
+ };
61
+ return Object.assign(t, helpers);
29
62
  };
@@ -0,0 +1 @@
1
+ export declare function warnOnce(message: string): void;
@@ -0,0 +1,6 @@
1
+ const warned = /* @__PURE__ */ new Set();
2
+ export function warnOnce(message) {
3
+ if (warned.has(message)) return;
4
+ warned.add(message);
5
+ console.warn(message);
6
+ }
@@ -0,0 +1,9 @@
1
+ import { FormatService } from '@i18n-micro/core';
2
+ import type { ResolvedI18nConfig } from '@i18n-micro/types';
3
+ /**
4
+ * The server's formatters. `Intl` objects are costly and depend only on what the service is built
5
+ * from, so requests share them: its input is this read-only config, and no request reaches it
6
+ * (`$tn` and `$td` pass the locale per call). One per resolved config: runtime overrides and hosts.
7
+ * No vue here: the page plugin and the Nitro helpers share it.
8
+ */
9
+ export declare function formatterFor(config: ResolvedI18nConfig): FormatService;
@@ -0,0 +1,7 @@
1
+ import { FormatService } from "@i18n-micro/core";
2
+ const formatters = /* @__PURE__ */ new WeakMap();
3
+ export function formatterFor(config) {
4
+ let formatter = formatters.get(config);
5
+ if (!formatter) formatters.set(config, formatter = new FormatService(config));
6
+ return formatter;
7
+ }
@@ -0,0 +1,8 @@
1
+ import type { I18nBeforeLocaleSwitchContext } from '@i18n-micro/types';
2
+ import type { NuxtApp } from 'nuxt/app';
3
+ /** What of the NuxtApp a hook needs. */
4
+ export type HookHost = Pick<NuxtApp, 'hooks' | 'callHook'>;
5
+ /** Has `name` a subscriber? `_hooks` is hookable's own, so without it the hook just runs. */
6
+ export declare function hasSubscriber(nuxtApp: HookHost, name: string): boolean;
7
+ /** `i18n:beforeLocaleSwitch`: what the subscribers decided, or null when nobody listens (no context is made). */
8
+ export declare function beforeLocaleSwitch(nuxtApp: HookHost, from: string, to: string): Promise<I18nBeforeLocaleSwitchContext | null>;
@@ -0,0 +1,10 @@
1
+ export function hasSubscriber(nuxtApp, name) {
2
+ const { _hooks: subscribers } = nuxtApp.hooks;
3
+ return !subscribers || Boolean(subscribers[name]?.length);
4
+ }
5
+ export async function beforeLocaleSwitch(nuxtApp, from, to) {
6
+ if (!hasSubscriber(nuxtApp, "i18n:beforeLocaleSwitch")) return null;
7
+ const ctx = { from, to, cancel: false };
8
+ await nuxtApp.callHook("i18n:beforeLocaleSwitch", ctx);
9
+ return ctx;
10
+ }
@@ -3,6 +3,7 @@ import type { DefineI18nRouteConfig, I18nRouteParams, MissingHandler, ResolvedI1
3
3
  import type { RouteLocationNamedRaw, RouteLocationNormalizedLoaded, RouteLocationResolved, RouteLocationResolvedGeneric, Router } from 'vue-router';
4
4
  import type { useI18nLocale } from '../composables/useI18nLocale.js';
5
5
  import type { NuxtI18n, NuxtTranslationLoader } from './nuxt-i18n.js';
6
+ import { type HookHost } from './hooks.js';
6
7
  /** URL of a switch result: the strategy hands out a URL string or a resolved route (fullPath set). */
7
8
  export declare function switchedPath(switched: RouteLike | string): string;
8
9
  export interface NuxtI18nPluginApiDeps {
@@ -22,6 +23,8 @@ export interface NuxtI18nPluginApiDeps {
22
23
  }) => unknown;
23
24
  setMissingHandler: (handler: MissingHandler | null) => void;
24
25
  isHydrating: () => boolean;
26
+ /** For `i18n:beforeLocaleSwitch`. */
27
+ nuxtApp: HookHost;
25
28
  }
26
29
  export declare function createNuxtI18nPluginApi(deps: NuxtI18nPluginApiDeps): {
27
30
  helper: {
@@ -34,8 +37,8 @@ export declare function createNuxtI18nPluginApi(deps: NuxtI18nPluginApiDeps): {
34
37
  defaultLocale: () => string;
35
38
  getLocales: () => import("@i18n-micro/types").Locale[];
36
39
  getRouteName: (route?: RouteLocationNormalizedLoaded | RouteLocationResolvedGeneric, _locale?: string) => string;
37
- t: (key: import("@i18n-micro/types").TranslationKey, params?: import("@i18n-micro/types").Params, defaultValue?: string | null, routeContext?: unknown) => import("@i18n-micro/types").CleanTranslation;
38
- ts: (key: import("@i18n-micro/types").TranslationKey, params?: import("@i18n-micro/types").Params, defaultValue?: string, routeContext?: unknown) => string;
40
+ t: (key: import("@i18n-micro/types").TranslationKey, params?: import("@i18n-micro/types").TranslateParams, defaultValue?: string | null, routeContext?: unknown) => import("@i18n-micro/types").CleanTranslation;
41
+ ts: (key: import("@i18n-micro/types").TranslationKey, params?: import("@i18n-micro/types").TranslateParams, defaultValue?: string, routeContext?: unknown) => string;
39
42
  _t: (route: RouteLocationNormalizedLoaded) => import("./nuxt-i18n.js").TranslationFn<import("@i18n-micro/types").CleanTranslation>;
40
43
  _ts: (route: RouteLocationNormalizedLoaded) => import("./nuxt-i18n.js").TranslationFn<string>;
41
44
  tc: (key: import("@i18n-micro/types").TranslationKey, count: number | import("@i18n-micro/types").Params, defaultValue?: string) => string;
@@ -71,7 +74,7 @@ export declare function createNuxtI18nPluginApi(deps: NuxtI18nPluginApiDeps): {
71
74
  export type NuxtI18nPluginApi = ReturnType<typeof createNuxtI18nPluginApi>;
72
75
  export type NuxtI18nPluginProvide = NuxtI18nPluginApi['provide'];
73
76
  /** The page's own translations: on the active locale, and again on each switch while the page lives. */
74
- declare function createDefineI18nRoute({ getLocale, mergeTranslations }: Pick<NuxtI18nPluginProvide, 'getLocale' | 'mergeTranslations'>): ({ locales }: DefineI18nRouteConfig) => Promise<void>;
77
+ declare function createDefineI18nRoute({ getLocale, mergeTranslations }: Pick<NuxtI18nPluginProvide, 'getLocale' | 'mergeTranslations'>): (config: DefineI18nRouteConfig | false) => Promise<void>;
75
78
  export interface I18nProvideDeps {
76
79
  api: NuxtI18nPluginApi;
77
80
  i18nConfig: ResolvedI18nConfig;
@@ -98,8 +101,8 @@ export declare function createI18nProvide({ api: { helper, provide }, i18nConfig
98
101
  defaultLocale: () => string;
99
102
  getLocales: () => import("@i18n-micro/types").Locale[];
100
103
  getRouteName: (route?: RouteLocationNormalizedLoaded | RouteLocationResolvedGeneric, _locale?: string) => string;
101
- t: (key: import("@i18n-micro/types").TranslationKey, params?: import("@i18n-micro/types").Params, defaultValue?: string | null, routeContext?: unknown) => import("@i18n-micro/types").CleanTranslation;
102
- ts: (key: import("@i18n-micro/types").TranslationKey, params?: import("@i18n-micro/types").Params, defaultValue?: string, routeContext?: unknown) => string;
104
+ t: (key: import("@i18n-micro/types").TranslationKey, params?: import("@i18n-micro/types").TranslateParams, defaultValue?: string | null, routeContext?: unknown) => import("@i18n-micro/types").CleanTranslation;
105
+ ts: (key: import("@i18n-micro/types").TranslationKey, params?: import("@i18n-micro/types").TranslateParams, defaultValue?: string, routeContext?: unknown) => string;
103
106
  _t: (route: RouteLocationNormalizedLoaded) => import("./nuxt-i18n.js").TranslationFn<import("@i18n-micro/types").CleanTranslation>;
104
107
  _ts: (route: RouteLocationNormalizedLoaded) => import("./nuxt-i18n.js").TranslationFn<string>;
105
108
  tc: (key: import("@i18n-micro/types").TranslationKey, count: number | import("@i18n-micro/types").Params, defaultValue?: string) => string;
@@ -142,8 +145,8 @@ export declare function createI18nProvide({ api: { helper, provide }, i18nConfig
142
145
  defaultLocale: () => string;
143
146
  getLocales: () => import("@i18n-micro/types").Locale[];
144
147
  getRouteName: (route?: RouteLocationNormalizedLoaded | RouteLocationResolvedGeneric, _locale?: string) => string;
145
- t: (key: import("@i18n-micro/types").TranslationKey, params?: import("@i18n-micro/types").Params, defaultValue?: string | null, routeContext?: unknown) => import("@i18n-micro/types").CleanTranslation;
146
- ts: (key: import("@i18n-micro/types").TranslationKey, params?: import("@i18n-micro/types").Params, defaultValue?: string, routeContext?: unknown) => string;
148
+ t: (key: import("@i18n-micro/types").TranslationKey, params?: import("@i18n-micro/types").TranslateParams, defaultValue?: string | null, routeContext?: unknown) => import("@i18n-micro/types").CleanTranslation;
149
+ ts: (key: import("@i18n-micro/types").TranslationKey, params?: import("@i18n-micro/types").TranslateParams, defaultValue?: string, routeContext?: unknown) => string;
147
150
  _t: (route: RouteLocationNormalizedLoaded) => import("./nuxt-i18n.js").TranslationFn<import("@i18n-micro/types").CleanTranslation>;
148
151
  _ts: (route: RouteLocationNormalizedLoaded) => import("./nuxt-i18n.js").TranslationFn<string>;
149
152
  tc: (key: import("@i18n-micro/types").TranslationKey, count: number | import("@i18n-micro/types").Params, defaultValue?: string) => string;
@@ -2,7 +2,8 @@ import { isNoPrefixStrategy, urlPath } from "@i18n-micro/core";
2
2
  import { trimTrailingSlashes } from "@i18n-micro/utils/app-path";
3
3
  import { hasProtocol } from "ufo";
4
4
  import { getCurrentInstance, onActivated, watch } from "vue";
5
- import { domains, defineRoute, serverPathFallback } from "#build/i18n.strategy.mjs";
5
+ import { domains, defineRoute, hooks, serverPathFallback } from "#build/i18n.strategy.mjs";
6
+ import { beforeLocaleSwitch } from "./hooks.js";
6
7
  import { usePageRoute } from "./page-head.js";
7
8
  import { applyTrailingSlash } from "./trailing-slash.js";
8
9
  function toRouteLocationResolved(result) {
@@ -34,7 +35,8 @@ export function createNuxtI18nPluginApi(deps) {
34
35
  i18nRouteParams,
35
36
  navigateTo,
36
37
  setMissingHandler,
37
- isHydrating
38
+ isHydrating,
39
+ nuxtApp
38
40
  } = deps;
39
41
  const { getEffectiveLocale, setLocale, isValidLocale } = localeState;
40
42
  const setLocaleState = (code) => {
@@ -116,6 +118,12 @@ export function createNuxtI18nPluginApi(deps) {
116
118
  return false;
117
119
  }
118
120
  const urlless = isNoPrefix || i18nConfig.hashMode;
121
+ const from = i18n.getLocale();
122
+ if (hooks && urlless && toLocale !== i18n.getLocale()) {
123
+ const ctx = await beforeLocaleSwitch(nuxtApp, i18n.getLocale(), toLocale);
124
+ if (ctx?.cancel) return false;
125
+ if (ctx && isValidLocale(ctx.to)) toLocale = ctx.to;
126
+ }
119
127
  const retry = getCurrentLocale() === toLocale && i18n.getLocale() !== toLocale;
120
128
  const route = router.currentRoute.value;
121
129
  const loaded = urlless || retry ? await loader.switchContext(toLocale, i18nStrategy.getPluginRouteName(route, toLocale)) : null;
@@ -129,7 +137,7 @@ export function createNuxtI18nPluginApi(deps) {
129
137
  if (!urlless) setLocaleState(i18n.getLocale());
130
138
  throw error;
131
139
  }
132
- return loaded ?? i18n.getLocale() === toLocale;
140
+ return loaded ?? (i18n.getLocale() === toLocale || i18n.getLocale() !== from);
133
141
  },
134
142
  switchRoute: async (route, toLocale) => {
135
143
  return switchLocaleLogic(toLocale ?? getCurrentLocale(), i18nRouteParams.value, route);
@@ -158,7 +166,8 @@ export function createNuxtI18nPluginApi(deps) {
158
166
  return { helper, provide };
159
167
  }
160
168
  function createDefineI18nRoute({ getLocale, mergeTranslations }) {
161
- return async ({ locales }) => {
169
+ return async (config) => {
170
+ const locales = config ? config.locales : void 0;
162
171
  if (!locales || Array.isArray(locales)) return;
163
172
  const apply = () => {
164
173
  const translations = locales[getLocale()];
@@ -1,5 +1,7 @@
1
1
  import {
2
2
  applyI18nRuntimeOverrides,
3
+ applyLocaleBaseUrls,
4
+ readLocaleBaseUrls,
3
5
  createI18nConfigResolver,
4
6
  readI18nRuntimeOverrides
5
7
  } from "@i18n-micro/utils/runtime-config";
@@ -20,11 +22,22 @@ function overridesOf(config) {
20
22
  };
21
23
  }
22
24
  export function shippedOverrides(config, base) {
23
- const shipped = overridesOf(config);
25
+ if (config === base) return null;
26
+ const shipped = Object.assign(overridesOf(config), originsOf(config, base));
24
27
  const built = overridesOf(base);
25
- const same = shipped.defaultLocale === built.defaultLocale && shipped.fallbackLocale === built.fallbackLocale && shipped.domain === built.domain && shipped.dateBuild === built.dateBuild && String(shipped.disabledLocales) === String(built.disabledLocales);
28
+ const same = shipped.defaultLocale === built.defaultLocale && shipped.fallbackLocale === built.fallbackLocale && shipped.domain === built.domain && shipped.dateBuild === built.dateBuild && String(shipped.disabledLocales) === String(built.disabledLocales) && !shipped.metaBaseUrl && !shipped.localeBaseUrls;
26
29
  return same ? null : shipped;
27
30
  }
31
+ function originsOf(config, base) {
32
+ const origins = {};
33
+ if (!config.domain && config.metaBaseUrl !== base.metaBaseUrl && config.metaBaseUrl) origins.metaBaseUrl = config.metaBaseUrl;
34
+ const built = base.locales ?? [];
35
+ const moved = (config.linkLocales ?? config.locales ?? []).filter(
36
+ (locale) => locale.baseUrl && locale.baseUrl !== built.find((entry) => entry.code === locale.code)?.baseUrl
37
+ );
38
+ if (moved.length) origins.localeBaseUrls = Object.fromEntries(moved.map((locale) => [locale.code, locale.baseUrl]));
39
+ return origins;
40
+ }
28
41
  function resolve(nuxtApp) {
29
42
  const base = getI18nConfig();
30
43
  const shipped = useState("i18n-runtime-overrides", () => null);
@@ -34,10 +47,14 @@ function resolve(nuxtApp) {
34
47
  return config2;
35
48
  }
36
49
  if (nuxtApp.payload.serverRendered) {
37
- const config2 = shipped.value ? applyI18nRuntimeOverrides(base, shipped.value, silent) : base;
50
+ const applied2 = shipped.value ? applyI18nRuntimeOverrides(base, shipped.value, silent) : base;
51
+ const config2 = domains && shipped.value ? applyLocaleBaseUrls(applied2, shipped.value, silent) : applied2;
38
52
  return domains && shipped.value?.domain ? configForHost(config2, shipped.value.domain) : config2;
39
53
  }
40
- const config = applyI18nRuntimeOverrides(base, readI18nRuntimeOverrides(nuxtApp.$config.public));
54
+ const runtimePublic = nuxtApp.$config.public;
55
+ const overrides = readI18nRuntimeOverrides(runtimePublic);
56
+ const applied = applyI18nRuntimeOverrides(base, overrides);
57
+ const config = domains ? applyLocaleBaseUrls(applied, { ...overrides, localeBaseUrls: readLocaleBaseUrls(runtimePublic) }) : applied;
41
58
  return domains ? configForHost(config, location.host) : config;
42
59
  }
43
60
  export function useResolvedI18nConfig(nuxtApp) {
@@ -1,4 +1,5 @@
1
1
  import { hooks } from "#build/i18n.strategy.mjs";
2
+ import { beforeLocaleSwitch, hasSubscriber } from "./hooks.js";
2
3
  function createOutgoingWatch(nuxtApp, i18n) {
3
4
  let stopWaiting;
4
5
  let noElement = null;
@@ -29,12 +30,8 @@ function createOutgoingWatch(nuxtApp, i18n) {
29
30
  };
30
31
  }
31
32
  function createRegister(nuxtApp, { mergeTranslation }) {
32
- const subscribed = () => {
33
- const { _hooks: subscribers } = nuxtApp.hooks;
34
- return !subscribers || Boolean(subscribers["i18n:register"]?.length);
35
- };
36
33
  return async (locale, routeName) => {
37
- if (!subscribed()) return;
34
+ if (!hasSubscriber(nuxtApp, "i18n:register")) return;
38
35
  const merges = [];
39
36
  const register = (translations, selectedLocale) => {
40
37
  merges.push(mergeTranslation(selectedLocale ?? locale, routeName, translations));
@@ -43,6 +40,21 @@ function createRegister(nuxtApp, { mergeTranslation }) {
43
40
  await Promise.all(merges);
44
41
  };
45
42
  }
43
+ function createSwitchVerdict({ nuxtApp, i18n, api, locale: { isValidLocale } }) {
44
+ let redirectedTo = null;
45
+ return async (to, locale) => {
46
+ const from = i18n.getLocale();
47
+ const redirected = redirectedTo === locale;
48
+ redirectedTo = null;
49
+ if (!import.meta.client || nuxtApp.isHydrating || !from || locale === from || redirected) return void 0;
50
+ const ctx = await beforeLocaleSwitch(nuxtApp, from, locale);
51
+ if (!ctx) return void 0;
52
+ if (ctx.cancel) return false;
53
+ if (ctx.to === locale || !isValidLocale(ctx.to)) return void 0;
54
+ redirectedTo = ctx.to;
55
+ return api.provide.localeRoute(to, ctx.to);
56
+ };
57
+ }
46
58
  export function setupI18nNavigation(deps) {
47
59
  const { nuxtApp, router, i18n, loader, i18nConfig, i18nStrategy, api, leavePage } = deps;
48
60
  const noPrefix = i18nConfig.strategy === "no_prefix";
@@ -51,10 +63,10 @@ export function setupI18nNavigation(deps) {
51
63
  const register = createRegister(nuxtApp, api.helper);
52
64
  const targets = /* @__PURE__ */ new WeakMap();
53
65
  if (hooks) nuxtApp.hooks.hookOnce("app:created", () => register(i18n.getLocale(), i18n.getCurrentRouteName()));
54
- const loadTarget = async (to) => {
66
+ const targetLocale = (to) => (i18nConfig.hashMode ? i18nStrategy.getLocaleFromPath(to.path) : null) || api.provide.getLocale(to);
67
+ const switchVerdict = hooks ? createSwitchVerdict(deps) : void 0;
68
+ const loadTarget = async (to, locale) => {
55
69
  try {
56
- const hashLocale = i18nConfig.hashMode ? i18nStrategy.getLocaleFromPath(to.path) : null;
57
- const locale = hashLocale || api.provide.getLocale(to);
58
70
  const routeName = i18nStrategy.getPluginRouteName(to, locale);
59
71
  const changed = locale !== i18n.getLocale() || routeName !== i18n.getCurrentRouteName();
60
72
  return { locale, routeName, changed, data: changed ? await loader.fetchContext(locale, routeName) : null };
@@ -68,8 +80,17 @@ export function setupI18nNavigation(deps) {
68
80
  const paths = noPrefix ? to.matched.at(-1)?.meta.i18n?.paths : void 0;
69
81
  const customPath = paths?.[api.provide.getLocale(to)];
70
82
  if (customPath) return { path: customPath, query: to.query, hash: to.hash };
83
+ let locale;
84
+ try {
85
+ locale = targetLocale(to);
86
+ } catch (e) {
87
+ if (process.env.NODE_ENV !== "production") console.error("[i18n] Navigation error:", e);
88
+ return;
89
+ }
90
+ const verdict = switchVerdict && await switchVerdict(to, locale);
91
+ if (verdict !== void 0) return verdict;
71
92
  loader.invalidatePendingSwitches();
72
- const target = await loadTarget(to);
93
+ const target = await loadTarget(to, locale);
73
94
  if (!target) return;
74
95
  targets.set(to, target);
75
96
  if (hooks) await register(target.locale, target.routeName);
@@ -1,8 +1,8 @@
1
- import { BaseI18n, FormatService, type BaseI18nOptions, type TranslationStorage } from '@i18n-micro/core';
2
- import type { CleanTranslation, ResolvedI18nConfig, Params, TranslationKey, Translations } from '@i18n-micro/types';
1
+ import { BaseI18n, type BaseI18nOptions, type TranslationStorage } from '@i18n-micro/core';
2
+ import type { CleanTranslation, TranslateParams, TranslationKey, Translations } from '@i18n-micro/types';
3
3
  import { type ShallowRef } from 'vue';
4
4
  import { type LoadOptions } from './storage.js';
5
- export type TranslationFn<T extends CleanTranslation> = (key: TranslationKey, params?: Params, defaultValue?: string | null) => T;
5
+ export type TranslationFn<T extends CleanTranslation> = (key: TranslationKey, params?: TranslateParams, defaultValue?: string | null) => T;
6
6
  export interface RouteTranslationContext {
7
7
  locale: string;
8
8
  routeName: string;
@@ -19,12 +19,6 @@ export interface NuxtI18nOptions extends BaseI18nOptions {
19
19
  */
20
20
  cacheMaxSize?: number;
21
21
  }
22
- /**
23
- * The server's formatters. `Intl` objects are costly and depend only on what the service is built
24
- * from, so requests share them: its input is this read-only config, and no request reaches it
25
- * (`$tn` and `$td` pass the locale per call). One per resolved config: runtime overrides and hosts.
26
- */
27
- export declare function formatterFor(config: ResolvedI18nConfig): FormatService;
28
22
  export declare class NuxtI18n extends BaseI18n {
29
23
  readonly storage: TranslationStorage;
30
24
  private readonly cacheMaxSize;
@@ -1,6 +1,5 @@
1
1
  import {
2
2
  BaseI18n,
3
- FormatService,
4
3
  mergeTranslationChunk,
5
4
  resolveCachedTranslation,
6
5
  setTranslationAtKey,
@@ -9,12 +8,6 @@ import {
9
8
  import { PREMERGED_ROOT_PAGE } from "@i18n-micro/utils/merge-source";
10
9
  import { shallowRef, triggerRef } from "vue";
11
10
  import { translationStorage } from "./storage.js";
12
- const formatters = /* @__PURE__ */ new WeakMap();
13
- export function formatterFor(config) {
14
- let formatter = formatters.get(config);
15
- if (!formatter) formatters.set(config, formatter = new FormatService(config));
16
- return formatter;
17
- }
18
11
  export class NuxtI18n extends BaseI18n {
19
12
  storage;
20
13
  cacheMaxSize;
package/dist/types.d.mts CHANGED
@@ -4,7 +4,7 @@ declare module '#app' {
4
4
  interface RuntimeNuxtHooks extends ModuleRuntimeHooks {}
5
5
  }
6
6
 
7
- export { type Getter, type GlobalLocaleRoutes, type Locale, type LocaleCode, type ModuleOptions, type PluralFunc, type Strategies } from '@i18n-micro/types'
7
+ export { type Getter, type GlobalLocaleRoutes, type I18nBeforeLocaleSwitchContext, type Locale, type LocaleCode, type ModuleOptions, type PluralFunc, type Strategies } from '@i18n-micro/types'
8
8
 
9
9
  export { type PluginsInjections } from '../dist/runtime/plugins/01.plugin.js'
10
10
 
package/internals.d.mts CHANGED
@@ -12,6 +12,8 @@ interface I18nPrivateConfig {
12
12
  debug: boolean
13
13
  translationDir: string
14
14
  additionalTranslationDirs?: string[]
15
+ /** `linkedMessages`, for servers that merge source files. */
16
+ linkedMessages?: boolean
15
17
  fallbackLocale?: string
16
18
  apiBaseUrl: string
17
19
  apiBaseClientHost?: string
@@ -108,6 +110,10 @@ declare module '#i18n-internal/strategy' {
108
110
  export function createI18nStrategy(router: I18nStrategyRouterLike): I18nStrategyInstance
109
111
  }
110
112
 
113
+ declare module '#i18n-internal/plural' {
114
+ export const plural: import('@i18n-micro/types').PluralFunc
115
+ }
116
+
111
117
  declare module '#i18n-internal/payload-source' {
112
118
  export const embeddedPayloads: boolean
113
119
  export function readPayload(relPath: string): Promise<Record<string, unknown>>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-i18n-micro",
3
- "version": "4.0.1",
3
+ "version": "4.1.0",
4
4
  "description": "Fast, lightweight i18n for Nuxt with strategy-based routing and minimal overhead.",
5
5
  "keywords": [
6
6
  "i18n",
@@ -78,11 +78,11 @@
78
78
  "h3": "^1.15.11",
79
79
  "sirv": "^3.0.2",
80
80
  "ufo": "^1.6.4",
81
- "@i18n-micro/core": "^2.0.1",
82
- "@i18n-micro/types": "^2.0.1",
83
- "@i18n-micro/path-strategy": "^2.0.1",
84
- "@i18n-micro/utils": "^2.0.1",
85
- "@i18n-micro/route-strategy": "^2.0.1"
81
+ "@i18n-micro/core": "^2.1.0",
82
+ "@i18n-micro/path-strategy": "^2.1.0",
83
+ "@i18n-micro/route-strategy": "^2.1.0",
84
+ "@i18n-micro/types": "^2.1.0",
85
+ "@i18n-micro/utils": "^2.1.0"
86
86
  },
87
87
  "devDependencies": {
88
88
  "@nuxt/devtools": "^3.4.2",
@@ -1 +0,0 @@
1
- {"id":"356b87e3-45dd-4f80-a40a-b649665c1844","timestamp":1793785771623,"prerendered":[]}