@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.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);
453
- const generate = /\.?json5?/.test(langInfo) ? bundleUtils.generateJSON : bundleUtils.generateYAML;
460
+ debug$3("langInfo", langInfo);
461
+ const generate = getGenerator(langInfo, bundleUtils.generateYAML);
462
+ const isGlobalBlock = globalSFCScope || !!query.global;
454
463
  const parseOptions = getOptions(
455
464
  filename,
456
465
  isProduction,
@@ -459,14 +468,16 @@ function resourcePlugin({
459
468
  {
460
469
  inSourceMap,
461
470
  isGlobal: globalSFCScope,
471
+ allowDynamic,
462
472
  jit: true,
463
473
  strictMessage,
464
474
  escapeHtml,
465
475
  onlyLocales,
466
- forceStringify
476
+ forceStringify,
477
+ usedKeyFilter: collector && isGlobalBlock ? (keyPath) => collector.shouldKeepKey(keyPath) : void 0
467
478
  }
468
479
  );
469
- debug$2("parseOptions", parseOptions);
480
+ debug$3("parseOptions", parseOptions);
470
481
  let source = await getCode(
471
482
  code,
472
483
  filename,
@@ -483,9 +494,12 @@ function resourcePlugin({
483
494
  warn("transformI18nBlock should return a string");
484
495
  }
485
496
  }
486
- const { code: generatedCode, map } = generate(source, parseOptions);
487
- debug$2("generated code", generatedCode);
488
- debug$2("sourcemap", map, sourceMap);
497
+ if (/\.?[cm]?[jt]s$/.test(langInfo)) {
498
+ source = `export default ${source.replace(/^[\s;]+/, "")}`;
499
+ }
500
+ const { code: generatedCode, map } = await generate(source, parseOptions);
501
+ debug$3("generated code", generatedCode);
502
+ debug$3("sourcemap", map, sourceMap);
489
503
  if (code === generatedCode) return;
490
504
  return {
491
505
  moduleType: "js",
@@ -503,13 +517,13 @@ function getGenerator(ext, fallback = bundleUtils.generateJSON) {
503
517
  if (/\.?json5?$/.test(ext)) {
504
518
  return bundleUtils.generateJSON;
505
519
  }
506
- if (/\.ya?ml$/.test(ext)) {
520
+ if (/\.?ya?ml$/.test(ext)) {
507
521
  return bundleUtils.generateYAML;
508
522
  }
509
- if (/\.[c|m]?js$/.test(ext)) {
523
+ if (/\.?[cm]?js$/.test(ext)) {
510
524
  return bundleUtils.generateJavaScript;
511
525
  }
512
- if (/\.[c|m]?ts$/.test(ext)) {
526
+ if (/\.?[cm]?ts$/.test(ext)) {
513
527
  return bundleUtils.generateTypescript;
514
528
  }
515
529
  return fallback;
@@ -541,11 +555,12 @@ async function generateBundleResources(resources, isProduction, {
541
555
  strictMessage = true,
542
556
  escapeHtml = false,
543
557
  jit = true,
544
- transformI18nBlock = void 0
558
+ transformI18nBlock = void 0,
559
+ usedKeyFilter = void 0
545
560
  }) {
546
561
  const codes = [];
547
562
  for (const res of resources) {
548
- debug$2(`${res} bundle loading ...`);
563
+ debug$3(`${res} bundle loading ...`);
549
564
  if (/\.(json5?|ya?ml)$/.test(res)) {
550
565
  const { ext, name } = path.parse(res);
551
566
  const source = await getRaw(res);
@@ -557,11 +572,12 @@ async function generateBundleResources(resources, isProduction, {
557
572
  strictMessage,
558
573
  escapeHtml,
559
574
  forceStringify,
560
- transformI18nBlock
575
+ transformI18nBlock,
576
+ usedKeyFilter
561
577
  });
562
578
  parseOptions.type = "bare";
563
579
  const { code } = generate(source, parseOptions);
564
- debug$2("generated code", code);
580
+ debug$3("generated code", code);
565
581
  codes.push(`${JSON.stringify(name)}: ${code}`);
566
582
  }
567
583
  }
@@ -596,7 +612,7 @@ async function getCode(source, filename, sourceMap, query, parser, framework = "
596
612
  }
597
613
  if (framework === "webpack") {
598
614
  if (issuerPath) {
599
- debug$2(`getCode (webpack) ${index} via issuerPath`, issuerPath);
615
+ debug$3(`getCode (webpack) ${index} via issuerPath`, issuerPath);
600
616
  return await getRaw(filename);
601
617
  } else {
602
618
  const result = parser(await getRaw(filename), {
@@ -606,7 +622,7 @@ async function getCode(source, filename, sourceMap, query, parser, framework = "
606
622
  const block = result.descriptor.customBlocks[index];
607
623
  if (block) {
608
624
  const code = block.src ? await getRaw(block.src) : block.content;
609
- debug$2(`getCode (webpack) ${index} from SFC`, code);
625
+ debug$3(`getCode (webpack) ${index} from SFC`, code);
610
626
  return code;
611
627
  } else {
612
628
  return source;
@@ -630,7 +646,8 @@ function getOptions(filename, isProduction, query, sourceMap, {
630
646
  strictMessage = true,
631
647
  escapeHtml = false,
632
648
  jit = true,
633
- transformI18nBlock = null
649
+ transformI18nBlock = null,
650
+ usedKeyFilter = void 0
634
651
  }) {
635
652
  const mode = isProduction ? "production" : "development";
636
653
  const baseOptions = {
@@ -643,6 +660,7 @@ function getOptions(filename, isProduction, query, sourceMap, {
643
660
  escapeHtml,
644
661
  jit,
645
662
  onlyLocales,
663
+ usedKeyFilter,
646
664
  env: mode,
647
665
  transformI18nBlock,
648
666
  onWarn: (msg) => {
@@ -687,7 +705,7 @@ async function getRaw(path) {
687
705
  return node_fs.promises.readFile(path, { encoding: "utf-8" });
688
706
  }
689
707
 
690
- const debug$1 = createDebug__default(resolveNamespace("directive"));
708
+ const debug$2 = createDebug__default(resolveNamespace("directive"));
691
709
  const tsEstree = {
692
710
  parse: void 0,
693
711
  simpleTraverse: void 0,
@@ -721,7 +739,7 @@ function directivePlugin({
721
739
  optimizeTranslationDirective,
722
740
  translationIdentifiers
723
741
  );
724
- debug$1(`vite:vue options['template']:`, vuePlugin.api.options);
742
+ debug$2(`vite:vue options['template']:`, vuePlugin.api.options);
725
743
  }
726
744
  },
727
745
  configResolved(config) {
@@ -764,7 +782,7 @@ function resolveVueOptions(vuePlugin, optimizeTranslationDirective, translationI
764
782
  const vuePluginOptions = getVuePluginOptions(vuePlugin);
765
783
  const normalizedFilename = normalizePath(path__default.relative(vuePluginOptions.root, filename));
766
784
  const resolveIdentifier2 = translationIdentifiers.get(normalizedFilename);
767
- debug$1("resolved vue-i18n Identifier", resolveIdentifier2);
785
+ debug$2("resolved vue-i18n Identifier", resolveIdentifier2);
768
786
  if (resolveIdentifier2 == null) {
769
787
  return void 0;
770
788
  }
@@ -782,7 +800,7 @@ function resolveVueOptions(vuePlugin, optimizeTranslationDirective, translationI
782
800
  }
783
801
  function analyzeIdentifiers(descriptor, { root }, translationIdentifiers) {
784
802
  const source = descriptor.scriptSetup?.content || descriptor.script?.content;
785
- debug$1("getDescriptor content", source);
803
+ debug$2("getDescriptor content", source);
786
804
  if (!source) {
787
805
  return;
788
806
  }
@@ -800,11 +818,11 @@ function analyzeIdentifiers(descriptor, { root }, translationIdentifiers) {
800
818
  if (importLocalName == null) {
801
819
  return;
802
820
  }
803
- debug$1("importLocalName", importLocalName);
821
+ debug$2("importLocalName", importLocalName);
804
822
  const resolvedIdentifier = getVueI18nIdentifier(scope, importLocalName);
805
823
  if (resolvedIdentifier) {
806
824
  const normalizedFilename = normalizePath(path__default.relative(root, descriptor.filename));
807
- debug$1("set vue-i18n resolved identifier: ", resolvedIdentifier);
825
+ debug$2("set vue-i18n resolved identifier: ", resolvedIdentifier);
808
826
  translationIdentifiers.set(normalizedFilename, resolvedIdentifier);
809
827
  }
810
828
  }
@@ -852,9 +870,9 @@ function getVueI18nIdentifier(scope, local) {
852
870
  return null;
853
871
  }
854
872
  const variableIdPairs = parseVariableId(id);
855
- debug$1("variableIdPairs:", variableIdPairs);
873
+ debug$2("variableIdPairs:", variableIdPairs);
856
874
  const returnVariableIdPairs = parseReturnStatement(returnStatement);
857
- debug$1("returnVariableIdPairs:", returnVariableIdPairs);
875
+ debug$2("returnVariableIdPairs:", returnVariableIdPairs);
858
876
  return resolveIdentifier(variableIdPairs, returnVariableIdPairs);
859
877
  }
860
878
  const EMPTY_NODE_RETURN = {
@@ -966,6 +984,270 @@ function resolveIdentifier(localVariables, returnVariable) {
966
984
  }
967
985
  }
968
986
 
987
+ function matchSafelistPattern(keyPath, pattern) {
988
+ const regexStr = pattern.replace(/\./g, "\\.").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^.]*").replace(/\{\{GLOBSTAR\}\}/g, ".*");
989
+ return new RegExp(`^${regexStr}$`).test(keyPath);
990
+ }
991
+ function createUsedKeysCollector(options) {
992
+ const usedKeys = /* @__PURE__ */ new Set();
993
+ const safelistPatterns = options.safelist || [];
994
+ const dynamicKeyStrategy = options.dynamicKeyStrategy || "keep-all";
995
+ const removedKeys = /* @__PURE__ */ new Map();
996
+ let dynamicKeysDetected = false;
997
+ return {
998
+ get usedKeys() {
999
+ return usedKeys;
1000
+ },
1001
+ get dynamicKeysDetected() {
1002
+ return dynamicKeysDetected;
1003
+ },
1004
+ set dynamicKeysDetected(val) {
1005
+ dynamicKeysDetected = val;
1006
+ },
1007
+ safelistPatterns,
1008
+ removedKeys,
1009
+ addKey(key) {
1010
+ usedKeys.add(key);
1011
+ },
1012
+ markDynamic() {
1013
+ dynamicKeysDetected = true;
1014
+ },
1015
+ shouldKeepKey(keyPath) {
1016
+ if (dynamicKeysDetected && dynamicKeyStrategy === "keep-all") {
1017
+ return true;
1018
+ }
1019
+ if (safelistPatterns.some((pattern) => matchSafelistPattern(keyPath, pattern))) {
1020
+ return true;
1021
+ }
1022
+ if (usedKeys.has(keyPath)) {
1023
+ return true;
1024
+ }
1025
+ for (const key of usedKeys) {
1026
+ if (key.startsWith(keyPath + ".")) {
1027
+ return true;
1028
+ }
1029
+ }
1030
+ return false;
1031
+ },
1032
+ reportRemoved(filename, keyPath) {
1033
+ const existing = removedKeys.get(filename);
1034
+ if (existing) {
1035
+ existing.push(keyPath);
1036
+ } else {
1037
+ removedKeys.set(filename, [keyPath]);
1038
+ }
1039
+ },
1040
+ getDiagnostics() {
1041
+ let totalRemoved = 0;
1042
+ for (const keys of removedKeys.values()) {
1043
+ totalRemoved += keys.length;
1044
+ }
1045
+ return { totalRemoved, byFile: removedKeys };
1046
+ }
1047
+ };
1048
+ }
1049
+
1050
+ const I18N_FUNCTIONS = /* @__PURE__ */ new Set(["t", "$t", "tc", "$tc", "te", "$te", "d", "$d", "n", "$n"]);
1051
+ const TEMPLATE_T_REGEX = /\$?t\s*\(\s*['"]([^'"]+)['"]/g;
1052
+ const VT_DIRECTIVE_STRING_REGEX = /v-t\s*=\s*"'([^']+)'"/g;
1053
+ const VT_DIRECTIVE_PATH_REGEX = /v-t\s*=\s*"\{[^}]*path\s*:\s*'([^']+)'/g;
1054
+ const TEMPLATE_DYNAMIC_T_REGEX = /\$?t\s*\(\s*[a-zA-Z_$]/g;
1055
+ async function extractKeysFromScript(source) {
1056
+ const keys = [];
1057
+ let hasDynamic = false;
1058
+ let tsEstree;
1059
+ try {
1060
+ tsEstree = await import('@typescript-eslint/typescript-estree');
1061
+ } catch {
1062
+ return extractKeysFromScriptRegex(source);
1063
+ }
1064
+ try {
1065
+ const ast = tsEstree.parse(source, {
1066
+ range: true,
1067
+ jsx: true,
1068
+ allowInvalidAST: true,
1069
+ suppressDeprecatedPropertyWarnings: true
1070
+ });
1071
+ tsEstree.simpleTraverse(ast, {
1072
+ enter(node) {
1073
+ if (node.type !== tsEstree.AST_NODE_TYPES.CallExpression) {
1074
+ return;
1075
+ }
1076
+ const calleeName = getCalleeName(node, tsEstree.AST_NODE_TYPES);
1077
+ if (!calleeName || !I18N_FUNCTIONS.has(calleeName)) {
1078
+ return;
1079
+ }
1080
+ const firstArg = node.arguments[0];
1081
+ if (!firstArg) {
1082
+ return;
1083
+ }
1084
+ if (firstArg.type === tsEstree.AST_NODE_TYPES.Literal && typeof firstArg.value === "string") {
1085
+ keys.push(firstArg.value);
1086
+ } else if (firstArg.type === tsEstree.AST_NODE_TYPES.TemplateLiteral && firstArg.expressions.length === 0 && firstArg.quasis.length === 1) {
1087
+ const value = firstArg.quasis[0].value.cooked;
1088
+ if (value) {
1089
+ keys.push(value);
1090
+ }
1091
+ } else {
1092
+ hasDynamic = true;
1093
+ }
1094
+ }
1095
+ });
1096
+ } catch {
1097
+ return extractKeysFromScriptRegex(source);
1098
+ }
1099
+ return { keys, hasDynamic };
1100
+ }
1101
+ function getCalleeName(node, AST_NODE_TYPES) {
1102
+ const callee = node.callee;
1103
+ if (callee.type === AST_NODE_TYPES.Identifier) {
1104
+ return callee.name;
1105
+ }
1106
+ if (callee.type === AST_NODE_TYPES.MemberExpression && callee.property.type === AST_NODE_TYPES.Identifier) {
1107
+ return callee.property.name;
1108
+ }
1109
+ return null;
1110
+ }
1111
+ function extractKeysFromScriptRegex(source) {
1112
+ const keys = [];
1113
+ let match;
1114
+ const regex = /\b(?:\$?t|tc|\$tc|te|\$te|d|\$d|n|\$n)\s*\(\s*['"]([^'"]+)['"]/g;
1115
+ while ((match = regex.exec(source)) !== null) {
1116
+ keys.push(match[1]);
1117
+ }
1118
+ return { keys, hasDynamic: false };
1119
+ }
1120
+ function extractKeysFromTemplate(templateContent) {
1121
+ const keys = [];
1122
+ let hasDynamic = false;
1123
+ let match;
1124
+ TEMPLATE_T_REGEX.lastIndex = 0;
1125
+ VT_DIRECTIVE_STRING_REGEX.lastIndex = 0;
1126
+ VT_DIRECTIVE_PATH_REGEX.lastIndex = 0;
1127
+ TEMPLATE_DYNAMIC_T_REGEX.lastIndex = 0;
1128
+ while ((match = TEMPLATE_T_REGEX.exec(templateContent)) !== null) {
1129
+ keys.push(match[1]);
1130
+ }
1131
+ while ((match = VT_DIRECTIVE_STRING_REGEX.exec(templateContent)) !== null) {
1132
+ keys.push(match[1]);
1133
+ }
1134
+ while ((match = VT_DIRECTIVE_PATH_REGEX.exec(templateContent)) !== null) {
1135
+ keys.push(match[1]);
1136
+ }
1137
+ if (TEMPLATE_DYNAMIC_T_REGEX.test(templateContent)) {
1138
+ hasDynamic = true;
1139
+ }
1140
+ return { keys, hasDynamic };
1141
+ }
1142
+ async function analyzeVueSFC(content) {
1143
+ const keys = [];
1144
+ let hasDynamic = false;
1145
+ let parse;
1146
+ try {
1147
+ const compilerSfc = await import('vue/compiler-sfc');
1148
+ parse = compilerSfc.parse;
1149
+ } catch {
1150
+ const templateResult = extractKeysFromTemplate(content);
1151
+ const scriptResult = await extractKeysFromScript(content);
1152
+ return {
1153
+ keys: [...templateResult.keys, ...scriptResult.keys],
1154
+ hasDynamic: templateResult.hasDynamic || scriptResult.hasDynamic
1155
+ };
1156
+ }
1157
+ const { descriptor } = parse(content);
1158
+ if (descriptor.template?.content) {
1159
+ const templateResult = extractKeysFromTemplate(descriptor.template.content);
1160
+ keys.push(...templateResult.keys);
1161
+ if (templateResult.hasDynamic) {
1162
+ hasDynamic = true;
1163
+ }
1164
+ }
1165
+ const scriptContent = descriptor.scriptSetup?.content || descriptor.script?.content;
1166
+ if (scriptContent) {
1167
+ const scriptResult = await extractKeysFromScript(scriptContent);
1168
+ keys.push(...scriptResult.keys);
1169
+ if (scriptResult.hasDynamic) {
1170
+ hasDynamic = true;
1171
+ }
1172
+ }
1173
+ return { keys, hasDynamic };
1174
+ }
1175
+ async function analyzeFile(content, filePath, collector) {
1176
+ let result;
1177
+ if (filePath.endsWith(".vue")) {
1178
+ result = await analyzeVueSFC(content);
1179
+ } else {
1180
+ result = await extractKeysFromScript(content);
1181
+ }
1182
+ for (const key of result.keys) {
1183
+ collector.addKey(key);
1184
+ }
1185
+ if (result.hasDynamic) {
1186
+ collector.markDynamic();
1187
+ }
1188
+ }
1189
+
1190
+ const debug$1 = createDebug__default(resolveNamespace("tree-shaking"));
1191
+ function treeShakingPlugin(resolvedOptions, collector) {
1192
+ let projectRoot = "";
1193
+ return {
1194
+ name: resolveNamespace("tree-shaking"),
1195
+ enforce: "pre",
1196
+ vite: {
1197
+ configResolved(config) {
1198
+ projectRoot = config.root;
1199
+ }
1200
+ },
1201
+ webpack(compiler) {
1202
+ projectRoot = compiler.options.context || process.cwd();
1203
+ },
1204
+ async buildStart() {
1205
+ const treeShaking = resolvedOptions.treeShaking;
1206
+ const patterns = treeShaking?.scanPatterns || [`${projectRoot}/src/**/*.{vue,ts,js,tsx,jsx}`];
1207
+ debug$1("scanning patterns:", patterns);
1208
+ const files = await fg__default(patterns, {
1209
+ ignore: ["**/node_modules/**"],
1210
+ absolute: true
1211
+ });
1212
+ debug$1(`found ${files.length} source files to scan`);
1213
+ for (const file of files) {
1214
+ try {
1215
+ const content = await node_fs.promises.readFile(file, "utf-8");
1216
+ await analyzeFile(content, file, collector);
1217
+ } catch (err) {
1218
+ debug$1(`failed to analyze file: ${file}`, err);
1219
+ }
1220
+ }
1221
+ debug$1(
1222
+ `scan complete: ${collector.usedKeys.size} used keys found, dynamic: ${collector.dynamicKeysDetected}`
1223
+ );
1224
+ if (collector.dynamicKeysDetected) {
1225
+ const strategy = treeShaking?.dynamicKeyStrategy || "keep-all";
1226
+ if (strategy === "keep-all") {
1227
+ warn(
1228
+ "Tree-shaking: dynamic key usage detected. All keys will be preserved (dynamicKeyStrategy: keep-all)."
1229
+ );
1230
+ } else {
1231
+ warn(
1232
+ "Tree-shaking: dynamic key usage detected. Unused keys will still be removed (dynamicKeyStrategy: ignore)."
1233
+ );
1234
+ }
1235
+ }
1236
+ },
1237
+ buildEnd() {
1238
+ const diagnostics = collector.getDiagnostics();
1239
+ if (diagnostics.totalRemoved > 0) {
1240
+ debug$1(`tree-shaking: removed ${diagnostics.totalRemoved} unused message keys`);
1241
+ for (const [file, keys] of diagnostics.byFile) {
1242
+ debug$1(` ${file}: removed ${keys.length} keys (${keys.join(", ")})`);
1243
+ }
1244
+ } else {
1245
+ debug$1("tree-shaking: no keys removed");
1246
+ }
1247
+ }
1248
+ };
1249
+ }
1250
+
969
1251
  const debug = createDebug__default(resolveNamespace("root"));
970
1252
  const unpluginFactory = (options = {}, meta) => {
971
1253
  debug("meta framework", meta.framework);
@@ -975,7 +1257,12 @@ const unpluginFactory = (options = {}, meta) => {
975
1257
  debug("plugin options (resolving):", options);
976
1258
  const resolvedOptions = resolveOptions(options);
977
1259
  debug("plugin options (resolved):", resolvedOptions);
978
- const plugins = [resourcePlugin(resolvedOptions, meta)];
1260
+ const collector = resolvedOptions.treeShaking ? createUsedKeysCollector(resolvedOptions.treeShaking) : null;
1261
+ const plugins = [];
1262
+ if (resolvedOptions.treeShaking && collector) {
1263
+ plugins.push(treeShakingPlugin(resolvedOptions, collector));
1264
+ }
1265
+ plugins.push(resourcePlugin(resolvedOptions, meta, collector));
979
1266
  if (resolvedOptions.optimizeTranslationDirective) {
980
1267
  if (meta.framework === "webpack") {
981
1268
  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>;