@umituz/react-native-localization 3.1.2 → 3.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-localization",
|
|
3
|
-
"version": "3.1
|
|
3
|
+
"version": "3.2.1",
|
|
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",
|
|
@@ -38,7 +38,9 @@
|
|
|
38
38
|
"zustand": "^5.0.2"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
|
+
"@expo/vector-icons": ">=14.0.0",
|
|
41
42
|
"@react-navigation/native": ">=6.0.0",
|
|
43
|
+
"@umituz/react-native-design-system-theme": "*",
|
|
42
44
|
"@umituz/react-native-filesystem": "latest",
|
|
43
45
|
"@umituz/react-native-storage": "latest",
|
|
44
46
|
"react": ">=18.2.0",
|
|
@@ -53,6 +55,7 @@
|
|
|
53
55
|
"@babel/preset-env": "^7.28.5",
|
|
54
56
|
"@babel/preset-react": "^7.28.5",
|
|
55
57
|
"@babel/preset-typescript": "^7.28.5",
|
|
58
|
+
"@expo/vector-icons": "^15.0.3",
|
|
56
59
|
"@react-native-async-storage/async-storage": "^2.2.0",
|
|
57
60
|
"@testing-library/jest-native": "^5.4.3",
|
|
58
61
|
"@testing-library/react": "^16.3.0",
|
|
@@ -60,6 +63,7 @@
|
|
|
60
63
|
"@testing-library/react-native": "^13.3.3",
|
|
61
64
|
"@types/react": "^18.2.45",
|
|
62
65
|
"@types/react-native": "^0.73.0",
|
|
66
|
+
"@umituz/react-native-design-system-theme": "^1.8.0",
|
|
63
67
|
"@umituz/react-native-filesystem": "^1.4.0",
|
|
64
68
|
"@umituz/react-native-storage": "^1.1.1",
|
|
65
69
|
"expo-localization": "~15.0.0",
|
|
@@ -86,4 +90,4 @@
|
|
|
86
90
|
"README.md",
|
|
87
91
|
"LICENSE"
|
|
88
92
|
]
|
|
89
|
-
}
|
|
93
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -29,6 +29,8 @@ export {
|
|
|
29
29
|
|
|
30
30
|
// Presentation
|
|
31
31
|
export { LanguageSelectionScreen } from './presentation/screens/LanguageSelectionScreen';
|
|
32
|
+
export { LanguageSection } from './presentation/components/LanguageSection';
|
|
33
|
+
export type { LanguageSectionProps, LanguageSectionConfig } from './presentation/components/LanguageSection';
|
|
32
34
|
|
|
33
35
|
// Types
|
|
34
36
|
export type { Language, ILocalizationRepository } from './domain/repositories/ILocalizationRepository';
|
|
@@ -18,9 +18,27 @@ export class NamespaceResolver {
|
|
|
18
18
|
const packageTranslations = TranslationLoader.loadPackageTranslations();
|
|
19
19
|
const packageLang = packageTranslations[languageCode] || {};
|
|
20
20
|
|
|
21
|
+
let appNamespaces: string[] = [];
|
|
22
|
+
|
|
23
|
+
// Check if appTranslations has language keys (format: xx-XX)
|
|
24
|
+
const hasLanguageKeys = Object.keys(appTranslations).some(key =>
|
|
25
|
+
/^[a-z]{2}-[A-Z]{2}$/.test(key)
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
if (hasLanguageKeys) {
|
|
29
|
+
// If structured by language, get namespaces from the requested language
|
|
30
|
+
const langTranslations = appTranslations[languageCode];
|
|
31
|
+
if (langTranslations && typeof langTranslations === 'object') {
|
|
32
|
+
appNamespaces = Object.keys(langTranslations);
|
|
33
|
+
}
|
|
34
|
+
} else {
|
|
35
|
+
// If structured by namespace (legacy/simple), keys are namespaces
|
|
36
|
+
appNamespaces = Object.keys(appTranslations);
|
|
37
|
+
}
|
|
38
|
+
|
|
21
39
|
const namespaces = new Set([
|
|
22
40
|
...Object.keys(packageLang),
|
|
23
|
-
...
|
|
41
|
+
...appNamespaces,
|
|
24
42
|
]);
|
|
25
43
|
|
|
26
44
|
if (!namespaces.has(DEFAULT_NAMESPACE)) {
|
|
@@ -15,26 +15,43 @@ export class ResourceBuilder {
|
|
|
15
15
|
): Record<string, Record<string, any>> {
|
|
16
16
|
const packageTranslations = TranslationLoader.loadPackageTranslations();
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
};
|
|
18
|
+
// Initialize with package translations
|
|
19
|
+
const resources: Record<string, Record<string, any>> = { ...packageTranslations };
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
for (const [namespace, translations] of Object.entries(packageLang)) {
|
|
26
|
-
resources[languageCode][namespace] = translations;
|
|
21
|
+
// Ensure initial language exists
|
|
22
|
+
if (!resources[languageCode]) {
|
|
23
|
+
resources[languageCode] = {};
|
|
27
24
|
}
|
|
28
25
|
|
|
29
|
-
//
|
|
30
|
-
for (const [
|
|
31
|
-
if (
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
)
|
|
26
|
+
// Process app translations
|
|
27
|
+
for (const [key, value] of Object.entries(appTranslations)) {
|
|
28
|
+
// Check if the key is a language code (format: xx-XX)
|
|
29
|
+
const isLanguageKey = /^[a-z]{2}-[A-Z]{2}$/.test(key);
|
|
30
|
+
|
|
31
|
+
if (isLanguageKey) {
|
|
32
|
+
// It's a language key (e.g., "en-US")
|
|
33
|
+
const lang = key;
|
|
34
|
+
if (!resources[lang]) {
|
|
35
|
+
resources[lang] = {};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Merge namespaces for this language
|
|
39
|
+
if (value && typeof value === 'object') {
|
|
40
|
+
for (const [namespace, translations] of Object.entries(value)) {
|
|
41
|
+
resources[lang][namespace] = TranslationLoader.mergeTranslations(
|
|
42
|
+
resources[lang][namespace] || {},
|
|
43
|
+
translations
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
36
47
|
} else {
|
|
37
|
-
|
|
48
|
+
// It's a namespace for the default/current language (backward compatibility)
|
|
49
|
+
if (value && typeof value === 'object') {
|
|
50
|
+
resources[languageCode][key] = TranslationLoader.mergeTranslations(
|
|
51
|
+
resources[languageCode][key] || {},
|
|
52
|
+
value
|
|
53
|
+
);
|
|
54
|
+
}
|
|
38
55
|
}
|
|
39
56
|
}
|
|
40
57
|
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, Text, Pressable, StyleSheet, ViewStyle } from 'react-native';
|
|
3
|
+
import { Feather } from '@expo/vector-icons';
|
|
4
|
+
import { useNavigation } from '@react-navigation/native';
|
|
5
|
+
import { useAppDesignTokens } from '@umituz/react-native-design-system-theme';
|
|
6
|
+
import { useLocalization } from '../../infrastructure/hooks/useLocalization';
|
|
7
|
+
import { getLanguageByCode } from '../../infrastructure/config/languages';
|
|
8
|
+
|
|
9
|
+
export interface LanguageSectionConfig {
|
|
10
|
+
route?: string;
|
|
11
|
+
title?: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
defaultLanguageDisplay?: string;
|
|
14
|
+
// actions?
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface LanguageSectionProps {
|
|
18
|
+
config?: LanguageSectionConfig;
|
|
19
|
+
containerStyle?: ViewStyle;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const LanguageSection: React.FC<LanguageSectionProps> = ({
|
|
23
|
+
config,
|
|
24
|
+
containerStyle,
|
|
25
|
+
}) => {
|
|
26
|
+
const navigation = useNavigation();
|
|
27
|
+
const tokens = useAppDesignTokens();
|
|
28
|
+
const colors = tokens.colors;
|
|
29
|
+
const { t, currentLanguage } = useLocalization();
|
|
30
|
+
|
|
31
|
+
const route = config?.route || 'LanguageSelection';
|
|
32
|
+
const title = config?.title || t('settings.language') || 'Language';
|
|
33
|
+
const description = config?.description || '';
|
|
34
|
+
|
|
35
|
+
const currentLang = getLanguageByCode(currentLanguage);
|
|
36
|
+
const defaultLanguageDisplay = config?.defaultLanguageDisplay || 'English';
|
|
37
|
+
const languageDisplay = currentLang
|
|
38
|
+
? `${currentLang.flag} ${currentLang.nativeName}`
|
|
39
|
+
: defaultLanguageDisplay;
|
|
40
|
+
|
|
41
|
+
const handlePress = () => {
|
|
42
|
+
navigation.navigate(route as never);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<View style={[styles.sectionContainer, { backgroundColor: colors.surface }, containerStyle]}>
|
|
47
|
+
<Text style={[styles.sectionTitle, { color: colors.textPrimary }]}>{t("settings.sections.app.title") || "App"}</Text>
|
|
48
|
+
|
|
49
|
+
<Pressable
|
|
50
|
+
style={({ pressed }) => [
|
|
51
|
+
styles.itemContainer,
|
|
52
|
+
{
|
|
53
|
+
backgroundColor: pressed ? `${colors.primary}08` : 'transparent',
|
|
54
|
+
},
|
|
55
|
+
]}
|
|
56
|
+
onPress={handlePress}
|
|
57
|
+
>
|
|
58
|
+
<View style={styles.content}>
|
|
59
|
+
<View
|
|
60
|
+
style={[
|
|
61
|
+
styles.iconContainer,
|
|
62
|
+
{ backgroundColor: `${colors.primary}15` },
|
|
63
|
+
]}
|
|
64
|
+
>
|
|
65
|
+
<Feather name="globe" size={24} color={colors.primary} />
|
|
66
|
+
</View>
|
|
67
|
+
<View style={styles.textContainer}>
|
|
68
|
+
<Text style={[styles.title, { color: colors.textPrimary }]}>{title}</Text>
|
|
69
|
+
<Text style={[styles.description, { color: colors.textSecondary }]}>
|
|
70
|
+
{languageDisplay}
|
|
71
|
+
</Text>
|
|
72
|
+
</View>
|
|
73
|
+
<Feather name="chevron-right" size={20} color={colors.textSecondary} />
|
|
74
|
+
</View>
|
|
75
|
+
</Pressable>
|
|
76
|
+
</View>
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const styles = StyleSheet.create({
|
|
81
|
+
sectionContainer: {
|
|
82
|
+
marginBottom: 16,
|
|
83
|
+
borderRadius: 12,
|
|
84
|
+
overflow: 'hidden',
|
|
85
|
+
},
|
|
86
|
+
sectionTitle: {
|
|
87
|
+
fontSize: 18,
|
|
88
|
+
fontWeight: '600',
|
|
89
|
+
paddingHorizontal: 16,
|
|
90
|
+
paddingTop: 16,
|
|
91
|
+
paddingBottom: 8,
|
|
92
|
+
},
|
|
93
|
+
itemContainer: {
|
|
94
|
+
flexDirection: 'row',
|
|
95
|
+
alignItems: 'center',
|
|
96
|
+
paddingHorizontal: 16,
|
|
97
|
+
paddingVertical: 16,
|
|
98
|
+
minHeight: 72,
|
|
99
|
+
},
|
|
100
|
+
content: {
|
|
101
|
+
flex: 1,
|
|
102
|
+
flexDirection: 'row',
|
|
103
|
+
alignItems: 'center',
|
|
104
|
+
},
|
|
105
|
+
iconContainer: {
|
|
106
|
+
width: 48,
|
|
107
|
+
height: 48,
|
|
108
|
+
borderRadius: 12,
|
|
109
|
+
justifyContent: 'center',
|
|
110
|
+
alignItems: 'center',
|
|
111
|
+
marginRight: 16,
|
|
112
|
+
},
|
|
113
|
+
textContainer: {
|
|
114
|
+
flex: 1,
|
|
115
|
+
marginRight: 8,
|
|
116
|
+
},
|
|
117
|
+
title: {
|
|
118
|
+
fontSize: 16,
|
|
119
|
+
fontWeight: '500',
|
|
120
|
+
marginBottom: 4,
|
|
121
|
+
},
|
|
122
|
+
description: {
|
|
123
|
+
fontSize: 14,
|
|
124
|
+
},
|
|
125
|
+
});
|