Next.js13 App目录:基于浏览器语言实现无URL参数的多端翻译
在Next.js 13 App目录下基于next-intl实现无URL参数多语言方案
需求:
- 无需动态URL参数,通过中间件自动检测浏览器默认语言完成本地化
- 支持手动切换语言按钮
以下是我当前的实现代码,但不清楚如何完成上述需求:
TranslationProvider.tsx
"use client"; import { useLocale } from 'next-intl'; import { IntlProvider } from 'react-intl' import Danish from '../../lang/da.json' import English from '../../lang/en.json' function TranslationProviders({ children }: { children: React.ReactNode }) { const locale = typeof window !== 'undefined' ? navigator.language : 'en'; let lang; if(locale === 'da'){ lang=Danish } else{ lang=English } return ( <IntlProvider locale={locale} messages={English}> {children} </IntlProvider> ) } export default TranslationProviders;
i18n.ts
import { getRequestConfig } from 'next-intl/server'; export default getRequestConfig(async ({locale}) => ({ messages: (await import(`./lang/${locale}.json`)).default }));
next.config.js
/** @type {import('next').NextConfig} */ const withNextIntl = require('next-intl/plugin')( './i18n.ts' ); module.exports = withNextIntl({ experimental: {appDir: true} });
app/layout.tsx
import Navbar from '@/components/Navbar/Navbar' import Zidebar from '@/components/Sidebar/Zidebar' import { useLocale } from 'next-intl'; import Providers from '@/components/Provider' import TranslationProviders from '../components/TranslationProvider' const inter = Inter({ subsets: ['latin'] }) export const metadata = { title: 'Quickview', description: 'Quickview app', } type Props = { children: React.ReactNode; }; export default async function RootLayout({children}: Props) { const locale = typeof window !== 'undefined' ? navigator.language : 'en'; return ( <html lang={locale}> <body className={inter.className}> <TranslationProviders> <Providers> <div className="flex gap-1"> <Zidebar /> <div className="w-full flex-1"> <Navbar /> {children} </div> </div> </Providers> </TranslationProviders> </body> </html> ) }
app/page.tsx(服务端页面)
import { store } from '@/store' import { setBuildingData } from '@/store/buildingSlice' import Mapbox from '@/components/map/Map' import Preloader from '@/components/Preloader' import Providers from '@/components/Provider' import { useTranslations } from 'next-intl'; export default async function Home() { const buildingAPIUrl = process.env.BUILDINGS_ENDPOINT as string const fetchBuildingsApi = await fetch(buildingAPIUrl, { next: { revalidate: 4000 } }) const buildingData = await fetchBuildingsApi.json() store.dispatch(setBuildingData(buildingData)) const t = useTranslations() return ( <main> <Preloader buildings={buildingData} /> <Providers> <h1>{t('key1')}</h1> <Mapbox /> </Providers> </main> ) }
解决方案
一、实现浏览器语言自动检测(无URL参数)
核心思路:用中间件读取浏览器Accept-Language头,同时优先使用用户手动切换后存在cookie里的语言,将语言信息传递给next-intl,无需URL参数。
1. 创建中间件文件 middleware.ts
import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; import { match } from '@formatjs/intl-localematcher'; import Negotiator from 'negotiator'; // 支持的语言列表,和后续配置保持一致 const locales = ['en', 'da']; const defaultLocale = 'en'; function getLocale(request: NextRequest) { // 优先读取用户手动设置的cookie const userLocale = request.cookies.get('NEXT_LOCALE')?.value; if (userLocale && locales.includes(userLocale)) { return userLocale; } // 读取浏览器Accept-Language头,匹配最适合的语言 const headers = { 'accept-language': request.headers.get('accept-language') || '' }; const languages = new Negotiator({ headers }).languages(); return match(languages, locales, defaultLocale); } export function middleware(request: NextRequest) { const pathname = request.nextUrl.pathname; // 排除静态文件和API路由,不做处理 if (pathname.startsWith('/_next/') || pathname.startsWith('/api/') || pathname.includes('.')) { return NextResponse.next(); } const locale = getLocale(request); // 把语言信息通过header传递给App组件,同时写入cookie const response = NextResponse.next(); response.headers.set('x-locale', locale); response.cookies.set('NEXT_LOCALE', locale, { path: '/', maxAge: 31536000 }); return response; } export const config = { matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'], };
2. 修正 i18n.ts
确保只加载支持的语言,避免无效请求:
import { getRequestConfig } from 'next-intl/server'; const locales = ['en', 'da']; export default getRequestConfig(async ({ locale }) => { if (!locales.includes(locale)) { throw new Error(`Locale ${locale} is not supported`); } return { messages: (await import(`./lang/${locale}.json`)).default }; });
3. 修正 app/layout.tsx(服务端组件)
服务端组件不能访问navigator,直接用next-intl提供的服务端API获取locale和messages:
import Navbar from '@/components/Navbar/Navbar' import Zidebar from '@/components/Sidebar/Zidebar' import { getLocale, getMessages } from 'next-intl/server'; import Providers from '@/components/Provider' import { NextIntlClientProvider } from 'next-intl'; const inter = Inter({ subsets: ['latin'] }) export const metadata = { title: 'Quickview', description: 'Quickview app', } type Props = { children: React.ReactNode; }; export default async function RootLayout({ children }: Props) { const locale = await getLocale(); const messages = await getMessages(); return ( <html lang={locale}> <body className={inter.className}> {/* 用next-intl官方客户端Provider替代自定义的TranslationProviders */} <NextIntlClientProvider locale={locale} messages={messages}> <Providers> <div className="flex gap-1"> <Zidebar /> <div className="w-full flex-1"> <Navbar /> {children} </div> </div> </Providers> </NextIntlClientProvider> </body> </html> ) }
4. 删除自定义的 TranslationProvider.tsx
直接使用next-intl官方的NextIntlClientProvider即可,无需自己实现。
二、添加语言切换按钮
创建客户端组件LanguageSwitcher.tsx,通过设置cookie并刷新页面生效:
"use client"; import { useLocale } from 'next-intl'; import { useRouter } from 'next/navigation'; const locales = [ { code: 'en', name: 'English' }, { code: 'da', name: 'Dansk' } ]; export default function LanguageSwitcher() { const currentLocale = useLocale(); const router = useRouter(); const handleSwitchLocale = (newLocale: string) => { if (newLocale === currentLocale) return; // 设置cookie,有效期1年 document.cookie = `NEXT_LOCALE=${newLocale}; path=/; max-age=31536000`; // 刷新页面让语言设置生效 router.refresh(); }; return ( <div className="flex gap-2"> {locales.map((locale) => ( <button key={locale.code} onClick={() => handleSwitchLocale(locale.code)} className={`px-3 py-1 rounded ${currentLocale === locale.code ? 'bg-blue-500 text-white' : 'bg-gray-100'}`} > {locale.name} </button> ))} </div> ); }
将该组件引入到Navbar.tsx中,即可在页面顶部显示语言切换按钮。
三、修正page.tsx中的问题
服务端组件不能用useTranslations钩子,改用服务端APIgetTranslations:
import { store } from '@/store' import { setBuildingData } from '@/store/buildingSlice' import Mapbox from '@/components/map/Map' import Preloader from '@/components/Preloader' import Providers from '@/components/Provider' import { getTranslations } from 'next-intl/server'; export default async function Home() { const buildingAPIUrl = process.env.BUILDINGS_ENDPOINT as string const fetchBuildingsApi = await fetch(buildingAPIUrl, { next: { revalidate: 4000 } }) const buildingData = await fetchBuildingsApi.json() store.dispatch(setBuildingData(buildingData)) // 服务端组件使用getTranslations const t = await getTranslations(); return ( <main> <Preloader buildings={buildingData} /> <Providers> <h1>{t('key1')}</h1> <Mapbox /> </Providers> </main> ) }
内容的提问来源于stack exchange,提问作者Keshav Tomar
相关产品推荐
相关产品推荐

