@umituz/react-native-localization 2.7.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.
Files changed (49) hide show
  1. package/package.json +21 -10
  2. package/scripts/prepublish.js +29 -16
  3. package/src/domain/repositories/ILocalizationRepository.ts +2 -2
  4. package/src/index.ts +5 -1
  5. package/src/infrastructure/components/LanguageSwitcher.tsx +90 -37
  6. package/src/infrastructure/components/LocalizationProvider.tsx +115 -7
  7. package/src/infrastructure/components/__tests__/LanguageSwitcher.test.tsx +91 -0
  8. package/src/infrastructure/components/useLanguageNavigation.ts +4 -4
  9. package/src/infrastructure/config/TranslationCache.ts +27 -0
  10. package/src/infrastructure/config/TranslationLoader.ts +4 -8
  11. package/src/infrastructure/config/__tests__/TranslationCache.test.ts +44 -0
  12. package/src/infrastructure/config/__tests__/languagesData.test.ts +49 -0
  13. package/src/infrastructure/config/languages.ts +1 -1
  14. package/src/infrastructure/config/languagesData.ts +91 -61
  15. package/src/infrastructure/hooks/__tests__/useTranslation.test.ts +52 -0
  16. package/src/infrastructure/hooks/useLocalization.ts +58 -0
  17. package/src/infrastructure/hooks/useTranslation.ts +84 -29
  18. package/src/infrastructure/storage/LanguageInitializer.ts +7 -5
  19. package/src/infrastructure/storage/LanguageSwitcher.ts +3 -3
  20. package/src/infrastructure/storage/LocalizationStore.ts +103 -94
  21. package/src/infrastructure/storage/types/LocalizationState.ts +31 -0
  22. package/src/presentation/components/LanguageItem.tsx +109 -0
  23. package/src/presentation/components/SearchInput.tsx +90 -0
  24. package/src/presentation/components/__tests__/LanguageItem.test.tsx +106 -0
  25. package/src/presentation/components/__tests__/SearchInput.test.tsx +95 -0
  26. package/src/presentation/screens/LanguageSelectionScreen.tsx +148 -0
  27. package/src/presentation/screens/__tests__/LanguageSelectionScreen.test.tsx +166 -0
  28. package/src/scripts/prepublish.ts +48 -0
  29. package/src/infrastructure/locales/en-US/alerts.json +0 -107
  30. package/src/infrastructure/locales/en-US/auth.json +0 -34
  31. package/src/infrastructure/locales/en-US/branding.json +0 -8
  32. package/src/infrastructure/locales/en-US/clipboard.json +0 -9
  33. package/src/infrastructure/locales/en-US/common.json +0 -57
  34. package/src/infrastructure/locales/en-US/datetime.json +0 -138
  35. package/src/infrastructure/locales/en-US/device.json +0 -14
  36. package/src/infrastructure/locales/en-US/editor.json +0 -64
  37. package/src/infrastructure/locales/en-US/errors.json +0 -41
  38. package/src/infrastructure/locales/en-US/general.json +0 -57
  39. package/src/infrastructure/locales/en-US/goals.json +0 -5
  40. package/src/infrastructure/locales/en-US/haptics.json +0 -6
  41. package/src/infrastructure/locales/en-US/home.json +0 -62
  42. package/src/infrastructure/locales/en-US/index.ts +0 -54
  43. package/src/infrastructure/locales/en-US/navigation.json +0 -6
  44. package/src/infrastructure/locales/en-US/onboarding.json +0 -26
  45. package/src/infrastructure/locales/en-US/projects.json +0 -34
  46. package/src/infrastructure/locales/en-US/settings.json +0 -45
  47. package/src/infrastructure/locales/en-US/sharing.json +0 -8
  48. package/src/infrastructure/locales/en-US/templates.json +0 -28
  49. 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": "2.7.0",
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
- "locales:generate": "node scripts/createLocaleLoaders.js",
11
- "locales:generate:lang": "node scripts/createLocaleLoaders.js",
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",
@@ -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
- "typescript": "^5.3.3",
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": {
@@ -2,34 +2,47 @@
2
2
  /* eslint-disable no-console */
3
3
 
4
4
  /**
5
- * Pre-Publish Script - Minimal Version
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
- const fs = require('fs');
11
- const path = require('path');
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 EN_US_DIR = path.join(PACKAGE_ROOT, 'src/infrastructure/locales/en-US');
17
+ const SRC_DIR = path.join(PACKAGE_ROOT, 'src');
15
18
 
16
19
  console.log('πŸ” Pre-publish checks...\n');
17
20
 
18
- // Check if en-US directory exists
19
- if (!fs.existsSync(EN_US_DIR)) {
20
- console.error('❌ en-US directory not found!');
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 en-US has JSON files
25
- const jsonFiles = fs.readdirSync(EN_US_DIR)
26
- .filter(file => file.endsWith('.json'));
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 (jsonFiles.length === 0) {
29
- console.error('❌ No JSON translation files found!');
43
+ if (!allFilesExist) {
30
44
  process.exit(1);
31
45
  }
32
46
 
33
- console.log(`βœ… Found ${jsonFiles.length} translation files`);
34
- console.log('βœ… Pre-publish checks passed!\n');
35
-
47
+ console.log('βœ… All required files found');
48
+ console.log('βœ… Pre-publish checks passed!\n');
@@ -7,8 +7,8 @@ export interface Language {
7
7
  code: string;
8
8
  name: string;
9
9
  nativeName: string;
10
- flag: string;
11
- rtl?: boolean;
10
+ flag?: string;
11
+ isRTL?: boolean;
12
12
  }
13
13
 
14
14
  export interface ILocalizationRepository {
package/src/index.ts CHANGED
@@ -4,7 +4,8 @@
4
4
  */
5
5
 
6
6
  // Hooks
7
- export { useLocalization, useLocalizationStore } from './infrastructure/storage/LocalizationStore';
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
@@ -26,5 +27,8 @@ export {
26
27
  searchLanguages,
27
28
  } from './infrastructure/config/languages';
28
29
 
30
+ // Presentation
31
+ export { LanguageSelectionScreen } from './presentation/screens/LanguageSelectionScreen';
32
+
29
33
  // Types
30
34
  export type { Language, ILocalizationRepository } from './domain/repositories/ILocalizationRepository';
@@ -1,64 +1,110 @@
1
- import React from 'react';
2
- import { TouchableOpacity, Text, View, StyleSheet } from 'react-native';
3
- // @ts-ignore - Optional peer dependency
4
- import { useNavigation } from '@react-navigation/native';
5
- import { useLocalization } from '../storage/LocalizationStore';
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
- navigationScreen?: string;
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 languageSwitcherConfig = {
20
+ const DEFAULT_CONFIG = {
19
21
  defaultIconSize: 20,
20
- defaultNavigationScreen: 'LanguageSelection',
21
- hitSlop: { top: 10, bottom: 10, left: 10, right: 10 },
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
- navigationScreen = languageSwitcherConfig.defaultNavigationScreen,
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 navigateToLanguageSelection = () => {
37
- if (navigation && navigationScreen) {
38
- navigation.navigate(navigationScreen as never);
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 || '#000000';
57
+ const iconColor = useMemo(() => color || DEFAULT_CONFIG.defaultColor, [color]);
43
58
 
44
- return (
45
- <TouchableOpacity
46
- style={[styles.container, style]}
47
- onPress={navigateToLanguageSelection}
48
- activeOpacity={0.7}
49
- hitSlop={languageSwitcherConfig.hitSlop}
50
- >
51
- {showFlag && (
52
- <Text style={[styles.flag, textStyle]}>{currentLang.flag}</Text>
53
- )}
54
- {showName && (
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
- {!showName && !showFlag && (
60
- <Text style={[styles.icon, { color: iconColor }]}>🌐</Text>
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: 20,
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: 20,
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 initialize = useLocalizationStore((state) => state.initialize);
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
- I18nInitializer.initialize(translations, defaultLanguage);
25
- initialize();
26
- }, [translations, defaultLanguage, initialize]);
42
+ memoizedTranslations.current = translations;
43
+ }, [translations]);
27
44
 
28
- return <>{children}</>;
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 '../storage/LocalizationStore';
4
- import { getLanguageByCode, getDefaultLanguage } from '../config/languages';
5
- import { Language } from '../../domain/repositories/ILocalizationRepository';
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 (en-US)
9
+ * Load package translations (empty by default - apps provide their own)
10
10
  */
11
11
  static loadPackageTranslations(): Record<string, any> {
12
- try {
13
- // eslint-disable-next-line @typescript-eslint/no-require-imports
14
- const translations = require('../locales/en-US');
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
+ });