Skip to content

多语言:给人看的文字 ​

目标读者:写插件、契约包或语言包的开发者。本文写做法;用到的函数的签名与消息格式见 schema-i18n,服务与出口见 api-i18n,默认实现的规则见 plugin-i18n。

给人看的文字按读者的语言显示:插件交出文字对象(取自自己原文字典的一个键),出口按读者渲染。插件不判断读者是谁、用什么语言,也不自己取语言来源。

哪些要翻、哪些不翻 ​

要翻的是给人看的文字:

  • 声明里的显示名、配置项的 label 与说明、指令的说明、页面的标题与组件、动作的 label 与确认句、工具分组的称呼与概述;
  • 运行中产出的句子:指令的回复、动作的回执、提示与诊断结论、宿主通知给人看的那句;
  • 给人看的报错:激活失败的原因、指令与动作的失败、配置校验的问题。

不翻、照原文写的:

  • 给模型的:工具说明、参数说明与工具结果,提示词与注入的内容,宿主通知写给模型的正文;
  • 日志与诊断记录;
  • 协议占位与标识:存档里的占位、配置键、命令名、文件名,指令的 usage 与示例(用户照着敲的写法);
  • 用户数据与模型产出:用户写的话、会话标题、摘要、档案、记忆;
  • 配置项的默认值:写进配置文件的是数据。

句子里的技术名词、配置键、命令与代码片段原样,不按语言改写。

字典的布局 ​

src/texts.ts            原文字典:defineTexts({ ns: <包名>, language: <原文语言>, source, locales })
src/locales/<语言>.ts   一种语言的译文:defineLocale<Source>()({ … })
  • ns 写包名。插件前端的字典由宿主按插件名取,ns 与包名不同时 frame.t 一律回键名。
  • 字典要由服务端入口导入:src/index.ts 里 import { t } from './texts.js'。字典在服务进程里登记进目录,宿主才取得到;插件前端的 frame.t 也靠它(见 api-webui 的「助手」)。
  • 原文语言写自己写原文的语言;第一方的原文是 zh-CN,带 en 译文,推荐的语言标签与第一方一致(en、zh-CN)。
  • 单份字典控制在几百键以内:defineLocale 逐条核参数,上千键的文件在编辑器里诊断会慢。大的按区域拆成几份同一 ns 的 defineTexts,目录按键合并。

声明里用 t() ​

收文字的字段写 t('键'),不写字面量:

ts
import { t } from './texts.js';

export default definePlugin({
  name: '@your-scope/plugin-x',
  displayName: t('displayName'),
  configSchema: defineConfig({
    greeting: { type: 'string', label: t('greetingLabel'), description: t('greetingDescription'), default: '<问候语>' },
  }),
  // …
});

契约里写成 string | TextRef 或 unknown 的展示字段都收文字对象:schema-config 的 label、description 与可选项的 label,指令与参数的说明,页面声明里给人看的字段,提示的 title 与 detail,诊断结论,provide 的 label 等。写普通字符串也可以,那就是不翻译的原文。

插件的显示名与 provide 的 label 在 core 里是展示值:core 0.21 起才收 unknown,0.20 只收字符串。用文字对象时,core 的 peer 下限写 >=0.21.0 <1.0.0。

运行中的句子 ​

  • 一句一个键,写整句,参数放变化的部分:t('saved', { count }),不拼「已保存」+ 数字 + 「项」。
  • 条件片段拆成独立的句子各占一个键,不按条件拼前后半句:别的语言里语序不同。
  • 数量用 {n, plural, …} 或 {n, number},按读者的语言写复数与千分位;时间用 date、time、duration,大小用 size。
  • 罗列用 {items, list}(或 list, or),多行用 {items, lines} 或 textLines,并列的几条原因用 textClauses。
  • 参数里可以放别的文字对象,它与外层按同一条语言链渲染。
  • 参数要放进代码段时用 {x, code},不在模板里写 `{x}`;字面的花括号与撇号按引号规则写。

写法与边界见 schema-i18n 的「消息格式」。

报错 ​

  • 给人看的报错抛 new TextError(t('…')):message 是原文,日志照旧可读,出口按读者语言显示;HTTP 出口的状态码写在 status,下层的错误写在 cause。
  • 已有的错误类(按 name 判定的,如 schema-config 的 ConfigError)用 withText(err, t('…')) 挂上文字对象,类与 name 不变。
  • 接住的报错要写进句子时用 textOf(err) 作参数:有文字对象取它,否则取 message。不要取 .message 拼句。
  • 判断一个错误有没有带文字对象用 isTextError,不用 instanceof:进程里装了两份 schema-i18n 时,两份的类互不认得。

一份内容两个读者 ​

同一件事既要告诉模型、又要给人看时,拆成两样:

  • 消息与通知:content 写给模型(原文,可以带处理办法与工具用法),text 写给人看的那句文字对象。schema-message 的 OutgoingMessage.text、IncomingMessage.text、宿主通知的 hostNotice.text、存档的 ArchiveNoticeOptions.text 都是这样分的。
  • 共用的回执:产出文字对象,给人看的一路交给出口,工具一路 toSource 后再交给模型。

交给模型的文字对象一律取 toSource,工具结果与注入的内容都是。

落盘的文字对象 ​

文字对象是纯数据,可以原样落盘;旧记录里的字符串照旧当原文显示。

  • 落盘前给参数封顶:truncateParams(text, 200),参数里可能带着上游整段报错。第一方一律截到 200 字;返回值可能退成字符串,存它的字段本来就收 string | TextRef。
  • 读回时校验要认 isTextRef:形状对的文字对象与字符串都收下。
  • 落盘的记录再交给模型前取 toSource。

出口 ​

把文字交给人看的地方(发到平台、写进页面响应)用 api-i18n 的 localizeFor 按读者渲染,日志与交给模型的用 toSource。多数插件不是出口:把文字对象交给收它的字段就够了。

自检与键的约定 ​

单测里一行:

ts
import { checkTexts } from '@aalis/schema-i18n';
import { t } from '../src/texts.js';

expect(checkTexts(t)).toEqual([]);

它查原文与译文能不能解析、参数对不对得上、引号与反引号的写法、各语言缺不缺键。

键一旦落盘就是约定:读回的文字对象按当时字典里这个键渲染。意思或参数变了换新键,删掉的键不复用;改了原文的措辞要同步改译文,语言包里对应的条目也要重译(见下一节)。

写语言包 ​

语言包给别人的字典补它们没有的语言,是一个普通插件,经贡献点 i18n:pack 交条目:

ts
import { contributions } from '@aalis/api-contributions';
import type {} from '@aalis/api-i18n'; // 带上 i18n:pack 的类型
import { fingerprint } from '@aalis/schema-i18n';

// uses: { contributions }
contributions.use.contribute('i18n:pack', {
  id: 'ja',
  language: 'ja',
  entries: {
    '@your-scope/plugin-x': { title: { text: 'チェックポイントを戻す', from: fingerprint('回滚检查点') } },
  },
});
  • 一包一种语言,提供哪种语言以贡献里的 language 为准。
  • from 是翻译时原文模板的指纹(fingerprint 算出)。只补缺、指纹不符与几个包给同一条时的取舍见 plugin-i18n 的「渲染与语言包」。
  • 罗列的分隔符也要补:{x, list} 用 @aalis/schema-i18n 字典的 listSeparator 键,只带 zh-CN 与 en。日文要用「、」就在 entries 里加 '@aalis/schema-i18n': { listSeparator: { text: '、', from: fingerprint('、') } }。
  • 骨架从 WebUI 的「翻译」页导出(plugin-i18n 的「翻译页」):选中一个字典与语言,「待译骨架:语言包的 entries」一栏只列字典没有自带译文的键,缺的与过时的先填了原文,from 已算好。

命名与关键词:

  • 包名:官方 @aalis/plugin-lang-<小写标签>,第三方 aalis-plugin-lang-<标签> 或 @scope/aalis-plugin-lang-<标签>。
  • keywords 带三个:aalis-plugin(加载器只认它)、aalis-language-pack(标明这是语言包)、aalis-lang-<小写规范标签>(按语言找,写这个包覆盖的最泛的一级,如 ja、zh-tw)。
  • 找语言包只看关键词、不解析包名:先按完整标签找,再按基础语言(ja-JP 先找 aalis-lang-ja-jp,再找 aalis-lang-ja)。npm create aalis 按这个顺序找,plugin-i18n 的诊断也写这两个关键词。

读别的插件交来的文字 ​

别的插件与契约交来的展示字段可能是文字对象。读法四条:

  1. 给人看的:交给收文字对象的字段(消息的 text、页面声明、提示),或自己是出口时用 localizeFor 按读者渲染。
  2. 给模型与写日志的:toSource。
  3. 只判有没有的:照真假判,文字对象是真值。
  4. 不要拼接,也不要 String() 或模板串插值:文字对象拼进字符串只会得到 [object Object]。

契约里写成 string | TextRef 或 unknown 的展示字段都照此读,具体字段看各包的契约页。下表只列类型看不出读法的:

来源字段读法
api-authorityAuthorityService.authorize 的返回string | TextRef | null:null 放行,其余是拒绝原因(authority)
schema-messageOutboundDeliveryResult.errorstring | TextRef;OneBot 的投递失败原因是文字对象,交给模型取 toSource
api-commandsCommand 的说明(含位置参数、选项与取值的说明)string | TextRef:给人看用 localizeFor,给模型或日志用 toSource。CommandService.execute 回的是字符串,已按 ctx 的会话渲染
util-cron、api-cron-engineValidateResult失败分支是 { ok: false; text },没有 reason;要抛给人看时,文字对象用 TextError
api-personagetPersonaName卡没写名字时回空串:点名检测跳过空串,要显示名字的一方自己兜底(persona)
api-session-protocoltool-call 条目生成了却没执行的带 notExecuted、没有 result,客户端据此写出没有执行(api-session-protocol)
存档元数据 Text实现存档的一方要把 IncomingMessage.text 记进元数据 Text;读历史的一方要认出带 Text 的 user 角色消息是平台事件合成的入站,画成通知,不是谁的发言(message-archive)
plugin-schedulerSchedulerService.getJobs() 的 lastResultstring | TextRef,还没执行过时是空串
api-doctorUnmetRoot.error(经 unmetDependencies 交出)string | TextRef(doctor)
api-webuithemeContrastIssues 返回的 ContrastIssue.pair文字对象,不是字符串
schema-configSchemaIssue.message仍是字符串(原文);给人看取 text
会话协议与 WebUI REST交给前端的字段已渲染好的字符串。按类型读线上形状用 Localized<…> 与 api-webui 的 WebuiRestResponse<K>
第一方插件抛出的给人看的报错TextError:message 是原文,取 .message 或 textOf;String(err) 的前缀是 TextError:

脚手架生成的样子 ​

create-aalis-plugin 生成的插件已按本文写好:src/texts.ts 与另一种语言的 src/locales/<语言>.ts,声明里用 t(),给模型的保持字符串并注明,npm test 跑字典自检,插件前端经 frame.t 显示。见 脚手架上手指南 的「生成的 src/index.ts 形状」与「页面用自带前端」两节。

文档对应 dev@c4e046e41,core 0.21.0,构建于 2026-11-02
基于 MIT 许可发布