@umituz/react-native-localization 3.2.5 → 3.4.0
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 +6 -3
- package/scripts/sync-translations.js +277 -0
- package/scripts/translate-missing.js +393 -3
- package/src/infrastructure/hooks/useTranslation.ts +28 -66
- package/src/infrastructure/storage/LanguageSwitcher.ts +1 -1
- package/src/infrastructure/storage/LocalizationStore.ts +1 -5
- package/src/presentation/components/LanguageItem.tsx +41 -18
- package/src/presentation/components/LanguageSection.tsx +4 -1
- package/src/presentation/components/SearchInput.tsx +31 -14
- package/src/infrastructure/config/TranslationCache.ts +0 -31
- package/src/infrastructure/config/__tests__/TranslationCache.test.ts +0 -44
package/package.json
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-localization",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.4.0",
|
|
4
4
|
"description": "Generic localization system for React Native apps with i18n support",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"typecheck": "tsc --noEmit --skipLibCheck",
|
|
9
|
-
"lint": "tsc --noEmit",
|
|
9
|
+
"lint": "tsc --noEmit --skipLibCheck",
|
|
10
10
|
"test": "jest",
|
|
11
11
|
"test:watch": "jest --watch",
|
|
12
12
|
"test:coverage": "jest --coverage",
|
|
13
13
|
"prepublishOnly": "node scripts/prepublish.js",
|
|
14
14
|
"version:patch": "npm version patch -m 'chore: release v%s'",
|
|
15
15
|
"version:minor": "npm version minor -m 'chore: release v%s'",
|
|
16
|
-
"version:major": "npm version major -m 'chore: release v%s'"
|
|
16
|
+
"version:major": "npm version major -m 'chore: release v%s'",
|
|
17
|
+
"i18n:setup": "node scripts/setup-languages.js",
|
|
18
|
+
"i18n:sync": "node scripts/sync-translations.js",
|
|
19
|
+
"i18n:translate": "node scripts/translate-missing.js"
|
|
17
20
|
},
|
|
18
21
|
"keywords": [
|
|
19
22
|
"react-native",
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* eslint-disable no-console */
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Sync Translations Script
|
|
6
|
+
*
|
|
7
|
+
* Synchronizes translation keys from en-US.ts to all other language files.
|
|
8
|
+
* - Adds missing keys (with English values as placeholders)
|
|
9
|
+
* - Removes extra keys not in en-US
|
|
10
|
+
* - Maintains existing translations
|
|
11
|
+
*
|
|
12
|
+
* Usage: node scripts/sync-translations.js [locales-dir]
|
|
13
|
+
* Default: src/domains/localization/infrastructure/locales
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Parse TypeScript translation file and extract the object
|
|
21
|
+
*/
|
|
22
|
+
function parseTypeScriptFile(filePath) {
|
|
23
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
24
|
+
|
|
25
|
+
// Extract the object from "export default { ... };"
|
|
26
|
+
const match = content.match(/export\s+default\s+(\{[\s\S]*\});?\s*$/);
|
|
27
|
+
if (!match) {
|
|
28
|
+
throw new Error(`Could not parse TypeScript file: ${filePath}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const objectStr = match[1].replace(/;$/, '');
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
// eslint-disable-next-line no-eval
|
|
35
|
+
return eval(`(${objectStr})`);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
throw new Error(`Failed to parse object in ${filePath}: ${error.message}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Generate TypeScript file content from object
|
|
43
|
+
*/
|
|
44
|
+
function generateTypeScriptContent(obj, langCode) {
|
|
45
|
+
const langName = getLangDisplayName(langCode);
|
|
46
|
+
|
|
47
|
+
function stringifyValue(value, indent = 2) {
|
|
48
|
+
if (typeof value === 'string') {
|
|
49
|
+
const escaped = value
|
|
50
|
+
.replace(/\\/g, '\\\\')
|
|
51
|
+
.replace(/"/g, '\\"')
|
|
52
|
+
.replace(/\n/g, '\\n');
|
|
53
|
+
return `"${escaped}"`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (Array.isArray(value)) {
|
|
57
|
+
if (value.length === 0) return '[]';
|
|
58
|
+
const items = value.map(v => stringifyValue(v, indent + 2));
|
|
59
|
+
return `[${items.join(', ')}]`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (typeof value === 'object' && value !== null) {
|
|
63
|
+
const spaces = ' '.repeat(indent);
|
|
64
|
+
const innerSpaces = ' '.repeat(indent + 2);
|
|
65
|
+
const entries = Object.entries(value)
|
|
66
|
+
.map(([k, v]) => {
|
|
67
|
+
const key = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(k) ? k : `"${k}"`;
|
|
68
|
+
return `${innerSpaces}${key}: ${stringifyValue(v, indent + 2)}`;
|
|
69
|
+
})
|
|
70
|
+
.join(',\n');
|
|
71
|
+
return `{\n${entries},\n${spaces}}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return String(value);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const objString = stringifyValue(obj, 0);
|
|
78
|
+
|
|
79
|
+
return `/**
|
|
80
|
+
* ${langName} Translations
|
|
81
|
+
* Auto-synced from en-US.ts
|
|
82
|
+
*/
|
|
83
|
+
|
|
84
|
+
export default ${objString};
|
|
85
|
+
`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Get display name for language code
|
|
90
|
+
*/
|
|
91
|
+
function getLangDisplayName(code) {
|
|
92
|
+
const names = {
|
|
93
|
+
'ar-SA': 'Arabic (Saudi Arabia)',
|
|
94
|
+
'bg-BG': 'Bulgarian',
|
|
95
|
+
'cs-CZ': 'Czech',
|
|
96
|
+
'da-DK': 'Danish',
|
|
97
|
+
'de-DE': 'German',
|
|
98
|
+
'el-GR': 'Greek',
|
|
99
|
+
'en-AU': 'English (Australia)',
|
|
100
|
+
'en-CA': 'English (Canada)',
|
|
101
|
+
'en-GB': 'English (UK)',
|
|
102
|
+
'en-US': 'English (US)',
|
|
103
|
+
'es-ES': 'Spanish (Spain)',
|
|
104
|
+
'es-MX': 'Spanish (Mexico)',
|
|
105
|
+
'fi-FI': 'Finnish',
|
|
106
|
+
'fr-CA': 'French (Canada)',
|
|
107
|
+
'fr-FR': 'French (France)',
|
|
108
|
+
'hi-IN': 'Hindi',
|
|
109
|
+
'hr-HR': 'Croatian',
|
|
110
|
+
'hu-HU': 'Hungarian',
|
|
111
|
+
'id-ID': 'Indonesian',
|
|
112
|
+
'it-IT': 'Italian',
|
|
113
|
+
'ja-JP': 'Japanese',
|
|
114
|
+
'ko-KR': 'Korean',
|
|
115
|
+
'ms-MY': 'Malay',
|
|
116
|
+
'nl-NL': 'Dutch',
|
|
117
|
+
'no-NO': 'Norwegian',
|
|
118
|
+
'pl-PL': 'Polish',
|
|
119
|
+
'pt-BR': 'Portuguese (Brazil)',
|
|
120
|
+
'pt-PT': 'Portuguese (Portugal)',
|
|
121
|
+
'ro-RO': 'Romanian',
|
|
122
|
+
'ru-RU': 'Russian',
|
|
123
|
+
'sk-SK': 'Slovak',
|
|
124
|
+
'sv-SE': 'Swedish',
|
|
125
|
+
'th-TH': 'Thai',
|
|
126
|
+
'tl-PH': 'Tagalog',
|
|
127
|
+
'tr-TR': 'Turkish',
|
|
128
|
+
'uk-UA': 'Ukrainian',
|
|
129
|
+
'vi-VN': 'Vietnamese',
|
|
130
|
+
'zh-CN': 'Chinese (Simplified)',
|
|
131
|
+
'zh-TW': 'Chinese (Traditional)',
|
|
132
|
+
};
|
|
133
|
+
return names[code] || code;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Add missing keys from en-US to target
|
|
138
|
+
*/
|
|
139
|
+
function addMissingKeys(enObj, targetObj, stats = { added: 0 }) {
|
|
140
|
+
for (const key in enObj) {
|
|
141
|
+
if (!Object.prototype.hasOwnProperty.call(targetObj, key)) {
|
|
142
|
+
targetObj[key] = enObj[key];
|
|
143
|
+
stats.added++;
|
|
144
|
+
} else if (
|
|
145
|
+
typeof enObj[key] === 'object' &&
|
|
146
|
+
enObj[key] !== null &&
|
|
147
|
+
!Array.isArray(enObj[key])
|
|
148
|
+
) {
|
|
149
|
+
if (!targetObj[key] || typeof targetObj[key] !== 'object') {
|
|
150
|
+
targetObj[key] = {};
|
|
151
|
+
}
|
|
152
|
+
addMissingKeys(enObj[key], targetObj[key], stats);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return stats;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Remove extra keys not in en-US
|
|
160
|
+
*/
|
|
161
|
+
function removeExtraKeys(enObj, targetObj, stats = { removed: 0 }) {
|
|
162
|
+
for (const key in targetObj) {
|
|
163
|
+
if (!Object.prototype.hasOwnProperty.call(enObj, key)) {
|
|
164
|
+
delete targetObj[key];
|
|
165
|
+
stats.removed++;
|
|
166
|
+
} else if (
|
|
167
|
+
typeof enObj[key] === 'object' &&
|
|
168
|
+
enObj[key] !== null &&
|
|
169
|
+
!Array.isArray(enObj[key]) &&
|
|
170
|
+
typeof targetObj[key] === 'object' &&
|
|
171
|
+
targetObj[key] !== null &&
|
|
172
|
+
!Array.isArray(targetObj[key])
|
|
173
|
+
) {
|
|
174
|
+
removeExtraKeys(enObj[key], targetObj[key], stats);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return stats;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Sync a single language file
|
|
182
|
+
*/
|
|
183
|
+
function syncLanguageFile(enUSPath, targetPath, langCode) {
|
|
184
|
+
const enUS = parseTypeScriptFile(enUSPath);
|
|
185
|
+
let target;
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
target = parseTypeScriptFile(targetPath);
|
|
189
|
+
} catch {
|
|
190
|
+
target = {};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const addStats = { added: 0 };
|
|
194
|
+
const removeStats = { removed: 0 };
|
|
195
|
+
|
|
196
|
+
addMissingKeys(enUS, target, addStats);
|
|
197
|
+
removeExtraKeys(enUS, target, removeStats);
|
|
198
|
+
|
|
199
|
+
const changed = addStats.added > 0 || removeStats.removed > 0;
|
|
200
|
+
|
|
201
|
+
if (changed) {
|
|
202
|
+
const content = generateTypeScriptContent(target, langCode);
|
|
203
|
+
fs.writeFileSync(targetPath, content);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
added: addStats.added,
|
|
208
|
+
removed: removeStats.removed,
|
|
209
|
+
changed,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Main function
|
|
215
|
+
*/
|
|
216
|
+
function main() {
|
|
217
|
+
const targetDir = process.argv[2] || 'src/domains/localization/infrastructure/locales';
|
|
218
|
+
const localesDir = path.resolve(process.cwd(), targetDir);
|
|
219
|
+
|
|
220
|
+
console.log('🚀 Starting translation synchronization...\n');
|
|
221
|
+
console.log(`📂 Locales directory: ${localesDir}\n`);
|
|
222
|
+
|
|
223
|
+
if (!fs.existsSync(localesDir)) {
|
|
224
|
+
console.error(`❌ Locales directory not found: ${localesDir}`);
|
|
225
|
+
process.exit(1);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const enUSPath = path.join(localesDir, 'en-US.ts');
|
|
229
|
+
if (!fs.existsSync(enUSPath)) {
|
|
230
|
+
console.error(`❌ Base file not found: ${enUSPath}`);
|
|
231
|
+
process.exit(1);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Find all language files
|
|
235
|
+
const files = fs.readdirSync(localesDir)
|
|
236
|
+
.filter(f => f.match(/^[a-z]{2}-[A-Z]{2}\.ts$/) && f !== 'en-US.ts')
|
|
237
|
+
.sort();
|
|
238
|
+
|
|
239
|
+
console.log(`📊 Languages to sync: ${files.length}\n`);
|
|
240
|
+
|
|
241
|
+
let totalAdded = 0;
|
|
242
|
+
let totalRemoved = 0;
|
|
243
|
+
let totalChanged = 0;
|
|
244
|
+
|
|
245
|
+
for (const file of files) {
|
|
246
|
+
const langCode = file.replace('.ts', '');
|
|
247
|
+
const targetPath = path.join(localesDir, file);
|
|
248
|
+
|
|
249
|
+
console.log(`🌍 Syncing ${langCode}...`);
|
|
250
|
+
|
|
251
|
+
const result = syncLanguageFile(enUSPath, targetPath, langCode);
|
|
252
|
+
|
|
253
|
+
if (result.changed) {
|
|
254
|
+
console.log(` ✏️ +${result.added} keys, -${result.removed} keys`);
|
|
255
|
+
totalAdded += result.added;
|
|
256
|
+
totalRemoved += result.removed;
|
|
257
|
+
totalChanged++;
|
|
258
|
+
} else {
|
|
259
|
+
console.log(` ✅ Already synchronized`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
console.log(`\n📊 Summary:`);
|
|
264
|
+
console.log(` Languages processed: ${files.length}`);
|
|
265
|
+
console.log(` Files changed: ${totalChanged}`);
|
|
266
|
+
console.log(` Keys added: ${totalAdded}`);
|
|
267
|
+
console.log(` Keys removed: ${totalRemoved}`);
|
|
268
|
+
|
|
269
|
+
if (totalChanged > 0) {
|
|
270
|
+
console.log(`\n✅ Synchronization completed!`);
|
|
271
|
+
console.log(` Next: Run 'node scripts/translate-missing.js' to translate new keys`);
|
|
272
|
+
} else {
|
|
273
|
+
console.log(`\n✅ All languages were already synchronized!`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
main();
|
|
@@ -1,6 +1,396 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* eslint-disable no-console */
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Translate Missing Script
|
|
6
|
+
*
|
|
7
|
+
* Translates missing strings from en-US.ts to all other language files
|
|
8
|
+
* using Google Translate free API.
|
|
9
|
+
*
|
|
10
|
+
* Usage: node scripts/translate-missing.js [locales-dir]
|
|
11
|
+
* Default: src/domains/localization/infrastructure/locales
|
|
12
|
+
*/
|
|
13
|
+
|
|
1
14
|
const fs = require('fs');
|
|
2
15
|
const path = require('path');
|
|
16
|
+
const https = require('https');
|
|
17
|
+
|
|
18
|
+
// Language codes mapping for Google Translate API
|
|
19
|
+
const LANGUAGE_MAP = {
|
|
20
|
+
'ar-SA': 'ar', // Arabic
|
|
21
|
+
'bg-BG': 'bg', // Bulgarian
|
|
22
|
+
'cs-CZ': 'cs', // Czech
|
|
23
|
+
'da-DK': 'da', // Danish
|
|
24
|
+
'de-DE': 'de', // German
|
|
25
|
+
'el-GR': 'el', // Greek
|
|
26
|
+
'en-AU': 'en', // English (skip)
|
|
27
|
+
'en-CA': 'en', // English (skip)
|
|
28
|
+
'en-GB': 'en', // English (skip)
|
|
29
|
+
'es-ES': 'es', // Spanish
|
|
30
|
+
'es-MX': 'es', // Spanish
|
|
31
|
+
'fi-FI': 'fi', // Finnish
|
|
32
|
+
'fr-CA': 'fr', // French
|
|
33
|
+
'fr-FR': 'fr', // French
|
|
34
|
+
'hi-IN': 'hi', // Hindi
|
|
35
|
+
'hr-HR': 'hr', // Croatian
|
|
36
|
+
'hu-HU': 'hu', // Hungarian
|
|
37
|
+
'id-ID': 'id', // Indonesian
|
|
38
|
+
'it-IT': 'it', // Italian
|
|
39
|
+
'ja-JP': 'ja', // Japanese
|
|
40
|
+
'ko-KR': 'ko', // Korean
|
|
41
|
+
'ms-MY': 'ms', // Malay
|
|
42
|
+
'nl-NL': 'nl', // Dutch
|
|
43
|
+
'no-NO': 'no', // Norwegian
|
|
44
|
+
'pl-PL': 'pl', // Polish
|
|
45
|
+
'pt-BR': 'pt', // Portuguese
|
|
46
|
+
'pt-PT': 'pt', // Portuguese
|
|
47
|
+
'ro-RO': 'ro', // Romanian
|
|
48
|
+
'ru-RU': 'ru', // Russian
|
|
49
|
+
'sk-SK': 'sk', // Slovak
|
|
50
|
+
'sv-SE': 'sv', // Swedish
|
|
51
|
+
'th-TH': 'th', // Thai
|
|
52
|
+
'tl-PH': 'tl', // Tagalog
|
|
53
|
+
'tr-TR': 'tr', // Turkish
|
|
54
|
+
'uk-UA': 'uk', // Ukrainian
|
|
55
|
+
'vi-VN': 'vi', // Vietnamese
|
|
56
|
+
'zh-CN': 'zh-CN', // Chinese Simplified
|
|
57
|
+
'zh-TW': 'zh-TW', // Chinese Traditional
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Common English words that don't need translation
|
|
61
|
+
const SKIP_WORDS = new Set([
|
|
62
|
+
'OK',
|
|
63
|
+
'Email',
|
|
64
|
+
'Google',
|
|
65
|
+
'Apple',
|
|
66
|
+
'Facebook',
|
|
67
|
+
'Premium',
|
|
68
|
+
'Pro',
|
|
69
|
+
'Plus',
|
|
70
|
+
'BPM',
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Parse TypeScript translation file and extract the object
|
|
75
|
+
*/
|
|
76
|
+
function parseTypeScriptFile(filePath) {
|
|
77
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
78
|
+
|
|
79
|
+
// Extract the object from "export default { ... };"
|
|
80
|
+
const match = content.match(/export\s+default\s+(\{[\s\S]*\});?\s*$/);
|
|
81
|
+
if (!match) {
|
|
82
|
+
throw new Error(`Could not parse TypeScript file: ${filePath}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Use eval to parse the object (safe since we control the files)
|
|
86
|
+
// Remove trailing semicolon if present
|
|
87
|
+
const objectStr = match[1].replace(/;$/, '');
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
// eslint-disable-next-line no-eval
|
|
91
|
+
return eval(`(${objectStr})`);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
throw new Error(`Failed to parse object in ${filePath}: ${error.message}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Generate TypeScript file content from object
|
|
99
|
+
*/
|
|
100
|
+
function generateTypeScriptContent(obj, langCode) {
|
|
101
|
+
const langName = getLangDisplayName(langCode);
|
|
102
|
+
|
|
103
|
+
function stringifyValue(value, indent = 2) {
|
|
104
|
+
if (typeof value === 'string') {
|
|
105
|
+
// Escape quotes and handle multiline
|
|
106
|
+
const escaped = value
|
|
107
|
+
.replace(/\\/g, '\\\\')
|
|
108
|
+
.replace(/"/g, '\\"')
|
|
109
|
+
.replace(/\n/g, '\\n');
|
|
110
|
+
return `"${escaped}"`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (Array.isArray(value)) {
|
|
114
|
+
if (value.length === 0) return '[]';
|
|
115
|
+
const items = value.map(v => stringifyValue(v, indent + 2));
|
|
116
|
+
return `[${items.join(', ')}]`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (typeof value === 'object' && value !== null) {
|
|
120
|
+
const spaces = ' '.repeat(indent);
|
|
121
|
+
const innerSpaces = ' '.repeat(indent + 2);
|
|
122
|
+
const entries = Object.entries(value)
|
|
123
|
+
.map(([k, v]) => {
|
|
124
|
+
const key = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(k) ? k : `"${k}"`;
|
|
125
|
+
return `${innerSpaces}${key}: ${stringifyValue(v, indent + 2)}`;
|
|
126
|
+
})
|
|
127
|
+
.join(',\n');
|
|
128
|
+
return `{\n${entries},\n${spaces}}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return String(value);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const objString = stringifyValue(obj, 0);
|
|
135
|
+
|
|
136
|
+
return `/**
|
|
137
|
+
* ${langName} Translations
|
|
138
|
+
* Auto-translated from en-US.ts
|
|
139
|
+
*/
|
|
140
|
+
|
|
141
|
+
export default ${objString};
|
|
142
|
+
`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Get display name for language code
|
|
147
|
+
*/
|
|
148
|
+
function getLangDisplayName(code) {
|
|
149
|
+
const names = {
|
|
150
|
+
'ar-SA': 'Arabic (Saudi Arabia)',
|
|
151
|
+
'bg-BG': 'Bulgarian',
|
|
152
|
+
'cs-CZ': 'Czech',
|
|
153
|
+
'da-DK': 'Danish',
|
|
154
|
+
'de-DE': 'German',
|
|
155
|
+
'el-GR': 'Greek',
|
|
156
|
+
'en-AU': 'English (Australia)',
|
|
157
|
+
'en-CA': 'English (Canada)',
|
|
158
|
+
'en-GB': 'English (UK)',
|
|
159
|
+
'en-US': 'English (US)',
|
|
160
|
+
'es-ES': 'Spanish (Spain)',
|
|
161
|
+
'es-MX': 'Spanish (Mexico)',
|
|
162
|
+
'fi-FI': 'Finnish',
|
|
163
|
+
'fr-CA': 'French (Canada)',
|
|
164
|
+
'fr-FR': 'French (France)',
|
|
165
|
+
'hi-IN': 'Hindi',
|
|
166
|
+
'hr-HR': 'Croatian',
|
|
167
|
+
'hu-HU': 'Hungarian',
|
|
168
|
+
'id-ID': 'Indonesian',
|
|
169
|
+
'it-IT': 'Italian',
|
|
170
|
+
'ja-JP': 'Japanese',
|
|
171
|
+
'ko-KR': 'Korean',
|
|
172
|
+
'ms-MY': 'Malay',
|
|
173
|
+
'nl-NL': 'Dutch',
|
|
174
|
+
'no-NO': 'Norwegian',
|
|
175
|
+
'pl-PL': 'Polish',
|
|
176
|
+
'pt-BR': 'Portuguese (Brazil)',
|
|
177
|
+
'pt-PT': 'Portuguese (Portugal)',
|
|
178
|
+
'ro-RO': 'Romanian',
|
|
179
|
+
'ru-RU': 'Russian',
|
|
180
|
+
'sk-SK': 'Slovak',
|
|
181
|
+
'sv-SE': 'Swedish',
|
|
182
|
+
'th-TH': 'Thai',
|
|
183
|
+
'tl-PH': 'Tagalog',
|
|
184
|
+
'tr-TR': 'Turkish',
|
|
185
|
+
'uk-UA': 'Ukrainian',
|
|
186
|
+
'vi-VN': 'Vietnamese',
|
|
187
|
+
'zh-CN': 'Chinese (Simplified)',
|
|
188
|
+
'zh-TW': 'Chinese (Traditional)',
|
|
189
|
+
};
|
|
190
|
+
return names[code] || code;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Simple Google Translate API call using free endpoint
|
|
195
|
+
*/
|
|
196
|
+
async function translateText(text, targetLang) {
|
|
197
|
+
return new Promise((resolve) => {
|
|
198
|
+
if (SKIP_WORDS.has(text)) {
|
|
199
|
+
resolve(text);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const encodedText = encodeURIComponent(text);
|
|
204
|
+
const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=en&tl=${targetLang}&dt=t&q=${encodedText}`;
|
|
205
|
+
|
|
206
|
+
https
|
|
207
|
+
.get(url, res => {
|
|
208
|
+
let data = '';
|
|
209
|
+
res.on('data', chunk => {
|
|
210
|
+
data += chunk;
|
|
211
|
+
});
|
|
212
|
+
res.on('end', () => {
|
|
213
|
+
try {
|
|
214
|
+
const parsed = JSON.parse(data);
|
|
215
|
+
const translated = parsed[0]
|
|
216
|
+
.map(item => item[0])
|
|
217
|
+
.join('')
|
|
218
|
+
.trim();
|
|
219
|
+
resolve(translated || text);
|
|
220
|
+
} catch (error) {
|
|
221
|
+
console.warn(` ⚠️ Translation failed for "${text.substring(0, 30)}...": ${error.message}`);
|
|
222
|
+
resolve(text);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
})
|
|
226
|
+
.on('error', err => {
|
|
227
|
+
console.warn(` ⚠️ Network error: ${err.message}`);
|
|
228
|
+
resolve(text);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Add delay between API calls to avoid rate limiting
|
|
235
|
+
*/
|
|
236
|
+
function delay(ms) {
|
|
237
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Check if a value needs translation (is still in English)
|
|
242
|
+
*/
|
|
243
|
+
function needsTranslation(value, enValue) {
|
|
244
|
+
if (typeof value !== 'string') return false;
|
|
245
|
+
if (value === enValue) return true;
|
|
246
|
+
if (SKIP_WORDS.has(value)) return false;
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Recursively translate object values
|
|
252
|
+
*/
|
|
253
|
+
async function translateObject(enObj, targetObj, targetLang, path = '', stats = { count: 0 }) {
|
|
254
|
+
for (const key in enObj) {
|
|
255
|
+
const currentPath = path ? `${path}.${key}` : key;
|
|
256
|
+
const enValue = enObj[key];
|
|
257
|
+
const targetValue = targetObj[key];
|
|
258
|
+
|
|
259
|
+
if (Array.isArray(enValue)) {
|
|
260
|
+
// Handle arrays
|
|
261
|
+
if (!Array.isArray(targetValue)) {
|
|
262
|
+
targetObj[key] = [];
|
|
263
|
+
}
|
|
264
|
+
for (let i = 0; i < enValue.length; i++) {
|
|
265
|
+
if (typeof enValue[i] === 'string') {
|
|
266
|
+
if (needsTranslation(targetObj[key][i], enValue[i])) {
|
|
267
|
+
console.log(` 🔄 ${currentPath}[${i}]: "${enValue[i].substring(0, 40)}..."`);
|
|
268
|
+
targetObj[key][i] = await translateText(enValue[i], targetLang);
|
|
269
|
+
stats.count++;
|
|
270
|
+
await delay(200);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
} else if (typeof enValue === 'object' && enValue !== null) {
|
|
275
|
+
// Handle nested objects
|
|
276
|
+
if (!targetObj[key] || typeof targetObj[key] !== 'object') {
|
|
277
|
+
targetObj[key] = {};
|
|
278
|
+
}
|
|
279
|
+
await translateObject(enValue, targetObj[key], targetLang, currentPath, stats);
|
|
280
|
+
} else if (typeof enValue === 'string') {
|
|
281
|
+
// Handle string values
|
|
282
|
+
if (needsTranslation(targetValue, enValue)) {
|
|
283
|
+
// Skip placeholders like {{count}}
|
|
284
|
+
if (enValue.includes('{{') && enValue.includes('}}')) {
|
|
285
|
+
// Translate but preserve placeholders
|
|
286
|
+
console.log(` 🔄 ${currentPath}: "${enValue.substring(0, 40)}${enValue.length > 40 ? '...' : ''}"`);
|
|
287
|
+
targetObj[key] = await translateText(enValue, targetLang);
|
|
288
|
+
stats.count++;
|
|
289
|
+
await delay(200);
|
|
290
|
+
} else {
|
|
291
|
+
console.log(` 🔄 ${currentPath}: "${enValue.substring(0, 40)}${enValue.length > 40 ? '...' : ''}"`);
|
|
292
|
+
targetObj[key] = await translateText(enValue, targetLang);
|
|
293
|
+
stats.count++;
|
|
294
|
+
await delay(200);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return stats.count;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Translate a single language file
|
|
305
|
+
*/
|
|
306
|
+
async function translateLanguageFile(enUSPath, targetPath, langCode) {
|
|
307
|
+
const targetLang = LANGUAGE_MAP[langCode];
|
|
308
|
+
|
|
309
|
+
if (!targetLang) {
|
|
310
|
+
console.log(` ⚠️ No language mapping for ${langCode}, skipping`);
|
|
311
|
+
return 0;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Skip English variants
|
|
315
|
+
if (targetLang === 'en') {
|
|
316
|
+
console.log(` ⏭️ Skipping English variant: ${langCode}`);
|
|
317
|
+
return 0;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const enUS = parseTypeScriptFile(enUSPath);
|
|
321
|
+
let target;
|
|
322
|
+
|
|
323
|
+
try {
|
|
324
|
+
target = parseTypeScriptFile(targetPath);
|
|
325
|
+
} catch {
|
|
326
|
+
// If target file doesn't exist or is invalid, start fresh
|
|
327
|
+
target = {};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const stats = { count: 0 };
|
|
331
|
+
await translateObject(enUS, target, targetLang, '', stats);
|
|
332
|
+
|
|
333
|
+
if (stats.count > 0) {
|
|
334
|
+
const content = generateTypeScriptContent(target, langCode);
|
|
335
|
+
fs.writeFileSync(targetPath, content);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return stats.count;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Main function
|
|
343
|
+
*/
|
|
344
|
+
async function main() {
|
|
345
|
+
const targetDir = process.argv[2] || 'src/domains/localization/infrastructure/locales';
|
|
346
|
+
const localesDir = path.resolve(process.cwd(), targetDir);
|
|
347
|
+
|
|
348
|
+
console.log('🚀 Starting automatic translation...\n');
|
|
349
|
+
console.log(`📂 Locales directory: ${localesDir}\n`);
|
|
350
|
+
|
|
351
|
+
if (!fs.existsSync(localesDir)) {
|
|
352
|
+
console.error(`❌ Locales directory not found: ${localesDir}`);
|
|
353
|
+
process.exit(1);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const enUSPath = path.join(localesDir, 'en-US.ts');
|
|
357
|
+
if (!fs.existsSync(enUSPath)) {
|
|
358
|
+
console.error(`❌ Base file not found: ${enUSPath}`);
|
|
359
|
+
process.exit(1);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Find all language files
|
|
363
|
+
const files = fs.readdirSync(localesDir)
|
|
364
|
+
.filter(f => f.match(/^[a-z]{2}-[A-Z]{2}\.ts$/) && f !== 'en-US.ts')
|
|
365
|
+
.sort();
|
|
366
|
+
|
|
367
|
+
console.log(`📊 Languages to translate: ${files.length}`);
|
|
368
|
+
console.log('⚡ Running with 200ms delay between API calls\n');
|
|
369
|
+
|
|
370
|
+
let totalTranslated = 0;
|
|
371
|
+
|
|
372
|
+
for (const file of files) {
|
|
373
|
+
const langCode = file.replace('.ts', '');
|
|
374
|
+
const targetPath = path.join(localesDir, file);
|
|
375
|
+
|
|
376
|
+
console.log(`\n🌍 Translating ${langCode}...`);
|
|
377
|
+
|
|
378
|
+
const count = await translateLanguageFile(enUSPath, targetPath, langCode);
|
|
379
|
+
totalTranslated += count;
|
|
380
|
+
|
|
381
|
+
if (count > 0) {
|
|
382
|
+
console.log(` ✅ Translated ${count} strings`);
|
|
383
|
+
} else {
|
|
384
|
+
console.log(` ✓ Already complete`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
console.log(`\n✅ Translation completed!`);
|
|
389
|
+
console.log(` Total strings translated: ${totalTranslated}`);
|
|
390
|
+
console.log(`\n📝 Next: Run 'node scripts/setup-languages.js' to update index.ts`);
|
|
391
|
+
}
|
|
3
392
|
|
|
4
|
-
|
|
5
|
-
console.
|
|
6
|
-
|
|
393
|
+
main().catch(error => {
|
|
394
|
+
console.error('❌ Translation failed:', error);
|
|
395
|
+
process.exit(1);
|
|
396
|
+
});
|
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Translation Hook
|
|
3
3
|
*
|
|
4
|
-
* Provides translation function with proper
|
|
5
|
-
* - React i18next integration
|
|
6
|
-
* - Memoized translation function
|
|
7
|
-
* - Type-safe translation function
|
|
4
|
+
* Provides translation function with proper language change reactivity
|
|
5
|
+
* - React i18next integration for automatic language change detection
|
|
8
6
|
* - Auto-namespace detection from dot notation
|
|
9
|
-
* -
|
|
7
|
+
* - Type-safe translation function
|
|
10
8
|
*/
|
|
11
9
|
|
|
12
|
-
import { useCallback
|
|
10
|
+
import { useCallback } from 'react';
|
|
11
|
+
import { useTranslation } from 'react-i18next';
|
|
13
12
|
import i18n from '../config/i18n';
|
|
14
|
-
import { translationCache } from '../config/TranslationCache';
|
|
15
13
|
|
|
16
14
|
export interface TranslationOptions {
|
|
17
15
|
count?: number;
|
|
@@ -20,90 +18,54 @@ export interface TranslationOptions {
|
|
|
20
18
|
[key: string]: any;
|
|
21
19
|
}
|
|
22
20
|
|
|
23
|
-
|
|
24
21
|
/**
|
|
25
22
|
* Hook for translation functionality
|
|
23
|
+
* Uses react-i18next for automatic language change reactivity
|
|
24
|
+
*
|
|
26
25
|
* Supports both formats:
|
|
27
26
|
* - t('namespace:key.subkey') - explicit namespace
|
|
28
27
|
* - t('namespace.key.subkey') - auto-detected namespace (first segment before dot)
|
|
29
28
|
*/
|
|
30
29
|
export const useTranslationFunction = () => {
|
|
31
|
-
const
|
|
30
|
+
const { t: i18nextT, ready } = useTranslation(undefined, { i18n });
|
|
32
31
|
|
|
33
32
|
const translate = useCallback((key: string, options: TranslationOptions = {}): string => {
|
|
34
|
-
if (!
|
|
33
|
+
if (!ready || !i18n.isInitialized) {
|
|
35
34
|
if (__DEV__) {
|
|
36
|
-
console.warn(`[Localization] i18n not
|
|
35
|
+
console.warn(`[Localization] i18n not ready, returning key: ${key}`);
|
|
37
36
|
}
|
|
38
37
|
return options.defaultValue || key;
|
|
39
38
|
}
|
|
40
39
|
|
|
41
|
-
// Create cache key
|
|
42
|
-
const cacheKey = `${key}:${JSON.stringify(options)}`;
|
|
43
|
-
|
|
44
|
-
// Check cache first
|
|
45
|
-
const cached = translationCache.get(cacheKey);
|
|
46
|
-
if (cached) {
|
|
47
|
-
return cached;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
let result: string;
|
|
51
|
-
|
|
52
40
|
// If key already has namespace separator (:), use as-is
|
|
53
41
|
if (key.includes(':')) {
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
// Auto-detect namespace from first dot segment
|
|
58
|
-
const firstDotIndex = key.indexOf('.');
|
|
59
|
-
if (firstDotIndex > 0) {
|
|
60
|
-
const potentialNamespace = key.substring(0, firstDotIndex);
|
|
61
|
-
const restOfKey = key.substring(firstDotIndex + 1);
|
|
42
|
+
const result = i18nextT(key, options);
|
|
43
|
+
return typeof result === 'string' ? result : key;
|
|
44
|
+
}
|
|
62
45
|
|
|
63
|
-
|
|
64
|
-
|
|
46
|
+
// Auto-detect namespace from first dot segment
|
|
47
|
+
const firstDotIndex = key.indexOf('.');
|
|
48
|
+
if (firstDotIndex > 0) {
|
|
49
|
+
const potentialNamespace = key.substring(0, firstDotIndex);
|
|
50
|
+
const restOfKey = key.substring(firstDotIndex + 1);
|
|
51
|
+
const hasNamespace = i18n.hasResourceBundle(i18n.language, potentialNamespace);
|
|
65
52
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
53
|
+
if (hasNamespace) {
|
|
54
|
+
const namespacedKey = `${potentialNamespace}:${restOfKey}`;
|
|
55
|
+
const namespacedResult = i18nextT(namespacedKey, options);
|
|
69
56
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
result = typeof namespacedResult === 'string' ? namespacedResult : key;
|
|
73
|
-
} else {
|
|
74
|
-
// Fallback to original key
|
|
75
|
-
const fallbackResult = i18n.t(key, options);
|
|
76
|
-
result = typeof fallbackResult === 'string' ? fallbackResult : key;
|
|
77
|
-
}
|
|
78
|
-
} else {
|
|
79
|
-
// Fallback to original key
|
|
80
|
-
const tempResult = i18n.t(key, options);
|
|
81
|
-
result = typeof tempResult === 'string' ? tempResult : key;
|
|
57
|
+
if (namespacedResult !== namespacedKey && namespacedResult !== restOfKey) {
|
|
58
|
+
return typeof namespacedResult === 'string' ? namespacedResult : key;
|
|
82
59
|
}
|
|
83
|
-
} else {
|
|
84
|
-
// No dot, use as-is
|
|
85
|
-
const noDotResult = i18n.t(key, options);
|
|
86
|
-
result = typeof noDotResult === 'string' ? noDotResult : key;
|
|
87
60
|
}
|
|
88
61
|
}
|
|
89
62
|
|
|
90
|
-
//
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
return finalResult;
|
|
95
|
-
}, [isInitialized]);
|
|
96
|
-
|
|
97
|
-
// Clear cache when language changes
|
|
98
|
-
const clearCache = useCallback(() => {
|
|
99
|
-
translationCache.clear();
|
|
100
|
-
if (__DEV__) {
|
|
101
|
-
console.log('[Localization] Translation cache cleared');
|
|
102
|
-
}
|
|
103
|
-
}, []);
|
|
63
|
+
// Fallback to original key
|
|
64
|
+
const result = i18nextT(key, options);
|
|
65
|
+
return typeof result === 'string' ? result : key;
|
|
66
|
+
}, [i18nextT, ready]);
|
|
104
67
|
|
|
105
68
|
return {
|
|
106
69
|
t: translate,
|
|
107
|
-
clearCache,
|
|
108
70
|
};
|
|
109
71
|
};
|
|
@@ -22,7 +22,7 @@ export class LanguageSwitcher {
|
|
|
22
22
|
}> {
|
|
23
23
|
const language = languageRegistry.getLanguageByCode(languageCode);
|
|
24
24
|
|
|
25
|
-
if (!language) {
|
|
25
|
+
if (!language && __DEV__) {
|
|
26
26
|
console.warn(`[LanguageSwitcher] Language ${languageCode} not found in registry, proceeding anyway.`);
|
|
27
27
|
}
|
|
28
28
|
|
|
@@ -4,11 +4,10 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { create } from 'zustand';
|
|
7
|
-
import type { LocalizationState, LocalizationActions, LocalizationGetters
|
|
7
|
+
import type { LocalizationState, LocalizationActions, LocalizationGetters } from './types/LocalizationState';
|
|
8
8
|
import { LanguageInitializer } from './LanguageInitializer';
|
|
9
9
|
import { LanguageSwitcher } from './LanguageSwitcher';
|
|
10
10
|
import { languageRegistry } from '../config/languagesData';
|
|
11
|
-
import { translationCache } from '../config/TranslationCache';
|
|
12
11
|
|
|
13
12
|
interface LocalizationStore extends LocalizationState, LocalizationActions, LocalizationGetters {
|
|
14
13
|
// Additional properties can be added here if needed
|
|
@@ -68,9 +67,6 @@ export const createLocalizationStore = () => {
|
|
|
68
67
|
try {
|
|
69
68
|
const result = await LanguageSwitcher.switchLanguage(languageCode);
|
|
70
69
|
|
|
71
|
-
// Clear translation cache to ensure new keys are fetched
|
|
72
|
-
translationCache.clear();
|
|
73
|
-
|
|
74
70
|
set({
|
|
75
71
|
currentLanguage: result.languageCode,
|
|
76
72
|
isRTL: result.isRTL,
|
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Language Item Component
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* Renders a single language item in the language selection list
|
|
5
|
+
* Theme-aware component that adapts to light/dark mode
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
|
-
import React from 'react';
|
|
8
|
+
import React, { useMemo } from 'react';
|
|
8
9
|
import {
|
|
9
10
|
View,
|
|
10
11
|
TouchableOpacity,
|
|
11
12
|
Text,
|
|
12
13
|
StyleSheet,
|
|
14
|
+
type StyleProp,
|
|
15
|
+
type ViewStyle,
|
|
16
|
+
type TextStyle,
|
|
13
17
|
} from 'react-native';
|
|
18
|
+
// @ts-ignore - Optional peer dependency
|
|
19
|
+
import { useAppDesignTokens } from '@umituz/react-native-design-system-theme';
|
|
14
20
|
import type { Language } from '../../infrastructure/storage/types/LocalizationState';
|
|
15
21
|
|
|
16
22
|
interface LanguageItemProps {
|
|
@@ -18,11 +24,11 @@ interface LanguageItemProps {
|
|
|
18
24
|
isSelected: boolean;
|
|
19
25
|
onSelect: (code: string) => void;
|
|
20
26
|
customStyles?: {
|
|
21
|
-
languageItem?:
|
|
22
|
-
languageContent?:
|
|
23
|
-
languageText?:
|
|
24
|
-
flag?:
|
|
25
|
-
nativeName?:
|
|
27
|
+
languageItem?: StyleProp<ViewStyle>;
|
|
28
|
+
languageContent?: StyleProp<ViewStyle>;
|
|
29
|
+
languageText?: StyleProp<ViewStyle>;
|
|
30
|
+
flag?: StyleProp<TextStyle>;
|
|
31
|
+
nativeName?: StyleProp<TextStyle>;
|
|
26
32
|
};
|
|
27
33
|
}
|
|
28
34
|
|
|
@@ -32,13 +38,36 @@ export const LanguageItem: React.FC<LanguageItemProps> = ({
|
|
|
32
38
|
onSelect,
|
|
33
39
|
customStyles,
|
|
34
40
|
}) => {
|
|
41
|
+
const tokens = useAppDesignTokens();
|
|
42
|
+
|
|
43
|
+
const themedStyles = useMemo(() => ({
|
|
44
|
+
languageItem: {
|
|
45
|
+
backgroundColor: tokens.colors.backgroundSecondary,
|
|
46
|
+
borderColor: tokens.colors.border,
|
|
47
|
+
} as ViewStyle,
|
|
48
|
+
selectedLanguageItem: {
|
|
49
|
+
borderColor: tokens.colors.primary,
|
|
50
|
+
backgroundColor: tokens.colors.primaryLight,
|
|
51
|
+
} as ViewStyle,
|
|
52
|
+
nativeName: {
|
|
53
|
+
color: tokens.colors.textPrimary,
|
|
54
|
+
} as TextStyle,
|
|
55
|
+
languageName: {
|
|
56
|
+
color: tokens.colors.textSecondary,
|
|
57
|
+
} as TextStyle,
|
|
58
|
+
checkIcon: {
|
|
59
|
+
color: tokens.colors.primary,
|
|
60
|
+
} as TextStyle,
|
|
61
|
+
}), [tokens]);
|
|
62
|
+
|
|
35
63
|
return (
|
|
36
64
|
<TouchableOpacity
|
|
37
65
|
testID="language-item-test"
|
|
38
66
|
style={[
|
|
39
67
|
styles.languageItem,
|
|
68
|
+
themedStyles.languageItem,
|
|
40
69
|
customStyles?.languageItem,
|
|
41
|
-
isSelected
|
|
70
|
+
isSelected ? [styles.selectedLanguageItem, themedStyles.selectedLanguageItem] : undefined,
|
|
42
71
|
]}
|
|
43
72
|
onPress={() => onSelect(item.code)}
|
|
44
73
|
activeOpacity={0.7}
|
|
@@ -48,16 +77,16 @@ export const LanguageItem: React.FC<LanguageItemProps> = ({
|
|
|
48
77
|
{item.flag || '🌐'}
|
|
49
78
|
</Text>
|
|
50
79
|
<View style={[styles.languageText, customStyles?.languageText]}>
|
|
51
|
-
<Text style={[styles.nativeName, customStyles?.nativeName]}>
|
|
80
|
+
<Text style={[styles.nativeName, themedStyles.nativeName, customStyles?.nativeName]}>
|
|
52
81
|
{item.nativeName}
|
|
53
82
|
</Text>
|
|
54
|
-
<Text style={[styles.languageName, customStyles?.nativeName]}>
|
|
83
|
+
<Text style={[styles.languageName, themedStyles.languageName, customStyles?.nativeName]}>
|
|
55
84
|
{item.name}
|
|
56
85
|
</Text>
|
|
57
86
|
</View>
|
|
58
87
|
</View>
|
|
59
88
|
{isSelected && (
|
|
60
|
-
<Text style={[styles.checkIcon, customStyles?.flag]}>✓</Text>
|
|
89
|
+
<Text style={[styles.checkIcon, themedStyles.checkIcon, customStyles?.flag]}>✓</Text>
|
|
61
90
|
)}
|
|
62
91
|
</TouchableOpacity>
|
|
63
92
|
);
|
|
@@ -71,13 +100,10 @@ const styles = StyleSheet.create({
|
|
|
71
100
|
padding: 16,
|
|
72
101
|
borderRadius: 12,
|
|
73
102
|
borderWidth: 1,
|
|
74
|
-
borderColor: '#e0e0e0',
|
|
75
103
|
marginBottom: 8,
|
|
76
|
-
backgroundColor: '#fff',
|
|
77
104
|
},
|
|
78
105
|
selectedLanguageItem: {
|
|
79
|
-
|
|
80
|
-
backgroundColor: '#f0f8ff',
|
|
106
|
+
borderWidth: 2,
|
|
81
107
|
},
|
|
82
108
|
languageContent: {
|
|
83
109
|
flexDirection: 'row',
|
|
@@ -94,16 +120,13 @@ const styles = StyleSheet.create({
|
|
|
94
120
|
nativeName: {
|
|
95
121
|
fontSize: 16,
|
|
96
122
|
fontWeight: '600',
|
|
97
|
-
color: '#333',
|
|
98
123
|
marginBottom: 2,
|
|
99
124
|
},
|
|
100
125
|
languageName: {
|
|
101
126
|
fontSize: 14,
|
|
102
|
-
color: '#666',
|
|
103
127
|
},
|
|
104
128
|
checkIcon: {
|
|
105
129
|
fontSize: 18,
|
|
106
|
-
color: '#007AFF',
|
|
107
130
|
fontWeight: 'bold',
|
|
108
131
|
},
|
|
109
132
|
});
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { View, Text, Pressable, StyleSheet, ViewStyle } from 'react-native';
|
|
2
|
+
import { View, Text, Pressable, StyleSheet, type ViewStyle } from 'react-native';
|
|
3
|
+
// @ts-ignore - Optional peer dependency
|
|
3
4
|
import { Feather } from '@expo/vector-icons';
|
|
5
|
+
// @ts-ignore - Optional peer dependency
|
|
4
6
|
import { useNavigation } from '@react-navigation/native';
|
|
7
|
+
// @ts-ignore - Optional peer dependency
|
|
5
8
|
import { useAppDesignTokens } from '@umituz/react-native-design-system-theme';
|
|
6
9
|
import { useLocalization } from '../../infrastructure/hooks/useLocalization';
|
|
7
10
|
import { getLanguageByCode } from '../../infrastructure/config/languages';
|
|
@@ -1,27 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Search Input Component
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* Renders search input for language filtering
|
|
5
|
+
* Theme-aware component that adapts to light/dark mode
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
|
-
import React from 'react';
|
|
8
|
+
import React, { useMemo } from 'react';
|
|
8
9
|
import {
|
|
9
10
|
View,
|
|
10
11
|
TextInput,
|
|
11
12
|
TouchableOpacity,
|
|
12
13
|
Text,
|
|
13
14
|
StyleSheet,
|
|
15
|
+
type StyleProp,
|
|
16
|
+
type ViewStyle,
|
|
17
|
+
type TextStyle,
|
|
14
18
|
} from 'react-native';
|
|
19
|
+
// @ts-ignore - Optional peer dependency
|
|
20
|
+
import { useAppDesignTokens } from '@umituz/react-native-design-system-theme';
|
|
15
21
|
|
|
16
22
|
interface SearchInputProps {
|
|
17
23
|
value: string;
|
|
18
24
|
onChange: (value: string) => void;
|
|
19
25
|
placeholder: string;
|
|
20
26
|
customStyles?: {
|
|
21
|
-
searchContainer?:
|
|
22
|
-
searchInput?:
|
|
23
|
-
searchIcon?:
|
|
24
|
-
clearButton?:
|
|
27
|
+
searchContainer?: StyleProp<ViewStyle>;
|
|
28
|
+
searchInput?: StyleProp<TextStyle>;
|
|
29
|
+
searchIcon?: StyleProp<TextStyle>;
|
|
30
|
+
clearButton?: StyleProp<ViewStyle>;
|
|
25
31
|
};
|
|
26
32
|
}
|
|
27
33
|
|
|
@@ -31,13 +37,28 @@ export const SearchInput: React.FC<SearchInputProps> = ({
|
|
|
31
37
|
placeholder,
|
|
32
38
|
customStyles,
|
|
33
39
|
}) => {
|
|
40
|
+
const tokens = useAppDesignTokens();
|
|
41
|
+
|
|
42
|
+
const themedStyles = useMemo(() => ({
|
|
43
|
+
searchContainer: {
|
|
44
|
+
backgroundColor: tokens.colors.backgroundSecondary,
|
|
45
|
+
borderColor: tokens.colors.border,
|
|
46
|
+
} as ViewStyle,
|
|
47
|
+
searchInput: {
|
|
48
|
+
color: tokens.colors.textPrimary,
|
|
49
|
+
} as TextStyle,
|
|
50
|
+
clearIcon: {
|
|
51
|
+
color: tokens.colors.textSecondary,
|
|
52
|
+
} as TextStyle,
|
|
53
|
+
}), [tokens]);
|
|
54
|
+
|
|
34
55
|
return (
|
|
35
|
-
<View style={[styles.searchContainer, customStyles?.searchContainer]}>
|
|
56
|
+
<View style={[styles.searchContainer, themedStyles.searchContainer, customStyles?.searchContainer]}>
|
|
36
57
|
<Text style={[styles.searchIcon, customStyles?.searchIcon]}>🔍</Text>
|
|
37
58
|
<TextInput
|
|
38
|
-
style={[styles.searchInput, customStyles?.searchInput]}
|
|
59
|
+
style={[styles.searchInput, themedStyles.searchInput, customStyles?.searchInput]}
|
|
39
60
|
placeholder={placeholder}
|
|
40
|
-
placeholderTextColor=
|
|
61
|
+
placeholderTextColor={tokens.colors.textTertiary}
|
|
41
62
|
value={value}
|
|
42
63
|
onChangeText={onChange}
|
|
43
64
|
autoCapitalize="none"
|
|
@@ -49,7 +70,7 @@ export const SearchInput: React.FC<SearchInputProps> = ({
|
|
|
49
70
|
style={[styles.clearButton, customStyles?.clearButton]}
|
|
50
71
|
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
|
51
72
|
>
|
|
52
|
-
<Text style={[styles.clearIcon, customStyles?.searchIcon]}>✕</Text>
|
|
73
|
+
<Text style={[styles.clearIcon, themedStyles.clearIcon, customStyles?.searchIcon]}>✕</Text>
|
|
53
74
|
</TouchableOpacity>
|
|
54
75
|
)}
|
|
55
76
|
</View>
|
|
@@ -64,10 +85,8 @@ const styles = StyleSheet.create({
|
|
|
64
85
|
marginBottom: 24,
|
|
65
86
|
paddingHorizontal: 16,
|
|
66
87
|
paddingVertical: 12,
|
|
67
|
-
backgroundColor: '#f5f5f5',
|
|
68
88
|
borderRadius: 12,
|
|
69
89
|
borderWidth: 1,
|
|
70
|
-
borderColor: '#e0e0e0',
|
|
71
90
|
},
|
|
72
91
|
searchIcon: {
|
|
73
92
|
marginRight: 12,
|
|
@@ -78,13 +97,11 @@ const styles = StyleSheet.create({
|
|
|
78
97
|
fontSize: 16,
|
|
79
98
|
padding: 0,
|
|
80
99
|
fontWeight: '500',
|
|
81
|
-
color: '#333',
|
|
82
100
|
},
|
|
83
101
|
clearButton: {
|
|
84
102
|
padding: 4,
|
|
85
103
|
},
|
|
86
104
|
clearIcon: {
|
|
87
105
|
fontSize: 14,
|
|
88
|
-
color: '#666',
|
|
89
106
|
},
|
|
90
107
|
});
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Translation Cache
|
|
3
|
-
*
|
|
4
|
-
* Performance optimization for translation caching
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
export class TranslationCache {
|
|
8
|
-
private cache = new Map<string, string>();
|
|
9
|
-
private maxSize = 1000;
|
|
10
|
-
|
|
11
|
-
get(key: string): string | undefined {
|
|
12
|
-
return this.cache.get(key);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
set(key: string, value: string): void {
|
|
16
|
-
if (this.cache.size >= this.maxSize) {
|
|
17
|
-
// Remove oldest entry
|
|
18
|
-
const firstKey = this.cache.keys().next().value;
|
|
19
|
-
if (firstKey !== undefined) {
|
|
20
|
-
this.cache.delete(firstKey);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
this.cache.set(key, value);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
clear(): void {
|
|
27
|
-
this.cache.clear();
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export const translationCache = new TranslationCache();
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Translation Cache Tests
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { TranslationCache } from '../TranslationCache';
|
|
6
|
-
|
|
7
|
-
describe('TranslationCache', () => {
|
|
8
|
-
let cache: TranslationCache;
|
|
9
|
-
|
|
10
|
-
beforeEach(() => {
|
|
11
|
-
cache = new TranslationCache();
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it('should store and retrieve values', () => {
|
|
15
|
-
cache.set('key1', 'value1');
|
|
16
|
-
expect(cache.get('key1')).toBe('value1');
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it('should return undefined for non-existent keys', () => {
|
|
20
|
-
expect(cache.get('nonexistent')).toBeUndefined();
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('should clear all values', () => {
|
|
24
|
-
cache.set('key1', 'value1');
|
|
25
|
-
cache.set('key2', 'value2');
|
|
26
|
-
cache.clear();
|
|
27
|
-
expect(cache.get('key1')).toBeUndefined();
|
|
28
|
-
expect(cache.get('key2')).toBeUndefined();
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('should limit cache size to maxSize', () => {
|
|
32
|
-
// Create a small cache for testing
|
|
33
|
-
const smallCache = new TranslationCache();
|
|
34
|
-
(smallCache as any).maxSize = 2;
|
|
35
|
-
|
|
36
|
-
smallCache.set('key1', 'value1');
|
|
37
|
-
smallCache.set('key2', 'value2');
|
|
38
|
-
smallCache.set('key3', 'value3'); // Should remove key1
|
|
39
|
-
|
|
40
|
-
expect(smallCache.get('key1')).toBeUndefined();
|
|
41
|
-
expect(smallCache.get('key2')).toBe('value2');
|
|
42
|
-
expect(smallCache.get('key3')).toBe('value3');
|
|
43
|
-
});
|
|
44
|
-
});
|