在 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

相关推荐
双向332 小时前
前端性能优化:Webpack Tree Shaking 的实践与踩坑
前端
NeverSettle_3 小时前
2025前端网络相关知识深度解析
前端·javascript·http
JarvanMo3 小时前
Flutter. Draggable 和 DragTarget
前端
练习时长一年3 小时前
后端接口防止XSS漏洞攻击
前端·xss
muchu_CSDN3 小时前
谷粒商城项目-P16快速开发-人人开源搭建后台管理系统
前端·javascript·vue.js
Bye丶L3 小时前
AI帮我写代码
前端·ai编程
PuddingSama3 小时前
Android 高级绘制技巧: BlendMode
android·前端·面试
Cache技术分享3 小时前
186. Java 模式匹配 - Java 21 新特性:Record Pattern(记录模式匹配)
前端·javascript·后端
卸任3 小时前
Electron运行环境判断(是否在虚拟机中)
前端·react.js·electron