fix: keep the desktop menus and tray in the language the app shows

This commit is contained in:
Linus Rath
2026-11-04 05:20:02 +01:00
parent ff0e16c755
commit 674845df4e
2 changed files with 33 additions and 4 deletions
@@ -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(<DesktopHost />);
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(<DesktopHost />);
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(<DesktopHost />);
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' })}`;
+9 -4
View File
@@ -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<typeof shellLabels>[0]));
}, [uiLocale]);
}, [activeLocale, uiLocale, messages]);
// What the user writes in, whichever composer asks for it too.
useEffect(() => {