likes
comments
collection
share

NextJs13的App路由如何添加国际化?

作者站长头像
站长
· 阅读数 21

1、安装依赖

yarn add i18next react-i18next i18next-resources-to-backend

2、更改配置

  1. 在App路由下,新增[lng]文件夹,将page.tsxlayout.tsx移入到[lng]文件夹下,并且确保后续的所有页面都建在[lng]文件夹下面

  2. 在根目录下新建i18n文件夹,并创建config.ts文件

    export const fallbackLng = 'en' //设置默认的语言,如果用户url不带语言,默认会使用这个语言
    export const languages = [fallbackLng, 'zh-CN']//设置支持的语言列表
    export const defaultNS = 'translation'
    export const cookieName = 'i18next'
    export type ILanguages = "en" | "zh-CN" //typescript可以导出这个类型,以便后续使用
    
    export function getOptions (lng = fallbackLng, ns:string|string[] = defaultNS) {
      return {
        // debug: true,
        supportedLngs: languages,
        fallbackLng,
        lng,
        fallbackNS: defaultNS,
        defaultNS,
        ns
      }
    }
    
  3. 在i8n文件夹下,新建server.ts,用于服务端渲染的国际化

    import { createInstance } from 'i18next'
    import resourcesToBackend from 'i18next-resources-to-backend'
    import { initReactI18next } from 'react-i18next/initReactI18next'
    import { ILanguages, getOptions } from './config'
    
    const initI18next = async (lng: ILanguages, ns: string|string[]) => {
      const i18nInstance = createInstance()
      await i18nInstance
        .use(initReactI18next)
        .use(resourcesToBackend((language: string, namespace: string) => import(`./locales/${language}/${namespace}.json`)))
        .init(getOptions(lng, ns))
      return i18nInstance
    }
    
    export async function useTranslation(lng: ILanguages, ns: string, options:any = {}) {
      const i18nextInstance = await initI18next(lng, ns)
      return {
        t: i18nextInstance.getFixedT(lng, Array.isArray(ns) ? ns[0] : ns, options.keyPrefix),
        i18n: i18nextInstance
      }
    }
    
  4. 在i18n文件夹创建locales文件夹,并创建你需要的语言文件夹,在语言文件夹下新建json文件

NextJs13的App路由如何添加国际化?

    //locales en home.json
    {
      "title":"Home"
    }
    //locales zh-CN home.json
    {
      "title":"主页"
    }
  1. 在layout文件中引入并在params中获取到lng
import '../globals.css'
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import { dir } from 'i18next'


const inter = Inter({ subsets: ['latin'] })
import { ILanguages, languages } from '@/i18n/config'
import Header from '@/components/Header'

export async function generateStaticParams() {
  return languages.map((lng) => ({ lng }))
}
export const metadata: Metadata = {
  title: 'Create Next App',
  description: 'Generated by create next app',
}

export default function RootLayout({
  children,
  params: {
    lng
  }
}: {
  children: React.ReactNode,
  params: {
    lng:ILanguages
  }
}) {
  return (
    <html lang={lng} dir={dir(lng)}>
      <body className={inter.className}>
        <Header/>
        {children}
      </body>
    </html>
  )
}

  1. 修改page.tsx的页面
import { ILanguages } from '@/i18n/config';
import Image from 'next/image'

export default function Home({params:{lng}}:{params:{lng:ILanguages}}) {
  console.log("lng",lng);
  return (
    <main className="flex min-h-screen flex-col items-center justify-between p-24">
      123
    </main>
  )
}
  1. 添加中间件middleware.js
import { NextResponse } from 'next/server'
import acceptLanguage from 'accept-language'
import { fallbackLng, languages, cookieName } from './i18n/config'

acceptLanguage.languages(languages)

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

export function middleware(req) {
  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") &&
    !req.nextUrl.pathname.startsWith("/api") &&
    !req.nextUrl.pathname.startsWith("/assets") &&
    !req.nextUrl.pathname.startsWith("/favicon.ico")
  ) {
    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()
}

至此,最简单的配置就完成了,我们也能看到,项目的路由上会自动添加上一个默认的语言

NextJs13的App路由如何添加国际化?

3、使用

  1. 服务端渲染国际化的使用
import { ILanguages } from '@/i18n/config';
import { useTranslation } from '@/i18n/server';

export default async function Home({params:{lng}}:{params:{lng:ILanguages}}) {
  console.log("lng",lng);
  const {t} =await useTranslation(lng,["home","common"]);
  return (
    <main className="flex min-h-screen flex-col items-center justify-between p-24">
      <span>{t("home:title")}</span>
      <div>{t("common:name")}</div>
    </main>
  )
}

这样,你就能看到不同的语言

NextJs13的App路由如何添加国际化?

NextJs13的App路由如何添加国际化?

4、使用客户端渲染国际化

  1. 安装依赖
yarn add react-cookie i18next-browser-languagedetector
  1. 在i8n文件夹下创建client.ts文件
'use client'

import { useEffect, useState } from 'react'
import i18next from 'i18next'
import { UseTranslationOptions, initReactI18next, useTranslation as useTranslationOrg } from 'react-i18next'
import { useCookies } from 'react-cookie'
import resourcesToBackend from 'i18next-resources-to-backend'
import LanguageDetector from 'i18next-browser-languagedetector'
import { getOptions, languages, cookieName, ILanguages } from './config'

const runsOnServerSide = typeof window === 'undefined'

// 
i18next
  .use(initReactI18next)
  .use(LanguageDetector)
  .use(resourcesToBackend((language: string, namespace: string) => import(`./locales/${language}/${namespace}.json`)))
  .init({
    ...getOptions(),
    lng: undefined, // let detect the language on client side
    detection: {
      order: ['path', 'htmlTag', 'cookie', 'navigator'],
    },
    preload: runsOnServerSide ? languages : []
  })

export function useTranslation(lng: ILanguages, ns: string|string[] = "common", options?: UseTranslationOptions<any>) {
  const [cookies, setCookie] = useCookies([cookieName])
  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])
    // eslint-disable-next-line react-hooks/rules-of-hooks
    useEffect(() => {
      if (cookies.i18next === lng) return
      setCookie(cookieName, lng, { path: '/' })
    }, [lng, cookies.i18next])
  }
  return ret
}
  1. 在i8n的locales文件夹中的各个语言中新增client.json
//en
{
  "title":"Client"
}
//zh-CN
{
  "title":"客户端"
}
  1. [lng]文件夹下新建client文件夹并创建page.tsx
"use client"
import { useTranslation } from '@/i18n/client'
import { ILanguages } from '@/i18n/config'
import { useParams } from 'next/navigation'
import React from 'react'

const ClientPage = () => {
  const params = useParams();
  const {t} = useTranslation(params?.lng as ILanguages,"client")
  console.log("params",params);
  
  return (
    <div>
      ClientPage
      <div>{t("title")}</div>
    </div>
  )
}

export default ClientPage
  1. 最后我们访问页面的

NextJs13的App路由如何添加国际化?

NextJs13的App路由如何添加国际化?

5、改变语言

封装一个名字叫useLanguage的hook

//useLanguage
import {ILanguages} from "@/i18n/config";
import {getPath} from "@/utils/common/getPath";
import {useParams, usePathname, useRouter} from "next/navigation";

const useLanguage = () => {
  const params = useParams();
  const path = usePathname();
  const router = useRouter();
  const changeLanguage = (lng: ILanguages) => {
    const newPath = getPath(path);
    router.push(`/${lng}${newPath}`);
  };
  return {
    lng: params.lng as ILanguages,
    changeLanguage,
  };
};

export default useLanguage;
//getPath
export function getPath(url: string) {
  const languagePattern = /\/(en|zh-CN)(\/.*)?/;
  const match = url.match(languagePattern);

  if (match) {
    return match[2] || "";
  } else {
    return url;
  }
}

在页面中使用

"use client"
import { ILanguages } from '@/i18n/config';
import React  from 'react'
import {languages} from "@/i18n/config"
import useLanguage from '@/hook/useLanguage';
const Header = () => {
  const {lng,changeLanguage} = useLanguage()
  const changeLanguages=(e:React.ChangeEvent<HTMLSelectElement>)=>{
    const newLng = e.target.value
    changeLanguage(newLng as ILanguages)
  }
  return (
    <div className='px-6 py-4'>
      <select defaultValue={lng} onChange={changeLanguages}>
        {
          languages.map((item)=>{
            return (<option key={item}>{item}</option>)
          })
        }
      </select>
    </div>
  )
}

export default Header

最后效果

切换成中文

NextJs13的App路由如何添加国际化? 切换成英文

NextJs13的App路由如何添加国际化?

6、补充

  1. 客户端国际化Hook封装
// useClientTranslation
import React, {useMemo} from "react";
import useLanguage from "./useLanguage";
import {useTranslation} from "@/i18n/client";

const useClientTranslation = (translationKey: string | string[] = "common") => {
  const {lng} = useLanguage();
  const {t, i18n} = useTranslation(lng, translationKey);
  return useMemo(() => {
    return {
      t,
      i18n,
    };
  }, [lng, translationKey]);
};

export default useClientTranslation;

用法

const {t} = useClientTranslation("json文件名" 或者 ["json文件名1""json文件名2"])
//单个文件
<span className="text-sm font-medium text-textTertiary">{t("title")}</span>
//多个语言文件
<span className="text-sm font-medium text-textTertiary">{t("common:title")}</span>
<span className="text-sm font-medium text-textTertiary">{t("release:title")}</span>
  1. 路由跳转的封装
import useLanguage from "./useLanguage";
import {useRouter} from "next-nprogress-bar";

const useRouterPush = () => {
  const router = useRouter();
  const {lng} = useLanguage();

  return (path: string) => {
    router.push(`/${lng}${path}`);
  };
};

export default useRouterPush;

用法

const routerPush = useRouterPush();
routerPush("/release");