@umituz/react-native-localization 2.2.2 → 2.4.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/infrastructure/config/I18nInitializer.ts +68 -65
- package/src/infrastructure/config/TranslationLoader.ts +44 -29
- package/src/infrastructure/config/i18n.ts +2 -3
- package/src/infrastructure/hooks/useTranslation.ts +0 -49
- package/src/infrastructure/locales/en-US/common.json +57 -0
- package/src/infrastructure/locales/en-US/index.ts +27 -46
- package/src/infrastructure/storage/AsyncStorageWrapper.ts +8 -13
- package/src/infrastructure/storage/LanguageInitializer.ts +7 -26
- package/src/infrastructure/storage/LanguageSwitcher.ts +2 -48
package/package.json
CHANGED
|
@@ -1,117 +1,120 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* i18n Initializer
|
|
3
3
|
*
|
|
4
|
-
* Handles i18n configuration
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
7
|
-
* -
|
|
4
|
+
* Handles i18n configuration with namespace support
|
|
5
|
+
* - Loads package translations
|
|
6
|
+
* - Loads app translations (merges with package)
|
|
7
|
+
* - Namespace-based organization (common, auth, etc.)
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import i18n from 'i18next';
|
|
11
11
|
import { initReactI18next } from 'react-i18next';
|
|
12
|
-
import { DEFAULT_LANGUAGE
|
|
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
|
-
*
|
|
21
|
+
* Build resources with package + app translations merged
|
|
20
22
|
*/
|
|
21
|
-
private static
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
23
|
+
private static buildResources(): Record<string, Record<string, any>> {
|
|
24
|
+
const packageTranslations = TranslationLoader.loadPackageTranslations();
|
|
25
|
+
const appTranslations = TranslationLoader.loadAppTranslations();
|
|
26
|
+
|
|
27
|
+
const resources: Record<string, Record<string, any>> = {
|
|
28
|
+
'en-US': {},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const enUSPackage = packageTranslations['en-US'] || {};
|
|
32
|
+
|
|
33
|
+
// Add package namespaces
|
|
34
|
+
for (const [namespace, translations] of Object.entries(enUSPackage)) {
|
|
35
|
+
resources['en-US'][namespace] = translations;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Merge app namespaces (app overrides package)
|
|
39
|
+
for (const [namespace, translations] of Object.entries(appTranslations)) {
|
|
40
|
+
if (resources['en-US'][namespace]) {
|
|
41
|
+
resources['en-US'][namespace] = TranslationLoader.mergeTranslations(
|
|
42
|
+
resources['en-US'][namespace],
|
|
43
|
+
translations
|
|
44
|
+
);
|
|
45
|
+
} else {
|
|
46
|
+
resources['en-US'][namespace] = translations;
|
|
35
47
|
}
|
|
36
48
|
}
|
|
37
49
|
|
|
38
|
-
return
|
|
50
|
+
return resources;
|
|
39
51
|
}
|
|
40
52
|
|
|
41
|
-
|
|
42
|
-
* Build resources object for all supported languages
|
|
43
|
-
*/
|
|
44
|
-
private static buildResources(): Record<string, { translation: any }> {
|
|
45
|
-
const resources: Record<string, { translation: any }> = {};
|
|
53
|
+
private static getNamespaces(): string[] {
|
|
46
54
|
const packageTranslations = TranslationLoader.loadPackageTranslations();
|
|
47
|
-
const
|
|
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
|
-
};
|
|
55
|
+
const appTranslations = TranslationLoader.loadAppTranslations();
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
const enUSPackage = packageTranslations['en-US'] || {};
|
|
58
|
+
const namespaces = new Set([
|
|
59
|
+
...Object.keys(enUSPackage),
|
|
60
|
+
...Object.keys(appTranslations),
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
if (!namespaces.has(DEFAULT_NAMESPACE)) {
|
|
64
|
+
namespaces.add(DEFAULT_NAMESPACE);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return Array.from(namespaces);
|
|
58
68
|
}
|
|
59
69
|
|
|
60
|
-
/**
|
|
61
|
-
* Initialize i18next
|
|
62
|
-
*/
|
|
63
70
|
static initialize(): void {
|
|
64
|
-
// Prevent multiple initializations
|
|
65
71
|
if (i18n.isInitialized) {
|
|
66
72
|
return;
|
|
67
73
|
}
|
|
68
74
|
|
|
69
75
|
try {
|
|
70
|
-
// Use initReactI18next once
|
|
71
76
|
if (!this.reactI18nextInitialized) {
|
|
72
77
|
i18n.use(initReactI18next);
|
|
73
78
|
this.reactI18nextInitialized = true;
|
|
74
79
|
}
|
|
75
80
|
|
|
76
81
|
const resources = this.buildResources();
|
|
82
|
+
const namespaces = this.getNamespaces();
|
|
77
83
|
|
|
78
84
|
i18n.init({
|
|
79
85
|
resources,
|
|
80
86
|
lng: DEFAULT_LANGUAGE,
|
|
81
87
|
fallbackLng: DEFAULT_LANGUAGE,
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
},
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
debug: typeof __DEV__ !== 'undefined' && __DEV__,
|
|
88
|
+
ns: namespaces,
|
|
89
|
+
defaultNS: DEFAULT_NAMESPACE,
|
|
90
|
+
fallbackNS: DEFAULT_NAMESPACE,
|
|
91
|
+
interpolation: { escapeValue: false },
|
|
92
|
+
react: { useSuspense: false },
|
|
93
|
+
compatibilityJSON: 'v3',
|
|
94
|
+
pluralSeparator: '_',
|
|
95
|
+
keySeparator: '.',
|
|
96
|
+
nsSeparator: ':',
|
|
97
|
+
saveMissing: false,
|
|
98
|
+
missingKeyHandler: false,
|
|
99
|
+
debug: false,
|
|
96
100
|
});
|
|
97
|
-
|
|
98
101
|
} catch (error) {
|
|
99
|
-
// Don't throw - allow app to continue without i18n
|
|
100
102
|
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
101
|
-
console.error('
|
|
103
|
+
console.error('[Localization] init error:', error);
|
|
102
104
|
}
|
|
103
105
|
}
|
|
104
106
|
}
|
|
105
107
|
|
|
106
108
|
/**
|
|
107
|
-
* Add
|
|
109
|
+
* Add translation resources at runtime
|
|
108
110
|
*/
|
|
109
|
-
static addTranslationResources(
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
111
|
+
static addTranslationResources(
|
|
112
|
+
languageCode: string,
|
|
113
|
+
namespaceResources: Record<string, any>
|
|
114
|
+
): void {
|
|
115
|
+
for (const [namespace, translations] of Object.entries(namespaceResources)) {
|
|
116
|
+
if (translations && typeof translations === 'object') {
|
|
117
|
+
i18n.addResourceBundle(languageCode, namespace, translations, true, true);
|
|
115
118
|
}
|
|
116
119
|
}
|
|
117
120
|
}
|
|
@@ -1,58 +1,73 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Translation Loader
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Loads translations from package and app
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
export class TranslationLoader {
|
|
8
8
|
/**
|
|
9
|
-
* Load package translations (en-US
|
|
9
|
+
* Load package translations (en-US)
|
|
10
10
|
*/
|
|
11
11
|
static loadPackageTranslations(): Record<string, any> {
|
|
12
12
|
try {
|
|
13
13
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
14
14
|
const translations = require('../locales/en-US');
|
|
15
15
|
return { 'en-US': translations.default || translations };
|
|
16
|
-
} catch
|
|
16
|
+
} catch {
|
|
17
17
|
return { 'en-US': {} };
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
-
*
|
|
22
|
+
* Load app translations from common paths
|
|
23
23
|
*/
|
|
24
|
-
static
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
static loadAppTranslations(): Record<string, any> {
|
|
25
|
+
const paths = [
|
|
26
|
+
'@/domains/localization/infrastructure/locales/en-US',
|
|
27
|
+
'./src/domains/localization/infrastructure/locales/en-US',
|
|
28
|
+
'../../../src/domains/localization/infrastructure/locales/en-US',
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
for (const path of paths) {
|
|
32
|
+
try {
|
|
33
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
34
|
+
const translations = require(path);
|
|
35
|
+
return translations.default || translations;
|
|
36
|
+
} catch {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Deep merge translations (app overrides package)
|
|
46
|
+
*/
|
|
47
|
+
static mergeTranslations(base: any, override: any): any {
|
|
48
|
+
if (!override || Object.keys(override).length === 0) {
|
|
49
|
+
return base;
|
|
30
50
|
}
|
|
31
51
|
|
|
32
|
-
const merged = { ...
|
|
33
|
-
|
|
34
|
-
for (const key in
|
|
35
|
-
if (
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
packageTranslations[key] !== null &&
|
|
42
|
-
!Array.isArray(packageTranslations[key])
|
|
43
|
-
) {
|
|
44
|
-
// Deep merge nested objects
|
|
45
|
-
merged[key] = this.mergeTranslations(
|
|
46
|
-
packageTranslations[key],
|
|
47
|
-
projectTranslations[key]
|
|
48
|
-
);
|
|
52
|
+
const merged = { ...base };
|
|
53
|
+
|
|
54
|
+
for (const key in override) {
|
|
55
|
+
if (Object.prototype.hasOwnProperty.call(override, key)) {
|
|
56
|
+
const baseVal = base[key];
|
|
57
|
+
const overrideVal = override[key];
|
|
58
|
+
|
|
59
|
+
if (this.isObject(baseVal) && this.isObject(overrideVal)) {
|
|
60
|
+
merged[key] = this.mergeTranslations(baseVal, overrideVal);
|
|
49
61
|
} else {
|
|
50
|
-
|
|
51
|
-
merged[key] = projectTranslations[key];
|
|
62
|
+
merged[key] = overrideVal;
|
|
52
63
|
}
|
|
53
64
|
}
|
|
54
65
|
}
|
|
55
66
|
|
|
56
67
|
return merged;
|
|
57
68
|
}
|
|
69
|
+
|
|
70
|
+
private static isObject(val: any): boolean {
|
|
71
|
+
return val !== null && typeof val === 'object' && !Array.isArray(val);
|
|
72
|
+
}
|
|
58
73
|
}
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* i18n Configuration
|
|
3
3
|
*
|
|
4
|
-
* Auto-initializes i18n with
|
|
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
|
|
2
|
+
* Translation loader for en-US with namespace support
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Storage Wrapper
|
|
3
|
+
* Uses @umituz/react-native-storage for persistence
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
return
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
28
|
-
const savedLanguage =
|
|
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
|
|
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
|
}
|