i18nsmith 0.4.3 → 0.6.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 +126 -0
- package/build.mjs +5 -0
- package/dist/commands/backup.d.ts.map +1 -1
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/coverage.d.ts.map +1 -1
- package/dist/commands/detect.d.ts.map +1 -1
- package/dist/commands/diagnose.d.ts.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/rename.d.ts.map +1 -1
- package/dist/commands/review.d.ts.map +1 -1
- package/dist/commands/scaffold-adapter.d.ts.map +1 -1
- package/dist/commands/scan.d.ts.map +1 -1
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/transform.d.ts.map +1 -1
- package/dist/index.cjs +13069 -6794
- package/dist/services/index.d.ts +12 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/transform-service.d.ts +64 -0
- package/dist/services/transform-service.d.ts.map +1 -0
- package/dist/utils/bootstrap.d.ts +83 -0
- package/dist/utils/bootstrap.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/commands/backup.ts +28 -10
- package/src/commands/check.ts +16 -4
- package/src/commands/config.ts +126 -0
- package/src/commands/coverage.ts +11 -4
- package/src/commands/detect.ts +11 -4
- package/src/commands/diagnose.ts +12 -2
- package/src/commands/init.test.ts +80 -0
- package/src/commands/init.ts +92 -49
- package/src/commands/rename.ts +24 -5
- package/src/commands/review.ts +10 -3
- package/src/commands/scaffold-adapter.ts +11 -2
- package/src/commands/scan.ts +20 -7
- package/src/commands/sync.ts +91 -23
- package/src/commands/transform.ts +8 -1
- package/src/index.ts +1 -1
- package/src/integration.test.ts +145 -12
- package/src/services/index.ts +12 -0
- package/src/services/transform-service.ts +203 -0
- package/src/utils/bootstrap.ts +221 -0
package/src/commands/sync.ts
CHANGED
|
@@ -4,9 +4,9 @@ import inquirer, { type CheckboxQuestion } from "inquirer";
|
|
|
4
4
|
import { promises as fs } from "fs";
|
|
5
5
|
import path from "path";
|
|
6
6
|
import {
|
|
7
|
+
// Legacy imports - kept for complex interactive flows
|
|
8
|
+
// TODO: Migrate interactive sync to use ISyncService when it supports passing syncer instance
|
|
7
9
|
Syncer,
|
|
8
|
-
KeyRenamer,
|
|
9
|
-
LocaleStore,
|
|
10
10
|
loadConfig,
|
|
11
11
|
loadConfigWithMeta,
|
|
12
12
|
generateRenameProposals,
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
type KeyRenameBatchSummary,
|
|
16
16
|
type SyncSelection,
|
|
17
17
|
} from "@i18nsmith/core";
|
|
18
|
+
import { getServiceContainer } from "../utils/bootstrap.js";
|
|
18
19
|
import {
|
|
19
20
|
printLocaleDiffs,
|
|
20
21
|
writeLocaleDiffPatches,
|
|
@@ -22,7 +23,7 @@ import {
|
|
|
22
23
|
import { applyPreviewFile, writePreviewFile } from "../utils/preview.js";
|
|
23
24
|
import { SYNC_EXIT_CODES } from "../utils/exit-codes.js";
|
|
24
25
|
import { runCheck } from "./check.js";
|
|
25
|
-
import { withErrorHandling } from "../utils/errors.js";
|
|
26
|
+
import { withErrorHandling, CliError } from "../utils/errors.js";
|
|
26
27
|
|
|
27
28
|
interface SyncCommandOptions {
|
|
28
29
|
config?: string;
|
|
@@ -48,7 +49,7 @@ interface SyncCommandOptions {
|
|
|
48
49
|
invalidateCache?: boolean;
|
|
49
50
|
autoRenameSuspicious?: boolean;
|
|
50
51
|
renameMapFile?: string;
|
|
51
|
-
namingConvention?: "kebab-case" | "camelCase" | "snake_case";
|
|
52
|
+
namingConvention?: "kebab-case" | "camelCase" | "snake_case" | "auto";
|
|
52
53
|
rewriteShape?: "flat" | "nested";
|
|
53
54
|
shapeDelimiter?: string;
|
|
54
55
|
seedTargetLocales?: boolean;
|
|
@@ -193,7 +194,7 @@ export function registerSync(program: Command) {
|
|
|
193
194
|
)
|
|
194
195
|
.option(
|
|
195
196
|
"--naming-convention <convention>",
|
|
196
|
-
"Naming convention for auto-rename (kebab-case, camelCase, snake_case)",
|
|
197
|
+
"Naming convention for auto-rename (kebab-case, camelCase, snake_case, auto)",
|
|
197
198
|
"kebab-case"
|
|
198
199
|
)
|
|
199
200
|
.option(
|
|
@@ -328,7 +329,14 @@ export function registerSync(program: Command) {
|
|
|
328
329
|
: writeEnabled
|
|
329
330
|
? "Syncing locale files..."
|
|
330
331
|
: "Checking locale drift...";
|
|
331
|
-
|
|
332
|
+
// When JSON output is requested, avoid human-readable preamble on stdout
|
|
333
|
+
// so callers can reliably parse the JSON summary. Send banner to stderr
|
|
334
|
+
// when --json is used.
|
|
335
|
+
if (options.json) {
|
|
336
|
+
console.error(chalk.blue(banner));
|
|
337
|
+
} else {
|
|
338
|
+
console.log(chalk.blue(banner));
|
|
339
|
+
}
|
|
332
340
|
|
|
333
341
|
try {
|
|
334
342
|
const { config, projectRoot, configPath } = await loadConfigWithMeta(
|
|
@@ -467,11 +475,16 @@ export function registerSync(program: Command) {
|
|
|
467
475
|
process.cwd(),
|
|
468
476
|
config.localesDir ?? "locales"
|
|
469
477
|
);
|
|
470
|
-
const
|
|
478
|
+
const localeContainer = getServiceContainer({ workspaceRoot: projectRoot });
|
|
479
|
+
const localeStore = localeContainer.localeStoreFactory.create(localesDir, {
|
|
471
480
|
sortKeys: config.locales?.sortKeys ?? "alphabetical",
|
|
472
481
|
});
|
|
473
482
|
const sourceLocale = config.sourceLanguage ?? "en";
|
|
474
|
-
const
|
|
483
|
+
const sourceResult = await localeStore.get(sourceLocale);
|
|
484
|
+
if (!sourceResult.success) {
|
|
485
|
+
throw new CliError(`Failed to read source locale: ${sourceResult.error.message}`);
|
|
486
|
+
}
|
|
487
|
+
const sourceData = sourceResult.data;
|
|
475
488
|
const existingKeys = new Set(Object.keys(sourceData));
|
|
476
489
|
|
|
477
490
|
const namingConvention = options.namingConvention ?? "kebab-case";
|
|
@@ -480,6 +493,8 @@ export function registerSync(program: Command) {
|
|
|
480
493
|
namingConvention,
|
|
481
494
|
workspaceRoot: projectRoot,
|
|
482
495
|
allowExistingConflicts: true,
|
|
496
|
+
allExistingKeys: Object.keys(sourceData), // Pass all existing keys for convention detection
|
|
497
|
+
preserveExistingConvention: true, // Respect existing project conventions
|
|
483
498
|
});
|
|
484
499
|
|
|
485
500
|
if (report.safeProposals.length > 0) {
|
|
@@ -488,15 +503,17 @@ export function registerSync(program: Command) {
|
|
|
488
503
|
to: proposal.proposedKey,
|
|
489
504
|
}));
|
|
490
505
|
|
|
491
|
-
const
|
|
492
|
-
workspaceRoot: projectRoot,
|
|
493
|
-
});
|
|
506
|
+
const renameContainer = getServiceContainer({ workspaceRoot: projectRoot });
|
|
494
507
|
// Run rename batch in dry-run mode with diffs
|
|
495
|
-
const
|
|
508
|
+
const renameResult = await renameContainer.keyRenamer.renameBatch(config, mappings, {
|
|
496
509
|
write: false,
|
|
497
510
|
diff: true,
|
|
498
511
|
allowConflicts: true,
|
|
499
512
|
});
|
|
513
|
+
if (!renameResult.success) {
|
|
514
|
+
throw new CliError(`Rename analysis failed: ${renameResult.error.message}`);
|
|
515
|
+
}
|
|
516
|
+
const batchSummary = renameResult.data;
|
|
500
517
|
|
|
501
518
|
// Merge rename diffs into summary
|
|
502
519
|
summary.renameDiffs = batchSummary.diffs;
|
|
@@ -575,7 +592,7 @@ export function registerSync(program: Command) {
|
|
|
575
592
|
options.rewriteShape &&
|
|
576
593
|
(options.rewriteShape === "flat" || options.rewriteShape === "nested")
|
|
577
594
|
) {
|
|
578
|
-
await handleRewriteShape(options, config);
|
|
595
|
+
await handleRewriteShape(options, config, projectRoot);
|
|
579
596
|
}
|
|
580
597
|
|
|
581
598
|
const shouldFailPlaceholders =
|
|
@@ -769,6 +786,18 @@ function printSyncSummary(summary: SyncSummary) {
|
|
|
769
786
|
console.log(chalk.green("No unused locale keys detected."));
|
|
770
787
|
}
|
|
771
788
|
|
|
789
|
+
if (summary.untranslatedKeys.length) {
|
|
790
|
+
console.log(chalk.blue("Untranslated keys (protected from pruning):"));
|
|
791
|
+
summary.untranslatedKeys.slice(0, 50).forEach((item) => {
|
|
792
|
+
console.log(` • ${item.key} (${item.locales.join(", ")})`);
|
|
793
|
+
});
|
|
794
|
+
if (summary.untranslatedKeys.length > 50) {
|
|
795
|
+
console.log(
|
|
796
|
+
chalk.gray(` ...and ${summary.untranslatedKeys.length - 50} more.`)
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
772
801
|
if (summary.validation.interpolations) {
|
|
773
802
|
if (summary.placeholderIssues.length) {
|
|
774
803
|
console.log(chalk.yellow("Placeholder mismatches:"));
|
|
@@ -873,18 +902,43 @@ async function handleAutoRenameSuspicious(
|
|
|
873
902
|
process.cwd(),
|
|
874
903
|
config.localesDir ?? "locales"
|
|
875
904
|
);
|
|
876
|
-
const
|
|
905
|
+
const container = getServiceContainer({ workspaceRoot: projectRoot });
|
|
906
|
+
const localeStore = container.localeStoreFactory.create(localesDir, {
|
|
877
907
|
sortKeys: config.locales?.sortKeys ?? "alphabetical",
|
|
878
908
|
});
|
|
879
909
|
const sourceLocale = config.sourceLanguage ?? "en";
|
|
880
|
-
const
|
|
910
|
+
const sourceResult = await localeStore.get(sourceLocale);
|
|
911
|
+
if (!sourceResult.success) {
|
|
912
|
+
throw new CliError(`Failed to read source locale: ${sourceResult.error.message}`);
|
|
913
|
+
}
|
|
914
|
+
const sourceData = sourceResult.data;
|
|
881
915
|
const existingKeys = new Set(Object.keys(sourceData));
|
|
882
916
|
|
|
883
|
-
//
|
|
917
|
+
// For auto-detection, collect all existing keys from all locales
|
|
918
|
+
let allExistingKeys: string[] | undefined;
|
|
884
919
|
const namingConvention = options.namingConvention ?? "kebab-case";
|
|
920
|
+
if (namingConvention === "auto") {
|
|
921
|
+
const storedLocalesResult = await localeStore.getStoredLocales();
|
|
922
|
+
if (storedLocalesResult.success) {
|
|
923
|
+
const allKeys = new Set<string>();
|
|
924
|
+
for (const locale of storedLocalesResult.data) {
|
|
925
|
+
const localeResult = await localeStore.get(locale);
|
|
926
|
+
if (localeResult.success) {
|
|
927
|
+
Object.keys(localeResult.data).forEach(key => allKeys.add(key));
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
allExistingKeys = Array.from(allKeys);
|
|
931
|
+
console.log(` Auto-detected naming convention from ${allKeys.size} existing keys`);
|
|
932
|
+
} else {
|
|
933
|
+
console.warn(chalk.yellow(" Warning: Could not load all locales for convention detection, falling back to kebab-case"));
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// Generate rename proposals
|
|
885
938
|
const report = generateRenameProposals(summary.suspiciousKeys, {
|
|
886
939
|
existingKeys,
|
|
887
940
|
namingConvention,
|
|
941
|
+
allExistingKeys,
|
|
888
942
|
allowExistingConflicts: true,
|
|
889
943
|
});
|
|
890
944
|
|
|
@@ -980,12 +1034,16 @@ async function handleAutoRenameSuspicious(
|
|
|
980
1034
|
to: proposal.proposedKey,
|
|
981
1035
|
}));
|
|
982
1036
|
|
|
983
|
-
const
|
|
984
|
-
const
|
|
1037
|
+
const renameContainer = getServiceContainer({ workspaceRoot: projectRoot });
|
|
1038
|
+
const renameResult = await renameContainer.keyRenamer.renameBatch(config, mappings, {
|
|
985
1039
|
write: true,
|
|
986
1040
|
diff: Boolean(options.diff),
|
|
987
1041
|
allowConflicts: true,
|
|
988
1042
|
});
|
|
1043
|
+
if (!renameResult.success) {
|
|
1044
|
+
throw new CliError(`Batch rename failed: ${renameResult.error.message}`);
|
|
1045
|
+
}
|
|
1046
|
+
const applySummary = renameResult.data;
|
|
989
1047
|
printRenameBatchSummary(applySummary);
|
|
990
1048
|
|
|
991
1049
|
if (!options.renameMapFile && hasMappings) {
|
|
@@ -1024,12 +1082,16 @@ async function handleAutoRenameSuspicious(
|
|
|
1024
1082
|
to: proposal.proposedKey,
|
|
1025
1083
|
}));
|
|
1026
1084
|
|
|
1027
|
-
const
|
|
1028
|
-
const
|
|
1085
|
+
const renameContainer = getServiceContainer({ workspaceRoot: projectRoot });
|
|
1086
|
+
const diffResult = await renameContainer.keyRenamer.renameBatch(config, mappings, {
|
|
1029
1087
|
write: false,
|
|
1030
1088
|
diff: true,
|
|
1031
1089
|
allowConflicts: true,
|
|
1032
1090
|
});
|
|
1091
|
+
if (!diffResult.success) {
|
|
1092
|
+
throw new CliError(`Diff generation failed: ${diffResult.error.message}`);
|
|
1093
|
+
}
|
|
1094
|
+
const diffSummary = diffResult.data;
|
|
1033
1095
|
|
|
1034
1096
|
return diffSummary.diffs;
|
|
1035
1097
|
}
|
|
@@ -1037,7 +1099,8 @@ async function handleAutoRenameSuspicious(
|
|
|
1037
1099
|
|
|
1038
1100
|
async function handleRewriteShape(
|
|
1039
1101
|
options: SyncCommandOptions,
|
|
1040
|
-
config: Awaited<ReturnType<typeof loadConfig
|
|
1102
|
+
config: Awaited<ReturnType<typeof loadConfig>>,
|
|
1103
|
+
projectRoot: string
|
|
1041
1104
|
) {
|
|
1042
1105
|
const targetFormat = options.rewriteShape as "flat" | "nested";
|
|
1043
1106
|
const delimiter = options.shapeDelimiter ?? ".";
|
|
@@ -1050,7 +1113,8 @@ async function handleRewriteShape(
|
|
|
1050
1113
|
process.cwd(),
|
|
1051
1114
|
config.localesDir ?? "locales"
|
|
1052
1115
|
);
|
|
1053
|
-
const
|
|
1116
|
+
const container = getServiceContainer({ workspaceRoot: projectRoot });
|
|
1117
|
+
const localeStore = container.localeStoreFactory.create(localesDir, {
|
|
1054
1118
|
delimiter,
|
|
1055
1119
|
sortKeys: config.locales?.sortKeys ?? "alphabetical",
|
|
1056
1120
|
});
|
|
@@ -1065,7 +1129,11 @@ async function handleRewriteShape(
|
|
|
1065
1129
|
}
|
|
1066
1130
|
|
|
1067
1131
|
// Rewrite all locales to the target format
|
|
1068
|
-
const
|
|
1132
|
+
const rewriteResult = await localeStore.rewriteShape(targetFormat, { delimiter });
|
|
1133
|
+
if (!rewriteResult.success) {
|
|
1134
|
+
throw new CliError(`Failed to rewrite locales: ${rewriteResult.error.message}`);
|
|
1135
|
+
}
|
|
1136
|
+
const stats = rewriteResult.data;
|
|
1069
1137
|
|
|
1070
1138
|
if (stats.length === 0) {
|
|
1071
1139
|
console.log(chalk.yellow(" No locale files found to rewrite."));
|
|
@@ -227,7 +227,14 @@ export function registerTransform(program: Command) {
|
|
|
227
227
|
: writeEnabled
|
|
228
228
|
? 'Running transform (write mode)...'
|
|
229
229
|
: 'Planning transform (dry-run)...';
|
|
230
|
-
|
|
230
|
+
// When JSON output is requested, avoid human-readable preamble on stdout
|
|
231
|
+
// so callers can reliably parse the JSON summary. Send banner to stderr
|
|
232
|
+
// when --json is used.
|
|
233
|
+
if (options.json) {
|
|
234
|
+
console.error(chalk.blue(banner));
|
|
235
|
+
} else {
|
|
236
|
+
console.log(chalk.blue(banner));
|
|
237
|
+
}
|
|
231
238
|
|
|
232
239
|
try {
|
|
233
240
|
const { config, projectRoot, configPath } = await loadConfigWithMeta(options.config);
|
package/src/index.ts
CHANGED
package/src/integration.test.ts
CHANGED
|
@@ -23,16 +23,25 @@ function runCli(
|
|
|
23
23
|
args: string[],
|
|
24
24
|
options: { cwd?: string } = {}
|
|
25
25
|
): { stdout: string; stderr: string; output: string; exitCode: number } {
|
|
26
|
+
// Clear known debug env vars so the test output is deterministic even when
|
|
27
|
+
// developer environments set DEBUG_* flags. Preserve essential env vars.
|
|
28
|
+
const env: NodeJS.ProcessEnv = {
|
|
29
|
+
...process.env,
|
|
30
|
+
CI: 'true',
|
|
31
|
+
NO_COLOR: '1',
|
|
32
|
+
FORCE_COLOR: '0',
|
|
33
|
+
};
|
|
34
|
+
// Remove any DEBUG_* flags that may leak into the spawned CLI process
|
|
35
|
+
delete env.DEBUG_VUE_PARSER;
|
|
36
|
+
delete env.DEBUG_REFEXT;
|
|
37
|
+
delete env.DEBUG_SYNC_REF;
|
|
38
|
+
delete env.DEBUG_VUE_MUTATE;
|
|
39
|
+
|
|
26
40
|
const result = spawnSync('node', [CLI_PATH, ...args], {
|
|
27
41
|
cwd: options.cwd ?? process.cwd(),
|
|
28
42
|
encoding: 'utf8',
|
|
29
43
|
timeout: 30000,
|
|
30
|
-
env
|
|
31
|
-
...process.env,
|
|
32
|
-
CI: 'true',
|
|
33
|
-
NO_COLOR: '1',
|
|
34
|
-
FORCE_COLOR: '0',
|
|
35
|
-
},
|
|
44
|
+
env,
|
|
36
45
|
});
|
|
37
46
|
|
|
38
47
|
// Log errors for debugging
|
|
@@ -53,11 +62,48 @@ function runCli(
|
|
|
53
62
|
|
|
54
63
|
// Helper to extract JSON from CLI output (may contain log messages before JSON)
|
|
55
64
|
function extractJson<T>(output: string): T {
|
|
56
|
-
|
|
57
|
-
|
|
65
|
+
// Use brace-counting to find all complete top-level JSON objects in the output.
|
|
66
|
+
// This correctly handles nested braces (arrays/objects inside the top-level object).
|
|
67
|
+
const candidates: string[] = [];
|
|
68
|
+
let i = 0;
|
|
69
|
+
while (i < output.length) {
|
|
70
|
+
if (output[i] === '{') {
|
|
71
|
+
let depth = 0;
|
|
72
|
+
let inString = false;
|
|
73
|
+
let escape = false;
|
|
74
|
+
let j = i;
|
|
75
|
+
for (; j < output.length; j++) {
|
|
76
|
+
const ch = output[j];
|
|
77
|
+
if (escape) { escape = false; continue; }
|
|
78
|
+
if (ch === '\\' && inString) { escape = true; continue; }
|
|
79
|
+
if (ch === '"') { inString = !inString; continue; }
|
|
80
|
+
if (inString) continue;
|
|
81
|
+
if (ch === '{') depth++;
|
|
82
|
+
else if (ch === '}') {
|
|
83
|
+
depth--;
|
|
84
|
+
if (depth === 0) { candidates.push(output.slice(i, j + 1)); break; }
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
i = j + 1;
|
|
88
|
+
} else {
|
|
89
|
+
i++;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!candidates.length) {
|
|
58
94
|
throw new Error(`No JSON found in output: ${output.slice(0, 200)}...`);
|
|
59
95
|
}
|
|
60
|
-
|
|
96
|
+
|
|
97
|
+
// Prefer the largest candidate (most likely the top-level summary).
|
|
98
|
+
candidates.sort((a, b) => b.length - a.length);
|
|
99
|
+
for (const candidate of candidates) {
|
|
100
|
+
try {
|
|
101
|
+
return JSON.parse(candidate);
|
|
102
|
+
} catch (_err) {
|
|
103
|
+
// try next candidate
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
throw new Error(`No valid JSON block found in output: ${output.slice(0, 400)}...`);
|
|
61
107
|
}
|
|
62
108
|
|
|
63
109
|
describe('CLI Integration Tests', () => {
|
|
@@ -188,7 +234,7 @@ export function App() {
|
|
|
188
234
|
);
|
|
189
235
|
|
|
190
236
|
const result = runCli(['scan', '--json'], { cwd: tmpDir });
|
|
191
|
-
|
|
237
|
+
const parsed = extractJson<{ filesScanned: number; candidates: unknown[] }>(result.output);
|
|
192
238
|
|
|
193
239
|
expect(parsed).toHaveProperty('filesScanned');
|
|
194
240
|
expect(parsed).toHaveProperty('candidates');
|
|
@@ -333,6 +379,61 @@ export function App() {
|
|
|
333
379
|
expect(backupExists).toBe(true);
|
|
334
380
|
});
|
|
335
381
|
|
|
382
|
+
it('CLI end-to-end: detects nested $t inside template object args (I18nDemo.vue)', async () => {
|
|
383
|
+
// Setup Vue SFC that uses a nested $t(...) inside an interpolation object
|
|
384
|
+
const vue = `
|
|
385
|
+
<template>
|
|
386
|
+
<p v-if="name">{{ $t('common.components.i18ndemo.arg0-name.4ac48a', { arg0: $t('demo.card.greeting'), name }) }}</p>
|
|
387
|
+
</template>
|
|
388
|
+
`;
|
|
389
|
+
|
|
390
|
+
await fs.mkdir(path.join(tmpDir, 'src', 'components'), { recursive: true });
|
|
391
|
+
await fs.writeFile(path.join(tmpDir, 'src', 'components', 'I18nDemo.vue'), vue);
|
|
392
|
+
|
|
393
|
+
const en = {
|
|
394
|
+
common: { components: { i18ndemo: { 'arg0-name': '{arg0} {name}' } } },
|
|
395
|
+
demo: { card: { greeting: 'Hello' } }
|
|
396
|
+
};
|
|
397
|
+
const es = {
|
|
398
|
+
common: { components: { i18ndemo: { 'arg0-name': '{arg0} {name}' } } },
|
|
399
|
+
demo: { card: { greeting: 'Hola' } }
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
await fs.writeFile(path.join(tmpDir, 'locales', 'en.json'), JSON.stringify(en, null, 2));
|
|
403
|
+
await fs.writeFile(path.join(tmpDir, 'locales', 'fr.json'), JSON.stringify(es, null, 2));
|
|
404
|
+
|
|
405
|
+
// Update config to include .vue files for this test
|
|
406
|
+
const cfgPath = path.join(tmpDir, 'i18n.config.json');
|
|
407
|
+
const cfg = JSON.parse(await fs.readFile(cfgPath, 'utf8'));
|
|
408
|
+
cfg.include = ['src/**/*.vue', 'src/**/*.{ts,js,tsx}'];
|
|
409
|
+
await fs.writeFile(cfgPath, JSON.stringify(cfg, null, 2));
|
|
410
|
+
|
|
411
|
+
const result = runCli(['sync', '--json'], { cwd: tmpDir });
|
|
412
|
+
expect(result.exitCode).toBe(0);
|
|
413
|
+
|
|
414
|
+
// Sanity check: CLI output should contain the quoted key somewhere so
|
|
415
|
+
// editors/CI that scan the output can detect the reference even when
|
|
416
|
+
// there are additional log lines present.
|
|
417
|
+
expect(result.output).toContain('"demo.card.greeting"');
|
|
418
|
+
|
|
419
|
+
// Also try to parse JSON summary if possible and validate references.
|
|
420
|
+
// If parsing fails during CI or debugging, print raw output for diagnosis.
|
|
421
|
+
try {
|
|
422
|
+
const parsed = extractJson<any>(result.output);
|
|
423
|
+
const referenced = parsed.references.map((r: any) => r.key);
|
|
424
|
+
const unused = parsed.unusedKeys.map((u: any) => u.key);
|
|
425
|
+
|
|
426
|
+
expect(referenced).toContain('demo.card.greeting');
|
|
427
|
+
expect(unused).not.toContain('demo.card.greeting');
|
|
428
|
+
} catch (err) {
|
|
429
|
+
// Dump output for debugging in test logs then rethrow so CI shows failure
|
|
430
|
+
// (this helps capture the raw CLI output when test fails).
|
|
431
|
+
// eslint-disable-next-line no-console
|
|
432
|
+
console.error('CLI raw output (truncated):', result.output.slice(0, 4000));
|
|
433
|
+
throw err;
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
|
|
336
437
|
it('should skip backup with --no-backup', async () => {
|
|
337
438
|
await fs.writeFile(
|
|
338
439
|
path.join(tmpDir, 'src', 'App.tsx'),
|
|
@@ -427,6 +528,38 @@ export function App() {
|
|
|
427
528
|
expect(result.output).toContain('Planning transform (dry-run)');
|
|
428
529
|
expect(result.exitCode).toBe(0);
|
|
429
530
|
});
|
|
531
|
+
|
|
532
|
+
it('extraction should not include structural opening punctuation (Items ({count}) case)', async () => {
|
|
533
|
+
const content = `export function App() {
|
|
534
|
+
const count = 5;
|
|
535
|
+
const items = ['a','b'];
|
|
536
|
+
return (
|
|
537
|
+
<>
|
|
538
|
+
<p>Items ({count}): {items.join(', ')}</p>
|
|
539
|
+
<p>{'Items (' + count + ')'}</p>
|
|
540
|
+
</>
|
|
541
|
+
);
|
|
542
|
+
}`;
|
|
543
|
+
|
|
544
|
+
await fs.writeFile(path.join(tmpDir, 'src', 'App.tsx'), content);
|
|
545
|
+
|
|
546
|
+
const result = runCli(['transform', '--json'], { cwd: tmpDir });
|
|
547
|
+
expect(result.exitCode).toBe(0);
|
|
548
|
+
const parsed = extractJson<any>(result.output);
|
|
549
|
+
const candidates = parsed.candidates || [];
|
|
550
|
+
|
|
551
|
+
// No candidate text should include the structural '(' token before the placeholder
|
|
552
|
+
const hasBadText = candidates.some((c: any) => typeof c.text === 'string' && c.text.includes('Items ('));
|
|
553
|
+
expect(hasBadText).toBe(false);
|
|
554
|
+
|
|
555
|
+
// Find the Items-related candidate and assert its suggestedKey is derived from the static "Items"
|
|
556
|
+
const itemsCandidate = candidates.find((c: any) => typeof c.text === 'string' && /Items/.test(c.text));
|
|
557
|
+
expect(itemsCandidate).toBeDefined();
|
|
558
|
+
expect(itemsCandidate.text).not.toContain('(');
|
|
559
|
+
if (itemsCandidate.interpolation) {
|
|
560
|
+
expect(itemsCandidate.interpolation.template).not.toMatch(/Items \(/);
|
|
561
|
+
}
|
|
562
|
+
});
|
|
430
563
|
});
|
|
431
564
|
|
|
432
565
|
describe('check command', () => {
|
|
@@ -477,7 +610,7 @@ export function App() {
|
|
|
477
610
|
);
|
|
478
611
|
|
|
479
612
|
const result = runCli(['check', '--json'], { cwd: tmpDir });
|
|
480
|
-
|
|
613
|
+
const parsed = extractJson<{ diagnostics: unknown; sync: unknown }>(result.output);
|
|
481
614
|
|
|
482
615
|
expect(parsed).toHaveProperty('diagnostics');
|
|
483
616
|
expect(parsed).toHaveProperty('sync');
|
|
@@ -524,7 +657,7 @@ export function App() {
|
|
|
524
657
|
);
|
|
525
658
|
|
|
526
659
|
const result = runCli(['check', '--audit', '--json'], { cwd: tmpDir });
|
|
527
|
-
|
|
660
|
+
const parsed = extractJson<{ audit?: { totalQualityIssues: number } }>(result.output);
|
|
528
661
|
|
|
529
662
|
expect(parsed).toHaveProperty('audit');
|
|
530
663
|
expect(parsed.audit?.totalQualityIssues ?? 0).toBeGreaterThan(0);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview
|
|
3
|
+
* CLI Services Module
|
|
4
|
+
*
|
|
5
|
+
* CLI-level service implementations that cannot live in @i18nsmith/core
|
|
6
|
+
* due to dependency constraints.
|
|
7
|
+
*
|
|
8
|
+
* @module @i18nsmith/cli/services
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export { CliTransformService, getTransformService } from './transform-service.js';
|
|
12
|
+
export type { CliTransformServiceConfig } from './transform-service.js';
|