@intlify/unplugin-vue-i18n 11.0.3 → 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
@@ -12,7 +12,6 @@ import pc from 'picocolors';
12
12
  import eslintUitls from '@eslint-community/eslint-utils';
13
13
  import { transformVTDirective } from '@intlify/vue-i18n-extensions';
14
14
  import { analyze } from '@typescript-eslint/scope-manager';
15
- import { parse as parse$2, simpleTraverse, AST_NODE_TYPES } from '@typescript-eslint/typescript-estree';
16
15
 
17
16
  function normalizeGlobOption(val) {
18
17
  if (!val) return void 0;
@@ -38,6 +37,11 @@ function resolveOptions(options) {
38
37
  const optimizeTranslationDirective = isString(options.optimizeTranslationDirective) || isArray(options.optimizeTranslationDirective) ? options.optimizeTranslationDirective : !!options.optimizeTranslationDirective;
39
38
  const translationIdentifiers = /* @__PURE__ */ new Map();
40
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;
41
45
  return {
42
46
  include: normalizeGlobOption(options.include || []),
43
47
  exclude: normalizeGlobOption(options.exclude),
@@ -56,7 +60,8 @@ function resolveOptions(options) {
56
60
  escapeHtml,
57
61
  optimizeTranslationDirective,
58
62
  translationIdentifiers,
59
- transformI18nBlock
63
+ transformI18nBlock,
64
+ treeShaking
60
65
  };
61
66
  }
62
67
 
@@ -168,7 +173,7 @@ const RE_VIRTUAL_PREFIXED_INTLIFY_BUNDLE_IMPORT_ID = new RegExp(
168
173
  const RE_RESOURCE_FORMAT = /\.(json5?|ya?ml|[c|m]?[j|t]s)$/;
169
174
  const RE_SFC_I18N_CUSTOM_BLOCK = /\?vue&type=i18n/;
170
175
  const RE_SFC_I18N_WEBPACK_CUSTOM_BLOCK = /blockType=i18n/;
171
- const debug$2 = createDebug(resolveNamespace("resource"));
176
+ const debug$3 = createDebug(resolveNamespace("resource"));
172
177
  function resourcePlugin({
173
178
  onlyLocales,
174
179
  include,
@@ -186,7 +191,7 @@ function resourcePlugin({
186
191
  allowDynamic,
187
192
  escapeHtml,
188
193
  transformI18nBlock
189
- }, meta) {
194
+ }, meta, collector) {
190
195
  let viteModule = null;
191
196
  async function getViteModule() {
192
197
  if (viteModule != null) {
@@ -214,15 +219,15 @@ function resourcePlugin({
214
219
  return _filter;
215
220
  }
216
221
  if (meta.framework == "webpack") {
217
- debug$2("Using filter for webpack");
222
+ debug$3("Using filter for webpack");
218
223
  _filter = createFilter(...resolveIncludeExclude());
219
224
  } else {
220
225
  const viteModule2 = await getViteModule();
221
226
  if (viteModule2.rolldownVersion) {
222
- debug$2("Using filter for rolldown-vite");
227
+ debug$3("Using filter for rolldown-vite");
223
228
  _filter = createFilter(...resolveIncludeExclude());
224
229
  } else {
225
- debug$2("Using filter for rollup-vite");
230
+ debug$3("Using filter for rollup-vite");
226
231
  _filter = createFilter(...resolveIncludeExcludeForLegacy());
227
232
  }
228
233
  }
@@ -234,7 +239,7 @@ function resourcePlugin({
234
239
  let isProduction = false;
235
240
  let sourceMap = false;
236
241
  const vueI18nAliasName = module;
237
- debug$2(`vue-i18n alias name: ${vueI18nAliasName}`);
242
+ debug$3(`vue-i18n alias name: ${vueI18nAliasName}`);
238
243
  let vuePlugin = null;
239
244
  const getSfcParser = () => {
240
245
  return vuePlugin ? getVueCompiler(vuePlugin).parse : parse$1;
@@ -260,7 +265,7 @@ function resourcePlugin({
260
265
  __VUE_I18N_PROD_DEVTOOLS__: false
261
266
  }
262
267
  };
263
- debug$2("define Config:", defineConfig);
268
+ debug$3("define Config:", defineConfig);
264
269
  const aliasConfig = {
265
270
  resolve: {
266
271
  alias: {
@@ -271,7 +276,7 @@ function resourcePlugin({
271
276
  }
272
277
  }
273
278
  };
274
- debug$2("alias Config:", aliasConfig);
279
+ debug$3("alias Config:", aliasConfig);
275
280
  return assign(defineConfig, aliasConfig);
276
281
  },
277
282
  async configResolved(config) {
@@ -281,9 +286,9 @@ function resourcePlugin({
281
286
  }
282
287
  isProduction = config.isProduction;
283
288
  sourceMap = config.command === "build" ? !!config.build.sourcemap : false;
284
- debug$2(`configResolved: isProduction = ${isProduction}, sourceMap = ${sourceMap}`);
289
+ debug$3(`configResolved: isProduction = ${isProduction}, sourceMap = ${sourceMap}`);
285
290
  const isRolldownVite = !!(await getViteModule()).rolldownVersion;
286
- debug$2(`Using ${isRolldownVite ? "rolldown-vite" : "vite"} for the build`);
291
+ debug$3(`Using ${isRolldownVite ? "rolldown-vite" : "vite"} for the build`);
287
292
  if (!isRolldownVite) {
288
293
  const jsonPlugin = getVitePlugin(config, "vite:json");
289
294
  if (jsonPlugin) {
@@ -297,7 +302,7 @@ function resourcePlugin({
297
302
  if (query.vue) {
298
303
  return;
299
304
  }
300
- debug$2("org json plugin");
305
+ debug$3("org json plugin");
301
306
  return orgTransform.apply(this, [code, id]);
302
307
  }
303
308
  jsonPlugin.transform = overrideJson;
@@ -322,7 +327,7 @@ function resourcePlugin({
322
327
  webpack(compiler) {
323
328
  isProduction = compiler.options.mode !== "development";
324
329
  sourceMap = !!compiler.options.devtool;
325
- debug$2(`webpack: isProduction = ${isProduction}, sourceMap = ${sourceMap}`);
330
+ debug$3(`webpack: isProduction = ${isProduction}, sourceMap = ${sourceMap}`);
326
331
  compiler.options.resolve = normalizeConfigResolveAlias(
327
332
  compiler.options.resolve,
328
333
  meta.framework
@@ -331,7 +336,7 @@ function resourcePlugin({
331
336
  ssr: ssrBuild,
332
337
  runtimeOnly
333
338
  });
334
- debug$2(
339
+ debug$3(
335
340
  `set ${vueI18nAliasName}: ${getVueI18nAliasPath({
336
341
  ssr: ssrBuild,
337
342
  runtimeOnly
@@ -344,15 +349,15 @@ function resourcePlugin({
344
349
  __INTLIFY_PROD_DEVTOOLS__: "false"
345
350
  })
346
351
  );
347
- debug$2(`set __VUE_I18N_LEGACY_API__ is '${compositionOnly}'`);
348
- 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}'`);
349
354
  const filter = createFilter(...resolveIncludeExclude());
350
355
  if (compiler.options.module) {
351
356
  compiler.options.module.rules.push({
352
357
  test: RE_RESOURCE_FORMAT,
353
358
  type: "javascript/auto",
354
359
  include(resource) {
355
- debug$2("webpack resource include", resource);
360
+ debug$3("webpack resource include", resource);
356
361
  return filter(resource);
357
362
  }
358
363
  });
@@ -371,7 +376,7 @@ function resourcePlugin({
371
376
  id: RE_VIRTUAL_PREFIXED_INTLIFY_BUNDLE_IMPORT_ID
372
377
  },
373
378
  async handler(id) {
374
- debug$2("load", id);
379
+ debug$3("load", id);
375
380
  if (INTLIFY_BUNDLE_IMPORT_ID === getVirtualId(id, meta.framework) && include) {
376
381
  let resourcePaths = [];
377
382
  for (const inc of include) {
@@ -381,7 +386,8 @@ function resourcePlugin({
381
386
  const code = await generateBundleResources(resourcePaths, isProduction, {
382
387
  forceStringify,
383
388
  strictMessage,
384
- escapeHtml
389
+ escapeHtml,
390
+ usedKeyFilter: collector ? (keyPath) => collector.shouldKeepKey(keyPath) : void 0
385
391
  });
386
392
  return {
387
393
  moduleType: "js",
@@ -398,7 +404,7 @@ function resourcePlugin({
398
404
  return;
399
405
  }
400
406
  const { filename, query } = parseVueRequest(id);
401
- debug$2("transform", id, JSON.stringify(query), filename);
407
+ debug$3("transform", id, JSON.stringify(query), filename);
402
408
  let langInfo = defaultSFCLang;
403
409
  let inSourceMap;
404
410
  if (!query.vue) {
@@ -417,13 +423,14 @@ function resourcePlugin({
417
423
  escapeHtml,
418
424
  jit: true,
419
425
  onlyLocales,
420
- forceStringify
426
+ forceStringify,
427
+ usedKeyFilter: collector ? (keyPath) => collector.shouldKeepKey(keyPath) : void 0
421
428
  }
422
429
  );
423
- debug$2("parseOptions", parseOptions);
430
+ debug$3("parseOptions", parseOptions);
424
431
  const { code: generatedCode, map } = await generate(code, parseOptions);
425
- debug$2("generated code", generatedCode);
426
- debug$2("sourcemap", map, sourceMap);
432
+ debug$3("generated code", generatedCode);
433
+ debug$3("sourcemap", map, sourceMap);
427
434
  if (code === generatedCode) return;
428
435
  return {
429
436
  moduleType: "js",
@@ -438,8 +445,9 @@ function resourcePlugin({
438
445
  } else if (defaultSFCLang) {
439
446
  langInfo = defaultSFCLang;
440
447
  }
441
- debug$2("langInfo", langInfo);
448
+ debug$3("langInfo", langInfo);
442
449
  const generate = /\.?json5?/.test(langInfo) ? generateJSON : generateYAML;
450
+ const isGlobalBlock = globalSFCScope || !!query.global;
443
451
  const parseOptions = getOptions(
444
452
  filename,
445
453
  isProduction,
@@ -452,10 +460,11 @@ function resourcePlugin({
452
460
  strictMessage,
453
461
  escapeHtml,
454
462
  onlyLocales,
455
- forceStringify
463
+ forceStringify,
464
+ usedKeyFilter: collector && isGlobalBlock ? (keyPath) => collector.shouldKeepKey(keyPath) : void 0
456
465
  }
457
466
  );
458
- debug$2("parseOptions", parseOptions);
467
+ debug$3("parseOptions", parseOptions);
459
468
  let source = await getCode(
460
469
  code,
461
470
  filename,
@@ -473,8 +482,8 @@ function resourcePlugin({
473
482
  }
474
483
  }
475
484
  const { code: generatedCode, map } = generate(source, parseOptions);
476
- debug$2("generated code", generatedCode);
477
- debug$2("sourcemap", map, sourceMap);
485
+ debug$3("generated code", generatedCode);
486
+ debug$3("sourcemap", map, sourceMap);
478
487
  if (code === generatedCode) return;
479
488
  return {
480
489
  moduleType: "js",
@@ -530,11 +539,12 @@ async function generateBundleResources(resources, isProduction, {
530
539
  strictMessage = true,
531
540
  escapeHtml = false,
532
541
  jit = true,
533
- transformI18nBlock = void 0
542
+ transformI18nBlock = void 0,
543
+ usedKeyFilter = void 0
534
544
  }) {
535
545
  const codes = [];
536
546
  for (const res of resources) {
537
- debug$2(`${res} bundle loading ...`);
547
+ debug$3(`${res} bundle loading ...`);
538
548
  if (/\.(json5?|ya?ml)$/.test(res)) {
539
549
  const { ext, name } = parse(res);
540
550
  const source = await getRaw(res);
@@ -546,11 +556,12 @@ async function generateBundleResources(resources, isProduction, {
546
556
  strictMessage,
547
557
  escapeHtml,
548
558
  forceStringify,
549
- transformI18nBlock
559
+ transformI18nBlock,
560
+ usedKeyFilter
550
561
  });
551
562
  parseOptions.type = "bare";
552
563
  const { code } = generate(source, parseOptions);
553
- debug$2("generated code", code);
564
+ debug$3("generated code", code);
554
565
  codes.push(`${JSON.stringify(name)}: ${code}`);
555
566
  }
556
567
  }
@@ -585,7 +596,7 @@ async function getCode(source, filename, sourceMap, query, parser, framework = "
585
596
  }
586
597
  if (framework === "webpack") {
587
598
  if (issuerPath) {
588
- debug$2(`getCode (webpack) ${index} via issuerPath`, issuerPath);
599
+ debug$3(`getCode (webpack) ${index} via issuerPath`, issuerPath);
589
600
  return await getRaw(filename);
590
601
  } else {
591
602
  const result = parser(await getRaw(filename), {
@@ -595,7 +606,7 @@ async function getCode(source, filename, sourceMap, query, parser, framework = "
595
606
  const block = result.descriptor.customBlocks[index];
596
607
  if (block) {
597
608
  const code = block.src ? await getRaw(block.src) : block.content;
598
- debug$2(`getCode (webpack) ${index} from SFC`, code);
609
+ debug$3(`getCode (webpack) ${index} from SFC`, code);
599
610
  return code;
600
611
  } else {
601
612
  return source;
@@ -619,7 +630,8 @@ function getOptions(filename, isProduction, query, sourceMap, {
619
630
  strictMessage = true,
620
631
  escapeHtml = false,
621
632
  jit = true,
622
- transformI18nBlock = null
633
+ transformI18nBlock = null,
634
+ usedKeyFilter = void 0
623
635
  }) {
624
636
  const mode = isProduction ? "production" : "development";
625
637
  const baseOptions = {
@@ -632,6 +644,7 @@ function getOptions(filename, isProduction, query, sourceMap, {
632
644
  escapeHtml,
633
645
  jit,
634
646
  onlyLocales,
647
+ usedKeyFilter,
635
648
  env: mode,
636
649
  transformI18nBlock,
637
650
  onWarn: (msg) => {
@@ -676,7 +689,12 @@ async function getRaw(path) {
676
689
  return promises.readFile(path, { encoding: "utf-8" });
677
690
  }
678
691
 
679
- const debug$1 = createDebug(resolveNamespace("directive"));
692
+ const debug$2 = createDebug(resolveNamespace("directive"));
693
+ const tsEstree = {
694
+ parse: void 0,
695
+ simpleTraverse: void 0,
696
+ AST_NODE_TYPES: void 0
697
+ };
680
698
  function directivePlugin({
681
699
  optimizeTranslationDirective,
682
700
  translationIdentifiers
@@ -688,7 +706,13 @@ function directivePlugin({
688
706
  name: resolveNamespace("directive"),
689
707
  enforce: "pre",
690
708
  vite: {
691
- config(config) {
709
+ async config(config) {
710
+ await import('@typescript-eslint/typescript-estree').then((r) => {
711
+ tsEstree.parse = r.parse;
712
+ tsEstree.simpleTraverse = r.simpleTraverse;
713
+ tsEstree.AST_NODE_TYPES = r.AST_NODE_TYPES;
714
+ return;
715
+ });
692
716
  vuePlugin = getVitePlugin(config, "vite:vue");
693
717
  if (!checkVuePlugin(vuePlugin)) {
694
718
  return;
@@ -699,7 +723,7 @@ function directivePlugin({
699
723
  optimizeTranslationDirective,
700
724
  translationIdentifiers
701
725
  );
702
- debug$1(`vite:vue options['template']:`, vuePlugin.api.options);
726
+ debug$2(`vite:vue options['template']:`, vuePlugin.api.options);
703
727
  }
704
728
  },
705
729
  configResolved(config) {
@@ -742,7 +766,7 @@ function resolveVueOptions(vuePlugin, optimizeTranslationDirective, translationI
742
766
  const vuePluginOptions = getVuePluginOptions(vuePlugin);
743
767
  const normalizedFilename = normalizePath(path.relative(vuePluginOptions.root, filename));
744
768
  const resolveIdentifier2 = translationIdentifiers.get(normalizedFilename);
745
- debug$1("resolved vue-i18n Identifier", resolveIdentifier2);
769
+ debug$2("resolved vue-i18n Identifier", resolveIdentifier2);
746
770
  if (resolveIdentifier2 == null) {
747
771
  return void 0;
748
772
  }
@@ -760,12 +784,12 @@ function resolveVueOptions(vuePlugin, optimizeTranslationDirective, translationI
760
784
  }
761
785
  function analyzeIdentifiers(descriptor, { root }, translationIdentifiers) {
762
786
  const source = descriptor.scriptSetup?.content || descriptor.script?.content;
763
- debug$1("getDescriptor content", source);
787
+ debug$2("getDescriptor content", source);
764
788
  if (!source) {
765
789
  return;
766
790
  }
767
- const ast = parse$2(source, { range: true });
768
- simpleTraverse(ast, {
791
+ const ast = tsEstree.parse(source, { range: true });
792
+ tsEstree.simpleTraverse(ast, {
769
793
  enter(node, parent) {
770
794
  if (parent) {
771
795
  node.parent = parent;
@@ -778,11 +802,11 @@ function analyzeIdentifiers(descriptor, { root }, translationIdentifiers) {
778
802
  if (importLocalName == null) {
779
803
  return;
780
804
  }
781
- debug$1("importLocalName", importLocalName);
805
+ debug$2("importLocalName", importLocalName);
782
806
  const resolvedIdentifier = getVueI18nIdentifier(scope, importLocalName);
783
807
  if (resolvedIdentifier) {
784
808
  const normalizedFilename = normalizePath(path.relative(root, descriptor.filename));
785
- debug$1("set vue-i18n resolved identifier: ", resolvedIdentifier);
809
+ debug$2("set vue-i18n resolved identifier: ", resolvedIdentifier);
786
810
  translationIdentifiers.set(normalizedFilename, resolvedIdentifier);
787
811
  }
788
812
  }
@@ -830,9 +854,9 @@ function getVueI18nIdentifier(scope, local) {
830
854
  return null;
831
855
  }
832
856
  const variableIdPairs = parseVariableId(id);
833
- debug$1("variableIdPairs:", variableIdPairs);
857
+ debug$2("variableIdPairs:", variableIdPairs);
834
858
  const returnVariableIdPairs = parseReturnStatement(returnStatement);
835
- debug$1("returnVariableIdPairs:", returnVariableIdPairs);
859
+ debug$2("returnVariableIdPairs:", returnVariableIdPairs);
836
860
  return resolveIdentifier(variableIdPairs, returnVariableIdPairs);
837
861
  }
838
862
  const EMPTY_NODE_RETURN = {
@@ -845,18 +869,18 @@ function getCallExpressionAndReturnStatement(scope, local) {
845
869
  return EMPTY_NODE_RETURN;
846
870
  }
847
871
  const callExpressionRef = variable.references.find((ref) => {
848
- return ref.identifier.parent?.type === AST_NODE_TYPES.CallExpression;
872
+ return ref.identifier.parent?.type === tsEstree.AST_NODE_TYPES.CallExpression;
849
873
  });
850
874
  if (callExpressionRef == null) {
851
875
  return EMPTY_NODE_RETURN;
852
876
  }
853
877
  let returnStatement = null;
854
- if (callExpressionRef.from.type === "function" && callExpressionRef.from.block.type === AST_NODE_TYPES.FunctionExpression && // @ts-expect-error -- FIXME: type error
855
- callExpressionRef.from.block.parent.type === AST_NODE_TYPES.Property && // @ts-expect-error -- FIXME: type error
856
- callExpressionRef.from.block.parent.key.type === AST_NODE_TYPES.Identifier && // @ts-expect-error -- FIXME: type error
878
+ if (callExpressionRef.from.type === "function" && callExpressionRef.from.block.type === tsEstree.AST_NODE_TYPES.FunctionExpression && // @ts-expect-error -- FIXME: type error
879
+ callExpressionRef.from.block.parent.type === tsEstree.AST_NODE_TYPES.Property && // @ts-expect-error -- FIXME: type error
880
+ callExpressionRef.from.block.parent.key.type === tsEstree.AST_NODE_TYPES.Identifier && // @ts-expect-error -- FIXME: type error
857
881
  callExpressionRef.from.block.parent.key.name === "setup") {
858
882
  returnStatement = callExpressionRef.from.block.body.body.find((statement) => {
859
- return statement.type === AST_NODE_TYPES.ReturnStatement;
883
+ return statement.type === tsEstree.AST_NODE_TYPES.ReturnStatement;
860
884
  });
861
885
  }
862
886
  return {
@@ -866,22 +890,22 @@ function getCallExpressionAndReturnStatement(scope, local) {
866
890
  };
867
891
  }
868
892
  function getVariableDeclarationIdFrom(node) {
869
- if (node.parent?.type !== AST_NODE_TYPES.VariableDeclarator) {
893
+ if (node.parent?.type !== tsEstree.AST_NODE_TYPES.VariableDeclarator) {
870
894
  return null;
871
895
  }
872
896
  return node.parent.id;
873
897
  }
874
898
  function parseVariableId(node) {
875
- if (node.type === AST_NODE_TYPES.Identifier) {
899
+ if (node.type === tsEstree.AST_NODE_TYPES.Identifier) {
876
900
  return [{ key: node.name, value: null }];
877
901
  } else {
878
902
  const props = node.properties.filter(
879
903
  // ignore RestElement
880
- (prop) => prop.type === AST_NODE_TYPES.Property
904
+ (prop) => prop.type === tsEstree.AST_NODE_TYPES.Property
881
905
  );
882
906
  const pairs = [];
883
907
  for (const prop of props) {
884
- if (prop?.key.type === AST_NODE_TYPES.Identifier && prop?.value.type === AST_NODE_TYPES.Identifier) {
908
+ if (prop?.key.type === tsEstree.AST_NODE_TYPES.Identifier && prop?.value.type === tsEstree.AST_NODE_TYPES.Identifier) {
885
909
  pairs.push({ key: prop.key.name, value: prop.value.name });
886
910
  }
887
911
  }
@@ -893,12 +917,12 @@ function parseReturnStatement(node) {
893
917
  if (node == null || node.argument == null) {
894
918
  return pairs;
895
919
  }
896
- if (node.argument.type === AST_NODE_TYPES.ObjectExpression) {
920
+ if (node.argument.type === tsEstree.AST_NODE_TYPES.ObjectExpression) {
897
921
  for (const prop of node.argument.properties) {
898
- if (prop.type === AST_NODE_TYPES.Property) {
899
- if (prop.key.type === AST_NODE_TYPES.Identifier && prop.value.type === AST_NODE_TYPES.Identifier) {
922
+ if (prop.type === tsEstree.AST_NODE_TYPES.Property) {
923
+ if (prop.key.type === tsEstree.AST_NODE_TYPES.Identifier && prop.value.type === tsEstree.AST_NODE_TYPES.Identifier) {
900
924
  pairs.push({ key: prop.key.name, value: prop.value.name });
901
- } else if (prop.key.type === AST_NODE_TYPES.Identifier && prop.value.type === AST_NODE_TYPES.MemberExpression && prop.value.object.type === AST_NODE_TYPES.Identifier && prop.value.property.type === AST_NODE_TYPES.Identifier) {
925
+ } else if (prop.key.type === tsEstree.AST_NODE_TYPES.Identifier && prop.value.type === tsEstree.AST_NODE_TYPES.MemberExpression && prop.value.object.type === tsEstree.AST_NODE_TYPES.Identifier && prop.value.property.type === tsEstree.AST_NODE_TYPES.Identifier) {
902
926
  pairs.push({
903
927
  key: prop.key.name,
904
928
  value: `${prop.value.object.name}.${prop.value.property.name}`
@@ -907,7 +931,7 @@ function parseReturnStatement(node) {
907
931
  }
908
932
  }
909
933
  return pairs;
910
- } else if (node.argument.type === AST_NODE_TYPES.Identifier) {
934
+ } else if (node.argument.type === tsEstree.AST_NODE_TYPES.Identifier) {
911
935
  return pairs;
912
936
  } else {
913
937
  return pairs;
@@ -944,6 +968,270 @@ function resolveIdentifier(localVariables, returnVariable) {
944
968
  }
945
969
  }
946
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
+
947
1235
  const debug = createDebug(resolveNamespace("root"));
948
1236
  const unpluginFactory = (options = {}, meta) => {
949
1237
  debug("meta framework", meta.framework);
@@ -953,7 +1241,12 @@ const unpluginFactory = (options = {}, meta) => {
953
1241
  debug("plugin options (resolving):", options);
954
1242
  const resolvedOptions = resolveOptions(options);
955
1243
  debug("plugin options (resolved):", resolvedOptions);
956
- 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));
957
1250
  if (resolvedOptions.optimizeTranslationDirective) {
958
1251
  if (meta.framework === "webpack") {
959
1252
  raiseError(