@intlify/unplugin-vue-i18n 11.0.7 → 11.1.0

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);
448
+ debug$3("langInfo", langInfo);
441
449
  const generate = /\.?json5?/.test(langInfo) ? generateJSON : generateYAML;
450
+ const isGlobalBlock = globalSFCScope || !!query.global;
442
451
  const parseOptions = getOptions(
443
452
  filename,
444
453
  isProduction,
@@ -451,10 +460,11 @@ function resourcePlugin({
451
460
  strictMessage,
452
461
  escapeHtml,
453
462
  onlyLocales,
454
- forceStringify
463
+ forceStringify,
464
+ usedKeyFilter: collector && isGlobalBlock ? (keyPath) => collector.shouldKeepKey(keyPath) : void 0
455
465
  }
456
466
  );
457
- debug$2("parseOptions", parseOptions);
467
+ debug$3("parseOptions", parseOptions);
458
468
  let source = await getCode(
459
469
  code,
460
470
  filename,
@@ -472,8 +482,8 @@ function resourcePlugin({
472
482
  }
473
483
  }
474
484
  const { code: generatedCode, map } = generate(source, parseOptions);
475
- debug$2("generated code", generatedCode);
476
- debug$2("sourcemap", map, sourceMap);
485
+ debug$3("generated code", generatedCode);
486
+ debug$3("sourcemap", map, sourceMap);
477
487
  if (code === generatedCode) return;
478
488
  return {
479
489
  moduleType: "js",
@@ -529,11 +539,12 @@ async function generateBundleResources(resources, isProduction, {
529
539
  strictMessage = true,
530
540
  escapeHtml = false,
531
541
  jit = true,
532
- transformI18nBlock = void 0
542
+ transformI18nBlock = void 0,
543
+ usedKeyFilter = void 0
533
544
  }) {
534
545
  const codes = [];
535
546
  for (const res of resources) {
536
- debug$2(`${res} bundle loading ...`);
547
+ debug$3(`${res} bundle loading ...`);
537
548
  if (/\.(json5?|ya?ml)$/.test(res)) {
538
549
  const { ext, name } = parse(res);
539
550
  const source = await getRaw(res);
@@ -545,11 +556,12 @@ async function generateBundleResources(resources, isProduction, {
545
556
  strictMessage,
546
557
  escapeHtml,
547
558
  forceStringify,
548
- transformI18nBlock
559
+ transformI18nBlock,
560
+ usedKeyFilter
549
561
  });
550
562
  parseOptions.type = "bare";
551
563
  const { code } = generate(source, parseOptions);
552
- debug$2("generated code", code);
564
+ debug$3("generated code", code);
553
565
  codes.push(`${JSON.stringify(name)}: ${code}`);
554
566
  }
555
567
  }
@@ -584,7 +596,7 @@ async function getCode(source, filename, sourceMap, query, parser, framework = "
584
596
  }
585
597
  if (framework === "webpack") {
586
598
  if (issuerPath) {
587
- debug$2(`getCode (webpack) ${index} via issuerPath`, issuerPath);
599
+ debug$3(`getCode (webpack) ${index} via issuerPath`, issuerPath);
588
600
  return await getRaw(filename);
589
601
  } else {
590
602
  const result = parser(await getRaw(filename), {
@@ -594,7 +606,7 @@ async function getCode(source, filename, sourceMap, query, parser, framework = "
594
606
  const block = result.descriptor.customBlocks[index];
595
607
  if (block) {
596
608
  const code = block.src ? await getRaw(block.src) : block.content;
597
- debug$2(`getCode (webpack) ${index} from SFC`, code);
609
+ debug$3(`getCode (webpack) ${index} from SFC`, code);
598
610
  return code;
599
611
  } else {
600
612
  return source;
@@ -618,7 +630,8 @@ function getOptions(filename, isProduction, query, sourceMap, {
618
630
  strictMessage = true,
619
631
  escapeHtml = false,
620
632
  jit = true,
621
- transformI18nBlock = null
633
+ transformI18nBlock = null,
634
+ usedKeyFilter = void 0
622
635
  }) {
623
636
  const mode = isProduction ? "production" : "development";
624
637
  const baseOptions = {
@@ -631,6 +644,7 @@ function getOptions(filename, isProduction, query, sourceMap, {
631
644
  escapeHtml,
632
645
  jit,
633
646
  onlyLocales,
647
+ usedKeyFilter,
634
648
  env: mode,
635
649
  transformI18nBlock,
636
650
  onWarn: (msg) => {
@@ -675,7 +689,7 @@ async function getRaw(path) {
675
689
  return promises.readFile(path, { encoding: "utf-8" });
676
690
  }
677
691
 
678
- const debug$1 = createDebug(resolveNamespace("directive"));
692
+ const debug$2 = createDebug(resolveNamespace("directive"));
679
693
  const tsEstree = {
680
694
  parse: void 0,
681
695
  simpleTraverse: void 0,
@@ -709,7 +723,7 @@ function directivePlugin({
709
723
  optimizeTranslationDirective,
710
724
  translationIdentifiers
711
725
  );
712
- debug$1(`vite:vue options['template']:`, vuePlugin.api.options);
726
+ debug$2(`vite:vue options['template']:`, vuePlugin.api.options);
713
727
  }
714
728
  },
715
729
  configResolved(config) {
@@ -752,7 +766,7 @@ function resolveVueOptions(vuePlugin, optimizeTranslationDirective, translationI
752
766
  const vuePluginOptions = getVuePluginOptions(vuePlugin);
753
767
  const normalizedFilename = normalizePath(path.relative(vuePluginOptions.root, filename));
754
768
  const resolveIdentifier2 = translationIdentifiers.get(normalizedFilename);
755
- debug$1("resolved vue-i18n Identifier", resolveIdentifier2);
769
+ debug$2("resolved vue-i18n Identifier", resolveIdentifier2);
756
770
  if (resolveIdentifier2 == null) {
757
771
  return void 0;
758
772
  }
@@ -770,7 +784,7 @@ function resolveVueOptions(vuePlugin, optimizeTranslationDirective, translationI
770
784
  }
771
785
  function analyzeIdentifiers(descriptor, { root }, translationIdentifiers) {
772
786
  const source = descriptor.scriptSetup?.content || descriptor.script?.content;
773
- debug$1("getDescriptor content", source);
787
+ debug$2("getDescriptor content", source);
774
788
  if (!source) {
775
789
  return;
776
790
  }
@@ -788,11 +802,11 @@ function analyzeIdentifiers(descriptor, { root }, translationIdentifiers) {
788
802
  if (importLocalName == null) {
789
803
  return;
790
804
  }
791
- debug$1("importLocalName", importLocalName);
805
+ debug$2("importLocalName", importLocalName);
792
806
  const resolvedIdentifier = getVueI18nIdentifier(scope, importLocalName);
793
807
  if (resolvedIdentifier) {
794
808
  const normalizedFilename = normalizePath(path.relative(root, descriptor.filename));
795
- debug$1("set vue-i18n resolved identifier: ", resolvedIdentifier);
809
+ debug$2("set vue-i18n resolved identifier: ", resolvedIdentifier);
796
810
  translationIdentifiers.set(normalizedFilename, resolvedIdentifier);
797
811
  }
798
812
  }
@@ -840,9 +854,9 @@ function getVueI18nIdentifier(scope, local) {
840
854
  return null;
841
855
  }
842
856
  const variableIdPairs = parseVariableId(id);
843
- debug$1("variableIdPairs:", variableIdPairs);
857
+ debug$2("variableIdPairs:", variableIdPairs);
844
858
  const returnVariableIdPairs = parseReturnStatement(returnStatement);
845
- debug$1("returnVariableIdPairs:", returnVariableIdPairs);
859
+ debug$2("returnVariableIdPairs:", returnVariableIdPairs);
846
860
  return resolveIdentifier(variableIdPairs, returnVariableIdPairs);
847
861
  }
848
862
  const EMPTY_NODE_RETURN = {
@@ -954,6 +968,270 @@ function resolveIdentifier(localVariables, returnVariable) {
954
968
  }
955
969
  }
956
970
 
971
+ function matchSafelistPattern(keyPath, pattern) {
972
+ const regexStr = pattern.replace(/\./g, "\\.").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^.]*").replace(/\{\{GLOBSTAR\}\}/g, ".*");
973
+ return new RegExp(`^${regexStr}$`).test(keyPath);
974
+ }
975
+ function createUsedKeysCollector(options) {
976
+ const usedKeys = /* @__PURE__ */ new Set();
977
+ const safelistPatterns = options.safelist || [];
978
+ const dynamicKeyStrategy = options.dynamicKeyStrategy || "keep-all";
979
+ const removedKeys = /* @__PURE__ */ new Map();
980
+ let dynamicKeysDetected = false;
981
+ return {
982
+ get usedKeys() {
983
+ return usedKeys;
984
+ },
985
+ get dynamicKeysDetected() {
986
+ return dynamicKeysDetected;
987
+ },
988
+ set dynamicKeysDetected(val) {
989
+ dynamicKeysDetected = val;
990
+ },
991
+ safelistPatterns,
992
+ removedKeys,
993
+ addKey(key) {
994
+ usedKeys.add(key);
995
+ },
996
+ markDynamic() {
997
+ dynamicKeysDetected = true;
998
+ },
999
+ shouldKeepKey(keyPath) {
1000
+ if (dynamicKeysDetected && dynamicKeyStrategy === "keep-all") {
1001
+ return true;
1002
+ }
1003
+ if (safelistPatterns.some((pattern) => matchSafelistPattern(keyPath, pattern))) {
1004
+ return true;
1005
+ }
1006
+ if (usedKeys.has(keyPath)) {
1007
+ return true;
1008
+ }
1009
+ for (const key of usedKeys) {
1010
+ if (key.startsWith(keyPath + ".")) {
1011
+ return true;
1012
+ }
1013
+ }
1014
+ return false;
1015
+ },
1016
+ reportRemoved(filename, keyPath) {
1017
+ const existing = removedKeys.get(filename);
1018
+ if (existing) {
1019
+ existing.push(keyPath);
1020
+ } else {
1021
+ removedKeys.set(filename, [keyPath]);
1022
+ }
1023
+ },
1024
+ getDiagnostics() {
1025
+ let totalRemoved = 0;
1026
+ for (const keys of removedKeys.values()) {
1027
+ totalRemoved += keys.length;
1028
+ }
1029
+ return { totalRemoved, byFile: removedKeys };
1030
+ }
1031
+ };
1032
+ }
1033
+
1034
+ const I18N_FUNCTIONS = /* @__PURE__ */ new Set(["t", "$t", "tc", "$tc", "te", "$te", "d", "$d", "n", "$n"]);
1035
+ const TEMPLATE_T_REGEX = /\$?t\s*\(\s*['"]([^'"]+)['"]/g;
1036
+ const VT_DIRECTIVE_STRING_REGEX = /v-t\s*=\s*"'([^']+)'"/g;
1037
+ const VT_DIRECTIVE_PATH_REGEX = /v-t\s*=\s*"\{[^}]*path\s*:\s*'([^']+)'/g;
1038
+ const TEMPLATE_DYNAMIC_T_REGEX = /\$?t\s*\(\s*[a-zA-Z_$]/g;
1039
+ async function extractKeysFromScript(source) {
1040
+ const keys = [];
1041
+ let hasDynamic = false;
1042
+ let tsEstree;
1043
+ try {
1044
+ tsEstree = await import('@typescript-eslint/typescript-estree');
1045
+ } catch {
1046
+ return extractKeysFromScriptRegex(source);
1047
+ }
1048
+ try {
1049
+ const ast = tsEstree.parse(source, {
1050
+ range: true,
1051
+ jsx: true,
1052
+ allowInvalidAST: true,
1053
+ suppressDeprecatedPropertyWarnings: true
1054
+ });
1055
+ tsEstree.simpleTraverse(ast, {
1056
+ enter(node) {
1057
+ if (node.type !== tsEstree.AST_NODE_TYPES.CallExpression) {
1058
+ return;
1059
+ }
1060
+ const calleeName = getCalleeName(node, tsEstree.AST_NODE_TYPES);
1061
+ if (!calleeName || !I18N_FUNCTIONS.has(calleeName)) {
1062
+ return;
1063
+ }
1064
+ const firstArg = node.arguments[0];
1065
+ if (!firstArg) {
1066
+ return;
1067
+ }
1068
+ if (firstArg.type === tsEstree.AST_NODE_TYPES.Literal && typeof firstArg.value === "string") {
1069
+ keys.push(firstArg.value);
1070
+ } else if (firstArg.type === tsEstree.AST_NODE_TYPES.TemplateLiteral && firstArg.expressions.length === 0 && firstArg.quasis.length === 1) {
1071
+ const value = firstArg.quasis[0].value.cooked;
1072
+ if (value) {
1073
+ keys.push(value);
1074
+ }
1075
+ } else {
1076
+ hasDynamic = true;
1077
+ }
1078
+ }
1079
+ });
1080
+ } catch {
1081
+ return extractKeysFromScriptRegex(source);
1082
+ }
1083
+ return { keys, hasDynamic };
1084
+ }
1085
+ function getCalleeName(node, AST_NODE_TYPES) {
1086
+ const callee = node.callee;
1087
+ if (callee.type === AST_NODE_TYPES.Identifier) {
1088
+ return callee.name;
1089
+ }
1090
+ if (callee.type === AST_NODE_TYPES.MemberExpression && callee.property.type === AST_NODE_TYPES.Identifier) {
1091
+ return callee.property.name;
1092
+ }
1093
+ return null;
1094
+ }
1095
+ function extractKeysFromScriptRegex(source) {
1096
+ const keys = [];
1097
+ let match;
1098
+ const regex = /\b(?:\$?t|tc|\$tc|te|\$te|d|\$d|n|\$n)\s*\(\s*['"]([^'"]+)['"]/g;
1099
+ while ((match = regex.exec(source)) !== null) {
1100
+ keys.push(match[1]);
1101
+ }
1102
+ return { keys, hasDynamic: false };
1103
+ }
1104
+ function extractKeysFromTemplate(templateContent) {
1105
+ const keys = [];
1106
+ let hasDynamic = false;
1107
+ let match;
1108
+ TEMPLATE_T_REGEX.lastIndex = 0;
1109
+ VT_DIRECTIVE_STRING_REGEX.lastIndex = 0;
1110
+ VT_DIRECTIVE_PATH_REGEX.lastIndex = 0;
1111
+ TEMPLATE_DYNAMIC_T_REGEX.lastIndex = 0;
1112
+ while ((match = TEMPLATE_T_REGEX.exec(templateContent)) !== null) {
1113
+ keys.push(match[1]);
1114
+ }
1115
+ while ((match = VT_DIRECTIVE_STRING_REGEX.exec(templateContent)) !== null) {
1116
+ keys.push(match[1]);
1117
+ }
1118
+ while ((match = VT_DIRECTIVE_PATH_REGEX.exec(templateContent)) !== null) {
1119
+ keys.push(match[1]);
1120
+ }
1121
+ if (TEMPLATE_DYNAMIC_T_REGEX.test(templateContent)) {
1122
+ hasDynamic = true;
1123
+ }
1124
+ return { keys, hasDynamic };
1125
+ }
1126
+ async function analyzeVueSFC(content) {
1127
+ const keys = [];
1128
+ let hasDynamic = false;
1129
+ let parse;
1130
+ try {
1131
+ const compilerSfc = await import('vue/compiler-sfc');
1132
+ parse = compilerSfc.parse;
1133
+ } catch {
1134
+ const templateResult = extractKeysFromTemplate(content);
1135
+ const scriptResult = await extractKeysFromScript(content);
1136
+ return {
1137
+ keys: [...templateResult.keys, ...scriptResult.keys],
1138
+ hasDynamic: templateResult.hasDynamic || scriptResult.hasDynamic
1139
+ };
1140
+ }
1141
+ const { descriptor } = parse(content);
1142
+ if (descriptor.template?.content) {
1143
+ const templateResult = extractKeysFromTemplate(descriptor.template.content);
1144
+ keys.push(...templateResult.keys);
1145
+ if (templateResult.hasDynamic) {
1146
+ hasDynamic = true;
1147
+ }
1148
+ }
1149
+ const scriptContent = descriptor.scriptSetup?.content || descriptor.script?.content;
1150
+ if (scriptContent) {
1151
+ const scriptResult = await extractKeysFromScript(scriptContent);
1152
+ keys.push(...scriptResult.keys);
1153
+ if (scriptResult.hasDynamic) {
1154
+ hasDynamic = true;
1155
+ }
1156
+ }
1157
+ return { keys, hasDynamic };
1158
+ }
1159
+ async function analyzeFile(content, filePath, collector) {
1160
+ let result;
1161
+ if (filePath.endsWith(".vue")) {
1162
+ result = await analyzeVueSFC(content);
1163
+ } else {
1164
+ result = await extractKeysFromScript(content);
1165
+ }
1166
+ for (const key of result.keys) {
1167
+ collector.addKey(key);
1168
+ }
1169
+ if (result.hasDynamic) {
1170
+ collector.markDynamic();
1171
+ }
1172
+ }
1173
+
1174
+ const debug$1 = createDebug(resolveNamespace("tree-shaking"));
1175
+ function treeShakingPlugin(resolvedOptions, collector) {
1176
+ let projectRoot = "";
1177
+ return {
1178
+ name: resolveNamespace("tree-shaking"),
1179
+ enforce: "pre",
1180
+ vite: {
1181
+ configResolved(config) {
1182
+ projectRoot = config.root;
1183
+ }
1184
+ },
1185
+ webpack(compiler) {
1186
+ projectRoot = compiler.options.context || process.cwd();
1187
+ },
1188
+ async buildStart() {
1189
+ const treeShaking = resolvedOptions.treeShaking;
1190
+ const patterns = treeShaking?.scanPatterns || [`${projectRoot}/src/**/*.{vue,ts,js,tsx,jsx}`];
1191
+ debug$1("scanning patterns:", patterns);
1192
+ const files = await fg(patterns, {
1193
+ ignore: ["**/node_modules/**"],
1194
+ absolute: true
1195
+ });
1196
+ debug$1(`found ${files.length} source files to scan`);
1197
+ for (const file of files) {
1198
+ try {
1199
+ const content = await promises.readFile(file, "utf-8");
1200
+ await analyzeFile(content, file, collector);
1201
+ } catch (err) {
1202
+ debug$1(`failed to analyze file: ${file}`, err);
1203
+ }
1204
+ }
1205
+ debug$1(
1206
+ `scan complete: ${collector.usedKeys.size} used keys found, dynamic: ${collector.dynamicKeysDetected}`
1207
+ );
1208
+ if (collector.dynamicKeysDetected) {
1209
+ const strategy = treeShaking?.dynamicKeyStrategy || "keep-all";
1210
+ if (strategy === "keep-all") {
1211
+ warn(
1212
+ "Tree-shaking: dynamic key usage detected. All keys will be preserved (dynamicKeyStrategy: keep-all)."
1213
+ );
1214
+ } else {
1215
+ warn(
1216
+ "Tree-shaking: dynamic key usage detected. Unused keys will still be removed (dynamicKeyStrategy: ignore)."
1217
+ );
1218
+ }
1219
+ }
1220
+ },
1221
+ buildEnd() {
1222
+ const diagnostics = collector.getDiagnostics();
1223
+ if (diagnostics.totalRemoved > 0) {
1224
+ debug$1(`tree-shaking: removed ${diagnostics.totalRemoved} unused message keys`);
1225
+ for (const [file, keys] of diagnostics.byFile) {
1226
+ debug$1(` ${file}: removed ${keys.length} keys (${keys.join(", ")})`);
1227
+ }
1228
+ } else {
1229
+ debug$1("tree-shaking: no keys removed");
1230
+ }
1231
+ }
1232
+ };
1233
+ }
1234
+
957
1235
  const debug = createDebug(resolveNamespace("root"));
958
1236
  const unpluginFactory = (options = {}, meta) => {
959
1237
  debug("meta framework", meta.framework);
@@ -963,7 +1241,12 @@ const unpluginFactory = (options = {}, meta) => {
963
1241
  debug("plugin options (resolving):", options);
964
1242
  const resolvedOptions = resolveOptions(options);
965
1243
  debug("plugin options (resolved):", resolvedOptions);
966
- const plugins = [resourcePlugin(resolvedOptions, meta)];
1244
+ const collector = resolvedOptions.treeShaking ? createUsedKeysCollector(resolvedOptions.treeShaking) : null;
1245
+ const plugins = [];
1246
+ if (resolvedOptions.treeShaking && collector) {
1247
+ plugins.push(treeShakingPlugin(resolvedOptions, collector));
1248
+ }
1249
+ plugins.push(resourcePlugin(resolvedOptions, meta, collector));
967
1250
  if (resolvedOptions.optimizeTranslationDirective) {
968
1251
  if (meta.framework === "webpack") {
969
1252
  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 };