You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.21 10:13:10