@umituz/react-native-localization 3.7.10 → 3.7.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-localization",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.12",
|
|
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",
|
|
@@ -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
|
|
|
@@ -3,41 +3,152 @@ import path from 'path';
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Key Extractor
|
|
6
|
-
* Scans source code for i18n keys
|
|
6
|
+
* Scans source code and dependencies for i18n keys and their default values
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
function beautify(key) {
|
|
10
|
+
const parts = key.split('.');
|
|
11
|
+
const lastPart = parts[parts.length - 1];
|
|
12
|
+
return lastPart
|
|
13
|
+
.replace(/[_-]/g, ' ')
|
|
14
|
+
.replace(/([A-Z])/g, ' $1')
|
|
15
|
+
.replace(/^./, str => str.toUpperCase())
|
|
16
|
+
.trim();
|
|
17
|
+
}
|
|
18
|
+
|
|
9
19
|
export function extractUsedKeys(srcDir) {
|
|
10
|
-
const
|
|
11
|
-
if (!srcDir) return
|
|
20
|
+
const keyMap = new Map();
|
|
21
|
+
if (!srcDir) return keyMap;
|
|
12
22
|
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
23
|
+
const projectRoot = process.cwd();
|
|
24
|
+
const absoluteSrcDir = path.resolve(projectRoot, srcDir);
|
|
25
|
+
|
|
26
|
+
// 1. PROJECT SPECIFIC: Read Scenarios and Categories
|
|
27
|
+
const enumFiles = [
|
|
28
|
+
{ name: 'domains/scenarios/domain/Scenario.ts', type: 'scenario' },
|
|
29
|
+
{ name: 'domains/scenarios/domain/CategoryHierarchy.ts', type: 'category' }
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
enumFiles.forEach(cfg => {
|
|
33
|
+
const fullPath = path.resolve(absoluteSrcDir, cfg.name);
|
|
34
|
+
if (fs.existsSync(fullPath)) {
|
|
35
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
36
|
+
|
|
37
|
+
if (cfg.type === 'scenario') {
|
|
38
|
+
const matches = content.matchAll(/([A-Z0-9_]+)\s*=\s*['"`]([a-z0-9_]+)['"`]/g);
|
|
39
|
+
for (const m of matches) {
|
|
40
|
+
const val = m[2];
|
|
41
|
+
const label = beautify(val);
|
|
42
|
+
keyMap.set(`scenario.${val}.title`, label);
|
|
43
|
+
keyMap.set(`scenario.${val}.description`, label);
|
|
44
|
+
keyMap.set(`scenario.${val}.details`, label);
|
|
45
|
+
keyMap.set(`scenario.${val}.tip`, label);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (cfg.type === 'category') {
|
|
50
|
+
const blockRegex = /\{[\s\S]*?id:\s*(?:MainCategory|SubCategory)\.([A-Z0-9_]+)[\s\S]*?title:\s*['"`](.*?)['"`][\s\S]*?description:\s*['"`](.*?)['"`]/g;
|
|
51
|
+
let blockMatch;
|
|
52
|
+
while ((blockMatch = blockRegex.exec(content)) !== null) {
|
|
53
|
+
const enumName = blockMatch[1];
|
|
54
|
+
const title = blockMatch[2];
|
|
55
|
+
const desc = blockMatch[3];
|
|
56
|
+
|
|
57
|
+
const valRegex = new RegExp(`${enumName}\\s*=\\s*['"\`]([a-z0-9_]+)['"\`]`, 'i');
|
|
58
|
+
const valMatch = content.match(valRegex);
|
|
59
|
+
const stringVal = valMatch ? valMatch[1] : enumName.toLowerCase();
|
|
60
|
+
|
|
61
|
+
keyMap.set(`scenario.main_category.${stringVal}.title`, title);
|
|
62
|
+
keyMap.set(`scenario.main_category.${stringVal}.description`, desc);
|
|
63
|
+
keyMap.set(`scenario.sub_category.${stringVal}.title`, title);
|
|
64
|
+
keyMap.set(`scenario.sub_category.${stringVal}.description`, desc);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// 2. Scan directories
|
|
71
|
+
const scanDirs = [
|
|
72
|
+
absoluteSrcDir,
|
|
73
|
+
path.resolve(projectRoot, 'node_modules/@umituz')
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
const IGNORED_DOMAINS = ['.com', '.org', '.net', '.io', '.co', '.app', '.ai', '.gov', '.edu'];
|
|
77
|
+
const IGNORED_EXTENSIONS = [
|
|
78
|
+
'.ts', '.tsx', '.js', '.jsx', '.json', '.yaml', '.yml',
|
|
79
|
+
'.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.pdf',
|
|
80
|
+
'.mp4', '.mov', '.avi', '.mp3', '.wav', '.css', '.scss', '.md'
|
|
81
|
+
];
|
|
82
|
+
const IGNORED_LAYOUT_VALS = new Set([
|
|
83
|
+
'center', 'row', 'column', 'flex', 'absolute', 'relative', 'hidden', 'visible',
|
|
84
|
+
'transparent', 'bold', 'normal', 'italic', 'contain', 'cover', 'stretch',
|
|
85
|
+
'top', 'bottom', 'left', 'right', 'middle', 'auto', 'none', 'underline',
|
|
86
|
+
'capitalize', 'uppercase', 'lowercase', 'solid', 'dotted', 'dashed', 'wrap',
|
|
87
|
+
'nowrap', 'space-between', 'space-around', 'flex-start', 'flex-end', 'baseline',
|
|
88
|
+
'react', 'index', 'default', 'string', 'number', 'boolean', 'key', 'id'
|
|
89
|
+
]);
|
|
17
90
|
|
|
18
91
|
function walk(dir) {
|
|
92
|
+
if (!fs.existsSync(dir)) return;
|
|
19
93
|
const files = fs.readdirSync(dir);
|
|
20
94
|
for (const file of files) {
|
|
21
95
|
const fullPath = path.join(dir, file);
|
|
22
96
|
const stat = fs.statSync(fullPath);
|
|
23
97
|
|
|
24
98
|
if (stat.isDirectory()) {
|
|
25
|
-
const skipDirs = ['node_modules', '.expo', '.git', 'build', 'ios', 'android', 'assets'];
|
|
26
|
-
if (!skipDirs.includes(file))
|
|
27
|
-
walk(fullPath);
|
|
28
|
-
}
|
|
99
|
+
const skipDirs = ['node_modules', '.expo', '.git', 'build', 'ios', 'android', 'assets', 'locales', '__tests__'];
|
|
100
|
+
if (!skipDirs.includes(file)) walk(fullPath);
|
|
29
101
|
} else if (/\.(ts|tsx|js|jsx)$/.test(file)) {
|
|
30
102
|
const content = fs.readFileSync(fullPath, 'utf8');
|
|
31
|
-
|
|
32
|
-
|
|
103
|
+
|
|
104
|
+
// Pattern 1: t('key')
|
|
105
|
+
const tRegex = /(?:^|\W)t\(['"`]([^'"`]+)['"`]\)/g;
|
|
33
106
|
let match;
|
|
34
|
-
while ((match =
|
|
35
|
-
|
|
107
|
+
while ((match = tRegex.exec(content)) !== null) {
|
|
108
|
+
const key = match[1];
|
|
109
|
+
if (!key.includes('${') && !keyMap.has(key)) {
|
|
110
|
+
keyMap.set(key, beautify(key));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Pattern 2: Global Dot-Notation Strings (non-template)
|
|
115
|
+
const dotRegex = /['"`]([a-z][a-z0-9_]*\.(?:[a-z0-9_]+\.)+[a-z0-9_]+)['"`]/gi;
|
|
116
|
+
while ((match = dotRegex.exec(content)) !== null) {
|
|
117
|
+
const key = match[1];
|
|
118
|
+
const isIgnoredDomain = IGNORED_DOMAINS.some(ext => key.toLowerCase().endsWith(ext));
|
|
119
|
+
const isIgnoredExt = IGNORED_EXTENSIONS.some(ext => key.toLowerCase().endsWith(ext));
|
|
120
|
+
if (!isIgnoredDomain && !isIgnoredExt && !key.includes(' ') && !key.includes('${') && !keyMap.has(key)) {
|
|
121
|
+
keyMap.set(key, beautify(key));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Pattern 3: Template Literals t(`prefix.${var}`)
|
|
126
|
+
const templateRegex = /t\(\`([a-z0-9_.]*?)\.\$\{/g;
|
|
127
|
+
while ((match = templateRegex.exec(content)) !== null) {
|
|
128
|
+
const prefix = match[1];
|
|
129
|
+
// Find potential option IDs in the same file
|
|
130
|
+
// We look specifically for strings in arrays [...] to reduce false positives
|
|
131
|
+
const arrayMatches = content.matchAll(/\[([\s\S]*?)\]/g);
|
|
132
|
+
for (const arrayMatch of arrayMatches) {
|
|
133
|
+
const inner = arrayMatch[1];
|
|
134
|
+
const idMatches = inner.matchAll(/['"`]([a-z0-9_]{2,40})['"`]/g);
|
|
135
|
+
for (const idMatch of idMatches) {
|
|
136
|
+
const id = idMatch[1];
|
|
137
|
+
if (IGNORED_LAYOUT_VALS.has(id.toLowerCase())) continue;
|
|
138
|
+
if (/^[0-9]+$/.test(id)) continue; // Skip numeric-only strings
|
|
139
|
+
|
|
140
|
+
const dynamicKey = `${prefix}.${id}`;
|
|
141
|
+
if (!keyMap.has(dynamicKey)) {
|
|
142
|
+
keyMap.set(dynamicKey, beautify(id));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
36
146
|
}
|
|
37
147
|
}
|
|
38
148
|
}
|
|
39
149
|
}
|
|
40
150
|
|
|
41
|
-
walk(
|
|
42
|
-
|
|
151
|
+
scanDirs.forEach(dir => walk(dir));
|
|
152
|
+
|
|
153
|
+
return keyMap;
|
|
43
154
|
}
|
|
@@ -1,53 +1,47 @@
|
|
|
1
|
-
import https from 'https';
|
|
2
|
-
import { shouldSkipWord } from './translation-config.js';
|
|
3
|
-
|
|
4
1
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* Translation Utilities
|
|
3
|
+
* Handles call to translation APIs
|
|
7
4
|
*/
|
|
8
5
|
|
|
9
|
-
|
|
10
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
11
|
-
}
|
|
6
|
+
import { getTargetLanguage, shouldSkipWord } from './translation-config.js';
|
|
12
7
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
8
|
+
let lastCallTime = 0;
|
|
9
|
+
const MIN_DELAY = 100; // ms
|
|
10
|
+
|
|
11
|
+
async function translateText(text, targetLang) {
|
|
12
|
+
if (!text || typeof text !== 'string') return text;
|
|
13
|
+
if (shouldSkipWord(text)) return text;
|
|
19
14
|
|
|
15
|
+
// Rate limiting
|
|
16
|
+
const now = Date.now();
|
|
17
|
+
const waitTime = Math.max(0, MIN_DELAY - (now - lastCallTime));
|
|
18
|
+
if (waitTime > 0) await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
19
|
+
lastCallTime = Date.now();
|
|
20
|
+
|
|
21
|
+
try {
|
|
20
22
|
const encodedText = encodeURIComponent(text);
|
|
21
23
|
const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=en&tl=${targetLang}&dt=t&q=${encodedText}`;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const parsed = JSON.parse(data);
|
|
32
|
-
const translated = parsed[0]
|
|
33
|
-
.map(item => item[0])
|
|
34
|
-
.join('')
|
|
35
|
-
.trim();
|
|
36
|
-
resolve(translated || text);
|
|
37
|
-
} catch (error) {
|
|
38
|
-
resolve(text);
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
})
|
|
42
|
-
.on('error', () => {
|
|
43
|
-
resolve(text);
|
|
44
|
-
});
|
|
45
|
-
});
|
|
24
|
+
|
|
25
|
+
const response = await fetch(url);
|
|
26
|
+
if (!response.ok) return text;
|
|
27
|
+
|
|
28
|
+
const data = await response.json();
|
|
29
|
+
return data && data[0] && data[0][0] && data[0][0][0] ? data[0][0][0] : text;
|
|
30
|
+
} catch (error) {
|
|
31
|
+
return text;
|
|
32
|
+
}
|
|
46
33
|
}
|
|
47
34
|
|
|
48
35
|
function needsTranslation(value, enValue) {
|
|
49
|
-
if (typeof enValue !== 'string') return false;
|
|
36
|
+
if (typeof enValue !== 'string' || !enValue.trim()) return false;
|
|
50
37
|
if (shouldSkipWord(enValue)) return false;
|
|
38
|
+
|
|
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
|
|
51
45
|
if (!value || typeof value !== 'string') return true;
|
|
52
46
|
|
|
53
47
|
if (value === enValue) {
|
|
@@ -58,40 +52,32 @@ function needsTranslation(value, enValue) {
|
|
|
58
52
|
return false;
|
|
59
53
|
}
|
|
60
54
|
|
|
61
|
-
export async function translateObject(enObj, targetObj, targetLang, path = '', stats = { count: 0,
|
|
62
|
-
|
|
63
|
-
|
|
55
|
+
export async function translateObject(enObj, targetObj, targetLang, path = '', stats = { count: 0, checked: 0, translatedKeys: [] }) {
|
|
56
|
+
const keys = Object.keys(enObj);
|
|
57
|
+
|
|
58
|
+
if (!stats.translatedKeys) stats.translatedKeys = [];
|
|
59
|
+
|
|
60
|
+
for (const key of keys) {
|
|
64
61
|
const enValue = enObj[key];
|
|
65
62
|
const targetValue = targetObj[key];
|
|
63
|
+
const currentPath = path ? `${path}.${key}` : key;
|
|
66
64
|
|
|
67
|
-
if (
|
|
68
|
-
if (!Array.isArray(targetValue)) targetObj[key] = [];
|
|
69
|
-
for (let i = 0; i < enValue.length; i++) {
|
|
70
|
-
if (typeof enValue[i] === 'string' && needsTranslation(targetObj[key][i], enValue[i])) {
|
|
71
|
-
const translated = await translateText(enValue[i], targetLang);
|
|
72
|
-
if (translated !== enValue[i]) {
|
|
73
|
-
const isNewKey = targetObj[key][i] === enValue[i];
|
|
74
|
-
console.log(` ${isNewKey ? '🆕 NEW' : '🔄'} ${currentPath}[${i}]: "${enValue[i].substring(0, 40)}"`);
|
|
75
|
-
targetObj[key][i] = translated;
|
|
76
|
-
stats.count++;
|
|
77
|
-
if (isNewKey) stats.newKeys.push(`${currentPath}[${i}]`);
|
|
78
|
-
}
|
|
79
|
-
await delay(200);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
} else if (typeof enValue === 'object' && enValue !== null) {
|
|
65
|
+
if (typeof enValue === 'object' && enValue !== null) {
|
|
83
66
|
if (!targetObj[key] || typeof targetObj[key] !== 'object') targetObj[key] = {};
|
|
84
67
|
await translateObject(enValue, targetObj[key], targetLang, currentPath, stats);
|
|
85
|
-
} else if (typeof enValue === 'string'
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (
|
|
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
|
+
}
|
|
93
80
|
}
|
|
94
|
-
await delay(200);
|
|
95
81
|
}
|
|
96
82
|
}
|
|
97
83
|
}
|