@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,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 };
@@ -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 };