i18ntk 1.10.2 → 2.0.2
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/LICENSE +1 -1
- package/README.md +141 -1191
- package/main/i18ntk-analyze.js +65 -84
- package/main/i18ntk-backup-class.js +420 -0
- package/main/i18ntk-backup.js +3 -3
- package/main/i18ntk-complete.js +90 -65
- package/main/i18ntk-doctor.js +123 -103
- package/main/i18ntk-fixer.js +61 -725
- package/main/i18ntk-go.js +14 -15
- package/main/i18ntk-init.js +77 -26
- package/main/i18ntk-java.js +27 -32
- package/main/i18ntk-js.js +70 -68
- package/main/i18ntk-manage.js +129 -30
- package/main/i18ntk-php.js +75 -75
- package/main/i18ntk-py.js +55 -56
- package/main/i18ntk-scanner.js +59 -57
- package/main/i18ntk-setup.js +9 -404
- package/main/i18ntk-sizing.js +6 -6
- package/main/i18ntk-summary.js +21 -18
- package/main/i18ntk-ui.js +11 -10
- package/main/i18ntk-usage.js +54 -18
- package/main/i18ntk-validate.js +13 -13
- package/main/manage/commands/AnalyzeCommand.js +1124 -0
- package/main/manage/commands/BackupCommand.js +62 -0
- package/main/manage/commands/CommandRouter.js +295 -0
- package/main/manage/commands/CompleteCommand.js +61 -0
- package/main/manage/commands/DoctorCommand.js +60 -0
- package/main/manage/commands/FixerCommand.js +624 -0
- package/main/manage/commands/InitCommand.js +62 -0
- package/main/manage/commands/ScannerCommand.js +654 -0
- package/main/manage/commands/SizingCommand.js +60 -0
- package/main/manage/commands/SummaryCommand.js +61 -0
- package/main/manage/commands/UsageCommand.js +60 -0
- package/main/manage/commands/ValidateCommand.js +978 -0
- package/main/manage/index-fixed.js +1447 -0
- package/main/manage/index.js +1462 -0
- package/main/manage/managers/DebugMenu.js +140 -0
- package/main/manage/managers/InteractiveMenu.js +177 -0
- package/main/manage/managers/LanguageMenu.js +62 -0
- package/main/manage/managers/SettingsMenu.js +53 -0
- package/main/manage/services/AuthenticationService.js +263 -0
- package/main/manage/services/ConfigurationService-fixed.js +449 -0
- package/main/manage/services/ConfigurationService.js +449 -0
- package/main/manage/services/FileManagementService.js +368 -0
- package/main/manage/services/FrameworkDetectionService.js +458 -0
- package/main/manage/services/InitService.js +1051 -0
- package/main/manage/services/SetupService.js +462 -0
- package/main/manage/services/SummaryService.js +450 -0
- package/main/manage/services/UsageService.js +1502 -0
- package/package.json +32 -29
- package/runtime/enhanced.d.ts +221 -221
- package/runtime/index.d.ts +29 -29
- package/runtime/index.full.d.ts +331 -331
- package/runtime/index.js +7 -6
- package/scripts/build-lite.js +17 -17
- package/scripts/deprecate-versions.js +23 -6
- package/scripts/export-translations.js +5 -5
- package/scripts/fix-all-i18n.js +3 -3
- package/scripts/fix-and-purify-i18n.js +3 -2
- package/scripts/fix-locale-control-chars.js +110 -0
- package/scripts/lint-locales.js +80 -0
- package/scripts/locale-optimizer.js +8 -8
- package/scripts/prepublish.js +21 -21
- package/scripts/security-check.js +117 -117
- package/scripts/sync-translations.js +4 -4
- package/scripts/sync-ui-locales.js +9 -8
- package/scripts/validate-all-translations.js +8 -7
- package/scripts/verify-deprecations.js +157 -161
- package/scripts/verify-translations.js +6 -5
- package/settings/i18ntk-config.json +282 -282
- package/settings/language-config.json +5 -5
- package/settings/settings-cli.js +9 -9
- package/settings/settings-manager.js +18 -18
- package/ui-locales/de.json +2417 -2348
- package/ui-locales/en.json +2415 -2352
- package/ui-locales/es.json +2425 -2353
- package/ui-locales/fr.json +2418 -2348
- package/ui-locales/ja.json +2463 -2361
- package/ui-locales/ru.json +2463 -2359
- package/ui-locales/zh.json +2418 -2351
- package/utils/admin-auth.js +2 -2
- package/utils/admin-cli.js +297 -297
- package/utils/admin-pin.js +9 -9
- package/utils/cli-helper.js +9 -9
- package/utils/config-helper.js +73 -104
- package/utils/config-manager.js +204 -171
- package/utils/config.js +5 -4
- package/utils/env-manager.js +249 -263
- package/utils/framework-detector.js +27 -24
- package/utils/i18n-helper.js +85 -41
- package/utils/init-helper.js +152 -94
- package/utils/json-output.js +98 -98
- package/utils/mini-commander.js +179 -0
- package/utils/missing-key-validator.js +5 -5
- package/utils/plugin-loader.js +40 -29
- package/utils/prompt.js +14 -44
- package/utils/safe-json.js +40 -0
- package/utils/secure-errors.js +3 -3
- package/utils/security-check-improved.js +390 -0
- package/utils/security-config.js +5 -5
- package/utils/security-fixed.js +607 -0
- package/utils/security.js +652 -602
- package/utils/setup-enforcer.js +136 -44
- package/utils/setup-validator.js +33 -32
- package/utils/ultra-performance-optimizer.js +11 -9
- package/utils/watch-locales.js +2 -1
- package/utils/prompt-fixed.js +0 -55
- package/utils/security-check.js +0 -454
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* I18NTK FIXER COMMAND
|
|
5
|
+
*
|
|
6
|
+
* Handles translation fixing operations.
|
|
7
|
+
* Contains embedded business logic for fixing translation issues.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const SecurityUtils = require('../../../utils/security');
|
|
13
|
+
const cliHelper = require('../../../utils/cli-helper');
|
|
14
|
+
const { loadTranslations, t } = require('../../../utils/i18n-helper');
|
|
15
|
+
const { getUnifiedConfig, parseCommonArgs, displayHelp } = require('../../../utils/config-helper');
|
|
16
|
+
const JsonOutput = require('../../../utils/json-output');
|
|
17
|
+
const SetupEnforcer = require('../../../utils/setup-enforcer');
|
|
18
|
+
|
|
19
|
+
class FixerCommand {
|
|
20
|
+
constructor(config = {}, ui = null) {
|
|
21
|
+
this.config = config;
|
|
22
|
+
this.ui = ui;
|
|
23
|
+
this.prompt = null;
|
|
24
|
+
this.isNonInteractiveMode = false;
|
|
25
|
+
this.safeClose = null;
|
|
26
|
+
|
|
27
|
+
// Initialize fixer properties
|
|
28
|
+
this.sourceDir = null;
|
|
29
|
+
this.outputDir = null;
|
|
30
|
+
this.backupDir = null;
|
|
31
|
+
this.dryRun = false;
|
|
32
|
+
this.force = false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Set runtime dependencies for interactive operations
|
|
37
|
+
*/
|
|
38
|
+
setRuntimeDependencies(prompt, isNonInteractiveMode, safeClose) {
|
|
39
|
+
this.prompt = prompt;
|
|
40
|
+
this.isNonInteractiveMode = isNonInteractiveMode;
|
|
41
|
+
this.safeClose = safeClose;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Initialize the fixer with configuration
|
|
46
|
+
*/
|
|
47
|
+
async initialize() {
|
|
48
|
+
try {
|
|
49
|
+
const args = this.parseArgs();
|
|
50
|
+
if (args.help) {
|
|
51
|
+
displayHelp('i18ntk-fixer', {
|
|
52
|
+
'source-dir': 'Source directory to scan (default: ./locales)',
|
|
53
|
+
'languages': 'Comma separated list of languages to fix',
|
|
54
|
+
'markers': 'Comma separated markers to treat as untranslated',
|
|
55
|
+
'no-backup': 'Skip automatic backup creation',
|
|
56
|
+
'dry-run': 'Show what would be fixed without making changes',
|
|
57
|
+
'force': 'Force fixes without confirmation',
|
|
58
|
+
'output-dir': 'Output directory for fixed files'
|
|
59
|
+
});
|
|
60
|
+
process.exit(0);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const baseConfig = await getUnifiedConfig('fixer', args);
|
|
64
|
+
this.config = { ...baseConfig, ...(this.config || {}) };
|
|
65
|
+
|
|
66
|
+
const uiLanguage = (this.config && this.config.uiLanguage) || 'en';
|
|
67
|
+
loadTranslations(uiLanguage, path.resolve(__dirname, '../../../resources', 'i18n', 'ui-locales'));
|
|
68
|
+
|
|
69
|
+
this.sourceDir = this.config.sourceDir;
|
|
70
|
+
this.outputDir = this.config.outputDir;
|
|
71
|
+
this.backupDir = path.join(this.sourceDir, 'backup');
|
|
72
|
+
|
|
73
|
+
// Validate source directory exists
|
|
74
|
+
const { validateSourceDir } = require('../../../utils/config-helper');
|
|
75
|
+
validateSourceDir(this.sourceDir, 'i18ntk-fixer');
|
|
76
|
+
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error(`Fatal fixer error: ${error.message}`);
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
parseArgs() {
|
|
84
|
+
try {
|
|
85
|
+
const args = process.argv.slice(2);
|
|
86
|
+
const parsed = parseCommonArgs(args);
|
|
87
|
+
|
|
88
|
+
args.forEach(arg => {
|
|
89
|
+
if (arg.startsWith('--')) {
|
|
90
|
+
const [key, value] = arg.substring(2).split('=');
|
|
91
|
+
const sanitizedKey = SecurityUtils.sanitizeInput(key);
|
|
92
|
+
const sanitizedValue = value ? SecurityUtils.sanitizeInput(value) : true;
|
|
93
|
+
|
|
94
|
+
if (sanitizedKey === 'source-dir') {
|
|
95
|
+
parsed.sourceDir = sanitizedValue;
|
|
96
|
+
} else if (sanitizedKey === 'languages') {
|
|
97
|
+
parsed.languages = sanitizedValue.split(',').map(l => l.trim());
|
|
98
|
+
} else if (sanitizedKey === 'markers') {
|
|
99
|
+
parsed.markers = sanitizedValue.split(',').map(m => m.trim());
|
|
100
|
+
} else if (sanitizedKey === 'no-backup') {
|
|
101
|
+
parsed.noBackup = true;
|
|
102
|
+
} else if (sanitizedKey === 'dry-run') {
|
|
103
|
+
parsed.dryRun = true;
|
|
104
|
+
} else if (sanitizedKey === 'force') {
|
|
105
|
+
parsed.force = true;
|
|
106
|
+
} else if (sanitizedKey === 'output-dir') {
|
|
107
|
+
parsed.outputDir = sanitizedValue;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return parsed;
|
|
113
|
+
} catch (error) {
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Get all available languages
|
|
119
|
+
getAvailableLanguages() {
|
|
120
|
+
try {
|
|
121
|
+
const items = SecurityUtils.safeReaddirSync(this.sourceDir, process.cwd(), { withFileTypes: true });
|
|
122
|
+
if (!items) {
|
|
123
|
+
console.error('Error reading source directory: Unable to access directory');
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const languages = [];
|
|
128
|
+
|
|
129
|
+
// Check for directory-based structure
|
|
130
|
+
const directories = items
|
|
131
|
+
.filter(item => item.isDirectory())
|
|
132
|
+
.map(item => item.name)
|
|
133
|
+
.filter(name => name !== 'node_modules' && !name.startsWith('.') && name !== this.config.sourceLanguage);
|
|
134
|
+
|
|
135
|
+
// Check for monolith files (language.json files)
|
|
136
|
+
const files = items
|
|
137
|
+
.filter(item => item.isFile() && item.name.endsWith('.json'))
|
|
138
|
+
.map(item => item.name);
|
|
139
|
+
|
|
140
|
+
// Add directories as languages
|
|
141
|
+
languages.push(...directories);
|
|
142
|
+
|
|
143
|
+
// Add monolith files as languages (without .json extension)
|
|
144
|
+
const monolithLanguages = files
|
|
145
|
+
.map(file => file.replace('.json', ''))
|
|
146
|
+
.filter(lang => !languages.includes(lang) && lang !== this.config.sourceLanguage);
|
|
147
|
+
languages.push(...monolithLanguages);
|
|
148
|
+
|
|
149
|
+
return [...new Set(languages)].sort();
|
|
150
|
+
} catch (error) {
|
|
151
|
+
console.error('Error reading source directory:', error.message);
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Get all JSON files from a language directory
|
|
157
|
+
getLanguageFiles(language) {
|
|
158
|
+
if (!this.sourceDir) {
|
|
159
|
+
console.warn('Source directory not set');
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const languageDir = path.resolve(this.sourceDir, language);
|
|
164
|
+
const languageFile = path.resolve(this.sourceDir, `${language}.json`);
|
|
165
|
+
const files = [];
|
|
166
|
+
|
|
167
|
+
// Handle monolith file structure
|
|
168
|
+
const languageFileStat = SecurityUtils.safeStatSync(languageFile, this.sourceDir);
|
|
169
|
+
if (languageFileStat && languageFileStat.isFile()) {
|
|
170
|
+
return [path.basename(languageFile)];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Handle directory-based structure
|
|
174
|
+
const languageDirStat = SecurityUtils.safeStatSync(languageDir, this.sourceDir);
|
|
175
|
+
if (languageDirStat && languageDirStat.isDirectory()) {
|
|
176
|
+
try {
|
|
177
|
+
// Ensure the path is within the source directory for security
|
|
178
|
+
const validatedPath = SecurityUtils.validatePath(languageDir, this.sourceDir);
|
|
179
|
+
if (!validatedPath) {
|
|
180
|
+
console.warn(`Language directory not found or invalid: ${languageDir}`);
|
|
181
|
+
return [];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const findJsonFiles = (dir) => {
|
|
185
|
+
const results = [];
|
|
186
|
+
const items = SecurityUtils.safeReaddirSync(dir, this.sourceDir, { withFileTypes: true });
|
|
187
|
+
|
|
188
|
+
if (!items) return results;
|
|
189
|
+
|
|
190
|
+
for (const item of items) {
|
|
191
|
+
const fullPath = path.join(dir, item.name);
|
|
192
|
+
|
|
193
|
+
if (item.isDirectory() && !item.name.startsWith('.') && item.name !== 'node_modules') {
|
|
194
|
+
// Recursively search subdirectories
|
|
195
|
+
results.push(...findJsonFiles(fullPath));
|
|
196
|
+
} else if (item.isFile() && item.name.endsWith('.json')) {
|
|
197
|
+
// Check exclusion patterns
|
|
198
|
+
const relativePath = path.relative(this.sourceDir, fullPath);
|
|
199
|
+
const shouldExclude = (this.config.excludeFiles || []).some(pattern => {
|
|
200
|
+
if (typeof pattern === 'string') {
|
|
201
|
+
return relativePath === pattern || relativePath.endsWith(path.sep + pattern);
|
|
202
|
+
}
|
|
203
|
+
if (pattern instanceof RegExp) {
|
|
204
|
+
return pattern.test(relativePath);
|
|
205
|
+
}
|
|
206
|
+
return false;
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
if (!shouldExclude && !item.name.startsWith('.')) {
|
|
210
|
+
results.push(path.relative(languageDir, fullPath));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return results;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
return findJsonFiles(validatedPath);
|
|
219
|
+
} catch (error) {
|
|
220
|
+
console.error(`Error reading language directory ${languageDir}:`, error.message);
|
|
221
|
+
return [];
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return files;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Get all keys recursively from an object
|
|
229
|
+
getAllKeys(obj, prefix = '') {
|
|
230
|
+
const keys = new Set();
|
|
231
|
+
|
|
232
|
+
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
|
|
233
|
+
return keys;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
237
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
238
|
+
keys.add(fullKey);
|
|
239
|
+
|
|
240
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
241
|
+
const nestedKeys = this.getAllKeys(value, fullKey);
|
|
242
|
+
nestedKeys.forEach(k => keys.add(k));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return keys;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Get value by key path
|
|
250
|
+
getValueByPath(obj, keyPath) {
|
|
251
|
+
const keys = keyPath.split('.');
|
|
252
|
+
let current = obj;
|
|
253
|
+
|
|
254
|
+
for (const key of keys) {
|
|
255
|
+
if (current && typeof current === 'object' && key in current) {
|
|
256
|
+
current = current[key];
|
|
257
|
+
} else {
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return current;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Set value by key path
|
|
266
|
+
setValueByPath(obj, keyPath, value) {
|
|
267
|
+
const keys = keyPath.split('.');
|
|
268
|
+
let current = obj;
|
|
269
|
+
|
|
270
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
271
|
+
const key = keys[i];
|
|
272
|
+
if (!current[key] || typeof current[key] !== 'object') {
|
|
273
|
+
current[key] = {};
|
|
274
|
+
}
|
|
275
|
+
current = current[key];
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
current[keys[keys.length - 1]] = value;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Create backup of translation files
|
|
282
|
+
async createBackup() {
|
|
283
|
+
if (this.dryRun) return;
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
287
|
+
this.backupDir = path.join(this.sourceDir, `backup-${timestamp}`);
|
|
288
|
+
|
|
289
|
+
console.log(t('fixer.creatingBackup', { dir: this.backupDir }));
|
|
290
|
+
|
|
291
|
+
// Ensure backup directory exists
|
|
292
|
+
const dirCreated = SecurityUtils.safeMkdirSync(this.backupDir, process.cwd(), { recursive: true });
|
|
293
|
+
if (!dirCreated) {
|
|
294
|
+
console.warn('Failed to create backup directory');
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Copy all translation files to backup
|
|
299
|
+
const languages = this.getAvailableLanguages();
|
|
300
|
+
languages.push(this.config.sourceLanguage); // Include source language
|
|
301
|
+
|
|
302
|
+
for (const language of languages) {
|
|
303
|
+
const languageFiles = this.getLanguageFiles(language);
|
|
304
|
+
|
|
305
|
+
for (const fileName of languageFiles) {
|
|
306
|
+
const sourcePath = path.join(this.sourceDir, language, fileName);
|
|
307
|
+
const backupPath = path.join(this.backupDir, language, fileName);
|
|
308
|
+
|
|
309
|
+
// Ensure backup subdirectory exists
|
|
310
|
+
const backupSubDir = path.dirname(backupPath);
|
|
311
|
+
SecurityUtils.safeMkdirSync(backupSubDir, process.cwd(), { recursive: true });
|
|
312
|
+
|
|
313
|
+
// Copy file
|
|
314
|
+
if (SecurityUtils.safeExistsSync(sourcePath, this.sourceDir)) {
|
|
315
|
+
const content = SecurityUtils.safeReadFileSync(sourcePath, this.sourceDir, 'utf8');
|
|
316
|
+
SecurityUtils.safeWriteFileSync(backupPath, content, process.cwd(), 'utf8');
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
console.log(t('fixer.backupCreated'));
|
|
322
|
+
} catch (error) {
|
|
323
|
+
console.warn(`Failed to create backup: ${error.message}`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Analyze translation issues for fixing
|
|
328
|
+
analyzeIssues(language, fileName) {
|
|
329
|
+
const issues = [];
|
|
330
|
+
const sourceFiles = this.getLanguageFiles(this.config.sourceLanguage);
|
|
331
|
+
const targetFiles = this.getLanguageFiles(language);
|
|
332
|
+
|
|
333
|
+
if (!sourceFiles.includes(fileName) || !targetFiles.includes(fileName)) {
|
|
334
|
+
return issues;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const sourceFilePath = path.join(this.sourceDir, this.config.sourceLanguage, fileName);
|
|
338
|
+
const targetFilePath = path.join(this.sourceDir, language, fileName);
|
|
339
|
+
|
|
340
|
+
try {
|
|
341
|
+
const sourceContent = SecurityUtils.safeReadFileSync(sourceFilePath, this.sourceDir, 'utf8');
|
|
342
|
+
const targetContent = SecurityUtils.safeReadFileSync(targetFilePath, this.sourceDir, 'utf8');
|
|
343
|
+
|
|
344
|
+
if (!sourceContent || !targetContent) {
|
|
345
|
+
return issues;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const sourceObj = SecurityUtils.safeParseJSON(sourceContent);
|
|
349
|
+
const targetObj = SecurityUtils.safeParseJSON(targetContent);
|
|
350
|
+
|
|
351
|
+
if (!sourceObj || !targetObj) {
|
|
352
|
+
return issues;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const sourceKeys = this.getAllKeys(sourceObj);
|
|
356
|
+
|
|
357
|
+
for (const key of sourceKeys) {
|
|
358
|
+
const sourceValue = this.getValueByPath(sourceObj, key);
|
|
359
|
+
const targetValue = this.getValueByPath(targetObj, key);
|
|
360
|
+
|
|
361
|
+
if (targetValue === undefined) {
|
|
362
|
+
// Missing key
|
|
363
|
+
issues.push({
|
|
364
|
+
type: 'missing_key',
|
|
365
|
+
key,
|
|
366
|
+
sourceValue,
|
|
367
|
+
fix: () => this.setValueByPath(targetObj, key, sourceValue)
|
|
368
|
+
});
|
|
369
|
+
} else if (targetValue === '') {
|
|
370
|
+
// Empty value
|
|
371
|
+
issues.push({
|
|
372
|
+
type: 'empty_value',
|
|
373
|
+
key,
|
|
374
|
+
sourceValue,
|
|
375
|
+
fix: () => this.setValueByPath(targetObj, key, sourceValue)
|
|
376
|
+
});
|
|
377
|
+
} else {
|
|
378
|
+
const markers = this.config.notTranslatedMarkers || [this.config.notTranslatedMarker];
|
|
379
|
+
if (markers.some(m => targetValue === m)) {
|
|
380
|
+
// Untranslated marker
|
|
381
|
+
issues.push({
|
|
382
|
+
type: 'untranslated_marker',
|
|
383
|
+
key,
|
|
384
|
+
sourceValue,
|
|
385
|
+
fix: () => this.setValueByPath(targetObj, key, sourceValue)
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return issues;
|
|
392
|
+
} catch (error) {
|
|
393
|
+
console.warn(`Error analyzing ${language}/${fileName}: ${error.message}`);
|
|
394
|
+
return issues;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Fix translation issues for a language
|
|
399
|
+
async fixLanguage(language) {
|
|
400
|
+
const fixes = {
|
|
401
|
+
language,
|
|
402
|
+
files: {},
|
|
403
|
+
totalIssues: 0,
|
|
404
|
+
fixedIssues: 0
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
const sourceFiles = this.getLanguageFiles(this.config.sourceLanguage);
|
|
408
|
+
|
|
409
|
+
for (const fileName of sourceFiles) {
|
|
410
|
+
const issues = this.analyzeIssues(language, fileName);
|
|
411
|
+
|
|
412
|
+
if (issues.length > 0) {
|
|
413
|
+
fixes.files[fileName] = {
|
|
414
|
+
issues: issues.length,
|
|
415
|
+
fixed: 0
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
fixes.totalIssues += issues.length;
|
|
419
|
+
|
|
420
|
+
if (!this.dryRun) {
|
|
421
|
+
// Apply fixes
|
|
422
|
+
const targetFilePath = path.join(this.sourceDir, language, fileName);
|
|
423
|
+
|
|
424
|
+
try {
|
|
425
|
+
const targetContent = SecurityUtils.safeReadFileSync(targetFilePath, this.sourceDir, 'utf8');
|
|
426
|
+
if (!targetContent) continue;
|
|
427
|
+
|
|
428
|
+
const targetObj = SecurityUtils.safeParseJSON(targetContent);
|
|
429
|
+
if (!targetObj) continue;
|
|
430
|
+
|
|
431
|
+
for (const issue of issues) {
|
|
432
|
+
if (typeof issue.fix === 'function') {
|
|
433
|
+
issue.fix();
|
|
434
|
+
fixes.files[fileName].fixed++;
|
|
435
|
+
fixes.fixedIssues++;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Write back the fixed content
|
|
440
|
+
const fixedContent = JSON.stringify(targetObj, null, 2);
|
|
441
|
+
SecurityUtils.safeWriteFileSync(targetFilePath, fixedContent, process.cwd(), 'utf8');
|
|
442
|
+
|
|
443
|
+
} catch (error) {
|
|
444
|
+
console.warn(`Error fixing ${language}/${fileName}: ${error.message}`);
|
|
445
|
+
}
|
|
446
|
+
} else {
|
|
447
|
+
// In dry run mode, just count potential fixes
|
|
448
|
+
fixes.files[fileName].fixed = issues.length;
|
|
449
|
+
fixes.fixedIssues += issues.length;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return fixes;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Main fixing process
|
|
458
|
+
async fix() {
|
|
459
|
+
try {
|
|
460
|
+
const args = this.parseArgs();
|
|
461
|
+
const jsonOutput = new JsonOutput('fixer');
|
|
462
|
+
|
|
463
|
+
// Set options from args
|
|
464
|
+
this.dryRun = args.dryRun || false;
|
|
465
|
+
this.force = args.force || false;
|
|
466
|
+
|
|
467
|
+
if (!args.json) {
|
|
468
|
+
console.log(t('fixer.starting'));
|
|
469
|
+
console.log(t('fixer.sourceDirectory', { dir: path.resolve(this.sourceDir) }));
|
|
470
|
+
console.log(t('fixer.dryRunMode', { mode: this.dryRun ? 'ON' : 'OFF' }));
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Create backup unless disabled
|
|
474
|
+
if (!args.noBackup && !this.dryRun) {
|
|
475
|
+
await this.createBackup();
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const languages = this.getAvailableLanguages();
|
|
479
|
+
|
|
480
|
+
if (languages.length === 0) {
|
|
481
|
+
const error = t('fixer.noLanguages') || 'No target languages found.';
|
|
482
|
+
if (args.json) {
|
|
483
|
+
jsonOutput.setStatus('error', error);
|
|
484
|
+
console.log(JSON.stringify(jsonOutput.data, null, args.indent || 2));
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
console.log(error);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (!args.json) {
|
|
492
|
+
console.log(t('fixer.foundLanguages', { count: languages.length, languages: languages.join(', ') }));
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const results = {};
|
|
496
|
+
let totalIssues = 0;
|
|
497
|
+
let totalFixed = 0;
|
|
498
|
+
|
|
499
|
+
for (const language of languages) {
|
|
500
|
+
if (!args.json) {
|
|
501
|
+
console.log(t('fixer.fixing', { language }));
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const fixes = await this.fixLanguage(language);
|
|
505
|
+
results[language] = fixes;
|
|
506
|
+
|
|
507
|
+
totalIssues += fixes.totalIssues;
|
|
508
|
+
totalFixed += fixes.fixedIssues;
|
|
509
|
+
|
|
510
|
+
if (!args.json) {
|
|
511
|
+
console.log(t('fixer.languageFixed', {
|
|
512
|
+
language,
|
|
513
|
+
issues: fixes.totalIssues,
|
|
514
|
+
fixed: fixes.fixedIssues
|
|
515
|
+
}));
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Prepare JSON output
|
|
520
|
+
if (args.json) {
|
|
521
|
+
jsonOutput.setStatus(totalFixed > 0 ? 'ok' : 'info', 'Fixer completed');
|
|
522
|
+
jsonOutput.addStats({
|
|
523
|
+
issues: totalIssues,
|
|
524
|
+
fixed: totalFixed,
|
|
525
|
+
languages: languages.length
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
console.log(JSON.stringify(jsonOutput.getOutput(), null, args.indent || 2));
|
|
529
|
+
return { success: true, totalIssues, totalFixed, results };
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Summary
|
|
533
|
+
console.log(t('fixer.summary'));
|
|
534
|
+
console.log('='.repeat(50));
|
|
535
|
+
console.log(t('fixer.totalIssues', { count: totalIssues }));
|
|
536
|
+
console.log(t('fixer.totalFixed', { count: totalFixed }));
|
|
537
|
+
|
|
538
|
+
if (this.backupDir && !args.noBackup) {
|
|
539
|
+
console.log(t('fixer.backupLocation', { dir: this.backupDir }));
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
console.log(t('fixer.completed'));
|
|
543
|
+
|
|
544
|
+
return { success: true, totalIssues, totalFixed, results };
|
|
545
|
+
|
|
546
|
+
} catch (error) {
|
|
547
|
+
console.error(t('fixer.error', { error: error.message }));
|
|
548
|
+
throw error;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Main run method for compatibility
|
|
553
|
+
async run(options = {}) {
|
|
554
|
+
const fromMenu = options.fromMenu || false;
|
|
555
|
+
|
|
556
|
+
try {
|
|
557
|
+
const args = this.parseArgs();
|
|
558
|
+
|
|
559
|
+
if (args.help) {
|
|
560
|
+
this.showHelp();
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Initialize configuration properly when called from menu
|
|
565
|
+
if (fromMenu && !this.sourceDir) {
|
|
566
|
+
const baseConfig = await getUnifiedConfig('fixer', args);
|
|
567
|
+
this.config = { ...baseConfig, ...this.config };
|
|
568
|
+
|
|
569
|
+
const uiLanguage = this.config.uiLanguage || 'en';
|
|
570
|
+
loadTranslations(uiLanguage, path.resolve(__dirname, '../../../resources', 'i18n', 'ui-locales'));
|
|
571
|
+
|
|
572
|
+
this.sourceDir = this.config.sourceDir;
|
|
573
|
+
this.outputDir = this.config.outputDir;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return await this.fix();
|
|
577
|
+
|
|
578
|
+
} catch (error) {
|
|
579
|
+
console.error(t('fixer.error', { error: error.message }));
|
|
580
|
+
if (!fromMenu) {
|
|
581
|
+
process.exit(1);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Show help message
|
|
587
|
+
showHelp() {
|
|
588
|
+
console.log(t('fixer.help_message'));
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Execute the fixer command
|
|
593
|
+
*/
|
|
594
|
+
async execute(options = {}) {
|
|
595
|
+
try {
|
|
596
|
+
await this.initialize();
|
|
597
|
+
await this.run(options);
|
|
598
|
+
return { success: true, command: 'fix' };
|
|
599
|
+
} catch (error) {
|
|
600
|
+
console.error(`Fixer command failed: ${error.message}`);
|
|
601
|
+
throw error;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Get command metadata
|
|
607
|
+
*/
|
|
608
|
+
getMetadata() {
|
|
609
|
+
return {
|
|
610
|
+
name: 'fix',
|
|
611
|
+
description: 'Automatically fix translation issues and inconsistencies',
|
|
612
|
+
category: 'maintenance',
|
|
613
|
+
aliases: ['fixer'],
|
|
614
|
+
usage: 'fix [options]',
|
|
615
|
+
examples: [
|
|
616
|
+
'fix',
|
|
617
|
+
'fix --dry-run',
|
|
618
|
+
'fix --backup'
|
|
619
|
+
]
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
module.exports = FixerCommand;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* I18NTK INIT COMMAND
|
|
5
|
+
*
|
|
6
|
+
* Handles project initialization and setup functionality.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const I18nInitializer = require('../../i18ntk-init');
|
|
10
|
+
|
|
11
|
+
class InitCommand {
|
|
12
|
+
constructor(config = {}, ui = null) {
|
|
13
|
+
this.config = config;
|
|
14
|
+
this.ui = ui;
|
|
15
|
+
this.prompt = null;
|
|
16
|
+
this.isNonInteractiveMode = false;
|
|
17
|
+
this.safeClose = null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Set runtime dependencies for interactive operations
|
|
22
|
+
*/
|
|
23
|
+
setRuntimeDependencies(prompt, isNonInteractiveMode, safeClose) {
|
|
24
|
+
this.prompt = prompt;
|
|
25
|
+
this.isNonInteractiveMode = isNonInteractiveMode;
|
|
26
|
+
this.safeClose = safeClose;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Execute the init command
|
|
31
|
+
*/
|
|
32
|
+
async execute(options = {}) {
|
|
33
|
+
try {
|
|
34
|
+
const initializer = new I18nInitializer(this.config);
|
|
35
|
+
await initializer.run(options);
|
|
36
|
+
return { success: true, command: 'init' };
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error(`Init command failed: ${error.message}`);
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get command metadata
|
|
45
|
+
*/
|
|
46
|
+
getMetadata() {
|
|
47
|
+
return {
|
|
48
|
+
name: 'init',
|
|
49
|
+
description: 'Initialize i18n project structure',
|
|
50
|
+
category: 'setup',
|
|
51
|
+
aliases: [],
|
|
52
|
+
usage: 'init [options]',
|
|
53
|
+
examples: [
|
|
54
|
+
'init',
|
|
55
|
+
'init --source-dir=./src/locales',
|
|
56
|
+
'init --i18n-dir=./locales'
|
|
57
|
+
]
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = InitCommand;
|