UNPKG

@intlify/unplugin-vue-i18n

Version:
1,362 lines (1,342 loc) 46.9 kB
import createDebug from 'debug'; import { createUnplugin } from 'unplugin'; import { isArray, isString, isBoolean, assign, isEmptyObject, isNumber, generateCodeFrame } from '@intlify/shared'; import { normalize, parse, resolve, dirname } from 'pathe'; import { generateJSON, generateYAML, generateJavaScript, generateTypescript } from '@intlify/bundle-utils'; import { createFilter } from '@rollup/pluginutils'; import fg from 'fast-glob'; import { promises } from 'node:fs'; import { parse as parse$1 } from 'vue/compiler-sfc'; import pc from 'picocolors'; import path from 'node:path'; import eslintUitls from '@eslint-community/eslint-utils'; import { transformVTDirective } from '@intlify/vue-i18n-extensions'; import { analyze } from '@typescript-eslint/scope-manager'; function normalizeGlobOption(val) { if (!val) return void 0; return isString(val) ? [normalize(val)] : val.map(normalize); } function resolveOptions(options) { const moduleType = options.module || "vue-i18n"; let onlyLocales = []; if (options.onlyLocales) { onlyLocales = isArray(options.onlyLocales) ? options.onlyLocales : [options.onlyLocales]; } const forceStringify = !!options.forceStringify; const defaultSFCLang = isString(options.defaultSFCLang) ? options.defaultSFCLang : "json"; const globalSFCScope = !!options.globalSFCScope; const runtimeOnly = isBoolean(options.runtimeOnly) ? options.runtimeOnly : true; const dropMessageCompiler = !!options.dropMessageCompiler; const compositionOnly = moduleType === "vue-i18n" ? isBoolean(options.compositionOnly) ? options.compositionOnly : true : true; const fullInstall = moduleType === "vue-i18n" ? isBoolean(options.fullInstall) ? options.fullInstall : true : false; const ssrBuild = !!options.ssr; const allowDynamic = !!options.allowDynamic; const strictMessage = isBoolean(options.strictMessage) ? options.strictMessage : true; const escapeHtml = !!options.escapeHtml; const optimizeTranslationDirective = isString(options.optimizeTranslationDirective) || isArray(options.optimizeTranslationDirective) ? options.optimizeTranslationDirective : !!options.optimizeTranslationDirective; const translationIdentifiers = /* @__PURE__ */ new Map(); const transformI18nBlock = typeof options.transformI18nBlock === "function" ? options.transformI18nBlock : null; const treeShaking = options.treeShaking ? typeof options.treeShaking === "object" ? { safelist: options.treeShaking.safelist || [], dynamicKeyStrategy: options.treeShaking.dynamicKeyStrategy || "keep-all", scanPatterns: options.treeShaking.scanPatterns } : { safelist: [], dynamicKeyStrategy: "keep-all" } : null; return { include: normalizeGlobOption(options.include || []), exclude: normalizeGlobOption(options.exclude), module: moduleType, onlyLocales, forceStringify, defaultSFCLang, globalSFCScope, runtimeOnly, dropMessageCompiler, compositionOnly, fullInstall, ssrBuild, allowDynamic, strictMessage, escapeHtml, optimizeTranslationDirective, translationIdentifiers, transformI18nBlock, treeShaking }; } const PKG_NAME = "unplugin-vue-i18n"; function warn(...args) { console.warn(pc.yellow(pc.bold(`[${PKG_NAME}] `)), ...args); } function error(...args) { console.error(pc.red(pc.bold(`[${PKG_NAME}] `)), ...args); } function raiseError(message) { throw new Error(`[${PKG_NAME}] ${message}`); } function resolveNamespace(name) { return `${PKG_NAME}:${name}`; } function getVitePlugin(config, name) { return config.plugins.find((p) => p.name === name); } function checkVuePlugin(vuePlugin) { if (vuePlugin == null || !vuePlugin.api) { error( "`@vitejs/plugin-vue` plugin is not found or invalid version. Please install `@vitejs/plugin-vue` v4.3.4 or later version." ); return false; } return true; } const isWindows = typeof process !== "undefined" && process.platform === "win32"; const windowsSlashRE = /\\/g; function slash(p) { return p.replace(windowsSlashRE, "/"); } function normalizePath(id) { return path.posix.normalize(isWindows ? slash(id) : id); } function parseVueRequest(id) { const [filename, rawQuery] = id.split(`?`, 2); const params = new URLSearchParams(rawQuery); const ret = {}; const langPart = Object.keys(Object.fromEntries(params)).find((key) => /lang\./i.test(key)); ret.vue = params.has("vue"); ret.global = params.has("global"); ret.src = params.has("src"); ret.raw = params.has("raw"); if (params.has("type")) { ret.type = params.get("type"); } if (params.has("blockType")) { ret.blockType = params.get("blockType"); } if (params.has("index")) { ret.index = Number(params.get("index")); } if (params.has("locale")) { ret.locale = params.get("locale"); } if (langPart) { const [, lang] = langPart.split("."); ret.lang = lang; } else if (params.has("lang")) { ret.lang = params.get("lang"); } if (params.has("issuerPath")) { ret.issuerPath = params.get("issuerPath"); } return { filename, query: ret }; } function getVueCompiler(vuePlugin) { return vuePlugin?.api?.options.compiler; } function getVuePluginOptions(vuePlugin) { return { isProduction: vuePlugin?.api?.options.isProduction, root: vuePlugin?.api?.options.root, template: vuePlugin?.api?.options.template, compiler: vuePlugin?.api?.options.compiler }; } function createDescriptor(filename, source, { template, compiler }) { const { descriptor, errors } = compiler.parse(source, { filename, templateParseOptions: template?.compilerOptions }); return { descriptor, errors }; } function getDescriptor(filename, code, options) { const { descriptor, errors } = createDescriptor(filename, code, options); if (errors.length) { throw errors[0]; } return descriptor; } const INTLIFY_BUNDLE_IMPORT_ID = "@intlify/unplugin-vue-i18n/messages"; const VIRTUAL_PREFIX = "\0"; const RE_RESOURCE_FORMAT = /\.(json5?|ya?ml|[c|m]?[j|t]s)$/; const RE_SFC_I18N_CUSTOM_BLOCK = /\?vue&type=i18n/; const RE_SFC_I18N_WEBPACK_CUSTOM_BLOCK = /blockType=i18n/; const VITE_VIRTUAL_PREFIX = "\0intlify-i18n-"; const debug$3 = createDebug(resolveNamespace("resource")); function resourcePlugin({ onlyLocales, include, exclude, module, forceStringify, defaultSFCLang, globalSFCScope, runtimeOnly, dropMessageCompiler, compositionOnly, fullInstall, ssrBuild, strictMessage, allowDynamic, escapeHtml, transformI18nBlock }, meta, collector) { function resolveIncludeExclude() { const customBlockInclude = meta.framework === "vite" ? RE_SFC_I18N_CUSTOM_BLOCK : RE_SFC_I18N_WEBPACK_CUSTOM_BLOCK; return include ? [[...include, customBlockInclude], exclude] : [[RE_RESOURCE_FORMAT, customBlockInclude], exclude]; } function resolveIncludeExcludeForLegacy() { const customBlockInclude = meta.framework === "vite" ? RE_SFC_I18N_CUSTOM_BLOCK : RE_SFC_I18N_WEBPACK_CUSTOM_BLOCK; return include ? [[...include, customBlockInclude], void 0] : [void 0, ["**/**"]]; } let _filter = null; async function getFilter() { if (_filter != null) { return _filter; } if (meta.framework == "webpack") { debug$3("Using filter for webpack"); _filter = createFilter(...resolveIncludeExclude()); } else if (hasViteJsonPlugin) { debug$3("Using filter for rollup-vite"); _filter = createFilter(...resolveIncludeExcludeForLegacy()); } else { debug$3("Using filter for rolldown-vite"); _filter = createFilter(...resolveIncludeExclude()); } return _filter; } const getVueI18nAliasPath = ({ ssr = false, runtimeOnly: runtimeOnly2 = false }) => { return `${module}/dist/${module}${runtimeOnly2 ? ".runtime" : ""}.${!ssr ? "esm-bundler.js" : "node.mjs"}`; }; let isProduction = false; let sourceMap = false; let hasViteJsonPlugin = false; const vueI18nAliasName = module; debug$3(`vue-i18n alias name: ${vueI18nAliasName}`); let vuePlugin = null; const getSfcParser = () => { return vuePlugin ? getVueCompiler(vuePlugin).parse : parse$1; }; const virtualIdToRealPath = /* @__PURE__ */ new Map(); const realPathToVirtualId = /* @__PURE__ */ new Map(); let virtualCounter = 0; function intlifyVirtualize(realPath) { let virtualId = realPathToVirtualId.get(realPath); if (!virtualId) { virtualId = `${VITE_VIRTUAL_PREFIX}${virtualCounter++}`; virtualIdToRealPath.set(virtualId, realPath); realPathToVirtualId.set(realPath, virtualId); } return virtualId; } function isIntlifyVirtualId(id) { return virtualIdToRealPath.has(id); } return { name: resolveNamespace("resource"), /** * NOTE: * * For vite, If we have json (including SFC's custom block), * transform it first because it will be transformed into javascript code by `vite:json` plugin. * * For webpack, This plugin will handle with 'post', because vue-loader generate the request query. */ enforce: meta.framework === "vite" ? "pre" : "post", vite: { config() { const defineConfig = { define: { __VUE_I18N_LEGACY_API__: !compositionOnly, __VUE_I18N_FULL_INSTALL__: fullInstall, __INTLIFY_DROP_MESSAGE_COMPILER__: dropMessageCompiler, __VUE_I18N_PROD_DEVTOOLS__: false } }; debug$3("define Config:", defineConfig); const aliasConfig = { resolve: { alias: { [vueI18nAliasName]: getVueI18nAliasPath({ ssr: ssrBuild, runtimeOnly }) } } }; debug$3("alias Config:", aliasConfig); return assign(defineConfig, aliasConfig); }, async configResolved(config) { vuePlugin = getVitePlugin(config, "vite:vue"); if (!checkVuePlugin(vuePlugin)) { return; } isProduction = config.isProduction; sourceMap = config.command === "build" ? !!config.build.sourcemap : false; debug$3(`configResolved: isProduction = ${isProduction}, sourceMap = ${sourceMap}`); const jsonPlugin = getVitePlugin(config, "vite:json"); hasViteJsonPlugin = !!jsonPlugin; if (jsonPlugin && jsonPlugin.transform) { const transform = jsonPlugin.transform; const isObjectHook = typeof transform !== "function" && "handler" in transform; const orgTransform = isObjectHook ? transform.handler : transform; async function overrideJson(code, id) { const filter = await getFilter(); if (!/\.json$/.test(id) || filter(id)) { return; } const { query } = parseVueRequest(id); if (query.vue) { return; } debug$3("org json plugin"); return orgTransform.apply(this, [code, id]); } if (isObjectHook) { transform.handler = overrideJson; } else { jsonPlugin.transform = overrideJson; } } }, async handleHotUpdate({ file, server }) { if (RE_RESOURCE_FORMAT.test(file)) { const module2 = server.moduleGraph.getModuleById( asVirtualId(INTLIFY_BUNDLE_IMPORT_ID, meta.framework) ); if (module2) { server.moduleGraph.invalidateModule(module2); return [module2]; } } } }, webpack(compiler) { isProduction = compiler.options.mode !== "development"; sourceMap = !!compiler.options.devtool; debug$3(`webpack: isProduction = ${isProduction}, sourceMap = ${sourceMap}`); compiler.options.resolve = normalizeConfigResolveAlias( compiler.options.resolve, meta.framework ); compiler.options.resolve.alias[vueI18nAliasName] = getVueI18nAliasPath({ ssr: ssrBuild, runtimeOnly }); debug$3( `set ${vueI18nAliasName}: ${getVueI18nAliasPath({ ssr: ssrBuild, runtimeOnly })}` ); compiler.options.plugins.push( new compiler.webpack.DefinePlugin({ __VUE_I18N_LEGACY_API__: JSON.stringify(compositionOnly), __VUE_I18N_FULL_INSTALL__: JSON.stringify(fullInstall), __INTLIFY_PROD_DEVTOOLS__: "false" }) ); debug$3(`set __VUE_I18N_LEGACY_API__ is '${compositionOnly}'`); debug$3(`set __VUE_I18N_FULL_INSTALL__ is '${fullInstall}'`); const filter = createFilter(...resolveIncludeExclude()); if (compiler.options.module) { compiler.options.module.rules.push({ test: RE_RESOURCE_FORMAT, type: "javascript/auto", include(resource) { debug$3("webpack resource include", resource); return filter(resource); } }); } }, resolveId: { async handler(id, importer) { if (id === INTLIFY_BUNDLE_IMPORT_ID) { return asVirtualId(id, meta.framework); } if (meta.framework === "vite" && !hasViteJsonPlugin) { if (id.includes("?vue&type=i18n") && /[?&]lang\.(?:json|json5)(?:$|&)/.test(id)) { return intlifyVirtualize(id); } const idPath = id.split("?")[0]; if (!RE_RESOURCE_FORMAT.test(idPath)) return; let resolvedPath; if (idPath.startsWith(".")) { const realImporter = importer && isIntlifyVirtualId(importer) ? virtualIdToRealPath.get(importer) : importer; if (!realImporter) return; resolvedPath = resolve(dirname(realImporter), idPath); } else if (idPath.startsWith("/") || /^[a-z]:[/\\]/i.test(idPath)) { resolvedPath = idPath; } else { return; } const filter = await getFilter(); if (!filter(resolvedPath)) return; return intlifyVirtualize(resolvedPath); } } }, load: { async handler(id) { debug$3("load", id); if (INTLIFY_BUNDLE_IMPORT_ID === getVirtualId(id, meta.framework) && include) { let resourcePaths = []; for (const inc of include) { resourcePaths = [...resourcePaths, ...await fg(inc)]; } resourcePaths = resourcePaths.filter((el, pos) => resourcePaths.indexOf(el) === pos); const code = await generateBundleResources(resourcePaths, isProduction, { forceStringify, strictMessage, escapeHtml, usedKeyFilter: collector ? (keyPath) => collector.shouldKeepKey(keyPath) : void 0 }); return { moduleType: "js", code, map: { mappings: "" } }; } if (isIntlifyVirtualId(id)) { const realId = virtualIdToRealPath.get(id); if (realId.includes("?vue&type=i18n")) { const { filename, query } = parseVueRequest(realId); this.addWatchFile(filename); const sfcSource = await promises.readFile(filename, "utf-8"); const { descriptor } = getSfcParser()(sfcSource, { sourceMap, filename }); const block = descriptor.customBlocks[query.index]; if (!block) return; let source = block.src ? await promises.readFile(block.src, "utf-8") : block.content; if (typeof transformI18nBlock === "function") { const modifiedSource = transformI18nBlock(source); if (modifiedSource && typeof modifiedSource === "string") { source = modifiedSource; } else { warn("transformI18nBlock should return a string"); } } let langInfo2 = defaultSFCLang; if (isString(query.lang)) { langInfo2 = query.src ? query.lang === "i18n" ? defaultSFCLang : query.lang : query.lang; } if (/\.?[cm]?[jt]s$/.test(langInfo2)) { source = `export default ${source.replace(/^[\s;]+/, "")}`; } const generate2 = getGenerator(langInfo2, generateYAML); const isGlobalBlock = globalSFCScope || !!query.global; const parseOptions2 = getOptions( filename, isProduction, query, sourceMap, { isGlobal: globalSFCScope, allowDynamic, jit: true, strictMessage, escapeHtml, onlyLocales, forceStringify, usedKeyFilter: collector && isGlobalBlock ? (keyPath) => collector.shouldKeepKey(keyPath) : void 0 } ); const { code: generatedCode2 } = await generate2(source, parseOptions2); return { moduleType: "js", code: generatedCode2, map: { mappings: "" } }; } this.addWatchFile(realId); const code = await promises.readFile(realId, "utf-8"); const langInfo = parse(realId).ext; const generate = getGenerator(langInfo); const parseOptions = getOptions( realId, isProduction, {}, sourceMap, { isGlobal: globalSFCScope, allowDynamic, strictMessage, escapeHtml, jit: true, onlyLocales, forceStringify, usedKeyFilter: collector ? (keyPath) => collector.shouldKeepKey(keyPath) : void 0 } ); const { code: generatedCode } = await generate(code, parseOptions); return { moduleType: "js", code: generatedCode, map: { mappings: "" } }; } } }, transform: { async handler(code, id) { if (isIntlifyVirtualId(id)) return; const filter = await getFilter(); if (!filter(id)) { return; } const { filename, query } = parseVueRequest(id); debug$3("transform", id, JSON.stringify(query), filename); let langInfo = defaultSFCLang; let inSourceMap; if (!query.vue) { langInfo = parse(filename).ext; const generate = getGenerator(langInfo); const parseOptions = getOptions( filename, isProduction, query, sourceMap, { inSourceMap, isGlobal: globalSFCScope, allowDynamic, strictMessage, escapeHtml, jit: true, onlyLocales, forceStringify, usedKeyFilter: collector ? (keyPath) => collector.shouldKeepKey(keyPath) : void 0 } ); debug$3("parseOptions", parseOptions); const { code: generatedCode, map } = await generate(code, parseOptions); debug$3("generated code", generatedCode); debug$3("sourcemap", map, sourceMap); if (code === generatedCode) return; return { moduleType: "js", code: generatedCode, // prettier-ignore map: { mappings: "" } }; } else { if (isCustomBlock(query)) { if (isString(query.lang)) { langInfo = query.src ? query.lang === "i18n" ? defaultSFCLang : query.lang : query.lang; } else if (defaultSFCLang) { langInfo = defaultSFCLang; } debug$3("langInfo", langInfo); const generate = getGenerator(langInfo, generateYAML); const isGlobalBlock = globalSFCScope || !!query.global; const parseOptions = getOptions( filename, isProduction, query, sourceMap, { inSourceMap, isGlobal: globalSFCScope, allowDynamic, jit: true, strictMessage, escapeHtml, onlyLocales, forceStringify, usedKeyFilter: collector && isGlobalBlock ? (keyPath) => collector.shouldKeepKey(keyPath) : void 0 } ); debug$3("parseOptions", parseOptions); let source = await getCode( code, filename, sourceMap, query, getSfcParser(), meta.framework ); if (typeof transformI18nBlock === "function") { const modifiedSource = transformI18nBlock(source); if (modifiedSource && typeof modifiedSource === "string") { source = modifiedSource; } else { warn("transformI18nBlock should return a string"); } } if (/\.?[cm]?[jt]s$/.test(langInfo)) { source = `export default ${source.replace(/^[\s;]+/, "")}`; } const { code: generatedCode, map } = await generate(source, parseOptions); debug$3("generated code", generatedCode); debug$3("sourcemap", map, sourceMap); if (code === generatedCode) return; return { moduleType: "js", code: generatedCode, // prettier-ignore map: { mappings: "" } }; } } } } }; } function getGenerator(ext, fallback = generateJSON) { if (/\.?json5?$/.test(ext)) { return generateJSON; } if (/\.?ya?ml$/.test(ext)) { return generateYAML; } if (/\.?[cm]?js$/.test(ext)) { return generateJavaScript; } if (/\.?[cm]?ts$/.test(ext)) { return generateTypescript; } return fallback; } function normalizeConfigResolveAlias(resolve2, framework) { if (resolve2 && resolve2.alias) { return resolve2; } if (!resolve2) { if (framework === "vite") { return { alias: [] }; } else if (framework === "webpack") { return { alias: {} }; } } else if (!resolve2.alias) { if (framework === "vite") { resolve2.alias = []; return resolve2; } else if (framework === "webpack") { resolve2.alias = {}; return resolve2; } } } async function generateBundleResources(resources, isProduction, { forceStringify = false, isGlobal = false, onlyLocales = [], strictMessage = true, escapeHtml = false, jit = true, transformI18nBlock = void 0, usedKeyFilter = void 0 }) { const codes = []; for (const res of resources) { debug$3(`${res} bundle loading ...`); if (/\.(json5?|ya?ml)$/.test(res)) { const { ext, name } = parse(res); const source = await getRaw(res); const generate = /json5?/.test(ext) ? generateJSON : generateYAML; const parseOptions = getOptions(res, isProduction, {}, false, { isGlobal, jit, onlyLocales, strictMessage, escapeHtml, forceStringify, transformI18nBlock, usedKeyFilter }); parseOptions.type = "bare"; const { code } = generate(source, parseOptions); debug$3("generated code", code); codes.push(`${JSON.stringify(name)}: ${code}`); } } return `const isObject = (item) => item && typeof item === 'object' && !Array.isArray(item); const mergeDeep = (target, ...sources) => { if (!sources.length) return target; const source = sources.shift(); if (isObject(target) && isObject(source)) { for (const key in source) { if (isObject(source[key])) { if (!target[key]) Object.assign(target, { [key]: {} }); mergeDeep(target[key], source[key]); } else { Object.assign(target, { [key]: source[key] }); } } } return mergeDeep(target, ...sources); } export default mergeDeep({}, ${codes.map((code) => `{${code}}`).join(",\n")} );`; } async function getCode(source, filename, sourceMap, query, parser, framework = "vite") { const { index, issuerPath } = query; if (!isNumber(index)) { raiseError(`unexpected index: ${index}`); } if (framework === "webpack") { if (issuerPath) { debug$3(`getCode (webpack) ${index} via issuerPath`, issuerPath); return await getRaw(filename); } else { const result = parser(await getRaw(filename), { sourceMap, filename }); const block = result.descriptor.customBlocks[index]; if (block) { const code = block.src ? await getRaw(block.src) : block.content; debug$3(`getCode (webpack) ${index} from SFC`, code); return code; } else { return source; } } } else { return source; } } function isCustomBlock(query) { return !isEmptyObject(query) && "vue" in query && (query["type"] === "custom" || // for webpack (vue-loader) query["type"] === "i18n" || // for vite (@vite/plugin-vue) query["blockType"] === "i18n"); } function getOptions(filename, isProduction, query, sourceMap, { inSourceMap = void 0, forceStringify = false, isGlobal = false, onlyLocales = [], allowDynamic = false, strictMessage = true, escapeHtml = false, jit = true, transformI18nBlock = null, usedKeyFilter = void 0 }) { const mode = isProduction ? "production" : "development"; const baseOptions = { filename, sourceMap, inSourceMap, forceStringify, allowDynamic, strictMessage, escapeHtml, jit, onlyLocales, usedKeyFilter, env: mode, transformI18nBlock, onWarn: (msg) => { warn(`${filename} ${msg}`); }, onError: (msg, extra) => { const codeFrame = generateCodeFrame( extra?.source || extra?.location?.source || "", extra?.location?.start.column, extra?.location?.end.column ); const errMssage = `${msg} (error code: ${extra?.code}) in ${filename} target message: ${extra?.source} target message path: ${extra?.path} ${codeFrame} `; error(errMssage); throw new Error(errMssage); } }; if (isCustomBlock(query)) { return assign(baseOptions, { type: "sfc", locale: isString(query.locale) ? query.locale : "", isGlobal: isGlobal || !!query.global }); } else { return assign(baseOptions, { type: "plain", isGlobal: false }); } } function getVirtualId(id, framework = "vite") { return framework === "vite" ? id.startsWith(VIRTUAL_PREFIX) ? id.slice(VIRTUAL_PREFIX.length) : "" : id; } function asVirtualId(id, framework = "vite") { return framework === "vite" ? VIRTUAL_PREFIX + id : id; } async function getRaw(path) { return promises.readFile(path, { encoding: "utf-8" }); } const debug$2 = createDebug(resolveNamespace("directive")); const tsEstree = { parse: void 0, simpleTraverse: void 0, AST_NODE_TYPES: void 0 }; function directivePlugin({ optimizeTranslationDirective, translationIdentifiers }) { let vuePlugin = null; let vuePluginOptions = null; const excludeLangs = ["pug", "jsx", "tsx"]; return { name: resolveNamespace("directive"), enforce: "pre", vite: { async config(config) { await import('@typescript-eslint/typescript-estree').then((r) => { tsEstree.parse = r.parse; tsEstree.simpleTraverse = r.simpleTraverse; tsEstree.AST_NODE_TYPES = r.AST_NODE_TYPES; return; }); vuePlugin = getVitePlugin(config, "vite:vue"); if (!checkVuePlugin(vuePlugin)) { return; } if (optimizeTranslationDirective) { vuePlugin.api.options = resolveVueOptions( vuePlugin, optimizeTranslationDirective, translationIdentifiers ); debug$2(`vite:vue options['template']:`, vuePlugin.api.options); } }, configResolved(config) { vuePlugin = getVitePlugin(config, "vite:vue"); if (!checkVuePlugin(vuePlugin)) { return; } } }, async transform(code, id) { if (id.endsWith(".vue")) { const { filename, query } = parseVueRequest(id); if (!excludeLangs.includes(query.lang ?? "")) { if (vuePluginOptions == null) { vuePluginOptions = getVuePluginOptions(vuePlugin); } if (vuePluginOptions?.compiler) { analyzeIdentifiers( getDescriptor(filename, code, vuePluginOptions), vuePluginOptions, translationIdentifiers ); return { code, map: { version: 3, mappings: "", sources: [] } }; } } } } }; } function resolveVueOptions(vuePlugin, optimizeTranslationDirective, translationIdentifiers) { const vueOptions = vuePlugin.api.options; vueOptions.template ||= {}; vueOptions.template.compilerOptions ||= {}; vueOptions.template.compilerOptions.directiveTransforms ||= {}; const translationSignatureResolver = (context, baseResolver) => { const { filename } = context; const vuePluginOptions = getVuePluginOptions(vuePlugin); const normalizedFilename = normalizePath(path.relative(vuePluginOptions.root, filename)); const resolveIdentifier2 = translationIdentifiers.get(normalizedFilename); debug$2("resolved vue-i18n Identifier", resolveIdentifier2); if (resolveIdentifier2 == null) { return void 0; } if (resolveIdentifier2.type === "identifier") { return baseResolver(context, resolveIdentifier2.key); } else { const resolvedSignature = baseResolver(context, resolveIdentifier2.key); return resolveIdentifier2?.style === "script-setup" ? `${resolvedSignature}.t` : resolvedSignature; } }; vueOptions.template.compilerOptions.directiveTransforms.t = transformVTDirective({ translationSignatures: isBoolean(optimizeTranslationDirective) ? translationSignatureResolver : optimizeTranslationDirective }); return vueOptions; } function analyzeIdentifiers(descriptor, { root }, translationIdentifiers) { const source = descriptor.scriptSetup?.content || descriptor.script?.content; debug$2("getDescriptor content", source); if (!source) { return; } const ast = tsEstree.parse(source, { range: true }); tsEstree.simpleTraverse(ast, { enter(node, parent) { if (parent) { node.parent = parent; } } }); const scopeManager = analyze(ast, { sourceType: "module" }); const scope = getScope(scopeManager, ast); const importLocalName = getImportLocalName(scope, "vue-i18n", "useI18n"); if (importLocalName == null) { return; } debug$2("importLocalName", importLocalName); const resolvedIdentifier = getVueI18nIdentifier(scope, importLocalName); if (resolvedIdentifier) { const normalizedFilename = normalizePath(path.relative(root, descriptor.filename)); debug$2("set vue-i18n resolved identifier: ", resolvedIdentifier); translationIdentifiers.set(normalizedFilename, resolvedIdentifier); } } function getScope(manager, node) { const scope = manager.acquire(node, true); if (scope) { if (scope.type === "function-expression-name") { return scope.childScopes[0]; } return scope; } return manager.scopes[0]; } function getImportLocalName(scope, source, imported) { const importDecl = getImportDeclaration(scope, source); if (importDecl) { const specifierNode = importDecl.specifiers.find( (specifierNode2) => isImportedIdentifierInImportClause(specifierNode2) && specifierNode2.imported.name === imported ); return specifierNode ? specifierNode.local.name : null; } return null; } function getImportDeclaration(scope, source) { const tracker = new eslintUitls.ReferenceTracker(scope); const traceMap = { [source]: { [eslintUitls.ReferenceTracker.ESM]: true, [eslintUitls.ReferenceTracker.READ]: true } }; const refs = Array.from(tracker.iterateEsmReferences(traceMap)); return refs.length ? refs[0].node : null; } function isImportedIdentifierInImportClause(node) { return "imported" in node; } function getVueI18nIdentifier(scope, local) { const { callExpression, returnStatement } = getCallExpressionAndReturnStatement(scope, local); if (callExpression == null) { return null; } const id = getVariableDeclarationIdFrom(callExpression); if (id == null) { return null; } const variableIdPairs = parseVariableId(id); debug$2("variableIdPairs:", variableIdPairs); const returnVariableIdPairs = parseReturnStatement(returnStatement); debug$2("returnVariableIdPairs:", returnVariableIdPairs); return resolveIdentifier(variableIdPairs, returnVariableIdPairs); } const EMPTY_NODE_RETURN = { callExpression: null, returnStatement: null }; function getCallExpressionAndReturnStatement(scope, local) { const variable = eslintUitls.findVariable(scope, local); if (variable == null) { return EMPTY_NODE_RETURN; } const callExpressionRef = variable.references.find((ref) => { return ref.identifier.parent?.type === tsEstree.AST_NODE_TYPES.CallExpression; }); if (callExpressionRef == null) { return EMPTY_NODE_RETURN; } let returnStatement = null; if (callExpressionRef.from.type === "function" && callExpressionRef.from.block.type === tsEstree.AST_NODE_TYPES.FunctionExpression && // @ts-expect-error -- FIXME: type error callExpressionRef.from.block.parent.type === tsEstree.AST_NODE_TYPES.Property && // @ts-expect-error -- FIXME: type error callExpressionRef.from.block.parent.key.type === tsEstree.AST_NODE_TYPES.Identifier && // @ts-expect-error -- FIXME: type error callExpressionRef.from.block.parent.key.name === "setup") { returnStatement = callExpressionRef.from.block.body.body.find((statement) => { return statement.type === tsEstree.AST_NODE_TYPES.ReturnStatement; }); } return { // @ts-expect-error -- FIXME: type error callExpression: callExpressionRef.identifier.parent, returnStatement }; } function getVariableDeclarationIdFrom(node) { if (node.parent?.type !== tsEstree.AST_NODE_TYPES.VariableDeclarator) { return null; } return node.parent.id; } function parseVariableId(node) { if (node.type === tsEstree.AST_NODE_TYPES.Identifier) { return [{ key: node.name, value: null }]; } else { const props = node.properties.filter( // ignore RestElement (prop) => prop.type === tsEstree.AST_NODE_TYPES.Property ); const pairs = []; for (const prop of props) { if (prop?.key.type === tsEstree.AST_NODE_TYPES.Identifier && prop?.value.type === tsEstree.AST_NODE_TYPES.Identifier) { pairs.push({ key: prop.key.name, value: prop.value.name }); } } return pairs; } } function parseReturnStatement(node) { const pairs = []; if (node == null || node.argument == null) { return pairs; } if (node.argument.type === tsEstree.AST_NODE_TYPES.ObjectExpression) { for (const prop of node.argument.properties) { if (prop.type === tsEstree.AST_NODE_TYPES.Property) { if (prop.key.type === tsEstree.AST_NODE_TYPES.Identifier && prop.value.type === tsEstree.AST_NODE_TYPES.Identifier) { pairs.push({ key: prop.key.name, value: prop.value.name }); } else if (prop.key.type === tsEstree.AST_NODE_TYPES.Identifier && prop.value.type === tsEstree.AST_NODE_TYPES.MemberExpression && prop.value.object.type === tsEstree.AST_NODE_TYPES.Identifier && prop.value.property.type === tsEstree.AST_NODE_TYPES.Identifier) { pairs.push({ key: prop.key.name, value: `${prop.value.object.name}.${prop.value.property.name}` }); } } } return pairs; } else if (node.argument.type === tsEstree.AST_NODE_TYPES.Identifier) { return pairs; } else { return pairs; } } function resolveIdentifier(localVariables, returnVariable) { if (returnVariable.length === 0) { const variable = localVariables.find((pair) => pair.key === "t"); if (variable && variable.value) { return { type: "identifier", key: variable.value }; } const identifierOnly = localVariables.find((pair) => pair.value === null); if (identifierOnly && identifierOnly.key) { return { type: "object", key: identifierOnly.key, style: "script-setup" }; } return null; } else { const variable = localVariables.find((pair) => pair.key === "t"); if (variable && variable.value) { const returnVar = returnVariable.find((pair) => pair.value === variable.value); if (returnVar && returnVar.key) { return { type: "identifier", key: returnVar.key }; } } const identifierOnly = localVariables.find((pair) => pair.value === null); if (identifierOnly && identifierOnly.key) { const targetKey = identifierOnly.key; const returnVar = returnVariable.find((pair) => pair.value?.startsWith(targetKey)); if (returnVar && returnVar.key) { return { type: "object", key: returnVar.key, style: "setup-hook" }; } } return null; } } function matchSafelistPattern(keyPath, pattern) { const regexStr = pattern.replace(/\./g, "\\.").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^.]*").replace(/\{\{GLOBSTAR\}\}/g, ".*"); return new RegExp(`^${regexStr}$`).test(keyPath); } function createUsedKeysCollector(options) { const usedKeys = /* @__PURE__ */ new Set(); const safelistPatterns = options.safelist || []; const dynamicKeyStrategy = options.dynamicKeyStrategy || "keep-all"; const removedKeys = /* @__PURE__ */ new Map(); let dynamicKeysDetected = false; return { get usedKeys() { return usedKeys; }, get dynamicKeysDetected() { return dynamicKeysDetected; }, set dynamicKeysDetected(val) { dynamicKeysDetected = val; }, safelistPatterns, removedKeys, addKey(key) { usedKeys.add(key); }, markDynamic() { dynamicKeysDetected = true; }, shouldKeepKey(keyPath) { if (dynamicKeysDetected && dynamicKeyStrategy === "keep-all") { return true; } if (safelistPatterns.some((pattern) => matchSafelistPattern(keyPath, pattern))) { return true; } if (usedKeys.has(keyPath)) { return true; } for (const key of usedKeys) { if (key.startsWith(keyPath + ".")) { return true; } } return false; }, reportRemoved(filename, keyPath) { const existing = removedKeys.get(filename); if (existing) { existing.push(keyPath); } else { removedKeys.set(filename, [keyPath]); } }, getDiagnostics() { let totalRemoved = 0; for (const keys of removedKeys.values()) { totalRemoved += keys.length; } return { totalRemoved, byFile: removedKeys }; } }; } const I18N_FUNCTIONS = /* @__PURE__ */ new Set(["t", "$t", "tc", "$tc", "te", "$te", "d", "$d", "n", "$n"]); const TEMPLATE_T_REGEX = /\$?t\s*\(\s*['"]([^'"]+)['"]/g; const VT_DIRECTIVE_STRING_REGEX = /v-t\s*=\s*"'([^']+)'"/g; const VT_DIRECTIVE_PATH_REGEX = /v-t\s*=\s*"\{[^}]*path\s*:\s*'([^']+)'/g; const TEMPLATE_DYNAMIC_T_REGEX = /\$?t\s*\(\s*[a-zA-Z_$]/g; async function extractKeysFromScript(source) { const keys = []; let hasDynamic = false; let tsEstree; try { tsEstree = await import('@typescript-eslint/typescript-estree'); } catch { return extractKeysFromScriptRegex(source); } try { const ast = tsEstree.parse(source, { range: true, jsx: true, allowInvalidAST: true, suppressDeprecatedPropertyWarnings: true }); tsEstree.simpleTraverse(ast, { enter(node) { if (node.type !== tsEstree.AST_NODE_TYPES.CallExpression) { return; } const calleeName = getCalleeName(node, tsEstree.AST_NODE_TYPES); if (!calleeName || !I18N_FUNCTIONS.has(calleeName)) { return; } const firstArg = node.arguments[0]; if (!firstArg) { return; } if (firstArg.type === tsEstree.AST_NODE_TYPES.Literal && typeof firstArg.value === "string") { keys.push(firstArg.value); } else if (firstArg.type === tsEstree.AST_NODE_TYPES.TemplateLiteral && firstArg.expressions.length === 0 && firstArg.quasis.length === 1) { const value = firstArg.quasis[0].value.cooked; if (value) { keys.push(value); } } else { hasDynamic = true; } } }); } catch { return extractKeysFromScriptRegex(source); } return { keys, hasDynamic }; } function getCalleeName(node, AST_NODE_TYPES) { const callee = node.callee; if (callee.type === AST_NODE_TYPES.Identifier) { return callee.name; } if (callee.type === AST_NODE_TYPES.MemberExpression && callee.property.type === AST_NODE_TYPES.Identifier) { return callee.property.name; } return null; } function extractKeysFromScriptRegex(source) { const keys = []; let match; const regex = /\b(?:\$?t|tc|\$tc|te|\$te|d|\$d|n|\$n)\s*\(\s*['"]([^'"]+)['"]/g; while ((match = regex.exec(source)) !== null) { keys.push(match[1]); } return { keys, hasDynamic: false }; } function extractKeysFromTemplate(templateContent) { const keys = []; let hasDynamic = false; let match; TEMPLATE_T_REGEX.lastIndex = 0; VT_DIRECTIVE_STRING_REGEX.lastIndex = 0; VT_DIRECTIVE_PATH_REGEX.lastIndex = 0; TEMPLATE_DYNAMIC_T_REGEX.lastIndex = 0; while ((match = TEMPLATE_T_REGEX.exec(templateContent)) !== null) { keys.push(match[1]); } while ((match = VT_DIRECTIVE_STRING_REGEX.exec(templateContent)) !== null) { keys.push(match[1]); } while ((match = VT_DIRECTIVE_PATH_REGEX.exec(templateContent)) !== null) { keys.push(match[1]); } if (TEMPLATE_DYNAMIC_T_REGEX.test(templateContent)) { hasDynamic = true; } return { keys, hasDynamic }; } async function analyzeVueSFC(content) { const keys = []; let hasDynamic = false; let parse; try { const compilerSfc = await import('vue/compiler-sfc'); parse = compilerSfc.parse; } catch { const templateResult = extractKeysFromTemplate(content); const scriptResult = await extractKeysFromScript(content); return { keys: [...templateResult.keys, ...scriptResult.keys], hasDynamic: templateResult.hasDynamic || scriptResult.hasDynamic }; } const { descriptor } = parse(content); if (descriptor.template?.content) { const templateResult = extractKeysFromTemplate(descriptor.template.content); keys.push(...templateResult.keys); if (templateResult.hasDynamic) { hasDynamic = true; } } const scriptContent = descriptor.scriptSetup?.content || descriptor.script?.content; if (scriptContent) { const scriptResult = await extractKeysFromScript(scriptContent); keys.push(...scriptResult.keys); if (scriptResult.hasDynamic) { hasDynamic = true; } } return { keys, hasDynamic }; } async function analyzeFile(content, filePath, collector) { let result; if (filePath.endsWith(".vue")) { result = await analyzeVueSFC(content); } else { result = await extractKeysFromScript(content); } for (const key of result.keys) { collector.addKey(key); } if (result.hasDynamic) { collector.markDynamic(); } } const debug$1 = createDebug(resolveNamespace("tree-shaking")); function treeShakingPlugin(resolvedOptions, collector) { let projectRoot = ""; return { name: resolveNamespace("tree-shaking"), enforce: "pre", vite: { configResolved(config) { projectRoot = config.root; } }, webpack(compiler) { projectRoot = compiler.options.context || process.cwd(); }, async buildStart() { const treeShaking = resolvedOptions.treeShaking; const patterns = treeShaking?.scanPatterns || [`${projectRoot}/src/**/*.{vue,ts,js,tsx,jsx}`]; debug$1("scanning patterns:", patterns); const files = await fg(patterns, { ignore: ["**/node_modules/**"], absolute: true }); debug$1(`found ${files.length} source files to scan`); for (const file of files) { try { const content = await promises.readFile(file, "utf-8"); await analyzeFile(content, file, collector); } catch (err) { debug$1(`failed to analyze file: ${file}`, err); } } debug$1( `scan complete: ${collector.usedKeys.size} used keys found, dynamic: ${collector.dynamicKeysDetected}` ); if (collector.dynamicKeysDetected) { const strategy = treeShaking?.dynamicKeyStrategy || "keep-all"; if (strategy === "keep-all") { warn( "Tree-shaking: dynamic key usage detected. All keys will be preserved (dynamicKeyStrategy: keep-all)." ); } else { warn( "Tree-shaking: dynamic key usage detected. Unused keys will still be removed (dynamicKeyStrategy: ignore)." ); } } }, buildEnd() { const diagnostics = collector.getDiagnostics(); if (diagnostics.totalRemoved > 0) { debug$1(`tree-shaking: removed ${diagnostics.totalRemoved} unused message keys`); for (const [file, keys] of diagnostics.byFile) { debug$1(` ${file}: removed ${keys.length} keys (${keys.join(", ")})`); } } else { debug$1("tree-shaking: no keys removed"); } } }; } const debug = createDebug(resolveNamespace("root")); const unpluginFactory = (options = {}, meta) => { debug("meta framework", meta.framework); if (!["vite", "webpack"].includes(meta.framework)) { raiseError(`This plugin is supported 'vite' and 'webpack' only`); } debug("plugin options (resolving):", options); const resolvedOptions = resolveOptions(options); debug("plugin options (resolved):", resolvedOptions); const collector = resolvedOptions.treeShaking ? createUsedKeysCollector(resolvedOptions.treeShaking) : null; const plugins = []; if (resolvedOptions.treeShaking && collector) { plugins.push(treeShakingPlugin(resolvedOptions, collector)); } plugins.push(resourcePlugin(resolvedOptions, meta, collector)); if (resolvedOptions.optimizeTranslationDirective) { if (meta.framework === "webpack") { raiseError( `The 'optimizeTranslationDirective' option still is not supported for webpack. We are waiting for your Pull Request \u{1F642}.` ); } plugins.push(directivePlugin(resolvedOptions)); } return plugins; }; const unplugin = /* @__PURE__ */ createUnplugin(unpluginFactory); export { unplugin as default, unplugin, unpluginFactory };