fix(i18n): résolution de langue déterministe — le préfixe d'URL décide, plus le cookie#258
Merged
Merged
Conversation
The language switcher kept reverting to English on its own, roughly half the time, and never the other way round. next-intl's syncCookie rewrites NEXT_LOCALE whenever the locale resolved from the URL differs from the cookie, and the URL prefix always wins — so any request to /en... while the cookie held fr-BE reset the stored language for a year. It is a race: an in-flight /en... request lands after the Server Action's Set-Cookie. The page keeps rendering in French while the cookie flips, and the NEXT navigation 307s to /en. The FR->EN direction is immune because its requests target unprefixed URLs, which is exactly why it always came back to English. The fix cannot live in the middleware. Three attempts were built and measured against production builds first: Next canonises RSC requests before middleware runs, stripping the rsc / next-router-prefetch / sec-purpose headers AND the ?_rsc query param, so a prefetch and a real navigation are indistinguishable there. The write had to be removed rather than the request qualified. localeDetection: false is not an extra — it is required by the line above. In next-intl's resolveLocale, the cookie and Accept-Language are two branches of the same localeDetection gate, so dropping the cookie alone would promote Accept-Language to sole detector. Since French lives on unprefixed URLs under as-needed, an English-browser visitor who picked French would then be 307'd back to /en on every unprefixed URL — French unreachable, deterministically, for a whole class of users; a Dutch-speaking visitor would be pushed to /nl-BE, which is not translated. plan-reviewer caught this before any code was written. Trade-off re-confirmed by @Thierry: / always renders French, whatever the browser language. It also stops the app auto-serving locales that have no validated translation. The cookie is kept: setLocaleAction still writes it, and it still has two readers — the post-OAuth redirect target and the root 404, both outside the [locale] segment. It simply no longer takes part in middleware routing. Tests. Every Playwright project pins locale: 'fr-BE', so the suite was structurally blind to the regression class above; locale-detection-off.spec.ts uses test.use({ locale: 'en-US' }) and asserts an English browser choosing French keeps French, on soft and hard navigation. Switcher specs 2 and 3 relied on the cookie-based resolution path this commit removes — they now navigate to the prefixed paths the app's own <Link>s emit, and spec 2 asserts the counterpart, that unprefixed URLs are French for everyone. routing.test.ts locks both flags so a silent revert breaks the build. Measured: the regression test was red on the previous config (Expected fr-BE, Received en) and is green here; e2e/i18n 7 passed on a production build; 1646 unit tests pass.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
|
🧙 Sourcery examine votre pull request ! Conseils et commandesInteragir avec Sourcery
Personnaliser votre expérienceAccédez à votre dashboard pour :
Obtenir de l’aide
Original review guide in English🧙 Sourcery is reviewing your pull request! Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
…ile projects The new locale-detection spec clicked the switcher directly, which times out on the mobile projects: there the switcher lives inside the nav drawer and is not visible. That is the same reason locale-switcher.spec.ts is testIgnore'd on mobile-safari and mobile-chrome. The contract under test — which locale an en-US browser resolves to — is viewport-independent, so a desktop viewport in that one test is enough, and the two navigation-only tests keep covering the contract on every project. Verified locally against a production build: 13 passed across all five projects, including mobile-safari and mobile-chrome.
…o audit verdicts i18n-auditor returned NO-GO on a real P0, confirmed by measurement rather than taken at face value: /app with a NEXT_LOCALE=en cookie 307s to /en/app on the current production config, and to an unprefixed /login here — French. The middleware was an implicit safety net for the ~9 bare next/navigation redirect() call sites in the auth guards and Server Actions. Not fixed here: those are auth guards and Server Actions, which is the heavy lane, and extending a PR's scope mid-flight is a banned action. @Thierry arbitrated on 2026-07-25 to merge this and chain a dedicated follow-up: the bug this closes is daily and in production, while the regression only affects authenticated English-speaking users, of which there are none before launch. Also records what both audits found outside this scope: a broken locale select in Settings, a third undocumented cookie reader, and four pre-existing SEO canonical/sitemap bugs.
thierryvm
added a commit
that referenced
this pull request
Jul 25, 2026
…ées par les audits (#259) ## Pourquoi @Thierry a constaté que `/app/commitments` (« Engagements ») est inatteignable depuis la nav mobile. Il a choisi de **ne pas corriger à chaud** : la nav mobile est refondue d'un bloc en Phase 1 du programme UX, un patch isolé serait jeté. Cette PR ne contient donc **aucun changement de code** — uniquement la trace, pour que le sujet entre dans le périmètre de la Phase 1 au lieu d'être redécouvert. ## Le trou, confirmé La route existe et fonctionne, mais aucune surface de navigation mobile ne la référence : | Surface | Engagements ? | |---|---| | `Header.tsx` (7 destinations) | oui, mais `hidden … lg:flex` → desktop ≥ 1024 px | | `BottomTabBar` (4 onglets) | non | | `MoreSheet` | **non** | | `EngagementsCard` (cockpit) | oui — seul point d'entrée mobile | Hors du cockpit, Engagements n'existe plus sur mobile. ## Le constat qui compte C'est structurel : les destinations sont **dupliquées dans trois composants** sans contrat commun. Rien n'empêche d'ajouter une route et d'oublier une ou deux surfaces — c'est exactement ce qui s'est produit. La Phase 1 reçoit donc une attente explicite : un registre unique de destinations, plus un test qui échoue si une route de `app/**` n'y figure pas. ## Également tracé `docs/audits/2026-07-25-dettes-tracees-audits-i18n-seo.md` recense ce que `i18n-auditor` et `seo-geo-auditor` ont remonté pendant #258, hors de son périmètre : - **P0** — les redirections serveur non préfixées perdent la langue (introduit par #258, arbitré par @Thierry, PR de suivi à faire). Mesuré : `/app` avec cookie `en` faisait `307 → /en/app`, fait désormais `307 → /login` (français). ~9 sites dans les gardes auth et Server Actions. - **P1** — le sélecteur de langue des Réglages ne peut rien enregistrer d'autre que `fr-BE` (le `<Select>` propose `fr-FR`/`en-GB`, absents de l'enum `LOCALES`), et son action n'écrit ni le cookie ni ne revalide. Préexistant. - **P1** — 4 bugs SEO préexistants : pages `noindex` soumises au sitemap, canoniques cross-locale sur la FAQ et les pages légales, glossaire qui se canonicalise vers l'accueil, locales non traduites indexables. - **P2** — 3ᵉ lecteur du cookie non documenté, deep-links des locales cachées non testés, `llms.txt` qui surestime le périmètre livré. Aucun n'est corrigé ici : étendre le scope d'une PR en vol est une action bannie par la doctrine. ## Périmètre Documentation uniquement — 2 fichiers, 0 ligne de code applicatif. ## Summary by Sourcery Documenter les problèmes connus d’UX et de SEO/i18n sans modifier le code de l’application afin de garantir qu’ils soient suivis et intégrés dans les travaux à venir. Documentation : - Étendre la spécification de conception du programme UX avec la documentation des lacunes de navigation sur mobile et des problèmes de routage structurel liés aux Engagements. - Ajouter un document d’audit recensant les dettes i18n, de gestion des locales et de SEO en suspens, identifiées lors des audits récents. <details> <summary>Original summary in English</summary> ## Summary by Sourcery Document known UX and SEO/i18n issues without changing application code to ensure they are tracked and included in upcoming work. Documentation: - Extend the UX program design spec with documented mobile navigation gaps and structural routing issues around Engagements. - Add an audit document capturing outstanding i18n, locale handling, and SEO debts identified during recent audits. </details>
thierryvm
added a commit
that referenced
this pull request
Jul 25, 2026
… et mergé (#260) Le handoff décrivait encore #258 comme en vol. Mis à jour sur l'état clos : 7 PR mergées, working tree propre, aucune PR ouverte. **Bug 1 fermé.** Le correctif livré est consigné, mais surtout la raison pour laquelle il ne pouvait pas vivre dans le middleware — pour que personne ne retente cette couche : Next canonise les requêtes RSC avant l'exécution du middleware, en retirant à la fois les en-têtes de prefetch et le paramètre `?_rsc`, donc un prefetch et une navigation réelle y sont indiscernables. Le piège d'ordonnancement `ResponseCookies` est conservé également. Le reste à faire est désormais explicite et ordonné : le P0 remonté par l'audit i18n (les redirections serveur non préfixées perdent la langue — arbitré, PR de suivi), le trou de nav mobile que @Thierry a choisi de reporter en Phase 1 plutôt que de patcher, et les défauts préexistants du sélecteur de langue des Réglages et des canoniques SEO. Miroir dans le vault Obsidian, conformément à la règle de double redondance. Documentation uniquement — 1 fichier, 0 ligne de code. ## Summary by Sourcery Documentation : - Réviser la remise locale/CSP afin de lister l’ensemble final des PR fusionnées, documenter la correction livrée et les limitations du middleware pour la gestion des paramètres régionaux, et énumérer les tâches en suspens concernant l’i18n, la navigation mobile, les paramètres et le SEO en tant qu’éléments suivis. <details> <summary>Original summary in English</summary> ## Summary by Sourcery Documentation: - Revise the locale/CSP handoff to list the final merged PR set, document the delivered fix and middleware limitations for locale handling, and enumerate outstanding i18n, mobile navigation, settings, and SEO tasks as tracked items. </details> - Réviser le document de transfert de session locale et CSP pour indiquer que tous les PR pertinents ont été fusionnés, décrire la correction apportée à la gestion des locales et les limitations du middleware, et énumérer les problèmes en suspens liés à l’i18n, à la navigation mobile, aux paramètres et au SEO en tant qu’éléments de travail suivis. <details> <summary>Original summary in English</summary> ## Summary by Sourcery Documentation : - Réviser la remise locale/CSP afin de lister l’ensemble final des PR fusionnées, documenter la correction livrée et les limitations du middleware pour la gestion des paramètres régionaux, et énumérer les tâches en suspens concernant l’i18n, la navigation mobile, les paramètres et le SEO en tant qu’éléments suivis. <details> <summary>Original summary in English</summary> ## Summary by Sourcery Documentation: - Revise the locale/CSP handoff to list the final merged PR set, document the delivered fix and middleware limitations for locale handling, and enumerate outstanding i18n, mobile navigation, settings, and SEO tasks as tracked items. </details> </details>
thierryvm
added a commit
that referenced
this pull request
Jul 25, 2026
#261) Ferme le P0 laissé ouvert par #258, que tu avais arbitré. ## Le problème `localePrefix: 'as-needed'` met le français sur les URLs **non préfixées** : le préfixe du français est `/fr-BE`, jamais la chaîne vide. Un chemin comme `/login` ne matche donc aucun préfixe. Jusqu'à #258, next-intl rattrapait le coup via la branche cookie de `localeDetection`. #258 a désactivé cette détection — c'était la **même garde** qui laissait un prefetch réécrire silencieusement ta langue. Le filet est parti avec, et chaque `redirect()` nu renvoyait un utilisateur anglophone sur une page française : session expirée, connexion, déconnexion, fin d'onboarding, refus admin. ## Le correctif Le helper que je comptais écrire **existait déjà** — `plan-reviewer` l'a relevé, et c'est meilleur que ma proposition : ```ts redirect({ href: '/login', locale: await getLocale() }) ``` Ce `redirect` de `@/i18n/navigation` retourne `never`, lève immédiatement, et **son type impose la locale**. ### Le piège évité J'avais envisagé un helper `async` (`await redirectLocalised('/login')`). Vérifié : `no-floating-promises` n'est pas activé (pas de lint typé). Un `await` oublié sur une garde d'auth n'aurait levé **ni erreur TypeScript ni erreur de lint** — la garde ne lèverait plus et l'utilisateur passerait **non authentifié**. Le redirect synchrone rend cette classe d'erreur impossible. ### Terminaison explicite TypeScript ne propage pas le narrowing `never` depuis un destructuring, contrairement au `redirect` de `next/navigation` : 8 erreurs `possibly null` sont apparues. Corrigé par un `return redirect(...)` à chaque site. ## Ce qui n'est délibérément PAS localisé - **L'URL OAuth Google** — absolue et externe, la préfixer la casserait. Elle garde le `redirect` de `next/navigation` sous l'alias explicite `redirectToExternalUrl`. - **`auth/callback`** garde sa résolution par cookie : la route est exclue du matcher du proxy, donc `getLocale()` y retomberait sur un chemin coûtant un `auth.getUser()` + un select `users` sur le chemin chaud de l'OAuth. - **`proxy.ts` et `routing.ts`** ne sont pas touchés — un 307 par lecture de cookie dans le proxy est l'alternative tentante en une ligne, interdite par la note « do not retry that layer ». Le paramètre `redirectTo` de `requireUser` est **supprimé** : aucun de ses 6 appelants ne le passe, le localiser aurait inventé une surface d'open redirect. ## Preuve | Requête | Production (avant) | Cette branche | |---|---|---| | `/en/app` — visiteur anglais | `307 → /login` ❌ français | `307 → /en/login` ✅ | | `/app` — visiteur français | `307 → /login` | `307 → /login` ✅ inchangé | **Ces branches avaient zéro couverture** : la suite voisine n'exerce que `getOptionalUser`. Le nouveau test mocke un `redirect` qui **lève** — un mock inerte laisserait `requireUser` retourner `undefined as User` et le test resterait vert au-dessus d'une garde qui ne garde plus. **Angle mort assumé** : les specs Playwright authentifiées sont skippées en CI. Ces tests unitaires sont le **seul** filet automatisé pour cette régression, pas un complément. `typecheck` 0 · `lint` 0 · `test` **1652/1652** Rapport : `docs/prs/PR-i18n-localised-server-redirects-report.md` ## Summary by Sourcery Garantir que les redirections d’authentification et d’onboarding côté serveur préservent la locale de l’utilisateur au lieu de cibler systématiquement les pages en français. Bug Fixes : - Corriger les redirections liées à l’auth (login, logout, signup, onboarding, accès admin, snapshot de workspace, statut de suppression) afin que les utilisateurs anglophones soient redirigés vers les pages en anglais sous des URL préfixées par la locale. Enhancements : - Adopter le helper de redirection compatible i18n (locale-aware) dans les actions et pages côté serveur, avec une terminaison explicite via `return` pour satisfaire l’analyse de contrôle de flux de TypeScript. - Supprimer le paramètre inutilisé de cible de redirection dans `requireUser` afin d’éviter d’introduire une surface de redirection ouverte inutile. Documentation : - Ajouter un rapport de PR détaillé documentant la régression des redirections i18n, la logique de la correction locale-aware, ainsi que les flux OAuth et callback non localisés. Tests : - Ajouter des tests unitaires pour couvrir les redirections qui transportent la locale dans `requireUser` et `requireUserWithWorkspace`, et garantir que ces gardes ne peuvent pas être contournés silencieusement. - Mettre à jour les tests existants liés à l’auth afin de mocker la redirection de navigation i18n et la résolution de locale de `next-intl` au lieu de `next/navigation`. <details> <summary>Original summary in English</summary> ## Summary by Sourcery Ensure server-side auth and onboarding redirects preserve the user’s locale instead of always targeting the French pages. Bug Fixes: - Fix auth-related redirects (login, logout, signup, onboarding, admin access, workspace snapshot, deletion status) so English users are redirected to English pages under locale-prefixed URLs. Enhancements: - Adopt the locale-aware redirect helper across server actions and pages, with explicit termination via return to satisfy TypeScript’s control-flow analysis. - Remove the unused redirect target parameter from requireUser to avoid introducing an unnecessary open-redirect surface. Documentation: - Add a detailed PR report documenting the i18n redirect regression, rationale for the locale-aware fix, and the non-localised OAuth and callback flows. Tests: - Add unit tests to cover locale-carrying redirects in requireUser and requireUserWithWorkspace and to ensure these guards cannot be silently bypassed. - Update existing auth-related tests to mock the i18n navigation redirect and next-intl locale resolution instead of next/navigation. </details>
thierryvm
added a commit
that referenced
this pull request
Jul 25, 2026
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 #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 `setLocaleAction` → **pré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. <details> <summary>Original summary in English</summary> ## 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. </details>
thierryvm
added a commit
that referenced
this pull request
Jul 26, 2026
Corrige les 4 défauts SEO **préexistants** que `seo-geo-auditor` avait relevés pendant #258. Tous silencieux : rien ne cassait, Search Console aurait juste remonté des erreurs des semaines plus tard. ## Ce qui était cassé | Défaut | Effet | |---|---| | Canoniques codées en dur (`/faq`, `/legal/*`) | `/en/faq` déclarait la page **française** comme canonique — soit « la version anglaise est un doublon », en contradiction avec le hreflang de la même URL | | Index du glossaire sans `alternates` | héritait de la canonique du layout → `/glossaire`, `/en/glossaire` et `/nl-BE/glossaire` pointaient chacun vers **leur accueil** | | 3 pages légales `noindex` au sitemap | 15 URLs (3 × 5 locales) garanties « Submitted URL marked noindex » | | nl-BE / de-DE / es-ES au sitemap | on demandait à Google d'indexer des pages dont le contenu est français verbatim | ## Mesuré sur build de production | | avant | après | |---|---|---| | URLs `/legal/*` au sitemap | 15 | **0** | | Canonique de `/en/faq` | `/faq` | `/en/faq` | | Canonique de `/en/glossaire` | l'accueil | `/en/glossaire` | | hreflang de l'index glossaire | aucun | 3 (fr-BE, nl-BE, en) | ## Ce que l'audit a rattrapé sur mon propre correctif L'index du glossaire n'émettait que la canonique, sans le bloc `languages` que sa page sœur `[slug]` produit déjà. J'aurais donc reproduit **sur le correctif lui-même** le défaut que la PR ferme : une canonique sans hreflang correspondant. Corrigé. Second point pris : la locale vient désormais de `params` et non de `getLocale()`. Ce dernier ne fonctionnait que parce que le layout appelle `cookies()` pour le thème, ce qui force le rendu dynamique — un couplage implicite à un détail sans rapport. ## Tests `tests/seo/sitemap.test.ts` (5 specs) verrouille : aucune route noindex, aucune locale non traduite hors glossaire, pas de doublon d'URL, et aucun hreflang pointant vers une URL absente du sitemap. **Falsifiabilité vérifiée** : en réintroduisant l'une ou l'autre régression, 2 specs passent au rouge. `npm run test` **1674/1674** · `typecheck` 0 · `lint` 0 (7 warnings préexistants) ## Volontairement hors périmètre - Le header `Link` du middleware next-intl annonce toujours les 5 locales, plus large que ce que le sitemap soumet désormais. Le corriger implique `proxy.ts`, que la note de `routing.ts` réserve à une PR dédiée. - `robots.txt` **ne doit pas** bloquer `/legal/` : Google ne pourrait plus lire le `noindex` qui les retire réellement. - `public/llms.txt` annonce toujours le néerlandais comme langue opérationnelle — correctif d'une ligne, tracé à part. - Asymétrie assumée : le glossaire reste indexé en nl-BE (ses termes sont réellement traduits) alors que la landing ne l'est plus.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Ferme le bug « le sélecteur de langue repasse tout seul en anglais ». Exécute l'option A du diagnostic mergé en #255, amendée après revue.
Le correctif
La résolution se réduit à un seul intrant déterministe : le préfixe d'URL, avec repli sur
defaultLocale.syncCookieretourne immédiatement — la course disparaît par construction, pas par contournement.Rappel de la cause :
syncCookieréécritNEXT_LOCALEdès que le locale résolu par l'URL diffère du cookie, et l'URL gagne toujours. Une requête/en…encore en vol se termine après leSet-Cookiede la Server Action → la langue bascule pour un an. Le sens FR→EN est immunisé (URLs non préfixées), d'où « ça revient toujours à l'anglais ».Le correctif ne pouvait pas vivre dans le middleware : Next canonise les requêtes RSC avant son exécution (en-têtes et
?_rscretirés), donc un prefetch et une navigation y sont indiscernables. Trois tentatives mesurées avant d'en conclure.Ce que
plan-reviewera rattrapéMa première version ne mettait que
localeCookie: false. Elle aurait transformé un bug intermittent en bug déterministe : dansresolveLocale, le cookie etAccept-Languagesont deux branches de la même gardelocaleDetection. Retirer le cookie seul promeutAccept-Languageen détecteur unique — et comme le français vit sur les URLs non préfixées enas-needed:/ensur toute URL non préfixée → français inatteignable, 100 % du temps ;/nl-BE, qui n'est pas traduit.@Thierry avait validé l'option A sur un énoncé de coût incomplet. L'énoncé corrigé — «
/affiche toujours le français, quelle que soit la langue du navigateur » — lui a été re-soumis et confirmé.Le cookie est conservé
setLocaleActionreste son écrivain, et il garde deux lecteurs vérifiés : la cible du retour OAuth Google (auth/callback/route.ts) et la 404 racine (not-found.tsx), toutes deux hors du segment[locale]. Le supprimer casserait la connexion Google. Il ne participe simplement plus au routage.Tests
Les 5 projets Playwright épinglent
locale: 'fr-BE'— aucune spec existante ne pouvait voir la régression ci-dessus.e2e/i18n/locale-detection-off.spec.ts(nouveau) utilisetest.use({ locale: 'en-US' })et vérifie qu'un navigateur anglais qui choisit le français garde le français, en navigation douce comme dure.Les specs 2 et 3 du switcher s'appuyaient explicitement sur « the cookie-based locale resolution path » — le contrat que cette PR supprime. Réécrites vers les chemins préfixés que les
<Link>de l'app émettent réellement, avec la contrepartie assertée : l'URL non préfixée est française pour tout le monde.tests/i18n/routing.test.tsverrouille les deux flags : un revert silencieux casse le build.Preuve
localeCookie: { … }(avant)Expected "fr-BE"/Received "en"— c'est ce qui l'avait fait passer entest.fixmeen #255localeCookie: false(cette PR)e2e/i18n/sur build de production : 7 passés, 0 échecnpm run test: 1646 / 1646 ·typecheck0 erreur ·lint0 erreurRapport complet :
docs/prs/PR-i18n-locale-deterministic-report.mdSummary by Sourcery
Rendre la résolution de la locale déterministe en s’appuyant uniquement sur le préfixe d’URL, en supprimant la détection basée sur les cookies et le navigateur afin d’éliminer les conditions de course et les états de langue inaccessibles.
Bug fixes :
NEXT_LOCALEqui réinitialisait de manière intermittente la langue sélectionnée vers l’anglais après des préfetches ou des navigations sur/en.Accept-Language.Enhancements :
NEXT_LOCALEqui n’influence plus le routage.Documentation :
Tests :
localeCookieetlocaleDetectionàfalseafin que les régressions de configuration soient détectées en CI.Original summary in English
Summary by Sourcery
Make locale resolution deterministic by relying solely on the URL prefix, removing cookie- and browser-based detection to eliminate race conditions and unreachable-language states.
Bug Fixes:
Enhancements:
Documentation:
Tests:
localeCookieandlocaleDetectiontofalseso configuration regressions are caught in CI.Bug Fixes :
NEXT_LOCALEqui rétablissait de façon intermittente la langue sélectionnée à l’anglais après des prefetch ou des navigations vers/en.Accept-Language.Enhancements :
NEXT_LOCALEn’est plus utilisé pour le routage.Documentation :
Tests :
localeCookieetlocaleDetectionàfalseafin que tout revert silencieux de configuration fasse échouer la build.Original summary in English
Summary by Sourcery
Rendre la résolution de la locale déterministe en s’appuyant uniquement sur le préfixe d’URL, en supprimant la détection basée sur les cookies et le navigateur afin d’éliminer les conditions de course et les états de langue inaccessibles.
Bug fixes :
NEXT_LOCALEqui réinitialisait de manière intermittente la langue sélectionnée vers l’anglais après des préfetches ou des navigations sur/en.Accept-Language.Enhancements :
NEXT_LOCALEqui n’influence plus le routage.Documentation :
Tests :
localeCookieetlocaleDetectionàfalseafin que les régressions de configuration soient détectées en CI.Original summary in English
Summary by Sourcery
Make locale resolution deterministic by relying solely on the URL prefix, removing cookie- and browser-based detection to eliminate race conditions and unreachable-language states.
Bug Fixes:
Enhancements:
Documentation:
Tests:
localeCookieandlocaleDetectiontofalseso configuration regressions are caught in CI.Bug Fixes :
NEXT_LOCALEqui réinitialisaient de façon intermittente la langue sélectionnée vers l’anglais après des préchargements ou navigations sur/en.Enhancements :
NEXT_LOCALEne participe plus au routage.Documentation :
Tests :
localeCookieetlocaleDetectionàfalseafin que toute réversion silencieuse de la configuration fasse échouer la build.Original summary in English
Summary by Sourcery
Rendre la résolution de la locale déterministe en s’appuyant uniquement sur le préfixe d’URL, en supprimant la détection basée sur les cookies et le navigateur afin d’éliminer les conditions de course et les états de langue inaccessibles.
Bug fixes :
NEXT_LOCALEqui réinitialisait de manière intermittente la langue sélectionnée vers l’anglais après des préfetches ou des navigations sur/en.Accept-Language.Enhancements :
NEXT_LOCALEqui n’influence plus le routage.Documentation :
Tests :
localeCookieetlocaleDetectionàfalseafin que les régressions de configuration soient détectées en CI.Original summary in English
Summary by Sourcery
Make locale resolution deterministic by relying solely on the URL prefix, removing cookie- and browser-based detection to eliminate race conditions and unreachable-language states.
Bug Fixes:
Enhancements:
Documentation:
Tests:
localeCookieandlocaleDetectiontofalseso configuration regressions are caught in CI.Bug Fixes :
NEXT_LOCALEqui rétablissait de façon intermittente la langue sélectionnée à l’anglais après des prefetch ou des navigations vers/en.Accept-Language.Enhancements :
NEXT_LOCALEn’est plus utilisé pour le routage.Documentation :
Tests :
localeCookieetlocaleDetectionàfalseafin que tout revert silencieux de configuration fasse échouer la build.Original summary in English
Summary by Sourcery
Rendre la résolution de la locale déterministe en s’appuyant uniquement sur le préfixe d’URL, en supprimant la détection basée sur les cookies et le navigateur afin d’éliminer les conditions de course et les états de langue inaccessibles.
Bug fixes :
NEXT_LOCALEqui réinitialisait de manière intermittente la langue sélectionnée vers l’anglais après des préfetches ou des navigations sur/en.Accept-Language.Enhancements :
NEXT_LOCALEqui n’influence plus le routage.Documentation :
Tests :
localeCookieetlocaleDetectionàfalseafin que les régressions de configuration soient détectées en CI.Original summary in English
Summary by Sourcery
Make locale resolution deterministic by relying solely on the URL prefix, removing cookie- and browser-based detection to eliminate race conditions and unreachable-language states.
Bug Fixes:
Enhancements:
Documentation:
Tests:
localeCookieandlocaleDetectiontofalseso configuration regressions are caught in CI.