@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/README.md +91 -0
- package/lib/index.cjs +328 -45
- package/lib/index.d.cts +1 -1
- package/lib/index.d.mts +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.mjs +328 -45
- package/lib/types.d.cts +26 -1
- package/lib/types.d.mts +26 -1
- package/lib/types.d.ts +26 -1
- package/package.json +2 -2
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$
|
|
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$
|
|
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$
|
|
227
|
+
debug$3("Using filter for rolldown-vite");
|
|
222
228
|
_filter = createFilter(...resolveIncludeExclude());
|
|
223
229
|
} else {
|
|
224
|
-
debug$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
289
|
+
debug$3(`configResolved: isProduction = ${isProduction}, sourceMap = ${sourceMap}`);
|
|
284
290
|
const isRolldownVite = !!(await getViteModule()).rolldownVersion;
|
|
285
|
-
debug$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
347
|
-
debug$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
430
|
+
debug$3("parseOptions", parseOptions);
|
|
423
431
|
const { code: generatedCode, map } = await generate(code, parseOptions);
|
|
424
|
-
debug$
|
|
425
|
-
debug$
|
|
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$
|
|
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$
|
|
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$
|
|
476
|
-
debug$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
857
|
+
debug$2("variableIdPairs:", variableIdPairs);
|
|
844
858
|
const returnVariableIdPairs = parseReturnStatement(returnStatement);
|
|
845
|
-
debug$
|
|
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
|
|
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 };
|