@umituz/react-native-localization 2.2.2 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-localization",
3
- "version": "2.2.2",
3
+ "version": "2.3.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",
@@ -1,117 +1,123 @@
1
1
  /**
2
2
  * i18n Initializer
3
3
  *
4
- * Handles i18n configuration and initialization
4
+ * Handles i18n configuration and initialization with namespace support
5
5
  * - Auto-discovers project translations
6
- * - i18n setup
6
+ * - Namespace-based organization (common, auth, etc.)
7
7
  * - React i18next integration
8
8
  */
9
9
 
10
10
  import i18n from 'i18next';
11
11
  import { initReactI18next } from 'react-i18next';
12
- import { DEFAULT_LANGUAGE, SUPPORTED_LANGUAGES } from './languages';
12
+ import { DEFAULT_LANGUAGE } from './languages';
13
13
  import { TranslationLoader } from './TranslationLoader';
14
14
 
15
+ const DEFAULT_NAMESPACE = 'common';
16
+
15
17
  export class I18nInitializer {
16
18
  private static reactI18nextInitialized = false;
17
19
 
18
20
  /**
19
- * Auto-discover project translations from common paths
21
+ * Build resources object with namespace support
20
22
  */
21
- private static loadProjectTranslations(): Record<string, any> {
22
- const possiblePaths = [
23
- './src/locales/en-US', // App structure
24
- './locales/en-US', // Alternative app structure
25
- '../src/locales/en-US', // Relative from package
26
- ];
27
-
28
- for (const path of possiblePaths) {
29
- try {
30
- // eslint-disable-next-line @typescript-eslint/no-require-imports
31
- const translations = require(path);
32
- return translations.default || translations;
33
- } catch {
34
- // Try next path
35
- }
23
+ private static buildResources(): Record<string, Record<string, any>> {
24
+ const packageTranslations = TranslationLoader.loadPackageTranslations();
25
+
26
+ // Create namespace-based resources structure
27
+ const resources: Record<string, Record<string, any>> = {
28
+ 'en-US': {},
29
+ };
30
+
31
+ // Package translations are already in namespace format (alerts, auth, etc.)
32
+ const enUSPackage = packageTranslations['en-US'] || {};
33
+
34
+ // Each key in packageTranslations is a namespace
35
+ for (const [namespace, translations] of Object.entries(enUSPackage)) {
36
+ resources['en-US'][namespace] = translations;
36
37
  }
37
38
 
38
- return {};
39
+ return resources;
39
40
  }
40
41
 
41
42
  /**
42
- * Build resources object for all supported languages
43
+ * Get all available namespaces from package translations
43
44
  */
44
- private static buildResources(): Record<string, { translation: any }> {
45
- const resources: Record<string, { translation: any }> = {};
45
+ private static getNamespaces(): string[] {
46
46
  const packageTranslations = TranslationLoader.loadPackageTranslations();
47
- const projectTranslations = this.loadProjectTranslations();
48
-
49
- // For en-US, merge package and project translations
50
- resources['en-US'] = {
51
- translation: TranslationLoader.mergeTranslations(
52
- packageTranslations['en-US'] || {},
53
- projectTranslations
54
- ),
55
- };
47
+ const enUSPackage = packageTranslations['en-US'] || {};
48
+ const namespaces = Object.keys(enUSPackage);
56
49
 
57
- return resources;
50
+ // Ensure default namespace is included
51
+ if (!namespaces.includes(DEFAULT_NAMESPACE)) {
52
+ namespaces.unshift(DEFAULT_NAMESPACE);
53
+ }
54
+
55
+ return namespaces;
58
56
  }
59
57
 
60
58
  /**
61
- * Initialize i18next
59
+ * Initialize i18next with namespace support
62
60
  */
63
61
  static initialize(): void {
64
- // Prevent multiple initializations
65
62
  if (i18n.isInitialized) {
66
63
  return;
67
64
  }
68
65
 
69
66
  try {
70
- // Use initReactI18next once
71
67
  if (!this.reactI18nextInitialized) {
72
68
  i18n.use(initReactI18next);
73
69
  this.reactI18nextInitialized = true;
74
70
  }
75
71
 
76
72
  const resources = this.buildResources();
73
+ const namespaces = this.getNamespaces();
77
74
 
78
75
  i18n.init({
79
76
  resources,
80
77
  lng: DEFAULT_LANGUAGE,
81
78
  fallbackLng: DEFAULT_LANGUAGE,
79
+ ns: namespaces,
80
+ defaultNS: DEFAULT_NAMESPACE,
81
+ fallbackNS: DEFAULT_NAMESPACE,
82
82
 
83
83
  interpolation: {
84
- escapeValue: false, // React already escapes values
84
+ escapeValue: false,
85
85
  },
86
86
 
87
87
  react: {
88
- useSuspense: false, // Disable suspense for React Native
88
+ useSuspense: false,
89
89
  },
90
90
 
91
- compatibilityJSON: 'v3', // Use v3 format for React Native
92
- pluralSeparator: '_', // Use underscore separator for plural keys
93
- keySeparator: '.', // Use dot separator for nested keys
91
+ compatibilityJSON: 'v3',
92
+ pluralSeparator: '_',
93
+ keySeparator: '.',
94
+ nsSeparator: ':',
95
+
96
+ saveMissing: false,
97
+ missingKeyHandler: false,
94
98
 
95
- debug: typeof __DEV__ !== 'undefined' && __DEV__,
99
+ debug: false,
96
100
  });
97
101
 
98
102
  } catch (error) {
99
- // Don't throw - allow app to continue without i18n
100
103
  if (typeof __DEV__ !== 'undefined' && __DEV__) {
101
- console.error(' i18n initialization error:', error);
104
+ console.error('[Localization] i18n initialization error:', error);
102
105
  }
103
106
  }
104
107
  }
105
108
 
106
109
  /**
107
- * Add additional translation resources
110
+ * Add additional translation resources with namespace support
111
+ * @param languageCode - Language code (e.g., 'en-US')
112
+ * @param namespaceResources - Object with namespace keys and translation objects
108
113
  */
109
- static addTranslationResources(resources: Record<string, { translation: any }>): void {
110
- for (const [langCode, resource] of Object.entries(resources)) {
111
- if (resource.translation) {
112
- const existingTranslations = i18n.getResourceBundle(langCode, 'translation') || {};
113
- const mergedTranslations = { ...existingTranslations, ...resource.translation };
114
- i18n.addResourceBundle(langCode, 'translation', mergedTranslations, true, true);
114
+ static addTranslationResources(
115
+ languageCode: string,
116
+ namespaceResources: Record<string, any>
117
+ ): void {
118
+ for (const [namespace, translations] of Object.entries(namespaceResources)) {
119
+ if (translations && typeof translations === 'object') {
120
+ i18n.addResourceBundle(languageCode, namespace, translations, true, true);
115
121
  }
116
122
  }
117
123
  }
@@ -1,15 +1,14 @@
1
1
  /**
2
2
  * i18n Configuration
3
3
  *
4
- * Auto-initializes i18n with project translations
4
+ * Auto-initializes i18n with namespace support
5
+ * Usage: t('namespace:key') e.g., t('common:cancel')
5
6
  */
6
7
 
7
8
  import { I18nInitializer } from './I18nInitializer';
8
9
  import i18n from 'i18next';
9
10
 
10
- // Initialize i18n automatically
11
11
  I18nInitializer.initialize();
12
12
 
13
- // Export for advanced usage
14
13
  export const addTranslationResources = I18nInitializer.addTranslationResources;
15
14
  export default i18n;
@@ -14,55 +14,6 @@ import i18n from '../config/i18n';
14
14
  * Hook for translation functionality
15
15
  */
16
16
  export const useTranslationFunction = (): ((key: string, options?: any) => string) => {
17
- // Ensure settings translations are loaded
18
- if (i18n.isInitialized && typeof i18n.t === 'function') {
19
- const existingTranslations = i18n.getResourceBundle('en-US', 'translation') || {};
20
-
21
- // Check if settings translations are missing
22
- if (!existingTranslations.settings) {
23
- // Add settings translations as fallback
24
- const settingsTranslations = {
25
- settings: {
26
- editProfile: "Edit Profile",
27
- sections: {
28
- physicalInfoAndGoals: "Physical Info and Goals",
29
- appSettings: "App Settings",
30
- accountManagement: "Account Management"
31
- },
32
- personalInfo: {
33
- title: "Personal Information",
34
- subtitle: "Height, Weight, Age, Gender"
35
- },
36
- nutritionGoals: {
37
- title: "Nutrition Goals",
38
- subtitle: "Calories, Protein, Carbs, Fat"
39
- },
40
- notifications: {
41
- title: "Notification Preferences",
42
- subtitle: "Water and meal reminders"
43
- },
44
- darkMode: {
45
- title: "Dark Mode",
46
- subtitle: "Change app theme"
47
- },
48
- passwordChange: {
49
- title: "Change Password"
50
- },
51
- saveChanges: "Save Changes",
52
- logout: "Logout",
53
- emptyState: {
54
- title: "No recipes yet",
55
- subtitle: "Create your first recipe to get started"
56
- },
57
- addToList: "Add to List"
58
- }
59
- };
60
-
61
- const mergedTranslations = { ...existingTranslations, ...settingsTranslations };
62
- i18n.addResourceBundle('en-US', 'translation', mergedTranslations, true, true);
63
- }
64
- }
65
-
66
17
  // Use direct i18n.t for reliability (no React context issues)
67
18
  return (key: string, options?: any): string => {
68
19
  if (i18n.isInitialized && typeof i18n.t === 'function') {
@@ -0,0 +1,57 @@
1
+ {
2
+ "cancel": "Cancel",
3
+ "confirm": "Confirm",
4
+ "continue": "Continue",
5
+ "delete": "Delete",
6
+ "edit": "Edit",
7
+ "error": "Error",
8
+ "loading": "Loading...",
9
+ "notNow": "Not Now",
10
+ "ok": "OK",
11
+ "optional": "Optional",
12
+ "save": "Save",
13
+ "somethingWentWrong": "Something went wrong",
14
+ "success": "Success",
15
+ "close": "Close",
16
+ "done": "Done",
17
+ "next": "Next",
18
+ "previous": "Previous",
19
+ "skip": "Skip",
20
+ "retry": "Retry",
21
+ "refresh": "Refresh",
22
+ "filter": "Filter",
23
+ "sort": "Sort",
24
+ "add": "Add",
25
+ "remove": "Remove",
26
+ "update": "Update",
27
+ "create": "Create",
28
+ "view": "View",
29
+ "details": "Details",
30
+ "share": "Share",
31
+ "download": "Download",
32
+ "upload": "Upload",
33
+ "submit": "Submit",
34
+ "reset": "Reset",
35
+ "clear": "Clear",
36
+ "apply": "Apply",
37
+ "yes": "Yes",
38
+ "no": "No",
39
+ "or": "or",
40
+ "all": "All",
41
+ "none": "None",
42
+ "of": "of",
43
+ "select": "Select",
44
+ "selected": "Selected",
45
+ "required": "Required",
46
+ "empty": "Empty",
47
+ "noData": "No data available",
48
+ "noResults": "No results found",
49
+ "tryAgain": "Try again",
50
+ "learnMore": "Learn more",
51
+ "getStarted": "Get started",
52
+ "viewAll": "View all",
53
+ "showMore": "Show more",
54
+ "showLess": "Show less",
55
+ "back": "Back",
56
+ "start": "Start"
57
+ }
@@ -1,16 +1,19 @@
1
1
  /**
2
- * en-US Translation Module
2
+ * Translation loader for en-US with namespace support
3
3
  *
4
- * Direct loading for maximum compatibility across platforms
5
- * - Explicit imports for reliable bundling
6
- * - Flattened with dot notation
7
- * - Production-ready and tested
4
+ * Each JSON file represents a namespace that can be accessed via:
5
+ * t('namespace:key') or t('namespace:nested.key')
6
+ *
7
+ * Example:
8
+ * t('common:cancel') -> "Cancel"
9
+ * t('auth:login.title') -> "Sign In"
8
10
  */
9
11
 
10
12
  import alerts from './alerts.json';
11
13
  import auth from './auth.json';
12
14
  import branding from './branding.json';
13
15
  import clipboard from './clipboard.json';
16
+ import common from './common.json';
14
17
  import datetime from './datetime.json';
15
18
  import device from './device.json';
16
19
  import editor from './editor.json';
@@ -26,48 +29,26 @@ import settings from './settings.json';
26
29
  import sharing from './sharing.json';
27
30
  import templates from './templates.json';
28
31
 
29
- /**
30
- * Flatten nested objects with dot notation
31
- */
32
- const flattenObject = (
33
- obj: Record<string, any>,
34
- prefix = '',
35
- ): Record<string, string> => {
36
- const flattened: Record<string, string> = {};
37
-
38
- Object.keys(obj).forEach((key) => {
39
- const newKey = prefix ? `${prefix}.${key}` : key;
40
-
41
- if (obj[key] && typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
42
- Object.assign(flattened, flattenObject(obj[key], newKey));
43
- } else {
44
- flattened[newKey] = obj[key];
45
- }
46
- });
47
-
48
- return flattened;
49
- };
50
-
51
- // Create flattened translations object
52
32
  const translations = {
53
- ...flattenObject(alerts, 'alerts'),
54
- ...flattenObject(auth, 'auth'),
55
- ...flattenObject(branding, 'branding'),
56
- ...flattenObject(clipboard, 'clipboard'),
57
- ...flattenObject(datetime, 'datetime'),
58
- ...flattenObject(device, 'device'),
59
- ...flattenObject(editor, 'editor'),
60
- ...flattenObject(errors, 'errors'),
61
- ...flattenObject(general, 'general'),
62
- ...flattenObject(goals, 'goals'),
63
- ...flattenObject(haptics, 'haptics'),
64
- ...flattenObject(home, 'home'),
65
- ...flattenObject(navigation, 'navigation'),
66
- ...flattenObject(onboarding, 'onboarding'),
67
- ...flattenObject(projects, 'projects'),
68
- ...flattenObject(settings, 'settings'),
69
- ...flattenObject(sharing, 'sharing'),
70
- ...flattenObject(templates, 'templates'),
33
+ alerts,
34
+ auth,
35
+ branding,
36
+ clipboard,
37
+ common,
38
+ datetime,
39
+ device,
40
+ editor,
41
+ errors,
42
+ general,
43
+ goals,
44
+ haptics,
45
+ home,
46
+ navigation,
47
+ onboarding,
48
+ projects,
49
+ settings,
50
+ sharing,
51
+ templates,
71
52
  };
72
53
 
73
54
  export default translations;
@@ -1,9 +1,9 @@
1
1
  /**
2
- * AsyncStorage Wrapper
3
- * Simple wrapper for AsyncStorage operations
2
+ * Storage Wrapper
3
+ * Uses @umituz/react-native-storage for persistence
4
4
  */
5
5
 
6
- import AsyncStorage from '@react-native-async-storage/async-storage';
6
+ import { storageRepository } from '@umituz/react-native-storage';
7
7
 
8
8
  export const STORAGE_KEYS = {
9
9
  LANGUAGE: '@localization:language',
@@ -11,19 +11,14 @@ export const STORAGE_KEYS = {
11
11
 
12
12
  export const StorageWrapper = {
13
13
  async getString(key: string, defaultValue: string): Promise<string> {
14
- try {
15
- const value = await AsyncStorage.getItem(key);
16
- return value ?? defaultValue;
17
- } catch (error) {
18
- return defaultValue;
14
+ const result = await storageRepository.getString(key, defaultValue);
15
+ if (result.success && result.data !== null) {
16
+ return result.data;
19
17
  }
18
+ return defaultValue;
20
19
  },
21
20
 
22
21
  async setString(key: string, value: string): Promise<void> {
23
- try {
24
- await AsyncStorage.setItem(key, value);
25
- } catch (error) {
26
- // Ignore storage errors
27
- }
22
+ await storageRepository.setString(key, value);
28
23
  },
29
24
  };
@@ -7,58 +7,42 @@
7
7
  * - i18n setup
8
8
  */
9
9
 
10
- import AsyncStorage from '@react-native-async-storage/async-storage';
10
+ import { storageRepository } from '@umituz/react-native-storage';
11
11
  import i18n from '../config/i18n';
12
12
  import { DEFAULT_LANGUAGE, getLanguageByCode, getDeviceLocale } from '../config/languages';
13
13
 
14
- // Storage key for language preference
15
14
  const LANGUAGE_STORAGE_KEY = '@localization:language';
16
15
 
17
16
  export class LanguageInitializer {
18
17
  /**
19
18
  * Initialize localization system
20
- * Detects device locale and sets up i18n
21
19
  */
22
20
  static async initialize(): Promise<{
23
21
  languageCode: string;
24
22
  isRTL: boolean;
25
23
  }> {
26
24
  try {
27
- // Get saved language preference
28
- const savedLanguage = await AsyncStorage.getItem(LANGUAGE_STORAGE_KEY) || DEFAULT_LANGUAGE;
29
-
30
- // Determine language code
25
+ const savedResult = await storageRepository.getString(LANGUAGE_STORAGE_KEY, DEFAULT_LANGUAGE);
26
+ const savedLanguage = savedResult.success && savedResult.data ? savedResult.data : DEFAULT_LANGUAGE;
31
27
  const languageCode = await this.determineLanguageCode(savedLanguage);
32
-
33
- // Validate and get language object
34
28
  const finalLanguage = await this.validateAndSetupLanguage(languageCode);
35
29
 
36
30
  return finalLanguage;
37
31
  } catch (error) {
38
- // Fallback to default language
39
32
  return await this.setupFallbackLanguage();
40
33
  }
41
34
  }
42
35
 
43
- /**
44
- * Determine which language code to use
45
- */
46
36
  private static async determineLanguageCode(savedLanguage: string): Promise<string> {
47
37
  if (savedLanguage && savedLanguage !== DEFAULT_LANGUAGE) {
48
- // User has previously selected a language
49
38
  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
39
  }
40
+
41
+ const deviceLocale = getDeviceLocale();
42
+ await storageRepository.setString(LANGUAGE_STORAGE_KEY, deviceLocale);
43
+ return deviceLocale;
57
44
  }
58
45
 
59
- /**
60
- * Validate language and set it up in i18n
61
- */
62
46
  private static async validateAndSetupLanguage(languageCode: string): Promise<{
63
47
  languageCode: string;
64
48
  isRTL: boolean;
@@ -75,9 +59,6 @@ export class LanguageInitializer {
75
59
  };
76
60
  }
77
61
 
78
- /**
79
- * Set up fallback language when initialization fails
80
- */
81
62
  private static async setupFallbackLanguage(): Promise<{
82
63
  languageCode: string;
83
64
  isRTL: boolean;
@@ -3,15 +3,13 @@
3
3
  *
4
4
  * Handles switching between languages
5
5
  * - Language validation
6
- * - Dynamic resource loading
7
6
  * - Persistence
8
7
  */
9
8
 
10
- import AsyncStorage from '@react-native-async-storage/async-storage';
9
+ import { storageRepository } from '@umituz/react-native-storage';
11
10
  import i18n from '../config/i18n';
12
11
  import { getLanguageByCode } from '../config/languages';
13
12
 
14
- // Storage key for language preference
15
13
  const LANGUAGE_STORAGE_KEY = '@localization:language';
16
14
 
17
15
  export class LanguageSwitcher {
@@ -24,60 +22,16 @@ export class LanguageSwitcher {
24
22
  }> {
25
23
  const language = getLanguageByCode(languageCode);
26
24
 
27
- // Validate language exists
28
25
  if (!language) {
29
26
  throw new Error(`Unsupported language: ${languageCode}`);
30
27
  }
31
28
 
32
- // Load language resources if needed
33
- await this.loadLanguageResources(languageCode);
34
-
35
- // Update i18n
36
29
  await i18n.changeLanguage(languageCode);
37
-
38
- // Persist language preference
39
- await AsyncStorage.setItem(LANGUAGE_STORAGE_KEY, languageCode);
30
+ await storageRepository.setString(LANGUAGE_STORAGE_KEY, languageCode);
40
31
 
41
32
  return {
42
33
  languageCode,
43
34
  isRTL: language.rtl || false,
44
35
  };
45
36
  }
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
37
  }