Files
ImpTune/.planning/phases/12-i18n-bugfixes/12-02-PLAN.md
T
kawaandClaude Sonnet 4.6 ef8fbc9692 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 <noreply@anthropic.com>
2026-04-15 15:18:53 +02:00

464 lines
28 KiB
Markdown

---
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\\("
---
<objective>
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.
</objective>
<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>
<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.md
</context>
<interfaces>
<!-- Alpine i18n pattern established in Phase 11 Plan 03 -->
<!-- All UI strings accessed via: $store.i18n.t('key') -->
<!-- Applied on elements via: x-data x-text="$store.i18n.t('key')" -->
<!-- For attribute bindings: :title="$store.i18n.t('key')" -->
<!-- For button text inside x-data parent scope: x-text="$store.i18n.t('key')" -->
<!-- Translation dictionary lives in base.html alpine:init script block -->
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:
<h1 x-data x-text="$store.i18n.t('dashboard')">Dashboard</h1>
Pattern for elements INSIDE an existing x-data parent:
<h3 x-text="$store.i18n.t('printer_library')">Printer Library</h3>
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: <button type="submit" x-data x-text="$store.i18n.t('save')">Save</button>
</interfaces>
<tasks>
<task type="auto">
<name>Task 1: Add missing translation keys to base.html dictionary</name>
<files>imptune/templates/base.html</files>
<action>
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.
</action>
<verify>
<automated>cd /c/Users/SebastienQUEROL/Documents/projets/ImpTune && python -m pytest tests/test_static.py -x -q 2>&1 | tail -10</automated>
</verify>
<done>
- base.html translations objects contain all new keys in both fr and en
- No existing keys removed
- test_static.py still passes
</done>
</task>
<task type="auto">
<name>Task 2: Wire all template strings to i18n store</name>
<files>
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
</files>
<action>
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. `<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-text` is not valid — instead wrap in a `<span x-data>` 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: `<td :data-val="{{ 'yes' if p.color_mode else 'no' }}"` — this does NOT work either. Instead use two `<span>` elements with `x-show`:
For printer_list.html Yes/No cells: Use Alpine x-show with Jinja2 condition:
Color: `<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:
<td x-data="{ val: {{ 'true' if p.color_mode else 'false' }} }"
x-text="val ? $store.i18n.t('yes') : $store.i18n.t('no')">{{ 'Yes' if p.color_mode else 'No' }}</td>
For printer_detail.html conditional strings (Color/Grayscale, Yes/No):
<dd x-data="{ val: {{ 'true' if printer.color_mode else 'false' }} }"
x-text="val ? $store.i18n.t('color_value') : $store.i18n.t('grayscale_value')">{{ "Color" if printer.color_mode else "Grayscale" }}</dd>
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:
<dd x-data x-text="{{ \"'client_label'\" if printer.client_id else \"'unassigned'\" }}">{{ printer.client.name if printer.client_id else "Unassigned" }}</dd>
— WRONG: Jinja2 emitting Alpine keys is fragile. Correct approach:
If printer.client_id: render `<dd>{{ printer.client.name }}</dd>` (client name is data, not translatable)
If not: render `<dd x-data x-text="$store.i18n.t('unassigned')">Unassigned</dd>`
Use Jinja2 if/else for this:
{% if printer.client_id %}
<dd>{{ printer.client.name }}</dd>
{% else %}
<dd x-data x-text="$store.i18n.t('unassigned')">Unassigned</dd>
{% 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>` → add `x-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>` → add `x-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>` → add `x-data x-text="$store.i18n.t('no_printers')"`
- `<p class="empty-state">No packages exported yet.</p>` → add `x-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>` → add `x-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>` → add `x-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')"`
- 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')"`
**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>` → add `x-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-text` with 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)
</action>
<verify>
<automated>cd /c/Users/SebastienQUEROL/Documents/projets/ImpTune && python -m pytest tests/ -x -q --ignore=tests/e2e 2>&1 | tail -15</automated>
</verify>
<done>
- 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
</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<name>Task 3: Human verification — full i18n coverage on all pages</name>
<files>imptune/templates/</files>
<action>Human verification step. Claude has wired all template strings in Task 2. This task pauses for user to confirm coverage by manually toggling the language on each page.</action>
<verify>
<automated>cd C:/Users/SebastienQUEROL/Documents/projets/ImpTune &amp;&amp; python -m pytest tests/ -q 2&gt;&amp;1 | tail -5</automated>
</verify>
<done>User confirms all visible strings switch between French and English on every page; no hardcoded strings remain</done>
<what-built>
All UI strings in all templates wired to Alpine i18n store (Task 2), plus browser language auto-detection (Plan 12-01). Both FR and EN coverage is complete.
</what-built>
<how-to-verify>
1. Start the server: cd /c/Users/SebastienQUEROL/Documents/projets/ImpTune && python -m uvicorn imptune.main:app --reload
2. Open http://localhost:8000 in a browser that has no imptune_lang localStorage key
3. Verify: if your browser language is French, the UI shows French labels. If English, it shows English.
4. Click the FR/EN toggle in the top-right corner
5. Verify: ALL labels on the page switch language immediately — headings, buttons, table headers, nav items, empty states
6. Navigate to /printers, /printers/new, /clients, /drivers, /packages, a printer detail page
7. On each page: verify that clicking the toggle switches all visible strings between French and English
8. Reload any page — verify the chosen language persists
9. Look for any remaining hardcoded strings that do NOT change with the toggle — report any found
</how-to-verify>
<resume-signal>Type "approved" if all strings switch correctly, or describe which strings remain hardcoded</resume-signal>
</task>
</tasks>
<verification>
Full test suite check:
cd /c/Users/SebastienQUEROL/Documents/projets/ImpTune && python -m pytest tests/ -q 2>&1 | tail -20
Expected: 122+ non-E2E tests passing + all 7 E2E tests passing (including the fixed test_port_autofill from Plan 12-01).
</verification>
<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>
<output>
After completion, create `.planning/phases/12-i18n-bugfixes/12-02-SUMMARY.md`
</output>