Skip to content

fix(i18n): les redirections serveur portent la langue de l'utilisateur#261

Merged
thierryvm merged 1 commit into
mainfrom
fix/i18n-localised-server-redirects
Jul 25, 2026
Merged

fix(i18n): les redirections serveur portent la langue de l'utilisateur#261
thierryvm merged 1 commit into
mainfrom
fix/i18n-localised-server-redirects

Conversation

@thierryvm

@thierryvm thierryvm commented Jul 25, 2026

Copy link
Copy Markdown
Owner

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 :

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.
Original summary in English

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.

localePrefix: 'as-needed' puts French on unprefixed URLs — the French prefix is
/fr-BE, never the empty string — so a path like /login matches no locale prefix
at all. Until #258 next-intl covered for that: the cookie branch of
localeDetection read NEXT_LOCALE and 307'd the request to /en/login. #258 turned
that detection off, because it was the same gate that let a background prefetch
silently rewrite the user's language. The safety net went with it, and every
bare redirect() started dropping English users on French pages — session
expiry, login, logout, end of onboarding, admin refusal.

The helper I planned to write already existed. src/i18n/navigation.ts exposes a
redirect from createNavigation(routing) that returns never, throws synchronously
like next/navigation's, and whose type FORCES the locale to be passed. No new
module, and it removes a real hazard: eslint has no type-aware linting here, so
no-floating-promises is off, and an async redirect helper would have let a
forgotten await fall through — the guard would not throw, and the visitor would
proceed unauthenticated with neither TypeScript nor lint complaining.

TypeScript does not propagate never-narrowing from a destructured binding, so
the code after each call read as reachable and surfaced 8 possibly-null errors.
Each call site now returns the redirect explicitly, which makes the termination
visible to both the reader and the compiler.

Two things are deliberately NOT localised. Google's OAuth URL is absolute and
external — prefixing it would turn it into a broken same-origin path — so it
keeps next/navigation's redirect under the explicit alias redirectToExternalUrl.
And auth/callback keeps resolving the locale from the cookie: the proxy matcher
excludes that route, so the middleware never runs there and the header
getLocale() depends on is absent; it would fall back to a path costing an extra
auth.getUser() plus a users select on the OAuth hot path, for a value the cookie
already carries. Both now say so in a comment.

requireUser's redirectTo parameter is removed rather than validated: none of its
six callers passes an argument, so localising a caller-supplied path would have
invented an open-redirect surface.

Measured A/B against production: /en/app used to 307 to /login (French) and now
307s to /en/login, while an unprefixed /app still goes to /login unchanged.

The redirecting branches had zero coverage — the neighbouring suite only
exercises getOptionalUser and never mocks navigation. The new test mocks a
redirect that THROWS, because an inert mock would let requireUser fall through
and return undefined as User, keeping the suite green over a guard that no
longer guards. Worth stating plainly: the authenticated Playwright specs are
skipped in CI, so these unit tests are the only automated net for this
regression class, not a complement to an E2E one.
@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 7:49pm

@github-actions github-actions Bot added area:i18n Internationalization 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

  • Lancer une nouvelle revue : Commentez @sourcery-ai review sur la pull request.
  • Continuer 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 répondant à celui-ci. Vous pouvez aussi répondre à un
    commentaire de revue avec @sourcery-ai issue pour créer un ticket à partir de ce commentaire.
  • 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à 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à
    pris en compte tous les commentaires et ne voulez 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 lancer 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.
  • Modifier la langue de la revue.
  • Ajouter, supprimer ou modifier des instructions de revue personnalisées.
  • 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 52ecb4e into main Jul 25, 2026
10 checks passed
@thierryvm
thierryvm deleted the fix/i18n-localised-server-redirects branch July 25, 2026 22:40
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:i18n Internationalization 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