i18ntk 4.0.0 → 4.2.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 +116 -29
- package/README.md +83 -18
- package/SECURITY.md +13 -5
- package/main/i18ntk-analyze.js +10 -20
- package/main/i18ntk-backup.js +227 -111
- package/main/i18ntk-init.js +153 -157
- package/main/i18ntk-scanner.js +9 -7
- package/main/i18ntk-setup.js +36 -13
- package/main/i18ntk-sizing.js +18 -50
- package/main/i18ntk-translate.js +169 -21
- package/main/i18ntk-usage.js +298 -154
- package/main/i18ntk-validate.js +49 -37
- package/main/manage/commands/AnalyzeCommand.js +7 -17
- package/main/manage/commands/CommandRouter.js +6 -6
- package/main/manage/commands/TranslateCommand.js +65 -56
- package/main/manage/commands/ValidateCommand.js +34 -26
- package/main/manage/index.js +11 -42
- package/main/manage/managers/InteractiveMenu.js +11 -40
- package/main/manage/services/InitService.js +114 -118
- package/main/manage/services/UsageService.js +244 -85
- package/package.json +55 -4
- package/runtime/enhanced.d.ts +5 -5
- package/runtime/enhanced.js +49 -25
- package/runtime/i18ntk.d.ts +30 -7
- package/runtime/index.d.ts +48 -19
- package/runtime/index.js +188 -97
- package/settings/settings-cli.js +115 -38
- package/settings/settings-manager.js +24 -6
- package/ui-locales/de.json +192 -11
- package/ui-locales/en.json +182 -8
- package/ui-locales/es.json +193 -12
- package/ui-locales/fr.json +189 -8
- package/ui-locales/ja.json +190 -8
- package/ui-locales/ru.json +191 -9
- package/ui-locales/zh.json +194 -9
- package/utils/cli-helper.js +8 -12
- package/utils/config-helper.js +1 -1
- package/utils/config-manager.js +8 -6
- package/utils/localized-confirm.js +55 -0
- package/utils/menu-layout.js +41 -0
- package/utils/report-writer.js +110 -0
- package/utils/security.js +15 -22
- package/utils/translate/api.js +31 -3
- package/utils/translate/placeholder.js +42 -1
- package/utils/translate/protection.js +17 -12
- package/utils/translate/report.js +3 -2
- package/utils/translate/safe-network.js +24 -4
- package/utils/usage-insights.js +435 -0
- package/utils/usage-source.js +50 -0
- package/utils/watch-locales.js +13 -9
package/main/i18ntk-validate.js
CHANGED
|
@@ -64,16 +64,23 @@ const { detectTranslationContentRisks } = require('../utils/validation-risk');
|
|
|
64
64
|
loadTranslations('en', path.resolve(__dirname, '..', 'ui-locales'));
|
|
65
65
|
|
|
66
66
|
class I18nValidator {
|
|
67
|
-
constructor(config = {}) {
|
|
68
|
-
this.config = config;
|
|
69
|
-
this.errors = [];
|
|
70
|
-
this.warnings = [];
|
|
71
|
-
this.keyNamingViolations = [];
|
|
72
|
-
this.rl = null;
|
|
73
|
-
|
|
67
|
+
constructor(config = {}) {
|
|
68
|
+
this.config = config;
|
|
69
|
+
this.errors = [];
|
|
70
|
+
this.warnings = [];
|
|
71
|
+
this.keyNamingViolations = [];
|
|
72
|
+
this.rl = null;
|
|
73
|
+
this.initialized = false;
|
|
74
|
+
this.pathsDisplayed = false;
|
|
75
|
+
this.validationBannerDisplayed = false;
|
|
76
|
+
}
|
|
74
77
|
|
|
75
|
-
async initialize() {
|
|
76
|
-
try {
|
|
78
|
+
async initialize() {
|
|
79
|
+
try {
|
|
80
|
+
if (this.initialized) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
77
84
|
// Initialize i18n with UI language first
|
|
78
85
|
const args = this.parseArgs();
|
|
79
86
|
if (args.help) {
|
|
@@ -120,7 +127,9 @@ class I18nValidator {
|
|
|
120
127
|
}
|
|
121
128
|
}
|
|
122
129
|
|
|
123
|
-
displayPaths({ sourceDir: this.sourceDir, i18nDir: this.i18nDir, outputDir: this.config.outputDir });
|
|
130
|
+
displayPaths({ sourceDir: this.sourceDir, i18nDir: this.i18nDir, outputDir: this.config.outputDir });
|
|
131
|
+
this.pathsDisplayed = true;
|
|
132
|
+
this.initialized = true;
|
|
124
133
|
|
|
125
134
|
SecurityUtils.logSecurityEvent(
|
|
126
135
|
'I18n validator initialized successfully',
|
|
@@ -164,12 +173,13 @@ class I18nValidator {
|
|
|
164
173
|
const args = process.argv.slice(2);
|
|
165
174
|
args.forEach(arg => {
|
|
166
175
|
const sanitizedArg = SecurityUtils.sanitizeInput(arg);
|
|
167
|
-
if (sanitizedArg.startsWith('--')
|
|
176
|
+
if (sanitizedArg.startsWith('--enforce-key-style')) {
|
|
177
|
+
const val = arg.split('=')[1];
|
|
178
|
+
baseArgs.enforceKeyStyle = val === undefined ? true : val !== 'false';
|
|
179
|
+
} else if (sanitizedArg.startsWith('--') && !sanitizedArg.includes('=')) {
|
|
168
180
|
const key = sanitizedArg.substring(2);
|
|
169
181
|
if (['en', 'de', 'es', 'fr', 'ru', 'ja', 'zh'].includes(key)) {
|
|
170
182
|
baseArgs.uiLanguage = key;
|
|
171
|
-
} else if (key === 'enforce-key-style') {
|
|
172
|
-
baseArgs.enforceKeyStyle = true;
|
|
173
183
|
}
|
|
174
184
|
}
|
|
175
185
|
});
|
|
@@ -242,7 +252,7 @@ class I18nValidator {
|
|
|
242
252
|
const files = items
|
|
243
253
|
.filter(item => {
|
|
244
254
|
return item.isFile() && item.name.endsWith('.json') &&
|
|
245
|
-
!this.config.excludeFiles.includes(item.name);
|
|
255
|
+
(!Array.isArray(this.config.excludeFiles) || !this.config.excludeFiles.includes(item.name));
|
|
246
256
|
}).map(item => item.name);
|
|
247
257
|
|
|
248
258
|
return files;
|
|
@@ -691,7 +701,8 @@ class I18nValidator {
|
|
|
691
701
|
const violations = [];
|
|
692
702
|
for (const key of allKeys) {
|
|
693
703
|
const sanitizedKey = SecurityUtils.sanitizeInput(key);
|
|
694
|
-
|
|
704
|
+
const testKey = keyStyle === 'flat' ? sanitizedKey.split('.').pop() : sanitizedKey;
|
|
705
|
+
if (!regex.test(testKey)) {
|
|
695
706
|
violations.push({
|
|
696
707
|
key: sanitizedKey,
|
|
697
708
|
suggestedFix: this.suggestKeyFix(sanitizedKey, keyStyle),
|
|
@@ -729,7 +740,7 @@ class I18nValidator {
|
|
|
729
740
|
case 'kebab-case':
|
|
730
741
|
return segments.map(s => s.toLowerCase()).join('-');
|
|
731
742
|
case 'flat':
|
|
732
|
-
return segments.map(
|
|
743
|
+
return segments.map(s => s.toLowerCase()).join('');
|
|
733
744
|
default:
|
|
734
745
|
return sanitizedKey;
|
|
735
746
|
}
|
|
@@ -778,9 +789,12 @@ class I18nValidator {
|
|
|
778
789
|
const args = this.parseArgs();
|
|
779
790
|
const jsonOutput = new JsonOutput('validate');
|
|
780
791
|
|
|
781
|
-
if (!args.json) {
|
|
782
|
-
console.log(
|
|
783
|
-
console.log(t('validate.
|
|
792
|
+
if (!args.json) {
|
|
793
|
+
console.log('');
|
|
794
|
+
console.log(t('validate.title'));
|
|
795
|
+
if (!this.validationBannerDisplayed) {
|
|
796
|
+
console.log(t('validate.message'));
|
|
797
|
+
}
|
|
784
798
|
|
|
785
799
|
// Delete old validation report if it exists
|
|
786
800
|
const reportPath = path.join(process.cwd(), 'validation-report.txt');
|
|
@@ -810,11 +824,14 @@ class I18nValidator {
|
|
|
810
824
|
this.config.strictMode = true;
|
|
811
825
|
}
|
|
812
826
|
|
|
813
|
-
if (!args.json) {
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
827
|
+
if (!args.json) {
|
|
828
|
+
if (!this.pathsDisplayed) {
|
|
829
|
+
console.log(t('validate.sourceDirectory', { dir: this.sourceDir }));
|
|
830
|
+
}
|
|
831
|
+
console.log(t('validate.sourceLanguage', { sourceLanguage: this.config.sourceLanguage }));
|
|
832
|
+
console.log(t('validate.strictMode', { mode: this.config.strictMode ? 'ON' : 'OFF' }));
|
|
833
|
+
console.log('');
|
|
834
|
+
}
|
|
818
835
|
|
|
819
836
|
// Validate source language directory exists
|
|
820
837
|
SecurityUtils.validatePath(this.sourceLanguageDir);
|
|
@@ -1054,23 +1071,16 @@ class I18nValidator {
|
|
|
1054
1071
|
|
|
1055
1072
|
const args = this.parseArgs();
|
|
1056
1073
|
|
|
1074
|
+
try {
|
|
1057
1075
|
// Ensure config is always initialized
|
|
1058
1076
|
if (!this.config) {
|
|
1059
1077
|
this.config = {};
|
|
1060
1078
|
}
|
|
1061
1079
|
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
this.config =
|
|
1066
|
-
|
|
1067
|
-
const uiLanguage = (this.config && this.config.uiLanguage) || 'en';
|
|
1068
|
-
loadTranslations(uiLanguage, path.resolve(__dirname, '..', 'ui-locales'));
|
|
1069
|
-
this.sourceDir = this.config.sourceDir;
|
|
1070
|
-
this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
|
|
1071
|
-
} else {
|
|
1072
|
-
await this.initialize();
|
|
1073
|
-
}
|
|
1080
|
+
if (!this.initialized) {
|
|
1081
|
+
await this.initialize();
|
|
1082
|
+
}
|
|
1083
|
+
this.config.enforceKeyStyle = args.enforceKeyStyle !== undefined ? args.enforceKeyStyle : this.config.enforceKeyStyle;
|
|
1074
1084
|
|
|
1075
1085
|
// Skip admin authentication when called from menu
|
|
1076
1086
|
if (!fromMenu) {
|
|
@@ -1101,7 +1111,8 @@ class I18nValidator {
|
|
|
1101
1111
|
}
|
|
1102
1112
|
const execute = async () => {
|
|
1103
1113
|
|
|
1104
|
-
console.log(t('validate.startingValidationProcess'));
|
|
1114
|
+
console.log('\n' + t('validate.startingValidationProcess'));
|
|
1115
|
+
this.validationBannerDisplayed = true;
|
|
1105
1116
|
SecurityUtils.logSecurityEvent(
|
|
1106
1117
|
t('validate.runStarted'),
|
|
1107
1118
|
'info',
|
|
@@ -1146,6 +1157,7 @@ class I18nValidator {
|
|
|
1146
1157
|
);
|
|
1147
1158
|
throw error;
|
|
1148
1159
|
}
|
|
1160
|
+
}
|
|
1149
1161
|
}
|
|
1150
1162
|
|
|
1151
1163
|
|
|
@@ -17,6 +17,8 @@ const AdminCLI = require('../../../utils/admin-cli');
|
|
|
17
17
|
const AdminAuth = require('../../../utils/admin-auth');
|
|
18
18
|
const watchLocales = require('../../../utils/watch-locales');
|
|
19
19
|
const JsonOutput = require('../../../utils/json-output');
|
|
20
|
+
const configManager = require('../../../utils/config-manager');
|
|
21
|
+
const { normalizeReportFormat, writeReportFile } = require('../../../utils/report-writer');
|
|
20
22
|
|
|
21
23
|
loadTranslations('en', path.resolve(__dirname, '../../../ui-locales'));
|
|
22
24
|
|
|
@@ -752,23 +754,11 @@ class AnalyzeCommand {
|
|
|
752
754
|
return null;
|
|
753
755
|
}
|
|
754
756
|
|
|
755
|
-
|
|
756
|
-
const
|
|
757
|
-
const
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
if (!reportPath.startsWith(validatedOutputDir)) {
|
|
761
|
-
console.error('Invalid report path detected, potential directory traversal attack');
|
|
762
|
-
return null;
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
// Use safeWriteFile for secure file writing
|
|
766
|
-
const success = await SecurityUtils.safeWriteFile(reportPath, JSON.stringify(report, null, 2), process.cwd(), 'utf8');
|
|
767
|
-
if (!success) {
|
|
768
|
-
throw new Error(t('analyze.failedToWriteReportFile') || 'Failed to write report file securely');
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
return reportPath;
|
|
757
|
+
const safeLanguage = language.replace(/[^\w-]/g, '_');
|
|
758
|
+
const settings = configManager.getConfig ? configManager.getConfig() : {};
|
|
759
|
+
const format = normalizeReportFormat(this.config?.reports?.format || settings.reports?.format || this.config?.reportFormat || 'markdown');
|
|
760
|
+
const reportPath = await writeReportFile(validatedOutputDir, `translation-report-${safeLanguage}`, report, { format, title: `Translation Report ${safeLanguage.toUpperCase()}` });
|
|
761
|
+
return reportPath;
|
|
772
762
|
|
|
773
763
|
} catch (error) {
|
|
774
764
|
console.error(`Failed to save report for ${language}:`, error.message);
|
|
@@ -119,11 +119,11 @@ class CommandRouter {
|
|
|
119
119
|
return true;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
/**
|
|
123
|
-
* Execute a command with proper routing and error handling
|
|
124
|
-
*/
|
|
125
|
-
async executeCommand(command, options = {}) {
|
|
126
|
-
console.log(t('menu.executingCommand', { command }));
|
|
122
|
+
/**
|
|
123
|
+
* Execute a command with proper routing and error handling
|
|
124
|
+
*/
|
|
125
|
+
async executeCommand(command, options = {}) {
|
|
126
|
+
console.log('\n' + t('menu.executingCommand', { command }));
|
|
127
127
|
|
|
128
128
|
// Enhanced context detection
|
|
129
129
|
const executionContext = this.getExecutionContext(options);
|
|
@@ -161,7 +161,7 @@ class CommandRouter {
|
|
|
161
161
|
const result = await this.routeCommand(command, options, executionContext);
|
|
162
162
|
|
|
163
163
|
// Handle command completion based on execution context
|
|
164
|
-
console.log(t('operations.completed'));
|
|
164
|
+
console.log('\n' + t('operations.completed'));
|
|
165
165
|
|
|
166
166
|
if (isManagerExecution && !this.isNonInteractiveMode && this.prompt) {
|
|
167
167
|
// Interactive menu execution - return to menu
|
|
@@ -11,7 +11,9 @@ const path = require('path');
|
|
|
11
11
|
const SecurityUtils = require('../../../utils/security');
|
|
12
12
|
const configManager = require('../../../utils/config-manager');
|
|
13
13
|
const { getUnifiedConfig } = require('../../../utils/config-helper');
|
|
14
|
-
const { loadTranslations } = require('../../../utils/i18n-helper');
|
|
14
|
+
const { loadTranslations, t } = require('../../../utils/i18n-helper');
|
|
15
|
+
const { parseConfirmation } = require('../../../utils/localized-confirm');
|
|
16
|
+
const { DEFAULT_CONCURRENCY, getProviderConcurrencyLimit } = require('../../../utils/translate/api');
|
|
15
17
|
const SetupEnforcer = require('../../../utils/setup-enforcer');
|
|
16
18
|
const {
|
|
17
19
|
createProtectionFile,
|
|
@@ -38,6 +40,11 @@ class TranslateCommand {
|
|
|
38
40
|
this.safeClose = safeClose;
|
|
39
41
|
}
|
|
40
42
|
|
|
43
|
+
tr(key, replacements = {}, fallback = '') {
|
|
44
|
+
const value = t(key, replacements);
|
|
45
|
+
return value && value !== key ? value : fallback;
|
|
46
|
+
}
|
|
47
|
+
|
|
41
48
|
async execute(options = {}) {
|
|
42
49
|
try {
|
|
43
50
|
await SetupEnforcer.checkSetupCompleteAsync();
|
|
@@ -46,15 +53,15 @@ class TranslateCommand {
|
|
|
46
53
|
return { success: false, error: 'Setup required' };
|
|
47
54
|
}
|
|
48
55
|
|
|
49
|
-
loadTranslations('en', path.resolve(__dirname, '..', '..', '..', 'ui-locales'));
|
|
50
|
-
|
|
51
56
|
const config = this.config || {};
|
|
52
57
|
let unified;
|
|
53
58
|
try {
|
|
54
|
-
unified = await getUnifiedConfig('translate', options);
|
|
59
|
+
unified = { ...(await getUnifiedConfig('translate', options)), ...config };
|
|
55
60
|
} catch (_) {
|
|
56
61
|
unified = config;
|
|
57
62
|
}
|
|
63
|
+
const uiLanguage = unified.uiLanguage || unified.language || 'en';
|
|
64
|
+
loadTranslations(uiLanguage, path.resolve(__dirname, '..', '..', '..', 'ui-locales'));
|
|
58
65
|
this.autoTranslateSettings = this.getAutoTranslateSettings(unified);
|
|
59
66
|
|
|
60
67
|
const defaultSourceDir = unified.sourceDir || unified.i18nDir || path.resolve(process.cwd(), 'locales', 'en');
|
|
@@ -62,13 +69,13 @@ class TranslateCommand {
|
|
|
62
69
|
this.configuredTargetLangs = this.getConfiguredTargetLanguages(unified, defaultSourceDir);
|
|
63
70
|
|
|
64
71
|
console.log('\n============================================================');
|
|
65
|
-
console.log('
|
|
72
|
+
console.log(` ${t('translate.title') || '\u{1F310} Auto Translate'}`);
|
|
66
73
|
console.log('============================================================');
|
|
67
74
|
|
|
68
75
|
if (this.isNonInteractiveMode) {
|
|
69
76
|
this.sourceDir = defaultSourceDir;
|
|
70
77
|
if (!SecurityUtils.safeExistsSync(this.sourceDir, path.dirname(this.sourceDir))) {
|
|
71
|
-
console.error(`Source locale directory not found: ${this.sourceDir}`);
|
|
78
|
+
console.error(this.tr('translate.errors.sourceDirectoryNotFound', { dir: this.sourceDir }, `Source locale directory not found: ${this.sourceDir}`));
|
|
72
79
|
return { success: false, error: 'Source directory not found' };
|
|
73
80
|
}
|
|
74
81
|
const resolvedSource = this.resolveSourceDirectoryForLanguage(this.sourceDir, this.sourceLang);
|
|
@@ -105,25 +112,25 @@ class TranslateCommand {
|
|
|
105
112
|
|
|
106
113
|
async promptSourceDir(ask, defaultDir) {
|
|
107
114
|
while (true) {
|
|
108
|
-
console.log('\n Source locale directory');
|
|
109
|
-
console.log(`
|
|
110
|
-
console.log(`
|
|
111
|
-
console.log(' Accepted: an absolute path, or a path relative to the current project.');
|
|
112
|
-
console.log(' Examples:');
|
|
115
|
+
console.log('\n ' + this.tr('translate.sourceDirectory.title', {}, 'Source locale directory'));
|
|
116
|
+
console.log(' ' + this.tr('translate.common.default', { value: defaultDir }, `Default: ${defaultDir}`));
|
|
117
|
+
console.log(' ' + this.tr('translate.sourceDirectory.currentProject', { dir: process.cwd() }, `Current project: ${process.cwd()}`));
|
|
118
|
+
console.log(' ' + this.tr('translate.sourceDirectory.accepted', {}, 'Accepted: an absolute path, or a path relative to the current project.'));
|
|
119
|
+
console.log(' ' + this.tr('translate.common.examples', {}, 'Examples:'));
|
|
113
120
|
console.log(' ./locales/en');
|
|
114
|
-
console.log(' ./locales (then choose source language: en)');
|
|
121
|
+
console.log(' ' + this.tr('translate.sourceDirectory.localeRootExample', {}, './locales (then choose source language: en)'));
|
|
115
122
|
console.log(` ${defaultDir}`);
|
|
116
|
-
console.log(' The folder can contain JSON files directly, or language folders such as ./locales/en.');
|
|
117
|
-
console.log(' Press Enter to use the default.');
|
|
123
|
+
console.log(' ' + this.tr('translate.sourceDirectory.folderHint', {}, 'The folder can contain JSON files directly, or language folders such as ./locales/en.'));
|
|
124
|
+
console.log(' ' + this.tr('translate.common.pressEnterDefault', {}, 'Press Enter to use the default.'));
|
|
118
125
|
const input = await ask(' > ');
|
|
119
126
|
|
|
120
127
|
if (!input.trim()) {
|
|
121
128
|
if (!SecurityUtils.safeExistsSync(defaultDir, path.dirname(defaultDir))) {
|
|
122
|
-
console.log(`
|
|
123
|
-
console.log(' Please enter an existing directory with JSON locale files.');
|
|
129
|
+
console.log(' ' + this.tr('translate.sourceDirectory.defaultNotFound', { dir: defaultDir }, `Default directory not found: ${defaultDir}`));
|
|
130
|
+
console.log(' ' + this.tr('translate.sourceDirectory.enterExisting', {}, 'Please enter an existing directory with JSON locale files.'));
|
|
124
131
|
continue;
|
|
125
132
|
}
|
|
126
|
-
console.log(`
|
|
133
|
+
console.log(' ' + this.tr('translate.common.usingDefault', { value: defaultDir }, `Using default: ${defaultDir}`));
|
|
127
134
|
return defaultDir;
|
|
128
135
|
}
|
|
129
136
|
|
|
@@ -132,31 +139,31 @@ class TranslateCommand {
|
|
|
132
139
|
? path.resolve(cleanInput)
|
|
133
140
|
: path.resolve(process.cwd(), cleanInput);
|
|
134
141
|
if (!SecurityUtils.safeExistsSync(resolved, path.dirname(resolved))) {
|
|
135
|
-
console.log(`
|
|
136
|
-
console.log(' Enter an existing folder, for example ./locales/en.');
|
|
142
|
+
console.log(' ' + this.tr('translate.sourceDirectory.directoryNotFound', { dir: resolved }, `Directory not found: ${resolved}`));
|
|
143
|
+
console.log(' ' + this.tr('translate.sourceDirectory.enterFolderExample', {}, 'Enter an existing folder, for example ./locales/en.'));
|
|
137
144
|
continue;
|
|
138
145
|
}
|
|
139
146
|
const stats = SecurityUtils.safeStatSync(resolved, path.dirname(resolved));
|
|
140
147
|
if (!stats || !stats.isDirectory()) {
|
|
141
|
-
console.log(`
|
|
148
|
+
console.log(' ' + this.tr('translate.sourceDirectory.notDirectory', { dir: resolved }, `Not a directory: ${resolved}`));
|
|
142
149
|
continue;
|
|
143
150
|
}
|
|
144
|
-
console.log(`
|
|
151
|
+
console.log(' ' + this.tr('translate.sourceDirectory.using', { dir: resolved }, `Using source directory: ${resolved}`));
|
|
145
152
|
return resolved;
|
|
146
153
|
}
|
|
147
154
|
}
|
|
148
155
|
|
|
149
156
|
async promptSourceLang(ask) {
|
|
150
157
|
while (true) {
|
|
151
|
-
console.log('\n Source language code');
|
|
152
|
-
console.log(`
|
|
153
|
-
console.log(' This should match the language of the source JSON values.');
|
|
154
|
-
console.log(' Example: en');
|
|
155
|
-
console.log(' Press Enter to use the default.');
|
|
158
|
+
console.log('\n ' + this.tr('translate.sourceLanguage.title', {}, 'Source language code'));
|
|
159
|
+
console.log(' ' + this.tr('translate.common.default', { value: this.sourceLang }, `Default: ${this.sourceLang}`));
|
|
160
|
+
console.log(' ' + this.tr('translate.sourceLanguage.hint', {}, 'This should match the language of the source JSON values.'));
|
|
161
|
+
console.log(' ' + this.tr('translate.common.exampleValue', { value: 'en' }, 'Example: en'));
|
|
162
|
+
console.log(' ' + this.tr('translate.common.pressEnterDefault', {}, 'Press Enter to use the default.'));
|
|
156
163
|
const input = await ask(' > ');
|
|
157
164
|
|
|
158
165
|
if (!input.trim()) {
|
|
159
|
-
console.log(`
|
|
166
|
+
console.log(' ' + this.tr('translate.sourceLanguage.using', { lang: this.sourceLang }, `Using source language: ${this.sourceLang}`));
|
|
160
167
|
return this.sourceLang;
|
|
161
168
|
}
|
|
162
169
|
|
|
@@ -164,46 +171,46 @@ class TranslateCommand {
|
|
|
164
171
|
if (lang.length >= 2) {
|
|
165
172
|
return lang;
|
|
166
173
|
}
|
|
167
|
-
console.log(' Invalid language code. Use 2+ characters (e.g. en, de, fr).');
|
|
174
|
+
console.log(' ' + this.tr('translate.sourceLanguage.invalid', {}, 'Invalid language code. Use 2+ characters (e.g. en, de, fr).'));
|
|
168
175
|
}
|
|
169
176
|
}
|
|
170
177
|
|
|
171
178
|
async interactiveFlow(jsonFiles, ask) {
|
|
172
179
|
await this.maybeConfigureProtection(ask);
|
|
173
180
|
|
|
174
|
-
console.log('\n Target language(s)');
|
|
181
|
+
console.log('\n ' + this.tr('translate.targetLanguages.title', {}, 'Target language(s)'));
|
|
175
182
|
if (this.configuredTargetLangs.length > 0) {
|
|
176
|
-
console.log(`
|
|
183
|
+
console.log(' ' + this.tr('translate.targetLanguages.allConfigured', { languages: this.configuredTargetLangs.join(', ') }, `a) All configured target languages: ${this.configuredTargetLangs.join(', ')}`));
|
|
177
184
|
} else {
|
|
178
|
-
console.log(' a) All configured target languages: none configured');
|
|
185
|
+
console.log(' ' + this.tr('translate.targetLanguages.noneConfigured', {}, 'a) All configured target languages: none configured'));
|
|
179
186
|
}
|
|
180
|
-
console.log(' Or enter one or more comma/space-separated language codes.');
|
|
181
|
-
console.log(' Examples: de, es, fr or de es fr or zh');
|
|
182
|
-
console.log(`
|
|
187
|
+
console.log(' ' + this.tr('translate.targetLanguages.enterCodes', {}, 'Or enter one or more comma/space-separated language codes.'));
|
|
188
|
+
console.log(' ' + this.tr('translate.common.examplesInline', { examples: 'de, es, fr or de es fr or zh' }, 'Examples: de, es, fr or de es fr or zh'));
|
|
189
|
+
console.log(' ' + this.tr('translate.targetLanguages.sourceExcluded', { lang: this.sourceLang }, `Source language "${this.sourceLang}" will be excluded automatically.`));
|
|
183
190
|
const langInput = await ask(' > ');
|
|
184
191
|
|
|
185
192
|
const targetLangs = this.parseTargetLanguages(langInput);
|
|
186
193
|
|
|
187
194
|
if (targetLangs.length === 0) {
|
|
188
|
-
console.log(' No valid target languages selected. Aborting.');
|
|
195
|
+
console.log(' ' + this.tr('translate.targetLanguages.noneSelected', {}, 'No valid target languages selected. Aborting.'));
|
|
189
196
|
if (this.configuredTargetLangs.length === 0) {
|
|
190
|
-
console.log(' Configure defaultLanguages in .i18ntk-config, or enter target codes manually.');
|
|
197
|
+
console.log(' ' + this.tr('translate.targetLanguages.configureHint', {}, 'Configure defaultLanguages in .i18ntk-config, or enter target codes manually.'));
|
|
191
198
|
}
|
|
192
199
|
return { success: false, error: 'Invalid language code' };
|
|
193
200
|
}
|
|
194
201
|
|
|
195
|
-
console.log(
|
|
202
|
+
console.log('\n ' + this.tr('translate.targetLanguages.selected', { languages: targetLangs.join(', ') }, `Target languages: ${targetLangs.join(', ')}`));
|
|
196
203
|
|
|
197
|
-
console.log(
|
|
204
|
+
console.log('\n ' + this.tr('translate.files.title', {}, 'Which file(s) to translate?'));
|
|
198
205
|
const filePreview = jsonFiles.length <= 6
|
|
199
206
|
? jsonFiles.join(', ')
|
|
200
207
|
: `${jsonFiles.slice(0, 6).join(', ')}, ...`;
|
|
201
|
-
console.log(`
|
|
208
|
+
console.log(' ' + this.tr('translate.files.all', { count: jsonFiles.length, files: filePreview }, `a) All JSON files (${jsonFiles.length}: ${filePreview})`));
|
|
202
209
|
jsonFiles.forEach((f, i) => {
|
|
203
210
|
console.log(` ${i + 1}) ${f}`);
|
|
204
211
|
});
|
|
205
212
|
|
|
206
|
-
const fileChoice = await ask('\n Choice [a/all or file number]: ');
|
|
213
|
+
const fileChoice = await ask('\n ' + this.tr('translate.files.choicePrompt', {}, 'Choice [a/all or file number]: '));
|
|
207
214
|
let sourceFiles;
|
|
208
215
|
|
|
209
216
|
if (['a', 'all', '*'].includes(fileChoice.trim().toLowerCase())) {
|
|
@@ -211,7 +218,7 @@ class TranslateCommand {
|
|
|
211
218
|
} else {
|
|
212
219
|
const idx = parseInt(fileChoice, 10) - 1;
|
|
213
220
|
if (isNaN(idx) || idx < 0 || idx >= jsonFiles.length) {
|
|
214
|
-
console.log(' Invalid choice. Aborting.');
|
|
221
|
+
console.log(' ' + this.tr('translate.files.invalidChoice', {}, 'Invalid choice. Aborting.'));
|
|
215
222
|
return { success: false, error: 'Invalid file choice' };
|
|
216
223
|
}
|
|
217
224
|
sourceFiles = [path.join(this.sourceDir, jsonFiles[idx])];
|
|
@@ -220,39 +227,39 @@ class TranslateCommand {
|
|
|
220
227
|
if (this.autoTranslateSettings.dryRunFirst !== false) {
|
|
221
228
|
// Dry-run for first language only (all languages use same source so same keys)
|
|
222
229
|
const firstLang = targetLangs[0];
|
|
223
|
-
console.log(
|
|
230
|
+
console.log('\n ' + this.tr('translate.dryRun.previewFor', { lang: firstLang }, `Dry-run preview for "${firstLang}"...`) + '\n');
|
|
224
231
|
await this.runTranslate(sourceFiles, firstLang, { dryRun: true });
|
|
225
232
|
}
|
|
226
233
|
|
|
227
|
-
console.log('\n Proceed with actual translation?');
|
|
228
|
-
const answer = await ask(' [y]es / [n]o: ');
|
|
229
|
-
if (
|
|
230
|
-
console.log(' Translation cancelled.');
|
|
234
|
+
console.log('\n ' + this.tr('translate.confirm.proceed', {}, 'Proceed with actual translation?'));
|
|
235
|
+
const answer = await ask(' ' + this.tr('translate.confirm.yesNoPrompt', {}, '[y]es / [n]o: '));
|
|
236
|
+
if (!parseConfirmation(answer, { language: this.config.uiLanguage || this.config.language || 'en', defaultValue: false })) {
|
|
237
|
+
console.log(' ' + this.tr('translate.confirm.cancelled', {}, 'Translation cancelled.'));
|
|
231
238
|
return { success: true, cancelled: true };
|
|
232
239
|
}
|
|
233
240
|
|
|
234
241
|
let results = [];
|
|
235
242
|
for (const lang of targetLangs) {
|
|
236
|
-
console.log(
|
|
243
|
+
console.log('\n ' + this.tr('translate.run.translatingTo', { lang }, `Translating to "${lang}"...`) + '\n');
|
|
237
244
|
try {
|
|
238
245
|
await this.runTranslate(sourceFiles, lang, { dryRun: false });
|
|
239
246
|
results.push({ lang, ok: true });
|
|
240
247
|
} catch (e) {
|
|
241
|
-
console.error(`
|
|
248
|
+
console.error(' ' + this.tr('translate.run.failedFor', { lang, error: e.message }, `Failed for "${lang}": ${e.message}`));
|
|
242
249
|
results.push({ lang, ok: false, error: e.message });
|
|
243
250
|
}
|
|
244
251
|
}
|
|
245
252
|
|
|
246
|
-
console.log('\n Summary:');
|
|
253
|
+
console.log('\n ' + this.tr('translate.summary.title', {}, 'Summary:'));
|
|
247
254
|
for (const r of results) {
|
|
248
255
|
console.log(` ${r.ok ? '\u{2705}' : '\u{274C}'} ${r.lang}${r.error ? ' (' + r.error + ')' : ''}`);
|
|
249
256
|
}
|
|
250
|
-
console.log('\n Translation complete!');
|
|
257
|
+
console.log('\n ' + this.tr('translate.summary.complete', {}, 'Translation complete!'));
|
|
251
258
|
return { success: true, results };
|
|
252
259
|
}
|
|
253
260
|
|
|
254
261
|
async nonInteractiveFlow(jsonFiles) {
|
|
255
|
-
console.log('\n Non-interactive mode. Use direct CLI instead:');
|
|
262
|
+
console.log('\n ' + this.tr('translate.nonInteractive.useDirect', {}, 'Non-interactive mode. Use direct CLI instead:'));
|
|
256
263
|
console.log(' i18ntk-translate <source> <lang> [options]');
|
|
257
264
|
return { success: false, error: 'Non-interactive mode not supported from menu' };
|
|
258
265
|
}
|
|
@@ -278,8 +285,8 @@ class TranslateCommand {
|
|
|
278
285
|
const languageJsonFiles = this.getJsonFiles(languageDir);
|
|
279
286
|
if (languageJsonFiles.length > 0) {
|
|
280
287
|
if (shouldLog) {
|
|
281
|
-
console.log(`
|
|
282
|
-
console.log(`
|
|
288
|
+
console.log(' ' + this.tr('translate.sourceDirectory.noJsonDirect', { dir: selectedDir }, `No JSON files found directly in: ${selectedDir}`));
|
|
289
|
+
console.log(' ' + this.tr('translate.sourceDirectory.usingLanguageFolder', { dir: languageDir }, `Using source language folder: ${languageDir}`));
|
|
283
290
|
}
|
|
284
291
|
return { ok: true, sourceDir: languageDir, jsonFiles: languageJsonFiles };
|
|
285
292
|
}
|
|
@@ -288,8 +295,8 @@ class TranslateCommand {
|
|
|
288
295
|
|
|
289
296
|
const checkedLanguageDir = cleanLang ? path.join(selectedDir, cleanLang) : null;
|
|
290
297
|
const message = checkedLanguageDir
|
|
291
|
-
? `No JSON files found in: ${selectedDir}\nAlso checked source language folder: ${checkedLanguageDir}`
|
|
292
|
-
: `No JSON files found in: ${selectedDir}
|
|
298
|
+
? this.tr('translate.sourceDirectory.noJsonWithLanguageFolder', { dir: selectedDir, languageDir: checkedLanguageDir }, `No JSON files found in: ${selectedDir}\nAlso checked source language folder: ${checkedLanguageDir}`)
|
|
299
|
+
: this.tr('translate.sourceDirectory.noJson', { dir: selectedDir }, `No JSON files found in: ${selectedDir}`);
|
|
293
300
|
return { ok: false, sourceDir: selectedDir, jsonFiles: [], message };
|
|
294
301
|
}
|
|
295
302
|
|
|
@@ -343,13 +350,14 @@ class TranslateCommand {
|
|
|
343
350
|
placeholderMode: ['preserve', 'skip', 'send'].includes(settings.placeholderMode)
|
|
344
351
|
? settings.placeholderMode
|
|
345
352
|
: 'preserve',
|
|
346
|
-
concurrency: this.toInt(settings.concurrency,
|
|
353
|
+
concurrency: this.toInt(settings.concurrency, DEFAULT_CONCURRENCY, 1, getProviderConcurrencyLimit(settings.provider || 'google')),
|
|
347
354
|
batchSize: this.toInt(settings.batchSize, 100, 1, 10000),
|
|
348
355
|
progressInterval: this.toInt(settings.progressInterval, 25, 1, 10000),
|
|
349
356
|
retryCount: this.toInt(settings.retryCount, 3, 0, 10),
|
|
350
357
|
retryDelay: this.toInt(settings.retryDelay, 1000, 0, 30000),
|
|
351
358
|
timeout: this.toInt(settings.timeout, 15000, 1000, 120000),
|
|
352
359
|
dryRunFirst: settings.dryRunFirst !== false,
|
|
360
|
+
onlyMissingOrEnglish: settings.onlyMissingOrEnglish !== false,
|
|
353
361
|
reportStdout: settings.reportStdout !== false,
|
|
354
362
|
bom: settings.bom === true,
|
|
355
363
|
protectionEnabled: settings.protectionEnabled !== false,
|
|
@@ -480,6 +488,7 @@ class TranslateCommand {
|
|
|
480
488
|
args.noConfirm = true;
|
|
481
489
|
args.sourceLang = this.sourceLang || 'en';
|
|
482
490
|
args.dryRun = opts.dryRun === true;
|
|
491
|
+
args.onlyMissingOrEnglish = settings.onlyMissingOrEnglish;
|
|
483
492
|
args.reportStdout = settings.reportStdout;
|
|
484
493
|
args.bom = settings.bom;
|
|
485
494
|
args.concurrency = settings.concurrency;
|
|
@@ -37,8 +37,11 @@ class ValidateCommand {
|
|
|
37
37
|
this.warnings = [];
|
|
38
38
|
this.rl = null;
|
|
39
39
|
this.sourceDir = null;
|
|
40
|
-
this.i18nDir = null;
|
|
41
|
-
this.sourceLanguageDir = null;
|
|
40
|
+
this.i18nDir = null;
|
|
41
|
+
this.sourceLanguageDir = null;
|
|
42
|
+
this.initialized = false;
|
|
43
|
+
this.pathsDisplayed = false;
|
|
44
|
+
this.validationBannerDisplayed = false;
|
|
42
45
|
}
|
|
43
46
|
|
|
44
47
|
/**
|
|
@@ -53,8 +56,12 @@ class ValidateCommand {
|
|
|
53
56
|
/**
|
|
54
57
|
* Initialize the validator with configuration
|
|
55
58
|
*/
|
|
56
|
-
async initialize() {
|
|
57
|
-
try {
|
|
59
|
+
async initialize() {
|
|
60
|
+
try {
|
|
61
|
+
if (this.initialized) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
58
65
|
// Initialize i18n with UI language first
|
|
59
66
|
const args = this.parseArgs();
|
|
60
67
|
if (args.help) {
|
|
@@ -101,7 +108,9 @@ class ValidateCommand {
|
|
|
101
108
|
}
|
|
102
109
|
}
|
|
103
110
|
|
|
104
|
-
displayPaths({ sourceDir: this.sourceDir, i18nDir: this.i18nDir, outputDir: this.config.outputDir });
|
|
111
|
+
displayPaths({ sourceDir: this.sourceDir, i18nDir: this.i18nDir, outputDir: this.config.outputDir });
|
|
112
|
+
this.pathsDisplayed = true;
|
|
113
|
+
this.initialized = true;
|
|
105
114
|
|
|
106
115
|
SecurityUtils.logSecurityEvent(
|
|
107
116
|
'I18n validator initialized successfully',
|
|
@@ -673,9 +682,12 @@ class ValidateCommand {
|
|
|
673
682
|
const args = this.parseArgs();
|
|
674
683
|
const jsonOutput = new JsonOutput('validate');
|
|
675
684
|
|
|
676
|
-
if (!args.json) {
|
|
677
|
-
console.log(
|
|
678
|
-
console.log(t('validate.
|
|
685
|
+
if (!args.json) {
|
|
686
|
+
console.log('');
|
|
687
|
+
console.log(t('validate.title'));
|
|
688
|
+
if (!this.validationBannerDisplayed) {
|
|
689
|
+
console.log(t('validate.message'));
|
|
690
|
+
}
|
|
679
691
|
|
|
680
692
|
// Delete old validation report if it exists
|
|
681
693
|
const reportPath = path.join(process.cwd(), 'validation-report.txt');
|
|
@@ -705,11 +717,14 @@ class ValidateCommand {
|
|
|
705
717
|
this.config.strictMode = true;
|
|
706
718
|
}
|
|
707
719
|
|
|
708
|
-
if (!args.json) {
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
720
|
+
if (!args.json) {
|
|
721
|
+
if (!this.pathsDisplayed) {
|
|
722
|
+
console.log(t('validate.sourceDirectory', { dir: this.sourceDir }));
|
|
723
|
+
}
|
|
724
|
+
console.log(t('validate.sourceLanguage', { sourceLanguage: this.config.sourceLanguage }));
|
|
725
|
+
console.log(t('validate.strictMode', { mode: this.config.strictMode ? 'ON' : 'OFF' }));
|
|
726
|
+
console.log('');
|
|
727
|
+
}
|
|
713
728
|
|
|
714
729
|
// Validate source language directory exists
|
|
715
730
|
SecurityUtils.validatePath(this.sourceLanguageDir);
|
|
@@ -933,17 +948,9 @@ class ValidateCommand {
|
|
|
933
948
|
this.config = {};
|
|
934
949
|
}
|
|
935
950
|
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
this.config = { ...baseConfig, ...this.config };
|
|
940
|
-
|
|
941
|
-
const uiLanguage = (this.config && this.config.uiLanguage) || 'en';
|
|
942
|
-
loadTranslations(uiLanguage, path.resolve(__dirname, '../../../resources', 'i18n', 'ui-locales'));this.sourceDir = this.config.sourceDir;
|
|
943
|
-
this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
|
|
944
|
-
} else {
|
|
945
|
-
await this.initialize();
|
|
946
|
-
}
|
|
951
|
+
if (!this.initialized) {
|
|
952
|
+
await this.initialize();
|
|
953
|
+
}
|
|
947
954
|
|
|
948
955
|
// Skip admin authentication when called from menu
|
|
949
956
|
if (!fromMenu) {
|
|
@@ -972,8 +979,9 @@ class ValidateCommand {
|
|
|
972
979
|
}
|
|
973
980
|
const execute = async () => {
|
|
974
981
|
|
|
975
|
-
console.log(t('validate.startingValidationProcess'));
|
|
976
|
-
|
|
982
|
+
console.log('\n' + t('validate.startingValidationProcess'));
|
|
983
|
+
this.validationBannerDisplayed = true;
|
|
984
|
+
SecurityUtils.logSecurityEvent(
|
|
977
985
|
t('validate.runStarted'),
|
|
978
986
|
'info',
|
|
979
987
|
{ message: 'Starting validation run' }
|