fix(i18n): localize shared MUI widget controls
This commit is contained in:
@@ -85,6 +85,13 @@ try {
|
|||||||
const titleId = await initialEdit.getAttribute('aria-labelledby');
|
const titleId = await initialEdit.getAttribute('aria-labelledby');
|
||||||
assert.ok(titleId);
|
assert.ok(titleId);
|
||||||
const edit = page.locator(`[role="dialog"][aria-labelledby="${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) {
|
if (flatEdit) {
|
||||||
await expect(edit.locator('.MuiDialogContent-root .MuiPaper-outlined')).toHaveCount(0);
|
await expect(edit.locator('.MuiDialogContent-root .MuiPaper-outlined')).toHaveCount(0);
|
||||||
for (const label of [copy.editJobApplicationDetails, copy.editJobStatusUpdate, copy.editJobRoleDetails, copy.editJobAttachmentsChecklist]) {
|
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');
|
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'];
|
const otherCopy = translations[language === 'nb' ? 'en' : 'nb'];
|
||||||
await assertDay(edit.getByRole('group', { name: otherCopy.editJobDeadline, exact: true }), 23, 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 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 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);
|
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);
|
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; });
|
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 { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
|
||||||
import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
|
import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
|
||||||
import { nbNO } from "@mui/x-date-pickers/locales";
|
import { nbNO } from "@mui/x-date-pickers/locales";
|
||||||
@@ -7,9 +10,14 @@ import { useI18n } from "./i18n/I18nProvider";
|
|||||||
|
|
||||||
export default function DateLocalizationProvider({ children }: { children: ReactNode }) {
|
export default function DateLocalizationProvider({ children }: { children: ReactNode }) {
|
||||||
const { language } = useI18n();
|
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}
|
dateAdapter={AdapterDateFns}
|
||||||
adapterLocale={language === "nb" ? nb : enUS}
|
adapterLocale={language === "nb" ? nb : enUS}
|
||||||
localeText={language === "nb" ? nbNO.components.MuiLocalizationProvider.defaultProps.localeText : undefined}
|
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 "@testing-library/jest-dom";
|
||||||
import { fireEvent, render, screen } from "@testing-library/react";
|
import { fireEvent, render, screen } from "@testing-library/react";
|
||||||
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
|
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
|
||||||
|
import { Autocomplete, TextField } from "@mui/material";
|
||||||
|
import JobTrackerThemeProvider from "./ThemeProvider";
|
||||||
import DateLocalizationProvider from "./DateLocalizationProvider";
|
import DateLocalizationProvider from "./DateLocalizationProvider";
|
||||||
import { I18nProvider, useI18n } from "./i18n/I18nProvider";
|
import { I18nProvider, useI18n } from "./i18n/I18nProvider";
|
||||||
|
|
||||||
@@ -13,6 +15,7 @@ function Form() {
|
|||||||
<button onClick={() => setLocalLanguage("nb")}>Bokmål</button>
|
<button onClick={() => setLocalLanguage("nb")}>Bokmål</button>
|
||||||
<button onClick={() => setLocalLanguage("en")}>English</button>
|
<button onClick={() => setLocalLanguage("en")}>English</button>
|
||||||
<DatePicker label="Deadline" value={date} onChange={setDate} />
|
<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");
|
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", () => {
|
test("live UI language changes retain the selected day and unsaved form draft", () => {
|
||||||
render(<I18nProvider><DateLocalizationProvider><Form /></DateLocalizationProvider></I18nProvider>);
|
render(<I18nProvider><DateLocalizationProvider><Form /></DateLocalizationProvider></I18nProvider>);
|
||||||
const title = screen.getByRole("textbox", { name: "Unsaved title" });
|
const title = screen.getByRole("textbox", { name: "Unsaved title" });
|
||||||
|
|||||||
Reference in New Issue
Block a user