---
description: "SnapOtter 支持的 21 种语言,以及如何使用受 TypeScript 强制约束的 i18n 系统创建或改进翻译。"
i18n_source_hash: 55837d9fdaef
i18n_provenance: human
i18n_output_hash: b82813334f15
---
# 翻译指南 {#translation-guide}
SnapOtter 开箱即支持 21 种语言。i18n 系统采用轻量级自定义运行时,具备受 TypeScript 强制约束的语言完整性和动态代码拆分能力。
## 支持的语言 {#supported-languages}
| Code | Language | Native Name | Direction |
|------|----------|-------------|-----------|
| `en` | English | English | LTR |
| `zh-CN` | Chinese (Simplified) | 简体中文 | LTR |
| `zh-TW` | Chinese (Traditional) | 繁體中文 | LTR |
| `ja` | Japanese | 日本語 | LTR |
| `ko` | Korean | 한국어 | LTR |
| `es` | Spanish | Español | LTR |
| `fr` | French | Français | LTR |
| `it` | Italian | Italiano | LTR |
| `pt-BR` | Portuguese (Brazil) | Português (Brasil) | LTR |
| `de` | German | Deutsch | LTR |
| `nl` | Dutch | Nederlands | LTR |
| `sv` | Swedish | Svenska | LTR |
| `ru` | Russian | Русский | LTR |
| `pl` | Polish | Polski | LTR |
| `uk` | Ukrainian | Українська | LTR |
| `ar` | Arabic | العربية | RTL |
| `tr` | Turkish | Türkçe | LTR |
| `hi` | Hindi | हिन्दी | LTR |
| `vi` | Vietnamese | Tiếng Việt | LTR |
| `id` | Indonesian | Bahasa Indonesia | LTR |
| `th` | Thai | ไทย | LTR |
## 语言检测的工作原理 {#how-language-detection-works}
SnapOtter 采用三层解析顺序:
1. **用户偏好** - 存储在 `localStorage("snapotter-locale")` 中,并在用户已登录时同步到用户设置
2. **浏览器自动检测** - 遍历 `navigator.languages` 数组,采用 BCP 47 前缀匹配
3. **实例默认值** - 管理员的 `DEFAULT_LOCALE` 环境变量(从 `GET /api/v1/config/locale` 获取)
4. **英语回退** - 始终可用
用户可以通过以下方式更改语言:
- **页脚地球图标选择器**(桌面端,始终可见)
- **登录页**语言选择器(登录前)
- **设置 > 通用**部分(每位用户的偏好)
- **移动端侧边栏**语言下拉菜单
- **设置 > 系统**部分设置全实例范围的默认值(仅限管理员)
## 翻译的工作原理 {#how-translations-work}
所有 UI 字符串都位于 `packages/shared/src/i18n/` 中。参考文件是 `en.ts`,它导出一个带类型的对象,包含应用使用的每个字符串(约 1500 个键)。其他语言是导出相同结构的独立文件(例如 `de.ts`、`fr.ts`)。
`TranslationKeys` 类型使用 `DeepStringRecord` 来接受任意字符串值,同时强制约束键结构。TypeScript 会在编译时捕获任何翻译文件中缺失的键。
运行时仅通过动态 `import()` 加载当前激活的语言,从而保持主包体积精简。
## 在组件中使用翻译 {#using-translations-in-components}
```tsx
import { useTranslation } from "@/contexts/i18n-context";
import { format, plural } from "@/lib/format";
function MyComponent() {
const { t, locale, setLocale } = useTranslation();
return (
{t.common.settings}
{format(t.settings.people.deleteConfirm, { username: "admin" })}
{plural(count, t.automate.fileCount, t.automate.fileCountPlural)}
);
}
```
## 贡献翻译 {#contributing-a-translation}
我们欢迎直接提交翻译 PR。你可以改进现有语言,也可以添加新语言。
若要在不提交代码的情况下报告误译,请提交一个 [GitHub Issue](https://github.com/snapotter-hq/SnapOtter/issues),附上语言、错误的字符串以及建议的修正。
::: tip
翻译 PR 无需事先批准。Fork 仓库、进行修改并提交 PR 即可。完整的 PR 流程和 CLA 要求参见 [贡献指南](/zh-CN/guide/contributing)。
:::
## 如何创建或更新翻译 {#how-to-create-or-update-a-translation}
### 1. Fork 并克隆 {#_1-fork-and-clone}
```bash
git clone https://github.com//snapotter.git
cd snapotter
pnpm install
```
### 2. 复制参考文件(仅限新语言){#_2-copy-the-reference-file-new-language-only}
如果你是在改进现有翻译,请跳过此步骤。
```bash
cp packages/shared/src/i18n/en.ts packages/shared/src/i18n/XX.ts
```
### 3. 翻译字符串 {#_3-translate-the-strings}
打开新文件并翻译每个字符串值。保持对象结构和键完全不变。
```ts
import type { TranslationKeys } from "./en.js";
export const xx: TranslationKeys = {
common: {
upload: "Your translation here",
// ... translate all entries
},
// ... translate all sections
} as const;
```
规则:
- 不要翻译对象键,只翻译字符串值
- 将 `as const` 保留在末尾
- 从 `./en.js` 导入 `TranslationKeys` 并为你的导出添加类型
- 将 `{variable}` 占位符原样保留
- 数组(`rotatingPhrases`、`progressMessages`)必须有相同数量的条目
- 不要翻译:SnapOtter、JPEG、PNG、WebP、EXIF、API 及其他技术术语
### 4. 注册语言(仅限新语言){#_4-register-the-locale-new-language-only}
将你的语言添加到 `packages/shared/src/i18n/index.ts` 中的 `SUPPORTED_LOCALES`:
```ts
{ code: "xx", name: "Language Name", nativeName: "Native Name", dir: "ltr" },
```
### 5. 验证 {#_5-verify}
```bash
pnpm typecheck # catches missing or mistyped keys
pnpm lint # formatting check
pnpm dev # manually verify strings appear correctly
```
### 6. 提交 {#_6-submit}
针对 `main` 提交一个 PR,标题类似 `feat(i18n): add Swedish translation` 或 `fix(i18n): correct German typos`。CLA 机器人会在你首次贡献时要求你签署。
## 添加新的翻译键 {#adding-new-translation-keys}
当添加需要新 UI 字符串的新功能时:
1. 首先将新键添加到 `en.ts`(参考文件)
2. 运行 `pnpm typecheck` - 缺少新键的每个语言文件都会失败
3. 将新键添加到所有语言文件(使用英语作为临时回退)
## 配置 {#configuration}
通过环境变量设置实例默认语言:
```yaml
DEFAULT_LOCALE: "de" # German as the default for all new users
```
## 文件参考 {#file-reference}
| File | Purpose |
|------|---------|
| `packages/shared/src/i18n/en.ts` | 英语字符串(参考语言,约 1500 个键)|
| `packages/shared/src/i18n/index.ts` | `SUPPORTED_LOCALES`、`loadTranslations()`、类型导出 |
| `packages/shared/src/i18n/.ts` | 各语言翻译文件 |
| `apps/web/src/contexts/i18n-context.tsx` | `I18nProvider`、`useTranslation()` 钩子 |
| `apps/web/src/lib/format.ts` | `format()`、`plural()`、`formatFileSize()` 辅助函数 |
| `apps/api/src/routes/config.ts` | `GET /api/v1/config/locale` 公共端点 |
## 翻译网站、文档和 API 参考 {#translating-the-web-surfaces}
上述 21 种语言支持涵盖的是**应用**。公共网站(snapotter.com)、本文档站点以及 REST API 参考也都被翻译成全部 21 种语言,由一条独立的哈希门控流水线完成,它复用了来自 `packages/shared/src/i18n` 的相同工具名称和描述,因此术语在各处保持一致。
### 默认采用机器翻译 {#machine-translated-by-default}
网站和文档中的每个非英语页面在首轮都是**机器翻译**的(由 Claude Code 会话完成,而非第三方服务),并带有一条小巧、可关闭的横幅进行说明,附有返回本页的链接。这是有意为之:它能快速而诚实地交付全部 21 种语言,然后邀请社区去精修最重要的页面。机器翻译能传达含义;人工审阅让文字读起来更自然。
### 流水线如何决定翻译内容 {#how-the-web-pipeline-decides}
每个可翻译的英语源单元都会被哈希,哈希值与其翻译存放在一起。每次运行时,流水线会:
- 翻译任何尚无翻译的单元,
- 跳过任何存储哈希仍与英语源匹配的单元,
- 当**机器**单元的英语源发生变化时重新翻译它,
- 并在经过**人工**精修的单元其英语源发生变化时将其标记为 `stale`(需要审阅),而不是覆盖你的成果。
### 通过 PR 精修网页翻译 {#refining-a-web-translation-by-pr}
改进网站、文档或 API 参考翻译的方式,与改进应用语言相同:编辑生成的文件并提交 PR。
1. 找到你所用语言的生成翻译:
- 网站 UI 字符串:`apps/landing/src/i18n/.json`
- 文档页面:`apps/docs//**.md`
- API 参考:`apps/api/src/openapi..yaml`
2. 编辑文本。将代码、链接、`{placeholders}` 以及任何 `⸤I18N…⸥` 标记完全原样保留;流水线的验证器会拒绝丢弃或重新排序它们的翻译。
3. 提交 PR。编辑一个单元会将其来源从 `machine` 翻转为 `human`,因此流水线在后续运行时**绝不会覆盖它**。如果之后英语源发生变化,你的单元会被标记为 `stale` 以待审阅,而不是被悄悄替换。
若要在不提交代码的情况下报告误译,请提交一个 [GitHub Issue](https://github.com/snapotter-hq/SnapOtter/issues),附上页面 URL、语言、错误的文本以及你建议的修正。
::: tip
维护者会运行翻译流水线;你无需 API 密钥即可贡献。只需编辑生成的文件并提交 PR。流水线的运行方式参见 [`scripts/i18n/README.md`](https://github.com/snapotter-hq/SnapOtter/blob/main/scripts/i18n/README.md)。
:::