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:
@@ -109,3 +109,14 @@ Full details: [`milestones/v1.0-ROADMAP.md`](milestones/v1.0-ROADMAP.md)
|
||||
| 9. UX Tech Debt Closure | 3/3 | Complete | 2026-04-13 | 2026-04-13 |
|
||||
| 10. Real-World Runtime Validation | v1.1 | 3/3 | Complete | 2026-04-13 |
|
||||
| 11. UI Enhancements | 4/4 | Complete | 2026-04-15 | |
|
||||
|
||||
### Phase 12: i18n bugfixes — full translation coverage and browser language auto-detection
|
||||
|
||||
**Goal:** All hardcoded UI strings in every template respond to the FR/EN language toggle; browser language auto-detected from navigator.language on first visit; E2E suite fully green.
|
||||
**Requirements**: TBD
|
||||
**Depends on:** Phase 11
|
||||
**Plans:** 2 plans
|
||||
|
||||
Plans:
|
||||
- [ ] 12-01-PLAN.md — Browser language auto-detection (navigator.language fallback) + fix test_port_autofill E2E
|
||||
- [ ] 12-02-PLAN.md — Full template i18n coverage: wire all hardcoded strings across 13 templates to Alpine i18n store
|
||||
|
||||
@@ -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 && python -m pytest tests/ -q 2>&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>
|
||||
Reference in New Issue
Block a user