@umituz/react-native-localization 2.0.0 ā 2.1.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/package.json +2 -12
- package/scripts/prepublish.js +7 -52
- package/src/infrastructure/storage/LocalizationStore.ts +5 -0
- package/scripts/analyze-keys.js +0 -230
- package/scripts/check-translations.js +0 -354
- package/scripts/createLocaleLoaders.js +0 -187
- package/scripts/remove-unused-keys.js +0 -216
- package/scripts/setup-languages.js +0 -290
- package/scripts/translate-missing.js +0 -619
- package/scripts/utils/findLocalesDir.js +0 -79
|
@@ -1,619 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/* eslint-disable no-console */
|
|
3
|
-
|
|
4
|
-
const fs = require('fs');
|
|
5
|
-
const path = require('path');
|
|
6
|
-
const https = require('https');
|
|
7
|
-
const { getLocalesDir } = require('./utils/findLocalesDir');
|
|
8
|
-
|
|
9
|
-
// Parse command line arguments
|
|
10
|
-
const args = process.argv.slice(2);
|
|
11
|
-
const options = {
|
|
12
|
-
verbose: args.includes('--verbose') || args.includes('-v'),
|
|
13
|
-
force: args.includes('--force') || args.includes('-f'),
|
|
14
|
-
keysFilter: args.find(a => a.startsWith('--keys='))?.split('=')[1]?.split(','),
|
|
15
|
-
report: args.includes('--report') || args.includes('-r'),
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
// Statistics tracking
|
|
19
|
-
const stats = {
|
|
20
|
-
totalLanguages: 0,
|
|
21
|
-
totalFiles: 0,
|
|
22
|
-
totalKeys: 0,
|
|
23
|
-
translated: 0,
|
|
24
|
-
skipped: 0,
|
|
25
|
-
errors: 0,
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
// Language codes mapping for Google Translate API
|
|
29
|
-
const LANGUAGE_MAP = {
|
|
30
|
-
'ar-SA': 'ar', // Arabic
|
|
31
|
-
'bg-BG': 'bg', // Bulgarian
|
|
32
|
-
'cs-CZ': 'cs', // Czech
|
|
33
|
-
'da-DK': 'da', // Danish
|
|
34
|
-
'de-DE': 'de', // German (already complete)
|
|
35
|
-
'el-GR': 'el', // Greek
|
|
36
|
-
'en-AU': 'en', // English (skip)
|
|
37
|
-
'en-CA': 'en', // English (skip)
|
|
38
|
-
'en-GB': 'en', // English (skip)
|
|
39
|
-
'es-ES': 'es', // Spanish (already complete)
|
|
40
|
-
'es-MX': 'es', // Spanish (already complete)
|
|
41
|
-
'fi-FI': 'fi', // Finnish
|
|
42
|
-
'fr-CA': 'fr', // French
|
|
43
|
-
'fr-FR': 'fr', // French (already complete)
|
|
44
|
-
'hi-IN': 'hi', // Hindi
|
|
45
|
-
'hr-HR': 'hr', // Croatian
|
|
46
|
-
'hu-HU': 'hu', // Hungarian
|
|
47
|
-
'id-ID': 'id', // Indonesian
|
|
48
|
-
'it-IT': 'it', // Italian
|
|
49
|
-
'ja-JP': 'ja', // Japanese
|
|
50
|
-
'ko-KR': 'ko', // Korean
|
|
51
|
-
'ms-MY': 'ms', // Malay
|
|
52
|
-
'nl-NL': 'nl', // Dutch
|
|
53
|
-
'no-NO': 'no', // Norwegian
|
|
54
|
-
'pl-PL': 'pl', // Polish
|
|
55
|
-
'pt-BR': 'pt', // Portuguese
|
|
56
|
-
'pt-PT': 'pt', // Portuguese
|
|
57
|
-
'ro-RO': 'ro', // Romanian
|
|
58
|
-
'ru-RU': 'ru', // Russian
|
|
59
|
-
'sk-SK': 'sk', // Slovak
|
|
60
|
-
'sv-SE': 'sv', // Swedish
|
|
61
|
-
'th-TH': 'th', // Thai
|
|
62
|
-
'tl-PH': 'tl', // Tagalog
|
|
63
|
-
'tr-TR': 'tr', // Turkish (already complete)
|
|
64
|
-
'uk-UA': 'uk', // Ukrainian
|
|
65
|
-
'vi-VN': 'vi', // Vietnamese
|
|
66
|
-
'zh-CN': 'zh-CN', // Chinese Simplified
|
|
67
|
-
'zh-TW': 'zh-TW', // Chinese Traditional
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
// Skip brand names and common words that are the same in most languages
|
|
71
|
-
// These words don't need translation and should be preserved as-is
|
|
72
|
-
const SKIP_WORDS = new Set([
|
|
73
|
-
// Brand names
|
|
74
|
-
'Google',
|
|
75
|
-
'Apple',
|
|
76
|
-
'Facebook',
|
|
77
|
-
'Instagram',
|
|
78
|
-
'Twitter',
|
|
79
|
-
'WhatsApp',
|
|
80
|
-
|
|
81
|
-
// Common UI words that are often the same
|
|
82
|
-
'OK',
|
|
83
|
-
'Yes',
|
|
84
|
-
'No',
|
|
85
|
-
'Cancel',
|
|
86
|
-
'Save',
|
|
87
|
-
'Delete',
|
|
88
|
-
'Edit',
|
|
89
|
-
'Back',
|
|
90
|
-
'Next',
|
|
91
|
-
'Previous',
|
|
92
|
-
'Close',
|
|
93
|
-
'Open',
|
|
94
|
-
'Menu',
|
|
95
|
-
'Settings',
|
|
96
|
-
'Help',
|
|
97
|
-
'Info',
|
|
98
|
-
'Error',
|
|
99
|
-
'Warning',
|
|
100
|
-
'Success',
|
|
101
|
-
'Loading',
|
|
102
|
-
'Search',
|
|
103
|
-
'Filter',
|
|
104
|
-
'Sort',
|
|
105
|
-
'View',
|
|
106
|
-
'Show',
|
|
107
|
-
'Hide',
|
|
108
|
-
|
|
109
|
-
// Technical terms
|
|
110
|
-
'API',
|
|
111
|
-
'URL',
|
|
112
|
-
'HTTP',
|
|
113
|
-
'HTTPS',
|
|
114
|
-
'JSON',
|
|
115
|
-
'XML',
|
|
116
|
-
'PDF',
|
|
117
|
-
'CSV',
|
|
118
|
-
'ID',
|
|
119
|
-
|
|
120
|
-
// Common abbreviations
|
|
121
|
-
'etc.',
|
|
122
|
-
'e.g.',
|
|
123
|
-
'i.e.',
|
|
124
|
-
'vs.',
|
|
125
|
-
'etc',
|
|
126
|
-
]);
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Simple Google Translate API call using free endpoint
|
|
130
|
-
* Note: This uses Google's unofficial API. For production, use official API with key.
|
|
131
|
-
* Includes retry mechanism for rate limiting and HTML error responses.
|
|
132
|
-
*/
|
|
133
|
-
async function translateText(text, targetLang, retryCount = 0) {
|
|
134
|
-
const MAX_RETRIES = 3;
|
|
135
|
-
const RETRY_DELAY = 1000; // 1 second
|
|
136
|
-
|
|
137
|
-
return new Promise((resolve, _reject) => {
|
|
138
|
-
if (SKIP_WORDS.has(text)) {
|
|
139
|
-
resolve(text);
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const encodedText = encodeURIComponent(text);
|
|
144
|
-
const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=en&tl=${targetLang}&dt=t&q=${encodedText}`;
|
|
145
|
-
|
|
146
|
-
https
|
|
147
|
-
.get(url, res => {
|
|
148
|
-
let data = '';
|
|
149
|
-
res.on('data', chunk => {
|
|
150
|
-
data += chunk;
|
|
151
|
-
});
|
|
152
|
-
res.on('end', () => {
|
|
153
|
-
// Check if response is HTML (error page)
|
|
154
|
-
if (data.trim().startsWith('<') || data.trim().startsWith('<!')) {
|
|
155
|
-
// HTML response - likely rate limit or error page
|
|
156
|
-
if (retryCount < MAX_RETRIES) {
|
|
157
|
-
if (options.verbose) {
|
|
158
|
-
console.warn(
|
|
159
|
-
` ā ļø HTML response received for "${text}" to ${targetLang}, retrying... (${retryCount + 1}/${MAX_RETRIES})`
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
// Retry after delay
|
|
163
|
-
setTimeout(() => {
|
|
164
|
-
translateText(text, targetLang, retryCount + 1).then(resolve);
|
|
165
|
-
}, RETRY_DELAY * (retryCount + 1)); // Exponential backoff
|
|
166
|
-
return;
|
|
167
|
-
} else {
|
|
168
|
-
console.warn(
|
|
169
|
-
`ā ļø Translation failed for "${text}" to ${targetLang}: HTML response (rate limit or API error)`
|
|
170
|
-
);
|
|
171
|
-
resolve(text); // Fallback to original
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Check HTTP status code
|
|
177
|
-
if (res.statusCode !== 200) {
|
|
178
|
-
if (retryCount < MAX_RETRIES) {
|
|
179
|
-
if (options.verbose) {
|
|
180
|
-
console.warn(
|
|
181
|
-
` ā ļø HTTP ${res.statusCode} for "${text}" to ${targetLang}, retrying... (${retryCount + 1}/${MAX_RETRIES})`
|
|
182
|
-
);
|
|
183
|
-
}
|
|
184
|
-
setTimeout(() => {
|
|
185
|
-
translateText(text, targetLang, retryCount + 1).then(resolve);
|
|
186
|
-
}, RETRY_DELAY * (retryCount + 1));
|
|
187
|
-
return;
|
|
188
|
-
} else {
|
|
189
|
-
console.warn(
|
|
190
|
-
`ā ļø Translation failed for "${text}" to ${targetLang}: HTTP ${res.statusCode}`
|
|
191
|
-
);
|
|
192
|
-
resolve(text);
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
try {
|
|
198
|
-
const parsed = JSON.parse(data);
|
|
199
|
-
if (!parsed || !parsed[0] || !Array.isArray(parsed[0])) {
|
|
200
|
-
throw new Error('Invalid response format');
|
|
201
|
-
}
|
|
202
|
-
const translated = parsed[0]
|
|
203
|
-
.map(item => item[0])
|
|
204
|
-
.join('')
|
|
205
|
-
.trim();
|
|
206
|
-
resolve(translated || text);
|
|
207
|
-
} catch (error) {
|
|
208
|
-
// JSON parse error - might be HTML or malformed response
|
|
209
|
-
if (retryCount < MAX_RETRIES) {
|
|
210
|
-
if (options.verbose) {
|
|
211
|
-
console.warn(
|
|
212
|
-
` ā ļø Parse error for "${text}" to ${targetLang}, retrying... (${retryCount + 1}/${MAX_RETRIES}): ${error.message}`
|
|
213
|
-
);
|
|
214
|
-
}
|
|
215
|
-
setTimeout(() => {
|
|
216
|
-
translateText(text, targetLang, retryCount + 1).then(resolve);
|
|
217
|
-
}, RETRY_DELAY * (retryCount + 1));
|
|
218
|
-
} else {
|
|
219
|
-
console.warn(
|
|
220
|
-
`ā ļø Translation failed for "${text}" to ${targetLang}: ${error.message}`
|
|
221
|
-
);
|
|
222
|
-
resolve(text); // Fallback to original
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
});
|
|
226
|
-
})
|
|
227
|
-
.on('error', err => {
|
|
228
|
-
if (retryCount < MAX_RETRIES) {
|
|
229
|
-
if (options.verbose) {
|
|
230
|
-
console.warn(
|
|
231
|
-
` ā ļø Network error for "${text}" to ${targetLang}, retrying... (${retryCount + 1}/${MAX_RETRIES}): ${err.message}`
|
|
232
|
-
);
|
|
233
|
-
}
|
|
234
|
-
setTimeout(() => {
|
|
235
|
-
translateText(text, targetLang, retryCount + 1).then(resolve);
|
|
236
|
-
}, RETRY_DELAY * (retryCount + 1));
|
|
237
|
-
} else {
|
|
238
|
-
console.warn(
|
|
239
|
-
`ā ļø Network error translating "${text}" to ${targetLang}: ${err.message}`
|
|
240
|
-
);
|
|
241
|
-
resolve(text); // Fallback to original
|
|
242
|
-
}
|
|
243
|
-
});
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Add delay between API calls to avoid rate limiting
|
|
249
|
-
*/
|
|
250
|
-
function delay(ms) {
|
|
251
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Check if a value needs translation (is missing or undefined)
|
|
256
|
-
*
|
|
257
|
-
* IMPROVED DETECTION:
|
|
258
|
-
* - Respects --force flag to retranslate everything
|
|
259
|
-
* - Respects --keys filter for selective retranslation
|
|
260
|
-
* - Shows skip reasons in --verbose mode
|
|
261
|
-
* - Protects manual translations by default
|
|
262
|
-
*/
|
|
263
|
-
function needsTranslation(key, value, enValue) {
|
|
264
|
-
// --force mode: Always translate
|
|
265
|
-
if (options.force) {
|
|
266
|
-
if (options.verbose) {
|
|
267
|
-
console.log(` š Force mode: translating "${key}"`);
|
|
268
|
-
}
|
|
269
|
-
return true;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// --keys filter: Only translate specified keys
|
|
273
|
-
if (options.keysFilter && !options.keysFilter.includes(key)) {
|
|
274
|
-
if (options.verbose) {
|
|
275
|
-
console.log(` āļø Skipping "${key}": not in --keys filter`);
|
|
276
|
-
}
|
|
277
|
-
stats.skipped++;
|
|
278
|
-
return false;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// Translate if value is missing/undefined
|
|
282
|
-
if (value === undefined || value === null) {
|
|
283
|
-
if (options.verbose) {
|
|
284
|
-
console.log(` ā
Translating "${key}": missing value`);
|
|
285
|
-
}
|
|
286
|
-
return true;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
if (typeof value !== 'string') {
|
|
290
|
-
if (options.verbose) {
|
|
291
|
-
console.log(` āļø Skipping "${key}": non-string value`);
|
|
292
|
-
}
|
|
293
|
-
stats.skipped++;
|
|
294
|
-
return false;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// Skip brand names only
|
|
298
|
-
if (SKIP_WORDS.has(value)) {
|
|
299
|
-
if (options.verbose) {
|
|
300
|
-
console.log(` āļø Skipping "${key}": brand name`);
|
|
301
|
-
}
|
|
302
|
-
stats.skipped++;
|
|
303
|
-
return false;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// Skip numeric values and placeholders (0.00, phone numbers, etc.)
|
|
307
|
-
if (/^[\d\s.+()-]+$/.test(value)) {
|
|
308
|
-
if (options.verbose) {
|
|
309
|
-
console.log(` āļø Skipping "${key}": numeric value`);
|
|
310
|
-
}
|
|
311
|
-
stats.skipped++;
|
|
312
|
-
return false;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// Skip email addresses
|
|
316
|
-
if (/^[\w.-]+@[\w.-]+\.\w+$/.test(value)) {
|
|
317
|
-
if (options.verbose) {
|
|
318
|
-
console.log(` āļø Skipping "${key}": email address`);
|
|
319
|
-
}
|
|
320
|
-
stats.skipped++;
|
|
321
|
-
return false;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// Skip URLs
|
|
325
|
-
if (value.startsWith('http://') || value.startsWith('https://')) {
|
|
326
|
-
if (options.verbose) {
|
|
327
|
-
console.log(` āļø Skipping "${key}": URL`);
|
|
328
|
-
}
|
|
329
|
-
stats.skipped++;
|
|
330
|
-
return false;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// Skip if already translated (value is different from English)
|
|
334
|
-
// This protects manual translations and prevents re-translation of auto-translated strings!
|
|
335
|
-
if (value !== enValue) {
|
|
336
|
-
if (options.verbose) {
|
|
337
|
-
console.log(` āļø Skipping "${key}": already translated (manual or previous auto)`);
|
|
338
|
-
}
|
|
339
|
-
stats.skipped++;
|
|
340
|
-
return false;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// If we get here, value exists and equals enValue
|
|
344
|
-
// This means it hasn't been translated yet - TRANSLATE IT!
|
|
345
|
-
if (options.verbose) {
|
|
346
|
-
console.log(` ā
Translating "${key}": equals English source`);
|
|
347
|
-
}
|
|
348
|
-
return true;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
/**
|
|
352
|
-
* Recursively find and translate missing values
|
|
353
|
-
*/
|
|
354
|
-
async function translateObject(enObj, targetObj, targetLang, path = '') {
|
|
355
|
-
let translatedCount = 0;
|
|
356
|
-
|
|
357
|
-
for (const key in enObj) {
|
|
358
|
-
const currentPath = path ? `${path}.${key}` : key;
|
|
359
|
-
stats.totalKeys++;
|
|
360
|
-
|
|
361
|
-
if (typeof enObj[key] === 'object' && enObj[key] !== null) {
|
|
362
|
-
if (!targetObj[key] || typeof targetObj[key] !== 'object') {
|
|
363
|
-
targetObj[key] = {};
|
|
364
|
-
}
|
|
365
|
-
translatedCount += await translateObject(
|
|
366
|
-
enObj[key],
|
|
367
|
-
targetObj[key],
|
|
368
|
-
targetLang,
|
|
369
|
-
currentPath
|
|
370
|
-
);
|
|
371
|
-
} else if (typeof enObj[key] === 'string') {
|
|
372
|
-
const originalValue = enObj[key];
|
|
373
|
-
|
|
374
|
-
// Skip placeholders like {{count}}, {{mood}}, etc.
|
|
375
|
-
if (originalValue.includes('{{') && originalValue.includes('}}')) {
|
|
376
|
-
if (options.verbose) {
|
|
377
|
-
console.log(
|
|
378
|
-
` āļø Skipping placeholder: ${currentPath} = "${originalValue}"`
|
|
379
|
-
);
|
|
380
|
-
}
|
|
381
|
-
stats.skipped++;
|
|
382
|
-
continue;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// Check if needs translation
|
|
386
|
-
if (needsTranslation(key, targetObj[key], originalValue)) {
|
|
387
|
-
if (!options.verbose) {
|
|
388
|
-
// Only show in non-verbose mode (verbose already shows in needsTranslation)
|
|
389
|
-
console.log(` š Translating: ${currentPath} = "${originalValue}"`);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
try {
|
|
393
|
-
const translated = await translateText(originalValue, targetLang);
|
|
394
|
-
targetObj[key] = translated;
|
|
395
|
-
translatedCount++;
|
|
396
|
-
stats.translated++;
|
|
397
|
-
|
|
398
|
-
// Add delay to avoid rate limiting (300ms between requests - increased for stability)
|
|
399
|
-
await delay(300);
|
|
400
|
-
} catch (error) {
|
|
401
|
-
console.error(` ā Failed to translate "${currentPath}":`, error.message);
|
|
402
|
-
stats.errors++;
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
return translatedCount;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
/**
|
|
412
|
-
* Translate missing strings for a single file
|
|
413
|
-
*/
|
|
414
|
-
async function translateFile(enUSFile, targetFile, langCode) {
|
|
415
|
-
const enUS = JSON.parse(fs.readFileSync(enUSFile, 'utf8'));
|
|
416
|
-
const target = JSON.parse(fs.readFileSync(targetFile, 'utf8'));
|
|
417
|
-
|
|
418
|
-
const targetLang = LANGUAGE_MAP[langCode];
|
|
419
|
-
if (!targetLang) {
|
|
420
|
-
console.log(` ā ļø No language mapping for ${langCode}, skipping`);
|
|
421
|
-
return 0;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// Note: English variants (en-AU, en-CA, en-GB) will also be translated
|
|
425
|
-
// Google Translate may use regional variations and accents
|
|
426
|
-
|
|
427
|
-
const translatedCount = await translateObject(enUS, target, targetLang);
|
|
428
|
-
|
|
429
|
-
if (translatedCount > 0) {
|
|
430
|
-
// Ensure parent directory exists before writing
|
|
431
|
-
const targetDir = path.dirname(targetFile);
|
|
432
|
-
if (!fs.existsSync(targetDir)) {
|
|
433
|
-
fs.mkdirSync(targetDir, { recursive: true });
|
|
434
|
-
}
|
|
435
|
-
fs.writeFileSync(targetFile, JSON.stringify(target, null, 2) + '\n');
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
return translatedCount;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
/**
|
|
442
|
-
* Show usage information
|
|
443
|
-
*/
|
|
444
|
-
function showUsage() {
|
|
445
|
-
console.log(`
|
|
446
|
-
š Google Translate Auto-Translation Script
|
|
447
|
-
|
|
448
|
-
USAGE:
|
|
449
|
-
npm run i18n:translate [options]
|
|
450
|
-
|
|
451
|
-
OPTIONS:
|
|
452
|
-
--verbose, -v Show detailed translation decisions
|
|
453
|
-
--force, -f Force retranslate all keys (overwrite manual translations)
|
|
454
|
-
--keys=key1,key2 Only translate specific keys (comma-separated)
|
|
455
|
-
--report, -r Show detailed statistics report
|
|
456
|
-
|
|
457
|
-
EXAMPLES:
|
|
458
|
-
npm run i18n:translate
|
|
459
|
-
ā Translate only missing keys, skip manual translations
|
|
460
|
-
|
|
461
|
-
npm run i18n:translate --verbose
|
|
462
|
-
ā Show why each key is translated or skipped
|
|
463
|
-
|
|
464
|
-
npm run i18n:translate --force
|
|
465
|
-
ā Retranslate ALL keys, overwriting manual translations
|
|
466
|
-
|
|
467
|
-
npm run i18n:translate --keys=save,cancel,delete
|
|
468
|
-
ā Only retranslate these specific keys
|
|
469
|
-
|
|
470
|
-
npm run i18n:translate --verbose --keys=title
|
|
471
|
-
ā Verbose mode for specific key translation
|
|
472
|
-
|
|
473
|
-
BEHAVIOR:
|
|
474
|
-
ā
Protects manual translations by default
|
|
475
|
-
ā
Only translates missing keys or keys that equal English
|
|
476
|
-
ā
Use --force to override protection
|
|
477
|
-
ā
Use --keys to selectively retranslate specific keys
|
|
478
|
-
|
|
479
|
-
NOTE:
|
|
480
|
-
Scripts automatically find your project's locales directory.
|
|
481
|
-
Supported paths:
|
|
482
|
-
- src/domains/localization/infrastructure/locales
|
|
483
|
-
- src/locales
|
|
484
|
-
- locales
|
|
485
|
-
`);
|
|
486
|
-
process.exit(0);
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
// Show usage if --help
|
|
490
|
-
if (args.includes('--help') || args.includes('-h')) {
|
|
491
|
-
showUsage();
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
/**
|
|
495
|
-
* Main function to translate all missing strings
|
|
496
|
-
*/
|
|
497
|
-
async function translateAllLanguages() {
|
|
498
|
-
// Find project's locales directory
|
|
499
|
-
const localesDir = getLocalesDir();
|
|
500
|
-
const enUSDir = path.join(localesDir, 'en-US');
|
|
501
|
-
|
|
502
|
-
// Automatically discover all JSON files in en-US directory
|
|
503
|
-
const files = fs
|
|
504
|
-
.readdirSync(enUSDir)
|
|
505
|
-
.filter(file => file.endsWith('.json'))
|
|
506
|
-
.sort();
|
|
507
|
-
|
|
508
|
-
// Get languages that need translation (skip complete ones)
|
|
509
|
-
const skipLanguages = new Set([
|
|
510
|
-
'en-US', // Base language - skip translation
|
|
511
|
-
'en-AU', // English variant - copy from en-US
|
|
512
|
-
'en-CA', // English variant - copy from en-US
|
|
513
|
-
'en-GB', // English variant - copy from en-US
|
|
514
|
-
]);
|
|
515
|
-
|
|
516
|
-
const allLanguages = fs
|
|
517
|
-
.readdirSync(localesDir)
|
|
518
|
-
.filter(
|
|
519
|
-
dir =>
|
|
520
|
-
!skipLanguages.has(dir) &&
|
|
521
|
-
fs.statSync(path.join(localesDir, dir)).isDirectory()
|
|
522
|
-
)
|
|
523
|
-
.sort();
|
|
524
|
-
|
|
525
|
-
console.log('š Starting automatic translation...\n');
|
|
526
|
-
|
|
527
|
-
// Show options if any are active
|
|
528
|
-
if (options.verbose || options.force || options.keysFilter || options.report) {
|
|
529
|
-
console.log('āļø Active options:');
|
|
530
|
-
if (options.verbose) console.log(' ⢠Verbose mode: ON');
|
|
531
|
-
if (options.force) console.log(' ⢠Force retranslate: ON (ā ļø will overwrite manual translations)');
|
|
532
|
-
if (options.keysFilter) console.log(` ⢠Keys filter: ${options.keysFilter.join(', ')}`);
|
|
533
|
-
if (options.report) console.log(' ⢠Detailed report: ON');
|
|
534
|
-
console.log('');
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
console.log(`š Languages to translate: ${allLanguages.length}`);
|
|
538
|
-
console.log(`š Files per language: ${files.length}`);
|
|
539
|
-
console.log(
|
|
540
|
-
`ā±ļø Estimated time: ~${Math.ceil((allLanguages.length * files.length * 50 * 0.2) / 60)} minutes\n`
|
|
541
|
-
);
|
|
542
|
-
console.log(
|
|
543
|
-
'ā” Running with optimized speed (200ms delay between translations)\n'
|
|
544
|
-
);
|
|
545
|
-
|
|
546
|
-
stats.totalLanguages = allLanguages.length;
|
|
547
|
-
stats.totalFiles = files.length;
|
|
548
|
-
|
|
549
|
-
let totalTranslated = 0;
|
|
550
|
-
let totalLanguages = 0;
|
|
551
|
-
|
|
552
|
-
for (const langCode of allLanguages) {
|
|
553
|
-
console.log(`\nš Translating ${langCode}...`);
|
|
554
|
-
totalLanguages++;
|
|
555
|
-
|
|
556
|
-
let langTranslated = 0;
|
|
557
|
-
|
|
558
|
-
for (const file of files) {
|
|
559
|
-
const enUSFile = path.join(enUSDir, file);
|
|
560
|
-
const targetDir = path.join(localesDir, langCode);
|
|
561
|
-
const targetFile = path.join(targetDir, file);
|
|
562
|
-
|
|
563
|
-
// Ensure target directory exists
|
|
564
|
-
if (!fs.existsSync(targetDir)) {
|
|
565
|
-
fs.mkdirSync(targetDir, { recursive: true });
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
// Create target file if it doesn't exist (create empty structure)
|
|
569
|
-
if (!fs.existsSync(targetFile)) {
|
|
570
|
-
console.log(` š Creating ${file} (new file)`);
|
|
571
|
-
// Create empty object - translation will populate it
|
|
572
|
-
fs.writeFileSync(targetFile, JSON.stringify({}, null, 2) + '\n');
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
const count = await translateFile(enUSFile, targetFile, langCode);
|
|
576
|
-
langTranslated += count;
|
|
577
|
-
totalTranslated += count;
|
|
578
|
-
|
|
579
|
-
// Only log files that were actually translated
|
|
580
|
-
if (count > 0) {
|
|
581
|
-
console.log(` ā
${file}: ${count} strings translated`);
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
console.log(
|
|
586
|
-
` š ${langCode} summary: ${langTranslated} strings translated`
|
|
587
|
-
);
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
console.log(`\nā
Translation completed!`);
|
|
591
|
-
console.log(` Languages processed: ${totalLanguages}`);
|
|
592
|
-
console.log(` Total strings translated: ${totalTranslated}`);
|
|
593
|
-
|
|
594
|
-
// Show detailed statistics if --report or --verbose
|
|
595
|
-
if (options.report || options.verbose) {
|
|
596
|
-
console.log(`\nš DETAILED STATISTICS:`);
|
|
597
|
-
console.log(` Total keys processed: ${stats.totalKeys}`);
|
|
598
|
-
console.log(` Translated: ${stats.translated} (${((stats.translated / stats.totalKeys) * 100).toFixed(1)}%)`);
|
|
599
|
-
console.log(` Skipped: ${stats.skipped} (${((stats.skipped / stats.totalKeys) * 100).toFixed(1)}%)`);
|
|
600
|
-
if (stats.errors > 0) {
|
|
601
|
-
console.log(` Errors: ${stats.errors} ā`);
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
console.log(`\nš” TIPS:`);
|
|
605
|
-
console.log(` ⢠Manual translations are protected (skipped) by default`);
|
|
606
|
-
console.log(` ⢠Use --force to retranslate everything`);
|
|
607
|
-
console.log(` ⢠Use --keys=key1,key2 to retranslate specific keys`);
|
|
608
|
-
console.log(` ⢠Use --verbose to see why keys are skipped`);
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
console.log(
|
|
612
|
-
`\nš Next step: Run 'npm run i18n:check' to verify translations.`
|
|
613
|
-
);
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
translateAllLanguages().catch(error => {
|
|
617
|
-
console.error('ā Translation failed:', error);
|
|
618
|
-
process.exit(1);
|
|
619
|
-
});
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Utility to find project's locales directory
|
|
4
|
-
* Searches common paths for localization files
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
const fs = require('fs');
|
|
8
|
-
const path = require('path');
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Find locales directory in project
|
|
12
|
-
* @returns {string|null} Path to locales directory or null if not found
|
|
13
|
-
*/
|
|
14
|
-
function findLocalesDir() {
|
|
15
|
-
const projectRoot = process.cwd();
|
|
16
|
-
|
|
17
|
-
// Common paths to search
|
|
18
|
-
const possiblePaths = [
|
|
19
|
-
// DDD structure
|
|
20
|
-
path.join(projectRoot, 'src/domains/localization/infrastructure/locales'),
|
|
21
|
-
path.join(projectRoot, 'src/domains/i18n/infrastructure/locales'),
|
|
22
|
-
path.join(projectRoot, 'src/infrastructure/localization/locales'),
|
|
23
|
-
// Simple structure
|
|
24
|
-
path.join(projectRoot, 'src/locales'),
|
|
25
|
-
path.join(projectRoot, 'locales'),
|
|
26
|
-
path.join(projectRoot, 'translations'),
|
|
27
|
-
// Alternative DDD
|
|
28
|
-
path.join(projectRoot, 'src/features/localization/locales'),
|
|
29
|
-
];
|
|
30
|
-
|
|
31
|
-
// Find first existing path with en-US directory
|
|
32
|
-
for (const possiblePath of possiblePaths) {
|
|
33
|
-
const enUSPath = path.join(possiblePath, 'en-US');
|
|
34
|
-
if (fs.existsSync(possiblePath) && fs.existsSync(enUSPath)) {
|
|
35
|
-
return possiblePath;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return null;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Get or create locales directory
|
|
44
|
-
* @param {boolean} createIfNotExists - Create directory if it doesn't exist (for setup script)
|
|
45
|
-
* @returns {string} Path to locales directory
|
|
46
|
-
*/
|
|
47
|
-
function getLocalesDir(createIfNotExists = false) {
|
|
48
|
-
let localesDir = findLocalesDir();
|
|
49
|
-
|
|
50
|
-
if (!localesDir) {
|
|
51
|
-
if (createIfNotExists) {
|
|
52
|
-
// Try to create in most common location
|
|
53
|
-
const projectRoot = process.cwd();
|
|
54
|
-
localesDir = path.join(projectRoot, 'src/domains/localization/infrastructure/locales');
|
|
55
|
-
const enUSDir = path.join(localesDir, 'en-US');
|
|
56
|
-
|
|
57
|
-
if (!fs.existsSync(localesDir)) {
|
|
58
|
-
fs.mkdirSync(localesDir, { recursive: true });
|
|
59
|
-
fs.mkdirSync(enUSDir, { recursive: true });
|
|
60
|
-
console.log(`ā
Created locales directory: ${localesDir}`);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return localesDir;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
console.error('ā Locales directory not found!');
|
|
67
|
-
console.error('\nPlease create a locales directory in one of these locations:');
|
|
68
|
-
console.error(' - src/domains/localization/infrastructure/locales');
|
|
69
|
-
console.error(' - src/locales');
|
|
70
|
-
console.error(' - locales');
|
|
71
|
-
console.error('\nOr run: npm run i18n:setup');
|
|
72
|
-
process.exit(1);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return localesDir;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
module.exports = { findLocalesDir, getLocalesDir };
|
|
79
|
-
|