@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
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-settings",
|
|
3
|
-
"version": "4.23.
|
|
4
|
-
"description": "Complete settings hub for React Native apps - consolidated package with settings, about, legal, appearance, feedback, FAQs, rating, and gamification",
|
|
3
|
+
"version": "4.23.37",
|
|
4
|
+
"description": "Complete settings hub for React Native apps - consolidated package with settings, localization, about, legal, appearance, feedback, FAQs, rating, and gamification",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
7
7
|
"scripts": {
|
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
"user-settings",
|
|
18
18
|
"theme",
|
|
19
19
|
"language",
|
|
20
|
+
"localization",
|
|
21
|
+
"i18n",
|
|
22
|
+
"translation",
|
|
20
23
|
"notifications",
|
|
21
24
|
"about",
|
|
22
25
|
"legal",
|
|
@@ -40,17 +43,22 @@
|
|
|
40
43
|
},
|
|
41
44
|
"peerDependencies": {
|
|
42
45
|
"@expo/vector-icons": ">=14.0.0",
|
|
46
|
+
"@react-native-async-storage/async-storage": ">=2.0.0",
|
|
43
47
|
"@react-navigation/native": ">=6.0.0",
|
|
44
48
|
"@react-navigation/stack": ">=6.0.0",
|
|
45
49
|
"@tanstack/react-query": ">=5.0.0",
|
|
46
50
|
"expo-notifications": ">=0.28.0",
|
|
47
51
|
"expo-device": ">=6.0.0",
|
|
48
52
|
"expo-haptics": ">=15.0.0",
|
|
53
|
+
"expo-localization": ">=16.0.0",
|
|
49
54
|
"expo-store-review": ">=14.0.0",
|
|
55
|
+
"i18next": ">=23.0.0",
|
|
56
|
+
"react-i18next": ">=15.0.0",
|
|
50
57
|
"@react-native-community/datetimepicker": ">=8.0.0",
|
|
51
58
|
"react": ">=19.0.0",
|
|
52
59
|
"react-native": ">=0.81.0",
|
|
53
|
-
"react-native-safe-area-context": ">=4.0.0"
|
|
60
|
+
"react-native-safe-area-context": ">=4.0.0",
|
|
61
|
+
"zustand": ">=4.0.0"
|
|
54
62
|
},
|
|
55
63
|
"devDependencies": {
|
|
56
64
|
"@expo/vector-icons": "^15.0.0",
|
|
@@ -69,7 +77,6 @@
|
|
|
69
77
|
"@typescript-eslint/parser": "^7.18.0",
|
|
70
78
|
"@umituz/react-native-auth": "*",
|
|
71
79
|
"@umituz/react-native-firebase": "*",
|
|
72
|
-
"@umituz/react-native-localization": "*",
|
|
73
80
|
"@umituz/react-native-sentry": "*",
|
|
74
81
|
"eslint": "^8.57.0",
|
|
75
82
|
"eslint-plugin-react": "^7.37.5",
|
|
@@ -92,7 +99,7 @@
|
|
|
92
99
|
"expo-sharing": "^14.0.8",
|
|
93
100
|
"expo-notifications": "~0.27.6",
|
|
94
101
|
"expo-secure-store": "^15.0.8",
|
|
95
|
-
"expo-store-review": "
|
|
102
|
+
"expo-store-review": "^8.0.0",
|
|
96
103
|
"expo-video": "^3.0.15",
|
|
97
104
|
"expo-web-browser": "^12.0.0",
|
|
98
105
|
"firebase": "^12.7.0",
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
NavigationHeader,
|
|
13
13
|
useAppNavigation
|
|
14
14
|
} from "@umituz/react-native-design-system";
|
|
15
|
-
import { useLocalization } from "
|
|
15
|
+
import { useLocalization } from "../../../localization";
|
|
16
16
|
import { useAppearance, useAppearanceActions } from "../../hooks";
|
|
17
17
|
import {
|
|
18
18
|
AppearanceHeader,
|
|
@@ -7,7 +7,7 @@ import { render, fireEvent } from '@testing-library/react-native';
|
|
|
7
7
|
import { DisclaimerSetting } from '../DisclaimerSetting';
|
|
8
8
|
|
|
9
9
|
// Mock dependencies
|
|
10
|
-
jest.mock('
|
|
10
|
+
jest.mock('../../../localization', () => ({
|
|
11
11
|
useLocalization: () => ({
|
|
12
12
|
t: (key: string) => key,
|
|
13
13
|
}),
|
|
@@ -21,7 +21,7 @@ import React, { useState, useEffect, useCallback } from 'react';
|
|
|
21
21
|
import { Modal } from 'react-native';
|
|
22
22
|
|
|
23
23
|
import { useAppDesignTokens, withAlpha } from '@umituz/react-native-design-system';
|
|
24
|
-
import { useLocalization } from '
|
|
24
|
+
import { useLocalization } from '../../../localization';
|
|
25
25
|
import { DisclaimerCard } from './DisclaimerCard';
|
|
26
26
|
import { DisclaimerModal } from './DisclaimerModal';
|
|
27
27
|
|
|
@@ -18,7 +18,7 @@ import { SafeAreaView } from 'react-native-safe-area-context';
|
|
|
18
18
|
|
|
19
19
|
import { useAppDesignTokens, withAlpha } from '@umituz/react-native-design-system';
|
|
20
20
|
import { AtomicText, AtomicIcon } from '@umituz/react-native-design-system';
|
|
21
|
-
import { useLocalization } from '
|
|
21
|
+
import { useLocalization } from '../../../localization';
|
|
22
22
|
|
|
23
23
|
export interface DisclaimerScreenProps {
|
|
24
24
|
/** Custom title (overrides translation) */
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Localization Repository Interface
|
|
3
|
+
* Defines language configuration and types
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface Language {
|
|
7
|
+
code: string;
|
|
8
|
+
name: string;
|
|
9
|
+
nativeName: string;
|
|
10
|
+
flag?: string;
|
|
11
|
+
isRTL?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ILocalizationRepository {
|
|
15
|
+
getSupportedLanguages(): Language[];
|
|
16
|
+
getLanguageByCode(code: string): Language | undefined;
|
|
17
|
+
isLanguageSupported(code: string): boolean;
|
|
18
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Hooks
|
|
2
|
+
export { useLocalization } from './infrastructure/hooks/useLocalization';
|
|
3
|
+
export { useLocalizationStore } from './infrastructure/storage/LocalizationStore';
|
|
4
|
+
export { useTranslationFunction } from './infrastructure/hooks/useTranslation';
|
|
5
|
+
export { useLanguageSelection } from './infrastructure/hooks/useLanguageSelection';
|
|
6
|
+
export { useLanguageSwitcher } from './infrastructure/components/useLanguageSwitcher';
|
|
7
|
+
export { useLanguageNavigation } from './infrastructure/components/useLanguageNavigation';
|
|
8
|
+
|
|
9
|
+
// Components
|
|
10
|
+
export { LanguageSwitcher } from './infrastructure/components/LanguageSwitcher';
|
|
11
|
+
|
|
12
|
+
// Screens
|
|
13
|
+
export { LanguageSelectionScreen } from './presentation/screens/LanguageSelectionScreen';
|
|
14
|
+
export type { LanguageSelectionScreenProps } from './presentation/screens/LanguageSelectionScreen.types';
|
|
15
|
+
export { LanguageSection } from './presentation/components/LanguageSection';
|
|
16
|
+
export type { LanguageSectionProps, LanguageSectionConfig } from './presentation/components/LanguageSection';
|
|
17
|
+
|
|
18
|
+
// Configuration
|
|
19
|
+
export { default as i18n } from './infrastructure/config/i18n';
|
|
20
|
+
export { I18nInitializer } from './infrastructure/config/I18nInitializer';
|
|
21
|
+
export {
|
|
22
|
+
SUPPORTED_LANGUAGES,
|
|
23
|
+
LANGUAGES,
|
|
24
|
+
DEFAULT_LANGUAGE,
|
|
25
|
+
getLanguageByCode,
|
|
26
|
+
isLanguageSupported,
|
|
27
|
+
getDefaultLanguage,
|
|
28
|
+
getDeviceLocale,
|
|
29
|
+
searchLanguages,
|
|
30
|
+
} from './infrastructure/config/languages';
|
|
31
|
+
|
|
32
|
+
// Types
|
|
33
|
+
export type { Language, ILocalizationRepository } from './domain/repositories/ILocalizationRepository';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Language Switcher Component Styles
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { StyleSheet } from 'react-native';
|
|
6
|
+
|
|
7
|
+
const DEFAULT_CONFIG = {
|
|
8
|
+
defaultIconSize: 20,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const styles = StyleSheet.create({
|
|
12
|
+
container: {
|
|
13
|
+
flexDirection: 'row',
|
|
14
|
+
alignItems: 'center',
|
|
15
|
+
gap: 8,
|
|
16
|
+
paddingHorizontal: 4,
|
|
17
|
+
paddingVertical: 4,
|
|
18
|
+
},
|
|
19
|
+
disabled: {
|
|
20
|
+
opacity: 0.5,
|
|
21
|
+
},
|
|
22
|
+
flag: {
|
|
23
|
+
fontSize: DEFAULT_CONFIG.defaultIconSize,
|
|
24
|
+
textAlign: 'center',
|
|
25
|
+
},
|
|
26
|
+
languageName: {
|
|
27
|
+
fontSize: 14,
|
|
28
|
+
fontWeight: '600',
|
|
29
|
+
textAlign: 'center',
|
|
30
|
+
},
|
|
31
|
+
icon: {
|
|
32
|
+
fontSize: DEFAULT_CONFIG.defaultIconSize,
|
|
33
|
+
textAlign: 'center',
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export const DEFAULT_CONFIG_VALUES = {
|
|
38
|
+
hitSlop: { top: 10, bottom: 10, left: 10, right: 10 } as const,
|
|
39
|
+
activeOpacity: 0.7,
|
|
40
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Language Switcher Component
|
|
3
|
+
* Displays current language and allows switching
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useMemo } from 'react';
|
|
7
|
+
import { TouchableOpacity } from 'react-native';
|
|
8
|
+
// @ts-ignore - Optional peer dependency
|
|
9
|
+
import { useAppDesignTokens, AtomicText } from '@umituz/react-native-design-system';
|
|
10
|
+
import { useLanguageSwitcher } from './useLanguageSwitcher';
|
|
11
|
+
import { styles, DEFAULT_CONFIG_VALUES } from './LanguageSwitcher.styles';
|
|
12
|
+
|
|
13
|
+
export interface LanguageSwitcherProps {
|
|
14
|
+
showName?: boolean;
|
|
15
|
+
showFlag?: boolean;
|
|
16
|
+
color?: string;
|
|
17
|
+
onPress?: () => void;
|
|
18
|
+
style?: any;
|
|
19
|
+
textStyle?: any;
|
|
20
|
+
iconStyle?: any;
|
|
21
|
+
testID?: string;
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
accessibilityLabel?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const LanguageSwitcher: React.FC<LanguageSwitcherProps> = ({
|
|
27
|
+
showName = false,
|
|
28
|
+
showFlag = true,
|
|
29
|
+
color,
|
|
30
|
+
onPress,
|
|
31
|
+
style,
|
|
32
|
+
textStyle,
|
|
33
|
+
iconStyle,
|
|
34
|
+
testID = 'language-switcher',
|
|
35
|
+
disabled = false,
|
|
36
|
+
accessibilityLabel,
|
|
37
|
+
}) => {
|
|
38
|
+
const tokens = useAppDesignTokens();
|
|
39
|
+
const { currentLang, handlePress } = useLanguageSwitcher({ onPress, disabled });
|
|
40
|
+
|
|
41
|
+
const accessibilityProps = useMemo(() => ({
|
|
42
|
+
accessibilityRole: 'button' as const,
|
|
43
|
+
accessibilityLabel: accessibilityLabel || `Current language: ${currentLang.nativeName}`,
|
|
44
|
+
accessibilityHint: disabled ? undefined : 'Double tap to change language',
|
|
45
|
+
accessible: true,
|
|
46
|
+
}), [accessibilityLabel, currentLang.nativeName, disabled]);
|
|
47
|
+
|
|
48
|
+
const textColor = color || tokens.colors.textPrimary;
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<TouchableOpacity
|
|
52
|
+
style={[
|
|
53
|
+
styles.container,
|
|
54
|
+
{ gap: tokens.spacing.xs },
|
|
55
|
+
style,
|
|
56
|
+
disabled && styles.disabled
|
|
57
|
+
]}
|
|
58
|
+
onPress={handlePress}
|
|
59
|
+
activeOpacity={disabled ? 1 : DEFAULT_CONFIG_VALUES.activeOpacity}
|
|
60
|
+
hitSlop={DEFAULT_CONFIG_VALUES.hitSlop}
|
|
61
|
+
testID={testID}
|
|
62
|
+
disabled={disabled}
|
|
63
|
+
{...accessibilityProps}
|
|
64
|
+
>
|
|
65
|
+
{showFlag && (
|
|
66
|
+
<AtomicText style={[styles.flag, iconStyle]}>
|
|
67
|
+
{currentLang.flag || '🌐'}
|
|
68
|
+
</AtomicText>
|
|
69
|
+
)}
|
|
70
|
+
{showName && (
|
|
71
|
+
<AtomicText
|
|
72
|
+
type="bodySmall"
|
|
73
|
+
style={[styles.languageName, { color: textColor, fontWeight: '600' }, textStyle]}
|
|
74
|
+
>
|
|
75
|
+
{currentLang.nativeName}
|
|
76
|
+
</AtomicText>
|
|
77
|
+
)}
|
|
78
|
+
{!showFlag && !showName && (
|
|
79
|
+
<AtomicText style={[styles.icon, { color: textColor }, iconStyle]}>
|
|
80
|
+
🌐
|
|
81
|
+
</AtomicText>
|
|
82
|
+
)}
|
|
83
|
+
</TouchableOpacity>
|
|
84
|
+
);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export default LanguageSwitcher;
|
|
88
|
+
|
package/src/domains/localization/infrastructure/components/__tests__/LanguageSwitcher.test.tsx
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple tests for LanguageSwitcher component
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import React from 'react';
|
|
6
|
+
import { render } from '@testing-library/react-native';
|
|
7
|
+
import { LanguageSwitcher } from '../LanguageSwitcher';
|
|
8
|
+
|
|
9
|
+
// Mock React Native
|
|
10
|
+
jest.mock('react-native', () => ({
|
|
11
|
+
View: 'View',
|
|
12
|
+
Text: 'Text',
|
|
13
|
+
TouchableOpacity: 'TouchableOpacity',
|
|
14
|
+
StyleSheet: {
|
|
15
|
+
create: jest.fn(() => ({})),
|
|
16
|
+
flatten: jest.fn((styles) => styles),
|
|
17
|
+
},
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
// Mock hooks
|
|
21
|
+
jest.mock('../../hooks/useLocalization', () => ({
|
|
22
|
+
useLocalization: () => ({
|
|
23
|
+
currentLanguage: 'en-US',
|
|
24
|
+
}),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
jest.mock('../../repository/LanguageRepository', () => ({
|
|
28
|
+
languageRepository: {
|
|
29
|
+
getLanguageByCode: jest.fn(() => ({
|
|
30
|
+
code: 'en-US',
|
|
31
|
+
name: 'English',
|
|
32
|
+
nativeName: 'English',
|
|
33
|
+
flag: '🇺🇸',
|
|
34
|
+
})),
|
|
35
|
+
getDefaultLanguage: () => ({
|
|
36
|
+
code: 'en-US',
|
|
37
|
+
name: 'English',
|
|
38
|
+
nativeName: 'English',
|
|
39
|
+
flag: '🇺🇸',
|
|
40
|
+
}),
|
|
41
|
+
},
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
describe('LanguageSwitcher', () => {
|
|
45
|
+
it('should render correctly', () => {
|
|
46
|
+
const { getByTestId } = render(
|
|
47
|
+
<LanguageSwitcher testID="language-switcher" />
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
expect(getByTestId('language-switcher')).toBeTruthy();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should render with flag', () => {
|
|
54
|
+
const { getByText } = render(
|
|
55
|
+
<LanguageSwitcher testID="language-switcher" showFlag />
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
expect(getByText('🇺🇸')).toBeTruthy();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should render with name', () => {
|
|
62
|
+
const { getByText } = render(
|
|
63
|
+
<LanguageSwitcher testID="language-switcher" showName />
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
expect(getByText('English')).toBeTruthy();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should handle onPress', () => {
|
|
70
|
+
const mockOnPress = jest.fn();
|
|
71
|
+
const { getByTestId } = render(
|
|
72
|
+
<LanguageSwitcher testID="language-switcher" onPress={mockOnPress} />
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
// Simulate press
|
|
76
|
+
const component = getByTestId('language-switcher');
|
|
77
|
+
component.props.onPress();
|
|
78
|
+
|
|
79
|
+
expect(mockOnPress).toHaveBeenCalledTimes(1);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should be accessible', () => {
|
|
83
|
+
const { getByTestId } = render(
|
|
84
|
+
<LanguageSwitcher testID="language-switcher" />
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const component = getByTestId('language-switcher');
|
|
88
|
+
expect(component.props.accessible).toBe(true);
|
|
89
|
+
expect(component.props.accessibilityRole).toBe('button');
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// @ts-ignore - Optional peer dependency
|
|
2
|
+
import { useAppNavigation } from '@umituz/react-native-design-system';
|
|
3
|
+
import { useLocalization } from '../hooks/useLocalization';
|
|
4
|
+
import { languageRepository } from '../repository/LanguageRepository';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
export const useLanguageNavigation = (navigationScreen: string) => {
|
|
8
|
+
const navigation = useAppNavigation();
|
|
9
|
+
const { currentLanguage } = useLocalization();
|
|
10
|
+
const currentLang = languageRepository.getLanguageByCode(currentLanguage) || languageRepository.getDefaultLanguage();
|
|
11
|
+
|
|
12
|
+
const navigateToLanguageSelection = () => {
|
|
13
|
+
if (navigation && navigationScreen) {
|
|
14
|
+
navigation.navigate(navigationScreen as never);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
return { currentLang, navigateToLanguageSelection };
|
|
19
|
+
};
|
|
20
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Language Switcher Hook
|
|
3
|
+
* Manages the logic for the LanguageSwitcher component
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useMemo, useCallback } from 'react';
|
|
7
|
+
import { useLocalization } from '../hooks/useLocalization';
|
|
8
|
+
import { languageRepository } from '../repository/LanguageRepository';
|
|
9
|
+
import type { Language } from '../storage/types/Language';
|
|
10
|
+
|
|
11
|
+
export interface UseLanguageSwitcherProps {
|
|
12
|
+
onPress?: () => void;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const useLanguageSwitcher = ({ onPress, disabled }: UseLanguageSwitcherProps) => {
|
|
17
|
+
const { currentLanguage } = useLocalization();
|
|
18
|
+
|
|
19
|
+
const currentLang = useMemo((): Language => {
|
|
20
|
+
return languageRepository.getLanguageByCode(currentLanguage) || languageRepository.getDefaultLanguage();
|
|
21
|
+
}, [currentLanguage]);
|
|
22
|
+
|
|
23
|
+
const handlePress = useCallback(() => {
|
|
24
|
+
if (disabled) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
onPress?.();
|
|
28
|
+
}, [disabled, onPress]);
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
currentLang,
|
|
32
|
+
handlePress,
|
|
33
|
+
};
|
|
34
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device Locale Detection
|
|
3
|
+
* Detects and maps device locale to supported language
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as Localization from 'expo-localization';
|
|
7
|
+
import { LOCALE_MAPPING } from './LocaleMapping';
|
|
8
|
+
import { languageRepository } from '../repository/LanguageRepository';
|
|
9
|
+
|
|
10
|
+
export const DEFAULT_LANGUAGE = 'en-US';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Get device locale and map it to supported language
|
|
14
|
+
* Called ONLY on first launch (when no saved language preference exists)
|
|
15
|
+
*/
|
|
16
|
+
export const getDeviceLocale = (): string => {
|
|
17
|
+
try {
|
|
18
|
+
const deviceLocale = Localization.getLocales()[0]?.languageTag;
|
|
19
|
+
|
|
20
|
+
if (!deviceLocale) {
|
|
21
|
+
return DEFAULT_LANGUAGE;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Check exact match
|
|
25
|
+
if (LOCALE_MAPPING[deviceLocale]) {
|
|
26
|
+
return LOCALE_MAPPING[deviceLocale];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Extract language code
|
|
30
|
+
const languageCode = deviceLocale.split('-')[0];
|
|
31
|
+
const mappedByCode = languageCode ? LOCALE_MAPPING[languageCode] : undefined;
|
|
32
|
+
|
|
33
|
+
// Check language code
|
|
34
|
+
if (mappedByCode) {
|
|
35
|
+
return mappedByCode;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Check if directly supported
|
|
39
|
+
if (languageRepository.isLanguageSupported(deviceLocale)) {
|
|
40
|
+
return deviceLocale;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return DEFAULT_LANGUAGE;
|
|
44
|
+
} catch {
|
|
45
|
+
return DEFAULT_LANGUAGE;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* i18n Initializer
|
|
3
|
+
*
|
|
4
|
+
* Namespace-based i18n configuration
|
|
5
|
+
* Usage: t('namespace:key') e.g., t('common:cancel')
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import i18n from 'i18next';
|
|
9
|
+
import { initReactI18next } from 'react-i18next';
|
|
10
|
+
import { DEFAULT_LANGUAGE } from './languages';
|
|
11
|
+
import { ResourceBuilder } from './ResourceBuilder';
|
|
12
|
+
import { NamespaceResolver } from './NamespaceResolver';
|
|
13
|
+
|
|
14
|
+
export class I18nInitializer {
|
|
15
|
+
private static reactI18nextInitialized = false;
|
|
16
|
+
|
|
17
|
+
static initialize(
|
|
18
|
+
appTranslations: Record<string, any>,
|
|
19
|
+
languageCode: string = DEFAULT_LANGUAGE
|
|
20
|
+
): void {
|
|
21
|
+
if (i18n.isInitialized) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
if (!this.reactI18nextInitialized) {
|
|
27
|
+
i18n.use(initReactI18next);
|
|
28
|
+
this.reactI18nextInitialized = true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const resources = ResourceBuilder.buildResources(appTranslations, languageCode);
|
|
32
|
+
const namespaces = NamespaceResolver.getNamespaces(appTranslations, languageCode);
|
|
33
|
+
const defaultNamespace = NamespaceResolver.getDefaultNamespace();
|
|
34
|
+
|
|
35
|
+
i18n.init({
|
|
36
|
+
resources,
|
|
37
|
+
lng: languageCode,
|
|
38
|
+
fallbackLng: DEFAULT_LANGUAGE,
|
|
39
|
+
ns: namespaces,
|
|
40
|
+
defaultNS: defaultNamespace,
|
|
41
|
+
fallbackNS: defaultNamespace,
|
|
42
|
+
interpolation: { escapeValue: false },
|
|
43
|
+
react: { useSuspense: false },
|
|
44
|
+
compatibilityJSON: 'v4',
|
|
45
|
+
pluralSeparator: '_',
|
|
46
|
+
keySeparator: '.',
|
|
47
|
+
nsSeparator: ':',
|
|
48
|
+
saveMissing: false,
|
|
49
|
+
missingKeyHandler: false,
|
|
50
|
+
debug: false,
|
|
51
|
+
returnEmptyString: false,
|
|
52
|
+
returnNull: false,
|
|
53
|
+
});
|
|
54
|
+
} catch {
|
|
55
|
+
// Silent error handling
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Add translations at runtime
|
|
61
|
+
*/
|
|
62
|
+
static addTranslations(
|
|
63
|
+
languageCode: string,
|
|
64
|
+
namespaceResources: Record<string, any>
|
|
65
|
+
): void {
|
|
66
|
+
for (const [namespace, translations] of Object.entries(namespaceResources)) {
|
|
67
|
+
if (translations && typeof translations === 'object') {
|
|
68
|
+
i18n.addResourceBundle(languageCode, namespace, translations, true, true);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Language Query Functions
|
|
3
|
+
* Provides functions to query and search languages
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { languageRepository } from '../repository/LanguageRepository';
|
|
7
|
+
import type { Language } from '../storage/types/Language';
|
|
8
|
+
|
|
9
|
+
export const getSupportedLanguages = () => languageRepository.getLanguages();
|
|
10
|
+
|
|
11
|
+
export const getLanguageByCode = (code: string): Language | undefined => {
|
|
12
|
+
return languageRepository.getLanguageByCode(code);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const isLanguageSupported = (code: string): boolean => {
|
|
16
|
+
return languageRepository.isLanguageSupported(code);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const getDefaultLanguage = (): Language => {
|
|
20
|
+
const langs = languageRepository.getLanguages();
|
|
21
|
+
const firstLang = langs[0];
|
|
22
|
+
if (firstLang) return firstLang;
|
|
23
|
+
|
|
24
|
+
// Final fallback to system defaults if repository is empty
|
|
25
|
+
return languageRepository.getDefaultLanguage();
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const searchLanguages = (query: string): Language[] => {
|
|
29
|
+
const lowerQuery = query.toLowerCase();
|
|
30
|
+
return languageRepository.getLanguages().filter(
|
|
31
|
+
(lang) =>
|
|
32
|
+
lang.name.toLowerCase().includes(lowerQuery) ||
|
|
33
|
+
lang.nativeName.toLowerCase().includes(lowerQuery)
|
|
34
|
+
);
|
|
35
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locale Mapping Configuration
|
|
3
|
+
* Maps device locales to supported app locales
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const LOCALE_MAPPING: Record<string, string> = {
|
|
7
|
+
// English variants map to en-US
|
|
8
|
+
'en': 'en-US',
|
|
9
|
+
'en-US': 'en-US',
|
|
10
|
+
'en-GB': 'en-US',
|
|
11
|
+
'en-AU': 'en-US',
|
|
12
|
+
'en-CA': 'en-US',
|
|
13
|
+
'en-NZ': 'en-US',
|
|
14
|
+
'en-IE': 'en-US',
|
|
15
|
+
'en-ZA': 'en-US',
|
|
16
|
+
'en-SG': 'en-US',
|
|
17
|
+
'en-IN': 'en-US',
|
|
18
|
+
|
|
19
|
+
// Portuguese mappings
|
|
20
|
+
'pt': 'pt-PT',
|
|
21
|
+
'pt-BR': 'pt-BR',
|
|
22
|
+
'pt-PT': 'pt-PT',
|
|
23
|
+
|
|
24
|
+
// Spanish variants
|
|
25
|
+
'es': 'es-ES',
|
|
26
|
+
'es-ES': 'es-ES',
|
|
27
|
+
'es-MX': 'es-ES',
|
|
28
|
+
'es-AR': 'es-ES',
|
|
29
|
+
'es-US': 'es-ES',
|
|
30
|
+
|
|
31
|
+
// French variants
|
|
32
|
+
'fr': 'fr-FR',
|
|
33
|
+
'fr-FR': 'fr-FR',
|
|
34
|
+
'fr-CA': 'fr-FR',
|
|
35
|
+
'fr-BE': 'fr-FR',
|
|
36
|
+
'fr-CH': 'fr-FR',
|
|
37
|
+
|
|
38
|
+
// Norwegian
|
|
39
|
+
'no': 'no-NO',
|
|
40
|
+
'nb': 'no-NO',
|
|
41
|
+
'nn': 'no-NO',
|
|
42
|
+
|
|
43
|
+
// Chinese variants
|
|
44
|
+
'zh': 'zh-CN',
|
|
45
|
+
'zh-CN': 'zh-CN',
|
|
46
|
+
'zh-Hans': 'zh-CN',
|
|
47
|
+
'zh-Hans-CN': 'zh-CN',
|
|
48
|
+
'zh-Hant': 'zh-TW',
|
|
49
|
+
'zh-TW': 'zh-TW',
|
|
50
|
+
'zh-HK': 'zh-TW',
|
|
51
|
+
|
|
52
|
+
// Others
|
|
53
|
+
'ar': 'ar-SA',
|
|
54
|
+
'bg': 'bg-BG',
|
|
55
|
+
'cs': 'cs-CZ',
|
|
56
|
+
'da': 'da-DK',
|
|
57
|
+
'de': 'de-DE',
|
|
58
|
+
'el': 'el-GR',
|
|
59
|
+
'fi': 'fi-FI',
|
|
60
|
+
'hi': 'hi-IN',
|
|
61
|
+
'hr': 'hr-HR',
|
|
62
|
+
'hu': 'hu-HU',
|
|
63
|
+
'id': 'id-ID',
|
|
64
|
+
'it': 'it-IT',
|
|
65
|
+
'ja': 'ja-JP',
|
|
66
|
+
'ko': 'ko-KR',
|
|
67
|
+
'ms': 'ms-MY',
|
|
68
|
+
'nl': 'nl-NL',
|
|
69
|
+
'pl': 'pl-PL',
|
|
70
|
+
'ro': 'ro-RO',
|
|
71
|
+
'ru': 'ru-RU',
|
|
72
|
+
'sk': 'sk-SK',
|
|
73
|
+
'sv': 'sv-SE',
|
|
74
|
+
'th': 'th-TH',
|
|
75
|
+
'tr': 'tr-TR',
|
|
76
|
+
'uk': 'uk-UA',
|
|
77
|
+
'vi': 'vi-VN',
|
|
78
|
+
};
|