diff --git a/components/desktop/__tests__/desktop-host.test.tsx b/components/desktop/__tests__/desktop-host.test.tsx
index 61f8a3b8d..2d757cbf2 100644
--- a/components/desktop/__tests__/desktop-host.test.tsx
+++ b/components/desktop/__tests__/desktop-host.test.tsx
@@ -14,6 +14,14 @@ import { offerReturnedComposer, takeReturnedComposer } from '@/lib/popout/return
const push = vi.fn();
vi.mock('@/i18n/navigation', () => ({ useRouter: () => ({ push }) }));
+// The catalog on screen: its language, and text that says which one it came from.
+const intl = vi.hoisted(() => ({ locale: 'en', prefix: '', messages: {} as object }));
+vi.mock('next-intl', () => ({
+ useTranslations: () => (key: string) => `${intl.prefix}${key}`,
+ useLocale: () => intl.locale,
+ useMessages: () => intl.messages,
+}));
+
const canPrint = vi.fn(() => true);
const canDownload = vi.fn(() => true);
vi.mock('@/lib/device-policy', () => ({ canPrintAnywhere: () => canPrint(), canDownloadAnywhere: () => canDownload() }));
@@ -68,6 +76,7 @@ function fakeBridge() {
const original = useAuthStore.getState();
beforeEach(() => {
+ Object.assign(intl, { locale: 'en', prefix: '', messages: {} });
push.mockClear();
resolveExternalLink.mockClear();
resolveExternalLink.mockImplementation(async (ctx) => ctx.href);
@@ -170,11 +179,26 @@ describe('DesktopHost', () => {
it('says which languages are in use', async () => {
const shell = fakeBridge();
act(() => useLocaleStore.setState({ locale: 'de' }));
+ intl.locale = 'de';
render();
await waitFor(() => expect(shell.setLanguages).toHaveBeenCalled());
expect(shell.setLanguages.mock.lastCall![0]).toMatchObject({ ui: 'de' });
});
+ it('follows the text on screen, not only the stored choice', async () => {
+ const shell = fakeBridge();
+ // English chosen, but the first render still has the catalog the server picked from the OS.
+ act(() => useLocaleStore.setState({ locale: 'en' }));
+ Object.assign(intl, { locale: 'de', prefix: 'de:', messages: { de: true } });
+ const view = render();
+ await waitFor(() => expect(shell.setLanguages).toHaveBeenCalled());
+ // The chosen catalog lands a render later.
+ Object.assign(intl, { locale: 'en', prefix: 'en:', messages: { en: true } });
+ view.rerender();
+ await waitFor(() => expect(shell.setLanguages.mock.lastCall![0].labels.menuFile).toBe('en:desktopUi.menu.file'));
+ expect(shell.setLanguages.mock.lastCall![0]).toMatchObject({ ui: 'en' });
+ });
+
describe('Open Link in a native menu', () => {
const openLink = (url: string) => `link.open:${JSON.stringify({ url, text: 'click' })}`;
diff --git a/components/desktop/desktop-host.tsx b/components/desktop/desktop-host.tsx
index 909ca657b..b80396b80 100644
--- a/components/desktop/desktop-host.tsx
+++ b/components/desktop/desktop-host.tsx
@@ -1,7 +1,7 @@
"use client";
import { useEffect, useRef } from "react";
-import { useTranslations } from "next-intl";
+import { useLocale, useMessages, useTranslations } from "next-intl";
import { useStrictestDevices } from "@/hooks/use-device-policy";
import { useRouter } from "@/i18n/navigation";
import { openKeyboardShortcuts, parkAppCommand, requestAppCommand, type AppCommand } from "@/lib/app-commands";
@@ -45,6 +45,8 @@ export function DesktopHost() {
const singleKeys = useSettingsStore((s) => s.singleKeyShortcuts);
const writingLanguage = useSettingsStore((s) => s.writingLanguage);
const uiLocale = useLocaleStore((s) => s.locale);
+ const activeLocale = useLocale();
+ const messages = useMessages();
const strictest = useStrictestDevices();
const { restoreWindows } = useDesktopPrefs();
const theme = useThemeStore((s) => s.theme);
@@ -116,11 +118,14 @@ export function DesktopHost() {
});
}, [isAuthenticated, singleKeys, strictest]);
- // Menus and spellcheck follow the interface.
+ // Menus, the tray and spellcheck follow the interface: the language its text is in now,
+ // with that text. The stored choice isn't it: on the first render the catalog can still
+ // be the one the server picked from the OS language, and the chosen one lands a render
+ // later. Going by the choice alone left the menus in the OS language for good.
useEffect(() => {
- const ui = uiLocale || document.documentElement.lang;
+ const ui = activeLocale || uiLocale || document.documentElement.lang;
if (ui) setDesktopInterfaceLanguage(ui, shellLabels(tRef.current as unknown as Parameters[0]));
- }, [uiLocale]);
+ }, [activeLocale, uiLocale, messages]);
// What the user writes in, whichever composer asks for it too.
useEffect(() => {