Skip to content

feat: add multiDomainLocales: { isolate: true } for regional brand domains - #4101

Draft
Vincentdevreede wants to merge 2 commits into
nuxt-modules:mainfrom
Vincentdevreede:feature/multi-domain-locales-isolate
Draft

feat: add multiDomainLocales: { isolate: true } for regional brand domains#4101
Vincentdevreede wants to merge 2 commits into
nuxt-modules:mainfrom
Vincentdevreede:feature/multi-domain-locales-isolate

Conversation

@Vincentdevreede

@Vincentdevreede Vincentdevreede commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds multiDomainLocales: { isolate: true } for the case where each domain sells the same product under its own regional brand rather than one brand serving every region. Each domain should
behave like a fully self-contained site. It should never redirect to or link into another domain's locale.

With isolate: true:

  • A locale-prefixed path not served on the current domain 404s instead of being redirected to the domain that serves it.
  • The locale switcher and hreflang/canonical alternates only list locales served on the current domain.
  • Browser-language detection and a detectBrowserLanguage.cookieDomain spanning multiple domains never cross domains. A first visit detected as another domain's locale stays on the current
    domain in its own locale instead of relocating.
  • A host matching no configured domain (staging, health checks) is unaffected. It still serves every locale, per the existing non-isolated fallback.

@BobbieGoede, feel free to accept this or not, and make any changes you see fit.

Summary by CodeRabbit

  • New Features

    • Added isolated multi-domain locale support with multiDomainLocales: { isolate: true }.
    • Unsupported locale paths now return 404 instead of redirecting to another domain.
    • Locale switchers and SEO alternate links show only locales available on the current domain.
    • Browser-language detection and locale cookies remain within the current domain.
  • Documentation

    • Updated configuration and guide documentation with isolation behavior and usage details.
  • Tests

    • Added coverage for domain-specific routing, detection, redirects, and SEO locale links.

Summary

  • Add multiDomainLocales: { isolate: true } for hosting genuinely unrelated brands on one shared build. A locale not served on the current domain now 404s instead of redirecting cross-domain, and the locale switcher and hreflang alternates only list locales actually served on the current domain.
  • Revert part of upstream fix(domain): resolve locale availability from host membership #4083, which made detectBrowserLanguage unconditionally stay within the current domain for every domain setup. That's the right call for { isolate: true }, since unrelated brands must never reveal a connection, but it was wrong for plain multiDomainLocales/differentDomains. The entire point of differentDomains is one product spanning several regional domains, so a detected locale should be able to send a visitor to the domain that actually serves it, exactly like an explicit locale-prefixed path already does. fix(domain): resolve locale availability from host membership #4083's actual bug was narrower than that: an adopted off-host locale built a broken path on the current domain instead of relocating anywhere. This restores the intended cross-domain behavior for differentDomains and plain multiDomainLocales without reintroducing that original bug, and keeps { isolate: true } exactly as strict as before.

This is an older summary from before I force-pushed the changes. I’m just keeping it here for reference.

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

The change adds multiDomainLocales: { isolate: true }. Isolated domains serve only their configured locales. Off-host locale paths return 404 without redirects. Browser, navigator, and cookie detection stays on the current host. Runtime locale lists, language switchers, and alternate links are host-specific. The change adds build configuration, route pruning, redirect and detection handling, documentation, unit tests, and end-to-end coverage.

Estimated code review effort: 4 (Complex) | ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% 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
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely identifies the main change: adding isolated multi-domain locale support for regional brand domains.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@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: 6

🧹 Nitpick comments (2)
src/runtime/plugins/i18n.ts (1)

63-70: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant useRequestURL call — reuse the already-computed host.

host is already computed once at line 31 in this same setup() closure; recomputing it as currentHost inside the composer.locales computed getter is redundant and could theoretically diverge if useRequestURL() were ever called from a different context than at setup time.

♻️ Proposed simplification
         composer.locales = computed(() => {
           const locales = runtimeI18n.locales.map(locale => withRuntimeDomain(locale, runtimeI18n.domainLocales))
           if (!__I18N_ISOLATE_MULTIDOMAINLOCALES__) { return locales }
-          const currentHost = useRequestURL({ xForwardedHost: true }).host
           return locales.filter(
-            l => typeof l === 'string' || isLocaleServedOnHost(locales as NormalizedLocaleObject[], currentHost, l.code),
+            l => typeof l === 'string' || isLocaleServedOnHost(locales as NormalizedLocaleObject[], host, l.code),
           )
         })
🤖 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 `@src/runtime/plugins/i18n.ts` around lines 63 - 70, Update the
composer.locales computed getter to reuse the existing host value from the
setup() closure instead of calling useRequestURL({ xForwardedHost: true }) again
or defining currentHost. Keep the existing locale filtering behavior unchanged.
src/runtime/routing/domain.ts (1)

31-44: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Isolate pruning logic checks out.

The new branch correctly runs before the usesDefaultVariant early exit, so pruning applies to all route-localization strategies before domain default variants are adjusted. Coverage for isolate with prefix/no_prefix would still be useful for edge cases like same-path, differently-named routes, but the current implementation behavior is sound for the tested prefix_except_default paths.

🤖 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 `@src/runtime/routing/domain.ts` around lines 31 - 44, The isolate pruning
logic in the route iteration is correct; no implementation changes are required.
Preserve the branch ordering before the usesDefaultVariant early exit and retain
the existing route removal behavior for locales not served on the host.
🤖 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 `@docs/content/docs/02.guide/10.multi-domain-locales.md`:
- Line 13: Correct the misspelled configuration option in the multi-domain
locales documentation: update `multiDomainLocaes` to `multiDomainLocales` in the
`isolate` caveat while preserving the surrounding guidance unchanged.
- Line 202: Update the multi-domain configuration example to use
multiDomainLocales: true instead of the isolated object form, preserving the
cross-domain redirects and off-domain locale links; leave the later isolated
example unchanged.

In `@docs/content/docs/04.api/00.options.md`:
- Line 356: Update the option documentation to describe locale `domains` and
`defaultForDomains` as optional fields, while retaining the array-of-objects
requirement and multi-domain locales reference. Clarify that locales without
`domains` are served on all configured domains and avoid wording that requires
either field.

In `@specs/multi_domains_locales/multi_domains_locales_redirect_scope.spec.ts`:
- Around line 44-49: The redirect test should follow the redirect chain before
asserting host locality. Update the test “an ambiguous domain with no signal
redirects within its own host, not to defaultLocale’s domain” to assert the
final destination after `/en`, or configure brand-c with a default locale and
assert that local route instead of validating only the first-hop location.

In `@src/runtime/server/plugin.ts`:
- Around line 147-149: Update the initialLocale condition in the server request
handling flow to suppress locale detection only when the current request is an
actual root redirect, rather than whenever rootRedirect is configured. Preserve
browser-detected cross-domain relocation for non-root paths, including /about,
while retaining the existing redirectOn behavior.

In `@src/runtime/shared/detection.ts`:
- Around line 155-156: Update the detection flow around the
detectors.cookie/header/navigator chain so it evaluates each source in order and
returns the first locale that passes isSupported, rather than stopping at an
unsupported truthy cookie. Preserve undefined when no detector yields a
supported locale, and add a regression case covering an unsupported stale cookie
followed by a supported header locale.

---

Nitpick comments:
In `@src/runtime/plugins/i18n.ts`:
- Around line 63-70: Update the composer.locales computed getter to reuse the
existing host value from the setup() closure instead of calling useRequestURL({
xForwardedHost: true }) again or defining currentHost. Keep the existing locale
filtering behavior unchanged.

In `@src/runtime/routing/domain.ts`:
- Around line 31-44: The isolate pruning logic in the route iteration is
correct; no implementation changes are required. Preserve the branch ordering
before the usesDefaultVariant early exit and retain the existing route removal
behavior for locales not served on the host.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: af4af9a6-1647-4ce8-b5f1-c1e9b7aa6abf

📥 Commits

Reviewing files that changed from the base of the PR and between 9574cc2 and c5efcc9.

📒 Files selected for processing (23)
  • docs/content/docs/02.guide/09.different-domains.md
  • docs/content/docs/02.guide/10.multi-domain-locales.md
  • docs/content/docs/04.api/00.options.md
  • specs/different_domains/different_domains_multi_locales_prefix_except_default.spec.ts
  • specs/multi_domains_locales/multi_domains_locales_isolate.spec.ts
  • specs/multi_domains_locales/multi_domains_locales_multi_locales.spec.ts
  • specs/multi_domains_locales/multi_domains_locales_redirect_scope.spec.ts
  • specs/multi_domains_locales/multi_domains_locales_root_redirect_scope.spec.ts
  • src/bundler.ts
  • src/env.d.ts
  • src/pages.ts
  • src/runtime/plugins/i18n.ts
  • src/runtime/routing/domain.ts
  • src/runtime/server/plugin.ts
  • src/runtime/server/utils/redirect.ts
  • src/runtime/shared/detection.ts
  • src/types.ts
  • test/detection.test.ts
  • test/kit.test.ts
  • test/pages/localize_routes.test.ts
  • test/redirect.test.ts
  • test/routing-head.test.ts
  • test/setup.ts

Comment thread docs/content/docs/02.guide/10.multi-domain-locales.md Outdated
Comment thread docs/content/docs/02.guide/10.multi-domain-locales.md Outdated
Comment thread docs/content/docs/04.api/00.options.md
Comment on lines +44 to +49
test('an ambiguous domain with no signal redirects within its own host, not to `defaultLocale`\'s domain', async () => {
const res = await undiciRequest('/', { headers: { Host: 'brand-c.nuxt-app.localhost' } })

expect(res.statusCode).toBe(302)
expect(res.headers.location).toBe('/en')
})

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Follow the redirect chain before claiming the visit stays host-local.

This test only checks the first hop to /en; Lines 51-56 establish that /en on brand-c then relocates to brand-a. Assert the final destination, or configure a brand-c default locale and assert that local route instead.

🤖 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 `@specs/multi_domains_locales/multi_domains_locales_redirect_scope.spec.ts`
around lines 44 - 49, The redirect test should follow the redirect chain before
asserting host locality. Update the test “an ambiguous domain with no signal
redirects within its own host, not to defaultLocale’s domain” to assert the
final destination after `/en`, or configure brand-c with a default locale and
assert that local route instead of validating only the first-hop location.

Comment thread src/runtime/server/plugin.ts Outdated
Comment thread src/runtime/shared/detection.ts Outdated
@BobbieGoede

Copy link
Copy Markdown
Member

Thanks for the PR!

Same as with #4080 this covers a few behaviors at once, could you split the fixes from the feature? As far as I can tell only one part of this fixes behavior that's expected today, the rest changes or adds behavior, and that's hard to prioritize in one PR.

From what I can tell the detection change isn't restoring behavior we broke. domainFromLocale in v10.4.1 and v10.5.0 ends at lang?.domains?.find(v => v === url.host), so the origin is always the current host, no released version redirects to another domain based on detection. v10.5.0 sends an es cookie on mydomain.com to mydomain.com/es and renders es there, a locale's domains didn't restrict routing yet. So this looks more like a change in behavior than a fix to me, and I'd like to work out what we actually want there before we change it.

I think #4083 may have introduced a regression, domainFromLocale now falls back to relocateHostForLocale (https://github.com/nuxt-modules/i18n/blob/917c043f/src/runtime/shared/domain.ts#L74) so the redirect origin can resolve to a domain that isn't the current host. That's what your multi_domains_locales_redirect_scope case runs into, I'll fix it separately. I don't think the fix here fully covers it, your two tests show brand-c/ going to /en and /en on brand-c relocating to brand-a, so it ends up on the same domain with an extra hop.

Could you open isolate as its own PR? Gating the route pruning behind the define resolves what I brought up on #4080, it's just easier to judge once the detection behavior is settled.

Could you also check this against the edge release (@nuxtjs/i18n-edge) or main? None of the domain changes are released yet so 10.5.0 behaves quite differently, it would help to know which behavior you're running into 🙏

@Vincentdevreede

Vincentdevreede commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

@BobbieGoede I think the detection change is restoring behavior that is broken along the way.
it was already mentioned in the creation of the doc file in 2022 (8190810). If i than just skip a year because i couldn't find the implementation commit and in the v8.0.0 release the navigate function tells it was working someday, because when differentDomains is true, the code deliberately bypasses the normal in-app router navigation.
So I’m not sure exactly when it broke. I can look into it further if you really want me to, but I’m fairly sure it’s broken 😄

I can make a separate PR for the isolate feature because it was all in 1 commit, but i don't think it makes much sense if cross domain will be removed anyway? Second commit is the cross domain in combination with the isolation feature and the other commits where just Coderabbit suggestions that it made.

Also, I wasn’t running a released version of i18n. I had checked out the main branch of yesterday evening and tested it in the playground.

@Vincentdevreede

Vincentdevreede commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

@BobbieGoede Okay i did some research.

History of crossdomain redirects

2022-11-03 - #1606, "fix: different domain loop redirection"
This is where useRedirectState was introduced, but it was already a bug fix for differentDomains:

if (process.client) {
  if (state.value !== redirectPath) {
    state.value = ''
    window.location.assign(redirectPath)
  }
} else if (process.server) {
  state.value = redirectPath
}

state.value starts as '', so on a client-initiated cross-domain switch the check was true immediately and window.location.assign fired. Server-side, state.value was set unconditionally, purely so a follow-up call could detect "we already redirected for this path" and avoid a loop.

2023-08-15 - #2318, "fix: loop redirection on differentDomains"
The client check picked up a state.value && guard:

if (process.client) {
  if (state.value && state.value !== redirectPath) {

Now the client branch also needs state.value to already be truthy before it'll fire. First-touch client redirects stop happening here. Server branch is untouched, still unconditional.

2023-09-13 - #2404, "fix: erratic redirection on different domains on server mode"
Both branches get folded under one shared gate:

if (state.value && state.value !== redirectPath) {
  if (process.client) {
    state.value = ''
    window.location.assign(redirectPath)
  } else if (process.server) {
    state.value = redirectPath
  }
}

This is the one that actually kills it. The server-side setter is now behind the same state.value && check as everything else, but state.value starts empty and useRedirectState has exactly one read/write site in the whole codebase. Nothing ever primes it. From this commit on, the condition can never become true, so neither branch including the window.location.assign can ever run.

2023-12-25 - v8.0.0 ships
The code is still sitting there, reads like it should work, and is what I originally linked to as proof cross-domain redirects worked. It didn't, it had already been dead for just over three months by release.

2025-05-20 - #3622, "refactor: remove dead code for different domains redirection"
So the bypass looked alive in the source through v8 and v9, but was functionally inert the whole time. It got deleted by you and I think your conclusion in the PR is correct

Conclusion

The intent was clearly that cross-origin redirects on detection should work for differentDomains, that's the only reason the useRedirectState / window.location.assign bypass exists at all, since Vue Router has no way to navigate across origins on its own.
It did work for at least 9 months, when it was silently broken. 😞

@BobbieGoede

Copy link
Copy Markdown
Member

@Vincentdevreede thanks for the deep dive! 🙏

You're right, it seems that at least redirection/navigation was broken for a long time for domain features.. (#4105 restores the cross-domain redirect) And our tests covered less than I thought they did. I'm also not sure how this went unreported for such a long time 🤔

I also found out that one of the things that broke language switching between domains is because NuxtLink (since nuxt v3) adds noreferrer, this triggered detection when switching domain (which in turn sends the user back to the original domain) because detection could not see where the user navigated from.

We can deal with that with a modified <SwitchLocalePathLink> (https://github.com/nuxt-modules/i18n/pull/4105/changes#diff-d23525f16313176c17032f17d1b4b42049ffe93b6fed332ef9d135c2ca6c7cd2R36-R56) and recommending its use in the docs.

A second issue is detectBrowserLanguage.cookieDomain, which defaults to the current host, which means it's not visible to other configured domains, this may be unexpected behavior to some. If locales are on different subdomains but share the same root domain, cookieDomain should be set to the root domain to be used across these subdomains.

I suspect the redirect state tracking was added to work around the symptoms of both the noreferrer and the cookieDomain behaviors.

I would like to get a release out this week (it includes a few fixes and some major perf improvements), but want to get differentDomains and multiDomainLocales to a working state for it. Domain isolation would likely come after that release in the interest of stability, and honestly I'm still thinking about whether domain isolation is within the scope of i18n at all. The domain features, linking to urls of the same website/brand in different languages makes sense. But isolating domains to share a build across unrelated websites/brands seems unrelated to i18n but more of an optimization to a specific use case (single server + build).

The fixes and hardening of the domain features are on #4105, feel free to try out the preview release and report feedback 🙏

@Vincentdevreede

Copy link
Copy Markdown
Contributor Author

@BobbieGoede I see the new release is live! 👍

What about the isolation function, I think is a fair concern. The “different brands sharing a single build” example may have made the feature sound more unrelated to i18n than I intended. It was mainly the clearest example I could think of to explain the isolation behaviour in the documentation.

The intended use case is not necessarily a set of completely unrelated websites. It can also be the same company or product operating under different names in different countries. A well-known example would be Lay’s, which is known as Walkers, Smith’s or Sabritas in different markets.

Those websites may still share the same application, content structure and translations, but not every configured locale should be considered available on every domain. Even when the visitor’s language is technically available elsewhere in the build, you may not want to redirect them to another country’s domain or brand name.

For example, a visitor on the Walkers website should not necessarily be redirected to the Lay’s or Smith’s domain just because their preferred language is available there. For that domain, the locale should instead be treated as unavailable.

That is why I see this as part of the i18n domain functionality rather than only as a build optimization. multiDomainLocales already defines which locales belong to which domains. Isolation makes that relationship consistent by ensuring that locale detection, routing and navigation only use the locales that are actually available for the current domain.

The shared build is useful for this setup, but it is not the main purpose of the feature. The main purpose is controlling locale availability per domain and preventing unintended navigation between different regional identities of the same product or organisation.

@Vincentdevreede
Vincentdevreede force-pushed the feature/multi-domain-locales-isolate branch from 7b55e96 to 4473427 Compare July 31, 2026 10:51
@Vincentdevreede

Copy link
Copy Markdown
Contributor Author

@BobbieGoede I’ve updated my PR. I used my original PR as a reference and reworked the feature from scratch on top of v10.6.0. I no longer refer to different brands, but instead use the example of a single brand operating under different regional names.

@Vincentdevreede

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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: 1

Caution

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

⚠️ Outside diff range comments (1)
docs/content/docs/02.guide/10.multi-domain-locales.md (1)

13-14: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use the standard spelling referrer.

Line 13 refers to referrer information, not the HTTP header field name. Change referer to referrer to avoid ambiguity.

🤖 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 `@docs/content/docs/02.guide/10.multi-domain-locales.md` around lines 13 - 14,
Update the referrer-policy sentence in the multi-domain locale documentation to
use the standard spelling “referrer” instead of “referer,” while leaving the
HTTP/header behavior and surrounding guidance unchanged.

Source: Linters/SAST tools

🤖 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 `@src/runtime/plugins/i18n.ts`:
- Around line 74-79: Update the composer localeCodes derivation to use the same
host-filtered locales produced by the isolation-aware composer.locales logic, so
off-host codes are excluded when multidomain isolation is enabled while
preserving all codes otherwise. Add an isolation-mode assertion covering
composer.localeCodes and its exclusion of locales removed by pruneOffHostRoutes.

---

Outside diff comments:
In `@docs/content/docs/02.guide/10.multi-domain-locales.md`:
- Around line 13-14: Update the referrer-policy sentence in the multi-domain
locale documentation to use the standard spelling “referrer” instead of
“referer,” while leaving the HTTP/header behavior and surrounding guidance
unchanged.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1a800fd4-f0f2-4e7a-9613-474373642c43

📥 Commits

Reviewing files that changed from the base of the PR and between 114440b and 4473427.

📒 Files selected for processing (17)
  • docs/content/docs/02.guide/10.multi-domain-locales.md
  • docs/content/docs/04.api/00.options.md
  • specs/multi_domains_locales/multi_domains_locales_isolate.spec.ts
  • src/bundler.ts
  • src/env.d.ts
  • src/pages.ts
  • src/runtime/plugins/i18n.ts
  • src/runtime/routing/domain.ts
  • src/runtime/server/plugin.ts
  • src/runtime/server/utils/redirect.ts
  • src/runtime/shared/detection.ts
  • src/runtime/utils.ts
  • src/types.ts
  • test/detection.test.ts
  • test/kit.test.ts
  • test/redirect.test.ts
  • test/setup.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • test/setup.ts
  • src/env.d.ts
  • src/types.ts
  • docs/content/docs/04.api/00.options.md
  • src/pages.ts
  • src/bundler.ts

Comment thread src/runtime/plugins/i18n.ts
@Vincentdevreede Vincentdevreede changed the title feat: Add multiDomainLocales isolate mode and fix domain detection scope feat: add multiDomainLocales: { isolate: true } for regional brand domains Jul 31, 2026
@BobbieGoede

Copy link
Copy Markdown
Member

Thank you for the further clarification 🙏 The intended use case is clear to me, and I think I managed to narrow my concerns about its implementation.

Today the module assumes a project has a single identity: every configured locale is an alternate representation of the same content, and the config describes where those alternates live. multiDomainLocales as it exists fits that model, it says where each locale is served, which is why an off-host locale path relocates instead of 404ing.

Looking at the diff makes explicit what isolation indirectly does:

  • request on host brand-a.com => use config
    [
      { code: 'en', domain: 'brand-a.com', defaultForDomain: true },
      { code: 'nl', domain: 'brand-a.com' }
    ]
  • request on host brand-b.nl => use config
    [
      { code: 'nl', domain: 'brand-b.nl', defaultForDomain: true },
      { code: 'en', domain: 'brand-b.nl' }
    ]

Within each selected sub-config the module's behavior is unchanged; everything new happens in the selection step. So what this feature really is, is configuration determined at runtime: which locales exist, which is the default, and how detection behaves would depend on the incoming request. (Not Nuxt's runtimeConfig, which is fixed at process startup, but configuration resolved per request.)

That puts it in a family with several open requests we've been collecting:

We're not opposed to some form of this, but it conflicts with how much of what the module produces is fixed at build time, and any real version of it has to confront these:

  • Routes are the most tractable of these, and multiDomainLocales already demonstrates the pattern: the build emits a route variant for every locale that is a domain default somewhere, and at plugin start the route table is rebuilt for the current host from those variants. But that only works because the build enumerated every possible shape in advance; runtime config can select among pre-built route shapes, not produce new ones. Every candidate default has to be declared up front so its variants exist (Runtime-configurable default locale: single build serving regions with different defaults (prefix_except_default) #4016), and the strategy has no variant mechanism at all (Support complex multi-domain i18n configurations in a single build #3748 asks to vary it per host).
  • Translations are similar: which locale files end up in the build and how they're loaded is decided at build time. On the server, compiled messages are additionally shared across requests on the assumption that the config is stable, and per-request configuration breaks that sharing at a significant performance cost.
  • Prerendering renders each page under exactly one config, and no request host exists at generate time, so any host-dependent behavior gets frozen into the HTML (Wrong locale of pre-rendered site #2656 is a live example of this class of bug).
  • A fully static build serves every host the same files, so a per-host decision like "404 instead of relocate" has nowhere to run at all.

To be fair, the proposed isolation feature avoids most of these problems, precisely because it is still configured at build time: every per-host sub-config is known up front, and they all share the same routes, messages and pages. That constraint is what makes it much easier to implement than general runtime configuration. But it gets there by leaking the identity semantic into places that shouldn't carry it: the locale config now encodes which identity a locale belongs to, and detection, redirects, the switcher and the head alternates each have to interpret that meaning, instead of the config selection happening in one explicit step. And the shortcut only holds while the sub-configs share everything; the moment the identities need different pages or translations, the built resources disagree with the selected config.

So rather than merging isolation as a flag that implicitly implements runtime config selection for one use case, I'd prefer to treat runtime-determined configuration as its own design question: decide which subset is honestly supportable given the constraints above, and state its boundaries up front. I'm keeping this use case on the table as part of that, but I don't want to land it in its current shape.

BobbieGoede added a commit to BobbieGoede/i18n that referenced this pull request Aug 2, 2026
Exercises the nuxt-modules#4101 use case in userland: one deployment hosting
unrelated brands, each host serving only its own locale cluster.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Vincentdevreede
Vincentdevreede marked this pull request as draft August 11, 2026 08:16
@sivo1981

Copy link
Copy Markdown

Great feature, also waiting for this to be merged.

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.

3 participants