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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAmBpC,eAAO,MAAM,OAAO,SAAgB,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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i18nsmith",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "CLI for i18nsmith",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -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 ? { ...summary, audit: localeAudit } : summary;
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
+ }
@@ -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
+
@@ -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.2');
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
- // Key generation includes hash and file slug
96
- expect(renameDiff.diff).toMatch(/\+ <h1>{t\('common\.badkeys\.hello-world\.[a-f0-9]+'\)}<\/h1>/);
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 () => {