@umituz/react-native-settings 4.23.35 → 4.23.37
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 +12 -5
- package/src/domains/appearance/presentation/screens/AppearanceScreen.tsx +1 -1
- package/src/domains/disclaimer/presentation/components/DisclaimerSetting.test.tsx +1 -1
- package/src/domains/disclaimer/presentation/components/DisclaimerSetting.tsx +1 -1
- package/src/domains/disclaimer/presentation/screens/DisclaimerScreen.tsx +1 -1
- package/src/domains/localization/domain/repositories/ILocalizationRepository.ts +18 -0
- package/src/domains/localization/index.ts +33 -0
- package/src/domains/localization/infrastructure/components/LanguageSwitcher.styles.ts +40 -0
- package/src/domains/localization/infrastructure/components/LanguageSwitcher.tsx +88 -0
- package/src/domains/localization/infrastructure/components/__tests__/LanguageSwitcher.test.tsx +91 -0
- package/src/domains/localization/infrastructure/components/useLanguageNavigation.ts +20 -0
- package/src/domains/localization/infrastructure/components/useLanguageSwitcher.ts +34 -0
- package/src/domains/localization/infrastructure/config/DeviceLocale.ts +47 -0
- package/src/domains/localization/infrastructure/config/I18nInitializer.ts +73 -0
- package/src/domains/localization/infrastructure/config/LanguageQuery.ts +35 -0
- package/src/domains/localization/infrastructure/config/LocaleMapping.ts +78 -0
- package/src/domains/localization/infrastructure/config/NamespaceResolver.ts +54 -0
- package/src/domains/localization/infrastructure/config/ResourceBuilder.ts +72 -0
- package/src/domains/localization/infrastructure/config/TranslationLoader.ts +46 -0
- package/src/domains/localization/infrastructure/config/__tests__/languagesData.test.ts +69 -0
- package/src/domains/localization/infrastructure/config/constants/defaultLanguages.ts +43 -0
- package/src/domains/localization/infrastructure/config/i18n.ts +9 -0
- package/src/domains/localization/infrastructure/config/languages.ts +28 -0
- package/src/domains/localization/infrastructure/config/languagesData.ts +26 -0
- package/src/domains/localization/infrastructure/hooks/TranslationHook.ts +39 -0
- package/src/domains/localization/infrastructure/hooks/__tests__/useTranslation.test.ts +52 -0
- package/src/domains/localization/infrastructure/hooks/useLanguageSelection.ts +44 -0
- package/src/domains/localization/infrastructure/hooks/useLocalization.ts +41 -0
- package/src/domains/localization/infrastructure/hooks/useTranslation.ts +94 -0
- package/src/domains/localization/infrastructure/repository/LanguageRepository.ts +53 -0
- package/src/domains/localization/infrastructure/storage/AsyncStorageWrapper.ts +24 -0
- package/src/domains/localization/infrastructure/storage/LanguageInitializer.ts +81 -0
- package/src/domains/localization/infrastructure/storage/LanguageSwitcher.ts +52 -0
- package/src/domains/localization/infrastructure/storage/LocalizationStore.ts +142 -0
- package/src/domains/localization/infrastructure/storage/types/Language.ts +13 -0
- package/src/domains/localization/infrastructure/storage/types/LocalizationState.ts +27 -0
- package/src/domains/localization/presentation/components/LanguageItem.styles.ts +40 -0
- package/src/domains/localization/presentation/components/LanguageItem.tsx +106 -0
- package/src/domains/localization/presentation/components/LanguageSection.tsx +83 -0
- package/src/domains/localization/presentation/components/__tests__/LanguageItem.test.tsx +106 -0
- package/src/domains/localization/presentation/screens/LanguageSelectionScreen.styles.ts +16 -0
- package/src/domains/localization/presentation/screens/LanguageSelectionScreen.tsx +132 -0
- package/src/domains/localization/presentation/screens/LanguageSelectionScreen.types.ts +27 -0
- package/src/domains/localization/presentation/screens/__tests__/LanguageSelectionScreen.test.tsx +165 -0
- package/src/domains/localization/scripts/prepublish.js +36 -0
- package/src/domains/localization/scripts/setup-languages.js +60 -0
- package/src/domains/localization/scripts/sync-translations.js +124 -0
- package/src/domains/localization/scripts/translate-missing.js +92 -0
- package/src/domains/localization/scripts/utils/file-parser.js +78 -0
- package/src/domains/localization/scripts/utils/key-detector.js +45 -0
- package/src/domains/localization/scripts/utils/key-extractor.js +105 -0
- package/src/domains/localization/scripts/utils/object-helper.js +29 -0
- package/src/domains/localization/scripts/utils/sync-helper.js +49 -0
- package/src/domains/localization/scripts/utils/translation-config.js +116 -0
- package/src/domains/localization/scripts/utils/translator.js +83 -0
- package/src/domains/notifications/presentation/components/NotificationsSection.tsx +1 -1
- package/src/domains/notifications/presentation/screens/NotificationSettingsScreen.tsx +1 -1
- package/src/index.ts +2 -0
- package/src/presentation/components/SettingsErrorBoundary.tsx +1 -1
- package/src/presentation/navigation/SettingsStackNavigator.tsx +1 -1
- package/src/presentation/screens/components/SettingsContent.tsx +1 -1
- package/src/presentation/screens/components/SettingsHeader.tsx +1 -1
- package/src/presentation/screens/components/sections/FeatureSettingsSection.tsx +1 -1
- package/src/presentation/screens/components/sections/IdentitySettingsSection.tsx +1 -1
- package/src/presentation/screens/components/sections/ProfileSectionLoader.tsx +1 -1
- package/src/presentation/screens/components/sections/SupportSettingsSection.tsx +1 -1
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Translate Missing Script
|
|
5
|
+
* Automatically translates missing strings using Google Translate
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { parseTypeScriptFile, generateTypeScriptContent } from './utils/file-parser.js';
|
|
11
|
+
import { getTargetLanguage, getLangDisplayName } from './utils/translation-config.js';
|
|
12
|
+
import { translateObject } from './utils/translator.js';
|
|
13
|
+
import { setupLanguages } from './setup-languages.js';
|
|
14
|
+
import { syncTranslations } from './sync-translations.js';
|
|
15
|
+
|
|
16
|
+
async function translateMissing(targetDir, srcDir) {
|
|
17
|
+
const localesDir = path.resolve(process.cwd(), targetDir);
|
|
18
|
+
const enUSPath = path.join(localesDir, 'en-US.ts');
|
|
19
|
+
|
|
20
|
+
// Integrated Workflow: Ensure setup and sync
|
|
21
|
+
const skipSync = process.argv.includes('--no-sync');
|
|
22
|
+
|
|
23
|
+
if (!fs.existsSync(path.join(localesDir, 'index.ts'))) {
|
|
24
|
+
console.log('🔄 Initializing localization setup...');
|
|
25
|
+
setupLanguages(targetDir);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!skipSync) {
|
|
29
|
+
console.log('\n🔄 Checking synchronization...');
|
|
30
|
+
syncTranslations(targetDir, srcDir);
|
|
31
|
+
} else {
|
|
32
|
+
console.log('\n⏭️ Skipping synchronization check...');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const files = fs.readdirSync(localesDir)
|
|
36
|
+
.filter(f => f.match(/^[a-z]{2}-[A-Z]{2}\.ts$/) && f !== 'en-US.ts')
|
|
37
|
+
.sort();
|
|
38
|
+
|
|
39
|
+
console.log(`\n📊 Languages to translate: ${files.length}\n`);
|
|
40
|
+
|
|
41
|
+
const enUS = parseTypeScriptFile(enUSPath);
|
|
42
|
+
|
|
43
|
+
for (const file of files) {
|
|
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
|
+
|
|
52
|
+
const targetPath = path.join(localesDir, file);
|
|
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
|
+
|
|
61
|
+
if (stats.count > 0) {
|
|
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
|
+
}
|
|
76
|
+
} else {
|
|
77
|
+
console.log(` ✨ Already up to date.`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
console.log('\n✅ All translations completed!');
|
|
82
|
+
}
|
|
83
|
+
|
|
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
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import { getLangDisplayName } from './translation-config.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* File Parser
|
|
6
|
+
* Parse and generate TypeScript translation files
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export function parseTypeScriptFile(filePath) {
|
|
10
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
11
|
+
const match = content.match(/export\s+default\s+(\{[\s\S]*\});?\s*$/);
|
|
12
|
+
|
|
13
|
+
if (!match) {
|
|
14
|
+
throw new Error(`Could not parse TypeScript file: ${filePath}`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const objectStr = match[1].replace(/;$/, '');
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
// Basic evaluation for simple objects
|
|
21
|
+
// eslint-disable-next-line no-eval
|
|
22
|
+
return eval(`(${objectStr})`);
|
|
23
|
+
} catch (error) {
|
|
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
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function stringifyValue(value, indent = 2) {
|
|
30
|
+
if (typeof value === 'string') {
|
|
31
|
+
const escaped = value
|
|
32
|
+
.replace(/\\/g, '\\\\')
|
|
33
|
+
.replace(/"/g, '\\"')
|
|
34
|
+
.replace(/\n/g, '\\n');
|
|
35
|
+
return `"${escaped}"`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (Array.isArray(value)) {
|
|
39
|
+
if (value.length === 0) return '[]';
|
|
40
|
+
const items = value.map(v => stringifyValue(v, indent + 2));
|
|
41
|
+
return `[${items.join(', ')}]`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (typeof value === 'object' && value !== null) {
|
|
45
|
+
const entries = Object.entries(value);
|
|
46
|
+
|
|
47
|
+
if (entries.length === 0) {
|
|
48
|
+
return '{}';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const spaces = ' '.repeat(indent);
|
|
52
|
+
const innerSpaces = ' '.repeat(indent + 2);
|
|
53
|
+
const entriesStr = entries
|
|
54
|
+
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
55
|
+
.map(([k, v]) => {
|
|
56
|
+
const key = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(k) ? k : `"${k}"`;
|
|
57
|
+
return `${innerSpaces}${key}: ${stringifyValue(v, indent + 2)}`;
|
|
58
|
+
})
|
|
59
|
+
.join(',\n');
|
|
60
|
+
return `{\n${entriesStr},\n${spaces}}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return String(value);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function generateTypeScriptContent(obj, langCode) {
|
|
67
|
+
const langName = getLangDisplayName(langCode);
|
|
68
|
+
const isBase = langCode === 'en-US';
|
|
69
|
+
const objString = stringifyValue(obj, 0);
|
|
70
|
+
|
|
71
|
+
return `/**
|
|
72
|
+
* ${langName} Translations
|
|
73
|
+
* ${isBase ? 'Base translations file' : 'Auto-synced from en-US.ts'}
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
export default ${objString};
|
|
77
|
+
`;
|
|
78
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Key Detector
|
|
3
|
+
* Detects new, missing, and removed keys between source and target objects
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export function detectNewKeys(sourceObj, targetObj, path = '', newKeys = []) {
|
|
7
|
+
for (const key in sourceObj) {
|
|
8
|
+
const currentPath = path ? `${path}.${key}` : key;
|
|
9
|
+
const sourceValue = sourceObj[key];
|
|
10
|
+
const targetValue = targetObj[key];
|
|
11
|
+
|
|
12
|
+
if (!Object.prototype.hasOwnProperty.call(targetObj, key)) {
|
|
13
|
+
newKeys.push({ path: currentPath, value: sourceValue });
|
|
14
|
+
} else if (
|
|
15
|
+
typeof sourceValue === 'object' &&
|
|
16
|
+
sourceValue !== null &&
|
|
17
|
+
!Array.isArray(sourceValue)
|
|
18
|
+
) {
|
|
19
|
+
if (typeof targetValue === 'object' && targetValue !== null && !Array.isArray(targetValue)) {
|
|
20
|
+
detectNewKeys(sourceValue, targetValue, currentPath, newKeys);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return newKeys;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function detectMissingKeys(sourceObj, targetObj, path = '', missingKeys = []) {
|
|
28
|
+
for (const key in targetObj) {
|
|
29
|
+
const currentPath = path ? `${path}.${key}` : key;
|
|
30
|
+
|
|
31
|
+
if (!Object.prototype.hasOwnProperty.call(sourceObj, key)) {
|
|
32
|
+
missingKeys.push(currentPath);
|
|
33
|
+
} else if (
|
|
34
|
+
typeof sourceObj[key] === 'object' &&
|
|
35
|
+
sourceObj[key] !== null &&
|
|
36
|
+
!Array.isArray(sourceObj[key]) &&
|
|
37
|
+
typeof targetObj[key] === 'object' &&
|
|
38
|
+
targetObj[key] !== null &&
|
|
39
|
+
!Array.isArray(targetObj[key])
|
|
40
|
+
) {
|
|
41
|
+
detectMissingKeys(sourceObj[key], targetObj[key], currentPath, missingKeys);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return missingKeys;
|
|
45
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generic Key Extractor
|
|
6
|
+
* Scans source code for i18n translation keys
|
|
7
|
+
* NO project-specific logic - works for any React Native app
|
|
8
|
+
*/
|
|
9
|
+
|
|
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
|
+
]);
|
|
24
|
+
|
|
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
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
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
|
+
}
|
|
46
|
+
|
|
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;
|
|
59
|
+
|
|
60
|
+
const dynamicKey = `${prefix}.${id}`;
|
|
61
|
+
if (!keyMap.has(dynamicKey)) {
|
|
62
|
+
keyMap.set(dynamicKey, dynamicKey);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
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);
|
|
76
|
+
|
|
77
|
+
if (stat.isDirectory()) {
|
|
78
|
+
if (!skipDirs.includes(file)) {
|
|
79
|
+
walkDirectory(fullPath, keyMap, skipDirs);
|
|
80
|
+
}
|
|
81
|
+
} else if (/\.(ts|tsx|js|jsx)$/.test(file)) {
|
|
82
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
83
|
+
extractFromFile(content, keyMap);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
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;
|
|
105
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Object Helper
|
|
3
|
+
* Utilities for deep object manipulation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Set a value in a nested object, creating intermediate objects if necessary
|
|
8
|
+
* Returns true if the key was newly added, false if it already existed
|
|
9
|
+
*/
|
|
10
|
+
export function setDeep(obj, path, value) {
|
|
11
|
+
const keys = path.split('.');
|
|
12
|
+
let current = obj;
|
|
13
|
+
|
|
14
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
15
|
+
const key = keys[i];
|
|
16
|
+
if (!current[key] || typeof current[key] !== 'object' || Array.isArray(current[key])) {
|
|
17
|
+
current[key] = {};
|
|
18
|
+
}
|
|
19
|
+
current = current[key];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const lastKey = keys[keys.length - 1];
|
|
23
|
+
if (current[lastKey] === undefined) {
|
|
24
|
+
current[lastKey] = value;
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync Helper
|
|
3
|
+
* Helper functions for synchronizing translation keys
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export function addMissingKeys(sourceObj, targetObj, stats = { added: 0, newKeys: [] }) {
|
|
7
|
+
for (const key in sourceObj) {
|
|
8
|
+
const sourceValue = sourceObj[key];
|
|
9
|
+
const isNewKey = !Object.prototype.hasOwnProperty.call(targetObj, key);
|
|
10
|
+
|
|
11
|
+
if (isNewKey) {
|
|
12
|
+
targetObj[key] = sourceValue;
|
|
13
|
+
stats.added++;
|
|
14
|
+
stats.newKeys.push(key);
|
|
15
|
+
} else if (
|
|
16
|
+
typeof sourceValue === 'object' &&
|
|
17
|
+
sourceValue !== null &&
|
|
18
|
+
!Array.isArray(sourceValue)
|
|
19
|
+
) {
|
|
20
|
+
if (!targetObj[key] || typeof targetObj[key] !== 'object') {
|
|
21
|
+
targetObj[key] = {};
|
|
22
|
+
}
|
|
23
|
+
addMissingKeys(sourceValue, targetObj[key], stats);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return stats;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function removeExtraKeys(sourceObj, targetObj, stats = { removed: 0, removedKeys: [] }) {
|
|
30
|
+
for (const key in targetObj) {
|
|
31
|
+
const isExtraKey = !Object.prototype.hasOwnProperty.call(sourceObj, key);
|
|
32
|
+
|
|
33
|
+
if (isExtraKey) {
|
|
34
|
+
delete targetObj[key];
|
|
35
|
+
stats.removed++;
|
|
36
|
+
stats.removedKeys.push(key);
|
|
37
|
+
} else if (
|
|
38
|
+
typeof sourceObj[key] === 'object' &&
|
|
39
|
+
sourceObj[key] !== null &&
|
|
40
|
+
!Array.isArray(sourceObj[key]) &&
|
|
41
|
+
typeof targetObj[key] === 'object' &&
|
|
42
|
+
targetObj[key] !== null &&
|
|
43
|
+
!Array.isArray(targetObj[key])
|
|
44
|
+
) {
|
|
45
|
+
removeExtraKeys(sourceObj[key], targetObj[key], stats);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return stats;
|
|
49
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translation Configuration
|
|
3
|
+
* Language mappings and constants for translation system
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const LANGUAGE_MAP = {
|
|
7
|
+
'ar-SA': 'ar',
|
|
8
|
+
'bg-BG': 'bg',
|
|
9
|
+
'cs-CZ': 'cs',
|
|
10
|
+
'da-DK': 'da',
|
|
11
|
+
'de-DE': 'de',
|
|
12
|
+
'el-GR': 'el',
|
|
13
|
+
'en-AU': 'en',
|
|
14
|
+
'en-CA': 'en',
|
|
15
|
+
'en-GB': 'en',
|
|
16
|
+
'es-ES': 'es',
|
|
17
|
+
'es-MX': 'es',
|
|
18
|
+
'fi-FI': 'fi',
|
|
19
|
+
'fr-CA': 'fr',
|
|
20
|
+
'fr-FR': 'fr',
|
|
21
|
+
'hi-IN': 'hi',
|
|
22
|
+
'hr-HR': 'hr',
|
|
23
|
+
'hu-HU': 'hu',
|
|
24
|
+
'id-ID': 'id',
|
|
25
|
+
'it-IT': 'it',
|
|
26
|
+
'ja-JP': 'ja',
|
|
27
|
+
'ko-KR': 'ko',
|
|
28
|
+
'ms-MY': 'ms',
|
|
29
|
+
'nl-NL': 'nl',
|
|
30
|
+
'no-NO': 'no',
|
|
31
|
+
'pl-PL': 'pl',
|
|
32
|
+
'pt-BR': 'pt',
|
|
33
|
+
'pt-PT': 'pt',
|
|
34
|
+
'ro-RO': 'ro',
|
|
35
|
+
'ru-RU': 'ru',
|
|
36
|
+
'sk-SK': 'sk',
|
|
37
|
+
'sl-SI': 'sl',
|
|
38
|
+
'sv-SE': 'sv',
|
|
39
|
+
'th-TH': 'th',
|
|
40
|
+
'tl-PH': 'tl',
|
|
41
|
+
'tr-TR': 'tr',
|
|
42
|
+
'uk-UA': 'uk',
|
|
43
|
+
'vi-VN': 'vi',
|
|
44
|
+
'zh-CN': 'zh-CN',
|
|
45
|
+
'zh-TW': 'zh-TW',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const SKIP_WORDS = new Set([
|
|
49
|
+
'Google',
|
|
50
|
+
'Apple',
|
|
51
|
+
'Facebook',
|
|
52
|
+
'Instagram',
|
|
53
|
+
'Twitter',
|
|
54
|
+
'YouTube',
|
|
55
|
+
'WhatsApp',
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
export const LANGUAGE_NAMES = {
|
|
59
|
+
'ar-SA': 'Arabic (Saudi Arabia)',
|
|
60
|
+
'bg-BG': 'Bulgarian',
|
|
61
|
+
'cs-CZ': 'Czech',
|
|
62
|
+
'da-DK': 'Danish',
|
|
63
|
+
'de-DE': 'German',
|
|
64
|
+
'el-GR': 'Greek',
|
|
65
|
+
'en-AU': 'English (Australia)',
|
|
66
|
+
'en-CA': 'English (Canada)',
|
|
67
|
+
'en-GB': 'English (UK)',
|
|
68
|
+
'en-US': 'English (US)',
|
|
69
|
+
'es-ES': 'Spanish (Spain)',
|
|
70
|
+
'es-MX': 'Spanish (Mexico)',
|
|
71
|
+
'fi-FI': 'Finnish',
|
|
72
|
+
'fr-CA': 'French (Canada)',
|
|
73
|
+
'fr-FR': 'French (France)',
|
|
74
|
+
'hi-IN': 'Hindi',
|
|
75
|
+
'hr-HR': 'Croatian',
|
|
76
|
+
'hu-HU': 'Hungarian',
|
|
77
|
+
'id-ID': 'Indonesian',
|
|
78
|
+
'it-IT': 'Italian',
|
|
79
|
+
'ja-JP': 'Japanese',
|
|
80
|
+
'ko-KR': 'Korean',
|
|
81
|
+
'ms-MY': 'Malay',
|
|
82
|
+
'nl-NL': 'Dutch',
|
|
83
|
+
'no-NO': 'Norwegian',
|
|
84
|
+
'pl-PL': 'Polish',
|
|
85
|
+
'pt-BR': 'Portuguese (Brazil)',
|
|
86
|
+
'pt-PT': 'Portuguese (Portugal)',
|
|
87
|
+
'ro-RO': 'Romanian',
|
|
88
|
+
'ru-RU': 'Russian',
|
|
89
|
+
'sk-SK': 'Slovak',
|
|
90
|
+
'sl-SI': 'Slovenian',
|
|
91
|
+
'sv-SE': 'Swedish',
|
|
92
|
+
'th-TH': 'Thai',
|
|
93
|
+
'tl-PH': 'Tagalog',
|
|
94
|
+
'tr-TR': 'Turkish',
|
|
95
|
+
'uk-UA': 'Ukrainian',
|
|
96
|
+
'vi-VN': 'Vietnamese',
|
|
97
|
+
'zh-CN': 'Chinese (Simplified)',
|
|
98
|
+
'zh-TW': 'Chinese (Traditional)',
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export function getLangDisplayName(code) {
|
|
102
|
+
return LANGUAGE_NAMES[code] || code;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function getTargetLanguage(langCode) {
|
|
106
|
+
return LANGUAGE_MAP[langCode];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function shouldSkipWord(word) {
|
|
110
|
+
return SKIP_WORDS.has(word);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function isEnglishVariant(langCode) {
|
|
114
|
+
const targetLang = LANGUAGE_MAP[langCode];
|
|
115
|
+
return targetLang === 'en';
|
|
116
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translation Utilities
|
|
3
|
+
* Handles call to translation APIs
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { getTargetLanguage, shouldSkipWord } from './translation-config.js';
|
|
7
|
+
|
|
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;
|
|
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 {
|
|
22
|
+
const encodedText = encodeURIComponent(text);
|
|
23
|
+
const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=en&tl=${targetLang}&dt=t&q=${encodedText}`;
|
|
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
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function needsTranslation(value, enValue) {
|
|
36
|
+
if (typeof enValue !== 'string' || !enValue.trim()) return false;
|
|
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
|
|
45
|
+
if (!value || typeof value !== 'string') return true;
|
|
46
|
+
|
|
47
|
+
if (value === enValue) {
|
|
48
|
+
const isSingleWord = !enValue.includes(' ') && enValue.length < 20;
|
|
49
|
+
return !isSingleWord;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
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) {
|
|
61
|
+
const enValue = enObj[key];
|
|
62
|
+
const targetValue = targetObj[key];
|
|
63
|
+
const currentPath = path ? `${path}.${key}` : key;
|
|
64
|
+
|
|
65
|
+
if (typeof enValue === 'object' && enValue !== null) {
|
|
66
|
+
if (!targetObj[key] || typeof targetObj[key] !== 'object') targetObj[key] = {};
|
|
67
|
+
await translateObject(enValue, targetObj[key], targetLang, currentPath, stats);
|
|
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
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { useCallback } from 'react';
|
|
2
2
|
import type { StyleProp, ViewStyle } from 'react-native';
|
|
3
3
|
import { useAppNavigation } from '@umituz/react-native-design-system';
|
|
4
|
-
import { useLocalization } from '
|
|
4
|
+
import { useLocalization } from '../../../localization';
|
|
5
5
|
import { SettingsItemCard } from '../../../../presentation/components/SettingsItemCard';
|
|
6
6
|
|
|
7
7
|
export interface NotificationsSectionConfig {
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
useAppDesignTokens,
|
|
14
14
|
useAppNavigation
|
|
15
15
|
} from '@umituz/react-native-design-system';
|
|
16
|
-
import { useLocalization } from '
|
|
16
|
+
import { useLocalization } from '../../../localization';
|
|
17
17
|
import { QuietHoursCard } from '../../quietHours/presentation/components/QuietHoursCard';
|
|
18
18
|
import { SettingRow } from '../components/SettingRow';
|
|
19
19
|
import { RemindersNavRow } from '../components/RemindersNavRow';
|
package/src/index.ts
CHANGED
|
@@ -138,6 +138,8 @@ export * from "./domains/dev";
|
|
|
138
138
|
// Gamification Domain - Achievements, levels, streaks
|
|
139
139
|
export * from "./domains/gamification";
|
|
140
140
|
|
|
141
|
+
// Localization Domain - i18n, language selection, translations
|
|
142
|
+
export * from "./domains/localization";
|
|
141
143
|
|
|
142
144
|
// =============================================================================
|
|
143
145
|
// PRESENTATION LAYER - Config Creator Utilities
|
|
@@ -7,7 +7,7 @@ import React, { Component, ReactNode } from 'react';
|
|
|
7
7
|
import { View, StyleSheet } from 'react-native';
|
|
8
8
|
import { useAppDesignTokens } from '@umituz/react-native-design-system';
|
|
9
9
|
import { AtomicText, AtomicIcon } from '@umituz/react-native-design-system';
|
|
10
|
-
import { useLocalization } from '
|
|
10
|
+
import { useLocalization } from '../../domains/localization';
|
|
11
11
|
|
|
12
12
|
interface Props {
|
|
13
13
|
children: ReactNode;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import React from "react";
|
|
9
9
|
import { useAppDesignTokens, StackNavigator, type StackScreen, type StackNavigatorConfig } from "@umituz/react-native-design-system";
|
|
10
|
-
import { useLocalization, LanguageSelectionScreen } from "
|
|
10
|
+
import { useLocalization, LanguageSelectionScreen } from "../../domains/localization";
|
|
11
11
|
import { NotificationSettingsScreen } from "../../domains/notifications";
|
|
12
12
|
import { AccountScreen } from "@umituz/react-native-auth";
|
|
13
13
|
import { AppearanceScreen } from "../screens/AppearanceScreen";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useMemo } from "react";
|
|
2
2
|
import { View, StyleSheet } from "react-native";
|
|
3
|
-
import { useLocalization } from "
|
|
3
|
+
import { useLocalization } from "../../../domains/localization";
|
|
4
4
|
import { SettingsFooter } from "../../components/SettingsFooter";
|
|
5
5
|
import { SettingsSection } from "../../components/SettingsSection";
|
|
6
6
|
import { DevSettingsSection, DevSettingsProps } from "../../../domains/dev";
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import React from "react";
|
|
7
7
|
import { View, Pressable, StyleSheet } from "react-native";
|
|
8
8
|
import { useAppDesignTokens, AtomicIcon, AtomicText, useAppNavigation } from "@umituz/react-native-design-system";
|
|
9
|
-
import { useLocalization } from "
|
|
9
|
+
import { useLocalization } from "../../../domains/localization";
|
|
10
10
|
|
|
11
11
|
interface SettingsHeaderProps {
|
|
12
12
|
showCloseButton?: boolean;
|
|
@@ -2,7 +2,7 @@ import React from "react";
|
|
|
2
2
|
import { useAppNavigation } from "@umituz/react-native-design-system";
|
|
3
3
|
import { AppearanceSection } from "../../../../domains/appearance/presentation/components/AppearanceSection";
|
|
4
4
|
import { NotificationsSection } from "../../../../domains/notifications";
|
|
5
|
-
import { useLocalization, getLanguageByCode } from "
|
|
5
|
+
import { useLocalization, getLanguageByCode } from "../../../../domains/localization";
|
|
6
6
|
import { SettingsItemCard } from "../../../components/SettingsItemCard";
|
|
7
7
|
import type { NormalizedConfig } from "../../utils/normalizeConfig";
|
|
8
8
|
|