@abdess76/i18nkit 1.0.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.
Files changed (48) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/LICENSE +21 -0
  3. package/README.md +309 -0
  4. package/bin/cli.js +48 -0
  5. package/bin/commands/apply.js +48 -0
  6. package/bin/commands/check-sync.js +35 -0
  7. package/bin/commands/extract-utils.js +216 -0
  8. package/bin/commands/extract.js +198 -0
  9. package/bin/commands/find-orphans.js +36 -0
  10. package/bin/commands/help.js +34 -0
  11. package/bin/commands/index.js +79 -0
  12. package/bin/commands/translate.js +51 -0
  13. package/bin/commands/version.js +17 -0
  14. package/bin/commands/watch.js +34 -0
  15. package/bin/core/applier-utils.js +144 -0
  16. package/bin/core/applier.js +165 -0
  17. package/bin/core/args.js +147 -0
  18. package/bin/core/backup.js +74 -0
  19. package/bin/core/command-interface.js +69 -0
  20. package/bin/core/config.js +108 -0
  21. package/bin/core/context.js +86 -0
  22. package/bin/core/detector.js +152 -0
  23. package/bin/core/file-walker.js +159 -0
  24. package/bin/core/fs-adapter.js +56 -0
  25. package/bin/core/help-generator.js +208 -0
  26. package/bin/core/index.js +63 -0
  27. package/bin/core/json-utils.js +213 -0
  28. package/bin/core/key-generator.js +75 -0
  29. package/bin/core/log-utils.js +26 -0
  30. package/bin/core/orphan-finder.js +208 -0
  31. package/bin/core/parser-utils.js +187 -0
  32. package/bin/core/paths.js +60 -0
  33. package/bin/core/plugin-interface.js +83 -0
  34. package/bin/core/plugin-resolver-utils.js +166 -0
  35. package/bin/core/plugin-resolver.js +211 -0
  36. package/bin/core/sync-checker-utils.js +99 -0
  37. package/bin/core/sync-checker.js +199 -0
  38. package/bin/core/translator.js +197 -0
  39. package/bin/core/types.js +297 -0
  40. package/bin/core/watcher.js +119 -0
  41. package/bin/plugins/adapter-transloco.js +156 -0
  42. package/bin/plugins/parser-angular.js +56 -0
  43. package/bin/plugins/parser-primeng.js +79 -0
  44. package/bin/plugins/parser-typescript.js +66 -0
  45. package/bin/plugins/provider-deepl.js +65 -0
  46. package/bin/plugins/provider-mymemory.js +192 -0
  47. package/package.json +123 -0
  48. package/types/index.d.ts +85 -0
@@ -0,0 +1,216 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @fileoverview Extract command utilities.
5
+ * File scanning, progress tracking, and output formatting.
6
+ * @module commands/extract-utils
7
+ */
8
+
9
+ const path = require('path');
10
+ const { getFileContent, setNestedValue, slugify, pathToScope } = require('../core');
11
+
12
+ const noop = () => undefined;
13
+
14
+ function createIntervalCallback(state) {
15
+ return () => {
16
+ state.dots = (state.dots + 1) % 4;
17
+ const dots = '.'.repeat(state.dots);
18
+ const spaces = ' '.repeat(3 - state.dots);
19
+ process.stdout.write(`\rScanning${dots}${spaces} ${state.scanned}/${state.total} files`);
20
+ };
21
+ }
22
+
23
+ function createProgressTracker(ctx) {
24
+ const { verbose, jsonOutput, totalFiles } = ctx;
25
+ const skipProgress = verbose || jsonOutput || !process.stdout.isTTY || totalFiles <= 50;
26
+ if (skipProgress) {
27
+ return { start: noop, stop: noop, increment: noop };
28
+ }
29
+
30
+ const state = { dots: 0, scanned: 0, total: totalFiles, interval: null };
31
+
32
+ return {
33
+ start: () => {
34
+ process.stdout.write('Scanning');
35
+ state.interval = setInterval(createIntervalCallback(state), 200);
36
+ },
37
+ stop: () => {
38
+ if (state.interval) {
39
+ clearInterval(state.interval);
40
+ process.stdout.write(`\r${' '.repeat(50)}\r`);
41
+ }
42
+ },
43
+ increment: () => {
44
+ state.scanned++;
45
+ },
46
+ };
47
+ }
48
+
49
+ function updateStats(stats, context, isNew) {
50
+ stats.total++;
51
+ if (isNew) {
52
+ stats.added++;
53
+ }
54
+ stats.byContext[context] = (stats.byContext[context] ?? 0) + 1;
55
+ }
56
+
57
+ function buildFinding(extraction, ctx) {
58
+ const { text, rawText, context, attr } = extraction;
59
+ const { relativePath, result, keyMapping, scope, stats } = ctx;
60
+
61
+ const keyPath = keyMapping[text]?.split('.') ?? [...scope, context, slugify(text)];
62
+ const { path: finalPath, isNew } = setNestedValue(result, keyPath, text);
63
+ const fullKey = finalPath.join('.');
64
+ const displayText = text.length > 80 ? `${text.substring(0, 77)}...` : text;
65
+
66
+ updateStats(stats, context, isNew);
67
+
68
+ return {
69
+ file: relativePath,
70
+ text,
71
+ rawText: rawText ?? text,
72
+ displayText,
73
+ context,
74
+ key: fullKey,
75
+ attr,
76
+ isNew,
77
+ };
78
+ }
79
+
80
+ function extractSingleSource(parser, source, extractCtx) {
81
+ const { options, seen, results } = extractCtx;
82
+ for (const r of parser.extract(source, '', options)) {
83
+ const key = `${r.context}:${r.text}`;
84
+ if (!seen.has(key)) {
85
+ seen.add(key);
86
+ results.push(r);
87
+ }
88
+ }
89
+ }
90
+
91
+ function extractFromContent(fileData, ctx) {
92
+ const { parsers, skipTranslated, extractTsObjects } = ctx;
93
+ const results = [];
94
+ const seen = new Set();
95
+ const sources = [fileData.template, fileData.typescript].filter(Boolean);
96
+ const extractCtx = { options: { skipTranslated, extractTsObjects }, seen, results };
97
+
98
+ for (const parser of parsers) {
99
+ for (const source of sources) {
100
+ extractSingleSource(parser, source, extractCtx);
101
+ }
102
+ }
103
+ return results;
104
+ }
105
+
106
+ function handleEmptyExtractions(stats) {
107
+ stats.clean++;
108
+ return [];
109
+ }
110
+
111
+ function mapExtractionsToFindings(extractions, filePath, ctx) {
112
+ const { srcDir, result, stats, keyMapping } = ctx;
113
+ stats.needsWork++;
114
+ const scope = pathToScope(filePath, srcDir);
115
+ const relativePath = path.relative(srcDir, filePath);
116
+ const findingCtx = { relativePath, result, keyMapping, scope, stats };
117
+ return extractions.map(e => buildFinding(e, findingCtx));
118
+ }
119
+
120
+ function processFile(filePath, ctx) {
121
+ const { processedTemplates, verbose, stats } = ctx;
122
+ const fileData = getFileContent(filePath, processedTemplates, verbose);
123
+ if (!fileData) {
124
+ return [];
125
+ }
126
+
127
+ stats.files++;
128
+ const extractions = extractFromContent(fileData, ctx);
129
+ if (extractions.length === 0) {
130
+ return handleEmptyExtractions(stats);
131
+ }
132
+
133
+ return mapExtractionsToFindings(extractions, filePath, ctx);
134
+ }
135
+
136
+ function scanFiles(files, ctx) {
137
+ const progress = createProgressTracker({ ...ctx, totalFiles: files.length });
138
+ progress.start();
139
+ const findings = files.flatMap(filePath => {
140
+ progress.increment();
141
+ return processFile(filePath, ctx);
142
+ });
143
+ progress.stop();
144
+ return findings;
145
+ }
146
+
147
+ function getFindingMarker(merge, isNew) {
148
+ if (!merge) {
149
+ return '-';
150
+ }
151
+ return isNew ? '+' : '=';
152
+ }
153
+
154
+ function formatFindingLines(finding, merge) {
155
+ return [
156
+ ` [${finding.context}] ${finding.key}`,
157
+ ` ${getFindingMarker(merge, finding.isNew)} "${finding.displayText}"`,
158
+ ];
159
+ }
160
+
161
+ function logFileGroup(file, fileFindings, ctx) {
162
+ ctx.log(`\n${file}`);
163
+ fileFindings.forEach(f => formatFindingLines(f, ctx.merge).forEach(l => ctx.log(l)));
164
+ }
165
+
166
+ function logVerboseFindings(findings, ctx) {
167
+ const findingsByFile = Object.groupBy(findings, f => f.file);
168
+ Object.entries(findingsByFile).forEach(([file, ff]) => logFileGroup(file, ff, ctx));
169
+ }
170
+
171
+ function logContextBreakdown(contexts, log) {
172
+ if (contexts.length === 0) {
173
+ return;
174
+ }
175
+ log('\nBy context:');
176
+ contexts.toSorted((a, b) => b[1] - a[1]).forEach(([k, v]) => log(` ${k}: ${v}`));
177
+ }
178
+
179
+ function logSummary(stats, ctx) {
180
+ const { merge, log } = ctx;
181
+ log('\nSummary');
182
+ log('-'.repeat(50));
183
+ log(`Files scanned: ${stats.files}`);
184
+ log(`Translated: ${stats.clean}`);
185
+ log(`Need translation: ${stats.needsWork}`);
186
+ log(`Strings found: ${stats.total}`);
187
+ if (merge) {
188
+ log(`New strings: ${stats.added}`);
189
+ }
190
+ logContextBreakdown(Object.entries(stats.byContext), log);
191
+ }
192
+
193
+ function getExtractMode(skipTranslated) {
194
+ return skipTranslated ? 'Extract Untranslated' : 'Extract All';
195
+ }
196
+
197
+ function logHeader(ctx) {
198
+ const { srcDir, outputFile, backupDir, skipTranslated, dryRun, backup, log } = ctx;
199
+ log(`Transloco ${getExtractMode(skipTranslated)}`);
200
+ log('='.repeat(50));
201
+ log(`Source: ${srcDir}`);
202
+ if (!dryRun) {
203
+ log(`Output: ${outputFile}`);
204
+ }
205
+ if (backup) {
206
+ log(`Backup: ${backupDir}`);
207
+ }
208
+ log();
209
+ }
210
+
211
+ module.exports = {
212
+ scanFiles,
213
+ logVerboseFindings,
214
+ logSummary,
215
+ logHeader,
216
+ };
@@ -0,0 +1,198 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @fileoverview Extract command - scans source files for translatable strings.
5
+ * Supports Angular templates, TypeScript, and PrimeNG components.
6
+ * @module commands/extract
7
+ */
8
+
9
+ const fs = require('../core/fs-adapter');
10
+ const path = require('path');
11
+
12
+ const {
13
+ readJsonFileSync,
14
+ readJsonFile,
15
+ writeJsonFile,
16
+ flattenJson,
17
+ unflattenJson,
18
+ mergeDeep,
19
+ collectFiles,
20
+ applyFindings,
21
+ } = require('../core');
22
+ const { scanFiles, logVerboseFindings, logSummary, logHeader } = require('./extract-utils');
23
+
24
+ async function initSingleLang(lang, sourceJson, ctx) {
25
+ const { i18nDir, format, log } = ctx;
26
+ const langFile = path.join(i18nDir, `${lang}.json`);
27
+ const existing = await readJsonFile(langFile);
28
+ const base = existing && format === 'flat' ? unflattenJson(existing) : existing || {};
29
+ const merged = mergeDeep(base, sourceJson);
30
+ const output = format === 'flat' ? flattenJson(merged) : merged;
31
+ await writeJsonFile(langFile, output);
32
+ log(` Created/Updated: ${lang}.json`);
33
+ }
34
+
35
+ async function initLanguageFiles(result, ctx) {
36
+ const { initLangs, i18nDir, log } = ctx;
37
+ if (initLangs.length === 0) {
38
+ return;
39
+ }
40
+ log('\nInitializing language files...');
41
+ await fs.mkdir(i18nDir, { recursive: true });
42
+ await Promise.all(initLangs.map(lang => initSingleLang(lang, result, ctx)));
43
+ }
44
+
45
+ function logFindingsSummary(findings, ctx) {
46
+ const { verbose, log } = ctx;
47
+ if (verbose || findings.length === 0) {
48
+ return;
49
+ }
50
+ const unique = [...new Map(findings.map(f => [f.key, f])).values()];
51
+ if (unique.length <= 15) {
52
+ log('\nStrings extracted:');
53
+ unique.forEach(i => log(` ${i.key}: "${i.displayText}"`));
54
+ } else {
55
+ log(`\nUse --verbose to see all ${unique.length} strings`);
56
+ }
57
+ }
58
+
59
+ async function writeReport(findings, stats, ctx) {
60
+ const { reportDir } = ctx;
61
+ await writeJsonFile(path.join(reportDir, 'report.json'), { stats, findings });
62
+ ctx.log(`Report: ${path.join(reportDir, 'report.json')}`);
63
+ }
64
+
65
+ function formatResult(result, format) {
66
+ return format === 'flat' ? flattenJson(result) : result;
67
+ }
68
+
69
+ function logDryRunOutput(result, format, log) {
70
+ log('\n[DRY RUN] Generated JSON:');
71
+ log(JSON.stringify(formatResult(result, format), null, 2));
72
+ }
73
+
74
+ async function writeMainOutput(ctx) {
75
+ const { outputFile, format, result, log } = ctx;
76
+ await writeJsonFile(outputFile, formatResult(result, format));
77
+ log(`\nExtracted: ${outputFile}`);
78
+ }
79
+
80
+ function shouldWriteReport(ctx) {
81
+ return (ctx.jsonOutput || ctx.autoApply) && !ctx.dryRun;
82
+ }
83
+
84
+ async function writeOutputs(findings, stats, ctx) {
85
+ if (ctx.dryRun && !ctx.autoApply) {
86
+ logDryRunOutput(ctx.result, ctx.format, ctx.log);
87
+ return;
88
+ }
89
+ if (!ctx.dryRun) {
90
+ await writeMainOutput(ctx);
91
+ }
92
+ if (shouldWriteReport(ctx)) {
93
+ await writeReport(findings, stats, ctx);
94
+ }
95
+ logFindingsSummary(findings, ctx);
96
+ }
97
+
98
+ async function loadMergeResult(ctx) {
99
+ const { outputFile, format, merge, log } = ctx;
100
+ if (!merge) {
101
+ return {};
102
+ }
103
+ const existing = await readJsonFile(outputFile);
104
+ if (!existing) {
105
+ return {};
106
+ }
107
+ log(`Merging with existing: ${path.basename(outputFile)}\n`);
108
+ return format === 'flat' ? unflattenJson(existing) : existing;
109
+ }
110
+
111
+ function getAutoApplyOpts(ctx) {
112
+ const { srcDir, backupDir, adapter, backup, dryRun, verbose, interactive, log } = ctx;
113
+ return { srcDir, backupDir, adapter, backup, dryRun, verbose, interactive, log };
114
+ }
115
+
116
+ async function runAutoApply(findings, ctx) {
117
+ ctx.log(`\n${'='.repeat(50)}`);
118
+ await applyFindings(findings, getAutoApplyOpts(ctx));
119
+ }
120
+
121
+ async function handleResults(findings, stats, ctx) {
122
+ await writeOutputs(findings, stats, ctx);
123
+ if (!ctx.dryRun) {
124
+ await initLanguageFiles(ctx.result, ctx);
125
+ }
126
+ if (ctx.autoApply) {
127
+ await runAutoApply(findings, ctx);
128
+ }
129
+ }
130
+
131
+ function buildRunContext(ctx, data) {
132
+ return { ...ctx, ...data, processedTemplates: new Set() };
133
+ }
134
+
135
+ function loadKeyMapping(keyMappingFile) {
136
+ return fs.existsSync(keyMappingFile) ? readJsonFileSync(keyMappingFile) || {} : {};
137
+ }
138
+
139
+ function initStats() {
140
+ return { files: 0, clean: 0, needsWork: 0, total: 0, added: 0, byContext: {} };
141
+ }
142
+
143
+ function logVerboseIfNeeded(findings, ctx) {
144
+ if (ctx.verbose && findings.length > 0) {
145
+ logVerboseFindings(findings, ctx);
146
+ }
147
+ }
148
+
149
+ async function scanAndPrepare(ctx) {
150
+ const keyMapping = loadKeyMapping(ctx.keyMappingFile);
151
+ const result = await loadMergeResult(ctx);
152
+ const stats = initStats();
153
+ const files = await collectFiles(ctx.srcDir, ctx.excludedFolders);
154
+ const runCtx = buildRunContext(ctx, { result, stats, keyMapping });
155
+ return { result, stats, findings: scanFiles(files, runCtx) };
156
+ }
157
+
158
+ async function processResults(data, ctx) {
159
+ logVerboseIfNeeded(data.findings, ctx);
160
+ logSummary(data.stats, ctx);
161
+ if (data.stats.total > 0) {
162
+ await handleResults(data.findings, data.stats, { ...ctx, result: data.result });
163
+ }
164
+ }
165
+
166
+ function buildExtractResult(data, strict) {
167
+ const { result, findings, stats } = data;
168
+ if (stats.total === 0) {
169
+ return { result, findings, stats, exitCode: 0, message: 'No untranslated strings found.' };
170
+ }
171
+ return { result, findings, stats, exitCode: strict ? 1 : 0 };
172
+ }
173
+
174
+ module.exports = {
175
+ name: 'extract',
176
+ category: 'extraction',
177
+ description: 'Extract i18n strings from source files',
178
+
179
+ options: [
180
+ { flag: '--src <path>', description: 'Source directory (default: src/app)' },
181
+ { flag: '--output <path>', description: 'Output JSON file' },
182
+ { flag: '--lang <code>', description: 'Language code for output file' },
183
+ { flag: '--merge', description: 'Merge with existing JSON file' },
184
+ { flag: '--include-translated', description: 'Include all strings' },
185
+ { flag: '--extract-ts-objects', description: 'Extract from TypeScript objects' },
186
+ { flag: '--format <type>', description: 'Output format: nested or flat' },
187
+ { flag: '--auto-apply', description: 'Extract AND apply in one command' },
188
+ ],
189
+
190
+ examples: ['i18nkit --dry-run', 'i18nkit --lang fr --merge', 'i18nkit --auto-apply --backup'],
191
+
192
+ async run(ctx) {
193
+ logHeader(ctx);
194
+ const data = await scanAndPrepare(ctx);
195
+ await processResults(data, ctx);
196
+ return buildExtractResult(data, ctx.strict);
197
+ },
198
+ };
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @fileoverview Find-orphans command - detects unused translation keys.
5
+ * Scans source files to identify keys not referenced in code.
6
+ * @module commands/find-orphans
7
+ */
8
+
9
+ const { findOrphans } = require('../core');
10
+
11
+ module.exports = {
12
+ name: 'find-orphans',
13
+ category: 'validation',
14
+ description: 'Find translation keys not used in source code',
15
+
16
+ options: [{ flag: '--strict', description: 'Exit with error if orphans found' }],
17
+
18
+ examples: ['i18nkit --find-orphans', 'i18nkit --find-orphans --strict'],
19
+
20
+ async run(ctx) {
21
+ const { srcDir, i18nDir, format, excludedFolders, verbose, strict, log, exitCodes } = ctx;
22
+
23
+ const result = await findOrphans({
24
+ srcDir,
25
+ i18nDir,
26
+ format,
27
+ excludedFolders,
28
+ verbose,
29
+ strict,
30
+ log,
31
+ exitCodes: { success: exitCodes.SUCCESS, untranslated: exitCodes.UNTRANSLATED },
32
+ });
33
+
34
+ return { exitCode: result.exitCode ?? exitCodes.SUCCESS };
35
+ },
36
+ };
@@ -0,0 +1,34 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @fileoverview Help command - displays CLI usage and available commands.
5
+ * Auto-detects project type and shows relevant plugin information.
6
+ * @module commands/help
7
+ */
8
+
9
+ const { generateFullHelp, detectProject, getResolver } = require('../core');
10
+
11
+ module.exports = {
12
+ name: 'help',
13
+
14
+ run(ctx) {
15
+ const detected = detectProject(ctx.cwd);
16
+ const resolver = getResolver(ctx.cwd);
17
+ const { getAllCommands } = require('./index');
18
+
19
+ console.log(
20
+ generateFullHelp({
21
+ packageInfo: require('../../package.json'),
22
+ detected,
23
+ plugins: {
24
+ parsers: resolver.getByType('parser'),
25
+ adapters: resolver.getByType('adapter'),
26
+ providers: resolver.getByType('provider'),
27
+ },
28
+ commands: getAllCommands(),
29
+ }),
30
+ );
31
+
32
+ return { exitCode: 0 };
33
+ },
34
+ };
@@ -0,0 +1,79 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @fileoverview CLI command registry with auto-discovery.
5
+ * Loads command modules from this directory and manages aliases.
6
+ * @module commands
7
+ */
8
+
9
+ const fs = require('../core/fs-adapter');
10
+ const path = require('path');
11
+
12
+ /**
13
+ * @typedef {Object} Command
14
+ * @property {string} name
15
+ * @property {string} [category]
16
+ * @property {string} [description]
17
+ * @property {Array<string>} [aliases]
18
+ * @property {Array<Object>} [options]
19
+ * @property {Array<string>} [examples]
20
+ * @property {function} run
21
+ */
22
+
23
+ const COMMANDS_DIR = __dirname;
24
+ const EXCLUDED_FILES = ['index.js'];
25
+
26
+ function loadCommand(filePath) {
27
+ try {
28
+ const cmd = require(filePath);
29
+ return cmd?.name && typeof cmd.run === 'function' ? cmd : null;
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Auto-discover and load all command modules from this directory
37
+ * @returns {Map<string, Command>}
38
+ */
39
+ function discoverCommands() {
40
+ const commands = new Map();
41
+
42
+ const files = fs
43
+ .readdirSync(COMMANDS_DIR)
44
+ .filter(f => f.endsWith('.js') && !EXCLUDED_FILES.includes(f));
45
+
46
+ for (const file of files) {
47
+ const command = loadCommand(path.join(COMMANDS_DIR, file));
48
+ if (!command) {
49
+ continue;
50
+ }
51
+
52
+ commands.set(command.name, command);
53
+ command.aliases?.forEach(alias => commands.set(alias, command));
54
+ }
55
+
56
+ return commands;
57
+ }
58
+
59
+ const COMMANDS = discoverCommands();
60
+
61
+ /**
62
+ * @param {string} name
63
+ * @returns {Command|undefined}
64
+ */
65
+ function getCommand(name) {
66
+ return COMMANDS.get(name);
67
+ }
68
+
69
+ /**
70
+ * @returns {Command[]}
71
+ */
72
+ function getAllCommands() {
73
+ return [...new Set(COMMANDS.values())];
74
+ }
75
+
76
+ module.exports = {
77
+ getCommand,
78
+ getAllCommands,
79
+ };
@@ -0,0 +1,51 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @fileoverview Translate command - automated translation via external APIs.
5
+ * Supports MyMemory (free) and DeepL (API key required).
6
+ * @module commands/translate
7
+ */
8
+
9
+ const { translateFile } = require('../core');
10
+
11
+ function parseTranslatePair(translatePair) {
12
+ return (translatePair || '').split(':');
13
+ }
14
+
15
+ function validateTranslatePair(source, target, exitCodes) {
16
+ if (!source || !target) {
17
+ console.error('Invalid --translate format. Use: --translate fr:en');
18
+ return { exitCode: exitCodes.ERROR };
19
+ }
20
+ return null;
21
+ }
22
+
23
+ function getTranslateOptions(ctx) {
24
+ const { i18nDir, provider, useDeepL, translateEmail, verbose, dryRun, log } = ctx;
25
+ return { i18nDir, provider, useDeepL, email: translateEmail, verbose, dryRun, log };
26
+ }
27
+
28
+ module.exports = {
29
+ name: 'translate',
30
+ category: 'translation',
31
+ description: 'Translate a language file to another language',
32
+
33
+ options: [
34
+ { flag: '--translate <src:tgt>', description: 'Source and target languages (e.g., fr:en)' },
35
+ { flag: '--deepl', description: 'Use DeepL API instead of MyMemory' },
36
+ { flag: '--email <email>', description: 'Email for MyMemory rate limits' },
37
+ ],
38
+
39
+ examples: ['i18nkit --translate fr:en', 'i18nkit --translate fr:en --deepl'],
40
+
41
+ async run(ctx) {
42
+ const { translatePair, exitCodes } = ctx;
43
+ const [source, target] = parseTranslatePair(translatePair);
44
+ const error = validateTranslatePair(source, target, exitCodes);
45
+ if (error) {
46
+ return error;
47
+ }
48
+ await translateFile(source, target, getTranslateOptions(ctx));
49
+ return { exitCode: exitCodes.SUCCESS };
50
+ },
51
+ };
@@ -0,0 +1,17 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @fileoverview Version command - displays package and Node.js versions.
5
+ * @module commands/version
6
+ */
7
+
8
+ module.exports = {
9
+ name: 'version',
10
+
11
+ run() {
12
+ const pkg = require('../../package.json');
13
+ console.log(`${pkg.name} v${pkg.version}`);
14
+ console.log(`Node.js ${process.version}`);
15
+ return { exitCode: 0 };
16
+ },
17
+ };
@@ -0,0 +1,34 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @fileoverview Watch command - file system watcher for incremental extraction.
5
+ * Monitors source files and re-runs extraction on changes.
6
+ * @module commands/watch
7
+ */
8
+
9
+ const { watchFiles } = require('../core');
10
+ const extract = require('./extract');
11
+
12
+ module.exports = {
13
+ name: 'watch',
14
+ category: 'development',
15
+ description: 'Watch for file changes and re-run extraction',
16
+
17
+ examples: ['i18nkit --watch', 'i18nkit --watch --verbose'],
18
+
19
+ run(ctx) {
20
+ const { srcDir, excludedFolders, log } = ctx;
21
+
22
+ const runExtract = async () => {
23
+ try {
24
+ await extract.run(ctx);
25
+ } catch (err) {
26
+ console.error('Error:', err.message);
27
+ }
28
+ };
29
+
30
+ watchFiles({ srcDir, excludedFolders, log, onFileChange: runExtract, onStart: runExtract });
31
+
32
+ return { exitCode: null };
33
+ },
34
+ };