@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-async-storage/async-storage
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.8.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-async-storage/async-storage": "^1.21.0"
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 English-only
3
- * Simple translation structure - only en-US supported
2
+ * i18next Configuration for Multi-language Support
3
+ * Loads all supported languages from project translations
4
4
  *
5
- * SINGLE LANGUAGE LOADING:
6
- * - Only loads en-US translations
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 package translations
16
+ * Load package translations (en-US only)
16
17
  */
17
18
  const loadPackageTranslations = (): Record<string, any> => {
18
19
  try {
19
- // Load only en-US translations
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
- * Try to load project-specific en-US translations
33
- * Metro bundler will resolve these at build time if they exist
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
- let projectTranslations: Record<string, any> = {};
37
-
38
- // Try to load project translations from common paths
39
- try {
40
- // Try DDD structure path
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
- // Try alternative DDD structure path
47
- // eslint-disable-next-line @typescript-eslint/no-require-imports
48
- const translations = require('../../../../../../domains/localization/infrastructure/locales/en-US');
49
- projectTranslations['en-US'] = translations.default || translations;
50
- } catch (e2) {
51
- try {
52
- // Try simple structure path
53
- // eslint-disable-next-line @typescript-eslint/no-require-imports
54
- const translations = require('../../../../../../src/locales/en-US');
55
- projectTranslations['en-US'] = translations.default || translations;
56
- } catch (e3) {
57
- // No project translations found - this is OK, use package defaults only
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 en-US only
113
+ * Build resources object for all supported languages
99
114
  */
100
115
  const buildResources = (): Record<string, { translation: any }> => {
101
- const packageTranslation = packageTranslations['en-US'] || {};
102
- const projectTranslation = projectTranslations['en-US'] || {};
103
-
104
- return {
105
- 'en-US': {
106
- translation: mergeTranslations(packageTranslation, projectTranslation),
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 StorageWrapper.getString(STORAGE_KEYS.LANGUAGE, DEFAULT_LANGUAGE);
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 StorageWrapper.setString(STORAGE_KEYS.LANGUAGE, languageCode);
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 StorageWrapper.setString(STORAGE_KEYS.LANGUAGE, languageCode);
153
+ await AsyncStorage.setItem(LANGUAGE_STORAGE_KEY, languageCode);
110
154
  } catch (error) {
111
155
  throw error;
112
156
  }