--- 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 `