[Next.js 14] 国际化的 2 种解决方案

1,700 阅读4分钟

本文主要介绍基于Next.js的国际化解决方案,会根据 2 种打包模式分别介绍不同的集成方式

项目地址:zhaoth/React-Next-Admin (github.com)

以 Nodejs 或者 docker 的部署方式国际化集成,分支参考feature/i18n,静态导出模式基于 master

线上地址:react-next-admin.pages.dev

Static Exports部署方式国际化集成

next.config.js中去掉 output:export 也可以直接服务部署

安装next-intl

npm install next-intl

next-intl 国内目前文档无法正常访问

设置 next.config.js

添加 output: 'export'配置,由于此配置,当运行 npm run build 时,Next.js 将在 out 文件夹中生成静态 HTML/CSS/JS 文件。

/** @type {import('next').NextConfig} */
const nextConfig = {
    output: 'export',
}

module.exports = nextConfig

设置国际化文件结构

image.png

创建翻译文件

export default {
  welcome: 'Welcome',
  dashboard: 'Dashboard',
  list: 'List',
  ahookList: 'ahookList',
  proList: 'proList',
};

引入新添加的翻译文件

import login from './locales/en/login';
import menu from '@/i18n/locales/en/menu';

export default {
  login,
  menu,
};

更新文件结构

image.png 首先,创建 [locale] 文件夹并移动其中的现有 page.tsx 文件、layout.tsx 文件和 about 文件夹。不要忘记更新导入。 然后在跟目录的 page.tsx中将用户重定向到默认语言,如下:

'use client';
import { redirect } from 'next/navigation';
import useSettingStore from '@/store/useSettingStore';
import { staticRouter } from '@/static/staticRouter';

export default function Home() {
  const defaultLocale = useSettingStore((state) => state.defaultLocale);
  // 静态 build 模式下 不能用 next/router 需要用next/navigation
  redirect(`/${defaultLocale}/${staticRouter.login}`);
}

注意:使用静态导出时,不能在没有前缀的情况下使用默认区域设置。我们必须将传入的请求重定向到默认语言。如文档中所述。

更新 app/[locale]/layout.tsx

import { EmptyLayout } from '@/components';
import React from 'react';
import { Props } from '@/typing/Layout';
import { locales } from '@/static/locales';

//function to generate the routes for all the locales
export function generateStaticParams() {
  return locales.map((locale) => ({ locale }));
}

export default function Layout({ children, params: { locale } }: Props) {
  return (
    <>
      <EmptyLayout params={{
        locale: locale,
      }}>{children}</EmptyLayout>
    </>
  );
}

EmptyLayout.tsx

import '@/app/globals.css';
import AntdStyledComponentsRegistry from '@/lib/antd-registry';
import React from 'react';
import { NextIntlClientProvider } from 'next-intl';
import { Props } from '@/typing/Layout';
import en from '@/i18n/en';
import zh from '@/i18n/zh';
import { timeZone } from '@/static/locales';

export const metadata: { title: string, description: string } = {
  title: 'React Next Admin',
  description: '',
};

export default function EmptyLayout({ children, params: { locale } }: Props) {
  const messages = locale === 'en' ? en : zh;
  return (
    <NextIntlClientProvider locale={locale} messages={messages} timeZone={timeZone}>
      <AntdStyledComponentsRegistry>
        {children}
      </AntdStyledComponentsRegistry>
    </NextIntlClientProvider>
  );
}

在这里我们除了往布局里面传递了 children 之后还传递了一个params参数,并添加 generateStaticParams 函数以生成所有区域设置的静态路由,同时我们在 emptylayout添加上下文提供程序 NextIntlClientProvider

更新页面和组件以使用翻译


'use client'
import { useTranslations } from 'next-intl'

export default function HomePage() {
  const t = useTranslations('HomePage')

  return (
    <div>
      {t('helloWorld')}
    </div>
  )
}

添加“use client”(截至目前,仅在客户端组件中支持使用 next-intl 的翻译)导入 useTranslations 钩子并在我们的 jsx 中使用它

创建语言切换组件

import React, { useState } from 'react';
import { Group } from 'antd/es/radio';
import { usePathname as useIntlPathname, useRouter as useIntlRouter } from '@/lib/language';
import useSettingStore from '@/store/useSettingStore';
import { RadioChangeEvent } from 'antd';

export default function ChangeLanguage() {
  const options = [
    { label: 'EN', value: 'en' },
    { label: '中', value: 'zh' },
  ];
  const intlPathname = useIntlPathname();
  const intlRouter = useIntlRouter();
  const setDefaultLocale = useSettingStore((state) => state.setDefaultLocale);
  const defaultLocale = useSettingStore((state) => state.defaultLocale);
  const [value, setValue] = useState(defaultLocale);

  const onLanguageChange = ({ target: { value } }: RadioChangeEvent) => {
    setValue(value);
    setDefaultLocale(value);
    intlRouter.replace(intlPathname, { locale: value });
  };
  return (
    <>
      <Group options={options} onChange={onLanguageChange} value={value} key={value}>
      </Group>
    </>
  );
}

封装基于国际化的Link, redirect, usePathname, useRouter

由于静态导出模式,用 nextjs 自带的路由跳转的时候都必须添加 locale 较为麻烦下面是基于next-intl的createLocalizedPathnamesNavigation封装的路由,用法和 nextjs 路由一致

export const { Link, redirect, usePathname, useRouter } =
  createLocalizedPathnamesNavigation({
    locales,
    pathnames,
    localePrefix,
  });

以 Nodejs 或者 docker 的部署方式国际化集成

安装依赖

npm install react-i18next i18next i18next-browser-languagedetector i18next-resources-to-backend 
accept-language

目录结构

image.png

添加 i18n配置文件夹及相关配置

其中包括语言文件 json 和 i18n 国际化设置工具类

image.png

语言json

{
  "title": "Hi there!",
  "to-second-page": "To second page"
}

setting.ts

国际化初始设置文件

export const fallbackLng = 'en'
export const languages = [fallbackLng, 'zh']
export const defaultNS = 'translation'

export function getOptions (lng = fallbackLng, ns = defaultNS) {
  return {
    // debug: true,
    supportedLngs: languages,
    fallbackLng,
    lng,
    fallbackNS: defaultNS,
    defaultNS,
    ns
  }
}

client.ts

封装组件中调用的useTranslation方法

'use client'

import { useEffect, useState } from 'react'
import i18next from 'i18next'
import { initReactI18next, useTranslation as useTranslationOrg } from 'react-i18next'
import resourcesToBackend from 'i18next-resources-to-backend'
import LanguageDetector from 'i18next-browser-languagedetector'
import { getOptions,languages } from '@/i18n/settings'

const runsOnServerSide = typeof window === 'undefined'

// 在客户端,正常的单例模式是可以的
i18next
  .use(initReactI18next)
  .use(LanguageDetector)
  // @ts-ignores
  .use(resourcesToBackend((language, namespace) => import(`./locales/${language}/${namespace}.json`)))
  .init({
    ...getOptions(),
    lng: undefined, // 在客户端检测语言
    detection: {
      order: ['path', 'htmlTag', 'cookie', 'navigator'],
    },
    preload: runsOnServerSide ? languages : []
  })

export function useTranslation(lng:any, ns:any, options?:any) {
  const ret = useTranslationOrg(ns, options)
  const { i18n } = ret
  if (runsOnServerSide && lng && i18n.resolvedLanguage !== lng) {
    i18n.changeLanguage(lng)
  } else {
    // eslint-disable-next-line react-hooks/rules-of-hooks
    const [activeLng, setActiveLng] = useState(i18n.resolvedLanguage)
    // eslint-disable-next-line react-hooks/rules-of-hooks
    useEffect(() => {
      if (activeLng === i18n.resolvedLanguage) return
      setActiveLng(i18n.resolvedLanguage)
    }, [activeLng, i18n.resolvedLanguage])
    // eslint-disable-next-line react-hooks/rules-of-hooks
    useEffect(() => {
      if (!lng || i18n.resolvedLanguage === lng) return
      i18n.changeLanguage(lng)
    }, [lng, i18n])
  }
  return ret
}

添加middleware

nextjs 的特性之一,基于中间件拦截i18n 并进行转发

'use client'
import { NextResponse } from 'next/server'
import acceptLanguage from 'accept-language'
import { fallbackLng, languages } from '@/i18n/settings'

acceptLanguage.languages(languages)

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

const cookieName = 'i18next'

export function middleware(req:any) {
  let lng
  if (req.cookies.has(cookieName)) lng = acceptLanguage.get(req.cookies.get(cookieName).value)
  if (!lng) lng = acceptLanguage.get(req.headers.get('Accept-Language'))
  if (!lng) lng = fallbackLng

  // Redirect if lng in path is not supported
  if (
    !languages.some(loc => req.nextUrl.pathname.startsWith(`/${loc}`)) &&
    !req.nextUrl.pathname.startsWith('/_next')
  ) {
    return NextResponse.redirect(new URL(`/${lng}${req.nextUrl.pathname}`, req.url))
  }

  if (req.headers.has('referer')) {
    const refererUrl = new URL(req.headers.get('referer'))
    const lngInReferer = languages.find((l) => refererUrl.pathname.startsWith(`/${l}`))
    const response = NextResponse.next()
    if (lngInReferer) response.cookies.set(cookieName, lngInReferer)
    return response
  }

  return NextResponse.next()
}

页面中使用国际化

初始化组件时添加语言参数

export default function Login({ params: { lng } }

引用封装好的useTranslation,使用时添加对应语言文件包

const { t } = useTranslation(lng, 'login')

页面中使用

<ProFormCheckbox noStyle name="autoLogin">
  {t('title')}自动登录
</ProFormCheck