@umituz/react-native-localization 3.7.11 → 3.7.13
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-localization",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.13",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Generic localization system for React Native apps with i18n support",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -10,7 +10,11 @@
|
|
|
10
10
|
"lint": "eslint src --ext .ts,.tsx --max-warnings 0",
|
|
11
11
|
"version:patch": "npm version patch -m 'chore: release v%s'",
|
|
12
12
|
"version:minor": "npm version minor -m 'chore: release v%s'",
|
|
13
|
-
"version:major": "npm version major -m 'chore: release v%s'"
|
|
13
|
+
"version:major": "npm version major -m 'chore: release v%s'",
|
|
14
|
+
"i18n:setup": "node src/scripts/setup-languages.js",
|
|
15
|
+
"i18n:check": "node src/scripts/sync-translations.js",
|
|
16
|
+
"i18n:sync": "node src/scripts/sync-translations.js",
|
|
17
|
+
"i18n:translate": "node src/scripts/translate-missing.js"
|
|
14
18
|
},
|
|
15
19
|
"keywords": [
|
|
16
20
|
"react-native",
|
|
@@ -27,7 +31,7 @@
|
|
|
27
31
|
},
|
|
28
32
|
"peerDependencies": {
|
|
29
33
|
"@react-native-async-storage/async-storage": ">=2.0.0",
|
|
30
|
-
"@umituz/react-native-design-system": "
|
|
34
|
+
"@umituz/react-native-design-system": "*",
|
|
31
35
|
"expo-localization": ">=16.0.0",
|
|
32
36
|
"i18next": ">=23.0.0",
|
|
33
37
|
"react": ">=18.2.0",
|
|
@@ -11,7 +11,6 @@ import path from 'path';
|
|
|
11
11
|
import { parseTypeScriptFile, generateTypeScriptContent } from './utils/file-parser.js';
|
|
12
12
|
import { addMissingKeys, removeExtraKeys } from './utils/sync-helper.js';
|
|
13
13
|
import { detectNewKeys } from './utils/key-detector.js';
|
|
14
|
-
import { getLangDisplayName } from './utils/translation-config.js';
|
|
15
14
|
import { extractUsedKeys } from './utils/key-extractor.js';
|
|
16
15
|
import { setDeep } from './utils/object-helper.js';
|
|
17
16
|
|
|
@@ -45,21 +44,47 @@ function syncLanguageFile(enUSPath, targetPath, langCode) {
|
|
|
45
44
|
function processExtraction(srcDir, enUSPath) {
|
|
46
45
|
if (!srcDir) return;
|
|
47
46
|
|
|
48
|
-
console.log(`🔍 Scanning source code: ${srcDir}...`);
|
|
49
|
-
const
|
|
50
|
-
console.log(` Found ${
|
|
47
|
+
console.log(`🔍 Scanning source code and dependencies: ${srcDir}...`);
|
|
48
|
+
const usedKeyMap = extractUsedKeys(srcDir);
|
|
49
|
+
console.log(` Found ${usedKeyMap.size} unique keys.`);
|
|
51
50
|
|
|
52
|
-
const
|
|
51
|
+
const oldEnUS = parseTypeScriptFile(enUSPath);
|
|
52
|
+
const newEnUS = {};
|
|
53
|
+
|
|
53
54
|
let addedCount = 0;
|
|
54
|
-
for (const key of
|
|
55
|
-
|
|
55
|
+
for (const [key, defaultValue] of usedKeyMap) {
|
|
56
|
+
// Try to keep existing translation if it exists
|
|
57
|
+
const existingValue = key.split('.').reduce((obj, k) => (obj && obj[k]), oldEnUS);
|
|
58
|
+
|
|
59
|
+
// We treat it as "not translated" if the value is exactly the key string
|
|
60
|
+
const isActuallyTranslated = typeof existingValue === 'string' && existingValue !== key;
|
|
61
|
+
const valueToSet = isActuallyTranslated ? existingValue : defaultValue;
|
|
62
|
+
|
|
63
|
+
if (setDeep(newEnUS, key, valueToSet)) {
|
|
64
|
+
if (!isActuallyTranslated) addedCount++;
|
|
65
|
+
}
|
|
56
66
|
}
|
|
57
67
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
68
|
+
// Count keys in objects
|
|
69
|
+
const getKeysCount = (obj) => {
|
|
70
|
+
let count = 0;
|
|
71
|
+
const walk = (o) => {
|
|
72
|
+
for (const k in o) {
|
|
73
|
+
if (typeof o[k] === 'object' && o[k] !== null) walk(o[k]);
|
|
74
|
+
else count++;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
walk(obj);
|
|
78
|
+
return count;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const oldTotal = getKeysCount(oldEnUS);
|
|
82
|
+
const newTotal = getKeysCount(newEnUS);
|
|
83
|
+
const removedCount = oldTotal - (newTotal - addedCount);
|
|
84
|
+
|
|
85
|
+
console.log(` ✨ Optimized en-US.ts: ${addedCount} keys populated/updated, pruned ${Math.max(0, removedCount)} unused.`);
|
|
86
|
+
const content = generateTypeScriptContent(newEnUS, 'en-US');
|
|
87
|
+
fs.writeFileSync(enUSPath, content);
|
|
63
88
|
}
|
|
64
89
|
|
|
65
90
|
export function syncTranslations(targetDir, srcDir) {
|
|
@@ -2,106 +2,91 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Translate Missing Script
|
|
5
|
-
*
|
|
6
|
-
* Usage: node translate-missing.js [locales-dir] [src-dir-optional] [lang-code-optional]
|
|
5
|
+
* Automatically translates missing strings using Google Translate
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
8
|
import fs from 'fs';
|
|
10
9
|
import path from 'path';
|
|
11
|
-
import { getTargetLanguage, isEnglishVariant, getLangDisplayName } from './utils/translation-config.js';
|
|
12
10
|
import { parseTypeScriptFile, generateTypeScriptContent } from './utils/file-parser.js';
|
|
11
|
+
import { getTargetLanguage, getLangDisplayName } from './utils/translation-config.js';
|
|
13
12
|
import { translateObject } from './utils/translator.js';
|
|
14
13
|
import { setupLanguages } from './setup-languages.js';
|
|
15
14
|
import { syncTranslations } from './sync-translations.js';
|
|
16
15
|
|
|
17
|
-
async function
|
|
18
|
-
const targetLang = getTargetLanguage(langCode);
|
|
19
|
-
|
|
20
|
-
if (!targetLang) {
|
|
21
|
-
console.log(` ⚠️ No language mapping for ${langCode}, skipping`);
|
|
22
|
-
return { count: 0, newKeys: [] };
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
if (isEnglishVariant(langCode)) {
|
|
26
|
-
console.log(` ⏭️ Skipping English variant: ${langCode}`);
|
|
27
|
-
return { count: 0, newKeys: [] };
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const enUS = parseTypeScriptFile(enUSPath);
|
|
31
|
-
let target;
|
|
32
|
-
|
|
33
|
-
try {
|
|
34
|
-
target = parseTypeScriptFile(targetPath);
|
|
35
|
-
} catch {
|
|
36
|
-
target = {};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const stats = { count: 0, newKeys: [] };
|
|
40
|
-
await translateObject(enUS, target, targetLang, '', stats);
|
|
41
|
-
|
|
42
|
-
if (stats.count > 0) {
|
|
43
|
-
const content = generateTypeScriptContent(target, langCode);
|
|
44
|
-
fs.writeFileSync(targetPath, content);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return stats;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async function main() {
|
|
51
|
-
const targetDir = process.argv[2] || 'src/domains/localization/infrastructure/locales';
|
|
52
|
-
const srcDir = process.argv[3];
|
|
53
|
-
const targetLangCode = process.argv[4];
|
|
54
|
-
|
|
16
|
+
async function translateMissing(targetDir, srcDir) {
|
|
55
17
|
const localesDir = path.resolve(process.cwd(), targetDir);
|
|
56
18
|
const enUSPath = path.join(localesDir, 'en-US.ts');
|
|
57
|
-
const indexPath = path.join(localesDir, 'index.ts');
|
|
58
19
|
|
|
59
|
-
|
|
20
|
+
// Integrated Workflow: Ensure setup and sync
|
|
21
|
+
const skipSync = process.argv.includes('--no-sync');
|
|
60
22
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
console.log('📦 Setup (index.ts) missing. Generating...');
|
|
23
|
+
if (!fs.existsSync(path.join(localesDir, 'index.ts'))) {
|
|
24
|
+
console.log('🔄 Initializing localization setup...');
|
|
64
25
|
setupLanguages(targetDir);
|
|
65
|
-
console.log('');
|
|
66
26
|
}
|
|
67
27
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (!fs.existsSync(localesDir) || !fs.existsSync(enUSPath)) {
|
|
74
|
-
console.error(`❌ Localization files not found in: ${localesDir}`);
|
|
75
|
-
process.exit(1);
|
|
28
|
+
if (!skipSync) {
|
|
29
|
+
console.log('\n🔄 Checking synchronization...');
|
|
30
|
+
syncTranslations(targetDir, srcDir);
|
|
31
|
+
} else {
|
|
32
|
+
console.log('\n⏭️ Skipping synchronization check...');
|
|
76
33
|
}
|
|
77
34
|
|
|
78
35
|
const files = fs.readdirSync(localesDir)
|
|
79
|
-
.filter(f => {
|
|
80
|
-
const isLangFile = f.match(/^[a-z]{2}-[A-Z]{2}\.ts$/) && f !== 'en-US.ts';
|
|
81
|
-
return isLangFile && (!targetLangCode || f === `${targetLangCode}.ts`);
|
|
82
|
-
})
|
|
36
|
+
.filter(f => f.match(/^[a-z]{2}-[A-Z]{2}\.ts$/) && f !== 'en-US.ts')
|
|
83
37
|
.sort();
|
|
84
38
|
|
|
85
|
-
console.log(
|
|
39
|
+
console.log(`\n📊 Languages to translate: ${files.length}\n`);
|
|
40
|
+
|
|
41
|
+
const enUS = parseTypeScriptFile(enUSPath);
|
|
86
42
|
|
|
87
|
-
let totalTranslated = 0;
|
|
88
43
|
for (const file of files) {
|
|
89
44
|
const langCode = file.replace('.ts', '');
|
|
45
|
+
const targetLang = getTargetLanguage(langCode);
|
|
46
|
+
const langName = getLangDisplayName(langCode);
|
|
47
|
+
|
|
48
|
+
if (!targetLang || targetLang === 'en') continue;
|
|
49
|
+
|
|
50
|
+
console.log(`🌍 Translating ${langCode} (${langName})...`);
|
|
51
|
+
|
|
90
52
|
const targetPath = path.join(localesDir, file);
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
53
|
+
const target = parseTypeScriptFile(targetPath);
|
|
54
|
+
|
|
55
|
+
const stats = { count: 0, checked: 0, translatedKeys: [] };
|
|
56
|
+
await translateObject(enUS, target, targetLang, '', stats);
|
|
57
|
+
|
|
58
|
+
// Clear progress line
|
|
59
|
+
process.stdout.write('\r' + ' '.repeat(80) + '\r');
|
|
60
|
+
|
|
94
61
|
if (stats.count > 0) {
|
|
95
|
-
|
|
62
|
+
const content = generateTypeScriptContent(target, langCode);
|
|
63
|
+
fs.writeFileSync(targetPath, content);
|
|
64
|
+
|
|
65
|
+
console.log(` ✅ Successfully translated ${stats.count} keys:`);
|
|
66
|
+
|
|
67
|
+
// Detailed logging of translated keys
|
|
68
|
+
const displayCount = Math.min(stats.translatedKeys.length, 15);
|
|
69
|
+
stats.translatedKeys.slice(0, displayCount).forEach(item => {
|
|
70
|
+
console.log(` • ${item.key}: "${item.from}" → "${item.to}"`);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
if (stats.translatedKeys.length > displayCount) {
|
|
74
|
+
console.log(` ... and ${stats.translatedKeys.length - displayCount} more.`);
|
|
75
|
+
}
|
|
96
76
|
} else {
|
|
97
|
-
console.log(`
|
|
77
|
+
console.log(` ✨ Already up to date.`);
|
|
98
78
|
}
|
|
99
79
|
}
|
|
100
80
|
|
|
101
|
-
console.log(
|
|
81
|
+
console.log('\n✅ All translations completed!');
|
|
102
82
|
}
|
|
103
83
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
process.
|
|
107
|
-
|
|
84
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
85
|
+
const targetDir = process.argv[2] || 'src/domains/localization/infrastructure/locales';
|
|
86
|
+
const srcDir = process.argv[3];
|
|
87
|
+
console.log('🚀 Starting integrated translation workflow...');
|
|
88
|
+
translateMissing(targetDir, srcDir).catch(err => {
|
|
89
|
+
console.error('\n❌ Translation workflow failed:', err.message);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
@@ -17,9 +17,12 @@ export function parseTypeScriptFile(filePath) {
|
|
|
17
17
|
const objectStr = match[1].replace(/;$/, '');
|
|
18
18
|
|
|
19
19
|
try {
|
|
20
|
+
// Basic evaluation for simple objects
|
|
21
|
+
// eslint-disable-next-line no-eval
|
|
20
22
|
return eval(`(${objectStr})`);
|
|
21
23
|
} catch (error) {
|
|
22
|
-
|
|
24
|
+
console.warn(`\n⚠️ Warning: Could not fully parse ${filePath}. Files with complex imports/spreads are currently limited.`);
|
|
25
|
+
return {}; // Return empty to avoid breaking the whole process
|
|
23
26
|
}
|
|
24
27
|
}
|
|
25
28
|
|
|
@@ -2,72 +2,104 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Key Extractor
|
|
6
|
-
* Scans source code for i18n keys
|
|
5
|
+
* Generic Key Extractor
|
|
6
|
+
* Scans source code for i18n translation keys
|
|
7
|
+
* NO project-specific logic - works for any React Native app
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
const IGNORED_DOMAINS = ['.com', '.org', '.net', '.io', '.co', '.app', '.ai', '.gov', '.edu'];
|
|
11
|
+
const IGNORED_EXTENSIONS = [
|
|
12
|
+
'.ts', '.tsx', '.js', '.jsx', '.json', '.yaml', '.yml',
|
|
13
|
+
'.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.pdf',
|
|
14
|
+
'.mp4', '.mov', '.avi', '.mp3', '.wav', '.css', '.scss', '.md'
|
|
15
|
+
];
|
|
16
|
+
const IGNORED_LAYOUT_VALS = new Set([
|
|
17
|
+
'center', 'row', 'column', 'flex', 'absolute', 'relative', 'hidden', 'visible',
|
|
18
|
+
'transparent', 'bold', 'normal', 'italic', 'contain', 'cover', 'stretch',
|
|
19
|
+
'top', 'bottom', 'left', 'right', 'middle', 'auto', 'none', 'underline',
|
|
20
|
+
'capitalize', 'uppercase', 'lowercase', 'solid', 'dotted', 'dashed', 'wrap',
|
|
21
|
+
'nowrap', 'space-between', 'space-around', 'flex-start', 'flex-end', 'baseline',
|
|
22
|
+
'react', 'index', 'default', 'string', 'number', 'boolean', 'key', 'id'
|
|
23
|
+
]);
|
|
15
24
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
function extractFromFile(content, keyMap) {
|
|
26
|
+
// Pattern 1: t('key') or t("key")
|
|
27
|
+
const tRegex = /(?:^|\W)t\(['"`]([^'"`]+)['"`]\)/g;
|
|
28
|
+
let match;
|
|
29
|
+
while ((match = tRegex.exec(content)) !== null) {
|
|
30
|
+
const key = match[1];
|
|
31
|
+
if (!key.includes('${') && !keyMap.has(key)) {
|
|
32
|
+
keyMap.set(key, key); // Use key itself as default
|
|
24
33
|
}
|
|
25
34
|
}
|
|
26
35
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
36
|
+
// Pattern 2: Dot-notation strings (potential i18n keys)
|
|
37
|
+
const dotRegex = /['"`]([a-z][a-z0-9_]*\.(?:[a-z0-9_]+\.)+[a-z0-9_]+)['"`]/gi;
|
|
38
|
+
while ((match = dotRegex.exec(content)) !== null) {
|
|
39
|
+
const key = match[1];
|
|
40
|
+
const isIgnoredDomain = IGNORED_DOMAINS.some(ext => key.toLowerCase().endsWith(ext));
|
|
41
|
+
const isIgnoredExt = IGNORED_EXTENSIONS.some(ext => key.toLowerCase().endsWith(ext));
|
|
42
|
+
if (!isIgnoredDomain && !isIgnoredExt && !key.includes(' ') && !keyMap.has(key)) {
|
|
43
|
+
keyMap.set(key, key);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
32
46
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
47
|
+
// Pattern 3: Template literals t(`prefix.${var}`)
|
|
48
|
+
const templateRegex = /t\(\`([a-z0-9_.]+)\.\$\{/g;
|
|
49
|
+
while ((match = templateRegex.exec(content)) !== null) {
|
|
50
|
+
const prefix = match[1];
|
|
51
|
+
const arrayMatches = content.matchAll(/\[([\s\S]*?)\]/g);
|
|
52
|
+
for (const arrayMatch of arrayMatches) {
|
|
53
|
+
const inner = arrayMatch[1];
|
|
54
|
+
const idMatches = inner.matchAll(/['"`]([a-z0-9_]{2,40})['"`]/g);
|
|
55
|
+
for (const idMatch of idMatches) {
|
|
56
|
+
const id = idMatch[1];
|
|
57
|
+
if (IGNORED_LAYOUT_VALS.has(id.toLowerCase())) continue;
|
|
58
|
+
if (/^[0-9]+$/.test(id)) continue;
|
|
38
59
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
while ((match = tRegex.exec(content)) !== null) {
|
|
43
|
-
if (!match[1].includes('${')) keys.add(match[1]);
|
|
60
|
+
const dynamicKey = `${prefix}.${id}`;
|
|
61
|
+
if (!keyMap.has(dynamicKey)) {
|
|
62
|
+
keyMap.set(dynamicKey, dynamicKey);
|
|
44
63
|
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
45
68
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
keys.add(`scenario.${val}.tip`);
|
|
54
|
-
}
|
|
55
|
-
});
|
|
69
|
+
function walkDirectory(dir, keyMap, skipDirs = ['node_modules', '.expo', '.git', 'build', 'ios', 'android', 'assets', 'locales', '__tests__']) {
|
|
70
|
+
if (!fs.existsSync(dir)) return;
|
|
71
|
+
|
|
72
|
+
const files = fs.readdirSync(dir);
|
|
73
|
+
for (const file of files) {
|
|
74
|
+
const fullPath = path.join(dir, file);
|
|
75
|
+
const stat = fs.statSync(fullPath);
|
|
56
76
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
while ((match = dotRegex.exec(content)) !== null) {
|
|
61
|
-
const key = match[1];
|
|
62
|
-
const isFile = /\.(ts|tsx|js|jsx|png|jpg|jpeg|svg|json)$/i.test(key);
|
|
63
|
-
if (!isFile && !key.includes(' ') && !key.includes('${')) {
|
|
64
|
-
keys.add(key);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
77
|
+
if (stat.isDirectory()) {
|
|
78
|
+
if (!skipDirs.includes(file)) {
|
|
79
|
+
walkDirectory(fullPath, keyMap, skipDirs);
|
|
67
80
|
}
|
|
81
|
+
} else if (/\.(ts|tsx|js|jsx)$/.test(file)) {
|
|
82
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
83
|
+
extractFromFile(content, keyMap);
|
|
68
84
|
}
|
|
69
85
|
}
|
|
86
|
+
}
|
|
70
87
|
|
|
71
|
-
|
|
72
|
-
|
|
88
|
+
export function extractUsedKeys(srcDir) {
|
|
89
|
+
const keyMap = new Map();
|
|
90
|
+
if (!srcDir) return keyMap;
|
|
91
|
+
|
|
92
|
+
const projectRoot = process.cwd();
|
|
93
|
+
const absoluteSrcDir = path.resolve(projectRoot, srcDir);
|
|
94
|
+
|
|
95
|
+
// Scan project source
|
|
96
|
+
walkDirectory(absoluteSrcDir, keyMap);
|
|
97
|
+
|
|
98
|
+
// Scan @umituz packages for shared keys
|
|
99
|
+
const packagesDir = path.resolve(projectRoot, 'node_modules/@umituz');
|
|
100
|
+
if (fs.existsSync(packagesDir)) {
|
|
101
|
+
walkDirectory(packagesDir, keyMap);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return keyMap;
|
|
73
105
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Handles call to translation APIs
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { getTargetLanguage,
|
|
6
|
+
import { getTargetLanguage, shouldSkipWord } from './translation-config.js';
|
|
7
7
|
|
|
8
8
|
let lastCallTime = 0;
|
|
9
9
|
const MIN_DELAY = 100; // ms
|
|
@@ -28,7 +28,6 @@ async function translateText(text, targetLang) {
|
|
|
28
28
|
const data = await response.json();
|
|
29
29
|
return data && data[0] && data[0][0] && data[0][0][0] ? data[0][0][0] : text;
|
|
30
30
|
} catch (error) {
|
|
31
|
-
if (__DEV__) console.error(` ❌ Translation error for "${text}":`, error.message);
|
|
32
31
|
return text;
|
|
33
32
|
}
|
|
34
33
|
}
|
|
@@ -37,15 +36,15 @@ function needsTranslation(value, enValue) {
|
|
|
37
36
|
if (typeof enValue !== 'string' || !enValue.trim()) return false;
|
|
38
37
|
if (shouldSkipWord(enValue)) return false;
|
|
39
38
|
|
|
40
|
-
// If
|
|
39
|
+
// CRITICAL OPTIMIZATION: If enValue is a technical key (e.g. "scenario.xxx.title"),
|
|
40
|
+
// skip translating it to other languages. We only translate REAL English content.
|
|
41
|
+
const isTechnicalKey = enValue.includes('.') && !enValue.includes(' ');
|
|
42
|
+
if (isTechnicalKey) return false;
|
|
43
|
+
|
|
44
|
+
// If value is missing or same as English, it needs translation
|
|
41
45
|
if (!value || typeof value !== 'string') return true;
|
|
42
46
|
|
|
43
|
-
// Heuristic: If English value looks like a technical key (e.g. "scenario.xxx.title")
|
|
44
|
-
// and the target value is exactly the same, it definitely needs translation.
|
|
45
|
-
const isTechnicalKey = enValue.includes('.') && !enValue.includes(' ');
|
|
46
|
-
|
|
47
47
|
if (value === enValue) {
|
|
48
|
-
if (isTechnicalKey) return true;
|
|
49
48
|
const isSingleWord = !enValue.includes(' ') && enValue.length < 20;
|
|
50
49
|
return !isSingleWord;
|
|
51
50
|
}
|
|
@@ -53,9 +52,11 @@ function needsTranslation(value, enValue) {
|
|
|
53
52
|
return false;
|
|
54
53
|
}
|
|
55
54
|
|
|
56
|
-
export async function translateObject(enObj, targetObj, targetLang, path = '', stats = { count: 0,
|
|
55
|
+
export async function translateObject(enObj, targetObj, targetLang, path = '', stats = { count: 0, checked: 0, translatedKeys: [] }) {
|
|
57
56
|
const keys = Object.keys(enObj);
|
|
58
57
|
|
|
58
|
+
if (!stats.translatedKeys) stats.translatedKeys = [];
|
|
59
|
+
|
|
59
60
|
for (const key of keys) {
|
|
60
61
|
const enValue = enObj[key];
|
|
61
62
|
const targetValue = targetObj[key];
|
|
@@ -64,17 +65,18 @@ export async function translateObject(enObj, targetObj, targetLang, path = '', s
|
|
|
64
65
|
if (typeof enValue === 'object' && enValue !== null) {
|
|
65
66
|
if (!targetObj[key] || typeof targetObj[key] !== 'object') targetObj[key] = {};
|
|
66
67
|
await translateObject(enValue, targetObj[key], targetLang, currentPath, stats);
|
|
67
|
-
} else if (typeof enValue === 'string'
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
68
|
+
} else if (typeof enValue === 'string') {
|
|
69
|
+
stats.checked++;
|
|
70
|
+
if (needsTranslation(targetValue, enValue)) {
|
|
71
|
+
// Show progress for translations
|
|
72
|
+
process.stdout.write(` \r Progress: ${stats.checked} keys checked, ${stats.count} translated...`);
|
|
73
|
+
|
|
74
|
+
const translated = await translateText(enValue, targetLang);
|
|
75
|
+
if (translated !== enValue) {
|
|
76
|
+
targetObj[key] = translated;
|
|
77
|
+
stats.count++;
|
|
78
|
+
stats.translatedKeys.push({ key: currentPath, from: enValue, to: translated });
|
|
79
|
+
}
|
|
78
80
|
}
|
|
79
81
|
}
|
|
80
82
|
}
|