🍭🍭🍭五分钟带你掌握next国际化最佳实践

Next.js系列文章

🔥🔥🔥念头通达:手把手带你学next+tailwind(一)

🔥🔥🔥念头通达:手把手带你学next:缓存配置、性能优化和SEO(二)

掘金相关文章next国际化实践都是使用第三方库,实际上next内置了国际化的实现,不需要引入任何三方包

由于Next.js新版本使用的是App router,所以我们用App router

项目初始化

  1. 新建项目
lua 复制代码
npx create-next-app@latest

2. 准备语言包,在public文件夹中新增locales文件夹,我们这里准备中英文两种语言包 ,目录结构如下

arduino 复制代码
|- public
 |- locales
  |- zh-Hans.json // 中文
  |- en.json // 英文

先配置最简单的

json 复制代码
//en.json
{
  "product": {
    "cart": "Add to Cart"
  }
}
json 复制代码
// zh-Hans.json
{
  "product": {
    "cart": "添加到购物车"
  }
}

3.根目录下新建i18n.config.ts文件

typescript 复制代码
// i18n.config.ts
export const i18n = {
  defaultLocale: "zh-Hans", // 默认中文
  locales: ["zh-Hans", "en"],// 支持中文,英文
};
export type Locale = (typeof i18n)["locales"][number];
  1. 根目录下新建dictionaries.ts
typescript 复制代码
// dictionaries.ts
import "server-only";
import { Locale, i18n } from "./i18.config";
const locales: { [key: string]: () => Promise<any> } = {
  en: () => import("./public/locales/en.json").then((module) => module.default),
  "zh-Hans": () =>
    import("./public/locales/zh-Hans.json").then((module) => module.default),
};

function getLocale(locale: Locale) {
  if (typeof locales[locale] === "function") {
    return locales[locale]();
  } else {
    console.error("Locale not found");
  }
}

export const getDictinary = async (locale = i18n.defaultLocale as Locale) =>
  getLocale(locale);
  1. app目录下新建[lang]/page.tsx
javascript 复制代码
// app/[lang]/page.tsx
import { getDictinary } from "@/dictionaries";
export default async function Page({
  params: { lang },
}: {
  params: { lang: string };
}) {
  const dict = await getDictinary(lang);
  return (
    <button className="bg-blue-500 text-white rounded-md px-2 py-3 m-5 hover:bg-blue-700">
      {dict.product.cart}
    </button>
  );
}
  1. 访问项目

    pnpm dev

换一个路由试试

可以看到国际化生效了

国际化配置

如果我们想把需求改一下呢,当前语言为中文时不展示/zh-Hans后缀,其他语言,比如英文展示/en后缀

此时需要使用middleware中间件了

根目录新建middleware.js 文件

javascript 复制代码
// middleware.js
import { NextResponse } from "next/server";
import { i18n } from "./i18.config.ts";

const { locales, defaultLocale } = i18n;

const publicFile = /\.(.*)|\/favicon.ico|\/*\/.$/;

function getLocale(request) {
  const { pathname } = request.nextUrl;
  const segments = pathname.split("/");
  const locale = i18n.locales.includes(segments[1])
    ? segments[1]
    : defaultLocale;
  return locale;
}
export function middleware(request) {
  const { pathname } = request.nextUrl;
  const pathnameHasLocale = locales.some((locale) => pathname.includes(locale));

  // 已经有语言配置了跳过
  if (pathnameHasLocale) {
    return;
  }
  // public文件不重定向
  if (publicFile.test(pathname)) {
    return;
  }
  const locale = getLocale(request); // pathname没有语言则重定向
  request.nextUrl.pathname = `/${locale}${pathname}`;
  return NextResponse.rewrite(request.nextUrl);
}
export const config = {
  matcher: [
    // Skip all internal paths (_next)
    "/((?!_next).*)",
    // Optional: only run on root (/) URL
    // '/'
  ],
};

访问默认路由,可以看到默认使用的是中文

切换到英文路由,语言包用的英文的

客户端组件使用

服务端组件可以通过引入getDictinary获取语言包,客户端组件如何使用国际化呢

我们可以借助react hooks实现

  1. 根目录下新建hooks目录useDict.ts
javascript 复制代码
// hooks/useDict.ts
import { i18n } from "@/i18.config.ts";
export default function useDict(locale: string) {
  if (!i18n.locales.includes(locale)) {
    locale = i18n.defaultLocale;
  }
  const res = require(`../public/locales/${locale}.json`);
  return res;
}
  1. next.config.mjs支持ts扩展
arduino 复制代码
// next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
  allowImportingTsExtensions: true,
};

export default nextConfig;
  1. 根目录新建components目录header/index.tsx文件
javascript 复制代码
// components/header/index.tsx
"use client";
import useDict from "@/hooks/useDict";
export default function Header({ lang }: { lang: string }) {
  const dict = useDict(lang);
  return <header>{dict.product.cart}</header>;
}
  1. app/[lang]/page.tsx引入header组件
javascript 复制代码
import { getDictinary } from "@/dictionaries";
import Header from "@/components/header";
export default async function Page({
  params: { lang },
}: {
  params: { lang: string };
}) {
  const dict = await getDictinary(lang);
  return (
    <>
      <button className="bg-blue-500 text-white rounded-md px-2 py-3 m-5 hover:bg-blue-700">
        {dict.product.cart}
      </button>
      <Header lang={lang} />
    </>
  );
}
  1. 启动项目一切正常

切换语言组件

接下来我们实现切换语言组件,点击选择框切换语言

  1. 安装clsx,这个库用来管理多个css

    pnpm install clsx

  2. components下新建locale目录localeProvider.tsx文件

typescript 复制代码
// components/localeProvider.tsx
"use client";
import { createContext, useContext } from "react";
import { i18n } from "@/i18.config";

type LocaleContextType = {
  lang: string;
  setLang: (locale: string) => void;
};
const LocaleContext = createContext({} as LocaleContextType);

export const useLocale = () => useContext(LocaleContext);

export const LocaleProvider = ({
  children,
  lang,
}: {
  children: React.ReactNode;
  lang: string;
}) => {
  function setLang(locale: string) {
    lang = locale;
  }
  if (!i18n.locales.includes(lang)) {
    lang = i18n.defaultLocale;
  }
  return (
    <LocaleContext.Provider value={{ lang, setLang }}>
      {children}
    </LocaleContext.Provider>
  );
};
  1. 同级目录下新建localeSwitcher.tsx
ini 复制代码
// localeSwitcher.tsx
"use client";
import { useState, useEffect } from "react";
import { useLocale } from "./localeProvider";
import { usePathname, useRouter } from "next/navigation";
import { Locale, i18n, LocaleMap } from "@/i18.config";
import clsx from "clsx";
export default function LocaleSwitcher({ className }: { className: string }) {
  const { lang, setLang } = useLocale();
  const [showTab, setShowTab] = useState(false);
  const [activeIndex, setActiveIndex] = useState(0);

  const pathname = usePathname();
  const router = useRouter();

  const toggleTab = () => {
    setShowTab(!showTab);
  };
  const findActiveLocale = () => {
    const segments = pathname.split("/");
    const locale = i18n.locales.includes(segments[1])
      ? segments[1]
      : i18n.defaultLocale;
    const index = i18n.locales.indexOf(locale);
    if (index > -1) {
      setActiveIndex(index);
      setLang(i18n.locales[index]);
    }
  };

  const redirectedPathName = (locale: Locale) => {
    if (!pathname || locale === i18n.defaultLocale) return "/";
    const segments = pathname.split("/");
    segments[1] = locale;
    return segments.join("/");
  };
  const changeLocale = (e: any, locale: string, index: number) => {
    e.preventDefault();
    setLang(locale);
    setActiveIndex(index);
    const url = redirectedPathName(locale || i18n.defaultLocale);
    if (url !== pathname) {
      router.push(url);
    }
  };
  useEffect(() => {
    findActiveLocale();
  }, []);

  return (
    <div className={clsx(className)}>
      <h2
        className="cursor-pointer border-yellow-600 border border-solid text-black rounded-lg py-2.5 px-3"
        onClick={toggleTab}
      >
        当前语言:{(i18n.localeMap as LocaleMap)[lang as string]}
      </h2>
      {showTab && (
        <ul className="absolute z-10 bg-white lg:w-[200px] flex flex-col">
          {i18n.locales.map((locale: Locale, index: number) => (
            <li
              onClick={(e) => {
                changeLocale(e, locale, index);
              }}
              key={index}
              className={clsx(
                "cursor-pointer h-8 flex items-center justify-center w-full text-[#333] bg-white rounded-none py-4 pl-[25px] hover:bg-[#f0f0f0] hover:rounded-none",
                `${
                  activeIndex === index &&
                  "w-full bg-[#f0f0f0] font-semibold  lg:text-black rounded-md"
                }`
              )}
            >
              {locale}
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}
  1. i18n.config.ts增加类型定义LocaleMap和localeMap
typescript 复制代码
// i18n.config.ts
export type LocaleMap = {
  [key: string]: string;
};
export const i18n = {
  defaultLocale: "zh-Hans",
  locales: ["zh-Hans", "en"],
  localeMap: {
    "zh-Hans": "简体中文",
    en: "英文",
  },
};
export type Locale = (typeof i18n)["locales"][number];
  1. 在根布局组件中引入LocaleProvider
javascript 复制代码
// layout.tsx
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import { LocaleProvider } from "@/components/locale/localeProvider";
import "./globals.css";
import { Locale } from "@/i18.config";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "国际化",
  description: "柠檬酱国际化测试",
};

export default function RootLayout({
  children,
  params,
}: Readonly<{
  children: React.ReactNode;
  params: { lang: Locale };
}>) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <LocaleProvider lang={params.lang}>{children}</LocaleProvider>
      </body>
    </html>
  );
}
  1. 在app/[lang]/page.tsx中引入语言切换组件LocaleSwitcher.tsx
javascript 复制代码
// app/[lang]/page.tsx
import { getDictinary } from "@/dictionaries";
import Header from "@/components/header";
import LocaleSwitcher from "@/components/locale/localeSwitcher";
export default async function Page({
  params: { lang },
}: {
  params: { lang: string };
}) {
  const dict = await getDictinary(lang);
  return (
    <div>
      <div className="flex w-screen justify-end">
        <LocaleSwitcher className="relative" />
      </div>
      <button className="bg-blue-500 text-white rounded-md px-2 py-3 m-5 hover:bg-blue-700">
        {dict.product.cart}
      </button>
      <Header lang={lang} />
    </div>
  );
}
  1. 启动项目
arduino 复制代码
pnpm dev // or
npm dev // or
yarn dev

看到可以正常切换

你学会了吗~

相关推荐
l1x1n023 分钟前
No.3 笔记 | Web安全基础:Web1.0 - 3.0 发展史
前端·http·html
昨天;明天。今天。38 分钟前
案例-任务清单
前端·javascript·css
夜流冰1 小时前
工具方法 - 面试中回答问题的技巧
面试·职场和发展
zqx_72 小时前
随记 前端框架React的初步认识
前端·react.js·前端框架
惜.己2 小时前
javaScript基础(8个案例+代码+效果图)
开发语言·前端·javascript·vscode·css3·html5
什么鬼昵称2 小时前
Pikachu-csrf-CSRF(get)
前端·csrf
长天一色3 小时前
【ECMAScript 从入门到进阶教程】第三部分:高级主题(高级函数与范式,元编程,正则表达式,性能优化)
服务器·开发语言·前端·javascript·性能优化·ecmascript
NiNg_1_2343 小时前
npm、yarn、pnpm之间的区别
前端·npm·node.js
秋殇与星河3 小时前
CSS总结
前端·css
BigYe程普3 小时前
我开发了一个出海全栈SaaS工具,还写了一套全栈开发教程
开发语言·前端·chrome·chatgpt·reactjs·个人开发