Skip to content

多语言 ​

启动器的界面语言是实时可变的——用户在设置里切换,你的插件下次读取时就是新值。内容源可以用它把站点标签显示成用户的语言。

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

接下来:设置与存储。

Celestial Launcher 基于 Modrinth App 构建,遵循其开源许可。