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.json2. 翻译内容
编辑新创建的文件,翻译所有文本:
{
"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>
);
}故障排查
翻译不显示
问题: 翻译键返回原始键名而不是翻译文本。
解决方案:
- 检查翻译键是否正确:
// 检查翻译文件中是否存在该键
console.log(messages.auth.login.title);- 确认语言代码正确:
// 确保语言代码在 i18nConfig.locales 中
console.log(i18nConfig.locales[locale]);- 检查命名空间是否正确:
// 确保命名空间与翻译文件结构匹配
const t = useTranslations("auth.login"); // 正确
const t = useTranslations("auth"); // 如果使用 t("login.title")邮件翻译回退
问题: 邮件翻译返回 "locale.mail.key" 格式的字符串。
解决方案:
检查翻译文件中是否包含 mail 字段:
{
"mail": {
"welcome": {
"subject": "欢迎加入",
"body": "感谢您的注册"
}
}
}中间件会话获取失败
问题: getSession 返回 null。
解决方案:
- 检查环境变量:
PORT=3000- 确认 API 路由可访问:
curl http://localhost:3000/api/auth/get-session- 检查 Cookie 是否正确传递:
// 确保请求包含 Cookie
console.log(req.headers.get("cookie"));