nuxt-i18n-micro 4.0.1 → 4.1.1

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/4cf3c3ff-2c34-458a-8031-474a528c7231.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 +90 -50
  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 +5 -2
  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:"4cf3c3ff-2c34-458a-8031-474a528c7231",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},1794130971663,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:"4cf3c3ff-2c34-458a-8031-474a528c7231",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},1794130971664,false]</script></body></html>
@@ -1 +1 @@
1
- {"id":"356b87e3-45dd-4f80-a40a-b649665c1844","timestamp":1793785771623}
1
+ {"id":"4cf3c3ff-2c34-458a-8031-474a528c7231","timestamp":1794130968604}
@@ -0,0 +1 @@
1
+ {"id":"4cf3c3ff-2c34-458a-8031-474a528c7231","timestamp":1794130968604,"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:"4cf3c3ff-2c34-458a-8031-474a528c7231",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},1794130971665,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.1",
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");
@@ -184,6 +183,13 @@ async function buildTranslationSourceLayers(rootDirs, translationDirName, output
184
183
  });
185
184
  }
186
185
 
186
+ function resolvePayloadHostSources(options, env = process.env) {
187
+ return {
188
+ apiBaseClientHost: env.NUXT_I18N_APP_BASE_CLIENT_HOST || options.apiBaseClientHost || void 0,
189
+ apiBaseServerHost: env.NUXT_I18N_APP_BASE_SERVER_HOST || options.apiBaseServerHost || void 0,
190
+ apiBaseUrl: env.NUXT_I18N_APP_BASE_URL || options.apiBaseUrl || "_locales"
191
+ };
192
+ }
187
193
  const DEFAULT_TRANSLATION_PAYLOAD_WARN_FILE_COUNT = 500;
188
194
  const DEFAULT_TRANSLATION_PAYLOAD_WARN_SIZE_BYTES = 10 * 1024 * 1024;
189
195
  function resolveTranslationPayloadOptions(options) {
@@ -275,6 +281,10 @@ function getTranslationPayloadSizeWarning(stats, thresholds) {
275
281
  return `Generated translation payloads are large (${stats.fileCount} files, ${formatBytes(stats.totalBytes)}). Consider translationPayloads.mode: 'source' for public output, hosting payloads externally, or disabling unused public/prerender outputs.`;
276
282
  }
277
283
 
284
+ function localesByLayerPriority(layers, merged = []) {
285
+ return [...layers.flatMap((layer) => layer.config.i18n?.locales ?? []), ...merged];
286
+ }
287
+
278
288
  function collectJsonFiles(dir) {
279
289
  return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
280
290
  const fullPath = join(dir, entry.name);
@@ -342,12 +352,14 @@ function hashTranslationSources(rootDirs, translationDirName, additionalTranslat
342
352
  }
343
353
  hashFiles(dir, files);
344
354
  };
355
+ const relativeExtras = additionalTranslationDirs.filter((extra) => !isAbsolute(extra));
345
356
  for (const rootDir of rootDirs) {
346
357
  hashDir(join(rootDir, translationDirName));
347
- for (const extra of additionalTranslationDirs) {
358
+ for (const extra of relativeExtras) {
348
359
  hashAdditionalRootLocales(join(rootDir, extra));
349
360
  }
350
361
  }
362
+ for (const extra of new Set(additionalTranslationDirs.filter((dir) => isAbsolute(dir)))) hashAdditionalRootLocales(extra);
351
363
  return seen > 0 ? hash.digest("hex").slice(0, 16) : null;
352
364
  }
353
365
  const NITRO_MIN_COMPRESS_BYTES = 1024;
@@ -451,7 +463,8 @@ function setupDevToolsUI(options, rootDirs) {
451
463
  const extraDir = file.startsWith("additional:") ? (options.additionalTranslationDirs ?? []).find((dir) => file.startsWith(`additional:${dir}/`)) : void 0;
452
464
  if (file.startsWith("additional:") && !extraDir) throw new Error(`Not a configured additional translation directory: ${file}`);
453
465
  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);
466
+ const dirs = extraDir ? additionalTranslationRoots(rootDirs, [extraDir]) : rootDirs.map((rootDir) => path.join(rootDir, options.translationDir || "locales"));
467
+ const target = dirs.reverse().map((dir) => resolveTranslationFile(dir, relativeFile)).find((candidate) => candidate !== null);
455
468
  if (!target) throw new Error(`File not found in the translation directories: ${file}`);
456
469
  fs.writeFileSync(target, JSON.stringify(content, null, 2), "utf-8");
457
470
  },
@@ -484,14 +497,12 @@ function setupDevToolsUI(options, rootDirs) {
484
497
  const pagesDir = path.join(localesDir, "pages");
485
498
  processDirectory(localesDir, localesDir);
486
499
  processDirectory(pagesDir, localesDir);
487
- for (const extra of options.additionalTranslationDirs ?? []) {
488
- const extraDir = path.join(rootDir, extra);
489
- if (!fs.existsSync(extraDir)) continue;
500
+ }
501
+ for (const extra of options.additionalTranslationDirs ?? []) {
502
+ for (const extraDir of additionalTranslationRoots(rootDirs, [extra]).filter((dir) => fs.existsSync(dir))) {
490
503
  for (const name of fs.readdirSync(extraDir)) {
491
- if (!name.endsWith(".json")) continue;
492
504
  const filePath = path.join(extraDir, name);
493
- if (!fs.statSync(filePath).isFile()) continue;
494
- readFile(`additional:${extra}/${name}`, filePath);
505
+ if (name.endsWith(".json") && fs.statSync(filePath).isFile()) readFile(`additional:${extra}/${name}`, filePath);
495
506
  }
496
507
  }
497
508
  }
@@ -499,8 +510,10 @@ function setupDevToolsUI(options, rootDirs) {
499
510
  }
500
511
  };
501
512
  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)));
513
+ const roots = [
514
+ ...rootDirs.map((root) => resolve(root, options.translationDir || "locales")),
515
+ ...additionalTranslationRoots(rootDirs, options.additionalTranslationDirs)
516
+ ];
504
517
  let timer;
505
518
  nuxt.hook("builder:watch", (_event, watched) => {
506
519
  const file = resolve(nuxt.options.srcDir, watched);
@@ -881,6 +894,8 @@ function extractDefineI18nRouteData(content, filePath) {
881
894
  warnUnextractable(filePath, result.error);
882
895
  return null;
883
896
  }
897
+ if (result.config === false) return false;
898
+ if (!result.config || typeof result.config !== "object") return null;
884
899
  return normalizeDefineConfig(result.config);
885
900
  } catch {
886
901
  return null;
@@ -919,6 +934,10 @@ function mergeDefineI18nRouteMeta(entries, config) {
919
934
  const routeDisableMeta = { ...config.routeDisableMeta };
920
935
  const fromPages = {};
921
936
  for (const { routePath, config: page } of entries) {
937
+ if (page === false) {
938
+ fromPages[routePath] = false;
939
+ continue;
940
+ }
922
941
  if (Array.isArray(page.locales)) routeLocales[routePath] = page.locales;
923
942
  else if (page.locales) routeLocales[routePath] = Object.keys(page.locales);
924
943
  if (page.localeRoutes) fromPages[routePath] = page.localeRoutes;
@@ -936,7 +955,7 @@ function collectDefineI18nRouteMetaFromFiles(pageFiles, rootDirs) {
936
955
  if (!rootDir) continue;
937
956
  try {
938
957
  const config = extractDefineI18nRouteData(readFileSync(pageFile, "utf-8"), pageFile);
939
- if (!config) continue;
958
+ if (config === null) continue;
940
959
  entries.push({
941
960
  routePath: pageFilePathToRoutePath(pageFile, rootDir),
942
961
  config
@@ -967,12 +986,13 @@ declare module '#app' {
967
986
 
968
987
  export {}`;
969
988
  }
989
+ const REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
970
990
  const DEFAULT_CANONICAL_QUERY_WHITELIST = ["page", "sort", "filter", "search", "q", "query", "tag"];
971
991
  function isLiveDictionary(options, mode, serverHost) {
972
992
  return mode === "source" || Boolean(options.cacheTtl) || Boolean(serverHost);
973
993
  }
974
994
  function buildFullConfig(params) {
975
- const { options, nuxt, defaultLocale, apiConfig, translationPayloadMode, locales, translationsHash, publicPayloads, rootDelta } = params;
995
+ const { options, nuxt, defaultLocale, apiConfig, translationPayloadMode, locales, publicPayloads, rootDelta } = params;
976
996
  const noScripts = nuxt.options.features.noScripts;
977
997
  return {
978
998
  locales: locales ?? [],
@@ -985,12 +1005,16 @@ function buildFullConfig(params) {
985
1005
  defaultLocale,
986
1006
  fallbackLocale: options.fallbackLocale ?? void 0,
987
1007
  localeCookie: options.localeCookie ?? null,
1008
+ // set only when it is not the default: 06.redirect reads it
1009
+ redirectStatusCode: options.redirectStatusCode ?? void 0,
1010
+ // only a build that writes a cookie ships its attributes
1011
+ localeCookieOptions: options.localeCookie || nuxt.options?.router?.options?.hashMode ? options.localeCookieOptions : void 0,
988
1012
  autoDetectLanguage: options.autoDetectLanguage,
989
1013
  autoDetectPath: options.autoDetectPath || "/",
990
1014
  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(),
1015
+ // the `?v=` of payload URLs: a fingerprint of the translations (set at modules:done), as a timestamp would make
1016
+ // clients re-download what never changed. `options.dateBuild` wins even when it is `0`/`''`: that opts out of busting.
1017
+ dateBuild: options.dateBuild ?? Date.now(),
994
1018
  hashMode: nuxt.options?.router?.options?.hashMode ?? false,
995
1019
  trailingSlash: options.trailingSlash ?? nuxt.options.experimental?.defaults?.nuxtLink?.trailingSlash,
996
1020
  apiBaseUrl: apiConfig.apiBaseUrl,
@@ -1027,6 +1051,8 @@ function buildPrivateConfig(options, nuxt, rootDirs, apiConfig, locales, payload
1027
1051
  additionalTranslationDirs: options.additionalTranslationDirs ?? [],
1028
1052
  apiBaseServerHost: apiConfig.apiBaseServerHost,
1029
1053
  serverTranslationPreload: options.serverTranslationPreload ?? false,
1054
+ // read where a server merges source files (mode 'source', dev): set only when on
1055
+ linkedMessages: options.linkedMessages || void 0,
1030
1056
  cacheStorage: options.cacheStorage,
1031
1057
  cacheStorageBuild: nuxt.options.buildId,
1032
1058
  payloadFsDir,
@@ -1145,6 +1171,7 @@ const module$1 = defineNuxtModule({
1145
1171
  httpCacheDuration: 31536e3
1146
1172
  },
1147
1173
  async setup(options, nuxt) {
1174
+ options.locales = localesByLayerPriority(nuxt.options._layers, options.locales);
1148
1175
  const defaultLocale = process.env.DEFAULT_LOCALE || options.defaultLocale || "en";
1149
1176
  let isSSG = false;
1150
1177
  const logger = useLogger("nuxt-i18n-micro");
@@ -1159,6 +1186,13 @@ const module$1 = defineNuxtModule({
1159
1186
  options.localeCookie = "user-locale";
1160
1187
  logger.info("Strategy 'no_prefix': localeCookie automatically set to 'user-locale' for locale persistence.");
1161
1188
  }
1189
+ if (options.redirectStatusCode !== void 0 && !REDIRECT_STATUSES.has(options.redirectStatusCode)) {
1190
+ logger.warn(`redirectStatusCode ${options.redirectStatusCode} is none of 301, 302, 303, 307, 308; 302 is used.`);
1191
+ options.redirectStatusCode = void 0;
1192
+ }
1193
+ if (options.localeCookieOptions?.sameSite === "none" && !options.localeCookieOptions.secure) {
1194
+ logger.warn("localeCookieOptions: sameSite 'none' needs secure: true, or browsers reject the locale cookie.");
1195
+ }
1162
1196
  if (options.strategy !== "no_prefix" && options.redirects !== false && !options.localeCookie) {
1163
1197
  logger.info(
1164
1198
  "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 +1203,11 @@ const module$1 = defineNuxtModule({
1169
1203
  const mergedLocalesDir = resolve(nuxt.options.buildDir, "i18n-merged");
1170
1204
  const sourceLocalesDir = resolve(nuxt.options.buildDir, "i18n-source");
1171
1205
  const translationDirName = options.translationDir || "locales";
1172
- const translationsHash = options.dateBuild === void 0 ? hashTranslationSources(
1206
+ const extraDirs = options.additionalTranslationDirs ??= [];
1207
+ const fingerprint = () => options.dateBuild === void 0 ? hashTranslationSources(
1173
1208
  rootDirs,
1174
1209
  translationDirName,
1175
- options.additionalTranslationDirs,
1210
+ extraDirs,
1176
1211
  JSON.stringify([options.fallbackLocale, options.disablePageLocales, (options.locales ?? []).map((l) => [l.code, l.fallbackLocale])])
1177
1212
  ) : null;
1178
1213
  const translationPayloads = resolveTranslationPayloadOptions(options);
@@ -1184,7 +1219,8 @@ const module$1 = defineNuxtModule({
1184
1219
  })) {
1185
1220
  logger.warn(warning);
1186
1221
  }
1187
- const rawServerHost = process.env.NUXT_I18N_APP_BASE_SERVER_HOST ?? options.apiBaseServerHost ?? void 0;
1222
+ const payloadSources = resolvePayloadHostSources(options);
1223
+ const rawServerHost = payloadSources.apiBaseServerHost;
1188
1224
  if (!translationPayloads.serverAssets && !translationPayloads.publicAssets && !rawServerHost) {
1189
1225
  throw new Error(
1190
1226
  "nuxt-i18n-micro: local SSR payloads are disabled (serverAssets/publicAssets false) and apiBaseServerHost is not set. SSR would load empty translations."
@@ -1200,10 +1236,11 @@ const module$1 = defineNuxtModule({
1200
1236
  localeInfosByCode.set(locale.code, {
1201
1237
  code: locale.code,
1202
1238
  fallbackLocale: prev?.fallbackLocale ?? locale.fallbackLocale,
1239
+ iso: prev?.iso ?? locale.iso,
1203
1240
  disabled: prev?.disabled ?? locale.disabled
1204
1241
  });
1205
1242
  }
1206
- const localeInfos = [...localeInfosByCode.values()].filter((locale) => !locale.disabled).map(({ code, fallbackLocale }) => ({ code, fallbackLocale }));
1243
+ const localeInfos = [...localeInfosByCode.values()].filter((locale) => !locale.disabled).map(({ code, fallbackLocale, iso }) => ({ code, fallbackLocale, iso }));
1207
1244
  let routeNames = [];
1208
1245
  nuxt.hook("build:before", async () => {
1209
1246
  if (translationPayloads.mode === "source") {
@@ -1220,7 +1257,9 @@ const module$1 = defineNuxtModule({
1220
1257
  additionalTranslationDirs: options.additionalTranslationDirs,
1221
1258
  routeNames,
1222
1259
  // dev reads live files, and a prerender merges what it reads like any server
1223
- rootDelta: !nuxt.options.dev && translationPayloads.rootDelta
1260
+ rootDelta: !nuxt.options.dev && translationPayloads.rootDelta,
1261
+ linkedMessages: options.linkedMessages,
1262
+ warn: (message) => logger.warn(message)
1224
1263
  });
1225
1264
  logger.info(`Pre-merged translations from ${rootDirs.length} layer(s) into ${mergedLocalesDir}`);
1226
1265
  }
@@ -1236,12 +1275,12 @@ const module$1 = defineNuxtModule({
1236
1275
  } = mergeDefineI18nRouteMeta(collectDefineI18nRouteMetaFromFiles(pageFiles, rootDirs), options);
1237
1276
  const resolvedStrategyPath = await resolveEntry("@i18n-micro/path-strategy");
1238
1277
  const resolvedHelpersPath = await resolveEntry("@i18n-micro/core/helpers");
1239
- let apiBaseClientHost = process.env.NUXT_I18N_APP_BASE_CLIENT_HOST ?? options.apiBaseClientHost ?? void 0;
1278
+ let apiBaseClientHost = payloadSources.apiBaseClientHost;
1240
1279
  if (apiBaseClientHost?.endsWith("/")) {
1241
1280
  apiBaseClientHost = apiBaseClientHost.slice(0, -1);
1242
1281
  }
1243
1282
  const apiBaseServerHost = rawServerHost?.endsWith("/") ? rawServerHost.slice(0, -1) : rawServerHost;
1244
- const rawUrl = process.env.NUXT_I18N_APP_BASE_URL ?? options.apiBaseUrl ?? "_locales";
1283
+ const rawUrl = payloadSources.apiBaseUrl;
1245
1284
  if (rawUrl.startsWith("http://") || rawUrl.startsWith("https://")) {
1246
1285
  throw new Error("Nuxt-i18n-micro: Please use NUXT_I18N_APP_BASE_CLIENT_HOST or NUXT_I18N_APP_BASE_SERVER_HOST instead.");
1247
1286
  }
@@ -1271,8 +1310,12 @@ const module$1 = defineNuxtModule({
1271
1310
  translationPayloadMode: translationPayloads.mode,
1272
1311
  publicPayloads: translationPayloads.mode === "premerged" && translationPayloads.publicAssets,
1273
1312
  rootDelta: !nuxt.options.dev && translationPayloads.mode === "premerged" && translationPayloads.rootDelta && !options.disablePageLocales && (translationPayloads.publicAssets || Boolean(options.apiBaseClientHost)),
1274
- locales: routeGenerator.locales ?? [],
1275
- translationsHash
1313
+ locales: routeGenerator.locales ?? []
1314
+ });
1315
+ nuxt.hook("modules:done", () => {
1316
+ for (const dir of nuxt.options.i18n?.additionalTranslationDirs ?? [])
1317
+ if (!extraDirs.includes(dir)) extraDirs.push(dir);
1318
+ fullConfig.dateBuild = fingerprint() ?? fullConfig.dateBuild;
1276
1319
  });
1277
1320
  const generateDomain = process.env.NUXT_I18N_DOMAIN;
1278
1321
  const buildConfig = generateDomain ? configForHost(fullConfig, generateDomain) : fullConfig;
@@ -1366,16 +1409,13 @@ export default defineNuxtPlugin(() => ({ provide: { defineI18nRoute: async () =>
1366
1409
  });
1367
1410
  }
1368
1411
  if (nuxt.options.dev && (options.hmr ?? true)) {
1369
- const translationDirs = resolveLiveTranslationDirs({
1370
- rootDirs,
1371
- translationDir: translationDirName,
1372
- additionalTranslationDirs: options.additionalTranslationDirs
1373
- });
1412
+ let translationDirs;
1413
+ const liveDirs = () => translationDirs ??= resolveLiveTranslationDirs({ rootDirs, translationDir: translationDirName, additionalTranslationDirs: extraDirs });
1374
1414
  addVitePlugin(
1375
1415
  {
1376
1416
  name: "i18n-micro:translations-hmr",
1377
1417
  hotUpdate({ file }) {
1378
- if (this.environment.name !== "client" || !translationRelativePath(file, translationDirs)) return;
1418
+ if (this.environment.name !== "client" || !translationRelativePath(file, liveDirs())) return;
1379
1419
  this.environment.hot.send({ type: "custom", event: TRANSLATIONS_HMR_EVENT });
1380
1420
  return [];
1381
1421
  }
@@ -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,13 +11,14 @@ 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();
17
18
  const requestBoundIo = typeof navigator !== "undefined" && navigator.userAgent === "Cloudflare-Workers";
18
19
  function runtimeHost() {
19
20
  try {
20
- return process.env.NUXT_I18N_APP_BASE_SERVER_HOST;
21
+ return process.env.NUXT_I18N_APP_BASE_SERVER_HOST || void 0;
21
22
  } catch {
22
23
  return void 0;
23
24
  }
@@ -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.1",
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.1",
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":[]}