@intlify/unplugin-vue-i18n 11.0.7 → 11.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.mjs CHANGED
@@ -37,6 +37,11 @@ function resolveOptions(options) {
37
37
  const optimizeTranslationDirective = isString(options.optimizeTranslationDirective) || isArray(options.optimizeTranslationDirective) ? options.optimizeTranslationDirective : !!options.optimizeTranslationDirective;
38
38
  const translationIdentifiers = /* @__PURE__ */ new Map();
39
39
  const transformI18nBlock = typeof options.transformI18nBlock === "function" ? options.transformI18nBlock : null;
40
+ const treeShaking = options.treeShaking ? typeof options.treeShaking === "object" ? {
41
+ safelist: options.treeShaking.safelist || [],
42
+ dynamicKeyStrategy: options.treeShaking.dynamicKeyStrategy || "keep-all",
43
+ scanPatterns: options.treeShaking.scanPatterns
44
+ } : { safelist: [], dynamicKeyStrategy: "keep-all" } : null;
40
45
  return {
41
46
  include: normalizeGlobOption(options.include || []),
42
47
  exclude: normalizeGlobOption(options.exclude),
@@ -55,7 +60,8 @@ function resolveOptions(options) {
55
60
  escapeHtml,
56
61
  optimizeTranslationDirective,
57
62
  translationIdentifiers,
58
- transformI18nBlock
63
+ transformI18nBlock,
64
+ treeShaking
59
65
  };
60
66
  }
61
67
 
@@ -167,7 +173,7 @@ const RE_VIRTUAL_PREFIXED_INTLIFY_BUNDLE_IMPORT_ID = new RegExp(
167
173
  const RE_RESOURCE_FORMAT = /\.(json5?|ya?ml|[c|m]?[j|t]s)$/;
168
174
  const RE_SFC_I18N_CUSTOM_BLOCK = /\?vue&type=i18n/;
169
175
  const RE_SFC_I18N_WEBPACK_CUSTOM_BLOCK = /blockType=i18n/;
170
- const debug$2 = createDebug(resolveNamespace("resource"));
176
+ const debug$3 = createDebug(resolveNamespace("resource"));
171
177
  function resourcePlugin({
172
178
  onlyLocales,
173
179
  include,
@@ -185,7 +191,7 @@ function resourcePlugin({
185
191
  allowDynamic,
186
192
  escapeHtml,
187
193
  transformI18nBlock
188
- }, meta) {
194
+ }, meta, collector) {
189
195
  let viteModule = null;
190
196
  async function getViteModule() {
191
197
  if (viteModule != null) {
@@ -213,15 +219,15 @@ function resourcePlugin({
213
219
  return _filter;
214
220
  }
215
221
  if (meta.framework == "webpack") {
216
- debug$2("Using filter for webpack");
222
+ debug$3("Using filter for webpack");
217
223
  _filter = createFilter(...resolveIncludeExclude());
218
224
  } else {
219
225
  const viteModule2 = await getViteModule();
220
226
  if (viteModule2.rolldownVersion) {
221
- debug$2("Using filter for rolldown-vite");
227
+ debug$3("Using filter for rolldown-vite");
222
228
  _filter = createFilter(...resolveIncludeExclude());
223
229
  } else {
224
- debug$2("Using filter for rollup-vite");
230
+ debug$3("Using filter for rollup-vite");
225
231
  _filter = createFilter(...resolveIncludeExcludeForLegacy());
226
232
  }
227
233
  }
@@ -233,7 +239,7 @@ function resourcePlugin({
233
239
  let isProduction = false;
234
240
  let sourceMap = false;
235
241
  const vueI18nAliasName = module;
236
- debug$2(`vue-i18n alias name: ${vueI18nAliasName}`);
242
+ debug$3(`vue-i18n alias name: ${vueI18nAliasName}`);
237
243
  let vuePlugin = null;
238
244
  const getSfcParser = () => {
239
245
  return vuePlugin ? getVueCompiler(vuePlugin).parse : parse$1;
@@ -259,7 +265,7 @@ function resourcePlugin({
259
265
  __VUE_I18N_PROD_DEVTOOLS__: false
260
266
  }
261
267
  };
262
- debug$2("define Config:", defineConfig);
268
+ debug$3("define Config:", defineConfig);
263
269
  const aliasConfig = {
264
270
  resolve: {
265
271
  alias: {
@@ -270,7 +276,7 @@ function resourcePlugin({
270
276
  }
271
277
  }
272
278
  };
273
- debug$2("alias Config:", aliasConfig);
279
+ debug$3("alias Config:", aliasConfig);
274
280
  return assign(defineConfig, aliasConfig);
275
281
  },
276
282
  async configResolved(config) {
@@ -280,9 +286,9 @@ function resourcePlugin({
280
286
  }
281
287
  isProduction = config.isProduction;
282
288
  sourceMap = config.command === "build" ? !!config.build.sourcemap : false;
283
- debug$2(`configResolved: isProduction = ${isProduction}, sourceMap = ${sourceMap}`);
289
+ debug$3(`configResolved: isProduction = ${isProduction}, sourceMap = ${sourceMap}`);
284
290
  const isRolldownVite = !!(await getViteModule()).rolldownVersion;
285
- debug$2(`Using ${isRolldownVite ? "rolldown-vite" : "vite"} for the build`);
291
+ debug$3(`Using ${isRolldownVite ? "rolldown-vite" : "vite"} for the build`);
286
292
  if (!isRolldownVite) {
287
293
  const jsonPlugin = getVitePlugin(config, "vite:json");
288
294
  if (jsonPlugin) {
@@ -296,7 +302,7 @@ function resourcePlugin({
296
302
  if (query.vue) {
297
303
  return;
298
304
  }
299
- debug$2("org json plugin");
305
+ debug$3("org json plugin");
300
306
  return orgTransform.apply(this, [code, id]);
301
307
  }
302
308
  jsonPlugin.transform = overrideJson;
@@ -321,7 +327,7 @@ function resourcePlugin({
321
327
  webpack(compiler) {
322
328
  isProduction = compiler.options.mode !== "development";
323
329
  sourceMap = !!compiler.options.devtool;
324
- debug$2(`webpack: isProduction = ${isProduction}, sourceMap = ${sourceMap}`);
330
+ debug$3(`webpack: isProduction = ${isProduction}, sourceMap = ${sourceMap}`);
325
331
  compiler.options.resolve = normalizeConfigResolveAlias(
326
332
  compiler.options.resolve,
327
333
  meta.framework
@@ -330,7 +336,7 @@ function resourcePlugin({
330
336
  ssr: ssrBuild,
331
337
  runtimeOnly
332
338
  });
333
- debug$2(
339
+ debug$3(
334
340
  `set ${vueI18nAliasName}: ${getVueI18nAliasPath({
335
341
  ssr: ssrBuild,
336
342
  runtimeOnly
@@ -343,15 +349,15 @@ function resourcePlugin({
343
349
  __INTLIFY_PROD_DEVTOOLS__: "false"
344
350
  })
345
351
  );
346
- debug$2(`set __VUE_I18N_LEGACY_API__ is '${compositionOnly}'`);
347
- debug$2(`set __VUE_I18N_FULL_INSTALL__ is '${fullInstall}'`);
352
+ debug$3(`set __VUE_I18N_LEGACY_API__ is '${compositionOnly}'`);
353
+ debug$3(`set __VUE_I18N_FULL_INSTALL__ is '${fullInstall}'`);
348
354
  const filter = createFilter(...resolveIncludeExclude());
349
355
  if (compiler.options.module) {
350
356
  compiler.options.module.rules.push({
351
357
  test: RE_RESOURCE_FORMAT,
352
358
  type: "javascript/auto",
353
359
  include(resource) {
354
- debug$2("webpack resource include", resource);
360
+ debug$3("webpack resource include", resource);
355
361
  return filter(resource);
356
362
  }
357
363
  });
@@ -370,7 +376,7 @@ function resourcePlugin({
370
376
  id: RE_VIRTUAL_PREFIXED_INTLIFY_BUNDLE_IMPORT_ID
371
377
  },
372
378
  async handler(id) {
373
- debug$2("load", id);
379
+ debug$3("load", id);
374
380
  if (INTLIFY_BUNDLE_IMPORT_ID === getVirtualId(id, meta.framework) && include) {
375
381
  let resourcePaths = [];
376
382
  for (const inc of include) {
@@ -380,7 +386,8 @@ function resourcePlugin({
380
386
  const code = await generateBundleResources(resourcePaths, isProduction, {
381
387
  forceStringify,
382
388
  strictMessage,
383
- escapeHtml
389
+ escapeHtml,
390
+ usedKeyFilter: collector ? (keyPath) => collector.shouldKeepKey(keyPath) : void 0
384
391
  });
385
392
  return {
386
393
  moduleType: "js",
@@ -397,7 +404,7 @@ function resourcePlugin({
397
404
  return;
398
405
  }
399
406
  const { filename, query } = parseVueRequest(id);
400
- debug$2("transform", id, JSON.stringify(query), filename);
407
+ debug$3("transform", id, JSON.stringify(query), filename);
401
408
  let langInfo = defaultSFCLang;
402
409
  let inSourceMap;
403
410
  if (!query.vue) {
@@ -416,13 +423,14 @@ function resourcePlugin({
416
423
  escapeHtml,
417
424
  jit: true,
418
425
  onlyLocales,
419
- forceStringify
426
+ forceStringify,
427
+ usedKeyFilter: collector ? (keyPath) => collector.shouldKeepKey(keyPath) : void 0
420
428
  }
421
429
  );
422
- debug$2("parseOptions", parseOptions);
430
+ debug$3("parseOptions", parseOptions);
423
431
  const { code: generatedCode, map } = await generate(code, parseOptions);
424
- debug$2("generated code", generatedCode);
425
- debug$2("sourcemap", map, sourceMap);
432
+ debug$3("generated code", generatedCode);
433
+ debug$3("sourcemap", map, sourceMap);
426
434
  if (code === generatedCode) return;
427
435
  return {
428
436
  moduleType: "js",
@@ -437,8 +445,9 @@ function resourcePlugin({
437
445
  } else if (defaultSFCLang) {
438
446
  langInfo = defaultSFCLang;
439
447
  }
440
- debug$2("langInfo", langInfo);
441
- const generate = /\.?json5?/.test(langInfo) ? generateJSON : generateYAML;
448
+ debug$3("langInfo", langInfo);
449
+ const generate = getGenerator(langInfo, generateYAML);
450
+ const isGlobalBlock = globalSFCScope || !!query.global;
442
451
  const parseOptions = getOptions(
443
452
  filename,
444
453
  isProduction,
@@ -447,14 +456,16 @@ function resourcePlugin({
447
456
  {
448
457
  inSourceMap,
449
458
  isGlobal: globalSFCScope,
459
+ allowDynamic,
450
460
  jit: true,
451
461
  strictMessage,
452
462
  escapeHtml,
453
463
  onlyLocales,
454
- forceStringify
464
+ forceStringify,
465
+ usedKeyFilter: collector && isGlobalBlock ? (keyPath) => collector.shouldKeepKey(keyPath) : void 0
455
466
  }
456
467
  );
457
- debug$2("parseOptions", parseOptions);
468
+ debug$3("parseOptions", parseOptions);
458
469
  let source = await getCode(
459
470
  code,
460
471
  filename,
@@ -471,9 +482,12 @@ function resourcePlugin({
471
482
  warn("transformI18nBlock should return a string");
472
483
  }
473
484
  }
474
- const { code: generatedCode, map } = generate(source, parseOptions);
475
- debug$2("generated code", generatedCode);
476
- debug$2("sourcemap", map, sourceMap);
485
+ if (/\.?[cm]?[jt]s$/.test(langInfo)) {
486
+ source = `export default ${source.replace(/^[\s;]+/, "")}`;
487
+ }
488
+ const { code: generatedCode, map } = await generate(source, parseOptions);
489
+ debug$3("generated code", generatedCode);
490
+ debug$3("sourcemap", map, sourceMap);
477
491
  if (code === generatedCode) return;
478
492
  return {
479
493
  moduleType: "js",
@@ -491,13 +505,13 @@ function getGenerator(ext, fallback = generateJSON) {
491
505
  if (/\.?json5?$/.test(ext)) {
492
506
  return generateJSON;
493
507
  }
494
- if (/\.ya?ml$/.test(ext)) {
508
+ if (/\.?ya?ml$/.test(ext)) {
495
509
  return generateYAML;
496
510
  }
497
- if (/\.[c|m]?js$/.test(ext)) {
511
+ if (/\.?[cm]?js$/.test(ext)) {
498
512
  return generateJavaScript;
499
513
  }
500
- if (/\.[c|m]?ts$/.test(ext)) {
514
+ if (/\.?[cm]?ts$/.test(ext)) {
501
515
  return generateTypescript;
502
516
  }
503
517
  return fallback;
@@ -529,11 +543,12 @@ async function generateBundleResources(resources, isProduction, {
529
543
  strictMessage = true,
530
544
  escapeHtml = false,
531
545
  jit = true,
532
- transformI18nBlock = void 0
546
+ transformI18nBlock = void 0,
547
+ usedKeyFilter = void 0
533
548
  }) {
534
549
  const codes = [];
535
550
  for (const res of resources) {
536
- debug$2(`${res} bundle loading ...`);
551
+ debug$3(`${res} bundle loading ...`);
537
552
  if (/\.(json5?|ya?ml)$/.test(res)) {
538
553
  const { ext, name } = parse(res);
539
554
  const source = await getRaw(res);
@@ -545,11 +560,12 @@ async function generateBundleResources(resources, isProduction, {
545
560
  strictMessage,
546
561
  escapeHtml,
547
562
  forceStringify,
548
- transformI18nBlock
563
+ transformI18nBlock,
564
+ usedKeyFilter
549
565
  });
550
566
  parseOptions.type = "bare";
551
567
  const { code } = generate(source, parseOptions);
552
- debug$2("generated code", code);
568
+ debug$3("generated code", code);
553
569
  codes.push(`${JSON.stringify(name)}: ${code}`);
554
570
  }
555
571
  }
@@ -584,7 +600,7 @@ async function getCode(source, filename, sourceMap, query, parser, framework = "
584
600
  }
585
601
  if (framework === "webpack") {
586
602
  if (issuerPath) {
587
- debug$2(`getCode (webpack) ${index} via issuerPath`, issuerPath);
603
+ debug$3(`getCode (webpack) ${index} via issuerPath`, issuerPath);
588
604
  return await getRaw(filename);
589
605
  } else {
590
606
  const result = parser(await getRaw(filename), {
@@ -594,7 +610,7 @@ async function getCode(source, filename, sourceMap, query, parser, framework = "
594
610
  const block = result.descriptor.customBlocks[index];
595
611
  if (block) {
596
612
  const code = block.src ? await getRaw(block.src) : block.content;
597
- debug$2(`getCode (webpack) ${index} from SFC`, code);
613
+ debug$3(`getCode (webpack) ${index} from SFC`, code);
598
614
  return code;
599
615
  } else {
600
616
  return source;
@@ -618,7 +634,8 @@ function getOptions(filename, isProduction, query, sourceMap, {
618
634
  strictMessage = true,
619
635
  escapeHtml = false,
620
636
  jit = true,
621
- transformI18nBlock = null
637
+ transformI18nBlock = null,
638
+ usedKeyFilter = void 0
622
639
  }) {
623
640
  const mode = isProduction ? "production" : "development";
624
641
  const baseOptions = {
@@ -631,6 +648,7 @@ function getOptions(filename, isProduction, query, sourceMap, {
631
648
  escapeHtml,
632
649
  jit,
633
650
  onlyLocales,
651
+ usedKeyFilter,
634
652
  env: mode,
635
653
  transformI18nBlock,
636
654
  onWarn: (msg) => {
@@ -675,7 +693,7 @@ async function getRaw(path) {
675
693
  return promises.readFile(path, { encoding: "utf-8" });
676
694
  }
677
695
 
678
- const debug$1 = createDebug(resolveNamespace("directive"));
696
+ const debug$2 = createDebug(resolveNamespace("directive"));
679
697
  const tsEstree = {
680
698
  parse: void 0,
681
699
  simpleTraverse: void 0,
@@ -709,7 +727,7 @@ function directivePlugin({
709
727
  optimizeTranslationDirective,
710
728
  translationIdentifiers
711
729
  );
712
- debug$1(`vite:vue options['template']:`, vuePlugin.api.options);
730
+ debug$2(`vite:vue options['template']:`, vuePlugin.api.options);
713
731
  }
714
732
  },
715
733
  configResolved(config) {
@@ -752,7 +770,7 @@ function resolveVueOptions(vuePlugin, optimizeTranslationDirective, translationI
752
770
  const vuePluginOptions = getVuePluginOptions(vuePlugin);
753
771
  const normalizedFilename = normalizePath(path.relative(vuePluginOptions.root, filename));
754
772
  const resolveIdentifier2 = translationIdentifiers.get(normalizedFilename);
755
- debug$1("resolved vue-i18n Identifier", resolveIdentifier2);
773
+ debug$2("resolved vue-i18n Identifier", resolveIdentifier2);
756
774
  if (resolveIdentifier2 == null) {
757
775
  return void 0;
758
776
  }
@@ -770,7 +788,7 @@ function resolveVueOptions(vuePlugin, optimizeTranslationDirective, translationI
770
788
  }
771
789
  function analyzeIdentifiers(descriptor, { root }, translationIdentifiers) {
772
790
  const source = descriptor.scriptSetup?.content || descriptor.script?.content;
773
- debug$1("getDescriptor content", source);
791
+ debug$2("getDescriptor content", source);
774
792
  if (!source) {
775
793
  return;
776
794
  }
@@ -788,11 +806,11 @@ function analyzeIdentifiers(descriptor, { root }, translationIdentifiers) {
788
806
  if (importLocalName == null) {
789
807
  return;
790
808
  }
791
- debug$1("importLocalName", importLocalName);
809
+ debug$2("importLocalName", importLocalName);
792
810
  const resolvedIdentifier = getVueI18nIdentifier(scope, importLocalName);
793
811
  if (resolvedIdentifier) {
794
812
  const normalizedFilename = normalizePath(path.relative(root, descriptor.filename));
795
- debug$1("set vue-i18n resolved identifier: ", resolvedIdentifier);
813
+ debug$2("set vue-i18n resolved identifier: ", resolvedIdentifier);
796
814
  translationIdentifiers.set(normalizedFilename, resolvedIdentifier);
797
815
  }
798
816
  }
@@ -840,9 +858,9 @@ function getVueI18nIdentifier(scope, local) {
840
858
  return null;
841
859
  }
842
860
  const variableIdPairs = parseVariableId(id);
843
- debug$1("variableIdPairs:", variableIdPairs);
861
+ debug$2("variableIdPairs:", variableIdPairs);
844
862
  const returnVariableIdPairs = parseReturnStatement(returnStatement);
845
- debug$1("returnVariableIdPairs:", returnVariableIdPairs);
863
+ debug$2("returnVariableIdPairs:", returnVariableIdPairs);
846
864
  return resolveIdentifier(variableIdPairs, returnVariableIdPairs);
847
865
  }
848
866
  const EMPTY_NODE_RETURN = {
@@ -954,6 +972,270 @@ function resolveIdentifier(localVariables, returnVariable) {
954
972
  }
955
973
  }
956
974
 
975
+ function matchSafelistPattern(keyPath, pattern) {
976
+ const regexStr = pattern.replace(/\./g, "\\.").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^.]*").replace(/\{\{GLOBSTAR\}\}/g, ".*");
977
+ return new RegExp(`^${regexStr}$`).test(keyPath);
978
+ }
979
+ function createUsedKeysCollector(options) {
980
+ const usedKeys = /* @__PURE__ */ new Set();
981
+ const safelistPatterns = options.safelist || [];
982
+ const dynamicKeyStrategy = options.dynamicKeyStrategy || "keep-all";
983
+ const removedKeys = /* @__PURE__ */ new Map();
984
+ let dynamicKeysDetected = false;
985
+ return {
986
+ get usedKeys() {
987
+ return usedKeys;
988
+ },
989
+ get dynamicKeysDetected() {
990
+ return dynamicKeysDetected;
991
+ },
992
+ set dynamicKeysDetected(val) {
993
+ dynamicKeysDetected = val;
994
+ },
995
+ safelistPatterns,
996
+ removedKeys,
997
+ addKey(key) {
998
+ usedKeys.add(key);
999
+ },
1000
+ markDynamic() {
1001
+ dynamicKeysDetected = true;
1002
+ },
1003
+ shouldKeepKey(keyPath) {
1004
+ if (dynamicKeysDetected && dynamicKeyStrategy === "keep-all") {
1005
+ return true;
1006
+ }
1007
+ if (safelistPatterns.some((pattern) => matchSafelistPattern(keyPath, pattern))) {
1008
+ return true;
1009
+ }
1010
+ if (usedKeys.has(keyPath)) {
1011
+ return true;
1012
+ }
1013
+ for (const key of usedKeys) {
1014
+ if (key.startsWith(keyPath + ".")) {
1015
+ return true;
1016
+ }
1017
+ }
1018
+ return false;
1019
+ },
1020
+ reportRemoved(filename, keyPath) {
1021
+ const existing = removedKeys.get(filename);
1022
+ if (existing) {
1023
+ existing.push(keyPath);
1024
+ } else {
1025
+ removedKeys.set(filename, [keyPath]);
1026
+ }
1027
+ },
1028
+ getDiagnostics() {
1029
+ let totalRemoved = 0;
1030
+ for (const keys of removedKeys.values()) {
1031
+ totalRemoved += keys.length;
1032
+ }
1033
+ return { totalRemoved, byFile: removedKeys };
1034
+ }
1035
+ };
1036
+ }
1037
+
1038
+ const I18N_FUNCTIONS = /* @__PURE__ */ new Set(["t", "$t", "tc", "$tc", "te", "$te", "d", "$d", "n", "$n"]);
1039
+ const TEMPLATE_T_REGEX = /\$?t\s*\(\s*['"]([^'"]+)['"]/g;
1040
+ const VT_DIRECTIVE_STRING_REGEX = /v-t\s*=\s*"'([^']+)'"/g;
1041
+ const VT_DIRECTIVE_PATH_REGEX = /v-t\s*=\s*"\{[^}]*path\s*:\s*'([^']+)'/g;
1042
+ const TEMPLATE_DYNAMIC_T_REGEX = /\$?t\s*\(\s*[a-zA-Z_$]/g;
1043
+ async function extractKeysFromScript(source) {
1044
+ const keys = [];
1045
+ let hasDynamic = false;
1046
+ let tsEstree;
1047
+ try {
1048
+ tsEstree = await import('@typescript-eslint/typescript-estree');
1049
+ } catch {
1050
+ return extractKeysFromScriptRegex(source);
1051
+ }
1052
+ try {
1053
+ const ast = tsEstree.parse(source, {
1054
+ range: true,
1055
+ jsx: true,
1056
+ allowInvalidAST: true,
1057
+ suppressDeprecatedPropertyWarnings: true
1058
+ });
1059
+ tsEstree.simpleTraverse(ast, {
1060
+ enter(node) {
1061
+ if (node.type !== tsEstree.AST_NODE_TYPES.CallExpression) {
1062
+ return;
1063
+ }
1064
+ const calleeName = getCalleeName(node, tsEstree.AST_NODE_TYPES);
1065
+ if (!calleeName || !I18N_FUNCTIONS.has(calleeName)) {
1066
+ return;
1067
+ }
1068
+ const firstArg = node.arguments[0];
1069
+ if (!firstArg) {
1070
+ return;
1071
+ }
1072
+ if (firstArg.type === tsEstree.AST_NODE_TYPES.Literal && typeof firstArg.value === "string") {
1073
+ keys.push(firstArg.value);
1074
+ } else if (firstArg.type === tsEstree.AST_NODE_TYPES.TemplateLiteral && firstArg.expressions.length === 0 && firstArg.quasis.length === 1) {
1075
+ const value = firstArg.quasis[0].value.cooked;
1076
+ if (value) {
1077
+ keys.push(value);
1078
+ }
1079
+ } else {
1080
+ hasDynamic = true;
1081
+ }
1082
+ }
1083
+ });
1084
+ } catch {
1085
+ return extractKeysFromScriptRegex(source);
1086
+ }
1087
+ return { keys, hasDynamic };
1088
+ }
1089
+ function getCalleeName(node, AST_NODE_TYPES) {
1090
+ const callee = node.callee;
1091
+ if (callee.type === AST_NODE_TYPES.Identifier) {
1092
+ return callee.name;
1093
+ }
1094
+ if (callee.type === AST_NODE_TYPES.MemberExpression && callee.property.type === AST_NODE_TYPES.Identifier) {
1095
+ return callee.property.name;
1096
+ }
1097
+ return null;
1098
+ }
1099
+ function extractKeysFromScriptRegex(source) {
1100
+ const keys = [];
1101
+ let match;
1102
+ const regex = /\b(?:\$?t|tc|\$tc|te|\$te|d|\$d|n|\$n)\s*\(\s*['"]([^'"]+)['"]/g;
1103
+ while ((match = regex.exec(source)) !== null) {
1104
+ keys.push(match[1]);
1105
+ }
1106
+ return { keys, hasDynamic: false };
1107
+ }
1108
+ function extractKeysFromTemplate(templateContent) {
1109
+ const keys = [];
1110
+ let hasDynamic = false;
1111
+ let match;
1112
+ TEMPLATE_T_REGEX.lastIndex = 0;
1113
+ VT_DIRECTIVE_STRING_REGEX.lastIndex = 0;
1114
+ VT_DIRECTIVE_PATH_REGEX.lastIndex = 0;
1115
+ TEMPLATE_DYNAMIC_T_REGEX.lastIndex = 0;
1116
+ while ((match = TEMPLATE_T_REGEX.exec(templateContent)) !== null) {
1117
+ keys.push(match[1]);
1118
+ }
1119
+ while ((match = VT_DIRECTIVE_STRING_REGEX.exec(templateContent)) !== null) {
1120
+ keys.push(match[1]);
1121
+ }
1122
+ while ((match = VT_DIRECTIVE_PATH_REGEX.exec(templateContent)) !== null) {
1123
+ keys.push(match[1]);
1124
+ }
1125
+ if (TEMPLATE_DYNAMIC_T_REGEX.test(templateContent)) {
1126
+ hasDynamic = true;
1127
+ }
1128
+ return { keys, hasDynamic };
1129
+ }
1130
+ async function analyzeVueSFC(content) {
1131
+ const keys = [];
1132
+ let hasDynamic = false;
1133
+ let parse;
1134
+ try {
1135
+ const compilerSfc = await import('vue/compiler-sfc');
1136
+ parse = compilerSfc.parse;
1137
+ } catch {
1138
+ const templateResult = extractKeysFromTemplate(content);
1139
+ const scriptResult = await extractKeysFromScript(content);
1140
+ return {
1141
+ keys: [...templateResult.keys, ...scriptResult.keys],
1142
+ hasDynamic: templateResult.hasDynamic || scriptResult.hasDynamic
1143
+ };
1144
+ }
1145
+ const { descriptor } = parse(content);
1146
+ if (descriptor.template?.content) {
1147
+ const templateResult = extractKeysFromTemplate(descriptor.template.content);
1148
+ keys.push(...templateResult.keys);
1149
+ if (templateResult.hasDynamic) {
1150
+ hasDynamic = true;
1151
+ }
1152
+ }
1153
+ const scriptContent = descriptor.scriptSetup?.content || descriptor.script?.content;
1154
+ if (scriptContent) {
1155
+ const scriptResult = await extractKeysFromScript(scriptContent);
1156
+ keys.push(...scriptResult.keys);
1157
+ if (scriptResult.hasDynamic) {
1158
+ hasDynamic = true;
1159
+ }
1160
+ }
1161
+ return { keys, hasDynamic };
1162
+ }
1163
+ async function analyzeFile(content, filePath, collector) {
1164
+ let result;
1165
+ if (filePath.endsWith(".vue")) {
1166
+ result = await analyzeVueSFC(content);
1167
+ } else {
1168
+ result = await extractKeysFromScript(content);
1169
+ }
1170
+ for (const key of result.keys) {
1171
+ collector.addKey(key);
1172
+ }
1173
+ if (result.hasDynamic) {
1174
+ collector.markDynamic();
1175
+ }
1176
+ }
1177
+
1178
+ const debug$1 = createDebug(resolveNamespace("tree-shaking"));
1179
+ function treeShakingPlugin(resolvedOptions, collector) {
1180
+ let projectRoot = "";
1181
+ return {
1182
+ name: resolveNamespace("tree-shaking"),
1183
+ enforce: "pre",
1184
+ vite: {
1185
+ configResolved(config) {
1186
+ projectRoot = config.root;
1187
+ }
1188
+ },
1189
+ webpack(compiler) {
1190
+ projectRoot = compiler.options.context || process.cwd();
1191
+ },
1192
+ async buildStart() {
1193
+ const treeShaking = resolvedOptions.treeShaking;
1194
+ const patterns = treeShaking?.scanPatterns || [`${projectRoot}/src/**/*.{vue,ts,js,tsx,jsx}`];
1195
+ debug$1("scanning patterns:", patterns);
1196
+ const files = await fg(patterns, {
1197
+ ignore: ["**/node_modules/**"],
1198
+ absolute: true
1199
+ });
1200
+ debug$1(`found ${files.length} source files to scan`);
1201
+ for (const file of files) {
1202
+ try {
1203
+ const content = await promises.readFile(file, "utf-8");
1204
+ await analyzeFile(content, file, collector);
1205
+ } catch (err) {
1206
+ debug$1(`failed to analyze file: ${file}`, err);
1207
+ }
1208
+ }
1209
+ debug$1(
1210
+ `scan complete: ${collector.usedKeys.size} used keys found, dynamic: ${collector.dynamicKeysDetected}`
1211
+ );
1212
+ if (collector.dynamicKeysDetected) {
1213
+ const strategy = treeShaking?.dynamicKeyStrategy || "keep-all";
1214
+ if (strategy === "keep-all") {
1215
+ warn(
1216
+ "Tree-shaking: dynamic key usage detected. All keys will be preserved (dynamicKeyStrategy: keep-all)."
1217
+ );
1218
+ } else {
1219
+ warn(
1220
+ "Tree-shaking: dynamic key usage detected. Unused keys will still be removed (dynamicKeyStrategy: ignore)."
1221
+ );
1222
+ }
1223
+ }
1224
+ },
1225
+ buildEnd() {
1226
+ const diagnostics = collector.getDiagnostics();
1227
+ if (diagnostics.totalRemoved > 0) {
1228
+ debug$1(`tree-shaking: removed ${diagnostics.totalRemoved} unused message keys`);
1229
+ for (const [file, keys] of diagnostics.byFile) {
1230
+ debug$1(` ${file}: removed ${keys.length} keys (${keys.join(", ")})`);
1231
+ }
1232
+ } else {
1233
+ debug$1("tree-shaking: no keys removed");
1234
+ }
1235
+ }
1236
+ };
1237
+ }
1238
+
957
1239
  const debug = createDebug(resolveNamespace("root"));
958
1240
  const unpluginFactory = (options = {}, meta) => {
959
1241
  debug("meta framework", meta.framework);
@@ -963,7 +1245,12 @@ const unpluginFactory = (options = {}, meta) => {
963
1245
  debug("plugin options (resolving):", options);
964
1246
  const resolvedOptions = resolveOptions(options);
965
1247
  debug("plugin options (resolved):", resolvedOptions);
966
- const plugins = [resourcePlugin(resolvedOptions, meta)];
1248
+ const collector = resolvedOptions.treeShaking ? createUsedKeysCollector(resolvedOptions.treeShaking) : null;
1249
+ const plugins = [];
1250
+ if (resolvedOptions.treeShaking && collector) {
1251
+ plugins.push(treeShakingPlugin(resolvedOptions, collector));
1252
+ }
1253
+ plugins.push(resourcePlugin(resolvedOptions, meta, collector));
967
1254
  if (resolvedOptions.optimizeTranslationDirective) {
968
1255
  if (meta.framework === "webpack") {
969
1256
  raiseError(
package/lib/types.d.cts CHANGED
@@ -1,5 +1,24 @@
1
1
  type SFCLangFormat = 'json' | 'json5' | 'yml' | 'yaml';
2
2
  type VueI18nModule = 'vue-i18n' | 'petite-vue-i18n';
3
+ interface TreeShakingOptions {
4
+ /**
5
+ * Message key patterns to always keep (never tree-shake).
6
+ * Supports glob-like patterns (e.g., `'errors.*'`, `'validation.**'`).
7
+ */
8
+ safelist?: string[];
9
+ /**
10
+ * Strategy when dynamic key usage (e.g., `t(variable)`) is detected.
11
+ * - `'keep-all'`: Disable tree-shaking and keep all keys (safe default).
12
+ * - `'ignore'`: Continue tree-shaking despite dynamic keys (risky, use with `safelist`).
13
+ * @default 'keep-all'
14
+ */
15
+ dynamicKeyStrategy?: 'keep-all' | 'ignore';
16
+ /**
17
+ * Glob patterns for source files to scan for used message keys.
18
+ * Defaults to `${projectRoot}/src/**\/*.{vue,ts,js,tsx,jsx}` when not specified.
19
+ */
20
+ scanPatterns?: string[];
21
+ }
3
22
  interface PluginOptions {
4
23
  include?: string | string[];
5
24
  exclude?: string | string[];
@@ -18,6 +37,12 @@ interface PluginOptions {
18
37
  escapeHtml?: boolean;
19
38
  optimizeTranslationDirective?: boolean | string | string[];
20
39
  transformI18nBlock?: (src: string | Buffer) => string;
40
+ /**
41
+ * Enable message key tree-shaking to remove unused locale message keys.
42
+ * Only effective in production builds.
43
+ * @default false
44
+ */
45
+ treeShaking?: boolean | TreeShakingOptions;
21
46
  }
22
47
 
23
- export type { PluginOptions, SFCLangFormat, VueI18nModule };
48
+ export type { PluginOptions, SFCLangFormat, TreeShakingOptions, VueI18nModule };