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.
- package/LICENSE +1 -1
- package/README.md +2 -2
- package/dist/client/200.html +1 -1
- package/dist/client/404.html +1 -1
- package/dist/client/_nuxt/BVna_KKx.js +3 -0
- package/dist/client/_nuxt/Bw81RhAv.js +1 -0
- package/dist/client/_nuxt/CPz_jhsD.js +1 -0
- package/dist/client/_nuxt/CmvObVGq.js +1 -0
- package/dist/client/_nuxt/DKUOBR6F.js +5 -0
- package/dist/client/_nuxt/builds/latest.json +1 -1
- package/dist/client/_nuxt/builds/meta/f0c8d4bf-40d2-4915-8b4a-ebe46bde61c8.json +1 -0
- package/dist/client/_nuxt/{entry.BM-mexAv.css → entry.PSWtHhay.css} +1 -1
- package/dist/client/_nuxt/error-404.xs3fxhVJ.css +1 -0
- package/dist/client/_nuxt/error-500.C2S-4iLg.css +1 -0
- package/dist/client/index.html +1 -1
- package/dist/module.d.mts +14 -5
- package/dist/module.json +4 -1
- package/dist/module.mjs +850 -475
- package/dist/runtime/components/i18n-group.d.vue.ts +17 -9
- package/dist/runtime/components/i18n-group.vue +2 -4
- package/dist/runtime/components/i18n-group.vue.d.ts +17 -9
- package/dist/runtime/components/i18n-link.d.vue.ts +9 -6
- package/dist/runtime/components/i18n-link.vue +7 -28
- package/dist/runtime/components/i18n-link.vue.d.ts +9 -6
- package/dist/runtime/components/i18n-switcher.d.vue.ts +128 -54
- package/dist/runtime/components/i18n-switcher.vue +56 -156
- package/dist/runtime/components/i18n-switcher.vue.d.ts +128 -54
- package/dist/runtime/components/i18n-t.d.vue.ts +59 -116
- package/dist/runtime/components/i18n-t.vue +24 -71
- package/dist/runtime/components/i18n-t.vue.d.ts +59 -116
- package/dist/runtime/composables/useI18n.js +2 -38
- package/dist/runtime/composables/useI18nHead.js +21 -6
- package/dist/runtime/composables/useI18nLocale.d.ts +4 -1
- package/dist/runtime/composables/useI18nLocale.js +20 -22
- package/dist/runtime/composables/useLocaleHead.d.ts +26 -31
- package/dist/runtime/composables/useLocaleHead.js +47 -167
- package/dist/runtime/devtools/bridge.d.ts +2 -2
- package/dist/runtime/devtools/vue-devtools.d.ts +2 -2
- package/dist/runtime/devtools/vue-devtools.js +5 -5
- package/dist/runtime/middleware/i18n-redirect.global.d.ts +3 -2
- package/dist/runtime/middleware/i18n-redirect.global.js +30 -41
- package/dist/runtime/plugins/01.plugin.d.ts +156 -34
- package/dist/runtime/plugins/01.plugin.js +184 -138
- package/dist/runtime/plugins/02.meta.js +36 -43
- package/dist/runtime/plugins/04.vue-devtools.js +1 -1
- package/dist/runtime/plugins/06.redirect.d.ts +3 -3
- package/dist/runtime/plugins/06.redirect.js +65 -91
- package/dist/runtime/server/{utils → internal}/dev-live-translations.js +4 -3
- package/dist/runtime/server/internal/server-translation-cache.d.ts +9 -0
- package/dist/runtime/server/internal/server-translation-cache.js +10 -0
- package/dist/runtime/server/internal/shared-cache.d.ts +24 -0
- package/dist/runtime/server/internal/shared-cache.js +71 -0
- package/dist/runtime/server/internal/translation-watcher.d.ts +79 -0
- package/dist/runtime/server/internal/translation-watcher.js +105 -0
- package/dist/runtime/server/middleware/i18n.global.js +24 -37
- package/dist/runtime/server/payload-source.assets.d.ts +1 -0
- package/dist/runtime/server/payload-source.assets.js +3 -8
- package/dist/runtime/server/payload-source.fs.d.ts +1 -0
- package/dist/runtime/server/payload-source.fs.js +18 -7
- package/dist/runtime/server/plugins/failed-read.d.ts +7 -0
- package/dist/runtime/server/plugins/failed-read.js +23 -0
- package/dist/runtime/server/plugins/payload-cache.d.ts +6 -0
- package/dist/runtime/server/plugins/payload-cache.js +16 -0
- package/dist/runtime/server/plugins/watcher.dev.js +15 -79
- package/dist/runtime/server/routes/i18n.js +13 -14
- package/dist/runtime/server/utils/locale-detector.d.ts +32 -2
- package/dist/runtime/server/utils/locale-detector.js +48 -27
- package/dist/runtime/server/utils/locale-server-middleware.js +14 -33
- package/dist/runtime/server/utils/payload-cache.d.ts +4 -0
- package/dist/runtime/server/utils/payload-cache.js +13 -0
- package/dist/runtime/server/utils/server-loader.d.ts +17 -6
- package/dist/runtime/server/utils/server-loader.js +105 -51
- package/dist/runtime/server/utils/translation-server-middleware.js +18 -35
- package/dist/runtime/utils/cache-keys.d.ts +6 -0
- package/dist/runtime/utils/cache-keys.js +2 -0
- package/dist/runtime/utils/i18n-api.d.ts +185 -0
- package/dist/runtime/utils/i18n-api.js +196 -0
- package/dist/runtime/utils/i18n-config.d.ts +18 -0
- package/dist/runtime/utils/i18n-config.js +47 -0
- package/dist/runtime/utils/i18n-navigation.d.ts +26 -0
- package/dist/runtime/utils/i18n-navigation.js +96 -0
- package/dist/runtime/utils/nuxt-i18n-server.d.ts +26 -0
- package/dist/runtime/utils/nuxt-i18n-server.js +83 -0
- package/dist/runtime/utils/nuxt-i18n.d.ts +81 -96
- package/dist/runtime/utils/nuxt-i18n.js +180 -252
- package/dist/runtime/utils/page-head.d.ts +12 -0
- package/dist/runtime/utils/page-head.js +6 -0
- package/dist/runtime/utils/request-url.d.ts +6 -0
- package/dist/runtime/utils/request-url.js +11 -0
- package/dist/runtime/utils/storage.d.ts +31 -19
- package/dist/runtime/utils/storage.js +100 -49
- package/dist/runtime/utils/translation-entry.d.ts +6 -0
- package/dist/runtime/utils/translation-entry.js +16 -0
- package/dist/types.d.mts +4 -6
- package/internals.d.mts +9 -2
- package/package.json +78 -79
- package/dist/client/_nuxt/BOxWElQP.js +0 -21
- package/dist/client/_nuxt/CUWB4CwN.js +0 -3
- package/dist/client/_nuxt/CmUbe-uv.js +0 -1
- package/dist/client/_nuxt/DAzShkZF.js +0 -1
- package/dist/client/_nuxt/MokPP5zi.js +0 -1
- package/dist/client/_nuxt/builds/meta/f020fec8-462b-480b-82d8-6b0cb65617e8.json +0 -1
- package/dist/client/_nuxt/error-404.D9Svp6Hk.css +0 -1
- package/dist/client/_nuxt/error-500.B-mjI9_9.css +0 -1
- package/dist/runtime/plugins/03.define.d.ts +0 -7
- package/dist/runtime/plugins/03.define.js +0 -42
- package/dist/runtime/plugins/05.hooks.d.ts +0 -2
- package/dist/runtime/plugins/05.hooks.js +0 -47
- package/dist/runtime/server/utils/server-translation-cache.d.ts +0 -11
- package/dist/runtime/server/utils/server-translation-cache.js +0 -20
- /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 {
|
|
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 {
|
|
9
|
-
import { useRuntimeConfig } from "
|
|
10
|
-
import {
|
|
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
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
if (isInternalPath(path))
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
);
|
|
20
|
-
const
|
|
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 (
|
|
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,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.
|
|
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,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
|
|
28
|
+
const dir = baseDir();
|
|
17
29
|
try {
|
|
18
|
-
return JSON.parse(await readFile(
|
|
30
|
+
return JSON.parse(await readFile(join(dir, rel), "utf8"));
|
|
19
31
|
} catch (error) {
|
|
20
|
-
const
|
|
21
|
-
if (
|
|
22
|
-
|
|
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
|
|
2
|
-
import
|
|
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
|
-
|
|
10
|
-
|
|
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 {
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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:
|
|
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 (
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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 {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
13
|
-
*
|
|
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 {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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 {
|
|
2
|
-
import {
|
|
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 =
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
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
|
|
30
|
-
default:
|
|
31
|
-
fallback
|
|
32
|
-
available:
|
|
33
|
-
locale:
|
|
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
|
|
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
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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>;
|