nuxt-i18n-micro
Version:
Nuxt I18n Micro is a lightweight, high-performance internationalization module for Nuxt, designed to handle multi-language support with minimal overhead, fast build times, and efficient runtime performance.
115 lines (114 loc) • 4.5 kB
JavaScript
import { existsSync, readdirSync, readFileSync } from "node:fs";
import path from "node:path";
import { SERVER_CC_KEY, STORAGE_CC_KEY } from "@i18n-micro/hmr/cache-keys";
import { handleTranslationWatchChange, parseTranslationWatchRelativePath } from "@i18n-micro/hmr/watcher";
import { CacheControl } from "@i18n-micro/utils/cache-control";
import { watch } from "chokidar";
import { defineNitroPlugin } from "nitropack/runtime";
import { getI18nPrivateConfig } from "#i18n-internal/config";
import { getI18nConfig } from "#i18n-internal/strategy";
function getCacheByKey(key) {
const g = globalThis;
const cc = g[key];
if (cc && typeof cc === "object" && "keys" in cc && "set" in cc) {
return cc;
}
return null;
}
function getServerCache() {
return getCacheByKey(SERVER_CC_KEY);
}
function getOrCreateServerCache() {
const existing = getServerCache();
if (existing) {
return existing;
}
const g = globalThis;
const created = new CacheControl();
g[SERVER_CC_KEY] = created;
return created;
}
function getStorageCache() {
return getCacheByKey(STORAGE_CC_KEY);
}
function readJsonSafe(filePath) {
try {
if (existsSync(filePath)) {
return JSON.parse(readFileSync(filePath, "utf-8"));
}
} catch {
}
return {};
}
let watcherInstance = null;
export default defineNitroPlugin((nitroApp) => {
if (watcherInstance) {
return;
}
const i18nConfig = getI18nPrivateConfig();
if (process.env.NODE_ENV !== "development") {
return;
}
const log = (...args) => i18nConfig.debug && console.log("[i18n-hmr]", ...args);
const warn = (...args) => i18nConfig.debug && console.warn("[i18n-hmr]", ...args);
const rawLocales = i18nConfig.locales;
const configuredLocales = new Set(
(Array.isArray(rawLocales) ? rawLocales : []).map((l) => l.code).filter((code) => typeof code === "string" && code.length > 0)
);
const routesLocaleLinks = i18nConfig.routesLocaleLinks || {};
const translationsRoot = path.resolve(i18nConfig.rootDir, i18nConfig.translationDir);
log(`Watching for translation changes in: ${translationsRoot}`);
const invalidateAndRefresh = async (filePath, event) => {
const relativePath = path.relative(translationsRoot, filePath).replace(/\\/g, "/");
const runtimeConfig = getI18nConfig();
try {
const result = await handleTranslationWatchChange({
relativePath,
configuredLocales,
listPageNames: () => {
const pagesDir = path.join(translationsRoot, "pages");
if (!existsSync(pagesDir)) {
return [];
}
return readdirSync(pagesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
},
serverCache: getOrCreateServerCache(),
storageCache: getStorageCache(),
routesLocaleLinks,
mergeInput: {
translationPayloadMode: runtimeConfig.translationPayloadMode,
locales: runtimeConfig.locales,
fallbackLocale: runtimeConfig.fallbackLocale,
disablePageLocales: runtimeConfig.disablePageLocales,
readLocaleFile: (relativeFilePath) => readJsonSafe(path.join(translationsRoot, relativeFilePath))
}
});
if (result === "root") {
const parsed = parseTranslationWatchRelativePath(relativePath);
if (parsed.type === "root") {
log(`Re-merged ALL pages for locale '${parsed.locale}'`);
}
} else if (result === "page") {
log(`Re-merged page cache for '${relativePath}'`);
} else if (result === "ignored") {
const parsed = parseTranslationWatchRelativePath(relativePath);
if (parsed.type === "root" && !configuredLocales.has(parsed.locale)) {
warn(`Detected ${event} for '${relativePath}', but locale '${parsed.locale}' is not in i18n.locales. Update config and restart dev server.`);
}
}
} catch (e) {
warn("Failed to refresh server cache for", filePath, e);
}
};
const watcher = watch(translationsRoot, { persistent: true, ignoreInitial: true, depth: 5 });
watcher.on("add", (filePath) => invalidateAndRefresh(filePath, "add"));
watcher.on("change", (filePath) => invalidateAndRefresh(filePath, "change"));
watcher.on("unlink", (filePath) => invalidateAndRefresh(filePath, "unlink"));
watcherInstance = watcher;
nitroApp.hooks.hook("close", async () => {
if (watcherInstance) {
await watcherInstance.close();
watcherInstance = null;
}
});
});