diff --git a/packages/app/src/renderer/features/settings/SettingsPage.test.tsx b/packages/app/src/renderer/features/settings/SettingsPage.test.tsx index 93cd4d52e5..c62f531d41 100644 --- a/packages/app/src/renderer/features/settings/SettingsPage.test.tsx +++ b/packages/app/src/renderer/features/settings/SettingsPage.test.tsx @@ -66,6 +66,7 @@ vi.mock('../../shell/theme-snapshot', () => ({ currentThemeSnapshot: () => ({ snapshot: true }), })); +import { toastQueue } from '../../../ui'; import { openSettingsSection } from './sections'; import { SettingsPage } from './SettingsPage'; @@ -269,6 +270,23 @@ describe('General settings', () => { expect(mocks.showToast).toHaveBeenCalledTimes(1); }); + it('replaces the language toast instead of stacking another', async () => { + const close = vi.spyOn(toastQueue, 'close'); + mocks.showToast.mockReturnValueOnce('de-toast').mockReturnValueOnce('en-toast'); + render(); + pickLanguage(); + await act(async () => {}); + act(() => mocks.i18n.switchTo('de')); + fireEvent.click(screen.getByRole('button', { name: /^(?!reset).*locale\.title/ })); + fireEvent.click(screen.getByRole('option', { name: 'English' })); + await act(async () => {}); + act(() => mocks.i18n.switchTo('en')); + expect(mocks.showToast).toHaveBeenCalledTimes(2); + expect(close).toHaveBeenCalledWith('de-toast'); + expect(close).not.toHaveBeenCalledWith('en-toast'); + close.mockRestore(); + }); + it('does not offer the relaunch when main rejects the language', async () => { mocks.settingsApi.SetSetting.mockRejectedValue(new Error('nope')); render(); diff --git a/packages/app/src/renderer/features/settings/SettingsPage.tsx b/packages/app/src/renderer/features/settings/SettingsPage.tsx index aaba0725f7..5b294a5a49 100644 --- a/packages/app/src/renderer/features/settings/SettingsPage.tsx +++ b/packages/app/src/renderer/features/settings/SettingsPage.tsx @@ -26,6 +26,7 @@ import { showToast, SideNav, TextField, + toastQueue, type IconName, type SelectOption, } from '../../../ui'; @@ -227,6 +228,8 @@ function languageName(code: string): string { /** The listener of the last language change that is waiting for its toast, so a choice that switches nothing can't leave one behind. */ let stopWaitingForLanguage: (() => void) | undefined; +/** The language toast showing, so the next language change replaces it instead of stacking another. */ +let languageToast: string | undefined; function GeneralSection() { const { t, i18n } = useTranslation('settings'); @@ -254,7 +257,8 @@ function GeneralSection() { stop(); // The editor's own strings and Chromium's follow after a relaunch. const tNew = i18n.getFixedT(language, 'settings'); - showToast({ + if (languageToast) toastQueue.close(languageToast); + languageToast = showToast({ title: tNew('locale.relaunchTitle'), description: tNew('locale.relaunchDescription'), actionLabel: tNew('locale.relaunch'),