Skip to content

fix(settings): un seul contrôle de langue, un seul écrivain#263

Merged
thierryvm merged 1 commit into
mainfrom
fix/settings-locale-select
Jul 25, 2026
Merged

fix(settings): un seul contrôle de langue, un seul écrivain#263
thierryvm merged 1 commit into
mainfrom
fix/settings-locale-select

Conversation

@thierryvm

@thierryvm thierryvm commented Jul 25, 2026

Copy link
Copy Markdown
Owner

Ferme un défaut préexistant remonté par i18n-auditor pendant #258, avec la simplification que tu as demandée : FR - EN, sans précision régionale.

Le défaut

Le sélecteur de langue des Réglages ne pouvait rien enregistrer d'autre que le français, et même ça ne changeait rien à l'écran.

  • Il proposait fr-BE, fr-FR, en-GB, alors que le serveur valide z.enum(LOCALES) qui ne contient ni fr-FR ni en-GB → toute autre sélection échouait avec un toast d'erreur générique.
  • Et updateProfileAction écrivait users.locale sans toucher au cookie ni revalider : depuis fix(i18n): résolution de langue déterministe — le préfixe d'URL décide, plus le cookie #258 la langue affichée vient du préfixe d'URL, donc la colonne bougeait et l'app restait dans la langue précédente.

Une préférence, deux écrivains divergents : setLocaleAction faisait tout le travail, updateProfileAction en faisait un tiers, mal.

Le correctif

La carte Profil rend désormais le LocaleSwitcher qui existe déjà — segmented control FR | EN, déjà audité a11y, déjà branché sur le bon chemin. setLocaleAction devient le seul écrivain.

Réparer le <Select> maison revenait à écrire une troisième implémentation du même contrôle. Réutiliser l'existant supprime d'un coup le double submit, les clés dupliquées et la question de la valeur initiale.

Le contrôle sort du formulaire : il persiste immédiatement et navigue, ce qui remonte la carte. À l'intérieur, il laissait croire que « Enregistrer » s'y appliquait — et un nom saisi non sauvegardé disparaissait au changement de langue.

Ce que l'audit UI a rattrapé

ui-auditor a relevé une régression que j'introduisais : à ≥1024px la page monte aussi le switcher du header, donc deux radiogroup annoncés « Changer de langue », indiscernables dans la liste des éléments d'un lecteur d'écran.

Corrigé plutôt que laissé en arbitrage : le champ des Réglages nomme son groupe par son libellé visible « Langue ». Header et MoreSheet inchangés. Bénéfice au passage — nom accessible et texte visible sont désormais identiques (WCAG 2.5.3) au lieu de simplement se recouper.

Le piège des deux clés d'erreur

settings.locale.invalid perdait son émetteur → supprimée des 5 fichiers. errors.locale.invalid est émise par setLocaleActionpréservée. Les confondre cassait le chemin d'erreur du switcher. i18n-auditor a vérifié les deux : ✅ GO, parité intacte.

Preuve

npm run test 1669 / 1669
typecheck · lint 0 erreur (8 warnings, niveau préexistant)
e2e settings-locale-field iPhone 14 3 / 3 en local
Smoke connecté libellé « Langue », options ["FR","EN"], ancien <Select> absent

Falsifiabilité vérifiée : en réintroduisant l'écriture de locale, deux specs passent au rouge.

⚠️ Une CI verte ne prouve pas ce correctif : aucun spec Playwright ne couvrait ce contrôle et les parcours authentifiés s'auto-skippent en CI. D'où le smoke seedé local, exigé par la revue.

Restent en P2, non traités

Pas d'aria-describedby « s'applique immédiatement » ; relation libellé/nom accessible non verrouillée par un test pour les autres instances ; CardTitle rend un <div> et jamais un vrai titre (concerne toute l'app, à tracer séparément).

Rapport : docs/prs/PR-settings-locale-select-report.md

Summary by Sourcery

Utiliser le LocaleSwitcher partagé comme unique contrôle de préférence de langue et supprimer la gestion de la locale du flux de mise à jour du profil.

New Features:

  • Exposer un LocaleSwitcher configurable qui peut être étiqueté par un élément visible externe.
  • Ajouter une section dédiée au champ de langue sur la carte de profil des paramètres en utilisant le LocaleSwitcher partagé.

Bug Fixes:

  • Corriger le sélecteur de langue des paramètres pour que le changement de langue utilise la bonne action de locale et mette effectivement à jour la langue de l’application.
  • Empêcher les valeurs de locale invalides ou héritées de casser les mises à jour de profil en ignorant le champ locale dans la payload du profil.

Enhancements:

  • Simplifier le schéma et l’action de mise à jour de profil pour ne gérer que le nom d’affichage, en laissant la langue à l’action de locale dédiée.
  • Améliorer l’accessibilité du contrôle de langue des paramètres en donnant à son radiogroup un nom accessible distinct basé sur une étiquette visible.

Documentation:

  • Ajouter un rapport détaillé documentant le défaut du champ de locale dans les paramètres, sa correction, les audits, et les suivis d’accessibilité restants.

Tests:

  • Ajouter des tests unitaires pour le schéma de mise à jour de profil afin de vérifier que la locale est ignorée et qu’aucune valeur par défaut n’est réécrite.
  • Ajouter des tests unitaires pour updateProfileAction afin de s’assurer qu’il n’écrit jamais la locale et qu’il valide la gestion du nom d’affichage.
  • Introduire une spécification Playwright mobile iOS pour le champ de langue des paramètres couvrant les options, la dénomination accessible et la séparation du formulaire de profil.

Chores:

  • Supprimer les traductions obsolètes liées à la locale ainsi que les clés d’erreur de schéma associées à l’ancien sélecteur de langue des paramètres.
Original summary in English

Summary by Sourcery

Use the shared LocaleSwitcher as the single language preference control and remove locale handling from the profile update flow.

New Features:

  • Expose a configurable LocaleSwitcher that can be labeled by an external visible element.
  • Add a dedicated language field section on the Settings profile card using the shared LocaleSwitcher.

Bug Fixes:

  • Fix the Settings language selector so changing language uses the proper locale action and actually updates the app language.
  • Prevent invalid or legacy locale values from breaking profile updates by ignoring the locale field in the profile payload.

Enhancements:

  • Simplify the profile update schema and action to manage display name only, leaving language to the dedicated locale action.
  • Improve accessibility of the Settings language control by giving its radiogroup a distinct visible label-based accessible name.

Documentation:

  • Add a detailed report documenting the Settings locale field defect, its fix, audits, and remaining accessibility follow-ups.

Tests:

  • Add unit tests for the profile update schema to assert locale is ignored and no default is written back.
  • Add unit tests for updateProfileAction to ensure it never writes locale and validates display name handling.
  • Introduce a Playwright mobile-iOS spec for the Settings language field covering options, accessibility naming, and separation from the profile form.

Chores:

  • Remove obsolete locale-related translations and schema error keys tied to the old Settings language select.

The Settings language selector could not save anything but French, and even
that changed nothing on screen. It offered fr-BE / fr-FR / en-GB while the
server validated z.enum(LOCALES), which holds neither fr-FR nor en-GB — so
every choice but fr-BE failed validation with a generic error toast. And
updateProfileAction wrote users.locale without touching the NEXT_LOCALE cookie
or revalidating the root layout, so an accepted fr-BE moved a database column
while the app stayed in the previous language: the rendered locale comes from
the URL prefix alone.

One preference, two divergent writers. setLocaleAction did the whole job —
cookie, database, revalidate, navigate — and updateProfileAction did a third of
it, badly.

ProfileCard now renders the LocaleSwitcher that already exists: a FR | EN
segmented control built on LOCALES_VISIBLE, already audited for a11y, already
wired to the canonical path. locale leaves profileUpdateSchema and
updateProfileAction, leaving setLocaleAction as the single writer. Repairing
the bespoke Select would have meant writing a third implementation of the same
control; reusing this one removes the double submit, the duplicated
localeOptions keys and the initial-value problem in one move.

The control sits outside the form on purpose. It persists immediately and
navigates, which remounts the card — inside the form it implied "Save" applied
to it, and a name typed but not yet saved would vanish on a switch with no
explanation.

ui-auditor caught a regression this change introduced: at >=1024px the page
also mounts the header's switcher (its hidden lg:flex block is in the DOM and
focusable), so two radiogroups both announced "Changer de langue" became
indistinguishable in a screen reader's element list. LocaleSwitcher now takes an
optional labelledById, and the Settings one is named by its visible "Langue"
text. Header and MoreSheet instances are untouched — without the prop the
original behaviour stands. Accessible name and visible label now match exactly
rather than merely overlap.

Two near-identical error keys had to be told apart. settings.locale.invalid lost
its only emitter with the schema field and is gone from all five message files;
errors.locale.invalid is emitted by setLocaleAction and is preserved. Confusing
them would have broken the switcher's own error path. The localeOptions group
went too — it duplicated ui.localeSwitcher.options, and that duplication is
precisely how fr-FR and en-GB drifted away from the server enum.

Three things deliberately did not move. users.locale is still written by
setLocaleAction and still read by i18n/request.ts when requestLocale is absent —
it is the cross-device fallback, not dead weight. profileUpdateSchema stays a
plain z.object and must never become .strict(): during a deploy, tabs on the old
bundle keep posting locale, and Zod stripping it is what makes them degrade
gracefully. And all three edits ship in one commit, because the schema carried
.default('fr-BE') — split apart, every profile save would have silently reset
the user's language.

The new action test pins the payload actually sent to Supabase, the only place
this could come back unnoticed now that the schema no longer types locale.
Verified falsifiable: reintroducing the locale write turns two specs red. The
two schema specs that asserted the old contract are rewritten rather than
deleted, so the change of contract leaves a trace.

No Playwright spec covered this control, and the authenticated ones auto-skip in
CI, so a green pipeline proves nothing here. A seeded spec now covers it and
says so in its header; run locally at 3/3 on iPhone 14.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@vercel

vercel Bot commented Jul 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ankora Ready Ready Preview, Comment Jul 25, 2026 11:42pm

@github-actions github-actions Bot added area:settings User settings status:review-needed Ready for review type:fix Bug fix labels Jul 25, 2026
@sourcery-ai

sourcery-ai Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

🧙 Sourcery examine votre pull request !


Conseils et commandes

Interagir avec Sourcery

  • Déclencher une nouvelle revue : Commentez @sourcery-ai review sur la pull request.
  • Poursuivre les discussions : Répondez directement aux commentaires de revue de Sourcery.
  • Générer un ticket GitHub à partir d’un commentaire de revue : Demandez à Sourcery de créer un
    ticket à partir d’un commentaire de revue en y répondant. Vous pouvez aussi répondre à un
    commentaire de revue avec @sourcery-ai issue pour créer un ticket à partir de celui-ci.
  • Générer un titre de pull request : Écrivez @sourcery-ai n’importe où dans le titre de la pull
    request pour générer un titre à tout moment. Vous pouvez aussi commenter
    @sourcery-ai title sur la pull request pour (re)générer le titre à tout moment.
  • Générer un résumé de pull request : Écrivez @sourcery-ai summary n’importe où dans
    le corps de la pull request pour générer un résumé de PR à tout moment, exactement à l’endroit
    où vous le souhaitez. Vous pouvez aussi commenter @sourcery-ai summary sur la pull request pour
    (re)générer le résumé à tout moment.
  • Générer le guide du relecteur : Commentez @sourcery-ai guide sur la pull
    request pour (re)générer le guide du relecteur à tout moment.
  • Résoudre tous les commentaires Sourcery : Commentez @sourcery-ai resolve sur la
    pull request pour résoudre tous les commentaires Sourcery. Utile si vous avez déjà
    traité tous les commentaires et ne souhaitez plus les voir.
  • Ignorer toutes les revues Sourcery : Commentez @sourcery-ai dismiss sur la pull
    request pour ignorer toutes les revues Sourcery existantes. Particulièrement utile si vous
    voulez repartir de zéro avec une nouvelle revue — n’oubliez pas de commenter
    @sourcery-ai review pour déclencher une nouvelle revue !

Personnaliser votre expérience

Accédez à votre tableau de bord pour :

  • Activer ou désactiver des fonctionnalités de revue comme le résumé de pull request
    généré par Sourcery, le guide du relecteur, et d’autres.
  • Changer la langue de la revue.
  • Ajouter, supprimer ou modifier des instructions personnalisées de revue.
  • Ajuster d’autres paramètres de revue.

Obtenir de l’aide

Original review guide in English

🧙 Sourcery is reviewing your pull request!


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @thierryvm, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@thierryvm
thierryvm merged commit e82c9c5 into main Jul 25, 2026
10 checks passed
@thierryvm
thierryvm deleted the fix/settings-locale-select branch July 25, 2026 23:56
thierryvm added a commit that referenced this pull request Jul 26, 2026
…SEO (#265)

Handoff de fin de session. Documentation uniquement, 1 fichier.

**Livré** : #261 (redirections localisées), #262 (registre des
destinations de nav), #263 (sélecteur de langue), #264 (SEO) — toutes
mergées.

**Reste à faire, ordonné** : le lot 2 de la refonte UX en tête, avec les
trois limites que le lot 1 s'est explicitement interdit de traiter
reprises comme entrées plutôt que laissées à re-découvrir. Puis la
reconnexion fantôme, sans diagnostic à ce jour. Puis les dettes tracées.

**Frictions d'environnement consignées** parce qu'elles ont coûté du
temps deux fois chacune : `playwright.config.ts` ne charge pas
`.env.local`, `e2e:auth` sert un build prod où le rate limit échoue en
fermé sur l'Upstash factice, la variable est `E2E_BASE_URL`, et le port
3000 appartient à un autre projet.

Miroir dans le vault Obsidian, slug vérifié via `_index.md`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:settings User settings status:review-needed Ready for review type:fix Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant