ProductReadyProductReady

国际化 (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 自动:

  1. 检测 [lang]
  2. 加载正确的翻译
  3. 渲染本地化内容

语言检测机制

ProductReady 根据不同场景使用两种不同的语言检测策略

Web 页面(营销页、文档、认证)

对于 //docs/pricing/sign-in 等基于 URL 的路由,中间件使用以下优先级:

优先级来源说明
1URL 前缀/zh-CN/docs → 中文
2Cookie (webpage_lang)回访用户
3Accept-Language 头首次访问(浏览器设置)
4默认语言回退到 en

当用户访问页面时:

  • 如果 URL 有语言前缀 → 使用该语言,设置 cookie
  • 如果没有前缀但有 cookie → 重定向到首选语言
  • 如果是首次访问 → 从浏览器的 Accept-Language 头检测
  • 否则 → 使用默认语言(英语)

Cookie 持续 1 年,因此回访用户会看到他们的首选语言。

SPA 页面(仪表板、Agent、空间)

对于 /dashboard/agent/spaces/* 等仪表板路由,LocaleProvider 使用以下优先级:

优先级来源说明
1用户数据库设置数据库中的 user.locale
2localStorage (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 页面:使用页脚/页头的语言切换器,它会:

  1. 导航到新的语言 URL(例如 /zh-CN/docs
  2. 设置 webpage_lang cookie

仪表板:使用设置 → 语言下拉菜单,它会:

  1. 更新数据库中的 user.locale(跨设备持久化)
  2. 更新 localStorage(立即生效)
  3. 无需页面刷新即可重新加载字典

项目结构

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 个项目"

添加新语言

让我们添加西班牙语!

更新配置

编辑 .typesafe-i18n.json

{
  "baseLocale": "en",
  "locales": [
    "en",
    "zh-CN",
    "es"  // ← 添加这个
  ]
}

创建翻译文件

创建 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;

生成类型

pnpm typesafe-i18n

这会自动生成类型定义!

创建西班牙语文档

创建 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: '点击这里提交',
};

下一步


资源

On this page