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.
898 lines (876 loc) • 35.7 kB
JavaScript
import * as fs from 'node:fs';
import fs__default, { readFileSync, mkdirSync, writeFileSync, existsSync } from 'node:fs';
import { createRequire } from 'node:module';
import path, { resolve, join, dirname } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { defaultPlural, isNoPrefixStrategy, withPrefixStrategy } from '@i18n-micro/core';
import { generateHmrPlugin } from '@i18n-micro/hmr/generate-plugin';
import { RouteGenerator, isLocaleAllowedForUnlocalizedRoute, normalizePath, isInternalPath } from '@i18n-micro/route-strategy';
import { buildTranslationSourceLayers, preMergeLocales } from '@i18n-micro/utils/build';
import { resolveTranslationPayloadOptions, getTranslationPayloadMisconfigurationWarnings, getTranslationPayloadSizeWarning, resolveTranslationPayloadWarningThresholds, resolveTranslationPayloadPublicDir } from '@i18n-micro/utils/payload-config';
export { resolveTranslationPayloadMode, resolveTranslationPayloadOptions, resolveTranslationPayloadPublicDir } from '@i18n-micro/utils/payload-config';
import { scanTranslationPayloadDirectory } from '@i18n-micro/utils/payload-stats';
import { useNuxt, defineNuxtModule, useLogger, createResolver, addTemplate, addImportsDir, addPlugin, addRouteMiddleware, addServerImportsDir, addServerHandler, addComponentsDir, addTypeTemplate, addVitePlugin, addPrerenderRoutes } from '@nuxt/kit';
import { globby } from 'globby';
import { onDevToolsInitialized, extendServerRpc } from '@nuxt/devtools-kit';
import { createUnplugin } from 'unplugin';
import { parse } from '@vue/compiler-sfc';
const DEVTOOLS_UI_PORT = 3030;
const DEVTOOLS_UI_ROUTE = "/__nuxt-i18n-micro";
const distDir = resolve(fileURLToPath(import.meta.url), "..");
resolve(distDir, "client");
function setupDevToolsUI(options, resolve2, rootDirs) {
const nuxt = useNuxt();
const clientPath = resolve2("./client");
const devtoolsUiDistPath = resolve2("./packages/devtools-ui/dist");
const clientDirExists = fs.existsSync(clientPath) || fs.existsSync(devtoolsUiDistPath);
const ROUTE_PATH = `${nuxt.options.app.baseURL || "/"}/__nuxt-i18n-micro`.replace(/\/+/g, "/");
const ROUTE_CLIENT = `${ROUTE_PATH}/client`;
if (clientDirExists) {
nuxt.hook("vite:extendConfig", (config) => {
const c = config;
c.server = c.server || {};
c.server.proxy = c.server.proxy || {};
const proxyConfig = {
target: `http://localhost:${DEVTOOLS_UI_PORT}`,
changeOrigin: true,
ws: true,
// Enable WebSocket proxying for HMR
// Don't rewrite - keep the full path since client server expects /__nuxt-i18n-micro/client/*
rewrite: (path2) => path2
};
c.server.proxy[`${ROUTE_CLIENT}`] = proxyConfig;
c.server.proxy[`${ROUTE_CLIENT}/`] = proxyConfig;
c.server.proxy[`${ROUTE_CLIENT}/*`] = proxyConfig;
});
} else {
nuxt.hook("vite:extendConfig", (config) => {
const c = config;
c.server = c.server || {};
c.server.proxy = c.server.proxy || {};
c.server.proxy[DEVTOOLS_UI_ROUTE] = {
target: `http://localhost:${DEVTOOLS_UI_PORT}${DEVTOOLS_UI_ROUTE}`,
changeOrigin: true,
followRedirects: true,
rewrite: (path2) => path2.replace(DEVTOOLS_UI_ROUTE, "")
};
});
}
onDevToolsInitialized(async () => {
const dirs = rootDirs.length > 0 ? rootDirs : [nuxt.options.rootDir];
extendServerRpc("nuxt-i18n-micro", {
async saveTranslationContent(file, content) {
let filePath = null;
for (const rootDir of dirs) {
const localesDir = path.join(rootDir, options.translationDir || "locales");
const candidatePath = path.join(localesDir, file);
if (fs.existsSync(candidatePath)) {
filePath = candidatePath;
break;
}
}
if (!filePath) {
filePath = path.resolve(file);
}
if (fs.existsSync(filePath)) {
fs.writeFileSync(filePath, JSON.stringify(content, null, 2), "utf-8");
} else {
throw new Error(`File not found: ${filePath}`);
}
},
async getConfigs() {
return Promise.resolve(options);
},
async getLocalesAndTranslations() {
const filesList = {};
for (const rootDir of dirs) {
const localesDir = path.join(rootDir, options.translationDir || "locales");
const pagesDir = path.join(localesDir, "pages");
const processDirectory = (dir, baseDir = localesDir) => {
if (!fs.existsSync(dir)) return;
fs.readdirSync(dir).forEach((file) => {
const filePath = path.join(dir, file);
const stat = fs.lstatSync(filePath);
if (stat.isDirectory()) {
processDirectory(filePath, baseDir);
} else if (file.endsWith(".json")) {
try {
const relativePath = path.relative(baseDir, filePath);
const normalizedPath = relativePath.replace(/\\/g, "/");
filesList[normalizedPath] = JSON.parse(fs.readFileSync(filePath, "utf-8"));
} catch (e) {
console.error(`Error parsing locale file ${filePath}:`, e);
}
}
});
};
processDirectory(localesDir, localesDir);
processDirectory(pagesDir, localesDir);
}
return filesList;
}
});
nuxt.hook("devtools:customTabs", (tabs) => {
tabs.push({
name: "nuxt-i18n-micro",
title: "i18n Micro",
icon: "carbon:language",
view: {
type: "iframe",
src: ROUTE_CLIENT
}
});
});
});
}
function shouldLocalizeRouteRulePath(originalPath) {
const routeRulePath = originalPath.trim();
if (!routeRulePath) return true;
const path = routeRulePath.startsWith("/") ? routeRulePath : `/${routeRulePath}`;
if (path === "/api" || path.startsWith("/api/")) return false;
const firstSegment = path.replace(/^\/+/, "").split("/")[0] ?? "";
return !firstSegment.startsWith("_");
}
function extractScriptContent(content) {
const { descriptor } = parse(content, { pad: false });
const parts = [];
if (descriptor.script?.content) parts.push(descriptor.script.content);
if (descriptor.scriptSetup?.content) parts.push(descriptor.scriptSetup.content);
return parts.length > 0 ? parts.join("\n") : null;
}
function removeTypeScriptTypes(scriptContent) {
return scriptContent.replace(/\((\w+):[^)]*\)/g, "($1)");
}
function findDefineI18nRouteConfig(scriptContent) {
try {
const defineStart = scriptContent.indexOf("$defineI18nRoute(");
if (defineStart === -1) {
return null;
}
const openParen = scriptContent.indexOf("(", defineStart);
if (openParen === -1) {
return null;
}
let braceCount = 0;
let parenCount = 1;
let i = openParen + 1;
for (; i < scriptContent.length; i++) {
if (scriptContent[i] === "{") braceCount++;
if (scriptContent[i] === "}") braceCount--;
if (scriptContent[i] === "(") parenCount++;
if (scriptContent[i] === ")") {
parenCount--;
if (parenCount === 0 && braceCount === 0) break;
}
}
if (i >= scriptContent.length) {
return null;
}
const configStr = scriptContent.substring(openParen + 1, i);
try {
const cleanConfigStr = removeTypeScriptTypes(configStr);
try {
const configObject = Function(`"use strict";return (${cleanConfigStr})`)();
try {
const serialized = JSON.stringify(configObject);
return JSON.parse(serialized);
} catch {
return configObject;
}
} catch {
const scriptWithoutImports = scriptContent.split("\n").filter((line) => !line.trim().startsWith("import ")).join("\n");
const cleanScript = removeTypeScriptTypes(scriptWithoutImports);
const safeScript = `
const $defineI18nRoute = () => {}
const defineI18nRoute = () => {}
const process = { env: { NODE_ENV: 'development' } }
${cleanScript}
return (${cleanConfigStr})
`;
const configObject = Function(`"use strict";${safeScript}`)();
try {
const serialized = JSON.stringify(configObject);
return JSON.parse(serialized);
} catch {
return configObject;
}
}
} catch {
return null;
}
} catch {
return null;
}
}
function normalizeDefineConfig(configObject) {
if (configObject.locales && typeof configObject.locales === "object" && !Array.isArray(configObject.locales)) {
const localesObj = configObject.locales;
const normalizedLocales = [];
const normalizedLocaleRoutes = {};
for (const [locale, value] of Object.entries(localesObj)) {
normalizedLocales.push(locale);
if (value && typeof value === "object" && "path" in value && typeof value.path === "string") {
normalizedLocaleRoutes[locale] = value.path;
}
}
return {
...configObject,
locales: normalizedLocales,
localeRoutes: configObject.localeRoutes || Object.keys(normalizedLocaleRoutes).length > 0 ? { ...configObject.localeRoutes, ...normalizedLocaleRoutes } : void 0
};
}
if (Array.isArray(configObject.locales) && configObject.locales.length > 0 && typeof configObject.locales[0] === "object") {
const normalizedLocales = configObject.locales.map((item) => {
if (item && typeof item === "object" && "code" in item) {
return item.code;
}
return String(item);
});
return {
...configObject,
locales: normalizedLocales
};
}
return configObject;
}
function extractDefineI18nRouteData(content, _filePath) {
try {
const scriptContent = extractScriptContent(content);
if (!scriptContent) {
return null;
}
const configObject = findDefineI18nRouteConfig(scriptContent);
if (!configObject) {
return null;
}
return normalizeDefineConfig(configObject);
} catch {
return null;
}
}
function pageFilePathToRoutePath(pageFile, rootDir) {
const relative = pageFile.replace(rootDir, "").replace(/^[/\\]+/, "");
const withoutPagesPrefix = relative.replace(/^(app[/\\])?pages[/\\]/, "");
if (withoutPagesPrefix === "index.vue") {
return "/";
}
const raw = withoutPagesPrefix.replace(/[/\\]index\.vue$/, "").replace(/\.vue$/, "").replace(/[/\\]$/, "").replace(/\\/g, "/");
return raw === "" ? "/" : raw;
}
function resolveRootDir(filePath, rootDirs) {
const sorted = [...rootDirs].sort((a, b) => b.length - a.length);
return sorted.find((root) => filePath.startsWith(root));
}
function createDefineI18nRoutePlugin(options) {
const metaByFile = /* @__PURE__ */ new Map();
const writeMetaFile = () => {
const entries = [...metaByFile.values()].filter((entry) => entry !== null);
options.onMetaUpdate?.(entries);
const metaPath = join(options.buildDir, "i18n-route-meta.json");
mkdirSync(dirname(metaPath), { recursive: true });
writeFileSync(metaPath, JSON.stringify(entries, null, 2));
};
return createUnplugin(() => ({
name: "nuxt-i18n-micro-define-route",
enforce: "pre",
transformInclude(id) {
return id.endsWith(".vue") && !id.includes("node_modules");
},
transform(code, id) {
const rootDir = resolveRootDir(id, options.rootDirs);
if (!rootDir) return null;
const config = extractDefineI18nRouteData(code);
if (!config) {
metaByFile.delete(id);
} else {
metaByFile.set(id, {
routePath: pageFilePathToRoutePath(id, rootDir),
config
});
}
writeMetaFile();
return null;
},
buildEnd() {
writeMetaFile();
}
}));
}
function collectDefineI18nRouteMetaFromFiles(pageFiles, rootDirs) {
const entries = [];
for (const pageFile of pageFiles) {
const rootDir = resolveRootDir(pageFile, rootDirs);
if (!rootDir) continue;
try {
const config = extractDefineI18nRouteData(readFileSync(pageFile, "utf-8"), pageFile);
if (!config) continue;
entries.push({
routePath: pageFilePathToRoutePath(pageFile, rootDir),
config
});
} catch {
}
}
return entries;
}
const DEFAULT_CANONICAL_QUERY_WHITELIST = ["page", "sort", "filter", "search", "q", "query", "tag"];
function generateI18nTypes() {
return `
import type {PluginsInjections} from "nuxt-i18n-micro";
declare module 'vue/types/vue' {
interface Vue extends PluginsInjections { }
}
declare module '@nuxt/types' {
interface NuxtAppOptions extends PluginsInjections { }
interface Context extends PluginsInjections { }
}
declare module '#app' {
interface NuxtApp extends PluginsInjections { }
}
export {}`;
}
const module = defineNuxtModule({
meta: {
name: "nuxt-i18n-micro",
configKey: "i18n"
},
// Default configuration options of the Nuxt module
defaults: {
locales: [],
meta: true,
debug: false,
define: true,
redirects: true,
plugin: true,
hooks: true,
components: true,
types: true,
defaultLocale: "en",
strategy: "prefix_except_default",
translationDir: "locales",
autoDetectPath: "/",
autoDetectLanguage: true,
disablePageLocales: false,
disableWatcher: false,
noPrefixRedirect: false,
fallbackLocale: void 0,
localeCookie: null,
apiBaseUrl: "_locales",
apiBaseClientHost: void 0,
apiBaseServerHost: void 0,
translationPayloads: {
mode: "premerged",
serverAssets: true,
serverHandler: true,
publicAssets: true,
prerenderRoutes: true
},
routesLocaleLinks: {},
globalLocaleRoutes: {},
canonicalQueryWhitelist: void 0,
plural: defaultPlural,
customRegexMatcher: void 0,
excludePatterns: void 0,
localizedRouteNamePrefix: "localized-",
missingWarn: true,
metaTrustForwardedHost: true,
metaTrustForwardedProto: true
},
async setup(options, nuxt) {
const defaultLocale = process.env.DEFAULT_LOCALE ?? options.defaultLocale ?? "en";
const isSSG = Boolean(nuxt.options.nitro?.static);
const logger = useLogger("nuxt-i18n-micro");
if (options.strategy === "no_prefix" && !options.localeCookie) {
options.localeCookie = "user-locale";
logger.info("Strategy 'no_prefix': localeCookie automatically set to 'user-locale' for locale persistence.");
}
if (options.strategy !== "no_prefix" && options.redirects !== false && !options.localeCookie) {
logger.warn(
"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."
);
}
const resolver = createResolver(import.meta.url);
const rootDirs = nuxt.options._layers.map((layer) => layer.config.rootDir).reverse();
const mergedLocalesDir = resolve(nuxt.options.buildDir, "i18n-merged");
const sourceLocalesDir = resolve(nuxt.options.buildDir, "i18n-source");
const translationDirName = options.translationDir || "locales";
const translationPayloads = resolveTranslationPayloadOptions(options);
const translationAssetsDir = translationPayloads.mode === "source" ? sourceLocalesDir : mergedLocalesDir;
for (const warning of getTranslationPayloadMisconfigurationWarnings({
translationPayloads,
apiBaseClientHost: options.apiBaseClientHost,
apiBaseServerHost: options.apiBaseServerHost
})) {
logger.warn(warning.replace("[nuxt-i18n-micro] ", ""));
}
const localeInfos = (options.locales ?? []).map(
(l) => typeof l === "string" ? { code: l } : { code: l.code, fallbackLocale: l.fallbackLocale }
);
nuxt.hook("build:before", async () => {
if (translationPayloads.mode === "source") {
await buildTranslationSourceLayers(rootDirs, translationDirName, sourceLocalesDir);
logger.info(`Built compact translation source from ${rootDirs.length} layer(s) into ${sourceLocalesDir}`);
} else {
await preMergeLocales(rootDirs, translationDirName, mergedLocalesDir, localeInfos, options.fallbackLocale, options.disablePageLocales);
logger.info(`Pre-merged translations from ${rootDirs.length} layer(s) into ${mergedLocalesDir}`);
}
const payloadStats = scanTranslationPayloadDirectory(translationAssetsDir);
const sizeWarning = getTranslationPayloadSizeWarning(payloadStats, resolveTranslationPayloadWarningThresholds(options.translationPayloads));
if (sizeWarning) {
logger.warn(sizeWarning.replace("[nuxt-i18n-micro] ", ""));
}
});
const routeLocales = { ...options.routeLocales ?? {} };
const globalLocaleRoutes = {};
const routeDisableMeta = {};
const pageGlobs = rootDirs.flatMap((root) => [join(root, "pages/**/*.vue"), join(root, "app/pages/**/*.vue")]);
const pageFiles = await globby(pageGlobs, { absolute: true });
for (const { routePath, config } of collectDefineI18nRouteMetaFromFiles(pageFiles, rootDirs)) {
try {
const { locales: extractedLocales, localeRoutes, disableMeta } = config;
if (extractedLocales) {
if (Array.isArray(extractedLocales)) {
routeLocales[routePath] = extractedLocales;
} else if (typeof extractedLocales === "object") {
routeLocales[routePath] = Object.keys(extractedLocales);
}
}
if (localeRoutes) {
globalLocaleRoutes[routePath] = localeRoutes;
}
if (disableMeta !== void 0) {
routeDisableMeta[routePath] = disableMeta;
}
} catch {
}
}
const mergedGlobalLocaleRoutes = { ...globalLocaleRoutes, ...options.globalLocaleRoutes };
if (options.debug) {
logger.debug("[i18n module] mergedGlobalLocaleRoutes keys:", Object.keys(mergedGlobalLocaleRoutes));
logger.debug('[i18n module] mergedGlobalLocaleRoutes["unlocalized"]:', mergedGlobalLocaleRoutes["unlocalized"]);
logger.debug("[i18n module] strategy:", options.strategy);
}
const require = createRequire(import.meta.url);
const strategyFiles = {
no_prefix: "no-prefix-strategy.mjs",
prefix: "prefix-strategy.mjs",
prefix_except_default: "prefix-except-default-strategy.mjs",
prefix_and_default: "prefix-and-default-strategy.mjs"
};
const strategyFile = strategyFiles[options.strategy] ?? strategyFiles.prefix_except_default;
const pkgPath = require.resolve("@i18n-micro/path-strategy/package.json");
const absoluteStrategyPath = join(dirname(pkgPath), "dist", strategyFile);
const resolvedStrategyPath = process.platform === "win32" ? pathToFileURL(absoluteStrategyPath).href : absoluteStrategyPath.replace(/\\/g, "/");
const routeGenerator = new RouteGenerator({
locales: options.locales ?? [],
defaultLocaleCode: defaultLocale,
strategy: options.strategy,
globalLocaleRoutes: mergedGlobalLocaleRoutes,
filesLocaleRoutes: globalLocaleRoutes,
routeLocales,
noPrefixRedirect: options.noPrefixRedirect,
excludePatterns: options.excludePatterns,
localizedRouteNamePrefix: options.localizedRouteNamePrefix,
customRegexMatcher: options.customRegexMatcher
});
const pluralTemplate = addTemplate({
filename: "i18n.plural.mjs",
write: true,
getContents: () => `export const plural = ${options.plural.toString()};`
});
let apiBaseClientHost = process.env.NUXT_I18N_APP_BASE_CLIENT_HOST ?? options.apiBaseClientHost ?? void 0;
if (apiBaseClientHost?.endsWith("/")) {
apiBaseClientHost = apiBaseClientHost.slice(0, -1);
}
let apiBaseServerHost = process.env.NUXT_I18N_APP_BASE_SERVER_HOST ?? options.apiBaseServerHost ?? void 0;
if (apiBaseServerHost?.endsWith("/")) {
apiBaseServerHost = apiBaseServerHost.slice(0, -1);
}
const rawUrl = process.env.NUXT_I18N_APP_BASE_URL ?? options.apiBaseUrl ?? "_locales";
if (rawUrl.startsWith("http://") || rawUrl.startsWith("https://")) {
throw new Error("Nuxt-i18n-micro: Please use NUXT_I18N_APP_BASE_CLIENT_HOST or NUXT_I18N_APP_BASE_SERVER_HOST instead.");
}
const apiBaseUrl = rawUrl.replace(/^\/+|\/+$/g, "").replace(/\/{2,}/g, "/");
const dateBuild = options.dateBuild ?? Date.now();
const fullConfig = {
locales: routeGenerator.locales ?? [],
metaBaseUrl: options.metaBaseUrl || void 0,
metaTrustForwardedHost: options.metaTrustForwardedHost ?? true,
metaTrustForwardedProto: options.metaTrustForwardedProto ?? true,
defaultLocale,
fallbackLocale: options.fallbackLocale ?? void 0,
localeCookie: options.localeCookie ?? null,
autoDetectLanguage: options.autoDetectLanguage ?? true,
autoDetectPath: options.autoDetectPath ?? "/",
strategy: options.strategy ?? "prefix_except_default",
dateBuild,
hashMode: nuxt.options?.router?.options?.hashMode ?? false,
apiBaseUrl,
apiBaseClientHost,
isSSG,
disablePageLocales: options.disablePageLocales ?? false,
canonicalQueryWhitelist: options.canonicalQueryWhitelist ?? DEFAULT_CANONICAL_QUERY_WHITELIST,
excludePatterns: options.excludePatterns ?? [],
routeLocales,
routeDisableMeta,
globalLocaleRoutes: mergedGlobalLocaleRoutes,
missingWarn: options.missingWarn ?? true,
redirects: options.redirects !== false,
hooks: options.hooks !== false,
hmr: options.hmr ?? true,
localizedRouteNamePrefix: options.localizedRouteNamePrefix ?? "localized-",
routesLocaleLinks: options.routesLocaleLinks ?? {},
noPrefixRedirect: options.noPrefixRedirect ?? false,
debug: options.debug ?? false,
customRegexMatcher: options.customRegexMatcher instanceof RegExp ? options.customRegexMatcher.source : options.customRegexMatcher,
cacheMaxSize: options.cacheMaxSize ?? 0,
cacheTtl: options.cacheTtl ?? 0,
translationPayloadMode: translationPayloads.mode
};
const fullConfigJson = JSON.stringify(fullConfig);
const strategyTemplate = addTemplate({
filename: "i18n.strategy.mjs",
write: true,
getContents: () => `import { Strategy } from '${resolvedStrategyPath}'
const __fullConfig = ${fullConfigJson}
export function getI18nConfig() { return __fullConfig }
export function createI18nStrategy(router) {
const routerAdapter = {
hasRoute(name) { return router.hasRoute(name) },
resolve(to) {
const r = router.resolve(to)
return {
name: r.name != null ? String(r.name) : null,
path: r.path,
fullPath: r.fullPath,
params: r.params || {},
query: r.query || {},
hash: r.hash || '',
}
},
}
return new Strategy({
strategy: __fullConfig.strategy,
defaultLocale: __fullConfig.defaultLocale,
locales: __fullConfig.locales,
localizedRouteNamePrefix: __fullConfig.localizedRouteNamePrefix,
globalLocaleRoutes: __fullConfig.globalLocaleRoutes,
routeLocales: __fullConfig.routeLocales,
routesLocaleLinks: __fullConfig.routesLocaleLinks,
noPrefixRedirect: __fullConfig.noPrefixRedirect,
debug: __fullConfig.debug,
router: routerAdapter,
hashMode: __fullConfig.hashMode,
disablePageLocales: __fullConfig.disablePageLocales,
})
}
`
});
if (typeof options.customRegexMatcher !== "undefined") {
const localeCodes = routeGenerator.locales.map((l) => l.code);
const failedCodes = localeCodes.filter((code) => !code.match(options.customRegexMatcher));
if (failedCodes.length > 0) {
throw new Error(
"Nuxt-i18n-micro: customRegexMatcher does not match the following locale codes: " + failedCodes.join(", ") + ". The regex must match ALL locale codes in your configuration."
);
}
}
const privateConfig = {
rootDir: nuxt.options.rootDir,
debug: options.debug ?? false,
locales: routeGenerator.locales ?? [],
fallbackLocale: options.fallbackLocale ?? void 0,
translationDir: options.translationDir ?? "locales",
customRegexMatcher: options.customRegexMatcher instanceof RegExp ? options.customRegexMatcher.source : options.customRegexMatcher,
routesLocaleLinks: options.routesLocaleLinks ?? {},
apiBaseUrl,
apiBaseClientHost,
apiBaseServerHost,
serverTranslationPreload: options.serverTranslationPreload ?? false
};
const privateConfigJson = JSON.stringify(privateConfig);
const configTemplate = addTemplate({
filename: "i18n.config.mjs",
write: true,
getContents: () => `const __privateConfig = ${privateConfigJson}
export function getI18nPrivateConfig() { return __privateConfig }
`
});
addImportsDir(resolver.resolve("./runtime/composables"));
if (options.plugin) {
addPlugin({
src: resolver.resolve("./runtime/plugins/01.plugin"),
name: "i18n-plugin-loader",
order: -5
});
}
if (options.hooks) {
addPlugin({
src: resolver.resolve("./runtime/plugins/05.hooks"),
name: "i18n-plugin-hooks",
order: 1
});
}
if (options.meta) {
addPlugin({
src: resolver.resolve("./runtime/plugins/02.meta"),
name: "i18n-plugin-meta",
order: 2
});
}
if (options.define) {
addPlugin({
src: resolver.resolve("./runtime/plugins/03.define"),
name: "i18n-plugin-define",
mode: "all",
order: 3
});
}
addPlugin({
src: resolver.resolve("./runtime/plugins/06.redirect"),
mode: "server",
name: "i18n-plugin-redirect",
order: 10
});
if (options.redirects !== false && options.plugin !== false) {
addRouteMiddleware({
name: "i18n-redirect",
path: resolver.resolve("./runtime/middleware/i18n-redirect.global"),
global: true
});
}
addServerImportsDir(resolver.resolve("./runtime/server/utils"));
if (translationPayloads.serverHandler) {
addServerHandler({
route: `/${apiBaseUrl}/:page/:locale/data.json`,
handler: resolver.resolve("./runtime/server/routes/i18n")
});
}
if (options.components !== false) {
addComponentsDir({
path: resolver.resolve("./runtime/components"),
pathPrefix: false,
extensions: ["vue"]
});
}
if (nuxt.options.dev && (options.hmr ?? true)) {
const translationsDir = join(nuxt.options.rootDir, options.translationDir || "locales");
const files = await globby(["**/*.json"], { cwd: translationsDir, absolute: true });
const tpl = addTemplate({
filename: "i18n.hmr.mjs",
write: true,
getContents: () => generateHmrPlugin(files.map((f) => f.replace(/\\/g, "/")))
});
addPlugin({
src: tpl.dst,
mode: "client",
name: "i18n-hmr",
order: 10
});
}
if (options.types) {
addTypeTemplate({
filename: "types/i18n-plugin.d.ts",
getContents: () => generateI18nTypes()
});
addTypeTemplate({
filename: "types/h3.d.ts",
getContents: () => `import type { Translations } from '@i18n-micro/types'
declare module 'h3' {
interface H3EventContext {
i18n?: {
locale: string
translations: Translations
}
}
}
export {}
`
});
}
addTypeTemplate({
filename: "types/i18n-internal.d.ts",
getContents: () => {
return `import type { ModuleOptionsExtend, ModulePrivateOptionsExtend, Params, Getter, PluralFunc } from '@i18n-micro/types'
import type { PathStrategy } from '@i18n-micro/path-strategy'
declare module '#build/i18n.plural.mjs' {
export function plural(key: string, count: number, params: Params, locale: string, getter: Getter): string | null
}
declare module '#build/i18n.strategy.mjs' {
export function getI18nConfig(): ModuleOptionsExtend
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
}
declare module '#i18n-internal/config' {
export function getI18nPrivateConfig(): ModulePrivateOptionsExtend
}
declare module '#i18n-internal/strategy' {
export function getI18nConfig(): ModuleOptionsExtend
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
}
declare module '#i18n-internal/plural' {
export const plural: PluralFunc
}
`;
}
});
const addDataRoutes = (pages = []) => {
if (!translationPayloads.prerenderRoutes) return;
const pagesForDataRoutes = pages.filter((p) => p.name !== void 0 && (!options.routesLocaleLinks || !options.routesLocaleLinks[p.name]));
const dataRoutes = routeGenerator.generateDataRoutes(pagesForDataRoutes, apiBaseUrl, !!options.disablePageLocales);
addPrerenderRoutes(dataRoutes);
};
nuxt.hook("pages:resolved", (pages) => {
const pagesNames = pages.map((page) => page.name).filter((name) => name !== void 0 && (!options.routesLocaleLinks || !options.routesLocaleLinks[name]));
if (!options.disableWatcher) {
routeGenerator.ensureTranslationFilesExist(pagesNames, options.translationDir, nuxt.options.rootDir, options.disablePageLocales);
}
addDataRoutes(pages);
routeGenerator.extendPages(pages);
});
if (options.disablePageLocales) {
nuxt.hook("build:before", () => addDataRoutes([]));
}
addVitePlugin(
createDefineI18nRoutePlugin({
buildDir: nuxt.options.buildDir,
rootDirs
}).vite()
);
nuxt.hook("nitro:config", (nitroConfig) => {
nitroConfig.alias = nitroConfig.alias || {};
nitroConfig.alias["#i18n-internal/plural"] = pluralTemplate.dst;
nitroConfig.alias["#i18n-internal/strategy"] = strategyTemplate.dst;
nitroConfig.alias["#i18n-internal/config"] = configTemplate.dst;
if (translationPayloads.serverAssets) {
nitroConfig.serverAssets = nitroConfig.serverAssets || [];
nitroConfig.serverAssets.push({
baseName: "i18n",
dir: translationAssetsDir
});
}
nitroConfig.routeRules = nitroConfig.routeRules || {};
nitroConfig.routeRules[`/${apiBaseUrl}/**`] = {
...nitroConfig.routeRules[`/${apiBaseUrl}/**`] || {},
cors: true,
...nuxt.options.dev ? {} : {
cache: {
maxAge: 60,
swr: true
}
}
};
const routeRules = nuxt.options.routeRules || {};
const strategy = options.strategy;
if (routeRules && Object.keys(routeRules).length && !isNoPrefixStrategy(strategy)) {
for (const [originalPath, ruleValue] of Object.entries(routeRules)) {
if (!shouldLocalizeRouteRulePath(originalPath)) continue;
routeGenerator.locales.forEach((localeObj) => {
const localeCode = localeObj.code;
if (!isLocaleAllowedForUnlocalizedRoute(routeGenerator.routeLocales, routeGenerator.locales, originalPath, localeCode)) {
return;
}
const localizedPath = routeGenerator.resolveLocalizedPath(originalPath, localeCode);
if (localizedPath === originalPath || localizedPath === normalizePath(originalPath)) {
return;
}
const { redirect, ...restRuleValue } = ruleValue;
if (!Object.keys(restRuleValue).length) return;
nitroConfig.routeRules[localizedPath] = {
...nitroConfig.routeRules[localizedPath],
...restRuleValue
};
logger.debug(`Replicated routeRule for ${localizedPath}: ${JSON.stringify(restRuleValue)}`);
});
}
}
nitroConfig.plugins = nitroConfig.plugins || [];
if (nuxt.options.dev && (options.hmr ?? true)) {
nitroConfig.plugins.push(resolver.resolve("./runtime/server/plugins/watcher.dev"));
}
nitroConfig.handlers = nitroConfig.handlers || [];
nitroConfig.handlers.unshift({
middleware: true,
handler: resolver.resolve("./runtime/server/middleware/i18n.global")
});
});
nuxt.hook("nitro:build:public-assets", (nitro) => {
const isProd = nuxt.options.dev === false;
if (isProd && translationPayloads.publicAssets) {
const publicDir = resolveTranslationPayloadPublicDir(nitro.options.output.publicDir, options);
try {
if (existsSync(translationAssetsDir)) {
fs__default.cpSync(translationAssetsDir, publicDir, { recursive: true });
logger.log(`Translation payloads copied to public directory`);
} else {
logger.warn(`Translation assets directory not found: ${translationAssetsDir}`);
}
} catch (err) {
logger.error("Error copying translations:", err);
}
}
});
nuxt.hook("prerender:routes", async (prerenderRoutes) => {
if (isNoPrefixStrategy(options.strategy)) {
return;
}
const routesSet = prerenderRoutes.routes;
const routeRules = nuxt.options.routeRules || {};
const additionalRoutes = /* @__PURE__ */ new Set();
const localeCodes = new Set(routeGenerator.locales.map((locale) => locale.code));
for (const route of routesSet) {
if (isInternalPath(route, options.excludePatterns)) {
routesSet.delete(route);
}
}
for (const route of routesSet) {
if (/\.[a-z0-9]+$/i.test(route)) {
continue;
}
if (routeRules[route]?.prerender === false) {
continue;
}
const firstSegment = route.replace(/^\//, "").split("/")[0];
if (firstSegment && localeCodes.has(firstSegment)) {
continue;
}
for (const locale of routeGenerator.locales) {
if (!isLocaleAllowedForUnlocalizedRoute(routeGenerator.routeLocales, routeGenerator.locales, route, locale.code)) {
continue;
}
const localizedRoute = routeGenerator.resolveLocalizedPath(route, locale.code);
if (localizedRoute === route) {
continue;
}
if (routeRules[localizedRoute]?.prerender === false) {
continue;
}
additionalRoutes.add(localizedRoute);
}
}
if (additionalRoutes.size > 0) {
logger.debug("[i18n prerender:routes] added localized routes:", [...additionalRoutes].sort().join(", "));
}
for (const route of additionalRoutes) {
routesSet.add(route);
}
if (withPrefixStrategy(options.strategy)) {
const deleted = [];
for (const route of routesSet) {
if (route === "/" || route === "") continue;
const firstSegment = route.replace(/^\//, "").split("/")[0];
if (firstSegment && !localeCodes.has(firstSegment)) {
routesSet.delete(route);
deleted.push(route);
}
}
if (deleted.length > 0) {
logger.info(`[i18n prerender:routes] removed from prerender list (no locale prefix): ${deleted.join(", ")}`);
}
}
});
if (nuxt.options.dev) {
setupDevToolsUI(options, resolver.resolve, rootDirs);
}
}
});
export { module as default };