@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.
- package/CHANGELOG.md +35 -0
- package/LICENSE +21 -0
- package/README.md +309 -0
- package/bin/cli.js +48 -0
- package/bin/commands/apply.js +48 -0
- package/bin/commands/check-sync.js +35 -0
- package/bin/commands/extract-utils.js +216 -0
- package/bin/commands/extract.js +198 -0
- package/bin/commands/find-orphans.js +36 -0
- package/bin/commands/help.js +34 -0
- package/bin/commands/index.js +79 -0
- package/bin/commands/translate.js +51 -0
- package/bin/commands/version.js +17 -0
- package/bin/commands/watch.js +34 -0
- package/bin/core/applier-utils.js +144 -0
- package/bin/core/applier.js +165 -0
- package/bin/core/args.js +147 -0
- package/bin/core/backup.js +74 -0
- package/bin/core/command-interface.js +69 -0
- package/bin/core/config.js +108 -0
- package/bin/core/context.js +86 -0
- package/bin/core/detector.js +152 -0
- package/bin/core/file-walker.js +159 -0
- package/bin/core/fs-adapter.js +56 -0
- package/bin/core/help-generator.js +208 -0
- package/bin/core/index.js +63 -0
- package/bin/core/json-utils.js +213 -0
- package/bin/core/key-generator.js +75 -0
- package/bin/core/log-utils.js +26 -0
- package/bin/core/orphan-finder.js +208 -0
- package/bin/core/parser-utils.js +187 -0
- package/bin/core/paths.js +60 -0
- package/bin/core/plugin-interface.js +83 -0
- package/bin/core/plugin-resolver-utils.js +166 -0
- package/bin/core/plugin-resolver.js +211 -0
- package/bin/core/sync-checker-utils.js +99 -0
- package/bin/core/sync-checker.js +199 -0
- package/bin/core/translator.js +197 -0
- package/bin/core/types.js +297 -0
- package/bin/core/watcher.js +119 -0
- package/bin/plugins/adapter-transloco.js +156 -0
- package/bin/plugins/parser-angular.js +56 -0
- package/bin/plugins/parser-primeng.js +79 -0
- package/bin/plugins/parser-typescript.js +66 -0
- package/bin/plugins/provider-deepl.js +65 -0
- package/bin/plugins/provider-mymemory.js +192 -0
- package/package.json +123 -0
- package/types/index.d.ts +85 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('./fs-adapter');
|
|
4
|
+
const readline = require('readline');
|
|
5
|
+
const { createBackup, getBackupFiles } = require('./backup');
|
|
6
|
+
|
|
7
|
+
const isValidFinding = f => f.file && f.text && f.key && !f.context?.startsWith('ts_');
|
|
8
|
+
|
|
9
|
+
function addFindingToMap(map, finding) {
|
|
10
|
+
if (!map.has(finding.file)) {
|
|
11
|
+
map.set(finding.file, []);
|
|
12
|
+
}
|
|
13
|
+
map.get(finding.file).push(finding);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function groupFindingsByFile(findings) {
|
|
17
|
+
const findingsByFile = new Map();
|
|
18
|
+
findings.filter(isValidFinding).forEach(f => addFindingToMap(findingsByFile, f));
|
|
19
|
+
return findingsByFile;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function promptUser(question, interactive) {
|
|
23
|
+
if (!interactive) {
|
|
24
|
+
return Promise.resolve(true);
|
|
25
|
+
}
|
|
26
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
27
|
+
const { promise, resolve } = Promise.withResolvers();
|
|
28
|
+
rl.question(`${question} (y/N) `, answer => {
|
|
29
|
+
rl.close();
|
|
30
|
+
resolve(['y', 'yes'].includes(answer.toLowerCase()));
|
|
31
|
+
});
|
|
32
|
+
return promise;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function confirmFileModifications(findingsByFile, interactive, log) {
|
|
36
|
+
if (!interactive || findingsByFile.size === 0) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
log(`\nAbout to modify ${findingsByFile.size} file(s):`);
|
|
40
|
+
for (const [file, fileFindings] of findingsByFile) {
|
|
41
|
+
log(` ${file} (${fileFindings.length} replacement(s))`);
|
|
42
|
+
}
|
|
43
|
+
const proceed = await promptUser('\nProceed with modifications?', interactive);
|
|
44
|
+
if (!proceed) {
|
|
45
|
+
log('Aborted by user.');
|
|
46
|
+
}
|
|
47
|
+
return proceed;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function loadFileForReplacement(filePath, relativeFile, verbose) {
|
|
51
|
+
if (!fs.existsSync(filePath)) {
|
|
52
|
+
if (verbose) {
|
|
53
|
+
console.warn(`Skipped (not found): ${relativeFile}`);
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
59
|
+
} catch {
|
|
60
|
+
if (verbose) {
|
|
61
|
+
console.warn(`Skipped (read error): ${relativeFile}`);
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function applyReplacementsToContent(content, fileFindings, adapter) {
|
|
68
|
+
let result = content;
|
|
69
|
+
let count = 0;
|
|
70
|
+
for (const finding of fileFindings) {
|
|
71
|
+
const transformed = adapter.transform(
|
|
72
|
+
result,
|
|
73
|
+
finding.rawText || finding.text,
|
|
74
|
+
finding.key,
|
|
75
|
+
finding.context,
|
|
76
|
+
);
|
|
77
|
+
result = transformed.content;
|
|
78
|
+
count += transformed.replacements;
|
|
79
|
+
}
|
|
80
|
+
return { content: result, count };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function tryUpdateTsImports(tsFile, opts) {
|
|
84
|
+
const { backupDir, adapter, backup = true, dryRun = false } = opts;
|
|
85
|
+
createBackup(tsFile, backupDir, { enabled: backup, dryRun });
|
|
86
|
+
const tsContent = fs.readFileSync(tsFile, 'utf-8');
|
|
87
|
+
const updatedTs = adapter.updateImports(tsContent);
|
|
88
|
+
if (updatedTs === tsContent) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
if (!dryRun) {
|
|
92
|
+
fs.writeFileSync(tsFile, updatedTs, 'utf-8');
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function logBackupInfo(opts, log) {
|
|
98
|
+
const backupFiles = getBackupFiles();
|
|
99
|
+
if (opts.backup && !opts.dryRun && backupFiles.size > 0) {
|
|
100
|
+
log(`Backups created: ${backupFiles.size} (in ${opts.backupDir})`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function logModifiedFiles(modifiedFiles, dryRun, log) {
|
|
105
|
+
if (!dryRun && modifiedFiles.length > 0) {
|
|
106
|
+
log('\nModified files:');
|
|
107
|
+
modifiedFiles.forEach(({ file, count }) => log(` ${file} (${count})`));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function logApplyResults(results, opts) {
|
|
112
|
+
const { log = console.log } = opts;
|
|
113
|
+
log(`\nFiles modified: ${results.totalFiles}`);
|
|
114
|
+
log(`Replacements: ${results.totalReplacements}`);
|
|
115
|
+
logBackupInfo(opts, log);
|
|
116
|
+
logModifiedFiles(results.modifiedFiles, opts.dryRun, log);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function loadAndValidateReport(reportPath) {
|
|
120
|
+
if (!fs.existsSync(reportPath)) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`Report file not found: ${reportPath}\nRun with --json first to generate a report file.`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
const report = JSON.parse(fs.readFileSync(reportPath, 'utf-8'));
|
|
127
|
+
if (!Array.isArray(report.findings)) {
|
|
128
|
+
throw new Error('missing findings array');
|
|
129
|
+
}
|
|
130
|
+
return report;
|
|
131
|
+
} catch (err) {
|
|
132
|
+
throw new Error(`Cannot parse report: ${err.message}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
module.exports = {
|
|
137
|
+
groupFindingsByFile,
|
|
138
|
+
confirmFileModifications,
|
|
139
|
+
loadFileForReplacement,
|
|
140
|
+
applyReplacementsToContent,
|
|
141
|
+
tryUpdateTsImports,
|
|
142
|
+
logApplyResults,
|
|
143
|
+
loadAndValidateReport,
|
|
144
|
+
};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview Translation replacement engine.
|
|
5
|
+
* Applies extracted i18n keys to source files with backup and rollback support.
|
|
6
|
+
* @module applier
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('./fs-adapter');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { createBackup } = require('./backup');
|
|
12
|
+
const {
|
|
13
|
+
groupFindingsByFile,
|
|
14
|
+
confirmFileModifications,
|
|
15
|
+
loadFileForReplacement,
|
|
16
|
+
applyReplacementsToContent,
|
|
17
|
+
tryUpdateTsImports,
|
|
18
|
+
logApplyResults,
|
|
19
|
+
loadAndValidateReport,
|
|
20
|
+
} = require('./applier-utils');
|
|
21
|
+
|
|
22
|
+
function getCompanionTsFile(htmlFilePath) {
|
|
23
|
+
return htmlFilePath.replace(/\.html$/, '.ts');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function logTsUpdate(updated, tsFile, opts) {
|
|
27
|
+
if (updated && opts.verbose) {
|
|
28
|
+
console.log(` + TranslocoPipe added to ${path.relative(opts.srcDir, tsFile)}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function updateCompanionTsFile(htmlFilePath, opts) {
|
|
33
|
+
const tsFile = getCompanionTsFile(htmlFilePath);
|
|
34
|
+
if (!fs.existsSync(tsFile)) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
logTsUpdate(tryUpdateTsImports(tsFile, opts), tsFile, opts);
|
|
39
|
+
} catch {
|
|
40
|
+
if (opts.verbose) {
|
|
41
|
+
console.warn(` Warning: Could not update ${tsFile}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function applyTsPostProcessing(content, opts) {
|
|
47
|
+
return opts.adapter.updateImports(content);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function applyPostProcessing(ctx) {
|
|
51
|
+
const { relativeFile, content, count, filePath, opts } = ctx;
|
|
52
|
+
if (count > 0 && /\.ts$/.test(relativeFile)) {
|
|
53
|
+
return applyTsPostProcessing(content, opts);
|
|
54
|
+
}
|
|
55
|
+
if (count > 0 && /\.html$/.test(relativeFile)) {
|
|
56
|
+
updateCompanionTsFile(filePath, opts);
|
|
57
|
+
}
|
|
58
|
+
return content;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function buildFileResult(ctx) {
|
|
62
|
+
const { filePath, content, originalContent, count } = ctx;
|
|
63
|
+
return {
|
|
64
|
+
filePath,
|
|
65
|
+
content,
|
|
66
|
+
originalContent,
|
|
67
|
+
fileReplacements: count,
|
|
68
|
+
hasChanges: content !== originalContent,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function processFileReplacements(relativeFile, fileFindings, opts) {
|
|
73
|
+
const { srcDir, backupDir, backup = true, dryRun = false, verbose = false, adapter } = opts;
|
|
74
|
+
const filePath = path.join(srcDir, relativeFile);
|
|
75
|
+
const originalContent = loadFileForReplacement(filePath, relativeFile, verbose);
|
|
76
|
+
if (!originalContent) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
createBackup(filePath, backupDir, { enabled: backup, dryRun });
|
|
80
|
+
const { content: replaced, count } = applyReplacementsToContent(
|
|
81
|
+
originalContent,
|
|
82
|
+
fileFindings,
|
|
83
|
+
adapter,
|
|
84
|
+
);
|
|
85
|
+
const content = applyPostProcessing({ relativeFile, content: replaced, count, filePath, opts });
|
|
86
|
+
return buildFileResult({ filePath, content, originalContent, count });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function writeFileChanges(ctx) {
|
|
90
|
+
const { relativeFile, result, modifiedFiles, dryRun, verbose, log } = ctx;
|
|
91
|
+
if (!result.hasChanges) {
|
|
92
|
+
return { files: 0, replacements: 0 };
|
|
93
|
+
}
|
|
94
|
+
if (dryRun) {
|
|
95
|
+
log(`[DRY RUN] ${relativeFile}: ${result.fileReplacements} replacement(s)`);
|
|
96
|
+
} else {
|
|
97
|
+
fs.writeFileSync(result.filePath, result.content, 'utf-8');
|
|
98
|
+
modifiedFiles.push({ file: relativeFile, count: result.fileReplacements });
|
|
99
|
+
if (verbose) {
|
|
100
|
+
log(`Modified: ${relativeFile} (${result.fileReplacements} replacement(s))`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return { files: 1, replacements: result.fileReplacements };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function processSingleFile(ctx) {
|
|
107
|
+
const { relativeFile, fileFindings, modifiedFiles, opts } = ctx;
|
|
108
|
+
const result = processFileReplacements(relativeFile, fileFindings, opts);
|
|
109
|
+
if (!result) {
|
|
110
|
+
return { files: 0, replacements: 0 };
|
|
111
|
+
}
|
|
112
|
+
const { dryRun = false, verbose = false, log = console.log } = opts;
|
|
113
|
+
return writeFileChanges({ relativeFile, result, modifiedFiles, dryRun, verbose, log });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function processAllFiles(findingsByFile, opts) {
|
|
117
|
+
const modifiedFiles = [];
|
|
118
|
+
let totalFiles = 0;
|
|
119
|
+
let totalReplacements = 0;
|
|
120
|
+
for (const [relativeFile, fileFindings] of findingsByFile) {
|
|
121
|
+
const counts = processSingleFile({ relativeFile, fileFindings, modifiedFiles, opts });
|
|
122
|
+
totalFiles += counts.files;
|
|
123
|
+
totalReplacements += counts.replacements;
|
|
124
|
+
}
|
|
125
|
+
return { totalFiles, totalReplacements, modifiedFiles };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Applies translation replacements from findings array
|
|
130
|
+
* @param {Finding[]} findings
|
|
131
|
+
* @param {ApplyOptions} [opts]
|
|
132
|
+
* @returns {Promise<ApplyResult>}
|
|
133
|
+
*/
|
|
134
|
+
async function applyFindings(findings, opts = {}) {
|
|
135
|
+
const { log = console.log, interactive = false } = opts;
|
|
136
|
+
log('Auto-Apply Translations');
|
|
137
|
+
log('-'.repeat(50));
|
|
138
|
+
const findingsByFile = groupFindingsByFile(findings);
|
|
139
|
+
const shouldProceed = await confirmFileModifications(findingsByFile, interactive, log);
|
|
140
|
+
if (!shouldProceed) {
|
|
141
|
+
return { success: false, aborted: true };
|
|
142
|
+
}
|
|
143
|
+
const results = processAllFiles(findingsByFile, opts);
|
|
144
|
+
logApplyResults(results, opts);
|
|
145
|
+
return { success: true, ...results };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Applies translations from a JSON report file
|
|
150
|
+
* @param {string} reportPath - Path to extracted-keys.json
|
|
151
|
+
* @param {ApplyOptions} [opts]
|
|
152
|
+
* @returns {Promise<ApplyResult>}
|
|
153
|
+
* @example
|
|
154
|
+
* await applyTranslations('./extracted-keys.json', { srcDir: './src', adapter });
|
|
155
|
+
*/
|
|
156
|
+
function applyTranslations(reportPath, opts = {}) {
|
|
157
|
+
const { log = console.log } = opts;
|
|
158
|
+
log('Apply Translations');
|
|
159
|
+
log('='.repeat(50));
|
|
160
|
+
log(`Report: ${reportPath}\n`);
|
|
161
|
+
const report = loadAndValidateReport(reportPath);
|
|
162
|
+
return applyFindings(report.findings, opts);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
module.exports = { applyFindings, applyTranslations };
|
package/bin/core/args.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {Object} ParsedFlags
|
|
5
|
+
* @property {boolean} verbose
|
|
6
|
+
* @property {boolean} jsonOutput
|
|
7
|
+
* @property {boolean} strict
|
|
8
|
+
* @property {boolean} dryRun
|
|
9
|
+
* @property {boolean} ciMode
|
|
10
|
+
* @property {boolean} useDeepL
|
|
11
|
+
* @property {boolean} merge
|
|
12
|
+
* @property {boolean} skipTranslated
|
|
13
|
+
* @property {boolean} extractTsObjects
|
|
14
|
+
* @property {boolean} autoApply
|
|
15
|
+
* @property {boolean} backup
|
|
16
|
+
* @property {boolean} interactive
|
|
17
|
+
* @property {string|null} lang
|
|
18
|
+
* @property {string} format
|
|
19
|
+
* @property {string|null} translatePair
|
|
20
|
+
* @property {string|null} translateEmail
|
|
21
|
+
* @property {string[]} initLangs
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const { getFlag, getArgValue, getArgList } = require('./config');
|
|
25
|
+
|
|
26
|
+
const flag = (args, config, opts) => getFlag(args, config, opts);
|
|
27
|
+
const value = (args, config, opts) => getArgValue(args, config, opts);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Parse command line arguments into structured flags
|
|
31
|
+
* @param {string[]} args
|
|
32
|
+
* @param {Object} config
|
|
33
|
+
* @returns {ParsedFlags}
|
|
34
|
+
*/
|
|
35
|
+
function parseCiMode(args, config) {
|
|
36
|
+
const ciMode = flag(args, config, { flag: '--ci', configKey: 'ci' });
|
|
37
|
+
if (!ciMode) {
|
|
38
|
+
return { ciMode: false, jsonOutput: false, strict: false };
|
|
39
|
+
}
|
|
40
|
+
return { ciMode: true, jsonOutput: true, strict: true };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function parseBooleanFlags(args, config) {
|
|
44
|
+
return {
|
|
45
|
+
verbose: flag(args, config, { flag: '--verbose', configKey: 'verbose' }),
|
|
46
|
+
dryRun: flag(args, config, { flag: '--dry-run', configKey: 'dryRun' }),
|
|
47
|
+
useDeepL: flag(args, config, { flag: '--deepl', configKey: 'deepl' }),
|
|
48
|
+
merge: flag(args, config, { flag: '--merge', configKey: 'merge' }),
|
|
49
|
+
skipTranslated: !flag(args, config, {
|
|
50
|
+
flag: '--include-translated',
|
|
51
|
+
configKey: 'includeTranslated',
|
|
52
|
+
}),
|
|
53
|
+
extractTsObjects: flag(args, config, {
|
|
54
|
+
flag: '--extract-ts-objects',
|
|
55
|
+
configKey: 'extractTsObjects',
|
|
56
|
+
}),
|
|
57
|
+
autoApply: flag(args, config, { flag: '--auto-apply', configKey: 'autoApply' }),
|
|
58
|
+
interactive: flag(args, config, { flag: '--interactive', configKey: 'interactive' }),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseBackupFlag(args, config) {
|
|
63
|
+
if (args.includes('--no-backup')) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
return flag(args, config, { flag: '--backup', configKey: 'backup', defaultValue: true });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function parseValueFlags(args, config) {
|
|
70
|
+
return {
|
|
71
|
+
lang: value(args, config, { flag: '--lang', configKey: 'lang' }),
|
|
72
|
+
format: value(args, config, { flag: '--format', configKey: 'format', defaultValue: 'nested' }),
|
|
73
|
+
translatePair: value(args, config, { flag: '--translate', configKey: 'translate' }),
|
|
74
|
+
translateEmail: value(args, config, { flag: '--email', configKey: 'email' }),
|
|
75
|
+
initLangs: getArgList(args, config, { flag: '--init-langs', configKey: 'initLangs' }),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function parseArgs(args, config = {}) {
|
|
80
|
+
const ci = parseCiMode(args, config);
|
|
81
|
+
const bools = parseBooleanFlags(args, config);
|
|
82
|
+
const vals = parseValueFlags(args, config);
|
|
83
|
+
const jsonOutput = ci.jsonOutput || flag(args, config, { flag: '--json', configKey: 'json' });
|
|
84
|
+
const strict = ci.strict || flag(args, config, { flag: '--strict', configKey: 'strict' });
|
|
85
|
+
return {
|
|
86
|
+
...bools,
|
|
87
|
+
...vals,
|
|
88
|
+
ciMode: ci.ciMode,
|
|
89
|
+
jsonOutput,
|
|
90
|
+
strict,
|
|
91
|
+
backup: parseBackupFlag(args, config),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const COMMAND_ALIASES = {
|
|
96
|
+
orphans: 'find-orphans',
|
|
97
|
+
'--orphans': 'find-orphans',
|
|
98
|
+
'--find-orphans': 'find-orphans',
|
|
99
|
+
'--check-sync': 'check-sync',
|
|
100
|
+
'--watch': 'watch',
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const POSITIONAL_COMMANDS = [
|
|
104
|
+
'check-sync',
|
|
105
|
+
'find-orphans',
|
|
106
|
+
'translate',
|
|
107
|
+
'apply',
|
|
108
|
+
'watch',
|
|
109
|
+
'extract',
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
function detectFromFlags(args) {
|
|
113
|
+
if (args.some(a => a.startsWith('--translate'))) {
|
|
114
|
+
return 'translate';
|
|
115
|
+
}
|
|
116
|
+
if (args.some(a => a.startsWith('--apply'))) {
|
|
117
|
+
return 'apply';
|
|
118
|
+
}
|
|
119
|
+
const flagCmd = args.find(a => COMMAND_ALIASES[a]);
|
|
120
|
+
return flagCmd ? COMMAND_ALIASES[flagCmd] : null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function detectHelpOrVersion(args) {
|
|
124
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
125
|
+
return 'help';
|
|
126
|
+
}
|
|
127
|
+
if (args.includes('--version') || args.includes('-v')) {
|
|
128
|
+
return 'version';
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function detectPositionalCommand(args) {
|
|
134
|
+
const firstArg = args[0];
|
|
135
|
+
if (!firstArg || !POSITIONAL_COMMANDS.includes(firstArg)) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
return COMMAND_ALIASES[firstArg] || firstArg;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function detectCommand(args) {
|
|
142
|
+
return (
|
|
143
|
+
detectHelpOrVersion(args) || detectPositionalCommand(args) || detectFromFlags(args) || 'extract'
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
module.exports = { parseArgs, detectCommand };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview File backup and restore for safe source modifications.
|
|
5
|
+
* Creates timestamped backups before applying changes.
|
|
6
|
+
* @module backup
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('./fs-adapter');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
const backupFiles = new Map();
|
|
13
|
+
|
|
14
|
+
function buildBackupPath(filePath, backupDir) {
|
|
15
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
16
|
+
const relativePath = path.relative(process.cwd(), filePath);
|
|
17
|
+
return path.join(backupDir, `${timestamp}_${relativePath.replace(/[/\\]/g, '_')}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function writeBackupFile(filePath, backupPath) {
|
|
21
|
+
try {
|
|
22
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
23
|
+
fs.writeFileSync(backupPath, content, 'utf-8');
|
|
24
|
+
backupFiles.set(filePath, backupPath);
|
|
25
|
+
return backupPath;
|
|
26
|
+
} catch (err) {
|
|
27
|
+
console.warn(`Warning: Cannot backup ${filePath}: ${err.message}`);
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function shouldSkipBackup(enabled, dryRun) {
|
|
33
|
+
return !enabled || dryRun;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Creates a timestamped backup of a file
|
|
38
|
+
* @param {string} filePath
|
|
39
|
+
* @param {string} backupDir
|
|
40
|
+
* @param {Object} [options]
|
|
41
|
+
* @returns {string|null} Backup path or null if skipped
|
|
42
|
+
*/
|
|
43
|
+
function createBackup(filePath, backupDir, options = {}) {
|
|
44
|
+
const { enabled = true, dryRun = false } = options;
|
|
45
|
+
if (shouldSkipBackup(enabled, dryRun)) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
fs.mkdirSync(backupDir, { recursive: true });
|
|
49
|
+
return writeBackupFile(filePath, buildBackupPath(filePath, backupDir));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Restores all backed-up files to their original locations
|
|
54
|
+
* @returns {number} Count of restored files
|
|
55
|
+
*/
|
|
56
|
+
function restoreBackups() {
|
|
57
|
+
let restored = 0;
|
|
58
|
+
for (const [original, backup] of backupFiles) {
|
|
59
|
+
try {
|
|
60
|
+
const content = fs.readFileSync(backup, 'utf-8');
|
|
61
|
+
fs.writeFileSync(original, content, 'utf-8');
|
|
62
|
+
restored++;
|
|
63
|
+
} catch {
|
|
64
|
+
console.error(`Cannot restore ${original} from ${backup}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return restored;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getBackupFiles() {
|
|
71
|
+
return backupFiles;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = { createBackup, restoreBackups, getBackupFiles };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview CLI command categorization and help formatting.
|
|
5
|
+
* Groups commands by category and generates formatted output.
|
|
6
|
+
* @module command-interface
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const COMMAND_CATEGORIES = Object.freeze([
|
|
10
|
+
{ id: 'extraction', label: 'EXTRACTION' },
|
|
11
|
+
{ id: 'validation', label: 'VALIDATION' },
|
|
12
|
+
{ id: 'translation', label: 'TRANSLATION' },
|
|
13
|
+
{ id: 'development', label: 'DEVELOPMENT' },
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
const getDescription = cmd => cmd.description || cmd.meta?.description || 'No description';
|
|
17
|
+
const getCategory = cmd => cmd.category || cmd.meta?.category;
|
|
18
|
+
|
|
19
|
+
function formatAliases(command) {
|
|
20
|
+
return command.aliases?.length ? ` (${command.aliases.join(', ')})` : '';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function formatOption(opt) {
|
|
24
|
+
const required = opt.required ? ' (required)' : '';
|
|
25
|
+
return ` ${opt.flag.padEnd(24)} ${opt.description}${required}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function formatOptions(command) {
|
|
29
|
+
return (command.options || []).map(formatOption);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Formats command for CLI help output.
|
|
34
|
+
* @param {Command} command
|
|
35
|
+
* @returns {string}
|
|
36
|
+
*/
|
|
37
|
+
function formatCommandHelp(command) {
|
|
38
|
+
const lines = [` ${command.name}${formatAliases(command)}`, ` ${getDescription(command)}`];
|
|
39
|
+
lines.push(...formatOptions(command));
|
|
40
|
+
return lines.join('\n');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Groups commands by category for organized help display.
|
|
45
|
+
* @param {Command[]} commands
|
|
46
|
+
* @returns {Record<string, {label: string, commands: Command[]}>}
|
|
47
|
+
*/
|
|
48
|
+
function groupCommandsByCategory(commands) {
|
|
49
|
+
const byCategory = Object.groupBy(commands, c => getCategory(c) || 'other');
|
|
50
|
+
|
|
51
|
+
const grouped = Object.fromEntries(
|
|
52
|
+
COMMAND_CATEGORIES.map(cat => [
|
|
53
|
+
cat.id,
|
|
54
|
+
{ label: cat.label, commands: byCategory[cat.id] || [] },
|
|
55
|
+
]),
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
if (byCategory.other?.length) {
|
|
59
|
+
grouped.other = { label: 'OTHER', commands: byCategory.other };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return grouped;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = {
|
|
66
|
+
COMMAND_CATEGORIES,
|
|
67
|
+
formatCommandHelp,
|
|
68
|
+
groupCommandsByCategory,
|
|
69
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview Configuration loader with CLI argument merging.
|
|
5
|
+
* Supports .js and .json config files with fallback chain.
|
|
6
|
+
* @module config
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('./fs-adapter');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
const CONFIG_FILES = ['i18nkit.config.js', 'i18nkit.config.json', '.i18nkit.config.js'];
|
|
13
|
+
|
|
14
|
+
function tryLoadConfigFile(configFile, cwd = process.cwd()) {
|
|
15
|
+
const configPath = path.join(cwd, configFile);
|
|
16
|
+
if (!fs.existsSync(configPath)) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
return configFile.endsWith('.js') ?
|
|
21
|
+
require(configPath)
|
|
22
|
+
: JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
23
|
+
} catch (err) {
|
|
24
|
+
console.warn(`Warning: Cannot load ${configFile}: ${err.message}`);
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Loads config from .i18nkit.config.js or .json variants
|
|
31
|
+
* @param {string} [cwd=process.cwd()]
|
|
32
|
+
* @returns {Object}
|
|
33
|
+
*/
|
|
34
|
+
function loadConfig(cwd = process.cwd()) {
|
|
35
|
+
for (const configFile of CONFIG_FILES) {
|
|
36
|
+
const config = tryLoadConfigFile(configFile, cwd);
|
|
37
|
+
if (config) {
|
|
38
|
+
return config;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getConfigValue(config, configKey, defaultValue) {
|
|
45
|
+
return configKey && config[configKey] !== undefined ? config[configKey] : defaultValue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Gets boolean flag from args or config with fallback
|
|
50
|
+
* @param {string[]} args
|
|
51
|
+
* @param {Object} config
|
|
52
|
+
* @param {Object} opts
|
|
53
|
+
* @returns {boolean}
|
|
54
|
+
*/
|
|
55
|
+
function getFlag(args, config, opts = {}) {
|
|
56
|
+
const { flag, configKey, defaultValue = false } = opts;
|
|
57
|
+
if (args.includes(flag)) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
return getConfigValue(config, configKey, defaultValue);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getValueFromArgs(args, flag) {
|
|
64
|
+
const idx = args.indexOf(flag);
|
|
65
|
+
if (idx === -1) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
const value = args[idx + 1];
|
|
69
|
+
return value && !value.startsWith('--') ? value : null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Gets string value from args or config with fallback
|
|
74
|
+
* @param {string[]} args
|
|
75
|
+
* @param {Object} config
|
|
76
|
+
* @param {Object} opts
|
|
77
|
+
* @returns {string|undefined}
|
|
78
|
+
*/
|
|
79
|
+
function getArgValue(args, config, opts = {}) {
|
|
80
|
+
const { flag, configKey, defaultValue } = opts;
|
|
81
|
+
const argValue = getValueFromArgs(args, flag);
|
|
82
|
+
if (argValue !== null) {
|
|
83
|
+
return argValue;
|
|
84
|
+
}
|
|
85
|
+
return configKey && config[configKey] !== undefined ? config[configKey] : defaultValue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function parseArgListValue(value) {
|
|
89
|
+
return value
|
|
90
|
+
.split(',')
|
|
91
|
+
.map(s => s.trim())
|
|
92
|
+
.filter(Boolean);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function getConfigAsList(config, configKey) {
|
|
96
|
+
if (!configKey || !config[configKey]) {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
return Array.isArray(config[configKey]) ? config[configKey] : [config[configKey]];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getArgList(args, config, opts) {
|
|
103
|
+
const { flag, configKey } = opts;
|
|
104
|
+
const argValue = getValueFromArgs(args, flag);
|
|
105
|
+
return argValue ? parseArgListValue(argValue) : getConfigAsList(config, configKey);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = { loadConfig, getFlag, getArgValue, getArgList };
|