# Fleet Ops final localization — gap audit Branch: `fix/fleet-ops-final-i18n-ux` (created from `master` @ `f7805579f7c73bd3085d73a725fa985b4a4892ed`, working tree clean at audit time). Deployed revision on Unraid at audit time: `de0bdea84fea01b4501deb7099107bc753c2e6d7` (the merge commit; `f780557` is an evidence-only commit not separately deployed). Both containers healthy. ## 1. Remaining untranslated/incorrect text ### nl-BE | File | Key | Current | Fix | |---|---|---|---| | `audit.json` | `title` | "Audit trail" | "Auditgeschiedenis" | | `audit.json` | `columns.actor` | "Actor" | "Uitvoerder" | | `navigation.json` | `items.audit` | "Audit trail" | "Auditgeschiedenis" | | `auth.json` | `exploreAsOperationsManager` | "Verken als Operations Manager" | "Verken als Operationsmanager" | | `auth.json` | `exploreAsRentalEmployee` | "Verken als Rental Employee" | "Verken als Verhuurmedewerker" | | `auth.json` | `roleOperationsManager` | "Operations manager" | "Operationsmanager" | | `auth.json` | `roleRentalEmployee` | "Rental employee" | "Verhuurmedewerker" | | `demo.json` | `scenarios.roles.operations_manager` | "Operations Manager" | "Operationsmanager" | | `demo.json` | `scenarios.roles.rental_employee` | "Rental Employee" | "Verhuurmedewerker" | | `demo.json` | `scenarios.startScenario` | "Start scenario" | "Scenario starten" | | `integrations.json` | `ledger.filterRecent` | "Recent" | "Recentste" | | `quality.json` | `list.statusOpen` | "Open" | "Openstaand" | | `audit.json`, `quality.json`, `integrations.json`, `demo.json` | 8 `managerOnly`/`whyItMatters`/`scopeBody`/etc. keys (16 nl+fr occurrences) | embedded "Operations Manager(s)" mid-sentence | "Operationsmanager(s)" | `columns.details` ("Details") judged fine as-is: short data-table column header, genuine NL/EN cognate, siblings are single-word labels too. ### fr-BE Same key set as nl-BE (role labels + embedded mentions), fr-BE `columns.actor` is already correctly "Acteur" (no fix needed). French role translations: "Responsable des opérations" / "Collaborateur de location", per the correction brief. ### Hardcoded JSX (bypasses i18n entirely) `frontend/src/pages/DataQualityIssueDetail.tsx` line ~213: `data-label="Field"` — literal English, never localized. Fix: reuse the already-existing, already-translated `detail.duplicateCustomer.fieldColumn` key (same table's `` seven lines above already uses it correctly) — zero locale-file changes needed, pure JSX fix. No other hardcoded `data-label`/`aria-label`/`title`/`placeholder` found across `frontend/src/**/*.tsx`. ## 2. Raw backend errors shown directly 13 call sites across 7 files (`Automation.tsx`, `ReturnForm.tsx` ×2, `DataQuality.tsx`, `DemoGuide.tsx`, `Layout.tsx`, `DataQualityIssueDetail.tsx` ×7, `Knowledge.tsx`) all follow: `err instanceof ApiError ? err.message : t("some:fallback")` — i.e. the **common** case (a real, structured `ApiError` from the backend) shows raw, un-localized English `error.message` verbatim; the translated fallback only fires for network-level failures where no `ApiError` could even be constructed. One site (`DataQualityIssueDetail.tsx` apply-status handler) already special-cases `err.code === "RECOMMENDATION_STALE"` inline — this needs migrating into the new central system rather than staying a one-off. Backend `AppError`/`HTTPException` codes found (32 semantic `AppError` codes + generic HTTP-status fallback codes "401"/"403"/"404"/"422" for plain `HTTPException`s, verified via `app/main.py`'s `error_body()` envelope — both AppError and HTTPException responses share the same `{"error": {"code", "message", "correlation_id"}}` shape): `BOOKING_NOT_ACTIVE, BOOKING_NOT_FOUND, CONFLICT_STILL_PRESENT, CORRECTED_VALUE_REQUIRED, CORRECTION_BELOW_CANONICAL, CUSTOMER_NOT_FOUND, EMPTY_VALUE, ENTITY_NOT_FOUND, EVENT_NOT_FOUND, IDEMPOTENCY_KEY_REUSED, INVALID_BOOKING_REFERENCE, INVALID_BOOKING_STATE, INVALID_EVENT_ID, INVALID_FIELD, INVALID_FIELD_OVERRIDE, INVALID_IDEMPOTENCY_KEY, INVALID_SURVIVOR, ISSUE_NOT_FOUND, ISSUE_NOT_OPEN, MANUAL_REVIEW_REQUIRED, NOT_AN_ODOMETER_ISSUE, NOT_AN_OVERLAP_ISSUE, NOT_A_DUPLICATE_ISSUE, NOT_A_MISSING_FIELD_ISSUE, NOT_A_STATUS_CONFLICT_ISSUE, NOT_RETRYABLE, NO_CONFLICT_DETECTED, NO_FIELDS_PROVIDED, OVERLAP_STILL_PRESENT, RECOMMENDATION_STALE, UNAUTHORIZED_SERVICE, UNSUPPORTED_ENTITY, VEHICLE_NOT_FOUND`. Only one code (`INVALID_SURVIVOR`) carries structured `details` params; the rest embed specifics only in the raw English `message` string — so localized messages will be generic per-code (title + explanation + optional next step), not parameterized with extracted specifics, with the raw string preserved verbatim under "Technical details". `errors.json` namespace already exists (all 3 locales) with 6 generic keys (`generic`, `workspaceLoadFailed`, `unauthorized`, `forbidden`, `notFound`, `networkUnavailable`) but is not wired to `ApiError.code` at all — only used as the non-`ApiError` fallback string. ## 3. i18n allowlist over-permissiveness `frontend/e2e/i18n-coverage.spec.ts`'s `IDENTICAL_VALUE_ALLOWLIST` currently contains 4 entries that must be removed once role labels are translated: `auth.roleOperationsManager`, `auth.roleRentalEmployee`, `demo.scenarios.roles.operations_manager`, `demo.scenarios.roles.rental_employee` (comment: "deliberately-untranslated role title" — no longer true once fixed). `audit.title` and `navigation.items.audit` ("Audit trail" kept as compliance term) also need removing once translated to "Auditgeschiedenis". Remaining ~19 allowlist entries are genuine cognates/proper nouns/templates (verified by the audit agent against a broad Dutch-word grep of fr-BE — zero Dutch leakage found) and should stay. Also noted: the coverage test's identical-value check only catches **whole-string** identity to en-GB, not **mid-sentence embedded English** (the 16 "Operations Manager(s)" occurrences above) — this is a real blind spot the new tests (section 8 of the correction brief) need to close with a targeted, explicit check for known English substrings appearing in nl-BE/fr-BE prose. ## 4. Dashboard greeting No time-of-day logic exists anywhere in the codebase — `dashboard.json`'s `title` key is a **static** string ("Good morning. Here's the fleet." / "Goedemorgen. Hier is je wagenpark." / "Bonjour. Voici votre flotte.") shown unconditionally at all times of day, despite implying dynamism. Needs: a central, testable, clock-injectable greeting function keyed on `Europe/Brussels` wall-clock hour, 4 periods per the brief, updating on language change and on period rollover while the app stays open. ## 5. Documentation staleness - `PROJECT_STATE.md` "Locked decisions" block: `"Product name: MobilityOps."` and `"PoC only..."` — predates the Fleet Ops rebrand, contradicts the later (correct) sections of the same file. - `PROJECT_STATE.md`'s final section header still reads `"...IN PROGRESS on fix/fleet-ops-i18n-status-flow"` and states `"Not yet merged to master"` / `"Do not claim PASS..."` — **false**: the merge (`de0bdea`) and final evidence commit (`f780557`) both already exist in git history, neither is mentioned in the file, and the branch name has moved on to `fix/fleet-ops-final-i18n-ux`. - A separate, older `"## Demo productization (in progress, same branch feat/mobilityops-functional-completion)"` section header was also never marked complete. - README.md is accurate and current — no fix needed there beyond a version-count refresh after this round's test additions. - No false "RAGcore live" / "MCP Hub connected" claims found anywhere — this part is already honest. ## Plan 1. Fix the ~10 nl-BE + ~10 fr-BE locale-file translations above (role labels, "Audit trail", "Actor", "Start scenario", "Recent", "Open", embedded mid-sentence mentions). 2. Fix the one hardcoded `data-label="Field"` JSX bug. 3. Build a central `describeApiError(t, err)` helper + shared rendering component, wire all 13 call sites through it, with a code→message map covering all 32 backend codes + generic HTTP fallbacks, raw text demoted to "Technical details". 4. Tighten the allowlist (remove the 6 now-stale entries) and add a targeted embedded-English-substring test, a `describeApiError` coverage test, and greeting boundary tests. 5. Build the time-of-day greeting function + wire into `Dashboard.tsx`, with matching locale copy for 4 periods × 3 languages + a localized description line replacing the current static one. 6. Fix `PROJECT_STATE.md` staleness (append a new dated entry, do not rewrite prior entries). 7. Full local validation → clean-checkout drill → deploy fix branch → live validation in 3 languages → merge to master → redeploy → final evidence.