@umituz/react-native-localization 1.0.0 → 1.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/README.md CHANGED
@@ -7,6 +7,7 @@ Universal localization system for React Native apps with i18n support. Built wit
7
7
  - **29+ Language Support**: Pre-configured support for 29 languages including RTL languages (Arabic)
8
8
  - **Automatic Device Locale Detection**: Automatically detects and applies device language on first launch
9
9
  - **Persistent Language Preferences**: Saves user's language choice using AsyncStorage
10
+ - **Translation Management Scripts**: Built-in scripts for setup, translation, and validation
10
11
  - **Type-Safe**: Full TypeScript support with type definitions
11
12
  - **Zero Configuration**: Works out of the box with sensible defaults
12
13
  - **Production Ready**: Battle-tested in production apps
@@ -231,26 +232,65 @@ function MyComponent() {
231
232
  }
232
233
  ```
233
234
 
234
- ### Custom Translations
235
+ ### Project-Specific Translations
236
+
237
+ The package includes common translations. For project-specific translations:
238
+
239
+ 1. **Create your translation files** in your project:
240
+ ```
241
+ src/domains/localization/infrastructure/locales/
242
+ en-US/
243
+ myFeature.json
244
+ auth.json
245
+ tr-TR/
246
+ myFeature.json
247
+ auth.json
248
+ ```
249
+
250
+ 2. **Load project translations** in your app initialization:
251
+ ```tsx
252
+ import { i18n } from '@umituz/react-native-localization';
253
+ import myFeatureEnUS from './locales/en-US/myFeature.json';
254
+ import myFeatureTrTR from './locales/tr-TR/myFeature.json';
255
+
256
+ // Add project translations
257
+ i18n.addResources('en-US', 'translation', myFeatureEnUS);
258
+ i18n.addResources('tr-TR', 'translation', myFeatureTrTR);
259
+ ```
260
+
261
+ 3. **Use in components**:
262
+ ```tsx
263
+ const { t } = useLocalization();
264
+ console.log(t('myFeature.title')); // 'My Feature'
265
+ ```
266
+
267
+ ### Translation Management Scripts
268
+
269
+ The package includes scripts for managing translations:
235
270
 
236
- You can extend the translations by adding your own:
271
+ ```bash
272
+ # Setup language directories (creates all language folders from en-US)
273
+ npm run i18n:setup
237
274
 
238
- ```tsx
239
- import { i18n } from '@umituz/react-native-localization';
240
-
241
- // Add custom translations
242
- i18n.addResources('en-US', 'translation', {
243
- myFeature: {
244
- title: 'My Feature',
245
- description: 'Feature description'
246
- }
247
- });
248
-
249
- // Use in components
250
- const { t } = useLocalization();
251
- console.log(t('myFeature.title')); // 'My Feature'
275
+ # Translate missing keys using Google Translate
276
+ npm run i18n:translate
277
+
278
+ # Check translation completeness
279
+ npm run i18n:check <language-code>
280
+ npm run i18n:check all # Check all languages
281
+
282
+ # Remove unused translation keys
283
+ npm run i18n:remove-unused <language-code>
284
+ npm run i18n:remove-unused --all # Remove from all languages
252
285
  ```
253
286
 
287
+ **Workflow:**
288
+ 1. Add new keys to `en-US/*.json` files
289
+ 2. Run `npm run i18n:setup` to create language directories
290
+ 3. Run `npm run i18n:translate` to auto-translate missing keys
291
+ 4. Run `npm run i18n:check all` to verify completeness
292
+ 5. Review and refine translations manually if needed
293
+
254
294
  ## TypeScript Support
255
295
 
256
296
  The package is written in TypeScript and includes full type definitions.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-localization",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Universal localization system for React Native apps with i18n support",
5
5
  "main": "./lib/index.js",
6
6
  "types": "./lib/index.d.ts",
@@ -13,7 +13,11 @@
13
13
  "prepublishOnly": "npm run build",
14
14
  "version:patch": "npm version patch -m 'chore: release v%s'",
15
15
  "version:minor": "npm version minor -m 'chore: release v%s'",
16
- "version:major": "npm version major -m 'chore: release v%s'"
16
+ "version:major": "npm version major -m 'chore: release v%s'",
17
+ "i18n:setup": "node scripts/setup-languages.js",
18
+ "i18n:translate": "node scripts/translate-missing.js",
19
+ "i18n:check": "node scripts/check-translations.js",
20
+ "i18n:remove-unused": "node scripts/remove-unused-keys.js"
17
21
  },
18
22
  "keywords": [
19
23
  "react-native",
@@ -58,6 +62,7 @@
58
62
  "files": [
59
63
  "lib",
60
64
  "src",
65
+ "scripts",
61
66
  "README.md",
62
67
  "LICENSE"
63
68
  ]
@@ -0,0 +1,354 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable no-console */
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const { getLocalesDir } = require('./utils/findLocalesDir');
7
+
8
+ // Function to get all keys from an object recursively
9
+ function getAllKeys(obj, prefix = '') {
10
+ let keys = [];
11
+ for (const key in obj) {
12
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
13
+ const fullKey = prefix ? `${prefix}.${key}` : key;
14
+ if (
15
+ typeof obj[key] === 'object' &&
16
+ obj[key] !== null &&
17
+ !Array.isArray(obj[key])
18
+ ) {
19
+ keys = keys.concat(getAllKeys(obj[key], fullKey));
20
+ } else {
21
+ keys.push(fullKey);
22
+ }
23
+ }
24
+ }
25
+ return keys;
26
+ }
27
+
28
+ // Function to get value by key path
29
+ function getValueByPath(obj, path) {
30
+ return path.split('.').reduce((current, key) => current && current[key], obj);
31
+ }
32
+
33
+ // Function to compare two translation objects
34
+ function compareTranslations(enUS, targetLang, filePath, langCode) {
35
+ const enKeys = getAllKeys(enUS);
36
+ const targetKeys = getAllKeys(targetLang);
37
+
38
+ const missingInTarget = enKeys.filter(key => !targetKeys.includes(key));
39
+ const extraInTarget = targetKeys.filter(key => !enKeys.includes(key));
40
+
41
+ const issues = [];
42
+
43
+ // Check for missing keys in target language
44
+ if (missingInTarget.length > 0) {
45
+ issues.push({
46
+ type: 'missing',
47
+ message: `Missing keys in ${langCode}: ${missingInTarget.join(', ')}`,
48
+ keys: missingInTarget,
49
+ });
50
+ }
51
+
52
+ // Check for extra keys in target language
53
+ if (extraInTarget.length > 0) {
54
+ issues.push({
55
+ type: 'extra',
56
+ message: `Extra keys in ${langCode}: ${extraInTarget.join(', ')}`,
57
+ keys: extraInTarget,
58
+ });
59
+ }
60
+
61
+ // Check for empty or placeholder values in target language
62
+ enKeys.forEach(key => {
63
+ if (targetKeys.includes(key)) {
64
+ const enValue = getValueByPath(enUS, key);
65
+ const targetValue = getValueByPath(targetLang, key);
66
+
67
+ // Skip checking if the value is the same and it's a common English word that doesn't need translation
68
+ const commonEnglishWords = [
69
+ 'Premium',
70
+ 'EULA',
71
+ 'Plan',
72
+ 'OK',
73
+ 'API',
74
+ 'URL',
75
+ 'iOS',
76
+ 'Android',
77
+ 'minute',
78
+ 'min',
79
+ 'Sessions',
80
+ 'Points',
81
+ 'Nature',
82
+ 'Instrumental',
83
+ 'Piano',
84
+ 'Silence',
85
+ 'Pause',
86
+ 'Gratitude',
87
+ 'Notes',
88
+ '5 min',
89
+ '10 min',
90
+ '15 min',
91
+ '20 min',
92
+ '30 min',
93
+ '{{count}} min',
94
+ '{{duration}} min',
95
+ '{{duration}} • {{cycles}} cycles',
96
+ 'No',
97
+ 'Error',
98
+ 'Try Again',
99
+ 'Back',
100
+ 'Oops! Something went wrong',
101
+ "We encountered an unexpected error. Don't worry, your meditation data is safe.",
102
+ "This screen doesn't exist.",
103
+ 'Go to home screen!',
104
+ 'No meditation cards available',
105
+ "Thank you for reporting this bug! We'll investigate and fix it as soon as possible.",
106
+ "Thank you for your feature request! We'll consider it for future updates.",
107
+ 'Thank you for your suggestion! We appreciate your input and will review it carefully.',
108
+ 'Your feedback has been submitted successfully. We appreciate your input!',
109
+ 'Continue',
110
+ 'Go Home',
111
+ 'Start Meditating',
112
+ 'Begin Your Journey',
113
+ 'Detailed statistics',
114
+ 'Achievement badges',
115
+ 'Weekly/monthly reports',
116
+ 'Sign Up',
117
+ 'Sign In',
118
+ 'Breathe In',
119
+ 'Hold',
120
+ 'Breathe Out',
121
+ 'Rest',
122
+ 'Find a comfortable seated position',
123
+ 'Focus on slow, controlled movements',
124
+ "Don't force the breath, let it flow naturally",
125
+ 'Cycle {{current}} of {{total}}',
126
+ '{{inhale}}s in • {{hold}}s hold • {{exhale}}s out',
127
+ 'Total elapsed: {{time}}',
128
+ 'Cancel',
129
+ 'End Session',
130
+ 'Reset',
131
+ "Today's Progress",
132
+ 'See All',
133
+ 'Total Time',
134
+ 'Streak',
135
+ 'Last Session',
136
+ 'minutes completed',
137
+ 'Find a quiet, comfortable space',
138
+ 'Focus on your breath naturally',
139
+ 'Let thoughts come and go without judgment',
140
+ 'Start with shorter sessions and build up',
141
+ 'Legal',
142
+ 'Loading sounds...',
143
+ ];
144
+ const shouldSkip =
145
+ commonEnglishWords.includes(enValue) && targetValue === enValue;
146
+
147
+ // Check if value needs translation
148
+ // Skip if already translated (different from English) - protects manual translations
149
+ const isAlreadyTranslated = targetValue !== enValue;
150
+
151
+ if (
152
+ !shouldSkip &&
153
+ !isAlreadyTranslated &&
154
+ (!targetValue ||
155
+ (typeof targetValue === 'string' && targetValue.trim() === '') ||
156
+ targetValue === enValue ||
157
+ targetValue.includes('[NEEDS TRANSLATION]') ||
158
+ targetValue.includes('[MISSING:') ||
159
+ targetValue.includes('[ƇEVİRİ GEREKLİ:') ||
160
+ targetValue.includes('[TRANSLATE:'))
161
+ ) {
162
+ issues.push({
163
+ type: 'translation',
164
+ message: `Key "${key}" has empty, missing, or untranslated value`,
165
+ key: key,
166
+ enValue: enValue,
167
+ targetValue: targetValue,
168
+ });
169
+ }
170
+ }
171
+ });
172
+
173
+ return {
174
+ file: filePath,
175
+ issues: issues,
176
+ totalEnKeys: enKeys.length,
177
+ totalTargetKeys: targetKeys.length,
178
+ missingCount: missingInTarget.length,
179
+ extraCount: extraInTarget.length,
180
+ };
181
+ }
182
+
183
+ // Main function
184
+ function main() {
185
+ const args = process.argv.slice(2);
186
+ const targetLanguage = args.find(arg => !arg.startsWith('--'));
187
+
188
+ if (!targetLanguage) {
189
+ console.log('Kullanım: npm run i18n:check <dil-kodu>');
190
+ console.log('Ɩrnek: npm run i18n:check de-DE');
191
+ console.log('Ɩrnek: npm run i18n:check fr-FR');
192
+ console.log('Ɩrnek: npm run i18n:check tr-TR');
193
+ console.log(
194
+ 'Ɩrnek: npm run i18n:check spanish (tüm İspanyolca varyantları)'
195
+ );
196
+ console.log(
197
+ '\nTüm dilleri kontrol etmek için: npm run i18n:check all'
198
+ );
199
+ process.exit(1);
200
+ }
201
+
202
+ // Find project's locales directory
203
+ const localesDir = getLocalesDir();
204
+ const enUSDir = path.join(localesDir, 'en-US');
205
+
206
+ // šŸ”„ CRITICAL FIX: Auto-discover all JSON files (NO HARDCODED LIST!)
207
+ // This ensures check-translations works with ANY en-US file structure
208
+ const files = fs
209
+ .readdirSync(enUSDir)
210
+ .filter(file => file.endsWith('.json'))
211
+ .sort();
212
+
213
+ if (files.length === 0) {
214
+ console.error('āŒ No JSON files found in en-US directory!');
215
+ process.exit(1);
216
+ }
217
+
218
+ console.log(`šŸ” Discovered ${files.length} translation files in en-US:\n ${files.join(', ')}\n`);
219
+
220
+ // Get all language directories
221
+ const allLanguageDirs = fs
222
+ .readdirSync(localesDir)
223
+ .filter(dir => {
224
+ const fullPath = path.join(localesDir, dir);
225
+ return fs.statSync(fullPath).isDirectory() && dir !== 'en-US';
226
+ })
227
+ .sort();
228
+
229
+ // Determine which languages to process
230
+ let languageDirs = allLanguageDirs;
231
+ if (targetLanguage !== 'all') {
232
+ if (targetLanguage === 'spanish' || targetLanguage === 'es') {
233
+ // Check all Spanish variants
234
+ languageDirs = allLanguageDirs.filter(dir => dir.startsWith('es-'));
235
+ if (languageDirs.length === 0) {
236
+ console.log('āŒ No Spanish variants found.');
237
+ process.exit(1);
238
+ }
239
+ } else if (allLanguageDirs.includes(targetLanguage)) {
240
+ languageDirs = [targetLanguage];
241
+ } else {
242
+ console.log(
243
+ `āŒ Language ${targetLanguage} not found. Available languages: ${allLanguageDirs.join(', ')}`
244
+ );
245
+ process.exit(1);
246
+ }
247
+ }
248
+
249
+ let totalIssues = 0;
250
+ let totalMissing = 0;
251
+ let totalExtra = 0;
252
+ let totalTranslationIssues = 0;
253
+ let languagesWithIssues = 0;
254
+
255
+ console.log(
256
+ `šŸ” Checking ${languageDirs.length} language(s) against English (en-US)...\n`
257
+ );
258
+
259
+ // Check each language
260
+ languageDirs.forEach(langCode => {
261
+ const langDir = path.join(localesDir, langCode);
262
+ let langIssues = 0;
263
+ let langMissing = 0;
264
+ let langExtra = 0;
265
+ let langTranslationIssues = 0;
266
+
267
+ console.log(`\nšŸŒ Checking ${langCode}...`);
268
+
269
+ files.forEach(file => {
270
+ const enUSPath = path.join(enUSDir, file);
271
+ const langPath = path.join(langDir, file);
272
+
273
+ if (!fs.existsSync(enUSPath)) {
274
+ console.log(` āŒ English file not found: ${file}`);
275
+ return;
276
+ }
277
+
278
+ if (!fs.existsSync(langPath)) {
279
+ console.log(` āŒ ${langCode} file not found: ${file}`);
280
+ return;
281
+ }
282
+
283
+ try {
284
+ const enUS = JSON.parse(fs.readFileSync(enUSPath, 'utf8'));
285
+ const langData = JSON.parse(fs.readFileSync(langPath, 'utf8'));
286
+
287
+ const comparison = compareTranslations(enUS, langData, file, langCode);
288
+
289
+ if (comparison.issues.length === 0) {
290
+ console.log(` āœ… ${file}: Complete`);
291
+ } else {
292
+ console.log(
293
+ ` šŸ“„ ${file}: ${comparison.missingCount} missing, ${comparison.extraCount} extra, ${comparison.issues.filter(i => i.type === 'translation').length} untranslated`
294
+ );
295
+
296
+ // Show detailed issues for debugging
297
+ comparison.issues.forEach(issue => {
298
+ if (issue.type === 'translation') {
299
+ console.log(
300
+ ` šŸ” ${issue.key}: "${issue.enValue}" → "${issue.targetValue || '[EMPTY]'}"`
301
+ );
302
+ } else if (issue.type === 'extra') {
303
+ console.log(` āž• Extra keys: ${issue.keys.join(', ')}`);
304
+ } else if (issue.type === 'missing') {
305
+ console.log(` āž– Missing keys: ${issue.keys.join(', ')}`);
306
+ }
307
+ langIssues++;
308
+ totalIssues++;
309
+ if (issue.type === 'missing') {
310
+ langMissing += issue.keys.length;
311
+ totalMissing += issue.keys.length;
312
+ } else if (issue.type === 'extra') {
313
+ langExtra += issue.keys.length;
314
+ totalExtra += issue.keys.length;
315
+ } else if (issue.type === 'translation') {
316
+ langTranslationIssues++;
317
+ totalTranslationIssues++;
318
+ }
319
+ });
320
+ }
321
+ } catch (error) {
322
+ console.log(` āŒ Error processing ${file}: ${error.message}`);
323
+ }
324
+ });
325
+
326
+ // Summary for this language
327
+ if (langIssues === 0) {
328
+ console.log(` šŸŽ‰ ${langCode}: All translations complete!`);
329
+ } else {
330
+ languagesWithIssues++;
331
+ console.log(
332
+ ` āš ļø ${langCode}: ${langIssues} issues (${langMissing} missing, ${langExtra} extra, ${langTranslationIssues} translation issues)`
333
+ );
334
+ }
335
+ });
336
+
337
+ console.log('\nšŸ“Š Summary:');
338
+ console.log(` Languages checked: ${languageDirs.length}`);
339
+ console.log(` Languages with issues: ${languagesWithIssues}`);
340
+ console.log(` Total issues found: ${totalIssues}`);
341
+ console.log(` Missing keys: ${totalMissing}`);
342
+ console.log(` Extra keys: ${totalExtra}`);
343
+ console.log(` Translation issues: ${totalTranslationIssues}`);
344
+
345
+ if (totalIssues === 0) {
346
+ console.log('\nšŸŽ‰ All translations are complete and correct!');
347
+ process.exit(0);
348
+ } else {
349
+ console.log(`\nāš ļø ${languagesWithIssues} language(s) need attention.`);
350
+ process.exit(1);
351
+ }
352
+ }
353
+
354
+ main();
@@ -0,0 +1,216 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable no-console */
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const { getLocalesDir } = require('./utils/findLocalesDir');
7
+
8
+ // Function to get all keys from an object recursively
9
+ function getAllKeys(obj, prefix = '') {
10
+ let keys = [];
11
+ for (const key in obj) {
12
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
13
+ const fullKey = prefix ? `${prefix}.${key}` : key;
14
+ if (
15
+ typeof obj[key] === 'object' &&
16
+ obj[key] !== null &&
17
+ !Array.isArray(obj[key])
18
+ ) {
19
+ keys = keys.concat(getAllKeys(obj[key], fullKey));
20
+ } else {
21
+ keys.push(fullKey);
22
+ }
23
+ }
24
+ }
25
+ return keys;
26
+ }
27
+
28
+ // Function to remove a key from an object by path
29
+ function removeKeyByPath(obj, keyPath) {
30
+ const keys = keyPath.split('.');
31
+ let current = obj;
32
+
33
+ for (let i = 0; i < keys.length - 1; i++) {
34
+ if (!current[keys[i]] || typeof current[keys[i]] !== 'object') {
35
+ return false; // Path doesn't exist
36
+ }
37
+ current = current[keys[i]];
38
+ }
39
+
40
+ const lastKey = keys[keys.length - 1];
41
+ if (current.hasOwnProperty(lastKey)) {
42
+ delete current[keys[keys.length - 1]];
43
+
44
+ // Clean up empty parent objects
45
+ if (keys.length > 1) {
46
+ let parent = obj;
47
+ for (let i = 0; i < keys.length - 2; i++) {
48
+ parent = parent[keys[i]];
49
+ }
50
+ const parentKey = keys[keys.length - 2];
51
+ if (Object.keys(parent[parentKey]).length === 0) {
52
+ delete parent[parentKey];
53
+ }
54
+ }
55
+
56
+ return true;
57
+ }
58
+ return false;
59
+ }
60
+
61
+ // Function to remove unused keys from a translation file
62
+ function removeUnusedKeys(enUS, targetLang, filePath, langCode, dryRun = false) {
63
+ const enKeys = getAllKeys(enUS);
64
+ const targetKeys = getAllKeys(targetLang);
65
+
66
+ const extraKeys = targetKeys.filter(key => !enKeys.includes(key));
67
+
68
+ if (extraKeys.length === 0) {
69
+ return { removed: 0, keys: [] };
70
+ }
71
+
72
+ // Create a deep copy to modify
73
+ const cleanedLang = JSON.parse(JSON.stringify(targetLang));
74
+
75
+ let removedCount = 0;
76
+ for (const key of extraKeys) {
77
+ if (removeKeyByPath(cleanedLang, key)) {
78
+ removedCount++;
79
+ }
80
+ }
81
+
82
+ if (!dryRun && removedCount > 0) {
83
+ // Write cleaned file
84
+ const targetFile = filePath;
85
+ fs.writeFileSync(targetFile, JSON.stringify(cleanedLang, null, 2) + '\n', 'utf8');
86
+ }
87
+
88
+ return { removed: removedCount, keys: extraKeys };
89
+ }
90
+
91
+ // Main function
92
+ function main() {
93
+ const args = process.argv.slice(2);
94
+ const targetLanguage = args.find(arg => !arg.startsWith('--'));
95
+ const dryRun = args.includes('--dry-run') || args.includes('-d');
96
+ const allLanguages = args.includes('--all') || args.includes('-a');
97
+
98
+ if (!targetLanguage && !allLanguages) {
99
+ console.log('Usage: npm run i18n:remove-unused <language-code> [--dry-run]');
100
+ console.log('Example: npm run i18n:remove-unused tr-TR');
101
+ console.log('Example: npm run i18n:remove-unused tr-TR --dry-run (preview only, no removal)');
102
+ console.log('Example: npm run i18n:remove-unused --all (all languages)');
103
+ process.exit(1);
104
+ }
105
+
106
+ // Find project's locales directory
107
+ const localesDir = getLocalesDir();
108
+ const enUSDir = path.join(localesDir, 'en-US');
109
+
110
+ // Auto-discover all JSON files
111
+ const files = fs
112
+ .readdirSync(enUSDir)
113
+ .filter(file => file.endsWith('.json'))
114
+ .sort();
115
+
116
+ if (files.length === 0) {
117
+ console.error('āŒ No JSON files found in en-US directory!');
118
+ process.exit(1);
119
+ }
120
+
121
+ // Get language directories
122
+ const allLanguageDirs = fs
123
+ .readdirSync(localesDir)
124
+ .filter(dir => {
125
+ const fullPath = path.join(localesDir, dir);
126
+ return fs.statSync(fullPath).isDirectory() && dir !== 'en-US';
127
+ })
128
+ .sort();
129
+
130
+ // Determine which languages to process
131
+ let languageDirs = allLanguageDirs;
132
+ if (!allLanguages) {
133
+ if (targetLanguage === 'spanish' || targetLanguage === 'es') {
134
+ languageDirs = allLanguageDirs.filter(dir => dir.startsWith('es-'));
135
+ } else if (allLanguageDirs.includes(targetLanguage)) {
136
+ languageDirs = [targetLanguage];
137
+ } else {
138
+ console.log(`āŒ Language ${targetLanguage} not found. Available: ${allLanguageDirs.join(', ')}`);
139
+ process.exit(1);
140
+ }
141
+ }
142
+
143
+ if (dryRun) {
144
+ console.log('šŸ” DRY RUN MODE - No files will be modified\n');
145
+ }
146
+
147
+ let totalRemoved = 0;
148
+ let totalFiles = 0;
149
+
150
+ console.log(`🧹 Removing unused keys from ${languageDirs.length} language(s)...\n`);
151
+
152
+ languageDirs.forEach(langCode => {
153
+ const langDir = path.join(localesDir, langCode);
154
+ let langRemoved = 0;
155
+ let langFiles = 0;
156
+
157
+ console.log(`\nšŸŒ Processing ${langCode}...`);
158
+
159
+ files.forEach(file => {
160
+ const enUSPath = path.join(enUSDir, file);
161
+ const langPath = path.join(langDir, file);
162
+
163
+ if (!fs.existsSync(enUSPath)) {
164
+ return;
165
+ }
166
+
167
+ if (!fs.existsSync(langPath)) {
168
+ return;
169
+ }
170
+
171
+ try {
172
+ const enUS = JSON.parse(fs.readFileSync(enUSPath, 'utf8'));
173
+ const langData = JSON.parse(fs.readFileSync(langPath, 'utf8'));
174
+
175
+ const result = removeUnusedKeys(enUS, langData, langPath, langCode, dryRun);
176
+
177
+ if (result.removed > 0) {
178
+ langRemoved += result.removed;
179
+ langFiles++;
180
+ totalRemoved += result.removed;
181
+ totalFiles++;
182
+
183
+ if (dryRun) {
184
+ console.log(` šŸ” ${file}: Would remove ${result.removed} key(s): ${result.keys.slice(0, 5).join(', ')}${result.keys.length > 5 ? '...' : ''}`);
185
+ } else {
186
+ console.log(` āœ… ${file}: Removed ${result.removed} unused key(s): ${result.keys.slice(0, 5).join(', ')}${result.keys.length > 5 ? '...' : ''}`);
187
+ }
188
+ }
189
+ } catch (error) {
190
+ console.log(` āŒ Error processing ${file}: ${error.message}`);
191
+ }
192
+ });
193
+
194
+ if (langRemoved === 0) {
195
+ console.log(` āœ… ${langCode}: No unused keys found`);
196
+ } else {
197
+ console.log(` šŸ“Š ${langCode}: Removed ${langRemoved} unused key(s) from ${langFiles} file(s)`);
198
+ }
199
+ });
200
+
201
+ console.log('\nšŸ“Š Summary:');
202
+ console.log(` Languages processed: ${languageDirs.length}`);
203
+ console.log(` Files modified: ${totalFiles}`);
204
+ console.log(` Total keys removed: ${totalRemoved}`);
205
+
206
+ if (dryRun) {
207
+ console.log('\nšŸ’” Run without --dry-run to actually remove the keys.');
208
+ } else if (totalRemoved > 0) {
209
+ console.log('\nāœ… Unused keys removed successfully!');
210
+ } else {
211
+ console.log('\nāœ… No unused keys found!');
212
+ }
213
+ }
214
+
215
+ main();
216
+
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable no-console */
3
+
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+ const { getLocalesDir } = require('./utils/findLocalesDir');
7
+
8
+ // All 38 languages (excluding en-US)
9
+ const LANGUAGES = [
10
+ "ar-SA", // Arabic
11
+ "bg-BG", // Bulgarian
12
+ "cs-CZ", // Czech
13
+ "da-DK", // Danish
14
+ "de-DE", // German
15
+ "el-GR", // Greek
16
+ "en-AU", // English (Australia)
17
+ "en-CA", // English (Canada)
18
+ "en-GB", // English (UK)
19
+ "es-ES", // Spanish (Spain)
20
+ "es-MX", // Spanish (Mexico)
21
+ "fi-FI", // Finnish
22
+ "fr-CA", // French (Canada)
23
+ "fr-FR", // French (France)
24
+ "hi-IN", // Hindi
25
+ "hr-HR", // Croatian
26
+ "hu-HU", // Hungarian
27
+ "id-ID", // Indonesian
28
+ "it-IT", // Italian
29
+ "ja-JP", // Japanese
30
+ "ko-KR", // Korean
31
+ "ms-MY", // Malay
32
+ "nl-NL", // Dutch
33
+ "no-NO", // Norwegian
34
+ "pl-PL", // Polish
35
+ "pt-BR", // Portuguese (Brazil)
36
+ "pt-PT", // Portuguese (Portugal)
37
+ "ro-RO", // Romanian
38
+ "ru-RU", // Russian
39
+ "sk-SK", // Slovak
40
+ "sv-SE", // Swedish
41
+ "th-TH", // Thai
42
+ "tl-PH", // Tagalog
43
+ "tr-TR", // Turkish
44
+ "uk-UA", // Ukrainian
45
+ "vi-VN", // Vietnamese
46
+ "zh-CN", // Chinese Simplified
47
+ "zh-TW", // Chinese Traditional
48
+ ];
49
+
50
+ async function setupLanguages() {
51
+ // Find or create project's locales directory
52
+ const localesDir = getLocalesDir(true); // Create if not exists
53
+ const enUSDir = path.join(localesDir, "en-US");
54
+
55
+ console.log("šŸš€ Setting up language directories and files...\n");
56
+
57
+ // Check if en-US directory exists
58
+ if (!fs.existsSync(enUSDir)) {
59
+ console.error("āŒ en-US directory not found!");
60
+ console.error(" Expected path:", enUSDir);
61
+ process.exit(1);
62
+ }
63
+
64
+ // Automatically discover all JSON files in en-US directory
65
+ const files = fs
66
+ .readdirSync(enUSDir)
67
+ .filter((file) => file.endsWith(".json"))
68
+ .sort();
69
+
70
+ if (files.length === 0) {
71
+ console.error("āŒ No JSON files found in en-US directory!");
72
+ process.exit(1);
73
+ }
74
+
75
+ console.log(`šŸ“„ Found ${files.length} translation files in en-US:`);
76
+ files.forEach((file) => console.log(` - ${file}`));
77
+ console.log("");
78
+
79
+ let createdDirs = 0;
80
+ let createdFiles = 0;
81
+
82
+ for (const langCode of LANGUAGES) {
83
+ const langDir = path.join(localesDir, langCode);
84
+
85
+ // Create language directory if it doesn't exist
86
+ if (!fs.existsSync(langDir)) {
87
+ fs.mkdirSync(langDir, { recursive: true });
88
+ console.log(`šŸ“ Created directory: ${langCode}/`);
89
+ createdDirs++;
90
+ }
91
+
92
+ // Copy each file from en-US
93
+ for (const file of files) {
94
+ const enUSFile = path.join(enUSDir, file);
95
+ const targetFile = path.join(langDir, file);
96
+
97
+ if (!fs.existsSync(targetFile)) {
98
+ // Copy the en-US file as a starting point
99
+ const content = fs.readFileSync(enUSFile, "utf8");
100
+ fs.writeFileSync(targetFile, content);
101
+ console.log(` šŸ“„ Created: ${langCode}/${file}`);
102
+ createdFiles++;
103
+ }
104
+ }
105
+ }
106
+
107
+ console.log(`\nāœ… Setup completed!`);
108
+ console.log(` Directories created: ${createdDirs}`);
109
+ console.log(` Files created: ${createdFiles}`);
110
+ console.log(` Total languages: ${LANGUAGES.length + 1} (including en-US)`);
111
+ console.log(
112
+ `\nšŸ“ Next steps:`,
113
+ );
114
+ console.log(` 1. Add your translation keys to en-US/*.json files`);
115
+ console.log(` 2. Run 'npm run i18n:translate' to auto-translate all languages`);
116
+ console.log(` 3. Run 'npm run i18n:check all' to verify completeness`);
117
+ }
118
+
119
+ setupLanguages().catch((error) => {
120
+ console.error("āŒ Setup failed:", error);
121
+ process.exit(1);
122
+ });
@@ -0,0 +1,500 @@
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
+ // Only skip actual brand names - everything else should be translated
71
+ // Note: Even common words like "Email", "OK", "Premium" should be localized
72
+ const SKIP_WORDS = new Set([
73
+ 'Google',
74
+ 'Apple',
75
+ 'Facebook',
76
+ 'Instagram',
77
+ 'Twitter',
78
+ 'WhatsApp',
79
+ ]);
80
+
81
+ /**
82
+ * Simple Google Translate API call using free endpoint
83
+ * Note: This uses Google's unofficial API. For production, use official API with key.
84
+ */
85
+ async function translateText(text, targetLang) {
86
+ return new Promise((resolve, _reject) => {
87
+ if (SKIP_WORDS.has(text)) {
88
+ resolve(text);
89
+ return;
90
+ }
91
+
92
+ const encodedText = encodeURIComponent(text);
93
+ const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=en&tl=${targetLang}&dt=t&q=${encodedText}`;
94
+
95
+ https
96
+ .get(url, res => {
97
+ let data = '';
98
+ res.on('data', chunk => {
99
+ data += chunk;
100
+ });
101
+ res.on('end', () => {
102
+ try {
103
+ const parsed = JSON.parse(data);
104
+ const translated = parsed[0]
105
+ .map(item => item[0])
106
+ .join('')
107
+ .trim();
108
+ resolve(translated || text);
109
+ } catch (error) {
110
+ console.warn(
111
+ `āš ļø Translation failed for "${text}" to ${targetLang}:`,
112
+ error.message
113
+ );
114
+ resolve(text); // Fallback to original
115
+ }
116
+ });
117
+ })
118
+ .on('error', err => {
119
+ console.warn(
120
+ `āš ļø Network error translating "${text}" to ${targetLang}:`,
121
+ err.message
122
+ );
123
+ resolve(text); // Fallback to original
124
+ });
125
+ });
126
+ }
127
+
128
+ /**
129
+ * Add delay between API calls to avoid rate limiting
130
+ */
131
+ function delay(ms) {
132
+ return new Promise(resolve => setTimeout(resolve, ms));
133
+ }
134
+
135
+ /**
136
+ * Check if a value needs translation (is missing or undefined)
137
+ *
138
+ * IMPROVED DETECTION:
139
+ * - Respects --force flag to retranslate everything
140
+ * - Respects --keys filter for selective retranslation
141
+ * - Shows skip reasons in --verbose mode
142
+ * - Protects manual translations by default
143
+ */
144
+ function needsTranslation(key, value, enValue) {
145
+ // --force mode: Always translate
146
+ if (options.force) {
147
+ if (options.verbose) {
148
+ console.log(` šŸ”„ Force mode: translating "${key}"`);
149
+ }
150
+ return true;
151
+ }
152
+
153
+ // --keys filter: Only translate specified keys
154
+ if (options.keysFilter && !options.keysFilter.includes(key)) {
155
+ if (options.verbose) {
156
+ console.log(` ā­ļø Skipping "${key}": not in --keys filter`);
157
+ }
158
+ stats.skipped++;
159
+ return false;
160
+ }
161
+
162
+ // Translate if value is missing/undefined
163
+ if (value === undefined || value === null) {
164
+ if (options.verbose) {
165
+ console.log(` āœ… Translating "${key}": missing value`);
166
+ }
167
+ return true;
168
+ }
169
+
170
+ if (typeof value !== 'string') {
171
+ if (options.verbose) {
172
+ console.log(` ā­ļø Skipping "${key}": non-string value`);
173
+ }
174
+ stats.skipped++;
175
+ return false;
176
+ }
177
+
178
+ // Skip brand names only
179
+ if (SKIP_WORDS.has(value)) {
180
+ if (options.verbose) {
181
+ console.log(` ā­ļø Skipping "${key}": brand name`);
182
+ }
183
+ stats.skipped++;
184
+ return false;
185
+ }
186
+
187
+ // Skip numeric values and placeholders (0.00, phone numbers, etc.)
188
+ if (/^[\d\s.+()-]+$/.test(value)) {
189
+ if (options.verbose) {
190
+ console.log(` ā­ļø Skipping "${key}": numeric value`);
191
+ }
192
+ stats.skipped++;
193
+ return false;
194
+ }
195
+
196
+ // Skip email addresses
197
+ if (/^[\w.-]+@[\w.-]+\.\w+$/.test(value)) {
198
+ if (options.verbose) {
199
+ console.log(` ā­ļø Skipping "${key}": email address`);
200
+ }
201
+ stats.skipped++;
202
+ return false;
203
+ }
204
+
205
+ // Skip URLs
206
+ if (value.startsWith('http://') || value.startsWith('https://')) {
207
+ if (options.verbose) {
208
+ console.log(` ā­ļø Skipping "${key}": URL`);
209
+ }
210
+ stats.skipped++;
211
+ return false;
212
+ }
213
+
214
+ // Skip if already translated (value is different from English)
215
+ // This protects manual translations!
216
+ if (value !== enValue) {
217
+ if (options.verbose) {
218
+ console.log(` ā­ļø Skipping "${key}": already translated (manual or previous auto)`);
219
+ }
220
+ stats.skipped++;
221
+ return false;
222
+ }
223
+
224
+ // If we get here, value exists and equals enValue
225
+ // This means it hasn't been translated yet - TRANSLATE IT!
226
+ if (options.verbose) {
227
+ console.log(` āœ… Translating "${key}": equals English source`);
228
+ }
229
+ return true;
230
+ }
231
+
232
+ /**
233
+ * Recursively find and translate missing values
234
+ */
235
+ async function translateObject(enObj, targetObj, targetLang, path = '') {
236
+ let translatedCount = 0;
237
+
238
+ for (const key in enObj) {
239
+ const currentPath = path ? `${path}.${key}` : key;
240
+ stats.totalKeys++;
241
+
242
+ if (typeof enObj[key] === 'object' && enObj[key] !== null) {
243
+ if (!targetObj[key] || typeof targetObj[key] !== 'object') {
244
+ targetObj[key] = {};
245
+ }
246
+ translatedCount += await translateObject(
247
+ enObj[key],
248
+ targetObj[key],
249
+ targetLang,
250
+ currentPath
251
+ );
252
+ } else if (typeof enObj[key] === 'string') {
253
+ const originalValue = enObj[key];
254
+
255
+ // Skip placeholders like {{count}}, {{mood}}, etc.
256
+ if (originalValue.includes('{{') && originalValue.includes('}}')) {
257
+ if (options.verbose) {
258
+ console.log(
259
+ ` ā­ļø Skipping placeholder: ${currentPath} = "${originalValue}"`
260
+ );
261
+ }
262
+ stats.skipped++;
263
+ continue;
264
+ }
265
+
266
+ // Check if needs translation
267
+ if (needsTranslation(key, targetObj[key], originalValue)) {
268
+ if (!options.verbose) {
269
+ // Only show in non-verbose mode (verbose already shows in needsTranslation)
270
+ console.log(` šŸ”„ Translating: ${currentPath} = "${originalValue}"`);
271
+ }
272
+
273
+ try {
274
+ const translated = await translateText(originalValue, targetLang);
275
+ targetObj[key] = translated;
276
+ translatedCount++;
277
+ stats.translated++;
278
+
279
+ // Add delay to avoid rate limiting (200ms between requests)
280
+ await delay(200);
281
+ } catch (error) {
282
+ console.error(` āŒ Failed to translate "${currentPath}":`, error.message);
283
+ stats.errors++;
284
+ }
285
+ }
286
+ }
287
+ }
288
+
289
+ return translatedCount;
290
+ }
291
+
292
+ /**
293
+ * Translate missing strings for a single file
294
+ */
295
+ async function translateFile(enUSFile, targetFile, langCode) {
296
+ const enUS = JSON.parse(fs.readFileSync(enUSFile, 'utf8'));
297
+ const target = JSON.parse(fs.readFileSync(targetFile, 'utf8'));
298
+
299
+ const targetLang = LANGUAGE_MAP[langCode];
300
+ if (!targetLang) {
301
+ console.log(` āš ļø No language mapping for ${langCode}, skipping`);
302
+ return 0;
303
+ }
304
+
305
+ // Note: English variants (en-AU, en-CA, en-GB) will also be translated
306
+ // Google Translate may use regional variations and accents
307
+
308
+ const translatedCount = await translateObject(enUS, target, targetLang);
309
+
310
+ if (translatedCount > 0) {
311
+ // Ensure parent directory exists before writing
312
+ const targetDir = path.dirname(targetFile);
313
+ if (!fs.existsSync(targetDir)) {
314
+ fs.mkdirSync(targetDir, { recursive: true });
315
+ }
316
+ fs.writeFileSync(targetFile, JSON.stringify(target, null, 2) + '\n');
317
+ }
318
+
319
+ return translatedCount;
320
+ }
321
+
322
+ /**
323
+ * Show usage information
324
+ */
325
+ function showUsage() {
326
+ console.log(`
327
+ šŸ“– Google Translate Auto-Translation Script
328
+
329
+ USAGE:
330
+ npm run i18n:translate [options]
331
+
332
+ OPTIONS:
333
+ --verbose, -v Show detailed translation decisions
334
+ --force, -f Force retranslate all keys (overwrite manual translations)
335
+ --keys=key1,key2 Only translate specific keys (comma-separated)
336
+ --report, -r Show detailed statistics report
337
+
338
+ EXAMPLES:
339
+ npm run i18n:translate
340
+ → Translate only missing keys, skip manual translations
341
+
342
+ npm run i18n:translate --verbose
343
+ → Show why each key is translated or skipped
344
+
345
+ npm run i18n:translate --force
346
+ → Retranslate ALL keys, overwriting manual translations
347
+
348
+ npm run i18n:translate --keys=save,cancel,delete
349
+ → Only retranslate these specific keys
350
+
351
+ npm run i18n:translate --verbose --keys=title
352
+ → Verbose mode for specific key translation
353
+
354
+ BEHAVIOR:
355
+ āœ… Protects manual translations by default
356
+ āœ… Only translates missing keys or keys that equal English
357
+ āœ… Use --force to override protection
358
+ āœ… Use --keys to selectively retranslate specific keys
359
+
360
+ NOTE:
361
+ Scripts automatically find your project's locales directory.
362
+ Supported paths:
363
+ - src/domains/localization/infrastructure/locales
364
+ - src/locales
365
+ - locales
366
+ `);
367
+ process.exit(0);
368
+ }
369
+
370
+ // Show usage if --help
371
+ if (args.includes('--help') || args.includes('-h')) {
372
+ showUsage();
373
+ }
374
+
375
+ /**
376
+ * Main function to translate all missing strings
377
+ */
378
+ async function translateAllLanguages() {
379
+ // Find project's locales directory
380
+ const localesDir = getLocalesDir();
381
+ const enUSDir = path.join(localesDir, 'en-US');
382
+
383
+ // Automatically discover all JSON files in en-US directory
384
+ const files = fs
385
+ .readdirSync(enUSDir)
386
+ .filter(file => file.endsWith('.json'))
387
+ .sort();
388
+
389
+ // Get languages that need translation (skip complete ones)
390
+ const skipLanguages = new Set([
391
+ 'en-US', // Base language - skip translation
392
+ 'en-AU', // English variant - copy from en-US
393
+ 'en-CA', // English variant - copy from en-US
394
+ 'en-GB', // English variant - copy from en-US
395
+ ]);
396
+
397
+ const allLanguages = fs
398
+ .readdirSync(localesDir)
399
+ .filter(
400
+ dir =>
401
+ !skipLanguages.has(dir) &&
402
+ fs.statSync(path.join(localesDir, dir)).isDirectory()
403
+ )
404
+ .sort();
405
+
406
+ console.log('šŸš€ Starting automatic translation...\n');
407
+
408
+ // Show options if any are active
409
+ if (options.verbose || options.force || options.keysFilter || options.report) {
410
+ console.log('āš™ļø Active options:');
411
+ if (options.verbose) console.log(' • Verbose mode: ON');
412
+ if (options.force) console.log(' • Force retranslate: ON (āš ļø will overwrite manual translations)');
413
+ if (options.keysFilter) console.log(` • Keys filter: ${options.keysFilter.join(', ')}`);
414
+ if (options.report) console.log(' • Detailed report: ON');
415
+ console.log('');
416
+ }
417
+
418
+ console.log(`šŸ“Š Languages to translate: ${allLanguages.length}`);
419
+ console.log(`šŸ“„ Files per language: ${files.length}`);
420
+ console.log(
421
+ `ā±ļø Estimated time: ~${Math.ceil((allLanguages.length * files.length * 50 * 0.2) / 60)} minutes\n`
422
+ );
423
+ console.log(
424
+ '⚔ Running with optimized speed (200ms delay between translations)\n'
425
+ );
426
+
427
+ stats.totalLanguages = allLanguages.length;
428
+ stats.totalFiles = files.length;
429
+
430
+ let totalTranslated = 0;
431
+ let totalLanguages = 0;
432
+
433
+ for (const langCode of allLanguages) {
434
+ console.log(`\nšŸŒ Translating ${langCode}...`);
435
+ totalLanguages++;
436
+
437
+ let langTranslated = 0;
438
+
439
+ for (const file of files) {
440
+ const enUSFile = path.join(enUSDir, file);
441
+ const targetDir = path.join(localesDir, langCode);
442
+ const targetFile = path.join(targetDir, file);
443
+
444
+ // Ensure target directory exists
445
+ if (!fs.existsSync(targetDir)) {
446
+ fs.mkdirSync(targetDir, { recursive: true });
447
+ }
448
+
449
+ // Create target file if it doesn't exist (create empty structure)
450
+ if (!fs.existsSync(targetFile)) {
451
+ console.log(` šŸ“ Creating ${file} (new file)`);
452
+ // Create empty object - translation will populate it
453
+ fs.writeFileSync(targetFile, JSON.stringify({}, null, 2) + '\n');
454
+ }
455
+
456
+ const count = await translateFile(enUSFile, targetFile, langCode);
457
+ langTranslated += count;
458
+ totalTranslated += count;
459
+
460
+ // Only log files that were actually translated
461
+ if (count > 0) {
462
+ console.log(` āœ… ${file}: ${count} strings translated`);
463
+ }
464
+ }
465
+
466
+ console.log(
467
+ ` šŸ“Š ${langCode} summary: ${langTranslated} strings translated`
468
+ );
469
+ }
470
+
471
+ console.log(`\nāœ… Translation completed!`);
472
+ console.log(` Languages processed: ${totalLanguages}`);
473
+ console.log(` Total strings translated: ${totalTranslated}`);
474
+
475
+ // Show detailed statistics if --report or --verbose
476
+ if (options.report || options.verbose) {
477
+ console.log(`\nšŸ“Š DETAILED STATISTICS:`);
478
+ console.log(` Total keys processed: ${stats.totalKeys}`);
479
+ console.log(` Translated: ${stats.translated} (${((stats.translated / stats.totalKeys) * 100).toFixed(1)}%)`);
480
+ console.log(` Skipped: ${stats.skipped} (${((stats.skipped / stats.totalKeys) * 100).toFixed(1)}%)`);
481
+ if (stats.errors > 0) {
482
+ console.log(` Errors: ${stats.errors} āŒ`);
483
+ }
484
+
485
+ console.log(`\nšŸ’” TIPS:`);
486
+ console.log(` • Manual translations are protected (skipped) by default`);
487
+ console.log(` • Use --force to retranslate everything`);
488
+ console.log(` • Use --keys=key1,key2 to retranslate specific keys`);
489
+ console.log(` • Use --verbose to see why keys are skipped`);
490
+ }
491
+
492
+ console.log(
493
+ `\nšŸ“ Next step: Run 'npm run i18n:check' to verify translations.`
494
+ );
495
+ }
496
+
497
+ translateAllLanguages().catch(error => {
498
+ console.error('āŒ Translation failed:', error);
499
+ process.exit(1);
500
+ });
@@ -0,0 +1,79 @@
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
+