@umituz/react-native-settings 4.23.35 → 4.23.36
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 +12 -5
- package/src/domains/appearance/presentation/screens/AppearanceScreen.tsx +1 -1
- package/src/domains/disclaimer/presentation/components/DisclaimerSetting.test.tsx +1 -1
- package/src/domains/disclaimer/presentation/components/DisclaimerSetting.tsx +1 -1
- package/src/domains/disclaimer/presentation/screens/DisclaimerScreen.tsx +1 -1
- package/src/domains/localization/domain/repositories/ILocalizationRepository.ts +18 -0
- package/src/domains/localization/index.ts +33 -0
- package/src/domains/localization/infrastructure/components/LanguageSwitcher.styles.ts +40 -0
- package/src/domains/localization/infrastructure/components/LanguageSwitcher.tsx +88 -0
- package/src/domains/localization/infrastructure/components/__tests__/LanguageSwitcher.test.tsx +91 -0
- package/src/domains/localization/infrastructure/components/useLanguageNavigation.ts +20 -0
- package/src/domains/localization/infrastructure/components/useLanguageSwitcher.ts +34 -0
- package/src/domains/localization/infrastructure/config/DeviceLocale.ts +47 -0
- package/src/domains/localization/infrastructure/config/I18nInitializer.ts +73 -0
- package/src/domains/localization/infrastructure/config/LanguageQuery.ts +35 -0
- package/src/domains/localization/infrastructure/config/LocaleMapping.ts +78 -0
- package/src/domains/localization/infrastructure/config/NamespaceResolver.ts +54 -0
- package/src/domains/localization/infrastructure/config/ResourceBuilder.ts +72 -0
- package/src/domains/localization/infrastructure/config/TranslationLoader.ts +46 -0
- package/src/domains/localization/infrastructure/config/__tests__/languagesData.test.ts +69 -0
- package/src/domains/localization/infrastructure/config/constants/defaultLanguages.ts +43 -0
- package/src/domains/localization/infrastructure/config/i18n.ts +9 -0
- package/src/domains/localization/infrastructure/config/languages.ts +28 -0
- package/src/domains/localization/infrastructure/config/languagesData.ts +26 -0
- package/src/domains/localization/infrastructure/hooks/TranslationHook.ts +39 -0
- package/src/domains/localization/infrastructure/hooks/__tests__/useTranslation.test.ts +52 -0
- package/src/domains/localization/infrastructure/hooks/useLanguageSelection.ts +44 -0
- package/src/domains/localization/infrastructure/hooks/useLocalization.ts +41 -0
- package/src/domains/localization/infrastructure/hooks/useTranslation.ts +94 -0
- package/src/domains/localization/infrastructure/repository/LanguageRepository.ts +53 -0
- package/src/domains/localization/infrastructure/storage/AsyncStorageWrapper.ts +24 -0
- package/src/domains/localization/infrastructure/storage/LanguageInitializer.ts +81 -0
- package/src/domains/localization/infrastructure/storage/LanguageSwitcher.ts +52 -0
- package/src/domains/localization/infrastructure/storage/LocalizationStore.ts +142 -0
- package/src/domains/localization/infrastructure/storage/types/Language.ts +13 -0
- package/src/domains/localization/infrastructure/storage/types/LocalizationState.ts +27 -0
- package/src/domains/localization/presentation/components/LanguageItem.styles.ts +40 -0
- package/src/domains/localization/presentation/components/LanguageItem.tsx +106 -0
- package/src/domains/localization/presentation/components/LanguageSection.tsx +83 -0
- package/src/domains/localization/presentation/components/__tests__/LanguageItem.test.tsx +106 -0
- package/src/domains/localization/presentation/screens/LanguageSelectionScreen.styles.ts +16 -0
- package/src/domains/localization/presentation/screens/LanguageSelectionScreen.tsx +132 -0
- package/src/domains/localization/presentation/screens/LanguageSelectionScreen.types.ts +27 -0
- package/src/domains/localization/presentation/screens/__tests__/LanguageSelectionScreen.test.tsx +165 -0
- package/src/domains/localization/scripts/prepublish.js +36 -0
- package/src/domains/localization/scripts/setup-languages.js +60 -0
- package/src/domains/localization/scripts/sync-translations.js +124 -0
- package/src/domains/localization/scripts/translate-missing.js +92 -0
- package/src/domains/localization/scripts/utils/file-parser.js +78 -0
- package/src/domains/localization/scripts/utils/key-detector.js +45 -0
- package/src/domains/localization/scripts/utils/key-extractor.js +105 -0
- package/src/domains/localization/scripts/utils/object-helper.js +29 -0
- package/src/domains/localization/scripts/utils/sync-helper.js +49 -0
- package/src/domains/localization/scripts/utils/translation-config.js +116 -0
- package/src/domains/localization/scripts/utils/translator.js +83 -0
- package/src/domains/notifications/presentation/components/NotificationsSection.tsx +1 -1
- package/src/domains/notifications/presentation/screens/NotificationSettingsScreen.tsx +1 -1
- package/src/index.ts +2 -0
- package/src/presentation/components/SettingsErrorBoundary.tsx +1 -1
- package/src/presentation/navigation/SettingsStackNavigator.tsx +1 -1
- package/src/presentation/screens/components/SettingsContent.tsx +1 -1
- package/src/presentation/screens/components/SettingsHeader.tsx +1 -1
- package/src/presentation/screens/components/sections/FeatureSettingsSection.tsx +1 -1
- package/src/presentation/screens/components/sections/IdentitySettingsSection.tsx +1 -1
- package/src/presentation/screens/components/sections/ProfileSectionLoader.tsx +1 -1
- package/src/presentation/screens/components/sections/SupportSettingsSection.tsx +1 -1
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Namespace Resolver
|
|
3
|
+
* Resolves available namespaces from translations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { TranslationLoader } from './TranslationLoader';
|
|
7
|
+
|
|
8
|
+
const DEFAULT_NAMESPACE = 'common';
|
|
9
|
+
|
|
10
|
+
export class NamespaceResolver {
|
|
11
|
+
/**
|
|
12
|
+
* Get all available namespaces from package and app translations
|
|
13
|
+
*/
|
|
14
|
+
static getNamespaces(
|
|
15
|
+
appTranslations: Record<string, any>,
|
|
16
|
+
languageCode: string
|
|
17
|
+
): string[] {
|
|
18
|
+
const packageTranslations = TranslationLoader.loadPackageTranslations();
|
|
19
|
+
const packageLang = packageTranslations[languageCode] || {};
|
|
20
|
+
|
|
21
|
+
let appNamespaces: string[] = [];
|
|
22
|
+
|
|
23
|
+
// Check if appTranslations has language keys (format: xx-XX)
|
|
24
|
+
const hasLanguageKeys = Object.keys(appTranslations).some(key =>
|
|
25
|
+
/^[a-z]{2}-[A-Z]{2}$/.test(key)
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
if (hasLanguageKeys) {
|
|
29
|
+
// If structured by language, get namespaces from the requested language
|
|
30
|
+
const langTranslations = appTranslations[languageCode];
|
|
31
|
+
if (langTranslations && typeof langTranslations === 'object') {
|
|
32
|
+
appNamespaces = Object.keys(langTranslations);
|
|
33
|
+
}
|
|
34
|
+
} else {
|
|
35
|
+
// If structured by namespace (legacy/simple), keys are namespaces
|
|
36
|
+
appNamespaces = Object.keys(appTranslations);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const namespaces = new Set([
|
|
40
|
+
...Object.keys(packageLang),
|
|
41
|
+
...appNamespaces,
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
if (!namespaces.has(DEFAULT_NAMESPACE)) {
|
|
45
|
+
namespaces.add(DEFAULT_NAMESPACE);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return Array.from(namespaces);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
static getDefaultNamespace(): string {
|
|
52
|
+
return DEFAULT_NAMESPACE;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resource Builder
|
|
3
|
+
* Builds i18n resources from package and app translations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { TranslationLoader } from './TranslationLoader';
|
|
7
|
+
|
|
8
|
+
export class ResourceBuilder {
|
|
9
|
+
/**
|
|
10
|
+
* Build resources with package + app translations
|
|
11
|
+
*/
|
|
12
|
+
static buildResources(
|
|
13
|
+
appTranslations: Record<string, any>,
|
|
14
|
+
languageCode: string
|
|
15
|
+
): Record<string, Record<string, any>> {
|
|
16
|
+
const packageTranslations = TranslationLoader.loadPackageTranslations();
|
|
17
|
+
|
|
18
|
+
// Initialize with package translations
|
|
19
|
+
const resources: Record<string, Record<string, any>> = { ...packageTranslations };
|
|
20
|
+
|
|
21
|
+
// Note: Do NOT create empty resources for unsupported languages
|
|
22
|
+
// i18next will properly fallback to fallbackLng when language not in resources
|
|
23
|
+
|
|
24
|
+
// Process app translations
|
|
25
|
+
for (const [key, value] of Object.entries(appTranslations)) {
|
|
26
|
+
// Check if the key is a language code (format: xx-XX)
|
|
27
|
+
const isLanguageKey = /^[a-z]{2}-[A-Z]{2}$/.test(key);
|
|
28
|
+
|
|
29
|
+
if (isLanguageKey) {
|
|
30
|
+
// It's a language key (e.g., "en-US")
|
|
31
|
+
const lang = key;
|
|
32
|
+
|
|
33
|
+
// Only process if value has actual content
|
|
34
|
+
if (value && typeof value === 'object' && Object.keys(value).length > 0) {
|
|
35
|
+
if (!resources[lang]) {
|
|
36
|
+
resources[lang] = {};
|
|
37
|
+
}
|
|
38
|
+
const namespaces = Object.keys(value);
|
|
39
|
+
const isFlatMap = namespaces.every(nsKey => typeof value[nsKey] === 'string');
|
|
40
|
+
|
|
41
|
+
if (isFlatMap) {
|
|
42
|
+
// It's a flat map (e.g. { hello: 'Hello' }), wrap in 'common'
|
|
43
|
+
const defaultNs = 'common';
|
|
44
|
+
resources[lang][defaultNs] = TranslationLoader.mergeTranslations(
|
|
45
|
+
resources[lang][defaultNs] || {},
|
|
46
|
+
value
|
|
47
|
+
);
|
|
48
|
+
} else {
|
|
49
|
+
// It's already namespaced (e.g. { auth: {...} })
|
|
50
|
+
for (const [namespace, translations] of Object.entries(value)) {
|
|
51
|
+
resources[lang][namespace] = TranslationLoader.mergeTranslations(
|
|
52
|
+
resources[lang][namespace] || {},
|
|
53
|
+
translations as Record<string, any>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
// It's a namespace for the default/current language (backward compatibility)
|
|
60
|
+
// Only add if the language exists in resources (to prevent creating empty resources)
|
|
61
|
+
if (value && typeof value === 'object' && resources[languageCode]) {
|
|
62
|
+
resources[languageCode][key] = TranslationLoader.mergeTranslations(
|
|
63
|
+
resources[languageCode][key] || {},
|
|
64
|
+
value
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return resources;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translation Loader
|
|
3
|
+
*
|
|
4
|
+
* Loads package translations
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export class TranslationLoader {
|
|
8
|
+
/**
|
|
9
|
+
* Load package translations (empty by default - apps provide their own)
|
|
10
|
+
*/
|
|
11
|
+
static loadPackageTranslations(): Record<string, any> {
|
|
12
|
+
// Package doesn't include any translations by default
|
|
13
|
+
// Consuming applications should provide their own translations
|
|
14
|
+
return { 'en-US': {} };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Deep merge translations (override wins)
|
|
19
|
+
*/
|
|
20
|
+
static mergeTranslations(base: any, override: any): any {
|
|
21
|
+
if (!override || Object.keys(override).length === 0) {
|
|
22
|
+
return base;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const merged = { ...base };
|
|
26
|
+
|
|
27
|
+
for (const key in override) {
|
|
28
|
+
if (Object.prototype.hasOwnProperty.call(override, key)) {
|
|
29
|
+
const baseVal = base[key];
|
|
30
|
+
const overrideVal = override[key];
|
|
31
|
+
|
|
32
|
+
if (this.isObject(baseVal) && this.isObject(overrideVal)) {
|
|
33
|
+
merged[key] = this.mergeTranslations(baseVal, overrideVal);
|
|
34
|
+
} else {
|
|
35
|
+
merged[key] = overrideVal;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return merged;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private static isObject(val: any): boolean {
|
|
44
|
+
return val !== null && typeof val === 'object' && !Array.isArray(val);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple tests for language repository
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { languageRepository } from '../../repository/LanguageRepository';
|
|
6
|
+
|
|
7
|
+
describe('LanguageRepository', () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
languageRepository.clearLanguages();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('should get default language', () => {
|
|
13
|
+
const defaultLang = languageRepository.getDefaultLanguage();
|
|
14
|
+
expect(defaultLang).toBeDefined();
|
|
15
|
+
expect(defaultLang.code).toBe('en-US');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should check if language is supported', () => {
|
|
19
|
+
const supported = languageRepository.isLanguageSupported('en-US');
|
|
20
|
+
expect(supported).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should check if language is not supported', () => {
|
|
24
|
+
const supported = languageRepository.isLanguageSupported('unknown');
|
|
25
|
+
expect(supported).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should get language by code', () => {
|
|
29
|
+
const language = languageRepository.getLanguageByCode('en-US');
|
|
30
|
+
expect(language).toBeDefined();
|
|
31
|
+
expect(language?.code).toBe('en-US');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should return undefined for unknown code', () => {
|
|
35
|
+
const language = languageRepository.getLanguageByCode('unknown');
|
|
36
|
+
expect(language).toBeUndefined();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should search languages', () => {
|
|
40
|
+
const results = languageRepository.searchLanguages('english');
|
|
41
|
+
expect(Array.isArray(results)).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should get supported languages', () => {
|
|
45
|
+
const languages = languageRepository.getLanguages();
|
|
46
|
+
expect(Array.isArray(languages)).toBe(true);
|
|
47
|
+
expect(languages.length).toBeGreaterThan(0);
|
|
48
|
+
// Should now support many languages (29+)
|
|
49
|
+
expect(languages.length).toBeGreaterThan(20);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should support newly added languages', () => {
|
|
53
|
+
expect(languageRepository.isLanguageSupported('cs-CZ')).toBe(true);
|
|
54
|
+
expect(languageRepository.isLanguageSupported('pt-BR')).toBe(true);
|
|
55
|
+
expect(languageRepository.isLanguageSupported('zh-TW')).toBe(true);
|
|
56
|
+
expect(languageRepository.isLanguageSupported('el-GR')).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should find language attributes correctly', () => {
|
|
60
|
+
const czech = languageRepository.getLanguageByCode('cs-CZ');
|
|
61
|
+
expect(czech).toBeDefined();
|
|
62
|
+
expect(czech?.name).toBe('Czech');
|
|
63
|
+
expect(czech?.flag).toBe('🇨🇿');
|
|
64
|
+
|
|
65
|
+
const brazil = languageRepository.getLanguageByCode('pt-BR');
|
|
66
|
+
expect(brazil).toBeDefined();
|
|
67
|
+
expect(brazil?.name).toBe('Portuguese (Brazil)');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default Languages Configuration
|
|
3
|
+
* Applications can override this by providing their own language list
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Language } from '../../storage/types/Language';
|
|
7
|
+
|
|
8
|
+
export const DEFAULT_LANGUAGES: Language[] = [
|
|
9
|
+
{ code: 'ar-SA', name: 'Arabic', nativeName: 'العربية', flag: '🇸🇦', isRTL: true },
|
|
10
|
+
{ code: 'bg-BG', name: 'Bulgarian', nativeName: 'Български', flag: '🇧🇬', isRTL: false },
|
|
11
|
+
{ code: 'cs-CZ', name: 'Czech', nativeName: 'Čeština', flag: '🇨🇿', isRTL: false },
|
|
12
|
+
{ code: 'da-DK', name: 'Danish', nativeName: 'Dansk', flag: '🇩🇰', isRTL: false },
|
|
13
|
+
{ code: 'de-DE', name: 'German', nativeName: 'Deutsch', flag: '🇩🇪', isRTL: false },
|
|
14
|
+
{ code: 'el-GR', name: 'Greek', nativeName: 'Ελληνικά', flag: '🇬🇷', isRTL: false },
|
|
15
|
+
{ code: 'en-US', name: 'English', nativeName: 'English', flag: '🇺🇸', isRTL: false },
|
|
16
|
+
{ code: 'es-ES', name: 'Spanish', nativeName: 'Español', flag: '🇪🇸', isRTL: false },
|
|
17
|
+
{ code: 'fi-FI', name: 'Finnish', nativeName: 'Suomi', flag: '🇫🇮', isRTL: false },
|
|
18
|
+
{ code: 'fr-FR', name: 'French', nativeName: 'Français', flag: '🇫🇷', isRTL: false },
|
|
19
|
+
{ code: 'hi-IN', name: 'Hindi', nativeName: 'हिन्दी', flag: '🇮🇳', isRTL: false },
|
|
20
|
+
{ code: 'hr-HR', name: 'Croatian', nativeName: 'Hrvatski', flag: '🇭🇷', isRTL: false },
|
|
21
|
+
{ code: 'hu-HU', name: 'Hungarian', nativeName: 'Magyar', flag: '🇭🇺', isRTL: false },
|
|
22
|
+
{ code: 'id-ID', name: 'Indonesian', nativeName: 'Bahasa Indonesia', flag: '🇮', isRTL: false },
|
|
23
|
+
{ code: 'it-IT', name: 'Italian', nativeName: 'Italiano', flag: '🇮🇹', isRTL: false },
|
|
24
|
+
{ code: 'ja-JP', name: 'Japanese', nativeName: '日本語', flag: '🇯🇵', isRTL: false },
|
|
25
|
+
{ code: 'ko-KR', name: 'Korean', nativeName: '한국어', flag: '🇰🇷', isRTL: false },
|
|
26
|
+
{ code: 'ms-MY', name: 'Malay', nativeName: 'Bahasa Melayu', flag: '🇲🇾', isRTL: false },
|
|
27
|
+
{ code: 'nl-NL', name: 'Dutch', nativeName: 'Nederlands', flag: '🇳🇱', isRTL: false },
|
|
28
|
+
{ code: 'no-NO', name: 'Norwegian', nativeName: 'Norsk', flag: '🇳🇴', isRTL: false },
|
|
29
|
+
{ code: 'pl-PL', name: 'Polish', nativeName: 'Polski', flag: '🇵🇱', isRTL: false },
|
|
30
|
+
{ code: 'pt-BR', name: 'Portuguese (Brazil)', nativeName: 'Português (Brasil)', flag: '🇧🇷', isRTL: false },
|
|
31
|
+
{ code: 'pt-PT', name: 'Portuguese', nativeName: 'Português', flag: '🇵🇹', isRTL: false },
|
|
32
|
+
{ code: 'ro-RO', name: 'Romanian', nativeName: 'Română', flag: '🇷🇴', isRTL: false },
|
|
33
|
+
{ code: 'ru-RU', name: 'Russian', nativeName: 'Русский', flag: '🇷🇺', isRTL: false },
|
|
34
|
+
{ code: 'sk-SK', name: 'Slovak', nativeName: 'Slovenčina', flag: '🇸🇰', isRTL: false },
|
|
35
|
+
{ code: 'sv-SE', name: 'Swedish', nativeName: 'Svenska', flag: '🇸🇪', isRTL: false },
|
|
36
|
+
{ code: 'th-TH', name: 'Thai', nativeName: 'ไทย', flag: '🇹🇭', isRTL: false },
|
|
37
|
+
{ code: 'tl-PH', name: 'Filipino', nativeName: 'Filipino', flag: '🇵🇭', isRTL: false },
|
|
38
|
+
{ code: 'tr-TR', name: 'Turkish', nativeName: 'Türkçe', flag: '🇹🇷', isRTL: false },
|
|
39
|
+
{ code: 'uk-UA', name: 'Ukrainian', nativeName: 'Українська', flag: '🇺🇦', isRTL: false },
|
|
40
|
+
{ code: 'vi-VN', name: 'Vietnamese', nativeName: 'Tiếng Việt', flag: '🇻🇳', isRTL: false },
|
|
41
|
+
{ code: 'zh-CN', name: 'Chinese (Simplified)', nativeName: '简体中文', flag: '🇨🇳', isRTL: false },
|
|
42
|
+
{ code: 'zh-TW', name: 'Chinese (Traditional)', nativeName: '繁體中文', flag: '🇹🇼', isRTL: false },
|
|
43
|
+
];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Languages Configuration - Main Export
|
|
3
|
+
* Central export point for all language-related functionality
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { languageRepository } from '../repository/LanguageRepository';
|
|
7
|
+
import type { Language } from '../storage/types/Language';
|
|
8
|
+
|
|
9
|
+
// Re-export from DeviceLocale
|
|
10
|
+
export { DEFAULT_LANGUAGE, getDeviceLocale } from './DeviceLocale';
|
|
11
|
+
|
|
12
|
+
// Re-export from LanguageQuery
|
|
13
|
+
export {
|
|
14
|
+
getSupportedLanguages,
|
|
15
|
+
getLanguageByCode,
|
|
16
|
+
isLanguageSupported,
|
|
17
|
+
getDefaultLanguage,
|
|
18
|
+
searchLanguages,
|
|
19
|
+
} from './LanguageQuery';
|
|
20
|
+
|
|
21
|
+
// Re-export from LocaleMapping
|
|
22
|
+
export { LOCALE_MAPPING } from './LocaleMapping';
|
|
23
|
+
|
|
24
|
+
// Backward compatibility
|
|
25
|
+
export const getSUPPORTED_LANGUAGES = () => languageRepository.getLanguages();
|
|
26
|
+
export const getLANGUAGES = () => languageRepository.getLanguages();
|
|
27
|
+
export const SUPPORTED_LANGUAGES: Language[] = languageRepository.getLanguages();
|
|
28
|
+
export const LANGUAGES = SUPPORTED_LANGUAGES;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Language Data Exports
|
|
3
|
+
* Centralized exports for language management
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { languageRepository } from '../repository/LanguageRepository';
|
|
7
|
+
import { DEFAULT_LANGUAGES } from './constants/defaultLanguages';
|
|
8
|
+
import type { Language } from '../storage/types/Language';
|
|
9
|
+
|
|
10
|
+
export { languageRepository };
|
|
11
|
+
export { DEFAULT_LANGUAGES };
|
|
12
|
+
export type { Language };
|
|
13
|
+
|
|
14
|
+
export const getLanguageByCode = (code: string) =>
|
|
15
|
+
languageRepository.getLanguageByCode(code);
|
|
16
|
+
|
|
17
|
+
export const searchLanguages = (query: string) =>
|
|
18
|
+
languageRepository.searchLanguages(query);
|
|
19
|
+
|
|
20
|
+
export const isLanguageSupported = (code: string) =>
|
|
21
|
+
languageRepository.isLanguageSupported(code);
|
|
22
|
+
|
|
23
|
+
export const getDefaultLanguage = () =>
|
|
24
|
+
languageRepository.getDefaultLanguage();
|
|
25
|
+
|
|
26
|
+
export const LANGUAGES = languageRepository.getLanguages();
|
|
@@ -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,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple tests for useTranslation hook
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useTranslationFunction } from '../useTranslation';
|
|
6
|
+
|
|
7
|
+
// Mock React hooks
|
|
8
|
+
jest.mock('react', () => ({
|
|
9
|
+
useMemo: jest.fn((fn) => fn()),
|
|
10
|
+
useCallback: jest.fn((fn) => fn),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
// Mock i18next
|
|
14
|
+
jest.mock('../../config/i18n', () => ({
|
|
15
|
+
isInitialized: true,
|
|
16
|
+
t: jest.fn((key) => key),
|
|
17
|
+
hasResourceBundle: jest.fn(() => false),
|
|
18
|
+
language: 'en-US',
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
describe('useTranslationFunction', () => {
|
|
22
|
+
it('should return translation function', () => {
|
|
23
|
+
const { t, clearCache } = useTranslationFunction();
|
|
24
|
+
|
|
25
|
+
expect(typeof t).toBe('function');
|
|
26
|
+
expect(typeof clearCache).toBe('function');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should return key when translation not found', () => {
|
|
30
|
+
const { t } = useTranslationFunction();
|
|
31
|
+
const result = t('test.key');
|
|
32
|
+
|
|
33
|
+
expect(result).toBe('test.key');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should handle translation options', () => {
|
|
37
|
+
const { t } = useTranslationFunction();
|
|
38
|
+
const result = t('test.key', { count: 1 });
|
|
39
|
+
|
|
40
|
+
expect(typeof result).toBe('string');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should clear cache', () => {
|
|
44
|
+
const { t, clearCache } = useTranslationFunction();
|
|
45
|
+
|
|
46
|
+
// Add to cache
|
|
47
|
+
t('test.key');
|
|
48
|
+
|
|
49
|
+
// Clear cache
|
|
50
|
+
expect(() => clearCache()).not.toThrow();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Language Selection Hook
|
|
3
|
+
* Manages language selection state and filtering
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, useMemo } from 'react';
|
|
7
|
+
import { useLocalization } from './useLocalization';
|
|
8
|
+
import { searchLanguages } from '../config/LanguageQuery';
|
|
9
|
+
|
|
10
|
+
export const useLanguageSelection = () => {
|
|
11
|
+
const { currentLanguage, setLanguage } = useLocalization();
|
|
12
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
13
|
+
const [selectedCode, setSelectedCode] = useState(currentLanguage);
|
|
14
|
+
|
|
15
|
+
const filteredLanguages = useMemo(() => {
|
|
16
|
+
return searchLanguages(searchQuery);
|
|
17
|
+
}, [searchQuery]);
|
|
18
|
+
|
|
19
|
+
const handleLanguageSelect = async (code: string, onComplete?: () => void) => {
|
|
20
|
+
if (__DEV__) {
|
|
21
|
+
console.log('[useLanguageSelection] handleLanguageSelect called:', { code, currentLanguage });
|
|
22
|
+
}
|
|
23
|
+
setSelectedCode(code);
|
|
24
|
+
if (__DEV__) {
|
|
25
|
+
console.log('[useLanguageSelection] Calling setLanguage...');
|
|
26
|
+
}
|
|
27
|
+
await setLanguage(code);
|
|
28
|
+
if (__DEV__) {
|
|
29
|
+
console.log('[useLanguageSelection] Language changed to:', code);
|
|
30
|
+
}
|
|
31
|
+
onComplete?.();
|
|
32
|
+
if (__DEV__) {
|
|
33
|
+
console.log('[useLanguageSelection] onComplete callback executed');
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
searchQuery,
|
|
39
|
+
setSearchQuery,
|
|
40
|
+
selectedCode,
|
|
41
|
+
filteredLanguages,
|
|
42
|
+
handleLanguageSelect,
|
|
43
|
+
};
|
|
44
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import { useLocalizationStore } from '../storage/LocalizationStore';
|
|
3
|
+
import { useTranslationFunction } from './useTranslation';
|
|
4
|
+
import type { Language } from '../../domain/repositories/ILocalizationRepository';
|
|
5
|
+
|
|
6
|
+
export const useLocalization = () => {
|
|
7
|
+
const store = useLocalizationStore();
|
|
8
|
+
const { t } = useTranslationFunction();
|
|
9
|
+
|
|
10
|
+
const getCurrentLanguageObject = useCallback((): Language | undefined => {
|
|
11
|
+
return store.getCurrentLanguage();
|
|
12
|
+
}, [store]);
|
|
13
|
+
|
|
14
|
+
const handleSetLanguage = useCallback(async (languageCode: string) => {
|
|
15
|
+
await store.setLanguage(languageCode);
|
|
16
|
+
}, [store]);
|
|
17
|
+
|
|
18
|
+
const handleInitialize = useCallback(async () => {
|
|
19
|
+
await store.initialize();
|
|
20
|
+
}, [store]);
|
|
21
|
+
|
|
22
|
+
const isLanguageSupported = useCallback((code: string) => {
|
|
23
|
+
return store.isLanguageSupported(code);
|
|
24
|
+
}, [store]);
|
|
25
|
+
|
|
26
|
+
const getSupportedLanguages = useCallback(() => {
|
|
27
|
+
return store.getSupportedLanguages();
|
|
28
|
+
}, [store]);
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
t,
|
|
32
|
+
currentLanguage: store.currentLanguage,
|
|
33
|
+
currentLanguageObject: getCurrentLanguageObject(),
|
|
34
|
+
isRTL: store.isRTL,
|
|
35
|
+
isInitialized: store.isInitialized,
|
|
36
|
+
setLanguage: handleSetLanguage,
|
|
37
|
+
initialize: handleInitialize,
|
|
38
|
+
isLanguageSupported,
|
|
39
|
+
supportedLanguages: getSupportedLanguages(),
|
|
40
|
+
};
|
|
41
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translation Hook
|
|
3
|
+
*
|
|
4
|
+
* Provides translation function with proper language change reactivity
|
|
5
|
+
* - React i18next integration for automatic language change detection
|
|
6
|
+
* - Auto-namespace detection from dot notation
|
|
7
|
+
* - Type-safe translation function
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useCallback } from 'react';
|
|
11
|
+
import { useTranslation } from 'react-i18next';
|
|
12
|
+
import i18n from '../config/i18n';
|
|
13
|
+
|
|
14
|
+
export interface TranslationOptions {
|
|
15
|
+
count?: number;
|
|
16
|
+
ns?: string | string[];
|
|
17
|
+
defaultValue?: string;
|
|
18
|
+
[key: string]: unknown;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Hook for translation functionality
|
|
23
|
+
* Uses react-i18next for automatic language change reactivity
|
|
24
|
+
*
|
|
25
|
+
* Supports both formats:
|
|
26
|
+
* - t('namespace:key.subkey') - explicit namespace
|
|
27
|
+
* - t('namespace.key.subkey') - auto-detected namespace (first segment before dot)
|
|
28
|
+
*/
|
|
29
|
+
export const useTranslationFunction = () => {
|
|
30
|
+
const { t: i18nextT, ready } = useTranslation(undefined, { i18n });
|
|
31
|
+
|
|
32
|
+
const translate = useCallback((key: string, defaultValueOrOptions?: string | TranslationOptions): string => {
|
|
33
|
+
const options: TranslationOptions = typeof defaultValueOrOptions === 'string'
|
|
34
|
+
? { defaultValue: defaultValueOrOptions }
|
|
35
|
+
: defaultValueOrOptions || {};
|
|
36
|
+
|
|
37
|
+
if (!ready || !i18n.isInitialized) {
|
|
38
|
+
return options.defaultValue || key;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let finalResult: string;
|
|
42
|
+
let translationFound = false;
|
|
43
|
+
|
|
44
|
+
// If key already has namespace separator (:), use as-is
|
|
45
|
+
if (key.includes(':')) {
|
|
46
|
+
const result = i18nextT(key, options);
|
|
47
|
+
finalResult = typeof result === 'string' ? result : key;
|
|
48
|
+
translationFound = finalResult !== key && finalResult !== options.defaultValue;
|
|
49
|
+
} else {
|
|
50
|
+
// Auto-detect namespace from first dot segment
|
|
51
|
+
const firstDotIndex = key.indexOf('.');
|
|
52
|
+
if (firstDotIndex > 0) {
|
|
53
|
+
const potentialNamespace = key.substring(0, firstDotIndex);
|
|
54
|
+
const restOfKey = key.substring(firstDotIndex + 1);
|
|
55
|
+
const hasNamespace = i18n.hasResourceBundle(i18n.language, potentialNamespace);
|
|
56
|
+
|
|
57
|
+
if (hasNamespace) {
|
|
58
|
+
const namespacedKey = `${potentialNamespace}:${restOfKey}`;
|
|
59
|
+
const namespacedResult = i18nextT(namespacedKey, options);
|
|
60
|
+
|
|
61
|
+
if (namespacedResult !== namespacedKey && namespacedResult !== restOfKey) {
|
|
62
|
+
finalResult = typeof namespacedResult === 'string' ? namespacedResult : key;
|
|
63
|
+
translationFound = true;
|
|
64
|
+
} else {
|
|
65
|
+
// Fallback to original key
|
|
66
|
+
const result = i18nextT(key, options);
|
|
67
|
+
finalResult = typeof result === 'string' ? result : key;
|
|
68
|
+
translationFound = finalResult !== key && finalResult !== options.defaultValue;
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
// Fallback to original key
|
|
72
|
+
const result = i18nextT(key, options);
|
|
73
|
+
finalResult = typeof result === 'string' ? result : key;
|
|
74
|
+
translationFound = finalResult !== key && finalResult !== options.defaultValue;
|
|
75
|
+
}
|
|
76
|
+
} else {
|
|
77
|
+
// Fallback to original key
|
|
78
|
+
const result = i18nextT(key, options);
|
|
79
|
+
finalResult = typeof result === 'string' ? result : key;
|
|
80
|
+
translationFound = finalResult !== key && finalResult !== options.defaultValue;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (__DEV__ && !translationFound) {
|
|
85
|
+
console.warn(`[Localization] Translation missing for key: "${key}" in language: "${i18n.language}"`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return finalResult;
|
|
89
|
+
}, [i18nextT, ready]);
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
t: translate,
|
|
93
|
+
};
|
|
94
|
+
};
|