docs(wiki): activity-log explainability, dates/i18n, KP-300H barcode fix

Record this session's work across the affected pages + three log entries.

- ticket-encoding: id 13→11 digits (guess-resistance rationale, legacy-safe
  validation) + a barcode-geometry rule (symbol dots must fit the narrowest
  deployed printer's line — the KP-300H 72mm overflow).
- rongta-printer: KP-300H raster-garbage root cause (line overflow, not
  corruption), sendRaw graceful-close fix, Albanian human dates (formatStampSq).
- i18n: localized ledger reason codes, relative/human dates + the
  "browser ICU lacks Albanian" gotcha, toggle stale-router-context fix.
- shift: Albanian Z-report, shift-history UI + permission scoping.
- booth-console: explainable activity log (inline reasons/badges, event-detail
  modal with snapshots + audit disclosure, subscriber names, failed-snapshot
  tiles).
- index/log updated; all added wikilinks resolve.

Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
2026-06-19 11:41:27 +02:00
parent bbf61c48df
commit 0074e82a2a
7 changed files with 154 additions and 20 deletions
+44 -8
View File
@@ -2,7 +2,7 @@
type: concept
tags: [parking, frontend, i18n, localization]
sources: []
updated: 2026-06-18
updated: 2026-06-19
status: open
---
@@ -40,16 +40,52 @@ any booth. (Decided + built 2026-06-18.)
key). Keys are dot-namespaced by area (`common`, `nav`, `status`, `auth`, `booth`, `pay`, `shift`,
`site`, `permits`, `tariff`).
- **Translated screens:** the booth ([[booth-console]] — screen, pay/exit modal, active sessions,
snapshots, status), Login, ShiftControl, SiteSettings, PermitManager, TariffComposer.
snapshots, status), Login, ShiftControl, SiteSettings, PermitManager, TariffComposer,
**SetupWizard devices tab** (2026-06-19 — the static chrome; driver/config-field labels still come
from the backend catalog), Users/Roles managers, and the **shift-history** screen.
## Localized ledger reasons & relative dates (2026-06-19)
Two patterns added on top of the catalog approach:
- **Reason codes (backend i18n for the signed ledger).** Anomaly/payment/override reasons used to be
free-text **English baked into the signed `payload.reason`** — unlocalizable at render time (the
bytes are immutable). Now the ledger signs a **stable `reasonCode` + `reasonParams`** (plus the
English `reason` as a fallback) from a closed set `REASON_CODES` in `@parking/shared`. The UI
translates `reason.<code>` via the sq/en catalogs, so an Albanian operator reads Albanian **from the
same immutable event**, and adding a language is a catalog change with **no re-signing** of past
events. Legacy events (no code) show the signed English fallback. This narrows — but doesn't close —
the "server API error strings are English" gap noted below: the *ledger* reasons are localized; raw
HTTP error strings still aren't. See [[append-only-event-chain]], [[booth-console]].
- **Relative + human dates (`formatRelativeDateTime`).** Sessions/logs/history show **`Sot/Today
10:48` · `Dje/Yesterday 17:33` · `17 Qershor/June 10:48`** instead of a bare time (a 2-day-old
session previously showed only `10:48`). today/yesterday come from `common.today`/`yesterday`.
> **Gotcha — the appliance browser's ICU has NO Albanian locale data.** `Intl.DateTimeFormat("sq",
> {month:"long"})` silently returns **English** ("June", not "Qershor") on this hardware. So month
> names come from a **`common.months` catalog array**, NOT `Intl`. Any future date formatting on
> this box must avoid relying on `Intl` for Albanian or it leaks English. (Printed slips already
> solved this with the `SQ_MONTHS` table in [[rongta-printer]].)
## Open / deferred
- **SetupWizard is NOT translated** (deliberate). Its content is mostly **server-provided** — driver
labels and config-field labels/help come from the backend device-catalog API ([[device-registry]],
[[first-run-setup]]). Translating only its static chrome would leave a half-English screen; it's
deferred until **backend catalog i18n** is scoped, then chrome + catalog localize together.
- **Server API error strings** are still English (surfaced raw in the UI). v1 relies on the
client mapping known errors; a fuller approach would translate by error *code*, not message.
- **SetupWizard chrome is now translated (2026-06-19)**; the remaining gap is the **backend
device-catalog** (driver + config-field labels/help from [[device-registry]], [[first-run-setup]]),
still English. Localizing the catalog is the open item.
- **Server API error strings** are still English (surfaced raw in the UI). The **ledger reason codes**
are now localized (above), but raw HTTP error messages aren't. A fuller approach translates by error
*code*, not message — the reason-code pattern is the template to follow.
- **Behaviour note (not a bug):** a *hard navigation* (new URL) re-bootstraps the language from the
user's stored preference via `/me` — so an un-persisted toggle resets. Correct: the stored pref
wins. The toggle persists via the PUT, so it survives once saved.
## Gotcha — language/theme toggle stale router-context (fixed 2026-06-19)
The header SQ/EN (and dark/light) toggles read the active value from `user`, which comes from the
**TanStack Router context** (`rootRoute.useRouteContext()`). Router context is **captured at
route-resolution time and does NOT re-render on `setUser`** — so after *one* switch the toggle's
`user.language` froze, the active-button highlight stuck, and the equality guard blocked switching
*back* until a page refresh (which re-resolved the context). Fix: drive the toggles off **live**
state, not the stale context — language reads `i18n.language` (`useTranslation()` subscribes to
i18next's `languageChanged`), theme uses local `useState`. Both still call `setUser` to keep the
context eventually-consistent + persisted, but no longer *depend* on it re-rendering. General lesson:
**router context is not reactive React state** — never read frequently-changing UI state from it.