i18ntk 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 +401 -0
- package/LICENSE +21 -0
- package/README.md +507 -0
- package/dev/README.md +37 -0
- package/dev/debug/README.md +30 -0
- package/dev/debug/complete-console-translations.js +295 -0
- package/dev/debug/console-key-checker.js +408 -0
- package/dev/debug/console-translations.js +335 -0
- package/dev/debug/debugger.js +408 -0
- package/dev/debug/export-missing-keys.js +432 -0
- package/dev/debug/final-normalize.js +236 -0
- package/dev/debug/find-extra-keys.js +68 -0
- package/dev/debug/normalize-locales.js +153 -0
- package/dev/debug/refactor-locales.js +240 -0
- package/dev/debug/reorder-locales.js +85 -0
- package/dev/debug/replace-hardcoded-console.js +378 -0
- package/docs/INSTALLATION.md +449 -0
- package/docs/README.md +222 -0
- package/docs/TODO_ROADMAP.md +279 -0
- package/docs/api/API_REFERENCE.md +377 -0
- package/docs/api/COMPONENTS.md +492 -0
- package/docs/api/CONFIGURATION.md +651 -0
- package/docs/api/NPM_PUBLISHING_GUIDE.md +434 -0
- package/docs/debug/DEBUG_README.md +30 -0
- package/docs/debug/DEBUG_TOOLS.md +494 -0
- package/docs/development/AGENTS.md +351 -0
- package/docs/development/DEVELOPMENT_RULES.md +165 -0
- package/docs/development/DEV_README.md +37 -0
- package/docs/release-notes/RELEASE_NOTES_v1.0.0.md +173 -0
- package/docs/release-notes/RELEASE_NOTES_v1.6.0.md +141 -0
- package/docs/release-notes/RELEASE_NOTES_v1.6.1.md +185 -0
- package/docs/release-notes/RELEASE_NOTES_v1.6.3.md +199 -0
- package/docs/reports/ANALYSIS_README.md +17 -0
- package/docs/reports/CONSOLE_MISMATCH_BUG_REPORT_v1.5.0.md +181 -0
- package/docs/reports/SIZING_README.md +18 -0
- package/docs/reports/SUMMARY_README.md +18 -0
- package/docs/reports/TRANSLATION_BUG_REPORT_v1.5.0.md +129 -0
- package/docs/reports/USAGE_README.md +18 -0
- package/docs/reports/VALIDATION_README.md +18 -0
- package/locales/de/auth.json +3 -0
- package/locales/de/common.json +16 -0
- package/locales/de/pagination.json +6 -0
- package/locales/en/auth.json +3 -0
- package/locales/en/common.json +16 -0
- package/locales/en/pagination.json +6 -0
- package/locales/es/auth.json +3 -0
- package/locales/es/common.json +16 -0
- package/locales/es/pagination.json +6 -0
- package/locales/fr/auth.json +3 -0
- package/locales/fr/common.json +16 -0
- package/locales/fr/pagination.json +6 -0
- package/locales/ru/auth.json +3 -0
- package/locales/ru/common.json +16 -0
- package/locales/ru/pagination.json +6 -0
- package/main/i18ntk-analyze.js +625 -0
- package/main/i18ntk-autorun.js +461 -0
- package/main/i18ntk-complete.js +494 -0
- package/main/i18ntk-init.js +686 -0
- package/main/i18ntk-manage.js +848 -0
- package/main/i18ntk-sizing.js +557 -0
- package/main/i18ntk-summary.js +671 -0
- package/main/i18ntk-usage.js +1282 -0
- package/main/i18ntk-validate.js +762 -0
- package/main/ui-i18n.js +332 -0
- package/package.json +152 -0
- package/scripts/fix-missing-translation-keys.js +214 -0
- package/scripts/verify-package.js +168 -0
- package/ui-locales/de.json +637 -0
- package/ui-locales/en.json +688 -0
- package/ui-locales/es.json +637 -0
- package/ui-locales/fr.json +637 -0
- package/ui-locales/ja.json +637 -0
- package/ui-locales/ru.json +637 -0
- package/ui-locales/zh.json +637 -0
- package/utils/admin-auth.js +317 -0
- package/utils/admin-cli.js +353 -0
- package/utils/admin-pin.js +409 -0
- package/utils/detect-language-mismatches.js +454 -0
- package/utils/i18n-helper.js +128 -0
- package/utils/maintain-language-purity.js +433 -0
- package/utils/native-translations.js +478 -0
- package/utils/security.js +384 -0
- package/utils/test-complete-system.js +356 -0
- package/utils/test-console-i18n.js +402 -0
- package/utils/translate-mismatches.js +571 -0
- package/utils/validate-language-purity.js +531 -0
|
@@ -0,0 +1,686 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* I18N INITIALIZATION SCRIPT
|
|
4
|
+
*
|
|
5
|
+
* This script initializes a new i18n project or adds new languages to an existing one.
|
|
6
|
+
* It uses the English (en) locale as the source of truth and generates translation files
|
|
7
|
+
* for specified languages with proper structure and __NOT_TRANSLATED__ markers.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* node scripts/i18n/01-init-i18n.js
|
|
11
|
+
* node scripts/i18n/01-init-i18n.js --languages=de,es,fr,ru
|
|
12
|
+
* node scripts/i18n/01-init-i18n.js --source-dir=./src/i18n/locales --target-languages=de,es
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const readline = require('readline');
|
|
18
|
+
const settingsManager = require('../settings/settings-manager');
|
|
19
|
+
const SecurityUtils = require('../utils/security');
|
|
20
|
+
const AdminAuth = require('../utils/admin-auth');
|
|
21
|
+
const UIi18n = require('./ui-i18n');
|
|
22
|
+
|
|
23
|
+
// Get configuration from settings manager
|
|
24
|
+
function getConfig() {
|
|
25
|
+
const settings = settingsManager.getSettings();
|
|
26
|
+
return {
|
|
27
|
+
sourceDir: settings.directories?.sourceDir || './locales',
|
|
28
|
+
sourceLanguage: settings.directories?.sourceLanguage || 'en',
|
|
29
|
+
defaultLanguages: settings.processing?.defaultLanguages || ['de', 'es', 'fr', 'ru'],
|
|
30
|
+
notTranslatedMarker: settings.processing?.notTranslatedMarker || 'NOT_TRANSLATED',
|
|
31
|
+
excludeFiles: settings.processing?.excludeFiles || ['.DS_Store', 'Thumbs.db'],
|
|
32
|
+
uiLanguage: settings.language || 'en'
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Language configurations with native names
|
|
37
|
+
const LANGUAGE_CONFIG = {
|
|
38
|
+
'de': { name: 'German', nativeName: 'Deutsch' },
|
|
39
|
+
'es': { name: 'Spanish', nativeName: 'Español' },
|
|
40
|
+
'fr': { name: 'French', nativeName: 'Français' },
|
|
41
|
+
'ru': { name: 'Russian', nativeName: 'Русский' },
|
|
42
|
+
'it': { name: 'Italian', nativeName: 'Italiano' },
|
|
43
|
+
'pt': { name: 'Portuguese', nativeName: 'Português' },
|
|
44
|
+
'ja': { name: 'Japanese', nativeName: '日本語' },
|
|
45
|
+
'ko': { name: 'Korean', nativeName: '한국어' },
|
|
46
|
+
'zh': { name: 'Chinese', nativeName: '中文' },
|
|
47
|
+
'ar': { name: 'Arabic', nativeName: 'العربية' },
|
|
48
|
+
'hi': { name: 'Hindi', nativeName: 'हिन्दी' },
|
|
49
|
+
'nl': { name: 'Dutch', nativeName: 'Nederlands' },
|
|
50
|
+
'sv': { name: 'Swedish', nativeName: 'Svenska' },
|
|
51
|
+
'da': { name: 'Danish', nativeName: 'Dansk' },
|
|
52
|
+
'no': { name: 'Norwegian', nativeName: 'Norsk' },
|
|
53
|
+
'fi': { name: 'Finnish', nativeName: 'Suomi' },
|
|
54
|
+
'pl': { name: 'Polish', nativeName: 'Polski' },
|
|
55
|
+
'cs': { name: 'Czech', nativeName: 'Čeština' },
|
|
56
|
+
'hu': { name: 'Hungarian', nativeName: 'Magyar' },
|
|
57
|
+
'tr': { name: 'Turkish', nativeName: 'Türkçe' }
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
class I18nInitializer {
|
|
61
|
+
constructor(config = {}) {
|
|
62
|
+
this.ui = new UIi18n();
|
|
63
|
+
this.config = { ...getConfig(), ...config };
|
|
64
|
+
this.sourceDir = path.resolve(this.config.sourceDir);
|
|
65
|
+
this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
|
|
66
|
+
|
|
67
|
+
// Use global readline interface to prevent doubling
|
|
68
|
+
if (global.activeReadlineInterface) {
|
|
69
|
+
this.rl = global.activeReadlineInterface;
|
|
70
|
+
this.shouldCloseRL = false;
|
|
71
|
+
} else {
|
|
72
|
+
this.rl = readline.createInterface({
|
|
73
|
+
input: process.stdin,
|
|
74
|
+
output: process.stdout,
|
|
75
|
+
terminal: true,
|
|
76
|
+
historySize: 0
|
|
77
|
+
});
|
|
78
|
+
global.activeReadlineInterface = this.rl;
|
|
79
|
+
this.shouldCloseRL = true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Add the missing checkI18nDependencies method
|
|
84
|
+
async checkI18nDependencies() {
|
|
85
|
+
const packageJsonPath = path.resolve('./package.json');
|
|
86
|
+
|
|
87
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
88
|
+
console.log(this.ui.t('init.warnings.noPackageJson'));
|
|
89
|
+
return await this.promptContinueWithoutI18n();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
94
|
+
// Include peerDependencies in the check
|
|
95
|
+
const dependencies = {
|
|
96
|
+
...packageJson.dependencies,
|
|
97
|
+
...packageJson.devDependencies,
|
|
98
|
+
...packageJson.peerDependencies
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const i18nFrameworks = [
|
|
102
|
+
'react-i18next',
|
|
103
|
+
'vue-i18n',
|
|
104
|
+
'angular-i18n',
|
|
105
|
+
'i18next',
|
|
106
|
+
'next-i18next',
|
|
107
|
+
'svelte-i18n',
|
|
108
|
+
'@nuxtjs/i18n'
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
const installedFrameworks = i18nFrameworks.filter(framework => dependencies[framework]);
|
|
112
|
+
|
|
113
|
+
if (installedFrameworks.length > 0) {
|
|
114
|
+
console.log(this.ui.t('init.detectedI18nFrameworks', { frameworks: installedFrameworks.join(', ') }));
|
|
115
|
+
return true;
|
|
116
|
+
} else {
|
|
117
|
+
console.log(this.ui.t('init.suggestions.noFramework'));
|
|
118
|
+
console.log(this.ui.t('init.frameworks.react'));
|
|
119
|
+
console.log(this.ui.t('init.frameworks.vue'));
|
|
120
|
+
console.log(this.ui.t('init.frameworks.i18next'));
|
|
121
|
+
console.log(this.ui.t('init.frameworks.nuxt'));
|
|
122
|
+
console.log(this.ui.t('init.frameworks.svelte'));
|
|
123
|
+
return await this.promptContinueWithoutI18n();
|
|
124
|
+
}
|
|
125
|
+
} catch (error) {
|
|
126
|
+
console.log(this.ui.t('init.errors.packageJsonRead'));
|
|
127
|
+
return await this.promptContinueWithoutI18n();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Add the missing promptContinueWithoutI18n method
|
|
132
|
+
async promptContinueWithoutI18n() {
|
|
133
|
+
const answer = await this.prompt('\n' + this.ui.t('init.continueWithoutI18nPrompt'));
|
|
134
|
+
return answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Add the missing prompt method
|
|
138
|
+
async prompt(question) {
|
|
139
|
+
return new Promise((resolve) => {
|
|
140
|
+
this.rl.question(question, (answer) => {
|
|
141
|
+
resolve(answer.trim());
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Parse command line arguments
|
|
147
|
+
parseArgs() {
|
|
148
|
+
const args = process.argv.slice(2);
|
|
149
|
+
const parsed = {};
|
|
150
|
+
|
|
151
|
+
args.forEach(arg => {
|
|
152
|
+
if (arg.startsWith('--')) {
|
|
153
|
+
const [key, value] = arg.substring(2).split('=');
|
|
154
|
+
if (key === 'languages' || key === 'target-languages') {
|
|
155
|
+
parsed.languages = value ? value.split(',').map(l => l.trim()) : [];
|
|
156
|
+
} else if (key === 'source-dir') {
|
|
157
|
+
parsed.sourceDir = value;
|
|
158
|
+
} else if (key === 'source-language') {
|
|
159
|
+
parsed.sourceLanguage = value;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
return parsed;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Setup initial directory structure if needed
|
|
168
|
+
async setupInitialStructure() {
|
|
169
|
+
// Validate paths
|
|
170
|
+
const validatedSourceDir = SecurityUtils.validatePath(this.sourceDir, process.cwd());
|
|
171
|
+
const validatedSourceLanguageDir = SecurityUtils.validatePath(this.sourceLanguageDir, process.cwd());
|
|
172
|
+
|
|
173
|
+
if (!validatedSourceDir || !validatedSourceLanguageDir) {
|
|
174
|
+
SecurityUtils.logSecurityEvent('Invalid directory paths in setupInitialStructure', 'error', { sourceDir: this.sourceDir, sourceLanguageDir: this.sourceLanguageDir });
|
|
175
|
+
throw new Error('Invalid directory paths detected');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Create source directory if it doesn't exist
|
|
179
|
+
if (!fs.existsSync(validatedSourceDir)) {
|
|
180
|
+
console.log(this.ui.t('init.creatingSourceDirectory', { dir: validatedSourceDir }));
|
|
181
|
+
fs.mkdirSync(validatedSourceDir, { recursive: true });
|
|
182
|
+
SecurityUtils.logSecurityEvent('Source directory created', 'info', { dir: validatedSourceDir });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Create source language directory if it doesn't exist
|
|
186
|
+
if (!fs.existsSync(validatedSourceLanguageDir)) {
|
|
187
|
+
console.log(this.ui.t('init.creatingSourceLanguageDirectory', { dir: validatedSourceLanguageDir }));
|
|
188
|
+
fs.mkdirSync(validatedSourceLanguageDir, { recursive: true });
|
|
189
|
+
|
|
190
|
+
// Create a sample common.json file
|
|
191
|
+
const sampleTranslations = {
|
|
192
|
+
"common": {
|
|
193
|
+
"welcome": "Welcome",
|
|
194
|
+
"hello": "Hello",
|
|
195
|
+
"goodbye": "Goodbye",
|
|
196
|
+
"yes": "Yes",
|
|
197
|
+
"no": "No",
|
|
198
|
+
"save": "Save",
|
|
199
|
+
"cancel": "Cancel",
|
|
200
|
+
"delete": "Delete",
|
|
201
|
+
"edit": "Edit",
|
|
202
|
+
"loading": "Loading..."
|
|
203
|
+
},
|
|
204
|
+
"navigation": {
|
|
205
|
+
"home": "Home",
|
|
206
|
+
"about": "About",
|
|
207
|
+
"contact": "Contact",
|
|
208
|
+
"settings": "Settings"
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const sampleFilePath = path.join(validatedSourceLanguageDir, 'common.json');
|
|
213
|
+
const validatedSampleFilePath = SecurityUtils.validatePath(sampleFilePath, process.cwd());
|
|
214
|
+
|
|
215
|
+
if (!validatedSampleFilePath) {
|
|
216
|
+
SecurityUtils.logSecurityEvent('Invalid sample file path', 'error', { path: sampleFilePath });
|
|
217
|
+
throw new Error('Invalid sample file path');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const success = await SecurityUtils.safeWriteFile(validatedSampleFilePath, JSON.stringify(sampleTranslations, null, 2), process.cwd());
|
|
221
|
+
|
|
222
|
+
if (success) {
|
|
223
|
+
console.log(this.ui.t('init.createdSampleTranslationFile', { file: validatedSampleFilePath }));
|
|
224
|
+
SecurityUtils.logSecurityEvent('Sample translation file created', 'info', { file: validatedSampleFilePath });
|
|
225
|
+
} else {
|
|
226
|
+
SecurityUtils.logSecurityEvent('Failed to create sample translation file', 'error', { file: validatedSampleFilePath });
|
|
227
|
+
throw new Error('Failed to create sample translation file');
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Check if source directory and language exist
|
|
233
|
+
validateSource() {
|
|
234
|
+
if (!fs.existsSync(this.sourceDir)) {
|
|
235
|
+
throw new Error(`Source directory not found: ${this.sourceDir}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (!fs.existsSync(this.sourceLanguageDir)) {
|
|
239
|
+
throw new Error(`Source language directory not found: ${this.sourceLanguageDir}`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Get all JSON files from source language directory
|
|
246
|
+
getSourceFiles() {
|
|
247
|
+
const files = fs.readdirSync(this.sourceLanguageDir)
|
|
248
|
+
.filter(file => {
|
|
249
|
+
return file.endsWith('.json') &&
|
|
250
|
+
!this.config.excludeFiles.includes(file);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
if (files.length === 0) {
|
|
254
|
+
throw new Error(`No JSON files found in source directory: ${this.sourceLanguageDir}`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return files;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Recursively mark all string values as not translated
|
|
261
|
+
markAsNotTranslated(obj) {
|
|
262
|
+
if (typeof obj === 'string') {
|
|
263
|
+
return this.config.notTranslatedMarker;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (Array.isArray(obj)) {
|
|
267
|
+
return obj.map(item => this.markAsNotTranslated(item));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (obj && typeof obj === 'object') {
|
|
271
|
+
const result = {};
|
|
272
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
273
|
+
result[key] = this.markAsNotTranslated(value);
|
|
274
|
+
}
|
|
275
|
+
return result;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return obj;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Create or update a language file securely
|
|
282
|
+
async createLanguageFile(sourceFile, targetLanguage, sourceContent) {
|
|
283
|
+
const targetDir = path.join(this.sourceDir, targetLanguage);
|
|
284
|
+
const targetFile = path.join(targetDir, sourceFile);
|
|
285
|
+
|
|
286
|
+
// Validate paths
|
|
287
|
+
const validatedTargetDir = SecurityUtils.validatePath(targetDir, this.sourceDir);
|
|
288
|
+
const validatedTargetFile = SecurityUtils.validatePath(targetFile, this.sourceDir);
|
|
289
|
+
|
|
290
|
+
if (!validatedTargetDir || !validatedTargetFile) {
|
|
291
|
+
SecurityUtils.logSecurityEvent('Invalid path detected in createLanguageFile', 'error', { targetDir, targetFile });
|
|
292
|
+
throw new Error('Invalid file path detected');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Create target directory if it doesn't exist
|
|
296
|
+
if (!fs.existsSync(validatedTargetDir)) {
|
|
297
|
+
fs.mkdirSync(validatedTargetDir, { recursive: true });
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
let targetContent;
|
|
301
|
+
|
|
302
|
+
// If target file exists, preserve existing translations
|
|
303
|
+
if (fs.existsSync(validatedTargetFile)) {
|
|
304
|
+
try {
|
|
305
|
+
const existingContent = await SecurityUtils.safeReadFile(validatedTargetFile, this.sourceDir);
|
|
306
|
+
if (existingContent) {
|
|
307
|
+
targetContent = this.mergeTranslations(sourceContent, JSON.parse(existingContent));
|
|
308
|
+
} else {
|
|
309
|
+
targetContent = this.markAsNotTranslated(sourceContent);
|
|
310
|
+
}
|
|
311
|
+
} catch (error) {
|
|
312
|
+
console.warn(`⚠️ Warning: Could not parse existing file ${validatedTargetFile}, creating new one`);
|
|
313
|
+
SecurityUtils.logSecurityEvent('File parse error', 'warn', { file: validatedTargetFile, error: error.message });
|
|
314
|
+
targetContent = this.markAsNotTranslated(sourceContent);
|
|
315
|
+
}
|
|
316
|
+
} else {
|
|
317
|
+
targetContent = this.markAsNotTranslated(sourceContent);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Write the file securely
|
|
321
|
+
const success = await SecurityUtils.safeWriteFile(validatedTargetFile, JSON.stringify(targetContent, null, 2), this.sourceDir);
|
|
322
|
+
|
|
323
|
+
if (!success) {
|
|
324
|
+
SecurityUtils.logSecurityEvent('Failed to write language file', 'error', { file: validatedTargetFile });
|
|
325
|
+
throw new Error(`Failed to write file: ${validatedTargetFile}`);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
SecurityUtils.logSecurityEvent('Language file created/updated', 'info', { file: validatedTargetFile, language: targetLanguage });
|
|
329
|
+
return validatedTargetFile;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Merge existing translations with new structure
|
|
333
|
+
mergeTranslations(sourceObj, existingObj) {
|
|
334
|
+
if (typeof sourceObj === 'string') {
|
|
335
|
+
// If existing translation exists and is not the marker, keep it
|
|
336
|
+
if (typeof existingObj === 'string' &&
|
|
337
|
+
existingObj !== this.config.notTranslatedMarker &&
|
|
338
|
+
existingObj.trim() !== '') {
|
|
339
|
+
return existingObj;
|
|
340
|
+
}
|
|
341
|
+
return this.config.notTranslatedMarker;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (Array.isArray(sourceObj)) {
|
|
345
|
+
return sourceObj.map((item, index) => {
|
|
346
|
+
const existingItem = Array.isArray(existingObj) ? existingObj[index] : undefined;
|
|
347
|
+
return this.mergeTranslations(item, existingItem);
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (sourceObj && typeof sourceObj === 'object') {
|
|
352
|
+
const result = {};
|
|
353
|
+
for (const [key, value] of Object.entries(sourceObj)) {
|
|
354
|
+
const existingValue = existingObj && typeof existingObj === 'object' ? existingObj[key] : undefined;
|
|
355
|
+
result[key] = this.mergeTranslations(value, existingValue);
|
|
356
|
+
}
|
|
357
|
+
return result;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return sourceObj;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Get translation statistics
|
|
364
|
+
getTranslationStats(obj) {
|
|
365
|
+
let total = 0;
|
|
366
|
+
let translated = 0;
|
|
367
|
+
|
|
368
|
+
const count = (item) => {
|
|
369
|
+
if (typeof item === 'string') {
|
|
370
|
+
total++;
|
|
371
|
+
if (item !== this.config.notTranslatedMarker && item.trim() !== '') {
|
|
372
|
+
translated++;
|
|
373
|
+
}
|
|
374
|
+
} else if (Array.isArray(item)) {
|
|
375
|
+
item.forEach(count);
|
|
376
|
+
} else if (item && typeof item === 'object') {
|
|
377
|
+
Object.values(item).forEach(count);
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
count(obj);
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
total,
|
|
385
|
+
translated,
|
|
386
|
+
percentage: total > 0 ? Math.round((translated / total) * 100) : 0,
|
|
387
|
+
missing: total - translated
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Interactive admin PIN setup
|
|
392
|
+
async promptAdminPinSetup() {
|
|
393
|
+
const readline = require('readline');
|
|
394
|
+
const rl = readline.createInterface({
|
|
395
|
+
input: process.stdin,
|
|
396
|
+
output: process.stdout,
|
|
397
|
+
terminal: true,
|
|
398
|
+
historySize: 0
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
const question = (query) => new Promise(resolve => rl.question(query, resolve));
|
|
402
|
+
|
|
403
|
+
console.log('\n' + this.ui.t('init.adminPinSetupOptional'));
|
|
404
|
+
console.log(this.ui.t('init.adminPinSeparator'));
|
|
405
|
+
console.log(this.ui.t('init.adminPinDescription1'));
|
|
406
|
+
console.log(this.ui.t('init.adminPinDescription2'));
|
|
407
|
+
console.log(this.ui.t('init.adminPinDescription3'));
|
|
408
|
+
console.log(this.ui.t('init.adminPinDescription4'));
|
|
409
|
+
|
|
410
|
+
const setupPin = await question('\n' + this.ui.t('init.adminPinSetupPrompt'));
|
|
411
|
+
|
|
412
|
+
if (setupPin.toLowerCase() === 'y' || setupPin.toLowerCase() === 'yes') {
|
|
413
|
+
try {
|
|
414
|
+
const adminAuth = new AdminAuth();
|
|
415
|
+
|
|
416
|
+
// Enable admin PIN in settings
|
|
417
|
+
settingsManager.setSecurity({ adminPinEnabled: true, adminPinPromptOnInit: true });
|
|
418
|
+
|
|
419
|
+
console.log('\n' + this.ui.t('init.settingUpAdminPin'));
|
|
420
|
+
|
|
421
|
+
let pin1, pin2;
|
|
422
|
+
do {
|
|
423
|
+
pin1 = await question(this.ui.t('init.enterAdminPin'));
|
|
424
|
+
|
|
425
|
+
if (!/^\d{4,8}$/.test(pin1)) {
|
|
426
|
+
console.log(this.ui.t('init.pinMustBe4To8Digits'));
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
pin2 = await question(this.ui.t('init.confirmAdminPin'));
|
|
431
|
+
|
|
432
|
+
if (pin1 !== pin2) {
|
|
433
|
+
console.log(this.ui.t('init.pinsDoNotMatch'));
|
|
434
|
+
}
|
|
435
|
+
} while (pin1 !== pin2 || !/^\d{4,8}$/.test(pin1));
|
|
436
|
+
|
|
437
|
+
await adminAuth.setupPin(pin1);
|
|
438
|
+
console.log(this.ui.t('init.adminPinSetupSuccess'));
|
|
439
|
+
console.log(this.ui.t('init.adminProtectionEnabled'));
|
|
440
|
+
|
|
441
|
+
} catch (error) {
|
|
442
|
+
console.error(this.ui.t('init.errorSettingUpAdminPin', { error: error.message }));
|
|
443
|
+
console.log(this.ui.t('init.continuingWithoutAdminPin'));
|
|
444
|
+
}
|
|
445
|
+
} else {
|
|
446
|
+
console.log(this.ui.t('init.skippingAdminPinSetup'));
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
rl.close();
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Interactive language selection
|
|
453
|
+
async selectLanguages() {
|
|
454
|
+
// Use the global readline interface if available, otherwise create one
|
|
455
|
+
let rl = this.rl;
|
|
456
|
+
let shouldClose = false;
|
|
457
|
+
|
|
458
|
+
if (!rl) {
|
|
459
|
+
const readline = require('readline');
|
|
460
|
+
rl = readline.createInterface({
|
|
461
|
+
input: process.stdin,
|
|
462
|
+
output: process.stdout,
|
|
463
|
+
terminal: true,
|
|
464
|
+
historySize: 0
|
|
465
|
+
});
|
|
466
|
+
shouldClose = true;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const question = (query) => new Promise(resolve => rl.question(query, resolve));
|
|
470
|
+
|
|
471
|
+
console.log('\n' + this.ui.t('init.languageSelectionTitle'));
|
|
472
|
+
console.log('=' .repeat(50));
|
|
473
|
+
console.log(this.ui.t('language.available'));
|
|
474
|
+
|
|
475
|
+
Object.entries(LANGUAGE_CONFIG).forEach(([code, config], index) => {
|
|
476
|
+
console.log(` ${(index + 1).toString().padStart(2)}. ${code} - ${config.name} (${config.nativeName})`);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
console.log('\n' + this.ui.t('init.defaultLanguages', { languages: this.config.defaultLanguages.join(', ') }));
|
|
480
|
+
|
|
481
|
+
const answer = await question('\n' + this.ui.t('init.enterLanguageCodes'));
|
|
482
|
+
|
|
483
|
+
// Only close if we created our own readline interface
|
|
484
|
+
if (shouldClose) {
|
|
485
|
+
rl.close();
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (answer.trim() === '') {
|
|
489
|
+
return this.config.defaultLanguages;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const selectedLanguages = answer.split(',').map(lang => lang.trim().toLowerCase());
|
|
493
|
+
const validLanguages = selectedLanguages.filter(lang => LANGUAGE_CONFIG[lang]);
|
|
494
|
+
const invalidLanguages = selectedLanguages.filter(lang => !LANGUAGE_CONFIG[lang]);
|
|
495
|
+
|
|
496
|
+
if (invalidLanguages.length > 0) {
|
|
497
|
+
console.warn(this.ui.t('init.warningInvalidLanguageCodes', { languages: invalidLanguages.join(', ') }));
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return validLanguages.length > 0 ? validLanguages : this.config.defaultLanguages;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Main initialization process
|
|
504
|
+
async init() {
|
|
505
|
+
try {
|
|
506
|
+
console.log(this.ui.t('init.initializationTitle'));
|
|
507
|
+
console.log('=' .repeat(50));
|
|
508
|
+
|
|
509
|
+
// Parse command line arguments
|
|
510
|
+
const args = this.parseArgs();
|
|
511
|
+
if (args.sourceDir) this.config.sourceDir = args.sourceDir;
|
|
512
|
+
if (args.sourceLanguage) this.config.sourceLanguage = args.sourceLanguage;
|
|
513
|
+
|
|
514
|
+
// Update paths
|
|
515
|
+
this.sourceDir = path.resolve(this.config.sourceDir);
|
|
516
|
+
this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
|
|
517
|
+
|
|
518
|
+
console.log(this.ui.t('init.sourceDirectoryLabel', { dir: this.sourceDir }));
|
|
519
|
+
console.log(this.ui.t('init.sourceLanguageLabel', { language: this.config.sourceLanguage }));
|
|
520
|
+
|
|
521
|
+
// Check i18n dependencies first and exit if user chooses not to continue
|
|
522
|
+
const hasI18n = await this.checkI18nDependencies();
|
|
523
|
+
|
|
524
|
+
if (!hasI18n) {
|
|
525
|
+
console.log(this.ui.t('init.errors.noFramework'));
|
|
526
|
+
console.log(this.ui.t('init.suggestions.installFramework'));
|
|
527
|
+
if (this.shouldCloseRL) {
|
|
528
|
+
this.rl.close();
|
|
529
|
+
global.activeReadlineInterface = null;
|
|
530
|
+
}
|
|
531
|
+
process.exit(0);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Call the enhanced initialize method with args
|
|
535
|
+
await this.initialize(hasI18n, args);
|
|
536
|
+
|
|
537
|
+
} catch (error) {
|
|
538
|
+
console.error('❌ Initialization failed:', error.message);
|
|
539
|
+
throw error;
|
|
540
|
+
} finally {
|
|
541
|
+
if (this.shouldCloseRL && this.rl) {
|
|
542
|
+
this.rl.close();
|
|
543
|
+
global.activeReadlineInterface = null;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Enhanced initialization with dependency checking
|
|
549
|
+
async initialize(hasI18n = true, args = {}) {
|
|
550
|
+
console.log(this.ui.t('init.initializingProject'));
|
|
551
|
+
|
|
552
|
+
if (!hasI18n) {
|
|
553
|
+
console.log(this.ui.t('init.warningProceedingWithoutFramework'));
|
|
554
|
+
console.log(this.ui.t('init.translationFilesCreatedWarning'));
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Continue with existing initialization logic
|
|
558
|
+
await this.setupInitialStructure();
|
|
559
|
+
|
|
560
|
+
// Validate source
|
|
561
|
+
this.validateSource();
|
|
562
|
+
|
|
563
|
+
// Prompt for admin PIN setup if not already configured
|
|
564
|
+
const securitySettings = settingsManager.getSecurity();
|
|
565
|
+
|
|
566
|
+
if (!securitySettings.adminPinEnabled && securitySettings.adminPinPromptOnInit !== false) {
|
|
567
|
+
await this.promptAdminPinSetup();
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Get target languages - use args.languages if provided
|
|
571
|
+
const targetLanguages = args.languages || await this.selectLanguages();
|
|
572
|
+
|
|
573
|
+
if (targetLanguages.length === 0) {
|
|
574
|
+
console.log(this.ui.t('init.noTargetLanguagesSpecified'));
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
console.log('\n' + this.ui.t('init.targetLanguages', { languages: targetLanguages.map(lang => `${lang} (${LANGUAGE_CONFIG[lang]?.name || 'Unknown'})`).join(', ') }));
|
|
579
|
+
|
|
580
|
+
// Get source files
|
|
581
|
+
const sourceFiles = this.getSourceFiles();
|
|
582
|
+
console.log('\n' + this.ui.t('init.foundSourceFiles', { count: sourceFiles.length, files: sourceFiles.join(', ') }));
|
|
583
|
+
|
|
584
|
+
// Process each language
|
|
585
|
+
const results = {};
|
|
586
|
+
|
|
587
|
+
for (const targetLanguage of targetLanguages) {
|
|
588
|
+
console.log('\n' + this.ui.t('init.processingLanguage', { language: targetLanguage, name: LANGUAGE_CONFIG[targetLanguage]?.name || 'Unknown' }));
|
|
589
|
+
|
|
590
|
+
const languageResults = {
|
|
591
|
+
files: [],
|
|
592
|
+
totalStats: { total: 0, translated: 0, missing: 0 }
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
for (const sourceFile of sourceFiles) {
|
|
596
|
+
const sourceFilePath = path.join(this.sourceLanguageDir, sourceFile);
|
|
597
|
+
const validatedSourceFilePath = SecurityUtils.validatePath(sourceFilePath, process.cwd());
|
|
598
|
+
|
|
599
|
+
if (!validatedSourceFilePath) {
|
|
600
|
+
SecurityUtils.logSecurityEvent('Invalid source file path', 'error', { path: sourceFilePath });
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const sourceContentRaw = await SecurityUtils.safeReadFile(validatedSourceFilePath, process.cwd());
|
|
605
|
+
if (!sourceContentRaw) {
|
|
606
|
+
SecurityUtils.logSecurityEvent('Failed to read source file', 'error', { file: validatedSourceFilePath });
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const sourceContent = JSON.parse(sourceContentRaw);
|
|
611
|
+
|
|
612
|
+
const targetFilePath = await this.createLanguageFile(sourceFile, targetLanguage, sourceContent);
|
|
613
|
+
|
|
614
|
+
// Get stats for this file
|
|
615
|
+
const targetContentRaw = await SecurityUtils.safeReadFile(targetFilePath, process.cwd());
|
|
616
|
+
if (!targetContentRaw) {
|
|
617
|
+
SecurityUtils.logSecurityEvent('Failed to read target file for stats', 'error', { file: targetFilePath });
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const targetContent = JSON.parse(targetContentRaw);
|
|
622
|
+
const stats = this.getTranslationStats(targetContent);
|
|
623
|
+
|
|
624
|
+
languageResults.files.push({
|
|
625
|
+
name: sourceFile,
|
|
626
|
+
path: targetFilePath,
|
|
627
|
+
stats
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
// Add to total stats
|
|
631
|
+
languageResults.totalStats.total += stats.total;
|
|
632
|
+
languageResults.totalStats.translated += stats.translated;
|
|
633
|
+
languageResults.totalStats.missing += stats.missing;
|
|
634
|
+
|
|
635
|
+
console.log(this.ui.t('init.fileProcessingResult', { file: sourceFile, translated: stats.translated, total: stats.total, percentage: stats.percentage }));
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// Calculate overall percentage
|
|
639
|
+
languageResults.totalStats.percentage = languageResults.totalStats.total > 0
|
|
640
|
+
? Math.round((languageResults.totalStats.translated / languageResults.totalStats.total) * 100)
|
|
641
|
+
: 0;
|
|
642
|
+
|
|
643
|
+
results[targetLanguage] = languageResults;
|
|
644
|
+
|
|
645
|
+
console.log(this.ui.t('init.overallProgress', { translated: languageResults.totalStats.translated, total: languageResults.totalStats.total, percentage: languageResults.totalStats.percentage }));
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Summary report
|
|
649
|
+
console.log('\n' + '=' .repeat(50));
|
|
650
|
+
console.log(this.ui.t('init.initializationSummaryTitle'));
|
|
651
|
+
console.log('=' .repeat(50));
|
|
652
|
+
|
|
653
|
+
Object.entries(results).forEach(([lang, data]) => {
|
|
654
|
+
const langName = LANGUAGE_CONFIG[lang]?.name || 'Unknown';
|
|
655
|
+
const statusIcon = data.totalStats.percentage === 100 ? '✅' : data.totalStats.percentage >= 80 ? '🟡' : '🔴';
|
|
656
|
+
|
|
657
|
+
console.log(this.ui.t('init.languageSummary', { icon: statusIcon, name: langName, code: lang, percentage: data.totalStats.percentage }));
|
|
658
|
+
console.log(this.ui.t('init.languageFiles', { count: data.files.length }));
|
|
659
|
+
console.log(this.ui.t('init.languageKeys', { translated: data.totalStats.translated, total: data.totalStats.total }));
|
|
660
|
+
console.log(this.ui.t('init.languageMissing', { count: data.totalStats.missing }));
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
console.log('\n' + this.ui.t('init.initializationCompletedSuccessfully'));
|
|
664
|
+
console.log('\n' + this.ui.t('init.nextStepsTitle'));
|
|
665
|
+
console.log(this.ui.t('init.nextStep1'));
|
|
666
|
+
console.log(this.ui.t('init.nextStep2'));
|
|
667
|
+
console.log(this.ui.t('init.nextStep3'));
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Add run method for compatibility with manager
|
|
671
|
+
async run() {
|
|
672
|
+
return await this.init();
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
module.exports = I18nInitializer;
|
|
677
|
+
|
|
678
|
+
// Run if called directly
|
|
679
|
+
if (require.main === module) {
|
|
680
|
+
const initializer = new I18nInitializer();
|
|
681
|
+
initializer.init().catch(error => {
|
|
682
|
+
console.error('❌ Initialization failed:', error.message);
|
|
683
|
+
console.error('Stack trace:', error.stack);
|
|
684
|
+
process.exit(1);
|
|
685
|
+
});
|
|
686
|
+
}
|