国际化 (i18n)
使用类型安全的翻译为您的 ProductReady 应用添加多语言支持
国际化 (i18n)
ProductReady 使用 typesafe-i18n 提供内置的 i18n 支持。为您的应用添加多种语言:
- ✅ 类型安全的翻译 - 所有翻译键的自动补全
- ✅ 开箱即用的英文 + 中文
- ✅ 易于添加新语言 - 只需添加翻译文件
- ✅ 处处可用 - 服务器组件、客户端组件、API 路由
零运行时开销:所有翻译都经过编译时检查和 tree-shaking!
快速开始
查看多语言内容
ProductReady URL 包含语言前缀:
http://localhost:3000/en ← 英语
http://localhost:3000/zh-CN ← 中文文档已完全翻译:
http://localhost:3000/en/docs
http://localhost:3000/zh-CN/docs工作原理
URL 结构
/[lang]/(home)/page.tsx ← 落地页(所有语言)
/[lang]/docs/[[...slug]]/page.tsx ← 文档(所有语言)Next.js 自动:
- 检测
[lang]段 - 加载正确的翻译
- 渲染本地化内容
语言检测机制
ProductReady 根据不同场景使用两种不同的语言检测策略:
Web 页面(营销页、文档、认证)
对于 /、/docs、/pricing、/sign-in 等基于 URL 的路由,中间件使用以下优先级:
| 优先级 | 来源 | 说明 |
|---|---|---|
| 1 | URL 前缀 | /zh-CN/docs → 中文 |
| 2 | Cookie (webpage_lang) | 回访用户 |
| 3 | Accept-Language 头 | 首次访问(浏览器设置) |
| 4 | 默认语言 | 回退到 en |
当用户访问页面时:
- 如果 URL 有语言前缀 → 使用该语言,设置 cookie
- 如果没有前缀但有 cookie → 重定向到首选语言
- 如果是首次访问 → 从浏览器的 Accept-Language 头检测
- 否则 → 使用默认语言(英语)
Cookie 持续 1 年,因此回访用户会看到他们的首选语言。
SPA 页面(仪表板、Agent、空间)
对于 /dashboard、/agent、/spaces/* 等仪表板路由,LocaleProvider 使用以下优先级:
| 优先级 | 来源 | 说明 |
|---|---|---|
| 1 | 用户数据库设置 | 数据库中的 user.locale |
| 2 | localStorage (app_locale) | 客户端偏好 |
| 3 | 浏览器语言 | navigator.languages 检测 |
| 4 | 默认语言 | 回退到 en |
与 Web 页面的主要区别:
- 无 URL 前缀 - 仪表板路由没有
/zh-CN/dashboard - 用户设置优先 - 已登录用户的数据库偏好优先
- localStorage 持久化 - 比 cookie 更快的 SPA 导航
- 客户端检测 - 使用
@formatjs/intl-localematcher进行浏览器语言匹配
为什么使用两种不同的机制?
| 方面 | Web 页面 | SPA 仪表板 |
|---|---|---|
| SEO | 需要基于 URL 的 i18n 供爬虫使用 | 不需要 SEO |
| 认证状态 | 可能是匿名用户 | 始终已登录 |
| 持久化 | Cookie(服务器可读) | 数据库 + localStorage |
| 路由 | 服务器端重定向 | 客户端 SPA |
切换语言
Web 页面:使用页脚/页头的语言切换器,它会:
- 导航到新的语言 URL(例如
/zh-CN/docs) - 设置
webpage_langcookie
仪表板:使用设置 → 语言下拉菜单,它会:
- 更新数据库中的
user.locale(跨设备持久化) - 更新 localStorage(立即生效)
- 无需页面刷新即可重新加载字典
项目结构
src/
├── lib/
│ └── i18n/
│ ├── en/ # 英文翻译
│ │ └── index.ts
│ ├── zh-CN/ # 中文翻译
│ │ └── index.ts
│ ├── i18n-types.ts # 生成的类型(自动)
│ ├── i18n-util.ts # 工具函数
│ └── formatters.ts # 自定义格式化器
├── app/
│ └── [lang]/ # 语言感知路由
└── content/
└── docs/
├── en/ # 英文文档
└── zh-CN/ # 中文文档使用翻译
在服务器组件中
// src/app/[lang]/(home)/page.tsx
import { loadLocale } from '~/lib/i18n/i18n-util.sync';
import LL from '~/lib/i18n/i18n-types';
export default function HomePage({
params,
}: {
params: { lang: string };
}) {
// 加载此语言的翻译
const { lang } = params;
loadLocale(lang);
const L = LL[lang];
return (
<div>
<h1>{L.home.title()}</h1>
<p>{L.home.description()}</p>
</div>
);
}在客户端组件中
'use client';
import { useI18nContext } from '~/lib/i18n/i18n-react';
export function WelcomeMessage() {
const { LL } = useI18nContext();
return (
<div>
<h2>{LL.welcome.greeting()}</h2>
<p>{LL.welcome.subtitle()}</p>
</div>
);
}使用参数
// 翻译文件
export default {
greeting: '你好,{name}!',
items: '你有 {count} 个项目',
} as const;
// 使用
LL.greeting({ name: '张三' }); // "你好,张三!"
LL.items({ count: 5 }); // "你有 5 个项目"添加新语言
让我们添加西班牙语!
创建翻译文件
创建 src/lib/i18n/es/index.ts:
import type { Translation } from '../i18n-types';
const es: Translation = {
home: {
title: 'Bienvenido a ProductReady',
description: 'Lanza aplicaciones agénticas en días, no semanas',
getStarted: 'Empezar',
},
nav: {
docs: 'Documentación',
blog: 'Blog',
login: 'Iniciar sesión',
},
auth: {
signUp: 'Registrarse',
signIn: 'Iniciar sesión',
email: 'Correo electrónico',
password: 'Contraseña',
},
};
export default es;创建西班牙语文档
创建 content/docs/es/ 目录:
mkdir -p content/docs/es
cp content/docs/en/index.mdx content/docs/es/index.mdx
# 翻译内容...更新语言切换器
编辑 src/components/language-switcher.tsx:
const languages = [
{ code: 'en', name: 'English' },
{ code: 'zh-CN', name: '中文' },
{ code: 'es', name: 'Español' }, // ← 添加这个
];完成! 西班牙语现在可在 /es/* 使用
翻译文件
结构
// src/lib/i18n/zh-CN/index.ts
import type { Translation } from '../i18n-types';
const zhCN: Translation = {
// 按功能/页面分组
home: {
title: '欢迎来到 ProductReady',
description: '更快发布',
},
auth: {
signIn: '登录',
signUp: '注册',
forgotPassword: '忘记密码?',
},
dashboard: {
title: '仪表板',
tasks: {
title: '任务',
create: '创建任务',
delete: '删除',
},
},
errors: {
notFound: '页面未找到',
serverError: '出了点问题',
},
};
export default zhCN;使用复数
const zhCN: Translation = {
items: '{count} {count|plural|个项目}',
};
// 使用
LL.items({ count: 0 }); // "0 个项目"
LL.items({ count: 1 }); // "1 个项目"
LL.items({ count: 5 }); // "5 个项目"语言切换器
创建语言切换器组件:
'use client';
import { useParams, useRouter } from 'next/navigation';
export function LanguageSwitcher() {
const router = useRouter();
const params = useParams();
const currentLang = params.lang as string;
const languages = [
{ code: 'en', name: 'English', flag: '🇺🇸' },
{ code: 'zh-CN', name: '中文', flag: '🇨🇳' },
];
function switchLanguage(newLang: string) {
const currentPath = window.location.pathname;
const newPath = currentPath.replace(`/${currentLang}`, `/${newLang}`);
router.push(newPath);
}
return (
<select
value={currentLang}
onChange={(e) => switchLanguage(e.target.value)}
>
{languages.map((lang) => (
<option key={lang.code} value={lang.code}>
{lang.flag} {lang.name}
</option>
))}
</select>
);
}SEO 和元数据
语言感知元数据
// src/app/[lang]/layout.tsx
export function generateMetadata({
params,
}: {
params: { lang: string };
}) {
const { lang } = params;
loadLocale(lang);
const L = LL[lang];
return {
title: L.seo.title(),
description: L.seo.description(),
alternates: {
languages: {
en: '/en',
'zh-CN': '/zh-CN',
},
},
};
}最佳实践
✅ 推荐做法
- 按功能分组 - 逻辑地组织翻译
- 使用命名空间 -
auth.signIn,而不是authSignIn - 保持键描述性 -
home.hero.title,而不是t1 - 提供上下文 - 为翻译者添加注释
- 使用复数 - 正确处理单数/复数
- 格式化日期/数字 - 使用内置格式化器
// ✅ 好的
const zhCN: Translation = {
auth: {
signIn: '登录',
signUp: '注册',
// 注释:显示在忘记密码链接上
forgotPassword: '忘记密码?',
},
};❌ 避免做法
- 不要硬编码字符串 - 始终使用翻译
- 不要拆分句子 - 翻译完整句子
- 不要假设词序 - 不同语言有不同的语法
- 不要使用缩写 - 它们翻译不佳
// ❌ 不好
const zhCN = {
s1: '点击', // ← 不要拆分
s2: '这里', // ← 句子
btnTxt: '提交', // ← 使用完整名称
};
// ✅ 好的
const zhCN = {
callToAction: '点击这里提交',
};下一步
资源
- typesafe-i18n: github.com/ivanhofer/typesafe-i18n
- 翻译文件:
src/lib/i18n/ - 配置:
.typesafe-i18n.json