Skip to content

[WIP] feat: runtime request config (proof of concept) - #4119

Open
BobbieGoede wants to merge 2 commits into
mainfrom
poc/runtime-request-config
Open

[WIP] feat: runtime request config (proof of concept)#4119
BobbieGoede wants to merge 2 commits into
mainfrom
poc/runtime-request-config

Conversation

@BobbieGoede

@BobbieGoede BobbieGoede commented Aug 2, 2026

Copy link
Copy Markdown
Member

🔗 Linked issue

📚 Description

This is an exploration and proof of concept on another approach to changing the module behavior by changing the config passed at setup, this would allow projects to introduce other semantics without introducing them to the module code directly.

This is based on what I describe in #4101 (comment), and could be a solution to some of the runtime config requests (a small subset of configs), which is a recurring feature request.

/cc @Vincentdevreede

Summary by CodeRabbit

  • New Features
    • Added support for request-specific locale configuration, allowing available languages and default locales to vary by request or domain.
    • Unavailable language routes are now automatically removed, preventing navigation to unsupported localized pages.
    • Improved locale detection using request-specific language settings, browser preferences, and persisted cookies.
    • Added Dutch (nl-NL) as an available locale in the playground.
  • Bug Fixes
    • Unknown or unsupported locales are filtered from runtime configuration to improve locale resolution and fallback behavior.
  • Tests
    • Added coverage for host-based locale selection, route handling, detection, and localized navigation.

@BobbieGoede BobbieGoede self-assigned this Aug 2, 2026
@pkg-pr-new

pkg-pr-new Bot commented Aug 2, 2026

Copy link
Copy Markdown
npm i https://pkg.pr.new/@nuxtjs/i18n@4119

commit: d7143ec

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The runtime now supports per-request locale configuration through the i18n:request-config hook. It resolves request locales, default locales, and runtime domains. It prunes routes for unserved locales and passes request locales to detection and context APIs. New fixtures and tests cover host-specific configuration, hydration, routing, browser detection, and cookie detection. The playground adds Dutch locale data and host-based Japanese locale filtering.

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 41.67% 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 identifies the main change as runtime request configuration and correctly labels it as a proof of concept.
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 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch poc/runtime-request-config

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: 2

🧹 Nitpick comments (1)
specs/request_config/request_config.spec.ts (1)

54-61: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Exercise the hydrated client configuration.

This test only inspects the server-rendered window.__NUXT__.config string. It does not run client hydration. A serialization assertion cannot detect a client-side mismatch in localeCodes, locales, or switchLocalePath.

Start the fixture with browser support. Then assert the rendered values after the restricted-host page hydrates. This verifies the hydration behavior described by this test and the PR objective.

🤖 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/request_config/request_config.spec.ts` around lines 54 - 61, Update the
test around getHydratedConfig to launch the fixture with browser support and
perform client hydration for the restricted host page. Assert the hydrated
rendered values for localeCodes, locales, and switchLocalePath, while preserving
the expected restricted-language behavior that includes nl-NL and excludes
fr-FR.
🤖 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/shared/locales.ts`:
- Around line 90-100: Update resolveRequestLocales so config.locales is always
assigned the normalized resolved list, moving config.locales = resolved outside
the resolved.length !== codes.size conditional. Keep the existing development
warning and unknown-locale filtering inside the conditional.
- Around line 102-105: Update the prerender narrowing warning condition near
warnedPrerenderNarrowing to remove the import.meta.dev requirement and rely only
on import.meta.prerender, while preserving the existing locale-length check,
one-time warning guard, and warning message.

---

Nitpick comments:
In `@specs/request_config/request_config.spec.ts`:
- Around line 54-61: Update the test around getHydratedConfig to launch the
fixture with browser support and perform client hydration for the restricted
host page. Assert the hydrated rendered values for localeCodes, locales, and
switchLocalePath, while preserving the expected restricted-language behavior
that includes nl-NL and excludes fr-FR.
🪄 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: e63f22d3-abaa-43de-ad71-0d225267ba30

📥 Commits

Reviewing files that changed from the base of the PR and between a0b2db7 and d7143ec.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (20)
  • playground/i18n/locales/nl.json
  • playground/nuxt.config.ts
  • playground/server/plugins/i18n-request-config.ts
  • specs/fixtures/request_config/i18n/i18n.config.ts
  • specs/fixtures/request_config/nuxt.config.ts
  • specs/fixtures/request_config/package.json
  • specs/fixtures/request_config/pages/index.vue
  • specs/fixtures/request_config/server/plugins/i18n-request-config.ts
  • specs/request_config/request_config.spec.ts
  • src/runtime/context.ts
  • src/runtime/plugins/i18n.ts
  • src/runtime/routing/runtime-locales.ts
  • src/runtime/server/context.ts
  • src/runtime/server/plugin.ts
  • src/runtime/shared/detection.ts
  • src/runtime/shared/locales.ts
  • src/runtime/utils.ts
  • src/types.ts
  • test/detection.test.ts
  • test/redirect.test.ts

Comment on lines +90 to +100
export function resolveRequestLocales(config: I18nPublicRuntimeConfig): NormalizedLocaleObject[] {
const codes = new Set(config.locales.map(locale => (isString(locale) ? locale : locale.code)))
const resolved = normalizedLocales.filter(locale => codes.has(locale.code))

if (resolved.length !== codes.size) {
if (import.meta.dev) {
const unknown = [...codes].filter(code => !resolved.some(l => l.code === code))
console.warn(`[nuxt-i18n] Ignoring locales that are not part of the build: ${unknown.join(', ')}`)
}
config.locales = resolved
}

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Normalize config.locales unconditionally, not only when codes are pruned.

config.locales = resolved only runs if (resolved.length !== codes.size). When the i18n:request-config hook narrows locales using valid plain-code strings (for example config.locales = ['en', 'nl']), no codes are dropped, so this branch is skipped and config.locales keeps holding raw strings.

composer.locales in src/runtime/plugins/i18n.ts reads runtimeI18n.locales (this same object) directly and exposes it as $i18n.locales. For this request, it would then return plain strings instead of LocaleObjects, while ctx.getLocales()/requestLocales stay fully normalized. Code that reads $i18n.locales[i].name, .dir, or .domains breaks silently for that request.

Move the assignment outside the if so config.locales always reflects the normalized list.

Proposed fix
   const resolved = normalizedLocales.filter(locale => codes.has(locale.code))

-  if (resolved.length !== codes.size) {
-    if (import.meta.dev) {
-      const unknown = [...codes].filter(code => !resolved.some(l => l.code === code))
-      console.warn(`[nuxt-i18n] Ignoring locales that are not part of the build: ${unknown.join(', ')}`)
-    }
-    config.locales = resolved
+  if (import.meta.dev && resolved.length !== codes.size) {
+    const unknown = [...codes].filter(code => !resolved.some(l => l.code === code))
+    console.warn(`[nuxt-i18n] Ignoring locales that are not part of the build: ${unknown.join(', ')}`)
   }
+  // keep `config.locales` normalized regardless of pruning, so readers such as `composer.locales`
+  // never see raw locale-code strings for this request
+  config.locales = resolved
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function resolveRequestLocales(config: I18nPublicRuntimeConfig): NormalizedLocaleObject[] {
const codes = new Set(config.locales.map(locale => (isString(locale) ? locale : locale.code)))
const resolved = normalizedLocales.filter(locale => codes.has(locale.code))
if (resolved.length !== codes.size) {
if (import.meta.dev) {
const unknown = [...codes].filter(code => !resolved.some(l => l.code === code))
console.warn(`[nuxt-i18n] Ignoring locales that are not part of the build: ${unknown.join(', ')}`)
}
config.locales = resolved
}
export function resolveRequestLocales(config: I18nPublicRuntimeConfig): NormalizedLocaleObject[] {
const codes = new Set(config.locales.map(locale => (isString(locale) ? locale : locale.code)))
const resolved = normalizedLocales.filter(locale => codes.has(locale.code))
if (import.meta.dev && resolved.length !== codes.size) {
const unknown = [...codes].filter(code => !resolved.some(l => l.code === code))
console.warn(`[nuxt-i18n] Ignoring locales that are not part of the build: ${unknown.join(', ')}`)
}
// keep `config.locales` normalized regardless of pruning, so readers such as `composer.locales`
// never see raw locale-code strings for this request
config.locales = resolved
🤖 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/shared/locales.ts` around lines 90 - 100, Update
resolveRequestLocales so config.locales is always assigned the normalized
resolved list, moving config.locales = resolved outside the resolved.length !==
codes.size conditional. Keep the existing development warning and unknown-locale
filtering inside the conditional.

Comment on lines +102 to +105
if (import.meta.dev && import.meta.prerender && !warnedPrerenderNarrowing && resolved.length !== localeCodes.length) {
warnedPrerenderNarrowing = true
console.warn('[nuxt-i18n] `i18n:request-config` narrowed the locales while prerendering - there is no request host at generate time, so this decision is frozen into every prerendered page.')
}

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 | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate locales.ts =="
git ls-files | rg '(^|/)locales\.ts$' || true

echo
echo "== inspect target file relevant section =="
if [ -f src/runtime/shared/locales.ts ]; then
  nl -ba src/runtime/shared/locales.ts | sed -n '1,160p'
fi

echo
echo "== search import.meta.dev/prerender usages =="
rg -n "import\.meta\.(dev|prerender)" -S . || true

echo
echo "== package/config hints =="
git ls-files | rg '(^|/)(package\.json|nuxt\.config\.(ts|js|mjs)|.nuxtignore)$' || true
sed -n '1,220p' package.json 2>/dev/null || true

Repository: nuxt-modules/i18n

Length of output: 294


🌐 Web query:

Nuxt import.meta.dev import.meta.prerender generate production mode Nuxt options

💡 Result:

In Nuxt, import.meta properties are tree-shakable build flags injected during the build process to provide runtime context about your application environment [1][2][3]. import.meta.dev This flag is true when running the Nuxt development server and false in production builds [1][2]. It is intended for use in runtime code (components, plugins, middleware, etc.) [4][5]. It is not available in configuration files like nuxt.config.ts or modules, where you should instead use environment variables or standard Node.js checks like import.meta.env.NODE_ENV === 'development' [4][5]. import.meta.prerender This flag is true only during the server-side prerendering stage of a build [1][2]. It allows your code to detect when it is being executed as part of the static generation process (e.g., triggered by npx nuxt generate or npx nuxt build --prerender) [6][7][8]. Recent updates have ensured this flag is correctly preserved in server builds so that it can be accurately evaluated by the Nitro engine during prerendering [9]. Production Mode and Generation When you run npx nuxt generate, Nuxt effectively executes a build and then performs prerendering [6][7]. This command pre-renders your application routes into static HTML files [6][7]. In this mode, import.meta.prerender will be true while individual pages are being generated [1][2]. Note that import.meta flags are explicitly designed for runtime usage [1][2]. If you need to change behavior based on the build mode within your nuxt.config.ts, you should use Nuxt configuration options or environment variables, as build-time flags are replaced statically during the bundling process [4][5][3].

Citations:


🌐 Web query:

Nuxt 3 nuxi generate prerender production mode import meta prerender

💡 Result:

In Nuxt 3, import.meta.prerender is a boolean flag that is true when your code is running on the server during the prerendering stage of a build [1]. This stage is triggered when using the nuxi generate command [2][3] or running nuxi build --prerender [4]. ### How it works During the generation process, Nuxt uses the Nitro crawler to discover and render your application routes into static HTML files [5][6]. When import.meta.prerender is true, it indicates that the current execution context is part of this static build-time rendering, rather than a standard server-side rendering (SSR) request at runtime [1]. ### Common use cases 1. Conditional logic: You can use this flag to execute code specifically during the build process, such as fetching static data or configuring plugins, while ensuring that code is tree-shaken (excluded) from your final client-side bundle [7]. 2. Payload optimization: It is often used to manage shared data or state. For example, you can use it to determine whether to fetch data from an external API or to use a pre-extracted payload that is available only during the prerendering phase [7]. 3. Route hinting: You can combine this with runtime composables like prerenderRoutes to programmatically signal to Nitro that specific dynamic routes should be prerendered [8]. ### Relationship to other modes - Production vs. Development: The import.meta.prerender flag is distinct from import.meta.dev [1]. Even in a production build, import.meta.prerender is only true during the phase where static assets are generated [1]. - Server-side vs. Client-side: While import.meta.server is true during both standard SSR and prerendering, import.meta.prerender specifically isolates the static generation step [1]. By leveraging this flag, you can optimize your application's bundle size by ensuring that server-only build logic does not leak into the browser [7].

Citations:


Remove the import.meta.dev guard from the prerender narrowing warning.

nuxi generate runs prerendering with import.meta.prerender set, not dev mode. Use only import.meta.prerender so the warning fires when locales are narrowed during generate-time prerendering.

🤖 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/shared/locales.ts` around lines 102 - 105, Update the prerender
narrowing warning condition near warnedPrerenderNarrowing to remove the
import.meta.dev requirement and rely only on import.meta.prerender, while
preserving the existing locale-length check, one-time warning guard, and warning
message.

@Togetic

Togetic commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

A consumer datapoint for the "which subset is honestly supportable" question, since #4016 is mine and you listed it in this family.

Our shape. One image, seven regional deployments, 13 locales in the build, four distinct default locales across those deployments (en, tr, ka, kk). prefix_except_default + compactRoutes. What varies per deployment is exactly the two fields this hook exposes: which locale owns the unprefixed tree, and which locales that region advertises. Same pages, same messages, same strategy everywhere.

The part that matters for scoping: for us the config is per-process, not per-request. Each pod serves one region and is pinned at boot by env (NUXT_PUBLIC_I18N_DEFAULT_LOCALE; the deploy fails when it is unset). Two configs are never alive in one process.

That slice avoids three of the four constraints you listed on #4101: compiled messages stay shareable across requests, because the config is constant for the lifetime of the process; prerendering has exactly one config to see, and freezing it into the HTML is the intent rather than a bug; and a per-region static build is unaffected. Only routes are left — the one you called most tractable. So if the general per-request version turns out to be too costly to support, the per-process slice is still worth having on its own, and it is all #4016 actually needs.

Where the hook stops short of #4016. defaultLocale from the hook does reach prefixable, so the prefixing decision is already correct. Name-based resolution is not: it still goes through the build-bound <base>___<buildDefault>. On a build whose default was en, with the hook selecting de, prefixable says de is unprefixed while localePath('index', 'de') returns /de. (Verified against a pristine 10.6.0 downstream.)

You named that gap here as "every candidate default has to be declared up front so its variants exist (#4016)". A locale-agnostic <base>___default name for the unprefixed tree removes the enumeration entirely: one unprefixed shape, and whichever locale the process defaults to resolves through it. That is all #4017 does, and it composes with this hook rather than competing — the hook decides which locale, the neutral name gives it something to resolve to. Equally happy if the flag isn't the shape you want; a hook that can influence the route-name getter would let the whole thing live in userland.

On the selection half, pruneUnservedLocaleRoutes is the supported version of something we do by hand today: our per-region locale subset comes from an API at runtime and only narrows the language picker, so /hu/... stays reachable on a pod that never advertises hu. We would use it.

One cross-link for the perf constraint you raised: @ikz2018 posted a CPU profile on #4130 (Aug 20) where the request hook's useRuntimeI18n(undefined, event)useRuntimeConfig(event) accounts for ~16% of non-idle samples on 10.6.0 as it ships (getEnv alone 8.9%). That was measured on static-asset requests served from their Node pods, so the share is specific to that traffic mix — but it is independent evidence for the cost you flagged, and per-request config would put that path on the critical path by design rather than by accident.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants