@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
@@ -47,8 +47,6 @@ This package follows Domain-Driven Design principles:
47
47
 
48
48
  ```bash
49
49
  npm install @umituz/react-native-localization
50
- # or
51
- yarn add @umituz/react-native-localization
52
50
  ```
53
51
 
54
52
  ### Peer Dependencies
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.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": "latest",
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 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
 
@@ -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
- export function extractUsedKeys(srcDir) {
10
- const keys = new Set();
11
- if (!srcDir) return keys;
12
-
13
- const absoluteSrcDir = path.resolve(process.cwd(), srcDir);
14
- if (!fs.existsSync(absoluteSrcDir)) return keys;
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
- // 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]);
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
- function walk(dir) {
28
- const files = fs.readdirSync(dir);
29
- for (const file of files) {
30
- const fullPath = path.join(dir, file);
31
- const stat = fs.statSync(fullPath);
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
- if (stat.isDirectory()) {
34
- const skipDirs = ['node_modules', '.expo', '.git', 'build', 'ios', 'android', 'assets'];
35
- if (!skipDirs.includes(file)) walk(fullPath);
36
- } else if (/\.(ts|tsx|js|jsx)$/.test(file)) {
37
- const content = fs.readFileSync(fullPath, 'utf8');
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
- // Pattern 1: Standard t('key') calls
40
- const tRegex = /(?:^|\W)t\(['"`]([^'"`]+)['"`]\)/g;
41
- let match;
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
- // 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
- });
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
- // 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
- }
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
- walk(absoluteSrcDir);
72
- return keys;
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, 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
  }