01MVP 标识01MVP
包文档UI 与工具i18n 国际化

i18n 国际化

统一的国际化(i18n)包,提供翻译管理、多语言支持和消息格式化功能

本页面介绍 API 使用方法。Monorepo 策略和配置请参考 开发指南

@mono/i18n

统一的国际化(i18n)包,基于 next-intl 构建,提供翻译管理、多语言支持、消息格式化等功能。

功能特性

  • next-intl 集成 - 基于 next-intl 的现代化国际化方案
  • 分层消息管理 - 支持共享翻译和应用级翻译
  • 智能回退机制 - 自动回退到默认语言
  • 邮件翻译支持 - 特殊的邮件翻译 Proxy 机制
  • 类型安全 - 完整的 TypeScript 支持
  • 中间件辅助 - 提供会话和组织查询工具

安装

pnpm add @mono/i18n

快速开始

在组件中使用翻译

import { useTranslations } from "next-intl";

export function LoginForm() {
  const t = useTranslations("auth.login");

  return (
    <form>
      <h1>{t("title")}</h1>
      <p>{t("subtitle")}</p>
      <input placeholder={t("email")} />
      <input placeholder={t("password")} />
      <button type="submit">{t("submit")}</button>
    </form>
  );
}

在服务端组件中使用

import { getTranslations } from "next-intl/server";

export default async function WelcomePage() {
  const t = await getTranslations("common");

  return (
    <div>
      <h1>{t("welcome")}</h1>
      <p>{t("description")}</p>
    </div>
  );
}

获取翻译消息

import { getMessagesForLocale } from "@mono/i18n";

const messages = await getMessagesForLocale("zh", {
  i18nConfig: {
    locales: {
      zh: { label: "简体中文", currency: "CNY" },
      en: { label: "English", currency: "USD" }
    },
    defaultLocale: "zh"
  }
});

核心 API

getMessagesForLocale

获取指定语言的完整翻译消息,包括共享翻译和应用级翻译。

async function getMessagesForLocale(
  locale: string,
  options: GetMessagesForLocaleOptions
): Promise<Messages>

示例:

const messages = await getMessagesForLocale("zh", {
  i18nConfig: {
    locales: {
      zh: { label: "简体中文", currency: "CNY" },
      en: { label: "English", currency: "USD" }
    },
    defaultLocale: "zh"
  },
  importAppLocaleMessages: async (locale) => {
    return (await import(`./translations/${locale}.json`)).default;
  }
});

mergeMessages

深度合并两个翻译消息对象。

import { mergeMessages } from "@mono/i18n";

const base = {
  auth: {
    login: { title: "Login" },
    signup: { title: "Sign Up" }
  }
};

const override = {
  auth: {
    login: { title: "登录", subtitle: "欢迎回来" }
  }
};

const merged = mergeMessages(base, override);
// 结果:
// {
//   auth: {
//     login: { title: "登录", subtitle: "欢迎回来" },
//     signup: { title: "Sign Up" }
//   }
// }

翻译文件结构

共享翻译

共享翻译文件位于 packages/i18n/translations/shared/

packages/i18n/translations/shared/
├── zh.json    # 中文翻译
└── en.json    # 英文翻译

应用级翻译

应用可以提供自己的翻译文件:

// apps/mono-web/src/modules/i18n/messages.ts
const importAppLocaleMessages = async (locale: string) => {
  return (await import(`./translations/${locale}.json`)).default;
};

export const getAppMessagesForLocale = (locale: string) => {
  return getMessagesForLocale(locale, {
    importAppLocaleMessages,
    defaultLocale: config.i18n.defaultLocale,
    i18nConfig: {
      locales: config.i18n.locales,
      defaultLocale: config.i18n.defaultLocale,
    },
  });
};

翻译文件格式

翻译文件使用嵌套的 JSON 结构:

{
  "auth": {
    "login": {
      "title": "登录",
      "subtitle": "欢迎回来",
      "email": "邮箱",
      "password": "密码",
      "submit": "登录",
      "errors": {
        "invalidEmail": "邮箱格式不正确",
        "invalidPassword": "密码错误"
      }
    }
  },
  "common": {
    "actions": {
      "continue": "继续",
      "cancel": "取消",
      "save": "保存"
    }
  }
}

消息格式化

变量插值

在翻译文本中使用变量:

{
  "welcome": "欢迎, {name}!",
  "greeting": "你好 {firstName} {lastName}"
}
const t = useTranslations("common");

// 使用变量
<p>{t("welcome", { name: "张三" })}</p>
// 输出: 欢迎, 张三!

<p>{t("greeting", { firstName: "张", lastName: "三" })}</p>
// 输出: 你好 张 三

复数形式

处理不同数量的文本:

{
  "items": "{count, plural, =0 {没有项目} =1 {1 个项目} other {# 个项目}}",
  "users": "{count, plural, one {# user} other {# users}}"
}
const t = useTranslations("common");

<p>{t("items", { count: 0 })}</p>  // 没有项目
<p>{t("items", { count: 1 })}</p>  // 1 个项目
<p>{t("items", { count: 5 })}</p>  // 5 个项目

日期和时间格式化

{
  "lastLogin": "上次登录: {date, date, long}",
  "publishedAt": "发布于 {date, date, short}"
}
const t = useTranslations("common");

<p>{t("lastLogin", { date: new Date() })}</p>
// 输出: 上次登录: 2024年3月9日

<p>{t("publishedAt", { date: new Date("2024-01-01") })}</p>
// 输出: 发布于 2024/1/1

富文本

在翻译中使用 HTML 标签:

{
  "terms": "我同意 <link>服务条款</link> 和 <privacy>隐私政策</privacy>",
  "welcome": "欢迎 <strong>{name}</strong>!"
}
const t = useTranslations("common");

{t.rich("terms", {
  link: (chunks) => <a href="/terms" className="underline">{chunks}</a>,
  privacy: (chunks) => <a href="/privacy" className="underline">{chunks}</a>
})}

{t.rich("welcome", {
  name: "张三",
  strong: (chunks) => <strong className="font-bold">{chunks}</strong>
})}

邮件翻译

特殊的 mail 字段

getMessagesForLocale 返回的消息对象包含一个特殊的 mail 字段,使用 Proxy 实现自动回退:

const messages = await getMessagesForLocale("zh", options);

// 如果 mail.welcome.subject 不存在,会自动返回 "zh.mail.welcome.subject"
const subject = messages.mail.welcome.subject;
const body = messages.mail.welcome.body;

邮件模板示例

import { getMessagesForLocale } from "@mono/i18n";

async function sendWelcomeEmail(locale: string, email: string, name: string) {
  const messages = await getMessagesForLocale(locale, {
    i18nConfig: config.i18n,
    importAppLocaleMessages
  });

  const subject = messages.mail.welcome.subject;
  const body = messages.mail.welcome.body
    .replace("{name}", name)
    .replace("{email}", email);

  await sendEmail({
    to: email,
    subject,
    html: body
  });
}

中间件辅助函数

getSession

在中间件中获取用户会话信息。

import { getSession } from "@mono/i18n";
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export async function middleware(req: NextRequest) {
  const session = await getSession(req);

  if (!session?.user) {
    return NextResponse.redirect(new URL("/login", req.url));
  }

  return NextResponse.next();
}

特性:

  • 自动处理 PaaS/Docker 环境的内部请求
  • 转发必要的请求头(Host, X-Forwarded-For, CF-Connecting-IP)
  • 禁用 Cookie 缓存以获取最新会话

getOrganizationsForSession

获取当前会话用户的组织列表。

import { getOrganizationsForSession } from "@mono/i18n";

export async function middleware(req: NextRequest) {
  const organizations = await getOrganizationsForSession(req);

  if (organizations.length === 0) {
    return NextResponse.redirect(new URL("/onboarding", req.url));
  }

  return NextResponse.next();
}

添加新语言

1. 创建翻译文件

packages/i18n/translations/shared/ 中创建新的语言文件:

# 创建日语翻译
cp packages/i18n/translations/shared/en.json \
   packages/i18n/translations/shared/ja.json

2. 翻译内容

编辑新创建的文件,翻译所有文本:

{
  "auth": {
    "login": {
      "title": "ログイン",
      "subtitle": "お帰りなさい",
      "email": "メールアドレス",
      "password": "パスワード"
    }
  }
}

3. 更新配置

在应用的配置文件中添加新语言:

// apps/mono-web/src/lib/config.ts
export const config = {
  i18n: {
    locales: {
      zh: { label: "简体中文", currency: "CNY" },
      en: { label: "English", currency: "USD" },
      ja: { label: "日本語", currency: "JPY" }  // 新增
    },
    defaultLocale: "zh"
  }
};

4. 创建应用级翻译(可选)

如果应用有自己的翻译,也需要创建对应的语言文件:

cp apps/mono-web/src/modules/i18n/translations/en.json \
   apps/mono-web/src/modules/i18n/translations/ja.json

最佳实践

1. 翻译键命名规范

使用清晰的层级结构:

{
  "模块": {
    "功能": {
      "具体内容": "翻译文本"
    }
  }
}

推荐的命名方式:

{
  "auth": {
    "login": {
      "title": "登录",
      "emailLabel": "邮箱",
      "emailPlaceholder": "请输入邮箱",
      "passwordLabel": "密码",
      "submitButton": "登录",
      "errors": {
        "invalidEmail": "邮箱格式不正确",
        "invalidPassword": "密码错误"
      }
    }
  }
}

2. 避免硬编码文本

// ❌ 不推荐 - 硬编码文本
<button>登录</button>
<p>欢迎回来</p>

// ✅ 推荐 - 使用翻译
const t = useTranslations("auth.login");
<button>{t("submitButton")}</button>
<p>{t("subtitle")}</p>

3. 使用命名空间

在组件中使用命名空间减少重复:

// ✅ 推荐 - 使用命名空间
const t = useTranslations("auth.login");
return (
  <div>
    <h1>{t("title")}</h1>
    <p>{t("subtitle")}</p>
  </div>
);

// ❌ 不推荐 - 重复完整路径
const t = useTranslations();
return (
  <div>
    <h1>{t("auth.login.title")}</h1>
    <p>{t("auth.login.subtitle")}</p>
  </div>
);

4. 处理缺失翻译

配置回退行为:

// apps/mono-web/src/modules/i18n/request.ts
export default getRequestConfig(async ({ requestLocale }) => {
  return {
    locale,
    messages,
    onError(error) {
      if (process.env.NODE_ENV !== "production") {
        console.error("[i18n]", error);
      }
    },
    getMessageFallback({ namespace, key }) {
      return namespace ? `${namespace}.${key}` : key;
    }
  };
});

5. 分层管理翻译

  • 共享翻译 (packages/i18n/translations/shared/) - 通用的 UI 文本(按钮、表单、错误消息等)
  • 应用翻译 (apps/mono-web/src/modules/i18n/translations/) - 特定业务逻辑的文本
  • 邮件翻译 - 邮件模板专用文本,使用 mail 命名空间

6. 提供有意义的键名

{
  "auth": {
    "login": {
      // ✅ 清晰明确
      "emailPlaceholder": "请输入邮箱",
      "passwordPlaceholder": "请输入密码",
      "submitButton": "登录",

      // ❌ 不清晰
      "input1": "请输入邮箱",
      "input2": "请输入密码",
      "btn": "登录"
    }
  }
}

实际应用示例

表单组件

import { useTranslations } from "next-intl";
import { useState } from "react";

export function ContactForm() {
  const t = useTranslations("contact.form");
  const [formData, setFormData] = useState({
    name: "",
    email: "",
    message: ""
  });

  return (
    <form>
      <h2>{t("title")}</h2>
      <p>{t("description")}</p>

      <div>
        <label>{t("nameLabel")}</label>
        <input
          type="text"
          placeholder={t("namePlaceholder")}
          value={formData.name}
          onChange={(e) => setFormData({ ...formData, name: e.target.value })}
        />
      </div>

      <div>
        <label>{t("emailLabel")}</label>
        <input
          type="email"
          placeholder={t("emailPlaceholder")}
          value={formData.email}
          onChange={(e) => setFormData({ ...formData, email: e.target.value })}
        />
      </div>

      <div>
        <label>{t("messageLabel")}</label>
        <textarea
          placeholder={t("messagePlaceholder")}
          value={formData.message}
          onChange={(e) => setFormData({ ...formData, message: e.target.value })}
        />
      </div>

      <button type="submit">{t("submitButton")}</button>
    </form>
  );
}

错误处理

import { useTranslations } from "next-intl";

export function ErrorDisplay({ errorCode }: { errorCode: string }) {
  const t = useTranslations("errors");

  const errorMessages: Record<string, string> = {
    "invalid_email": t("invalidEmail"),
    "invalid_password": t("invalidPassword"),
    "network_error": t("networkError"),
    "server_error": t("serverError")
  };

  return (
    <div className="error">
      {errorMessages[errorCode] || t("unknown")}
    </div>
  );
}

动态内容

import { useTranslations } from "next-intl";

export function UserProfile({ user }: { user: { name: string; joinDate: Date } }) {
  const t = useTranslations("profile");

  return (
    <div>
      <h1>{t("welcome", { name: user.name })}</h1>
      <p>{t("memberSince", { date: user.joinDate })}</p>
      <p>{t("postsCount", { count: user.postsCount })}</p>
    </div>
  );
}

故障排查

翻译不显示

问题: 翻译键返回原始键名而不是翻译文本。

解决方案:

  1. 检查翻译键是否正确:
// 检查翻译文件中是否存在该键
console.log(messages.auth.login.title);
  1. 确认语言代码正确:
// 确保语言代码在 i18nConfig.locales 中
console.log(i18nConfig.locales[locale]);
  1. 检查命名空间是否正确:
// 确保命名空间与翻译文件结构匹配
const t = useTranslations("auth.login"); // 正确
const t = useTranslations("auth");       // 如果使用 t("login.title")

邮件翻译回退

问题: 邮件翻译返回 "locale.mail.key" 格式的字符串。

解决方案:

检查翻译文件中是否包含 mail 字段:

{
  "mail": {
    "welcome": {
      "subject": "欢迎加入",
      "body": "感谢您的注册"
    }
  }
}

中间件会话获取失败

问题: getSession 返回 null

解决方案:

  1. 检查环境变量:
PORT=3000
  1. 确认 API 路由可访问:
curl http://localhost:3000/api/auth/get-session
  1. 检查 Cookie 是否正确传递:
// 确保请求包含 Cookie
console.log(req.headers.get("cookie"));

相关资源