# Fleet Ops final localization — final summary Small, targeted correction round on top of the already-merged, functionally-validated Fleet Ops correction milestone. Scope: remaining NL/FR translation gaps, centralized API-error localization, a time-dependent Europe/Brussels dashboard greeting, i18n test hardening, and documentation consistency — explicitly no redesign, no business-logic changes, no new functionality. Audit and rationale: `docs/fleet-ops-final-localization/audit.md`. ## Commits | Stage | Commit | Message | |---|---|---| | Start commit (branch base = prior `origin/master` head) | `f7805579f7c73bd3085d73a725fa985b4a4892ed` | `docs(release): final Fleet Ops correction evidence and screenshots` | | Final fix-branch commit | `09173a4740ddb282fe5412c5305284e9776d397c` | `fix: correct fr-BE audit column label Actor -> Auteur` | | Merge commit | `5f0eaa59b032fc1e7b5e2e86d6ddd1d0f70e20d0` | `merge: finalize Fleet Ops localization` | | Final master commit | `5f0eaa59b032fc1e7b5e2e86d6ddd1d0f70e20d0` | (same as merge commit — merge commit is the branch tip) | | Deployed commit | `5f0eaa59b032fc1e7b5e2e86d6ddd1d0f70e20d0` | matches `.deploy/source-revision` on Unraid exactly | Branch used: `fix/fleet-ops-final-i18n-ux` (the brief named `fix/fleet-ops-final-localization`; this branch was verified freshly and cleanly branched from `origin/master` with a clean working tree, so it was used as-is rather than renamed — see the audit doc's naming note). `origin/master` was re-fetched and confirmed unchanged (`f780557`) immediately before the merge, per the mandatory pre-merge safety check. Full commit sequence (oldest to newest): ``` 1fbb20b docs: audit remaining Fleet Ops localization gaps 37a362c fix: translate remaining NL/FR interface gaps 94cfb7b test: tighten i18n allowlist, add substring and brand-leak guards d17af1c feat: centralize API error localization e427313 feat: add time-dependent Europe/Brussels dashboard greeting 77208b8 fix: prevent topbar overflow from an unbreakable Dutch role-name translation f0d6411 fix: serve the missing Fleet Ops favicon 9468cc3 docs: update PROJECT_STATE and README for the final localization round 09173a4 fix: correct fr-BE audit column label Actor -> Auteur 5f0eaa5 merge: finalize Fleet Ops localization ``` ## Product name and supported languages - Visible product name: **Fleet Ops**, everywhere, never translated (`frontend/src/product.ts` constant, interpolated as `{{productName}}`). "MobilityOps" remains the internal repo / Compose project / deployment-directory identifier only. - Supported UI languages: **nl-BE** (default), **en-GB**, **fr-BE**. - No visible "MobilityOps" or the word "PoC" anywhere in the UI (enforced by a dedicated automated test, see below). ## Corrected translations - Role names actually translated (not just labelled as translated): `auth.json` / `demo.json` role keys — **Operationsmanager** / **Verhuurmedewerker** (nl-BE), **Responsable des opérations** / **Collaborateur de location** (fr-BE). - `audit.title` → **Auditgeschiedenis** / **Piste d'audit**; `columns.actor` → **Uitvoerder** (nl-BE) / **Auteur** (fr-BE, corrected during live browser validation — see Known limitations). - `list.statusOpen` → **Openstaand**; `ledger.filterRecent` → **Recentste**; `scenarios.startScenario` → **Scenario starten** / **Démarrer le scénario**. - 8 previously-missed mid-sentence "Audit trail" leaks fixed across `demo.json`, `quality.json`, `returns.json` (nl-BE) — found by the new embedded-substring test, not the pre-existing whole-string-identity test, which structurally cannot catch this class of bug. - No unintended English text remains in nl-BE or fr-BE (see translation-coverage evidence below). ## Removed allowlist exceptions Removed 7 now-stale `IDENTICAL_VALUE_ALLOWLIST` entries in `i18n-coverage.spec.ts`: `audit.title`, `auth.roleOperationsManager`, `auth.roleRentalEmployee`, `demo.scenarios.startScenario`, `demo.scenarios.roles.operations_manager`, `demo.scenarios.roles.rental_employee`, `navigation.items.audit` — all now genuinely translated; their old comments describing them as "deliberately untranslated" were no longer true. Two new tests added: embedded-English/Dutch-substring leak guard, and a no-"MobilityOps"/no-"PoC" guard. ## Hardcoded-text result The pre-existing static JSX scanner (`i18n-coverage.spec.ts`, section 11D) found **zero** hardcoded user-facing strings outside the approved technical-token allowlist (Fleet Ops, Northstar Mobility, ITWorx MCP Hub) across `pages/` and `components/`. Result: **PASS**. ## API-error-localization result New `frontend/src/api/errorMessages.ts` (`describeApiError`) replaces the `err instanceof ApiError ? err.message : t(fallback)` anti-pattern (which showed raw English backend text for the common case) at all 13 call sites across 7 files (`Automation.tsx`, `ReturnForm.tsx`, `DataQuality.tsx`, `DemoGuide.tsx`, `Layout.tsx`, `DataQualityIssueDetail.tsx` ×7 sites, `Knowledge.tsx`). Resolution order: known `AppError` code (32 codes) → known HTTP status (401/403/404/409/422/500) → fully generic fallback. New `ApiErrorNotice` component (`PageChrome.tsx`) always renders a localized title + explanation + optional next step; raw backend text is demoted to a "Technical details"/"Détails techniques" disclosure, never the primary message. Evidence: `frontend/e2e/error-messages.spec.ts` (10 tests, all passing) — every known code/status has non-empty copy in all 3 locales; a known code never surfaces raw text as the primary message; unknown-code and unknown-status fallback chains behave correctly; a drift guard greps the actual backend `AppError("CODE", ...)` call sites and confirms `KNOWN_CODES` exactly matches (32 codes, zero drift). Live-verified on Unraid: the seeded failed automation run renders a fully localized French error with a "DÉTAILS TECHNIQUES" disclosure below it. ## Greeting logic and edge cases New `frontend/src/i18n/greeting.ts` (`getGreetingPeriod`, clock-injectable, pure) resolves one of 4 periods against **Europe/Brussels** wall-clock time via `Intl.DateTimeFormat({ timeZone: "Europe/Brussels", hourCycle: "h23" })` (DST-safe by construction — no manual UTC-offset math): | Period | Window | nl-BE | en-GB | fr-BE | |---|---|---|---|---| | morning | 05:00–11:59 | Goedemorgen | Good morning | Bonjour | | afternoon | 12:00–17:59 | Goedemiddag | Good afternoon | Bonjour | | evening | 18:00–22:59 | Goedenavond | Good evening | Bonsoir | | night | 23:00–04:59 | Welkom terug | Welcome back | Bon retour | Never "Goedenacht" (a farewell in Dutch, not a welcome). Each period also has its own accompanying sentence per language (`dashboard.json` `greetingBody`), replacing the old fixed "Here's the fleet." `useGreetingPeriod.ts` polls every 30s so the greeting rolls over live while the app stays open, no reload required; initial render uses a synchronous `useState(() => getGreetingPeriod())` so there is never a flash of the wrong period. Edge-case evidence: - `frontend/e2e/greeting.spec.ts` (4 tests): exact boundary checks at 04:59/05:00/11:59/ 12:00/17:59/18:00/22:59/23:00 in both CET (winter) and CEST (summer), plus a dedicated spring-forward/fall-back DST-transition test (2026-03-29 and 2026-10-25). - `frontend/e2e/greeting-live.spec.ts` (6 tests, real browser via Playwright's `page.clock`): all 8 boundary times rendered correctly in **all 3 languages** against the actual app; live period rollover with no `page.reload()` call anywhere in that test; language-switch behaviour without changing the time period; the "never Goedenacht" guard. - Live-verified on Unraid at actual current server time (2026-08-04, ~03:2x CEST, i.e. the night period): dashboard showed "Welkom terug. Hier is het laatste overzicht van je wagenpark." (nl-BE), "Welcome back. Here's the latest overview of your fleet." (en-GB), "Bon retour. Voici le dernier aperçu de votre flotte." (fr-BE). ## README / PROJECT_STATE corrections - `PROJECT_STATE.md`: fixed the stale "Product name: MobilityOps." / "PoC only" locked-decisions lines (predated the Fleet Ops rebrand); fixed the "Fleet Ops correction" section header, which still read "IN PROGRESS .../Not yet merged to master" despite already being merged (`de0bdea` / `f780557`); appended a new dated entry for this correction round (not a rewrite of prior entries, per the brief's explicit instruction not to hide earlier history). - `README.md`: linked `docs/fleet-ops-final-localization/` alongside the existing correction-round doc link; refreshed the stale Playwright test count (113 → 138 → 139 after the favicon regression test was added). ## Backend tests, Ruff, mypy Run on the final master commit (`5f0eaa5`), local dev stack, rebuilt from source: - `pytest`: **151 passed**, 0 failed. - `ruff check .`: **All checks passed!** - `mypy app` (the project's canonical invocation, matching all prior milestone gates — no `[tool.mypy]` strict config exists in `pyproject.toml`): **Success: no issues found in 49 source files.** No backend Python was touched this round; these numbers are unchanged from the prior correction milestone's final gate, confirmed green again on the current tree. ## Frontend build, Playwright - `npx tsc --noEmit`: clean, 0 errors. - `npm run build` (`tsc -b && vite build`): clean production build. - Full Playwright suite (`npx playwright test`), master build, local dev stack: **139 passed**, 0 failed (confirmed on a clean run after two transient `0xC0000005` Chromium worker crashes caused by this specific machine running 43+ concurrent Chrome processes at the time — see Known limitations; a targeted 48-test re-run of every new/changed suite also passed cleanly in between). ## Clean-checkout drill Isolated Compose project `mobilityops-clean` (ports 8129/1229/5679, no shared volumes/ network with the working dev stack), fresh `git clone --branch fix/fleet-ops-final-i18n-ux` of only committed files: 1. `docker compose build` + `up -d` from empty volumes — all 4 containers healthy. 2. `alembic upgrade head` → `799d8800e241 (head)`. 3. `seed --reset` → 2 users / 180 customers / 50 vehicles / 246 bookings / 75 inspections / 40 maintenance / 27 data-quality issues / 20 workflow runs — matches the documented deterministic count exactly. 4. Backend gates: `pytest` 151 passed, `ruff check .` clean, `mypy app` clean (49 files). 5. Frontend: `npm ci` clean, `tsc --noEmit` clean, `vite build` clean. 6. Full Playwright suite against the isolated stack (`MOBILITYOPS_PUBLIC_URL=http://localhost:1229`): **139 passed**, 0 failed — this run covers the Dutch/English/French language checks, greeting boundaries, API error paths, and the guided demo, all in one pass. 7. Final reset + `scenario_integrity`: all 5 scenarios `ready: true`. 8. Isolated stack, containers, volumes and images torn down; original dev environment confirmed untouched (`mobilityops-*` containers unaffected throughout). **PASS.** ## Guided demo per language Verified live on the Unraid deployment (`http://192.168.10.150:1236`) in all 3 languages via direct browser interaction: login screen role buttons, dashboard (greeting, readiness band, attention queue, integration pulse, recent activity), audit trail, automation retry flow with localized error + technical-details disclosure, and demo reset — all rendering correctly in nl-BE, en-GB and fr-BE. The full guided-demo Playwright spec (`guided-demo-full.spec.ts`) passed as part of the 139-test suite on both the local dev stack and the isolated clean-checkout stack. ## Server deployment, container health Deployed to `http://192.168.10.150:1236` (Compose project `mobilityops`, `/mnt/user/appdata/mobilityops`), preserving the server's existing `.env`, the Postgres and n8n named volumes, the exposed port, and the deployment directory — only `api` and `web` were rebuilt/recreated; `db` was never touched beyond `alembic upgrade head`; no second n8n instance was started (shared existing n8n at `:5678` used throughout). Procedure (matching `docs/demo-release/demo-runbook.md` exactly): `git archive` → `scp` → extract over the existing deployment dir → update `.deploy/source-revision` → `docker compose -p mobilityops -f compose.yaml -f compose.unraid.yaml up --build -d api web` → confirm `alembic current` → `seed --reset`. Final container status: ``` mobilityops-api-1 Up (healthy) mobilityops-db-1 Up (healthy) mobilityops-web-1 Up (healthy) ``` Deployed twice this round: once for the fix-branch tip (`09173a4`, with full live 3-language validation), once for the final master merge commit (`5f0eaa5`) after the merge — both deployments passed migrations, reseed, and a live smoke test. ## Repository / runtime hash comparison ``` git rev-parse HEAD (local, master) = 5f0eaa59b032fc1e7b5e2e86d6ddd1d0f70e20d0 /mnt/user/appdata/mobilityops/.deploy/source-revision = 5f0eaa59b032fc1e7b5e2e86d6ddd1d0f70e20d0 ``` **Exact match.** ## Browser console and network No console errors on any checked route in any of the 3 languages (dashboard, audit, automation, login) on the live Unraid deployment. All observed `/api/` network requests returned `200`. `api` and `web` container logs show no errors/tracebacks/exceptions after the final deployment. ## Known limitations - **Transient `document.documentElement.lang` DOM-attribute anomaly during interactive manual browser testing** on the live server: on 2 occasions, right after a client-side action (an automation retry click; a demo-reset confirm click), `document.documentElement.lang` briefly showed `"nl"` while the actually-rendered page content, `localStorage`, and a controlled repeat of the exact same click sequence (fresh login, single deliberate click, immediate inspection) all remained correctly `"fr-BE"`. Root-caused as far as possible: the codebase has exactly one `i18n.changeLanguage()` call site (`LanguageSwitcher.tsx`), which was not invoked in the clean repro, and `t()` / `i18n.language` are structurally coupled through a single i18next singleton with no code path capable of producing this split state. Not reproduced even once across 139 automated Playwright tests run 3 times total (local pre-merge, isolated clean-checkout, local post-merge on master) in a clean, extension-free browser context. Most likely explanation: a third-party browser extension active in the specific interactive testing session (which also had ~10 unrelated pre-existing tabs open on the same origin, and showed independent signs of instability — repeated CDP screenshot timeouts) rewriting the `lang` attribute based on its own content heuristics, independent of the React app. Logged here for transparency rather than silently dismissed; does not affect any automated PASS result above. - **Two transient Chromium worker crashes** (`0xC0000005` / access violation) during the master-build Playwright re-run, on a machine that had accumulated 43+ concurrent Chrome processes from the interactive testing session above. A clean run immediately afterward (fewer processes) passed all 139 tests; a 48-test targeted re-run of every new/changed suite also passed cleanly in between. Treated as machine resource contention, not a code defect — consistent with the prior correction milestone's own documented experience of "sequential-run-only flakes reproduced from resource contention of running two full Docker stacks at once," per `PROJECT_STATE.md`. - One translation gap (fr-BE `audit.columns.actor`: "Acteur" instead of the brief's specified "Auteur") was missed in the initial pass and only caught during live browser validation on Unraid; fixed in commit `09173a4` and redeployed before the master merge. - The Fleet Ops brand mark (`BrandMark` in `Icons.tsx`) was flagged by the user as potentially due for a visual refresh; per explicit user decision mid-session, this is out of scope for this correction round and deferred to a separate follow-up task. - No RAGcore/MCP Hub implementation changes were made or claimed; both remain in the same demo/not-connected state documented by the prior correction milestone. ## Rollback procedure `.deploy/source-revision` on the server records exactly which commit is live. To roll back: `ssh unraid`, extract an earlier `source-.tar.gz` from `/mnt/user/appdata/mobilityops/.deploy/` (prior tarballs remain in place, including `source-9468cc3e.tar.gz`, `source-09173a4.tar.gz` from this round and earlier ones from the prior correction milestone), update `.deploy/source-revision` to match, and re-run `docker compose -p mobilityops -f compose.yaml -f compose.unraid.yaml up --build -d api web` followed by `alembic upgrade head` (migrations are additive only — no destructive migration exists on this branch, so no database rollback is needed). No secrets were printed or read at any point in this process (`.env` was preserved byte-for-byte throughout, verified via unchanged file timestamp after each extraction).