From 7b132ec4b4354b49f2eef5584954edb407168f2d Mon Sep 17 00:00:00 2001 From: cesnimda Date: Mon, 2 Nov 2026 22:23:26 +0100 Subject: [PATCH] fix(i18n): preserve inherited-name labels as untranslated data --- docs/i18n/UNKNOWN_LABEL_FALLBACKS.md | 15 +++++++++++++++ .../evidence/session-boundary-message.json.gz | Bin 0 -> 1253 bytes .../evidence/translation-map-fallbacks.json.gz | Bin 0 -> 2374 bytes .../src/application-intelligence.test.tsx | 6 ++++++ .../src/career-completeness-labels.test.ts | 13 +++++++++++++ .../src/components/ApplicationIntelligence.tsx | 2 +- .../src/views/career/careerCompletenessLabels.ts | 2 +- 7 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 docs/i18n/UNKNOWN_LABEL_FALLBACKS.md create mode 100644 docs/i18n/evidence/session-boundary-message.json.gz create mode 100644 docs/i18n/evidence/translation-map-fallbacks.json.gz create mode 100644 job-tracker-ui/src/career-completeness-labels.test.ts 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 0000000000000000000000000000000000000000..6be686004a8b8d7ecde4d309fd237dea7cf34425 GIT binary patch literal 1253 zcmV>G*ZTp#Lu(#P*k!vpe4BhYG zv`t&yy)Ps=n9;amKj{*e>=`CoKug6h1cuU4;cNiUW!jm7GLcZapu=V<%M}zxOr@bBS z4;JI{eIe258_v@^>hudatzeV`DGTW;1WyKK`IazHS)O?3d??GO%4@tR%Y_f;=}sz3 z(>`*ogM$`CZMZD|^h3b`)H#N-{NRVJdeMreEGek6tfgoTeRLYavQ@wRGfM5`MVgUqg+P>1P#NK>gq&j#ty77BvRuDZk*9*6#tD`VfWJP&Hht~ z)~ZXi#98DCIyMz@0xWr<4JinkGV|W*Tm&|a_wgpYw`O{8=@x`W3rp0i4lE$eyd%6a zj({^ii#B~ypeD&VM~vaKyC!zF0=a!(kb4^=`;J7|BjlNrq4!K2V3z&tXH4_9D~A0X zLvCq=583Tf6civFF6>g|Hgx5~KjxFaWQKlfwh3$ZdYcu|pkVup_>D-BCVY)+XX7}m zNxAmsJ5@iQns?=$HaU#eXN0l{p^Bp^hKhyZN#dW3MaOn2GK#5;05OgH4T0;PgR*|R zzcFy_T$sJ7;l<076h~8_O?6!6e7$_jd8`h`Nq%fNLG(AqrH6BSj}g$$%1gJL`bc6H zht{Z8R`$fChnGToBvB=>kI@9ITv1p*HchA8{^nS+J=^;mBveJ~=xXRhY^)V&3`{F; z0`Y~8q-k_Fj}5+w^UFJD=Kc1w_II6`U(L$DFe>jIb?>ye^-=lzLN0**aEu|u)UDL^ Parwi4yQ_E5Jski5T{LrW literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..200875345e432b1bf1d826e3f655213e7dfd7932 GIT binary patch literal 2374 zcmV-M3Ay$kiwFP!000003e{WNa@#l(ec!J@@mWeqvhHsg?@sN#wlYaxMyV7`wj``U zfP)5PYf|}~{gVB$tp-S1PGky^NG1Db!J>5fpwX9u)~7_d{AVsCWN!iOFHTs%WI5

3z9gzxo^O@u&UILPGcg z_w2J>{jr~W30iLKTaiPu9L^Ru)5&G&JNh0mnPYLa$ zu|lh7iz)@Q+PoFE<}!1J)BBR;a5M~*3AOo>!asuLLY9w|YySX5zNtQE?WFuCC!h(r zfD{T?VF1DG0GU(wN-{07w6r_Yq?5wLRc%I$F&Ax^GV=skAgnU>&>iXUJa-W z46=>Uu7HeXe2+brQy8^c0Wz*KW`Jk>P69)h=iCUu^Z1=a@;ez0qxd}+{Ar$NZAaX- zkd~m{?Q0+C)<-AfxR`nj*lB)-ZJ%%4>Id2{26d3p`_bU7j9-S;Fm@)tgBitKWt>A{Wiu zVU;T`_b9LqJ9Y~U9P!sxuP$ky6;SUlT- zvEjtnEf`<|nbHcnb;Q|l9K~Qf+=zHNXqn40_AseAHG5_7xr4Pf;L&E>a>k7jyK}R3 zG@s*}C_cXpa)f`j6fJ#^_w2l2stxoQ>_i4C1z$#>+iU;>ft?mVvq__SemmNTywj3* z25cPf!=d3Ub&4apjI98+*~cSZOi8my#!SDg`hQ^PiMU(4}>PmBl!K_yQ)_48#Mc;|l3Qf9N@uH9- zaM%~hf|O!IG3^zpXs;|jCdZYxoB8pe8Ct$QLHB-khW0idwc~wjVbJ!CQ;Z10f~TcE zpe&Mll?sF^>hze%3vFzVZ9swHlHI=dVVy|B0#dOSDdXt{*=Qc;hqLLRv5oA`s)!!F zrMkY1_)iAt9hjYf2M;b_E{m0rc9~G@Cw-s)_dl}JQeZZlG|G1u0gvFfr5@jabw*M4 zd_o#(!CZJ=>SQa_TAMsXt{~l{plt0fU-`=I`!l_N8o2@UQKR>FYgt5R+magigdQFX zjS%X}BHSZ=G~j*enLl~a(g3>LOKaiKp0^h_k$bTiG`1OS*>F8Q;W*;o95~3nKiiW= z3HDO#r&*qK$}&CmBe1+^R2nbE{yidyfja+JjXp)UG7$D@Ey#Mv9)#U$XPHntP_~i; z9Cj+8%?FcN9OrFVa=j{sGX!hNF-rfaQV-pKu&u~S@1de!f&~SLp#>`wlBKH=rj^_? zyEWQvwoF)L@F>t*q#qQRPV1fdaL}yGUB|>L?wP7D2FugzElIgj+Okw~ku#;s|6#8( zjEV?t*i-GjaUvtc-5#9beAKM-wQS7keVDe?oAEDVst1mi@1HJi$${-hX&{5aEDyE2R1m77uHG3mshhz*&yH90`ezzuaTRZ;TH&HCQW0_F;3 z%9U0k<JKj%%u|H*SQz&@ zEJFxVSM5?hgH}&#Lj@?%#9=t{27YsZwo|!pF(SG1<$dYcwBsuD!e206QH2h=TEFfD z`UT&dl%S_`stP-ipxbfFBg1Hib+Vs8`*MtuZe%;Xo?9%MZ#&Nhefqq&blRg39u84n zgVV;lqw^5|ScEsFdrV=tm~{yKmX zWy&p=dgCa0?!PO&mmz;4SuaRlyMD4h1m5?+vC7ylB<#iPxWwH;!fvtE@~z-OXTdW@ z)vqG})Rb=w?Nr^AkjfH-%5o^Re6m9!Vlo&^nD1=d<$*N)OG94IBqngZa zISt5cMV0~m3Zvk6LJGU@u*;nc2D4%FWMoG+MF`mKY>HOEQfItyQlU4Hdw-&4kFKJg zMh@!Jt=H=?!_6zHghso)a$kNJYG*o?vuSf^e)Q$d>Dsm!ZK%C8l4>b=L6cyWcZJT~ zK7ic8)3HyrvhZjfmo@xyAcqk{ 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; }