@umituz/react-native-localization 1.16.2 → 2.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-localization",
3
- "version": "1.16.2",
3
+ "version": "2.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",
package/src/index.ts CHANGED
@@ -5,6 +5,7 @@
5
5
 
6
6
  // Hooks
7
7
  export { useLocalization, useLocalizationStore } from './infrastructure/storage/LocalizationStore';
8
+ export { useTranslationFunction } from './infrastructure/hooks/useTranslation';
8
9
 
9
10
  // Components
10
11
  export { LocalizationProvider } from './infrastructure/components/LocalizationProvider';
@@ -0,0 +1,113 @@
1
+ /**
2
+ * i18n Initializer
3
+ *
4
+ * Handles i18n configuration and initialization
5
+ * - Resource building
6
+ * - i18n setup
7
+ * - React i18next integration
8
+ */
9
+
10
+ import i18n from 'i18next';
11
+ import { initReactI18next } from 'react-i18next';
12
+ import { DEFAULT_LANGUAGE, SUPPORTED_LANGUAGES } from './languages';
13
+ import { TranslationLoader } from './TranslationLoader';
14
+
15
+ export class I18nInitializer {
16
+ private static reactI18nextInitialized = false;
17
+
18
+ /**
19
+ * Build resources object for all supported languages
20
+ */
21
+ private static buildResources(): Record<string, { translation: any }> {
22
+ const resources: Record<string, { translation: any }> = {};
23
+ const packageTranslations = TranslationLoader.loadPackageTranslations();
24
+ const projectTranslations = TranslationLoader.loadProjectTranslations();
25
+
26
+ // Build resources for each supported language
27
+ for (const lang of SUPPORTED_LANGUAGES) {
28
+ const langCode = lang.code;
29
+ const packageTranslation = langCode === 'en-US' ? (packageTranslations['en-US'] || {}) : {};
30
+ const projectTranslation = projectTranslations[langCode] || {};
31
+
32
+ // For en-US, merge package and project translations
33
+ // For other languages, use project translations only (fallback to en-US handled by i18n)
34
+ if (langCode === 'en-US') {
35
+ resources[langCode] = {
36
+ translation: TranslationLoader.mergeTranslations(packageTranslation, projectTranslation),
37
+ };
38
+ } else if (projectTranslation && Object.keys(projectTranslation).length > 0) {
39
+ resources[langCode] = {
40
+ translation: projectTranslation,
41
+ };
42
+ }
43
+ }
44
+
45
+ // Ensure en-US is always present
46
+ if (!resources['en-US']) {
47
+ resources['en-US'] = {
48
+ translation: packageTranslations['en-US'] || {},
49
+ };
50
+ }
51
+
52
+ return resources;
53
+ }
54
+
55
+ /**
56
+ * Initialize i18next
57
+ */
58
+ static initialize(): void {
59
+ // Prevent multiple initializations
60
+ if (i18n.isInitialized) {
61
+ return;
62
+ }
63
+
64
+ try {
65
+ // Use initReactI18next once
66
+ if (!this.reactI18nextInitialized) {
67
+ i18n.use(initReactI18next);
68
+ this.reactI18nextInitialized = true;
69
+ }
70
+
71
+ const resources = this.buildResources();
72
+
73
+ i18n.init({
74
+ resources,
75
+ lng: DEFAULT_LANGUAGE,
76
+ fallbackLng: DEFAULT_LANGUAGE,
77
+
78
+ interpolation: {
79
+ escapeValue: false, // React already escapes values
80
+ },
81
+
82
+ react: {
83
+ useSuspense: false, // Disable suspense for React Native
84
+ },
85
+
86
+ compatibilityJSON: 'v3', // Use v3 format for React Native
87
+ pluralSeparator: '_', // Use underscore separator for plural keys
88
+ keySeparator: '.', // Use dot separator for nested keys
89
+
90
+ debug: typeof __DEV__ !== 'undefined' && __DEV__,
91
+ });
92
+
93
+ } catch (error) {
94
+ // Don't throw - allow app to continue without i18n
95
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
96
+ console.error('❌ i18n initialization error:', error);
97
+ }
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Add additional translation resources
103
+ */
104
+ static addTranslationResources(resources: Record<string, { translation: any }>): void {
105
+ for (const [langCode, resource] of Object.entries(resources)) {
106
+ if (resource.translation) {
107
+ const existingTranslations = i18n.getResourceBundle(langCode, 'translation') || {};
108
+ const mergedTranslations = { ...existingTranslations, ...resource.translation };
109
+ i18n.addResourceBundle(langCode, 'translation', mergedTranslations, true, true);
110
+ }
111
+ }
112
+ }
113
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Translation Loader
3
+ *
4
+ * Handles loading of translations from different sources
5
+ * - Package translations
6
+ * - Project translations
7
+ * - Resource merging
8
+ */
9
+
10
+ export class TranslationLoader {
11
+ /**
12
+ * Load package translations (en-US only)
13
+ */
14
+ static loadPackageTranslations(): Record<string, any> {
15
+ try {
16
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
17
+ const translations = require('../locales/en-US');
18
+ return { 'en-US': translations.default || translations };
19
+ } catch (error) {
20
+ return { 'en-US': {} };
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Load project translations for all supported languages
26
+ * Currently returns empty as projects manage their own translations
27
+ */
28
+ static loadProjectTranslations(): Record<string, any> {
29
+ return {};
30
+ }
31
+
32
+ /**
33
+ * Merge package defaults with project-specific translations
34
+ */
35
+ static mergeTranslations(
36
+ packageTranslations: any,
37
+ projectTranslations: any
38
+ ): any {
39
+ if (!projectTranslations || Object.keys(projectTranslations).length === 0) {
40
+ return packageTranslations;
41
+ }
42
+
43
+ const merged = { ...packageTranslations };
44
+
45
+ for (const key in projectTranslations) {
46
+ if (projectTranslations.hasOwnProperty(key)) {
47
+ if (
48
+ typeof projectTranslations[key] === 'object' &&
49
+ projectTranslations[key] !== null &&
50
+ !Array.isArray(projectTranslations[key]) &&
51
+ typeof packageTranslations[key] === 'object' &&
52
+ packageTranslations[key] !== null &&
53
+ !Array.isArray(packageTranslations[key])
54
+ ) {
55
+ // Deep merge nested objects
56
+ merged[key] = this.mergeTranslations(
57
+ packageTranslations[key],
58
+ projectTranslations[key]
59
+ );
60
+ } else {
61
+ // Override with project translation
62
+ merged[key] = projectTranslations[key];
63
+ }
64
+ }
65
+ }
66
+
67
+ return merged;
68
+ }
69
+ }
@@ -1,215 +1,17 @@
1
1
  /**
2
- * i18next Configuration for Multi-language Support
3
- * Loads all supported languages from project translations
2
+ * i18n Configuration Entry Point
4
3
  *
5
- * MULTI-LANGUAGE LOADING:
6
- * - Loads all languages from project translations
7
- * - Project translations merged with package defaults
8
- * - Metro bundler resolves all requires at build time
4
+ * Delegates to I18nInitializer for setup
5
+ * Exports i18n instance and utility functions
9
6
  */
10
7
 
11
8
  import i18n from 'i18next';
12
- import { initReactI18next } from 'react-i18next';
13
- import { DEFAULT_LANGUAGE, SUPPORTED_LANGUAGES } from './languages';
9
+ import { I18nInitializer } from './I18nInitializer';
14
10
 
15
- /**
16
- * Load package translations (en-US only)
17
- */
18
- const loadPackageTranslations = (): Record<string, any> => {
19
- try {
20
- // Load en-US package translations
21
- // eslint-disable-next-line @typescript-eslint/no-require-imports
22
- const translations = require('../locales/en-US');
23
- return { 'en-US': translations.default || translations };
24
- } catch (error) {
25
- // Fallback to empty translations
26
- return { 'en-US': {} };
27
- }
28
- };
29
-
30
- const packageTranslations = loadPackageTranslations();
31
-
32
- /**
33
- * Load project translations for all supported languages
34
- * This function is a placeholder for future extensibility
35
- * Currently returns empty translations as projects should manage their own translations
36
- */
37
- const loadProjectTranslations = (): Record<string, any> => {
38
- // Projects should create their own localization domains
39
- // and manage translations within their app structure
40
- // This package provides only the core i18n infrastructure
41
- return {};
42
- };
43
-
44
- const projectTranslations = loadProjectTranslations();
45
-
46
- /**
47
- * Translation Resources
48
- * Merge package defaults with project-specific translations
49
- * Project translations override package defaults (deep merge)
50
- */
51
- const mergeTranslations = (packageTranslations: any, projectTranslations: any): any => {
52
- if (!projectTranslations || Object.keys(projectTranslations).length === 0) {
53
- return packageTranslations;
54
- }
55
-
56
- // Deep merge: project translations override package defaults
57
- const merged = { ...packageTranslations };
58
-
59
- for (const key in projectTranslations) {
60
- if (projectTranslations.hasOwnProperty(key)) {
61
- if (
62
- typeof projectTranslations[key] === 'object' &&
63
- projectTranslations[key] !== null &&
64
- !Array.isArray(projectTranslations[key]) &&
65
- typeof packageTranslations[key] === 'object' &&
66
- packageTranslations[key] !== null &&
67
- !Array.isArray(packageTranslations[key])
68
- ) {
69
- // Deep merge nested objects
70
- merged[key] = mergeTranslations(packageTranslations[key], projectTranslations[key]);
71
- } else {
72
- // Override with project translation
73
- merged[key] = projectTranslations[key];
74
- }
75
- }
76
- }
77
-
78
- return merged;
79
- };
80
-
81
- /**
82
- * Build resources object for all supported languages
83
- */
84
- const buildResources = (): Record<string, { translation: any }> => {
85
- const resources: Record<string, { translation: any }> = {};
86
-
87
- // Build resources for each supported language
88
- for (const lang of SUPPORTED_LANGUAGES) {
89
- const langCode = lang.code;
90
- const packageTranslation = langCode === 'en-US' ? (packageTranslations['en-US'] || {}) : {};
91
- const projectTranslation = projectTranslations[langCode] || {};
92
-
93
- // For en-US, merge package and project translations
94
- // For other languages, use project translations only (fallback to en-US handled by i18n)
95
- if (langCode === 'en-US') {
96
- resources[langCode] = {
97
- translation: mergeTranslations(packageTranslation, projectTranslation),
98
- };
99
- } else if (projectTranslation && Object.keys(projectTranslation).length > 0) {
100
- resources[langCode] = {
101
- translation: projectTranslation,
102
- };
103
- }
104
- }
11
+ // Initialize i18n immediately
12
+ I18nInitializer.initialize();
105
13
 
106
- // Ensure en-US is always present
107
- if (!resources['en-US']) {
108
- resources['en-US'] = {
109
- translation: packageTranslations['en-US'] || {},
110
- };
111
- }
112
-
113
- return resources;
114
- };
115
-
116
- const resources = buildResources();
117
-
118
- // Debug: Log loaded resources in development (only once to prevent spam)
119
- if (typeof globalThis !== 'undefined' && !(globalThis as any).__i18n_resources_logged) {
120
- /* eslint-disable-next-line no-console */
121
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
122
- console.log('🌍 i18n Resources loaded:', {
123
- languages: Object.keys(resources),
124
- enUSKeys: resources['en-US']?.translation ? Object.keys(resources['en-US'].translation) : [],
125
- });
126
- (globalThis as any).__i18n_resources_logged = true;
127
- }
128
- }
129
-
130
- // Global flag to ensure initReactI18next is only used once
131
- let reactI18nextInitialized = false;
132
-
133
- /**
134
- * Initialize i18next
135
- * CRITICAL: Check i18n.isInitialized to prevent multiple initializations
136
- * This prevents "i18next is already initialized" warnings when module is imported multiple times
137
- */
138
- const initializeI18n = () => {
139
- // CRITICAL: Check if i18n is already initialized (prevents multiple init calls)
140
- if (i18n.isInitialized) {
141
- return;
142
- }
143
-
144
- try {
145
- // Check if initReactI18next is available
146
- if (!initReactI18next) {
147
- throw new Error('initReactI18next is undefined');
148
- }
149
-
150
- // CRITICAL: Only use initReactI18next once (prevents context registration issues)
151
- if (!reactI18nextInitialized) {
152
- i18n.use(initReactI18next);
153
- reactI18nextInitialized = true;
154
- }
155
-
156
- i18n.init({
157
- resources,
158
- lng: DEFAULT_LANGUAGE,
159
- fallbackLng: DEFAULT_LANGUAGE,
160
-
161
- interpolation: {
162
- escapeValue: false, // React already escapes values
163
- },
164
-
165
- react: {
166
- useSuspense: false, // Disable suspense for React Native
167
- },
168
-
169
- compatibilityJSON: 'v3', // Use v3 format for React Native (no Intl.PluralRules support)
170
- pluralSeparator: '_', // Use underscore separator for plural keys
171
- keySeparator: '.', // Use dot separator for nested keys
172
-
173
- // Debug options
174
- debug: typeof __DEV__ !== 'undefined' && __DEV__,
175
- });
176
-
177
- // Debug: Verify initialization
178
- /* eslint-disable-next-line no-console */
179
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
180
- console.log('✅ i18n initialized:', {
181
- language: i18n.language,
182
- hasResource: !!i18n.getResourceBundle(i18n.language, 'translation'),
183
- });
184
- }
185
- } catch (error) {
186
- /* eslint-disable-next-line no-console */
187
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
188
- console.error('❌ i18n initialization error:', error);
189
- }
190
- // Don't throw - allow app to continue without i18n
191
- }
192
- };
193
-
194
- // Initialize immediately - no need to defer
195
- // React Native and React are ready when this module loads
196
- // Deferring causes race conditions with useTranslation hook
197
- // CRITICAL: i18n.isInitialized check prevents multiple initializations
198
- initializeI18n();
199
-
200
- /**
201
- * Add additional translation resources to the existing i18n instance
202
- * This allows projects to add their own translations to the package translations
203
- */
204
- export const addTranslationResources = (resources: Record<string, { translation: any }>) => {
205
- for (const [langCode, resource] of Object.entries(resources)) {
206
- if (resource.translation) {
207
- // Merge with existing translations if any
208
- const existingTranslations = i18n.getResourceBundle(langCode, 'translation') || {};
209
- const mergedTranslations = { ...existingTranslations, ...resource.translation };
210
- i18n.addResourceBundle(langCode, 'translation', mergedTranslations, true, true);
211
- }
212
- }
213
- };
14
+ // Export utility functions
15
+ export const addTranslationResources = I18nInitializer.addTranslationResources;
214
16
 
215
17
  export default i18n;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Translation Hook
3
+ *
4
+ * Provides translation function with fallback logic
5
+ * - React i18next integration
6
+ * - Direct i18n fallback
7
+ * - Type-safe translation function
8
+ */
9
+
10
+ import { useTranslation } from 'react-i18next';
11
+ import i18n from '../config/i18n';
12
+
13
+ export class TranslationHook {
14
+ /**
15
+ * Get translation function with proper fallbacks
16
+ */
17
+ static useTranslationFunction(): (key: string, options?: any) => string {
18
+ // Always call useTranslation hook (React hooks rules)
19
+ const translationResult = useTranslation(undefined, { i18n });
20
+
21
+ // Use react-i18next if available, otherwise fallback to direct i18n
22
+ if (translationResult?.t && typeof translationResult.t === 'function' && i18n.isInitialized) {
23
+ return (key: string, options?: any): string => {
24
+ const result = translationResult.t(key, options);
25
+ return typeof result === 'string' ? result : String(result);
26
+ };
27
+ } else {
28
+ return (key: string, options?: any): string => {
29
+ // Fallback to direct i18n.t
30
+ if (i18n.isInitialized && typeof i18n.t === 'function') {
31
+ const result = i18n.t(key, options);
32
+ return typeof result === 'string' ? result : String(result);
33
+ }
34
+ // Final fallback: return key
35
+ return key;
36
+ };
37
+ }
38
+ }
39
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Translation Hook
3
+ *
4
+ * Provides translation function with proper fallbacks
5
+ * - React i18next integration
6
+ * - Direct i18n fallback
7
+ * - Type-safe translation function
8
+ */
9
+
10
+ import { useTranslation } from 'react-i18next';
11
+ import i18n from '../config/i18n';
12
+
13
+ /**
14
+ * Hook for translation functionality
15
+ */
16
+ export const useTranslationFunction = (): ((key: string, options?: any) => string) => {
17
+ // Always call useTranslation hook (React hooks rules)
18
+ const translationResult = useTranslation(undefined, { i18n });
19
+
20
+ // Use react-i18next if available, otherwise fallback to direct i18n
21
+ if (translationResult?.t && typeof translationResult.t === 'function' && i18n.isInitialized) {
22
+ return (key: string, options?: any): string => {
23
+ const result = translationResult.t(key, options);
24
+ return typeof result === 'string' ? result : String(result);
25
+ };
26
+ } else {
27
+ return (key: string, options?: any): string => {
28
+ // Fallback to direct i18n.t
29
+ if (i18n.isInitialized && typeof i18n.t === 'function') {
30
+ const result = i18n.t(key, options);
31
+ return typeof result === 'string' ? result : String(result);
32
+ }
33
+ // Final fallback: return key
34
+ return key;
35
+ };
36
+ }
37
+ };
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Language Initializer
3
+ *
4
+ * Handles the initialization of localization system
5
+ * - Device locale detection
6
+ * - Language validation and fallback
7
+ * - i18n setup
8
+ */
9
+
10
+ import AsyncStorage from '@react-native-async-storage/async-storage';
11
+ import i18n from '../config/i18n';
12
+ import { DEFAULT_LANGUAGE, getLanguageByCode, getDeviceLocale } from '../config/languages';
13
+
14
+ // Storage key for language preference
15
+ const LANGUAGE_STORAGE_KEY = '@localization:language';
16
+
17
+ export class LanguageInitializer {
18
+ /**
19
+ * Initialize localization system
20
+ * Detects device locale and sets up i18n
21
+ */
22
+ static async initialize(): Promise<{
23
+ languageCode: string;
24
+ isRTL: boolean;
25
+ }> {
26
+ try {
27
+ // Get saved language preference
28
+ const savedLanguage = await AsyncStorage.getItem(LANGUAGE_STORAGE_KEY) || DEFAULT_LANGUAGE;
29
+
30
+ // Determine language code
31
+ const languageCode = await this.determineLanguageCode(savedLanguage);
32
+
33
+ // Validate and get language object
34
+ const finalLanguage = await this.validateAndSetupLanguage(languageCode);
35
+
36
+ return finalLanguage;
37
+ } catch (error) {
38
+ // Fallback to default language
39
+ return await this.setupFallbackLanguage();
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Determine which language code to use
45
+ */
46
+ private static async determineLanguageCode(savedLanguage: string): Promise<string> {
47
+ if (savedLanguage && savedLanguage !== DEFAULT_LANGUAGE) {
48
+ // User has previously selected a language
49
+ return savedLanguage;
50
+ } else {
51
+ // First launch - detect device locale
52
+ const deviceLocale = getDeviceLocale();
53
+ // Save detected locale for future launches
54
+ await AsyncStorage.setItem(LANGUAGE_STORAGE_KEY, deviceLocale);
55
+ return deviceLocale;
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Validate language and set it up in i18n
61
+ */
62
+ private static async validateAndSetupLanguage(languageCode: string): Promise<{
63
+ languageCode: string;
64
+ isRTL: boolean;
65
+ }> {
66
+ const language = getLanguageByCode(languageCode);
67
+ const finalLanguageCode = language ? languageCode : DEFAULT_LANGUAGE;
68
+ const finalLanguageObj = getLanguageByCode(finalLanguageCode);
69
+
70
+ await i18n.changeLanguage(finalLanguageCode);
71
+
72
+ return {
73
+ languageCode: finalLanguageCode,
74
+ isRTL: finalLanguageObj?.rtl || false,
75
+ };
76
+ }
77
+
78
+ /**
79
+ * Set up fallback language when initialization fails
80
+ */
81
+ private static async setupFallbackLanguage(): Promise<{
82
+ languageCode: string;
83
+ isRTL: boolean;
84
+ }> {
85
+ try {
86
+ await i18n.changeLanguage(DEFAULT_LANGUAGE);
87
+ return {
88
+ languageCode: DEFAULT_LANGUAGE,
89
+ isRTL: false,
90
+ };
91
+ } catch (fallbackError) {
92
+ throw fallbackError;
93
+ }
94
+ }
95
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Language Switcher
3
+ *
4
+ * Handles switching between languages
5
+ * - Language validation
6
+ * - Dynamic resource loading
7
+ * - Persistence
8
+ */
9
+
10
+ import AsyncStorage from '@react-native-async-storage/async-storage';
11
+ import i18n from '../config/i18n';
12
+ import { getLanguageByCode } from '../config/languages';
13
+
14
+ // Storage key for language preference
15
+ const LANGUAGE_STORAGE_KEY = '@localization:language';
16
+
17
+ export class LanguageSwitcher {
18
+ /**
19
+ * Switch to a new language
20
+ */
21
+ static async switchLanguage(languageCode: string): Promise<{
22
+ languageCode: string;
23
+ isRTL: boolean;
24
+ }> {
25
+ const language = getLanguageByCode(languageCode);
26
+
27
+ // Validate language exists
28
+ if (!language) {
29
+ throw new Error(`Unsupported language: ${languageCode}`);
30
+ }
31
+
32
+ // Load language resources if needed
33
+ await this.loadLanguageResources(languageCode);
34
+
35
+ // Update i18n
36
+ await i18n.changeLanguage(languageCode);
37
+
38
+ // Persist language preference
39
+ await AsyncStorage.setItem(LANGUAGE_STORAGE_KEY, languageCode);
40
+
41
+ return {
42
+ languageCode,
43
+ isRTL: language.rtl || false,
44
+ };
45
+ }
46
+
47
+ /**
48
+ * Load language resources dynamically
49
+ */
50
+ private static async loadLanguageResources(languageCode: string): Promise<void> {
51
+ if (i18n.hasResourceBundle(languageCode, 'translation')) {
52
+ return; // Already loaded
53
+ }
54
+
55
+ try {
56
+ // Try to load project translations from common paths
57
+ let translations: any = null;
58
+
59
+ const loadPaths = [
60
+ `../../../../../../src/domains/localization/infrastructure/locales/${languageCode}`,
61
+ `../../../../../../domains/localization/infrastructure/locales/${languageCode}`,
62
+ `../../../../../../src/locales/${languageCode}`,
63
+ ];
64
+
65
+ for (const path of loadPaths) {
66
+ try {
67
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
68
+ translations = require(path);
69
+ break;
70
+ } catch {
71
+ // Try next path
72
+ }
73
+ }
74
+
75
+ if (translations) {
76
+ const translationData = translations.default || translations;
77
+ i18n.addResourceBundle(languageCode, 'translation', translationData, true, true);
78
+ }
79
+ } catch (loadError) {
80
+ // If loading fails, continue with changeLanguage (will fallback to en-US)
81
+ }
82
+ }
83
+ }
@@ -1,23 +1,17 @@
1
1
  /**
2
2
  * Localization Store
3
- * Zustand state management for language preferences with AsyncStorage persistence
3
+ * Zustand state management for language preferences
4
4
  *
5
- * DDD ARCHITECTURE: Uses @umituz/react-native-storage for all storage operations
6
- * - Type-safe storage with StorageKey
7
- * - Result pattern for error handling
8
- * - Single source of truth for all storage
5
+ * Uses separate classes for initialization, switching, and translation
6
+ * Follows Single Responsibility Principle
9
7
  */
10
8
 
11
- import AsyncStorage from '@react-native-async-storage/async-storage';
12
9
  import { create } from 'zustand';
13
- import { useTranslation } from 'react-i18next';
14
- import i18n from '../config/i18n';
15
- import { SUPPORTED_LANGUAGES, DEFAULT_LANGUAGE, getLanguageByCode, getDeviceLocale } from '../config/languages';
10
+ import { SUPPORTED_LANGUAGES, DEFAULT_LANGUAGE, getLanguageByCode } from '../config/languages';
11
+ import { LanguageInitializer } from './LanguageInitializer';
12
+ import { LanguageSwitcher } from './LanguageSwitcher';
16
13
  import type { Language } from '../../domain/repositories/ILocalizationRepository';
17
14
 
18
- // Storage key for language preference
19
- const LANGUAGE_STORAGE_KEY = '@localization:language';
20
-
21
15
  interface LocalizationState {
22
16
  currentLanguage: string;
23
17
  isRTL: boolean;
@@ -34,123 +28,44 @@ export const useLocalizationStore = create<LocalizationState>((set, get) => ({
34
28
  supportedLanguages: SUPPORTED_LANGUAGES,
35
29
 
36
30
  /**
37
- * Initialize localization
38
- * DEVICE LOCALE DETECTION:
39
- * - First launch (no saved language): Automatically detect device locale
40
- * - After manual selection: Use saved language preference
41
- * - Fallback: English (en-US) if device locale not supported
31
+ * Initialize localization system
42
32
  */
43
33
  initialize: async () => {
44
- try {
45
- // CRITICAL FIX: Don't reset isInitialized if already initialized
46
- // This prevents UI flash on re-initialization
47
- const { isInitialized: alreadyInitialized } = get();
48
- if (alreadyInitialized) {
49
- return;
50
- }
51
-
52
- // Get saved language preference
53
- const savedLanguage = await AsyncStorage.getItem(LANGUAGE_STORAGE_KEY) || DEFAULT_LANGUAGE;
54
-
55
- // ✅ DEVICE LOCALE DETECTION: Use device locale on first launch
56
- let languageCode: string;
57
- if (savedLanguage && savedLanguage !== DEFAULT_LANGUAGE) {
58
- // User has previously selected a language → Use their choice
59
- languageCode = savedLanguage;
60
- } else {
61
- // First launch → Detect device locale automatically
62
- languageCode = getDeviceLocale();
63
- // Save detected locale for future launches
64
- await AsyncStorage.setItem(LANGUAGE_STORAGE_KEY, languageCode);
65
- }
34
+ // Prevent re-initialization
35
+ const { isInitialized: alreadyInitialized } = get();
36
+ if (alreadyInitialized) {
37
+ return;
38
+ }
66
39
 
67
- // ✅ DEFENSIVE: Validate language exists, fallback to default
68
- const language = getLanguageByCode(languageCode);
69
- const finalLanguage = language ? languageCode : DEFAULT_LANGUAGE;
70
- const finalLanguageObj = getLanguageByCode(finalLanguage);
40
+ try {
41
+ const result = await LanguageInitializer.initialize();
71
42
 
72
- await i18n.changeLanguage(finalLanguage);
73
-
74
43
  set({
75
- currentLanguage: finalLanguage,
76
- isRTL: finalLanguageObj?.rtl || false,
77
- isInitialized: true, // ✅ Always set true to unblock UI
44
+ currentLanguage: result.languageCode,
45
+ isRTL: result.isRTL,
46
+ isInitialized: true,
78
47
  });
79
48
  } catch (error) {
80
- // Set to default language even on error to prevent app from breaking
81
- try {
82
- await i18n.changeLanguage(DEFAULT_LANGUAGE);
83
- set({
84
- currentLanguage: DEFAULT_LANGUAGE,
85
- isRTL: false,
86
- isInitialized: true, // Set true even on error to unblock UI
87
- });
88
- } catch (fallbackError) {
89
- throw fallbackError;
90
- }
49
+ // Set fallback state even on error
50
+ set({
51
+ currentLanguage: DEFAULT_LANGUAGE,
52
+ isRTL: false,
53
+ isInitialized: true,
54
+ });
91
55
  }
92
56
  },
93
57
 
94
58
  /**
95
59
  * Change language
96
- * Updates i18n, state, and persists to AsyncStorage
97
- * Dynamically loads language resources if not already loaded
98
60
  */
99
61
  setLanguage: async (languageCode: string) => {
100
62
  try {
101
- const language = getLanguageByCode(languageCode);
102
-
103
- // ✅ DEFENSIVE: Early return if unsupported language
104
- if (!language) {
105
- return;
106
- }
107
-
108
- // ✅ DYNAMIC RESOURCE LOADING: Load language resource if not already loaded
109
- if (!i18n.hasResourceBundle(languageCode, 'translation')) {
110
- try {
111
- // Try to load project translations from common paths
112
- let translations: any = null;
113
-
114
- try {
115
- // Try DDD structure path
116
- // eslint-disable-next-line @typescript-eslint/no-require-imports
117
- translations = require(`../../../../../../src/domains/localization/infrastructure/locales/${languageCode}`);
118
- } catch (e1) {
119
- try {
120
- // Try alternative DDD structure path
121
- // eslint-disable-next-line @typescript-eslint/no-require-imports
122
- translations = require(`../../../../../../domains/localization/infrastructure/locales/${languageCode}`);
123
- } catch (e2) {
124
- try {
125
- // Try simple structure path
126
- // eslint-disable-next-line @typescript-eslint/no-require-imports
127
- translations = require(`../../../../../../src/locales/${languageCode}`);
128
- } catch (e3) {
129
- // No translations found - will fallback to en-US
130
- }
131
- }
132
- }
133
-
134
- if (translations) {
135
- const translationData = translations.default || translations;
136
- i18n.addResourceBundle(languageCode, 'translation', translationData, true, true);
137
- }
138
- } catch (loadError) {
139
- // If loading fails, continue with changeLanguage (will fallback to en-US)
140
- }
141
- }
63
+ const result = await LanguageSwitcher.switchLanguage(languageCode);
142
64
 
143
- // Update i18n
144
- await i18n.changeLanguage(languageCode);
145
-
146
- // Update state
147
65
  set({
148
- currentLanguage: languageCode,
149
- isRTL: language.rtl || false,
66
+ currentLanguage: result.languageCode,
67
+ isRTL: result.isRTL,
150
68
  });
151
-
152
- // Persist language preference
153
- await AsyncStorage.setItem(LANGUAGE_STORAGE_KEY, languageCode);
154
69
  } catch (error) {
155
70
  throw error;
156
71
  }
@@ -160,8 +75,6 @@ export const useLocalizationStore = create<LocalizationState>((set, get) => ({
160
75
  /**
161
76
  * Hook to use localization
162
77
  * Provides current language, RTL state, language switching, and translation function
163
- * Uses react-i18next's useTranslation hook to ensure proper i18n instance
164
- * Falls back to direct i18n.t if react-i18next is not ready
165
78
  */
166
79
  export const useLocalization = () => {
167
80
  const {
@@ -175,33 +88,7 @@ export const useLocalization = () => {
175
88
 
176
89
  const currentLanguageObject = getLanguageByCode(currentLanguage);
177
90
 
178
- // Always call useTranslation hook (React hooks rules - must be unconditional)
179
- // Pass i18n instance explicitly to ensure react-i18next finds it
180
- // This fixes the "NO_I18NEXT_INSTANCE" error
181
- // Even if i18n is not fully initialized, useTranslation will handle it gracefully
182
- // with the explicit i18n instance passed
183
- const translationResult = useTranslation(undefined, { i18n });
184
-
185
- // Use translation function from react-i18next if available and valid
186
- // Otherwise fallback to direct i18n.t
187
- // Type assertion needed because react-i18next's TFunction can return string | object
188
- const t = (translationResult?.t && typeof translationResult.t === 'function' && i18n.isInitialized)
189
- ? ((key: string, options?: any): string => {
190
- const result = translationResult.t(key, options);
191
- return typeof result === 'string' ? result : String(result);
192
- })
193
- : ((key: string, options?: any): string => {
194
- // Fallback to direct i18n.t if react-i18next is not ready
195
- if (i18n.isInitialized && typeof i18n.t === 'function') {
196
- const result = i18n.t(key, options);
197
- return typeof result === 'string' ? result : String(result);
198
- }
199
- // Final fallback: return key if i18n is not ready
200
- return key;
201
- }) as (key: string, options?: any) => string;
202
-
203
91
  return {
204
- t, // Translation function from react-i18next or i18n fallback
205
92
  currentLanguage,
206
93
  currentLanguageObject,
207
94
  isRTL,