在 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

相关推荐
崔庆才丨静觅13 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606114 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了14 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅14 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅15 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅15 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment15 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅16 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊16 小时前
jwt介绍
前端
爱敲代码的小鱼16 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax