UNPKG

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
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 };