nuxt-i18n-micro 3.30.3 → 4.0.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 (111) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +2 -2
  3. package/dist/client/200.html +1 -1
  4. package/dist/client/404.html +1 -1
  5. package/dist/client/_nuxt/BVna_KKx.js +3 -0
  6. package/dist/client/_nuxt/Bw81RhAv.js +1 -0
  7. package/dist/client/_nuxt/CPz_jhsD.js +1 -0
  8. package/dist/client/_nuxt/CmvObVGq.js +1 -0
  9. package/dist/client/_nuxt/DKUOBR6F.js +5 -0
  10. package/dist/client/_nuxt/builds/latest.json +1 -1
  11. package/dist/client/_nuxt/builds/meta/356b87e3-45dd-4f80-a40a-b649665c1844.json +1 -0
  12. package/dist/client/_nuxt/{entry.BM-mexAv.css → entry.PSWtHhay.css} +1 -1
  13. package/dist/client/_nuxt/error-404.xs3fxhVJ.css +1 -0
  14. package/dist/client/_nuxt/error-500.C2S-4iLg.css +1 -0
  15. package/dist/client/index.html +1 -1
  16. package/dist/module.d.mts +14 -5
  17. package/dist/module.json +4 -1
  18. package/dist/module.mjs +857 -476
  19. package/dist/runtime/components/i18n-group.d.vue.ts +17 -9
  20. package/dist/runtime/components/i18n-group.vue +2 -4
  21. package/dist/runtime/components/i18n-group.vue.d.ts +17 -9
  22. package/dist/runtime/components/i18n-link.d.vue.ts +9 -6
  23. package/dist/runtime/components/i18n-link.vue +7 -28
  24. package/dist/runtime/components/i18n-link.vue.d.ts +9 -6
  25. package/dist/runtime/components/i18n-switcher.d.vue.ts +128 -54
  26. package/dist/runtime/components/i18n-switcher.vue +56 -156
  27. package/dist/runtime/components/i18n-switcher.vue.d.ts +128 -54
  28. package/dist/runtime/components/i18n-t.d.vue.ts +59 -116
  29. package/dist/runtime/components/i18n-t.vue +24 -71
  30. package/dist/runtime/components/i18n-t.vue.d.ts +59 -116
  31. package/dist/runtime/composables/useI18n.js +2 -38
  32. package/dist/runtime/composables/useI18nHead.js +21 -6
  33. package/dist/runtime/composables/useI18nLocale.d.ts +4 -1
  34. package/dist/runtime/composables/useI18nLocale.js +20 -22
  35. package/dist/runtime/composables/useLocaleHead.d.ts +26 -31
  36. package/dist/runtime/composables/useLocaleHead.js +47 -167
  37. package/dist/runtime/devtools/bridge.d.ts +2 -2
  38. package/dist/runtime/devtools/vue-devtools.d.ts +2 -2
  39. package/dist/runtime/devtools/vue-devtools.js +5 -5
  40. package/dist/runtime/middleware/i18n-redirect.global.d.ts +3 -2
  41. package/dist/runtime/middleware/i18n-redirect.global.js +30 -41
  42. package/dist/runtime/plugins/01.plugin.d.ts +156 -34
  43. package/dist/runtime/plugins/01.plugin.js +184 -138
  44. package/dist/runtime/plugins/02.meta.js +36 -43
  45. package/dist/runtime/plugins/04.vue-devtools.js +1 -1
  46. package/dist/runtime/plugins/06.redirect.d.ts +3 -3
  47. package/dist/runtime/plugins/06.redirect.js +65 -91
  48. package/dist/runtime/server/{utils → internal}/dev-live-translations.js +4 -3
  49. package/dist/runtime/server/internal/server-translation-cache.d.ts +9 -0
  50. package/dist/runtime/server/internal/server-translation-cache.js +10 -0
  51. package/dist/runtime/server/internal/shared-cache.d.ts +24 -0
  52. package/dist/runtime/server/internal/shared-cache.js +71 -0
  53. package/dist/runtime/server/internal/translation-watcher.d.ts +79 -0
  54. package/dist/runtime/server/internal/translation-watcher.js +105 -0
  55. package/dist/runtime/server/middleware/i18n.global.js +24 -37
  56. package/dist/runtime/server/payload-source.assets.d.ts +1 -0
  57. package/dist/runtime/server/payload-source.assets.js +3 -8
  58. package/dist/runtime/server/payload-source.fs.d.ts +1 -0
  59. package/dist/runtime/server/payload-source.fs.js +18 -7
  60. package/dist/runtime/server/plugins/failed-read.d.ts +7 -0
  61. package/dist/runtime/server/plugins/failed-read.js +23 -0
  62. package/dist/runtime/server/plugins/payload-cache.d.ts +6 -0
  63. package/dist/runtime/server/plugins/payload-cache.js +16 -0
  64. package/dist/runtime/server/plugins/watcher.dev.js +15 -79
  65. package/dist/runtime/server/routes/i18n.js +13 -14
  66. package/dist/runtime/server/utils/locale-detector.d.ts +32 -2
  67. package/dist/runtime/server/utils/locale-detector.js +48 -27
  68. package/dist/runtime/server/utils/locale-server-middleware.js +14 -33
  69. package/dist/runtime/server/utils/payload-cache.d.ts +4 -0
  70. package/dist/runtime/server/utils/payload-cache.js +13 -0
  71. package/dist/runtime/server/utils/server-loader.d.ts +17 -6
  72. package/dist/runtime/server/utils/server-loader.js +105 -51
  73. package/dist/runtime/server/utils/translation-server-middleware.js +18 -35
  74. package/dist/runtime/utils/cache-keys.d.ts +6 -0
  75. package/dist/runtime/utils/cache-keys.js +2 -0
  76. package/dist/runtime/utils/i18n-api.d.ts +185 -0
  77. package/dist/runtime/utils/i18n-api.js +196 -0
  78. package/dist/runtime/utils/i18n-config.d.ts +18 -0
  79. package/dist/runtime/utils/i18n-config.js +47 -0
  80. package/dist/runtime/utils/i18n-navigation.d.ts +26 -0
  81. package/dist/runtime/utils/i18n-navigation.js +96 -0
  82. package/dist/runtime/utils/nuxt-i18n-server.d.ts +26 -0
  83. package/dist/runtime/utils/nuxt-i18n-server.js +83 -0
  84. package/dist/runtime/utils/nuxt-i18n.d.ts +81 -96
  85. package/dist/runtime/utils/nuxt-i18n.js +180 -252
  86. package/dist/runtime/utils/page-head.d.ts +12 -0
  87. package/dist/runtime/utils/page-head.js +6 -0
  88. package/dist/runtime/utils/request-url.d.ts +6 -0
  89. package/dist/runtime/utils/request-url.js +11 -0
  90. package/dist/runtime/utils/storage.d.ts +31 -19
  91. package/dist/runtime/utils/storage.js +100 -49
  92. package/dist/runtime/utils/translation-entry.d.ts +6 -0
  93. package/dist/runtime/utils/translation-entry.js +16 -0
  94. package/dist/types.d.mts +4 -6
  95. package/internals.d.mts +9 -2
  96. package/package.json +78 -79
  97. package/dist/client/_nuxt/BOxWElQP.js +0 -21
  98. package/dist/client/_nuxt/CUWB4CwN.js +0 -3
  99. package/dist/client/_nuxt/CmUbe-uv.js +0 -1
  100. package/dist/client/_nuxt/DAzShkZF.js +0 -1
  101. package/dist/client/_nuxt/MokPP5zi.js +0 -1
  102. package/dist/client/_nuxt/builds/meta/60974e48-d00e-4f9b-9467-9d4b322519da.json +0 -1
  103. package/dist/client/_nuxt/error-404.D9Svp6Hk.css +0 -1
  104. package/dist/client/_nuxt/error-500.B-mjI9_9.css +0 -1
  105. package/dist/runtime/plugins/03.define.d.ts +0 -7
  106. package/dist/runtime/plugins/03.define.js +0 -42
  107. package/dist/runtime/plugins/05.hooks.d.ts +0 -2
  108. package/dist/runtime/plugins/05.hooks.js +0 -47
  109. package/dist/runtime/server/utils/server-translation-cache.d.ts +0 -11
  110. package/dist/runtime/server/utils/server-translation-cache.js +0 -20
  111. /package/dist/runtime/server/{utils → internal}/dev-live-translations.d.ts +0 -0
package/dist/module.mjs CHANGED
@@ -1,24 +1,377 @@
1
1
  import * as fs from 'node:fs';
2
- import fs__default, { readFileSync, mkdirSync, writeFileSync, existsSync } from 'node:fs';
3
- import path, { dirname, resolve, join } from 'node:path';
4
- import { fileURLToPath, pathToFileURL } from 'node:url';
5
- import { defaultPlural, isNoPrefixStrategy, withPrefixStrategy } from '@i18n-micro/core';
6
- import { generateHmrPlugin } from '@i18n-micro/hmr/generate-plugin';
7
- import { RouteGenerator, isLocaleAllowedForUnlocalizedRoute, normalizePath, isInternalPath } from '@i18n-micro/route-strategy';
8
- import { buildTranslationSourceLayers, preMergeLocales } from '@i18n-micro/utils/build';
9
- import { resolveTranslationPayloadOptions, getTranslationPayloadMisconfigurationWarnings, getTranslationPayloadSizeWarning, resolveTranslationPayloadWarningThresholds, resolveTranslationPayloadPublicRel, shouldReadPayloadsFromPublicDir, shouldRegisterNitroServerAssets, shouldCopyTranslationPayloadsToPublic, resolveTranslationPayloadPublicDir } from '@i18n-micro/utils/payload-config';
10
- export { resolveTranslationPayloadMode, resolveTranslationPayloadOptions, resolveTranslationPayloadPublicDir, resolveTranslationPayloadPublicRel, shouldCopyTranslationPayloadsToPublic, shouldRegisterNitroServerAssets } from '@i18n-micro/utils/payload-config';
11
- import { hashTranslationSources, scanTranslationPayloadDirectory, compressTranslationPayloads } from '@i18n-micro/utils/payload-stats';
12
- import { buildTranslationPayloadCacheControl } from '@i18n-micro/utils/payload-url';
13
- import { useNuxt, addDevServerHandler, defineNuxtModule, useLogger, createResolver, addImportsDir, addPlugin, addRouteMiddleware, addServerImportsDir, addServerHandler, addComponentsDir, addTemplate, addTypeTemplate, addVitePlugin, resolvePath, addPrerenderRoutes } from '@nuxt/kit';
2
+ import fs__default, { existsSync, mkdirSync, writeFileSync, readdirSync, statSync, readFileSync } from 'node:fs';
3
+ import path, { join, dirname, relative, basename, resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { defaultPlural, isNoPrefixStrategy } from '@i18n-micro/core';
6
+ import { forwardSlashes, trimLeadingSlashes, isInternalPath } from '@i18n-micro/utils/app-path';
7
+ import { readTranslationFile, resolveLiveTranslationDirs, translationRelativePath } from '../dist/runtime/server/internal/translation-watcher.js';
8
+ import { isLocaleAllowedForUnlocalizedRoute, normalizePath, routeKey, nameOfPath, RouteGenerator, localesOutsideMatcher, pageNames } from '@i18n-micro/route-strategy';
9
+ import { configForHost } from '@i18n-micro/utils/domains';
10
+ import { createHash } from 'node:crypto';
14
11
  import { globby } from 'globby';
12
+ import { deepMergeTranslationsRecursive } from '@i18n-micro/utils/deep-merge';
13
+ import { PREMERGED_ROOT_PAGE, mergeSourceTranslations, PREMERGED_ROOT_MARK, mergeOverRoot, buildFallbackLocaleChain } from '@i18n-micro/utils/merge-source';
14
+ import { gzipSync, brotliCompressSync, constants } from 'node:zlib';
15
+ import { resolveTranslationPayloadCachePolicy } from '@i18n-micro/utils/payload-url';
16
+ import { useNuxt, addDevServerHandler, defineNuxtModule, useLogger, createResolver, addImportsDir, addPlugin, addRouteMiddleware, addPluginTemplate, addServerImports, addServerHandler, addComponentsDir, addVitePlugin, addTemplate, addTypeTemplate, resolvePath, addPrerenderRoutes } from '@nuxt/kit';
15
17
  import sirv from 'sirv';
16
18
  import { startSubprocess, addCustomTab, onDevToolsInitialized, extendServerRpc } from '@nuxt/devtools-kit';
19
+ import { isInsideDir, resolveTranslationFile } from '@i18n-micro/utils/load-dir';
17
20
  import { fromNodeMiddleware, defineEventHandler, proxyRequest } from 'h3';
18
21
  import { joinURL } from 'ufo';
19
- import { createUnplugin } from 'unplugin';
22
+ import { DomainPathStrategy, createPathStrategy } from '@i18n-micro/path-strategy';
20
23
  import { parse } from '@vue/compiler-sfc';
21
24
 
25
+ const TRANSLATIONS_HMR_EVENT = "i18n-micro:update";
26
+ function generateHmrPlugin() {
27
+ return `
28
+ import { defineNuxtPlugin } from '#imports'
29
+
30
+ export default defineNuxtPlugin((nuxtApp) => {
31
+ if (!import.meta.hot) return
32
+ import.meta.hot.on(${JSON.stringify(TRANSLATIONS_HMR_EVENT)}, () => {
33
+ try {
34
+ nuxtApp.$clearCache()
35
+ console.log('[i18n HMR] Translations reloaded')
36
+ }
37
+ catch (e) {
38
+ console.warn('[i18n HMR] Failed to reload translations', e)
39
+ }
40
+ })
41
+ })
42
+ `.trim();
43
+ }
44
+
45
+ function buildFullConfigSource(config) {
46
+ const { excludePatterns = [], ...rest } = config;
47
+ const patterns = excludePatterns.map((pattern) => pattern instanceof RegExp ? String(pattern) : JSON.stringify(pattern));
48
+ return `{ ...${JSON.stringify(rest)}, excludePatterns: [${patterns.join(", ")}] }`;
49
+ }
50
+ function buildPrivateConfigSource(config, dev) {
51
+ const { rootDirs, payloadFsDir, ...rest } = config;
52
+ const read = `export function getI18nPrivateConfig() { return __privateConfig }
53
+ `;
54
+ if (dev) return `const __privateConfig = ${JSON.stringify(config)}
55
+ ${read}`;
56
+ return `const __privateConfig = ${JSON.stringify(rest)}
57
+ if (import.meta.prerender) Object.assign(__privateConfig, ${JSON.stringify({ rootDirs, payloadFsDir })})
58
+ ${read}`;
59
+ }
60
+
61
+ function mergeAdditionalRootLocaleFiles(rootDirs, additionalDirNames, locale, primaryContent = {}) {
62
+ if (!additionalDirNames.length) return primaryContent;
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
+ }
69
+ return deepMergeTranslationsRecursive(content, primaryContent);
70
+ }
71
+ function listAdditionalRootLocales(rootDirs, additionalDirNames) {
72
+ 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
+ }
82
+ }
83
+ return [...locales];
84
+ }
85
+ const isTree = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
86
+ function diffTree(page, root) {
87
+ const delta = {};
88
+ for (const key of Object.keys(page)) {
89
+ const value = page[key];
90
+ const base = root[key];
91
+ if (isTree(value) && isTree(base)) {
92
+ const nested = diffTree(value, base);
93
+ if (Object.keys(nested).length) delta[key] = nested;
94
+ } else if (!Object.hasOwn(root, key) || JSON.stringify(value) !== JSON.stringify(base)) delta[key] = value;
95
+ }
96
+ return delta;
97
+ }
98
+ function pageFileOverRoot(page, root, mark) {
99
+ const delta = diffTree(page, root);
100
+ const merged = mergeOverRoot(root, delta);
101
+ const whole = Object.keys(merged).length !== Object.keys(page).length;
102
+ if (whole || Object.keys(page).some((key) => JSON.stringify(merged[key]) !== JSON.stringify(page[key]))) return page;
103
+ return { ...delta, [PREMERGED_ROOT_MARK]: mark };
104
+ }
105
+ async function mergedFiles(rootDirs, translationDirName, outputDir, additionalTranslationDirs = []) {
106
+ if (existsSync(outputDir)) fs__default.rmSync(outputDir, { recursive: true, force: true });
107
+ mkdirSync(outputDir, { recursive: true });
108
+ const layerPaths = rootDirs.map((dir) => join(dir, translationDirName));
109
+ const filesByLayer = await Promise.all(layerPaths.filter((lp) => existsSync(lp)).map((lp) => globby("**/*.json", { cwd: lp })));
110
+ const merged = /* @__PURE__ */ new Map();
111
+ for (const file of new Set(filesByLayer.flat())) {
112
+ let content = {};
113
+ for (const lp of layerPaths) content = deepMergeTranslationsRecursive(content, readTranslationFile(join(lp, file)));
114
+ merged.set(file, content);
115
+ }
116
+ for (const locale of listAdditionalRootLocales(rootDirs, additionalTranslationDirs))
117
+ if (!merged.has(`${locale}.json`)) merged.set(`${locale}.json`, {});
118
+ for (const file of [...merged.keys()].filter((f) => dirname(f) === "."))
119
+ merged.set(file, mergeAdditionalRootLocaleFiles(rootDirs, additionalTranslationDirs, file.slice(0, -".json".length), merged.get(file)));
120
+ return merged;
121
+ }
122
+ function depthOf(tree) {
123
+ let depth = 0;
124
+ for (let level = [tree]; level.length > 0; depth++)
125
+ level = level.flatMap((node) => node !== null && typeof node === "object" ? Object.values(node) : []);
126
+ return depth - 1;
127
+ }
128
+ async function namingDeepFile(rootDirs, dirNames, step) {
129
+ try {
130
+ await step();
131
+ } catch (error) {
132
+ 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));
134
+ const files = (await Promise.all(dirs.map((cwd) => globby("**/*.json", { cwd, absolute: true })))).flat();
135
+ const [deepest] = files.map((file) => ({ file, depth: depthOf(readTranslationFile(file)) })).sort((a, b) => b.depth - a.depth);
136
+ if (!deepest) throw error;
137
+ throw new Error(`[i18n] ${deepest.file} is nested ${deepest.depth} levels deep, too deep to build. Flatten it.`, { cause: error });
138
+ }
139
+ }
140
+ async function preMergeLocales(options) {
141
+ await namingDeepFile(options.rootDirs, [options.translationDirName, ...options.additionalTranslationDirs ?? []], () => writeChunks(options));
142
+ }
143
+ async function writeChunks(options) {
144
+ const { rootDirs, translationDirName, outputDir, locales, globalFallbackLocale, disablePageLocales, additionalTranslationDirs } = options;
145
+ const { routeNames = [], rootDelta = false } = options;
146
+ const merged = await mergedFiles(rootDirs, translationDirName, outputDir, additionalTranslationDirs);
147
+ const pages = /* @__PURE__ */ new Set([...[...merged.keys()].filter((f) => f.startsWith("pages/")).map((f) => dirname(f).slice("pages/".length)), ...routeNames]);
148
+ const targets = disablePageLocales || pages.size === 0 ? ["index"] : [...pages, ...pages.has("index") ? [] : ["index"]];
149
+ const chunk = (page, locale) => mergeSourceTranslations({
150
+ locale,
151
+ pageName: page,
152
+ locales,
153
+ globalFallbackLocale,
154
+ disablePageLocales,
155
+ readLocaleFile: (relativePath) => merged.get(relativePath) ?? {}
156
+ });
157
+ const write = (page, locale, data) => {
158
+ const targetPath = join(outputDir, page, locale, "data.json");
159
+ mkdirSync(dirname(targetPath), { recursive: true });
160
+ writeFileSync(targetPath, JSON.stringify(data));
161
+ };
162
+ const split = rootDelta && !disablePageLocales;
163
+ await Promise.all(
164
+ locales.map(async ({ code: locale }) => {
165
+ const root = await chunk(PREMERGED_ROOT_PAGE, locale);
166
+ const mark = createHash("sha256").update(JSON.stringify(root)).digest("hex").slice(0, 8);
167
+ write(PREMERGED_ROOT_PAGE, locale, split ? { ...root, [PREMERGED_ROOT_MARK]: mark } : root);
168
+ await Promise.all(
169
+ targets.map(async (page) => {
170
+ const data = await chunk(page, locale);
171
+ write(page, locale, split ? pageFileOverRoot(data, root, mark) : data);
172
+ })
173
+ );
174
+ })
175
+ );
176
+ }
177
+ async function buildTranslationSourceLayers(rootDirs, translationDirName, outputDir, additionalTranslationDirs) {
178
+ await namingDeepFile(rootDirs, [translationDirName, ...additionalTranslationDirs ?? []], async () => {
179
+ for (const [file, content] of await mergedFiles(rootDirs, translationDirName, outputDir, additionalTranslationDirs)) {
180
+ const targetPath = join(outputDir, file);
181
+ mkdirSync(dirname(targetPath), { recursive: true });
182
+ writeFileSync(targetPath, JSON.stringify(content));
183
+ }
184
+ });
185
+ }
186
+
187
+ const DEFAULT_TRANSLATION_PAYLOAD_WARN_FILE_COUNT = 500;
188
+ const DEFAULT_TRANSLATION_PAYLOAD_WARN_SIZE_BYTES = 10 * 1024 * 1024;
189
+ function resolveTranslationPayloadOptions(options) {
190
+ const isSourceMode = options.translationPayloads?.mode === "source";
191
+ return {
192
+ mode: isSourceMode ? "source" : "premerged",
193
+ serverAssets: options.translationPayloads?.serverAssets !== false,
194
+ serverHandler: options.translationPayloads?.serverHandler !== false,
195
+ publicAssets: isSourceMode ? options.translationPayloads?.publicAssets === true : options.translationPayloads?.publicAssets !== false,
196
+ // Off by default: prerendering `/_locales` without a live handler writes empty public stubs.
197
+ // Opt in when you need static `/{apiBaseUrl}/.../data.json` and payloads are already available.
198
+ prerenderRoutes: options.translationPayloads?.prerenderRoutes === true,
199
+ rootDelta: !isSourceMode && options.translationPayloads?.rootDelta !== false
200
+ };
201
+ }
202
+ const pathSegments = (path) => path.split("/").filter(Boolean).join("/");
203
+ function resolveTranslationPayloadPublicRel(options, apiBaseUrl) {
204
+ return pathSegments(options.translationPayloads?.publicDir || apiBaseUrl) || "_locales";
205
+ }
206
+ function resolveTranslationPayloadPublicDir(outputPublicDir, options, apiBaseUrl) {
207
+ return join(outputPublicDir ?? "./dist", resolveTranslationPayloadPublicRel(options, apiBaseUrl));
208
+ }
209
+ function domainPayloadFilter(options) {
210
+ const { mode, root, served, linkLocales = served, fallbackLocale } = options;
211
+ const needed = served.flatMap((locale) => mode === "source" ? buildFallbackLocaleChain(locale.code, linkLocales, fallbackLocale) : [locale.code]);
212
+ const foreign = new Set(linkLocales.map((locale) => locale.code).filter((code) => !needed.includes(code)));
213
+ return (source) => {
214
+ const name = basename(source);
215
+ if (mode === "source") return !name.endsWith(".json") || !foreign.has(name.slice(0, -".json".length));
216
+ return !foreign.has(name) || dirname(source) === root;
217
+ };
218
+ }
219
+ function shouldCopyTranslationPayloadsToPublic(translationPayloads, readsPublicDir) {
220
+ if (translationPayloads.publicAssets) return true;
221
+ return readsPublicDir && translationPayloads.serverAssets;
222
+ }
223
+ function shouldReadPayloadsFromPublicDir(nitroOptions) {
224
+ const { node, serveStatic, output } = nitroOptions;
225
+ return node && !!serveStatic && serveStatic !== "inline" && relative(output.serverDir, output.publicDir) === join("..", "public");
226
+ }
227
+ function shouldRegisterNitroServerAssets(translationPayloads, readsPublicDir) {
228
+ return !readsPublicDir && translationPayloads.serverAssets;
229
+ }
230
+ function hasLocalTranslationPayloadOutput(translationPayloads) {
231
+ return translationPayloads.serverAssets || translationPayloads.serverHandler || translationPayloads.publicAssets || translationPayloads.prerenderRoutes;
232
+ }
233
+ function getTranslationPayloadMisconfigurationWarnings(input) {
234
+ const warnings = [];
235
+ if (!input.translationPayloads.serverAssets && !input.translationPayloads.publicAssets && !input.apiBaseServerHost) {
236
+ warnings.push(
237
+ "translationPayloads.serverAssets and publicAssets are false and apiBaseServerHost is not set. SSR will load empty translations unless you provide an external server payload host."
238
+ );
239
+ }
240
+ if (hasLocalTranslationPayloadOutput(input.translationPayloads)) {
241
+ return warnings;
242
+ }
243
+ if (!input.apiBaseServerHost) {
244
+ warnings.push(
245
+ "translationPayloads disabled all local outputs but apiBaseServerHost is not set. SSR will load empty translations unless you provide an external server payload host."
246
+ );
247
+ }
248
+ if (!input.apiBaseClientHost) {
249
+ warnings.push(
250
+ "translationPayloads disabled all local outputs but apiBaseClientHost is not set. Client-side navigation may load empty translations unless you provide an external client payload host."
251
+ );
252
+ }
253
+ return warnings;
254
+ }
255
+ function getStaticPayloadWarning(input) {
256
+ const { mode, publicAssets, prerenderRoutes } = input.translationPayloads;
257
+ if (input.apiBaseClientHost || prerenderRoutes || mode === "premerged" && publicAssets) return null;
258
+ return `Static output has no server route, and this build writes no /_locales/<page>/<locale>/data.json (mode: '${mode}', publicAssets: ${publicAssets}): the browser gets a 404 and raw keys, unless something else serves that path. Use the default premerged mode with public assets, translationPayloads.prerenderRoutes, or apiBaseClientHost.`;
259
+ }
260
+ function formatBytes(bytes) {
261
+ if (bytes >= 1024 * 1024) {
262
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
263
+ }
264
+ if (bytes >= 1024) {
265
+ return `${(bytes / 1024).toFixed(1)} KB`;
266
+ }
267
+ return `${bytes} B`;
268
+ }
269
+ function getTranslationPayloadSizeWarning(stats, thresholds) {
270
+ const warnFileCount = thresholds?.warnFileCount ?? DEFAULT_TRANSLATION_PAYLOAD_WARN_FILE_COUNT;
271
+ const warnSizeBytes = thresholds?.warnSizeBytes ?? DEFAULT_TRANSLATION_PAYLOAD_WARN_SIZE_BYTES;
272
+ if (stats.fileCount < warnFileCount && stats.totalBytes < warnSizeBytes) {
273
+ return null;
274
+ }
275
+ 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
+ }
277
+
278
+ function collectJsonFiles(dir) {
279
+ return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
280
+ const fullPath = join(dir, entry.name);
281
+ if (entry.isDirectory()) return collectJsonFiles(fullPath);
282
+ return entry.name.endsWith(".json") ? [fullPath] : [];
283
+ });
284
+ }
285
+ function scanTranslationPayloadDirectory(dir) {
286
+ const stats = { fileCount: 0, totalBytes: 0 };
287
+ if (!existsSync(dir)) return stats;
288
+ for (const file of collectJsonFiles(dir)) {
289
+ stats.fileCount += 1;
290
+ stats.totalBytes += statSync(file).size;
291
+ }
292
+ return stats;
293
+ }
294
+ const MiB = 1024 * 1024;
295
+ const EMBED_LIMITS = [
296
+ {
297
+ preset: (name) => name === "vercel-edge",
298
+ bytes: 1e6,
299
+ compressed: true,
300
+ text: "Vercel Edge allows 1 MB gzip on Hobby, 2 MB on Pro and 4 MB on Enterprise"
301
+ },
302
+ { preset: (name) => name === "netlify-edge", bytes: 2e7, compressed: true, text: "Netlify Edge allows 20 MB compressed" },
303
+ { preset: (name) => name.startsWith("cloudflare-"), bytes: 64 * MiB, compressed: false, text: "Cloudflare Workers allow 64 MiB uncompressed" }
304
+ ];
305
+ function getEmbeddedPayloadSizeWarning(preset, dir, mode) {
306
+ const limit = EMBED_LIMITS.find((entry) => entry.preset(preset));
307
+ if (!limit) return null;
308
+ const { totalBytes } = scanTranslationPayloadDirectory(dir);
309
+ if (totalBytes <= limit.bytes) return null;
310
+ if (limit.compressed) {
311
+ const gzipBytes = collectJsonFiles(dir).reduce((sum, file) => sum + gzipSync(readFileSync(file)).length, 0);
312
+ if (gzipBytes <= limit.bytes) return null;
313
+ return embeddedSizeMessage(preset, `${(gzipBytes / 1e6).toFixed(1)} MB gzip`, limit.text, mode);
314
+ }
315
+ return embeddedSizeMessage(preset, `${(totalBytes / MiB).toFixed(1)} MiB`, limit.text, mode);
316
+ }
317
+ function embeddedSizeMessage(preset, size, limitText, mode) {
318
+ const advice = mode === "premerged" ? "Use translationPayloads.mode: 'source'" : "Load dictionaries from apiBaseServerHost";
319
+ return `Preset "${preset}" embeds ${size} of dictionaries in the server bundle, and ${limitText}. ${advice}, or the deploy may be refused.`;
320
+ }
321
+ function hashTranslationSources(rootDirs, translationDirName, additionalTranslationDirs = [], salt = "") {
322
+ const hash = createHash("sha256").update(salt);
323
+ let seen = 0;
324
+ const hashFiles = (baseDir, files) => {
325
+ files.sort();
326
+ for (const file of files) {
327
+ hash.update(relative(baseDir, file));
328
+ hash.update("\0");
329
+ hash.update(readFileSync(file));
330
+ seen += 1;
331
+ }
332
+ };
333
+ const hashDir = (dir) => {
334
+ if (!existsSync(dir)) return;
335
+ hashFiles(dir, collectJsonFiles(dir));
336
+ };
337
+ const hashAdditionalRootLocales = (dir) => {
338
+ if (!existsSync(dir)) return;
339
+ const files = [];
340
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
341
+ if (entry.isFile() && entry.name.endsWith(".json")) files.push(join(dir, entry.name));
342
+ }
343
+ hashFiles(dir, files);
344
+ };
345
+ for (const rootDir of rootDirs) {
346
+ hashDir(join(rootDir, translationDirName));
347
+ for (const extra of additionalTranslationDirs) {
348
+ hashAdditionalRootLocales(join(rootDir, extra));
349
+ }
350
+ }
351
+ return seen > 0 ? hash.digest("hex").slice(0, 16) : null;
352
+ }
353
+ const NITRO_MIN_COMPRESS_BYTES = 1024;
354
+ function compressTranslationPayloads(dir, compression) {
355
+ if (!compression || !existsSync(dir)) return 0;
356
+ const gzip = compression === true || compression.gzip !== false;
357
+ const brotli = compression === true || compression.brotli !== false;
358
+ if (!gzip && !brotli) return 0;
359
+ let compressed = 0;
360
+ for (const file of collectJsonFiles(dir)) {
361
+ const raw = readFileSync(file);
362
+ if (raw.length < NITRO_MIN_COMPRESS_BYTES) continue;
363
+ if (gzip) writeFileSync(`${file}.gz`, gzipSync(raw, { level: 9 }));
364
+ if (brotli) {
365
+ writeFileSync(
366
+ `${file}.br`,
367
+ brotliCompressSync(raw, { params: { [constants.BROTLI_PARAM_QUALITY]: 11, [constants.BROTLI_PARAM_SIZE_HINT]: raw.length } })
368
+ );
369
+ }
370
+ compressed += 1;
371
+ }
372
+ return compressed;
373
+ }
374
+
22
375
  function buildI18nPluralTemplate(plural, helpersImportPath) {
23
376
  if (!plural || plural === defaultPlural) {
24
377
  return `export { defaultPlural as plural } from '${helpersImportPath}'
@@ -90,45 +443,30 @@ function setupDevToolsUI(options, rootDirs) {
90
443
  }
91
444
  });
92
445
  onDevToolsInitialized(() => {
93
- const dirs = rootDirs.length > 0 ? rootDirs : [nuxt.options.rootDir];
94
- extendServerRpc(DEVTOOLS_RPC_NAMESPACE, {
446
+ const functions = {
447
+ // Writes only an existing `.json` inside a translation directory: `file` comes from the
448
+ // client, so `../` and absolute paths must not reach the filesystem. The project layer (listed
449
+ // last, shown in the tab) wins over extended layers.
95
450
  async saveTranslationContent(file, content) {
96
- let filePath = null;
97
- const additionalMatch = /^additional:([^/]+)\/(.+)$/.exec(file);
98
- if (additionalMatch?.[1] && additionalMatch[2]) {
99
- const extraDir = additionalMatch[1];
100
- const relFile = additionalMatch[2];
101
- for (const rootDir of dirs) {
102
- const candidatePath = path.join(rootDir, extraDir, relFile);
103
- if (fs.existsSync(candidatePath)) {
104
- filePath = candidatePath;
105
- break;
106
- }
107
- }
108
- } else {
109
- for (const rootDir of dirs) {
110
- const localesDir = path.join(rootDir, options.translationDir || "locales");
111
- const candidatePath = path.join(localesDir, file);
112
- if (fs.existsSync(candidatePath)) {
113
- filePath = candidatePath;
114
- break;
115
- }
116
- }
117
- }
118
- if (!filePath) {
119
- filePath = path.resolve(file);
120
- }
121
- if (fs.existsSync(filePath)) {
122
- fs.writeFileSync(filePath, JSON.stringify(content, null, 2), "utf-8");
123
- } else {
124
- throw new Error(`File not found: ${filePath}`);
125
- }
451
+ const extraDir = file.startsWith("additional:") ? (options.additionalTranslationDirs ?? []).find((dir) => file.startsWith(`additional:${dir}/`)) : void 0;
452
+ if (file.startsWith("additional:") && !extraDir) throw new Error(`Not a configured additional translation directory: ${file}`);
453
+ 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
+ if (!target) throw new Error(`File not found in the translation directories: ${file}`);
456
+ fs.writeFileSync(target, JSON.stringify(content, null, 2), "utf-8");
126
457
  },
127
458
  async getConfigs() {
128
459
  return Promise.resolve(options);
129
460
  },
130
461
  async getLocalesAndTranslations() {
131
462
  const filesList = {};
463
+ const readFile = (key, filePath) => {
464
+ try {
465
+ filesList[key] = JSON.parse(fs.readFileSync(filePath, "utf-8"));
466
+ } catch (e) {
467
+ console.error(`Error parsing locale file ${filePath}:`, e);
468
+ }
469
+ };
132
470
  const processDirectory = (dir, baseDir) => {
133
471
  if (!fs.existsSync(dir)) return;
134
472
  fs.readdirSync(dir).forEach((file) => {
@@ -137,16 +475,11 @@ function setupDevToolsUI(options, rootDirs) {
137
475
  if (stat.isDirectory()) {
138
476
  processDirectory(filePath, baseDir);
139
477
  } else if (file.endsWith(".json")) {
140
- try {
141
- const relativePath = path.relative(baseDir, filePath).replace(/\\/g, "/");
142
- filesList[relativePath] = JSON.parse(fs.readFileSync(filePath, "utf-8"));
143
- } catch (e) {
144
- console.error(`Error parsing locale file ${filePath}:`, e);
145
- }
478
+ readFile(forwardSlashes(path.relative(baseDir, filePath)), filePath);
146
479
  }
147
480
  });
148
481
  };
149
- for (const rootDir of dirs) {
482
+ for (const rootDir of rootDirs) {
150
483
  const localesDir = path.join(rootDir, options.translationDir || "locales");
151
484
  const pagesDir = path.join(localesDir, "pages");
152
485
  processDirectory(localesDir, localesDir);
@@ -158,35 +491,147 @@ function setupDevToolsUI(options, rootDirs) {
158
491
  if (!name.endsWith(".json")) continue;
159
492
  const filePath = path.join(extraDir, name);
160
493
  if (!fs.statSync(filePath).isFile()) continue;
161
- try {
162
- filesList[`additional:${extra}/${name}`] = JSON.parse(fs.readFileSync(filePath, "utf-8"));
163
- } catch (e) {
164
- console.error(`Error parsing locale file ${filePath}:`, e);
165
- }
494
+ readFile(`additional:${extra}/${name}`, filePath);
166
495
  }
167
496
  }
168
497
  }
169
498
  return filesList;
170
499
  }
500
+ };
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)));
504
+ let timer;
505
+ nuxt.hook("builder:watch", (_event, watched) => {
506
+ const file = resolve(nuxt.options.srcDir, watched);
507
+ if (!file.endsWith(".json") || !roots.some((root) => isInsideDir(file, root))) return;
508
+ clearTimeout(timer);
509
+ timer = setTimeout(() => {
510
+ functions.getLocalesAndTranslations().then((locales) => rpc.broadcast.localesUpdated.asEvent(locales)).catch((error) => console.error("[i18n-devtools] Failed to push locales:", error));
511
+ }, 100);
171
512
  });
172
513
  });
173
514
  }
174
515
 
175
- function shouldLocalizeRouteRulePath(originalPath) {
516
+ function shouldLocalizeRouteRulePath(originalPath, localeCodes = []) {
176
517
  const routeRulePath = originalPath.trim();
177
518
  if (!routeRulePath) return true;
178
519
  const path = routeRulePath.startsWith("/") ? routeRulePath : `/${routeRulePath}`;
179
520
  if (path === "/api" || path.startsWith("/api/")) return false;
180
- const firstSegment = path.replace(/^\/+/, "").split("/")[0] ?? "";
181
- return !firstSegment.startsWith("_");
521
+ const firstSegment = trimLeadingSlashes(path).split("/")[0] ?? "";
522
+ return !firstSegment.startsWith("_") && !localeCodes.includes(firstSegment);
523
+ }
524
+ function localizedRulePaths(path, generator) {
525
+ return generator.locales.filter(({ code }) => isLocaleAllowedForUnlocalizedRoute(generator.routeLocales, generator.locales, path, code)).flatMap(({ code }) => generator.resolveLocalizedRulePaths(path, code)).filter((localized) => localized !== path && localized !== normalizePath(path));
526
+ }
527
+ function localizeRouteRules(rules, target, generator, onCopy, noDocuments = false) {
528
+ const localeCodes = [...generator.locales, ...generator.disabledLocales].map((locale) => locale.code);
529
+ for (const [path, value] of Object.entries(rules)) {
530
+ const { redirect: _redirect, ...rule } = value;
531
+ if (noDocuments) delete rule.prerender;
532
+ if (!Object.keys(rule).length || !shouldLocalizeRouteRulePath(path, localeCodes)) continue;
533
+ for (const localized of localizedRulePaths(path, generator)) {
534
+ target[localized] = { ...target[localized], ...rule };
535
+ onCopy(localized, rule);
536
+ }
537
+ }
538
+ }
539
+ function varyRouteCacheByHost(rule, trustForwardedHost) {
540
+ if (rule.cache === false || !(rule.swr || rule.cache)) return rule;
541
+ const hosts = trustForwardedHost ? ["host", "x-forwarded-host"] : ["host"];
542
+ return { ...rule, cache: { ...rule.cache, varies: [.../* @__PURE__ */ new Set([...rule.cache?.varies ?? [], ...hosts])] } };
182
543
  }
183
- const PAYLOAD_ROUTE_CACHE_MAX_AGE = 60;
184
544
  function buildTranslationPayloadRouteRule(input) {
185
- if (input.dev) return { cors: true };
545
+ if (input.dev || !input.cacheControl) return { cors: true };
546
+ return { cors: true, headers: { "Cache-Control": input.cacheControl } };
547
+ }
548
+
549
+ function staticPagePaths(pages, parent = "/") {
550
+ return pages.flatMap((page) => {
551
+ if (page.redirect || page.path.includes(":")) return [];
552
+ const path = joinURL(parent, page.path);
553
+ return [path, ...staticPagePaths(page.children ?? [], path)];
554
+ });
555
+ }
556
+ function unfixedOriginWarning(locales, defaultLocale, metaBaseUrl, siteUrl) {
557
+ if (metaBaseUrl || siteUrl || !locales.some((locale) => locale.baseUrl)) return null;
558
+ if (locales.find((locale) => locale.code === defaultLocale)?.baseUrl) return null;
559
+ return "locales[].baseUrl is set, but no public origin is fixed: an unknown host reaches canonical and hreflang from the request. Set metaBaseUrl, site.url, or a baseUrl on the default locale.";
560
+ }
561
+ function seedStaticRoutes(routes, staticPaths, options) {
562
+ const { localize, prerendered } = options;
563
+ if (prerendered("/")) routes.add("/");
564
+ for (const path of staticPaths) {
565
+ if (!prerendered(path)) continue;
566
+ if (localize.isServed(path)) routes.add(path);
567
+ addLocalized(routes, path, options);
568
+ }
569
+ addLocalized(routes, "/", options);
570
+ }
571
+ function addLocalized(routes, path, { localize, locales, prerendered }) {
572
+ for (const { code } of locales) {
573
+ const localized = localize.path(path, code);
574
+ if (localized && prerendered(localized)) routes.add(localized);
575
+ }
576
+ }
577
+ function dropUnprefixed(routes, localize) {
578
+ const removed = [...routes].filter(
579
+ (route) => route !== "/" && route !== "" && !localize.localeOf(route) && !localize.isUnlocalized(route) && localize.isPage(route)
580
+ );
581
+ for (const route of removed) routes.delete(route);
582
+ return removed;
583
+ }
584
+ function seedPrerenderRoutes(routes, options) {
585
+ const { localize, staticPaths, prerendered, isInternal } = options;
586
+ if (staticPaths) seedStaticRoutes(routes, staticPaths, options);
587
+ for (const route of routes) if (isInternal(route)) routes.delete(route);
588
+ const additional = /* @__PURE__ */ new Set();
589
+ for (const route of routes) {
590
+ if (/\.[a-z0-9]+$/i.test(route) || !prerendered(route) || localize.localeOf(route)) continue;
591
+ addLocalized(additional, route, options);
592
+ }
593
+ for (const route of additional) routes.add(route);
594
+ const { strategy, locales } = options;
595
+ const prefixedOnly = strategy === "prefix" || strategy === "prefix_and_default" && !locales.some((locale) => locale.baseDefault);
596
+ return { added: [...additional], removed: prefixedOnly ? dropUnprefixed(routes, localize) : [] };
597
+ }
598
+ const toRecord = (page) => ({
599
+ path: page.path,
600
+ name: page.name,
601
+ meta: page.meta,
602
+ ...page.redirect ? { redirect: page.redirect } : { component: {} },
603
+ ...page.alias?.length ? { alias: page.alias } : {},
604
+ ...page.children?.length ? { children: page.children.map(toRecord) } : {}
605
+ });
606
+ async function createLocalizedPathResolver(pages, ctx) {
607
+ const { createMemoryHistory, createRouter } = await import('vue-router');
608
+ const router = createRouter({ history: createMemoryHistory(), routes: pages.map(toRecord) });
609
+ const strategy = ctx.locales.some((locale) => locale.baseUrl) ? new DomainPathStrategy({ ...ctx, router }) : createPathStrategy({ ...ctx, router });
610
+ const seen = /* @__PURE__ */ new Map();
611
+ const matched = (path) => {
612
+ let records = seen.get(path);
613
+ if (records) return records;
614
+ records = strategy.probe(path)?.matched ?? [];
615
+ seen.set(path, records);
616
+ return records;
617
+ };
618
+ const isPage = (path) => matched(path).length > 0 || strategy.pageLocales({ path }) !== null;
619
+ const isUnlocalized = (path) => {
620
+ const records = matched(path);
621
+ return records.length > 0 && !records.at(-1).meta?.i18n;
622
+ };
186
623
  return {
187
- cors: true,
188
- ...input.hasCacheBuster ? { cache: { maxAge: PAYLOAD_ROUTE_CACHE_MAX_AGE, swr: true } } : {},
189
- ...input.cacheControl ? { headers: { "Cache-Control": input.cacheControl } } : {}
624
+ path(path, locale) {
625
+ const served = strategy.pageLocales({ path });
626
+ if (!isPage(path) || served && !served.includes(locale) || isUnlocalized(path)) return null;
627
+ const target = strategy.localeRoute(locale, path).fullPath;
628
+ return target && target !== path && !target.startsWith("https:") && !target.startsWith("http:") ? target : null;
629
+ },
630
+ localeOf: (path) => strategy.getLocaleFromPath(path),
631
+ isUnlocalized,
632
+ isPage,
633
+ // a known domain also leaves out what it answers with a 404 or a redirect (another host's URL)
634
+ isServed: (path) => matched(path).length > 0 && (!ctx.domain || !strategy.shouldReturn404(path) && !strategy.getClientRedirect(path, ctx.defaultLocale))
190
635
  };
191
636
  }
192
637
 
@@ -365,9 +810,12 @@ function evaluateConfig(scriptContent, configStr) {
365
810
  const safeScript = `
366
811
  const $defineI18nRoute = () => {}
367
812
  const defineI18nRoute = () => {}
813
+ const useNuxtApp = () => ({ $defineI18nRoute })
368
814
  const process = { env: { NODE_ENV: 'development' } }
815
+ {
369
816
  ${removeTypeScriptTypes(scriptWithoutImports)}
370
817
  return (${cleanConfigStr})
818
+ }
371
819
  `;
372
820
  try {
373
821
  return { ok: true, config: toPlainConfig(Function(`"use strict";${safeScript}`)()) };
@@ -438,54 +886,48 @@ function extractDefineI18nRouteData(content, filePath) {
438
886
  return null;
439
887
  }
440
888
  }
889
+ const isSeparator = (char) => char === "/" || char === "\\";
890
+ const isIndexPage = (file) => file.endsWith("/index.vue") || file.endsWith("\\index.vue");
891
+ function parentPagePaths(pageFiles, rootDirs) {
892
+ const candidates = pageFiles.filter((file) => !isIndexPage(file));
893
+ return candidates.map((file) => pageFilePathToRoutePath(file, resolveRootDir(file, rootDirs) ?? ""));
894
+ }
895
+ function resolveRootDir(filePath, rootDirs) {
896
+ const beneath = (root) => filePath.startsWith(root) && (isSeparator(root[root.length - 1]) || isSeparator(filePath[root.length]));
897
+ return [...rootDirs].sort((a, b) => b.length - a.length).find(beneath);
898
+ }
441
899
  function pageFilePathToRoutePath(pageFile, rootDir) {
442
- const relative = pageFile.replace(rootDir, "").replace(/^[/\\]+/, "");
443
- const withoutPagesPrefix = relative.replace(/^(app[/\\])?pages[/\\]/, "");
900
+ const withoutRoot = pageFile.replace(rootDir, "");
901
+ let start = 0;
902
+ while (isSeparator(withoutRoot[start])) start++;
903
+ const relative = withoutRoot.slice(start);
904
+ const pagesAt = relative.startsWith("app/") || relative.startsWith("app\\") ? 4 : 0;
905
+ const inPages = relative.startsWith("pages", pagesAt) && isSeparator(relative[pagesAt + 5]);
906
+ const withoutPagesPrefix = inPages ? relative.slice(pagesAt + 6) : relative;
444
907
  if (withoutPagesPrefix === "index.vue") {
445
908
  return "/";
446
909
  }
447
- const raw = withoutPagesPrefix.replace(/[/\\]index\.vue$/, "").replace(/\.vue$/, "").replace(/[/\\]$/, "").replace(/\\/g, "/");
910
+ let page = isIndexPage(withoutPagesPrefix) ? withoutPagesPrefix.slice(0, -10) : withoutPagesPrefix;
911
+ if (page.endsWith(".vue")) page = page.slice(0, -4);
912
+ if (isSeparator(page[page.length - 1])) page = page.slice(0, -1);
913
+ const raw = forwardSlashes(page).split("/").filter((segment) => !(segment.length > 2 && segment[0] === "(" && segment.endsWith(")"))).join("/");
448
914
  return raw === "" ? "/" : raw;
449
915
  }
450
916
 
451
- function resolveRootDir(filePath, rootDirs) {
452
- const sorted = [...rootDirs].sort((a, b) => b.length - a.length);
453
- return sorted.find((root) => filePath.startsWith(root));
454
- }
455
- function createDefineI18nRoutePlugin(options) {
456
- const metaByFile = /* @__PURE__ */ new Map();
457
- const writeMetaFile = () => {
458
- const entries = [...metaByFile.values()].filter((entry) => entry !== null);
459
- options.onMetaUpdate?.(entries);
460
- const metaPath = join(options.buildDir, "i18n-route-meta.json");
461
- mkdirSync(dirname(metaPath), { recursive: true });
462
- writeFileSync(metaPath, JSON.stringify(entries, null, 2));
463
- };
464
- return createUnplugin(() => ({
465
- name: "nuxt-i18n-micro-define-route",
466
- enforce: "pre",
467
- transformInclude(id) {
468
- return id.endsWith(".vue") && !id.includes("node_modules");
469
- },
470
- transform(code, id) {
471
- const rootDir = resolveRootDir(id, options.rootDirs);
472
- if (!rootDir) return null;
473
- const config = extractDefineI18nRouteData(code, id);
474
- if (!config) {
475
- metaByFile.delete(id);
476
- } else {
477
- metaByFile.set(id, {
478
- routePath: pageFilePathToRoutePath(id, rootDir),
479
- config
480
- });
481
- }
482
- writeMetaFile();
483
- return null;
484
- },
485
- buildEnd() {
486
- writeMetaFile();
487
- }
488
- }));
917
+ function mergeDefineI18nRouteMeta(entries, config) {
918
+ const routeLocales = { ...config.routeLocales };
919
+ const routeDisableMeta = { ...config.routeDisableMeta };
920
+ const fromPages = {};
921
+ for (const { routePath, config: page } of entries) {
922
+ if (Array.isArray(page.locales)) routeLocales[routePath] = page.locales;
923
+ else if (page.locales) routeLocales[routePath] = Object.keys(page.locales);
924
+ if (page.localeRoutes) fromPages[routePath] = page.localeRoutes;
925
+ if (page.disableMeta !== void 0) routeDisableMeta[routePath] = page.disableMeta;
926
+ }
927
+ const configured = config.globalLocaleRoutes ?? {};
928
+ const configuredKeys = new Set(Object.keys(configured).map(routeKey));
929
+ for (const key of Object.keys(fromPages)) if (configuredKeys.has(routeKey(key)) || configuredKeys.has(nameOfPath(key))) delete fromPages[key];
930
+ return { routeLocales, globalLocaleRoutes: { ...fromPages, ...configured }, routeDisableMeta };
489
931
  }
490
932
  function collectDefineI18nRouteMetaFromFiles(pageFiles, rootDirs) {
491
933
  const entries = [];
@@ -506,46 +948,19 @@ function collectDefineI18nRouteMetaFromFiles(pageFiles, rootDirs) {
506
948
  return entries;
507
949
  }
508
950
 
509
- const strategyFiles = {
510
- no_prefix: "no-prefix-strategy.mjs",
511
- prefix: "prefix-strategy.mjs",
512
- prefix_except_default: "prefix-except-default-strategy.mjs",
513
- prefix_and_default: "prefix-and-default-strategy.mjs"
514
- };
515
- async function resolveStrategyPath(strategy) {
516
- const strategyFile = strategyFiles[strategy] ?? strategyFiles.prefix_except_default;
517
- const specifier = `@i18n-micro/path-strategy/dist/${strategyFile}`;
518
- let absoluteStrategyPath;
519
- try {
520
- absoluteStrategyPath = fileURLToPath(import.meta.resolve(specifier));
521
- } catch {
522
- absoluteStrategyPath = await resolvePath(specifier);
523
- }
524
- return process.platform === "win32" ? pathToFileURL(absoluteStrategyPath).href : absoluteStrategyPath.replace(/\\/g, "/");
525
- }
526
- async function resolveHelpersPath() {
527
- const specifier = "@i18n-micro/core/helpers";
528
- let absoluteHelpersPath;
951
+ async function resolveEntry(specifier) {
952
+ let absolutePath;
529
953
  try {
530
- absoluteHelpersPath = fileURLToPath(import.meta.resolve(specifier));
954
+ absolutePath = fileURLToPath(import.meta.resolve(specifier));
531
955
  } catch {
532
- absoluteHelpersPath = await resolvePath(specifier);
956
+ absolutePath = await resolvePath(specifier);
533
957
  }
534
- return process.platform === "win32" ? pathToFileURL(absoluteHelpersPath).href : absoluteHelpersPath.replace(/\\/g, "/");
958
+ return forwardSlashes(absolutePath);
535
959
  }
536
960
  function generateI18nTypes() {
537
961
  return `
538
962
  import type { PluginsInjections } from "nuxt-i18n-micro";
539
963
 
540
- declare module 'vue/types/vue' {
541
- interface Vue extends PluginsInjections { }
542
- }
543
-
544
- declare module '@nuxt/types' {
545
- interface NuxtAppOptions extends PluginsInjections { }
546
- interface Context extends PluginsInjections { }
547
- }
548
-
549
964
  declare module '#app' {
550
965
  interface NuxtApp extends PluginsInjections { }
551
966
  }
@@ -553,87 +968,72 @@ declare module '#app' {
553
968
  export {}`;
554
969
  }
555
970
  const DEFAULT_CANONICAL_QUERY_WHITELIST = ["page", "sort", "filter", "search", "q", "query", "tag"];
556
- function resolveDateBuild(options, translationsHash) {
557
- return options.dateBuild ?? translationsHash ?? Date.now();
971
+ function isLiveDictionary(options, mode, serverHost) {
972
+ return mode === "source" || Boolean(options.cacheTtl) || Boolean(serverHost);
558
973
  }
559
974
  function buildFullConfig(params) {
560
- const {
561
- options,
562
- nuxt,
563
- defaultLocale,
564
- isSSG,
565
- apiConfig,
566
- translationPayloadMode,
567
- routeLocales,
568
- routeDisableMeta,
569
- mergedGlobalLocaleRoutes,
570
- locales,
571
- translationsHash
572
- } = params;
573
- const dateBuild = resolveDateBuild(options, translationsHash);
975
+ const { options, nuxt, defaultLocale, apiConfig, translationPayloadMode, locales, translationsHash, publicPayloads, rootDelta } = params;
976
+ const noScripts = nuxt.options.features.noScripts;
574
977
  return {
575
978
  locales: locales ?? [],
576
- metaBaseUrl: options.metaBaseUrl || void 0,
577
- metaTrustForwardedHost: options.metaTrustForwardedHost ?? true,
578
- metaTrustForwardedProto: options.metaTrustForwardedProto ?? true,
579
- hreflangBaseLanguage: options.hreflangBaseLanguage ?? false,
979
+ // an unknown host of a build whose default has a domain takes that origin, not the request's
980
+ metaBaseUrl: options.metaBaseUrl || locales?.find((locale) => locale.code === defaultLocale)?.baseUrl || void 0,
981
+ metaTrustForwardedHost: options.metaTrustForwardedHost,
982
+ metaTrustForwardedProto: options.metaTrustForwardedProto,
983
+ trustForwardedHost: options.trustForwardedHost,
984
+ hreflangBaseLanguage: options.hreflangBaseLanguage,
580
985
  defaultLocale,
581
986
  fallbackLocale: options.fallbackLocale ?? void 0,
582
987
  localeCookie: options.localeCookie ?? null,
583
- autoDetectLanguage: options.autoDetectLanguage ?? true,
584
- autoDetectPath: options.autoDetectPath ?? "/",
585
- strategy: options.strategy ?? "prefix_except_default",
586
- dateBuild,
988
+ autoDetectLanguage: options.autoDetectLanguage,
989
+ autoDetectPath: options.autoDetectPath || "/",
990
+ 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(),
587
994
  hashMode: nuxt.options?.router?.options?.hashMode ?? false,
588
995
  trailingSlash: options.trailingSlash ?? nuxt.options.experimental?.defaults?.nuxtLink?.trailingSlash,
589
996
  apiBaseUrl: apiConfig.apiBaseUrl,
590
997
  apiBaseClientHost: apiConfig.apiBaseClientHost,
591
- isSSG,
592
- disablePageLocales: options.disablePageLocales ?? false,
998
+ disablePageLocales: options.disablePageLocales,
593
999
  canonicalQueryWhitelist: options.canonicalQueryWhitelist ?? DEFAULT_CANONICAL_QUERY_WHITELIST,
594
1000
  excludePatterns: options.excludePatterns ?? [],
595
- routeLocales,
596
- routeDisableMeta,
597
- globalLocaleRoutes: mergedGlobalLocaleRoutes,
598
- missingWarn: options.missingWarn ?? true,
1001
+ missingWarn: options.missingWarn,
599
1002
  redirects: options.redirects !== false,
600
- hooks: options.hooks !== false,
601
- hmr: options.hmr ?? true,
602
- localizedRouteNamePrefix: options.localizedRouteNamePrefix ?? "localized-",
1003
+ prefixCaseRedirect: options.prefixCaseRedirect !== false,
1004
+ localizedRouteNamePrefix: options.localizedRouteNamePrefix,
603
1005
  routesLocaleLinks: options.routesLocaleLinks ?? {},
604
- noPrefixRedirect: options.noPrefixRedirect ?? false,
605
- debug: options.debug ?? false,
1006
+ debug: options.debug,
606
1007
  customRegexMatcher: options.customRegexMatcher instanceof RegExp ? options.customRegexMatcher.source : options.customRegexMatcher,
607
1008
  cacheMaxSize: options.cacheMaxSize ?? 0,
608
1009
  cacheTtl: options.cacheTtl ?? 0,
609
1010
  httpCacheDuration: options.httpCacheDuration,
1011
+ httpCacheControl: options.httpCacheControl,
610
1012
  numberFormats: options.numberFormats ?? {},
611
1013
  datetimeFormats: options.datetimeFormats ?? {},
612
- translationPayloadMode
1014
+ timeZone: options.timeZone,
1015
+ translationPayloadMode,
1016
+ publicPayloads,
1017
+ rootDelta,
1018
+ noScripts: noScripts === "all" || !!noScripts && !nuxt.options.dev || void 0
613
1019
  };
614
1020
  }
615
- function buildPrivateConfig(options, nuxt, apiConfig, locales, payloadFsDir, payloadPublicRel) {
1021
+ function buildPrivateConfig(options, nuxt, rootDirs, apiConfig, locales, payloadFsDir, payloadPublicRel) {
616
1022
  return {
617
- rootDir: nuxt.options.rootDir,
618
- // Same order as build translation merge: extends first, project last (wins).
619
- rootDirs: nuxt.options._layers.map((layer) => layer.config.rootDir).reverse(),
1023
+ rootDirs,
620
1024
  debug: options.debug ?? false,
621
1025
  locales: locales ?? [],
622
- fallbackLocale: options.fallbackLocale ?? void 0,
623
1026
  translationDir: options.translationDir ?? "locales",
624
1027
  additionalTranslationDirs: options.additionalTranslationDirs ?? [],
625
- customRegexMatcher: options.customRegexMatcher instanceof RegExp ? options.customRegexMatcher.source : options.customRegexMatcher,
626
- routesLocaleLinks: options.routesLocaleLinks ?? {},
627
- apiBaseUrl: apiConfig.apiBaseUrl,
628
- apiBaseClientHost: apiConfig.apiBaseClientHost,
629
1028
  apiBaseServerHost: apiConfig.apiBaseServerHost,
630
1029
  serverTranslationPreload: options.serverTranslationPreload ?? false,
1030
+ cacheStorage: options.cacheStorage,
1031
+ cacheStorageBuild: nuxt.options.buildId,
631
1032
  payloadFsDir,
632
1033
  payloadPublicRel
633
1034
  };
634
1035
  }
635
- function registerI18nTemplates(options, resolvedStrategyPath, resolvedHelpersPath, fullConfig, privateConfig) {
636
- let runtimeFullConfig = fullConfig;
1036
+ function registerI18nTemplates(options, resolvedStrategyPath, resolvedHelpersPath, fullConfig, privateConfig, disabledLocales, dev, serverPathFallback) {
637
1037
  const pluralTemplate = addTemplate({
638
1038
  filename: "i18n.plural.mjs",
639
1039
  write: true,
@@ -643,42 +1043,42 @@ function registerI18nTemplates(options, resolvedStrategyPath, resolvedHelpersPat
643
1043
  filename: "i18n.strategy.mjs",
644
1044
  write: true,
645
1045
  getContents: () => {
646
- const fullConfigJson = JSON.stringify(runtimeFullConfig);
647
- return `import { Strategy } from '${resolvedStrategyPath}'
1046
+ const domains = fullConfig.locales.some((locale) => locale.baseUrl);
1047
+ return `import { ${domains ? "DomainPathStrategy" : "I18nPathStrategy"} as Strategy } from '${resolvedStrategyPath}'
648
1048
 
649
- const __fullConfig = ${fullConfigJson}
1049
+ const __fullConfig = ${buildFullConfigSource(fullConfig)}
1050
+ // disabled at build: no routes and not in the config, but routing must answer for their prefix
1051
+ const __disabledLocales = ${JSON.stringify(disabledLocales)}
650
1052
 
651
1053
  export function getI18nConfig() { return __fullConfig }
652
1054
 
653
- export function createI18nStrategy(router) {
654
- const routerAdapter = {
655
- hasRoute(name) { return router.hasRoute(name) },
656
- resolve(to) {
657
- const r = router.resolve(to)
658
- return {
659
- name: r.name != null ? String(r.name) : null,
660
- path: r.path,
661
- fullPath: r.fullPath,
662
- params: r.params || {},
663
- query: r.query || {},
664
- hash: r.hash || '',
665
- }
666
- },
667
- }
1055
+ // a constant, so a build without locale domains drops their code
1056
+ export const domains = ${domains}
1057
+
1058
+ // a constant, so a build that keeps the locale in no cookie drops useCookie (no runtime override reaches these two)
1059
+ export const cookies = ${fullConfig.localeCookie !== null || fullConfig.hashMode}
1060
+
1061
+ // constants too, so a build with these options off drops their code from the loader plugin
1062
+ export const hooks = ${options.hooks !== false}
1063
+ export const defineRoute = ${options.define !== false}
668
1064
 
1065
+ // Nuxt 4.6+: a client navigation to a URL with no record becomes a document load (experimental.serverPathFallback)
1066
+ export const serverPathFallback = ${serverPathFallback}
1067
+
1068
+ // vue-router's Router is the adapter as is: no wrapper copying every resolve result.
1069
+ // config: the config with runtime overrides (defaultLocale, disabledLocales) \u2014 routing follows them.
1070
+ export function createI18nStrategy(router, config = __fullConfig) {
669
1071
  return new Strategy({
670
- strategy: __fullConfig.strategy,
671
- defaultLocale: __fullConfig.defaultLocale,
672
- locales: __fullConfig.locales,
673
- localizedRouteNamePrefix: __fullConfig.localizedRouteNamePrefix,
674
- globalLocaleRoutes: __fullConfig.globalLocaleRoutes,
675
- routeLocales: __fullConfig.routeLocales,
676
- routesLocaleLinks: __fullConfig.routesLocaleLinks,
677
- noPrefixRedirect: __fullConfig.noPrefixRedirect,
678
- debug: __fullConfig.debug,
679
- router: routerAdapter,
680
- hashMode: __fullConfig.hashMode,
681
- disablePageLocales: __fullConfig.disablePageLocales,
1072
+ strategy: config.strategy,
1073
+ defaultLocale: config.defaultLocale,
1074
+ locales: [...config.locales, ...__disabledLocales],
1075
+ localizedRouteNamePrefix: config.localizedRouteNamePrefix,
1076
+ router,
1077
+ hashMode: config.hashMode,
1078
+ disablePageLocales: config.disablePageLocales,
1079
+ customRegexMatcher: config.customRegexMatcher,
1080
+ // a prefix in another case is a 404 when nothing may redirect it
1081
+ prefixCaseRedirect: config.redirects !== false && config.prefixCaseRedirect !== false,${domains ? "\n domain: config.domain,\n linkLocales: config.linkLocales," : ""}
682
1082
  })
683
1083
  }
684
1084
  `;
@@ -687,23 +1087,20 @@ export function createI18nStrategy(router) {
687
1087
  const configTemplate = addTemplate({
688
1088
  filename: "i18n.config.mjs",
689
1089
  write: true,
690
- getContents: () => `const __privateConfig = ${JSON.stringify(privateConfig)}
691
- export function getI18nPrivateConfig() { return __privateConfig }
692
- `
1090
+ getContents: () => buildPrivateConfigSource(privateConfig, dev)
693
1091
  });
694
1092
  return {
695
1093
  pluralTemplate,
696
1094
  strategyTemplate,
697
- configTemplate,
698
- setFullConfig(next) {
699
- runtimeFullConfig = next;
700
- }
1095
+ configTemplate
701
1096
  };
702
1097
  }
703
1098
  const module$1 = defineNuxtModule({
704
1099
  meta: {
705
1100
  name: "nuxt-i18n-micro",
706
- configKey: "i18n"
1101
+ configKey: "i18n",
1102
+ // Vite environment hotUpdate (HMR) and NuxtPage's ~transitionPromise
1103
+ compatibility: { nuxt: ">=3.18.0" }
707
1104
  },
708
1105
  defaults: {
709
1106
  locales: [],
@@ -711,6 +1108,7 @@ const module$1 = defineNuxtModule({
711
1108
  debug: false,
712
1109
  define: true,
713
1110
  redirects: true,
1111
+ prefixCaseRedirect: true,
714
1112
  plugin: true,
715
1113
  hooks: true,
716
1114
  components: true,
@@ -724,11 +1122,8 @@ const module$1 = defineNuxtModule({
724
1122
  disablePageLocales: false,
725
1123
  disableWatcher: false,
726
1124
  noPrefixRedirect: false,
727
- fallbackLocale: void 0,
728
1125
  localeCookie: null,
729
1126
  apiBaseUrl: "_locales",
730
- apiBaseClientHost: void 0,
731
- apiBaseServerHost: void 0,
732
1127
  translationPayloads: {
733
1128
  mode: "premerged",
734
1129
  serverAssets: true,
@@ -740,20 +1135,18 @@ const module$1 = defineNuxtModule({
740
1135
  },
741
1136
  routesLocaleLinks: {},
742
1137
  globalLocaleRoutes: {},
743
- canonicalQueryWhitelist: void 0,
744
1138
  plural: defaultPlural,
745
- customRegexMatcher: void 0,
746
- excludePatterns: void 0,
747
1139
  localizedRouteNamePrefix: "localized-",
748
1140
  missingWarn: true,
749
1141
  metaTrustForwardedHost: true,
750
1142
  metaTrustForwardedProto: true,
1143
+ trustForwardedHost: false,
751
1144
  hreflangBaseLanguage: false,
752
1145
  httpCacheDuration: 31536e3
753
1146
  },
754
1147
  async setup(options, nuxt) {
755
- const defaultLocale = process.env.DEFAULT_LOCALE ?? options.defaultLocale ?? "en";
756
- const isSSG = Boolean(nuxt.options.nitro?.static);
1148
+ const defaultLocale = process.env.DEFAULT_LOCALE || options.defaultLocale || "en";
1149
+ let isSSG = false;
757
1150
  const logger = useLogger("nuxt-i18n-micro");
758
1151
  nuxt.options.typescript.hoist ||= [];
759
1152
  for (const pkg of ["@i18n-micro/types", "@i18n-micro/path-strategy"]) {
@@ -761,12 +1154,13 @@ const module$1 = defineNuxtModule({
761
1154
  nuxt.options.typescript.hoist.push(pkg);
762
1155
  }
763
1156
  }
764
- if (options.strategy === "no_prefix" && !options.localeCookie) {
1157
+ const cookieOptOut = nuxt.options.i18n?.localeCookie === null;
1158
+ if (options.strategy === "no_prefix" && !options.localeCookie && !cookieOptOut) {
765
1159
  options.localeCookie = "user-locale";
766
1160
  logger.info("Strategy 'no_prefix': localeCookie automatically set to 'user-locale' for locale persistence.");
767
1161
  }
768
1162
  if (options.strategy !== "no_prefix" && options.redirects !== false && !options.localeCookie) {
769
- logger.warn(
1163
+ logger.info(
770
1164
  "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."
771
1165
  );
772
1166
  }
@@ -775,7 +1169,12 @@ const module$1 = defineNuxtModule({
775
1169
  const mergedLocalesDir = resolve(nuxt.options.buildDir, "i18n-merged");
776
1170
  const sourceLocalesDir = resolve(nuxt.options.buildDir, "i18n-source");
777
1171
  const translationDirName = options.translationDir || "locales";
778
- const translationsHash = options.dateBuild === void 0 ? hashTranslationSources(rootDirs, translationDirName, options.additionalTranslationDirs) : null;
1172
+ const translationsHash = options.dateBuild === void 0 ? hashTranslationSources(
1173
+ rootDirs,
1174
+ translationDirName,
1175
+ options.additionalTranslationDirs,
1176
+ JSON.stringify([options.fallbackLocale, options.disablePageLocales, (options.locales ?? []).map((l) => [l.code, l.fallbackLocale])])
1177
+ ) : null;
779
1178
  const translationPayloads = resolveTranslationPayloadOptions(options);
780
1179
  const publicAssetsDir = translationPayloads.mode === "source" ? sourceLocalesDir : mergedLocalesDir;
781
1180
  for (const warning of getTranslationPayloadMisconfigurationWarnings({
@@ -783,9 +1182,10 @@ const module$1 = defineNuxtModule({
783
1182
  apiBaseClientHost: options.apiBaseClientHost,
784
1183
  apiBaseServerHost: options.apiBaseServerHost
785
1184
  })) {
786
- logger.warn(warning.replace("[nuxt-i18n-micro] ", ""));
1185
+ logger.warn(warning);
787
1186
  }
788
- if (!translationPayloads.serverAssets && !translationPayloads.publicAssets && !(process.env.NUXT_I18N_APP_BASE_SERVER_HOST ?? options.apiBaseServerHost)) {
1187
+ const rawServerHost = process.env.NUXT_I18N_APP_BASE_SERVER_HOST ?? options.apiBaseServerHost ?? void 0;
1188
+ if (!translationPayloads.serverAssets && !translationPayloads.publicAssets && !rawServerHost) {
789
1189
  throw new Error(
790
1190
  "nuxt-i18n-micro: local SSR payloads are disabled (serverAssets/publicAssets false) and apiBaseServerHost is not set. SSR would load empty translations."
791
1191
  );
@@ -799,79 +1199,53 @@ const module$1 = defineNuxtModule({
799
1199
  const prev = localeInfosByCode.get(locale.code);
800
1200
  localeInfosByCode.set(locale.code, {
801
1201
  code: locale.code,
802
- fallbackLocale: locale.fallbackLocale ?? prev?.fallbackLocale,
803
- disabled: !!(prev?.disabled || locale.disabled)
1202
+ fallbackLocale: prev?.fallbackLocale ?? locale.fallbackLocale,
1203
+ disabled: prev?.disabled ?? locale.disabled
804
1204
  });
805
1205
  }
806
1206
  const localeInfos = [...localeInfosByCode.values()].filter((locale) => !locale.disabled).map(({ code, fallbackLocale }) => ({ code, fallbackLocale }));
1207
+ let routeNames = [];
807
1208
  nuxt.hook("build:before", async () => {
808
1209
  if (translationPayloads.mode === "source") {
809
1210
  await buildTranslationSourceLayers(rootDirs, translationDirName, sourceLocalesDir, options.additionalTranslationDirs);
810
1211
  logger.info(`Built compact translation source from ${rootDirs.length} layer(s) into ${sourceLocalesDir}`);
811
1212
  } else {
812
- await preMergeLocales(
1213
+ await preMergeLocales({
813
1214
  rootDirs,
814
1215
  translationDirName,
815
- mergedLocalesDir,
816
- localeInfos,
817
- options.fallbackLocale,
818
- options.disablePageLocales,
819
- options.additionalTranslationDirs
820
- );
1216
+ outputDir: mergedLocalesDir,
1217
+ locales: localeInfos,
1218
+ globalFallbackLocale: options.fallbackLocale,
1219
+ disablePageLocales: options.disablePageLocales,
1220
+ additionalTranslationDirs: options.additionalTranslationDirs,
1221
+ routeNames,
1222
+ // dev reads live files, and a prerender merges what it reads like any server
1223
+ rootDelta: !nuxt.options.dev && translationPayloads.rootDelta
1224
+ });
821
1225
  logger.info(`Pre-merged translations from ${rootDirs.length} layer(s) into ${mergedLocalesDir}`);
822
1226
  }
823
1227
  const payloadStats = scanTranslationPayloadDirectory(publicAssetsDir);
824
- const sizeWarning = getTranslationPayloadSizeWarning(payloadStats, resolveTranslationPayloadWarningThresholds(options.translationPayloads));
825
- if (sizeWarning) {
826
- logger.warn(sizeWarning.replace("[nuxt-i18n-micro] ", ""));
827
- }
1228
+ const sizeWarning = getTranslationPayloadSizeWarning(payloadStats, options.translationPayloads);
1229
+ if (sizeWarning) logger.warn(sizeWarning);
828
1230
  });
829
- const routeLocales = { ...options.routeLocales ?? {} };
830
- const globalLocaleRoutes = {};
831
- const routeDisableMeta = {};
832
- const pageGlobs = rootDirs.flatMap((root) => [join(root, "pages/**/*.vue"), join(root, "app/pages/**/*.vue")]);
833
- const pageFiles = await globby(pageGlobs, { absolute: true });
834
- for (const { routePath, config } of collectDefineI18nRouteMetaFromFiles(pageFiles, rootDirs)) {
835
- try {
836
- const { locales: extractedLocales, localeRoutes, disableMeta } = config;
837
- if (extractedLocales) {
838
- if (Array.isArray(extractedLocales)) {
839
- routeLocales[routePath] = extractedLocales;
840
- } else if (typeof extractedLocales === "object") {
841
- routeLocales[routePath] = Object.keys(extractedLocales);
842
- }
843
- }
844
- if (localeRoutes) {
845
- globalLocaleRoutes[routePath] = localeRoutes;
846
- }
847
- if (disableMeta !== void 0) {
848
- routeDisableMeta[routePath] = disableMeta;
849
- }
850
- } catch (error) {
851
- logger.debug(`Failed to parse defineI18nRoute config for ${routePath}:`, error);
852
- }
853
- }
854
- const mergedGlobalLocaleRoutes = { ...globalLocaleRoutes, ...options.globalLocaleRoutes };
855
- if (options.debug) {
856
- logger.debug("[i18n module] mergedGlobalLocaleRoutes keys:", Object.keys(mergedGlobalLocaleRoutes));
857
- logger.debug('[i18n module] mergedGlobalLocaleRoutes["unlocalized"]:', mergedGlobalLocaleRoutes["unlocalized"]);
858
- logger.debug("[i18n module] strategy:", options.strategy);
859
- }
860
- const resolvedStrategyPath = await resolveStrategyPath(options.strategy);
861
- const resolvedHelpersPath = await resolveHelpersPath();
1231
+ const pageFiles = (await Promise.all(rootDirs.map((cwd) => globby(["pages/**/*.vue", "app/pages/**/*.vue"], { cwd, absolute: true })))).flat();
1232
+ const {
1233
+ routeLocales,
1234
+ routeDisableMeta,
1235
+ globalLocaleRoutes: mergedGlobalLocaleRoutes
1236
+ } = mergeDefineI18nRouteMeta(collectDefineI18nRouteMetaFromFiles(pageFiles, rootDirs), options);
1237
+ const resolvedStrategyPath = await resolveEntry("@i18n-micro/path-strategy");
1238
+ const resolvedHelpersPath = await resolveEntry("@i18n-micro/core/helpers");
862
1239
  let apiBaseClientHost = process.env.NUXT_I18N_APP_BASE_CLIENT_HOST ?? options.apiBaseClientHost ?? void 0;
863
1240
  if (apiBaseClientHost?.endsWith("/")) {
864
1241
  apiBaseClientHost = apiBaseClientHost.slice(0, -1);
865
1242
  }
866
- let apiBaseServerHost = process.env.NUXT_I18N_APP_BASE_SERVER_HOST ?? options.apiBaseServerHost ?? void 0;
867
- if (apiBaseServerHost?.endsWith("/")) {
868
- apiBaseServerHost = apiBaseServerHost.slice(0, -1);
869
- }
1243
+ const apiBaseServerHost = rawServerHost?.endsWith("/") ? rawServerHost.slice(0, -1) : rawServerHost;
870
1244
  const rawUrl = process.env.NUXT_I18N_APP_BASE_URL ?? options.apiBaseUrl ?? "_locales";
871
1245
  if (rawUrl.startsWith("http://") || rawUrl.startsWith("https://")) {
872
1246
  throw new Error("Nuxt-i18n-micro: Please use NUXT_I18N_APP_BASE_CLIENT_HOST or NUXT_I18N_APP_BASE_SERVER_HOST instead.");
873
1247
  }
874
- const apiBaseUrl = rawUrl.replace(/^\/+|\/+$/g, "").replace(/\/{2,}/g, "/");
1248
+ const apiBaseUrl = pathSegments(rawUrl);
875
1249
  const apiConfig = { apiBaseUrl, apiBaseClientHost, apiBaseServerHost };
876
1250
  const payloadPublicRel = resolveTranslationPayloadPublicRel(options, apiBaseUrl);
877
1251
  const routeGenerator = new RouteGenerator({
@@ -879,31 +1253,50 @@ const module$1 = defineNuxtModule({
879
1253
  defaultLocaleCode: defaultLocale,
880
1254
  strategy: options.strategy,
881
1255
  globalLocaleRoutes: mergedGlobalLocaleRoutes,
882
- filesLocaleRoutes: globalLocaleRoutes,
883
1256
  routeLocales,
1257
+ routeDisableMeta,
884
1258
  noPrefixRedirect: options.noPrefixRedirect,
1259
+ redirects: options.redirects,
885
1260
  excludePatterns: options.excludePatterns,
1261
+ apiBaseUrl,
886
1262
  localizedRouteNamePrefix: options.localizedRouteNamePrefix,
887
- customRegexMatcher: options.customRegexMatcher
1263
+ customRegexMatcher: options.customRegexMatcher,
1264
+ parentPaths: parentPagePaths(pageFiles, rootDirs)
888
1265
  });
889
1266
  const fullConfig = buildFullConfig({
890
1267
  options,
891
1268
  nuxt,
892
1269
  defaultLocale,
893
- isSSG,
894
1270
  apiConfig,
895
1271
  translationPayloadMode: translationPayloads.mode,
896
- routeLocales,
897
- routeDisableMeta,
898
- mergedGlobalLocaleRoutes,
1272
+ publicPayloads: translationPayloads.mode === "premerged" && translationPayloads.publicAssets,
1273
+ rootDelta: !nuxt.options.dev && translationPayloads.mode === "premerged" && translationPayloads.rootDelta && !options.disablePageLocales && (translationPayloads.publicAssets || Boolean(options.apiBaseClientHost)),
899
1274
  locales: routeGenerator.locales ?? [],
900
1275
  translationsHash
901
1276
  });
902
- const privateConfig = buildPrivateConfig(options, nuxt, apiConfig, routeGenerator.locales ?? [], publicAssetsDir, payloadPublicRel);
903
- const templates = registerI18nTemplates(options, resolvedStrategyPath, resolvedHelpersPath, fullConfig, privateConfig);
1277
+ const generateDomain = process.env.NUXT_I18N_DOMAIN;
1278
+ const buildConfig = generateDomain ? configForHost(fullConfig, generateDomain) : fullConfig;
1279
+ if (generateDomain && !buildConfig.domain)
1280
+ logger.warn(`NUXT_I18N_DOMAIN "${generateDomain}" is no host of locales[].baseUrl: the build config applies.`);
1281
+ const siteUrl = nuxt.options.site?.url || process.env.NUXT_PUBLIC_SITE_URL || process.env.NUXT_SITE_URL;
1282
+ const originWarning = buildConfig.domain ? null : unfixedOriginWarning(routeGenerator.locales, defaultLocale, options.metaBaseUrl, siteUrl);
1283
+ if (originWarning) logger.warn(originWarning);
1284
+ const privateConfig = buildPrivateConfig(options, nuxt, rootDirs, apiConfig, routeGenerator.locales ?? [], publicAssetsDir, payloadPublicRel);
1285
+ const templates = registerI18nTemplates(
1286
+ options,
1287
+ resolvedStrategyPath,
1288
+ resolvedHelpersPath,
1289
+ fullConfig,
1290
+ privateConfig,
1291
+ routeGenerator.disabledLocales,
1292
+ nuxt.options.dev,
1293
+ nuxt.options.experimental.serverPathFallback === true && !nuxt.options.router?.options?.hashMode
1294
+ );
904
1295
  if (typeof options.customRegexMatcher !== "undefined") {
905
- const localeCodes = routeGenerator.locales.map((l) => l.code);
906
- const failedCodes = localeCodes.filter((code) => !code.match(options.customRegexMatcher));
1296
+ const failedCodes = localesOutsideMatcher(
1297
+ routeGenerator.locales.map((l) => l.code),
1298
+ options.customRegexMatcher
1299
+ );
907
1300
  if (failedCodes.length > 0) {
908
1301
  throw new Error(
909
1302
  "Nuxt-i18n-micro: customRegexMatcher does not match the following locale codes: " + failedCodes.join(", ") + ". The regex must match ALL locale codes in your configuration."
@@ -917,51 +1310,48 @@ const module$1 = defineNuxtModule({
917
1310
  name: "i18n-plugin-loader",
918
1311
  order: -5
919
1312
  });
920
- }
921
- if (options.hooks) {
922
- addPlugin({
923
- src: resolver.resolve("./runtime/plugins/05.hooks"),
924
- name: "i18n-plugin-hooks",
925
- order: 1
926
- });
927
- }
928
- if (options.meta) {
929
- addPlugin({
930
- src: resolver.resolve("./runtime/plugins/02.meta"),
931
- name: "i18n-plugin-meta",
932
- order: 2
933
- });
934
- }
935
- if (options.define) {
936
- addPlugin({
937
- src: resolver.resolve("./runtime/plugins/03.define"),
938
- name: "i18n-plugin-define",
939
- mode: "all",
940
- order: 3
941
- });
942
- }
943
- if (nuxt.options.dev && options.plugin !== false && options.vueDevtools !== false) {
1313
+ if (options.meta) {
1314
+ addPlugin({
1315
+ src: resolver.resolve("./runtime/plugins/02.meta"),
1316
+ name: "i18n-plugin-meta",
1317
+ order: 2
1318
+ });
1319
+ }
1320
+ if (nuxt.options.dev && options.vueDevtools !== false) {
1321
+ addPlugin({
1322
+ src: resolver.resolve("./runtime/plugins/04.vue-devtools"),
1323
+ name: "i18n-vue-devtools",
1324
+ mode: "client",
1325
+ order: 5
1326
+ });
1327
+ }
944
1328
  addPlugin({
945
- src: resolver.resolve("./runtime/plugins/04.vue-devtools"),
946
- name: "i18n-vue-devtools",
947
- mode: "client",
948
- order: 5
1329
+ src: resolver.resolve("./runtime/plugins/06.redirect"),
1330
+ mode: "server",
1331
+ name: "i18n-plugin-redirect",
1332
+ order: 10
949
1333
  });
950
- }
951
- addPlugin({
952
- src: resolver.resolve("./runtime/plugins/06.redirect"),
953
- mode: "server",
954
- name: "i18n-plugin-redirect",
955
- order: 10
956
- });
957
- if (options.redirects !== false && options.plugin !== false) {
958
- addRouteMiddleware({
959
- name: "i18n-redirect",
960
- path: resolver.resolve("./runtime/middleware/i18n-redirect.global"),
961
- global: true
1334
+ if (options.redirects !== false && options.strategy !== "no_prefix") {
1335
+ addRouteMiddleware({
1336
+ name: "i18n-redirect",
1337
+ path: resolver.resolve("./runtime/middleware/i18n-redirect.global"),
1338
+ global: true
1339
+ });
1340
+ }
1341
+ } else if (options.define) {
1342
+ addPluginTemplate({
1343
+ filename: "i18n.define.mjs",
1344
+ getContents: () => `import { defineNuxtPlugin } from '#imports'
1345
+ export default defineNuxtPlugin(() => ({ provide: { defineI18nRoute: async () => {} } }))
1346
+ `
962
1347
  });
963
1348
  }
964
- addServerImportsDir(resolver.resolve("./runtime/server/utils"));
1349
+ addServerImports([
1350
+ { name: "useLocaleServerMiddleware", from: resolver.resolve("./runtime/server/utils/locale-server-middleware") },
1351
+ { name: "useTranslationServerMiddleware", from: resolver.resolve("./runtime/server/utils/translation-server-middleware") },
1352
+ { name: "loadTranslationsFromServer", from: resolver.resolve("./runtime/server/utils/server-loader") },
1353
+ { name: "detectCurrentLocale", from: resolver.resolve("./runtime/server/utils/locale-detector") }
1354
+ ]);
965
1355
  if (translationPayloads.serverHandler) {
966
1356
  addServerHandler({
967
1357
  route: `/${apiBaseUrl}/:page/:locale/data.json`,
@@ -976,30 +1366,26 @@ const module$1 = defineNuxtModule({
976
1366
  });
977
1367
  }
978
1368
  if (nuxt.options.dev && (options.hmr ?? true)) {
979
- const files = (await Promise.all(
980
- rootDirs.map(async (layerRoot) => {
981
- const translationsDir = join(layerRoot, options.translationDir || "locales");
982
- if (!existsSync(translationsDir)) return [];
983
- return globby(["**/*.json"], { cwd: translationsDir, absolute: true });
984
- })
985
- )).flat();
986
- const additionalRootFiles = (await Promise.all(
987
- (options.additionalTranslationDirs ?? []).flatMap(
988
- (extra) => rootDirs.map(async (layerRoot) => {
989
- const extraDir = join(layerRoot, extra);
990
- if (!existsSync(extraDir)) return [];
991
- const extras = await globby(["*.json"], { cwd: extraDir, absolute: true, onlyFiles: true, deep: 0 });
992
- return extras.map((f) => f.replace(/\\/g, "/"));
993
- })
994
- )
995
- )).flat();
1369
+ const translationDirs = resolveLiveTranslationDirs({
1370
+ rootDirs,
1371
+ translationDir: translationDirName,
1372
+ additionalTranslationDirs: options.additionalTranslationDirs
1373
+ });
1374
+ addVitePlugin(
1375
+ {
1376
+ name: "i18n-micro:translations-hmr",
1377
+ hotUpdate({ file }) {
1378
+ if (this.environment.name !== "client" || !translationRelativePath(file, translationDirs)) return;
1379
+ this.environment.hot.send({ type: "custom", event: TRANSLATIONS_HMR_EVENT });
1380
+ return [];
1381
+ }
1382
+ },
1383
+ { server: false }
1384
+ );
996
1385
  const tpl = addTemplate({
997
1386
  filename: "i18n.hmr.mjs",
998
1387
  write: true,
999
- getContents: () => generateHmrPlugin(
1000
- files.map((f) => f.replace(/\\/g, "/")),
1001
- { additionalRootFiles }
1002
- )
1388
+ getContents: () => generateHmrPlugin()
1003
1389
  });
1004
1390
  addPlugin({
1005
1391
  src: tpl.dst,
@@ -1015,13 +1401,19 @@ const module$1 = defineNuxtModule({
1015
1401
  });
1016
1402
  addTypeTemplate({
1017
1403
  filename: "types/h3.d.ts",
1018
- getContents: () => `import type { Translations } from '@i18n-micro/types'
1404
+ getContents: () => `import type { ResolvedI18nConfig, Translations } from '@i18n-micro/types'
1019
1405
 
1020
1406
  declare module 'h3' {
1021
1407
  interface H3EventContext {
1022
1408
  i18n?: {
1023
1409
  locale: string
1024
1410
  translations: Translations
1411
+ /** Loads a chunk for this request's render (the server loader, no local HTTP round trip), or \`null\` when the host has no file for it. */
1412
+ load?: (locale: string, routeName: string) => Promise<Translations | null>
1413
+ /** The module config with runtime overrides, resolved once for this request. */
1414
+ config?: ResolvedI18nConfig
1415
+ /** A dictionary of this page render could not be read: no cache may keep the page. */
1416
+ failedRead?: boolean
1025
1417
  }
1026
1418
  }
1027
1419
  }
@@ -1033,101 +1425,116 @@ export {}
1033
1425
  addTypeTemplate({
1034
1426
  filename: "types/i18n-internal.d.ts",
1035
1427
  getContents: () => {
1036
- return `import type { ModuleOptionsExtend, ModulePrivateOptionsExtend, Params, Getter, PluralFunc } from '@i18n-micro/types'
1428
+ const strategyDeclaration = `
1429
+ export const domains: boolean
1430
+ export const cookies: boolean
1431
+ export const hooks: boolean
1432
+ export const defineRoute: boolean
1433
+ export const serverPathFallback: boolean
1434
+ export function getI18nConfig(): ResolvedI18nConfig
1435
+ export function createI18nStrategy(router: I18nStrategyRouter, config?: ModuleOptionsExtend): PathStrategy
1436
+ `;
1437
+ return `import type { ModuleOptionsExtend, ModulePrivateOptionsExtend, Params, Getter, PluralFunc, ResolvedI18nConfig } from '@i18n-micro/types'
1037
1438
  import type { PathStrategy } from '@i18n-micro/path-strategy'
1439
+ import type { Router } from 'vue-router'
1440
+
1441
+ /** The app's vue-router: createI18nStrategy reads hasRoute, resolve and getRoutes from it. */
1442
+ type I18nStrategyRouter = Pick<Router, 'hasRoute' | 'resolve' | 'getRoutes'>
1038
1443
 
1039
1444
  declare module '#build/i18n.plural.mjs' {
1040
1445
  export function plural(key: string, count: number, params: Params, locale: string, getter: Getter): string | null
1041
1446
  }
1042
1447
 
1043
- declare module '#build/i18n.strategy.mjs' {
1044
- export function getI18nConfig(): ModuleOptionsExtend
1045
- export function createI18nStrategy(router: { hasRoute: (name: string) => boolean, resolve: (to: unknown) => { name: string | null, path: string, fullPath: string, params?: Record<string, unknown>, query?: Record<string, unknown>, hash?: string } }): PathStrategy
1046
- }
1448
+ declare module '#build/i18n.strategy.mjs' {${strategyDeclaration}}
1047
1449
 
1048
1450
  declare module '#i18n-internal/config' {
1049
1451
  export function getI18nPrivateConfig(): ModulePrivateOptionsExtend
1050
1452
  }
1051
1453
 
1052
- declare module '#i18n-internal/strategy' {
1053
- export function getI18nConfig(): ModuleOptionsExtend
1054
- export function createI18nStrategy(router: { hasRoute: (name: string) => boolean, resolve: (to: unknown) => { name: string | null, path: string, fullPath: string, params?: Record<string, unknown>, query?: Record<string, unknown>, hash?: string } }): PathStrategy
1055
- }
1454
+ declare module '#i18n-internal/strategy' {${strategyDeclaration}}
1056
1455
 
1057
1456
  declare module '#i18n-internal/plural' {
1058
1457
  export const plural: PluralFunc
1059
1458
  }
1060
1459
 
1061
1460
  declare module '#i18n-internal/payload-source' {
1461
+ export const embeddedPayloads: boolean
1062
1462
  export function readPayload(relPath: string): Promise<Record<string, unknown>>
1063
1463
  }
1064
1464
  `;
1065
1465
  }
1066
1466
  });
1467
+ let generatedPages = [];
1468
+ let staticPaths = [];
1469
+ const createBuildStrategy = () => createLocalizedPathResolver(generatedPages, {
1470
+ strategy: options.strategy,
1471
+ defaultLocale: buildConfig.defaultLocale,
1472
+ locales: buildConfig.locales,
1473
+ domain: buildConfig.domain,
1474
+ linkLocales: buildConfig.linkLocales,
1475
+ localizedRouteNamePrefix: options.localizedRouteNamePrefix,
1476
+ customRegexMatcher: fullConfig.customRegexMatcher
1477
+ });
1478
+ const unlinked = (name) => !options.routesLocaleLinks?.[name];
1067
1479
  const addDataRoutes = (pages = []) => {
1068
1480
  if (!translationPayloads.prerenderRoutes) return;
1069
- const pagesForDataRoutes = pages.filter((p) => p.name !== void 0 && (!options.routesLocaleLinks || !options.routesLocaleLinks[p.name]));
1481
+ const pagesForDataRoutes = pages.filter((p) => p.name !== void 0 && unlinked(p.name));
1070
1482
  const dataRoutes = routeGenerator.generateDataRoutes(pagesForDataRoutes, apiBaseUrl, !!options.disablePageLocales);
1071
1483
  addPrerenderRoutes(dataRoutes);
1072
1484
  };
1073
1485
  nuxt.hook("pages:resolved", (pages) => {
1074
- const pagesNames = pages.map((page) => page.name).filter((name) => name !== void 0 && (!options.routesLocaleLinks || !options.routesLocaleLinks[name]));
1486
+ const pagesNames = pages.map((page) => page.name).filter((name) => name !== void 0 && unlinked(name));
1075
1487
  if (!options.disableWatcher) {
1076
- routeGenerator.ensureTranslationFilesExist(pagesNames, options.translationDir, nuxt.options.rootDir, options.disablePageLocales);
1488
+ routeGenerator.ensureTranslationFilesExist(pagesNames, translationDirName, nuxt.options.rootDir, options.disablePageLocales);
1077
1489
  }
1078
1490
  addDataRoutes(pages);
1491
+ routeNames = pageNames(pages).filter(unlinked);
1492
+ staticPaths = staticPagePaths(pages);
1079
1493
  routeGenerator.extendPages(pages);
1494
+ generatedPages = pages;
1080
1495
  });
1081
1496
  if (options.disablePageLocales) {
1082
1497
  nuxt.hook("build:before", () => addDataRoutes([]));
1083
1498
  }
1084
- addVitePlugin(
1085
- createDefineI18nRoutePlugin({
1086
- buildDir: nuxt.options.buildDir,
1087
- rootDirs
1088
- }).vite()
1089
- );
1090
1499
  const { pluralTemplate, strategyTemplate, configTemplate } = templates;
1091
1500
  nuxt.hook("nitro:config", (nitroConfig) => {
1092
1501
  nitroConfig.alias = nitroConfig.alias || {};
1093
1502
  nitroConfig.alias["#i18n-internal/plural"] = pluralTemplate.dst;
1094
1503
  nitroConfig.alias["#i18n-internal/strategy"] = strategyTemplate.dst;
1095
1504
  nitroConfig.alias["#i18n-internal/config"] = configTemplate.dst;
1505
+ nitroConfig.externals = nitroConfig.externals || {};
1506
+ nitroConfig.externals.inline = [...nitroConfig.externals.inline || [], pluralTemplate.dst, strategyTemplate.dst];
1096
1507
  nitroConfig.alias["#i18n-internal/payload-source"] = resolver.resolve("./runtime/server/payload-source.fs");
1097
1508
  nitroConfig.plugins = nitroConfig.plugins || [];
1509
+ nitroConfig.plugins.push(resolver.resolve("./runtime/server/plugins/failed-read"));
1510
+ if (!nuxt.options.dev) nitroConfig.plugins.push(resolver.resolve("./runtime/server/plugins/payload-cache"));
1098
1511
  if (nuxt.options.dev && (options.hmr ?? true)) {
1099
1512
  nitroConfig.plugins.push(resolver.resolve("./runtime/server/plugins/watcher.dev"));
1100
1513
  }
1101
1514
  nitroConfig.routeRules = nitroConfig.routeRules || {};
1102
- const httpCacheDuration = Math.floor(options.httpCacheDuration ?? 0);
1103
- const hasCacheBuster = Boolean(resolveDateBuild(options, translationsHash));
1104
- const cacheControl = buildTranslationPayloadCacheControl(httpCacheDuration, false);
1515
+ const { file: cacheControl } = resolveTranslationPayloadCachePolicy({
1516
+ httpCacheDuration: options.httpCacheDuration,
1517
+ httpCacheControl: options.httpCacheControl,
1518
+ dateBuild: fullConfig.dateBuild,
1519
+ live: isLiveDictionary(options, translationPayloads.mode, rawServerHost)
1520
+ });
1105
1521
  nitroConfig.routeRules[`/${apiBaseUrl}/**`] = {
1106
1522
  ...nitroConfig.routeRules[`/${apiBaseUrl}/**`] || {},
1107
- ...buildTranslationPayloadRouteRule({ dev: nuxt.options.dev, hasCacheBuster, cacheControl })
1523
+ ...buildTranslationPayloadRouteRule({ dev: nuxt.options.dev, cacheControl })
1108
1524
  };
1109
- const routeRules = nuxt.options.routeRules || {};
1110
- const strategy = options.strategy;
1111
- if (routeRules && Object.keys(routeRules).length && !isNoPrefixStrategy(strategy)) {
1112
- for (const [originalPath, ruleValue] of Object.entries(routeRules)) {
1113
- if (!shouldLocalizeRouteRulePath(originalPath)) continue;
1114
- routeGenerator.locales.forEach((localeObj) => {
1115
- const localeCode = localeObj.code;
1116
- if (!isLocaleAllowedForUnlocalizedRoute(routeGenerator.routeLocales, routeGenerator.locales, originalPath, localeCode)) {
1117
- return;
1118
- }
1119
- const localizedPath = routeGenerator.resolveLocalizedPath(originalPath, localeCode);
1120
- if (localizedPath === originalPath || localizedPath === normalizePath(originalPath)) {
1121
- return;
1122
- }
1123
- const { redirect, ...restRuleValue } = ruleValue;
1124
- if (!Object.keys(restRuleValue).length) return;
1125
- nitroConfig.routeRules[localizedPath] = {
1126
- ...nitroConfig.routeRules[localizedPath],
1127
- ...restRuleValue
1128
- };
1129
- logger.debug(`Replicated routeRule for ${localizedPath}: ${JSON.stringify(restRuleValue)}`);
1130
- });
1525
+ const noDocuments = Boolean(buildConfig.hashMode) && options.strategy === "prefix";
1526
+ localizeRouteRules(
1527
+ nuxt.options.routeRules || {},
1528
+ nitroConfig.routeRules,
1529
+ routeGenerator,
1530
+ (path, rule) => {
1531
+ logger.debug(`Replicated routeRule for ${path}: ${JSON.stringify(rule)}`);
1532
+ },
1533
+ noDocuments
1534
+ );
1535
+ if (routeGenerator.locales.some((locale) => locale.baseUrl)) {
1536
+ for (const [path, rule] of Object.entries(nitroConfig.routeRules)) {
1537
+ nitroConfig.routeRules[path] = varyRouteCacheByHost(rule, Boolean(options.trustForwardedHost));
1131
1538
  }
1132
1539
  }
1133
1540
  nitroConfig.handlers = nitroConfig.handlers || [];
@@ -1137,10 +1544,30 @@ declare module '#i18n-internal/payload-source' {
1137
1544
  });
1138
1545
  });
1139
1546
  nuxt.hook("nitro:init", (nitro) => {
1140
- if (nitro.options.static) return;
1547
+ if (buildConfig.domain) {
1548
+ let localize;
1549
+ nitro.hooks.hook("prerender:generate", async (route) => {
1550
+ localize ??= await createBuildStrategy();
1551
+ if (route.route !== "/" && localize.isPage(route.route) && !localize.isServed(route.route)) route.skip = true;
1552
+ });
1553
+ }
1554
+ isSSG = Boolean(nitro.options.static);
1555
+ if (isSSG) {
1556
+ if (!generateDomain && routeGenerator.locales.some((locale) => locale.baseUrl)) {
1557
+ logger.warn(
1558
+ "locales[].baseUrl in a static build without NUXT_I18N_DOMAIN: the build config applies, and locales with a baseUrl are links. Generate once per domain."
1559
+ );
1560
+ }
1561
+ const staticWarning = getStaticPayloadWarning({ translationPayloads, apiBaseClientHost });
1562
+ if (staticWarning) logger.warn(staticWarning);
1563
+ return;
1564
+ }
1141
1565
  const readsPublicDir = shouldReadPayloadsFromPublicDir(nitro.options);
1142
1566
  if (!readsPublicDir) {
1143
1567
  nitro.options.alias["#i18n-internal/payload-source"] = resolver.resolve("./runtime/server/payload-source.assets");
1568
+ if (options.cacheStorage && !apiBaseServerHost) {
1569
+ logger.info("cacheStorage: dictionaries are embedded on this preset, so the shared cache only applies with apiBaseServerHost.");
1570
+ }
1144
1571
  }
1145
1572
  if (shouldRegisterNitroServerAssets(translationPayloads, readsPublicDir)) {
1146
1573
  nitro.options.serverAssets.push({
@@ -1148,11 +1575,6 @@ declare module '#i18n-internal/payload-source' {
1148
1575
  // Same layout as public (`mode`): premerged matrix or compact source.
1149
1576
  dir: publicAssetsDir
1150
1577
  });
1151
- if (translationPayloads.mode === "premerged") {
1152
- logger.warn(
1153
- `Preset "${nitro.options.preset}" does not ship public/ with the server, so translationPayloads.mode:"premerged" embeds the full page/locale matrix via Nitro serverAssets (Rollup raw:). Prefer mode:'source' for large catalogs.`
1154
- );
1155
- }
1156
1578
  }
1157
1579
  if (readsPublicDir && translationPayloads.serverAssets && !translationPayloads.publicAssets) {
1158
1580
  logger.info("translationPayloads.serverAssets: on Node, payloads are served from public/ (publicAssets copy enabled for SSR).");
@@ -1162,11 +1584,22 @@ declare module '#i18n-internal/payload-source' {
1162
1584
  const isProd = nuxt.options.dev === false;
1163
1585
  const readsPublicDir = shouldReadPayloadsFromPublicDir(nitro.options);
1164
1586
  const copyPublicPayloads = shouldCopyTranslationPayloadsToPublic(translationPayloads, readsPublicDir);
1587
+ if (isProd && !isSSG && shouldRegisterNitroServerAssets(translationPayloads, readsPublicDir)) {
1588
+ const sizeWarning = getEmbeddedPayloadSizeWarning(nitro.options.preset, publicAssetsDir, translationPayloads.mode);
1589
+ if (sizeWarning) logger.warn(sizeWarning);
1590
+ }
1165
1591
  if (isProd && copyPublicPayloads) {
1166
1592
  const publicDir = resolveTranslationPayloadPublicDir(nitro.options.output.publicDir, options, apiBaseUrl);
1167
1593
  try {
1168
1594
  if (existsSync(publicAssetsDir)) {
1169
- fs__default.cpSync(publicAssetsDir, publicDir, { recursive: true });
1595
+ const filter = domainPayloadFilter({
1596
+ mode: translationPayloads.mode,
1597
+ root: publicAssetsDir,
1598
+ served: buildConfig.locales,
1599
+ linkLocales: buildConfig.linkLocales,
1600
+ fallbackLocale: options.fallbackLocale
1601
+ });
1602
+ fs__default.cpSync(publicAssetsDir, publicDir, { recursive: true, filter });
1170
1603
  logger.log(`Translation payloads copied to public directory`);
1171
1604
  const compressed = compressTranslationPayloads(publicDir, nitro.options.compressPublicAssets);
1172
1605
  if (compressed > 0) {
@@ -1184,73 +1617,21 @@ declare module '#i18n-internal/payload-source' {
1184
1617
  if (isNoPrefixStrategy(options.strategy)) {
1185
1618
  return;
1186
1619
  }
1187
- const routesSet = prerenderRoutes.routes;
1188
1620
  const routeRules = nuxt.options.routeRules || {};
1189
- const additionalRoutes = /* @__PURE__ */ new Set();
1190
- const localeCodes = new Set(routeGenerator.locales.map((locale) => locale.code));
1191
- if (isSSG) {
1192
- if (routeRules["/"]?.prerender !== false) {
1193
- routesSet.add("/");
1194
- }
1195
- for (const locale of routeGenerator.locales) {
1196
- if (!isLocaleAllowedForUnlocalizedRoute(routeGenerator.routeLocales, routeGenerator.locales, "/", locale.code)) {
1197
- continue;
1198
- }
1199
- const localizedRoot = routeGenerator.resolveLocalizedPath("/", locale.code);
1200
- if (localizedRoot && localizedRoot !== "/" && routeRules[localizedRoot]?.prerender !== false) {
1201
- routesSet.add(localizedRoot);
1202
- }
1203
- }
1204
- }
1205
- for (const route of routesSet) {
1206
- if (isInternalPath(route, options.excludePatterns)) {
1207
- routesSet.delete(route);
1208
- }
1209
- }
1210
- for (const route of routesSet) {
1211
- if (/\.[a-z0-9]+$/i.test(route)) {
1212
- continue;
1213
- }
1214
- if (routeRules[route]?.prerender === false) {
1215
- continue;
1216
- }
1217
- const firstSegment = route.replace(/^\//, "").split("/")[0];
1218
- if (firstSegment && localeCodes.has(firstSegment)) {
1219
- continue;
1220
- }
1221
- for (const locale of routeGenerator.locales) {
1222
- if (!isLocaleAllowedForUnlocalizedRoute(routeGenerator.routeLocales, routeGenerator.locales, route, locale.code)) {
1223
- continue;
1224
- }
1225
- const localizedRoute = routeGenerator.resolveLocalizedPath(route, locale.code);
1226
- if (localizedRoute === route) {
1227
- continue;
1228
- }
1229
- if (routeRules[localizedRoute]?.prerender === false) {
1230
- continue;
1231
- }
1232
- additionalRoutes.add(localizedRoute);
1233
- }
1234
- }
1235
- if (additionalRoutes.size > 0) {
1236
- logger.debug("[i18n prerender:routes] added localized routes:", [...additionalRoutes].sort().join(", "));
1237
- }
1238
- for (const route of additionalRoutes) {
1239
- routesSet.add(route);
1621
+ const { added, removed } = seedPrerenderRoutes(prerenderRoutes.routes, {
1622
+ // the same router and strategy the app runs: custom paths, `false` pages and routeLocales as generated
1623
+ localize: await createBuildStrategy(),
1624
+ locales: buildConfig.locales,
1625
+ staticPaths: isSSG ? staticPaths : null,
1626
+ strategy: options.strategy,
1627
+ prerendered: (route) => routeRules[route]?.prerender !== false,
1628
+ isInternal: (route) => isInternalPath(route, options.excludePatterns, apiBaseUrl)
1629
+ });
1630
+ if (added.length > 0) {
1631
+ logger.debug("[i18n prerender:routes] added localized routes:", [...added].sort().join(", "));
1240
1632
  }
1241
- if (withPrefixStrategy(options.strategy)) {
1242
- const deleted = [];
1243
- for (const route of routesSet) {
1244
- if (route === "/" || route === "") continue;
1245
- const firstSegment = route.replace(/^\//, "").split("/")[0];
1246
- if (firstSegment && !localeCodes.has(firstSegment)) {
1247
- routesSet.delete(route);
1248
- deleted.push(route);
1249
- }
1250
- }
1251
- if (deleted.length > 0) {
1252
- logger.info(`[i18n prerender:routes] removed from prerender list (no locale prefix): ${deleted.join(", ")}`);
1253
- }
1633
+ if (removed.length > 0) {
1634
+ logger.info(`[i18n prerender:routes] removed from prerender list (no locale prefix): ${removed.join(", ")}`);
1254
1635
  }
1255
1636
  });
1256
1637
  const devtoolsOptions = nuxt.options.devtools;