@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.10",
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 usedKeys = extractUsedKeys(srcDir);
50
- console.log(` Found ${usedKeys.size} unique keys in code.`);
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 enUS = parseTypeScriptFile(enUSPath);
51
+ const oldEnUS = parseTypeScriptFile(enUSPath);
52
+ const newEnUS = {};
53
+
53
54
  let addedCount = 0;
54
- for (const key of usedKeys) {
55
- if (setDeep(enUS, key, key)) addedCount++;
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
- if (addedCount > 0) {
59
- console.log(` ✨ Added ${addedCount} new keys to en-US.ts`);
60
- const content = generateTypeScriptContent(enUS, 'en-US');
61
- fs.writeFileSync(enUSPath, content);
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
- * Translates missing strings from en-US.ts to all other language files
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 translateLanguageFile(enUSPath, targetPath, langCode) {
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
- console.log('🚀 Starting integrated translation workflow...\n');
20
+ // Integrated Workflow: Ensure setup and sync
21
+ const skipSync = process.argv.includes('--no-sync');
60
22
 
61
- // 1. Ensure setup exists (index.ts)
62
- if (!fs.existsSync(indexPath)) {
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
- // 2. Synchronize keys (includes code scanning if srcDir is provided)
69
- console.log('🔄 Checking synchronization...');
70
- syncTranslations(targetDir, srcDir);
71
- console.log('');
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(`📊 Languages to translate: ${files.length}\n`);
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
- console.log(`🌍 Translating ${langCode} (${getLangDisplayName(langCode)})...`);
92
- const stats = await translateLanguageFile(enUSPath, targetPath, langCode);
93
- totalTranslated += stats.count;
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
- console.log(` ✅ Translated ${stats.count} strings`);
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(` Already complete`);
77
+ console.log(` Already up to date.`);
98
78
  }
99
79
  }
100
80
 
101
- console.log(`\n✅ Workflow completed! (Total translated: ${totalTranslated})`);
81
+ console.log('\n✅ All translations completed!');
102
82
  }
103
83
 
104
- main().catch(error => {
105
- console.error('❌ Translation failed:', error.message);
106
- process.exit(1);
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
- throw new Error(`Failed to parse object in ${filePath}: ${error.message}`);
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 keys = new Set();
11
- if (!srcDir) return keys;
20
+ const keyMap = new Map();
21
+ if (!srcDir) return keyMap;
12
22
 
13
- const absoluteSrcDir = path.resolve(process.cwd(), srcDir);
14
- if (!fs.existsSync(absoluteSrcDir)) {
15
- return keys;
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
- // Regex for t('key') or t("key") or i18n.t('key')
32
- const regex = /(?:^|\W)t\(['"]([^'"]+)['"]\)/g;
103
+
104
+ // Pattern 1: t('key')
105
+ const tRegex = /(?:^|\W)t\(['"`]([^'"`]+)['"`]\)/g;
33
106
  let match;
34
- while ((match = regex.exec(content)) !== null) {
35
- keys.add(match[1]);
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(absoluteSrcDir);
42
- return keys;
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
- * Translator
6
- * Google Translate API integration and translation logic
2
+ * Translation Utilities
3
+ * Handles call to translation APIs
7
4
  */
8
5
 
9
- export function delay(ms) {
10
- return new Promise(resolve => setTimeout(resolve, ms));
11
- }
6
+ import { getTargetLanguage, shouldSkipWord } from './translation-config.js';
12
7
 
13
- export async function translateText(text, targetLang) {
14
- return new Promise((resolve) => {
15
- if (shouldSkipWord(text)) {
16
- resolve(text);
17
- return;
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
- https
24
- .get(url, res => {
25
- let data = '';
26
- res.on('data', chunk => {
27
- data += chunk;
28
- });
29
- res.on('end', () => {
30
- try {
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, newKeys: [] }) {
62
- for (const key in enObj) {
63
- const currentPath = path ? `${path}.${key}` : key;
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 (Array.isArray(enValue)) {
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' && needsTranslation(targetValue, enValue)) {
86
- const translated = await translateText(enValue, targetLang);
87
- const isNewKey = targetValue === undefined;
88
- if (translated !== enValue || isNewKey) {
89
- console.log(` ${isNewKey ? '🆕 NEW' : '🔄'} ${currentPath}: "${enValue.substring(0, 40)}"`);
90
- targetObj[key] = translated;
91
- stats.count++;
92
- if (isNewKey) stats.newKeys.push(currentPath);
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
  }