Languages (i18n)
How a bundled plugin provides Chinese and English. Manifest strings get English versions in `i18n.en`, screen and widget text goes in a `defineStrings` table, dates go through the SDK formatters, and text for the model stays untranslated.
The app is available in Simplified Chinese (zh-Hans) and English (en). Chinese is the source
language: you write every string in Chinese first and then add the English version. Users choose
跟随系统 (follow the system), 简体中文 or English in Settings.
Every bundled plugin MUST be complete in both languages. The test plugins/i18n.test.ts in the
plugins repository checks each plugin and fails when an English string is missing. This page covers
bundled plugins. A plugin document puts all its English text in the i18n.en section of its
manifest (see Declarative plugins).
What gets translated
Text that people read is translated. Text that only the model reads, such as tool descriptions and instructions, is written once and not translated.
| Text | Where | Translate? |
|---|---|---|
Plugin name, description | definePlugin (Chinese) + i18n.en | Yes |
Tool titles | tools.<name>.title + i18n.en.tools.<name>.title | Yes |
Tool summary (plugin documents only) | summary in the tool file + i18n.en.tools.<name>.summary | Yes |
| Widget titles and descriptions | contributes.widgets + i18n.en.widgets | Yes |
| Screen titles | contributes.screens + i18n.en.screens | Yes |
| Settings rows | contributes.settings + i18n.en.settings.<screen> | Yes |
| Commands | contributes.commands + i18n.en.commands.<name> (description, argHint, aliases, prompt) | Yes |
| Data source titles | dataSources + i18n.en.dataSources | Yes |
| Provider names, hints, model names | providers + i18n.en.providers | Yes |
| Screen and widget text | src/strings.ts (defineStrings) | Yes |
Tool description, instructions, context, tool results | For the model | No |
Manifest overlay
The Chinese strings stay where they are in definePlugin. The English versions go in i18n.en.
This example is from the shuttle plugin:
definePlugin({
id: 'shuttle',
name: '校车',
description: '燕园—新燕园(昌平)通勤班车和新燕园—地铁北安河快速公交时刻表,算出下一班车。',
i18n: {
en: {
name: 'Shuttle bus',
description: 'Timetables for the Yanyuan–Xinyanyuan (Changping) commuter shuttle and the Xinyanyuan–Bei’anhe metro express bus, with the next departure worked out for you.',
tools: { next: { title: 'Next shuttle' }, timetable: { title: 'Shuttle timetable' } },
widgets: { next: { title: 'Next shuttle', description: '…' } },
commands: { shuttle: { description: 'Next shuttle (works offline)', argHint: 'e.g. Yanyuan to Xinyanyuan', aliases: ['bus'] } },
dataSources: { next: { title: 'Next shuttle' } },
},
},
// …
});At runtime, anything missing from en falls back to Chinese. The coverage test still fails, so fill
in everything.
String tables
Text inside screens and widgets lives in a string table, one per plugin, in src/strings.ts. You
define its keys once as a type, then give a value for each language:
import { defineStrings } from '@pku-harness/sdk';
type Strings = {
none: string;
later: (list: string) => string;
};
export const strings = defineStrings<Strings>({
'zh-Hans': { none: '今明两天没有班次', later: (list) => `之后 ${list}` },
en: { none: 'No buses today or tomorrow', later: (list) => `Then ${list}` },
});const s = strings[host.useLocale()];
<Text>{s.none}</Text>In a component, pick the table for the user's language with host.useLocale().
defineStrings type-checks en against zh-Hans, so both need the same keys with the same value
types. A string with parameters is a function. Never build a sentence by joining translated
fragments, because word order differs between the two languages.
Dates and numbers
Hermes, the JavaScript engine the app runs on, ignores Intl locales. toLocaleDateString and
similar functions therefore give the wrong format. Use the SDK formatters instead. Most of them take
the locale l from host.useLocale():
| Function | zh-Hans | en |
|---|---|---|
formatClock(d) | 21:04 | 21:04 |
formatWeekday(d, l) | 周一 | Mon |
formatLocalDate(d, l) | 10月18日 | Oct 18 |
formatLocalDateTime(d, l) | 10月18日 21:04 | Oct 18, 21:04 |
formatRelative(ms, l) | 刚刚, 3 分钟前, 昨天 21:04 | Just now, 3 min ago, Yesterday 21:04 |
formatDay(d, l) | 今天, 明天, 周三 | Today, Tomorrow, Wed |
formatCountdown(ms, l) | 2小时10分 | 2h 10m |
plural(n, one) | — | 1 book, 3 books |
Details in SDK → format.
English copy rules
The coverage test also checks how English strings are written:
- No Chinese except inside a parenthesised gloss or quotes.
Treehole (树洞)is fine. - Curly apostrophes (
’), not straight ones ('). - No exclamation marks.
- Keep it short and in sentence case, which capitalises only the first word and proper nouns. Campus proper nouns use the official English name or pinyin: Peking University, Treehole (树洞), Course (教学网 / Blackboard), Campus Card.
Chinese text follows the same idea: keep it short and use no exclamation marks.
UI contributions
How a bundled plugin adds screens, Settings rows, tab content, dashboard widgets and slash commands through `contributes`, using the components the app passes in as HostUI.
Testing
How to test each kind of plugin. Replay recorded responses for plugin documents, unit-test parsers and pure logic with bun test, run the assistant end to end with the scripted mock model, and try remote plugins with the demo-canteen example.