@umituz/react-native-settings 4.23.35 → 4.23.37

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.
Files changed (66) hide show
  1. package/package.json +12 -5
  2. package/src/domains/appearance/presentation/screens/AppearanceScreen.tsx +1 -1
  3. package/src/domains/disclaimer/presentation/components/DisclaimerSetting.test.tsx +1 -1
  4. package/src/domains/disclaimer/presentation/components/DisclaimerSetting.tsx +1 -1
  5. package/src/domains/disclaimer/presentation/screens/DisclaimerScreen.tsx +1 -1
  6. package/src/domains/localization/domain/repositories/ILocalizationRepository.ts +18 -0
  7. package/src/domains/localization/index.ts +33 -0
  8. package/src/domains/localization/infrastructure/components/LanguageSwitcher.styles.ts +40 -0
  9. package/src/domains/localization/infrastructure/components/LanguageSwitcher.tsx +88 -0
  10. package/src/domains/localization/infrastructure/components/__tests__/LanguageSwitcher.test.tsx +91 -0
  11. package/src/domains/localization/infrastructure/components/useLanguageNavigation.ts +20 -0
  12. package/src/domains/localization/infrastructure/components/useLanguageSwitcher.ts +34 -0
  13. package/src/domains/localization/infrastructure/config/DeviceLocale.ts +47 -0
  14. package/src/domains/localization/infrastructure/config/I18nInitializer.ts +73 -0
  15. package/src/domains/localization/infrastructure/config/LanguageQuery.ts +35 -0
  16. package/src/domains/localization/infrastructure/config/LocaleMapping.ts +78 -0
  17. package/src/domains/localization/infrastructure/config/NamespaceResolver.ts +54 -0
  18. package/src/domains/localization/infrastructure/config/ResourceBuilder.ts +72 -0
  19. package/src/domains/localization/infrastructure/config/TranslationLoader.ts +46 -0
  20. package/src/domains/localization/infrastructure/config/__tests__/languagesData.test.ts +69 -0
  21. package/src/domains/localization/infrastructure/config/constants/defaultLanguages.ts +43 -0
  22. package/src/domains/localization/infrastructure/config/i18n.ts +9 -0
  23. package/src/domains/localization/infrastructure/config/languages.ts +28 -0
  24. package/src/domains/localization/infrastructure/config/languagesData.ts +26 -0
  25. package/src/domains/localization/infrastructure/hooks/TranslationHook.ts +39 -0
  26. package/src/domains/localization/infrastructure/hooks/__tests__/useTranslation.test.ts +52 -0
  27. package/src/domains/localization/infrastructure/hooks/useLanguageSelection.ts +44 -0
  28. package/src/domains/localization/infrastructure/hooks/useLocalization.ts +41 -0
  29. package/src/domains/localization/infrastructure/hooks/useTranslation.ts +94 -0
  30. package/src/domains/localization/infrastructure/repository/LanguageRepository.ts +53 -0
  31. package/src/domains/localization/infrastructure/storage/AsyncStorageWrapper.ts +24 -0
  32. package/src/domains/localization/infrastructure/storage/LanguageInitializer.ts +81 -0
  33. package/src/domains/localization/infrastructure/storage/LanguageSwitcher.ts +52 -0
  34. package/src/domains/localization/infrastructure/storage/LocalizationStore.ts +142 -0
  35. package/src/domains/localization/infrastructure/storage/types/Language.ts +13 -0
  36. package/src/domains/localization/infrastructure/storage/types/LocalizationState.ts +27 -0
  37. package/src/domains/localization/presentation/components/LanguageItem.styles.ts +40 -0
  38. package/src/domains/localization/presentation/components/LanguageItem.tsx +106 -0
  39. package/src/domains/localization/presentation/components/LanguageSection.tsx +83 -0
  40. package/src/domains/localization/presentation/components/__tests__/LanguageItem.test.tsx +106 -0
  41. package/src/domains/localization/presentation/screens/LanguageSelectionScreen.styles.ts +16 -0
  42. package/src/domains/localization/presentation/screens/LanguageSelectionScreen.tsx +132 -0
  43. package/src/domains/localization/presentation/screens/LanguageSelectionScreen.types.ts +27 -0
  44. package/src/domains/localization/presentation/screens/__tests__/LanguageSelectionScreen.test.tsx +165 -0
  45. package/src/domains/localization/scripts/prepublish.js +36 -0
  46. package/src/domains/localization/scripts/setup-languages.js +60 -0
  47. package/src/domains/localization/scripts/sync-translations.js +124 -0
  48. package/src/domains/localization/scripts/translate-missing.js +92 -0
  49. package/src/domains/localization/scripts/utils/file-parser.js +78 -0
  50. package/src/domains/localization/scripts/utils/key-detector.js +45 -0
  51. package/src/domains/localization/scripts/utils/key-extractor.js +105 -0
  52. package/src/domains/localization/scripts/utils/object-helper.js +29 -0
  53. package/src/domains/localization/scripts/utils/sync-helper.js +49 -0
  54. package/src/domains/localization/scripts/utils/translation-config.js +116 -0
  55. package/src/domains/localization/scripts/utils/translator.js +83 -0
  56. package/src/domains/notifications/presentation/components/NotificationsSection.tsx +1 -1
  57. package/src/domains/notifications/presentation/screens/NotificationSettingsScreen.tsx +1 -1
  58. package/src/index.ts +2 -0
  59. package/src/presentation/components/SettingsErrorBoundary.tsx +1 -1
  60. package/src/presentation/navigation/SettingsStackNavigator.tsx +1 -1
  61. package/src/presentation/screens/components/SettingsContent.tsx +1 -1
  62. package/src/presentation/screens/components/SettingsHeader.tsx +1 -1
  63. package/src/presentation/screens/components/sections/FeatureSettingsSection.tsx +1 -1
  64. package/src/presentation/screens/components/sections/IdentitySettingsSection.tsx +1 -1
  65. package/src/presentation/screens/components/sections/ProfileSectionLoader.tsx +1 -1
  66. 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,9 @@
1
+ /**
2
+ * i18n Instance
3
+ *
4
+ * Raw i18n instance - use I18nInitializer for configuration
5
+ */
6
+
7
+ import i18n from 'i18next';
8
+
9
+ export default i18n;
@@ -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
+ };