@umituz/react-native-localization 3.7.9 ā 3.7.11
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
|
@@ -9,14 +9,12 @@
|
|
|
9
9
|
import fs from 'fs';
|
|
10
10
|
import path from 'path';
|
|
11
11
|
|
|
12
|
-
function
|
|
13
|
-
const targetDir = process.argv[2] || 'src/domains/localization/infrastructure/locales';
|
|
12
|
+
export function setupLanguages(targetDir) {
|
|
14
13
|
const localesDir = path.resolve(process.cwd(), targetDir);
|
|
15
14
|
|
|
16
|
-
console.log('š Setting up language files...\n');
|
|
17
15
|
if (!fs.existsSync(localesDir)) {
|
|
18
16
|
console.error(`ā Locales directory not found: ${localesDir}`);
|
|
19
|
-
|
|
17
|
+
return false;
|
|
20
18
|
}
|
|
21
19
|
|
|
22
20
|
const files = fs.readdirSync(localesDir)
|
|
@@ -52,6 +50,11 @@ export default translations;
|
|
|
52
50
|
|
|
53
51
|
fs.writeFileSync(path.join(localesDir, 'index.ts'), content);
|
|
54
52
|
console.log(`ā
Generated index.ts with ${files.length} languages`);
|
|
53
|
+
return true;
|
|
55
54
|
}
|
|
56
55
|
|
|
57
|
-
|
|
56
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
57
|
+
const targetDir = process.argv[2] || 'src/domains/localization/infrastructure/locales';
|
|
58
|
+
console.log('š Setting up language files...\n');
|
|
59
|
+
setupLanguages(targetDir);
|
|
60
|
+
}
|
|
@@ -62,16 +62,13 @@ function processExtraction(srcDir, enUSPath) {
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
function
|
|
66
|
-
const targetDir = process.argv[2] || 'src/domains/localization/infrastructure/locales';
|
|
67
|
-
const srcDir = process.argv[3];
|
|
65
|
+
export function syncTranslations(targetDir, srcDir) {
|
|
68
66
|
const localesDir = path.resolve(process.cwd(), targetDir);
|
|
69
67
|
const enUSPath = path.join(localesDir, 'en-US.ts');
|
|
70
68
|
|
|
71
|
-
console.log('š Starting translation synchronization...\n');
|
|
72
69
|
if (!fs.existsSync(localesDir) || !fs.existsSync(enUSPath)) {
|
|
73
70
|
console.error(`ā Localization files not found in: ${localesDir}`);
|
|
74
|
-
|
|
71
|
+
return false;
|
|
75
72
|
}
|
|
76
73
|
|
|
77
74
|
processExtraction(srcDir, enUSPath);
|
|
@@ -84,16 +81,19 @@ function main() {
|
|
|
84
81
|
files.forEach(file => {
|
|
85
82
|
const langCode = file.replace('.ts', '');
|
|
86
83
|
const targetPath = path.join(localesDir, file);
|
|
87
|
-
console.log(`š Syncing ${langCode} (${getLangDisplayName(langCode)})...`);
|
|
88
84
|
const result = syncLanguageFile(enUSPath, targetPath, langCode);
|
|
89
85
|
if (result.changed) {
|
|
90
|
-
console.log(` āļø +${result.added} keys, -${result.removed} keys`);
|
|
91
|
-
} else {
|
|
92
|
-
console.log(` ā
Already synchronized`);
|
|
86
|
+
console.log(` š ${langCode}: āļø +${result.added} keys, -${result.removed} keys`);
|
|
93
87
|
}
|
|
94
88
|
});
|
|
95
89
|
|
|
96
90
|
console.log(`\nā
Synchronization completed!`);
|
|
91
|
+
return true;
|
|
97
92
|
}
|
|
98
93
|
|
|
99
|
-
|
|
94
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
95
|
+
const targetDir = process.argv[2] || 'src/domains/localization/infrastructure/locales';
|
|
96
|
+
const srcDir = process.argv[3];
|
|
97
|
+
console.log('š Starting translation synchronization...\n');
|
|
98
|
+
syncTranslations(targetDir, srcDir);
|
|
99
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Translate Missing Script
|
|
5
5
|
* Translates missing strings from en-US.ts to all other language files
|
|
6
|
-
* Usage: node translate-missing.js [locales-dir] [lang-code-optional]
|
|
6
|
+
* Usage: node translate-missing.js [locales-dir] [src-dir-optional] [lang-code-optional]
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import fs from 'fs';
|
|
@@ -11,6 +11,8 @@ import path from 'path';
|
|
|
11
11
|
import { getTargetLanguage, isEnglishVariant, getLangDisplayName } from './utils/translation-config.js';
|
|
12
12
|
import { parseTypeScriptFile, generateTypeScriptContent } from './utils/file-parser.js';
|
|
13
13
|
import { translateObject } from './utils/translator.js';
|
|
14
|
+
import { setupLanguages } from './setup-languages.js';
|
|
15
|
+
import { syncTranslations } from './sync-translations.js';
|
|
14
16
|
|
|
15
17
|
async function translateLanguageFile(enUSPath, targetPath, langCode) {
|
|
16
18
|
const targetLang = getTargetLanguage(langCode);
|
|
@@ -47,11 +49,27 @@ async function translateLanguageFile(enUSPath, targetPath, langCode) {
|
|
|
47
49
|
|
|
48
50
|
async function main() {
|
|
49
51
|
const targetDir = process.argv[2] || 'src/domains/localization/infrastructure/locales';
|
|
50
|
-
const
|
|
52
|
+
const srcDir = process.argv[3];
|
|
53
|
+
const targetLangCode = process.argv[4];
|
|
54
|
+
|
|
51
55
|
const localesDir = path.resolve(process.cwd(), targetDir);
|
|
52
56
|
const enUSPath = path.join(localesDir, 'en-US.ts');
|
|
57
|
+
const indexPath = path.join(localesDir, 'index.ts');
|
|
58
|
+
|
|
59
|
+
console.log('š Starting integrated translation workflow...\n');
|
|
60
|
+
|
|
61
|
+
// 1. Ensure setup exists (index.ts)
|
|
62
|
+
if (!fs.existsSync(indexPath)) {
|
|
63
|
+
console.log('š¦ Setup (index.ts) missing. Generating...');
|
|
64
|
+
setupLanguages(targetDir);
|
|
65
|
+
console.log('');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 2. Synchronize keys (includes code scanning if srcDir is provided)
|
|
69
|
+
console.log('š Checking synchronization...');
|
|
70
|
+
syncTranslations(targetDir, srcDir);
|
|
71
|
+
console.log('');
|
|
53
72
|
|
|
54
|
-
console.log('š Starting automatic translation...\n');
|
|
55
73
|
if (!fs.existsSync(localesDir) || !fs.existsSync(enUSPath)) {
|
|
56
74
|
console.error(`ā Localization files not found in: ${localesDir}`);
|
|
57
75
|
process.exit(1);
|
|
@@ -80,7 +98,7 @@ async function main() {
|
|
|
80
98
|
}
|
|
81
99
|
}
|
|
82
100
|
|
|
83
|
-
console.log(`\nā
|
|
101
|
+
console.log(`\nā
Workflow completed! (Total translated: ${totalTranslated})`);
|
|
84
102
|
}
|
|
85
103
|
|
|
86
104
|
main().catch(error => {
|
|
@@ -11,8 +11,17 @@ export function extractUsedKeys(srcDir) {
|
|
|
11
11
|
if (!srcDir) return keys;
|
|
12
12
|
|
|
13
13
|
const absoluteSrcDir = path.resolve(process.cwd(), srcDir);
|
|
14
|
-
if (!fs.existsSync(absoluteSrcDir))
|
|
15
|
-
|
|
14
|
+
if (!fs.existsSync(absoluteSrcDir)) return keys;
|
|
15
|
+
|
|
16
|
+
// Step 1: Find all ScenarioId possible values once
|
|
17
|
+
const scenarioValues = new Set();
|
|
18
|
+
const scenarioFile = path.resolve(absoluteSrcDir, 'domains/scenarios/domain/Scenario.ts');
|
|
19
|
+
if (fs.existsSync(scenarioFile)) {
|
|
20
|
+
const content = fs.readFileSync(scenarioFile, 'utf8');
|
|
21
|
+
const matches = content.matchAll(/([A-Z0-9_]+)\s*=\s*['"`]([a-z0-9_]+)['"`]/g);
|
|
22
|
+
for (const match of matches) {
|
|
23
|
+
scenarioValues.add(match[2]);
|
|
24
|
+
}
|
|
16
25
|
}
|
|
17
26
|
|
|
18
27
|
function walk(dir) {
|
|
@@ -23,16 +32,37 @@ export function extractUsedKeys(srcDir) {
|
|
|
23
32
|
|
|
24
33
|
if (stat.isDirectory()) {
|
|
25
34
|
const skipDirs = ['node_modules', '.expo', '.git', 'build', 'ios', 'android', 'assets'];
|
|
26
|
-
if (!skipDirs.includes(file))
|
|
27
|
-
walk(fullPath);
|
|
28
|
-
}
|
|
35
|
+
if (!skipDirs.includes(file)) walk(fullPath);
|
|
29
36
|
} else if (/\.(ts|tsx|js|jsx)$/.test(file)) {
|
|
30
37
|
const content = fs.readFileSync(fullPath, 'utf8');
|
|
31
|
-
|
|
32
|
-
|
|
38
|
+
|
|
39
|
+
// Pattern 1: Standard t('key') calls
|
|
40
|
+
const tRegex = /(?:^|\W)t\(['"`]([^'"`]+)['"`]\)/g;
|
|
33
41
|
let match;
|
|
34
|
-
while ((match =
|
|
35
|
-
keys.add(match[1]);
|
|
42
|
+
while ((match = tRegex.exec(content)) !== null) {
|
|
43
|
+
if (!match[1].includes('${')) keys.add(match[1]);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Pattern 2: Scenario ID usage
|
|
47
|
+
// If we see ScenarioId.XXXX or just the string value matching a scenario
|
|
48
|
+
scenarioValues.forEach(val => {
|
|
49
|
+
if (content.includes(val)) {
|
|
50
|
+
keys.add(`scenario.${val}.title`);
|
|
51
|
+
keys.add(`scenario.${val}.description`);
|
|
52
|
+
keys.add(`scenario.${val}.details`);
|
|
53
|
+
keys.add(`scenario.${val}.tip`);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Pattern 3: Dot-notation keys in configs/literals (e.g. "common.button.save")
|
|
58
|
+
// We look for at least two segments (one dot)
|
|
59
|
+
const dotRegex = /['"`]([a-z][a-z0-9_]*\.[a-z0-9_.]*[a-z0-9_])['"`]/gi;
|
|
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
|
+
}
|
|
36
66
|
}
|
|
37
67
|
}
|
|
38
68
|
}
|
|
@@ -1,56 +1,51 @@
|
|
|
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, isSingleWord as checkSingleWord, shouldSkipWord } from './translation-config.js';
|
|
12
7
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
if (shouldSkipWord(text)) {
|
|
16
|
-
resolve(text);
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
8
|
+
let lastCallTime = 0;
|
|
9
|
+
const MIN_DELAY = 100; // ms
|
|
19
10
|
|
|
11
|
+
async function translateText(text, targetLang) {
|
|
12
|
+
if (!text || typeof text !== 'string') return text;
|
|
13
|
+
if (shouldSkipWord(text)) return text;
|
|
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
|
-
|
|
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
|
+
if (__DEV__) console.error(` ā Translation error for "${text}":`, error.message);
|
|
32
|
+
return text;
|
|
33
|
+
}
|
|
46
34
|
}
|
|
47
35
|
|
|
48
36
|
function needsTranslation(value, enValue) {
|
|
49
|
-
if (typeof enValue !== 'string') return false;
|
|
37
|
+
if (typeof enValue !== 'string' || !enValue.trim()) return false;
|
|
50
38
|
if (shouldSkipWord(enValue)) return false;
|
|
39
|
+
|
|
40
|
+
// If value is missing or same as technical key
|
|
51
41
|
if (!value || typeof value !== 'string') return true;
|
|
52
42
|
|
|
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
|
+
|
|
53
47
|
if (value === enValue) {
|
|
48
|
+
if (isTechnicalKey) return true;
|
|
54
49
|
const isSingleWord = !enValue.includes(' ') && enValue.length < 20;
|
|
55
50
|
return !isSingleWord;
|
|
56
51
|
}
|
|
@@ -59,39 +54,28 @@ function needsTranslation(value, enValue) {
|
|
|
59
54
|
}
|
|
60
55
|
|
|
61
56
|
export async function translateObject(enObj, targetObj, targetLang, path = '', stats = { count: 0, newKeys: [] }) {
|
|
62
|
-
|
|
63
|
-
|
|
57
|
+
const keys = Object.keys(enObj);
|
|
58
|
+
|
|
59
|
+
for (const key of keys) {
|
|
64
60
|
const enValue = enObj[key];
|
|
65
61
|
const targetValue = targetObj[key];
|
|
62
|
+
const currentPath = path ? `${path}.${key}` : key;
|
|
66
63
|
|
|
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) {
|
|
64
|
+
if (typeof enValue === 'object' && enValue !== null) {
|
|
83
65
|
if (!targetObj[key] || typeof targetObj[key] !== 'object') targetObj[key] = {};
|
|
84
66
|
await translateObject(enValue, targetObj[key], targetLang, currentPath, stats);
|
|
85
67
|
} else if (typeof enValue === 'string' && needsTranslation(targetValue, enValue)) {
|
|
86
68
|
const translated = await translateText(enValue, targetLang);
|
|
69
|
+
|
|
87
70
|
const isNewKey = targetValue === undefined;
|
|
88
|
-
if
|
|
89
|
-
|
|
71
|
+
// Force increment if it looks like a technical key that we just "translated"
|
|
72
|
+
// even if the API returned the same string (placeholder)
|
|
73
|
+
const isTechnicalKey = enValue.includes('.') && !enValue.includes(' ');
|
|
74
|
+
|
|
75
|
+
if (translated !== enValue || isNewKey || (isTechnicalKey && translated === enValue)) {
|
|
90
76
|
targetObj[key] = translated;
|
|
91
77
|
stats.count++;
|
|
92
|
-
if (isNewKey) stats.newKeys.push(currentPath);
|
|
93
78
|
}
|
|
94
|
-
await delay(200);
|
|
95
79
|
}
|
|
96
80
|
}
|
|
97
81
|
}
|