From 78a9512e0dd55ae9bcfc81eff30657df0d2f1653 Mon Sep 17 00:00:00 2001 From: shuki Date: Sun, 12 Apr 2026 03:43:19 +0300 Subject: [PATCH 1/2] feat: configurable localePrefix via NEXT_PUBLIC_LOCALE_PREFIX Allow the next-intl localePrefix mode to be set via environment variable at build time, defaulting to the existing 'never' behavior. This is useful when proxying Bulwark under a sub-path (where 'never' can trigger rewrite loops) or when users prefer URL-embedded locales (/en/settings vs /settings). Usage: NEXT_PUBLIC_LOCALE_PREFIX=always npm run build Accepted values: 'never' (default), 'always', 'as-needed'. --- i18n/routing.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/i18n/routing.ts b/i18n/routing.ts index e872638..372146e 100644 --- a/i18n/routing.ts +++ b/i18n/routing.ts @@ -1,9 +1,21 @@ import { defineRouting } from 'next-intl/routing'; +// Locale prefix mode can be configured via NEXT_PUBLIC_LOCALE_PREFIX. +// - "never" (default): /settings — locale from cookie/Accept-Language +// - "always": /en/settings — locale always in the URL +// - "as-needed": /settings for default locale, /fr/settings otherwise +// When proxying Bulwark under a sub-path (NEXT_PUBLIC_BASE_PATH), "always" is +// recommended to avoid next-intl rewrite loops caused by locale detection +// conflicting with the proxy's path rewriting. +const localePrefix = (process.env.NEXT_PUBLIC_LOCALE_PREFIX ?? 'never') as + | 'never' + | 'always' + | 'as-needed'; + export const routing = defineRouting({ locales: ['en', 'fr', 'de', 'es', 'it', 'ja', 'ko', 'lv', 'nl', 'pl', 'pt', 'ru', 'zh'], defaultLocale: 'en', - localePrefix: 'never' + localePrefix }); export const locales = routing.locales; From 6bb57855c352753d2e1705a898389e9691198c8c Mon Sep 17 00:00:00 2001 From: shuki Date: Sun, 12 Apr 2026 03:44:26 +0300 Subject: [PATCH 2/2] fix: skip intl middleware for paths already containing a locale prefix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When localePrefix is 'always' (or 'as-needed' with a non-default locale), paths like /en/settings already have the locale in the URL. Running them through the next-intl middleware a second time can trigger rewrite loops, especially when combined with a proxy basePath where the middleware's detection of the 'current' path conflicts with the rewritten one. Skip the intl middleware in this case — the path is already in the canonical locale-prefixed form and no further rewriting is needed. This makes NEXT_PUBLIC_LOCALE_PREFIX=always reliable for sub-path deployments. --- proxy.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/proxy.ts b/proxy.ts index 2a2352c..a9ed124 100644 --- a/proxy.ts +++ b/proxy.ts @@ -34,8 +34,16 @@ export function proxy(request: NextRequest) { const pathname = request.nextUrl.pathname; const isAdminRoute = pathname === '/admin' || pathname.startsWith('/admin/'); + // When localePrefix is 'always', paths that already have a locale prefix + // (e.g. /en/settings) should not be re-processed by the intl middleware — + // doing so can trigger rewrite loops when combined with a proxy basePath. + const locales = routing.locales as readonly string[]; + const hasLocalePrefix = locales.some( + (l) => pathname === `/${l}` || pathname.startsWith(`/${l}/`) + ); + let intlResponse: ReturnType | null = null; - if (!isAdminRoute) { + if (!isAdminRoute && !hasLocalePrefix) { try { intlResponse = intlMiddleware(request); } catch (error) {