@umituz/react-native-localization 3.7.11 → 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.11",
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,71 +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;
22
+
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
+ ];
12
31
 
13
- const absoluteSrcDir = path.resolve(process.cwd(), srcDir);
14
- if (!fs.existsSync(absoluteSrcDir)) return keys;
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();
15
60
 
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]);
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
+ }
24
67
  }
25
- }
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
+ ]);
26
90
 
27
91
  function walk(dir) {
92
+ if (!fs.existsSync(dir)) return;
28
93
  const files = fs.readdirSync(dir);
29
94
  for (const file of files) {
30
95
  const fullPath = path.join(dir, file);
31
96
  const stat = fs.statSync(fullPath);
32
97
 
33
98
  if (stat.isDirectory()) {
34
- const skipDirs = ['node_modules', '.expo', '.git', 'build', 'ios', 'android', 'assets'];
99
+ const skipDirs = ['node_modules', '.expo', '.git', 'build', 'ios', 'android', 'assets', 'locales', '__tests__'];
35
100
  if (!skipDirs.includes(file)) walk(fullPath);
36
101
  } else if (/\.(ts|tsx|js|jsx)$/.test(file)) {
37
102
  const content = fs.readFileSync(fullPath, 'utf8');
38
103
 
39
- // Pattern 1: Standard t('key') calls
104
+ // Pattern 1: t('key')
40
105
  const tRegex = /(?:^|\W)t\(['"`]([^'"`]+)['"`]\)/g;
41
106
  let match;
42
107
  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`);
108
+ const key = match[1];
109
+ if (!key.includes('${') && !keyMap.has(key)) {
110
+ keyMap.set(key, beautify(key));
54
111
  }
55
- });
112
+ }
56
113
 
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;
114
+ // Pattern 2: Global Dot-Notation Strings (non-template)
115
+ const dotRegex = /['"`]([a-z][a-z0-9_]*\.(?:[a-z0-9_]+\.)+[a-z0-9_]+)['"`]/gi;
60
116
  while ((match = dotRegex.exec(content)) !== null) {
61
117
  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);
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
+ }
65
145
  }
66
146
  }
67
147
  }
68
148
  }
69
149
  }
70
150
 
71
- walk(absoluteSrcDir);
72
- return keys;
151
+ scanDirs.forEach(dir => walk(dir));
152
+
153
+ return keyMap;
73
154
  }
@@ -3,7 +3,7 @@
3
3
  * Handles call to translation APIs
4
4
  */
5
5
 
6
- import { getTargetLanguage, isSingleWord as checkSingleWord, shouldSkipWord } from './translation-config.js';
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 value is missing or same as technical key
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, newKeys: [] }) {
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' && needsTranslation(targetValue, enValue)) {
68
- const translated = await translateText(enValue, targetLang);
69
-
70
- const isNewKey = targetValue === undefined;
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)) {
76
- targetObj[key] = translated;
77
- stats.count++;
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
  }