diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md
index 1d8dce1..6900d45 100644
--- a/.planning/ROADMAP.md
+++ b/.planning/ROADMAP.md
@@ -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
diff --git a/.planning/phases/12-i18n-bugfixes/12-01-PLAN.md b/.planning/phases/12-i18n-bugfixes/12-01-PLAN.md
new file mode 100644
index 0000000..524d202
--- /dev/null
+++ b/.planning/phases/12-i18n-bugfixes/12-01-PLAN.md
@@ -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"
+---
+
+
+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.
+
+
+
+@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md
+@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+@.planning/phases/11-ui-enhancements/11-03-SUMMARY.md
+@.planning/phases/11-ui-enhancements/deferred-items.md
+
+
+
+
+
+ Task 1: Browser language auto-detection in i18n store
+ imptune/templates/base.html
+
+ - 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'
+
+
+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).
+
+
+ cd /c/Users/SebastienQUEROL/Documents/projets/ImpTune && python -m pytest tests/e2e/test_i18n_toggle.py -x -q 2>&1 | tail -10
+
+
+ - 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"
+
+
+
+
+ Task 2: Fix test_port_autofill navigating to /printers/new
+ tests/e2e/test_port_autofill.py
+
+ - 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
+
+
+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.
+
+
+ cd /c/Users/SebastienQUEROL/Documents/projets/ImpTune && python -m pytest tests/e2e/test_port_autofill.py -x -q 2>&1 | tail -10
+
+
+ - test_port_autofill[chromium] passes
+ - The one-line URL fix is the only change in the file
+
+
+
+
+
+
+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]).
+
+
+
+- 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
+
+
+
diff --git a/.planning/phases/12-i18n-bugfixes/12-02-PLAN.md b/.planning/phases/12-i18n-bugfixes/12-02-PLAN.md
new file mode 100644
index 0000000..6b1a212
--- /dev/null
+++ b/.planning/phases/12-i18n-bugfixes/12-02-PLAN.md
@@ -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\\("
+---
+
+
+Wire every hardcoded UI string in every template to the Alpine i18n store, so switching the FR/EN toggle updates all labels, buttons, headings, and messages instantly across all pages.
+
+Purpose: Phase 11 implemented the toggle mechanism and translated nav links, but template bodies still contain hardcoded English. This plan completes the translation coverage.
+Output: All templates fully wired; extended translation dictionary in base.html; passing test suite.
+
+
+
+@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md
+@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+@.planning/phases/11-ui-enhancements/11-03-SUMMARY.md
+@.planning/phases/12-i18n-bugfixes/12-01-SUMMARY.md
+
+
+
+
+
+
+
+
+
+
+Existing translation keys already in base.html (FR + EN both):
+ dashboard, drivers, printers, clients, packages,
+ add_printer, add_client, printer_library,
+ edit, delete, save, cancel,
+ upload_driver, printer_name, ip_address, port_name,
+ driver, duplex_mode, one_sided, long_edge, short_edge,
+ color_mode, paper_size, collate, client,
+ edit_printer, no_printers, no_clients,
+ client_list, name, created, back_to_printers,
+ theme_label, lang_label
+
+Pattern for elements OUTSIDE an x-data parent:
+
Dashboard
+
+Pattern for elements INSIDE an existing x-data parent:
+
Printer Library
+
+Pattern for static fallback text (shown before Alpine hydrates):
+ Keep the English literal as the element's inner text — Alpine replaces it on hydration.
+ Example:
+
+
+
+
+
+ Task 1: Add missing translation keys to base.html dictionary
+ imptune/templates/base.html
+
+Extend the translations.fr and translations.en objects in base.html with all keys needed by templates that currently have hardcoded strings. Add the following keys to BOTH fr and en objects:
+
+New keys to add (add to BOTH fr and en translation objects):
+
+ // Dashboard page
+ new_printer: fr='Nouvelle imprimante', en='New Printer'
+ upload_driver_btn: fr='Télécharger un pilote', en='Upload Driver'
+ export_package: fr='Exporter un paquet', en='Export Package'
+ recent_activity: fr='Activité récente', en='Recent Activity'
+ recent_printers: fr='Imprimantes récentes', en='Recent Printers'
+ recent_packages: fr='Paquets récents', en='Recent Packages'
+ no_packages: fr='Aucun paquet exporté pour l\'instant.', en='No packages exported yet.'
+ // Note: no_printers key already exists
+
+ // Printers new page
+ add_printer_title: fr='Ajouter une imprimante', en='Add Printer'
+ back_to_printer_library: fr='← Retour à la bibliothèque', en='← Back to Printer Library'
+ save_printer: fr='Enregistrer l\'imprimante', en='Save Printer'
+ upload_new_driver: fr='Télécharger un nouveau pilote', en='Upload New Driver'
+ no_driver_option: fr='-- Aucun pilote --', en='-- No driver --'
+ unassigned_option: fr='-- Non assigné --', en='-- Unassigned --'
+
+ // Printer list table headers
+ th_name: fr='Nom', en='Name'
+ th_ip: fr='Adresse IP', en='IP Address'
+ th_port: fr='Port', en='Port'
+ th_driver: fr='Pilote', en='Driver'
+ th_duplex: fr='Recto-verso', en='Duplex'
+ th_color: fr='Couleur', en='Color'
+ th_paper: fr='Format', en='Paper'
+ th_collate: fr='Assemblage', en='Collate'
+ th_actions: fr='Actions', en='Actions'
+ yes: fr='Oui', en='Yes'
+ no: fr='Non', en='No'
+ no_driver_assigned: fr='—', en='—'
+
+ // Clients page
+ client_name_label: fr='Nom du client', en='Client Name'
+ add_client_section: fr='Ajouter un client', en='Add Client'
+ client_list_section: fr='Liste des clients', en='Client List'
+
+ // Client detail page
+ back_to_clients: fr='← Tous les clients', en='← All Clients'
+ printers_section: fr='Imprimantes', en='Printers'
+
+ // Drivers page
+ drivers_title: fr='Pilotes', en='Drivers'
+ upload_driver_section: fr='Télécharger un package de pilote', en='Upload Driver Package'
+ driver_library: fr='Bibliothèque de pilotes', en='Driver Library'
+ uploading: fr='Téléchargement...', en='Uploading...'
+ upload_btn: fr='Télécharger', en='Upload'
+ driver_filename: fr='Nom du fichier', en='Filename'
+ driver_names_col: fr='Nom(s) du pilote', en='Driver Name(s)'
+ architecture: fr='Architecture', en='Architecture'
+ uploaded_at: fr='Téléchargé le', en='Uploaded'
+ unknown: fr='Inconnu', en='Unknown'
+ no_drivers: fr='Aucun pilote téléchargé.', en='No drivers uploaded yet.'
+
+ // Packages page
+ packages_title: fr='Paquets', en='Packages'
+ packages_description: fr='Imprimantes avec pilotes assignés — prêtes pour l\'export.', en='Printers with drivers assigned — ready for deployment package export.'
+ printer_col: fr='Imprimante', en='Printer'
+ client_col: fr='Client', en='Client'
+ driver_col: fr='Pilote', en='Driver'
+ downloads_col: fr='Téléchargements', en='Downloads'
+ no_packages_ready: fr='Aucune imprimante prête. Assignez un pilote pour activer l\'export.', en='No package-ready printers yet. Assign a driver to a printer to enable package export.'
+
+ // Printer detail page
+ configuration: fr='Configuration', en='Configuration'
+ duplex_mode_label: fr='Mode recto-verso', en='Duplex Mode'
+ color_mode_label: fr='Mode couleur', en='Color Mode'
+ color_value: fr='Couleur', en='Color'
+ grayscale_value: fr='Niveaux de gris', en='Grayscale'
+ paper_size_label: fr='Format papier', en='Paper Size'
+ collate_label: fr='Assemblage', en='Collate'
+ client_label: fr='Client', en='Client'
+ unassigned: fr='Non assigné', en='Unassigned'
+ driver_section: fr='Pilote', en='Driver'
+ package_label: fr='Package', en='Package'
+ driver_names_label: fr='Nom(s) du pilote', en='Driver Name(s)'
+ architecture_label: fr='Architecture', en='Architecture'
+ no_driver_detail: fr='Aucun pilote assigné', en='No driver assigned'
+ intune_commands: fr='Commandes Intune', en='Intune Commands'
+ install_cmd_label: fr='Commande d\'installation', en='Install command'
+ uninstall_cmd_label: fr='Commande de désinstallation', en='Uninstall command'
+ copy: fr='Copier', en='Copy'
+ copied: fr='Copié !', en='Copied!'
+ scripts_section: fr='Scripts', en='Scripts'
+ download_install: fr='Télécharger le script d\'installation', en='Download Install Script'
+ download_uninstall: fr='Télécharger le script de désinstallation', en='Download Uninstall Script'
+ download_detect: fr='Télécharger le script de détection', en='Download Detect Script'
+ export_section: fr='Export', en='Export'
+ download_ninja: fr='Télécharger NinjaRMM ZIP', en='Download NinjaRMM ZIP'
+ download_intunewin: fr='Télécharger .intunewin', en='Download .intunewin'
+ icon_section: fr='Icône', en='Icon'
+ icon_uploaded: fr='Icône téléchargée', en='Icon uploaded'
+ upload_icon: fr='Télécharger l\'icône', en='Upload Icon'
+ back_to_printers_btn: fr='Retour aux imprimantes', en='Back to Printers'
+
+ // Edit modal
+ edit_printer_title: fr='Modifier l\'imprimante', en='Edit Printer'
+
+ // Driver upload section (in printers_new.html)
+ driver_package_label: fr='Package de pilote (ZIP contenant .inf + fichiers pilote)', en='Driver Package (ZIP containing .inf + driver files)'
+
+Add each of these to the `fr` object and the `en` object in the translations structure. Maintain the same indentation and formatting. Do NOT remove any existing keys.
+
+
+ cd /c/Users/SebastienQUEROL/Documents/projets/ImpTune && python -m pytest tests/test_static.py -x -q 2>&1 | tail -10
+
+
+ - base.html translations objects contain all new keys in both fr and en
+ - No existing keys removed
+ - test_static.py still passes
+
+
+
+
+ Task 2: Wire all template strings to i18n store
+
+ imptune/templates/dashboard.html,
+ imptune/templates/printers.html,
+ imptune/templates/printers_new.html,
+ imptune/templates/clients.html,
+ imptune/templates/client_detail.html,
+ imptune/templates/drivers.html,
+ imptune/templates/packages.html,
+ imptune/templates/printer_detail.html,
+ imptune/templates/partials/printer_list.html,
+ imptune/templates/partials/printer_edit_modal.html,
+ imptune/templates/partials/client_list.html,
+ imptune/templates/partials/driver_list.html
+
+
+Wire every hardcoded UI string in every template to the Alpine i18n store using the x-text binding pattern. The x-data directive is required only on elements that are NOT already inside an x-data parent scope.
+
+Rules:
+- Elements with no x-data ancestor: add `x-data x-text="$store.i18n.t('key')"` inline on the element
+- Elements inside an existing x-data parent: add `x-text="$store.i18n.t('key')"` only (no x-data needed)
+- For attribute text (e.g. placeholder, title, aria-label): use `:placeholder="$store.i18n.t('key')"` inside an x-data scope
+- Keep a static fallback text inside the element (shown before Alpine hydrates): e.g. ``
+- For Jinja2-rendered values (like `{{ printer.name }}`, `{{ 'Yes' if p.color_mode else 'No' }}`): the Yes/No values should use Alpine ternary: `:x-text` is not valid — instead wrap in a `` and use `x-text="$store.i18n.t({{ 'yes' if p.color_mode else 'no' }})"` — WAIT: Jinja2 can't embed Alpine keys. Use a data attribute trick: `
` elements with `x-show`:
+
+ For printer_list.html Yes/No cells: Use Alpine x-show with Jinja2 condition:
+ Color: `
{{ 'Oui' if p.color_mode else 'Non' }}
` is WRONG (hardcoded FR).
+ Correct approach: keep Jinja2 rendering but pass a data attribute, then let Alpine read it:
+
{{ 'Yes' if p.color_mode else 'No' }}
+
+ For printer_detail.html conditional strings (Color/Grayscale, Yes/No):
+
{{ "Color" if printer.color_mode else "Grayscale" }}
+
+ For printer_detail.html copy button text (already has x-data="{ copiedInstall: false }"):
+ x-text="copiedInstall ? $store.i18n.t('copied') : $store.i18n.t('copy')"
+
+ For printer_detail.html Unassigned:
+
{{ printer.client.name if printer.client_id else "Unassigned" }}
+ — WRONG: Jinja2 emitting Alpine keys is fragile. Correct approach:
+ If printer.client_id: render `
{{ printer.client.name }}
` (client name is data, not translatable)
+ If not: render `
Unassigned
`
+ Use Jinja2 if/else for this:
+ {% if printer.client_id %}
+