- Add a single shared, pure vehicle-status evaluator (app/services/vehicle_status.py)
used identically by the data-quality scanner, a new non-mutating status-recommendation
preview endpoint, and a transactional apply endpoint with optimistic-concurrency token
revalidation -- eliminates the old opaque "calculate and apply" action and the unsafe
"maintenance + active booking -> auto rented" shortcut. Frontend
DataQualityIssueDetail.tsx now shows a review/decide/confirm panel with localized
why/evidence/consequence text in nl-BE/en-GB/fr-BE, with an exact "Change status to
<status>" confirm action per the brief.
- Fix MO-016 issue-order dependency: resolving the booking-overlap issue before vs.
after the status-conflict issue now converges on the same final vehicle status,
proven by test_mo_016_status_conflict_recommendation_is_order_independent.
- Make "Fleet Ops" a non-localizable brand constant (frontend/src/product.ts,
backend PRODUCT_NAME) via {{productName}} interpolation everywhere the brand name
appeared in locale prose; add a permanent test guarding against a translation file
ever defining the brand name or an "appName" key again.
- Convert dynamic backend prose to stable message codes + params: return status
reasons, audit field/actor-type labels, automation last_error, and search
section/vehicle/booking/issue results all now carry codes the frontend localizes,
with raw technical text demoted to a "Technical details" disclosure.
- docs/fleet-ops-correction/: gap audit, i18n inventory, and the vehicle-status
decision table documenting the evaluator's rules and safe-status principles.
148 backend tests + Ruff + mypy green; Alembic migration verified upgrade/downgrade;
frontend tsc/build and the i18n-coverage Playwright suite green.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
3.7 KiB
i18n inventory — dynamic/backend content requiring message-code treatment
Static UI chrome (navigation, dashboard, forms, filters, dialogs, empty/loading states,
Demo Guide, About page, accessibility labels) was already moved to the i18next
namespace system in the prior task and is not re-inventoried here in full — see
docs/final-product-polish/audit.md and docs/final-product-polish/i18n-inventory.md
for that pass. This inventory covers only the sources confirmed still bypassing
translation (per current-gap-audit.md), the fix chosen, and the tests that verify it.
| # | Source | Location | Static/dynamic | Fix | Tests |
|---|---|---|---|---|---|
| 1 | Duplicate-customer evidence signals | backend/app/services/data_quality.py::_scan_duplicate_customers |
dynamic | evidence.signals: [{code, params}] (duplicate.exact_email, duplicate.exact_phone, duplicate.same_postal_code, duplicate.similar_name + score param); frontend maps code→localized phrase |
test_data_quality.py::test_duplicate_customer_evidence_has_structured_signals; Playwright DQ evidence-language test |
| 2 | Missing-required-field evidence | _scan_missing_required_fields |
dynamic | evidence.signals: [{code: "missing_field", params: {field}}]; frontend maps field through the existing CUSTOMER_FIELD_LABELS/VEHICLE_FIELD_LABELS-equivalent i18n keys |
same |
| 3 | Booking-overlap evidence | _scan_booking_overlaps |
dynamic | evidence.signals: [{code: "overlap.reserved_bookings", params: {refs: [...]}}] |
same |
| 4 | Vehicle-status-conflict evidence | _scan_vehicle_status_conflicts |
dynamic | replaced entirely by the new evaluate_vehicle_status() evaluator's recommendation_code; scanner reuses the evaluator instead of its own reason strings |
new evaluator unit tests |
| 5 | Odometer-regression evidence | _scan_odometer_regressions |
dynamic | evidence.signals: [{code: "odometer.regression", params: {later_ref, later_km, earlier_ref, earlier_km}}] |
same |
| 6 | Status-recommendation reason | _recommend_vehicle_status / new evaluate_vehicle_status |
dynamic | recommendation_code + facts (structured), no free prose from the backend at all; frontend renders the full explanation from quality:statusRecommendation.codes.<code> |
evaluator unit tests + preview/apply contract tests |
| 7 | Return status reason | backend/app/services/returns.py::_derive_vehicle_status_with_reason |
dynamic | status_reason_code + params alongside the existing human string (kept for backward-compat, demoted to technical fallback) |
test_returns.py updated; Playwright return-flow-language test |
| 8 | Automation last_error |
dispatcher.py / seed data, rendered in Automation.tsx |
dynamic | known-cause codes (n8n.connection_timeout, n8n.http_error, etc.) mapped to a localized summary + "what happened / what's pending / what retry does"; raw string demoted to Technical details |
Playwright automation-language test |
| 9 | aria-label="Demo scenario" |
frontend/src/pages/BookingDetail.tsx:76 |
static, just un-wired | t("returns:scenario.ariaLabel"), 3 locales |
i18n-coverage (key parity) + route-matrix test |
| 10 | Brand name (common:appName + 8 embedded mentions) |
see gap audit §1 | static, wrongly translatable | PRODUCT_NAME constant, {{productName}} interpolation |
new brand-invariant test |
Message-code mapping module
Centralised in frontend/src/i18n/messageCodes.ts (new): a single resolveMessageCode(t, code, params) helper used by the Data Quality evidence renderer, the status-recommendation panel, the return-result panel, and the automation ledger, so there is one place mapping code → i18next key rather than per-page switch statements.