UNPKG

@nuxtjs/i18n

Version:

Internationalization for Nuxt

1,398 lines (1,381 loc) 77.2 kB
import { directoryToURL, resolveModule, useNuxt, resolvePath, useLogger, addTemplate, tryUseNuxt, addBuildPlugin, addWebpackPlugin, addRspackPlugin, extendViteConfig, addServerTemplate, addServerImports, addServerPlugin, createResolver, addPlugin, addVitePlugin, useNitro, addTypeTemplate, addComponent, addImports, defineNuxtModule } from '@nuxt/kit'; import createDebug from 'debug'; import { readFileSync, existsSync } from 'node:fs'; import { assign, isArray, isString, isObject } from '@intlify/shared'; import { parse as parse$1, compileScript } from '@vue/compiler-sfc'; import { walk } from 'estree-walker'; import { mkdir, readFile, writeFile } from 'node:fs/promises'; import { createHash } from 'node:crypto'; import { resolve, relative, join as join$1, isAbsolute, parse, dirname, basename } from 'pathe'; import defu$1, { defu } from 'defu'; import { encodePath, parseURL, parseQuery } from 'ufo'; import { createRoutesContext } from 'unplugin-vue-router'; import { resolveOptions } from 'unplugin-vue-router/options'; import { findStaticImports, resolveModuleExportNames } from 'mlly'; import yamlPlugin from '@rollup/plugin-yaml'; import json5Plugin from '@miyaneee/rollup-plugin-json5'; import VueI18nPlugin from '@intlify/unplugin-vue-i18n'; import MagicString from 'magic-string'; import { createUnplugin } from 'unplugin'; import { pathToFileURL, fileURLToPath } from 'node:url'; import { transform as transform$1 } from 'esbuild'; import { genArrayFromRaw, genObjectFromRaw, genObjectFromValues, genString, genSafeVariableName, genImport, genDynamicImport } from 'knitwork'; import { mkdir as mkdir$1 } from 'fs/promises'; const NUXT_I18N_MODULE_ID = "@nuxtjs/i18n"; const VUE_I18N_PKG = "vue-i18n"; const SHARED_PKG = "@intlify/shared"; const MESSAGE_COMPILER_PKG = "@intlify/message-compiler"; const CORE_PKG = "@intlify/core"; const CORE_BASE_PKG = "@intlify/core-base"; const H3_PKG = "@intlify/h3"; const UTILS_PKG = "@intlify/utils"; const UTILS_H3_PKG = "@intlify/utils/h3"; const UFO_PKG = "ufo"; const STRATEGY_PREFIX_EXCEPT_DEFAULT = "prefix_except_default"; const DEFAULT_DYNAMIC_PARAMS_KEY = "nuxtI18nInternal"; const DEFAULT_COOKIE_KEY = "i18n_redirected"; const SWITCH_LOCALE_PATH_LINK_IDENTIFIER = "nuxt-i18n-slp"; const DEFAULT_OPTIONS = { restructureDir: "i18n", experimental: { localeDetector: "", switchLocalePathLinkSSR: false, autoImportTranslationFunctions: false, typedPages: true, typedOptionsAndMessages: false, generatedLocaleFilePathFormat: "absolute", alternateLinkCanonicalQueries: false, hmr: true }, bundle: { compositionOnly: true, runtimeOnly: false, fullInstall: true, dropMessageCompiler: false, optimizeTranslationDirective: true }, compilation: { strictMessage: true, escapeHtml: false }, customBlocks: { defaultSFCLang: "json", globalSFCScope: false }, vueI18n: "", locales: [], defaultLocale: "", defaultDirection: "ltr", routesNameSeparator: "___", trailingSlash: false, defaultLocaleRouteNameSuffix: "default", strategy: STRATEGY_PREFIX_EXCEPT_DEFAULT, lazy: false, langDir: "locales", rootRedirect: void 0, detectBrowserLanguage: { alwaysRedirect: false, cookieCrossOrigin: false, cookieDomain: null, cookieKey: DEFAULT_COOKIE_KEY, cookieSecure: false, fallbackLocale: "", redirectOn: "root", useCookie: true }, differentDomains: false, baseUrl: "", customRoutes: "page", pages: {}, skipSettingLocaleOnNavigate: false, types: "composition", debug: false, parallelPlugin: false, multiDomainLocales: false }; const NUXT_I18N_TEMPLATE_OPTIONS_KEY = "i18n.options.mjs"; const NUXT_I18N_COMPOSABLE_DEFINE_ROUTE = "defineI18nRoute"; const NUXT_I18N_COMPOSABLE_DEFINE_LOCALE = "defineI18nLocale"; const NUXT_I18N_COMPOSABLE_DEFINE_CONFIG = "defineI18nConfig"; const NUXT_I18N_COMPOSABLE_DEFINE_LOCALE_DETECTOR = "defineI18nLocaleDetector"; const NUXT_I18N_VIRTUAL_PREFIX = "#nuxt-i18n"; const TS_EXTENSIONS = [".ts", ".cts", ".mts"]; const JS_EXTENSIONS = [".js", ".cjs", ".mjs"]; const EXECUTABLE_EXTENSIONS = [...JS_EXTENSIONS, ...TS_EXTENSIONS]; const EXECUTABLE_EXT_RE = /\.[c|m]?[j|t]s$/; const debug$a = createDebug("@nuxtjs/i18n:alias"); function setupAlias({ userOptions: options, isDev, isPrepare }, nuxt) { const modules = { [VUE_I18N_PKG]: `${VUE_I18N_PKG}/dist/vue-i18n${!isDev && !isPrepare && options.bundle?.runtimeOnly ? ".runtime" : ""}.mjs`, [SHARED_PKG]: `${SHARED_PKG}/dist/shared.mjs`, [MESSAGE_COMPILER_PKG]: `${MESSAGE_COMPILER_PKG}/dist/message-compiler.mjs`, [CORE_BASE_PKG]: `${CORE_BASE_PKG}/dist/core-base.mjs`, [CORE_PKG]: `${CORE_PKG}/dist/core.node.mjs`, [UTILS_H3_PKG]: `${UTILS_PKG}/dist/h3.mjs`, // for `@intlify/utils/h3` [UFO_PKG]: UFO_PKG }; const moduleDirs = [].concat( nuxt.options.modulesDir, nuxt.options.modulesDir.map((dir) => `${dir}/${NUXT_I18N_MODULE_ID}/node_modules`) ).map((x) => directoryToURL(x)); for (const [moduleName, moduleFile] of Object.entries(modules)) { const module = resolveModule(moduleFile, { url: moduleDirs }); if (!module) throw new Error(`Could not resolve module "${moduleFile}"`); nuxt.options.alias[moduleName] = module; nuxt.options.build.transpile.push(moduleName); debug$a(`${moduleName} alias`, nuxt.options.alias[moduleName]); } } let parseSync; async function initParser() { try { parseSync = await import('oxc-parser').then((r) => r.parseSync); } catch (_) { console.warn("[nuxt-i18n]: Unable to import `oxc-parser`, falling back to `@oxc-parser/wasm`."); const { parseSync: parse } = await import('@oxc-parser/wasm'); parseSync = (filename, sourceText, options) => ( // @ts-expect-error sourceType property conflict parse(sourceText, { ...options || {}, sourceFilename: filename.replace(/\?.*$/, "") + `.${options?.lang || "ts"}`, sourceType: "module" }) ); } } function formatMessage(message) { return `[${NUXT_I18N_MODULE_ID}]: ${message}`; } function filterLocales(options, nuxt) { const project = getLayerI18n(nuxt.options._layers[0]); const includingLocales = toArray(project?.bundle?.onlyLocales ?? []).filter(isString); if (!includingLocales.length) { return; } options.locales = options.locales.filter( (locale) => includingLocales.includes(isString(locale) ? locale : locale.code) ); } function getNormalizedLocales(locales = []) { const normalized = []; for (const locale of locales) { normalized.push(isString(locale) ? { code: locale, language: locale } : locale); } return normalized; } function resolveLocales(srcDir, locales, buildDir) { const localesResolved = []; for (const locale of locales) { const resolved = assign({ meta: [] }, locale); delete resolved.file; delete resolved.files; const files = getLocaleFiles(locale); for (const f of files) { const path = resolve(srcDir, f.path); const type = getLocaleType(path); resolved.meta.push({ type, path, hash: getHash(path), loadPath: relative(buildDir, path), file: { path: f.path, cache: f.cache ?? type !== "dynamic" } }); } localesResolved.push(resolved); } return localesResolved; } function getLocaleType(path) { if (!EXECUTABLE_EXT_RE.test(path)) { return "static"; } const parsed = parseSync(path, readFileSync(path, "utf-8")); const analyzed = scanProgram(parsed.program); return analyzed === "object" ? "static" : analyzed === "function" ? "dynamic" : "unknown"; } function scanProgram(program) { let varDeclarationName; const varDeclarations = []; for (const node of program.body) { switch (node.type) { case "VariableDeclaration": for (const decl of node.declarations) { if (decl.type !== "VariableDeclarator" || decl.init == null) continue; if ("name" in decl.id === false) continue; varDeclarations.push(decl); } break; case "ExportDefaultDeclaration": if (node.declaration.type === "Identifier") { varDeclarationName = node.declaration; break; } if (node.declaration.type === "ObjectExpression") { return "object"; } if (node.declaration.type === "CallExpression" && node.declaration.callee.type === "Identifier") { const [fnNode] = node.declaration.arguments; if (fnNode.type === "FunctionExpression" || fnNode.type === "ArrowFunctionExpression") { return "function"; } } break; } } if (varDeclarationName) { const n = varDeclarations.find((x) => x.id.type === "Identifier" && x.id.name === varDeclarationName.name); if (n) { if (n.init?.type === "ObjectExpression") { return "object"; } if (n.init?.type === "CallExpression" && n.init.callee.type === "Identifier") { const [fnNode] = n.init.arguments; if (fnNode.type === "FunctionExpression" || fnNode.type === "ArrowFunctionExpression") { return "function"; } } } } return false; } async function resolveVueI18nConfigInfo(rootDir, configPath = "i18n.config", buildDir = useNuxt().options.buildDir) { const absolutePath = await resolvePath(configPath, { cwd: rootDir, extensions: EXECUTABLE_EXTENSIONS }); if (!existsSync(absolutePath)) return void 0; const relativeBase = relative(buildDir, rootDir); return { rootDir, meta: { loadPath: join$1(relativeBase, relative(rootDir, absolutePath)), // relative path: absolutePath, // absolute hash: getHash(absolutePath), type: getLocaleType(absolutePath) } }; } const getLocaleFiles = (locale) => { return toArray(locale.file ?? locale.files).filter((x) => x != null).map((x) => isString(x) ? { path: x, cache: void 0 } : x); }; function resolveRelativeLocales(locale, config) { const rootDir = useNuxt().options.rootDir; return getLocaleFiles(locale).map((file) => ({ path: resolve(rootDir, resolve(config.langDir ?? "", file.path)), cache: file.cache })); } const mergeConfigLocales = (configs, baseLocales = []) => { const mergedLocales = /* @__PURE__ */ new Map(); for (const locale of baseLocales) { mergedLocales.set(locale.code, locale); } for (const config of configs) { for (const locale of config.locales ?? []) { if (isString(locale)) { mergedLocales.set(locale, mergedLocales.get(locale) ?? { language: locale, code: locale }); continue; } const files = resolveRelativeLocales(locale, config); delete locale.file; const merged = mergedLocales.get(locale.code); if (merged != null) { merged.files ??= []; merged.files.unshift(...files); mergedLocales.set(locale.code, assign({}, locale, merged)); continue; } mergedLocales.set(locale.code, assign({}, locale, { files })); } } return Array.from(mergedLocales.values()); }; const mergeI18nModules = async (options, nuxt) => { const i18nModules = []; await nuxt.callHook( "i18n:registerModule", (config) => config.langDir && config.locales && i18nModules.push({ langDir: config.langDir, locales: config.locales }) ); if (i18nModules.length > 0) { const baseLocales = []; for (const locale of options.locales) { if (!isObject(locale)) continue; baseLocales.push(assign({}, locale, { file: void 0, files: getLocaleFiles(locale) })); } options.locales = mergeConfigLocales(i18nModules, baseLocales); } options.i18nModules = i18nModules; }; function getHash(text) { return createHash("sha256").update(text).digest("hex").substring(0, 8); } function getLayerI18n(configLayer) { const layerInlineOptions = (configLayer.config.modules || []).find( (mod) => isArray(mod) && isString(mod[0]) && [NUXT_I18N_MODULE_ID, `${NUXT_I18N_MODULE_ID}-edge`].includes(mod[0]) )?.[1]; if (configLayer.config.i18n) { return defu(configLayer.config.i18n, layerInlineOptions); } return layerInlineOptions; } const applyOptionOverrides = (options, nuxt) => { const project = nuxt.options._layers[0]; const { overrides, ...mergedOptions } = options; if (overrides) { delete options.overrides; project.config.i18n = defu(overrides, project.config.i18n); assign(options, defu(overrides, mergedOptions)); } }; function toArray(value) { return Array.isArray(value) ? value : [value]; } const COLON_RE = /:/g; function getRoutePath(tokens) { return tokens.reduce((path, token) => { return path + (token.type === 2 /* optional */ ? `:${token.value}?` : token.type === 1 /* dynamic */ ? `:${token.value}()` : token.type === 3 /* catchall */ ? `:${token.value}(.*)*` : token.type === 4 /* group */ ? "" : encodePath(token.value).replace(COLON_RE, "\\:")); }, "/"); } const PARAM_CHAR_RE = /[\w.]/; function parseSegment(segment) { let state = 0 /* initial */; let i = 0; let buffer = ""; const tokens = []; function consumeBuffer() { if (!buffer) { return; } if (state === 0 /* initial */) { throw new Error("wrong state"); } tokens.push({ type: state === 1 /* static */ ? 0 /* static */ : state === 2 /* dynamic */ ? 1 /* dynamic */ : state === 3 /* optional */ ? 2 /* optional */ : state === 4 /* catchall */ ? 3 /* catchall */ : 4 /* group */, value: buffer }); buffer = ""; } while (i < segment.length) { const c = segment[i]; switch (state) { case 0 /* initial */: buffer = ""; if (c === "[") { state = 2 /* dynamic */; } else if (c === "(") { state = 5 /* group */; } else { i--; state = 1 /* static */; } break; case 1 /* static */: if (c === "[") { consumeBuffer(); state = 2 /* dynamic */; } else if (c === "(") { consumeBuffer(); state = 5 /* group */; } else { buffer += c; } break; case 4 /* catchall */: case 2 /* dynamic */: case 3 /* optional */: case 5 /* group */: if (buffer === "...") { buffer = ""; state = 4 /* catchall */; } if (c === "[" && state === 2 /* dynamic */) { state = 3 /* optional */; } if (c === "]" && (state !== 3 /* optional */ || segment[i - 1] === "]")) { if (!buffer) { throw new Error("Empty param"); } else { consumeBuffer(); } state = 0 /* initial */; } else if (c === ")" && state === 5 /* group */) { if (!buffer) { throw new Error("Empty group"); } else { consumeBuffer(); } state = 0 /* initial */; } else if (c && PARAM_CHAR_RE.test(c)) { buffer += c; } else ; break; } i++; } if (state === 2 /* dynamic */) { throw new Error(`Unfinished param "${buffer}"`); } consumeBuffer(); return tokens; } const join = (...args) => args.filter(Boolean).join(""); function shouldPrefix(localizeOptions, options, extra = false) { const isDefaultLocale = localizeOptions.locale === (localizeOptions.defaultLocale ?? ""); const isChildWithRelativePath = localizeOptions.parent != null && !localizeOptions.path.startsWith("/"); return !extra && !isChildWithRelativePath && options.strategy !== "no_prefix" && // skip default locale if strategy is 'prefix_except_default' !(isDefaultLocale && options.strategy === "prefix_except_default"); } function adjustRoutePathForTrailingSlash(localized, trailingSlash) { const isChildWithRelativePath = localized.parent != null && !localized.path.startsWith("/"); return localized.path.replace(/\/+$/, "") + (trailingSlash ? "/" : "") || (isChildWithRelativePath ? "" : "/"); } function shouldLocalizeRoutes(options) { if (options.strategy === "no_prefix") { if (!options.differentDomains) return false; const domains = /* @__PURE__ */ new Set(); for (const locale of options.locales || []) { if (isString(locale)) continue; if (locale.domain) { if (domains.has(locale.domain)) { console.error( `Cannot use \`strategy: no_prefix\` when using multiple locales on the same domain - found multiple entries with ${locale.domain}` ); return false; } domains.add(locale.domain); } } } return true; } function localizeSingleRoute(route, options, { locales = [], parent, parentLocalized, extra = false, defaultLocales }) { if (route.redirect && !route.file) { return [route]; } const routeOptions = options.optionsResolver?.(route, locales); if (options.optionsResolver != null && routeOptions == null) { return [route]; } const componentOptions = { // filter locales to prevent child routes from being localized even though they are disabled in the configuration. locales: locales.filter((locale) => (routeOptions?.locales ?? locales).includes(locale)), paths: {}, ...routeOptions }; const { strategy, trailingSlash, multiDomainLocales, routesNameSeparator, defaultLocaleRouteNameSuffix } = options; const resultRoutes = []; for (const locale of componentOptions.locales) { const localized = { ...route, locale, parent }; const isDefaultLocale = defaultLocales.includes(locale); const addDefaultTree = isDefaultLocale && strategy === "prefix_and_default" && parent == null && !extra; if (addDefaultTree && parent == null && !extra) { const extraRoutes = localizeSingleRoute(route, options, { locales: [locale], extra: true, defaultLocales }); resultRoutes.push(...extraRoutes); } if (localized.name) { const nameSegments = [localized.name, routesNameSeparator, locale]; if (extra) { nameSegments.push(routesNameSeparator, defaultLocaleRouteNameSuffix); } localized.name = join(...nameSegments); } localized.path = componentOptions.paths?.[locale] ?? localized.path; const defaultLocale = isDefaultLocale ? locale : options.defaultLocale; if (shouldPrefix({ defaultLocale, ...localized }, options, extra)) { if (multiDomainLocales && (strategy === "prefix_except_default" || strategy === "prefix_and_default")) { resultRoutes.push({ ...localized, name: join(localized.name, routesNameSeparator, defaultLocaleRouteNameSuffix) }); } localized.path = join("/", locale, localized.path); if (isDefaultLocale && strategy === "prefix" && options.includeUnprefixedFallback) { resultRoutes.push({ ...route, locale, parent }); } } if (localized.alias) { const newAliases = []; for (const alias of toArray(localized.alias)) { let localizedAlias = alias; if (shouldPrefix({ defaultLocale, ...localized, path: alias }, options, extra)) { localizedAlias = join("/", locale, localizedAlias); } localizedAlias &&= adjustRoutePathForTrailingSlash({ ...localized, path: localizedAlias }, trailingSlash); newAliases.push(localizedAlias); } localized.alias = newAliases; } localized.path &&= adjustRoutePathForTrailingSlash(localized, trailingSlash); if (parentLocalized != null) { localized.path = localized.path.replace(parentLocalized.path + "/", ""); if (localized.path === parentLocalized.path) { localized.path = ""; } } if (localized.children) { let children = []; for (const child of localized.children) { children = children.concat( localizeSingleRoute(child, options, { locales: [locale], parent: route, parentLocalized: localized, extra, defaultLocales }) ); } localized.children = children; } resultRoutes.push(localized); } for (const x of resultRoutes) { delete x.parent; delete x.locale; } return resultRoutes; } function localizeRoutes(routes, options) { if (!shouldLocalizeRoutes(options)) return routes; let defaultLocales = [options.defaultLocale ?? ""]; if (options.differentDomains) { const domainDefaults = options.locales.filter((locale) => isObject(locale) ? locale.domainDefault : false).map((locale) => isObject(locale) ? locale.code : locale); defaultLocales = defaultLocales.concat(domainDefaults); } let processed = []; for (const route of routes) { processed = processed.concat(localizeSingleRoute(route, options, { locales: options.localeCodes, defaultLocales })); } return processed; } const debug$9 = createDebug("@nuxtjs/i18n:layers"); function checkLayerOptions(_options, nuxt) { const logger = useLogger(NUXT_I18N_MODULE_ID); const project = nuxt.options._layers[0]; const layers = nuxt.options._layers; for (const layer of layers) { const layerI18n = getLayerI18n(layer); if (layerI18n == null) continue; const configLocation = project.config.rootDir === layer.config.rootDir ? "project" : "extended"; const layerHint = `In ${configLocation} layer (\`${resolve(project.config.rootDir, layer.configFile)}\`) -`; try { if (layerI18n.langDir) { if (isString(layerI18n.langDir) && isAbsolute(layerI18n.langDir)) { logger.warn( `${layerHint} \`langDir\` is set to an absolute path (\`${layerI18n.langDir}\`) but should be set a path relative to \`srcDir\` (\`${layer.config.srcDir}\`). Absolute paths will not work in production, see https://i18n.nuxtjs.org/options/lazy#langdir for more details.` ); } for (const locale of layerI18n.locales ?? []) { if (isString(locale)) { throw new Error("When using the `langDir` option the `locales` must be a list of objects."); } if (locale.file || locale.files) continue; throw new Error( `All locales must have the \`file\` or \`files\` property set when using \`langDir\`. Found none in: ${JSON.stringify(locale, null, 2)}.` ); } } } catch (err) { if (!(err instanceof Error)) throw err; throw new Error(formatMessage(`${layerHint} ${err.message}`)); } } } function mergeLayerPages(analyzer, nuxt) { const project = nuxt.options._layers[0]; const layers = nuxt.options._layers; if (layers.length === 1) return; for (const l of layers) { const lPath = resolve(project.config.rootDir, l.config.srcDir, l.config.dir?.pages ?? "pages"); debug$9("mergeLayerPages: path ->", lPath); analyzer(lPath); } } function resolveI18nDir(layer, i18n, fromRootDir = false) { if (i18n.restructureDir !== false) { return resolve(layer.config.rootDir, i18n.restructureDir ?? "i18n"); } return resolve(layer.config.rootDir, fromRootDir ? "" : layer.config.srcDir); } function resolveLayerLangDir(layer, i18n) { i18n.restructureDir ??= "i18n"; i18n.langDir ??= i18n.restructureDir !== false ? "locales" : ""; return resolve(resolveI18nDir(layer, i18n), i18n.langDir); } function applyLayerOptions(options, nuxt) { options.locales ??= []; const configs = []; for (const layer of nuxt.options._layers) { const i18n = getLayerI18n(layer); if (i18n?.locales == null) continue; configs.push(assign({}, i18n, { langDir: resolveLayerLangDir(layer, i18n), locales: i18n.locales })); } const installModuleConfigMap = /* @__PURE__ */ new Map(); outer: for (const locale of options.locales) { if (isString(locale)) continue; const files = getLocaleFiles(locale); if (files.length === 0) continue; const langDir = parse(files[0].path).dir; const locales = installModuleConfigMap.get(langDir)?.locales ?? []; for (const file of files) { if (!isAbsolute(file.path)) continue outer; if (configs.find((config) => config.langDir === parse(file.path).dir) != null) continue outer; } locales.push(locale); installModuleConfigMap.set(langDir, { langDir, locales }); } configs.unshift(...installModuleConfigMap.values()); debug$9("merged locales", configs); options.locales = mergeConfigLocales(configs); } async function resolveLayerVueI18nConfigInfo(options) { const logger = useLogger(NUXT_I18N_MODULE_ID); const nuxt = useNuxt(); const resolved = await Promise.all( nuxt.options._layers.map(async (layer) => { const i18n = getLayerI18n(layer); const i18nDirPath = resolveI18nDir(layer, i18n || {}, true); const res = await resolveVueI18nConfigInfo(i18nDirPath, i18n?.vueI18n); if (res == null && i18n?.vueI18n != null) { logger.warn(`Vue I18n configuration file \`${i18n.vueI18n}\` not found in \`${i18nDirPath}\`. Skipping...`); return void 0; } return res; }) ); if (options.vueI18n && isAbsolute(options.vueI18n)) { resolved.unshift(await resolveVueI18nConfigInfo(parse(options.vueI18n).dir, options.vueI18n)); } return resolved.filter((x) => x != null); } const debug$8 = createDebug("@nuxtjs/i18n:pages"); async function setupPages({ localeCodes, options, isSSR }, nuxt) { if (!localeCodes.length) return; let includeUnprefixedFallback = !isSSR; nuxt.hook("nitro:init", () => { debug$8("enable includeUprefixedFallback"); includeUnprefixedFallback = options.strategy !== "prefix"; }); const pagesDir = nuxt.options.dir && nuxt.options.dir.pages ? nuxt.options.dir.pages : "pages"; const srcDir = nuxt.options.srcDir; debug$8(`pagesDir: ${pagesDir}, srcDir: ${srcDir}, trailingSlash: ${options.trailingSlash}`); const typedRouter = await setupExperimentalTypedRoutes(options, nuxt); const pagesHook = nuxt.options.experimental.scanPageMeta === "after-resolve" ? "pages:resolved" : "pages:extend"; nuxt.hook(pagesHook, async (pages) => { debug$8("pages making ...", pages); const ctx = { stack: [], srcDir, pagesDir, pages: /* @__PURE__ */ new Map() }; analyzeNuxtPages(ctx, pages); const analyzer = (pageDirOverride) => analyzeNuxtPages(ctx, pages, pageDirOverride); mergeLayerPages(analyzer, nuxt); if (typedRouter) { await typedRouter.createContext(pages).scanPages(false); } const localizedPages = localizeRoutes(pages, { ...options, localeCodes, includeUnprefixedFallback, optionsResolver: getRouteOptionsResolver(ctx, options) }); const indexPage = pages.find((x) => x.path === "/"); if (!nuxt.options._generate && options.strategy === "prefix" && indexPage != null) { localizedPages.unshift(indexPage); } if (pages !== localizedPages) { pages.length = 0; pages.unshift(...localizedPages); } debug$8("... made pages", pages); }); } const routeNamedMapTypeRE = /RouteNamedMap\b/; const declarationFile = "./types/typed-router-i18n.d.ts"; async function setupExperimentalTypedRoutes(userOptions, nuxt) { if (!nuxt.options.experimental.typedPages || userOptions.experimental?.typedPages === false) { return void 0; } const dtsFile = resolve(nuxt.options.buildDir, declarationFile); function createContext(pages) { const typedRouteroptions = { routesFolder: [], dts: dtsFile, logs: !!nuxt.options.debug, watch: false, // eslint-disable-next-line @typescript-eslint/require-await async beforeWriteFiles(rootPage) { rootPage.children.forEach((child) => child.delete()); function addPage(parent, page) { const route = parent.insert(page.path, page.file); if (page.meta) { route.addToMeta(page.meta); } if (page.alias) { route.addAlias(page.alias); } if (page.name) { route.name = page.name; } if (page.children) { page.children.forEach((child) => addPage(route, child)); } } for (const page of pages) { addPage(rootPage, page); } } }; const context = createRoutesContext(resolveOptions(typedRouteroptions)); const originalScanPages = context.scanPages.bind(context); context.scanPages = async function(watchers = false) { await mkdir(dirname(dtsFile), { recursive: true }); await originalScanPages(watchers); const dtsContent = await readFile(dtsFile, "utf-8"); if (routeNamedMapTypeRE.test(dtsContent)) { await writeFile(dtsFile, dtsContent.replace(routeNamedMapTypeRE, "RouteNamedMapI18n")); } }; return context; } addTemplate({ filename: resolve(nuxt.options.buildDir, "./types/i18n-generated-route-types.d.ts"), getContents: () => { return `// Generated by @nuxtjs/i18n declare module 'vue-router' { import type { RouteNamedMapI18n } from 'vue-router/auto-routes' export interface TypesConfig { RouteNamedMapI18n: RouteNamedMapI18n } } export {}`; } }); nuxt.hook("prepare:types", ({ references }) => { references.push({ path: declarationFile }); references.push({ types: "./types/i18n-generated-route-types.d.ts" }); }); await createContext(nuxt.apps.default?.pages ?? []).scanPages(false); return { createContext }; } function analyzePagePath(pagePath, parents = 0) { const { dir, name } = parse(pagePath); if (parents > 0 || dir !== "/") { return `${dir.slice(1, dir.length)}/${name}`; } return name; } function analyzeNuxtPages(ctx, pages, pageDirOverride) { if (pages == null || pages.length === 0) return; const pagesPath = resolve(ctx.srcDir, pageDirOverride ?? ctx.pagesDir); for (const page of pages) { if (page.file == null) continue; const splits = page.file.split(pagesPath); const filePath = splits.at(1); if (filePath == null) continue; ctx.pages.set(page, { path: analyzePagePath(filePath, ctx.stack.length), // if route has an index child the parent will not have a name name: page.name ?? page.children?.find((x) => x.path.endsWith("/index"))?.name, inRoot: ctx.stack.length === 0 }); ctx.stack.push(page.path); analyzeNuxtPages(ctx, page.children, pageDirOverride); ctx.stack.pop(); } } function getRouteOptionsResolver(ctx, options) { const { pages, defaultLocale, customRoutes } = options; const useConfig = customRoutes === "config"; debug$8("getRouteOptionsResolver useConfig", useConfig); const getter = useConfig ? getRouteOptionsFromPages : getRouteOptionsFromComponent; return (route, localeCodes) => { const ret = getter(route, localeCodes, ctx, pages, defaultLocale); debug$8("getRouteOptionsResolver resolved", route.path, route.name, ret); return ret; }; } function resolveRoutePath(path) { const normalizePath = path.slice(1, path.length); const tokens = parseSegment(normalizePath); return getRoutePath(tokens); } function getRouteOptionsFromPages(route, localeCodes, ctx, pages, defaultLocale) { const options = { locales: localeCodes, paths: {} }; const pageMeta = ctx.pages.get(route); if (pageMeta == null) { console.warn( formatMessage(`Couldn't find AnalyzedNuxtPageMeta by NuxtPage (${route.path}), so no custom route for it`) ); return options; } const valueByName = pageMeta.name ? pages[pageMeta.name] : void 0; const pageOptions = valueByName ?? pages[pageMeta.path]; if (pageOptions === false) { return void 0; } if (!pageOptions) { return options; } options.locales = options.locales.filter((locale) => pageOptions[locale] !== false); for (const locale of options.locales) { if (isString(pageOptions[locale])) { options.paths[locale] = resolveRoutePath(pageOptions[locale]); continue; } if (isString(pageOptions[defaultLocale])) { options.paths[locale] = resolveRoutePath(pageOptions[defaultLocale]); } } return options; } function getRouteOptionsFromComponent(route, localeCodes) { debug$8("getRouteOptionsFromComponent", route); if (route.file == null) { return void 0; } const options = { locales: localeCodes, paths: {} }; const componentOptions = readComponent(route.file); if (componentOptions == null) { return options; } if (componentOptions === false) { return void 0; } options.locales = componentOptions.locales || localeCodes; for (const locale in componentOptions.paths) { if (isString(componentOptions.paths[locale])) { options.paths[locale] = resolveRoutePath(componentOptions.paths[locale]); } } return options; } function readComponent(target) { try { const content = readFileSync(target, "utf-8"); const { descriptor } = parse$1(content); if (!content.includes(NUXT_I18N_COMPOSABLE_DEFINE_ROUTE)) { return void 0; } const desc = compileScript(descriptor, { id: target }); let extract = ""; const genericSetupAst = desc.scriptSetupAst || desc.scriptAst || []; for (const ast of genericSetupAst) { walk(ast, { enter(node) { if (node.type !== "CallExpression") return; if (node.callee.type === "Identifier" && node.callee.name === NUXT_I18N_COMPOSABLE_DEFINE_ROUTE) { const arg = node.arguments[0]; if (arg.type === "BooleanLiteral" || arg.type === "ObjectExpression" && verifyObjectValue(arg.properties)) { extract = desc.loc.source.slice(arg.start, arg.end); } } } }); } if (extract) { return evalValue(extract); } } catch (e) { console.warn(formatMessage(`Couldn't read component data at ${target}: (${e.message})`)); } return void 0; } function nodeNameOrValue(val, name) { return val.type === "Identifier" && val.name === name || val.type === "StringLiteral" && val.value === name; } function verifyObjectValue(properties) { for (const prop of properties) { if (prop.type !== "ObjectProperty") { console.warn(formatMessage(`'defineI18nRoute' requires an object as argument`)); return false; } if (nodeNameOrValue(prop.key, "locales")) { if (prop.value.type !== "ArrayExpression" || !verifyLocalesArrayExpression(prop.value.elements)) { console.warn(formatMessage(`expected 'locale' to be an array`)); return false; } } if (nodeNameOrValue(prop.key, "paths")) { if (prop.value.type !== "ObjectExpression" || !verifyPathsObjectExpress(prop.value.properties)) { console.warn(formatMessage(`expected 'paths' to be an object`)); return false; } } } return true; } function verifyPathsObjectExpress(properties) { for (const prop of properties) { if (prop.type !== "ObjectProperty") { console.warn(formatMessage(`'paths' is required object`)); return false; } if (prop.key.type === "Identifier" && prop.value.type !== "StringLiteral") { console.warn(formatMessage(`expected 'paths.${prop.key.name}' to be a string literal`)); return false; } if (prop.key.type === "StringLiteral" && prop.value.type !== "StringLiteral") { console.warn(formatMessage(`expected 'paths.${prop.key.value}' to be a string literal`)); return false; } } return true; } function verifyLocalesArrayExpression(elements) { for (const element of elements) { if (element?.type !== "StringLiteral") { console.warn(formatMessage(`required 'locales' value string literal`)); return false; } } return true; } function evalValue(value) { try { return new Function(`return (${value})`)(); } catch (_e) { console.error(formatMessage(`Cannot evaluate value: ${value}`)); return; } } const VIRTUAL_PREFIX_HEX = "\0"; function asI18nVirtual(val) { return NUXT_I18N_VIRTUAL_PREFIX + "/" + val; } function isVue(id, opts = {}) { const { search } = parseURL(decodeURIComponent(pathToFileURL(id).href)); if (id.endsWith(".vue") && !search) { return true; } if (!search) { return false; } const query = parseQuery(search); if (query.nuxt_component) { return false; } if (query.macro && (search === "?macro=true" || !opts.type || opts.type.includes("script"))) { return true; } const type = "setup" in query ? "script" : query.type; if (!("vue" in query) || opts.type && !opts.type.includes(type)) { return false; } return true; } const debug$7 = createDebug("@nuxtjs/i18n:transform:macros"); const TransformMacroPlugin = (options) => createUnplugin(() => { return { name: "nuxtjs:i18n-macros-transform", enforce: "pre", transformInclude(id) { if (!id || id.startsWith(VIRTUAL_PREFIX_HEX)) { return false; } return isVue(id, { type: ["script"] }); }, transform(code, id) { debug$7("transform", id); const parsed = parse$1(code, { sourceMap: false }); const script = parsed.descriptor.scriptSetup ?? parsed.descriptor.script; if (!script) { return; } const s = new MagicString(code); const match = script.content.match(new RegExp(`\\b${NUXT_I18N_COMPOSABLE_DEFINE_ROUTE}\\s*\\(\\s*`)); if (match?.[0]) { const scriptString = new MagicString(script.content); scriptString.overwrite(match.index, match.index + match[0].length, `false && /*#__PURE__*/ ${match[0]}`); s.overwrite(script.loc.start.offset, script.loc.end.offset, scriptString.toString()); } if (s.hasChanged()) { debug$7("transformed: id -> ", id); debug$7("transformed: code -> ", s.toString()); return { code: s.toString(), map: options.sourcemap ? s.generateMap({ hires: true }) : void 0 }; } } }; }); async function transform(input, options) { return await transform$1(input, { ...tryUseNuxt()?.options.esbuild?.options, ...options }); } const debug$6 = createDebug("@nuxtjs/i18n:transform:resource"); const ResourcePlugin = (options, ctx) => createUnplugin(() => { debug$6("options", options); const pattern = [NUXT_I18N_COMPOSABLE_DEFINE_LOCALE, NUXT_I18N_COMPOSABLE_DEFINE_CONFIG].join("|"); const DEFINE_I18N_FN_RE = new RegExp(`\\b(${pattern})\\s*\\((.+)\\s*\\)`, "gms"); const i18nFileMetas = [...ctx.localeInfo.flatMap((x) => x.meta), ...ctx.vueI18nConfigPaths.map((x) => x.meta)]; const i18nPathSet = /* @__PURE__ */ new Set(); const i18nFileHashSet = /* @__PURE__ */ new Map(); for (const meta of i18nFileMetas) { if (i18nPathSet.has(meta.path)) continue; i18nPathSet.add(meta.path); i18nFileHashSet.set(asI18nVirtual(meta.hash), meta.path); } return { name: "nuxtjs:i18n-resource", enforce: "pre", // resolve virtual hash to file path resolveId(id) { if (!id || id.startsWith(VIRTUAL_PREFIX_HEX) || !id.startsWith(NUXT_I18N_VIRTUAL_PREFIX)) { return; } if (i18nFileHashSet.has(id)) { return i18nFileHashSet.get(id); } }, transformInclude(id) { if (!id || id.startsWith(VIRTUAL_PREFIX_HEX)) { return false; } if (i18nPathSet.has(id)) { debug$6("transformInclude", id); return /\.([c|m]?[j|t]s)$/.test(id); } }, /** * Match and replace `defineI18nX(<content>)` with its `<content>` */ async transform(_code, id) { debug$6("transform", id); let code = _code; const staticImports = findStaticImports(_code); for (const x of staticImports) { i18nPathSet.add(await resolvePath(resolve(dirname(id), x.specifier))); } if (/(c|m)?ts$/.test(id)) { code = (await transform(_code, { loader: "ts" })).code; } const s = new MagicString(code); const matches = code.matchAll(DEFINE_I18N_FN_RE); for (const match of matches) { s.overwrite(match.index, match.index + match[0].length, match[2]); } if (s.hasChanged()) { return { code: s.toString(), map: options.sourcemap && !/\.([c|m]?ts)$/.test(id) ? s.generateMap({ hires: true }) : null }; } } }; }); const debug$5 = createDebug("@nuxtjs/i18n:function:injection"); const TRANSLATION_FUNCTIONS = ["$t", "$rt", "$d", "$n", "$tm", "$te"]; const TRANSLATION_FUNCTIONS_RE = /\$(t|rt|d|n|tm|te)\s*\(\s*/; const TRANSLATION_FUNCTIONS_MAP = { $t: "t: $t", $rt: "rt: $rt", $d: "d: $d", $n: "n: $n", $tm: "tm: $tm", $te: "te: $te" }; const TransformI18nFunctionPlugin = (options) => createUnplugin(() => { return { name: "nuxtjs:i18n-function-injection", enforce: "pre", transformInclude(id) { return isVue(id, { type: ["script"] }); }, transform(code, id) { debug$5("transform", id); const script = extractScriptContent(code); if (!script || !TRANSLATION_FUNCTIONS_RE.test(script)) { return; } const scriptSetup = parse$1(code, { sourceMap: false }).descriptor.scriptSetup; if (!scriptSetup) { return; } const ast = parseSync(id, script, { lang: "tsx" }); let scopeTracker = new ScopeTracker(); const varCollector = new ScopedVarsCollector(); walk(ast.program, { enter(_node) { if (_node.type === "BlockStatement") { scopeTracker.enterScope(); varCollector.refresh(scopeTracker.curScopeKey); } else if (_node.type === "FunctionDeclaration" && _node.id) { varCollector.addVar(_node.id.name); } else if (_node.type === "VariableDeclarator") { varCollector.collect(_node.id); } }, leave(_node) { if (_node.type === "BlockStatement") { scopeTracker.leaveScope(); varCollector.refresh(scopeTracker.curScopeKey); } } }); const missingFunctionDeclarators = /* @__PURE__ */ new Set(); scopeTracker = new ScopeTracker(); walk(ast.program, { enter(_node) { if (_node.type === "BlockStatement") { scopeTracker.enterScope(); } if (_node.type !== "CallExpression" || _node.callee.type !== "Identifier") { return; } const node = _node; const name = "name" in node.callee && node.callee.name; if (!name || !TRANSLATION_FUNCTIONS.includes(name)) { return; } if (varCollector.hasVar(scopeTracker.curScopeKey, name)) { return; } missingFunctionDeclarators.add(name); }, leave(_node) { if (_node.type === "BlockStatement") { scopeTracker.leaveScope(); } } }); const s = new MagicString(code); if (missingFunctionDeclarators.size > 0) { debug$5(`injecting ${Array.from(missingFunctionDeclarators).join(", ")} declaration to ${id}`); const assignments = []; for (const missing of missingFunctionDeclarators) { assignments.push(TRANSLATION_FUNCTIONS_MAP[missing]); } s.overwrite( scriptSetup.loc.start.offset, scriptSetup.loc.end.offset, ` const { ${assignments.join(", ")} } = useI18n() ` + scriptSetup.content ); } if (s.hasChanged()) { debug$5("transformed: id -> ", id); debug$5("transformed: code -> ", s.toString()); return { code: s.toString(), map: options.sourcemap ? s.generateMap({ hires: true }) : void 0 }; } } }; }); const SFC_SCRIPT_RE = /<script\s*[^>]*>([\s\S]*?)<\/script\s*[^>]*>/i; function extractScriptContent(html) { const match = html.match(SFC_SCRIPT_RE); if (match && match[1]) { return match[1].trim(); } return null; } class ScopeTracker { // the top of the stack is not a part of current key, it is used for next level scopeIndexStack; curScopeKey; constructor() { this.scopeIndexStack = [0]; this.curScopeKey = ""; } getKey() { return this.scopeIndexStack.slice(0, -1).join("-"); } enterScope() { this.scopeIndexStack.push(0); this.curScopeKey = this.getKey(); } leaveScope() { this.scopeIndexStack.pop(); this.curScopeKey = this.getKey(); this.scopeIndexStack[this.scopeIndexStack.length - 1]++; } } class ScopedVarsCollector { curScopeKey; all; constructor() { this.all = /* @__PURE__ */ new Map(); this.curScopeKey = ""; } refresh(scopeKey) { this.curScopeKey = scopeKey; } addVar(name) { let vars = this.all.get(this.curScopeKey); if (!vars) { vars = /* @__PURE__ */ new Set(); this.all.set(this.curScopeKey, vars); } vars.add(name); } hasVar(scopeKey, name) { const indices = scopeKey.split("-").map(Number); for (let i = indices.length; i >= 0; i--) { if (this.all.get(indices.slice(0, i).join("-"))?.has(name)) { return true; } } return false; } collect(n) { const t = n.type; if (t === "Identifier") { this.addVar(n.name); } else if (t === "RestElement") { this.collect(n.argument); } else if (t === "AssignmentPattern") { this.collect(n.left); } else if (t === "ArrayPattern") { n.elements.forEach((e) => e && this.collect(e)); } else if (t === "ObjectPattern") { n.properties.forEach((p) => { if (p.type === "RestElement") { this.collect(p); } else { this.collect(p.value); } }); } } } const version = "9.5.5"; const debug$4 = createDebug("@nuxtjs/i18n:bundler"); async function extendBundler(ctx, nuxt) { addTemplate({ write: true, filename: "nuxt-i18n-logger.mjs", getContents() { if (!ctx.options.debug && !nuxt.options._i18nTest) { return `export function createLogger() {}`; } return ` import { createConsola } from 'consola' const debugLogger = createConsola({ level: ${ctx.options.debug === "verbose" ? 999 : 4} }).withTag('i18n') export function createLogger(label) { return debugLogger.withTag(label) }`; } }); nuxt.options.alias[asI18nVirtual("logger")] = ctx.resolver.resolve(nuxt.options.buildDir, "./nuxt-i18n-logger.mjs"); const pluginOptions = { sourcemap: !!nuxt.options.sourcemap.server || !!nuxt.options.sourcemap.client }; const resourcePlugin = ResourcePlugin(pluginOptions, ctx); addBuildPlugin(resourcePlugin); nuxt.hook("nitro:config", async (cfg) => { cfg.rollupConfig.plugins = await cfg.rollupConfig.plugins || []; cfg.rollupConfig.plugins = toArray(cfg.rollupConfig.plugins); cfg.rollupConfig.plugins.push(resourcePlugin.rollup()); }); const { options } = ctx; const localePaths = [.../* @__PURE__ */ new Set([...ctx.localeInfo.flatMap((x) => x.meta.map((m) => m.path))])]; const vueI18nPluginOptions = { allowDynamic: true, include: localePaths.length ? localePaths : void 0, runtimeOnly: options.bundle.runtimeOnly, fullInstall: options.bundle.fullInstall, onlyLocales: options.bundle.onlyLocales, escapeHtml: options.compilation.escapeHtml, compositionOnly: options.bundle.compositionOnly, strictMessage: options.compilation.strictMessage, defaultSFCLang: options.customBlocks.defaultSFCLang, globalSFCScope: options.customBlocks.globalSFCScope, dropMessageCompiler: options.bundle.dropMessageCompiler, optimizeTranslationDirective: options.bundle.optimizeTranslationDirective }; addBuildPlugin({ vite: () => VueI18nPlugin.vite(vueI18nPluginOptions), webpack: () => VueI18nPlugin.webpack(vueI18nPluginOptions) }); addBuildPlugin(TransformMacroPlugin(pluginOptions)); if (options.experimental.autoImportTranslationFunctions) { addBuildPlugin(TransformI18nFunctionPlugin(pluginOptions)); } const defineConfig = { ...getFeatureFlags(options.bundle), __DEBUG__: String(!!options.debug), __TEST__: String(!!options.debug || nuxt.options._i18nTest), __NUXT_I18N_VERSION__: JSON.stringify(version) }; if (nuxt.options.builder === "@nuxt/webpack-builder") { try { const webpack = await import('webpack').then((m) => m.default || m); addWebpackPlugin(new webpack.DefinePlugin(defineConfig)); } catch (e) { debug$4(e.message); } } if (nuxt.options.builder === "@nuxt/rspack-builder") { try { const { rspack } = await import('@rspack/core'); addRspackPlugin(new rspack.DefinePlugin(defineConfig)); } catch (e) { debug$4(e.message); } } extendViteConfig((config) => { config.define ??= {}; config.define["__DEBUG__"] = defineConfig["__DEBUG__"]; config.define["__TEST__"] = defineConfig["__TEST__"]; config.define["__NUXT_I18N_VERSION__"] = JSON.stringify(version); debug$4("vite.config.define", config.define); }); } function getFeatureFlags({ compositionOnly = true, fullInstall = true, dropMessageCompiler = false }) { return { __VUE_I18N_FULL_INSTALL__: String(fullInstall), __VUE_I18N_LEGACY_API__: String(!compositionOnly), __INTLIFY_PROD_DEVTOOLS__: "false", __INTLIFY_DROP_MESSAGE_COMPILER__: String(dropMessageCompiler) }; } const debug$3 = createDebug("@nuxtjs/i18n:nitro"); async function setupNitro(ctx, nuxt) { const [enableServerIntegration, localeDetectionPath] = await resolveLocaleDetectorPath(nuxt); const setupServer = enableServerIntegration || ctx.options.experimental.typedOptionsAndMessages && ctx.isDev; if (setupServer) { addServerTemplate({ filename: "#internal/i18n/options.mjs", getContents: () => nuxt.vfs["#build/i18n.options.mjs"]?.replace(/\/\*\* client \*\*\/[\s\S]*\/\*\* client-end \*\*\//, "") });