nuxt-i18n-micro 3.30.2 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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/f0c8d4bf-40d2-4915-8b4a-ebe46bde61c8.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 +850 -475
  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/f020fec8-462b-480b-82d8-6b0cb65617e8.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';
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
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';
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 process.platform === "win32" ? pathToFileURL(absolutePath).href : 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,71 @@ 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
+ localizedRouteNamePrefix: options.localizedRouteNamePrefix,
603
1004
  routesLocaleLinks: options.routesLocaleLinks ?? {},
604
- noPrefixRedirect: options.noPrefixRedirect ?? false,
605
- debug: options.debug ?? false,
1005
+ debug: options.debug,
606
1006
  customRegexMatcher: options.customRegexMatcher instanceof RegExp ? options.customRegexMatcher.source : options.customRegexMatcher,
607
1007
  cacheMaxSize: options.cacheMaxSize ?? 0,
608
1008
  cacheTtl: options.cacheTtl ?? 0,
609
1009
  httpCacheDuration: options.httpCacheDuration,
1010
+ httpCacheControl: options.httpCacheControl,
610
1011
  numberFormats: options.numberFormats ?? {},
611
1012
  datetimeFormats: options.datetimeFormats ?? {},
612
- translationPayloadMode
1013
+ timeZone: options.timeZone,
1014
+ translationPayloadMode,
1015
+ publicPayloads,
1016
+ rootDelta,
1017
+ noScripts: noScripts === "all" || !!noScripts && !nuxt.options.dev || void 0
613
1018
  };
614
1019
  }
615
- function buildPrivateConfig(options, nuxt, apiConfig, locales, payloadFsDir, payloadPublicRel) {
1020
+ function buildPrivateConfig(options, nuxt, rootDirs, apiConfig, locales, payloadFsDir, payloadPublicRel) {
616
1021
  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(),
1022
+ rootDirs,
620
1023
  debug: options.debug ?? false,
621
1024
  locales: locales ?? [],
622
- fallbackLocale: options.fallbackLocale ?? void 0,
623
1025
  translationDir: options.translationDir ?? "locales",
624
1026
  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
1027
  apiBaseServerHost: apiConfig.apiBaseServerHost,
630
1028
  serverTranslationPreload: options.serverTranslationPreload ?? false,
1029
+ cacheStorage: options.cacheStorage,
1030
+ cacheStorageBuild: nuxt.options.buildId,
631
1031
  payloadFsDir,
632
1032
  payloadPublicRel
633
1033
  };
634
1034
  }
635
- function registerI18nTemplates(options, resolvedStrategyPath, resolvedHelpersPath, fullConfig, privateConfig) {
636
- let runtimeFullConfig = fullConfig;
1035
+ function registerI18nTemplates(options, resolvedStrategyPath, resolvedHelpersPath, fullConfig, privateConfig, disabledLocales, dev, serverPathFallback) {
637
1036
  const pluralTemplate = addTemplate({
638
1037
  filename: "i18n.plural.mjs",
639
1038
  write: true,
@@ -643,42 +1042,40 @@ function registerI18nTemplates(options, resolvedStrategyPath, resolvedHelpersPat
643
1042
  filename: "i18n.strategy.mjs",
644
1043
  write: true,
645
1044
  getContents: () => {
646
- const fullConfigJson = JSON.stringify(runtimeFullConfig);
647
- return `import { Strategy } from '${resolvedStrategyPath}'
1045
+ const domains = fullConfig.locales.some((locale) => locale.baseUrl);
1046
+ return `import { ${domains ? "DomainPathStrategy" : "I18nPathStrategy"} as Strategy } from '${resolvedStrategyPath}'
648
1047
 
649
- const __fullConfig = ${fullConfigJson}
1048
+ const __fullConfig = ${buildFullConfigSource(fullConfig)}
1049
+ // disabled at build: no routes and not in the config, but routing must answer for their prefix
1050
+ const __disabledLocales = ${JSON.stringify(disabledLocales)}
650
1051
 
651
1052
  export function getI18nConfig() { return __fullConfig }
652
1053
 
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
- }
1054
+ // a constant, so a build without locale domains drops their code
1055
+ export const domains = ${domains}
1056
+
1057
+ // a constant, so a build that keeps the locale in no cookie drops useCookie (no runtime override reaches these two)
1058
+ export const cookies = ${fullConfig.localeCookie !== null || fullConfig.hashMode}
1059
+
1060
+ // constants too, so a build with these options off drops their code from the loader plugin
1061
+ export const hooks = ${options.hooks !== false}
1062
+ export const defineRoute = ${options.define !== false}
668
1063
 
1064
+ // Nuxt 4.6+: a client navigation to a URL with no record becomes a document load (experimental.serverPathFallback)
1065
+ export const serverPathFallback = ${serverPathFallback}
1066
+
1067
+ // vue-router's Router is the adapter as is: no wrapper copying every resolve result.
1068
+ // config: the config with runtime overrides (defaultLocale, disabledLocales) \u2014 routing follows them.
1069
+ export function createI18nStrategy(router, config = __fullConfig) {
669
1070
  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,
1071
+ strategy: config.strategy,
1072
+ defaultLocale: config.defaultLocale,
1073
+ locales: [...config.locales, ...__disabledLocales],
1074
+ localizedRouteNamePrefix: config.localizedRouteNamePrefix,
1075
+ router,
1076
+ hashMode: config.hashMode,
1077
+ disablePageLocales: config.disablePageLocales,
1078
+ customRegexMatcher: config.customRegexMatcher,${domains ? "\n domain: config.domain,\n linkLocales: config.linkLocales," : ""}
682
1079
  })
683
1080
  }
684
1081
  `;
@@ -687,23 +1084,20 @@ export function createI18nStrategy(router) {
687
1084
  const configTemplate = addTemplate({
688
1085
  filename: "i18n.config.mjs",
689
1086
  write: true,
690
- getContents: () => `const __privateConfig = ${JSON.stringify(privateConfig)}
691
- export function getI18nPrivateConfig() { return __privateConfig }
692
- `
1087
+ getContents: () => buildPrivateConfigSource(privateConfig, dev)
693
1088
  });
694
1089
  return {
695
1090
  pluralTemplate,
696
1091
  strategyTemplate,
697
- configTemplate,
698
- setFullConfig(next) {
699
- runtimeFullConfig = next;
700
- }
1092
+ configTemplate
701
1093
  };
702
1094
  }
703
1095
  const module$1 = defineNuxtModule({
704
1096
  meta: {
705
1097
  name: "nuxt-i18n-micro",
706
- configKey: "i18n"
1098
+ configKey: "i18n",
1099
+ // Vite environment hotUpdate (HMR) and NuxtPage's ~transitionPromise
1100
+ compatibility: { nuxt: ">=3.18.0" }
707
1101
  },
708
1102
  defaults: {
709
1103
  locales: [],
@@ -724,11 +1118,8 @@ const module$1 = defineNuxtModule({
724
1118
  disablePageLocales: false,
725
1119
  disableWatcher: false,
726
1120
  noPrefixRedirect: false,
727
- fallbackLocale: void 0,
728
1121
  localeCookie: null,
729
1122
  apiBaseUrl: "_locales",
730
- apiBaseClientHost: void 0,
731
- apiBaseServerHost: void 0,
732
1123
  translationPayloads: {
733
1124
  mode: "premerged",
734
1125
  serverAssets: true,
@@ -740,20 +1131,18 @@ const module$1 = defineNuxtModule({
740
1131
  },
741
1132
  routesLocaleLinks: {},
742
1133
  globalLocaleRoutes: {},
743
- canonicalQueryWhitelist: void 0,
744
1134
  plural: defaultPlural,
745
- customRegexMatcher: void 0,
746
- excludePatterns: void 0,
747
1135
  localizedRouteNamePrefix: "localized-",
748
1136
  missingWarn: true,
749
1137
  metaTrustForwardedHost: true,
750
1138
  metaTrustForwardedProto: true,
1139
+ trustForwardedHost: false,
751
1140
  hreflangBaseLanguage: false,
752
1141
  httpCacheDuration: 31536e3
753
1142
  },
754
1143
  async setup(options, nuxt) {
755
- const defaultLocale = process.env.DEFAULT_LOCALE ?? options.defaultLocale ?? "en";
756
- const isSSG = Boolean(nuxt.options.nitro?.static);
1144
+ const defaultLocale = process.env.DEFAULT_LOCALE || options.defaultLocale || "en";
1145
+ let isSSG = false;
757
1146
  const logger = useLogger("nuxt-i18n-micro");
758
1147
  nuxt.options.typescript.hoist ||= [];
759
1148
  for (const pkg of ["@i18n-micro/types", "@i18n-micro/path-strategy"]) {
@@ -761,12 +1150,13 @@ const module$1 = defineNuxtModule({
761
1150
  nuxt.options.typescript.hoist.push(pkg);
762
1151
  }
763
1152
  }
764
- if (options.strategy === "no_prefix" && !options.localeCookie) {
1153
+ const cookieOptOut = nuxt.options.i18n?.localeCookie === null;
1154
+ if (options.strategy === "no_prefix" && !options.localeCookie && !cookieOptOut) {
765
1155
  options.localeCookie = "user-locale";
766
1156
  logger.info("Strategy 'no_prefix': localeCookie automatically set to 'user-locale' for locale persistence.");
767
1157
  }
768
1158
  if (options.strategy !== "no_prefix" && options.redirects !== false && !options.localeCookie) {
769
- logger.warn(
1159
+ logger.info(
770
1160
  "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
1161
  );
772
1162
  }
@@ -775,7 +1165,12 @@ const module$1 = defineNuxtModule({
775
1165
  const mergedLocalesDir = resolve(nuxt.options.buildDir, "i18n-merged");
776
1166
  const sourceLocalesDir = resolve(nuxt.options.buildDir, "i18n-source");
777
1167
  const translationDirName = options.translationDir || "locales";
778
- const translationsHash = options.dateBuild === void 0 ? hashTranslationSources(rootDirs, translationDirName, options.additionalTranslationDirs) : null;
1168
+ const translationsHash = options.dateBuild === void 0 ? hashTranslationSources(
1169
+ rootDirs,
1170
+ translationDirName,
1171
+ options.additionalTranslationDirs,
1172
+ JSON.stringify([options.fallbackLocale, options.disablePageLocales, (options.locales ?? []).map((l) => [l.code, l.fallbackLocale])])
1173
+ ) : null;
779
1174
  const translationPayloads = resolveTranslationPayloadOptions(options);
780
1175
  const publicAssetsDir = translationPayloads.mode === "source" ? sourceLocalesDir : mergedLocalesDir;
781
1176
  for (const warning of getTranslationPayloadMisconfigurationWarnings({
@@ -783,9 +1178,10 @@ const module$1 = defineNuxtModule({
783
1178
  apiBaseClientHost: options.apiBaseClientHost,
784
1179
  apiBaseServerHost: options.apiBaseServerHost
785
1180
  })) {
786
- logger.warn(warning.replace("[nuxt-i18n-micro] ", ""));
1181
+ logger.warn(warning);
787
1182
  }
788
- if (!translationPayloads.serverAssets && !translationPayloads.publicAssets && !(process.env.NUXT_I18N_APP_BASE_SERVER_HOST ?? options.apiBaseServerHost)) {
1183
+ const rawServerHost = process.env.NUXT_I18N_APP_BASE_SERVER_HOST ?? options.apiBaseServerHost ?? void 0;
1184
+ if (!translationPayloads.serverAssets && !translationPayloads.publicAssets && !rawServerHost) {
789
1185
  throw new Error(
790
1186
  "nuxt-i18n-micro: local SSR payloads are disabled (serverAssets/publicAssets false) and apiBaseServerHost is not set. SSR would load empty translations."
791
1187
  );
@@ -799,79 +1195,53 @@ const module$1 = defineNuxtModule({
799
1195
  const prev = localeInfosByCode.get(locale.code);
800
1196
  localeInfosByCode.set(locale.code, {
801
1197
  code: locale.code,
802
- fallbackLocale: locale.fallbackLocale ?? prev?.fallbackLocale,
803
- disabled: !!(prev?.disabled || locale.disabled)
1198
+ fallbackLocale: prev?.fallbackLocale ?? locale.fallbackLocale,
1199
+ disabled: prev?.disabled ?? locale.disabled
804
1200
  });
805
1201
  }
806
1202
  const localeInfos = [...localeInfosByCode.values()].filter((locale) => !locale.disabled).map(({ code, fallbackLocale }) => ({ code, fallbackLocale }));
1203
+ let routeNames = [];
807
1204
  nuxt.hook("build:before", async () => {
808
1205
  if (translationPayloads.mode === "source") {
809
1206
  await buildTranslationSourceLayers(rootDirs, translationDirName, sourceLocalesDir, options.additionalTranslationDirs);
810
1207
  logger.info(`Built compact translation source from ${rootDirs.length} layer(s) into ${sourceLocalesDir}`);
811
1208
  } else {
812
- await preMergeLocales(
1209
+ await preMergeLocales({
813
1210
  rootDirs,
814
1211
  translationDirName,
815
- mergedLocalesDir,
816
- localeInfos,
817
- options.fallbackLocale,
818
- options.disablePageLocales,
819
- options.additionalTranslationDirs
820
- );
1212
+ outputDir: mergedLocalesDir,
1213
+ locales: localeInfos,
1214
+ globalFallbackLocale: options.fallbackLocale,
1215
+ disablePageLocales: options.disablePageLocales,
1216
+ additionalTranslationDirs: options.additionalTranslationDirs,
1217
+ routeNames,
1218
+ // dev reads live files, and a prerender merges what it reads like any server
1219
+ rootDelta: !nuxt.options.dev && translationPayloads.rootDelta
1220
+ });
821
1221
  logger.info(`Pre-merged translations from ${rootDirs.length} layer(s) into ${mergedLocalesDir}`);
822
1222
  }
823
1223
  const payloadStats = scanTranslationPayloadDirectory(publicAssetsDir);
824
- const sizeWarning = getTranslationPayloadSizeWarning(payloadStats, resolveTranslationPayloadWarningThresholds(options.translationPayloads));
825
- if (sizeWarning) {
826
- logger.warn(sizeWarning.replace("[nuxt-i18n-micro] ", ""));
827
- }
1224
+ const sizeWarning = getTranslationPayloadSizeWarning(payloadStats, options.translationPayloads);
1225
+ if (sizeWarning) logger.warn(sizeWarning);
828
1226
  });
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();
1227
+ const pageFiles = (await Promise.all(rootDirs.map((cwd) => globby(["pages/**/*.vue", "app/pages/**/*.vue"], { cwd, absolute: true })))).flat();
1228
+ const {
1229
+ routeLocales,
1230
+ routeDisableMeta,
1231
+ globalLocaleRoutes: mergedGlobalLocaleRoutes
1232
+ } = mergeDefineI18nRouteMeta(collectDefineI18nRouteMetaFromFiles(pageFiles, rootDirs), options);
1233
+ const resolvedStrategyPath = await resolveEntry("@i18n-micro/path-strategy");
1234
+ const resolvedHelpersPath = await resolveEntry("@i18n-micro/core/helpers");
862
1235
  let apiBaseClientHost = process.env.NUXT_I18N_APP_BASE_CLIENT_HOST ?? options.apiBaseClientHost ?? void 0;
863
1236
  if (apiBaseClientHost?.endsWith("/")) {
864
1237
  apiBaseClientHost = apiBaseClientHost.slice(0, -1);
865
1238
  }
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
- }
1239
+ const apiBaseServerHost = rawServerHost?.endsWith("/") ? rawServerHost.slice(0, -1) : rawServerHost;
870
1240
  const rawUrl = process.env.NUXT_I18N_APP_BASE_URL ?? options.apiBaseUrl ?? "_locales";
871
1241
  if (rawUrl.startsWith("http://") || rawUrl.startsWith("https://")) {
872
1242
  throw new Error("Nuxt-i18n-micro: Please use NUXT_I18N_APP_BASE_CLIENT_HOST or NUXT_I18N_APP_BASE_SERVER_HOST instead.");
873
1243
  }
874
- const apiBaseUrl = rawUrl.replace(/^\/+|\/+$/g, "").replace(/\/{2,}/g, "/");
1244
+ const apiBaseUrl = pathSegments(rawUrl);
875
1245
  const apiConfig = { apiBaseUrl, apiBaseClientHost, apiBaseServerHost };
876
1246
  const payloadPublicRel = resolveTranslationPayloadPublicRel(options, apiBaseUrl);
877
1247
  const routeGenerator = new RouteGenerator({
@@ -879,31 +1249,50 @@ const module$1 = defineNuxtModule({
879
1249
  defaultLocaleCode: defaultLocale,
880
1250
  strategy: options.strategy,
881
1251
  globalLocaleRoutes: mergedGlobalLocaleRoutes,
882
- filesLocaleRoutes: globalLocaleRoutes,
883
1252
  routeLocales,
1253
+ routeDisableMeta,
884
1254
  noPrefixRedirect: options.noPrefixRedirect,
1255
+ redirects: options.redirects,
885
1256
  excludePatterns: options.excludePatterns,
1257
+ apiBaseUrl,
886
1258
  localizedRouteNamePrefix: options.localizedRouteNamePrefix,
887
- customRegexMatcher: options.customRegexMatcher
1259
+ customRegexMatcher: options.customRegexMatcher,
1260
+ parentPaths: parentPagePaths(pageFiles, rootDirs)
888
1261
  });
889
1262
  const fullConfig = buildFullConfig({
890
1263
  options,
891
1264
  nuxt,
892
1265
  defaultLocale,
893
- isSSG,
894
1266
  apiConfig,
895
1267
  translationPayloadMode: translationPayloads.mode,
896
- routeLocales,
897
- routeDisableMeta,
898
- mergedGlobalLocaleRoutes,
1268
+ publicPayloads: translationPayloads.mode === "premerged" && translationPayloads.publicAssets,
1269
+ rootDelta: !nuxt.options.dev && translationPayloads.mode === "premerged" && translationPayloads.rootDelta && !options.disablePageLocales && (translationPayloads.publicAssets || Boolean(options.apiBaseClientHost)),
899
1270
  locales: routeGenerator.locales ?? [],
900
1271
  translationsHash
901
1272
  });
902
- const privateConfig = buildPrivateConfig(options, nuxt, apiConfig, routeGenerator.locales ?? [], publicAssetsDir, payloadPublicRel);
903
- const templates = registerI18nTemplates(options, resolvedStrategyPath, resolvedHelpersPath, fullConfig, privateConfig);
1273
+ const generateDomain = process.env.NUXT_I18N_DOMAIN;
1274
+ const buildConfig = generateDomain ? configForHost(fullConfig, generateDomain) : fullConfig;
1275
+ if (generateDomain && !buildConfig.domain)
1276
+ logger.warn(`NUXT_I18N_DOMAIN "${generateDomain}" is no host of locales[].baseUrl: the build config applies.`);
1277
+ const siteUrl = nuxt.options.site?.url || process.env.NUXT_PUBLIC_SITE_URL || process.env.NUXT_SITE_URL;
1278
+ const originWarning = buildConfig.domain ? null : unfixedOriginWarning(routeGenerator.locales, defaultLocale, options.metaBaseUrl, siteUrl);
1279
+ if (originWarning) logger.warn(originWarning);
1280
+ const privateConfig = buildPrivateConfig(options, nuxt, rootDirs, apiConfig, routeGenerator.locales ?? [], publicAssetsDir, payloadPublicRel);
1281
+ const templates = registerI18nTemplates(
1282
+ options,
1283
+ resolvedStrategyPath,
1284
+ resolvedHelpersPath,
1285
+ fullConfig,
1286
+ privateConfig,
1287
+ routeGenerator.disabledLocales,
1288
+ nuxt.options.dev,
1289
+ nuxt.options.experimental.serverPathFallback === true && !nuxt.options.router?.options?.hashMode
1290
+ );
904
1291
  if (typeof options.customRegexMatcher !== "undefined") {
905
- const localeCodes = routeGenerator.locales.map((l) => l.code);
906
- const failedCodes = localeCodes.filter((code) => !code.match(options.customRegexMatcher));
1292
+ const failedCodes = localesOutsideMatcher(
1293
+ routeGenerator.locales.map((l) => l.code),
1294
+ options.customRegexMatcher
1295
+ );
907
1296
  if (failedCodes.length > 0) {
908
1297
  throw new Error(
909
1298
  "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 +1306,48 @@ const module$1 = defineNuxtModule({
917
1306
  name: "i18n-plugin-loader",
918
1307
  order: -5
919
1308
  });
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) {
1309
+ if (options.meta) {
1310
+ addPlugin({
1311
+ src: resolver.resolve("./runtime/plugins/02.meta"),
1312
+ name: "i18n-plugin-meta",
1313
+ order: 2
1314
+ });
1315
+ }
1316
+ if (nuxt.options.dev && options.vueDevtools !== false) {
1317
+ addPlugin({
1318
+ src: resolver.resolve("./runtime/plugins/04.vue-devtools"),
1319
+ name: "i18n-vue-devtools",
1320
+ mode: "client",
1321
+ order: 5
1322
+ });
1323
+ }
944
1324
  addPlugin({
945
- src: resolver.resolve("./runtime/plugins/04.vue-devtools"),
946
- name: "i18n-vue-devtools",
947
- mode: "client",
948
- order: 5
1325
+ src: resolver.resolve("./runtime/plugins/06.redirect"),
1326
+ mode: "server",
1327
+ name: "i18n-plugin-redirect",
1328
+ order: 10
949
1329
  });
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
1330
+ if (options.redirects !== false && options.strategy !== "no_prefix") {
1331
+ addRouteMiddleware({
1332
+ name: "i18n-redirect",
1333
+ path: resolver.resolve("./runtime/middleware/i18n-redirect.global"),
1334
+ global: true
1335
+ });
1336
+ }
1337
+ } else if (options.define) {
1338
+ addPluginTemplate({
1339
+ filename: "i18n.define.mjs",
1340
+ getContents: () => `import { defineNuxtPlugin } from '#imports'
1341
+ export default defineNuxtPlugin(() => ({ provide: { defineI18nRoute: async () => {} } }))
1342
+ `
962
1343
  });
963
1344
  }
964
- addServerImportsDir(resolver.resolve("./runtime/server/utils"));
1345
+ addServerImports([
1346
+ { name: "useLocaleServerMiddleware", from: resolver.resolve("./runtime/server/utils/locale-server-middleware") },
1347
+ { name: "useTranslationServerMiddleware", from: resolver.resolve("./runtime/server/utils/translation-server-middleware") },
1348
+ { name: "loadTranslationsFromServer", from: resolver.resolve("./runtime/server/utils/server-loader") },
1349
+ { name: "detectCurrentLocale", from: resolver.resolve("./runtime/server/utils/locale-detector") }
1350
+ ]);
965
1351
  if (translationPayloads.serverHandler) {
966
1352
  addServerHandler({
967
1353
  route: `/${apiBaseUrl}/:page/:locale/data.json`,
@@ -976,30 +1362,26 @@ const module$1 = defineNuxtModule({
976
1362
  });
977
1363
  }
978
1364
  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();
1365
+ const translationDirs = resolveLiveTranslationDirs({
1366
+ rootDirs,
1367
+ translationDir: translationDirName,
1368
+ additionalTranslationDirs: options.additionalTranslationDirs
1369
+ });
1370
+ addVitePlugin(
1371
+ {
1372
+ name: "i18n-micro:translations-hmr",
1373
+ hotUpdate({ file }) {
1374
+ if (this.environment.name !== "client" || !translationRelativePath(file, translationDirs)) return;
1375
+ this.environment.hot.send({ type: "custom", event: TRANSLATIONS_HMR_EVENT });
1376
+ return [];
1377
+ }
1378
+ },
1379
+ { server: false }
1380
+ );
996
1381
  const tpl = addTemplate({
997
1382
  filename: "i18n.hmr.mjs",
998
1383
  write: true,
999
- getContents: () => generateHmrPlugin(
1000
- files.map((f) => f.replace(/\\/g, "/")),
1001
- { additionalRootFiles }
1002
- )
1384
+ getContents: () => generateHmrPlugin()
1003
1385
  });
1004
1386
  addPlugin({
1005
1387
  src: tpl.dst,
@@ -1015,13 +1397,19 @@ const module$1 = defineNuxtModule({
1015
1397
  });
1016
1398
  addTypeTemplate({
1017
1399
  filename: "types/h3.d.ts",
1018
- getContents: () => `import type { Translations } from '@i18n-micro/types'
1400
+ getContents: () => `import type { ResolvedI18nConfig, Translations } from '@i18n-micro/types'
1019
1401
 
1020
1402
  declare module 'h3' {
1021
1403
  interface H3EventContext {
1022
1404
  i18n?: {
1023
1405
  locale: string
1024
1406
  translations: Translations
1407
+ /** 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. */
1408
+ load?: (locale: string, routeName: string) => Promise<Translations | null>
1409
+ /** The module config with runtime overrides, resolved once for this request. */
1410
+ config?: ResolvedI18nConfig
1411
+ /** A dictionary of this page render could not be read: no cache may keep the page. */
1412
+ failedRead?: boolean
1025
1413
  }
1026
1414
  }
1027
1415
  }
@@ -1033,60 +1421,77 @@ export {}
1033
1421
  addTypeTemplate({
1034
1422
  filename: "types/i18n-internal.d.ts",
1035
1423
  getContents: () => {
1036
- return `import type { ModuleOptionsExtend, ModulePrivateOptionsExtend, Params, Getter, PluralFunc } from '@i18n-micro/types'
1424
+ const strategyDeclaration = `
1425
+ export const domains: boolean
1426
+ export const cookies: boolean
1427
+ export const hooks: boolean
1428
+ export const defineRoute: boolean
1429
+ export const serverPathFallback: boolean
1430
+ export function getI18nConfig(): ResolvedI18nConfig
1431
+ export function createI18nStrategy(router: I18nStrategyRouter, config?: ModuleOptionsExtend): PathStrategy
1432
+ `;
1433
+ return `import type { ModuleOptionsExtend, ModulePrivateOptionsExtend, Params, Getter, PluralFunc, ResolvedI18nConfig } from '@i18n-micro/types'
1037
1434
  import type { PathStrategy } from '@i18n-micro/path-strategy'
1435
+ import type { Router } from 'vue-router'
1436
+
1437
+ /** The app's vue-router: createI18nStrategy reads hasRoute, resolve and getRoutes from it. */
1438
+ type I18nStrategyRouter = Pick<Router, 'hasRoute' | 'resolve' | 'getRoutes'>
1038
1439
 
1039
1440
  declare module '#build/i18n.plural.mjs' {
1040
1441
  export function plural(key: string, count: number, params: Params, locale: string, getter: Getter): string | null
1041
1442
  }
1042
1443
 
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
- }
1444
+ declare module '#build/i18n.strategy.mjs' {${strategyDeclaration}}
1047
1445
 
1048
1446
  declare module '#i18n-internal/config' {
1049
1447
  export function getI18nPrivateConfig(): ModulePrivateOptionsExtend
1050
1448
  }
1051
1449
 
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
- }
1450
+ declare module '#i18n-internal/strategy' {${strategyDeclaration}}
1056
1451
 
1057
1452
  declare module '#i18n-internal/plural' {
1058
1453
  export const plural: PluralFunc
1059
1454
  }
1060
1455
 
1061
1456
  declare module '#i18n-internal/payload-source' {
1457
+ export const embeddedPayloads: boolean
1062
1458
  export function readPayload(relPath: string): Promise<Record<string, unknown>>
1063
1459
  }
1064
1460
  `;
1065
1461
  }
1066
1462
  });
1463
+ let generatedPages = [];
1464
+ let staticPaths = [];
1465
+ const createBuildStrategy = () => createLocalizedPathResolver(generatedPages, {
1466
+ strategy: options.strategy,
1467
+ defaultLocale: buildConfig.defaultLocale,
1468
+ locales: buildConfig.locales,
1469
+ domain: buildConfig.domain,
1470
+ linkLocales: buildConfig.linkLocales,
1471
+ localizedRouteNamePrefix: options.localizedRouteNamePrefix,
1472
+ customRegexMatcher: fullConfig.customRegexMatcher
1473
+ });
1474
+ const unlinked = (name) => !options.routesLocaleLinks?.[name];
1067
1475
  const addDataRoutes = (pages = []) => {
1068
1476
  if (!translationPayloads.prerenderRoutes) return;
1069
- const pagesForDataRoutes = pages.filter((p) => p.name !== void 0 && (!options.routesLocaleLinks || !options.routesLocaleLinks[p.name]));
1477
+ const pagesForDataRoutes = pages.filter((p) => p.name !== void 0 && unlinked(p.name));
1070
1478
  const dataRoutes = routeGenerator.generateDataRoutes(pagesForDataRoutes, apiBaseUrl, !!options.disablePageLocales);
1071
1479
  addPrerenderRoutes(dataRoutes);
1072
1480
  };
1073
1481
  nuxt.hook("pages:resolved", (pages) => {
1074
- const pagesNames = pages.map((page) => page.name).filter((name) => name !== void 0 && (!options.routesLocaleLinks || !options.routesLocaleLinks[name]));
1482
+ const pagesNames = pages.map((page) => page.name).filter((name) => name !== void 0 && unlinked(name));
1075
1483
  if (!options.disableWatcher) {
1076
- routeGenerator.ensureTranslationFilesExist(pagesNames, options.translationDir, nuxt.options.rootDir, options.disablePageLocales);
1484
+ routeGenerator.ensureTranslationFilesExist(pagesNames, translationDirName, nuxt.options.rootDir, options.disablePageLocales);
1077
1485
  }
1078
1486
  addDataRoutes(pages);
1487
+ routeNames = pageNames(pages).filter(unlinked);
1488
+ staticPaths = staticPagePaths(pages);
1079
1489
  routeGenerator.extendPages(pages);
1490
+ generatedPages = pages;
1080
1491
  });
1081
1492
  if (options.disablePageLocales) {
1082
1493
  nuxt.hook("build:before", () => addDataRoutes([]));
1083
1494
  }
1084
- addVitePlugin(
1085
- createDefineI18nRoutePlugin({
1086
- buildDir: nuxt.options.buildDir,
1087
- rootDirs
1088
- }).vite()
1089
- );
1090
1495
  const { pluralTemplate, strategyTemplate, configTemplate } = templates;
1091
1496
  nuxt.hook("nitro:config", (nitroConfig) => {
1092
1497
  nitroConfig.alias = nitroConfig.alias || {};
@@ -1095,39 +1500,35 @@ declare module '#i18n-internal/payload-source' {
1095
1500
  nitroConfig.alias["#i18n-internal/config"] = configTemplate.dst;
1096
1501
  nitroConfig.alias["#i18n-internal/payload-source"] = resolver.resolve("./runtime/server/payload-source.fs");
1097
1502
  nitroConfig.plugins = nitroConfig.plugins || [];
1503
+ nitroConfig.plugins.push(resolver.resolve("./runtime/server/plugins/failed-read"));
1504
+ if (!nuxt.options.dev) nitroConfig.plugins.push(resolver.resolve("./runtime/server/plugins/payload-cache"));
1098
1505
  if (nuxt.options.dev && (options.hmr ?? true)) {
1099
1506
  nitroConfig.plugins.push(resolver.resolve("./runtime/server/plugins/watcher.dev"));
1100
1507
  }
1101
1508
  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);
1509
+ const { file: cacheControl } = resolveTranslationPayloadCachePolicy({
1510
+ httpCacheDuration: options.httpCacheDuration,
1511
+ httpCacheControl: options.httpCacheControl,
1512
+ dateBuild: fullConfig.dateBuild,
1513
+ live: isLiveDictionary(options, translationPayloads.mode, rawServerHost)
1514
+ });
1105
1515
  nitroConfig.routeRules[`/${apiBaseUrl}/**`] = {
1106
1516
  ...nitroConfig.routeRules[`/${apiBaseUrl}/**`] || {},
1107
- ...buildTranslationPayloadRouteRule({ dev: nuxt.options.dev, hasCacheBuster, cacheControl })
1517
+ ...buildTranslationPayloadRouteRule({ dev: nuxt.options.dev, cacheControl })
1108
1518
  };
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
- });
1519
+ const noDocuments = Boolean(buildConfig.hashMode) && options.strategy === "prefix";
1520
+ localizeRouteRules(
1521
+ nuxt.options.routeRules || {},
1522
+ nitroConfig.routeRules,
1523
+ routeGenerator,
1524
+ (path, rule) => {
1525
+ logger.debug(`Replicated routeRule for ${path}: ${JSON.stringify(rule)}`);
1526
+ },
1527
+ noDocuments
1528
+ );
1529
+ if (routeGenerator.locales.some((locale) => locale.baseUrl)) {
1530
+ for (const [path, rule] of Object.entries(nitroConfig.routeRules)) {
1531
+ nitroConfig.routeRules[path] = varyRouteCacheByHost(rule, Boolean(options.trustForwardedHost));
1131
1532
  }
1132
1533
  }
1133
1534
  nitroConfig.handlers = nitroConfig.handlers || [];
@@ -1137,10 +1538,30 @@ declare module '#i18n-internal/payload-source' {
1137
1538
  });
1138
1539
  });
1139
1540
  nuxt.hook("nitro:init", (nitro) => {
1140
- if (nitro.options.static) return;
1541
+ if (buildConfig.domain) {
1542
+ let localize;
1543
+ nitro.hooks.hook("prerender:generate", async (route) => {
1544
+ localize ??= await createBuildStrategy();
1545
+ if (route.route !== "/" && localize.isPage(route.route) && !localize.isServed(route.route)) route.skip = true;
1546
+ });
1547
+ }
1548
+ isSSG = Boolean(nitro.options.static);
1549
+ if (isSSG) {
1550
+ if (!generateDomain && routeGenerator.locales.some((locale) => locale.baseUrl)) {
1551
+ logger.warn(
1552
+ "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."
1553
+ );
1554
+ }
1555
+ const staticWarning = getStaticPayloadWarning({ translationPayloads, apiBaseClientHost });
1556
+ if (staticWarning) logger.warn(staticWarning);
1557
+ return;
1558
+ }
1141
1559
  const readsPublicDir = shouldReadPayloadsFromPublicDir(nitro.options);
1142
1560
  if (!readsPublicDir) {
1143
1561
  nitro.options.alias["#i18n-internal/payload-source"] = resolver.resolve("./runtime/server/payload-source.assets");
1562
+ if (options.cacheStorage && !apiBaseServerHost) {
1563
+ logger.info("cacheStorage: dictionaries are embedded on this preset, so the shared cache only applies with apiBaseServerHost.");
1564
+ }
1144
1565
  }
1145
1566
  if (shouldRegisterNitroServerAssets(translationPayloads, readsPublicDir)) {
1146
1567
  nitro.options.serverAssets.push({
@@ -1148,11 +1569,6 @@ declare module '#i18n-internal/payload-source' {
1148
1569
  // Same layout as public (`mode`): premerged matrix or compact source.
1149
1570
  dir: publicAssetsDir
1150
1571
  });
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
1572
  }
1157
1573
  if (readsPublicDir && translationPayloads.serverAssets && !translationPayloads.publicAssets) {
1158
1574
  logger.info("translationPayloads.serverAssets: on Node, payloads are served from public/ (publicAssets copy enabled for SSR).");
@@ -1162,11 +1578,22 @@ declare module '#i18n-internal/payload-source' {
1162
1578
  const isProd = nuxt.options.dev === false;
1163
1579
  const readsPublicDir = shouldReadPayloadsFromPublicDir(nitro.options);
1164
1580
  const copyPublicPayloads = shouldCopyTranslationPayloadsToPublic(translationPayloads, readsPublicDir);
1581
+ if (isProd && !isSSG && shouldRegisterNitroServerAssets(translationPayloads, readsPublicDir)) {
1582
+ const sizeWarning = getEmbeddedPayloadSizeWarning(nitro.options.preset, publicAssetsDir, translationPayloads.mode);
1583
+ if (sizeWarning) logger.warn(sizeWarning);
1584
+ }
1165
1585
  if (isProd && copyPublicPayloads) {
1166
1586
  const publicDir = resolveTranslationPayloadPublicDir(nitro.options.output.publicDir, options, apiBaseUrl);
1167
1587
  try {
1168
1588
  if (existsSync(publicAssetsDir)) {
1169
- fs__default.cpSync(publicAssetsDir, publicDir, { recursive: true });
1589
+ const filter = domainPayloadFilter({
1590
+ mode: translationPayloads.mode,
1591
+ root: publicAssetsDir,
1592
+ served: buildConfig.locales,
1593
+ linkLocales: buildConfig.linkLocales,
1594
+ fallbackLocale: options.fallbackLocale
1595
+ });
1596
+ fs__default.cpSync(publicAssetsDir, publicDir, { recursive: true, filter });
1170
1597
  logger.log(`Translation payloads copied to public directory`);
1171
1598
  const compressed = compressTranslationPayloads(publicDir, nitro.options.compressPublicAssets);
1172
1599
  if (compressed > 0) {
@@ -1184,73 +1611,21 @@ declare module '#i18n-internal/payload-source' {
1184
1611
  if (isNoPrefixStrategy(options.strategy)) {
1185
1612
  return;
1186
1613
  }
1187
- const routesSet = prerenderRoutes.routes;
1188
1614
  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);
1615
+ const { added, removed } = seedPrerenderRoutes(prerenderRoutes.routes, {
1616
+ // the same router and strategy the app runs: custom paths, `false` pages and routeLocales as generated
1617
+ localize: await createBuildStrategy(),
1618
+ locales: buildConfig.locales,
1619
+ staticPaths: isSSG ? staticPaths : null,
1620
+ strategy: options.strategy,
1621
+ prerendered: (route) => routeRules[route]?.prerender !== false,
1622
+ isInternal: (route) => isInternalPath(route, options.excludePatterns, apiBaseUrl)
1623
+ });
1624
+ if (added.length > 0) {
1625
+ logger.debug("[i18n prerender:routes] added localized routes:", [...added].sort().join(", "));
1240
1626
  }
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
- }
1627
+ if (removed.length > 0) {
1628
+ logger.info(`[i18n prerender:routes] removed from prerender list (no locale prefix): ${removed.join(", ")}`);
1254
1629
  }
1255
1630
  });
1256
1631
  const devtoolsOptions = nuxt.options.devtools;