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
@@ -1,43 +1,27 @@
1
- import { getEnabledLocaleCodes } from "@i18n-micro/utils/active-locales";
2
1
  import { isInternalPath, withoutAppBaseURL } from "@i18n-micro/utils/app-path";
3
- import { getLocaleCookieName } from "@i18n-micro/utils/cookie";
4
- import { resolveServerLocale } from "@i18n-micro/utils/resolve-locale";
5
- import { resolveI18nConfigWithRuntimeOverrides } from "@i18n-micro/utils/runtime-config";
6
- import { defineEventHandler, getCookie, getHeader, getQuery, getRequestURL } from "h3";
2
+ import { defineEventHandler } from "h3";
7
3
  import { getI18nPrivateConfig } from "#i18n-internal/config";
8
- import { getI18nConfig } from "#i18n-internal/strategy";
9
- import { useRuntimeConfig } from "#imports";
10
- import { loadTranslationsFromServer } from "../utils/server-loader.js";
4
+ import { domains } from "#i18n-internal/strategy";
5
+ import { useRuntimeConfig } from "nitropack/runtime";
6
+ import { requestURL } from "../../utils/request-url.js";
7
+ import { detectRequestLocale, getI18nServerConfig } from "../utils/locale-detector.js";
8
+ import { loadTranslationsFromServer, NO_FILE } from "../utils/server-loader.js";
9
+ async function load(locale, routeName) {
10
+ const entry = await loadTranslationsFromServer(locale, routeName);
11
+ return entry === NO_FILE ? null : entry.data;
12
+ }
11
13
  export default defineEventHandler(async (event) => {
12
- const appBaseURL = useRuntimeConfig(event).app.baseURL;
13
- const rawPath = getRequestURL(event).pathname;
14
- const path = withoutAppBaseURL(rawPath, appBaseURL);
15
- if (isInternalPath(path)) return;
16
- const config = resolveI18nConfigWithRuntimeOverrides(
17
- getI18nConfig(),
18
- useRuntimeConfig(event).public
19
- );
20
- const privateConfig = getI18nPrivateConfig();
21
- const validLocales = getEnabledLocaleCodes(config.locales);
22
- const defaultLocale = config.defaultLocale || "en";
23
- const pathSegments = path.split("/").filter(Boolean);
24
- const firstSegment = pathSegments[0];
25
- const hasLocaleInUrl = Boolean(firstSegment && validLocales.includes(firstSegment));
26
- const cookieName = getLocaleCookieName(config);
27
- const queryLocale = getQuery(event).locale ? String(getQuery(event).locale) : null;
28
- const cookieLocale = cookieName ? getCookie(event, cookieName) : null;
29
- const locale = resolveServerLocale({
30
- defaultLocale,
31
- validLocales,
32
- autoDetectLanguage: config.autoDetectLanguage,
33
- hasLocaleInUrl,
34
- urlLocale: firstSegment ?? null,
35
- queryLocale,
36
- cookieLocale,
37
- acceptLanguageHeader: getHeader(event, "accept-language")
38
- });
14
+ const config = getI18nServerConfig(event);
15
+ const path = withoutAppBaseURL(requestURL(event).pathname, useRuntimeConfig().app.baseURL);
16
+ const { serverTranslationPreload } = getI18nPrivateConfig();
17
+ if (isInternalPath(path, void 0, config.apiBaseUrl)) {
18
+ event.context.i18n = domains ? { load, config } : { load };
19
+ return;
20
+ }
21
+ if (config.hashMode && config.strategy === "prefix" && path === "/") event.context.nuxt = { ...event.context.nuxt, noSSR: true };
22
+ const locale = detectRequestLocale(event, config, "page", path);
39
23
  let translations = {};
40
- if (privateConfig.serverTranslationPreload) {
24
+ if (serverTranslationPreload) {
41
25
  try {
42
26
  const loaded = await loadTranslationsFromServer(locale, "index");
43
27
  translations = loaded.data;
@@ -47,6 +31,9 @@ export default defineEventHandler(async (event) => {
47
31
  }
48
32
  event.context.i18n = {
49
33
  locale,
50
- translations
34
+ translations,
35
+ load,
36
+ // the render reuses this request's resolved config (runtime overrides applied once)
37
+ config
51
38
  };
52
39
  });
@@ -1 +1,2 @@
1
+ export declare const embeddedPayloads = true;
1
2
  export declare function readPayload(relPath: string): Promise<Record<string, unknown>>;
@@ -1,18 +1,13 @@
1
1
  import { resolveContainedRelPath } from "@i18n-micro/utils/app-path";
2
2
  import { useStorage } from "nitropack/runtime";
3
+ export const embeddedPayloads = true;
3
4
  export async function readPayload(relPath) {
4
5
  const safeRel = resolveContainedRelPath(relPath);
5
6
  if (!safeRel) return {};
6
- const key = safeRel.replace(/\//g, ":");
7
+ const key = safeRel.split("/").join(":");
7
8
  const value = await useStorage().getItem(`assets:i18n:${key}`);
8
9
  if (value === null || value === void 0) return {};
9
- if (typeof value === "string") {
10
- try {
11
- return JSON.parse(value);
12
- } catch {
13
- return {};
14
- }
15
- }
10
+ if (typeof value === "string") return JSON.parse(value);
16
11
  if (typeof value === "object") return value;
17
12
  return {};
18
13
  }
@@ -1 +1,2 @@
1
+ export declare const embeddedPayloads = false;
1
2
  export declare function readPayload(relPath: string): Promise<Record<string, unknown>>;
@@ -1,8 +1,11 @@
1
- import { readFile } from "node:fs/promises";
1
+ import { access, readFile } from "node:fs/promises";
2
2
  import { dirname, join } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { resolveContainedRelPath } from "@i18n-micro/utils/app-path";
5
5
  import { getI18nPrivateConfig } from "#i18n-internal/config";
6
+ export const embeddedPayloads = false;
7
+ const MISSING_FILE = /* @__PURE__ */ new Set(["ENOENT", "ENOTDIR", "ENAMETOOLONG"]);
8
+ let treeChecked = false;
6
9
  function baseDir() {
7
10
  const cfg = getI18nPrivateConfig();
8
11
  if (import.meta.dev || import.meta.prerender) {
@@ -10,17 +13,25 @@ function baseDir() {
10
13
  }
11
14
  return join(dirname(fileURLToPath(import.meta.url)), "..", "public", cfg.payloadPublicRel);
12
15
  }
16
+ async function reportMissingTree(dir) {
17
+ if (treeChecked) return;
18
+ treeChecked = true;
19
+ await access(dir).catch(
20
+ () => console.error(
21
+ `[nuxt-i18n-micro] ${dir} does not exist: public/ is not deployed next to the server, so pages render raw keys. Build with nitro.serveStatic: false to embed the dictionaries.`
22
+ )
23
+ );
24
+ }
13
25
  export async function readPayload(relPath) {
14
26
  const rel = resolveContainedRelPath(relPath);
15
27
  if (!rel) return {};
16
- const path = join(baseDir(), rel);
28
+ const dir = baseDir();
17
29
  try {
18
- return JSON.parse(await readFile(path, "utf8"));
30
+ return JSON.parse(await readFile(join(dir, rel), "utf8"));
19
31
  } catch (error) {
20
- const err = error;
21
- if (err.code !== "ENOENT") {
22
- console.error(`[nuxt-i18n-micro] cannot read ${relPath} from ${baseDir()}:`, error);
23
- }
32
+ const { code } = error;
33
+ if (!MISSING_FILE.has(code)) throw error;
34
+ if (code === "ENOENT") await reportMissingTree(dir);
24
35
  return {};
25
36
  }
26
37
  }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * A page rendered with a dictionary that could not be read keeps its HTML, and the browser reads the dictionary again
3
+ * on hydration. A 200 would be kept by a route rule and a CDN for their TTL, raw keys included.
4
+ * An island has no `render:response`: it stays a 200, as a 503 would take it out of the page that embeds it.
5
+ */
6
+ declare const _default: import("nitropack/types").NitroAppPlugin;
7
+ export default _default;
@@ -0,0 +1,23 @@
1
+ import { defineNitroPlugin } from "nitropack/runtime";
2
+ import { getResponseHeader, removeResponseHeader, setResponseHeaders } from "h3";
3
+ const MARK = "x-i18n-failed-read";
4
+ const NOT_KEPT = {
5
+ "Cache-Control": "no-store",
6
+ "CDN-Cache-Control": "no-store",
7
+ "Cloudflare-CDN-Cache-Control": "no-store",
8
+ "Netlify-CDN-Cache-Control": "no-store",
9
+ "Vercel-CDN-Cache-Control": "no-store",
10
+ "Surrogate-Control": "no-store"
11
+ };
12
+ export default defineNitroPlugin((nitroApp) => {
13
+ nitroApp.hooks.hook("render:response", (response, { event }) => {
14
+ if (!event.context.i18n?.failedRead || (response.statusCode ?? 200) !== 200) return;
15
+ response.statusCode = 503;
16
+ response.headers = { ...response.headers, [MARK]: "1" };
17
+ });
18
+ nitroApp.hooks.hook("beforeResponse", (event) => {
19
+ if (!getResponseHeader(event, MARK)) return;
20
+ removeResponseHeader(event, MARK);
21
+ setResponseHeaders(event, { ...NOT_KEPT, "Retry-After": "60" });
22
+ });
23
+ });
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Nitro's static handler ignores the query, and answers before any middleware. The response of a payload file takes
3
+ * its header here, on its way out: only what serves `public/` from this process gets one, a host stays on the route rule.
4
+ */
5
+ declare const _default: import("nitropack/types").NitroAppPlugin;
6
+ export default _default;
@@ -0,0 +1,16 @@
1
+ import { joinURL } from "ufo";
2
+ import { defineNitroPlugin, useRuntimeConfig } from "nitropack/runtime";
3
+ import { removeResponseHeader, setResponseHeader } from "h3";
4
+ import { getI18nServerConfig } from "../utils/locale-detector.js";
5
+ import { payloadCacheControl } from "../utils/payload-cache.js";
6
+ export default defineNitroPlugin((nitroApp) => {
7
+ let prefix;
8
+ nitroApp.hooks.hook("beforeResponse", (event) => {
9
+ prefix ??= `${joinURL(useRuntimeConfig().app.baseURL, getI18nServerConfig().apiBaseUrl)}/`;
10
+ if (!event.path.startsWith(prefix) || event.node.res.statusCode >= 400) return;
11
+ removeResponseHeader(event, "Content-Length");
12
+ const config = getI18nServerConfig(event);
13
+ const cacheControl = config.httpCacheControl !== false && payloadCacheControl(event, config);
14
+ if (cacheControl) setResponseHeader(event, "Cache-Control", cacheControl);
15
+ });
16
+ });
@@ -1,114 +1,50 @@
1
- import { existsSync, readdirSync } from "node:fs";
2
- import path from "node:path";
3
- import { STORAGE_CC_KEY } from "@i18n-micro/hmr/cache-keys";
1
+ import { existsSync } from "node:fs";
2
+ import { STORAGE_CC_KEY } from "../../utils/cache-keys.js";
4
3
  import {
5
- createTranslationDirReader,
6
4
  handleTranslationWatchChange,
7
5
  resolveLiveTranslationDirs,
8
- TranslationContentTracker
9
- } from "@i18n-micro/hmr/watcher";
10
- import { parseTranslationRelativePath } from "@i18n-micro/utils/parse-path";
6
+ TranslationContentTracker,
7
+ translationRelativePath
8
+ } from "../internal/translation-watcher.js";
11
9
  import { watch } from "chokidar";
12
10
  import { defineNitroPlugin } from "nitropack/runtime";
13
11
  import { getI18nPrivateConfig } from "#i18n-internal/config";
14
- import { getI18nConfig } from "#i18n-internal/strategy";
15
- import { getOrCreateServerTranslationCache } from "../utils/server-translation-cache.js";
16
- function getCacheByKey(key) {
17
- const g = globalThis;
18
- const cc = g[key];
19
- if (cc && typeof cc === "object" && "keys" in cc && "set" in cc) {
20
- return cc;
21
- }
22
- return null;
23
- }
24
- function getStorageCache() {
25
- return getCacheByKey(STORAGE_CC_KEY);
26
- }
12
+ import { getOrCreateServerTranslationCache } from "../internal/server-translation-cache.js";
27
13
  let watcherInstance = null;
28
14
  export default defineNitroPlugin((nitroApp) => {
29
15
  if (watcherInstance) {
30
16
  return;
31
17
  }
32
18
  const i18nConfig = getI18nPrivateConfig();
33
- if (process.env.NODE_ENV !== "development") {
34
- return;
35
- }
36
19
  const log = (...args) => i18nConfig.debug && console.log("[i18n-hmr]", ...args);
37
20
  const warn = (...args) => i18nConfig.debug && console.warn("[i18n-hmr]", ...args);
38
- const rawLocales = i18nConfig.locales;
39
- const configuredLocales = new Set(
40
- (Array.isArray(rawLocales) ? rawLocales : []).map((l) => l.code).filter((code) => typeof code === "string" && code.length > 0)
41
- );
42
- const routesLocaleLinks = i18nConfig.routesLocaleLinks || {};
21
+ const configuredLocales = new Set((i18nConfig.locales ?? []).map((l) => l.code));
43
22
  const { translationRoots, additionalRoots } = resolveLiveTranslationDirs({
44
- rootDirs: i18nConfig.rootDirs?.length ? i18nConfig.rootDirs : [i18nConfig.rootDir],
23
+ rootDirs: i18nConfig.rootDirs,
45
24
  translationDir: i18nConfig.translationDir,
46
25
  additionalTranslationDirs: i18nConfig.additionalTranslationDirs
47
26
  });
48
27
  log(`Watching for translation changes in: ${translationRoots.join(", ")}`, additionalRoots.length ? `(+${additionalRoots.length} additional)` : "");
49
28
  const contentTracker = new TranslationContentTracker();
50
- const readMergedLocaleFile = createTranslationDirReader(translationRoots, additionalRoots);
51
- const toWatchRelativePath = (filePath) => {
52
- for (const translationsRoot of translationRoots) {
53
- const fromPrimary = path.relative(translationsRoot, filePath);
54
- if (!fromPrimary.startsWith("..") && !path.isAbsolute(fromPrimary)) {
55
- return fromPrimary.replace(/\\/g, "/");
56
- }
57
- }
58
- for (const extraRoot of additionalRoots) {
59
- const fromExtra = path.relative(extraRoot, filePath);
60
- if (fromExtra.startsWith("..") || path.isAbsolute(fromExtra)) continue;
61
- const normalized = fromExtra.replace(/\\/g, "/");
62
- if (!normalized.includes("/") && normalized.endsWith(".json")) return normalized;
63
- }
64
- return null;
65
- };
66
29
  const invalidateAndRefresh = async (filePath, event) => {
67
- const relativePath = toWatchRelativePath(filePath);
30
+ const relativePath = translationRelativePath(filePath, { translationRoots, additionalRoots });
68
31
  if (!relativePath) return;
69
32
  if (event === "unlink") {
70
33
  contentTracker.forget(filePath);
71
34
  } else if (!contentTracker.shouldProcess(filePath)) {
72
35
  return;
73
36
  }
74
- const runtimeConfig = getI18nConfig();
75
37
  try {
76
- const result = await handleTranslationWatchChange({
38
+ const change = await handleTranslationWatchChange({
77
39
  relativePath,
78
40
  configuredLocales,
79
- listPageNames: () => {
80
- const names = /* @__PURE__ */ new Set();
81
- for (const translationsRoot of translationRoots) {
82
- const pagesDir = path.join(translationsRoot, "pages");
83
- if (!existsSync(pagesDir)) continue;
84
- for (const entry of readdirSync(pagesDir, { withFileTypes: true })) {
85
- if (entry.isDirectory()) names.add(entry.name);
86
- }
87
- }
88
- return [...names];
89
- },
90
41
  serverCache: getOrCreateServerTranslationCache(),
91
- storageCache: getStorageCache(),
92
- routesLocaleLinks,
93
- mergeInput: {
94
- locales: runtimeConfig.locales,
95
- fallbackLocale: runtimeConfig.fallbackLocale,
96
- disablePageLocales: runtimeConfig.disablePageLocales,
97
- readLocaleFile: readMergedLocaleFile
98
- }
42
+ storageCache: globalThis[STORAGE_CC_KEY]
99
43
  });
100
- if (result === "root") {
101
- const parsed = parseTranslationRelativePath(relativePath);
102
- if (parsed.type === "root") {
103
- log(`Re-merged ALL pages for locale '${parsed.locale}'`);
104
- }
105
- } else if (result === "page") {
106
- log(`Re-merged page cache for '${relativePath}'`);
107
- } else if (result === "ignored") {
108
- const parsed = parseTranslationRelativePath(relativePath);
109
- if (parsed.type === "root" && !configuredLocales.has(parsed.locale)) {
110
- warn(`Detected ${event} for '${relativePath}', but locale '${parsed.locale}' is not in i18n.locales. Update config and restart dev server.`);
111
- }
44
+ if (change.dropped) {
45
+ log(`Dropped cached translations after a change to '${change.type === "root" ? change.locale : relativePath}'`);
46
+ } else if (change.type === "root") {
47
+ warn(`Detected ${event} for '${relativePath}', but locale '${change.locale}' is not in i18n.locales. Update config and restart dev server.`);
112
48
  }
113
49
  } catch (e) {
114
50
  contentTracker.forget(filePath);
@@ -1,29 +1,28 @@
1
1
  import { isEnabledLocale } from "@i18n-micro/utils/active-locales";
2
- import { buildTranslationPayloadCacheControl } from "@i18n-micro/utils/payload-url";
3
- import { resolveI18nConfigWithRuntimeOverrides } from "@i18n-micro/utils/runtime-config";
4
- import { createError, defineEventHandler, getQuery, getRouterParam, send, setResponseHeader } from "h3";
5
- import { getI18nConfig } from "#i18n-internal/strategy";
6
- import { useRuntimeConfig } from "#imports";
7
- import { loadTranslationsFromServer } from "../utils/server-loader.js";
2
+ import { createError, defineEventHandler, getRouterParam, send, setResponseHeader } from "h3";
3
+ import { getI18nServerConfig } from "../utils/locale-detector.js";
4
+ import { payloadCacheControl } from "../utils/payload-cache.js";
5
+ import { loadTranslationsFromServer, NO_FILE } from "../utils/server-loader.js";
8
6
  export default defineEventHandler(async (event) => {
9
7
  const page = getRouterParam(event, "page");
10
8
  const locale = getRouterParam(event, "locale");
11
9
  if (!locale || !page) {
12
10
  throw createError({ statusCode: 400, statusMessage: "Missing locale or page" });
13
11
  }
14
- const config = resolveI18nConfigWithRuntimeOverrides(
15
- getI18nConfig(),
16
- useRuntimeConfig(event).public
17
- );
12
+ const config = getI18nServerConfig(event);
18
13
  if (!isEnabledLocale(config.locales, locale)) {
19
14
  throw createError({ statusCode: 404, statusMessage: "Locale not found" });
20
15
  }
21
- const { json } = await loadTranslationsFromServer(locale, page);
16
+ const entry = await loadTranslationsFromServer(locale, page).catch((cause) => {
17
+ console.warn("[i18n] Translation load failed:", locale, page);
18
+ setResponseHeader(event, "Cache-Control", "no-store");
19
+ throw createError({ statusCode: 503, statusMessage: "Translations unavailable", cause });
20
+ });
21
+ if (entry === NO_FILE) throw createError({ statusCode: 404, statusMessage: "No such file" });
22
+ const { json } = entry;
22
23
  setResponseHeader(event, "Content-Type", "application/json; charset=utf-8");
23
24
  if (!import.meta.dev) {
24
- const requested = getQuery(event).v;
25
- const versioned = Boolean(config.dateBuild) && String(requested ?? "") === String(config.dateBuild);
26
- const cacheControl = buildTranslationPayloadCacheControl(config.httpCacheDuration, versioned);
25
+ const cacheControl = payloadCacheControl(event, config, json === "{}");
27
26
  if (cacheControl) {
28
27
  setResponseHeader(event, "Cache-Control", cacheControl);
29
28
  }
@@ -1,4 +1,27 @@
1
+ import type { ResolvedI18nConfig } from '@i18n-micro/types';
1
2
  import type { H3Event } from 'h3';
3
+ /**
4
+ * The module config with runtime overrides (`runtimeConfig.public.i18nRuntime`, then env), resolved
5
+ * once: every caller of a request, and every request with the same overrides and host, gets the
6
+ * same object (so override warnings are logged once). Read-only: deep-frozen in dev.
7
+ */
8
+ export declare function getI18nServerConfig(event?: H3Event): ResolvedI18nConfig;
9
+ type DetectionConfig = Pick<ResolvedI18nConfig, 'defaultLocale' | 'localeCookie' | 'autoDetectLanguage' | 'strategy' | 'hashMode'> & {
10
+ locales?: {
11
+ code: string;
12
+ disabled?: boolean;
13
+ }[];
14
+ };
15
+ /**
16
+ * - `page`: what the page render shows. The URL decides wherever the router does: a prefix, or the
17
+ * default locale for an unprefixed URL under prefix_except_default and prefix_and_default (a cookie
18
+ * must not render `de` on the `en` page). `?locale=` counts only under no_prefix, where no URL
19
+ * locale exists.
20
+ * - `api`: server API helpers — `?locale=`, a `[locale]` route param or prefix, cookie, header.
21
+ */
22
+ export type RequestLocaleMode = 'page' | 'api';
23
+ /** The one server detection: i18n.global stores it in `event.context.i18n.locale`, the helpers reuse it. */
24
+ export declare function detectRequestLocale(event: H3Event, config: DetectionConfig, mode: RequestLocaleMode, path?: string): string;
2
25
  export interface DetectCurrentLocaleConfig {
3
26
  fallbackLocale?: string;
4
27
  defaultLocale?: string;
@@ -9,7 +32,14 @@ export interface DetectCurrentLocaleConfig {
9
32
  autoDetectLanguage?: boolean;
10
33
  }
11
34
  /**
12
- * Detects the current locale based on various sources (server-only).
13
- * Thin wrapper around `resolveServerLocale` with configured cookie name.
35
+ * Detects the current locale of a server API request: `detectRequestLocale` with the `api` rules.
36
+ * @deprecated use `useLocaleServerMiddleware(event).current`, which applies the same rules.
14
37
  */
15
38
  export declare const detectCurrentLocale: (event: H3Event, config: DetectCurrentLocaleConfig, defaultLocale?: string) => string;
39
+ /**
40
+ * This request's locale for server API helpers, with the documented API order (query → route
41
+ * param / prefix → cookie → Accept-Language) on every route, `/api` or not — never the page
42
+ * render's locale, which ignores `?locale` under the prefix strategies.
43
+ */
44
+ export declare function getRequestLocale(event: H3Event, config: ResolvedI18nConfig, defaultLocale?: string): string;
45
+ export {};
@@ -1,32 +1,53 @@
1
1
  import { getEnabledLocaleCodes } from "@i18n-micro/utils/active-locales";
2
2
  import { withoutAppBaseURL } from "@i18n-micro/utils/app-path";
3
- import { getLocaleCookieName } from "@i18n-micro/utils/cookie";
4
- import { resolveServerLocale } from "@i18n-micro/utils/resolve-locale";
5
- import { getCookie, getQuery, getRequestURL } from "h3";
6
- import { useRuntimeConfig } from "#imports";
7
- export const detectCurrentLocale = (event, config, defaultLocale) => {
8
- const { fallbackLocale, defaultLocale: configDefaultLocale, locales, localeCookie, autoDetectLanguage } = config;
9
- const resolvedDefault = defaultLocale || configDefaultLocale || "en";
10
- const validLocales = getEnabledLocaleCodes(locales ?? []);
11
- const url = getRequestURL(event);
12
- const rawPath = url.pathname.split("?")[0]?.split("#")[0] ?? url.pathname;
13
- const cleanPath = withoutAppBaseURL(rawPath, useRuntimeConfig(event).app.baseURL);
14
- const firstSegment = cleanPath.split("/").filter(Boolean)[0] ?? "";
15
- const fromParams = event.context.params?.locale?.toString();
16
- const urlLocale = (fromParams && validLocales.includes(fromParams) ? fromParams : firstSegment) || null;
17
- const hasLocaleInUrl = Boolean(urlLocale && validLocales.includes(urlLocale));
18
- const queryLocale = getQuery(event)?.locale ? String(getQuery(event).locale) : null;
19
- const cookieName = getLocaleCookieName({ localeCookie });
20
- const cookieLocale = cookieName ? getCookie(event, cookieName) : null;
21
- const locale = resolveServerLocale({
22
- defaultLocale: resolvedDefault,
3
+ import { getPreferenceCookieName } from "@i18n-micro/utils/cookie";
4
+ import { configForHost } from "@i18n-micro/utils/domains";
5
+ import { firstQueryValue, localeFromPath, resolveLocale } from "@i18n-micro/utils/resolve-locale";
6
+ import { createI18nConfigResolver } from "@i18n-micro/utils/runtime-config";
7
+ import { getCookie, getHeader, getQuery, getRequestHost } from "h3";
8
+ import { domains, getI18nConfig } from "#i18n-internal/strategy";
9
+ import { useRuntimeConfig } from "nitropack/runtime";
10
+ import { requestURL } from "../../utils/request-url.js";
11
+ const resolveConfig = createI18nConfigResolver();
12
+ function fixedHost() {
13
+ try {
14
+ return process.env.NUXT_I18N_DOMAIN;
15
+ } catch {
16
+ return void 0;
17
+ }
18
+ }
19
+ export function getI18nServerConfig(event) {
20
+ const config = resolveConfig(getI18nConfig(), useRuntimeConfig().public);
21
+ if (!domains) return config;
22
+ const host = fixedHost() || event && getRequestHost(event, { xForwardedHost: config.trustForwardedHost === true });
23
+ return host ? configForHost(config, host) : config;
24
+ }
25
+ export function detectRequestLocale(event, config, mode, path) {
26
+ const validLocales = getEnabledLocaleCodes(config.locales);
27
+ const defaultLocale = config.defaultLocale;
28
+ const urlless = config.strategy === "no_prefix" || Boolean(config.hashMode);
29
+ const unprefixedIsDefault = config.strategy === "prefix_except_default" || config.strategy === "prefix_and_default";
30
+ const cookieName = getPreferenceCookieName(config);
31
+ return resolveLocale({
32
+ defaultLocale,
23
33
  validLocales,
24
- autoDetectLanguage,
25
- hasLocaleInUrl,
26
- urlLocale,
27
- queryLocale,
28
- cookieLocale,
29
- acceptLanguageHeader: event.headers.get("accept-language")
34
+ autoDetectLanguage: config.autoDetectLanguage,
35
+ url: urlless && mode === "page" ? null : urlLocale(event, mode, path, validLocales, unprefixedIsDefault ? defaultLocale : null),
36
+ query: mode === "api" || config.strategy === "no_prefix" ? firstQueryValue(getQuery(event).locale) : null,
37
+ queryFirst: mode === "api",
38
+ cookie: cookieName ? getCookie(event, cookieName) : null,
39
+ acceptLanguage: getHeader(event, "accept-language")
30
40
  });
31
- return locale || fallbackLocale || resolvedDefault;
41
+ }
42
+ function urlLocale(event, mode, path, validLocales, unprefixed) {
43
+ const param = firstQueryValue(event.context.params?.locale);
44
+ if (mode === "api" && param && validLocales.includes(param)) return param;
45
+ const prefix = localeFromPath(path ?? withoutAppBaseURL(requestURL(event).pathname, useRuntimeConfig().app.baseURL), validLocales);
46
+ return prefix ?? (mode === "page" ? unprefixed : null);
47
+ }
48
+ export const detectCurrentLocale = (event, config, defaultLocale) => {
49
+ return detectRequestLocale(event, { ...config, defaultLocale: defaultLocale || config.defaultLocale }, "api");
32
50
  };
51
+ export function getRequestLocale(event, config, defaultLocale) {
52
+ return detectRequestLocale(event, defaultLocale ? { ...config, defaultLocale } : config, "api");
53
+ }
@@ -1,37 +1,18 @@
1
- import { getEnabledLocaleCodes, getEnabledLocales } from "@i18n-micro/utils/active-locales";
2
- import { resolveI18nConfigWithRuntimeOverrides } from "@i18n-micro/utils/runtime-config";
3
- import { getI18nConfig } from "#i18n-internal/strategy";
4
- import { useRuntimeConfig } from "#imports";
5
- import { detectCurrentLocale } from "./locale-detector.js";
1
+ import { getEnabledLocales } from "@i18n-micro/utils/active-locales";
2
+ import { getI18nServerConfig, getRequestLocale } from "./locale-detector.js";
6
3
  export const useLocaleServerMiddleware = (event, defaultLocale, currentLocale) => {
7
- const config = resolveI18nConfigWithRuntimeOverrides(
8
- getI18nConfig(),
9
- useRuntimeConfig(event).public
10
- );
11
- const { locales, defaultLocale: configDefaultLocale, fallbackLocale } = config;
12
- const enabledLocales = getEnabledLocales(locales);
13
- const detectedLocale = currentLocale || detectCurrentLocale(
14
- event,
15
- {
16
- fallbackLocale,
17
- defaultLocale: defaultLocale || configDefaultLocale,
18
- locales: enabledLocales,
19
- localeCookie: config.localeCookie || "user-locale",
20
- autoDetectLanguage: true
21
- },
22
- defaultLocale
23
- );
24
- const localeConfig = enabledLocales.find((l) => l.code === detectedLocale) ?? null;
25
- const availableLocales = getEnabledLocaleCodes(locales);
26
- const isDefault = detectedLocale === (defaultLocale || configDefaultLocale || "en");
27
- const isFallback = detectedLocale === (fallbackLocale || defaultLocale || configDefaultLocale || "en");
4
+ const config = getI18nServerConfig(event);
5
+ const enabledLocales = getEnabledLocales(config.locales);
6
+ const resolvedDefault = defaultLocale || config.defaultLocale;
7
+ const fallback = config.fallbackLocale || resolvedDefault;
8
+ const current = currentLocale || getRequestLocale(event, config, defaultLocale);
28
9
  return {
29
- current: detectedLocale,
30
- default: defaultLocale || configDefaultLocale || "en",
31
- fallback: fallbackLocale || defaultLocale || configDefaultLocale || "en",
32
- available: availableLocales,
33
- locale: localeConfig,
34
- isDefault,
35
- isFallback
10
+ current,
11
+ default: resolvedDefault,
12
+ fallback,
13
+ available: enabledLocales.map((l) => l.code),
14
+ locale: enabledLocales.find((l) => l.code === current) ?? null,
15
+ isDefault: current === resolvedDefault,
16
+ isFallback: current === fallback
36
17
  };
37
18
  };
@@ -0,0 +1,4 @@
1
+ import type { ResolvedI18nConfig } from '@i18n-micro/types';
2
+ import { type H3Event } from 'h3';
3
+ /** The header of a payload response, by the `?v=` of its request. */
4
+ export declare function payloadCacheControl(event: H3Event, config: ResolvedI18nConfig, empty?: boolean): string | null;
@@ -0,0 +1,13 @@
1
+ import { resolveTranslationPayloadCachePolicy } from "@i18n-micro/utils/payload-url";
2
+ import { getQuery } from "h3";
3
+ import { resolveServerHost } from "./server-loader.js";
4
+ export function payloadCacheControl(event, config, empty = false) {
5
+ const policy = resolveTranslationPayloadCachePolicy({
6
+ httpCacheDuration: config.httpCacheDuration,
7
+ httpCacheControl: config.httpCacheControl,
8
+ dateBuild: config.dateBuild,
9
+ live: config.translationPayloadMode === "source" || Boolean(config.cacheTtl) || Boolean(resolveServerHost())
10
+ });
11
+ const current = Boolean(config.dateBuild) && String(getQuery(event).v ?? "") === String(config.dateBuild) && !empty;
12
+ return current ? policy.current : policy.stale;
13
+ }
@@ -2,11 +2,22 @@
2
2
  * Server-side translation loader.
3
3
  * - Node: JSON from public locales dir (`#i18n-internal/payload-source` → fs)
4
4
  * - Edge: Nitro `serverAssets` (`assets:i18n`)
5
- * - Dev cache miss: live locale-file merge (not stale `.nuxt/i18n-*`)
5
+ * - Dev: live locale files on every read (no cache to go stale on edits)
6
+ * - `cacheStorage`: a storage mount shared by instances, between the LRU and the above
6
7
  * `premerged` reads one file; `source` merges root+page+fallback at runtime.
8
+ *
9
+ * Bounded: entries live in one LRU cache, concurrent loads of a chunk share one read, and in
10
+ * premerged mode every page without its own payload shares the root entry — so request URLs
11
+ * with made-up page names cannot grow memory. (With `cacheStorage` such a name keeps one LRU slot.)
12
+ *
13
+ * A failed read rejects: no level may keep it, the next request reads again. A 404 from a host is an
14
+ * answer, `NO_FILE`, for this request only: a file can appear with the host's next deploy, and `cacheTtl`
15
+ * is off by default. ponytail: a page the host has no file for costs one request per render; a negative
16
+ * cache would hide the file for as long as it lives.
7
17
  */
8
- import type { Translations } from '@i18n-micro/types';
9
- export declare function loadTranslationsFromServer(locale: string, routeName: string): Promise<{
10
- data: Translations;
11
- json: string;
12
- }>;
18
+ import { type ServerTranslationCacheEntry } from '../internal/server-translation-cache.js';
19
+ /** The host has no such file. By identity: a host's own empty file is a dictionary, and stays cached. */
20
+ export declare const NO_FILE: ServerTranslationCacheEntry;
21
+ /** The host dictionaries come from: read per load, as `$fetch` in the page render does, so one artifact can run against several. */
22
+ export declare function resolveServerHost(): string | undefined;
23
+ export declare function loadTranslationsFromServer(locale: string, routeName: string): Promise<ServerTranslationCacheEntry>;