fix(i18n): localise session-change feedback outside private providers

This commit is contained in:
cesnimda
2026-11-02 22:18:30 +01:00
parent bab240914e
commit 2108ea5037
3 changed files with 44 additions and 1 deletions
+11
View File
@@ -0,0 +1,11 @@
# Session-change message localisation — 2 November 2026
Implemented in the existing App invalidation branch. Reuses the existing `sessionChangedCleared` English/Bokmål catalogue message. The canonical document language (`nb-NO`, set by I18nProvider) selects Bokmål; other values safely select English. Reading it occurs only in the invalidated client branch. No provider, hook, storage access, request, dialog or private callback is retained to obtain the message.
Callers: root App renders Application only while valid; invalidation replaces it with a main alert and preserves the existing passive redirect to /login?sessionChanged=1. I18nProvider already publishes the current UI language to the document; generated-content language is unrelated. All routes, auth/CSRF/context tokens, abort logic and provider teardown remain unchanged. No new translation key or dependency.
RED: two new root-render tests failed against the previous literal English alert. PASSED after patch: 17 tests in session-boundary-message.test.tsx and session-boundary.test.ts, zero failures/pending, exit 0. New cases verify EN/NB feedback outside the private provider tree and zero API requests. Existing cases exercise cross-tab/cookie invalidation, abort/stale callbacks and session-context handling. These are local Jest/DOM tests; jsdom navigation is not browser redirect qualification.
PASSED: full TypeScript typecheck; scoped ESLint of App.tsx and the new test, zero warnings. Local ignored native Jest result: job-tracker-ui/.artifacts/session-boundary-message.json. No fresh full build/browser or complete security acceptance claimed. Rollback: revert the two source/test files in this package; no data changes. The attachment-label fix remains independently preserved.
The original literal-inventory App.tsx entry is now implemented; the inventory remains a historical snapshot. Final integrated security regression must run after relevant application changes are complete.
+2 -1
View File
@@ -29,6 +29,7 @@ import JobTable from "./components/JobTable";
import NotificationsPopover from "./components/NotificationsPopover";
import type { JobTableColumns } from "./components/JobTable";
import { I18nProvider, useI18n } from "./i18n/I18nProvider";
import { translations } from "./i18n/translations";
import LoginPage from "./views/LoginPage";
import LandingPage from "./views/LandingPage";
import ForgotPasswordPage from "./views/ForgotPasswordPage";
@@ -485,7 +486,7 @@ export default function App() {
}, [invalidated]);
// Unmount providers as well as routes: confirmations/toasts can retain private text and
// callbacks. Passive effect navigation runs after the old forms' unload guards are removed.
return invalidated ? <main role="alert">Session changed. Clearing this page…</main> : <Application />;
return invalidated ? <main role="alert">{translations[document.documentElement.lang === "nb-NO" ? "nb" : "en"].sessionChangedCleared}</main> : <Application />;
}
function Application() {
@@ -0,0 +1,31 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import "@testing-library/jest-dom";
import App from "./App";
import { I18nProvider } from "./i18n/I18nProvider";
import { api } from "./api";
jest.mock("./sessionBoundary", () => ({
isSessionInvalidated: () => true,
SESSION_INVALIDATED_EVENT: "session-invalidated",
}));
jest.mock("./i18n/I18nProvider", () => ({
...jest.requireActual("./i18n/I18nProvider"),
I18nProvider: jest.fn(() => { throw new Error("Invalidated sessions must not mount providers"); }),
}));
test.each([
["en", "Your session changed. Private data and unsaved edits were cleared from this page. Sign in again to continue."],
["nb-NO", "Økten din ble endret. Private data og ulagrede endringer ble fjernet fra denne siden. Logg inn igjen for å fortsette."],
])("invalidated session shows %s feedback outside the private provider tree", (language, message) => {
const previousLanguage = document.documentElement.lang;
document.documentElement.lang = language;
try {
render(<App />);
expect(screen.getByRole("alert")).toHaveTextContent(message);
expect(I18nProvider).not.toHaveBeenCalled();
expect(api.get).not.toHaveBeenCalled();
} finally {
document.documentElement.lang = previousLanguage;
}
});