i18nsmith 0.4.2 → 0.4.3
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/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/coverage.d.ts +12 -0
- package/dist/commands/coverage.d.ts.map +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/index.cjs +2956 -2167
- package/dist/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/commands/check.ts +123 -3
- package/src/commands/coverage.ts +97 -0
- package/src/commands/init.ts +94 -1
- package/src/commands/sync.ts +5 -0
- package/src/e2e.test.ts +57 -5
- package/src/index.ts +3 -1
- package/src/rename-suspicious.test.ts +2 -2
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAoBpC,eAAO,MAAM,OAAO,SAAgB,CAAC"}
|
package/package.json
CHANGED
package/src/commands/check.ts
CHANGED
|
@@ -2,8 +2,8 @@ import fs from 'fs/promises';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import type { Command } from 'commander';
|
|
5
|
-
import { loadConfigWithMeta, CheckRunner } from '@i18nsmith/core';
|
|
6
|
-
import type { CheckSummary } from '@i18nsmith/core';
|
|
5
|
+
import { loadConfigWithMeta, CheckRunner, isPackageResolvable } from '@i18nsmith/core';
|
|
6
|
+
import type { CheckSummary, I18nConfig } from '@i18nsmith/core';
|
|
7
7
|
import { printLocaleDiffs } from '../utils/diff-utils.js';
|
|
8
8
|
import { getDiagnosisExitSignal } from '../utils/diagnostics-exit.js';
|
|
9
9
|
import { CHECK_EXIT_CODES } from '../utils/exit-codes.js';
|
|
@@ -39,6 +39,12 @@ interface CheckCommandOptions {
|
|
|
39
39
|
auditOrphaned?: boolean;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
interface ParserWarning {
|
|
43
|
+
dependency: string;
|
|
44
|
+
message: string;
|
|
45
|
+
installHint: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
42
48
|
const collectAssumedKeys = (value: string, previous: string[]) => {
|
|
43
49
|
const tokens = value
|
|
44
50
|
.split(',')
|
|
@@ -75,6 +81,8 @@ function printCheckSummary(summary: CheckSummary) {
|
|
|
75
81
|
)
|
|
76
82
|
);
|
|
77
83
|
|
|
84
|
+
printDynamicKeyCoverage(summary);
|
|
85
|
+
|
|
78
86
|
if (summary.actionableItems.length) {
|
|
79
87
|
console.log(chalk.blue('\nActionable items'));
|
|
80
88
|
summary.actionableItems.slice(0, 25).forEach((item) => {
|
|
@@ -101,6 +109,46 @@ function printCheckSummary(summary: CheckSummary) {
|
|
|
101
109
|
}
|
|
102
110
|
}
|
|
103
111
|
|
|
112
|
+
function printDynamicKeyCoverage(summary: CheckSummary) {
|
|
113
|
+
const coverage = summary.sync.dynamicKeyCoverage ?? [];
|
|
114
|
+
if (!coverage.length) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const entriesWithGaps = coverage.filter((entry) =>
|
|
119
|
+
entry.missingByLocale && Object.keys(entry.missingByLocale).length > 0
|
|
120
|
+
);
|
|
121
|
+
const totalMissing = entriesWithGaps.reduce((total, entry) => {
|
|
122
|
+
return (
|
|
123
|
+
total +
|
|
124
|
+
Object.values(entry.missingByLocale).reduce((sum, list) => sum + (Array.isArray(list) ? list.length : 0), 0)
|
|
125
|
+
);
|
|
126
|
+
}, 0);
|
|
127
|
+
|
|
128
|
+
if (!entriesWithGaps.length) {
|
|
129
|
+
console.log(chalk.green('Dynamic key coverage: all expanded keys present.'));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
console.log(
|
|
134
|
+
chalk.yellow(
|
|
135
|
+
`Dynamic key coverage: ${totalMissing} missing translation${totalMissing === 1 ? '' : 's'} across ${entriesWithGaps.length} pattern${entriesWithGaps.length === 1 ? '' : 's'}.`
|
|
136
|
+
)
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
entriesWithGaps.slice(0, 5).forEach((entry) => {
|
|
140
|
+
const localeSummary = Object.entries(entry.missingByLocale)
|
|
141
|
+
.map(([locale, missing]) => `${locale}(${missing.length})`)
|
|
142
|
+
.join(', ');
|
|
143
|
+
console.log(chalk.gray(` • ${entry.pattern}: ${localeSummary}`));
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
if (entriesWithGaps.length > 5) {
|
|
147
|
+
console.log(chalk.gray(` ...and ${entriesWithGaps.length - 5} more.`));
|
|
148
|
+
}
|
|
149
|
+
console.log(chalk.gray(' Tip: run `i18nsmith sync --write` to scaffold missing dynamic keys.'));
|
|
150
|
+
}
|
|
151
|
+
|
|
104
152
|
function formatSeverityLabel(severity: 'info' | 'warn' | 'error'): string {
|
|
105
153
|
if (severity === 'error') {
|
|
106
154
|
return chalk.red('ERROR');
|
|
@@ -157,6 +205,19 @@ export async function runCheck(options: CheckCommandOptions): Promise<void> {
|
|
|
157
205
|
...options.assumeGlobs,
|
|
158
206
|
];
|
|
159
207
|
}
|
|
208
|
+
const parserWarnings: ParserWarning[] = [];
|
|
209
|
+
const parserStatus = buildParserStatus(config, projectRoot);
|
|
210
|
+
const vueStatus = parserStatus.vue;
|
|
211
|
+
if (vueStatus?.required && !vueStatus.available) {
|
|
212
|
+
parserWarnings.push({
|
|
213
|
+
dependency: 'vue-eslint-parser',
|
|
214
|
+
message: 'Vue files detected but "vue-eslint-parser" is not installed. Results may be incomplete.',
|
|
215
|
+
installHint: 'npm install --save-dev vue-eslint-parser',
|
|
216
|
+
});
|
|
217
|
+
console.log(chalk.yellow('⚠️ Vue files detected but "vue-eslint-parser" is not installed.'));
|
|
218
|
+
console.log(chalk.yellow(' Some Vue template references may be skipped.'));
|
|
219
|
+
}
|
|
220
|
+
|
|
160
221
|
const runner = new CheckRunner(config, { workspaceRoot: projectRoot });
|
|
161
222
|
const summary = await runner.run({
|
|
162
223
|
assumedKeys: options.assume,
|
|
@@ -183,7 +244,9 @@ export async function runCheck(options: CheckCommandOptions): Promise<void> {
|
|
|
183
244
|
);
|
|
184
245
|
}
|
|
185
246
|
|
|
186
|
-
const payload = localeAudit
|
|
247
|
+
const payload = localeAudit
|
|
248
|
+
? { ...summary, audit: localeAudit, parserStatus, ...(parserWarnings.length ? { parserWarnings } : {}) }
|
|
249
|
+
: { ...summary, parserStatus, ...(parserWarnings.length ? { parserWarnings } : {}) };
|
|
187
250
|
|
|
188
251
|
if (options.report) {
|
|
189
252
|
const outputPath = path.resolve(process.cwd(), options.report);
|
|
@@ -196,6 +259,13 @@ export async function runCheck(options: CheckCommandOptions): Promise<void> {
|
|
|
196
259
|
console.log(JSON.stringify(payload, null, 2));
|
|
197
260
|
} else {
|
|
198
261
|
printCheckSummary(summary);
|
|
262
|
+
if (parserWarnings.length) {
|
|
263
|
+
console.log(chalk.yellow('\nParser warnings'));
|
|
264
|
+
parserWarnings.forEach((warning) => {
|
|
265
|
+
console.log(` • ${warning.message}`);
|
|
266
|
+
console.log(chalk.gray(` Install: ${warning.installHint}`));
|
|
267
|
+
});
|
|
268
|
+
}
|
|
199
269
|
if (options.diff) {
|
|
200
270
|
printLocaleDiffs(summary.sync.diffs);
|
|
201
271
|
}
|
|
@@ -240,3 +310,53 @@ export async function runCheck(options: CheckCommandOptions): Promise<void> {
|
|
|
240
310
|
throw new CliError(`Check failed: ${message}`);
|
|
241
311
|
}
|
|
242
312
|
}
|
|
313
|
+
|
|
314
|
+
type ParserStatusEntry = {
|
|
315
|
+
available: boolean;
|
|
316
|
+
required: boolean;
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
type ParserStatusMap = Record<string, ParserStatusEntry>;
|
|
320
|
+
|
|
321
|
+
function buildParserStatus(config: I18nConfig, projectRoot: string): ParserStatusMap {
|
|
322
|
+
const includePatterns = config.include ?? [];
|
|
323
|
+
const requiresVue = includesExtension(includePatterns, '.vue');
|
|
324
|
+
const requiresTypeScript =
|
|
325
|
+
includesExtension(includePatterns, '.ts') ||
|
|
326
|
+
includesExtension(includePatterns, '.tsx') ||
|
|
327
|
+
includesExtension(includePatterns, '.js') ||
|
|
328
|
+
includesExtension(includePatterns, '.jsx');
|
|
329
|
+
|
|
330
|
+
let vueAvailable = false;
|
|
331
|
+
try {
|
|
332
|
+
vueAvailable = isPackageResolvable('vue-eslint-parser', projectRoot);
|
|
333
|
+
} catch {
|
|
334
|
+
vueAvailable = false;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
typescript: { available: true, required: requiresTypeScript },
|
|
339
|
+
vue: { available: vueAvailable, required: requiresVue },
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function includesExtension(patterns: string[], extension: string): boolean {
|
|
344
|
+
const ext = extension.toLowerCase();
|
|
345
|
+
const extToken = ext.startsWith('.') ? ext.slice(1) : ext;
|
|
346
|
+
const extRegex = new RegExp(`\\.${extToken}(?:\\b|\\}|,|$)`, 'i');
|
|
347
|
+
|
|
348
|
+
return patterns.some((pattern) => {
|
|
349
|
+
const normalized = pattern.toLowerCase();
|
|
350
|
+
if (normalized.includes(ext)) {
|
|
351
|
+
return true;
|
|
352
|
+
}
|
|
353
|
+
const braceMatch = normalized.match(/\{([^}]+)\}/);
|
|
354
|
+
if (braceMatch) {
|
|
355
|
+
const entries = braceMatch[1].split(',').map((value) => value.trim());
|
|
356
|
+
if (entries.includes(extToken)) {
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return extRegex.test(normalized);
|
|
361
|
+
});
|
|
362
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import type { Command } from 'commander';
|
|
5
|
+
import { loadConfigWithMeta, Syncer } from '@i18nsmith/core';
|
|
6
|
+
import type { DynamicKeyCoverage, I18nConfig } from '@i18nsmith/core';
|
|
7
|
+
import { CliError, withErrorHandling } from '../utils/errors.js';
|
|
8
|
+
|
|
9
|
+
interface CoverageCommandOptions {
|
|
10
|
+
config?: string;
|
|
11
|
+
report?: string;
|
|
12
|
+
json?: boolean;
|
|
13
|
+
target?: string[];
|
|
14
|
+
invalidateCache?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const collectTargetPatterns = (value: string | string[], previous: string[]) => {
|
|
18
|
+
const list = Array.isArray(value) ? value : [value];
|
|
19
|
+
const tokens = list
|
|
20
|
+
.flatMap((entry) => entry.split(','))
|
|
21
|
+
.map((token) => token.trim())
|
|
22
|
+
.filter(Boolean);
|
|
23
|
+
return [...previous, ...tokens];
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function buildCoverageSummary(coverage: DynamicKeyCoverage[]): {
|
|
27
|
+
patterns: number;
|
|
28
|
+
missing: number;
|
|
29
|
+
} {
|
|
30
|
+
const entriesWithGaps = coverage.filter((entry) => Object.keys(entry.missingByLocale ?? {}).length > 0);
|
|
31
|
+
const missing = entriesWithGaps.reduce((total, entry) => {
|
|
32
|
+
return (
|
|
33
|
+
total +
|
|
34
|
+
Object.values(entry.missingByLocale ?? {}).reduce((sum, list) => sum + (Array.isArray(list) ? list.length : 0), 0)
|
|
35
|
+
);
|
|
36
|
+
}, 0);
|
|
37
|
+
return { patterns: entriesWithGaps.length, missing };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function buildCoverageReport(
|
|
41
|
+
coverage: DynamicKeyCoverage[],
|
|
42
|
+
config: I18nConfig,
|
|
43
|
+
projectRoot: string,
|
|
44
|
+
configPath: string
|
|
45
|
+
) {
|
|
46
|
+
const summary = buildCoverageSummary(coverage);
|
|
47
|
+
return {
|
|
48
|
+
generatedAt: new Date().toISOString(),
|
|
49
|
+
projectRoot,
|
|
50
|
+
configPath,
|
|
51
|
+
sourceLanguage: config.sourceLanguage ?? 'en',
|
|
52
|
+
targetLanguages: config.targetLanguages ?? [],
|
|
53
|
+
summary,
|
|
54
|
+
coverage,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function registerCoverage(program: Command) {
|
|
59
|
+
program
|
|
60
|
+
.command('coverage')
|
|
61
|
+
.description('Export dynamic key coverage report')
|
|
62
|
+
.option('-c, --config <path>', 'Path to i18nsmith config file', 'i18n.config.json')
|
|
63
|
+
.option('--report <path>', 'Write coverage report JSON to a file', '.i18nsmith/dynamic-key-coverage.json')
|
|
64
|
+
.option('--json', 'Print report JSON to stdout', false)
|
|
65
|
+
.option('--target <pattern...>', 'Limit reference scanning to specific files or patterns', collectTargetPatterns, [])
|
|
66
|
+
.option('--invalidate-cache', 'Ignore cached sync analysis and rescan all source files', false)
|
|
67
|
+
.action(withErrorHandling(async (options: CoverageCommandOptions) => runCoverage(options)));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function runCoverage(options: CoverageCommandOptions): Promise<void> {
|
|
71
|
+
const { config, projectRoot, configPath } = await loadConfigWithMeta(options.config);
|
|
72
|
+
const syncer = new Syncer(config, { workspaceRoot: projectRoot });
|
|
73
|
+
|
|
74
|
+
const summary = await syncer.run({
|
|
75
|
+
write: false,
|
|
76
|
+
targets: options.target,
|
|
77
|
+
invalidateCache: options.invalidateCache,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const coverage = summary.dynamicKeyCoverage ?? [];
|
|
81
|
+
const report = buildCoverageReport(coverage, config, projectRoot, configPath);
|
|
82
|
+
|
|
83
|
+
if (options.report) {
|
|
84
|
+
const outputPath = path.resolve(process.cwd(), options.report);
|
|
85
|
+
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
86
|
+
await fs.writeFile(outputPath, JSON.stringify(report, null, 2));
|
|
87
|
+
console.log(chalk.green(`Dynamic key coverage report written to ${outputPath}`));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (options.json) {
|
|
91
|
+
console.log(JSON.stringify(report, null, 2));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!options.report && !options.json) {
|
|
95
|
+
throw new CliError('No output specified. Use --report to write a file or --json to print to stdout.');
|
|
96
|
+
}
|
|
97
|
+
}
|
package/src/commands/init.ts
CHANGED
|
@@ -3,7 +3,7 @@ import inquirer from 'inquirer';
|
|
|
3
3
|
import fs from 'fs/promises';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import chalk from 'chalk';
|
|
6
|
-
import { diagnoseWorkspace, I18nConfig, TranslationConfig, ensureGitignore, ProjectIntelligenceService, type ProjectIntelligence, type SuggestedConfig, Scanner, KeyGenerator } from '@i18nsmith/core';
|
|
6
|
+
import { diagnoseWorkspace, I18nConfig, TranslationConfig, ensureGitignore, ProjectIntelligenceService, type ProjectIntelligence, type SuggestedConfig, Scanner, KeyGenerator, LocaleStore, createBackup } from '@i18nsmith/core';
|
|
7
7
|
import { scaffoldTranslationContext, scaffoldI18next } from '../utils/scaffold.js';
|
|
8
8
|
import { hasDependency, readPackageJson } from '../utils/pkg.js';
|
|
9
9
|
import { detectPackageManager, installDependencies } from '../utils/package-manager.js';
|
|
@@ -651,6 +651,10 @@ export function registerInit(program: Command) {
|
|
|
651
651
|
return;
|
|
652
652
|
}
|
|
653
653
|
|
|
654
|
+
if (mergeDecision?.strategy) {
|
|
655
|
+
config.mergeStrategy = mergeDecision.strategy;
|
|
656
|
+
}
|
|
657
|
+
|
|
654
658
|
const configPath = path.join(workspaceRoot, 'i18n.config.json');
|
|
655
659
|
|
|
656
660
|
try {
|
|
@@ -679,6 +683,8 @@ export function registerInit(program: Command) {
|
|
|
679
683
|
console.log(chalk.yellow('Could not create source locale file automatically.'));
|
|
680
684
|
}
|
|
681
685
|
|
|
686
|
+
await applyMergeStrategy(mergeDecision, config, workspaceRoot);
|
|
687
|
+
|
|
682
688
|
if (answers.scaffoldAdapter && answers.scaffoldAdapterPath) {
|
|
683
689
|
try {
|
|
684
690
|
await scaffoldTranslationContext(answers.scaffoldAdapterPath, answers.sourceLanguage, {
|
|
@@ -816,3 +822,90 @@ async function maybePromptMergeStrategy(
|
|
|
816
822
|
}
|
|
817
823
|
}
|
|
818
824
|
|
|
825
|
+
async function applyMergeStrategy(
|
|
826
|
+
mergeDecision: MergeDecision | null,
|
|
827
|
+
config: I18nConfig,
|
|
828
|
+
workspaceRoot: string
|
|
829
|
+
): Promise<void> {
|
|
830
|
+
const strategy = mergeDecision?.strategy;
|
|
831
|
+
if (!strategy || strategy === 'keep-source') {
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
const localesDirPath = path.join(workspaceRoot, config.localesDir || 'locales');
|
|
836
|
+
const localeStore = new LocaleStore(localesDirPath, {
|
|
837
|
+
format: config.locales?.format ?? 'auto',
|
|
838
|
+
delimiter: config.locales?.delimiter ?? '.',
|
|
839
|
+
sortKeys: config.locales?.sortKeys ?? 'alphabetical',
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
const sourceLocale = config.sourceLanguage ?? 'en';
|
|
843
|
+
let sourceData: Record<string, string> = {};
|
|
844
|
+
try {
|
|
845
|
+
sourceData = await localeStore.get(sourceLocale);
|
|
846
|
+
} catch {
|
|
847
|
+
sourceData = {};
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
const sourceKeys = Object.keys(sourceData);
|
|
851
|
+
if (!sourceKeys.length) {
|
|
852
|
+
console.log(chalk.yellow('No source locale keys found to apply merge strategy.'));
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
const targetLocales = (config.targetLanguages ?? []).filter(Boolean);
|
|
857
|
+
if (!targetLocales.length) {
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
let localesToOverwrite = targetLocales;
|
|
862
|
+
if (strategy === 'interactive' && process.stdout.isTTY) {
|
|
863
|
+
const { locales } = await inquirer.prompt<{ locales: string[] }>([
|
|
864
|
+
{
|
|
865
|
+
type: 'checkbox',
|
|
866
|
+
name: 'locales',
|
|
867
|
+
message: 'Select target locales to overwrite with placeholders',
|
|
868
|
+
choices: targetLocales,
|
|
869
|
+
default: targetLocales,
|
|
870
|
+
},
|
|
871
|
+
]);
|
|
872
|
+
localesToOverwrite = locales?.length ? locales : [];
|
|
873
|
+
if (!localesToOverwrite.length) {
|
|
874
|
+
console.log(chalk.gray('No locales selected. Skipping merge overwrite.'));
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
try {
|
|
880
|
+
const backup = await createBackup(localesDirPath, workspaceRoot, {}, `init --merge ${strategy}`);
|
|
881
|
+
if (backup) {
|
|
882
|
+
console.log(chalk.blue(`\n📦 ${backup.summary}`));
|
|
883
|
+
}
|
|
884
|
+
} catch (error) {
|
|
885
|
+
console.log(chalk.yellow(`Could not create backup: ${(error as Error).message}`));
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
const seedValue = config.sync?.seedValue ?? '[TODO]';
|
|
889
|
+
for (const locale of localesToOverwrite) {
|
|
890
|
+
let existingData: Record<string, string> = {};
|
|
891
|
+
try {
|
|
892
|
+
existingData = await localeStore.get(locale);
|
|
893
|
+
} catch {
|
|
894
|
+
existingData = {};
|
|
895
|
+
}
|
|
896
|
+
for (const key of Object.keys(existingData)) {
|
|
897
|
+
await localeStore.remove(locale, key);
|
|
898
|
+
}
|
|
899
|
+
for (const key of sourceKeys) {
|
|
900
|
+
await localeStore.upsert(locale, key, seedValue);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
await localeStore.flush();
|
|
905
|
+
console.log(
|
|
906
|
+
chalk.green(
|
|
907
|
+
`Merge strategy "${strategy}" applied to locales: ${localesToOverwrite.join(', ')}`
|
|
908
|
+
)
|
|
909
|
+
);
|
|
910
|
+
}
|
|
911
|
+
|
package/src/commands/sync.ts
CHANGED
|
@@ -350,6 +350,11 @@ export function registerSync(program: Command) {
|
|
|
350
350
|
if (options.exclude?.length) {
|
|
351
351
|
config.exclude = options.exclude;
|
|
352
352
|
}
|
|
353
|
+
|
|
354
|
+
if (!options.interactive && config.mergeStrategy === 'interactive') {
|
|
355
|
+
options.interactive = true;
|
|
356
|
+
console.log(chalk.gray('Using interactive merge strategy from i18n.config.json.'));
|
|
357
|
+
}
|
|
353
358
|
// Merge --assume-globs with config
|
|
354
359
|
if (options.assumeGlobs?.length) {
|
|
355
360
|
config.sync = config.sync ?? {};
|
package/src/e2e.test.ts
CHANGED
|
@@ -284,11 +284,6 @@ describe('E2E Fixture Tests', () => {
|
|
|
284
284
|
const saveKey = renameMap['Save'];
|
|
285
285
|
expect(saveKey).toBeDefined();
|
|
286
286
|
|
|
287
|
-
expect(enLocale).toHaveProperty(helloKey!);
|
|
288
|
-
expect(frLocale).toHaveProperty(helloKey!);
|
|
289
|
-
expect(enLocale).toHaveProperty(saveKey!);
|
|
290
|
-
expect(frLocale).toHaveProperty(saveKey!);
|
|
291
|
-
|
|
292
287
|
const sourceFile = await fs.readFile(path.join(fixtureDir, 'src', 'BadKeys.tsx'), 'utf8');
|
|
293
288
|
expect(sourceFile).toContain(`t('${helloKey!}')`);
|
|
294
289
|
expect(sourceFile).not.toContain("t('Hello World')");
|
|
@@ -457,6 +452,63 @@ describe('E2E Fixture Tests', () => {
|
|
|
457
452
|
});
|
|
458
453
|
});
|
|
459
454
|
|
|
455
|
+
describe('dynamic key coverage reporting', () => {
|
|
456
|
+
let fixtureDir: string;
|
|
457
|
+
|
|
458
|
+
beforeEach(async () => {
|
|
459
|
+
fixtureDir = await setupFixture('basic-react');
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
afterEach(async () => {
|
|
463
|
+
await cleanupFixture(fixtureDir);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it('includes dynamic key coverage details in check JSON', async () => {
|
|
467
|
+
const configPath = path.join(fixtureDir, 'i18n.config.json');
|
|
468
|
+
const config = JSON.parse(await fs.readFile(configPath, 'utf8'));
|
|
469
|
+
config.dynamicKeys = {
|
|
470
|
+
expand: {
|
|
471
|
+
'workingHours.*': ['monday', 'tuesday'],
|
|
472
|
+
},
|
|
473
|
+
};
|
|
474
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2));
|
|
475
|
+
|
|
476
|
+
const enPath = path.join(fixtureDir, 'locales', 'en.json');
|
|
477
|
+
const enLocale = JSON.parse(await fs.readFile(enPath, 'utf8'));
|
|
478
|
+
enLocale['workingHours.monday'] = 'Monday';
|
|
479
|
+
await fs.writeFile(enPath, JSON.stringify(enLocale, null, 2));
|
|
480
|
+
|
|
481
|
+
const result = runCli(['check', '--json'], { cwd: fixtureDir });
|
|
482
|
+
const parsed = extractJson<{ sync: { dynamicKeyCoverage: Array<{ pattern: string; missingByLocale: Record<string, string[]> }> } }>(result.stdout);
|
|
483
|
+
|
|
484
|
+
const coverage = parsed.sync.dynamicKeyCoverage.find((entry) => entry.pattern === 'workingHours.*');
|
|
485
|
+
expect(coverage).toBeDefined();
|
|
486
|
+
expect(coverage!.missingByLocale.en).toEqual(['workingHours.tuesday']);
|
|
487
|
+
expect(coverage!.missingByLocale.fr).toContain('workingHours.monday');
|
|
488
|
+
expect(coverage!.missingByLocale.de).toContain('workingHours.monday');
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it('writes a dynamic key coverage report file', async () => {
|
|
492
|
+
const configPath = path.join(fixtureDir, 'i18n.config.json');
|
|
493
|
+
const config = JSON.parse(await fs.readFile(configPath, 'utf8'));
|
|
494
|
+
config.dynamicKeys = {
|
|
495
|
+
expand: {
|
|
496
|
+
'workingHours.*': ['monday', 'tuesday'],
|
|
497
|
+
},
|
|
498
|
+
};
|
|
499
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2));
|
|
500
|
+
|
|
501
|
+
const reportPath = path.join(fixtureDir, 'coverage-report.json');
|
|
502
|
+
const result = runCli(['coverage', '--report', reportPath], { cwd: fixtureDir });
|
|
503
|
+
expect(result.exitCode).toBe(0);
|
|
504
|
+
|
|
505
|
+
const payload = JSON.parse(await fs.readFile(reportPath, 'utf8')) as {
|
|
506
|
+
summary: { patterns: number; missing: number };
|
|
507
|
+
};
|
|
508
|
+
expect(payload.summary.patterns).toBeGreaterThan(0);
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
|
|
460
512
|
describe('--invalidate-cache flag', () => {
|
|
461
513
|
let fixtureDir: string;
|
|
462
514
|
|
package/src/index.ts
CHANGED
|
@@ -17,13 +17,14 @@ import { registerInstallHooks } from './commands/install-hooks.js';
|
|
|
17
17
|
import { registerConfig } from './commands/config.js';
|
|
18
18
|
import { registerReview } from './commands/review.js';
|
|
19
19
|
import { registerDetect } from './commands/detect.js';
|
|
20
|
+
import { registerCoverage } from './commands/coverage.js';
|
|
20
21
|
|
|
21
22
|
export const program = new Command();
|
|
22
23
|
|
|
23
24
|
program
|
|
24
25
|
.name('i18nsmith')
|
|
25
26
|
.description('Universal Automated i18n Library')
|
|
26
|
-
.version('0.4.
|
|
27
|
+
.version('0.4.3');
|
|
27
28
|
|
|
28
29
|
registerInit(program);
|
|
29
30
|
registerScaffoldAdapter(program);
|
|
@@ -42,6 +43,7 @@ registerInstallHooks(program);
|
|
|
42
43
|
registerConfig(program);
|
|
43
44
|
registerReview(program);
|
|
44
45
|
registerDetect(program);
|
|
46
|
+
registerCoverage(program);
|
|
45
47
|
|
|
46
48
|
|
|
47
49
|
program.parse();
|
|
@@ -92,8 +92,8 @@ describe('Rename Suspicious Keys E2E', () => {
|
|
|
92
92
|
expect(renameDiff).toBeDefined();
|
|
93
93
|
// console.log('Actual diff:', renameDiff.diff);
|
|
94
94
|
expect(renameDiff.diff).toContain('- <h1>{t(\'Hello World\')}</h1>');
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
// Key generation preserves namespace and omits hash suffixes
|
|
96
|
+
expect(renameDiff.diff).toMatch(/\+ <h1>{t\('common\.hello-world'\)}<\/h1>/);
|
|
97
97
|
});
|
|
98
98
|
|
|
99
99
|
it('should handle existing keys that are suspicious (key-equals-value or contains-spaces)', async () => {
|