From 656e75e573f5f77eb369971738a2b0857b7a665f Mon Sep 17 00:00:00 2001
From: JJ Kasper
Date: Mon, 28 Jul 2025 21:36:48 -0700
Subject: [PATCH] Fix i18n fallback: false collision
---
packages/next/src/server/lib/i18n-provider.ts | 4 +-
packages/next/src/server/next-server.ts | 6 +--
.../i18n-fallback-collision.test.ts | 45 +++++++++++++++++++
.../i18n-fallback-collision/next.config.ts | 13 ++++++
.../[second]/[third]/[fourth]/index.js | 32 +++++++++++++
.../pages/[first]/[second]/[third]/index.js | 27 +++++++++++
.../pages/[first]/[second]/index.js | 27 +++++++++++
.../pages/[first]/index.js | 27 +++++++++++
.../i18n-fallback-collision/pages/_app.tsx | 5 +++
.../pages/_document.tsx | 13 ++++++
.../i18n-fallback-collision/pages/index.tsx | 7 +++
11 files changed, 201 insertions(+), 5 deletions(-)
create mode 100644 test/e2e/i18n-fallback-collision/i18n-fallback-collision.test.ts
create mode 100644 test/e2e/i18n-fallback-collision/next.config.ts
create mode 100644 test/e2e/i18n-fallback-collision/pages/[first]/[second]/[third]/[fourth]/index.js
create mode 100644 test/e2e/i18n-fallback-collision/pages/[first]/[second]/[third]/index.js
create mode 100644 test/e2e/i18n-fallback-collision/pages/[first]/[second]/index.js
create mode 100644 test/e2e/i18n-fallback-collision/pages/[first]/index.js
create mode 100644 test/e2e/i18n-fallback-collision/pages/_app.tsx
create mode 100644 test/e2e/i18n-fallback-collision/pages/_document.tsx
create mode 100644 test/e2e/i18n-fallback-collision/pages/index.tsx
diff --git a/packages/next/src/server/lib/i18n-provider.ts b/packages/next/src/server/lib/i18n-provider.ts
index 5582113a7e5e..6a5f316a48e4 100644
--- a/packages/next/src/server/lib/i18n-provider.ts
+++ b/packages/next/src/server/lib/i18n-provider.ts
@@ -118,8 +118,8 @@ export class I18NProvider {
// query and strip it from the pathname.
if (analysis.detectedLocale) {
if (analysis.detectedLocale !== detectedLocale) {
- throw new Error(
- `Invariant: The detected locale does not match the locale in the query. Expected to find '${detectedLocale}' in '${pathname}' but found '${analysis.detectedLocale}'}`
+ console.warn(
+ `The detected locale does not match the locale in the query. Expected to find '${detectedLocale}' in '${pathname}' but found '${analysis.detectedLocale}'}`
)
}
diff --git a/packages/next/src/server/next-server.ts b/packages/next/src/server/next-server.ts
index 338df2ce1edd..708fdbcc75f2 100644
--- a/packages/next/src/server/next-server.ts
+++ b/packages/next/src/server/next-server.ts
@@ -1095,9 +1095,9 @@ export default class NextNodeServer extends BaseServer<
throw new Error('Invariant: pathname is undefined')
}
- // This is a catch-all route, there should be no fallbacks so mark it as
- // such.
- addRequestMeta(req, 'bubbleNoFallback', true)
+ // When in minimal mode we do not bubble the fallback as the
+ // router-server is not present to handle the error
+ addRequestMeta(req, 'bubbleNoFallback', this.minimalMode ? undefined : true)
// This is needed to expose render404 and nextConfig
// for environments without router-server
diff --git a/test/e2e/i18n-fallback-collision/i18n-fallback-collision.test.ts b/test/e2e/i18n-fallback-collision/i18n-fallback-collision.test.ts
new file mode 100644
index 000000000000..5c89029aec20
--- /dev/null
+++ b/test/e2e/i18n-fallback-collision/i18n-fallback-collision.test.ts
@@ -0,0 +1,45 @@
+import { nextTestSetup } from 'e2e-utils'
+
+describe('i18n-disallow-multiple-locales', () => {
+ const { next } = nextTestSetup({
+ files: __dirname,
+ })
+
+ it.each([
+ ['/non-existent'],
+ ['/es/non-existent'],
+ ['/first/non-existent'],
+ ['/es/first/non-existent'],
+ ['/first/second/non-existent'],
+ ['/es/first/second/non-existent'],
+ ])(
+ 'should 404 properly for fallback: false non-prerendered %s',
+ async (pathname) => {
+ const res = await next.fetch(pathname)
+ expect(res.status).toBe(404)
+ }
+ )
+
+ it.each([
+ { urlPath: '/first', page: '/[first]' },
+ { urlPath: '/first/second', page: '/[first]/[second]' },
+ { urlPath: '/first/second/third', page: '/[first]/[second]/[third]' },
+ {
+ urlPath: '/first/second/third/fourth',
+ page: '/[first]/[second]/[third]/[fourth]',
+ },
+ ])(
+ 'should render properly for fallback: false prerendered $urlPath',
+ async ({ urlPath, page }) => {
+ const res = await next.fetch(urlPath)
+ expect(res.status).toBe(200)
+ expect(await res.text()).toContain(page)
+ }
+ )
+
+ it('should render properly for fallback: blocking', async () => {
+ const res = await next.fetch('/first/second/third/another')
+ expect(res.status).toBe(200)
+ expect(await res.text()).toContain('/[first]/[second]/[third]/[fourth]')
+ })
+})
diff --git a/test/e2e/i18n-fallback-collision/next.config.ts b/test/e2e/i18n-fallback-collision/next.config.ts
new file mode 100644
index 000000000000..d322e5a79fe5
--- /dev/null
+++ b/test/e2e/i18n-fallback-collision/next.config.ts
@@ -0,0 +1,13 @@
+import type { NextConfig } from 'next'
+
+const nextConfig: NextConfig = {
+ /* config options here */
+ reactStrictMode: true,
+
+ i18n: {
+ defaultLocale: 'en',
+ locales: ['en', 'es'],
+ },
+}
+
+export default nextConfig
diff --git a/test/e2e/i18n-fallback-collision/pages/[first]/[second]/[third]/[fourth]/index.js b/test/e2e/i18n-fallback-collision/pages/[first]/[second]/[third]/[fourth]/index.js
new file mode 100644
index 000000000000..6d4904e9b466
--- /dev/null
+++ b/test/e2e/i18n-fallback-collision/pages/[first]/[second]/[third]/[fourth]/index.js
@@ -0,0 +1,32 @@
+export default function Page() {
+ return (
+ <>
+ page: /[first]/[second]/[third]/[fourth]
+ >
+ )
+}
+
+export function getStaticProps({ params }) {
+ return {
+ props: {
+ params,
+ now: Date.now(),
+ },
+ }
+}
+
+export function getStaticPaths() {
+ return {
+ paths: [
+ {
+ params: {
+ first: 'first',
+ second: 'second',
+ third: 'third',
+ fourth: 'fourth',
+ },
+ },
+ ],
+ fallback: 'blocking',
+ }
+}
diff --git a/test/e2e/i18n-fallback-collision/pages/[first]/[second]/[third]/index.js b/test/e2e/i18n-fallback-collision/pages/[first]/[second]/[third]/index.js
new file mode 100644
index 000000000000..f32ce55db64b
--- /dev/null
+++ b/test/e2e/i18n-fallback-collision/pages/[first]/[second]/[third]/index.js
@@ -0,0 +1,27 @@
+export default function Page() {
+ return (
+ <>
+ page: /[first]/[second]/[third]
+ >
+ )
+}
+
+export function getStaticProps({ params }) {
+ return {
+ props: {
+ params,
+ now: Date.now(),
+ },
+ }
+}
+
+export function getStaticPaths() {
+ return {
+ paths: [
+ {
+ params: { first: 'first', second: 'second', third: 'third' },
+ },
+ ],
+ fallback: false,
+ }
+}
diff --git a/test/e2e/i18n-fallback-collision/pages/[first]/[second]/index.js b/test/e2e/i18n-fallback-collision/pages/[first]/[second]/index.js
new file mode 100644
index 000000000000..c8e4e5befa96
--- /dev/null
+++ b/test/e2e/i18n-fallback-collision/pages/[first]/[second]/index.js
@@ -0,0 +1,27 @@
+export default function Page() {
+ return (
+ <>
+ page: /[first]/[second]
+ >
+ )
+}
+
+export function getStaticProps({ params }) {
+ return {
+ props: {
+ params,
+ now: Date.now(),
+ },
+ }
+}
+
+export function getStaticPaths() {
+ return {
+ paths: [
+ {
+ params: { first: 'first', second: 'second' },
+ },
+ ],
+ fallback: false,
+ }
+}
diff --git a/test/e2e/i18n-fallback-collision/pages/[first]/index.js b/test/e2e/i18n-fallback-collision/pages/[first]/index.js
new file mode 100644
index 000000000000..3e5fd7e387b8
--- /dev/null
+++ b/test/e2e/i18n-fallback-collision/pages/[first]/index.js
@@ -0,0 +1,27 @@
+export default function Page() {
+ return (
+ <>
+ page: /[first]
+ >
+ )
+}
+
+export function getStaticProps({ params }) {
+ return {
+ props: {
+ params,
+ now: Date.now(),
+ },
+ }
+}
+
+export function getStaticPaths() {
+ return {
+ paths: [
+ {
+ params: { first: 'first' },
+ },
+ ],
+ fallback: false,
+ }
+}
diff --git a/test/e2e/i18n-fallback-collision/pages/_app.tsx b/test/e2e/i18n-fallback-collision/pages/_app.tsx
new file mode 100644
index 000000000000..93928c3da70e
--- /dev/null
+++ b/test/e2e/i18n-fallback-collision/pages/_app.tsx
@@ -0,0 +1,5 @@
+import type { AppProps } from 'next/app'
+
+export default function App({ Component, pageProps }: AppProps) {
+ return
+}
diff --git a/test/e2e/i18n-fallback-collision/pages/_document.tsx b/test/e2e/i18n-fallback-collision/pages/_document.tsx
new file mode 100644
index 000000000000..89f88e106823
--- /dev/null
+++ b/test/e2e/i18n-fallback-collision/pages/_document.tsx
@@ -0,0 +1,13 @@
+import { Html, Head, Main, NextScript } from 'next/document'
+
+export default function Document() {
+ return (
+
+
+
+
+
+
+
+ )
+}
diff --git a/test/e2e/i18n-fallback-collision/pages/index.tsx b/test/e2e/i18n-fallback-collision/pages/index.tsx
new file mode 100644
index 000000000000..a7954953fb47
--- /dev/null
+++ b/test/e2e/i18n-fallback-collision/pages/index.tsx
@@ -0,0 +1,7 @@
+export default function Home() {
+ return (
+ <>
+ index page
+ >
+ )
+}