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,454 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Language Mismatch Detection Tool
|
|
5
|
+
*
|
|
6
|
+
* This script detects and reports language mismatches in translation files:
|
|
7
|
+
* - English text in foreign language files
|
|
8
|
+
* - Untranslated markers like [TRANSLATE], [DE], [FR], etc.
|
|
9
|
+
* - Missing translations that should be in native language
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const { execSync } = require('child_process');
|
|
15
|
+
|
|
16
|
+
class LanguageMismatchDetector {
|
|
17
|
+
constructor() {
|
|
18
|
+
this.localesDir = path.join(__dirname, 'ui-locales');
|
|
19
|
+
this.sourceLanguage = 'en';
|
|
20
|
+
this.results = {
|
|
21
|
+
mismatches: {},
|
|
22
|
+
summary: {
|
|
23
|
+
totalFiles: 0,
|
|
24
|
+
filesWithMismatches: 0,
|
|
25
|
+
totalMismatches: 0
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Common English words that shouldn't appear in foreign translations
|
|
30
|
+
this.englishIndicators = [
|
|
31
|
+
'the', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by',
|
|
32
|
+
'from', 'up', 'about', 'into', 'through', 'during', 'before', 'after',
|
|
33
|
+
'above', 'below', 'between', 'among', 'under', 'over', 'inside', 'outside',
|
|
34
|
+
'this', 'that', 'these', 'those', 'here', 'there', 'where', 'when', 'why',
|
|
35
|
+
'how', 'what', 'which', 'who', 'whom', 'whose', 'all', 'any', 'some',
|
|
36
|
+
'many', 'much', 'few', 'little', 'more', 'most', 'less', 'least',
|
|
37
|
+
'good', 'better', 'best', 'bad', 'worse', 'worst', 'big', 'bigger',
|
|
38
|
+
'biggest', 'small', 'smaller', 'smallest', 'new', 'newer', 'newest',
|
|
39
|
+
'old', 'older', 'oldest', 'first', 'second', 'third', 'last', 'next',
|
|
40
|
+
'previous', 'same', 'different', 'other', 'another', 'each', 'every',
|
|
41
|
+
'both', 'either', 'neither', 'one', 'two', 'three', 'four', 'five',
|
|
42
|
+
'Error', 'Warning', 'Success', 'Failed', 'Loading', 'Saving', 'Delete',
|
|
43
|
+
'Create', 'Update', 'Add', 'Remove', 'Edit', 'View', 'Show', 'Hide',
|
|
44
|
+
'Open', 'Close', 'Start', 'Stop', 'Run', 'Execute', 'Process', 'Generate',
|
|
45
|
+
'Analysis', 'Report', 'Summary', 'Details', 'Configuration', 'Settings',
|
|
46
|
+
'Options', 'Preferences', 'Tools', 'Debug', 'Test', 'Validate',
|
|
47
|
+
'Check', 'Verify', 'Confirm', 'Cancel', 'Continue', 'Finish', 'Complete',
|
|
48
|
+
'Please', 'try', 'again', 'Invalid', 'choice', 'Select', 'option',
|
|
49
|
+
'Back', 'Main', 'Menu', 'File', 'files', 'found', 'not', 'available'
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
// Language-specific patterns
|
|
53
|
+
this.languagePatterns = {
|
|
54
|
+
de: {
|
|
55
|
+
name: 'German',
|
|
56
|
+
prefixes: ['[DE]', '[GERMAN]'],
|
|
57
|
+
commonWords: ['der', 'die', 'das', 'und', 'oder', 'aber', 'mit', 'von', 'zu', 'für', 'auf', 'in', 'an', 'bei', 'nach', 'vor', 'über', 'unter', 'durch', 'gegen', 'ohne', 'um', 'bis', 'seit', 'während', 'wegen', 'trotz']
|
|
58
|
+
},
|
|
59
|
+
fr: {
|
|
60
|
+
name: 'French',
|
|
61
|
+
prefixes: ['[FR]', '[FRENCH]'],
|
|
62
|
+
commonWords: ['le', 'la', 'les', 'un', 'une', 'des', 'et', 'ou', 'mais', 'avec', 'de', 'du', 'pour', 'sur', 'dans', 'par', 'sans', 'sous', 'vers', 'chez', 'depuis', 'pendant', 'avant', 'après']
|
|
63
|
+
},
|
|
64
|
+
es: {
|
|
65
|
+
name: 'Spanish',
|
|
66
|
+
prefixes: ['[ES]', '[SPANISH]'],
|
|
67
|
+
commonWords: ['el', 'la', 'los', 'las', 'un', 'una', 'y', 'o', 'pero', 'con', 'de', 'del', 'para', 'por', 'en', 'a', 'desde', 'hasta', 'durante', 'antes', 'después', 'sobre', 'bajo', 'entre']
|
|
68
|
+
},
|
|
69
|
+
ru: {
|
|
70
|
+
name: 'Russian',
|
|
71
|
+
prefixes: ['[RU]', '[RUSSIAN]'],
|
|
72
|
+
commonWords: ['и', 'в', 'не', 'на', 'я', 'быть', 'он', 'с', 'что', 'а', 'по', 'это', 'она', 'к', 'но', 'они', 'мы', 'как', 'из', 'у', 'который', 'то', 'за', 'свой', 'что', 'её', 'так', 'же', 'от', 'может', 'когда', 'очень', 'где', 'уже', 'если', 'да', 'его', 'нет', 'ещё']
|
|
73
|
+
},
|
|
74
|
+
ja: {
|
|
75
|
+
name: 'Japanese',
|
|
76
|
+
prefixes: ['[JA]', '[JAPANESE]'],
|
|
77
|
+
commonWords: ['の', 'に', 'は', 'を', 'た', 'が', 'で', 'て', 'と', 'し', 'れ', 'さ', 'ある', 'いる', 'も', 'する', 'から', 'な', 'こと', 'として', 'い', 'や', 'れる', 'など', 'なっ', 'ない', 'この', 'ため', 'その', 'あっ', 'よう', 'また', 'もの', 'という', 'あり', 'まで', 'られ', 'なる', 'へ', 'か', 'だ', 'これ', 'によって', 'により', 'おり', 'より', 'による', 'ず', 'なり', 'られる', 'において', 'ば', 'なかっ', 'なく', 'しかし', 'について', 'せ', 'だっ', 'その後', 'できる', 'それ']
|
|
78
|
+
},
|
|
79
|
+
zh: {
|
|
80
|
+
name: 'Chinese',
|
|
81
|
+
prefixes: ['[ZH]', '[CHINESE]'],
|
|
82
|
+
commonWords: ['的', '一', '是', '在', '不', '了', '有', '和', '人', '这', '中', '大', '为', '上', '个', '国', '我', '以', '要', '他', '时', '来', '用', '们', '生', '到', '作', '地', '于', '出', '就', '分', '对', '成', '会', '可', '主', '发', '年', '动', '同', '工', '也', '能', '下', '过', '子', '说', '产', '种', '面', '而', '方', '后', '多', '定', '行', '学', '法', '所', '民', '得', '经', '十', '三', '之', '进', '着', '等', '部', '度', '家', '电', '力', '里', '如', '水', '化', '高', '自', '二', '理', '起', '小', '物', '现', '实', '加', '量', '都', '两', '体', '制', '机', '当', '使', '点', '从', '业', '本', '去', '把', '性', '好', '应', '开', '它', '合', '还', '因', '由', '其', '些', '然', '前', '外', '天', '政', '四', '日', '那', '社', '义', '事', '平', '形', '相', '全', '表', '间', '样', '与', '关', '各', '重', '新', '线', '内', '数', '正', '心', '反', '你', '明', '看', '原', '又', '么', '利', '比', '或', '但', '质', '气', '第', '向', '道', '命', '此', '变', '条', '只', '没', '结', '解', '问', '意', '建', '月', '公', '无', 'ä¸\u200d', 'ä¸\u200d', 'ä¸\u200d']
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Main detection method
|
|
89
|
+
*/
|
|
90
|
+
async detectMismatches() {
|
|
91
|
+
console.log('🔍 Language Mismatch Detection Tool');
|
|
92
|
+
console.log('=====================================\n');
|
|
93
|
+
|
|
94
|
+
const localeFiles = this.getLocaleFiles();
|
|
95
|
+
|
|
96
|
+
for (const file of localeFiles) {
|
|
97
|
+
if (file.language === this.sourceLanguage) {
|
|
98
|
+
continue; // Skip source language
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
console.log(`📄 Analyzing ${file.language}.json...`);
|
|
102
|
+
await this.analyzeFile(file);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
this.generateReport();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get all locale files
|
|
110
|
+
*/
|
|
111
|
+
getLocaleFiles() {
|
|
112
|
+
const files = fs.readdirSync(this.localesDir)
|
|
113
|
+
.filter(file => file.endsWith('.json'))
|
|
114
|
+
.map(file => ({
|
|
115
|
+
filename: file,
|
|
116
|
+
language: path.basename(file, '.json'),
|
|
117
|
+
path: path.join(this.localesDir, file)
|
|
118
|
+
}));
|
|
119
|
+
|
|
120
|
+
this.results.summary.totalFiles = files.length - 1; // Exclude source language
|
|
121
|
+
return files;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Analyze a single translation file
|
|
126
|
+
*/
|
|
127
|
+
async analyzeFile(file) {
|
|
128
|
+
try {
|
|
129
|
+
const content = fs.readFileSync(file.path, 'utf8');
|
|
130
|
+
const translations = JSON.parse(content);
|
|
131
|
+
|
|
132
|
+
const mismatches = [];
|
|
133
|
+
this.analyzeObject(translations, '', mismatches, file.language);
|
|
134
|
+
|
|
135
|
+
if (mismatches.length > 0) {
|
|
136
|
+
this.results.mismatches[file.language] = {
|
|
137
|
+
filename: file.filename,
|
|
138
|
+
language: this.languagePatterns[file.language]?.name || file.language,
|
|
139
|
+
mismatches: mismatches,
|
|
140
|
+
count: mismatches.length
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
this.results.summary.filesWithMismatches++;
|
|
144
|
+
this.results.summary.totalMismatches += mismatches.length;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
} catch (error) {
|
|
148
|
+
console.error(`❌ Error analyzing ${file.filename}: ${error.message}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Recursively analyze translation object
|
|
154
|
+
*/
|
|
155
|
+
analyzeObject(obj, keyPath, mismatches, language) {
|
|
156
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
157
|
+
const currentPath = keyPath ? `${keyPath}.${key}` : key;
|
|
158
|
+
|
|
159
|
+
if (typeof value === 'string') {
|
|
160
|
+
const issues = this.detectStringIssues(value, language);
|
|
161
|
+
if (issues.length > 0) {
|
|
162
|
+
mismatches.push({
|
|
163
|
+
key: currentPath,
|
|
164
|
+
value: value,
|
|
165
|
+
issues: issues
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
169
|
+
this.analyzeObject(value, currentPath, mismatches, language);
|
|
170
|
+
} else if (Array.isArray(value)) {
|
|
171
|
+
value.forEach((item, index) => {
|
|
172
|
+
if (typeof item === 'string') {
|
|
173
|
+
const issues = this.detectStringIssues(item, language);
|
|
174
|
+
if (issues.length > 0) {
|
|
175
|
+
mismatches.push({
|
|
176
|
+
key: `${currentPath}[${index}]`,
|
|
177
|
+
value: item,
|
|
178
|
+
issues: issues
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Detect issues in a translation string
|
|
189
|
+
*/
|
|
190
|
+
detectStringIssues(text, language) {
|
|
191
|
+
const issues = [];
|
|
192
|
+
const lowerText = text.toLowerCase();
|
|
193
|
+
|
|
194
|
+
// Check for translation markers
|
|
195
|
+
if (text.includes('[TRANSLATE]')) {
|
|
196
|
+
issues.push({
|
|
197
|
+
type: 'untranslated_marker',
|
|
198
|
+
description: 'Contains [TRANSLATE] marker - needs translation'
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Check for language prefixes
|
|
203
|
+
const langPattern = this.languagePatterns[language];
|
|
204
|
+
if (langPattern) {
|
|
205
|
+
for (const prefix of langPattern.prefixes) {
|
|
206
|
+
if (text.includes(prefix)) {
|
|
207
|
+
issues.push({
|
|
208
|
+
type: 'language_prefix',
|
|
209
|
+
description: `Contains ${prefix} prefix - should be translated to ${langPattern.name}`
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Check for English indicators
|
|
216
|
+
const words = lowerText.match(/\b\w+\b/g) || [];
|
|
217
|
+
const englishWords = words.filter(word =>
|
|
218
|
+
this.englishIndicators.includes(word) &&
|
|
219
|
+
word.length > 2 // Ignore very short words that might be common across languages
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
if (englishWords.length > 0) {
|
|
223
|
+
// Calculate English word ratio
|
|
224
|
+
const englishRatio = englishWords.length / words.length;
|
|
225
|
+
|
|
226
|
+
if (englishRatio > 0.3 || englishWords.length > 3) {
|
|
227
|
+
issues.push({
|
|
228
|
+
type: 'english_content',
|
|
229
|
+
description: `Contains English words: ${englishWords.slice(0, 5).join(', ')}${englishWords.length > 5 ? '...' : ''}`,
|
|
230
|
+
englishWords: englishWords,
|
|
231
|
+
ratio: englishRatio
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Check for common English phrases
|
|
237
|
+
const englishPhrases = [
|
|
238
|
+
'error', 'warning', 'success', 'failed', 'loading', 'saving',
|
|
239
|
+
'please try again', 'invalid choice', 'select option', 'back to main menu',
|
|
240
|
+
'configuration debug', 'translation debug', 'performance debug',
|
|
241
|
+
'full system debug', 'debug tools', 'admin pin setup',
|
|
242
|
+
'would you like', 'enter admin pin', 'confirm admin pin',
|
|
243
|
+
'pins do not match', 'authentication failed', 'access denied'
|
|
244
|
+
];
|
|
245
|
+
|
|
246
|
+
for (const phrase of englishPhrases) {
|
|
247
|
+
if (lowerText.includes(phrase)) {
|
|
248
|
+
issues.push({
|
|
249
|
+
type: 'english_phrase',
|
|
250
|
+
description: `Contains English phrase: "${phrase}"`
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return issues;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Generate and display the report
|
|
260
|
+
*/
|
|
261
|
+
generateReport() {
|
|
262
|
+
console.log('\n📊 LANGUAGE MISMATCH DETECTION RESULTS');
|
|
263
|
+
console.log('========================================\n');
|
|
264
|
+
|
|
265
|
+
// Summary
|
|
266
|
+
console.log('📋 SUMMARY:');
|
|
267
|
+
console.log(` Total files analyzed: ${this.results.summary.totalFiles}`);
|
|
268
|
+
console.log(` Files with mismatches: ${this.results.summary.filesWithMismatches}`);
|
|
269
|
+
console.log(` Total mismatches found: ${this.results.summary.totalMismatches}\n`);
|
|
270
|
+
|
|
271
|
+
if (this.results.summary.totalMismatches === 0) {
|
|
272
|
+
console.log('✅ No language mismatches found! All translations appear to be in their correct languages.\n');
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Detailed results
|
|
277
|
+
console.log('🔍 DETAILED RESULTS:\n');
|
|
278
|
+
|
|
279
|
+
for (const [language, data] of Object.entries(this.results.mismatches)) {
|
|
280
|
+
console.log(`📄 ${data.filename} (${data.language})`);
|
|
281
|
+
console.log(` Found ${data.count} mismatch(es):\n`);
|
|
282
|
+
|
|
283
|
+
data.mismatches.forEach((mismatch, index) => {
|
|
284
|
+
console.log(` ${index + 1}. Key: ${mismatch.key}`);
|
|
285
|
+
console.log(` Value: "${mismatch.value.substring(0, 100)}${mismatch.value.length > 100 ? '...' : ''}"}`);
|
|
286
|
+
|
|
287
|
+
mismatch.issues.forEach(issue => {
|
|
288
|
+
console.log(` Issue: ${issue.description}`);
|
|
289
|
+
});
|
|
290
|
+
console.log('');
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Recommendations
|
|
295
|
+
console.log('💡 RECOMMENDATIONS:\n');
|
|
296
|
+
console.log('1. Review and translate all entries marked with [TRANSLATE]');
|
|
297
|
+
console.log('2. Replace language prefixes (e.g., [DE], [FR]) with proper translations');
|
|
298
|
+
console.log('3. Translate English text to the appropriate target language');
|
|
299
|
+
console.log('4. Use native language equivalents for all user-facing text');
|
|
300
|
+
console.log('5. Run this tool again after making corrections to verify fixes\n');
|
|
301
|
+
|
|
302
|
+
// Save report
|
|
303
|
+
this.saveReport();
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Save report to file
|
|
308
|
+
*/
|
|
309
|
+
saveReport() {
|
|
310
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
311
|
+
const reportPath = path.join(__dirname, 'i18ntk-reports', 'language-mismatches', `language-mismatches-${timestamp}.json`);
|
|
312
|
+
|
|
313
|
+
// Ensure directory exists
|
|
314
|
+
const reportDir = path.dirname(reportPath);
|
|
315
|
+
if (!fs.existsSync(reportDir)) {
|
|
316
|
+
fs.mkdirSync(reportDir, { recursive: true });
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const report = {
|
|
320
|
+
timestamp: new Date().toISOString(),
|
|
321
|
+
summary: this.results.summary,
|
|
322
|
+
mismatches: this.results.mismatches,
|
|
323
|
+
recommendations: [
|
|
324
|
+
'Review and translate all entries marked with [TRANSLATE]',
|
|
325
|
+
'Replace language prefixes (e.g., [DE], [FR]) with proper translations',
|
|
326
|
+
'Translate English text to the appropriate target language',
|
|
327
|
+
'Use native language equivalents for all user-facing text',
|
|
328
|
+
'Run this tool again after making corrections to verify fixes'
|
|
329
|
+
]
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
try {
|
|
333
|
+
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
|
|
334
|
+
console.log(`📄 Detailed report saved to: ${reportPath}\n`);
|
|
335
|
+
} catch (error) {
|
|
336
|
+
console.error(`❌ Error saving report: ${error.message}\n`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Auto-fix some common issues
|
|
342
|
+
*/
|
|
343
|
+
async autoFix(dryRun = true) {
|
|
344
|
+
console.log(`🔧 Auto-fix mode ${dryRun ? '(DRY RUN)' : '(LIVE)'}`);
|
|
345
|
+
console.log('=====================================\n');
|
|
346
|
+
|
|
347
|
+
const fixes = [];
|
|
348
|
+
|
|
349
|
+
for (const [language, data] of Object.entries(this.results.mismatches)) {
|
|
350
|
+
const filePath = path.join(this.localesDir, data.filename);
|
|
351
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
352
|
+
let translations = JSON.parse(content);
|
|
353
|
+
let modified = false;
|
|
354
|
+
|
|
355
|
+
for (const mismatch of data.mismatches) {
|
|
356
|
+
for (const issue of mismatch.issues) {
|
|
357
|
+
if (issue.type === 'language_prefix') {
|
|
358
|
+
// Remove language prefixes
|
|
359
|
+
const newValue = mismatch.value.replace(/\[\w+\]\s*/g, '');
|
|
360
|
+
if (newValue !== mismatch.value) {
|
|
361
|
+
this.setNestedValue(translations, mismatch.key, newValue);
|
|
362
|
+
modified = true;
|
|
363
|
+
fixes.push({
|
|
364
|
+
file: data.filename,
|
|
365
|
+
key: mismatch.key,
|
|
366
|
+
old: mismatch.value,
|
|
367
|
+
new: newValue,
|
|
368
|
+
type: 'prefix_removal'
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (modified && !dryRun) {
|
|
376
|
+
fs.writeFileSync(filePath, JSON.stringify(translations, null, 2));
|
|
377
|
+
console.log(`✅ Fixed ${data.filename}`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
console.log(`\n📊 Auto-fix Summary:`);
|
|
382
|
+
console.log(` Total fixes applied: ${fixes.length}`);
|
|
383
|
+
|
|
384
|
+
if (dryRun && fixes.length > 0) {
|
|
385
|
+
console.log('\n🔍 Preview of fixes:');
|
|
386
|
+
fixes.slice(0, 10).forEach(fix => {
|
|
387
|
+
console.log(` ${fix.file}: ${fix.key}`);
|
|
388
|
+
console.log(` Old: "${fix.old}"`);
|
|
389
|
+
console.log(` New: "${fix.new}"\n`);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
if (fixes.length > 10) {
|
|
393
|
+
console.log(` ... and ${fixes.length - 10} more fixes\n`);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
console.log('💡 Run with --apply to apply these fixes\n');
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Set nested object value by dot notation key
|
|
402
|
+
*/
|
|
403
|
+
setNestedValue(obj, key, value) {
|
|
404
|
+
const keys = key.split('.');
|
|
405
|
+
let current = obj;
|
|
406
|
+
|
|
407
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
408
|
+
if (!(keys[i] in current)) {
|
|
409
|
+
current[keys[i]] = {};
|
|
410
|
+
}
|
|
411
|
+
current = current[keys[i]];
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
current[keys[keys.length - 1]] = value;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// CLI Interface
|
|
419
|
+
if (require.main === module) {
|
|
420
|
+
const args = process.argv.slice(2);
|
|
421
|
+
const detector = new LanguageMismatchDetector();
|
|
422
|
+
|
|
423
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
424
|
+
console.log(`
|
|
425
|
+
Language Mismatch Detection Tool
|
|
426
|
+
|
|
427
|
+
Usage:
|
|
428
|
+
node detect-language-mismatches.js [options]
|
|
429
|
+
|
|
430
|
+
Options:
|
|
431
|
+
--auto-fix Show auto-fixable issues (dry run)
|
|
432
|
+
--apply Apply auto-fixes (live mode)
|
|
433
|
+
--help, -h Show this help message
|
|
434
|
+
|
|
435
|
+
Examples:
|
|
436
|
+
node detect-language-mismatches.js # Detect mismatches
|
|
437
|
+
node detect-language-mismatches.js --auto-fix # Preview auto-fixes
|
|
438
|
+
node detect-language-mismatches.js --apply # Apply auto-fixes
|
|
439
|
+
`);
|
|
440
|
+
process.exit(0);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
detector.detectMismatches().then(() => {
|
|
444
|
+
if (args.includes('--auto-fix') || args.includes('--apply')) {
|
|
445
|
+
const dryRun = !args.includes('--apply');
|
|
446
|
+
return detector.autoFix(dryRun);
|
|
447
|
+
}
|
|
448
|
+
}).catch(error => {
|
|
449
|
+
console.error('❌ Error:', error.message);
|
|
450
|
+
process.exit(1);
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
module.exports = LanguageMismatchDetector;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const settingsManager = require('../settings/settings-manager');
|
|
4
|
+
|
|
5
|
+
// Get configuration from settings manager
|
|
6
|
+
function getConfig() {
|
|
7
|
+
const settings = settingsManager.getSettings();
|
|
8
|
+
return {
|
|
9
|
+
uiLocalesDir: settings.directories?.uiLocalesDir || path.join(__dirname, '..', 'ui-locales')
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Global translations object
|
|
14
|
+
let translations = {};
|
|
15
|
+
let currentLanguage = 'en';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Load translations from the ui-locales directory
|
|
19
|
+
* @param {string} language - Language code (default: 'en')
|
|
20
|
+
*/
|
|
21
|
+
function loadTranslations(language = 'en') {
|
|
22
|
+
currentLanguage = language;
|
|
23
|
+
const config = getConfig();
|
|
24
|
+
const localesDir = path.resolve(config.uiLocalesDir);
|
|
25
|
+
const translationFile = path.join(localesDir, `${language}.json`);
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
if (fs.existsSync(translationFile)) {
|
|
29
|
+
const content = fs.readFileSync(translationFile, 'utf8');
|
|
30
|
+
translations = JSON.parse(content);
|
|
31
|
+
} else {
|
|
32
|
+
console.warn(`Translation file not found: ${translationFile}`);
|
|
33
|
+
translations = {};
|
|
34
|
+
}
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error(`Error loading translations: ${error.message}`);
|
|
37
|
+
translations = {};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get a translated string by key
|
|
43
|
+
* @param {string} key - Translation key (e.g., 'module.subkey')
|
|
44
|
+
* @param {object} params - Parameters to interpolate into the translation
|
|
45
|
+
* @returns {string} - Translated string or the key if translation not found
|
|
46
|
+
*/
|
|
47
|
+
function t(key, params = {}) {
|
|
48
|
+
// Split the key into parts (e.g., 'module.subkey' -> ['module', 'subkey'])
|
|
49
|
+
const keyParts = key.split('.');
|
|
50
|
+
let value = translations;
|
|
51
|
+
|
|
52
|
+
// Navigate through the nested object
|
|
53
|
+
for (const part of keyParts) {
|
|
54
|
+
if (value && typeof value === 'object' && part in value) {
|
|
55
|
+
value = value[part];
|
|
56
|
+
} else {
|
|
57
|
+
// Return the key if translation not found
|
|
58
|
+
console.warn(`Translation not found for key: ${key}`);
|
|
59
|
+
return key;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// If we found a string, interpolate parameters
|
|
64
|
+
if (typeof value === 'string') {
|
|
65
|
+
return interpolateParams(value, params);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Return the key if the final value is not a string
|
|
69
|
+
console.warn(`Translation key does not resolve to a string: ${key}`);
|
|
70
|
+
return key;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Interpolate parameters into a translation string
|
|
75
|
+
* @param {string} template - Template string with {{param}} or {param} placeholders
|
|
76
|
+
* @param {object} params - Parameters to interpolate
|
|
77
|
+
* @returns {string} - Interpolated string
|
|
78
|
+
*/
|
|
79
|
+
function interpolateParams(template, params) {
|
|
80
|
+
// Handle both {{param}} and {param} formats
|
|
81
|
+
return template
|
|
82
|
+
.replace(/\{\{(\w+)\}\}/g, (match, paramName) => {
|
|
83
|
+
if (paramName in params) {
|
|
84
|
+
return params[paramName];
|
|
85
|
+
}
|
|
86
|
+
return match;
|
|
87
|
+
})
|
|
88
|
+
.replace(/\{(\w+)\}/g, (match, paramName) => {
|
|
89
|
+
if (paramName in params) {
|
|
90
|
+
return params[paramName];
|
|
91
|
+
}
|
|
92
|
+
return match;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get the current language
|
|
98
|
+
* @returns {string} - Current language code
|
|
99
|
+
*/
|
|
100
|
+
function getCurrentLanguage() {
|
|
101
|
+
return currentLanguage;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get all available languages
|
|
106
|
+
* @returns {string[]} - Array of available language codes
|
|
107
|
+
*/
|
|
108
|
+
function getAvailableLanguages() {
|
|
109
|
+
const config = getConfig();
|
|
110
|
+
const localesDir = path.resolve(config.uiLocalesDir);
|
|
111
|
+
try {
|
|
112
|
+
if (fs.existsSync(localesDir)) {
|
|
113
|
+
return fs.readdirSync(localesDir)
|
|
114
|
+
.filter(file => file.endsWith('.json'))
|
|
115
|
+
.map(file => path.basename(file, '.json'));
|
|
116
|
+
}
|
|
117
|
+
} catch (error) {
|
|
118
|
+
console.error(`Error reading locales directory: ${error.message}`);
|
|
119
|
+
}
|
|
120
|
+
return ['en']; // Default fallback
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = {
|
|
124
|
+
loadTranslations,
|
|
125
|
+
t,
|
|
126
|
+
getCurrentLanguage,
|
|
127
|
+
getAvailableLanguages
|
|
128
|
+
};
|