From 80d3849125c3e5a59ac12bc5b6f0f59c958f027f Mon Sep 17 00:00:00 2001 From: cesnimda Date: Mon, 2 Nov 2026 23:01:23 +0100 Subject: [PATCH] fix(i18n): preserve safe interview and attachment purpose fallbacks --- docs/i18n/DYNAMIC_RESOLUTION_REGISTER.json | 47 ++++++++++++++----- docs/i18n/INTERVIEW_DOCUMENT_FALLBACKS.md | 11 +++++ .../src/components/InterviewPrep.tsx | 2 +- job-tracker-ui/src/documents-page.test.tsx | 9 ++++ job-tracker-ui/src/interview-prep.test.tsx | 10 ++++ job-tracker-ui/src/views/DocumentsPage.tsx | 2 +- 6 files changed, 66 insertions(+), 15 deletions(-) create mode 100644 docs/i18n/INTERVIEW_DOCUMENT_FALLBACKS.md diff --git a/docs/i18n/DYNAMIC_RESOLUTION_REGISTER.json b/docs/i18n/DYNAMIC_RESOLUTION_REGISTER.json index 80122689..69daf9e6 100644 --- a/docs/i18n/DYNAMIC_RESOLUTION_REGISTER.json +++ b/docs/i18n/DYNAMIC_RESOLUTION_REGISTER.json @@ -3,9 +3,9 @@ "inventorySha256": "79928688a7b1a4961286aaaf48c028eb13ae8286d72d132561d6b5fdd192e323", "historicalCalls": 188, "previouslyResolved": 108, - "additionalResolved": 78, - "resolved": 186, - "pending": 2, + "additionalResolved": 80, + "resolved": 188, + "pending": 0, "evidence": [ "CHECKLIST_DYNAMIC_REVIEW.md", "FINITE_OWNER_MAP_REVIEW.md", @@ -18,7 +18,8 @@ "CV_BOARD_KEY_REVIEW.md", "CHECKLIST_UNKNOWN_SYSTEM_KEYS.md", "ERROR_STATE_KEY_REVIEW.md", - "IMPORT_OPERATION_KEY_REVIEW.md" + "IMPORT_OPERATION_KEY_REVIEW.md", + "INTERVIEW_DOCUMENT_FALLBACKS.md" ], "entries": [ { @@ -256,7 +257,7 @@ "historicalLine": 236, "expression": "t(selected.labelKey)", "historicalSourceSha256": "eadf345d3613ad867788f6a74a8fd1fbba508bb81ec7bc87fe54e4f66dadc69b", - "reviewedSourceSha256": "eadf345d3613ad867788f6a74a8fd1fbba508bb81ec7bc87fe54e4f66dadc69b", + "reviewedSourceSha256": "82d0c3d15403e270942dae326169d4736fc169005867c9e0d774a92a5cf30cac", "status": "finite-key-domain-reviewed", "runtimeCoverage": "static owner-map review only; no inference or provider calls", "inventoryEntryIndex": 63 @@ -266,7 +267,7 @@ "historicalLine": 261, "expression": "t(selected.labelKey)", "historicalSourceSha256": "eadf345d3613ad867788f6a74a8fd1fbba508bb81ec7bc87fe54e4f66dadc69b", - "reviewedSourceSha256": "eadf345d3613ad867788f6a74a8fd1fbba508bb81ec7bc87fe54e4f66dadc69b", + "reviewedSourceSha256": "82d0c3d15403e270942dae326169d4736fc169005867c9e0d774a92a5cf30cac", "status": "finite-key-domain-reviewed", "runtimeCoverage": "static owner-map review only; no inference or provider calls", "inventoryEntryIndex": 64 @@ -276,7 +277,7 @@ "historicalLine": 285, "expression": "t(item.labelKey)", "historicalSourceSha256": "eadf345d3613ad867788f6a74a8fd1fbba508bb81ec7bc87fe54e4f66dadc69b", - "reviewedSourceSha256": "eadf345d3613ad867788f6a74a8fd1fbba508bb81ec7bc87fe54e4f66dadc69b", + "reviewedSourceSha256": "82d0c3d15403e270942dae326169d4736fc169005867c9e0d774a92a5cf30cac", "status": "finite-key-domain-reviewed", "runtimeCoverage": "static owner-map review only; no inference or provider calls", "inventoryEntryIndex": 65 @@ -396,7 +397,7 @@ "historicalLine": 222, "expression": "t(label)", "historicalSourceSha256": "fa53a826ebbdbc868c8b21a0387138c12611d3e2867de0fa0e5f6d8483471eb9", - "reviewedSourceSha256": "fa53a826ebbdbc868c8b21a0387138c12611d3e2867de0fa0e5f6d8483471eb9", + "reviewedSourceSha256": "42049d64e96babde5f36ae325169f5d83a92399018b92ad2cf8acd01466d53b5", "status": "finite-key-domain-reviewed", "runtimeCoverage": "static source review; no marketing or accessibility acceptance", "inventoryEntryIndex": 134 @@ -406,7 +407,7 @@ "historicalLine": 225, "expression": "t(states.find(([value]) => value === item.state)?.[1] ?? \"documentsCurrent\")", "historicalSourceSha256": "fa53a826ebbdbc868c8b21a0387138c12611d3e2867de0fa0e5f6d8483471eb9", - "reviewedSourceSha256": "fa53a826ebbdbc868c8b21a0387138c12611d3e2867de0fa0e5f6d8483471eb9", + "reviewedSourceSha256": "42049d64e96babde5f36ae325169f5d83a92399018b92ad2cf8acd01466d53b5", "status": "finite-key-domain-reviewed", "runtimeCoverage": "static source review; no marketing or accessibility acceptance", "inventoryEntryIndex": 135 @@ -416,7 +417,7 @@ "historicalLine": 231, "expression": "t(types.find(([value]) => value === item.type)?.[1] ?? \"documentsTypeAttachment\")", "historicalSourceSha256": "fa53a826ebbdbc868c8b21a0387138c12611d3e2867de0fa0e5f6d8483471eb9", - "reviewedSourceSha256": "fa53a826ebbdbc868c8b21a0387138c12611d3e2867de0fa0e5f6d8483471eb9", + "reviewedSourceSha256": "42049d64e96babde5f36ae325169f5d83a92399018b92ad2cf8acd01466d53b5", "status": "finite-key-domain-reviewed", "runtimeCoverage": "static source review; no marketing or accessibility acceptance", "inventoryEntryIndex": 137 @@ -426,7 +427,7 @@ "historicalLine": 264, "expression": "t(title)", "historicalSourceSha256": "fa53a826ebbdbc868c8b21a0387138c12611d3e2867de0fa0e5f6d8483471eb9", - "reviewedSourceSha256": "fa53a826ebbdbc868c8b21a0387138c12611d3e2867de0fa0e5f6d8483471eb9", + "reviewedSourceSha256": "42049d64e96babde5f36ae325169f5d83a92399018b92ad2cf8acd01466d53b5", "status": "finite-key-domain-reviewed", "runtimeCoverage": "static source review; no marketing or accessibility acceptance", "inventoryEntryIndex": 144 @@ -436,7 +437,7 @@ "historicalLine": 265, "expression": "t(title)", "historicalSourceSha256": "fa53a826ebbdbc868c8b21a0387138c12611d3e2867de0fa0e5f6d8483471eb9", - "reviewedSourceSha256": "fa53a826ebbdbc868c8b21a0387138c12611d3e2867de0fa0e5f6d8483471eb9", + "reviewedSourceSha256": "42049d64e96babde5f36ae325169f5d83a92399018b92ad2cf8acd01466d53b5", "status": "finite-key-domain-reviewed", "runtimeCoverage": "static source review; no marketing or accessibility acceptance", "inventoryEntryIndex": 145 @@ -446,7 +447,7 @@ "historicalLine": 265, "expression": "t(label)", "historicalSourceSha256": "fa53a826ebbdbc868c8b21a0387138c12611d3e2867de0fa0e5f6d8483471eb9", - "reviewedSourceSha256": "fa53a826ebbdbc868c8b21a0387138c12611d3e2867de0fa0e5f6d8483471eb9", + "reviewedSourceSha256": "42049d64e96babde5f36ae325169f5d83a92399018b92ad2cf8acd01466d53b5", "status": "finite-key-domain-reviewed", "runtimeCoverage": "static source review; no marketing or accessibility acceptance", "inventoryEntryIndex": 146 @@ -884,6 +885,26 @@ "reviewedSourceSha256": "bdecd17437d919a33d9deaa855262b1f1baf78b4a22eef0e88ac1fdbb5ebeda4", "status": "finite-key-domain-reviewed", "runtimeCoverage": "existing Object.hasOwn guards statically reviewed; no service execution" + }, + { + "inventoryEntryIndex": 67, + "file": "job-tracker-ui/src/components/InterviewPrep.tsx", + "historicalLine": 505, + "expression": "t(keys[category])", + "historicalSourceSha256": "eadf345d3613ad867788f6a74a8fd1fbba508bb81ec7bc87fe54e4f66dadc69b", + "reviewedSourceSha256": "82d0c3d15403e270942dae326169d4736fc169005867c9e0d774a92a5cf30cac", + "status": "finite-key-domain-reviewed", + "runtimeCoverage": "INTERVIEW_DOCUMENT_FALLBACKS.md; 41 full selected-suite tests, including 12 inherited-name/language cases" + }, + { + "inventoryEntryIndex": 138, + "file": "job-tracker-ui/src/views/DocumentsPage.tsx", + "historicalLine": 243, + "expression": "t(({ resume: \"attachmentsPurposeResume\", \"cover-letter\": \"attachmentsPurposeCoverLetter\", portfolio: \"attachmentsPurposePortfolio\", \"case-study\": \"attachmentsPurposeCaseStudy\", certificate: \"attachmentsPurposeCertificate\" } as Record)[item.purpose.toLowerCase()] ?? \"attachmentsPurposeOther\")", + "historicalSourceSha256": "fa53a826ebbdbc868c8b21a0387138c12611d3e2867de0fa0e5f6d8483471eb9", + "reviewedSourceSha256": "42049d64e96babde5f36ae325169f5d83a92399018b92ad2cf8acd01466d53b5", + "status": "finite-key-domain-reviewed", + "runtimeCoverage": "INTERVIEW_DOCUMENT_FALLBACKS.md; 41 full selected-suite tests, including 12 inherited-name/language cases" } ], "limitations": [ diff --git a/docs/i18n/INTERVIEW_DOCUMENT_FALLBACKS.md b/docs/i18n/INTERVIEW_DOCUMENT_FALLBACKS.md new file mode 100644 index 00000000..ac113419 --- /dev/null +++ b/docs/i18n/INTERVIEW_DOCUMENT_FALLBACKS.md @@ -0,0 +1,11 @@ +# Interview category and document-purpose fallbacks + +2 November 2026. Two confirmed functional defects: inherited Object-property names bypassed interview category raw-text fallback and document-purpose localized Other fallback. Synthetic EN/NB diagnostic run: ten failures, two passing lowercase-purpose controls, 29 unselected cases. This is not a prototype mutation or account-access finding. + +Minimal fixes: Object.hasOwn in the existing interview category helper; a null-prototype object for the existing inline document-purpose map. Exact known labels, purpose lowercasing, existing unknown-value choices and all surrounding workflows remain intact. No role, API, CV/import acceptance, revision, model, provider, storage, database or final UI design changes. + +GREEN: 41 tests across full interview-prep and documents-page suites, zero failures/pending, exit 0. Twelve new cases cover constructor/toString/__proto__ in EN/Bokmål, checking unknown category text and localized Other purpose. Existing cases protect normal interview/follow-up, document filtering/date/link/download and account-safety behavior. Full typecheck and four-file scoped lint passed, zero warnings. Native report: job-tracker-ui/.artifacts/interview-document-fallbacks.json. All 13 dictionary/fallback keys verified in both catalogues. No real model, mail or provider operation. + +Rollback: revert the two lookup changes and twelve test cases; no data conversion or recovery action. Existing labels/history/content are not rewritten. Final integrated security/browser/build qualification remains separately recorded; this test report is not a full application pass. + +The historical 188-call census is now completely accounted for: 108 prior finite-branch sites plus 80 individually reviewed supplemental sites, no overlapping inventory indices, zero pending finite key domains. Register schema 2 distinguishes same-line repeated calls. This does NOT mean complete I18N-00, every-string sensitivity/context, backend-message extraction, plural/select architecture, European locale publication, native translation, accessibility or full runtime coverage is accepted. New code and changed owner dependencies require renewed review. Preserve the original inventory as immutable historical evidence. diff --git a/job-tracker-ui/src/components/InterviewPrep.tsx b/job-tracker-ui/src/components/InterviewPrep.tsx index 2d514011..4231d362 100644 --- a/job-tracker-ui/src/components/InterviewPrep.tsx +++ b/job-tracker-ui/src/components/InterviewPrep.tsx @@ -502,5 +502,5 @@ function interviewCategoryLabel(t: (key: any) => string, category: string) { note: "interviewCategoryNotes", debrief: "interviewCategoryDebrief", }; - return keys[category] ? t(keys[category]) : category; + return Object.hasOwn(keys, category) ? t(keys[category]) : category; } diff --git a/job-tracker-ui/src/documents-page.test.tsx b/job-tracker-ui/src/documents-page.test.tsx index dbb7a48b..42389a51 100644 --- a/job-tracker-ui/src/documents-page.test.tsx +++ b/job-tracker-ui/src/documents-page.test.tsx @@ -22,6 +22,15 @@ function page(items: ReturnType[] = [item()], currentPage = 1, tota return { items, page: currentPage, pageSize: 25, total, totalPages: Math.ceil(total / 25) }; } +describe.each(['en', 'nb'])('%s unknown attachment purposes', language => { + test.each(['constructor', 'toString', '__proto__'])('uses the localized Other label for %s', async purpose => { + localStorage.setItem('uiLanguage', language); + mockedApi.get.mockResolvedValue({ data: page([item({ type: 'attachment', purpose, visibility: 'private' })]) } as never); + renderPage(); + expect(await screen.findByText(`${copy('attachmentsPurpose')}: ${copy('attachmentsPurposeOther')}`)).toBeVisible(); + }); +}); + function deferred() { let resolve!: (value: T) => void; let reject!: (reason: unknown) => void; diff --git a/job-tracker-ui/src/interview-prep.test.tsx b/job-tracker-ui/src/interview-prep.test.tsx index 0871a345..5b91ee20 100644 --- a/job-tracker-ui/src/interview-prep.test.tsx +++ b/job-tracker-ui/src/interview-prep.test.tsx @@ -62,6 +62,16 @@ beforeEach(() => { window.localStorage.setItem("uiLanguage", "en"); }); +describe.each(['en', 'nb'])('%s unknown interview categories', language => { + test.each(['constructor', 'toString', '__proto__'])('preserves %s as a category label', async category => { + window.localStorage.setItem('uiLanguage', language); + routeGet({ board: { ...board, groups: [{ ...board.groups[0], category }] } }); + render(); + expect(await screen.findByText(category)).toBeInTheDocument(); + expect(screen.getByText('Funding history')).toBeInTheDocument(); + }); +}); + test("localizes interview categories while preserving user-authored preparation", async () => { window.localStorage.setItem("uiLanguage", "nb"); routeGet(); diff --git a/job-tracker-ui/src/views/DocumentsPage.tsx b/job-tracker-ui/src/views/DocumentsPage.tsx index cb2bd813..e6d953ce 100644 --- a/job-tracker-ui/src/views/DocumentsPage.tsx +++ b/job-tracker-ui/src/views/DocumentsPage.tsx @@ -240,7 +240,7 @@ export default function DocumentsPage() { {item.type === "email-draft" ? {t("documentsEmailHelp")} : null} {item.type === "attachment" ? <> {[item.fileType, item.fileSize != null ? t("documentsFileSize", { size: new Intl.NumberFormat(locale, { maximumFractionDigits: 1 }).format(item.fileSize / 1024) }) : null].filter(Boolean).join(" · ")} - {item.purpose ? {t("attachmentsPurpose")}: {t(({ resume: "attachmentsPurposeResume", "cover-letter": "attachmentsPurposeCoverLetter", portfolio: "attachmentsPurposePortfolio", "case-study": "attachmentsPurposeCaseStudy", certificate: "attachmentsPurposeCertificate" } as Record)[item.purpose.toLowerCase()] ?? "attachmentsPurposeOther")} : null} + {item.purpose ? {t("attachmentsPurpose")}: {t((Object.assign(Object.create(null), { resume: "attachmentsPurposeResume", "cover-letter": "attachmentsPurposeCoverLetter", portfolio: "attachmentsPurposePortfolio", "case-study": "attachmentsPurposeCaseStudy", certificate: "attachmentsPurposeCertificate" }) as Record)[item.purpose.toLowerCase()] ?? "attachmentsPurposeOther")} : null} {item.purpose?.toLowerCase() === "certificate" ? {t("documentsCertificateHelp")} : null} {item.useForAi != null ? {t("attachmentsAiUse")}: {t(item.useForAi ? "attachmentsAiEnabled" : "attachmentsAiDisabled")} : null} : null}