Skip to content

fix(i18n): restore en/de key parity, translate usePhotos errors, add parity guard test#1846

Merged
steilerDev merged 1 commit into
betafrom
fix/1814-i18n-parity-usephotos
Jul 7, 2026
Merged

fix(i18n): restore en/de key parity, translate usePhotos errors, add parity guard test#1846
steilerDev merged 1 commit into
betafrom
fix/1814-i18n-parity-usephotos

Conversation

@steilerDev

Copy link
Copy Markdown
Owner

Summary

  • usePhotos.ts now routes API errors through translateApiError()/t() instead of hardcoded English strings (ApiClientErrorerrors namespace by code; NetworkError/unexpected → new photoViewer.networkError/unexpectedError keys, translated in both en and de).
  • Removed 5 orphaned photoAnnotator keys (reset/resetTitle/resetBody/resetConfirm/resetComplete) left over from a "reset to original photo" feature that was replaced by PhotoViewer's "Clear annotations" entry point; also removed the associated dead CSS (.resetButton, .modalActions, .confirmButton).
  • Fixed 5 pre-existing literal duplicate JSON keys (same key twice in the same object, silently shadowed by JSON.parse's last-wins behavior) across diary.json, householdItems.json, and schedule.json in both locales — verified behavior-preserving in every case (the shadowed/first occurrence was already unreachable at runtime).
  • Added client/src/i18n/i18n.parity.test.ts: a generalized en/de key-parity guard test across all 14 namespaces, plus a raw-text duplicate-key guard across all 28 locale files, so both bug classes fail CI going forward instead of accumulating.

Fixes #1814

Test plan

  • Unit tests pass (usePhotos.test.ts 38/38, 100% stmts/lines coverage; new i18n.parity.test.ts 46/46)
  • tsc --noEmit clean, npm run lint clean
  • Regression check across all downstream consumers of touched namespaces/components (PhotoAnnotator, DiaryEntryCreatePage, DiaryEntryEditPage, HouseholdItemDetailPage, MilestoneDetailPage, errorTranslation) — 227/233 passing, 6 pre-existing unrelated todo skips, zero failures
  • Pre-commit hook quality gates pass (CI)

Co-Authored-By: Claude dev-team-lead (Sonnet 4.6) noreply@anthropic.com
Co-Authored-By: Claude frontend-developer (Haiku 4.5) noreply@anthropic.com
Co-Authored-By: Claude translator (Sonnet 4.5) noreply@anthropic.com
Co-Authored-By: Claude qa-integration-tester (Sonnet 4.5) noreply@anthropic.com

…parity guard test

- usePhotos.ts now routes API errors through translateApiError()/t() instead
  of hardcoded English strings (ApiClientError -> errors namespace by code,
  NetworkError/unexpected -> new photoViewer.networkError/unexpectedError
  keys, translated in both en and de).
- Removed 5 orphaned photoAnnotator keys (reset/resetTitle/resetBody/
  resetConfirm/resetComplete) left over from a "reset to original photo"
  feature that was replaced by PhotoViewer's "Clear annotations" entry
  point; also removed the associated dead CSS rules (.resetButton,
  .modalActions, .confirmButton).
- Fixed 5 pre-existing literal duplicate JSON keys (same key twice in the
  same object, silently shadowed by JSON.parse's last-wins behavior) across
  diary.json, householdItems.json, and schedule.json in both locales.
  Verified behavior-preserving: the shadowed (first) occurrence was already
  unreachable at runtime in every case.
- Added client/src/i18n/i18n.parity.test.ts: a generalized en/de key-parity
  test across all 14 namespaces plus a raw-text duplicate-key guard across
  all 28 locale files, so both bug classes fail CI instead of accumulating.

Fixes #1814

Co-Authored-By: Claude dev-team-lead (Sonnet 4.6) <noreply@anthropic.com>
Co-Authored-By: Claude frontend-developer (Haiku 4.5) <noreply@anthropic.com>
Co-Authored-By: Claude translator (Sonnet 4.5) <noreply@anthropic.com>
Co-Authored-By: Claude qa-integration-tester (Sonnet 4.5) <noreply@anthropic.com>
@steilerDev

Copy link
Copy Markdown
Owner Author

[security-engineer] Security review of #1846 (fix/1814-i18n-parity-usephotos → beta)

Scope confirmed: client-only i18n hygiene (dead-key removal, duplicate-JSON-key fixes, dead CSS rule removal) plus a usePhotos error-translation change and a new parity guard test. No server changes, no new dependencies.

Security-relevant change reviewed: client/src/hooks/usePhotos.ts

Previously:

setError(err.error.message ?? 'Failed to load photos.');

Now:

setError(translateApiError(err.error.code, tErrors));

Verified translateApiError (client/src/lib/errorTranslation.ts:3-10) resolves strictly from the machine-readable code — it looks up the code in the errors i18n namespace, and if absent, falls back to humanizing the code string itself (e.g. SOME_UNKNOWN_CODE"Some Unknown Code"). The raw server-supplied message field is never read or displayed anywhere in this path. This confirms the stated security benefit: any free-text detail an API error response might carry in message (stack fragments, internal identifiers, SQL/file-path leakage, etc.) can no longer reach the UI via this hook — only the bounded, developer-controlled code enum surfaces. This is a net reduction in information-disclosure surface (OWASP A05 — Security Misconfiguration / verbose error messages), consistent with the existing translateApiError pattern already used elsewhere in the app (per prior audit).

Also checked:

  • NetworkError / unknown-error branches now use translated static strings (t('networkError'), t('unexpectedError')) instead of hardcoded English — no new data flow, no injection surface.
  • Remaining diff is JSON locale files (removing orphaned keys tied to a deleted "reset to original photo" feature, and fixing duplicate-key shadowing in diary.json/householdItems.json/schedule.json) and a new client/src/i18n/i18n.parity.test.ts (static filesystem/object comparisons only, no user input, no runtime app code).
  • Corresponding dead CSS rules (.resetButton, .modalActions, .confirmButton) removed from PhotoAnnotator.module.css — cosmetic cleanup only, no security implication.
  • No dangerouslySetInnerHTML/innerHTML/eval introduced. No new dependencies (package.json/lockfiles untouched). No auth/authorization code touched.

Findings: None. No Critical/High/Medium/Low issues identified.

Verdict: APPROVED

@steilerDev

Copy link
Copy Markdown
Owner Author

[product-architect] APPROVED

Architecture/contract review of the i18n hygiene changes. No API, schema, shared-type, or ADR impact — this is client-side i18n + a hook. Verified the following:

Remove-vs-wire decision (orphaned photoAnnotator keys) — correct. The 5 reset* keys were dead: zero t('photoAnnotator...reset...') references in source, the component already dropped the reset entry point (PhotoAnnotator.test.tsx asserts annotator-reset is not in the document), and the superseding capability already exists and is wired (photoViewer.clearAnnotations* in PhotoViewer.tsx). Removing dead keys rather than re-wiring a superseded feature is the right call. Associated dead CSS (.resetButton, .modalActions, .confirmButton) was scoped to PhotoAnnotator.module.css and unreferenced by the component — the same-named classes elsewhere are independent CSS-Module-scoped declarations, so removal is safe.

Duplicate-key removals — behavior-preserving. All 5 removed keys were the earlier occurrence of a same-scope duplicate (schedule.detail.edit/view, diary.create.title/diary.edit.title, householdItems.detail.delete). Since JSON.parse is last-wins, those earlier occurrences were already unreachable at runtime; the surviving siblings are confirmed present. Runtime output is unchanged, so no rendered-text regression (and by extension no E2E text-assertion risk) can arise from these removals. de/en removals are symmetric.

usePhotos error pattern — consistent with the established convention. Matches InvoiceDepositsSection exactly: translateApiError(err.error.code, tErrors) for ApiClientError, namespace-local t() for the non-API branches. It refines the pattern slightly by splitting NetworkError (networkError) from the generic fallback (unexpectedError) — an improvement, still consistent. The translateApiError humanize fallback for unknown codes is covered by a new unit test. New keys added to both en and de.

Guard test design — sound. The flatten parity check plus the recursive-descent findDuplicateKeys scanner (correctly avoiding the {{interpolation}} false-positive that a naive brace-counter hits, and self-tested) close both bug classes going forward. The readdirSync cross-check against NAMESPACES.length is the key safety net catching "added a namespace file, forgot the guard."

Informational (non-blocking):

  • The hand-maintained NAMESPACES array is cross-checked against files on disk but not against the resources/ns registration in i18n/index.ts. A file could be present + in NAMESPACES yet unregistered in index.ts and still pass. Low risk (the comment flags the sync requirement, and importing index.ts would trigger i18n.init() side effects, so re-declaration is a reasonable tradeoff) — worth a follow-up only if namespace churn increases.
  • E2E shards 3/4/14 are red, but Quality Gates (the only required beta gate) is green and full E2E is non-blocking for beta. These shards match the known diary-E2E flake profile and are not reachable from this diff (behavior-preserving JSON + a hook). Recommend a quick triage confirmation before promotion, but not a blocker for this beta PR.

@steilerDev

Copy link
Copy Markdown
Owner Author

[ux-designer]

Reviewed the client diff (git diff origin/beta..HEAD) against the design system and script-verified the two risk areas called out: duplicate-key removal correctness and dead-CSS removal correctness.

1. Duplicate-key removals — verified behavior-preserving

Parsed origin/beta copies of diary.json, householdItems.json, schedule.json (en+de) with a duplicate-key-detecting JSON loader (object_pairs_hook) to reproduce exactly what JSON.parse/webpack's JSON loader resolves for each duplicate (last-wins), then cross-referenced the winning value against actual t() call sites in the consuming components:

  • schedule.json milestones.detail: two edit keys (string "Edit" vs. object {title, form}) and two view keys existed. MilestoneDetailPage.tsx uses detail.view.targetDate / .projectedDate / .description / .completedAt / .status.completed / .status.pending — these all resolve to the second (surviving, last-wins) view block, which is exactly the one the PR kept. The first view block (with linkedItems, workItem, noItems, searchPlaceholder, etc. — now removed) was already dead: the component consumes those concepts via sibling top-level keys (detail.linkedItems, detail.workItem, detail.noItemsLinked, detail.searchPlaceholder, ...), never detail.view.linkedItems. Confirmed behavior-preserving.
  • householdItems.json detail.delete: first occurrence was a bare string "Delete" (unused — no t('detail.delete') call), second was the confirm-modal object. HouseholdItemDetailPage.tsx calls detail.delete.delete, .confirm, .message, .warning, .cancel, .deleting — all resolve correctly to the surviving object. Confirmed behavior-preserving.
  • diary.json create.title / edit.title: both duplicate incidents. Traced DiaryEntryCreatePage.tsx and DiaryEntryEditPage.tsx and found neither page heading uses create.title/edit.title at all — headings use createPage.title/editPage.title (separate top-level keys), and form field labels use entryForm.title (also separate). So both the removed (shadowed) value and the surviving value in this key path were already unreachable — removing the dead first occurrence is a no-op either way. Confirmed behavior-preserving (no regression either way).

All matches what CI's new i18n.parity.test.ts would catch going forward. Ran it locally: 46/46 passing.

Informational, non-blocking, out of scope for this PR: the surviving schedule.json detail.edit object (title/form.*) and diary.json create.title/edit.title keys themselves appear to be genuinely orphaned (never referenced by any t() call), not just their now-removed duplicate siblings. This PR's scope was duplicate-key dedup, not exhaustive orphan-key sweep (that was handled separately and explicitly for the 5 photoAnnotator keys) — flagging for a future cleanup pass, not a finding against this PR.

2. Dead CSS removal — verified unreferenced

PhotoAnnotator.module.css is only imported by PhotoAnnotator.tsx (CSS Modules are locally scoped, so grep hits for modalActions/confirmButton elsewhere in the app belong to unrelated components' own modules). Grepped PhotoAnnotator.tsx for resetButton, modalActions, confirmButton — zero references. The pre-existing PhotoAnnotator.test.tsx already asserts queryByTestId('annotator-reset') is not in the document, consistent with the feature having been removed before this PR (this PR is cleaning up the leftover dead CSS/i18n keys, not removing a live feature). No visual impact.

3. German error strings — quality check

photoViewer.json networkError / unexpectedError:

  • EN: "Network error: Unable to connect to the server." / "An unexpected error occurred."
  • DE: "Netzwerkfehler: Verbindung zum Server nicht möglich." / "Ein unerwarteter Fehler ist aufgetreten."

Both match established phrasing already used elsewhere in the app (errors.json INTERNAL_ERROR, budget.json revertNetworkError/stateConfirmNetworkError use the same "Netzwerkfehler" / "Ein unerwarteter Fehler ist aufgetreten" constructions). Grammatically correct, terminology-consistent, no glossary concerns.

Other checks

  • errorTranslation.ts's translateApiError fallback (humanized unknown code) is pre-existing, reused correctly, not modified.
  • No hardcoded colors/spacing/tokens touched — this PR has zero visual/CSS-value changes beyond the two dead-rule removals above.
  • No dark mode, responsive, or accessibility surface touched.

No findings that warrant blocking.

Verdict: APPROVED

@steilerDev steilerDev merged commit 58b8379 into beta Jul 7, 2026
27 of 32 checks passed
@steilerDev steilerDev deleted the fix/1814-i18n-parity-usephotos branch July 7, 2026 19:24
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 2.13.0-beta.15 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant