@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 +56 -16
- package/package.json +7 -2
- package/scripts/check-translations.js +354 -0
- package/scripts/remove-unused-keys.js +216 -0
- package/scripts/setup-languages.js +122 -0
- package/scripts/translate-missing.js +500 -0
- package/scripts/utils/findLocalesDir.js +79 -0
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
|
-
###
|
|
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
|
-
|
|
271
|
+
```bash
|
|
272
|
+
# Setup language directories (creates all language folders from en-US)
|
|
273
|
+
npm run i18n:setup
|
|
237
274
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
i18n
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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.
|
|
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
|
+
|