From ef8fbc9692944aeb04fb54c07dc1e69094a85cf4 Mon Sep 17 00:00:00 2001 From: Kawa Date: Wed, 15 Apr 2026 15:18:53 +0200 Subject: [PATCH] docs(12-i18n-bugfixes): create phase plan 2 plans: browser lang auto-detection + fix test_port_autofill (wave 1), full template i18n coverage across 13 templates (wave 2). Co-Authored-By: Claude Sonnet 4.6 --- .planning/ROADMAP.md | 11 + .../phases/12-i18n-bugfixes/12-01-PLAN.md | 136 +++++ .../phases/12-i18n-bugfixes/12-02-PLAN.md | 463 ++++++++++++++++++ 3 files changed, 610 insertions(+) create mode 100644 .planning/phases/12-i18n-bugfixes/12-01-PLAN.md create mode 100644 .planning/phases/12-i18n-bugfixes/12-02-PLAN.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 1d8dce1..6900d45 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -109,3 +109,14 @@ Full details: [`milestones/v1.0-ROADMAP.md`](milestones/v1.0-ROADMAP.md) | 9. UX Tech Debt Closure | 3/3 | Complete | 2026-04-13 | 2026-04-13 | | 10. Real-World Runtime Validation | v1.1 | 3/3 | Complete | 2026-04-13 | | 11. UI Enhancements | 4/4 | Complete | 2026-04-15 | | + +### Phase 12: i18n bugfixes — full translation coverage and browser language auto-detection + +**Goal:** All hardcoded UI strings in every template respond to the FR/EN language toggle; browser language auto-detected from navigator.language on first visit; E2E suite fully green. +**Requirements**: TBD +**Depends on:** Phase 11 +**Plans:** 2 plans + +Plans: +- [ ] 12-01-PLAN.md — Browser language auto-detection (navigator.language fallback) + fix test_port_autofill E2E +- [ ] 12-02-PLAN.md — Full template i18n coverage: wire all hardcoded strings across 13 templates to Alpine i18n store diff --git a/.planning/phases/12-i18n-bugfixes/12-01-PLAN.md b/.planning/phases/12-i18n-bugfixes/12-01-PLAN.md new file mode 100644 index 0000000..524d202 --- /dev/null +++ b/.planning/phases/12-i18n-bugfixes/12-01-PLAN.md @@ -0,0 +1,136 @@ +--- +phase: 12-i18n-bugfixes +plan: "01" +type: execute +wave: 1 +depends_on: [] +files_modified: + - imptune/templates/base.html + - tests/e2e/test_port_autofill.py +autonomous: true +requirements: [] + +must_haves: + truths: + - "When a user's browser is set to English (navigator.language starts with 'en'), the UI loads in English by default without touching the toggle" + - "When localStorage has no saved language preference, the browser's navigator.language is used as the initial language" + - "When localStorage does have a saved preference, it wins over navigator.language" + - "test_port_autofill passes in CI — navigates to /printers/new (not /printers)" + artifacts: + - path: "imptune/templates/base.html" + provides: "i18n store with navigator.language fallback" + contains: "navigator.language" + - path: "tests/e2e/test_port_autofill.py" + provides: "Fixed E2E test navigating to /printers/new" + contains: "/printers/new" + key_links: + - from: "imptune/templates/base.html" + to: "Alpine.store('i18n').lang" + via: "localStorage.getItem || navigator.language fallback" + pattern: "navigator\\.language" +--- + + +Fix the two remaining correctness bugs from Phase 11: (1) browser language auto-detection not honouring navigator.language when no localStorage preference exists, and (2) the pre-existing test_port_autofill E2E failure caused by Phase 11 moving the add-printer form to /printers/new. + +Purpose: Users whose browser is set to English should get the English UI on first visit, without manually clicking the toggle. The E2E suite should be fully green. +Output: Updated base.html i18n store, fixed test_port_autofill.py. + + + +@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md +@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/11-ui-enhancements/11-03-SUMMARY.md +@.planning/phases/11-ui-enhancements/deferred-items.md + + + + + + Task 1: Browser language auto-detection in i18n store + imptune/templates/base.html + + - When localStorage key 'imptune_lang' is absent and navigator.language starts with 'en', lang initialises to 'en' + - When localStorage key 'imptune_lang' is absent and navigator.language starts with 'fr' (or anything else), lang initialises to 'fr' + - When localStorage key 'imptune_lang' is 'fr', it wins over navigator.language === 'en-US' + - When localStorage key 'imptune_lang' is 'en', it wins over navigator.language === 'fr-FR' + + +In imptune/templates/base.html, inside the alpine:init script, update the i18n store's lang initialisation line. Currently: + + lang: localStorage.getItem('imptune_lang') || 'fr', + +Change to a function-based initialisation that checks navigator.language when localStorage is absent: + + lang: (() => { + const saved = localStorage.getItem('imptune_lang'); + if (saved) return saved; + return navigator.language && navigator.language.startsWith('en') ? 'en' : 'fr'; + })(), + +This is a single targeted change. Do not modify anything else in base.html. The toggle() method, translations object, and all x-text bindings remain unchanged. + +Note: Do NOT use a top-level property shorthand that would require Alpine to evaluate it lazily — the IIFE pattern evaluates at store creation time, which is the correct moment (stores are created inside alpine:init, before any hydration). + + + cd /c/Users/SebastienQUEROL/Documents/projets/ImpTune && python -m pytest tests/e2e/test_i18n_toggle.py -x -q 2>&1 | tail -10 + + + - base.html i18n store lang field uses IIFE with navigator.language fallback + - All 2 existing test_i18n_toggle.py tests still pass (they test toggle + persistence, not initial detection) + - Manual verification: open a fresh browser with no imptune_lang in localStorage; if browser language is English, nav shows "Drivers" not "Pilotes" + + + + + Task 2: Fix test_port_autofill navigating to /printers/new + tests/e2e/test_port_autofill.py + + - test_port_autofill navigates to /printers/new (not /printers) + - Typing an IP into input[name='ip_address'] auto-populates port_name with 'IP_192_168_1_100' + - Test passes on chromium + + +In tests/e2e/test_port_autofill.py, change the page.goto line: + + BEFORE: page.goto(f"{live_server}/printers", wait_until="domcontentloaded") + AFTER: page.goto(f"{live_server}/printers/new", wait_until="domcontentloaded") + +That is the only change needed. The input[name='ip_address'] and port_name assertions remain exactly as is — they already match the form markup in printers_new.html. + +Context: Plan 11-01 moved the add-printer form from /printers to /printers/new. The E2E test was deferred (logged in deferred-items.md) because it was a pre-existing failure at the time of 11-04 execution. Phase 12 is the correct place to fix it. + + + cd /c/Users/SebastienQUEROL/Documents/projets/ImpTune && python -m pytest tests/e2e/test_port_autofill.py -x -q 2>&1 | tail -10 + + + - test_port_autofill[chromium] passes + - The one-line URL fix is the only change in the file + + + + + + +Run the full E2E suite to confirm no regressions: +cd /c/Users/SebastienQUEROL/Documents/projets/ImpTune && python -m pytest tests/e2e/ -q 2>&1 | tail -15 + +All 7 E2E tests should pass (the 7th was the previously failing test_port_autofill[chromium]). + + + +- base.html i18n store reads navigator.language as fallback when localStorage is empty +- test_port_autofill[chromium] passes +- Full E2E suite: 7/7 passing +- Non-E2E test suite unchanged and passing + + + +After completion, create `.planning/phases/12-i18n-bugfixes/12-01-SUMMARY.md` + diff --git a/.planning/phases/12-i18n-bugfixes/12-02-PLAN.md b/.planning/phases/12-i18n-bugfixes/12-02-PLAN.md new file mode 100644 index 0000000..6b1a212 --- /dev/null +++ b/.planning/phases/12-i18n-bugfixes/12-02-PLAN.md @@ -0,0 +1,463 @@ +--- +phase: 12-i18n-bugfixes +plan: "02" +type: execute +wave: 2 +depends_on: ["12-01"] +files_modified: + - imptune/templates/base.html + - imptune/templates/dashboard.html + - imptune/templates/printers.html + - imptune/templates/printers_new.html + - imptune/templates/clients.html + - imptune/templates/client_detail.html + - imptune/templates/drivers.html + - imptune/templates/packages.html + - imptune/templates/printer_detail.html + - imptune/templates/partials/printer_list.html + - imptune/templates/partials/printer_edit_modal.html + - imptune/templates/partials/client_list.html + - imptune/templates/partials/driver_list.html +autonomous: false +requirements: [] + +must_haves: + truths: + - "Switching the language toggle causes ALL visible page labels (headings, buttons, table headers, form labels, empty states) to update immediately — no hardcoded English or French string remains" + - "Switching to French on dashboard shows 'Tableau de bord', 'Pilotes', 'Imprimantes' etc.; switching to English shows 'Dashboard', 'Drivers', 'Printers'" + - "All pages (dashboard, printers list, printers/new, clients, drivers, packages, printer detail, client detail) update when the toggle is clicked" + - "The non-E2E test suite still passes after the template changes" + artifacts: + - path: "imptune/templates/base.html" + provides: "Extended translation dictionary with all new keys" + min_lines: 150 + - path: "imptune/templates/dashboard.html" + provides: "All strings wired to x-text/$store.i18n.t()" + - path: "imptune/templates/printers_new.html" + provides: "All form labels wired to i18n" + - path: "imptune/templates/partials/printer_edit_modal.html" + provides: "All modal labels wired to i18n" + - path: "imptune/templates/partials/printer_list.html" + provides: "Table headers and buttons wired to i18n" + key_links: + - from: "any template" + to: "Alpine.store('i18n').t('key')" + via: "x-text binding or :title/:aria-label binding" + pattern: "store\\.i18n\\.t\\(" +--- + + +Wire every hardcoded UI string in every template to the Alpine i18n store, so switching the FR/EN toggle updates all labels, buttons, headings, and messages instantly across all pages. + +Purpose: Phase 11 implemented the toggle mechanism and translated nav links, but template bodies still contain hardcoded English. This plan completes the translation coverage. +Output: All templates fully wired; extended translation dictionary in base.html; passing test suite. + + + +@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md +@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/11-ui-enhancements/11-03-SUMMARY.md +@.planning/phases/12-i18n-bugfixes/12-01-SUMMARY.md + + + + + + + + + + +Existing translation keys already in base.html (FR + EN both): + dashboard, drivers, printers, clients, packages, + add_printer, add_client, printer_library, + edit, delete, save, cancel, + upload_driver, printer_name, ip_address, port_name, + driver, duplex_mode, one_sided, long_edge, short_edge, + color_mode, paper_size, collate, client, + edit_printer, no_printers, no_clients, + client_list, name, created, back_to_printers, + theme_label, lang_label + +Pattern for elements OUTSIDE an x-data parent: +

Dashboard

+ +Pattern for elements INSIDE an existing x-data parent: +

Printer Library

+ +Pattern for static fallback text (shown before Alpine hydrates): + Keep the English literal as the element's inner text — Alpine replaces it on hydration. + Example: +
+ + + + + Task 1: Add missing translation keys to base.html dictionary + imptune/templates/base.html + +Extend the translations.fr and translations.en objects in base.html with all keys needed by templates that currently have hardcoded strings. Add the following keys to BOTH fr and en objects: + +New keys to add (add to BOTH fr and en translation objects): + + // Dashboard page + new_printer: fr='Nouvelle imprimante', en='New Printer' + upload_driver_btn: fr='Télécharger un pilote', en='Upload Driver' + export_package: fr='Exporter un paquet', en='Export Package' + recent_activity: fr='Activité récente', en='Recent Activity' + recent_printers: fr='Imprimantes récentes', en='Recent Printers' + recent_packages: fr='Paquets récents', en='Recent Packages' + no_packages: fr='Aucun paquet exporté pour l\'instant.', en='No packages exported yet.' + // Note: no_printers key already exists + + // Printers new page + add_printer_title: fr='Ajouter une imprimante', en='Add Printer' + back_to_printer_library: fr='← Retour à la bibliothèque', en='← Back to Printer Library' + save_printer: fr='Enregistrer l\'imprimante', en='Save Printer' + upload_new_driver: fr='Télécharger un nouveau pilote', en='Upload New Driver' + no_driver_option: fr='-- Aucun pilote --', en='-- No driver --' + unassigned_option: fr='-- Non assigné --', en='-- Unassigned --' + + // Printer list table headers + th_name: fr='Nom', en='Name' + th_ip: fr='Adresse IP', en='IP Address' + th_port: fr='Port', en='Port' + th_driver: fr='Pilote', en='Driver' + th_duplex: fr='Recto-verso', en='Duplex' + th_color: fr='Couleur', en='Color' + th_paper: fr='Format', en='Paper' + th_collate: fr='Assemblage', en='Collate' + th_actions: fr='Actions', en='Actions' + yes: fr='Oui', en='Yes' + no: fr='Non', en='No' + no_driver_assigned: fr='—', en='—' + + // Clients page + client_name_label: fr='Nom du client', en='Client Name' + add_client_section: fr='Ajouter un client', en='Add Client' + client_list_section: fr='Liste des clients', en='Client List' + + // Client detail page + back_to_clients: fr='← Tous les clients', en='← All Clients' + printers_section: fr='Imprimantes', en='Printers' + + // Drivers page + drivers_title: fr='Pilotes', en='Drivers' + upload_driver_section: fr='Télécharger un package de pilote', en='Upload Driver Package' + driver_library: fr='Bibliothèque de pilotes', en='Driver Library' + uploading: fr='Téléchargement...', en='Uploading...' + upload_btn: fr='Télécharger', en='Upload' + driver_filename: fr='Nom du fichier', en='Filename' + driver_names_col: fr='Nom(s) du pilote', en='Driver Name(s)' + architecture: fr='Architecture', en='Architecture' + uploaded_at: fr='Téléchargé le', en='Uploaded' + unknown: fr='Inconnu', en='Unknown' + no_drivers: fr='Aucun pilote téléchargé.', en='No drivers uploaded yet.' + + // Packages page + packages_title: fr='Paquets', en='Packages' + packages_description: fr='Imprimantes avec pilotes assignés — prêtes pour l\'export.', en='Printers with drivers assigned — ready for deployment package export.' + printer_col: fr='Imprimante', en='Printer' + client_col: fr='Client', en='Client' + driver_col: fr='Pilote', en='Driver' + downloads_col: fr='Téléchargements', en='Downloads' + no_packages_ready: fr='Aucune imprimante prête. Assignez un pilote pour activer l\'export.', en='No package-ready printers yet. Assign a driver to a printer to enable package export.' + + // Printer detail page + configuration: fr='Configuration', en='Configuration' + duplex_mode_label: fr='Mode recto-verso', en='Duplex Mode' + color_mode_label: fr='Mode couleur', en='Color Mode' + color_value: fr='Couleur', en='Color' + grayscale_value: fr='Niveaux de gris', en='Grayscale' + paper_size_label: fr='Format papier', en='Paper Size' + collate_label: fr='Assemblage', en='Collate' + client_label: fr='Client', en='Client' + unassigned: fr='Non assigné', en='Unassigned' + driver_section: fr='Pilote', en='Driver' + package_label: fr='Package', en='Package' + driver_names_label: fr='Nom(s) du pilote', en='Driver Name(s)' + architecture_label: fr='Architecture', en='Architecture' + no_driver_detail: fr='Aucun pilote assigné', en='No driver assigned' + intune_commands: fr='Commandes Intune', en='Intune Commands' + install_cmd_label: fr='Commande d\'installation', en='Install command' + uninstall_cmd_label: fr='Commande de désinstallation', en='Uninstall command' + copy: fr='Copier', en='Copy' + copied: fr='Copié !', en='Copied!' + scripts_section: fr='Scripts', en='Scripts' + download_install: fr='Télécharger le script d\'installation', en='Download Install Script' + download_uninstall: fr='Télécharger le script de désinstallation', en='Download Uninstall Script' + download_detect: fr='Télécharger le script de détection', en='Download Detect Script' + export_section: fr='Export', en='Export' + download_ninja: fr='Télécharger NinjaRMM ZIP', en='Download NinjaRMM ZIP' + download_intunewin: fr='Télécharger .intunewin', en='Download .intunewin' + icon_section: fr='Icône', en='Icon' + icon_uploaded: fr='Icône téléchargée', en='Icon uploaded' + upload_icon: fr='Télécharger l\'icône', en='Upload Icon' + back_to_printers_btn: fr='Retour aux imprimantes', en='Back to Printers' + + // Edit modal + edit_printer_title: fr='Modifier l\'imprimante', en='Edit Printer' + + // Driver upload section (in printers_new.html) + driver_package_label: fr='Package de pilote (ZIP contenant .inf + fichiers pilote)', en='Driver Package (ZIP containing .inf + driver files)' + +Add each of these to the `fr` object and the `en` object in the translations structure. Maintain the same indentation and formatting. Do NOT remove any existing keys. + + + cd /c/Users/SebastienQUEROL/Documents/projets/ImpTune && python -m pytest tests/test_static.py -x -q 2>&1 | tail -10 + + + - base.html translations objects contain all new keys in both fr and en + - No existing keys removed + - test_static.py still passes + + + + + Task 2: Wire all template strings to i18n store + + imptune/templates/dashboard.html, + imptune/templates/printers.html, + imptune/templates/printers_new.html, + imptune/templates/clients.html, + imptune/templates/client_detail.html, + imptune/templates/drivers.html, + imptune/templates/packages.html, + imptune/templates/printer_detail.html, + imptune/templates/partials/printer_list.html, + imptune/templates/partials/printer_edit_modal.html, + imptune/templates/partials/client_list.html, + imptune/templates/partials/driver_list.html + + +Wire every hardcoded UI string in every template to the Alpine i18n store using the x-text binding pattern. The x-data directive is required only on elements that are NOT already inside an x-data parent scope. + +Rules: +- Elements with no x-data ancestor: add `x-data x-text="$store.i18n.t('key')"` inline on the element +- Elements inside an existing x-data parent: add `x-text="$store.i18n.t('key')"` only (no x-data needed) +- For attribute text (e.g. placeholder, title, aria-label): use `:placeholder="$store.i18n.t('key')"` inside an x-data scope +- Keep a static fallback text inside the element (shown before Alpine hydrates): e.g. `` +- For Jinja2-rendered values (like `{{ printer.name }}`, `{{ 'Yes' if p.color_mode else 'No' }}`): the Yes/No values should use Alpine ternary: `:x-text` is not valid — instead wrap in a `` and use `x-text="$store.i18n.t({{ 'yes' if p.color_mode else 'no' }})"` — WAIT: Jinja2 can't embed Alpine keys. Use a data attribute trick: `` elements with `x-show`: + + For printer_list.html Yes/No cells: Use Alpine x-show with Jinja2 condition: + Color: `{{ 'Oui' if p.color_mode else 'Non' }}` is WRONG (hardcoded FR). + Correct approach: keep Jinja2 rendering but pass a data attribute, then let Alpine read it: + {{ 'Yes' if p.color_mode else 'No' }} + + For printer_detail.html conditional strings (Color/Grayscale, Yes/No): +
{{ "Color" if printer.color_mode else "Grayscale" }}
+ + For printer_detail.html copy button text (already has x-data="{ copiedInstall: false }"): + x-text="copiedInstall ? $store.i18n.t('copied') : $store.i18n.t('copy')" + + For printer_detail.html Unassigned: +
{{ printer.client.name if printer.client_id else "Unassigned" }}
+ — WRONG: Jinja2 emitting Alpine keys is fragile. Correct approach: + If printer.client_id: render `
{{ printer.client.name }}
` (client name is data, not translatable) + If not: render `
Unassigned
` + Use Jinja2 if/else for this: + {% if printer.client_id %} +
{{ printer.client.name }}
+ {% else %} +
Unassigned
+ {% endif %} + +Specific changes per file: + +**dashboard.html**: +- `

Dashboard

` → `

Dashboard

` +- `New Printer` → add `x-data x-text="$store.i18n.t('new_printer')"` (keep href) +- `Upload Driver` → `x-data x-text="$store.i18n.t('upload_driver_btn')"` +- `Export Package` → `x-data x-text="$store.i18n.t('export_package')"` +- `

Recent Activity

` → add `x-data x-text="$store.i18n.t('recent_activity')"` +- `

Recent Printers

` → `x-data x-text="$store.i18n.t('recent_printers')"` +- `

Recent Packages

` → `x-data x-text="$store.i18n.t('recent_packages')"` +- `

No printers configured yet.

` → add `x-data x-text="$store.i18n.t('no_printers')"` +- `

No packages exported yet.

` → add `x-data x-text="$store.i18n.t('no_packages')"` + +**printers.html**: +- `

Printers

` → `x-data x-text="$store.i18n.t('printers')"` +- `Add Printer` → add `x-data x-text="$store.i18n.t('add_printer')"` +- `

Printer Library

` → `x-data x-text="$store.i18n.t('printer_library')"` + +**printers_new.html**: +The top x-data wrapper is `
` — all children are inside this scope. +- `

Add Printer

` → add `x-data x-text` (it's OUTSIDE the div, before the div) → `

Add Printer

` +- `

← Back to Printer Library

` → `← Back to Printer Library` +- Inside the div (x-data is available from parent): + - Each `