diff --git a/docs/i18n/UNKNOWN_LABEL_FALLBACKS.md b/docs/i18n/UNKNOWN_LABEL_FALLBACKS.md
new file mode 100644
index 00000000..e3dc430f
--- /dev/null
+++ b/docs/i18n/UNKNOWN_LABEL_FALLBACKS.md
@@ -0,0 +1,15 @@
+# Unknown translation-map labels — 2 November 2026
+
+CONFIRMED functional defects, repaired locally with synthetic regressions: Career Profile completeness and application timeline category helpers used ordinary object property lookup. Unknown labels matching inherited Object properties (`constructor`, `toString`, `__proto__`) were incorrectly sent to the translator, losing the original text and producing missing visible/accessible labels. This is not a prototype-pollution or account-access finding; no such impact is claimed.
+
+Minimal fix: Object.hasOwn guards in the two existing helpers. Known keys still translate normally, other strings remain data. Callers: ProfileCompleteness and CareerWorkspaceOverview; ApplicationTimeline. No API, persisted profile/category, UI language preference, generated-content language or revision behavior changes; no dependencies added.
+
+RED: three inherited-name Career Profile cases failed (two known/custom control cases passed); three timeline-category cases failed before the patch (11 unselected cases were skipped in that diagnostic run). GREEN final combined execution: 41 tests across four suites, zero failures/pending, native exit 0: career-completeness-labels, career-history-view, career-workspace-page and application-intelligence. Do not add the overlapping earlier 27-case run to this total. Full typecheck and scoped four-file ESLint passed, zero warnings.
+
+Native ignored JSON: job-tracker-ui/.artifacts/translation-map-fallbacks.json. Known-key test checks the translated catalogue key; timeline tests verify accessible button names for all three inherited-name canaries. No production or live provider/model used. Full integrated security testing remains deferred until relevant application changes are complete.
+
+Correction to earlier static review: finite literal-key coverage alone did not prove the stated unknown-label fallback for these ordinary object maps. These regressions now verify it. The checklist category helper's current caller iterates CHECKLIST_CATEGORIES, so arbitrary unknown categories never reach its lookup; no speculative checklist change was made. Other maps with external keys, including processing-monitor maps, still need equivalent reachability/fallback review. Dynamic-call total remains 122 resolved / 66 pending; this remediation is not another resolved call count.
+
+Rollback: revert the two helper lines and associated tests only; no data rollback needed. Remaining limits: no claim of native-language quality, full WCAG, all map coverage or final integrated security acceptance.
+
+Final build: Next.js 16.4.1 static production build PASSED, exit 0, including TypeScript and all three generated pages. This also builds the session-message and preceding attachment-label fixes on this branch. API build-time base was local; no deployment or live service validation. Sanitized native reports are retained in evidence/translation-map-fallbacks.json.gz and evidence/session-boundary-message.json.gz (41 and 17 disjoint selected tests respectively).
diff --git a/docs/i18n/evidence/session-boundary-message.json.gz b/docs/i18n/evidence/session-boundary-message.json.gz
new file mode 100644
index 00000000..6be68600
Binary files /dev/null and b/docs/i18n/evidence/session-boundary-message.json.gz differ
diff --git a/docs/i18n/evidence/translation-map-fallbacks.json.gz b/docs/i18n/evidence/translation-map-fallbacks.json.gz
new file mode 100644
index 00000000..20087534
Binary files /dev/null and b/docs/i18n/evidence/translation-map-fallbacks.json.gz differ
diff --git a/job-tracker-ui/src/application-intelligence.test.tsx b/job-tracker-ui/src/application-intelligence.test.tsx
index 19a1a0ef..2f8a49e8 100644
--- a/job-tracker-ui/src/application-intelligence.test.tsx
+++ b/job-tracker-ui/src/application-intelligence.test.tsx
@@ -78,6 +78,12 @@ const match = {
beforeEach(() => jest.clearAllMocks());
+test.each(["constructor", "toString", "__proto__"])("timeline preserves unknown category %s as a label", async category => {
+ mockedApi.get.mockResolvedValue({ data: { ...timeline, categories: ["stage", category] } } as any);
+ render();
+ expect(await screen.findByRole("button", { name: category })).toBeInTheDocument();
+});
+
// ---------- Timeline ----------
test("timeline renders grouped days, milestones and readable summaries", async () => {
diff --git a/job-tracker-ui/src/career-completeness-labels.test.ts b/job-tracker-ui/src/career-completeness-labels.test.ts
new file mode 100644
index 00000000..c0454987
--- /dev/null
+++ b/job-tracker-ui/src/career-completeness-labels.test.ts
@@ -0,0 +1,13 @@
+import { localizeCompletenessLabel } from "./views/career/careerCompletenessLabels";
+
+test.each(["constructor", "toString", "__proto__", "Custom section"])("preserves unknown completeness label %s as data", label => {
+ const translate = jest.fn(() => "translated");
+ expect(localizeCompletenessLabel(label, translate)).toBe(label);
+ expect(translate).not.toHaveBeenCalled();
+});
+
+test("translates a known completeness label using its catalogue key", () => {
+ const translate = jest.fn(() => "Personopplysninger");
+ expect(localizeCompletenessLabel("Personal details", translate)).toBe("Personopplysninger");
+ expect(translate).toHaveBeenCalledWith("profileCvCompletenessPersonal");
+});
diff --git a/job-tracker-ui/src/components/ApplicationIntelligence.tsx b/job-tracker-ui/src/components/ApplicationIntelligence.tsx
index 4a376999..4cb96fd8 100644
--- a/job-tracker-ui/src/components/ApplicationIntelligence.tsx
+++ b/job-tracker-ui/src/components/ApplicationIntelligence.tsx
@@ -364,7 +364,7 @@ function timelineCategoryLabel(t: (key: any) => string, category: string): strin
communication: "intelligenceCategoryCommunication",
ai: "intelligenceCategoryAi",
};
- return keys[category] ? t(keys[category]) : category;
+ return Object.hasOwn(keys, category) ? t(keys[category]) : category;
}
function matchBandLabel(t: (key: any) => string, band?: string): string {
diff --git a/job-tracker-ui/src/views/career/careerCompletenessLabels.ts b/job-tracker-ui/src/views/career/careerCompletenessLabels.ts
index d8116fc2..a3e1d7c1 100644
--- a/job-tracker-ui/src/views/career/careerCompletenessLabels.ts
+++ b/job-tracker-ui/src/views/career/careerCompletenessLabels.ts
@@ -11,6 +11,6 @@ const completenessLabelKeys: Record = {
};
export function localizeCompletenessLabel(label: string, t: (key: TranslationKey) => string) {
- const key = completenessLabelKeys[label];
+ const key = Object.hasOwn(completenessLabelKeys, label) ? completenessLabelKeys[label] : undefined;
return key ? t(key) : label;
}