From 80bb6686a98d9e05932e51e24a551987b0bd772c Mon Sep 17 00:00:00 2001 From: Deepak Bhagat Date: Sun, 9 Aug 2026 13:05:47 +0530 Subject: [PATCH 1/2] fix: apply language at runtime when changed in LanguageView LanguageView.changeLanguage saved the language to server preferences, Redux and the local database but never called setLanguage from app/i18n, so the i18n locale was not updated until a full app restart. Call setLanguage(language) after changeLanguage resolves so the selected language is applied immediately for non-RTL switches. Fixes #5823 Signed-off-by: Deepak Bhagat --- app/views/LanguageView/index.test.tsx | 91 +++++++++++++++++++++++++++ app/views/LanguageView/index.tsx | 4 +- 2 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 app/views/LanguageView/index.test.tsx diff --git a/app/views/LanguageView/index.test.tsx b/app/views/LanguageView/index.test.tsx new file mode 100644 index 00000000000..7ccab404c12 --- /dev/null +++ b/app/views/LanguageView/index.test.tsx @@ -0,0 +1,91 @@ +import React from 'react'; +import { render, fireEvent, waitFor } from '@testing-library/react-native'; + +import LanguageView from './index'; +import * as i18n from '../../i18n'; + +jest.mock('@react-navigation/native', () => ({ + useNavigation: () => ({ setOptions: jest.fn(), navigate: jest.fn() }) +})); + +jest.mock('../../lib/hooks/useAppSelector', () => ({ + useAppSelector: () => ({ languageDefault: 'en', id: 'user-id' }) +})); + +jest.mock('react-redux', () => ({ + useDispatch: () => jest.fn() +})); + +jest.mock('../../actions/app', () => ({ + appStart: jest.fn() +})); + +jest.mock('../../actions/login', () => ({ + setUser: jest.fn() +})); + +jest.mock('react-native-restart', () => ({ + Restart: { Restart: jest.fn() } +})); + +jest.mock('react-native', () => { + const rn = jest.requireActual('react-native'); + rn.FlatList = ({ data, renderItem }: any) => ( + <> + {data.map((item: any, index: number) => ( + {renderItem({ item, index })} + ))} + + ); + return rn; +}); + +jest.mock('../../lib/services/restApi', () => ({ + saveUserPreferences: jest.fn(() => Promise.resolve()) +})); + +jest.mock('../../lib/database', () => ({ + default: { + servers: { + get: jest.fn(() => ({ + write: jest.fn((fn: any) => fn()) + })), + find: jest.fn(() => Promise.resolve({ update: jest.fn() })) + } + } +})); + +jest.mock('../../i18n', () => { + const setLanguage = jest.fn(); + const i18nMock = { + t: (key: string) => key, + locale: 'en', + translations: { en: {} } + }; + return { + __esModule: true, + default: i18nMock, + setLanguage, + isRTL: jest.fn(() => false), + LANGUAGES: [ + { label: 'English', value: 'en', file: () => ({}) }, + { label: 'EspaƱol', value: 'es', file: () => ({}) } + ] + }; +}); + +describe('LanguageView language change', () => { + it('applies the selected language at runtime via setLanguage', async () => { + const { findByTestId } = render(); + + const spanishOption = await findByTestId('language-view-es', {}, { timeout: 5000 }); + fireEvent(spanishOption, 'onPress'); + + await waitFor( + () => { + expect((i18n as any).setLanguage).toHaveBeenCalledWith('es'); + }, + { timeout: 5000 } + ); + }); +}); diff --git a/app/views/LanguageView/index.tsx b/app/views/LanguageView/index.tsx index 8aec1f66cb4..d0635e5a4ca 100644 --- a/app/views/LanguageView/index.tsx +++ b/app/views/LanguageView/index.tsx @@ -12,7 +12,7 @@ import { setUser } from '../../actions/login'; import * as List from '../../containers/List'; import SafeAreaView from '../../containers/SafeAreaView'; import { RootEnum } from '../../definitions'; -import I18n, { isRTL, LANGUAGES } from '../../i18n'; +import I18n, { isRTL, LANGUAGES, setLanguage } from '../../i18n'; import database from '../../lib/database'; import { getUserSelector } from '../../selectors/login'; import { type SettingsStackParamList } from '../../stacks/types'; @@ -48,6 +48,8 @@ const LanguageView = () => { // shows loading for at least 300ms await Promise.all([changeLanguage(language), new Promise(resolve => setTimeout(resolve, 300))]); + setLanguage(language); + if (shouldRestart) { await RNRestart.Restart(); } else { From 1685e14f303f2c3ea8923b47ef85d5256e52c211 Mon Sep 17 00:00:00 2001 From: Deepak Bhagat Date: Sun, 9 Aug 2026 16:02:48 +0530 Subject: [PATCH 2/2] fix: only apply language at runtime after successful preference save Address CodeRabbit review on #7558: - changeLanguage now returns a boolean and rethrows persistence failures instead of resolving silently, so setLanguage is only invoked when the server preference and local record were saved successfully. - fix the database mock so servers.write is defined directly (matching the call made by changeLanguage) so persistence succeeds in the test. Signed-off-by: Deepak Bhagat --- app/views/LanguageView/index.test.tsx | 6 +++--- app/views/LanguageView/index.tsx | 23 ++++++++++++----------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/app/views/LanguageView/index.test.tsx b/app/views/LanguageView/index.test.tsx index 7ccab404c12..ccb5d9c1c91 100644 --- a/app/views/LanguageView/index.test.tsx +++ b/app/views/LanguageView/index.test.tsx @@ -47,10 +47,10 @@ jest.mock('../../lib/services/restApi', () => ({ jest.mock('../../lib/database', () => ({ default: { servers: { + write: jest.fn((fn: any) => fn()), get: jest.fn(() => ({ - write: jest.fn((fn: any) => fn()) - })), - find: jest.fn(() => Promise.resolve({ update: jest.fn() })) + find: jest.fn(() => Promise.resolve({ update: jest.fn() })) + })) } } })); diff --git a/app/views/LanguageView/index.tsx b/app/views/LanguageView/index.tsx index d0635e5a4ca..850e53f6ff0 100644 --- a/app/views/LanguageView/index.tsx +++ b/app/views/LanguageView/index.tsx @@ -46,9 +46,11 @@ const LanguageView = () => { dispatch(appStart({ root: RootEnum.ROOT_LOADING, text: I18n.t('Change_language_loading') })); // shows loading for at least 300ms - await Promise.all([changeLanguage(language), new Promise(resolve => setTimeout(resolve, 300))]); + const [changed] = await Promise.all([changeLanguage(language), new Promise(resolve => setTimeout(resolve, 300))]); - setLanguage(language); + if (changed) { + setLanguage(language); + } if (shouldRestart) { await RNRestart.Restart(); @@ -57,7 +59,7 @@ const LanguageView = () => { } }; - const changeLanguage = async (language: string) => { + const changeLanguage = async (language: string): Promise => { logEvent(events.LANG_SET_LANGUAGE); const params: { language?: string } = {}; @@ -74,19 +76,18 @@ const LanguageView = () => { const serversDB = database.servers; const usersCollection = serversDB.get('users'); await serversDB.write(async () => { - try { - const userRecord = await usersCollection.find(id); - await userRecord.update(record => { - record.language = params.language; - }); - } catch (e) { - logEvent(events.LANG_SET_LANGUAGE_F); - } + const userRecord = await usersCollection.find(id); + await userRecord.update(record => { + record.language = params.language; + }); }); + + return true; } catch (e) { logEvent(events.LANG_SET_LANGUAGE_F); showErrorAlert(I18n.t('There_was_an_error_while_action', { action: I18n.t('saving_preferences') })); log(e); + return false; } };