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 <noreply@anthropic.com>
28 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 12-i18n-bugfixes | 02 | execute | 2 |
|
|
false |
|
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.
<execution_context> @C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md @C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md </execution_context>
@.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.mdExisting 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: Save
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
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.
<button x-data x-text="$store.i18n.t('save')">Save</button> -
For Jinja2-rendered values (like
{{ printer.name }},{{ 'Yes' if p.color_mode else 'No' }}): the Yes/No values should use Alpine ternary::x-textis not valid — instead wrap in a<span x-data>and usex-text="$store.i18n.t({{ 'yes' if p.color_mode else 'no' }})"— WAIT: Jinja2 can't embed Alpine keys. Use a data attribute trick:<td :data-val="{{ 'yes' if p.color_mode else 'no' }}"— this does NOT work either. Instead use two<span>elements withx-show:For printer_list.html Yes/No cells: Use Alpine x-show with Jinja2 condition: Color:
{{ 'Yes' if p.color_mode else 'No' }}<td>{{ 'Oui' if p.color_mode else 'Non' }}</td>is WRONG (hardcoded FR). Correct approach: keep Jinja2 rendering but pass a data attribute, then let Alpine read it: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:
<h1>Dashboard</h1>→<h1 x-data x-text="$store.i18n.t('dashboard')">Dashboard</h1><a href="/printers" class="btn-action">New Printer</a>→ addx-data x-text="$store.i18n.t('new_printer')"(keep href)<a href="/drivers" class="btn-action">Upload Driver</a>→x-data x-text="$store.i18n.t('upload_driver_btn')"<a href="/packages" class="btn-action">Export Package</a>→x-data x-text="$store.i18n.t('export_package')"<h2>Recent Activity</h2>→ addx-data x-text="$store.i18n.t('recent_activity')"<h3>Recent Printers</h3>→x-data x-text="$store.i18n.t('recent_printers')"<h3>Recent Packages</h3>→x-data x-text="$store.i18n.t('recent_packages')"<p class="empty-state">No printers configured yet.</p>→ addx-data x-text="$store.i18n.t('no_printers')"<p class="empty-state">No packages exported yet.</p>→ addx-data x-text="$store.i18n.t('no_packages')"
printers.html:
<h1>Printers</h1>→x-data x-text="$store.i18n.t('printers')"<a href="/printers/new" role="button">Add Printer</a>→ addx-data x-text="$store.i18n.t('add_printer')"<h2>Printer Library</h2>→x-data x-text="$store.i18n.t('printer_library')"
printers_new.html:
The top x-data wrapper is <div x-data="{ ip: '', port: '', portEdited: false }"> — all children are inside this scope.
<h1>Add Printer</h1>→ addx-data x-text(it's OUTSIDE the div, before the div) →<h1 x-data x-text="$store.i18n.t('add_printer_title')">Add Printer</h1><p><a href="/printers">← Back to Printer Library</a></p>→<a href="/printers" x-data x-text="$store.i18n.t('back_to_printer_library')">← Back to Printer Library</a>- Inside the div (x-data is available from parent):
- Each
<label>text node: wrap the label text in a<span x-text="$store.i18n.t('...')">since label text can't have x-text directly on the label (it contains child input). Use:<label><span x-text="$store.i18n.t('printer_name')">Printer Name</span><input ...></label> - Apply same span pattern for: ip_address, port_name, driver, duplex_mode, color_mode, paper_size, collate, client labels
<option value="">-- No driver --</option>→<option value="" x-text="$store.i18n.t('no_driver_option')">-- No driver --</option><option value="OneSided" selected>One-Sided</option>→x-text="$store.i18n.t('one_sided')"<option value="LongEdge">Long Edge</option>→x-text="$store.i18n.t('long_edge')"<option value="ShortEdge">Short Edge</option>→x-text="$store.i18n.t('short_edge')"<option value="">-- Unassigned --</option>→x-text="$store.i18n.t('unassigned_option')"<button type="submit">Save Printer</button>→x-text="$store.i18n.t('save_printer')"
- Each
- Upload driver form section (also inside the outer div):
<label>Upload New Driver→ span pattern:<label><span x-text="$store.i18n.t('upload_new_driver')">Upload New Driver</span><input ...></label><button type="submit" class="secondary">Upload Driver</button>→x-text="$store.i18n.t('upload_driver')"
partials/printer_list.html: The div#printer-list has no x-data wrapper. Add x-data on individual elements.
<p>No printers configured yet.</p>→<p x-data x-text="$store.i18n.t('no_printers')">No printers configured yet.</p>- Table headers:
<th x-data x-text="$store.i18n.t('th_name')">Name</th>etc. for each th (th_name, th_ip, th_port, th_driver, th_duplex, th_color, th_paper, th_collate, th_actions) - Delete button:
<button ... x-data x-text="$store.i18n.t('delete')">Delete</button>— keep all hx-* attributes, add x-data and x-text
partials/printer_edit_modal.html:
The dialog contains <div x-data="{ ip: ..., port: ..., portEdited: true }"> — elements inside this div can use $store without x-data.
<h3>Edit Printer</h3>is in<header>OUTSIDE the x-data div — add x-data:<h3 x-data x-text="$store.i18n.t('edit_printer_title')">Edit Printer</h3>- Edit trigger button (in Actions column, outside any x-data):
<button ... x-data x-text="$store.i18n.t('edit')">Edit</button> - Inside the x-data div (the form):
- Each label: use span pattern:
<label><span x-text="$store.i18n.t('printer_name')">Printer Name</span><input ...></label> - Apply for: printer_name, ip_address, port_name, driver, duplex_mode, color_mode, paper_size, collate, client
<option value="">-- No driver --</option>→x-text="$store.i18n.t('no_driver_option')"- Duplex options: x-text for one_sided, long_edge, short_edge
- Paper size options: A4, Letter, Legal — these are standard values, keep as-is (not translatable)
<option value="">-- Unassigned --</option>→x-text="$store.i18n.t('unassigned_option')"<button type="submit">Save</button>→x-text="$store.i18n.t('save')"<button type="button" class="secondary" ...>Cancel</button>→x-text="$store.i18n.t('cancel')"
- Each label: use span pattern:
clients.html:
<h1>Clients</h1>→x-data x-text="$store.i18n.t('clients')"<h2>Add Client</h2>→ inside form's hx-post context but no x-data:x-data x-text="$store.i18n.t('add_client_section')"<label>Client Name→<label><span x-data x-text="$store.i18n.t('client_name_label')">Client Name</span><input ...></label><button type="submit">Add Client</button>→x-data x-text="$store.i18n.t('add_client')"<h2>Client List</h2>→x-data x-text="$store.i18n.t('client_list_section')"
partials/client_list.html:
<p>No clients configured yet.</p>→x-data x-text="$store.i18n.t('no_clients')"<th>Name</th>→<th x-data x-text="$store.i18n.t('th_name')">Name</th><th>Created</th>→<th x-data x-text="$store.i18n.t('created')">Created</th>
client_detail.html:
<p><a href="/clients">← All Clients</a></p>→<a href="/clients" x-data x-text="$store.i18n.t('back_to_clients')">← All Clients</a><h2>Printers</h2>→x-data x-text="$store.i18n.t('printers_section')"
drivers.html:
<h1>Drivers</h1>→x-data x-text="$store.i18n.t('drivers_title')"<h2>Upload Driver Package</h2>→x-data x-text="$store.i18n.t('upload_driver_section')"<label for="driver-file">Driver Package (ZIP containing .inf + driver files)</label>→x-data x-text="$store.i18n.t('driver_package_label')"<button type="submit">Upload</button>→x-data x-text="$store.i18n.t('upload_btn')"<span id="upload-spinner" ...>Uploading...</span>→ addx-data x-text="$store.i18n.t('uploading')"<h2>Driver Library</h2>→x-data x-text="$store.i18n.t('driver_library')"
partials/driver_list.html:
<th>Filename</th>→x-data x-text="$store.i18n.t('driver_filename')"<th>Driver Name(s)</th>→x-data x-text="$store.i18n.t('driver_names_col')"<th>Architecture</th>→x-data x-text="$store.i18n.t('architecture')"<th>Uploaded</th>→x-data x-text="$store.i18n.t('uploaded_at')"<em>Unknown</em>→<em x-data x-text="$store.i18n.t('unknown')">Unknown</em>{{ item.driver.architecture or "Unknown" }}— this is Jinja2 rendered text; change to:{{ item.driver.architecture if item.driver.architecture else '' }}<span {% if not item.driver.architecture %}x-data x-text="$store.i18n.t('unknown')"{% endif %}>{% if not item.driver.architecture %}Unknown{% endif %}</span>— SIMPLER: use a Jinja2 conditional td:<td>{% if item.driver.architecture %}{{ item.driver.architecture }}{% else %}<span x-data x-text="$store.i18n.t('unknown')">Unknown</span>{% endif %}</td><p>No drivers uploaded yet.</p>→x-data x-text="$store.i18n.t('no_drivers')"
packages.html:
<h1>Packages</h1>→x-data x-text="$store.i18n.t('packages_title')"<p>Printers with drivers assigned...→x-data x-text="$store.i18n.t('packages_description')"- Table headers:
<th scope="col" x-data x-text="$store.i18n.t('printer_col')">Printer</th>etc. <p class="empty-state">No package-ready printers yet...→x-data x-text="$store.i18n.t('no_packages_ready')"
printer_detail.html:
Most content is in <article> with no x-data parent. Add x-data on individual elements.
<h2>Configuration</h2>→x-data x-text="$store.i18n.t('configuration')"<dt>IP Address</dt>→x-data x-text="$store.i18n.t('ip_address')"<dt>Port Name</dt>→x-data x-text="$store.i18n.t('port_name')"<dt>Duplex Mode</dt>→x-data x-text="$store.i18n.t('duplex_mode_label')"<dt>Color Mode</dt>→x-data x-text="$store.i18n.t('color_mode_label')"<dd>{{ "Color" if printer.color_mode else "Grayscale" }}</dd>→{% if printer.color_mode %}<dd x-data x-text="$store.i18n.t('color_value')">Color</dd>{% else %}<dd x-data x-text="$store.i18n.t('grayscale_value')">Grayscale</dd>{% endif %}<dt>Paper Size</dt>→x-data x-text="$store.i18n.t('paper_size_label')"<dt>Collate</dt>→x-data x-text="$store.i18n.t('collate_label')"<dd>{{ "Yes" if printer.collate else "No" }}</dd>→ same pattern as color_mode<dt>Client</dt>→x-data x-text="$store.i18n.t('client_label')"<dd>{{ printer.client.name if printer.client_id else "Unassigned" }}</dd>→ Jinja2 conditional: if client_id, render name as-is; if not, render<dd x-data x-text="$store.i18n.t('unassigned')">Unassigned</dd><h2>Driver</h2>→x-data x-text="$store.i18n.t('driver_section')"<dt>Package</dt>→x-data x-text="$store.i18n.t('package_label')"<dt>Driver Name(s)</dt>→x-data x-text="$store.i18n.t('driver_names_label')"<dt>Architecture</dt>→x-data x-text="$store.i18n.t('architecture_label')"<p>No driver assigned</p>→x-data x-text="$store.i18n.t('no_driver_detail')"<h2>Intune Commands</h2>→x-data x-text="$store.i18n.t('intune_commands')"<label>Install command</label>→x-data x-text="$store.i18n.t('install_cmd_label')"<label>Uninstall command</label>→x-data x-text="$store.i18n.t('uninstall_cmd_label')"- Copy buttons already have
x-data="{ copiedInstall: false }"parent scope:x-text="copiedInstall ? $store.i18n.t('copied') : $store.i18n.t('copy')" <h2>Scripts</h2>→x-data x-text="$store.i18n.t('scripts_section')"- Script download link texts →
x-data x-text="$store.i18n.t('download_install')"etc. <h2>Export</h2>→x-data x-text="$store.i18n.t('export_section')"- NinjaRMM / intunewin link texts →
x-data x-textwith download_ninja / download_intunewin keys <h2>Icon</h2>→x-data x-text="$store.i18n.t('icon_section')"<p>Icon uploaded</p>→x-data x-text="$store.i18n.t('icon_uploaded')"<button type="submit">Upload Icon</button>→x-data x-text="$store.i18n.t('upload_icon')"<a href="/printers" role="button" class="secondary">Back to Printers</a>→x-data x-text="$store.i18n.t('back_to_printers_btn')"
IMPORTANT: Do NOT translate:
- Printer names, IP addresses, port names (data values — not UI strings)
- Driver filenames (data values)
- Paper sizes A4/Letter/Legal (universal standard values)
- Client names (data values)
{{ printer.name }}in h1 (data value)- The
hx-confirm="Delete '{{ p.name }}'?"attribute — keep as-is (HTMX attribute, not visible UI text in Alpine context) cd /c/Users/SebastienQUEROL/Documents/projets/ImpTune && python -m pytest tests/ -x -q --ignore=tests/e2e 2>&1 | tail -15- All 13 template files have x-text bindings for all UI strings
- No hardcoded English or French UI strings remain outside of Alpine bindings (data values are exempt)
- Full non-E2E test suite passes (122+ tests green)
- Switching the i18n toggle on any page updates all visible labels immediately
Expected: 122+ non-E2E tests passing + all 7 E2E tests passing (including the fixed test_port_autofill from Plan 12-01).
<success_criteria>
- All UI strings in all 13 templates respond to the i18n toggle
- Switching FR/EN on any page updates all headings, buttons, table headers, labels, empty state messages instantly
- No hardcoded English or French UI strings remain (data values like printer names are exempt)
- Full test suite: 122+ unit tests + 7 E2E tests all passing
- Human-verified: toggling on all pages produces correct bilingual output </success_criteria>