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,1051 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* I18NTK INIT SERVICE
|
|
5
|
+
*
|
|
6
|
+
* Core business logic for i18n project initialization.
|
|
7
|
+
* Handles directory setup, language file creation, and project configuration.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const SecurityUtils = require('../../../utils/security');
|
|
13
|
+
const configManager = require('../../../utils/config-manager');
|
|
14
|
+
const { loadTranslations, t } = require('../../../utils/i18n-helper');
|
|
15
|
+
const { detectFramework } = require('../../../utils/framework-detector');
|
|
16
|
+
const { getFormatAdapter } = require('../../../utils/format-manager');
|
|
17
|
+
const AdminAuth = require('../../../utils/admin-auth');
|
|
18
|
+
|
|
19
|
+
// Language configurations with native names
|
|
20
|
+
const LANGUAGE_CONFIG = {
|
|
21
|
+
'de': { name: 'German', nativeName: 'Deutsch' },
|
|
22
|
+
'es': { name: 'Spanish', nativeName: 'EspaƱol' },
|
|
23
|
+
'fr': { name: 'French', nativeName: 'FranƧais' },
|
|
24
|
+
'ru': { name: 'Russian', nativeName: 'Š ŃŃŃŠŗŠøŠ¹' },
|
|
25
|
+
'it': { name: 'Italian', nativeName: 'Italiano' },
|
|
26
|
+
'ja': { name: 'Japanese', nativeName: 'ę„ę¬čŖ' },
|
|
27
|
+
'ko': { name: 'Korean', nativeName: 'ķźµģ“' },
|
|
28
|
+
'zh': { name: 'Chinese', nativeName: 'äøę' },
|
|
29
|
+
'ar': { name: 'Arabic', nativeName: 'Ų§ŁŲ¹Ų±ŲØŁŲ©' },
|
|
30
|
+
'hi': { name: 'Hindi', nativeName: 'ą¤¹ą¤æą¤Øą„ą¤¦ą„' },
|
|
31
|
+
'nl': { name: 'Dutch', nativeName: 'Nederlands' },
|
|
32
|
+
'sv': { name: 'Swedish', nativeName: 'Svenska' },
|
|
33
|
+
'da': { name: 'Danish', nativeName: 'Dansk' },
|
|
34
|
+
'no': { name: 'Norwegian', nativeName: 'Norsk' },
|
|
35
|
+
'fi': { name: 'Finnish', nativeName: 'Suomi' },
|
|
36
|
+
'pl': { name: 'Polish', nativeName: 'Polski' },
|
|
37
|
+
'cs': { name: 'Czech', nativeName: 'ÄeÅ”tina' },
|
|
38
|
+
'hu': { name: 'Hungarian', nativeName: 'Magyar' },
|
|
39
|
+
'tr': { name: 'Turkish', nativeName: 'Türkçe' }
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
class InitService {
|
|
43
|
+
constructor(config = {}) {
|
|
44
|
+
this.config = {
|
|
45
|
+
sourceLanguage: 'en',
|
|
46
|
+
excludeFiles: ['.DS_Store', 'Thumbs.db'],
|
|
47
|
+
supportedExtensions: ['.json'],
|
|
48
|
+
// Default structure: modular (folder per language)
|
|
49
|
+
structure: 'modular', // one of: 'single' | 'modular' | 'existing'
|
|
50
|
+
perLanguageStructure: {}, // optional map lang -> 'single' | 'modular'
|
|
51
|
+
noPrompt: false,
|
|
52
|
+
...config
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
this.format = getFormatAdapter(this.config.format);
|
|
56
|
+
this.config.supportedExtensions = [this.format.extension];
|
|
57
|
+
this.detectedFramework = detectFramework(process.cwd());
|
|
58
|
+
if (this.detectedFramework && !this.config.translationPatterns) {
|
|
59
|
+
this.config.translationPatterns = this.detectedFramework.patterns;
|
|
60
|
+
}
|
|
61
|
+
this.sourceDir = this.config.sourceDir || './locales';
|
|
62
|
+
// Source language directory depends on structure
|
|
63
|
+
this.sourceLanguageDir = this.config.structure === 'single'
|
|
64
|
+
? this.sourceDir
|
|
65
|
+
: path.join(this.sourceDir, this.config.sourceLanguage);
|
|
66
|
+
|
|
67
|
+
// Ensure defaultLanguages is properly initialized from config
|
|
68
|
+
this.config.defaultLanguages = this.config.defaultLanguages || ['de', 'es', 'fr', 'ru'];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Check i18n dependencies
|
|
72
|
+
async checkI18nDependencies() {
|
|
73
|
+
const packageJsonPath = path.resolve('./package.json');
|
|
74
|
+
|
|
75
|
+
if (!SecurityUtils.safeExistsSync(packageJsonPath)) {
|
|
76
|
+
console.log(t('errors.noPackageJson'));
|
|
77
|
+
return true; // Allow to continue without framework
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const packageJson = JSON.parse(SecurityUtils.safeReadFileSync(packageJsonPath, path.dirname(packageJsonPath), 'utf8'));
|
|
82
|
+
// Include peerDependencies in the check
|
|
83
|
+
const dependencies = {
|
|
84
|
+
...packageJson.dependencies,
|
|
85
|
+
...packageJson.devDependencies,
|
|
86
|
+
...packageJson.peerDependencies
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const i18nFrameworks = [
|
|
90
|
+
'react-i18next',
|
|
91
|
+
'vue-i18n',
|
|
92
|
+
'angular-i18n',
|
|
93
|
+
'i18next',
|
|
94
|
+
'next-i18next',
|
|
95
|
+
'svelte-i18n',
|
|
96
|
+
'@nuxtjs/i18n'
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
const installedFrameworks = i18nFrameworks.filter(framework => dependencies[framework]);
|
|
100
|
+
|
|
101
|
+
if (installedFrameworks.length > 0) {
|
|
102
|
+
console.log(t('init.detectedI18nFrameworks', { frameworks: installedFrameworks.join(', ') }));
|
|
103
|
+
const cfg = configManager.loadSettings ? configManager.loadSettings() : (configManager.getConfig ? configManager.getConfig() : {});
|
|
104
|
+
cfg.framework = cfg.framework || {};
|
|
105
|
+
cfg.framework.detected = true;
|
|
106
|
+
cfg.framework.installed = installedFrameworks;
|
|
107
|
+
if (configManager.saveSettings) {
|
|
108
|
+
configManager.saveSettings(cfg);
|
|
109
|
+
} else if (configManager.saveConfig) {
|
|
110
|
+
configManager.saveConfig(cfg);
|
|
111
|
+
}
|
|
112
|
+
return true;
|
|
113
|
+
} else {
|
|
114
|
+
const cfg = configManager.loadSettings ? configManager.loadSettings() : (configManager.getConfig ? configManager.getConfig() : {});
|
|
115
|
+
if (cfg.framework) {
|
|
116
|
+
cfg.framework.detected = false;
|
|
117
|
+
if (configManager.saveSettings) {
|
|
118
|
+
configManager.saveSettings(cfg);
|
|
119
|
+
} else if (configManager.saveConfig) {
|
|
120
|
+
configManager.saveConfig(cfg);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
} catch (error) {
|
|
126
|
+
console.log(t('init.errors.packageJsonRead'));
|
|
127
|
+
return true; // Allow to continue on error
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Detect existing translation directories and allow user selection
|
|
132
|
+
async detectAndSelectDirectory(skipPrompt = false) {
|
|
133
|
+
const possibleLocations = [
|
|
134
|
+
'./locales',
|
|
135
|
+
'./src/locales',
|
|
136
|
+
'./src/i18n/locales',
|
|
137
|
+
'./app/locales',
|
|
138
|
+
'./public/locales',
|
|
139
|
+
'./translations',
|
|
140
|
+
'./lang',
|
|
141
|
+
'./i18n/locales',
|
|
142
|
+
'./assets/locales',
|
|
143
|
+
'./client/locales',
|
|
144
|
+
'./frontend/locales'
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
const existingLocations = [];
|
|
148
|
+
|
|
149
|
+
// Check for existing translation directories
|
|
150
|
+
for (const location of possibleLocations) {
|
|
151
|
+
if (SecurityUtils.safeExistsSync(location)) {
|
|
152
|
+
try {
|
|
153
|
+
const items = fs.readdirSync(location);
|
|
154
|
+
const englishFormats = ['en', 'en-US', 'en-GB', 'english'];
|
|
155
|
+
|
|
156
|
+
// Check for English directories first
|
|
157
|
+
for (const format of englishFormats) {
|
|
158
|
+
const englishPath = path.join(location, format);
|
|
159
|
+
if (SecurityUtils.safeExistsSync(englishPath) && fs.statSync(englishPath).isDirectory()) {
|
|
160
|
+
const englishFiles = fs.readdirSync(englishPath).filter(file => file.endsWith(this.format.extension));
|
|
161
|
+
if (englishFiles.length > 0) {
|
|
162
|
+
// Found English files, prioritize this
|
|
163
|
+
existingLocations.unshift(location);
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Also check for any language directories or format files
|
|
170
|
+
const hasLanguageDirs = items.some(item => {
|
|
171
|
+
const itemPath = path.join(location, item);
|
|
172
|
+
if (fs.statSync(itemPath).isDirectory()) {
|
|
173
|
+
return ['en', 'de', 'es', 'fr', 'ru', 'ja', 'zh', 'en-US', 'en-GB'].includes(item);
|
|
174
|
+
}
|
|
175
|
+
return item.endsWith(this.format.extension);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
if (hasLanguageDirs && !existingLocations.includes(location)) {
|
|
179
|
+
existingLocations.push(location);
|
|
180
|
+
}
|
|
181
|
+
} catch (error) {
|
|
182
|
+
// Continue checking other locations
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (existingLocations.length > 0) {
|
|
188
|
+
if (skipPrompt || !process.stdin.isTTY) {
|
|
189
|
+
const selectedDir = existingLocations[0];
|
|
190
|
+
this.config.sourceDir = selectedDir;
|
|
191
|
+
this.sourceDir = path.resolve(selectedDir);
|
|
192
|
+
this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
|
|
193
|
+
const rel = configManager.toRelative(selectedDir);
|
|
194
|
+
await configManager.updateConfig({ sourceDir: rel, i18nDir: rel });
|
|
195
|
+
return selectedDir;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
console.log('\n' + t('init.existingDirectoriesFound'));
|
|
199
|
+
console.log(t('common.separator'));
|
|
200
|
+
|
|
201
|
+
// List existing locations
|
|
202
|
+
existingLocations.forEach((location, index) => {
|
|
203
|
+
console.log(` ${index + 1}. ${location}`);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Add options for new directory and exit
|
|
207
|
+
console.log(` ${existingLocations.length + 1}. Create new directory`);
|
|
208
|
+
console.log(` 0. Exit`);
|
|
209
|
+
|
|
210
|
+
let answer;
|
|
211
|
+
let selectedIndex;
|
|
212
|
+
|
|
213
|
+
// Keep asking until we get a valid number
|
|
214
|
+
while (true) {
|
|
215
|
+
answer = await this.prompt('\n' + t('init.selectDirectoryPrompt') + ' (0-' + (existingLocations.length + 1) + '):');
|
|
216
|
+
|
|
217
|
+
// Check for exit (0)
|
|
218
|
+
if (answer === '0') {
|
|
219
|
+
console.log(t('init.initializationCancelled'));
|
|
220
|
+
process.exit(0);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Parse the selection
|
|
224
|
+
selectedIndex = parseInt(answer) - 1;
|
|
225
|
+
|
|
226
|
+
// Validate the selection
|
|
227
|
+
if (!isNaN(selectedIndex) && selectedIndex >= 0 && selectedIndex <= existingLocations.length) {
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
console.log(t('errors.invalidOption', { option: answer }));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (selectedIndex >= 0 && selectedIndex < existingLocations.length) {
|
|
235
|
+
const selectedDir = existingLocations[selectedIndex];
|
|
236
|
+
if (!this.announcedExistingDir) {
|
|
237
|
+
console.log(t('init.usingExistingDirectory', { dir: selectedDir }));
|
|
238
|
+
this.announcedExistingDir = true;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
this.config.sourceDir = selectedDir;
|
|
242
|
+
this.sourceDir = path.resolve(selectedDir);
|
|
243
|
+
this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
|
|
244
|
+
|
|
245
|
+
const rel = configManager.toRelative(selectedDir);
|
|
246
|
+
await configManager.updateConfig({ sourceDir: rel, i18nDir: rel });
|
|
247
|
+
|
|
248
|
+
return selectedDir;
|
|
249
|
+
} else if (selectedIndex === existingLocations.length) {
|
|
250
|
+
const newDirName = await this.prompt('\n' + t('init.enterNewDirectoryName') + ': ');
|
|
251
|
+
if (newDirName && newDirName.trim()) {
|
|
252
|
+
const newDirPath = path.resolve(newDirName.trim());
|
|
253
|
+
|
|
254
|
+
if (!SecurityUtils.safeExistsSync(newDirPath)) {
|
|
255
|
+
fs.mkdirSync(newDirPath, { recursive: true });
|
|
256
|
+
console.log(t('init.createdNewDirectory', { dir: newDirPath }));
|
|
257
|
+
} else {
|
|
258
|
+
console.log(t('init.directoryAlreadyExists', { dir: newDirPath }));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const sourceLangDir = path.join(newDirPath, this.config.sourceLanguage);
|
|
262
|
+
if (!SecurityUtils.safeExistsSync(sourceLangDir)) {
|
|
263
|
+
fs.mkdirSync(sourceLangDir, { recursive: true });
|
|
264
|
+
console.log(t('init.createdSourceLanguageDirectory', { dir: sourceLangDir }));
|
|
265
|
+
await this.createSampleTranslationFile(sourceLangDir);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
this.config.sourceDir = newDirPath;
|
|
269
|
+
this.sourceDir = newDirPath;
|
|
270
|
+
this.sourceLanguageDir = sourceLangDir;
|
|
271
|
+
|
|
272
|
+
const rel = configManager.toRelative(newDirPath);
|
|
273
|
+
await configManager.updateConfig({ sourceDir: rel, i18nDir: rel });
|
|
274
|
+
|
|
275
|
+
return newDirPath;
|
|
276
|
+
} else {
|
|
277
|
+
console.log(t('init.invalidDirectoryName'));
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (skipPrompt || !process.stdin.isTTY) {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Setup initial directory structure if needed
|
|
291
|
+
async setupInitialStructure(skipPrompt = false) {
|
|
292
|
+
// First, detect if there are existing translation directories with English files
|
|
293
|
+
const usedExisting = await this.detectAndSelectDirectory(skipPrompt);
|
|
294
|
+
|
|
295
|
+
if (usedExisting) {
|
|
296
|
+
console.log(t('init.usingExistingDirectory', { dir: this.sourceDir }));
|
|
297
|
+
// When using existing, sourceLanguageDir might already be set to English directory
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Validate paths
|
|
302
|
+
const validatedSourceDir = SecurityUtils.validatePath(this.sourceDir, process.cwd());
|
|
303
|
+
if (!validatedSourceDir) {
|
|
304
|
+
throw new Error(t('validate.invalidSourceDirectory', { sourceDir: this.sourceDir }) || `Invalid source directory: ${this.sourceDir}`);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// For modular structure, ensure per-language subdirectory exists
|
|
308
|
+
let validatedSourceLanguageDir = this.sourceLanguageDir;
|
|
309
|
+
if (this.config.structure !== 'single') {
|
|
310
|
+
validatedSourceLanguageDir = SecurityUtils.validatePath(this.sourceLanguageDir, process.cwd());
|
|
311
|
+
if (!validatedSourceLanguageDir) {
|
|
312
|
+
throw new Error(t('validate.invalidSourceLanguageDirectory', { sourceDir: this.sourceLanguageDir }) || `Invalid source language directory: ${this.sourceLanguageDir}`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Create directories if they do not exist
|
|
317
|
+
if (!SecurityUtils.safeExistsSync(validatedSourceDir)) {
|
|
318
|
+
fs.mkdirSync(validatedSourceDir, { recursive: true });
|
|
319
|
+
}
|
|
320
|
+
if (this.config.structure !== 'single' && !SecurityUtils.safeExistsSync(validatedSourceLanguageDir)) {
|
|
321
|
+
fs.mkdirSync(validatedSourceLanguageDir, { recursive: true });
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Create sample translation file if none exist
|
|
325
|
+
const englishFiles = (this.config.structure === 'single'
|
|
326
|
+
? fs.readdirSync(validatedSourceDir)
|
|
327
|
+
: fs.readdirSync(validatedSourceLanguageDir))
|
|
328
|
+
.filter(file => file.endsWith(this.format.extension));
|
|
329
|
+
|
|
330
|
+
if (englishFiles.length === 0) {
|
|
331
|
+
await this.createSampleTranslationFile(this.config.structure === 'single' ? validatedSourceDir : validatedSourceLanguageDir);
|
|
332
|
+
} else {
|
|
333
|
+
// Directory exists, check if we need to create a sample file
|
|
334
|
+
const existingFiles = (this.config.structure === 'single' ? fs.readdirSync(validatedSourceDir) : fs.readdirSync(validatedSourceLanguageDir))
|
|
335
|
+
.filter(file => file.endsWith(this.format.extension));
|
|
336
|
+
|
|
337
|
+
if (existingFiles.length === 0) {
|
|
338
|
+
// No format files exist, create sample file
|
|
339
|
+
await this.createSampleTranslationFile(this.config.structure === 'single' ? validatedSourceDir : validatedSourceLanguageDir);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const rel = configManager.toRelative(this.sourceDir);
|
|
344
|
+
await configManager.updateConfig({ sourceDir: rel, i18nDir: rel });
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Create sample translation file with smart naming
|
|
348
|
+
async createSampleTranslationFile(validatedSourceLanguageDir) {
|
|
349
|
+
const sampleTranslations = {
|
|
350
|
+
"common": {
|
|
351
|
+
"welcome": "Welcome",
|
|
352
|
+
"hello": "Hello",
|
|
353
|
+
"goodbye": "Goodbye",
|
|
354
|
+
"yes": "Yes",
|
|
355
|
+
"no": "No",
|
|
356
|
+
"save": "Save",
|
|
357
|
+
"cancel": "Cancel",
|
|
358
|
+
"delete": "Delete",
|
|
359
|
+
"edit": "Edit",
|
|
360
|
+
"loading": "Loading..."
|
|
361
|
+
},
|
|
362
|
+
"navigation": {
|
|
363
|
+
"home": "Home",
|
|
364
|
+
"about": "About",
|
|
365
|
+
"contact": "Contact",
|
|
366
|
+
"settings": "Settings"
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
// Determine filename: use common.json if it doesn't exist, otherwise i18ntk-common.json
|
|
371
|
+
const commonFilePath = path.join(validatedSourceLanguageDir, `common${this.format.extension}`);
|
|
372
|
+
const i18ntkCommonFilePath = path.join(validatedSourceLanguageDir, `i18ntk-common${this.format.extension}`);
|
|
373
|
+
|
|
374
|
+
let sampleFilePath;
|
|
375
|
+
if (!SecurityUtils.safeExistsSync(commonFilePath)) {
|
|
376
|
+
sampleFilePath = commonFilePath;
|
|
377
|
+
} else {
|
|
378
|
+
sampleFilePath = i18ntkCommonFilePath;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const validatedSampleFilePath = SecurityUtils.validatePath(sampleFilePath, process.cwd());
|
|
382
|
+
|
|
383
|
+
if (!validatedSampleFilePath) {
|
|
384
|
+
SecurityUtils.logSecurityEvent('Invalid sample file path', 'error', { path: sampleFilePath });
|
|
385
|
+
throw new Error(t('validate.invalidSampleFilePath') || 'Invalid sample file path');
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const success = await SecurityUtils.safeWriteFile(validatedSampleFilePath, this.format.serialize(sampleTranslations), process.cwd());
|
|
389
|
+
|
|
390
|
+
if (success) {
|
|
391
|
+
console.log(t('init.createdSampleTranslationFile', { file: validatedSampleFilePath }));
|
|
392
|
+
SecurityUtils.logSecurityEvent('Sample translation file created', 'info', { file: validatedSampleFilePath });
|
|
393
|
+
} else {
|
|
394
|
+
SecurityUtils.logSecurityEvent('Failed to create sample translation file', 'error', { file: validatedSampleFilePath });
|
|
395
|
+
throw new Error(t('validate.failedToCreateSampleTranslationFile') || 'Failed to create sample translation file');
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Check if source directory and language exist
|
|
400
|
+
validateSource() {
|
|
401
|
+
if (!SecurityUtils.safeExistsSync(this.sourceDir)) {
|
|
402
|
+
throw new Error(t('validate.sourceLanguageDirectoryNotFound', { sourceDir: this.sourceDir }) || `Source directory not found: ${this.sourceDir}`);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (!SecurityUtils.safeExistsSync(this.sourceLanguageDir)) {
|
|
406
|
+
throw new Error(t('validate.sourceLanguageDirectoryNotFound', { sourceDir: this.sourceLanguageDir }) || `Source language directory not found: ${this.sourceLanguageDir}`);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return true;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
createBootstrapSourceFile(targetDir) {
|
|
413
|
+
try {
|
|
414
|
+
if (!SecurityUtils.safeExistsSync(targetDir)) {
|
|
415
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const sampleName = `common${this.format.extension}`;
|
|
419
|
+
const samplePath = path.join(targetDir, sampleName);
|
|
420
|
+
|
|
421
|
+
if (SecurityUtils.safeExistsSync(samplePath)) {
|
|
422
|
+
return sampleName;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const sampleContent = {
|
|
426
|
+
app: {
|
|
427
|
+
title: 'Application',
|
|
428
|
+
description: 'Application description'
|
|
429
|
+
},
|
|
430
|
+
common: {
|
|
431
|
+
yes: 'Yes',
|
|
432
|
+
no: 'No',
|
|
433
|
+
save: 'Save',
|
|
434
|
+
cancel: 'Cancel'
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
const serializer = typeof this.format?.write === 'function'
|
|
439
|
+
? this.format.write.bind(this.format)
|
|
440
|
+
: (typeof this.format?.serialize === 'function'
|
|
441
|
+
? this.format.serialize.bind(this.format)
|
|
442
|
+
: (data) => JSON.stringify(data, null, 2));
|
|
443
|
+
|
|
444
|
+
const serialized = serializer(sampleContent);
|
|
445
|
+
SecurityUtils.safeWriteFileSync(samplePath, `${serialized}\n`, path.dirname(samplePath), 'utf8');
|
|
446
|
+
console.log(t('init.createdSampleTranslationFile', { file: samplePath }) || `Created sample translation file: ${samplePath}`);
|
|
447
|
+
|
|
448
|
+
return sampleName;
|
|
449
|
+
} catch {
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Get all JSON files from source language directory (supports single/modular)
|
|
455
|
+
getSourceFiles() {
|
|
456
|
+
try {
|
|
457
|
+
if (this.config.structure === 'single') {
|
|
458
|
+
if (!SecurityUtils.safeExistsSync(this.sourceDir)) {
|
|
459
|
+
throw new Error(t('validate.sourceLanguageDirectoryNotFound', { sourceDir: this.sourceDir }) || `Source directory not found: ${this.sourceDir}`);
|
|
460
|
+
}
|
|
461
|
+
const files = fs.readdirSync(this.sourceDir)
|
|
462
|
+
.filter(file => file.endsWith(this.format.extension) && !this.config.excludeFiles.includes(file));
|
|
463
|
+
if (files.length === 0) {
|
|
464
|
+
const sampleFile = this.createBootstrapSourceFile(this.sourceDir);
|
|
465
|
+
if (sampleFile) return [sampleFile];
|
|
466
|
+
throw new Error(t('validate.noJsonFilesFound', { sourceDir: this.sourceDir }) || `No JSON files found in source directory: ${this.sourceDir}`);
|
|
467
|
+
}
|
|
468
|
+
return files;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (!SecurityUtils.safeExistsSync(this.sourceLanguageDir)) {
|
|
472
|
+
// Try to find English files in parent directory or subdirectories
|
|
473
|
+
const parentDir = path.dirname(this.sourceLanguageDir);
|
|
474
|
+
if (SecurityUtils.safeExistsSync(parentDir)) {
|
|
475
|
+
const subdirs = fs.readdirSync(parentDir).filter(item => {
|
|
476
|
+
const fullPath = path.join(parentDir, item);
|
|
477
|
+
return fs.statSync(fullPath).isDirectory();
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
// Look for English files in any subdirectory
|
|
481
|
+
for (const subdir of subdirs) {
|
|
482
|
+
const englishDir = path.join(parentDir, subdir);
|
|
483
|
+
if (SecurityUtils.safeExistsSync(englishDir)) {
|
|
484
|
+
const files = fs.readdirSync(englishDir);
|
|
485
|
+
const formatFiles = files.filter(file =>
|
|
486
|
+
file.endsWith(this.format.extension) &&
|
|
487
|
+
!this.config.excludeFiles.includes(file)
|
|
488
|
+
);
|
|
489
|
+
if (formatFiles.length > 0) {
|
|
490
|
+
// Found English files, use this directory
|
|
491
|
+
this.sourceLanguageDir = englishDir;
|
|
492
|
+
return formatFiles;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
throw new Error(t('validate.noJsonFilesFound', { sourceDir: this.sourceLanguageDir }) || `No JSON files found in source directory: ${this.sourceLanguageDir}`);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const files = fs.readdirSync(this.sourceLanguageDir)
|
|
501
|
+
.filter(file => {
|
|
502
|
+
return file.endsWith(this.format.extension) &&
|
|
503
|
+
!this.config.excludeFiles.includes(file);
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
if (files.length === 0) {
|
|
507
|
+
const sampleFile = this.createBootstrapSourceFile(this.sourceLanguageDir);
|
|
508
|
+
if (sampleFile) return [sampleFile];
|
|
509
|
+
throw new Error(t('validate.noJsonFilesFound', { sourceDir: this.sourceLanguageDir }) || `No JSON files found in source directory: ${this.sourceLanguageDir}`);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return files;
|
|
513
|
+
} catch (error) {
|
|
514
|
+
console.warn(t('init.warningCannotReadSourceDir', { dir: this.sourceLanguageDir, error: error.message }));
|
|
515
|
+
throw error;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Recursively mark all string values with country code markers
|
|
520
|
+
markWithCountryCode(obj, countryCode) {
|
|
521
|
+
if (typeof obj === 'string') {
|
|
522
|
+
return `[${countryCode.toUpperCase()}] ${obj}`;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (Array.isArray(obj)) {
|
|
526
|
+
return obj.map(item => this.markWithCountryCode(item, countryCode));
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (obj && typeof obj === 'object') {
|
|
530
|
+
const result = {};
|
|
531
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
532
|
+
result[key] = this.markWithCountryCode(value, countryCode);
|
|
533
|
+
}
|
|
534
|
+
return result;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
return obj;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Get the structure type for a specific language
|
|
541
|
+
getLanguageStructure(language) {
|
|
542
|
+
// Check for per-language structure first
|
|
543
|
+
if (this.config.perLanguageStructure && this.config.perLanguageStructure[language]) {
|
|
544
|
+
return this.config.perLanguageStructure[language];
|
|
545
|
+
}
|
|
546
|
+
// Fall back to the global structure
|
|
547
|
+
return this.config.structure || 'modular';
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Create or update a language file securely (supports single/modular)
|
|
551
|
+
async createLanguageFile(sourceFile, targetLanguage, sourceContent) {
|
|
552
|
+
try {
|
|
553
|
+
const sourceFilePath = path.join(this.sourceLanguageDir, sourceFile);
|
|
554
|
+
let targetFilePath;
|
|
555
|
+
if (this.getLanguageStructure(targetLanguage) === 'single') {
|
|
556
|
+
// Single-file per language, write to <lang>.json in sourceDir
|
|
557
|
+
const baseName = `${targetLanguage}${this.format.extension}`;
|
|
558
|
+
targetFilePath = path.join(this.sourceDir, baseName);
|
|
559
|
+
} else {
|
|
560
|
+
// Modular: folder per language mirroring source file
|
|
561
|
+
const targetDir = path.join(this.sourceDir, targetLanguage);
|
|
562
|
+
if (!SecurityUtils.safeExistsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true });
|
|
563
|
+
targetFilePath = path.join(targetDir, sourceFile);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Validate source and target paths
|
|
567
|
+
const validatedSourcePath = SecurityUtils.validatePath(sourceFilePath, process.cwd());
|
|
568
|
+
const validatedTargetPath = SecurityUtils.validatePath(targetFilePath, process.cwd());
|
|
569
|
+
const targetDir = path.dirname(validatedTargetPath);
|
|
570
|
+
|
|
571
|
+
if (!validatedSourcePath || !validatedTargetPath) {
|
|
572
|
+
SecurityUtils.logSecurityEvent('Invalid path detected in createLanguageFile', 'error', {
|
|
573
|
+
sourcePath: sourceFilePath,
|
|
574
|
+
targetPath: targetFilePath
|
|
575
|
+
});
|
|
576
|
+
throw new Error(t('validate.invalidFilePathDetected') || 'Invalid file path detected');
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Create target directory if it doesn't exist
|
|
580
|
+
if (!SecurityUtils.safeExistsSync(targetDir)) {
|
|
581
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
let targetContent;
|
|
585
|
+
|
|
586
|
+
// If target file exists, preserve existing translations
|
|
587
|
+
if (SecurityUtils.safeExistsSync(validatedTargetPath)) {
|
|
588
|
+
try {
|
|
589
|
+
const existingContent = await SecurityUtils.safeReadFile(validatedTargetPath, process.cwd());
|
|
590
|
+
if (existingContent) {
|
|
591
|
+
targetContent = this.mergeTranslations(sourceContent, this.format.read(existingContent), targetLanguage);
|
|
592
|
+
} else {
|
|
593
|
+
targetContent = this.markWithCountryCode(sourceContent, targetLanguage);
|
|
594
|
+
}
|
|
595
|
+
} catch (error) {
|
|
596
|
+
console.warn(`ā ļø Warning: Could not parse existing file ${validatedTargetPath}, creating new one`);
|
|
597
|
+
SecurityUtils.logSecurityEvent('File parse error', 'warn', { file: validatedTargetPath, error: error.message });
|
|
598
|
+
targetContent = this.markWithCountryCode(sourceContent, targetLanguage);
|
|
599
|
+
}
|
|
600
|
+
} else {
|
|
601
|
+
targetContent = this.markWithCountryCode(sourceContent, targetLanguage);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Write the file securely
|
|
605
|
+
const success = await SecurityUtils.safeWriteFile(validatedTargetPath, this.format.serialize(targetContent), process.cwd());
|
|
606
|
+
|
|
607
|
+
if (!success) {
|
|
608
|
+
SecurityUtils.logSecurityEvent('Failed to write language file', 'error', { file: validatedTargetPath });
|
|
609
|
+
throw new Error(t('validate.failedToWriteFile', { filePath: validatedTargetPath }) || `Failed to write file: ${validatedTargetPath}`);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
SecurityUtils.logSecurityEvent('Language file created/updated', 'info', { file: validatedTargetPath, language: targetLanguage });
|
|
613
|
+
return validatedTargetPath;
|
|
614
|
+
} catch (error) {
|
|
615
|
+
console.error(t('init.errors.initializationFailed', { error: error.message }));
|
|
616
|
+
return false;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Merge existing translations with new structure using country code markers
|
|
621
|
+
mergeTranslations(sourceObj, existingObj, countryCode) {
|
|
622
|
+
if (typeof sourceObj === 'string') {
|
|
623
|
+
// If existing translation exists and doesn't contain country code marker, keep it
|
|
624
|
+
if (typeof existingObj === 'string' &&
|
|
625
|
+
!existingObj.startsWith(`[${countryCode.toUpperCase()}]`) &&
|
|
626
|
+
existingObj.trim() !== '') {
|
|
627
|
+
return existingObj;
|
|
628
|
+
}
|
|
629
|
+
return this.markWithCountryCode(sourceObj, countryCode);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
if (Array.isArray(sourceObj)) {
|
|
633
|
+
return sourceObj.map((item, index) => {
|
|
634
|
+
const existingItem = Array.isArray(existingObj) ? existingObj[index] : undefined;
|
|
635
|
+
return this.mergeTranslations(item, existingItem, countryCode);
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
if (sourceObj && typeof sourceObj === 'object') {
|
|
640
|
+
const result = {};
|
|
641
|
+
for (const [key, value] of Object.entries(sourceObj)) {
|
|
642
|
+
const existingValue = existingObj && typeof existingObj === 'object' ? existingObj[key] : undefined;
|
|
643
|
+
result[key] = this.mergeTranslations(value, existingValue, countryCode);
|
|
644
|
+
}
|
|
645
|
+
return result;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
return sourceObj;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Get translation statistics
|
|
652
|
+
getTranslationStats(obj) {
|
|
653
|
+
let total = 0;
|
|
654
|
+
let translated = 0;
|
|
655
|
+
let missing = 0;
|
|
656
|
+
|
|
657
|
+
const count = (item) => {
|
|
658
|
+
if (typeof item === 'string') {
|
|
659
|
+
total++;
|
|
660
|
+
const isCountryCodeMarker = /^\[([A-Z]{2})\]/.test(item);
|
|
661
|
+
if (isCountryCodeMarker) {
|
|
662
|
+
missing++;
|
|
663
|
+
} else if (item.trim() !== '') {
|
|
664
|
+
translated++;
|
|
665
|
+
}
|
|
666
|
+
} else if (Array.isArray(item)) {
|
|
667
|
+
item.forEach(count);
|
|
668
|
+
} else if (item && typeof item === 'object') {
|
|
669
|
+
Object.values(item).forEach(count);
|
|
670
|
+
}
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
count(obj);
|
|
674
|
+
|
|
675
|
+
return {
|
|
676
|
+
total,
|
|
677
|
+
translated,
|
|
678
|
+
percentage: total > 0 ? Math.round((translated / total) * 100) : 0,
|
|
679
|
+
missing
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// Interactive admin PIN setup
|
|
684
|
+
async promptAdminPinSetup() {
|
|
685
|
+
const { ask, askHidden, flushStdout } = require('../../../utils/cli');
|
|
686
|
+
|
|
687
|
+
console.log('\n' + t('init.adminPinSetupOptional'));
|
|
688
|
+
console.log(t('init.adminPinSeparator'));
|
|
689
|
+
console.log(t('init.adminPinDescription1'));
|
|
690
|
+
console.log(t('init.adminPinDescription2'));
|
|
691
|
+
console.log(t('init.adminPinDescription3'));
|
|
692
|
+
console.log(t('init.adminPinDescription4'));
|
|
693
|
+
|
|
694
|
+
await flushStdout();
|
|
695
|
+
const enableProtection = await ask('\n' + t('adminPin.setup_prompt'));
|
|
696
|
+
|
|
697
|
+
if (enableProtection.toLowerCase() === 'y' || enableProtection.toLowerCase() === 'yes') {
|
|
698
|
+
try {
|
|
699
|
+
const adminAuth = new AdminAuth();
|
|
700
|
+
await adminAuth.initialize();
|
|
701
|
+
|
|
702
|
+
let pin = null;
|
|
703
|
+
do {
|
|
704
|
+
pin = await askHidden(t('init.enterAdminPin'));
|
|
705
|
+
if (!/^\d{4}$/.test(pin)) {
|
|
706
|
+
console.log(t('init.pinMustBe4Digits'));
|
|
707
|
+
pin = null;
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
const confirm = await askHidden(t('init.confirmAdminPin'));
|
|
711
|
+
if (pin !== confirm) {
|
|
712
|
+
console.log(t('init.pinMismatch'));
|
|
713
|
+
pin = null;
|
|
714
|
+
}
|
|
715
|
+
} while (!pin);
|
|
716
|
+
|
|
717
|
+
const saved = await adminAuth.setupPin(pin);
|
|
718
|
+
if (saved) {
|
|
719
|
+
await configManager.updateConfig({
|
|
720
|
+
security: {
|
|
721
|
+
adminPinEnabled: true,
|
|
722
|
+
adminPinPromptOnInit: true,
|
|
723
|
+
pinProtection: { enabled: true }
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
console.log(t('init.adminPinSetupSuccess'));
|
|
727
|
+
} else {
|
|
728
|
+
console.error(t('init.errorSettingUpAdminPin', { error: 'Failed to save PIN' }));
|
|
729
|
+
}
|
|
730
|
+
} catch (error) {
|
|
731
|
+
console.error(t('init.errorSettingUpAdminPin', { error: error.message }));
|
|
732
|
+
console.log(t('init.continuingWithoutAdminPin'));
|
|
733
|
+
}
|
|
734
|
+
} else {
|
|
735
|
+
console.log(t('init.skippingAdminPinSetup'));
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Interactive language selection
|
|
740
|
+
async selectLanguages(skipPrompt = false) {
|
|
741
|
+
if (skipPrompt || !process.stdin.isTTY) {
|
|
742
|
+
return this.config.defaultLanguages;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const { ask } = require('../../../utils/cli');
|
|
746
|
+
|
|
747
|
+
console.log('\n' + t('init.languageSelectionTitle'));
|
|
748
|
+
console.log(t('common.separator'));
|
|
749
|
+
console.log(t('init.available'));
|
|
750
|
+
|
|
751
|
+
Object.entries(LANGUAGE_CONFIG).forEach(([code, config], index) => {
|
|
752
|
+
console.log(` ${(index + 1).toString().padStart(2)}. ${code} - ${config.name} (${config.nativeName})`);
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
console.log('\n' + t('init.defaultLanguages', { languages: this.config.defaultLanguages.join(', ') }));
|
|
756
|
+
|
|
757
|
+
const answer = await ask('\n' + t('init.enterLanguageCodes'));
|
|
758
|
+
|
|
759
|
+
if (answer.trim() === '') {
|
|
760
|
+
return this.config.defaultLanguages;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
const selectedLanguages = answer.split(',').map(lang => lang.trim().toLowerCase());
|
|
764
|
+
const validLanguages = selectedLanguages.filter(lang => LANGUAGE_CONFIG[lang]);
|
|
765
|
+
const invalidLanguages = selectedLanguages.filter(lang => !LANGUAGE_CONFIG[lang]);
|
|
766
|
+
|
|
767
|
+
if (invalidLanguages.length > 0) {
|
|
768
|
+
console.warn(t('init.warningInvalidLanguageCodes', { languages: invalidLanguages.join(', ') }));
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
return validLanguages.length > 0 ? validLanguages : this.config.defaultLanguages;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// Interactive setup configuration (internationalized)
|
|
775
|
+
async promptSetupConfiguration(skipPrompt = false) {
|
|
776
|
+
if (skipPrompt || !process.stdin.isTTY) {
|
|
777
|
+
return { structure: 'modular', duplicateStructure: true };
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
const { ask } = require('../../../utils/cli');
|
|
781
|
+
|
|
782
|
+
console.log('\n' + t('init.setup.title'));
|
|
783
|
+
console.log(t('common.separator'));
|
|
784
|
+
// Determine recommended option
|
|
785
|
+
const recommended = ' (recommended)';
|
|
786
|
+
console.log(t('init.setup.question'));
|
|
787
|
+
console.log(' 1. ' + t('init.setup.opt_single') + (this.config.structure === 'single' ? recommended : ''));
|
|
788
|
+
console.log(' 2. ' + t('init.setup.opt_modular') + (this.config.structure !== 'single' ? recommended : ''));
|
|
789
|
+
console.log(' 3. ' + t('init.setup.opt_existing'));
|
|
790
|
+
|
|
791
|
+
const structureChoice = await ask('\n' + t('init.setup.choice_prompt'));
|
|
792
|
+
|
|
793
|
+
let structure = 'modular';
|
|
794
|
+
if (structureChoice === '1') structure = 'single';
|
|
795
|
+
else if (structureChoice === '2') structure = 'modular';
|
|
796
|
+
else structure = 'existing';
|
|
797
|
+
|
|
798
|
+
let duplicateStructure = true;
|
|
799
|
+
let perLanguage = [];
|
|
800
|
+
if (structure !== 'existing') {
|
|
801
|
+
const duplicateChoice = await ask('\n' + t('init.setup.apply_all_prompt'));
|
|
802
|
+
duplicateStructure = duplicateChoice.toLowerCase() === 'y' || duplicateChoice.toLowerCase() === 'yes';
|
|
803
|
+
if (!duplicateStructure) {
|
|
804
|
+
// Prompt for languages to include/exclude
|
|
805
|
+
console.log(t('init.setup.per_language_intro'));
|
|
806
|
+
const available = Object.keys(LANGUAGE_CONFIG).join(', ');
|
|
807
|
+
console.log(t('init.setup.available_languages', { languages: available }));
|
|
808
|
+
const includeAns = await ask(t('init.setup.include_prompt'));
|
|
809
|
+
perLanguage = includeAns.split(',').map(l => l.trim().toLowerCase()).filter(Boolean);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
return { structure, duplicateStructure, perLanguage };
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// Enhanced initialization with dependency checking
|
|
817
|
+
async initialize(hasI18n = true, args = {}) {
|
|
818
|
+
console.log(t('init.initializingProject'));
|
|
819
|
+
|
|
820
|
+
if (!hasI18n) {
|
|
821
|
+
console.log(t('init.warningProceedingWithoutFramework'));
|
|
822
|
+
console.log(t('init.translationFilesCreatedWarning'));
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// Get setup configuration
|
|
826
|
+
const setupConfig = await this.promptSetupConfiguration(args.noPrompt);
|
|
827
|
+
|
|
828
|
+
// Handle directory selection and structure setup
|
|
829
|
+
const selectedDir = await this.detectAndSelectDirectory(args.noPrompt);
|
|
830
|
+
if (selectedDir) {
|
|
831
|
+
this.config.sourceDir = selectedDir;
|
|
832
|
+
this.sourceDir = path.resolve(selectedDir);
|
|
833
|
+
this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
|
|
834
|
+
if (!this.announcedExistingDir) {
|
|
835
|
+
console.log(t('init.usingExistingDirectory', { dir: selectedDir }));
|
|
836
|
+
this.announcedExistingDir = true;
|
|
837
|
+
}
|
|
838
|
+
} else {
|
|
839
|
+
await this.setupInitialStructure(args.noPrompt);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// Validate source
|
|
843
|
+
this.validateSource();
|
|
844
|
+
|
|
845
|
+
// Prompt for admin PIN setup if not already configured
|
|
846
|
+
const securitySettings = configManager.getConfig().security || {};
|
|
847
|
+
|
|
848
|
+
if (!securitySettings.adminPinEnabled && securitySettings.adminPinPromptOnInit !== false && !args.noPrompt) {
|
|
849
|
+
const { flushStdout } = require('../../../utils/cli');
|
|
850
|
+
await flushStdout();
|
|
851
|
+
await this.promptAdminPinSetup();
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// Get target languages - use args.languages if provided
|
|
855
|
+
let targetLanguages = args.languages || await this.selectLanguages(args.noPrompt);
|
|
856
|
+
|
|
857
|
+
// Ensure targetLanguages is always an array
|
|
858
|
+
targetLanguages = Array.isArray(targetLanguages) ? targetLanguages : [];
|
|
859
|
+
|
|
860
|
+
if (targetLanguages.length === 0) {
|
|
861
|
+
console.log(t('init.noTargetLanguagesSpecified'));
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
console.log('\n' + t('init.targetLanguages', { languages: targetLanguages.map(lang => `${lang} (${LANGUAGE_CONFIG[lang]?.name || 'Unknown'})`).join(', ') }));
|
|
866
|
+
|
|
867
|
+
// Get source files
|
|
868
|
+
const sourceFiles = this.getSourceFiles();
|
|
869
|
+
console.log('\n' + t('init.foundSourceFiles', { count: sourceFiles.length }));
|
|
870
|
+
|
|
871
|
+
// Process each language
|
|
872
|
+
const results = {};
|
|
873
|
+
|
|
874
|
+
for (const targetLanguage of targetLanguages) {
|
|
875
|
+
console.log('\n' + t('init.processingLanguage', { language: targetLanguage, name: LANGUAGE_CONFIG[targetLanguage]?.name || 'Unknown' }));
|
|
876
|
+
|
|
877
|
+
const languageResults = {
|
|
878
|
+
files: [],
|
|
879
|
+
totalStats: { total: 0, translated: 0, missing: 0 }
|
|
880
|
+
};
|
|
881
|
+
|
|
882
|
+
for (const sourceFile of sourceFiles) {
|
|
883
|
+
const sourceFilePath = path.join(this.sourceLanguageDir, sourceFile);
|
|
884
|
+
const validatedSourceFilePath = SecurityUtils.validatePath(sourceFilePath, process.cwd());
|
|
885
|
+
|
|
886
|
+
if (!validatedSourceFilePath) {
|
|
887
|
+
SecurityUtils.logSecurityEvent('Invalid source file path', 'error', { path: sourceFilePath });
|
|
888
|
+
continue;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
const sourceContentRaw = await SecurityUtils.safeReadFile(validatedSourceFilePath, process.cwd());
|
|
892
|
+
if (!sourceContentRaw) {
|
|
893
|
+
SecurityUtils.logSecurityEvent('Failed to read source file', 'error', { file: validatedSourceFilePath });
|
|
894
|
+
continue;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
const sourceContent = this.format.read(sourceContentRaw);
|
|
898
|
+
|
|
899
|
+
const targetFilePath = await this.createLanguageFile(sourceFile, targetLanguage, sourceContent);
|
|
900
|
+
|
|
901
|
+
// Get stats for this file
|
|
902
|
+
const targetContentRaw = await SecurityUtils.safeReadFile(targetFilePath, process.cwd());
|
|
903
|
+
if (!targetContentRaw) {
|
|
904
|
+
SecurityUtils.logSecurityEvent('Failed to read target file for stats', 'error', { file: targetFilePath });
|
|
905
|
+
continue;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
const targetContent = this.format.read(targetContentRaw);
|
|
909
|
+
const stats = this.getTranslationStats(targetContent);
|
|
910
|
+
|
|
911
|
+
languageResults.files.push({
|
|
912
|
+
name: sourceFile,
|
|
913
|
+
path: targetFilePath,
|
|
914
|
+
stats
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
// Add to total stats
|
|
918
|
+
languageResults.totalStats.total += stats.total;
|
|
919
|
+
languageResults.totalStats.translated += stats.translated;
|
|
920
|
+
languageResults.totalStats.missing += stats.missing;
|
|
921
|
+
|
|
922
|
+
console.log(t('init.fileProcessingResult', { file: sourceFile, translated: stats.translated, total: stats.total, percentage: stats.percentage }));
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// Calculate overall percentage
|
|
926
|
+
languageResults.totalStats.percentage = languageResults.totalStats.total > 0
|
|
927
|
+
? Math.round((languageResults.totalStats.translated / languageResults.totalStats.total) * 100)
|
|
928
|
+
: 0;
|
|
929
|
+
|
|
930
|
+
results[targetLanguage] = languageResults;
|
|
931
|
+
|
|
932
|
+
console.log(t('init.overallProgress', { translated: languageResults.totalStats.translated, total: languageResults.totalStats.total, percentage: languageResults.totalStats.percentage }));
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// Generate and display completion summary
|
|
936
|
+
await this.generateCompletionSummary(results, targetLanguages);
|
|
937
|
+
|
|
938
|
+
console.log('\n' + t('init.initializationCompletedSuccessfully'));
|
|
939
|
+
console.log('\n' + t('init.nextStepsTitle'));
|
|
940
|
+
console.log(t('init.nextStep1'));
|
|
941
|
+
console.log(t('init.nextStep2'));
|
|
942
|
+
console.log(t('init.nextStep3'));
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// Generate completion summary with proper error handling
|
|
946
|
+
async generateCompletionSummary(results, targetLanguages) {
|
|
947
|
+
try {
|
|
948
|
+
console.log('\n' + '='.repeat(50));
|
|
949
|
+
console.log(t('init.initializationSummaryTitle'));
|
|
950
|
+
console.log(t('common.separator'));
|
|
951
|
+
|
|
952
|
+
let totalChanges = 0;
|
|
953
|
+
let languagesProcessed = 0;
|
|
954
|
+
let missingKeysAdded = 0;
|
|
955
|
+
|
|
956
|
+
Object.entries(results || {}).forEach(([lang, data]) => {
|
|
957
|
+
if (!data || typeof data !== 'object') return;
|
|
958
|
+
|
|
959
|
+
const langName = LANGUAGE_CONFIG[lang]?.name || 'Unknown';
|
|
960
|
+
const stats = data.totalStats || { total: 0, translated: 0, percentage: 0, missing: 0 };
|
|
961
|
+
|
|
962
|
+
const statusIcon = stats.percentage === 100 ? 'ā
' : stats.percentage >= 80 ? 'š”' : 'š“';
|
|
963
|
+
|
|
964
|
+
console.log(
|
|
965
|
+
t('init.languageSummary', {
|
|
966
|
+
icon: statusIcon,
|
|
967
|
+
name: langName,
|
|
968
|
+
code: lang,
|
|
969
|
+
percentage: stats.percentage || 0,
|
|
970
|
+
})
|
|
971
|
+
);
|
|
972
|
+
|
|
973
|
+
if (data.files && Array.isArray(data.files)) {
|
|
974
|
+
console.log(t('init.languageFiles', { count: data.files.length }));
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
console.log(
|
|
978
|
+
t('init.languageKeys', {
|
|
979
|
+
translated: stats.translated || 0,
|
|
980
|
+
total: stats.total || 0,
|
|
981
|
+
})
|
|
982
|
+
);
|
|
983
|
+
|
|
984
|
+
console.log(t('init.languageMissing', { count: stats.missing || 0 }));
|
|
985
|
+
|
|
986
|
+
totalChanges += (stats.translated || 0) + (stats.missing || 0);
|
|
987
|
+
languagesProcessed += 1;
|
|
988
|
+
missingKeysAdded += stats.missing || 0;
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
console.log('\nš COMPLETION SUMMARY');
|
|
992
|
+
console.log(t('common.separator'));
|
|
993
|
+
console.log(`š Total changes: ${totalChanges}`);
|
|
994
|
+
console.log(`š Languages processed: ${languagesProcessed}`);
|
|
995
|
+
console.log(`ā Missing keys added: ${missingKeysAdded}`);
|
|
996
|
+
|
|
997
|
+
if (process.stdin.isTTY && !this.config?.noPrompt) {
|
|
998
|
+
const { ask } = require('../../../utils/cli');
|
|
999
|
+
const generateReport = await ask('\nš¤ Would you like a report generated? (Y/N): ');
|
|
1000
|
+
if (generateReport.toLowerCase() === 'y' || generateReport.toLowerCase() === 'yes') {
|
|
1001
|
+
await this.generateDetailedReport(results, targetLanguages);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
} catch (error) {
|
|
1005
|
+
console.error('\nā Error during completion:', error.message);
|
|
1006
|
+
console.log('š COMPLETION SUMMARY (Basic)');
|
|
1007
|
+
console.log(t('common.separator'));
|
|
1008
|
+
console.log(`š Languages processed: ${Object.keys(results || {}).length}`);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// Generate detailed report
|
|
1013
|
+
async generateDetailedReport(results, targetLanguages) {
|
|
1014
|
+
try {
|
|
1015
|
+
const outputDir = this.config.outputDir || path.join(process.cwd(), 'i18ntk-reports');
|
|
1016
|
+
if (!SecurityUtils.safeExistsSync(outputDir)) {
|
|
1017
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
const reportPath = path.join(outputDir, 'init-report.json');
|
|
1021
|
+
const report = {
|
|
1022
|
+
timestamp: new Date().toISOString(),
|
|
1023
|
+
languages: targetLanguages,
|
|
1024
|
+
results: results,
|
|
1025
|
+
summary: {
|
|
1026
|
+
languagesProcessed: targetLanguages.length,
|
|
1027
|
+
totalFiles: Object.values(results).reduce((sum, data) => sum + (data.files?.length || 0), 0),
|
|
1028
|
+
totalKeys: Object.values(results).reduce((sum, data) => sum + (data.totalStats?.total || 0), 0),
|
|
1029
|
+
totalMissing: Object.values(results).reduce((sum, data) => sum + (data.totalStats?.missing || 0), 0)
|
|
1030
|
+
}
|
|
1031
|
+
};
|
|
1032
|
+
|
|
1033
|
+
await fs.promises.writeFile(reportPath, JSON.stringify(report, null, 2));
|
|
1034
|
+
console.log(`ā
Report generated: ${reportPath}`);
|
|
1035
|
+
} catch (error) {
|
|
1036
|
+
console.error('ā Failed to generate report:', error.message);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// Set prompt function for interactive operations
|
|
1041
|
+
setPromptFunction(promptFn) {
|
|
1042
|
+
this.prompt = promptFn;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// Set announcedExistingDir flag
|
|
1046
|
+
setAnnouncedExistingDir(value) {
|
|
1047
|
+
this.announcedExistingDir = value;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
module.exports = InitService;
|