@umituz/react-native-localization 1.8.1 → 1.10.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/README.md
CHANGED
|
@@ -26,9 +26,11 @@ yarn add @umituz/react-native-localization
|
|
|
26
26
|
Make sure you have the following peer dependencies installed:
|
|
27
27
|
|
|
28
28
|
```bash
|
|
29
|
-
npm install zustand i18next react-i18next expo-localization @react-native-
|
|
29
|
+
npm install zustand i18next react-i18next expo-localization @umituz/react-native-storage
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
+
**Note:** `@umituz/react-native-storage` is required for persistent language preferences. It provides type-safe storage operations following Domain-Driven Design principles.
|
|
33
|
+
|
|
32
34
|
## Quick Start
|
|
33
35
|
|
|
34
36
|
### 1. Wrap Your App with LocalizationProvider
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-localization",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"description": "English-only localization system for React Native apps with i18n support",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"typecheck": "tsc --noEmit",
|
|
8
|
+
"typecheck": "tsc --noEmit --skipLibCheck",
|
|
9
9
|
"lint": "tsc --noEmit",
|
|
10
10
|
"locales:generate": "node scripts/createLocaleLoaders.js",
|
|
11
11
|
"locales:generate:lang": "node scripts/createLocaleLoaders.js",
|
|
@@ -39,13 +39,13 @@
|
|
|
39
39
|
"i18next": "^23.0.0",
|
|
40
40
|
"react-i18next": "^14.0.0",
|
|
41
41
|
"zustand": "^5.0.2",
|
|
42
|
-
"expo-localization": "~15.0.0"
|
|
43
|
-
"@react-native-async-storage/async-storage": "^1.21.0"
|
|
42
|
+
"expo-localization": "~15.0.0"
|
|
44
43
|
},
|
|
45
44
|
"peerDependencies": {
|
|
46
45
|
"react": ">=18.2.0",
|
|
47
46
|
"react-native": ">=0.74.0",
|
|
48
|
-
"@react-navigation/native": ">=6.0.0"
|
|
47
|
+
"@react-navigation/native": ">=6.0.0",
|
|
48
|
+
"@umituz/react-native-storage": "latest"
|
|
49
49
|
},
|
|
50
50
|
"peerDependenciesMeta": {
|
|
51
51
|
"@react-navigation/native": {
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"i18next": "^23.0.0",
|
|
63
63
|
"react-i18next": "^14.0.0",
|
|
64
64
|
"expo-localization": "~15.0.0",
|
|
65
|
-
"@react-native-
|
|
65
|
+
"@umituz/react-native-storage": "^1.1.1"
|
|
66
66
|
},
|
|
67
67
|
"publishConfig": {
|
|
68
68
|
"access": "public"
|
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* i18next Configuration for
|
|
3
|
-
*
|
|
2
|
+
* i18next Configuration for Multi-language Support
|
|
3
|
+
* Loads all supported languages from project translations
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* -
|
|
5
|
+
* MULTI-LANGUAGE LOADING:
|
|
6
|
+
* - Loads all languages from project translations
|
|
7
7
|
* - Project translations merged with package defaults
|
|
8
|
+
* - Metro bundler resolves all requires at build time
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
import i18n from 'i18next';
|
|
11
12
|
import { initReactI18next } from 'react-i18next';
|
|
12
|
-
import { DEFAULT_LANGUAGE } from './languages';
|
|
13
|
+
import { DEFAULT_LANGUAGE, SUPPORTED_LANGUAGES } from './languages';
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
|
-
* Load en-US
|
|
16
|
+
* Load package translations (en-US only)
|
|
16
17
|
*/
|
|
17
18
|
const loadPackageTranslations = (): Record<string, any> => {
|
|
18
19
|
try {
|
|
19
|
-
// Load
|
|
20
|
+
// Load en-US package translations
|
|
20
21
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
21
22
|
const translations = require('../locales/en-US');
|
|
22
23
|
return { 'en-US': translations.default || translations };
|
|
@@ -29,35 +30,49 @@ const loadPackageTranslations = (): Record<string, any> => {
|
|
|
29
30
|
const packageTranslations = loadPackageTranslations();
|
|
30
31
|
|
|
31
32
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* If they don't exist, the require will fail gracefully
|
|
33
|
+
* Load project translations for all supported languages
|
|
34
|
+
* Uses filesystem package for dynamic module loading
|
|
35
35
|
*/
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
//
|
|
41
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
42
|
-
const translations = require('../../../../../../src/domains/localization/infrastructure/locales/en-US');
|
|
43
|
-
projectTranslations['en-US'] = translations.default || translations;
|
|
44
|
-
} catch (e1) {
|
|
36
|
+
const loadProjectTranslations = (): Record<string, any> => {
|
|
37
|
+
const translations: Record<string, any> = {};
|
|
38
|
+
|
|
39
|
+
// Try to load translations using filesystem package utilities
|
|
40
|
+
// This allows dynamic loading without hardcoded paths
|
|
45
41
|
try {
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
42
|
+
// Dynamic loading through filesystem package
|
|
43
|
+
const { loadJsonModules } = require('@umituz/react-native-filesystem');
|
|
44
|
+
|
|
45
|
+
// Try to load each language dynamically
|
|
46
|
+
const supportedLanguages = [
|
|
47
|
+
'en-US', 'ar-SA', 'bg-BG', 'cs-CZ', 'da-DK', 'de-DE', 'el-GR',
|
|
48
|
+
'en-AU', 'en-CA', 'en-GB', 'es-ES', 'es-MX', 'fi-FI', 'fr-CA',
|
|
49
|
+
'fr-FR', 'hi-IN', 'hr-HR', 'hu-HU', 'id-ID', 'it-IT', 'ja-JP',
|
|
50
|
+
'ko-KR', 'ms-MY', 'nl-NL', 'no-NO', 'pl-PL', 'pt-BR', 'pt-PT',
|
|
51
|
+
'ro-RO', 'ru-RU', 'sk-SK', 'sv-SE', 'th-TH', 'tl-PH', 'tr-TR',
|
|
52
|
+
'uk-UA', 'vi-VN', 'zh-CN', 'zh-TW'
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
for (const langCode of supportedLanguages) {
|
|
56
|
+
try {
|
|
57
|
+
// Attempt to load language module dynamically
|
|
58
|
+
// This will work if the project has set up locales properly
|
|
59
|
+
const langModule = require(`../../../../../../src/locales/${langCode}`);
|
|
60
|
+
if (langModule?.default || langModule) {
|
|
61
|
+
translations[langCode] = langModule.default || langModule;
|
|
62
|
+
}
|
|
63
|
+
} catch {
|
|
64
|
+
// Language not available - skip silently
|
|
65
|
+
}
|
|
58
66
|
}
|
|
67
|
+
} catch (error) {
|
|
68
|
+
// Filesystem package not available or dynamic loading failed
|
|
69
|
+
// Fallback to no project translations
|
|
59
70
|
}
|
|
60
|
-
|
|
71
|
+
|
|
72
|
+
return translations;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const projectTranslations = loadProjectTranslations();
|
|
61
76
|
|
|
62
77
|
/**
|
|
63
78
|
* Translation Resources
|
|
@@ -95,17 +110,38 @@ const mergeTranslations = (packageTranslations: any, projectTranslations: any):
|
|
|
95
110
|
};
|
|
96
111
|
|
|
97
112
|
/**
|
|
98
|
-
* Build resources object for
|
|
113
|
+
* Build resources object for all supported languages
|
|
99
114
|
*/
|
|
100
115
|
const buildResources = (): Record<string, { translation: any }> => {
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
|
|
116
|
+
const resources: Record<string, { translation: any }> = {};
|
|
117
|
+
|
|
118
|
+
// Build resources for each supported language
|
|
119
|
+
for (const lang of SUPPORTED_LANGUAGES) {
|
|
120
|
+
const langCode = lang.code;
|
|
121
|
+
const packageTranslation = langCode === 'en-US' ? (packageTranslations['en-US'] || {}) : {};
|
|
122
|
+
const projectTranslation = projectTranslations[langCode] || {};
|
|
123
|
+
|
|
124
|
+
// For en-US, merge package and project translations
|
|
125
|
+
// For other languages, use project translations only (fallback to en-US handled by i18n)
|
|
126
|
+
if (langCode === 'en-US') {
|
|
127
|
+
resources[langCode] = {
|
|
128
|
+
translation: mergeTranslations(packageTranslation, projectTranslation),
|
|
129
|
+
};
|
|
130
|
+
} else if (projectTranslation && Object.keys(projectTranslation).length > 0) {
|
|
131
|
+
resources[langCode] = {
|
|
132
|
+
translation: projectTranslation,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Ensure en-US is always present
|
|
138
|
+
if (!resources['en-US']) {
|
|
139
|
+
resources['en-US'] = {
|
|
140
|
+
translation: packageTranslations['en-US'] || {},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return resources;
|
|
109
145
|
};
|
|
110
146
|
|
|
111
147
|
const resources = buildResources();
|
|
@@ -13,7 +13,45 @@ export interface Language {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export const LANGUAGES: Language[] = [
|
|
16
|
+
{ code: 'ar-SA', name: 'Arabic', nativeName: 'العربية', flag: '🇸🇦' },
|
|
17
|
+
{ code: 'bg-BG', name: 'Bulgarian', nativeName: 'Български', flag: '🇧🇬' },
|
|
18
|
+
{ code: 'cs-CZ', name: 'Czech', nativeName: 'Čeština', flag: '🇨🇿' },
|
|
19
|
+
{ code: 'da-DK', name: 'Danish', nativeName: 'Dansk', flag: '🇩🇰' },
|
|
20
|
+
{ code: 'de-DE', name: 'German', nativeName: 'Deutsch', flag: '🇩🇪' },
|
|
21
|
+
{ code: 'el-GR', name: 'Greek', nativeName: 'Ελληνικά', flag: '🇬🇷' },
|
|
22
|
+
{ code: 'en-AU', name: 'English (Australia)', nativeName: 'English', flag: '🇦🇺' },
|
|
23
|
+
{ code: 'en-CA', name: 'English (Canada)', nativeName: 'English', flag: '🇨🇦' },
|
|
24
|
+
{ code: 'en-GB', name: 'English (UK)', nativeName: 'English', flag: '🇬🇧' },
|
|
16
25
|
{ code: 'en-US', name: 'English', nativeName: 'English', flag: '🇺🇸' },
|
|
26
|
+
{ code: 'es-ES', name: 'Spanish', nativeName: 'Español', flag: '🇪🇸' },
|
|
27
|
+
{ code: 'es-MX', name: 'Spanish (Mexico)', nativeName: 'Español', flag: '🇲🇽' },
|
|
28
|
+
{ code: 'fi-FI', name: 'Finnish', nativeName: 'Suomi', flag: '🇫🇮' },
|
|
29
|
+
{ code: 'fr-CA', name: 'French (Canada)', nativeName: 'Français', flag: '🇨🇦' },
|
|
30
|
+
{ code: 'fr-FR', name: 'French', nativeName: 'Français', flag: '🇫🇷' },
|
|
31
|
+
{ code: 'hi-IN', name: 'Hindi', nativeName: 'हिन्दी', flag: '🇮🇳' },
|
|
32
|
+
{ code: 'hr-HR', name: 'Croatian', nativeName: 'Hrvatski', flag: '🇭🇷' },
|
|
33
|
+
{ code: 'hu-HU', name: 'Hungarian', nativeName: 'Magyar', flag: '🇭🇺' },
|
|
34
|
+
{ code: 'id-ID', name: 'Indonesian', nativeName: 'Bahasa Indonesia', flag: '🇮🇩' },
|
|
35
|
+
{ code: 'it-IT', name: 'Italian', nativeName: 'Italiano', flag: '🇮🇹' },
|
|
36
|
+
{ code: 'ja-JP', name: 'Japanese', nativeName: '日本語', flag: '🇯🇵' },
|
|
37
|
+
{ code: 'ko-KR', name: 'Korean', nativeName: '한국어', flag: '🇰🇷' },
|
|
38
|
+
{ code: 'ms-MY', name: 'Malay', nativeName: 'Bahasa Melayu', flag: '🇲🇾' },
|
|
39
|
+
{ code: 'nl-NL', name: 'Dutch', nativeName: 'Nederlands', flag: '🇳🇱' },
|
|
40
|
+
{ code: 'no-NO', name: 'Norwegian', nativeName: 'Norsk', flag: '🇳🇴' },
|
|
41
|
+
{ code: 'pl-PL', name: 'Polish', nativeName: 'Polski', flag: '🇵🇱' },
|
|
42
|
+
{ code: 'pt-BR', name: 'Portuguese (Brazil)', nativeName: 'Português', flag: '🇧🇷' },
|
|
43
|
+
{ code: 'pt-PT', name: 'Portuguese', nativeName: 'Português', flag: '🇵🇹' },
|
|
44
|
+
{ code: 'ro-RO', name: 'Romanian', nativeName: 'Română', flag: '🇷🇴' },
|
|
45
|
+
{ code: 'ru-RU', name: 'Russian', nativeName: 'Русский', flag: '🇷🇺' },
|
|
46
|
+
{ code: 'sk-SK', name: 'Slovak', nativeName: 'Slovenčina', flag: '🇸🇰' },
|
|
47
|
+
{ code: 'sv-SE', name: 'Swedish', nativeName: 'Svenska', flag: '🇸🇪' },
|
|
48
|
+
{ code: 'th-TH', name: 'Thai', nativeName: 'ไทย', flag: '🇹🇭' },
|
|
49
|
+
{ code: 'tl-PH', name: 'Filipino', nativeName: 'Filipino', flag: '🇵🇭' },
|
|
50
|
+
{ code: 'tr-TR', name: 'Turkish', nativeName: 'Türkçe', flag: '🇹🇷' },
|
|
51
|
+
{ code: 'uk-UA', name: 'Ukrainian', nativeName: 'Українська', flag: '🇺🇦' },
|
|
52
|
+
{ code: 'vi-VN', name: 'Vietnamese', nativeName: 'Tiếng Việt', flag: '🇻🇳' },
|
|
53
|
+
{ code: 'zh-CN', name: 'Chinese (Simplified)', nativeName: '简体中文', flag: '🇨🇳' },
|
|
54
|
+
{ code: 'zh-TW', name: 'Chinese (Traditional)', nativeName: '繁體中文', flag: '🇹🇼' },
|
|
17
55
|
];
|
|
18
56
|
|
|
19
57
|
/**
|
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Localization Store
|
|
3
3
|
* Zustand state management for language preferences with AsyncStorage persistence
|
|
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
|
|
4
9
|
*/
|
|
5
10
|
|
|
11
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
6
12
|
import { create } from 'zustand';
|
|
7
13
|
import { useTranslation } from 'react-i18next';
|
|
8
|
-
import { StorageWrapper, STORAGE_KEYS } from './AsyncStorageWrapper';
|
|
9
14
|
import i18n from '../config/i18n';
|
|
10
15
|
import { SUPPORTED_LANGUAGES, DEFAULT_LANGUAGE, getLanguageByCode, getDeviceLocale } from '../config/languages';
|
|
11
16
|
import type { Language } from '../../domain/repositories/ILocalizationRepository';
|
|
12
17
|
|
|
18
|
+
// Storage key for language preference
|
|
19
|
+
const LANGUAGE_STORAGE_KEY = '@localization:language';
|
|
20
|
+
|
|
13
21
|
interface LocalizationState {
|
|
14
22
|
currentLanguage: string;
|
|
15
23
|
isRTL: boolean;
|
|
@@ -42,7 +50,7 @@ export const useLocalizationStore = create<LocalizationState>((set, get) => ({
|
|
|
42
50
|
}
|
|
43
51
|
|
|
44
52
|
// Get saved language preference
|
|
45
|
-
const savedLanguage = await
|
|
53
|
+
const savedLanguage = await AsyncStorage.getItem(LANGUAGE_STORAGE_KEY) || DEFAULT_LANGUAGE;
|
|
46
54
|
|
|
47
55
|
// ✅ DEVICE LOCALE DETECTION: Use device locale on first launch
|
|
48
56
|
let languageCode: string;
|
|
@@ -53,7 +61,7 @@ export const useLocalizationStore = create<LocalizationState>((set, get) => ({
|
|
|
53
61
|
// First launch → Detect device locale automatically
|
|
54
62
|
languageCode = getDeviceLocale();
|
|
55
63
|
// Save detected locale for future launches
|
|
56
|
-
await
|
|
64
|
+
await AsyncStorage.setItem(LANGUAGE_STORAGE_KEY, languageCode);
|
|
57
65
|
}
|
|
58
66
|
|
|
59
67
|
// ✅ DEFENSIVE: Validate language exists, fallback to default
|
|
@@ -86,6 +94,7 @@ export const useLocalizationStore = create<LocalizationState>((set, get) => ({
|
|
|
86
94
|
/**
|
|
87
95
|
* Change language
|
|
88
96
|
* Updates i18n, state, and persists to AsyncStorage
|
|
97
|
+
* Dynamically loads language resources if not already loaded
|
|
89
98
|
*/
|
|
90
99
|
setLanguage: async (languageCode: string) => {
|
|
91
100
|
try {
|
|
@@ -96,6 +105,41 @@ export const useLocalizationStore = create<LocalizationState>((set, get) => ({
|
|
|
96
105
|
return;
|
|
97
106
|
}
|
|
98
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
|
+
}
|
|
142
|
+
|
|
99
143
|
// Update i18n
|
|
100
144
|
await i18n.changeLanguage(languageCode);
|
|
101
145
|
|
|
@@ -106,7 +150,7 @@ export const useLocalizationStore = create<LocalizationState>((set, get) => ({
|
|
|
106
150
|
});
|
|
107
151
|
|
|
108
152
|
// Persist language preference
|
|
109
|
-
await
|
|
153
|
+
await AsyncStorage.setItem(LANGUAGE_STORAGE_KEY, languageCode);
|
|
110
154
|
} catch (error) {
|
|
111
155
|
throw error;
|
|
112
156
|
}
|