@umituz/react-native-localization 2.8.0 β 3.0.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 +22 -11
- package/scripts/prepublish.js +29 -16
- package/src/domain/repositories/ILocalizationRepository.ts +2 -2
- package/src/index.ts +2 -1
- package/src/infrastructure/components/LanguageSwitcher.tsx +90 -37
- package/src/infrastructure/components/LocalizationProvider.tsx +115 -7
- package/src/infrastructure/components/__tests__/LanguageSwitcher.test.tsx +91 -0
- package/src/infrastructure/components/useLanguageNavigation.ts +4 -4
- package/src/infrastructure/config/TranslationCache.ts +27 -0
- package/src/infrastructure/config/TranslationLoader.ts +4 -8
- package/src/infrastructure/config/__tests__/TranslationCache.test.ts +44 -0
- package/src/infrastructure/config/__tests__/languagesData.test.ts +49 -0
- package/src/infrastructure/config/languages.ts +1 -1
- package/src/infrastructure/config/languagesData.ts +91 -61
- package/src/infrastructure/hooks/__tests__/useTranslation.test.ts +52 -0
- package/src/infrastructure/hooks/useLocalization.ts +58 -0
- package/src/infrastructure/hooks/useTranslation.ts +84 -29
- package/src/infrastructure/storage/LanguageInitializer.ts +7 -5
- package/src/infrastructure/storage/LanguageSwitcher.ts +3 -3
- package/src/infrastructure/storage/LocalizationStore.ts +103 -94
- package/src/infrastructure/storage/types/LocalizationState.ts +31 -0
- package/src/presentation/components/LanguageItem.tsx +109 -0
- package/src/presentation/components/SearchInput.tsx +90 -0
- package/src/presentation/components/__tests__/LanguageItem.test.tsx +106 -0
- package/src/presentation/components/__tests__/SearchInput.test.tsx +95 -0
- package/src/presentation/screens/LanguageSelectionScreen.tsx +90 -146
- package/src/presentation/screens/__tests__/LanguageSelectionScreen.test.tsx +166 -0
- package/src/scripts/prepublish.ts +48 -0
- package/src/infrastructure/locales/en-US/alerts.json +0 -107
- package/src/infrastructure/locales/en-US/auth.json +0 -34
- package/src/infrastructure/locales/en-US/branding.json +0 -8
- package/src/infrastructure/locales/en-US/clipboard.json +0 -9
- package/src/infrastructure/locales/en-US/common.json +0 -57
- package/src/infrastructure/locales/en-US/datetime.json +0 -138
- package/src/infrastructure/locales/en-US/device.json +0 -14
- package/src/infrastructure/locales/en-US/editor.json +0 -64
- package/src/infrastructure/locales/en-US/errors.json +0 -41
- package/src/infrastructure/locales/en-US/general.json +0 -57
- package/src/infrastructure/locales/en-US/goals.json +0 -5
- package/src/infrastructure/locales/en-US/haptics.json +0 -6
- package/src/infrastructure/locales/en-US/home.json +0 -62
- package/src/infrastructure/locales/en-US/index.ts +0 -54
- package/src/infrastructure/locales/en-US/navigation.json +0 -6
- package/src/infrastructure/locales/en-US/onboarding.json +0 -26
- package/src/infrastructure/locales/en-US/projects.json +0 -34
- package/src/infrastructure/locales/en-US/settings.json +0 -45
- package/src/infrastructure/locales/en-US/sharing.json +0 -8
- package/src/infrastructure/locales/en-US/templates.json +0 -28
- package/src/infrastructure/scripts/createLocaleLoaders.js +0 -177
package/package.json
CHANGED
|
@@ -1,23 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-localization",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "English-only 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
9
|
"lint": "tsc --noEmit",
|
|
10
|
-
"
|
|
11
|
-
"
|
|
10
|
+
"test": "jest",
|
|
11
|
+
"test:watch": "jest --watch",
|
|
12
|
+
"test:coverage": "jest --coverage",
|
|
13
|
+
|
|
12
14
|
"prepublishOnly": "node scripts/prepublish.js",
|
|
13
15
|
"version:patch": "npm version patch -m 'chore: release v%s'",
|
|
14
16
|
"version:minor": "npm version minor -m 'chore: release v%s'",
|
|
15
|
-
"version:major": "npm version major -m 'chore: release v%s'"
|
|
16
|
-
"i18n:setup": "node scripts/setup-languages.js",
|
|
17
|
-
"i18n:translate": "node scripts/translate-missing.js",
|
|
18
|
-
"i18n:check": "node scripts/check-translations.js",
|
|
19
|
-
"i18n:analyze": "node scripts/analyze-keys.js",
|
|
20
|
-
"i18n:remove-unused": "node scripts/remove-unused-keys.js"
|
|
17
|
+
"version:major": "npm version major -m 'chore: release v%s'"
|
|
21
18
|
},
|
|
22
19
|
"keywords": [
|
|
23
20
|
"react-native",
|
|
@@ -33,7 +30,7 @@
|
|
|
33
30
|
"license": "MIT",
|
|
34
31
|
"repository": {
|
|
35
32
|
"type": "git",
|
|
36
|
-
"url": "
|
|
33
|
+
"url": "https://github.com/umituz/react-native-localization"
|
|
37
34
|
},
|
|
38
35
|
"dependencies": {
|
|
39
36
|
"expo-localization": "~15.0.0",
|
|
@@ -54,16 +51,30 @@
|
|
|
54
51
|
}
|
|
55
52
|
},
|
|
56
53
|
"devDependencies": {
|
|
54
|
+
"@babel/preset-env": "^7.28.5",
|
|
55
|
+
"@babel/preset-react": "^7.28.5",
|
|
56
|
+
"@babel/preset-typescript": "^7.28.5",
|
|
57
|
+
"@testing-library/jest-native": "^5.4.3",
|
|
58
|
+
"@testing-library/react": "^16.3.0",
|
|
59
|
+
"@testing-library/react-hooks": "^8.0.1",
|
|
60
|
+
"@testing-library/react-native": "^13.3.3",
|
|
57
61
|
"@types/react": "^18.2.45",
|
|
58
62
|
"@types/react-native": "^0.73.0",
|
|
59
63
|
"@umituz/react-native-filesystem": "^1.4.0",
|
|
60
64
|
"@umituz/react-native-storage": "^1.1.1",
|
|
61
65
|
"expo-localization": "~15.0.0",
|
|
62
66
|
"i18next": "^23.0.0",
|
|
67
|
+
"jest": "^30.2.0",
|
|
68
|
+
"jest-environment-jsdom": "^30.2.0",
|
|
69
|
+
"jest-transform-stub": "^2.0.0",
|
|
63
70
|
"react": ">=18.2.0",
|
|
64
71
|
"react-i18next": "^14.0.0",
|
|
65
72
|
"react-native": ">=0.74.0",
|
|
66
|
-
"
|
|
73
|
+
"react-native-gesture-handler": "^2.29.1",
|
|
74
|
+
"react-test-renderer": "^19.2.0",
|
|
75
|
+
"ts-jest": "^29.4.6",
|
|
76
|
+
"ts-node": "^10.9.2",
|
|
77
|
+
"typescript": "^5.9.3",
|
|
67
78
|
"zustand": "^5.0.2"
|
|
68
79
|
},
|
|
69
80
|
"publishConfig": {
|
package/scripts/prepublish.js
CHANGED
|
@@ -2,34 +2,47 @@
|
|
|
2
2
|
/* eslint-disable no-console */
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Pre-Publish Script -
|
|
5
|
+
* Pre-Publish Script - Generic Package Version
|
|
6
6
|
*
|
|
7
|
-
* Basic checks before publishing
|
|
7
|
+
* Basic checks before publishing for generic localization package
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
import * as fs from 'fs';
|
|
11
|
+
import * as path from 'path';
|
|
12
|
+
import { fileURLToPath } from 'url';
|
|
12
13
|
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = path.dirname(__filename);
|
|
13
16
|
const PACKAGE_ROOT = path.resolve(__dirname, '..');
|
|
14
|
-
const
|
|
17
|
+
const SRC_DIR = path.join(PACKAGE_ROOT, 'src');
|
|
15
18
|
|
|
16
19
|
console.log('π Pre-publish checks...\n');
|
|
17
20
|
|
|
18
|
-
// Check if
|
|
19
|
-
if (!fs.existsSync(
|
|
20
|
-
console.error('β
|
|
21
|
+
// Check if src directory exists
|
|
22
|
+
if (!fs.existsSync(SRC_DIR)) {
|
|
23
|
+
console.error('β src directory not found!');
|
|
21
24
|
process.exit(1);
|
|
22
25
|
}
|
|
23
26
|
|
|
24
|
-
// Check if
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
+
// Check if main files exist
|
|
28
|
+
const mainFiles = [
|
|
29
|
+
'src/index.ts',
|
|
30
|
+
'src/infrastructure/config/i18n.ts',
|
|
31
|
+
'src/infrastructure/storage/LocalizationStore.ts',
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
let allFilesExist = true;
|
|
35
|
+
for (const file of mainFiles) {
|
|
36
|
+
const filePath = path.join(PACKAGE_ROOT, file);
|
|
37
|
+
if (!fs.existsSync(filePath)) {
|
|
38
|
+
console.error(`β Required file not found: ${file}`);
|
|
39
|
+
allFilesExist = false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
27
42
|
|
|
28
|
-
if (
|
|
29
|
-
console.error('β No JSON translation files found!');
|
|
43
|
+
if (!allFilesExist) {
|
|
30
44
|
process.exit(1);
|
|
31
45
|
}
|
|
32
46
|
|
|
33
|
-
console.log(
|
|
34
|
-
console.log('β
Pre-publish checks passed!\n');
|
|
35
|
-
|
|
47
|
+
console.log('β
All required files found');
|
|
48
|
+
console.log('β
Pre-publish checks passed!\n');
|
package/src/index.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// Hooks
|
|
7
|
-
export { useLocalization
|
|
7
|
+
export { useLocalization } from './infrastructure/hooks/useLocalization';
|
|
8
|
+
export { useLocalizationStore } from './infrastructure/storage/LocalizationStore';
|
|
8
9
|
export { useTranslationFunction } from './infrastructure/hooks/useTranslation';
|
|
9
10
|
|
|
10
11
|
// Components
|
|
@@ -1,64 +1,110 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { TouchableOpacity, Text,
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { getLanguageByCode, getDefaultLanguage } from '../config/languages';
|
|
7
|
-
import { Language } from '../../domain/repositories/ILocalizationRepository';
|
|
1
|
+
import React, { useCallback, useMemo } from 'react';
|
|
2
|
+
import { TouchableOpacity, Text, StyleSheet } from 'react-native';
|
|
3
|
+
import { useLocalization } from '../hooks/useLocalization';
|
|
4
|
+
import { languageRegistry } from '../config/languagesData';
|
|
5
|
+
import type { Language } from '../storage/types/LocalizationState';
|
|
8
6
|
|
|
9
|
-
interface LanguageSwitcherProps {
|
|
7
|
+
export interface LanguageSwitcherProps {
|
|
10
8
|
showName?: boolean;
|
|
11
9
|
showFlag?: boolean;
|
|
12
10
|
color?: string;
|
|
13
|
-
|
|
11
|
+
onPress?: () => void;
|
|
14
12
|
style?: any;
|
|
15
13
|
textStyle?: any;
|
|
14
|
+
iconStyle?: any;
|
|
15
|
+
testID?: string;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
accessibilityLabel?: string;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
const
|
|
20
|
+
const DEFAULT_CONFIG = {
|
|
19
21
|
defaultIconSize: 20,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
hitSlop: { top: 10, bottom: 10, left: 10, right: 10 } as const,
|
|
23
|
+
defaultColor: '#000000',
|
|
24
|
+
activeOpacity: 0.7,
|
|
22
25
|
};
|
|
23
26
|
|
|
24
27
|
export const LanguageSwitcher: React.FC<LanguageSwitcherProps> = ({
|
|
25
28
|
showName = false,
|
|
26
29
|
showFlag = true,
|
|
27
30
|
color,
|
|
28
|
-
|
|
31
|
+
onPress,
|
|
29
32
|
style,
|
|
30
33
|
textStyle,
|
|
34
|
+
iconStyle,
|
|
35
|
+
testID = 'language-switcher',
|
|
36
|
+
disabled = false,
|
|
37
|
+
accessibilityLabel,
|
|
31
38
|
}) => {
|
|
32
|
-
const navigation = useNavigation();
|
|
33
39
|
const { currentLanguage } = useLocalization();
|
|
34
|
-
const currentLang = getLanguageByCode(currentLanguage) || getDefaultLanguage();
|
|
35
40
|
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
41
|
+
const currentLang = useMemo((): Language => {
|
|
42
|
+
return languageRegistry.getLanguageByCode(currentLanguage) || languageRegistry.getDefaultLanguage();
|
|
43
|
+
}, [currentLanguage]);
|
|
44
|
+
|
|
45
|
+
const handlePress = useCallback(() => {
|
|
46
|
+
if (disabled) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (__DEV__) {
|
|
51
|
+
console.log('[LanguageSwitcher] Pressed, current language:', currentLanguage);
|
|
39
52
|
}
|
|
40
|
-
|
|
53
|
+
|
|
54
|
+
onPress?.();
|
|
55
|
+
}, [disabled, onPress, currentLanguage]);
|
|
41
56
|
|
|
42
|
-
const iconColor = color ||
|
|
57
|
+
const iconColor = useMemo(() => color || DEFAULT_CONFIG.defaultColor, [color]);
|
|
43
58
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
59
|
+
const accessibilityProps = useMemo(() => ({
|
|
60
|
+
accessibilityRole: 'button' as const,
|
|
61
|
+
accessibilityLabel: accessibilityLabel || `Current language: ${currentLang.nativeName}`,
|
|
62
|
+
accessibilityHint: disabled ? undefined : 'Double tap to change language',
|
|
63
|
+
accessible: true,
|
|
64
|
+
}), [accessibilityLabel, currentLang.nativeName, disabled]);
|
|
65
|
+
|
|
66
|
+
const content = useMemo(() => {
|
|
67
|
+
if (showFlag && showName) {
|
|
68
|
+
return (
|
|
69
|
+
<>
|
|
70
|
+
<Text style={[styles.flag, iconStyle]}>{currentLang.flag}</Text>
|
|
71
|
+
<Text style={[styles.languageName, { color: iconColor }, textStyle]}>
|
|
72
|
+
{currentLang.nativeName}
|
|
73
|
+
</Text>
|
|
74
|
+
</>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (showFlag) {
|
|
79
|
+
return (
|
|
80
|
+
<Text style={[styles.flag, iconStyle]}>{currentLang.flag}</Text>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (showName) {
|
|
85
|
+
return (
|
|
55
86
|
<Text style={[styles.languageName, { color: iconColor }, textStyle]}>
|
|
56
87
|
{currentLang.nativeName}
|
|
57
88
|
</Text>
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<Text style={[styles.icon, { color: iconColor }, iconStyle]}>π</Text>
|
|
94
|
+
);
|
|
95
|
+
}, [showFlag, showName, currentLang, iconColor, textStyle, iconStyle]);
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<TouchableOpacity
|
|
99
|
+
style={[styles.container, style, disabled && styles.disabled]}
|
|
100
|
+
onPress={handlePress}
|
|
101
|
+
activeOpacity={disabled ? 1 : DEFAULT_CONFIG.activeOpacity}
|
|
102
|
+
hitSlop={DEFAULT_CONFIG.hitSlop}
|
|
103
|
+
testID={testID}
|
|
104
|
+
disabled={disabled}
|
|
105
|
+
{...accessibilityProps}
|
|
106
|
+
>
|
|
107
|
+
{content}
|
|
62
108
|
</TouchableOpacity>
|
|
63
109
|
);
|
|
64
110
|
};
|
|
@@ -69,16 +115,23 @@ const styles = StyleSheet.create({
|
|
|
69
115
|
alignItems: 'center',
|
|
70
116
|
gap: 8,
|
|
71
117
|
paddingHorizontal: 4,
|
|
118
|
+
paddingVertical: 4,
|
|
119
|
+
},
|
|
120
|
+
disabled: {
|
|
121
|
+
opacity: 0.5,
|
|
72
122
|
},
|
|
73
123
|
flag: {
|
|
74
|
-
fontSize:
|
|
124
|
+
fontSize: DEFAULT_CONFIG.defaultIconSize,
|
|
125
|
+
textAlign: 'center',
|
|
75
126
|
},
|
|
76
127
|
languageName: {
|
|
77
128
|
fontSize: 14,
|
|
78
129
|
fontWeight: '600',
|
|
130
|
+
textAlign: 'center',
|
|
79
131
|
},
|
|
80
132
|
icon: {
|
|
81
|
-
fontSize:
|
|
133
|
+
fontSize: DEFAULT_CONFIG.defaultIconSize,
|
|
134
|
+
textAlign: 'center',
|
|
82
135
|
},
|
|
83
136
|
});
|
|
84
137
|
|
|
@@ -1,29 +1,137 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* LocalizationProvider Component
|
|
3
3
|
* Initializes localization system with app translations
|
|
4
|
+
* Includes memory leak prevention and performance optimizations
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
|
-
import React, { useEffect, ReactNode } from 'react';
|
|
7
|
+
import React, { useEffect, useRef, ReactNode, useCallback } from 'react';
|
|
7
8
|
import { useLocalizationStore } from '../storage/LocalizationStore';
|
|
8
9
|
import { I18nInitializer } from '../config/I18nInitializer';
|
|
9
10
|
|
|
10
|
-
interface LocalizationProviderProps {
|
|
11
|
+
export interface LocalizationProviderProps {
|
|
11
12
|
children: ReactNode;
|
|
12
13
|
translations: Record<string, any>;
|
|
13
14
|
defaultLanguage?: string;
|
|
15
|
+
onLanguageChange?: (languageCode: string) => void;
|
|
16
|
+
onError?: (error: Error) => void;
|
|
17
|
+
enableCache?: boolean;
|
|
14
18
|
}
|
|
15
19
|
|
|
16
20
|
export const LocalizationProvider: React.FC<LocalizationProviderProps> = ({
|
|
17
21
|
children,
|
|
18
22
|
translations,
|
|
19
23
|
defaultLanguage = 'en-US',
|
|
24
|
+
onLanguageChange,
|
|
25
|
+
onError,
|
|
26
|
+
enableCache = true,
|
|
20
27
|
}) => {
|
|
21
|
-
const
|
|
28
|
+
const store = useLocalizationStore();
|
|
29
|
+
const initialize = store.initialize;
|
|
30
|
+
const setLanguage = store.setLanguage;
|
|
31
|
+
const isInitialized = store.isInitialized;
|
|
32
|
+
const currentLanguage = store.currentLanguage;
|
|
33
|
+
|
|
34
|
+
const isInitializingRef = useRef(false);
|
|
35
|
+
const previousLanguageRef = useRef(currentLanguage);
|
|
22
36
|
|
|
37
|
+
// Memoize translations to prevent unnecessary re-renders
|
|
38
|
+
const memoizedTranslations = useRef(translations);
|
|
39
|
+
|
|
40
|
+
// Update memoized translations when they change
|
|
23
41
|
useEffect(() => {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}, [translations, defaultLanguage, initialize]);
|
|
42
|
+
memoizedTranslations.current = translations;
|
|
43
|
+
}, [translations]);
|
|
27
44
|
|
|
28
|
-
|
|
45
|
+
// Initialize localization system
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (isInitializingRef.current || isInitialized) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
isInitializingRef.current = true;
|
|
52
|
+
|
|
53
|
+
const initializeLocalization = async () => {
|
|
54
|
+
try {
|
|
55
|
+
if (__DEV__) {
|
|
56
|
+
console.log('[LocalizationProvider] Initializing with language:', defaultLanguage);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
await I18nInitializer.initialize(memoizedTranslations.current, defaultLanguage);
|
|
60
|
+
await initialize();
|
|
61
|
+
|
|
62
|
+
if (__DEV__) {
|
|
63
|
+
console.log('[LocalizationProvider] Initialization complete');
|
|
64
|
+
}
|
|
65
|
+
} catch (error) {
|
|
66
|
+
if (__DEV__) {
|
|
67
|
+
console.error('[LocalizationProvider] Initialization failed:', error);
|
|
68
|
+
}
|
|
69
|
+
onError?.(error instanceof Error ? error : new Error('Initialization failed'));
|
|
70
|
+
} finally {
|
|
71
|
+
isInitializingRef.current = false;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
initializeLocalization();
|
|
76
|
+
|
|
77
|
+
// Cleanup function
|
|
78
|
+
return () => {
|
|
79
|
+
if (__DEV__) {
|
|
80
|
+
console.log('[LocalizationProvider] Cleanup');
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}, [defaultLanguage, initialize, onError, isInitialized]);
|
|
84
|
+
|
|
85
|
+
// Handle language changes
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (previousLanguageRef.current !== currentLanguage && currentLanguage !== previousLanguageRef.current) {
|
|
88
|
+
previousLanguageRef.current = currentLanguage;
|
|
89
|
+
onLanguageChange?.(currentLanguage);
|
|
90
|
+
|
|
91
|
+
if (__DEV__) {
|
|
92
|
+
console.log('[LocalizationProvider] Language changed to:', currentLanguage);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}, [currentLanguage, onLanguageChange]);
|
|
96
|
+
|
|
97
|
+
// Handle language change with error handling
|
|
98
|
+
const handleLanguageChange = useCallback(async (languageCode: string) => {
|
|
99
|
+
try {
|
|
100
|
+
await setLanguage(languageCode);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if (__DEV__) {
|
|
103
|
+
console.error('[LocalizationProvider] Language change failed:', error);
|
|
104
|
+
}
|
|
105
|
+
onError?.(error instanceof Error ? error : new Error('Language change failed'));
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
}, [setLanguage, onError]);
|
|
109
|
+
|
|
110
|
+
// Context value with memoized functions
|
|
111
|
+
const contextValue = React.useMemo(() => ({
|
|
112
|
+
handleLanguageChange,
|
|
113
|
+
isInitialized,
|
|
114
|
+
currentLanguage,
|
|
115
|
+
}), [handleLanguageChange, isInitialized, currentLanguage]);
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<LocalizationContext.Provider value={contextValue}>
|
|
119
|
+
{children}
|
|
120
|
+
</LocalizationContext.Provider>
|
|
121
|
+
);
|
|
29
122
|
};
|
|
123
|
+
|
|
124
|
+
// Context for language change handling
|
|
125
|
+
const LocalizationContext = React.createContext<{
|
|
126
|
+
handleLanguageChange: (languageCode: string) => Promise<void>;
|
|
127
|
+
isInitialized: boolean;
|
|
128
|
+
currentLanguage: string;
|
|
129
|
+
} | null>(null);
|
|
130
|
+
|
|
131
|
+
export const useLocalizationContext = () => {
|
|
132
|
+
const context = React.useContext(LocalizationContext);
|
|
133
|
+
if (!context) {
|
|
134
|
+
throw new Error('useLocalizationContext must be used within LocalizationProvider');
|
|
135
|
+
}
|
|
136
|
+
return context;
|
|
137
|
+
};
|
|
@@ -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('../../config/languagesData', () => ({
|
|
28
|
+
languageRegistry: {
|
|
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
|
+
});
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
// @ts-ignore - Optional peer dependency
|
|
2
2
|
import { useNavigation } from '@react-navigation/native';
|
|
3
|
-
import { useLocalization } from '../
|
|
4
|
-
import {
|
|
5
|
-
import { Language } from '
|
|
3
|
+
import { useLocalization } from '../hooks/useLocalization';
|
|
4
|
+
import { languageRegistry } from '../config/languagesData';
|
|
5
|
+
import type { Language } from '../storage/types/LocalizationState';
|
|
6
6
|
|
|
7
7
|
export const useLanguageNavigation = (navigationScreen: string) => {
|
|
8
8
|
const navigation = useNavigation();
|
|
9
9
|
const { currentLanguage } = useLocalization();
|
|
10
|
-
const currentLang = getLanguageByCode(currentLanguage) || getDefaultLanguage();
|
|
10
|
+
const currentLang = languageRegistry.getLanguageByCode(currentLanguage) || languageRegistry.getDefaultLanguage();
|
|
11
11
|
|
|
12
12
|
const navigateToLanguageSelection = () => {
|
|
13
13
|
if (navigation && navigationScreen) {
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
this.cache.delete(firstKey);
|
|
20
|
+
}
|
|
21
|
+
this.cache.set(key, value);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
clear(): void {
|
|
25
|
+
this.cache.clear();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -6,16 +6,12 @@
|
|
|
6
6
|
|
|
7
7
|
export class TranslationLoader {
|
|
8
8
|
/**
|
|
9
|
-
* Load package translations (
|
|
9
|
+
* Load package translations (empty by default - apps provide their own)
|
|
10
10
|
*/
|
|
11
11
|
static loadPackageTranslations(): Record<string, any> {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
return { 'en-US': translations.default || translations };
|
|
16
|
-
} catch {
|
|
17
|
-
return { 'en-US': {} };
|
|
18
|
-
}
|
|
12
|
+
// Package doesn't include any translations by default
|
|
13
|
+
// Consuming applications should provide their own translations
|
|
14
|
+
return { 'en-US': {} };
|
|
19
15
|
}
|
|
20
16
|
|
|
21
17
|
/**
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
});
|