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

Next.js 14 集成i18next语言切换后旧路由语言回退问题

问题描述

使用Next.js 14结合i18next实现多语言切换时,遇到路由导航语言回退问题:

  • 框架与依赖版本:
    "next": "14.2.13",
    "i18next": "^23.15.1",
    "i18next-resources-to-backend": "^1.2.1",
    "next-i18n-router": "^5.5.1",
    "react-i18next": "^15.0.2"
    
  • 问题表现:当语言设置为en时访问/dashboard/path1、/dashboard/path2等路由,切换至其他语言后,新路由(如/dashboard/path3)可正常显示新语言,但返回之前访问过的旧路由时,语言会自动回退至en。

相关代码片段

i18n-config.ts

import { Config } from 'next-i18n-router/dist/types';

const i18nConfig: Config = {
  locales: [ 'en', 'fa' ],
  defaultLocale: 'fa'
} as const;

export type TLocale = (typeof i18nConfig)['locales'][number];

export default i18nConfig;

i18n.ts

import { createInstance, i18n, Resource } from 'i18next';
import { initReactI18next } from 'react-i18next/initReactI18next';
import resourcesToBackend from 'i18next-resources-to-backend';
import i18nConfig from '@/i18n-config';

export default async function initTranslations(
  locale: string,
  namespaces: string[],
  i18nInstance?: i18n,
  resources?: Resource
) {
  i18nInstance = i18nInstance || createInstance();
  
  i18nInstance.use(initReactI18next);
  
  if (!resources) {
    i18nInstance.use(
      resourcesToBackend(
        (language: string, namespace: string) =>
          import(`@/locales/${ language }/${ namespace }.json`)
      )
    );
  }
  
  await i18nInstance.init({
    lng: locale,
    resources,
    fallbackLng: i18nConfig.defaultLocale,
    supportedLngs: i18nConfig.locales,
    defaultNS: namespaces[ 0 ],
    fallbackNS: namespaces[ 0 ],
    ns: namespaces,
    preload: resources ? [] : i18nConfig.locales
  });
  
  return {
    i18n: i18nInstance,
    resources: i18nInstance.services.resourceStore.data,
    t: i18nInstance.t
  };
}

middleware.ts

import { withAuth } from 'next-auth/middleware';
import { i18nRouter } from 'next-i18n-router';
import i18nConfig from './i18n-config';

export default withAuth(
  async function middleware(request) {
    return i18nRouter(request, i18nConfig);
  },
  {
    callbacks: {
      authorized: ({ req, token }) => {
        return !((req.nextUrl.pathname.includes('/dashboard') || req.nextUrl.pathname.includes(
          '/auth/create-app')) && token === null);
      }
    }
  }
);

export const config = {
  matcher: [ '/((?!api|_next/static|_next/image|assets|.*\\..*|favicon.ico|sw.js).*)' ]
};

layout.tsx

export const generateStaticParams = async () => i18nConfig?.locales?.map(locale => ({ locale }));

const namespaces = [
  'common',
  'auth',
  'rulesMessage',
  'dashboard',
  'applications',
  'providers',
  'apis',
  'industries',
  'services',
  'reports',
  'requestMessages'
];

const RootLayout: FC<PropsWithChildren & TPropsWithLocaleParam> = async ({ children, params: { locale } }) => {
  const session = await getServerSession(authOptions);
  
  const { resources, i18n: { dir } } = await initTranslations(locale, namespaces);
  
  return (
    <html lang={ locale } dir={ dir() }>
    <body className="!font-dynamicFont overflow-x-hidden">
    <AntdRegistry>
      <NextTopLoader />
      
      <TranslationsProvider locale={ locale } namespaces={ namespaces } resources={ resources }>
        <NextAuth session={ session }>
          { children }
        </NextAuth>
      </TranslationsProvider>
    </AntdRegistry>
    </body>
    </html>
  );
};

export default RootLayout;

translations-provider.tsx

'use client';

import { I18nextProvider } from 'react-i18next';
import { ReactNode } from 'react';
import { createInstance, Resource } from 'i18next';
import initTranslations from '@/i18n';

export default function TranslationsProvider({
  children,
  locale,
  namespaces,
  resources
}: {
  children: ReactNode;
  locale: string;
  namespaces: string[];
  resources: Resource;
}) {
  const i18n = createInstance();
  
  initTranslations(locale, namespaces, i18n, resources);
  
  return <I18nextProvider i18n={ i18n }>{ children }</I18nextProvider>;
}

language-switcher.tsx

'use client';

import { useTranslation } from 'react-i18next';
import { usePathname, useRouter } from 'next/navigation';
import i18nConfig from '@/i18n-config';

export const LanguageSwitcher = () => {
  const { i18n } = useTranslation();
  const currentLocale = i18n.language;
  const router = useRouter();
  const currentPathname = usePathname();
  
  const handleChangeLanguage = () => {
    const newLocale = currentLocale === 'en' ? 'fa' : 'en';
    
    // set cookie for next-i18n-router
    const days = 30;
    const date = new Date();
    date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
    const expires = date.toUTCString();
    document.cookie = `NEXT_LOCALE=${ newLocale };expires=${ expires };path=/`;
    
    // Set the new locale in the i18n instance
    i18n.changeLanguage(newLocale);
    
    // Update the URL
    if (currentLocale === i18nConfig.defaultLocale && !i18nConfig.prefixDefault) {
      router.replace('/' + newLocale + currentPathname);
    }
    else {
      router.push(currentPathname.replace(`/${ currentLocale }`, `/${ newLocale }`));
    }
  };
  
  return (
    <div
      className="--headerItems --rounded text-primary-0 text-captionMd capitalize cursor-pointer"
      onClick={ handleChangeLanguage }
    >
      { currentLocale === 'fa' ? 'en' : 'fa' }
    </div>
  );
};

原因分析

  1. Next.js客户端路由缓存:App Router默认缓存已访问页面的客户端状态,返回旧路由时直接读取缓存,未重新获取最新语言设置。
  2. i18n实例未全局同步:TranslationsProvider每次创建新的i18n实例,切换语言时仅更新当前页面实例,缓存页面的旧实例未同步更新。
  3. 路由跳转未触发刷新:语言切换时的路由跳转未强制刷新页面,缓存的旧页面组件未重新渲染。

解决方案

1. 禁用客户端缓存

在[locale]/layout.tsx中添加动态渲染配置,强制每次访问都重新获取语言状态:

// [locale]/layout.tsx
export const dynamic = "force-dynamic";

2. 全局共享i18n实例

修改TranslationsProvider,使用单例实例确保语言状态全局同步:

'use client';

import { I18nextProvider } from 'react-i18next';
import { ReactNode, useEffect, useState } from 'react';
import { createInstance, i18n, Resource } from 'i18next';
import initTranslations from '@/i18n';

// 全局单例i18n实例
let i18nInstance: i18n | null = null;

export default function TranslationsProvider({
  children,
  locale,
  namespaces,
  resources
}: {
  children: ReactNode;
  locale: string;
  namespaces: string[];
  resources: Resource;
}) {
  const [i18n, setI18n] = useState<i18n | null>(null);

  useEffect(() => {
    if (!i18nInstance) {
      i18nInstance = createInstance();
      initTranslations(locale, namespaces, i18nInstance, resources).then(setI18n);
    } else {
      // 同步更新语言和资源
      i18nInstance.changeLanguage(locale);
      namespaces.forEach(ns => {
        if (resources[locale]?.[ns]) {
          i18nInstance!.addResources(locale, ns, resources[locale][ns]);
        }
      });
      setI18n(i18nInstance);
    }
  }, [locale, namespaces, resources]);

  if (!i18n) return null;

  return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>;
}

3. 优化语言切换的路由逻辑

添加router.refresh()强制刷新页面,清除客户端缓存:

// language-switcher.tsx
const handleChangeLanguage = () => {
  const newLocale = currentLocale === 'en' ? 'fa' : 'en';
  
  // 设置cookie
  const days = 30;
  const date = new Date();
  date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
  const expires = date.toUTCString();
  document.cookie = `NEXT_LOCALE=${newLocale};expires=${expires};path=/`;
  
  // 构造新路径
  let newPathname = currentPathname.replace(`/${currentLocale}`, `/${newLocale}`);
  if (currentLocale === i18nConfig.defaultLocale && !i18nConfig.prefixDefault) {
    newPathname = `/${newLocale}${currentPathname}`;
  }
  
  // 跳转并强制刷新
  router.replace(newPathname);
  router.refresh();
};

4. 明确middleware的cookie配置

确保i18nRouter优先读取NEXT_LOCALE cookie:

// middleware.ts
export default withAuth(
  async function middleware(request) {
    return i18nRouter(request, {
      ...i18nConfig,
      localeCookie: 'NEXT_LOCALE'
    });
  },
  // ...其他配置
);

内容的提问来源于stack exchange,提问作者Masoud Tesna

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 22:45:54