diff --git a/docs/i18n/SESSION_BOUNDARY_MESSAGE.md b/docs/i18n/SESSION_BOUNDARY_MESSAGE.md
new file mode 100644
index 00000000..fc645efa
--- /dev/null
+++ b/docs/i18n/SESSION_BOUNDARY_MESSAGE.md
@@ -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.
diff --git a/job-tracker-ui/src/App.tsx b/job-tracker-ui/src/App.tsx
index 90037ab0..309fdf0b 100644
--- a/job-tracker-ui/src/App.tsx
+++ b/job-tracker-ui/src/App.tsx
@@ -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 ? Session changed. Clearing this page… : ;
+ return invalidated ? {translations[document.documentElement.lang === "nb-NO" ? "nb" : "en"].sessionChangedCleared} : ;
}
function Application() {
diff --git a/job-tracker-ui/src/session-boundary-message.test.tsx b/job-tracker-ui/src/session-boundary-message.test.tsx
new file mode 100644
index 00000000..b55805ba
--- /dev/null
+++ b/job-tracker-ui/src/session-boundary-message.test.tsx
@@ -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();
+ expect(screen.getByRole("alert")).toHaveTextContent(message);
+ expect(I18nProvider).not.toHaveBeenCalled();
+ expect(api.get).not.toHaveBeenCalled();
+ } finally {
+ document.documentElement.lang = previousLanguage;
+ }
+});