i18nsmith 0.3.3 → 0.4.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/build.mjs +1 -1
- package/dist/commands/detect.d.ts +3 -0
- package/dist/commands/detect.d.ts.map +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/rename.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/commands/translate/csv-handler.d.ts.map +1 -1
- package/dist/index.cjs +47711 -42734
- package/dist/index.d.ts.map +1 -1
- package/dist/test-helpers/ensure-cli-built.d.ts.map +1 -1
- package/dist/utils/adapter-preflight.d.ts +10 -0
- package/dist/utils/adapter-preflight.d.ts.map +1 -0
- package/i18n.config.json +14 -0
- package/package.json +4 -2
- package/src/commands/detect.ts +342 -0
- package/src/commands/init.test.ts +208 -1
- package/src/commands/init.ts +472 -195
- package/src/commands/rename.ts +13 -0
- package/src/commands/review.ts +1 -1
- package/src/commands/scan.ts +4 -1
- package/src/commands/sync.ts +23 -3
- package/src/commands/transform.ts +54 -2
- package/src/commands/translate/csv-handler.ts +2 -1
- package/src/e2e.test.ts +4 -4
- package/src/fixtures/suspicious-keys/locales/en.json +8 -8
- package/src/fixtures/suspicious-keys/locales/fr.json +8 -8
- package/src/fixtures/suspicious-keys/preview.json +419 -0
- package/src/fixtures/suspicious-keys/src/BadKeys.tsx.backup +19 -0
- package/src/index.ts +3 -1
- package/src/integration.test.ts +2 -6
- package/src/rename-suspicious.test.ts +3 -3
- package/src/test-helpers/ensure-cli-built.ts +18 -0
- package/src/utils/adapter-preflight.ts +53 -0
- package/test.vue +33 -0
package/src/commands/rename.ts
CHANGED
|
@@ -4,6 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import { promises as fs } from 'node:fs';
|
|
5
5
|
import { loadConfig, KeyRenamer, type KeyRenameSummary, type KeyRenameBatchSummary, type KeyRenameMapping } from '@i18nsmith/core';
|
|
6
6
|
import { applyPreviewFile, writePreviewFile } from '../utils/preview.js';
|
|
7
|
+
import { runAdapterPreflight } from '../utils/adapter-preflight.js';
|
|
7
8
|
import { CliError, withErrorHandling } from '../utils/errors.js';
|
|
8
9
|
|
|
9
10
|
interface ScanOptions {
|
|
@@ -59,6 +60,12 @@ export function registerRename(program: Command): void {
|
|
|
59
60
|
|
|
60
61
|
try {
|
|
61
62
|
const config = await loadConfig(options.config);
|
|
63
|
+
|
|
64
|
+
// Run preflight checks for write operations
|
|
65
|
+
if (writeEnabled) {
|
|
66
|
+
await runAdapterPreflight();
|
|
67
|
+
}
|
|
68
|
+
|
|
62
69
|
const renamer = new KeyRenamer(config);
|
|
63
70
|
const summary = await renamer.rename(oldKey, newKey, {
|
|
64
71
|
write: options.write,
|
|
@@ -113,6 +120,12 @@ export function registerRename(program: Command): void {
|
|
|
113
120
|
|
|
114
121
|
try {
|
|
115
122
|
const config = await loadConfig(options.config);
|
|
123
|
+
|
|
124
|
+
// Run preflight checks for write operations
|
|
125
|
+
if (options.write) {
|
|
126
|
+
await runAdapterPreflight();
|
|
127
|
+
}
|
|
128
|
+
|
|
116
129
|
const mappings = await loadRenameMappings(options.map);
|
|
117
130
|
const renamer = new KeyRenamer(config);
|
|
118
131
|
const summary = await renamer.renameBatch(mappings, { write: options.write, diff: options.diff });
|
package/src/commands/review.ts
CHANGED
|
@@ -138,7 +138,7 @@ export function registerReview(program: Command) {
|
|
|
138
138
|
withErrorHandling(async (options: ReviewCommandOptions) => {
|
|
139
139
|
try {
|
|
140
140
|
const { config, projectRoot, configPath } = await loadConfigWithMeta(options.config);
|
|
141
|
-
const scanner =
|
|
141
|
+
const scanner = await Scanner.create(config, { workspaceRoot: projectRoot });
|
|
142
142
|
const summary = scanner.scan({ scanCalls: options.scanCalls }) as BucketedScanSummary;
|
|
143
143
|
const buckets = summary.buckets ?? {};
|
|
144
144
|
const needsReview = buckets.needsReview ?? [];
|
package/src/commands/scan.ts
CHANGED
|
@@ -75,7 +75,10 @@ export function registerScan(program: Command) {
|
|
|
75
75
|
if (options.exclude?.length) {
|
|
76
76
|
config.exclude = options.exclude;
|
|
77
77
|
}
|
|
78
|
-
|
|
78
|
+
// Use factory that registers framework adapters (React/Vue) so
|
|
79
|
+
// scans include files handled by adapters. Backwards compatible
|
|
80
|
+
// API: Scanner.create will return a scanner with adapters wired.
|
|
81
|
+
const scanner = await Scanner.create(config, { workspaceRoot: projectRoot });
|
|
79
82
|
const summary = scanner.scan();
|
|
80
83
|
|
|
81
84
|
if (options.report) {
|
package/src/commands/sync.ts
CHANGED
|
@@ -554,12 +554,15 @@ export function registerSync(program: Command) {
|
|
|
554
554
|
|
|
555
555
|
// Handle --auto-rename-suspicious
|
|
556
556
|
if (options.autoRenameSuspicious && summary.suspiciousKeys.length > 0) {
|
|
557
|
-
await handleAutoRenameSuspicious(
|
|
557
|
+
const renameDiffs = await handleAutoRenameSuspicious(
|
|
558
558
|
summary,
|
|
559
559
|
options,
|
|
560
560
|
config,
|
|
561
561
|
projectRoot
|
|
562
562
|
);
|
|
563
|
+
if (renameDiffs && renameDiffs.length > 0) {
|
|
564
|
+
summary.renameDiffs = renameDiffs;
|
|
565
|
+
}
|
|
563
566
|
}
|
|
564
567
|
|
|
565
568
|
// Handle --rewrite-shape
|
|
@@ -827,7 +830,7 @@ function printSyncSummary(summary: SyncSummary) {
|
|
|
827
830
|
} else {
|
|
828
831
|
console.log(
|
|
829
832
|
chalk.gray(
|
|
830
|
-
"Use --assume
|
|
833
|
+
"Use --assume to prevent false positives for known runtime-only translation keys."
|
|
831
834
|
)
|
|
832
835
|
);
|
|
833
836
|
}
|
|
@@ -857,7 +860,7 @@ async function handleAutoRenameSuspicious(
|
|
|
857
860
|
options: SyncCommandOptions,
|
|
858
861
|
config: Awaited<ReturnType<typeof loadConfig>>,
|
|
859
862
|
projectRoot: string
|
|
860
|
-
) {
|
|
863
|
+
): Promise<any[] | undefined> {
|
|
861
864
|
console.log(chalk.blue("\n📝 Auto-rename suspicious keys analysis:"));
|
|
862
865
|
|
|
863
866
|
// Get existing keys from locale data to check for conflicts
|
|
@@ -1008,6 +1011,23 @@ async function handleAutoRenameSuspicious(
|
|
|
1008
1011
|
chalk.gray(" Run with --write to apply safe proposals automatically.")
|
|
1009
1012
|
);
|
|
1010
1013
|
}
|
|
1014
|
+
|
|
1015
|
+
// Generate diffs for preview mode
|
|
1016
|
+
if (options.diff && report.safeProposals.length > 0) {
|
|
1017
|
+
const mappings = report.safeProposals.map((proposal) => ({
|
|
1018
|
+
from: proposal.originalKey,
|
|
1019
|
+
to: proposal.proposedKey,
|
|
1020
|
+
}));
|
|
1021
|
+
|
|
1022
|
+
const renamer = new KeyRenamer(config, { workspaceRoot: projectRoot });
|
|
1023
|
+
const diffSummary = await renamer.renameBatch(mappings, {
|
|
1024
|
+
write: false,
|
|
1025
|
+
diff: true,
|
|
1026
|
+
allowConflicts: true,
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
return diffSummary.diffs;
|
|
1030
|
+
}
|
|
1011
1031
|
}
|
|
1012
1032
|
|
|
1013
1033
|
async function handleRewriteShape(
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { createRequire } from 'module';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
3
5
|
import chalk from 'chalk';
|
|
4
6
|
import type { Command } from 'commander';
|
|
5
7
|
import { loadConfigWithMeta } from '@i18nsmith/core';
|
|
@@ -8,6 +10,8 @@ import type { TransformProgress, TransformSummary } from '@i18nsmith/transformer
|
|
|
8
10
|
import { printLocaleDiffs, writeLocaleDiffPatches } from '../utils/diff-utils.js';
|
|
9
11
|
import { applyPreviewFile, writePreviewFile } from '../utils/preview.js';
|
|
10
12
|
import { CliError, withErrorHandling } from '../utils/errors.js';
|
|
13
|
+
import inquirer from 'inquirer';
|
|
14
|
+
import { detectPackageManager, installDependencies } from '../utils/package-manager.js';
|
|
11
15
|
|
|
12
16
|
interface TransformOptions {
|
|
13
17
|
config?: string;
|
|
@@ -32,6 +36,7 @@ const collectTargetPatterns = (value: string | string[], previous: string[]) =>
|
|
|
32
36
|
return [...previous, ...tokens];
|
|
33
37
|
};
|
|
34
38
|
|
|
39
|
+
/* Re-enabled after framework migration stabilization */
|
|
35
40
|
function printTransformSummary(summary: TransformSummary) {
|
|
36
41
|
const counts = summary.candidates.reduce(
|
|
37
42
|
(acc, c) => {
|
|
@@ -68,7 +73,8 @@ function printTransformSummary(summary: TransformSummary) {
|
|
|
68
73
|
|
|
69
74
|
console.table(preview);
|
|
70
75
|
|
|
71
|
-
const pending = summary.
|
|
76
|
+
const pending = summary.candidateStats?.pending
|
|
77
|
+
?? summary.candidates.filter((candidate) => candidate.status === 'pending').length;
|
|
72
78
|
if (pending > 0) {
|
|
73
79
|
console.log(
|
|
74
80
|
chalk.yellow(
|
|
@@ -97,6 +103,13 @@ function printTransformSummary(summary: TransformSummary) {
|
|
|
97
103
|
console.log(chalk.yellow('Skipped items:'));
|
|
98
104
|
summary.skippedFiles.forEach((item) => console.log(` • ${item.filePath}: ${item.reason}`));
|
|
99
105
|
}
|
|
106
|
+
|
|
107
|
+
if (summary.skippedReasons && Object.keys(summary.skippedReasons).length) {
|
|
108
|
+
console.log(chalk.yellow('Skipped reasons:'));
|
|
109
|
+
(Object.entries(summary.skippedReasons) as [string, number][])
|
|
110
|
+
.sort((a, b) => b[1] - a[1])
|
|
111
|
+
.forEach(([reason, count]) => console.log(` • ${reason}: ${count}`));
|
|
112
|
+
}
|
|
100
113
|
}
|
|
101
114
|
|
|
102
115
|
function createProgressLogger() {
|
|
@@ -194,7 +207,7 @@ export function registerTransform(program: Command) {
|
|
|
194
207
|
.option('--apply-preview <path>', 'Apply a previously saved transform preview JSON file safely')
|
|
195
208
|
.action(
|
|
196
209
|
withErrorHandling(async (options: TransformOptions) => {
|
|
197
|
-
|
|
210
|
+
if (options.applyPreview) {
|
|
198
211
|
await applyPreviewFile('transform', options.applyPreview);
|
|
199
212
|
return;
|
|
200
213
|
}
|
|
@@ -225,6 +238,45 @@ export function registerTransform(program: Command) {
|
|
|
225
238
|
console.log(chalk.gray(`Config found at ${path.relative(cwd, configPath)}`));
|
|
226
239
|
console.log(chalk.gray(`Using project root: ${projectRoot}\n`));
|
|
227
240
|
}
|
|
241
|
+
|
|
242
|
+
// Proactively check if Vue files are targeted but the parser is missing
|
|
243
|
+
const includesVue = config.include?.some(pattern => pattern.includes('.vue')) ?? false;
|
|
244
|
+
let isVueParserAvailable = false;
|
|
245
|
+
try {
|
|
246
|
+
const require = createRequire(import.meta.url);
|
|
247
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
248
|
+
require.resolve('vue-eslint-parser', { paths: [projectRoot, moduleDir] });
|
|
249
|
+
isVueParserAvailable = true;
|
|
250
|
+
} catch {
|
|
251
|
+
isVueParserAvailable = false;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (includesVue && !isVueParserAvailable) {
|
|
255
|
+
console.log(chalk.yellow('⚠️ Vue files detected but "vue-eslint-parser" is not installed.'));
|
|
256
|
+
console.log(chalk.yellow(' Verification of Vue templates might be incomplete or fail.'));
|
|
257
|
+
|
|
258
|
+
if (process.stdout.isTTY) {
|
|
259
|
+
const { install } = await inquirer.prompt<{ install: boolean }>([
|
|
260
|
+
{
|
|
261
|
+
type: 'confirm',
|
|
262
|
+
name: 'install',
|
|
263
|
+
message: 'Do you want to install "vue-eslint-parser" (dev dependency) now?',
|
|
264
|
+
default: true,
|
|
265
|
+
},
|
|
266
|
+
]);
|
|
267
|
+
|
|
268
|
+
if (install) {
|
|
269
|
+
const pm = await detectPackageManager(projectRoot);
|
|
270
|
+
const cmd = pm === 'npm' ? 'npm install --save-dev vue-eslint-parser' : `${pm} add -D vue-eslint-parser`;
|
|
271
|
+
console.log(chalk.gray(`> ${cmd}`));
|
|
272
|
+
// We use installDependencies helper but need to pass dev flag args if not flexible
|
|
273
|
+
// The helper seems simple: const args = manager === 'npm' ? ['install', ...deps] : ['add', ...deps];
|
|
274
|
+
// So for dev we need to include -D in deps
|
|
275
|
+
await installDependencies(pm, ['-D', 'vue-eslint-parser'], projectRoot);
|
|
276
|
+
console.log(chalk.green('✔ Installed. Continuing...'));
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
228
280
|
|
|
229
281
|
const transformer = new Transformer(config, { workspaceRoot: projectRoot });
|
|
230
282
|
const progressLogger = createProgressLogger();
|
|
@@ -78,6 +78,8 @@ export async function handleCsvExport(options: TranslateCommandOptions): Promise
|
|
|
78
78
|
const plan = await translationService.buildPlan({
|
|
79
79
|
locales: options.locales,
|
|
80
80
|
force: options.force,
|
|
81
|
+
// Export should represent what the UI calls "missing" (usually includes empty strings)
|
|
82
|
+
treatEmptyAsMissing: true,
|
|
81
83
|
});
|
|
82
84
|
|
|
83
85
|
if (!plan.totalTasks) {
|
|
@@ -165,7 +167,6 @@ export async function handleCsvImport(options: TranslateCommandOptions): Promise
|
|
|
165
167
|
// Parse header
|
|
166
168
|
const headerFields = parseCsvLine(lines[0]);
|
|
167
169
|
const keyIdx = headerFields.indexOf('key');
|
|
168
|
-
const sourceLocaleIdx = headerFields.indexOf('sourceLocale');
|
|
169
170
|
const sourceValueIdx = headerFields.indexOf('sourceValue');
|
|
170
171
|
const targetLocaleIdx = headerFields.indexOf('targetLocale');
|
|
171
172
|
const translatedValueIdx = headerFields.indexOf('translatedValue');
|
package/src/e2e.test.ts
CHANGED
|
@@ -281,13 +281,13 @@ describe('E2E Fixture Tests', () => {
|
|
|
281
281
|
const renameMap = parseRenameMap(mapContents);
|
|
282
282
|
const helloKey = renameMap['Hello World'];
|
|
283
283
|
expect(helloKey).toBeDefined();
|
|
284
|
-
const
|
|
285
|
-
expect(
|
|
284
|
+
const saveKey = renameMap['Save'];
|
|
285
|
+
expect(saveKey).toBeDefined();
|
|
286
286
|
|
|
287
287
|
expect(enLocale).toHaveProperty(helloKey!);
|
|
288
288
|
expect(frLocale).toHaveProperty(helloKey!);
|
|
289
|
-
expect(enLocale).toHaveProperty(
|
|
290
|
-
expect(frLocale).toHaveProperty(
|
|
289
|
+
expect(enLocale).toHaveProperty(saveKey!);
|
|
290
|
+
expect(frLocale).toHaveProperty(saveKey!);
|
|
291
291
|
|
|
292
292
|
const sourceFile = await fs.readFile(path.join(fixtureDir, 'src', 'BadKeys.tsx'), 'utf8');
|
|
293
293
|
expect(sourceFile).toContain(`t('${helloKey!}')`);
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"
|
|
2
|
+
"common.badkeys.buttons-submit.3b94a6": "Submit",
|
|
3
|
+
"common.badkeys.click-here.788c1e": "Click Here",
|
|
4
|
+
"common.badkeys.hello-world.66bf53": "Hello World",
|
|
5
|
+
"common.badkeys.save.a495bf": "Save",
|
|
6
|
+
"common.badkeys.welcome-to-our-app.a53bf0": "Welcome to our app!",
|
|
7
|
+
"common.title": "Application Title",
|
|
8
8
|
"proper.namespaced.key": "This is a properly namespaced key",
|
|
9
|
-
"
|
|
10
|
-
"
|
|
9
|
+
"The Quick Brown Fox": "The Quick Brown Fox",
|
|
10
|
+
"When to use this feature:": "When to use this feature:"
|
|
11
11
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"
|
|
2
|
+
"common.badkeys.buttons-submit.3b94a6": "Soumettre",
|
|
3
|
+
"common.badkeys.click-here.788c1e": "Cliquez ici",
|
|
4
|
+
"common.badkeys.hello-world.66bf53": "Bonjour le monde",
|
|
5
|
+
"common.badkeys.save.a495bf": "Sauvegarder",
|
|
6
|
+
"common.badkeys.welcome-to-our-app.a53bf0": "Bienvenue dans notre application!",
|
|
7
|
+
"common.title": "Titre de l'application",
|
|
8
8
|
"proper.namespaced.key": "Ceci est une clé correctement nommée",
|
|
9
|
-
"
|
|
10
|
-
"
|
|
9
|
+
"The Quick Brown Fox": "Le Renard Brun Rapide",
|
|
10
|
+
"When to use this feature:": "Quand utiliser cette fonctionnalité:"
|
|
11
11
|
}
|