@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.
@@ -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
-