Next.js系列文章
🔥🔥🔥念头通达:手把手带你学next+tailwind(一)
🔥🔥🔥念头通达:手把手带你学next:缓存配置、性能优化和SEO(二)
掘金相关文章next国际化实践都是使用第三方库,实际上next内置了国际化的实现,不需要引入任何三方包
由于Next.js新版本使用的是App router,所以我们用App router
项目初始化
- 新建项目
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];
- 根目录下新建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);
- 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>
);
}
-
访问项目
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实现
- 根目录下新建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;
}
- next.config.mjs支持ts扩展
arduino
// next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
allowImportingTsExtensions: true,
};
export default nextConfig;
- 根目录新建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>;
}
- 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} />
</>
);
}
- 启动项目一切正常
切换语言组件
接下来我们实现切换语言组件,点击选择框切换语言
-
安装clsx,这个库用来管理多个css
pnpm install clsx
-
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>
);
};
- 同级目录下新建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>
);
}
- 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];
- 在根布局组件中引入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>
);
}
- 在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>
);
}
- 启动项目
arduino
pnpm dev // or
npm dev // or
yarn dev
看到可以正常切换
你学会了吗~