From 0264ffbc9703f6c0fcf755a099b9002b2e0ff9e7 Mon Sep 17 00:00:00 2001 From: Ramon Candel Date: Fri, 6 Feb 2026 11:23:50 +0100 Subject: [PATCH 1/2] Fixed language refresh and added device language detection --- assets/lang/strings.ts | 4 ++-- src/components/BottomTabNavigator/index.tsx | 18 ++++++++------- src/hooks/useLanguage.ts | 5 ++++ src/screens/HomeScreen/index.tsx | 2 ++ src/screens/SettingsScreen/index.tsx | 2 ++ .../DriveFolderScreen/DriveFolderScreen.tsx | 2 ++ .../drive/SharedScreen/SharedScreen.tsx | 2 ++ src/services/LanguageService.ts | 16 +++++++++---- src/store/slices/app/index.ts | 23 +++++++++++++++---- 9 files changed, 54 insertions(+), 20 deletions(-) create mode 100644 src/hooks/useLanguage.ts diff --git a/assets/lang/strings.ts b/assets/lang/strings.ts index 2f9e1ff0c..3ca3cad5c 100644 --- a/assets/lang/strings.ts +++ b/assets/lang/strings.ts @@ -554,7 +554,7 @@ const translations = { }, Language: { title: 'Select language', - info: 'Restart the application to see the language change.', + info: 'Language changed successfully.', }, ChangeProfilePicture: { title: 'Edit photo', @@ -1338,7 +1338,7 @@ const translations = { }, Language: { title: 'Selecciona idioma', - info: 'Reinicie la aplicación para ver el cambio de idioma.', + info: 'Idioma cambiado correctamente.', }, ChangeProfilePicture: { title: 'Editar foto', diff --git a/src/components/BottomTabNavigator/index.tsx b/src/components/BottomTabNavigator/index.tsx index eb5d7eb57..884380c4e 100644 --- a/src/components/BottomTabNavigator/index.tsx +++ b/src/components/BottomTabNavigator/index.tsx @@ -6,22 +6,24 @@ import { storageThunks } from 'src/store/slices/storage'; import { useTailwind } from 'tailwind-rn'; import strings from '../../../assets/lang/strings'; import useGetColor from '../../hooks/useColor'; +import { useLanguage } from '../../hooks/useLanguage'; import { useAppDispatch } from '../../store/hooks'; import { uiActions } from '../../store/slices/ui'; import globalStyle from '../../styles/global'; -const tabs = { - Home: { label: strings.tabs.Home, icon: House }, - Drive: { label: strings.tabs.Drive, icon: FolderSimple }, - Add: { label: strings.tabs.Add, icon: PlusCircle }, - Shared: { label: strings.tabs.Shared, icon: Users }, - Settings: { label: strings.tabs.Settings, icon: Gear }, -}; - function BottomTabNavigator(props: BottomTabBarProps): JSX.Element { const tailwind = useTailwind(); const getColor = useGetColor(); const dispatch = useAppDispatch(); + useLanguage(); + + const tabs = { + Home: { label: strings.tabs.Home, icon: House }, + Drive: { label: strings.tabs.Drive, icon: FolderSimple }, + Add: { label: strings.tabs.Add, icon: PlusCircle }, + Shared: { label: strings.tabs.Shared, icon: Users }, + Settings: { label: strings.tabs.Settings, icon: Gear }, + }; const items = props.state.routes .filter((route) => Object.keys(tabs).includes(route.name)) diff --git a/src/hooks/useLanguage.ts b/src/hooks/useLanguage.ts new file mode 100644 index 000000000..bbbf535c2 --- /dev/null +++ b/src/hooks/useLanguage.ts @@ -0,0 +1,5 @@ +import { useAppSelector } from '../store/hooks'; + +export const useLanguage = () => { + return useAppSelector((state) => state.app.language); +}; diff --git a/src/screens/HomeScreen/index.tsx b/src/screens/HomeScreen/index.tsx index 79f49edfc..f2996b78a 100644 --- a/src/screens/HomeScreen/index.tsx +++ b/src/screens/HomeScreen/index.tsx @@ -10,6 +10,7 @@ import { useTailwind } from 'tailwind-rn'; import { useUseCase } from '@internxt-mobile/hooks/common'; import * as useCases from '@internxt-mobile/useCases/drive'; import { SearchInput } from 'src/components/SearchInput'; +import { useLanguage } from '../../hooks/useLanguage'; enum HomeTab { Recents = 'recents', @@ -17,6 +18,7 @@ enum HomeTab { const HomeScreen = (): JSX.Element => { const tailwind = useTailwind(); + useLanguage(); const [searchText, setSearchText] = useState(''); const { diff --git a/src/screens/SettingsScreen/index.tsx b/src/screens/SettingsScreen/index.tsx index cad1d292d..a3aaf40b2 100644 --- a/src/screens/SettingsScreen/index.tsx +++ b/src/screens/SettingsScreen/index.tsx @@ -25,6 +25,7 @@ import AppVersionWidget from '../../components/AppVersionWidget'; import SettingsGroup from '../../components/SettingsGroup'; import UserProfilePicture from '../../components/UserProfilePicture'; import useGetColor from '../../hooks/useColor'; +import { useLanguage } from '../../hooks/useLanguage'; import { useScreenProtection } from '../../hooks/useScreenProtection'; import appService from '../../services/AppService'; import { useAppDispatch, useAppSelector } from '../../store/hooks'; @@ -50,6 +51,7 @@ function SettingsScreen({ navigation }: SettingsScreenProps<'SettingsHome'>): JS const isDarkMode = theme === 'dark'; const { isEnabled: isScreenProtectionEnabled, setScreenProtection } = useScreenProtection(); + useLanguage(); const showBilling = useAppSelector(paymentsSelectors.shouldShowBilling); const { user } = useAppSelector((state) => state.auth); const usagePercent = useAppSelector(storageSelectors.usagePercent); diff --git a/src/screens/drive/DriveFolderScreen/DriveFolderScreen.tsx b/src/screens/drive/DriveFolderScreen/DriveFolderScreen.tsx index f1cf903be..89bdfe9ba 100644 --- a/src/screens/drive/DriveFolderScreen/DriveFolderScreen.tsx +++ b/src/screens/drive/DriveFolderScreen/DriveFolderScreen.tsx @@ -20,12 +20,14 @@ import { NotificationType } from '../../../types'; import { DriveItemStatus, DriveListItem } from '../../../types/drive/item'; import { DriveListType, SortDirection, SortType } from '../../../types/drive/ui'; import { DriveScreenProps, DriveStackParamList } from '../../../types/navigation'; +import { useLanguage } from '../../../hooks/useLanguage'; import { DriveFolderEmpty } from './DriveFolderEmpty'; import { DriveFolderError } from './DriveFolderError'; import { DriveFolderScreenHeader } from './DriveFolderScreenHeader'; export function DriveFolderScreen({ navigation }: DriveScreenProps<'DriveFolder'>): JSX.Element { const route = useRoute>(); + useLanguage(); const [loadingMore, setLoadingMore] = useState(false); const { isRootFolder, folderUuid, folderName, parentFolderName, parentUuid } = route.params; diff --git a/src/screens/drive/SharedScreen/SharedScreen.tsx b/src/screens/drive/SharedScreen/SharedScreen.tsx index f497f2ea0..da0166eee 100644 --- a/src/screens/drive/SharedScreen/SharedScreen.tsx +++ b/src/screens/drive/SharedScreen/SharedScreen.tsx @@ -18,12 +18,14 @@ import DriveItem from '../../../components/drive/lists/items'; import DriveItemSkinSkeleton from '../../../components/DriveItemSkinSkeleton'; import EmptyList from '../../../components/EmptyList'; import useGetColor from '../../../hooks/useColor'; +import { useLanguage } from '../../../hooks/useLanguage'; import { DriveItemStatus } from '../../../types/drive/item'; type SharedItem = SharedFolders & SharedFiles; export const SharedScreen: React.FC> = (props) => { const tailwind = useTailwind(); const getColor = useGetColor(); + useLanguage(); const { loading: sharedLoading, executeUseCase: getSharedItems } = useUseCase(driveUseCases.getSharedItems); const [sharedItemsPage, setSharedItemsPage] = useState(1); diff --git a/src/services/LanguageService.ts b/src/services/LanguageService.ts index a0f52e882..e5219b391 100644 --- a/src/services/LanguageService.ts +++ b/src/services/LanguageService.ts @@ -1,5 +1,6 @@ import strings from 'assets/lang/strings'; +import * as Localization from 'expo-localization'; import { Settings } from 'luxon'; import { AsyncStorageKey, Language, NotificationType } from 'src/types'; import asyncStorageService from './AsyncStorageService'; @@ -9,11 +10,17 @@ class LanguageService { this.initialize(); } private async initialize() { - const language = await asyncStorageService.getItem(AsyncStorageKey.Language); + const savedLanguage = await asyncStorageService.getItem(AsyncStorageKey.Language); - Settings.defaultLocale = language ?? strings.getLanguage(); - - language && strings.setLanguage(language); + if (savedLanguage) { + strings.setLanguage(savedLanguage); + Settings.defaultLocale = savedLanguage; + } else { + const deviceLocale = Localization.getLocales()[0]?.languageCode; + const detectedLanguage = deviceLocale === 'es' ? Language.Spanish : Language.English; + strings.setLanguage(detectedLanguage); + Settings.defaultLocale = detectedLanguage; + } } public async setLanguage(language: Language) { @@ -22,7 +29,6 @@ class LanguageService { Settings.defaultLocale = language ?? strings.getLanguage(); notificationsService.show({ text1: strings.modals.Language.info, type: NotificationType.Info }); - // TODO: ADD WAY TO RESTART THE LANGUAGE IN RUNTIME WHEN IT CHANGES } } diff --git a/src/store/slices/app/index.ts b/src/store/slices/app/index.ts index 96c76520d..06e03abfd 100644 --- a/src/store/slices/app/index.ts +++ b/src/store/slices/app/index.ts @@ -5,6 +5,7 @@ import drive from '@internxt-mobile/services/drive'; import { BiometricAccessType } from '@internxt-mobile/types/app'; import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'; import strings from 'assets/lang/strings'; +import * as Localization from 'expo-localization'; import languageService from 'src/services/LanguageService'; import notificationsService from 'src/services/NotificationsService'; import { Language, NotificationType } from 'src/types'; @@ -22,8 +23,14 @@ export interface AppState { screenLocked: boolean; lastScreenLock: number | null; initialScreenLocked: boolean; + language: Language; } +const getInitialLanguage = (): Language => { + const deviceLocale = Localization.getLocales()[0]?.languageCode; + return deviceLocale === 'es' ? Language.Spanish : Language.English; +}; + const initialState: AppState = { isInitializing: true, deviceHasBiometricAccess: false, @@ -32,6 +39,7 @@ const initialState: AppState = { screenLocked: false, lastScreenLock: null, initialScreenLocked: false, + language: getInitialLanguage(), }; const initializeThunk = createAsyncThunk( @@ -46,10 +54,11 @@ const initializeThunk = createAsyncThunk( }, ); -const changeLanguageThunk = createAsyncThunk( +const changeLanguageThunk = createAsyncThunk( 'app/changeLanguage', async (language) => { - return languageService.setLanguage(language); + await languageService.setLanguage(language); + return language; }, ); @@ -125,9 +134,13 @@ export const appSlice = createSlice({ state.isInitializing = false; }); - builder.addCase(changeLanguageThunk.rejected, () => { - notificationsService.show({ type: NotificationType.Error, text1: strings.errors.changeLanguage }); - }); + builder + .addCase(changeLanguageThunk.fulfilled, (state, action) => { + state.language = action.payload; + }) + .addCase(changeLanguageThunk.rejected, () => { + notificationsService.show({ type: NotificationType.Error, text1: strings.errors.changeLanguage }); + }); }, }); From 98983ffc0ae72e3c7b07e8c443c2fcc79d4c1650 Mon Sep 17 00:00:00 2001 From: Ramon Candel Date: Fri, 6 Feb 2026 12:41:41 +0100 Subject: [PATCH 2/2] Fixed language change race condition, added async storage language key to async storage clean function and execute initialize language thunk when open app --- __mocks__/react-native-localization.ts | 22 ----------------- src/App.tsx | 2 ++ src/hooks/useLanguage.ts | 3 ++- src/screens/SettingsScreen/index.tsx | 1 + src/screens/SignInScreen/index.tsx | 5 ++-- src/services/AsyncStorageService.ts | 1 + src/services/LanguageService.ts | 20 +-------------- src/store/slices/app/index.ts | 34 +++++++++++++++++++++----- 8 files changed, 38 insertions(+), 50 deletions(-) delete mode 100644 __mocks__/react-native-localization.ts diff --git a/__mocks__/react-native-localization.ts b/__mocks__/react-native-localization.ts deleted file mode 100644 index f1b3f767e..000000000 --- a/__mocks__/react-native-localization.ts +++ /dev/null @@ -1,22 +0,0 @@ -export default class mockRNLocalization { - _language = 'en'; - private props: Record = {}; - constructor(props) { - this.props = props; - this._setLanguage(this._language); - } - - _setLanguage(interfaceLanguage) { - this._language = interfaceLanguage; - if (this.props[interfaceLanguage]) { - const localizedStrings: Record = this.props[this._language]; - for (const key in localizedStrings) { - if (localizedStrings[key]) { - this[key] = localizedStrings[key]; - } - } - } - } -} - -jest.mock('react-native-localization', () => mockRNLocalization); diff --git a/src/App.tsx b/src/App.tsx index b8f16dbb7..f0bbb4214 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -145,6 +145,8 @@ function AppContent(): JSX.Element { try { logger.info(`--- Starting new app session at ${time.getFormattedDate(new Date(), 'dd/LL/yyyy - HH:mm')} ---`); + await dispatch(appThunks.initializeLanguageThunk()).unwrap(); + // 1. Get remote updates await getRemoteUpdateIfAvailable(); diff --git a/src/hooks/useLanguage.ts b/src/hooks/useLanguage.ts index bbbf535c2..9b2a80178 100644 --- a/src/hooks/useLanguage.ts +++ b/src/hooks/useLanguage.ts @@ -1,5 +1,6 @@ import { useAppSelector } from '../store/hooks'; export const useLanguage = () => { - return useAppSelector((state) => state.app.language); + const language = useAppSelector((state) => state.app.language); + return language; }; diff --git a/src/screens/SettingsScreen/index.tsx b/src/screens/SettingsScreen/index.tsx index a3aaf40b2..5247e4fe2 100644 --- a/src/screens/SettingsScreen/index.tsx +++ b/src/screens/SettingsScreen/index.tsx @@ -52,6 +52,7 @@ function SettingsScreen({ navigation }: SettingsScreenProps<'SettingsHome'>): JS const { isEnabled: isScreenProtectionEnabled, setScreenProtection } = useScreenProtection(); useLanguage(); + const showBilling = useAppSelector(paymentsSelectors.shouldShowBilling); const { user } = useAppSelector((state) => state.auth); const usagePercent = useAppSelector(storageSelectors.usagePercent); diff --git a/src/screens/SignInScreen/index.tsx b/src/screens/SignInScreen/index.tsx index e74e7841f..ac2e6ef59 100644 --- a/src/screens/SignInScreen/index.tsx +++ b/src/screens/SignInScreen/index.tsx @@ -13,19 +13,20 @@ import AppScreen from '../../components/AppScreen'; import AppVersionWidget from '../../components/AppVersionWidget'; import { useTheme } from '../../contexts/Theme/Theme.context'; import useGetColor from '../../hooks/useColor'; +import { useLanguage } from '../../hooks/useLanguage'; import analytics, { AnalyticsEventKey } from '../../services/AnalyticsService'; import appService from '../../services/AppService'; import { logger } from '../../services/common'; import errorService from '../../services/ErrorService'; import notificationsService from '../../services/NotificationsService'; import { NotificationType } from '../../types'; -import { RootStackScreenProps } from '../../types/navigation'; -function SignInScreen({ navigation }: RootStackScreenProps<'SignIn'>): JSX.Element { +function SignInScreen(): JSX.Element { const tailwind = useTailwind(); const getColor = useGetColor(); const { theme } = useTheme(); const isDark = theme === 'dark'; + useLanguage(); const [error, setError] = useState(''); const dimensions = Dimensions.get('screen'); diff --git a/src/services/AsyncStorageService.ts b/src/services/AsyncStorageService.ts index 21023eb7d..1883ba9b1 100644 --- a/src/services/AsyncStorageService.ts +++ b/src/services/AsyncStorageService.ts @@ -119,6 +119,7 @@ class AsyncStorageService { AsyncStorageKey.ScreenLockIsEnabled, AsyncStorageKey.LastScreenLock, AsyncStorageKey.ThemePreference, + AsyncStorageKey.Language, ]; await AsyncStorage.multiRemove(nonSensitiveKeys); diff --git a/src/services/LanguageService.ts b/src/services/LanguageService.ts index e5219b391..bbc27fd9f 100644 --- a/src/services/LanguageService.ts +++ b/src/services/LanguageService.ts @@ -1,32 +1,14 @@ import strings from 'assets/lang/strings'; -import * as Localization from 'expo-localization'; import { Settings } from 'luxon'; import { AsyncStorageKey, Language, NotificationType } from 'src/types'; import asyncStorageService from './AsyncStorageService'; import notificationsService from './NotificationsService'; -class LanguageService { - constructor() { - this.initialize(); - } - private async initialize() { - const savedLanguage = await asyncStorageService.getItem(AsyncStorageKey.Language); - - if (savedLanguage) { - strings.setLanguage(savedLanguage); - Settings.defaultLocale = savedLanguage; - } else { - const deviceLocale = Localization.getLocales()[0]?.languageCode; - const detectedLanguage = deviceLocale === 'es' ? Language.Spanish : Language.English; - strings.setLanguage(detectedLanguage); - Settings.defaultLocale = detectedLanguage; - } - } +class LanguageService { public async setLanguage(language: Language) { await asyncStorageService.saveItem(AsyncStorageKey.Language, language); strings.setLanguage(language); - Settings.defaultLocale = language ?? strings.getLanguage(); notificationsService.show({ text1: strings.modals.Language.info, type: NotificationType.Info }); } diff --git a/src/store/slices/app/index.ts b/src/store/slices/app/index.ts index 06e03abfd..10b75df52 100644 --- a/src/store/slices/app/index.ts +++ b/src/store/slices/app/index.ts @@ -26,11 +26,6 @@ export interface AppState { language: Language; } -const getInitialLanguage = (): Language => { - const deviceLocale = Localization.getLocales()[0]?.languageCode; - return deviceLocale === 'es' ? Language.Spanish : Language.English; -}; - const initialState: AppState = { isInitializing: true, deviceHasBiometricAccess: false, @@ -39,14 +34,34 @@ const initialState: AppState = { screenLocked: false, lastScreenLock: null, initialScreenLocked: false, - language: getInitialLanguage(), + language: Language.English, }; +const initializeLanguageThunk = createAsyncThunk( + 'app/initializeLanguage', + async () => { + const savedLanguage = await asyncStorageService.getItem('language' as any); + + if (savedLanguage) { + strings.setLanguage(savedLanguage); + return savedLanguage as Language; + } else { + const deviceLocale = Localization.getLocales()[0]?.languageCode; + const detectedLanguage = deviceLocale === 'es' ? Language.Spanish : Language.English; + strings.setLanguage(detectedLanguage); + await asyncStorageService.saveItem('language' as any, detectedLanguage); + return detectedLanguage; + } + }, +); + const initializeThunk = createAsyncThunk( 'app/initialize', async (_, { dispatch }) => { await drive.start(); + await dispatch(initializeLanguageThunk()).unwrap(); + dispatch(authThunks.initializeThunk()); dispatch(driveThunks.initializeThunk()); dispatch(paymentsThunks.initializeThunk()); @@ -57,7 +72,9 @@ const initializeThunk = createAsyncThunk( const changeLanguageThunk = createAsyncThunk( 'app/changeLanguage', async (language) => { + console.log('[changeLanguageThunk] Starting with language:', language); await languageService.setLanguage(language); + console.log('[changeLanguageThunk] Completed, returning:', language); return language; }, ); @@ -134,6 +151,10 @@ export const appSlice = createSlice({ state.isInitializing = false; }); + builder.addCase(initializeLanguageThunk.fulfilled, (state, action) => { + state.language = action.payload; + }); + builder .addCase(changeLanguageThunk.fulfilled, (state, action) => { state.language = action.payload; @@ -148,6 +169,7 @@ export const appActions = appSlice.actions; export const appThunks = { initializeThunk, + initializeLanguageThunk, changeLanguageThunk, initializeUserPreferencesThunk, lockScreenIfNeededThunk,