@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 +1 -1
- package/src/index.ts +1 -0
- package/src/infrastructure/config/I18nInitializer.ts +113 -0
- package/src/infrastructure/config/TranslationLoader.ts +69 -0
- package/src/infrastructure/config/i18n.ts +8 -206
- package/src/infrastructure/hooks/TranslationHook.ts +39 -0
- package/src/infrastructure/hooks/useTranslation.ts +37 -0
- package/src/infrastructure/storage/LanguageInitializer.ts +95 -0
- package/src/infrastructure/storage/LanguageSwitcher.ts +83 -0
- package/src/infrastructure/storage/LocalizationStore.ts +26 -139
package/package.json
CHANGED
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
|
-
*
|
|
3
|
-
* Loads all supported languages from project translations
|
|
2
|
+
* i18n Configuration Entry Point
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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 {
|
|
13
|
-
import { DEFAULT_LANGUAGE, SUPPORTED_LANGUAGES } from './languages';
|
|
9
|
+
import { I18nInitializer } from './I18nInitializer';
|
|
14
10
|
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
107
|
-
|
|
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
|
|
3
|
+
* Zustand state management for language preferences
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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 {
|
|
14
|
-
import
|
|
15
|
-
import {
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
68
|
-
const
|
|
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:
|
|
76
|
-
isRTL:
|
|
77
|
-
isInitialized: true,
|
|
44
|
+
currentLanguage: result.languageCode,
|
|
45
|
+
isRTL: result.isRTL,
|
|
46
|
+
isInitialized: true,
|
|
78
47
|
});
|
|
79
48
|
} catch (error) {
|
|
80
|
-
// Set
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
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:
|
|
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,
|