在 Next.js 中企业官网国际化的实践

前言

背景:在工作中需要搭建一个企业官网,实现国际化功能

项目场景:企业官网,需要多语言

技术选型:Next.js App Router + next-intl + ts

一、国际化整体思路

路由级别:所有页面路径都带语言前缀(如 /en/products)

文案管理:本地 JSON 词典

组件支持:语言切换器

二、核心代码

1. 本地 json 字典创建

创建如下目录,以完成本地国际化字典的创建

tree 复制代码
locales
├── index.ts
├── zh.json
├── en.json
├── es.json
├── fr.json
└── ru.json

2. index.ts

2.1 导出语言类型

2.2 设置默认语言

2.3 通过locale拿到对应的字典

2.4 返回Message类型

ts 复制代码
export const locales = ["zh", "en", "fr", "es", "ru"] as const;
export type Locale = typeof locales[number];

export const defaultLocale: Locale = "zh";

export async function getDictionary(locale: Locale) {
  let cur: any;
  switch (locale) {
    case 'en':
      cur = (await import('./en.json')).default; break;
    case 'fr':
      cur = (await import('./fr.json')).default; break;
    case 'es':
      cur = (await import('./es.json')).default; break;
    case 'ru':
      cur = (await import('./ru.json')).default; break;
    case 'zh':
      cur = (await import('./zh.json')).default; break;
    default:
      cur = (await import('./zh.json')).default; break;
  }
  return cur;
}

export type Messages = Awaited<ReturnType<typeof getDictionary>>;
  1. as const 将元素定义为字面类型,此处的意思为,"zh" 不再是 string 类型,而是 "zh" 类型
  2. typeof locales[number] 此为索引访问,将typeof locales 存在数组中的类型转为联合类型
  3. cur = (await import('./zh.json')).default; 动态导入 json 文件,json 文件通常为默认导出,所以取 default
  4. typeof getDictionary 用来获取 getDictionary 的数据类型,此处结果为(locale: Locale) => Promise<any>
  5. ReturnType<typeof getDictionary> 提取函数的返回类型,此处结果为 Promise<any>
  6. Awaited<ReturnType<typeof getDictionary>> 解开 Promise 类型,获取其解析的类型,此处结果为 Messages

3. 本地国际化的路由配置 next-intl/routing

3.1 导入 defineRouting

3.2 设置支持的语言,默认语言,语言前缀

ts 复制代码
import { defineRouting } from 'next-intl/routing';
import { locales as supportedLocales, defaultLocale } from '@/locales';

export const routing = defineRouting({
  locales: [...supportedLocales],
  defaultLocale: defaultLocale,
  localePrefix: 'always'
});
  1. locales as supportedLocales 将导出的 locales 赋予别名 supportedLocales
  2. defineRoutingnext-intl 的方法,用到的配置参数有:
    2.1 locales:国际化支持的全部语言
    2.2 defaultLocale:当没有语言时的默认语言
    2.3 localePrefix:默认情况下,应用的路径名将在与目录结构匹配的前缀下提供(例如 /en/aboutapp/[locale]/about/page.tsx) 默认值为 always
    了解更多

4. 国际化中间件配置 next-intl/middleware

4.1 createMiddlewarenext-intl 提供的方法,可以通过此方法创建一个中间件

4.2 可以在 config 中去配置匹配器 matcher 来表明需要应用此中间件的路由

ts 复制代码
import createMiddleware from 'next-intl/middleware';
import {routing} from './i18n/routing';

export default createMiddleware(routing);

export const config = {
  matcher: [
    '/((?!_next|.*\\..*|api).*)'
  ]
};

此文件应处于项目根目录或者src目录下
了解更多

5. 服务器和客户端组件 next-intl/server

5.1 获取语言并判断合法性 5.2 获取国际化字典 5.3 返回语言和对应的字典

ts 复制代码
import { getRequestConfig } from 'next-intl/server';
import { routing } from './routing';
import type { Locale } from '@/locales';

export default getRequestConfig(async ({ requestLocale }) => {
  let locale = await requestLocale;
  
  if (!locale || !routing.locales.includes(locale as Locale)) {
    locale = routing.defaultLocale;
  }

  const messages = (await import(`../locales/${locale}.json`)).default;

  return {
    locale,
    messages
  };
});
  1. i18n/request.ts 可用于为仅服务器代码提供配置,配置通过 getRequestConfig 函数提供
  2. NextIntlClientProvider 可用于为客户端组件提供配置
    了解更多

6. 引入国际化navigation next-intl/navigation

ts 复制代码
import {createNavigation} from 'next-intl/navigation';
import {routing} from './routing';

export const {Link, redirect, usePathname, useRouter, getPathname} = createNavigation(routing);

7. 多语言切换组件

7.1 定义选项数据

7.2 实现路由跳转

7.3 定义item和点击事件

7.4 预览语言的lable

ts 复制代码
export const LanguageSwitcher: FC<{ lang: Locale; messages: Messages }> = ({
  lang,
  messages,
}) => {
  const router = useRouter();
  const pathname = usePathname();

  const options = useMemo(() => {
    return (locales as readonly string[]).map((code) => ({
      code,
      label:
        messages.language?.[code as keyof typeof messages.language] ??
        code,
    }));
  }, [messages.language]);

  const navigateTo = (next: Locale) => {
    router.replace({ pathname: pathname || '/'}, { locale: next });
  };

  const items: MenuProps["items"] = options.map((opt) => ({
    key: opt.code,
    label: (
      <div className="flex items-center gap-2">
        <span>{opt.label}</span>
        {opt.code === lang && <Check size={16} />}
      </div>
    ),
  }));

  const onClick: MenuProps["onClick"] = ({ key }) => {
    navigateTo(key as Locale);
  };

  const currentLabel = useMemo(() => {
    const map = messages.language as undefined | Record<string, string>;
    const native = map?.[lang] ?? lang.toUpperCase();
    const prefixMap: Record<Locale, string> = {
      zh: 'China',
      en: 'English',
      fr: 'France',
      es: 'Spain',
      ru: 'Russia',
    };
    return `${prefixMap[lang]}-${native}`;
  }, [messages.language, lang]);

  return (
    <Dropdown
      menu={{ items, onClick }}
      trigger={["hover"]}
      placement="bottomRight"
      className="-mr-4"
    >
      <AntButton
        type="text"
        size="middle"
        aria-label={messages.ui?.selectLanguage ?? "Select Language"}
        icon={<LanguagesIcon size={18} />
      }
      >
        <span className="ml-1 text-sm text-[#555]">{currentLabel}</span>
      </AntButton>
    </Dropdown>
  );
};
  1. useMemo 每次重新渲染的时候能够缓存计算的结果 了解更多
  2. router.replace 是由 next-intl/navigationcreateNavigation 方法导出,可以穿入第二个参数作为国际化支持 了解更多
  3. 定义 MenuProps 类型的项和点击事件,类型由antd提供 了解更多
  4. Check 图标由 lucide-react 提供 了解更多

8. 国际化使用

8.1 在服务端组件中-同步组件使用 next-intl 提供的同步方法: learn more

ts 复制代码
import {useTranslations} from 'next-intl';

export default function HomePage() {
  const t = useTranslations('xxx');
  return <h1>{t('xxx')}</h1>;
}

8.2 在服务端组件中-异步组件使用 next-intl 提供的异步方法: learn more

ts 复制代码
import {getTranslations} from 'next-intl/server';

export default async function ProfilePage() {
  const t = await getTranslations('xxx');
  return <h1>{t('xxx')}</h1>;
}

8.3 在客户端组件中,使 NextIntlClientProvider 处于客户端组件的上游:learn more

ts 复制代码
import {NextIntlClientProvider} from 'next-intl';
 
export default async function RootLayout(/* ... */) {
  return (
    <html lang={locale}>
      <body>
        <NextIntlClientProvider>...</NextIntlClientProvider>
      </body>
    </html>
  );
}

三、常见坑与解决方案

To be continue

四、效果展示

To be continue

五、总结

To be continue

相关推荐
子兮曰1 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰1 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
前端小万1 天前
写公众号赚了 3000 块后,我做了一款叫 "一键成稿" 的软件
前端·微信小程序
爱勇宝1 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
三十而立洋1 天前
Cookie 详解:从产生到安全,一次讲透
前端·javascript
卡布鲁1 天前
把一个 Vite + Vue3 应用塞进 qiankun (React + Umi3) 主站:十个坑的复盘
前端·javascript·react.js
汉堡大王95271 天前
Jev:不是聊天机器人, 而是一个智能 if 语句
前端·人工智能·后端
梦想很大很大1 天前
从运行事实到回归证据:Workrun 的 Telemetry 与 Evaluation 实践
前端·人工智能·后端
计算机魔术师1 天前
Meta Muse agent 接入 Shopify 的 Shop Pay 实现代理式购物
前端
沙蒿同学1 天前
我用 Go 搭了一条 AI Agent 流水线:从 1 张商品图到一整套淘宝详情页
前端·javascript·后端