-
Notifications
You must be signed in to change notification settings - Fork 80
feat: localization middleware #3185
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feat/multilanguage
Are you sure you want to change the base?
Conversation
WalkthroughAdds localization-aware server middleware and client link-resolution utilities, removes legacy redirect middleware, updates components to use the new resolver, and toggles middleware files from the CLI based on localization configuration. Changes
Sequence DiagramssequenceDiagram
participant Client as Client (Browser)
participant Middleware as middleware.ts
participant Config as discovery.config / bindings
participant NextApp as Next.js App
Client->>Middleware: Request /products
activate Middleware
Middleware->>Config: Read custom path bindings
activate Config
Config-->>Middleware: Return bindings
deactivate Config
Middleware->>Middleware: Build/Match rewrite rules
alt match found and valid locale
Middleware-->>Client: Rewrite to /<locale>/products (preserve query)
else no match
Middleware-->>Client: Pass through (no rewrite)
end
deactivate Middleware
Client->>NextApp: Request (possibly locale-prefixed)
NextApp-->>Client: Response
sequenceDiagram
participant Component as UI Component
participant useLink as useLink Hook
participant Router as Next.js Router
participant CustomPaths as customPaths utils
Component->>useLink: Call resolveLink('/product')
activate useLink
useLink->>Router: read asPath
activate Router
Router-->>useLink: current asPath
deactivate Router
useLink->>CustomPaths: addCustomPathPrefix('/product', asPath)
activate CustomPaths
CustomPaths-->>useLink: maybe-prefixed '/<locale>/product'
deactivate CustomPaths
useLink-->>Component: return resolved href
deactivate useLink
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
|
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. |
|
@coderabbitai configuration |
|
Configuration used: defaults CodeRabbit configurationlanguage: en-US
tone_instructions: ''
early_access: false
enable_free_tier: true
inheritance: false
reviews:
profile: chill
request_changes_workflow: false
high_level_summary: true
high_level_summary_instructions: ''
high_level_summary_placeholder: '@coderabbitai summary'
high_level_summary_in_walkthrough: false
auto_title_placeholder: '@coderabbitai'
auto_title_instructions: ''
review_status: true
review_details: false
commit_status: true
fail_commit_status: false
collapse_walkthrough: true
changed_files_summary: true
sequence_diagrams: true
estimate_code_review_effort: true
assess_linked_issues: true
related_issues: true
related_prs: true
suggested_labels: true
labeling_instructions: []
auto_apply_labels: false
suggested_reviewers: true
auto_assign_reviewers: false
in_progress_fortune: true
poem: true
enable_prompt_for_ai_agents: true
path_filters: []
path_instructions: []
abort_on_close: true
disable_cache: false
auto_review:
enabled: true
auto_incremental_review: true
ignore_title_keywords: []
labels: []
drafts: false
base_branches: []
ignore_usernames: []
finishing_touches:
docstrings:
enabled: true
unit_tests:
enabled: true
pre_merge_checks:
docstrings:
mode: warning
threshold: 80
title:
mode: warning
requirements: ''
description:
mode: warning
issue_assessment:
mode: warning
custom_checks: []
tools:
ast-grep:
rule_dirs: []
util_dirs: []
essential_rules: true
packages: []
shellcheck:
enabled: true
ruff:
enabled: true
markdownlint:
enabled: true
github-checks:
enabled: true
timeout_ms: 90000
languagetool:
enabled: true
enabled_rules: []
disabled_rules: []
enabled_categories: []
disabled_categories: []
enabled_only: false
level: default
biome:
enabled: true
hadolint:
enabled: true
swiftlint:
enabled: true
phpstan:
enabled: true
level: default
phpmd:
enabled: true
phpcs:
enabled: true
golangci-lint:
enabled: true
yamllint:
enabled: true
gitleaks:
enabled: true
checkov:
enabled: true
detekt:
enabled: true
eslint:
enabled: true
flake8:
enabled: true
fortitudeLint:
enabled: true
rubocop:
enabled: true
buf:
enabled: true
regal:
enabled: true
actionlint:
enabled: true
pmd:
enabled: true
clang:
enabled: true
cppcheck:
enabled: true
semgrep:
enabled: true
circleci:
enabled: true
clippy:
enabled: true
sqlfluff:
enabled: true
prismaLint:
enabled: true
pylint:
enabled: true
oxc:
enabled: true
shopifyThemeCheck:
enabled: true
luacheck:
enabled: true
brakeman:
enabled: true
dotenvLint:
enabled: true
htmlhint:
enabled: true
checkmake:
enabled: true
osvScanner:
enabled: true
chat:
art: true
auto_reply: true
integrations:
jira:
usage: auto
linear:
usage: auto
knowledge_base:
opt_out: false
web_search:
enabled: true
code_guidelines:
enabled: true
filePatterns: []
learnings:
scope: auto
issues:
scope: auto
jira:
usage: auto
project_keys: []
linear:
usage: auto
team_keys: []
pull_requests:
scope: auto
mcp:
usage: auto
disabled_servers: []
code_generation:
docstrings:
language: en-US
path_instructions: []
unit_tests:
path_instructions: []
issue_enrichment:
auto_enrich:
enabled: false
planning:
enabled: true
auto_planning:
enabled: true
labels: []
labeling:
labeling_instructions: []
auto_apply_labels: false
|
Rename middleware to middleware__I18N_DISABLED.ts on build/dev when localization.enabled is false so Next.js does not run the middleware.1
a922cf8 to
6b1f949
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@packages/core/src/components/sections/Hero/Hero.tsx`:
- Line 72: The call to addCustomPathPrefix uses link?.url which can be undefined
but the function signature addCustomPathPrefix(link: string, currentPathname:
string): string requires a string; update the call in Hero (where link?.url and
router?.asPath are used) to ensure a string is passed—either by short-circuiting
(e.g., only call when link?.url exists) or by providing a safe default like an
empty string (e.g., addCustomPathPrefix(link?.url ?? '', router?.asPath ?? ''))
so the first argument is always a string and types align.
In `@packages/core/src/utils/customPaths.ts`:
- Around line 99-107: extractCustomPathPrefix uses pathname.startsWith(path)
which falsely matches partial segments (e.g., '/it' vs '/item'); update the
match logic in extractCustomPathPrefix (and the analogous check later around
lines 116-127) to require either an exact match or a segment boundary by
replacing startsWith(path) with (pathname === path || pathname.startsWith(path +
'/')) (or equivalent logic that normalizes/trims trailing slashes first) so only
whole-path prefixes are accepted.
In `@packages/core/test/utils/customPaths.test.ts`:
- Around line 4-6: The mock factory for discovery.config.js in
customPaths.test.ts should use Vitest's vi.importActual() instead of a plain
dynamic import to avoid re-entry/circular import issues; update the vi.mock
factory (the anonymous factory passed to vi.mock('../../discovery.config.js'))
to call await vi.importActual('../../discovery.config.js') to obtain the real
module, then spread or reference its exports when returning the partial mock so
tests use the actual exports safely.
🧹 Nitpick comments (5)
packages/core/src/middleware.ts (5)
51-56: ReDoS concern is mitigated by proper escaping.Static analysis flags this as a potential ReDoS vector, but the
escapedPathis properly escaped on lines 51-54, neutralizing special regex characters. The resulting pattern^${escapedPath}(?:\\/(.*))?$is linear and safe.However, since the input originates from
discovery.configbindings (trusted config), the risk is minimal. If you want extra safety for future-proofing:🛡️ Optional: Add path validation
+const MAX_PATH_LENGTH = 256 + for (const customPath of customPaths) { + if (customPath.path.length > MAX_PATH_LENGTH) { + console.warn(`Skipping excessively long path: ${customPath.path.slice(0, 50)}...`) + continue + } const localeConfig = locales[customPath.locale]
69-73: Sorting by regex source length is indirect and fragile.The intent is to match longer paths first, but sorting by
regex.source.lengthis affected by escaping. A path with special characters will have a longer source than its actual length.♻️ Proposed fix: Sort by original path length
+type RewriteRule = { + regex: RegExp + locale: string + hostname?: string + pathLength: number +} // In the rule creation: rules.push({ regex, locale: customPath.locale, hostname: bindingUrl.hostname, + pathLength: customPath.path.length, }) // In the sort: - return rules.sort((a, b) => { - const aLength = a.regex.source.length - const bLength = b.regex.source.length - return bLength - aLength - }) + return rules.sort((a, b) => b.pathLength - a.pathLength)
83-83: Unusedhostnamevariable.
hostnameis destructured but unused since hostname validation is disabled (lines 86-89). Remove it to avoid confusion, or keep only if you plan to re-enable soon.- const { pathname, search, hostname } = request.nextUrl + const { pathname, search } = request.nextUrl
97-100:console.warnin middleware can be noisy.This logs on every request that hits an invalid locale rule. Consider logging once at startup during
generateRewriteRules()instead, or use a debug flag.♻️ Move validation to rule generation
// In generateRewriteRules(), after creating the rule: + if (!isValidLocale(customPath.locale)) { + console.warn(`Skipping rule for invalid locale: ${customPath.locale}`) + continue + } + rules.push({ regex, locale: customPath.locale, hostname: bindingUrl.hostname, }) // Then remove the runtime check in middleware: - if (!isValidLocale(rule.locale)) { - console.warn(`Locale ${rule.locale} not found in configuration`) - continue - }
105-109: Redundanturl.search = searchassignment.
request.nextUrl.clone()already preserves the search params. This line is harmless but unnecessary.const url = request.nextUrl.clone() url.pathname = normalizedPath - url.search = search return NextResponse.rewrite(url)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@packages/core/test/utils/customPaths.test.ts`:
- Around line 167-176: The test contains a conditional wrapper around the
assertion which can cause the test to pass silently; remove the if
(currentPath.startsWith('/europe/it')) check and assert directly that
addCustomPathPrefix(link, currentPath) returns '/europe/it/apparel' (referencing
the test using addCustomPathPrefix and the currentPath variable) so the
expectation always runs and the intent is explicit.
- Around line 187-196: The test uses a redundant conditional guard around the
assertion; remove the if block and assert directly that addCustomPathPrefix('/',
'/europe/it/apparel') returns '/europe/it/'. Update the test in
customPaths.test.ts to call addCustomPathPrefix with link='/' and
currentPath='/europe/it/apparel' and replace the guarded expect with a direct
expect(result).toBe('/europe/it/') referencing the addCustomPathPrefix helper.
🧹 Nitpick comments (2)
packages/core/test/utils/customPaths.test.ts (2)
1-2: Consolidate vitest imports.Minor style nit: both imports are from the same package.
-import { vi } from 'vitest' -import { describe, expect, it } from 'vitest' +import { vi, describe, expect, it } from 'vitest'
4-6: Add type parameter tovi.importActualfor type safety.Per TypeScript guidelines, avoid implicit
any. Adding the type parameter improves IDE support and catches type mismatches in the mock spread.Suggested fix
vi.mock('../../discovery.config.js', async () => { - const original = await vi.importActual('../../discovery.config.js') + const original = await vi.importActual<typeof import('../../discovery.config.js')>( + '../../discovery.config.js' + ) return {
| if (!pathname || pathname === '/') { | ||
| continue | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question: could we add it to the isCustomPath check? Half of it is already there
| * @param link - The link to check (e.g., '/apparel' or '/europe/it/apparel') | ||
| * @returns true if link already has a custom path prefix | ||
| */ | ||
| function hasCustomPathPrefix(link: string): boolean { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question: do we need both extractCustomPathPrefix and hasCustomPathPrefix? We could use extractCustomPathPrefix to test if the link has a custom path prefix
packages/core/src/middleware.ts
Outdated
| const locales = (storeConfig.localization.locales || | ||
| {}) as LocalesSettings['locales'] | ||
|
|
||
| for (const customPath of customPaths) { | ||
| const localeConfig = locales[customPath.locale] | ||
|
|
||
| if (!localeConfig?.bindings || !Array.isArray(localeConfig.bindings)) { | ||
| continue | ||
| } | ||
|
|
||
| for (const binding of localeConfig.bindings) { | ||
| if (!binding.url) { | ||
| continue | ||
| } | ||
|
|
||
| try { | ||
| const bindingUrl = new URL(binding.url) | ||
| const pathname = bindingUrl.pathname.replace(/\/$/, '') | ||
|
|
||
| if (pathname !== customPath.path) { | ||
| continue | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: I believe we could refactor it so we don't redo things that were already calculated in getCustomPathsFromBindings
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@packages/core/src/utils/customPaths.ts`:
- Around line 103-133: The prefix detection functions extractCustomPathPrefix
and hasCustomPathPrefix currently compare raw pathname/link values and can miss
matches when inputs include query strings or hash fragments; update both
functions to normalize the input by stripping anything from the first '?' or '#'
onward (so only the pathname is compared) before calling
getCustomPathsFromBindings and performing the equality or startsWith checks
against each { path }, ensuring you still match exact path or segment-boundary
cases (e.g., '/it' vs '/it/apparel').
🧹 Nitpick comments (1)
packages/core/src/utils/customPaths.ts (1)
58-60: Avoid the type assertion and narrow safely.The cast to
LocalesSettings['locales']skips type safety. Consider using a runtime guard or a typed helper to keep this section safe without assertions.As per coding guidelines: Ensure type safety and avoid type assertions when possible.
…y/hash, remove redundant helpers, improve readability
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@packages/core/src/middleware.ts`:
- Around line 83-109: The hostname validation that prevents cross-host locale
rewrites is currently disabled; re-enable it in the middleware that iterates
rewriteRules by restoring the check on rule.hostname against
request.nextUrl.hostname (or adding an environment-gated check) so that in the
loop over rewriteRules you skip rules whose rule.hostname exists and does not
match hostname (e.g., if (rule.hostname && rule.hostname !== hostname)
continue), and if you want dev convenience only, wrap that skip in a
NODE_ENV/dev-only conditional so production always enforces hostname matching.
In `@packages/core/src/utils/localization/customPaths.ts`:
- Around line 112-136: The function addCustomPathPrefix currently treats
protocol-relative URLs (those starting with "//") as internal and may prefix
them; update addCustomPathPrefix to detect and skip protocol-relative URLs by
returning link unchanged when link startsWith("//") (in addition to the existing
non-slash and extractCustomPathPrefix checks). Locate addCustomPathPrefix and
add the guard before extracting/adding a prefix (use the existing
extractCustomPathPrefix and getPathOnly helpers unchanged).
…me), no re-iterate bindings or re-parse URLs
|
@lemagnetic I think we missed the Alert section link: |
thanks |
| ) | ||
|
|
||
| if (isInternalLink) { | ||
| return ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ps: We can create another task just for these updates after this one 👍
| import { Link as UILink } from '@faststore/ui' | ||
| import type { LinkProps as UILinkProps, LinkElementType } from '@faststore/ui' | ||
|
|
||
| import { useLink } from '../../../sdk/ui/useLink' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| import { useLink } from '../../../sdk/ui/useLink' | |
| import { useLink } from 'src/sdk/ui/useLink'' |
| export type CustomPathInfo = { | ||
| path: string | ||
| locale: string | ||
| /** Hostname from binding URL (for middleware rewrite rules; TODO: re-enable host validation) */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| /** Hostname from binding URL (for middleware rewrite rules; TODO: re-enable host validation) */ | |
| /** Hostname from binding URL (for middleware rewrite rules) */ |
Is this TODO still needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
comment removed
65538a4
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@packages/core/src/components/ui/Link/Link.tsx`:
- Around line 20-30: The code indexes and treats href as a string (href[0],
href[1], resolveLink(href)) but Next.js LinkProps allows UrlObject; add a type
guard so you only index/resolve when typeof href === 'string' (e.g., compute
isInternalLink using typeof href === 'string' && href.startsWith('/') && href[1]
!== '/'), and compute finalHref by calling resolveLink(href) only when href is a
string (otherwise leave finalHref as the original href UrlObject); also ensure
the value passed to NextLink uses this guarded finalHref.
🧹 Nitpick comments (1)
packages/core/src/utils/localization/customPaths.ts (1)
51-89: Type assertion could be tightened, but acceptable for config access.Line 64 uses
as LocalesSettings['locales']which bypasses type checking. Consider narrowing the type via a type guard ifLocalesSettingsevolves, but this is acceptable given the static nature ofdiscovery.config.The longest-first sorting (line 87) correctly ensures
/europe/itmatches before/europe, preventing partial-prefix collisions.
| const { resolveLink } = useLink() | ||
|
|
||
| const isInternalLink = useMemo( | ||
| () => href[0] === '/' && href[1] !== '/', | ||
| [href] | ||
| ) | ||
|
|
||
| const finalHref = useMemo( | ||
| () => (isInternalLink ? (resolveLink(href) ?? href) : href), | ||
| [href, isInternalLink, resolveLink] | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
cd packages/core/src/components/ui/Link && cat -n Link.tsxRepository: vtex/faststore
Length of output: 1981
🏁 Script executed:
# Also check the imports to understand where LinkProps comes from
grep -r "LinkProps" packages/core/src/components/ui/Link/ -A 2 -B 2Repository: vtex/faststore
Length of output: 1741
🌐 Web query:
Next.js LinkProps href type definition - can it be UrlObject
💡 Result:
Yes. In Next.js, next/link’s LinkProps['href'] supports a URL object (UrlObject from Node’s url types).
- In the shipped type definitions,
hrefisUrlwheretype Url = string | UrlObject. [1] - If you enable statically typed links (
typedRoutes),hrefbecomesRouteImpl<...> | UrlObject(soUrlObjectis still allowed). [2] - The official docs also show passing an object
{ pathname, query }ashref. [3]
How to type it in your own component
import type { LinkProps } from 'next/link'
type Href = LinkProps['href'] // (string | UrlObject) OR (RouteImpl | UrlObject) with typedRoutesOr explicitly:
import type { UrlObject } from 'url'
type Href = string | UrlObjectExample
import Link from 'next/link'
<Link href={{ pathname: '/about', query: { name: 'test' } }}>About</Link>References: [1] (app.unpkg.com) [2] (app.unpkg.com) [3] (nextjs.org)
Citations:
- 1: https://app.unpkg.com/next%4015.0.4/files/dist/client/link.d.ts?utm_source=openai
- 2: https://app.unpkg.com/next%4015.5.4/files/dist/esm/server/lib/router-utils/typegen.js.map?utm_source=openai
- 3: https://nextjs.org/docs/app/api-reference/components/link?utm_source=openai
Guard non‑string href before indexing or resolving.
href can be a UrlObject per Next.js LinkProps (e.g., { pathname: '...', query: {...} }), so indexing href[0] and calling resolveLink(href) will throw at runtime. Add a type guard.
Safe guard + fallback
const isInternalLink = useMemo(
- () => href[0] === '/' && href[1] !== '/',
+ () => typeof href === 'string' && href[0] === '/' && href[1] !== '/',
[href]
)
const finalHref = useMemo(
- () => (isInternalLink ? (resolveLink(href) ?? href) : href),
+ () => {
+ if (typeof href !== 'string') return href
+ return isInternalLink ? (resolveLink(href) ?? href) : href
+ },
[href, isInternalLink, resolveLink]
)Also applies to: line 40 (finalHref passed to NextLink)
🤖 Prompt for AI Agents
In `@packages/core/src/components/ui/Link/Link.tsx` around lines 20 - 30, The code
indexes and treats href as a string (href[0], href[1], resolveLink(href)) but
Next.js LinkProps allows UrlObject; add a type guard so you only index/resolve
when typeof href === 'string' (e.g., compute isInternalLink using typeof href
=== 'string' && href.startsWith('/') && href[1] !== '/'), and compute finalHref
by calling resolveLink(href) only when href is a string (otherwise leave
finalHref as the original href UrlObject); also ensure the value passed to
NextLink uses this guarded finalHref.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.





What's the purpose of this pull request?
Add custom path support for i18n in FastStore stores: in addition to Next.js canonical paths (
/pt-BR,/en-US), stores can now support URLs like/america/fr/produto/pand/europe/it/apparel, defined by localization bindings indiscovery.config. The middleware rewrites these URLs to the format Next.js i18n expects, and internal links (Link, ProductCard, Hero, etc.) automatically get the custom path prefix when the user is on a customized route.How it works?
Middleware (
packages/core/src/middleware.ts)storeConfig.localization?.enabledistrue.discovery.config: for each binding whose URL has a custom path (e.g.https://store.com/america/fr), a rule is created that:/america/fr,/america/fr/produto/p);/fr-CA,/fr-CA/produto/p).api,_next/static,_next/image,favicon.ico, and static assets.Custom path utilities (
packages/core/src/utils/customPaths.ts)isCustomPath(url)– Returns whether the URL uses a custom path (e.g./america/fr) instead of a canonical one (/pt-BR).getCustomPathsFromBindings()– Reads custom paths from localization bindings; the result is cached so the loop is not repeated on every link.addCustomPathPrefix(link, currentPathname)– For internal links (/apparel), adds the current custom path prefix when the user is on a customized route (e.g. on/america/fr→/apparelbecomes/america/fr/apparel). Does not change external links or links that already have the prefix.Where the prefix is applied to links
Link(core) – UsesaddCustomPathPrefix(href, router.asPath)for internal links, so all links that go through the core Link component get the prefix when applicable.useProductLink– Applies the prefix to the producthref(/${slug}/p), fixing ProductCard links that did not use the core Link.link?.urlbefore passing it toHeroHeader, fixing the “See Offers” button (fs-link-button) on customized routes.Middleware toggle by localization feature flag (CLI)
When the localization feature flag (
localization.enabled) isfalse, the CLI disables the i18n middlewaretoggleI18nMiddlewareByLocalizationFlag(packages/cli/src/utils/generate.ts) – Called from build and dev after the existing localization check and (optional)generate-i18nstep:middleware.tsis present (renamesmiddleware__I18N_DISABLED.ts→middleware.tsif needed).middleware.ts→middleware__I18N_DISABLED.tsso Next.js does not load or run the i18n middleware.Binding URL format in the admin
Binding URLs must be only the path base, without product or page path. E.g.:
✅
https://store.com/america/fr❌
https://store.com/america/fr/produto/pIf the binding URL includes a product/page path, the middleware treats that entire path as the custom path for that locale and rewrite rules become incorrect.
Outstanding items (TODOs in code)
useLocaleValidation); the current validation does not account for custom paths (only hostname + locale).How to test it?
localization.enabledistrueindiscovery.configand that there are bindings with a custom path (e.g.https://yoursite.com/america/frfor localefr-CA). To generate or refresh locales/bindings from the SDK, run from the repo root:VTEX_ACCOUNT,FS_DISCOVERY_APP_KEY, andFS_DISCOVERY_APP_TOKEN.pnpm devin core) and open the custom path, e.g.http://localhost:3000/america/fr.lang="fr-CA"on<html>)./america/fr/technology,/america/fr/produto-x/p)./pt-BR), confirm links remain without a custom prefix (e.g./apparel,/produto/p).localization.enabled, runpnpm devorpnpm buildagain, and confirm the CLI renamesmiddleware.tstomiddleware__I18N_DISABLED.ts(so Next.js does not run the i18n middleware and links do not get a prefix).Starters Deploy Preview
Preview custom path ->
Locale: fr-CA
Home with custom path: https://brandless-cma5xay4001f6dn4xjwato8b4-qqih78fto.b.vtex.app/america/fr
References
Jira task
Summary by CodeRabbit
New Features
Removed
Refactor
Tests