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>
This commit is contained in:
2026-04-15 15:18:53 +02:00
co-authored by Claude Sonnet 4.6
parent b0078a4fb2
commit ef8fbc9692
3 changed files with 610 additions and 0 deletions
@@ -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"
---
<objective>
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.
</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/11-ui-enhancements/deferred-items.md
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Browser language auto-detection in i18n store</name>
<files>imptune/templates/base.html</files>
<behavior>
- 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'
</behavior>
<action>
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).
</action>
<verify>
<automated>cd /c/Users/SebastienQUEROL/Documents/projets/ImpTune && python -m pytest tests/e2e/test_i18n_toggle.py -x -q 2>&1 | tail -10</automated>
</verify>
<done>
- 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"
</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: Fix test_port_autofill navigating to /printers/new</name>
<files>tests/e2e/test_port_autofill.py</files>
<behavior>
- 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
</behavior>
<action>
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.
</action>
<verify>
<automated>cd /c/Users/SebastienQUEROL/Documents/projets/ImpTune && python -m pytest tests/e2e/test_port_autofill.py -x -q 2>&1 | tail -10</automated>
</verify>
<done>
- test_port_autofill[chromium] passes
- The one-line URL fix is the only change in the file
</done>
</task>
</tasks>
<verification>
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]).
</verification>
<success_criteria>
- 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
</success_criteria>
<output>
After completion, create `.planning/phases/12-i18n-bugfixes/12-01-SUMMARY.md`
</output>
@@ -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\\("
---
<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>