@umituz/react-native-localization 2.8.0 → 3.0.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.
Files changed (49) hide show
  1. package/package.json +21 -11
  2. package/scripts/prepublish.js +29 -16
  3. package/src/domain/repositories/ILocalizationRepository.ts +2 -2
  4. package/src/index.ts +2 -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 +90 -146
  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
@@ -1,105 +1,114 @@
1
1
  /**
2
- * Localization Store
3
- * Zustand state management for language preferences
4
- *
5
- * Uses separate classes for initialization, switching, and translation
6
- * Follows Single Responsibility Principle
2
+ * Localization Store Factory
3
+ * Creates and manages localization state with proper separation of concerns
7
4
  */
8
5
 
9
6
  import { create } from 'zustand';
10
- import { SUPPORTED_LANGUAGES, DEFAULT_LANGUAGE, getLanguageByCode } from '../config/languages';
7
+ import type { LocalizationState, LocalizationActions, LocalizationGetters, Language } from './types/LocalizationState';
11
8
  import { LanguageInitializer } from './LanguageInitializer';
12
9
  import { LanguageSwitcher } from './LanguageSwitcher';
13
- import type { Language } from '../../domain/repositories/ILocalizationRepository';
14
-
15
- interface LocalizationState {
16
- currentLanguage: string;
17
- isRTL: boolean;
18
- isInitialized: boolean;
19
- supportedLanguages: Language[];
20
- setLanguage: (languageCode: string) => Promise<void>;
21
- initialize: () => Promise<void>;
22
- }
10
+ import { languageRegistry } from '../config/languagesData';
23
11
 
24
- export const useLocalizationStore = create<LocalizationState>((set, get) => ({
25
- currentLanguage: DEFAULT_LANGUAGE,
26
- isRTL: false,
27
- isInitialized: false,
28
- supportedLanguages: SUPPORTED_LANGUAGES,
29
-
30
- /**
31
- * Initialize localization system
32
- */
33
- initialize: async () => {
34
- // Prevent re-initialization
35
- const { isInitialized: alreadyInitialized } = get();
36
- if (alreadyInitialized) {
37
- return;
38
- }
39
-
40
- try {
41
- const result = await LanguageInitializer.initialize();
42
-
43
- set({
44
- currentLanguage: result.languageCode,
45
- isRTL: result.isRTL,
46
- isInitialized: true,
47
- });
48
- } catch (error) {
49
- // Set fallback state even on error
50
- set({
51
- currentLanguage: DEFAULT_LANGUAGE,
52
- isRTL: false,
53
- isInitialized: true,
54
- });
55
- }
56
- },
57
-
58
- /**
59
- * Change language
60
- */
61
- setLanguage: async (languageCode: string) => {
62
- try {
63
- const result = await LanguageSwitcher.switchLanguage(languageCode);
64
-
65
- set({
66
- currentLanguage: result.languageCode,
67
- isRTL: result.isRTL,
68
- });
69
- } catch (error) {
70
- throw error;
71
- }
72
- },
73
- }));
12
+ interface LocalizationStore extends LocalizationState, LocalizationActions, LocalizationGetters {
13
+ // Additional properties can be added here if needed
14
+ }
74
15
 
75
16
  /**
76
- * Hook to use localization
77
- * Provides current language, RTL state, language switching, and translation function
17
+ * Create localization store with proper dependency injection
78
18
  */
79
- export const useLocalization = () => {
80
- const {
81
- currentLanguage,
82
- isRTL,
83
- isInitialized,
84
- supportedLanguages,
85
- setLanguage,
86
- initialize,
87
- } = useLocalizationStore();
88
-
89
- const currentLanguageObject = getLanguageByCode(currentLanguage);
90
-
91
- // Import translation function here to avoid circular dependencies
92
- const { useTranslationFunction } = require('../hooks/useTranslation');
93
- const t = useTranslationFunction();
94
-
95
- return {
96
- t,
97
- currentLanguage,
98
- currentLanguageObject,
99
- isRTL,
100
- isInitialized,
101
- supportedLanguages,
102
- setLanguage,
103
- initialize,
104
- };
19
+ export const createLocalizationStore = () => {
20
+ return create<LocalizationStore>()(
21
+ (set, get) => ({
22
+ // State
23
+ currentLanguage: 'en-US',
24
+ isRTL: false,
25
+ isInitialized: false,
26
+ supportedLanguages: languageRegistry.getLanguages(),
27
+
28
+ // Actions
29
+ initialize: async () => {
30
+ const { isInitialized: alreadyInitialized } = get();
31
+ if (alreadyInitialized) {
32
+ if (__DEV__) {
33
+ console.log('[Localization] Already initialized');
34
+ }
35
+ return;
36
+ }
37
+
38
+ try {
39
+ const result = await LanguageInitializer.initialize();
40
+
41
+ set({
42
+ currentLanguage: result.languageCode,
43
+ isRTL: result.isRTL,
44
+ isInitialized: true,
45
+ });
46
+
47
+ if (__DEV__) {
48
+ console.log(`[Localization] Initialized with language: ${result.languageCode}`);
49
+ }
50
+ } catch (error) {
51
+ // Set fallback state even on error
52
+ set({
53
+ currentLanguage: 'en-US',
54
+ isRTL: false,
55
+ isInitialized: true,
56
+ });
57
+
58
+ if (__DEV__) {
59
+ console.error('[Localization] Initialization failed, using fallback:', error);
60
+ }
61
+ }
62
+ },
63
+
64
+ setLanguage: async (languageCode: string) => {
65
+ try {
66
+ const result = await LanguageSwitcher.switchLanguage(languageCode);
67
+
68
+ set({
69
+ currentLanguage: result.languageCode,
70
+ isRTL: result.isRTL,
71
+ });
72
+
73
+ if (__DEV__) {
74
+ console.log(`[Localization] Language changed to: ${result.languageCode}`);
75
+ }
76
+ } catch (error) {
77
+ if (__DEV__) {
78
+ console.error('[Localization] Language change failed:', error);
79
+ }
80
+ throw error;
81
+ }
82
+ },
83
+
84
+ reset: () => {
85
+ set({
86
+ currentLanguage: 'en-US',
87
+ isRTL: false,
88
+ isInitialized: false,
89
+ });
90
+
91
+ if (__DEV__) {
92
+ console.log('[Localization] Store reset');
93
+ }
94
+ },
95
+
96
+ // Getters
97
+ getCurrentLanguage: () => {
98
+ const { currentLanguage } = get();
99
+ return languageRegistry.getLanguageByCode(currentLanguage);
100
+ },
101
+
102
+ isLanguageSupported: (code: string) => {
103
+ return languageRegistry.isLanguageSupported(code);
104
+ },
105
+
106
+ getSupportedLanguages: () => {
107
+ return languageRegistry.getLanguages();
108
+ },
109
+ })
110
+ );
105
111
  };
112
+
113
+ // Create singleton instance
114
+ export const useLocalizationStore = createLocalizationStore();
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Localization State Interface
3
+ * Defines the shape of localization state management
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 LocalizationState {
15
+ currentLanguage: string;
16
+ isRTL: boolean;
17
+ isInitialized: boolean;
18
+ supportedLanguages: Language[];
19
+ }
20
+
21
+ export interface LocalizationActions {
22
+ initialize: () => Promise<void>;
23
+ setLanguage: (languageCode: string) => Promise<void>;
24
+ reset: () => void;
25
+ }
26
+
27
+ export interface LocalizationGetters {
28
+ getCurrentLanguage: () => Language | undefined;
29
+ isLanguageSupported: (code: string) => boolean;
30
+ getSupportedLanguages: () => Language[];
31
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Language Item Component
3
+ *
4
+ * Renders a single language item in the language selection list
5
+ */
6
+
7
+ import React from 'react';
8
+ import {
9
+ View,
10
+ TouchableOpacity,
11
+ Text,
12
+ StyleSheet,
13
+ } from 'react-native';
14
+ import type { Language } from '../../infrastructure/storage/types/LocalizationState';
15
+
16
+ interface LanguageItemProps {
17
+ item: Language;
18
+ isSelected: boolean;
19
+ onSelect: (code: string) => void;
20
+ customStyles?: {
21
+ languageItem?: any;
22
+ languageContent?: any;
23
+ languageText?: any;
24
+ flag?: any;
25
+ nativeName?: any;
26
+ };
27
+ }
28
+
29
+ export const LanguageItem: React.FC<LanguageItemProps> = ({
30
+ item,
31
+ isSelected,
32
+ onSelect,
33
+ customStyles,
34
+ }) => {
35
+ return (
36
+ <TouchableOpacity
37
+ testID="language-item-test"
38
+ style={[
39
+ styles.languageItem,
40
+ customStyles?.languageItem,
41
+ isSelected && styles.selectedLanguageItem,
42
+ ]}
43
+ onPress={() => onSelect(item.code)}
44
+ activeOpacity={0.7}
45
+ >
46
+ <View style={[styles.languageContent, customStyles?.languageContent]}>
47
+ <Text style={[styles.flag, customStyles?.flag]}>
48
+ {item.flag || '🌐'}
49
+ </Text>
50
+ <View style={[styles.languageText, customStyles?.languageText]}>
51
+ <Text style={[styles.nativeName, customStyles?.nativeName]}>
52
+ {item.nativeName}
53
+ </Text>
54
+ <Text style={[styles.languageName, customStyles?.nativeName]}>
55
+ {item.name}
56
+ </Text>
57
+ </View>
58
+ </View>
59
+ {isSelected && (
60
+ <Text style={[styles.checkIcon, customStyles?.flag]}>✓</Text>
61
+ )}
62
+ </TouchableOpacity>
63
+ );
64
+ };
65
+
66
+ const styles = StyleSheet.create({
67
+ languageItem: {
68
+ flexDirection: 'row',
69
+ alignItems: 'center',
70
+ justifyContent: 'space-between',
71
+ padding: 16,
72
+ borderRadius: 12,
73
+ borderWidth: 1,
74
+ borderColor: '#e0e0e0',
75
+ marginBottom: 8,
76
+ backgroundColor: '#fff',
77
+ },
78
+ selectedLanguageItem: {
79
+ borderColor: '#007AFF',
80
+ backgroundColor: '#f0f8ff',
81
+ },
82
+ languageContent: {
83
+ flexDirection: 'row',
84
+ alignItems: 'center',
85
+ flex: 1,
86
+ },
87
+ flag: {
88
+ fontSize: 24,
89
+ marginRight: 16,
90
+ },
91
+ languageText: {
92
+ flex: 1,
93
+ },
94
+ nativeName: {
95
+ fontSize: 16,
96
+ fontWeight: '600',
97
+ color: '#333',
98
+ marginBottom: 2,
99
+ },
100
+ languageName: {
101
+ fontSize: 14,
102
+ color: '#666',
103
+ },
104
+ checkIcon: {
105
+ fontSize: 18,
106
+ color: '#007AFF',
107
+ fontWeight: 'bold',
108
+ },
109
+ });
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Search Input Component
3
+ *
4
+ * Renders search input for language filtering
5
+ */
6
+
7
+ import React from 'react';
8
+ import {
9
+ View,
10
+ TextInput,
11
+ TouchableOpacity,
12
+ Text,
13
+ StyleSheet,
14
+ } from 'react-native';
15
+
16
+ interface SearchInputProps {
17
+ value: string;
18
+ onChange: (value: string) => void;
19
+ placeholder: string;
20
+ customStyles?: {
21
+ searchContainer?: any;
22
+ searchInput?: any;
23
+ searchIcon?: any;
24
+ clearButton?: any;
25
+ };
26
+ }
27
+
28
+ export const SearchInput: React.FC<SearchInputProps> = ({
29
+ value,
30
+ onChange,
31
+ placeholder,
32
+ customStyles,
33
+ }) => {
34
+ return (
35
+ <View style={[styles.searchContainer, customStyles?.searchContainer]}>
36
+ <Text style={[styles.searchIcon, customStyles?.searchIcon]}>🔍</Text>
37
+ <TextInput
38
+ style={[styles.searchInput, customStyles?.searchInput]}
39
+ placeholder={placeholder}
40
+ placeholderTextColor="#666"
41
+ value={value}
42
+ onChangeText={onChange}
43
+ autoCapitalize="none"
44
+ autoCorrect={false}
45
+ />
46
+ {value.length > 0 && (
47
+ <TouchableOpacity
48
+ onPress={() => onChange('')}
49
+ style={[styles.clearButton, customStyles?.clearButton]}
50
+ hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
51
+ >
52
+ <Text style={[styles.clearIcon, customStyles?.searchIcon]}>✕</Text>
53
+ </TouchableOpacity>
54
+ )}
55
+ </View>
56
+ );
57
+ };
58
+
59
+ const styles = StyleSheet.create({
60
+ searchContainer: {
61
+ flexDirection: 'row',
62
+ alignItems: 'center',
63
+ marginHorizontal: 20,
64
+ marginBottom: 24,
65
+ paddingHorizontal: 16,
66
+ paddingVertical: 12,
67
+ backgroundColor: '#f5f5f5',
68
+ borderRadius: 12,
69
+ borderWidth: 1,
70
+ borderColor: '#e0e0e0',
71
+ },
72
+ searchIcon: {
73
+ marginRight: 12,
74
+ fontSize: 16,
75
+ },
76
+ searchInput: {
77
+ flex: 1,
78
+ fontSize: 16,
79
+ padding: 0,
80
+ fontWeight: '500',
81
+ color: '#333',
82
+ },
83
+ clearButton: {
84
+ padding: 4,
85
+ },
86
+ clearIcon: {
87
+ fontSize: 14,
88
+ color: '#666',
89
+ },
90
+ });
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Language Item Component Tests
3
+ */
4
+
5
+ import React from 'react';
6
+ import { render, fireEvent } from '@testing-library/react-native';
7
+ import { LanguageItem } from '../LanguageItem';
8
+ import type { Language } from '../../infrastructure/storage/types/LocalizationState';
9
+
10
+ const mockLanguage: Language = {
11
+ code: 'en-US',
12
+ name: 'English',
13
+ nativeName: 'English',
14
+ flag: '🇺🇸',
15
+ isRTL: false,
16
+ };
17
+
18
+ describe('LanguageItem', () => {
19
+ const mockOnSelect = jest.fn();
20
+
21
+ beforeEach(() => {
22
+ mockOnSelect.mockClear();
23
+ });
24
+
25
+ it('should render language information correctly', () => {
26
+ const { getAllByText } = render(
27
+ <LanguageItem
28
+ item={mockLanguage}
29
+ isSelected={false}
30
+ onSelect={mockOnSelect}
31
+ />
32
+ );
33
+
34
+ expect(getAllByText('English')).toHaveLength(2); // nativeName and name
35
+ expect(getAllByText('🇺🇸')).toHaveLength(1);
36
+ });
37
+
38
+ it('should show check icon when selected', () => {
39
+ const { getByText } = render(
40
+ <LanguageItem
41
+ item={mockLanguage}
42
+ isSelected={true}
43
+ onSelect={mockOnSelect}
44
+ />
45
+ );
46
+
47
+ expect(getByText('✓')).toBeTruthy();
48
+ });
49
+
50
+ it('should not show check icon when not selected', () => {
51
+ const { queryByText } = render(
52
+ <LanguageItem
53
+ item={mockLanguage}
54
+ isSelected={false}
55
+ onSelect={mockOnSelect}
56
+ />
57
+ );
58
+
59
+ expect(queryByText('✓')).toBeFalsy();
60
+ });
61
+
62
+ it('should call onSelect when pressed', () => {
63
+ const { getByTestId } = render(
64
+ <LanguageItem
65
+ item={mockLanguage}
66
+ isSelected={false}
67
+ onSelect={mockOnSelect}
68
+ testID="language-item-test"
69
+ />
70
+ );
71
+
72
+ fireEvent.press(getByTestId('language-item-test'));
73
+ expect(mockOnSelect).toHaveBeenCalledWith('en-US');
74
+ });
75
+
76
+ it('should use default flag when none provided', () => {
77
+ const languageWithoutFlag = { ...mockLanguage, flag: undefined };
78
+ const { getByText } = render(
79
+ <LanguageItem
80
+ item={languageWithoutFlag}
81
+ isSelected={false}
82
+ onSelect={mockOnSelect}
83
+ />
84
+ );
85
+
86
+ expect(getByText('🌐')).toBeTruthy();
87
+ });
88
+
89
+ it('should apply custom styles', () => {
90
+ const customStyles = {
91
+ languageItem: { backgroundColor: 'red' },
92
+ flag: { fontSize: 30 },
93
+ };
94
+
95
+ const { getByTestId } = render(
96
+ <LanguageItem
97
+ item={mockLanguage}
98
+ isSelected={false}
99
+ onSelect={mockOnSelect}
100
+ customStyles={customStyles}
101
+ />
102
+ );
103
+
104
+ expect(getByTestId('language-item-test')).toBeTruthy();
105
+ });
106
+ });
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Search Input Component Tests
3
+ */
4
+
5
+ import React from 'react';
6
+ import { render, fireEvent } from '@testing-library/react-native';
7
+ import { SearchInput } from '../SearchInput';
8
+
9
+ describe('SearchInput', () => {
10
+ const mockOnChange = jest.fn();
11
+
12
+ beforeEach(() => {
13
+ mockOnChange.mockClear();
14
+ });
15
+
16
+ it('should render with placeholder', () => {
17
+ const { getByPlaceholderText } = render(
18
+ <SearchInput
19
+ value=""
20
+ onChange={mockOnChange}
21
+ placeholder="Search languages..."
22
+ />
23
+ );
24
+
25
+ expect(getByPlaceholderText('Search languages...')).toBeTruthy();
26
+ });
27
+
28
+ it('should call onChange when text changes', () => {
29
+ const { getByPlaceholderText } = render(
30
+ <SearchInput
31
+ value=""
32
+ onChange={mockOnChange}
33
+ placeholder="Search languages..."
34
+ />
35
+ );
36
+
37
+ fireEvent.changeText(getByPlaceholderText('Search languages...'), 'test');
38
+ expect(mockOnChange).toHaveBeenCalledWith('test');
39
+ });
40
+
41
+ it('should show clear button when text is present', () => {
42
+ const { getByText } = render(
43
+ <SearchInput
44
+ value="test"
45
+ onChange={mockOnChange}
46
+ placeholder="Search languages..."
47
+ />
48
+ );
49
+
50
+ expect(getByText('✕')).toBeTruthy();
51
+ });
52
+
53
+ it('should not show clear button when text is empty', () => {
54
+ const { queryByText } = render(
55
+ <SearchInput
56
+ value=""
57
+ onChange={mockOnChange}
58
+ placeholder="Search languages..."
59
+ />
60
+ );
61
+
62
+ expect(queryByText('✕')).toBeFalsy();
63
+ });
64
+
65
+ it('should clear text when clear button is pressed', () => {
66
+ const { getByText } = render(
67
+ <SearchInput
68
+ value="test"
69
+ onChange={mockOnChange}
70
+ placeholder="Search languages..."
71
+ />
72
+ );
73
+
74
+ fireEvent.press(getByText('✕'));
75
+ expect(mockOnChange).toHaveBeenCalledWith('');
76
+ });
77
+
78
+ it('should apply custom styles', () => {
79
+ const customStyles = {
80
+ searchContainer: { backgroundColor: 'red' },
81
+ searchInput: { fontSize: 20 },
82
+ };
83
+
84
+ const { getByPlaceholderText } = render(
85
+ <SearchInput
86
+ value=""
87
+ onChange={mockOnChange}
88
+ placeholder="Search languages..."
89
+ customStyles={customStyles}
90
+ />
91
+ );
92
+
93
+ expect(getByPlaceholderText('Search languages...')).toBeTruthy();
94
+ });
95
+ });