@umituz/react-native-localization 3.2.5 → 3.3.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 CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@umituz/react-native-localization",
3
- "version": "3.2.5",
3
+ "version": "3.3.0",
4
4
  "description": "Generic localization system for React Native apps with i18n support",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
7
7
  "scripts": {
8
8
  "typecheck": "tsc --noEmit --skipLibCheck",
9
- "lint": "tsc --noEmit",
9
+ "lint": "tsc --noEmit --skipLibCheck",
10
10
  "test": "jest",
11
11
  "test:watch": "jest --watch",
12
12
  "test:coverage": "jest --coverage",
@@ -1,17 +1,15 @@
1
1
  /**
2
2
  * Translation Hook
3
3
  *
4
- * Provides translation function with proper fallbacks and performance optimization
5
- * - React i18next integration
6
- * - Memoized translation function
7
- * - Type-safe translation function
4
+ * Provides translation function with proper language change reactivity
5
+ * - React i18next integration for automatic language change detection
8
6
  * - Auto-namespace detection from dot notation
9
- * - Performance optimizations
7
+ * - Type-safe translation function
10
8
  */
11
9
 
12
- import { useCallback, useMemo } from 'react';
10
+ import { useCallback } from 'react';
11
+ import { useTranslation } from 'react-i18next';
13
12
  import i18n from '../config/i18n';
14
- import { translationCache } from '../config/TranslationCache';
15
13
 
16
14
  export interface TranslationOptions {
17
15
  count?: number;
@@ -20,90 +18,54 @@ export interface TranslationOptions {
20
18
  [key: string]: any;
21
19
  }
22
20
 
23
-
24
21
  /**
25
22
  * Hook for translation functionality
23
+ * Uses react-i18next for automatic language change reactivity
24
+ *
26
25
  * Supports both formats:
27
26
  * - t('namespace:key.subkey') - explicit namespace
28
27
  * - t('namespace.key.subkey') - auto-detected namespace (first segment before dot)
29
28
  */
30
29
  export const useTranslationFunction = () => {
31
- const isInitialized = useMemo(() => i18n.isInitialized, []);
30
+ const { t: i18nextT, ready } = useTranslation(undefined, { i18n });
32
31
 
33
32
  const translate = useCallback((key: string, options: TranslationOptions = {}): string => {
34
- if (!isInitialized || typeof i18n.t !== 'function') {
33
+ if (!ready || !i18n.isInitialized) {
35
34
  if (__DEV__) {
36
- console.warn(`[Localization] i18n not initialized, returning key: ${key}`);
35
+ console.warn(`[Localization] i18n not ready, returning key: ${key}`);
37
36
  }
38
37
  return options.defaultValue || key;
39
38
  }
40
39
 
41
- // Create cache key
42
- const cacheKey = `${key}:${JSON.stringify(options)}`;
43
-
44
- // Check cache first
45
- const cached = translationCache.get(cacheKey);
46
- if (cached) {
47
- return cached;
48
- }
49
-
50
- let result: string;
51
-
52
40
  // If key already has namespace separator (:), use as-is
53
41
  if (key.includes(':')) {
54
- const tempResult = i18n.t(key, options);
55
- result = typeof tempResult === 'string' ? tempResult : key;
56
- } else {
57
- // Auto-detect namespace from first dot segment
58
- const firstDotIndex = key.indexOf('.');
59
- if (firstDotIndex > 0) {
60
- const potentialNamespace = key.substring(0, firstDotIndex);
61
- const restOfKey = key.substring(firstDotIndex + 1);
42
+ const result = i18nextT(key, options);
43
+ return typeof result === 'string' ? result : key;
44
+ }
62
45
 
63
- // Check if this namespace exists in i18n resources
64
- const hasNamespace = i18n.hasResourceBundle(i18n.language, potentialNamespace);
46
+ // Auto-detect namespace from first dot segment
47
+ const firstDotIndex = key.indexOf('.');
48
+ if (firstDotIndex > 0) {
49
+ const potentialNamespace = key.substring(0, firstDotIndex);
50
+ const restOfKey = key.substring(firstDotIndex + 1);
51
+ const hasNamespace = i18n.hasResourceBundle(i18n.language, potentialNamespace);
65
52
 
66
- if (hasNamespace) {
67
- const namespacedKey = `${potentialNamespace}:${restOfKey}`;
68
- const namespacedResult = i18n.t(namespacedKey, options);
53
+ if (hasNamespace) {
54
+ const namespacedKey = `${potentialNamespace}:${restOfKey}`;
55
+ const namespacedResult = i18nextT(namespacedKey, options);
69
56
 
70
- // If translation found (not same as key), use it
71
- if (namespacedResult !== namespacedKey && namespacedResult !== restOfKey) {
72
- result = typeof namespacedResult === 'string' ? namespacedResult : key;
73
- } else {
74
- // Fallback to original key
75
- const fallbackResult = i18n.t(key, options);
76
- result = typeof fallbackResult === 'string' ? fallbackResult : key;
77
- }
78
- } else {
79
- // Fallback to original key
80
- const tempResult = i18n.t(key, options);
81
- result = typeof tempResult === 'string' ? tempResult : key;
57
+ if (namespacedResult !== namespacedKey && namespacedResult !== restOfKey) {
58
+ return typeof namespacedResult === 'string' ? namespacedResult : key;
82
59
  }
83
- } else {
84
- // No dot, use as-is
85
- const noDotResult = i18n.t(key, options);
86
- result = typeof noDotResult === 'string' ? noDotResult : key;
87
60
  }
88
61
  }
89
62
 
90
- // Convert to string and cache
91
- const finalResult: string = typeof result === 'string' ? result : key;
92
- translationCache.set(cacheKey, finalResult);
93
-
94
- return finalResult;
95
- }, [isInitialized]);
96
-
97
- // Clear cache when language changes
98
- const clearCache = useCallback(() => {
99
- translationCache.clear();
100
- if (__DEV__) {
101
- console.log('[Localization] Translation cache cleared');
102
- }
103
- }, []);
63
+ // Fallback to original key
64
+ const result = i18nextT(key, options);
65
+ return typeof result === 'string' ? result : key;
66
+ }, [i18nextT, ready]);
104
67
 
105
68
  return {
106
69
  t: translate,
107
- clearCache,
108
70
  };
109
71
  };
@@ -22,7 +22,7 @@ export class LanguageSwitcher {
22
22
  }> {
23
23
  const language = languageRegistry.getLanguageByCode(languageCode);
24
24
 
25
- if (!language) {
25
+ if (!language && __DEV__) {
26
26
  console.warn(`[LanguageSwitcher] Language ${languageCode} not found in registry, proceeding anyway.`);
27
27
  }
28
28
 
@@ -4,11 +4,10 @@
4
4
  */
5
5
 
6
6
  import { create } from 'zustand';
7
- import type { LocalizationState, LocalizationActions, LocalizationGetters, Language } from './types/LocalizationState';
7
+ import type { LocalizationState, LocalizationActions, LocalizationGetters } from './types/LocalizationState';
8
8
  import { LanguageInitializer } from './LanguageInitializer';
9
9
  import { LanguageSwitcher } from './LanguageSwitcher';
10
10
  import { languageRegistry } from '../config/languagesData';
11
- import { translationCache } from '../config/TranslationCache';
12
11
 
13
12
  interface LocalizationStore extends LocalizationState, LocalizationActions, LocalizationGetters {
14
13
  // Additional properties can be added here if needed
@@ -68,9 +67,6 @@ export const createLocalizationStore = () => {
68
67
  try {
69
68
  const result = await LanguageSwitcher.switchLanguage(languageCode);
70
69
 
71
- // Clear translation cache to ensure new keys are fetched
72
- translationCache.clear();
73
-
74
70
  set({
75
71
  currentLanguage: result.languageCode,
76
72
  isRTL: result.isRTL,
@@ -1,16 +1,22 @@
1
1
  /**
2
2
  * Language Item Component
3
- *
3
+ *
4
4
  * Renders a single language item in the language selection list
5
+ * Theme-aware component that adapts to light/dark mode
5
6
  */
6
7
 
7
- import React from 'react';
8
+ import React, { useMemo } from 'react';
8
9
  import {
9
10
  View,
10
11
  TouchableOpacity,
11
12
  Text,
12
13
  StyleSheet,
14
+ type StyleProp,
15
+ type ViewStyle,
16
+ type TextStyle,
13
17
  } from 'react-native';
18
+ // @ts-ignore - Optional peer dependency
19
+ import { useAppDesignTokens } from '@umituz/react-native-design-system-theme';
14
20
  import type { Language } from '../../infrastructure/storage/types/LocalizationState';
15
21
 
16
22
  interface LanguageItemProps {
@@ -18,11 +24,11 @@ interface LanguageItemProps {
18
24
  isSelected: boolean;
19
25
  onSelect: (code: string) => void;
20
26
  customStyles?: {
21
- languageItem?: any;
22
- languageContent?: any;
23
- languageText?: any;
24
- flag?: any;
25
- nativeName?: any;
27
+ languageItem?: StyleProp<ViewStyle>;
28
+ languageContent?: StyleProp<ViewStyle>;
29
+ languageText?: StyleProp<ViewStyle>;
30
+ flag?: StyleProp<TextStyle>;
31
+ nativeName?: StyleProp<TextStyle>;
26
32
  };
27
33
  }
28
34
 
@@ -32,13 +38,36 @@ export const LanguageItem: React.FC<LanguageItemProps> = ({
32
38
  onSelect,
33
39
  customStyles,
34
40
  }) => {
41
+ const tokens = useAppDesignTokens();
42
+
43
+ const themedStyles = useMemo(() => ({
44
+ languageItem: {
45
+ backgroundColor: tokens.colors.backgroundSecondary,
46
+ borderColor: tokens.colors.border,
47
+ } as ViewStyle,
48
+ selectedLanguageItem: {
49
+ borderColor: tokens.colors.primary,
50
+ backgroundColor: tokens.colors.primaryLight,
51
+ } as ViewStyle,
52
+ nativeName: {
53
+ color: tokens.colors.textPrimary,
54
+ } as TextStyle,
55
+ languageName: {
56
+ color: tokens.colors.textSecondary,
57
+ } as TextStyle,
58
+ checkIcon: {
59
+ color: tokens.colors.primary,
60
+ } as TextStyle,
61
+ }), [tokens]);
62
+
35
63
  return (
36
64
  <TouchableOpacity
37
65
  testID="language-item-test"
38
66
  style={[
39
67
  styles.languageItem,
68
+ themedStyles.languageItem,
40
69
  customStyles?.languageItem,
41
- isSelected && styles.selectedLanguageItem,
70
+ isSelected ? [styles.selectedLanguageItem, themedStyles.selectedLanguageItem] : undefined,
42
71
  ]}
43
72
  onPress={() => onSelect(item.code)}
44
73
  activeOpacity={0.7}
@@ -48,16 +77,16 @@ export const LanguageItem: React.FC<LanguageItemProps> = ({
48
77
  {item.flag || '🌐'}
49
78
  </Text>
50
79
  <View style={[styles.languageText, customStyles?.languageText]}>
51
- <Text style={[styles.nativeName, customStyles?.nativeName]}>
80
+ <Text style={[styles.nativeName, themedStyles.nativeName, customStyles?.nativeName]}>
52
81
  {item.nativeName}
53
82
  </Text>
54
- <Text style={[styles.languageName, customStyles?.nativeName]}>
83
+ <Text style={[styles.languageName, themedStyles.languageName, customStyles?.nativeName]}>
55
84
  {item.name}
56
85
  </Text>
57
86
  </View>
58
87
  </View>
59
88
  {isSelected && (
60
- <Text style={[styles.checkIcon, customStyles?.flag]}>✓</Text>
89
+ <Text style={[styles.checkIcon, themedStyles.checkIcon, customStyles?.flag]}>✓</Text>
61
90
  )}
62
91
  </TouchableOpacity>
63
92
  );
@@ -71,13 +100,10 @@ const styles = StyleSheet.create({
71
100
  padding: 16,
72
101
  borderRadius: 12,
73
102
  borderWidth: 1,
74
- borderColor: '#e0e0e0',
75
103
  marginBottom: 8,
76
- backgroundColor: '#fff',
77
104
  },
78
105
  selectedLanguageItem: {
79
- borderColor: '#007AFF',
80
- backgroundColor: '#f0f8ff',
106
+ borderWidth: 2,
81
107
  },
82
108
  languageContent: {
83
109
  flexDirection: 'row',
@@ -94,16 +120,13 @@ const styles = StyleSheet.create({
94
120
  nativeName: {
95
121
  fontSize: 16,
96
122
  fontWeight: '600',
97
- color: '#333',
98
123
  marginBottom: 2,
99
124
  },
100
125
  languageName: {
101
126
  fontSize: 14,
102
- color: '#666',
103
127
  },
104
128
  checkIcon: {
105
129
  fontSize: 18,
106
- color: '#007AFF',
107
130
  fontWeight: 'bold',
108
131
  },
109
132
  });
@@ -1,7 +1,10 @@
1
1
  import React from 'react';
2
- import { View, Text, Pressable, StyleSheet, ViewStyle } from 'react-native';
2
+ import { View, Text, Pressable, StyleSheet, type ViewStyle } from 'react-native';
3
+ // @ts-ignore - Optional peer dependency
3
4
  import { Feather } from '@expo/vector-icons';
5
+ // @ts-ignore - Optional peer dependency
4
6
  import { useNavigation } from '@react-navigation/native';
7
+ // @ts-ignore - Optional peer dependency
5
8
  import { useAppDesignTokens } from '@umituz/react-native-design-system-theme';
6
9
  import { useLocalization } from '../../infrastructure/hooks/useLocalization';
7
10
  import { getLanguageByCode } from '../../infrastructure/config/languages';
@@ -1,27 +1,33 @@
1
1
  /**
2
2
  * Search Input Component
3
- *
3
+ *
4
4
  * Renders search input for language filtering
5
+ * Theme-aware component that adapts to light/dark mode
5
6
  */
6
7
 
7
- import React from 'react';
8
+ import React, { useMemo } from 'react';
8
9
  import {
9
10
  View,
10
11
  TextInput,
11
12
  TouchableOpacity,
12
13
  Text,
13
14
  StyleSheet,
15
+ type StyleProp,
16
+ type ViewStyle,
17
+ type TextStyle,
14
18
  } from 'react-native';
19
+ // @ts-ignore - Optional peer dependency
20
+ import { useAppDesignTokens } from '@umituz/react-native-design-system-theme';
15
21
 
16
22
  interface SearchInputProps {
17
23
  value: string;
18
24
  onChange: (value: string) => void;
19
25
  placeholder: string;
20
26
  customStyles?: {
21
- searchContainer?: any;
22
- searchInput?: any;
23
- searchIcon?: any;
24
- clearButton?: any;
27
+ searchContainer?: StyleProp<ViewStyle>;
28
+ searchInput?: StyleProp<TextStyle>;
29
+ searchIcon?: StyleProp<TextStyle>;
30
+ clearButton?: StyleProp<ViewStyle>;
25
31
  };
26
32
  }
27
33
 
@@ -31,13 +37,28 @@ export const SearchInput: React.FC<SearchInputProps> = ({
31
37
  placeholder,
32
38
  customStyles,
33
39
  }) => {
40
+ const tokens = useAppDesignTokens();
41
+
42
+ const themedStyles = useMemo(() => ({
43
+ searchContainer: {
44
+ backgroundColor: tokens.colors.backgroundSecondary,
45
+ borderColor: tokens.colors.border,
46
+ } as ViewStyle,
47
+ searchInput: {
48
+ color: tokens.colors.textPrimary,
49
+ } as TextStyle,
50
+ clearIcon: {
51
+ color: tokens.colors.textSecondary,
52
+ } as TextStyle,
53
+ }), [tokens]);
54
+
34
55
  return (
35
- <View style={[styles.searchContainer, customStyles?.searchContainer]}>
56
+ <View style={[styles.searchContainer, themedStyles.searchContainer, customStyles?.searchContainer]}>
36
57
  <Text style={[styles.searchIcon, customStyles?.searchIcon]}>🔍</Text>
37
58
  <TextInput
38
- style={[styles.searchInput, customStyles?.searchInput]}
59
+ style={[styles.searchInput, themedStyles.searchInput, customStyles?.searchInput]}
39
60
  placeholder={placeholder}
40
- placeholderTextColor="#666"
61
+ placeholderTextColor={tokens.colors.textTertiary}
41
62
  value={value}
42
63
  onChangeText={onChange}
43
64
  autoCapitalize="none"
@@ -49,7 +70,7 @@ export const SearchInput: React.FC<SearchInputProps> = ({
49
70
  style={[styles.clearButton, customStyles?.clearButton]}
50
71
  hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
51
72
  >
52
- <Text style={[styles.clearIcon, customStyles?.searchIcon]}>✕</Text>
73
+ <Text style={[styles.clearIcon, themedStyles.clearIcon, customStyles?.searchIcon]}>✕</Text>
53
74
  </TouchableOpacity>
54
75
  )}
55
76
  </View>
@@ -64,10 +85,8 @@ const styles = StyleSheet.create({
64
85
  marginBottom: 24,
65
86
  paddingHorizontal: 16,
66
87
  paddingVertical: 12,
67
- backgroundColor: '#f5f5f5',
68
88
  borderRadius: 12,
69
89
  borderWidth: 1,
70
- borderColor: '#e0e0e0',
71
90
  },
72
91
  searchIcon: {
73
92
  marginRight: 12,
@@ -78,13 +97,11 @@ const styles = StyleSheet.create({
78
97
  fontSize: 16,
79
98
  padding: 0,
80
99
  fontWeight: '500',
81
- color: '#333',
82
100
  },
83
101
  clearButton: {
84
102
  padding: 4,
85
103
  },
86
104
  clearIcon: {
87
105
  fontSize: 14,
88
- color: '#666',
89
106
  },
90
107
  });
@@ -1,31 +0,0 @@
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
- if (firstKey !== undefined) {
20
- this.cache.delete(firstKey);
21
- }
22
- }
23
- this.cache.set(key, value);
24
- }
25
-
26
- clear(): void {
27
- this.cache.clear();
28
- }
29
- }
30
-
31
- export const translationCache = new TranslationCache();
@@ -1,44 +0,0 @@
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
- });