多语言
启动器的界面语言是实时可变的——用户在设置里切换,你的插件下次读取时就是新值。内容源可以用它把站点标签显示成用户的语言。
ts
const isChinese = api.i18n.locale.toLowerCase().startsWith('zh')能翻译什么
只有显示给用户看的东西:
| 可以翻译 | 不能翻译 |
|---|---|
filters() 里选项的 label | 选项的 id |
手动条目的字段 label(在 manifest 里,见下) | 字段的 key |
| 你自己在详情页正文里拼的文案 | 项目名、版本号、文件名 |
翻译 id 会让功能失灵
筛选用的 id 绝对不能翻译。 它是回传给你做筛选的值:
ts
// ✅ id 稳定,label 翻译
{ id: '4780', label: isChinese ? '数据包' : 'Data Pack' }
// ❌ id 也被翻译了 —— 用户勾选回传 '数据包',你的 API 不认识它,筛选直接失效
{ id: isChinese ? '数据包' : 'Data Pack', label: isChinese ? '数据包' : 'Data Pack' }基本用法
ts
const CATEGORY_ZH: Record<string, string> = {
'4780': '数据包',
'12': '科技',
'422': '魔法',
}
async filters(contentType: ContentType): Promise<SourceFilterGroup[]> {
const categories = await get<any>('/categories')
const isChinese = api.i18n.locale.toLowerCase().startsWith('zh')
return [
{
id: 'category',
header: 'categories',
options: categories.map((category: any) => ({
id: String(category.id), // 永远稳定
label: isChinese
? (CATEGORY_ZH[category.id] ?? category.name) // 有译文用译文,没有用原文
: category.name,
})),
},
]
}api.i18n.locale 是什么
启动器当前界面语言,一个 BCP 47 字符串,比如:
| 值 | 语言 |
|---|---|
zh-CN | 简体中文 |
zh-TW | 繁体中文 |
en-US | 英语 |
ja-JP | 日语 |
用 startsWith 而不是全等
语言标签可能带地区后缀(zh-CN、zh-TW、en-US)。判断中文用:
ts
api.i18n.locale.toLowerCase().startsWith('zh')这样 zh-CN 和 zh-TW 都能命中。如果你要区分简繁,再单独判断 zh-TW / zh-HK。
它是实时的
api.i18n.locale 是读取时求值的,不是一个常量。用户切换语言后,你下次读就是新值。
这意味着:
- 不要在
activate()里读一次存起来——那样切换语言后不会更新。 - 在每次调用
filters()时读,因为filters()会在用户操作时重新调用。
ts
// ❌ 在 activate 里读一次
export async function activate(api) {
const isChinese = api.i18n.locale.startsWith('zh') // 只读这一次
api.register({
async filters() {
return [{ options: cats.map((c) => ({ id: c.id, label: isChinese ? ... : ... })) }]
},
})
}
// ✅ 每次调用时读
api.register({
async filters() {
const isChinese = api.i18n.locale.toLowerCase().startsWith('zh') // 每次都是新的
return [{ options: cats.map((c) => ({ id: c.id, label: isChinese ? ... : ... })) }]
},
})翻译表放哪
官方的 CurseForge 源把中文翻译表放在插件源码里的一个常量:
ts
// src/i18n.ts
export const CATEGORY_ZH: Record<string, string> = {
'4069': '世界生成',
'4068': '生物',
// ...
}翻译表长的话单独一个文件更清楚。你也可以只翻常用的那些——没有译文的回落到站点原文即可(CATEGORY_ZH[id] ?? original)。
正文的翻译
详情页正文不归你管。启动器有一个内容翻译功能,用户可以打开它,把详情页的正文和更新日志自动翻译。
你不需要做任何事——正文按 Markdown 渲染后,启动器的翻译层会处理它。
manifest 里的文案不能翻译
manifest 是静态 JSON,name、description、settings[].label、manual_entry[].label 这些没有 i18n 机制——写什么就显示什么。
所以:
- manifest 里的文案,写用户群最通用的语言(你的源如果面向中文用户,就写中文)。
- 需要跟随语言变化的东西,只能通过
filters()之类在运行时返回。
官方的做法
官方四个内容源的 manifest 文案都是中文(面向中文用户),而 CurseForge 源的分类标签用运行时翻译——因为分类是浏览时才知道的,而且是给用户筛选用的。
完整例子
ts
const CATEGORY_ZH: Record<string, string> = { '4780': '数据包', '12': '科技' }
api.register({
async filters(contentType: ContentType): Promise<SourceFilterGroup[]> {
if (contentType !== 'mod') return []
const isChinese = api.i18n.locale.toLowerCase().startsWith('zh')
const categories = await get<any[]>('/categories')
return [
{
id: 'category',
header: 'categories',
options: categories.map((category) => ({
id: String(category.id),
label: isChinese ? (CATEGORY_ZH[String(category.id)] ?? category.name) : category.name,
})),
},
]
},
})常见错误
| 现象 | 原因 |
|---|---|
| 切换语言后标签不变 | 在 activate() 里读了一次 locale 存起来了 |
| 筛选失灵 | 把 id 也翻译了 |
| 繁体用户看到简体 | 只判断了 zh-CN,应该用 startsWith('zh') |
| manifest 里的描述不跟着变 | manifest 是静态的,没有 i18n |
接下来:设置与存储。