Skip to content

feat: add i18n support for /pvt/account navigation#3400

Open
lemagnetic wants to merge 8 commits into
devfrom
feat/my-account-urls-localized
Open

feat: add i18n support for /pvt/account navigation#3400
lemagnetic wants to merge 8 commits into
devfrom
feat/my-account-urls-localized

Conversation

@lemagnetic

@lemagnetic lemagnetic commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

What's the purpose of this pull request?

Localize My Account private routes so navigation and SSR redirects respect the active locale (e.g. /pt-BR/pvt/account/orders instead of dropping to /pvt/account/... on the default binding).

Jira: SFS-3228

How it works?

  • Client links: useLink / resolveLink on account menu, orders list (pagination, search, filters), order details back link, and filter slider.
  • SSR redirects: localizeRedirectDestination on My Account GSSPs (index, profile, user-details, 404, catch-all).
  • Proxy: skip subdomain rewrite when request locale differs from root binding (avoids /pt-BR/en-US/... double-prefix).
  • Hydration: useRouter().locale for price/date formatting in My Account (instead of useSession().locale on SSR).
  • Session (client): reconcileSessionLocale via SDK persisted reconcile seam, keeps URL-derived locale on IDB hydrate and tab focus sync.

How to test it?

  1. Enable localization with a path-based binding (e.g. pt-BR/pt-BR).
  2. Run @faststore/core locally and sign in.
  3. Navigate: /pt-BR/pvt/account/profile → orders → order detail → back.
  4. Confirm links, pagination, filters, and redirects keep the /pt-BR prefix.
  5. Check DevTools: no hydration mismatch on orders (price/date).
  6. Refocus the tab: session locale should not reset to default.
  7. Regression: default locale paths and localization.enabled: false unchanged.

Video (localhost):
account: b2bfaststoredev

Gravacao.de.Tela.2026-06-25.as.21.58.28.mov

Summary by CodeRabbit

  • New Features

    • Account pages now use locale-aware redirects and links, improving navigation across localized storefronts.
    • Session and pricing formatting now follow the active locale from the current route.
  • Bug Fixes

    • Fixed double-locale URL issues that could cause incorrect redirects or hydration mismatches.
    • Improved account orders and profile navigation so filters, pagination, and back links keep working with localized paths.
    • Redirects for account access and error pages now preserve the correct language version.

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Account navigation now routes through the shared link resolver and router locale, while redirect destinations, proxy rewriting, and persisted session hydration all use locale-aware helpers instead of hardcoded paths or session-derived locale values.

Changes

Account navigation and locale display

Layer / File(s) Summary
Shared links
packages/core/src/components/account/Drawer/OrganizationDrawer/OrganizationDrawerBody.tsx, packages/core/src/components/account/Menu/Menu.tsx, packages/core/src/components/ui/Link/Link.tsx
OrganizationDrawerBody and Menu import the local Link component, and the shared Link wrapper passes locale={false} to internal NextLink rendering.
Orders routes
packages/core/src/components/account/orders/ListOrders/FilterSlider/FilterSlider.tsx, packages/core/src/components/account/orders/ListOrders/ListOrders.tsx, packages/core/src/components/account/orders/ListOrders/ListOrdersTable/ListOrdersTable.tsx, packages/core/src/components/account/orders/OrderDetails/OrderDetailsHeader.tsx
Orders filter, list, pagination, and detail navigation compute the orders href through useLink() instead of hardcoded /pvt/account/orders strings.
Order locale source
packages/core/src/components/account/orders/ListOrders/ListOrdersTable/ListOrdersTable.tsx, packages/core/src/components/account/orders/ListOrders/SelectedTags/SelectedTags.tsx, packages/core/src/components/account/utils/useFormatPrice.ts
ListOrdersTable, SelectedTags, and useFormatPrice read locale from useRouter() rather than from the session.

Localized redirects and validation

Layer / File(s) Summary
Redirect helper
packages/core/src/utils/localization/localizeRedirectDestination.ts, packages/core/test/utils/localization/localizeRedirectDestination.test.ts, packages/core/src/experimental/index.ts
localizeRedirectDestination localizes internal redirect destinations, the tests cover internal, external, vanity-path, and locale-prefix cases, and the experimental barrel exports unstable aliases for the link and redirect helpers.
Locale validation
packages/core/src/utils/localization/withLocaleValidation.ts
withLocaleValidationSSR no longer bypasses locale validation for private /pvt/ routes.
Account redirects
packages/core/src/experimental/myAccountServerSideProps.ts, packages/core/src/pages/pvt/account/404.tsx, packages/core/src/pages/pvt/account/[...unknown].tsx, packages/core/src/pages/pvt/account/index.tsx, packages/core/src/pages/pvt/account/profile.tsx, packages/core/src/pages/pvt/account/user-details.tsx
Account SSR and SSG redirect destinations are wrapped with localizeRedirectDestination, and the unknown-path SSG now receives Next.js locale inputs.

Proxy, store, and session locale plumbing

Layer / File(s) Summary
Proxy rewrite
packages/core/src/proxy.ts, packages/core/test/proxy.localization.test.ts
proxy.ts resolves the active locale from the request and skips subdomain rewriting when Next.js already assigned a different locale; the proxy localization tests cover matching, mismatching, and _next/data requests.
Store reconcile
packages/sdk/src/store/persisted.ts, packages/sdk/src/store/composed.ts, packages/sdk/src/session/index.ts, packages/sdk/src/index.ts, packages/sdk/test/store/persisted.test.ts
The SDK store APIs add Reconcile and CreateStoreOptions, thread reconciliation through persisted, createStore, and createSessionStore, and the persisted-store tests cover hydration and sync reconciliation.
Session reconciliation
packages/core/src/sdk/session/initialSession.ts, packages/core/src/sdk/session/index.ts, packages/core/test/sdk/session/reconcileSessionLocale*.browser.test.ts
reconcileSessionLocale replaces the old locale-corrector flow, session initialization passes it into the persisted store when localization is enabled, and browser tests cover enabled and disabled cases.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • vtex/faststore#3185: Shares the link-resolution and locale-prefix handling changes around Link and localized navigation.
  • vtex/faststore#3239: Touches the same proxy subdomain locale rewrite path and _next/data locale handling.
  • vtex/faststore#3281: Changes the session locale hydration and reconciliation path that now feeds the persisted reconcile hook.

Suggested labels

bug

Suggested reviewers

  • renatomaurovtex
  • lucasfp13

Poem

🌍 A path was shared, a locale found,
The router kept the words well-bound.
Redirects and sessions sang in tune,
And orders read the right-side moon.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.71% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the PR’s main theme of adding i18n support for /pvt/account navigation and related locale handling.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/my-account-urls-localized

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codesandbox-ci

codesandbox-ci Bot commented Jun 19, 2026

Copy link
Copy Markdown

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

@pkg-pr-new

pkg-pr-new Bot commented Jun 19, 2026

Copy link
Copy Markdown

Open in StackBlitz

@faststore/api

npm i https://pkg.pr.new/vtex/faststore/@faststore/api@5052d06

@faststore/cli

npm i https://pkg.pr.new/vtex/faststore/@faststore/cli@5052d06

@faststore/components

npm i https://pkg.pr.new/vtex/faststore/@faststore/components@5052d06

@faststore/core

npm i https://pkg.pr.new/vtex/faststore/@faststore/core@5052d06

@faststore/diagnostics

npm i https://pkg.pr.new/vtex/faststore/@faststore/diagnostics@5052d06

@faststore/lighthouse

npm i https://pkg.pr.new/vtex/faststore/@faststore/lighthouse@5052d06

@faststore/sdk

npm i https://pkg.pr.new/vtex/faststore/@faststore/sdk@5052d06

@faststore/ui

npm i https://pkg.pr.new/vtex/faststore/@faststore/ui@5052d06

commit: 5052d06

@vtex-pr-sentinel

Copy link
Copy Markdown

🛡️ SDD Check — action required

I couldn't detect an SDD in this PR. Please check one option below (requires write access to the repo):

  • SDD lives in another PR — paste the SDD PR URL here:
  • This PR doesn't need an SDD
  • SDD applies, but I'm not adopting it in this PR

Route My Account links through the core Link so they pick up the active
locale's path prefix, and localize getServerSideProps/getStaticProps
redirect destinations via the new localizeRedirectDestination helper.
Remove the /pvt/ skip in withLocaleValidation now that account routes are
locale-aware.
Pass locale={false} to NextLink so resolveLink stays the single source of
truth for the localized href. This prevents the server from prepending the
router locale on top of a custom-path href (e.g. /it-IT/europe/it/...),
which diverged from the client (/europe/it/...) and triggered a hydration
mismatch.
…ot binding

When the default locale uses a root binding on the same hostname as
path-prefixed locales (e.g. /pt-BR), rewriteSubdomainRequest was injecting
/en-US on top of an already-resolved locale, producing broken paths like
/pt-BR/en-US/... and 404s in dev and production.

Skip the subdomain rewrite when Next has already assigned a different
valid locale to the request (including _next/data routes).
… sync

The session locale could reset to the default (e.g. en-US) when refocusing
the window: the persisted middleware re-read a stale locale from IndexedDB
on focus/visibilitychange and clobbered the correct in-memory value. The URL
is the source of truth for locale/currency/salesChannel, but neither IDB nor
validateSession (called with an empty search on path-based bindings) could
heal it.

Add an optional `reconcile` seam to the `persisted` middleware, applied both
on hydration and on the cross-tab sync, threaded through `createStore` /
`createSessionStore` via `CreateStoreOptions`. @faststore/core supplies the
locale-aware policy (`reconcileSessionLocale`) that forces locale/currency/
salesChannel from the URL while preserving the rest of the payload, replacing
the one-shot `installLocaleCorrector` (which only fixed memory and never
covered the focus sync).

The new params are optional and backward-compatible, but they alter public
type signatures of @faststore/sdk.
The persisted middleware reconciled stale fields (e.g. URL-derived locale)
into memory on hydration, but the IDB write subscriber was still gated off
(`hydrated` flipped only after the hydration set), so the persisted payload
stayed stale until the next write (a validateSession response or the focus
sync) — which is non-deterministic under load churn.

Flip `hydrated` right after the initial IDB read, before the hydration set,
so the reconciled value is persisted immediately at load. The read-before-write
protection is preserved (writes are still blocked until the initial read
completes).
@lemagnetic lemagnetic force-pushed the feat/my-account-urls-localized branch from e0432dd to 79ccfb1 Compare June 24, 2026 19:45
@lemagnetic lemagnetic changed the title feat: my account urls localized feat: add i18n support for /pvt/account navigation Jun 26, 2026
@lemagnetic lemagnetic marked this pull request as ready for review June 26, 2026 01:04
@lemagnetic lemagnetic requested a review from a team as a code owner June 26, 2026 01:04
@lemagnetic lemagnetic requested review from eduardoformiga and renatamottam and removed request for a team June 26, 2026 01:04
@sonar-workflows

Copy link
Copy Markdown

Failed Quality Gate failed

  • 7 New Issues (is greater than 0)
  • 68.30% Coverage on New Code (is less than 80.00%)

Project ID: vtex_faststore_f0a862d5-9557-49f9-8d09-de40caa76622

View in SonarQube

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/core/src/components/account/orders/ListOrders/SelectedTags/SelectedTags.tsx (1)

51-59: 🎯 Functional Correctness | 🟡 Minor

Default the router locale before formatting dates. useRouter().locale is optional, while formatFilterDate requires a string. Fall back to the app’s default locale (or accept string | undefined in the helper and handle it there) before calling it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/core/src/components/account/orders/ListOrders/SelectedTags/SelectedTags.tsx`
around lines 51 - 59, The date formatting in SelectedTags uses
useRouter().locale directly, but locale can be undefined while formatFilterDate
expects a string. Update SelectedTags to default to the app’s fallback locale
before formatting dateInitial and dateFinal, or adjust formatFilterDate to
accept string | undefined and resolve the default internally. Use the existing
useRouter, formatFilterDate, formattedDateInitial, and formattedDateFinal logic
to keep the fix localized.

Source: Path instructions

🧹 Nitpick comments (2)
packages/core/src/proxy.ts (2)

55-72: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Flatten the data-route conditional.

The nested if inside the dataMatch block can collapse into a single guard, improving readability. While here, RegExp.exec() is the preferred (and slightly faster) form over String.match() for a non-global regex (per SonarQube).

♻️ Flatten and switch to exec
-  const { pathname } = request.nextUrl
-  const dataMatch = pathname.match(DATA_ROUTE_RE)
-
-  if (dataMatch) {
-    const localeFromData = dataMatch[1]
-    if (validLocales.has(localeFromData)) {
-      return localeFromData
-    }
-  }
+  const { pathname } = request.nextUrl
+  const dataMatch = DATA_ROUTE_RE.exec(pathname)
+
+  if (dataMatch && validLocales.has(dataMatch[1])) {
+    return dataMatch[1]
+  }

As per path instructions: "Prefer flat conditionals over nested ifs."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/proxy.ts` around lines 55 - 72, In
getActiveLocaleFromRequest, flatten the nested dataMatch check and switch from
pathname.match(DATA_ROUTE_RE) to DATA_ROUTE_RE.exec(pathname) for the data-route
lookup. Use a single guard that verifies a match exists and that the captured
locale is valid before returning it, keeping the existing locale fallback logic
from request.nextUrl unchanged.

Sources: Path instructions, Linters/SAST tools


92-96: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant locale validation in the guard.

getActiveLocaleFromRequest only ever returns a locale already present in validLocales (or undefined), so the validLocales.has(activeLocale) check here is dead weight. A truthy activeLocale is already valid.

♻️ Simplify guard
-  if (
-    activeLocale &&
-    validLocales.has(activeLocale) &&
-    activeLocale !== locale
-  ) {
-    return null
-  }
+  if (activeLocale && activeLocale !== locale) {
+    return null
+  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/proxy.ts` around lines 92 - 96, The guard in
getActiveLocaleFromRequest handling is redundant because activeLocale is already
guaranteed to be valid when defined. Simplify the conditional in proxy.ts by
removing the validLocales.has(activeLocale) check and keeping only the truthy
activeLocale comparison against locale, so the intent stays clear and the logic
matches the return contract of getActiveLocaleFromRequest.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@packages/core/src/components/account/orders/ListOrders/ListOrdersTable/ListOrdersTable.tsx`:
- Line 111: The locale from useRouter() in ListOrdersTable may be undefined, but
formatOrderDate expects a string, so make the date formatting path type-safe.
Update the ListOrdersTable component to either provide a fallback locale before
calling formatOrderDate or adjust formatOrderDate to accept string | undefined,
and make sure all calls from ListOrdersTable use the safe value.

In `@packages/core/test/sdk/session/reconcileSessionLocale.browser.test.ts`:
- Line 86: The test suite currently targets the source module initialSession.ts
but the filename is keyed to the exported symbol reconcileSessionLocale; rename
the test file to match the source file name convention (for example,
initialSession.browser.test.ts) so it tracks the module under test. Update any
references or adjacent test organization to keep the suite aligned with the
source file and preserve predictable discovery for getInitialSession and
reconcileSessionLocale.

---

Outside diff comments:
In
`@packages/core/src/components/account/orders/ListOrders/SelectedTags/SelectedTags.tsx`:
- Around line 51-59: The date formatting in SelectedTags uses useRouter().locale
directly, but locale can be undefined while formatFilterDate expects a string.
Update SelectedTags to default to the app’s fallback locale before formatting
dateInitial and dateFinal, or adjust formatFilterDate to accept string |
undefined and resolve the default internally. Use the existing useRouter,
formatFilterDate, formattedDateInitial, and formattedDateFinal logic to keep the
fix localized.

---

Nitpick comments:
In `@packages/core/src/proxy.ts`:
- Around line 55-72: In getActiveLocaleFromRequest, flatten the nested dataMatch
check and switch from pathname.match(DATA_ROUTE_RE) to
DATA_ROUTE_RE.exec(pathname) for the data-route lookup. Use a single guard that
verifies a match exists and that the captured locale is valid before returning
it, keeping the existing locale fallback logic from request.nextUrl unchanged.
- Around line 92-96: The guard in getActiveLocaleFromRequest handling is
redundant because activeLocale is already guaranteed to be valid when defined.
Simplify the conditional in proxy.ts by removing the
validLocales.has(activeLocale) check and keeping only the truthy activeLocale
comparison against locale, so the intent stays clear and the logic matches the
return contract of getActiveLocaleFromRequest.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2adfeeb6-f828-4c19-8f06-9415f4caedf5

📥 Commits

Reviewing files that changed from the base of the PR and between 961a529 and 5052d06.

📒 Files selected for processing (31)
  • packages/core/src/components/account/Drawer/OrganizationDrawer/OrganizationDrawerBody.tsx
  • packages/core/src/components/account/Menu/Menu.tsx
  • packages/core/src/components/account/orders/ListOrders/FilterSlider/FilterSlider.tsx
  • packages/core/src/components/account/orders/ListOrders/ListOrders.tsx
  • packages/core/src/components/account/orders/ListOrders/ListOrdersTable/ListOrdersTable.tsx
  • packages/core/src/components/account/orders/ListOrders/SelectedTags/SelectedTags.tsx
  • packages/core/src/components/account/orders/OrderDetails/OrderDetailsHeader.tsx
  • packages/core/src/components/account/utils/useFormatPrice.ts
  • packages/core/src/components/ui/Link/Link.tsx
  • packages/core/src/experimental/index.ts
  • packages/core/src/experimental/myAccountServerSideProps.ts
  • packages/core/src/pages/pvt/account/404.tsx
  • packages/core/src/pages/pvt/account/[...unknown].tsx
  • packages/core/src/pages/pvt/account/index.tsx
  • packages/core/src/pages/pvt/account/profile.tsx
  • packages/core/src/pages/pvt/account/user-details.tsx
  • packages/core/src/proxy.ts
  • packages/core/src/sdk/session/index.ts
  • packages/core/src/sdk/session/initialSession.ts
  • packages/core/src/utils/localization/localizeRedirectDestination.ts
  • packages/core/src/utils/localization/withLocaleValidation.ts
  • packages/core/test/proxy.localization.test.ts
  • packages/core/test/sdk/session/installLocaleCorrector.browser.test.ts
  • packages/core/test/sdk/session/reconcileSessionLocale.browser.test.ts
  • packages/core/test/sdk/session/reconcileSessionLocale.disabled.browser.test.ts
  • packages/core/test/utils/localization/localizeRedirectDestination.test.ts
  • packages/sdk/src/index.ts
  • packages/sdk/src/session/index.ts
  • packages/sdk/src/store/composed.ts
  • packages/sdk/src/store/persisted.ts
  • packages/sdk/test/store/persisted.test.ts
💤 Files with no reviewable changes (2)
  • packages/core/test/sdk/session/installLocaleCorrector.browser.test.ts
  • packages/core/src/utils/localization/withLocaleValidation.ts

},
}))

import { reconcileSessionLocale } from '../../../src/sdk/session/initialSession'

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Test filename should track its source file. This suite exercises initialSession.ts, but the file is named after the exported symbol (reconcileSessionLocale.browser.test.ts). The guideline expects the test filename to match the source file (e.g. initialSession.browser.test.ts), which also keeps test discovery predictable when both getInitialSession and reconcileSessionLocale live in the same module.

If the intent is per-symbol test files, confirm that's an accepted repo convention before merging.

As per coding guidelines: "Test files MUST match the source file name with .test. infix".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/test/sdk/session/reconcileSessionLocale.browser.test.ts` at
line 86, The test suite currently targets the source module initialSession.ts
but the filename is keyed to the exported symbol reconcileSessionLocale; rename
the test file to match the source file name convention (for example,
initialSession.browser.test.ts) so it tracks the module under test. Update any
references or adjacent test organization to keep the suite aligned with the
source file and preserve predictable discovery for getInitialSession and
reconcileSessionLocale.

Source: Coding guidelines

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant