From cb9d039786d1ce32e85dc857c55e6e8a3c2007bc Mon Sep 17 00:00:00 2001 From: cesnimda Date: Wed, 11 Nov 2026 13:21:57 +0100 Subject: [PATCH] fix(i18n): localize shared MUI widget controls --- .../scripts/verify-job-date-widgets.mjs | 9 ++++++++ .../src/DateLocalizationProvider.tsx | 14 ++++++++++--- .../src/date-picker-localisation.test.tsx | 21 +++++++++++++++++++ 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/job-tracker-ui/scripts/verify-job-date-widgets.mjs b/job-tracker-ui/scripts/verify-job-date-widgets.mjs index 1f271dbe..aec37c88 100644 --- a/job-tracker-ui/scripts/verify-job-date-widgets.mjs +++ b/job-tracker-ui/scripts/verify-job-date-widgets.mjs @@ -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; }); diff --git a/job-tracker-ui/src/DateLocalizationProvider.tsx b/job-tracker-ui/src/DateLocalizationProvider.tsx index 96ca1a11..054cc21d 100644 --- a/job-tracker-ui/src/DateLocalizationProvider.tsx +++ b/job-tracker-ui/src/DateLocalizationProvider.tsx @@ -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 ({ + ...theme, + components: deepmerge(theme.components ?? {}, language === "nb" ? materialNbNO.components : {}), + }), [theme, language]); + return {children}; + >{children}; } diff --git a/job-tracker-ui/src/date-picker-localisation.test.tsx b/job-tracker-ui/src/date-picker-localisation.test.tsx index 266334de..b60719c9 100644 --- a/job-tracker-ui/src/date-picker-localisation.test.tsx +++ b/job-tracker-ui/src/date-picker-localisation.test.tsx @@ -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() { + } /> ; } @@ -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(
); + 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(); const title = screen.getByRole("textbox", { name: "Unsaved title" });