@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.cjs CHANGED
@@ -49,6 +49,11 @@ function resolveOptions(options) {
49
49
  const optimizeTranslationDirective = shared.isString(options.optimizeTranslationDirective) || shared.isArray(options.optimizeTranslationDirective) ? options.optimizeTranslationDirective : !!options.optimizeTranslationDirective;
50
50
  const translationIdentifiers = /* @__PURE__ */ new Map();
51
51
  const transformI18nBlock = typeof options.transformI18nBlock === "function" ? options.transformI18nBlock : null;
52
+ const treeShaking = options.treeShaking ? typeof options.treeShaking === "object" ? {
53
+ safelist: options.treeShaking.safelist || [],
54
+ dynamicKeyStrategy: options.treeShaking.dynamicKeyStrategy || "keep-all",
55
+ scanPatterns: options.treeShaking.scanPatterns
56
+ } : { safelist: [], dynamicKeyStrategy: "keep-all" } : null;
52
57
  return {
53
58
  include: normalizeGlobOption(options.include || []),
54
59
  exclude: normalizeGlobOption(options.exclude),
@@ -67,7 +72,8 @@ function resolveOptions(options) {
67
72
  escapeHtml,
68
73
  optimizeTranslationDirective,
69
74
  translationIdentifiers,
70
- transformI18nBlock
75
+ transformI18nBlock,
76
+ treeShaking
71
77
  };
72
78
  }
73
79
 
@@ -179,7 +185,7 @@ const RE_VIRTUAL_PREFIXED_INTLIFY_BUNDLE_IMPORT_ID = new RegExp(
179
185
  const RE_RESOURCE_FORMAT = /\.(json5?|ya?ml|[c|m]?[j|t]s)$/;
180
186
  const RE_SFC_I18N_CUSTOM_BLOCK = /\?vue&type=i18n/;
181
187
  const RE_SFC_I18N_WEBPACK_CUSTOM_BLOCK = /blockType=i18n/;
182
- const debug$2 = createDebug__default(resolveNamespace("resource"));
188
+ const debug$3 = createDebug__default(resolveNamespace("resource"));
183
189
  function resourcePlugin({
184
190
  onlyLocales,
185
191
  include,
@@ -197,7 +203,7 @@ function resourcePlugin({
197
203
  allowDynamic,
198
204
  escapeHtml,
199
205
  transformI18nBlock
200
- }, meta) {
206
+ }, meta, collector) {
201
207
  let viteModule = null;
202
208
  async function getViteModule() {
203
209
  if (viteModule != null) {
@@ -225,15 +231,15 @@ function resourcePlugin({
225
231
  return _filter;
226
232
  }
227
233
  if (meta.framework == "webpack") {
228
- debug$2("Using filter for webpack");
234
+ debug$3("Using filter for webpack");
229
235
  _filter = pluginutils.createFilter(...resolveIncludeExclude());
230
236
  } else {
231
237
  const viteModule2 = await getViteModule();
232
238
  if (viteModule2.rolldownVersion) {
233
- debug$2("Using filter for rolldown-vite");
239
+ debug$3("Using filter for rolldown-vite");
234
240
  _filter = pluginutils.createFilter(...resolveIncludeExclude());
235
241
  } else {
236
- debug$2("Using filter for rollup-vite");
242
+ debug$3("Using filter for rollup-vite");
237
243
  _filter = pluginutils.createFilter(...resolveIncludeExcludeForLegacy());
238
244
  }
239
245
  }
@@ -245,7 +251,7 @@ function resourcePlugin({
245
251
  let isProduction = false;
246
252
  let sourceMap = false;
247
253
  const vueI18nAliasName = module;
248
- debug$2(`vue-i18n alias name: ${vueI18nAliasName}`);
254
+ debug$3(`vue-i18n alias name: ${vueI18nAliasName}`);
249
255
  let vuePlugin = null;
250
256
  const getSfcParser = () => {
251
257
  return vuePlugin ? getVueCompiler(vuePlugin).parse : compilerSfc.parse;
@@ -271,7 +277,7 @@ function resourcePlugin({
271
277
  __VUE_I18N_PROD_DEVTOOLS__: false
272
278
  }
273
279
  };
274
- debug$2("define Config:", defineConfig);
280
+ debug$3("define Config:", defineConfig);
275
281
  const aliasConfig = {
276
282
  resolve: {
277
283
  alias: {
@@ -282,7 +288,7 @@ function resourcePlugin({
282
288
  }
283
289
  }
284
290
  };
285
- debug$2("alias Config:", aliasConfig);
291
+ debug$3("alias Config:", aliasConfig);
286
292
  return shared.assign(defineConfig, aliasConfig);
287
293
  },
288
294
  async configResolved(config) {
@@ -292,9 +298,9 @@ function resourcePlugin({
292
298
  }
293
299
  isProduction = config.isProduction;
294
300
  sourceMap = config.command === "build" ? !!config.build.sourcemap : false;
295
- debug$2(`configResolved: isProduction = ${isProduction}, sourceMap = ${sourceMap}`);
301
+ debug$3(`configResolved: isProduction = ${isProduction}, sourceMap = ${sourceMap}`);
296
302
  const isRolldownVite = !!(await getViteModule()).rolldownVersion;
297
- debug$2(`Using ${isRolldownVite ? "rolldown-vite" : "vite"} for the build`);
303
+ debug$3(`Using ${isRolldownVite ? "rolldown-vite" : "vite"} for the build`);
298
304
  if (!isRolldownVite) {
299
305
  const jsonPlugin = getVitePlugin(config, "vite:json");
300
306
  if (jsonPlugin) {
@@ -308,7 +314,7 @@ function resourcePlugin({
308
314
  if (query.vue) {
309
315
  return;
310
316
  }
311
- debug$2("org json plugin");
317
+ debug$3("org json plugin");
312
318
  return orgTransform.apply(this, [code, id]);
313
319
  }
314
320
  jsonPlugin.transform = overrideJson;
@@ -333,7 +339,7 @@ function resourcePlugin({
333
339
  webpack(compiler) {
334
340
  isProduction = compiler.options.mode !== "development";
335
341
  sourceMap = !!compiler.options.devtool;
336
- debug$2(`webpack: isProduction = ${isProduction}, sourceMap = ${sourceMap}`);
342
+ debug$3(`webpack: isProduction = ${isProduction}, sourceMap = ${sourceMap}`);
337
343
  compiler.options.resolve = normalizeConfigResolveAlias(
338
344
  compiler.options.resolve,
339
345
  meta.framework
@@ -342,7 +348,7 @@ function resourcePlugin({
342
348
  ssr: ssrBuild,
343
349
  runtimeOnly
344
350
  });
345
- debug$2(
351
+ debug$3(
346
352
  `set ${vueI18nAliasName}: ${getVueI18nAliasPath({
347
353
  ssr: ssrBuild,
348
354
  runtimeOnly
@@ -355,15 +361,15 @@ function resourcePlugin({
355
361
  __INTLIFY_PROD_DEVTOOLS__: "false"
356
362
  })
357
363
  );
358
- debug$2(`set __VUE_I18N_LEGACY_API__ is '${compositionOnly}'`);
359
- debug$2(`set __VUE_I18N_FULL_INSTALL__ is '${fullInstall}'`);
364
+ debug$3(`set __VUE_I18N_LEGACY_API__ is '${compositionOnly}'`);
365
+ debug$3(`set __VUE_I18N_FULL_INSTALL__ is '${fullInstall}'`);
360
366
  const filter = pluginutils.createFilter(...resolveIncludeExclude());
361
367
  if (compiler.options.module) {
362
368
  compiler.options.module.rules.push({
363
369
  test: RE_RESOURCE_FORMAT,
364
370
  type: "javascript/auto",
365
371
  include(resource) {
366
- debug$2("webpack resource include", resource);
372
+ debug$3("webpack resource include", resource);
367
373
  return filter(resource);
368
374
  }
369
375
  });
@@ -382,7 +388,7 @@ function resourcePlugin({
382
388
  id: RE_VIRTUAL_PREFIXED_INTLIFY_BUNDLE_IMPORT_ID
383
389
  },
384
390
  async handler(id) {
385
- debug$2("load", id);
391
+ debug$3("load", id);
386
392
  if (INTLIFY_BUNDLE_IMPORT_ID === getVirtualId(id, meta.framework) && include) {
387
393
  let resourcePaths = [];
388
394
  for (const inc of include) {
@@ -392,7 +398,8 @@ function resourcePlugin({
392
398
  const code = await generateBundleResources(resourcePaths, isProduction, {
393
399
  forceStringify,
394
400
  strictMessage,
395
- escapeHtml
401
+ escapeHtml,
402
+ usedKeyFilter: collector ? (keyPath) => collector.shouldKeepKey(keyPath) : void 0
396
403
  });
397
404
  return {
398
405
  moduleType: "js",
@@ -409,7 +416,7 @@ function resourcePlugin({
409
416
  return;
410
417
  }
411
418
  const { filename, query } = parseVueRequest(id);
412
- debug$2("transform", id, JSON.stringify(query), filename);
419
+ debug$3("transform", id, JSON.stringify(query), filename);
413
420
  let langInfo = defaultSFCLang;
414
421
  let inSourceMap;
415
422
  if (!query.vue) {
@@ -428,13 +435,14 @@ function resourcePlugin({
428
435
  escapeHtml,
429
436
  jit: true,
430
437
  onlyLocales,
431
- forceStringify
438
+ forceStringify,
439
+ usedKeyFilter: collector ? (keyPath) => collector.shouldKeepKey(keyPath) : void 0
432
440
  }
433
441
  );
434
- debug$2("parseOptions", parseOptions);
442
+ debug$3("parseOptions", parseOptions);
435
443
  const { code: generatedCode, map } = await generate(code, parseOptions);
436
- debug$2("generated code", generatedCode);
437
- debug$2("sourcemap", map, sourceMap);
444
+ debug$3("generated code", generatedCode);
445
+ debug$3("sourcemap", map, sourceMap);
438
446
  if (code === generatedCode) return;
439
447
  return {
440
448
  moduleType: "js",
@@ -449,8 +457,9 @@ function resourcePlugin({
449
457
  } else if (defaultSFCLang) {
450
458
  langInfo = defaultSFCLang;
451
459
  }
452
- debug$2("langInfo", langInfo);
460
+ debug$3("langInfo", langInfo);
453
461
  const generate = /\.?json5?/.test(langInfo) ? bundleUtils.generateJSON : bundleUtils.generateYAML;
462
+ const isGlobalBlock = globalSFCScope || !!query.global;
454
463
  const parseOptions = getOptions(
455
464
  filename,
456
465
  isProduction,
@@ -463,10 +472,11 @@ function resourcePlugin({
463
472
  strictMessage,
464
473
  escapeHtml,
465
474
  onlyLocales,
466
- forceStringify
475
+ forceStringify,
476
+ usedKeyFilter: collector && isGlobalBlock ? (keyPath) => collector.shouldKeepKey(keyPath) : void 0
467
477
  }
468
478
  );
469
- debug$2("parseOptions", parseOptions);
479
+ debug$3("parseOptions", parseOptions);
470
480
  let source = await getCode(
471
481
  code,
472
482
  filename,
@@ -484,8 +494,8 @@ function resourcePlugin({
484
494
  }
485
495
  }
486
496
  const { code: generatedCode, map } = generate(source, parseOptions);
487
- debug$2("generated code", generatedCode);
488
- debug$2("sourcemap", map, sourceMap);
497
+ debug$3("generated code", generatedCode);
498
+ debug$3("sourcemap", map, sourceMap);
489
499
  if (code === generatedCode) return;
490
500
  return {
491
501
  moduleType: "js",
@@ -541,11 +551,12 @@ async function generateBundleResources(resources, isProduction, {
541
551
  strictMessage = true,
542
552
  escapeHtml = false,
543
553
  jit = true,
544
- transformI18nBlock = void 0
554
+ transformI18nBlock = void 0,
555
+ usedKeyFilter = void 0
545
556
  }) {
546
557
  const codes = [];
547
558
  for (const res of resources) {
548
- debug$2(`${res} bundle loading ...`);
559
+ debug$3(`${res} bundle loading ...`);
549
560
  if (/\.(json5?|ya?ml)$/.test(res)) {
550
561
  const { ext, name } = path.parse(res);
551
562
  const source = await getRaw(res);
@@ -557,11 +568,12 @@ async function generateBundleResources(resources, isProduction, {
557
568
  strictMessage,
558
569
  escapeHtml,
559
570
  forceStringify,
560
- transformI18nBlock
571
+ transformI18nBlock,
572
+ usedKeyFilter
561
573
  });
562
574
  parseOptions.type = "bare";
563
575
  const { code } = generate(source, parseOptions);
564
- debug$2("generated code", code);
576
+ debug$3("generated code", code);
565
577
  codes.push(`${JSON.stringify(name)}: ${code}`);
566
578
  }
567
579
  }
@@ -596,7 +608,7 @@ async function getCode(source, filename, sourceMap, query, parser, framework = "
596
608
  }
597
609
  if (framework === "webpack") {
598
610
  if (issuerPath) {
599
- debug$2(`getCode (webpack) ${index} via issuerPath`, issuerPath);
611
+ debug$3(`getCode (webpack) ${index} via issuerPath`, issuerPath);
600
612
  return await getRaw(filename);
601
613
  } else {
602
614
  const result = parser(await getRaw(filename), {
@@ -606,7 +618,7 @@ async function getCode(source, filename, sourceMap, query, parser, framework = "
606
618
  const block = result.descriptor.customBlocks[index];
607
619
  if (block) {
608
620
  const code = block.src ? await getRaw(block.src) : block.content;
609
- debug$2(`getCode (webpack) ${index} from SFC`, code);
621
+ debug$3(`getCode (webpack) ${index} from SFC`, code);
610
622
  return code;
611
623
  } else {
612
624
  return source;
@@ -630,7 +642,8 @@ function getOptions(filename, isProduction, query, sourceMap, {
630
642
  strictMessage = true,
631
643
  escapeHtml = false,
632
644
  jit = true,
633
- transformI18nBlock = null
645
+ transformI18nBlock = null,
646
+ usedKeyFilter = void 0
634
647
  }) {
635
648
  const mode = isProduction ? "production" : "development";
636
649
  const baseOptions = {
@@ -643,6 +656,7 @@ function getOptions(filename, isProduction, query, sourceMap, {
643
656
  escapeHtml,
644
657
  jit,
645
658
  onlyLocales,
659
+ usedKeyFilter,
646
660
  env: mode,
647
661
  transformI18nBlock,
648
662
  onWarn: (msg) => {
@@ -687,7 +701,7 @@ async function getRaw(path) {
687
701
  return node_fs.promises.readFile(path, { encoding: "utf-8" });
688
702
  }
689
703
 
690
- const debug$1 = createDebug__default(resolveNamespace("directive"));
704
+ const debug$2 = createDebug__default(resolveNamespace("directive"));
691
705
  const tsEstree = {
692
706
  parse: void 0,
693
707
  simpleTraverse: void 0,
@@ -721,7 +735,7 @@ function directivePlugin({
721
735
  optimizeTranslationDirective,
722
736
  translationIdentifiers
723
737
  );
724
- debug$1(`vite:vue options['template']:`, vuePlugin.api.options);
738
+ debug$2(`vite:vue options['template']:`, vuePlugin.api.options);
725
739
  }
726
740
  },
727
741
  configResolved(config) {
@@ -764,7 +778,7 @@ function resolveVueOptions(vuePlugin, optimizeTranslationDirective, translationI
764
778
  const vuePluginOptions = getVuePluginOptions(vuePlugin);
765
779
  const normalizedFilename = normalizePath(path__default.relative(vuePluginOptions.root, filename));
766
780
  const resolveIdentifier2 = translationIdentifiers.get(normalizedFilename);
767
- debug$1("resolved vue-i18n Identifier", resolveIdentifier2);
781
+ debug$2("resolved vue-i18n Identifier", resolveIdentifier2);
768
782
  if (resolveIdentifier2 == null) {
769
783
  return void 0;
770
784
  }
@@ -782,7 +796,7 @@ function resolveVueOptions(vuePlugin, optimizeTranslationDirective, translationI
782
796
  }
783
797
  function analyzeIdentifiers(descriptor, { root }, translationIdentifiers) {
784
798
  const source = descriptor.scriptSetup?.content || descriptor.script?.content;
785
- debug$1("getDescriptor content", source);
799
+ debug$2("getDescriptor content", source);
786
800
  if (!source) {
787
801
  return;
788
802
  }
@@ -800,11 +814,11 @@ function analyzeIdentifiers(descriptor, { root }, translationIdentifiers) {
800
814
  if (importLocalName == null) {
801
815
  return;
802
816
  }
803
- debug$1("importLocalName", importLocalName);
817
+ debug$2("importLocalName", importLocalName);
804
818
  const resolvedIdentifier = getVueI18nIdentifier(scope, importLocalName);
805
819
  if (resolvedIdentifier) {
806
820
  const normalizedFilename = normalizePath(path__default.relative(root, descriptor.filename));
807
- debug$1("set vue-i18n resolved identifier: ", resolvedIdentifier);
821
+ debug$2("set vue-i18n resolved identifier: ", resolvedIdentifier);
808
822
  translationIdentifiers.set(normalizedFilename, resolvedIdentifier);
809
823
  }
810
824
  }
@@ -852,9 +866,9 @@ function getVueI18nIdentifier(scope, local) {
852
866
  return null;
853
867
  }
854
868
  const variableIdPairs = parseVariableId(id);
855
- debug$1("variableIdPairs:", variableIdPairs);
869
+ debug$2("variableIdPairs:", variableIdPairs);
856
870
  const returnVariableIdPairs = parseReturnStatement(returnStatement);
857
- debug$1("returnVariableIdPairs:", returnVariableIdPairs);
871
+ debug$2("returnVariableIdPairs:", returnVariableIdPairs);
858
872
  return resolveIdentifier(variableIdPairs, returnVariableIdPairs);
859
873
  }
860
874
  const EMPTY_NODE_RETURN = {
@@ -966,6 +980,270 @@ function resolveIdentifier(localVariables, returnVariable) {
966
980
  }
967
981
  }
968
982
 
983
+ function matchSafelistPattern(keyPath, pattern) {
984
+ const regexStr = pattern.replace(/\./g, "\\.").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^.]*").replace(/\{\{GLOBSTAR\}\}/g, ".*");
985
+ return new RegExp(`^${regexStr}$`).test(keyPath);
986
+ }
987
+ function createUsedKeysCollector(options) {
988
+ const usedKeys = /* @__PURE__ */ new Set();
989
+ const safelistPatterns = options.safelist || [];
990
+ const dynamicKeyStrategy = options.dynamicKeyStrategy || "keep-all";
991
+ const removedKeys = /* @__PURE__ */ new Map();
992
+ let dynamicKeysDetected = false;
993
+ return {
994
+ get usedKeys() {
995
+ return usedKeys;
996
+ },
997
+ get dynamicKeysDetected() {
998
+ return dynamicKeysDetected;
999
+ },
1000
+ set dynamicKeysDetected(val) {
1001
+ dynamicKeysDetected = val;
1002
+ },
1003
+ safelistPatterns,
1004
+ removedKeys,
1005
+ addKey(key) {
1006
+ usedKeys.add(key);
1007
+ },
1008
+ markDynamic() {
1009
+ dynamicKeysDetected = true;
1010
+ },
1011
+ shouldKeepKey(keyPath) {
1012
+ if (dynamicKeysDetected && dynamicKeyStrategy === "keep-all") {
1013
+ return true;
1014
+ }
1015
+ if (safelistPatterns.some((pattern) => matchSafelistPattern(keyPath, pattern))) {
1016
+ return true;
1017
+ }
1018
+ if (usedKeys.has(keyPath)) {
1019
+ return true;
1020
+ }
1021
+ for (const key of usedKeys) {
1022
+ if (key.startsWith(keyPath + ".")) {
1023
+ return true;
1024
+ }
1025
+ }
1026
+ return false;
1027
+ },
1028
+ reportRemoved(filename, keyPath) {
1029
+ const existing = removedKeys.get(filename);
1030
+ if (existing) {
1031
+ existing.push(keyPath);
1032
+ } else {
1033
+ removedKeys.set(filename, [keyPath]);
1034
+ }
1035
+ },
1036
+ getDiagnostics() {
1037
+ let totalRemoved = 0;
1038
+ for (const keys of removedKeys.values()) {
1039
+ totalRemoved += keys.length;
1040
+ }
1041
+ return { totalRemoved, byFile: removedKeys };
1042
+ }
1043
+ };
1044
+ }
1045
+
1046
+ const I18N_FUNCTIONS = /* @__PURE__ */ new Set(["t", "$t", "tc", "$tc", "te", "$te", "d", "$d", "n", "$n"]);
1047
+ const TEMPLATE_T_REGEX = /\$?t\s*\(\s*['"]([^'"]+)['"]/g;
1048
+ const VT_DIRECTIVE_STRING_REGEX = /v-t\s*=\s*"'([^']+)'"/g;
1049
+ const VT_DIRECTIVE_PATH_REGEX = /v-t\s*=\s*"\{[^}]*path\s*:\s*'([^']+)'/g;
1050
+ const TEMPLATE_DYNAMIC_T_REGEX = /\$?t\s*\(\s*[a-zA-Z_$]/g;
1051
+ async function extractKeysFromScript(source) {
1052
+ const keys = [];
1053
+ let hasDynamic = false;
1054
+ let tsEstree;
1055
+ try {
1056
+ tsEstree = await import('@typescript-eslint/typescript-estree');
1057
+ } catch {
1058
+ return extractKeysFromScriptRegex(source);
1059
+ }
1060
+ try {
1061
+ const ast = tsEstree.parse(source, {
1062
+ range: true,
1063
+ jsx: true,
1064
+ allowInvalidAST: true,
1065
+ suppressDeprecatedPropertyWarnings: true
1066
+ });
1067
+ tsEstree.simpleTraverse(ast, {
1068
+ enter(node) {
1069
+ if (node.type !== tsEstree.AST_NODE_TYPES.CallExpression) {
1070
+ return;
1071
+ }
1072
+ const calleeName = getCalleeName(node, tsEstree.AST_NODE_TYPES);
1073
+ if (!calleeName || !I18N_FUNCTIONS.has(calleeName)) {
1074
+ return;
1075
+ }
1076
+ const firstArg = node.arguments[0];
1077
+ if (!firstArg) {
1078
+ return;
1079
+ }
1080
+ if (firstArg.type === tsEstree.AST_NODE_TYPES.Literal && typeof firstArg.value === "string") {
1081
+ keys.push(firstArg.value);
1082
+ } else if (firstArg.type === tsEstree.AST_NODE_TYPES.TemplateLiteral && firstArg.expressions.length === 0 && firstArg.quasis.length === 1) {
1083
+ const value = firstArg.quasis[0].value.cooked;
1084
+ if (value) {
1085
+ keys.push(value);
1086
+ }
1087
+ } else {
1088
+ hasDynamic = true;
1089
+ }
1090
+ }
1091
+ });
1092
+ } catch {
1093
+ return extractKeysFromScriptRegex(source);
1094
+ }
1095
+ return { keys, hasDynamic };
1096
+ }
1097
+ function getCalleeName(node, AST_NODE_TYPES) {
1098
+ const callee = node.callee;
1099
+ if (callee.type === AST_NODE_TYPES.Identifier) {
1100
+ return callee.name;
1101
+ }
1102
+ if (callee.type === AST_NODE_TYPES.MemberExpression && callee.property.type === AST_NODE_TYPES.Identifier) {
1103
+ return callee.property.name;
1104
+ }
1105
+ return null;
1106
+ }
1107
+ function extractKeysFromScriptRegex(source) {
1108
+ const keys = [];
1109
+ let match;
1110
+ const regex = /\b(?:\$?t|tc|\$tc|te|\$te|d|\$d|n|\$n)\s*\(\s*['"]([^'"]+)['"]/g;
1111
+ while ((match = regex.exec(source)) !== null) {
1112
+ keys.push(match[1]);
1113
+ }
1114
+ return { keys, hasDynamic: false };
1115
+ }
1116
+ function extractKeysFromTemplate(templateContent) {
1117
+ const keys = [];
1118
+ let hasDynamic = false;
1119
+ let match;
1120
+ TEMPLATE_T_REGEX.lastIndex = 0;
1121
+ VT_DIRECTIVE_STRING_REGEX.lastIndex = 0;
1122
+ VT_DIRECTIVE_PATH_REGEX.lastIndex = 0;
1123
+ TEMPLATE_DYNAMIC_T_REGEX.lastIndex = 0;
1124
+ while ((match = TEMPLATE_T_REGEX.exec(templateContent)) !== null) {
1125
+ keys.push(match[1]);
1126
+ }
1127
+ while ((match = VT_DIRECTIVE_STRING_REGEX.exec(templateContent)) !== null) {
1128
+ keys.push(match[1]);
1129
+ }
1130
+ while ((match = VT_DIRECTIVE_PATH_REGEX.exec(templateContent)) !== null) {
1131
+ keys.push(match[1]);
1132
+ }
1133
+ if (TEMPLATE_DYNAMIC_T_REGEX.test(templateContent)) {
1134
+ hasDynamic = true;
1135
+ }
1136
+ return { keys, hasDynamic };
1137
+ }
1138
+ async function analyzeVueSFC(content) {
1139
+ const keys = [];
1140
+ let hasDynamic = false;
1141
+ let parse;
1142
+ try {
1143
+ const compilerSfc = await import('vue/compiler-sfc');
1144
+ parse = compilerSfc.parse;
1145
+ } catch {
1146
+ const templateResult = extractKeysFromTemplate(content);
1147
+ const scriptResult = await extractKeysFromScript(content);
1148
+ return {
1149
+ keys: [...templateResult.keys, ...scriptResult.keys],
1150
+ hasDynamic: templateResult.hasDynamic || scriptResult.hasDynamic
1151
+ };
1152
+ }
1153
+ const { descriptor } = parse(content);
1154
+ if (descriptor.template?.content) {
1155
+ const templateResult = extractKeysFromTemplate(descriptor.template.content);
1156
+ keys.push(...templateResult.keys);
1157
+ if (templateResult.hasDynamic) {
1158
+ hasDynamic = true;
1159
+ }
1160
+ }
1161
+ const scriptContent = descriptor.scriptSetup?.content || descriptor.script?.content;
1162
+ if (scriptContent) {
1163
+ const scriptResult = await extractKeysFromScript(scriptContent);
1164
+ keys.push(...scriptResult.keys);
1165
+ if (scriptResult.hasDynamic) {
1166
+ hasDynamic = true;
1167
+ }
1168
+ }
1169
+ return { keys, hasDynamic };
1170
+ }
1171
+ async function analyzeFile(content, filePath, collector) {
1172
+ let result;
1173
+ if (filePath.endsWith(".vue")) {
1174
+ result = await analyzeVueSFC(content);
1175
+ } else {
1176
+ result = await extractKeysFromScript(content);
1177
+ }
1178
+ for (const key of result.keys) {
1179
+ collector.addKey(key);
1180
+ }
1181
+ if (result.hasDynamic) {
1182
+ collector.markDynamic();
1183
+ }
1184
+ }
1185
+
1186
+ const debug$1 = createDebug__default(resolveNamespace("tree-shaking"));
1187
+ function treeShakingPlugin(resolvedOptions, collector) {
1188
+ let projectRoot = "";
1189
+ return {
1190
+ name: resolveNamespace("tree-shaking"),
1191
+ enforce: "pre",
1192
+ vite: {
1193
+ configResolved(config) {
1194
+ projectRoot = config.root;
1195
+ }
1196
+ },
1197
+ webpack(compiler) {
1198
+ projectRoot = compiler.options.context || process.cwd();
1199
+ },
1200
+ async buildStart() {
1201
+ const treeShaking = resolvedOptions.treeShaking;
1202
+ const patterns = treeShaking?.scanPatterns || [`${projectRoot}/src/**/*.{vue,ts,js,tsx,jsx}`];
1203
+ debug$1("scanning patterns:", patterns);
1204
+ const files = await fg__default(patterns, {
1205
+ ignore: ["**/node_modules/**"],
1206
+ absolute: true
1207
+ });
1208
+ debug$1(`found ${files.length} source files to scan`);
1209
+ for (const file of files) {
1210
+ try {
1211
+ const content = await node_fs.promises.readFile(file, "utf-8");
1212
+ await analyzeFile(content, file, collector);
1213
+ } catch (err) {
1214
+ debug$1(`failed to analyze file: ${file}`, err);
1215
+ }
1216
+ }
1217
+ debug$1(
1218
+ `scan complete: ${collector.usedKeys.size} used keys found, dynamic: ${collector.dynamicKeysDetected}`
1219
+ );
1220
+ if (collector.dynamicKeysDetected) {
1221
+ const strategy = treeShaking?.dynamicKeyStrategy || "keep-all";
1222
+ if (strategy === "keep-all") {
1223
+ warn(
1224
+ "Tree-shaking: dynamic key usage detected. All keys will be preserved (dynamicKeyStrategy: keep-all)."
1225
+ );
1226
+ } else {
1227
+ warn(
1228
+ "Tree-shaking: dynamic key usage detected. Unused keys will still be removed (dynamicKeyStrategy: ignore)."
1229
+ );
1230
+ }
1231
+ }
1232
+ },
1233
+ buildEnd() {
1234
+ const diagnostics = collector.getDiagnostics();
1235
+ if (diagnostics.totalRemoved > 0) {
1236
+ debug$1(`tree-shaking: removed ${diagnostics.totalRemoved} unused message keys`);
1237
+ for (const [file, keys] of diagnostics.byFile) {
1238
+ debug$1(` ${file}: removed ${keys.length} keys (${keys.join(", ")})`);
1239
+ }
1240
+ } else {
1241
+ debug$1("tree-shaking: no keys removed");
1242
+ }
1243
+ }
1244
+ };
1245
+ }
1246
+
969
1247
  const debug = createDebug__default(resolveNamespace("root"));
970
1248
  const unpluginFactory = (options = {}, meta) => {
971
1249
  debug("meta framework", meta.framework);
@@ -975,7 +1253,12 @@ const unpluginFactory = (options = {}, meta) => {
975
1253
  debug("plugin options (resolving):", options);
976
1254
  const resolvedOptions = resolveOptions(options);
977
1255
  debug("plugin options (resolved):", resolvedOptions);
978
- const plugins = [resourcePlugin(resolvedOptions, meta)];
1256
+ const collector = resolvedOptions.treeShaking ? createUsedKeysCollector(resolvedOptions.treeShaking) : null;
1257
+ const plugins = [];
1258
+ if (resolvedOptions.treeShaking && collector) {
1259
+ plugins.push(treeShakingPlugin(resolvedOptions, collector));
1260
+ }
1261
+ plugins.push(resourcePlugin(resolvedOptions, meta, collector));
979
1262
  if (resolvedOptions.optimizeTranslationDirective) {
980
1263
  if (meta.framework === "webpack") {
981
1264
  raiseError(
package/lib/index.d.cts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { UnpluginInstance, UnpluginFactory } from 'unplugin';
2
2
  import { PluginOptions } from './types.cjs';
3
- export { SFCLangFormat, VueI18nModule } from './types.cjs';
3
+ export { SFCLangFormat, TreeShakingOptions, VueI18nModule } from './types.cjs';
4
4
 
5
5
  declare const unpluginFactory: UnpluginFactory<PluginOptions | undefined>;
6
6
  declare const unplugin: UnpluginInstance<PluginOptions | undefined, boolean>;
package/lib/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { UnpluginInstance, UnpluginFactory } from 'unplugin';
2
2
  import { PluginOptions } from './types.mjs';
3
- export { SFCLangFormat, VueI18nModule } from './types.mjs';
3
+ export { SFCLangFormat, TreeShakingOptions, VueI18nModule } from './types.mjs';
4
4
 
5
5
  declare const unpluginFactory: UnpluginFactory<PluginOptions | undefined>;
6
6
  declare const unplugin: UnpluginInstance<PluginOptions | undefined, boolean>;
package/lib/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { UnpluginInstance, UnpluginFactory } from 'unplugin';
2
2
  import { PluginOptions } from './types.js';
3
- export { SFCLangFormat, VueI18nModule } from './types.js';
3
+ export { SFCLangFormat, TreeShakingOptions, VueI18nModule } from './types.js';
4
4
 
5
5
  declare const unpluginFactory: UnpluginFactory<PluginOptions | undefined>;
6
6
  declare const unplugin: UnpluginInstance<PluginOptions | undefined, boolean>;