fix(i18n): preserve reading context across locale changes - #82
Conversation
NavigationFocusManager forced window.scrollTo(top:0) and focus(main) on
every non-hash pathname change, including ES<->EN switches mid-page,
because it could not tell a locale switch from a normal navigation.
RoutedLanguageProvider now captures a section-relative scroll context
(centered section id + progress, page-ratio fallback, top-anchor flag)
before navigating to the equivalent-locale route and tags the navigation
with locale-switch state. NavigationFocusManager skips its forced
top-scroll/focus for that state, and a new LocaleSwitchScrollRestoration
component re-aligns the same section after the equivalent page renders
(existing hash targets still take priority). Focus is left on the
language toggle instead of being moved to main. SeoHead now commits
Helmet updates synchronously (defer={false}) to close a theoretical
one-paint window where title/canonical could lag the new locale.
Cover the new localeScroll capture/restore functions directly, plus integration coverage through App: mid-page scroll survives a locale switch, top stays top, normal Navbar navigation still scrolls to top, an existing hash target still wins over section-relative restoration, the mobile menu closes on switch, and html lang/title/canonical/OG/ hreflang/JSON-LD reconcile to the target locale with no duplicates. Updates the pre-existing focus assertion in Semantics.test.tsx: a locale switch now keeps focus on the language toggle instead of jumping to main, matching the new contract.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
The DevRodri brand link is a plain react-router Link to the localized home path. Clicking it from another route already worked (pathname changes, so NavigationFocusManager's existing effect fires and scrolls to top). But clicking it while already on the clean localized Home produced no pathname or hash change at all, so that same effect short-circuited and never scrolled - while React Router still silently pushed a redundant history entry to the identical URL. Navbar now intercepts the brand click only for that exact case (current pathname/search/hash already equal the target Home) and scrolls to top directly, with no navigation and no history push. Every other click (different route, or a modified click for new-tab/window) still falls through to the normal Link navigation unchanged. The mobile menu is closed on every brand click. No navigation state is attached, so a brand click is never treated as a locale switch and never consumes a pending section-scroll snapshot from a prior language switch.
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bcddfc58cd
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const el = document.getElementById(key); | ||
| if (el === null) continue; | ||
| const rect = el.getBoundingClientRect(); |
There was a problem hiding this comment.
Measure section containers instead of heading IDs
On the services, business-websites, custom-software, and LEM-BOX pages, the configured keys identify <h2> elements rather than their enclosing sections (for example, services-choose in ServicesHubPage.tsx). When the reader is below a heading, this calculation clamps sectionProgress to 1, and restoration centers the heading's bottom instead of preserving the position within the section, producing a potentially large jump toward the section start. Resolve each key to its enclosing section or place the IDs on the section containers before calculating progress.
Useful? React with 👍 / 👎.
| function getNavbarHeight(): number { | ||
| const navbar = document.querySelector<HTMLElement>("[data-nojs-navbar]"); | ||
| return navbar?.getBoundingClientRect().height ?? 0; |
There was a problem hiding this comment.
Exclude the open mobile panel from navbar height
During a mobile language switch, captureLocaleScrollContext runs while the dropdown is still open because the language handler calls setLanguage before closeMenu. The selected navbar element therefore includes the entire expanded panel, while restoration measures the collapsed navbar; the two viewport centers can differ by hundreds of pixels and shift the reader after switching languages. Measure only the persistent top bar or capture after the mobile panel has closed.
AGENTS.md reference: AGENTS.md:L4-L11
Useful? React with 👍 / 👎.
Summary
Fixes DevRodri UX-LANG-01: switching ES↔EN mid-page (Home Portfolio/FAQ, Services hub, LEM-BOX case) reset scroll to the top instead of staying in the equivalent section. Also hardens the pre-existing
SPA_LANGUAGE_SWITCH_STALE_HEADbacklog item defensively.Root cause (confirmed):
NavigationFocusManagerinsrc/App.tsxforcedwindow.scrollTo(top:0)+focus(main)on every pathname change with no hash — it had no way to distinguish a locale switch from a normal page navigation, since a locale switch is a normalPUSHnavigation to a different pathname with the same (usually empty) hash.Fix:
RoutedLanguageProvidercaptures a section-relative scroll context (centered section id + progress within it, page-ratio fallback, top-anchor flag) from stable, locale-invariant DOM ids that already exist (portfolio,faq,services-directory,lem-box-ecosystem, etc.) before navigating, and tags the navigation with{ localeSwitch: true, scrollContext }state.NavigationFocusManagerskips its forced top-scroll/focus only for that tagged state; normal navigation is untouched.LocaleSwitchScrollRestorationcomponent re-aligns the same section (doublerequestAnimationFrame, no smooth scroll) once the equivalent-locale page renders. An existing hash target still wins over section-relative restoration. Focus stays on the language toggle instead of jumping tomain.SeoHeadnow sets<Helmet defer={false}>to force synchronous head commits, closing a theoretical one-paint window where title/canonical could lag the new locale during an SPA switch.No visible copy, design, routing, URLs, CSP, or dependencies changed.
Test plan
npm run typecheck/npm run lint— cleannpm run test:run— 32/32 suites, 343/343 tests (331 baseline + 12 new)npm run build(client + SSR + prerender + verify-prerender + verify-http) — all passsrc/lib/localeScroll.tsunit tests: section capture/restore math, top-anchor short-circuit, page-ratio fallbackSemantics.test.tsxassertion: locale switch keeps focus on the language toggle (notmain)🤖 Generated with Claude Code