fix(i18n): preserve safe interview and attachment purpose fallbacks

This commit is contained in:
cesnimda
2026-11-02 23:01:23 +01:00
parent fb48563557
commit 80d3849125
6 changed files with 66 additions and 15 deletions
+34 -13
View File
@@ -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<string, TranslationKey>)[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": [
+11
View File
@@ -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.
@@ -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;
}
@@ -22,6 +22,15 @@ function page(items: ReturnType<typeof item>[] = [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<T>() {
let resolve!: (value: T) => void;
let reject!: (reason: unknown) => void;
@@ -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(<ApplicationInterviewPrep jobId={7} />);
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();
+1 -1
View File
@@ -240,7 +240,7 @@ export default function DocumentsPage() {
{item.type === "email-draft" ? <Typography variant="caption" color="text.secondary">{t("documentsEmailHelp")}</Typography> : null}
{item.type === "attachment" ? <>
<Typography variant="caption" color="text.secondary">{[item.fileType, item.fileSize != null ? t("documentsFileSize", { size: new Intl.NumberFormat(locale, { maximumFractionDigits: 1 }).format(item.fileSize / 1024) }) : null].filter(Boolean).join(" · ")}</Typography>
{item.purpose ? <Typography variant="caption" color="text.secondary">{t("attachmentsPurpose")}: {t(({ resume: "attachmentsPurposeResume", "cover-letter": "attachmentsPurposeCoverLetter", portfolio: "attachmentsPurposePortfolio", "case-study": "attachmentsPurposeCaseStudy", certificate: "attachmentsPurposeCertificate" } as Record<string, TranslationKey>)[item.purpose.toLowerCase()] ?? "attachmentsPurposeOther")}</Typography> : null}
{item.purpose ? <Typography variant="caption" color="text.secondary">{t("attachmentsPurpose")}: {t((Object.assign(Object.create(null), { resume: "attachmentsPurposeResume", "cover-letter": "attachmentsPurposeCoverLetter", portfolio: "attachmentsPurposePortfolio", "case-study": "attachmentsPurposeCaseStudy", certificate: "attachmentsPurposeCertificate" }) as Record<string, TranslationKey>)[item.purpose.toLowerCase()] ?? "attachmentsPurposeOther")}</Typography> : null}
{item.purpose?.toLowerCase() === "certificate" ? <Typography variant="caption" color="text.secondary">{t("documentsCertificateHelp")}</Typography> : null}
{item.useForAi != null ? <Typography variant="caption" color="text.secondary">{t("attachmentsAiUse")}: {t(item.useForAi ? "attachmentsAiEnabled" : "attachmentsAiDisabled")}</Typography> : null}
</> : null}