fix(i18n): localize shared MUI widget controls

This commit is contained in:
cesnimda
2026-11-11 13:21:57 +01:00
parent c172427fb2
commit cb9d039786
3 changed files with 41 additions and 3 deletions
@@ -85,6 +85,13 @@ try {
const titleId = await initialEdit.getAttribute('aria-labelledby');
assert.ok(titleId);
const edit = page.locator(`[role="dialog"][aria-labelledby="${titleId}"]`);
const assertSharedLabels = async lang => {
await expect(edit.locator('.MuiAutocomplete-popupIndicator')).toHaveAttribute('aria-label', lang === 'nb' ? 'Åpne' : 'Open');
await expect(edit.locator('.MuiAutocomplete-clearIndicator')).toHaveAttribute('aria-label', lang === 'nb' ? 'Tøm' : 'Clear');
await expect(page.locator('html')).toHaveAttribute('data-color-scheme', theme);
await expect(edit.getByRole('combobox', { name: translations[lang].company, exact: true })).toHaveValue('Synthetic school');
};
await assertSharedLabels(language);
if (flatEdit) {
await expect(edit.locator('.MuiDialogContent-root .MuiPaper-outlined')).toHaveCount(0);
for (const label of [copy.editJobApplicationDetails, copy.editJobStatusUpdate, copy.editJobRoleDetails, copy.editJobAttachmentsChecklist]) {
@@ -98,8 +105,10 @@ try {
await page.evaluate(value => { localStorage.setItem('uiLanguage', value); window.dispatchEvent(new StorageEvent('storage', { key: 'uiLanguage', newValue: value })); }, language === 'nb' ? 'en' : 'nb');
const otherCopy = translations[language === 'nb' ? 'en' : 'nb'];
await assertDay(edit.getByRole('group', { name: otherCopy.editJobDeadline, exact: true }), 23, language === 'nb' ? 'en' : 'nb');
await assertSharedLabels(language === 'nb' ? 'en' : 'nb');
await page.evaluate(value => { localStorage.setItem('uiLanguage', value); window.dispatchEvent(new StorageEvent('storage', { key: 'uiLanguage', newValue: value })); }, language);
await expect(edit.getByRole('textbox', { name: copy.editJobJobTitle, exact: true })).toHaveValue('Retain teacher draft');
await assertSharedLabels(language);
assert.equal(detailReads, reads); assert.equal(writes.length, 0);
for (const label of [copy.editJobAppliedOn, copy.editJobStatusChangedOn, copy.editJobReplyReceivedOn, copy.editJobFollowUpOn, copy.editJobDeadline]) await choose24(edit.getByRole('group', { name: label, exact: true }), label === copy.editJobDeadline);
if (flatEdit) await edit.locator('.MuiDialogContent-root').evaluate(node => { node.scrollTop = 0; });
@@ -1,4 +1,7 @@
import type { ReactNode } from "react";
import { useMemo, type ReactNode } from "react";
import { ThemeProvider, useTheme } from "@mui/material/styles";
import { nbNO as materialNbNO } from "@mui/material/locale";
import { deepmerge } from "@mui/utils";
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
import { nbNO } from "@mui/x-date-pickers/locales";
@@ -7,9 +10,14 @@ import { useI18n } from "./i18n/I18nProvider";
export default function DateLocalizationProvider({ children }: { children: ReactNode }) {
const { language } = useI18n();
return <LocalizationProvider
const theme = useTheme();
const localizedTheme = useMemo(() => ({
...theme,
components: deepmerge(theme.components ?? {}, language === "nb" ? materialNbNO.components : {}),
}), [theme, language]);
return <ThemeProvider theme={localizedTheme}><LocalizationProvider
dateAdapter={AdapterDateFns}
adapterLocale={language === "nb" ? nb : enUS}
localeText={language === "nb" ? nbNO.components.MuiLocalizationProvider.defaultProps.localeText : undefined}
>{children}</LocalizationProvider>;
>{children}</LocalizationProvider></ThemeProvider>;
}
@@ -2,6 +2,8 @@ import React, { useState } from "react";
import "@testing-library/jest-dom";
import { fireEvent, render, screen } from "@testing-library/react";
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
import { Autocomplete, TextField } from "@mui/material";
import JobTrackerThemeProvider from "./ThemeProvider";
import DateLocalizationProvider from "./DateLocalizationProvider";
import { I18nProvider, useI18n } from "./i18n/I18nProvider";
@@ -13,6 +15,7 @@ function Form() {
<button onClick={() => setLocalLanguage("nb")}>Bokmål</button>
<button onClick={() => setLocalLanguage("en")}>English</button>
<DatePicker label="Deadline" value={date} onChange={setDate} />
<Autocomplete options={["Teacher", "Engineer"]} defaultValue="Teacher" renderInput={params => <TextField {...params} label="Occupation" />} />
</>;
}
@@ -28,6 +31,24 @@ test("Bokmål localizes date sections, calendar controls and displayed format",
expect(screen.getByRole("group", { name: "Deadline" }).querySelector("input")).toHaveValue("23.11.2026");
});
test("shared widget controls follow live UI language without losing selection or dark theme", () => {
localStorage.setItem("uiLanguage", "nb");
render(<JobTrackerThemeProvider preference="dark" systemPrefersDark={false}><I18nProvider><DateLocalizationProvider><Form /></DateLocalizationProvider></I18nProvider></JobTrackerThemeProvider>);
expect(screen.getByRole("button", { name: "Åpne" })).toBeInTheDocument();
fireEvent.focus(screen.getByRole("combobox", { name: "Occupation" }));
expect(screen.getByTitle("Tøm")).toHaveAttribute("aria-label", "Tøm");
const occupation = screen.getByRole("combobox", { name: "Occupation" });
expect(occupation).toHaveValue("Teacher");
fireEvent.click(screen.getByRole("button", { name: "English" }));
expect(screen.getByRole("button", { name: "Open" })).toBeInTheDocument();
expect(screen.getByTitle("Clear")).toHaveAttribute("aria-label", "Clear");
expect(occupation).toHaveValue("Teacher");
fireEvent.click(screen.getByRole("button", { name: "Bokmål" }));
expect(screen.getByRole("button", { name: "Åpne" })).toBeInTheDocument();
expect(occupation).toHaveValue("Teacher");
expect(document.documentElement).toHaveAttribute("data-color-scheme", "dark");
});
test("live UI language changes retain the selected day and unsaved form draft", () => {
render(<I18nProvider><DateLocalizationProvider><Form /></DateLocalizationProvider></I18nProvider>);
const title = screen.getByRole("textbox", { name: "Unsaved title" });