i18n —— 多语言(A 表 / B 表)
读者:做工具的用户,和替他干活的 AI。两者读同一份。
它是什么
包里有两类需要翻译的字符串,它们的写法完全不同,混用不会报错、只会静默不生效:
- A 表(内容文案):组件上、页面上你自己写的字,包括取数流里拼出来的成品句子。写在文件顶层一张
i18n表里,正文用${@i18n.key}引用。 - B 表(元数据):平台读走、平台自己显示的字 —— 包名、包简介、组件标题、路由标题。裸字段就是基准语言那一门,译文以
i18n旁挂在同一个对象上,正文里没有任何引用。
一句话判据:
宿主读来直接显示的字段一律 B 表;包内容自己经求值器引用的一律 A 表。
包不做多语言也能跑 —— 但发布时会被拦:商店门面(包名 / 简介)必须有中英双份。
A 表 / B 表对照
| A 表(内容文案) | B 表(元数据) | |
|---|---|---|
| 放哪 | 文件顶层 i18n(.rcn 是 rc.i18n) |
对象旁挂 i18n: {locale:{字段}},裸字段 = 基准语言 |
| 形状 | { locale: { key: 文案 } } 扁平两层 |
{ locale: { 字段名: 文案 } } |
| 引用 | 正文写 ${@i18n.key} |
无引用,宿主自己挑 |
| 载体 | .rcn .xpage .xform .xmenu .af .df |
manifest.json .xwidget router.json 的 routes[] .xjob |
| 空串的意思 | 合法译文 = 刻意为空,照渲空,不兜底 | 空串 = 缺,继续往下兜 |
| 兜底粒度 | 逐 key(某门漏一句只补那一句) | 逐字段 |
check |
G8 | G19(manifest,E)· G20(.xwidget,W)· G8 段内(router,W)· G47(.xjob) |
两张表对「空串」的规矩正好相反,这是刻意的。A 表里 zh-CN 写「天」、en-US 写 ""(英文不要那个量词后缀)是常态,当成缺就会渲出「Day 3天」;而 B 表的空标题从来没有意义,只能是漏了。
.xjob(提醒与后台任务)是 B 表里的特例:它的 message 要代入这一次取到的数据,所以译文里允许直接写 ${},而且引用集必须和基准语言逐字相同(G47);form 槽还能覆盖输入框提示文字与选项文字。完整写法见 numable docs alerts 步骤 5。
语言解析:fallback 链
要哪一门就先找哪一门,没有就找英语,再没有才找作者自己那门:
locale → 同语言段的其它门(按门名排序) → en-US → manifest.lang(缺省 zh-CN) → zh-CN → 表里其余门
- 只取表里实际存在的门;
zh-Hans-CN与zh-CN这类写法归一后匹配。 - A 表按这条链逐 key 铺层:某门缺一句就只补那一句,不会整张表退到别的语言。
- B 表把裸字段当作
manifest.lang那门,再走同一条链的前四段(精确 → 同语言段其它门 → en-US → 基准语言,不再兜到其余门)取第一个非空值。
manifest.lang 就是「基准语言」,缺省 zh-CN。它决定了所有裸字段属于哪一门,写错整包的兜底方向都会歪。
两个语言量
| 量 | 是什么 | 谁看得到 |
|---|---|---|
| 外壳语言 | App 界面自己的语言,只有中 / 英两档 | 平台的按钮、菜单、系统文案 |
| 内容 locale | 喂给包内容的语言,可以是任意 BCP-47(ja-JP、de-DE…) |
@app.language / @app.locale、A / B 表解析、H5 的 appInfo().language |
用户显式选了中文或 English 时两者同值。只有「跟随系统 + 系统既非中也非英」时才分叉:外壳退英文,而内容仍是 ja-JP —— 包里有日文表就渲日文,没有就按上面的链兜到英文。所以包可以支持任意语言门,不受外壳只有两档的限制。
包内容里读语言一律用 ${@app.language}。
取数流:按语言取数,也可以出文案
.df 里可以直接读 ${@app.language}(以及 @device.language / @time.locale)。接口本身分语言的数据(新闻标题、城市名、商店描述),直接把语言拼进请求:
{
"id": "news",
"action": "request",
"params": {
"url": "https://api.example.com/news?lang=${@app.language}",
"formatType": "json"
}
}
切换语言时,仪表盘会对每个包做一次 widget.refresh(清取数计时 + 真取数),按语言取的数据随之更新。切语言是低频操作,重新取一遍数可以接受。交互流(.af)同样可以读语言。
.df 与 .af 完全同构:.df 也可以有顶层 i18n 表,正文写 ${@i18n.key},取数流入参里注入的是宿主页表 ⊕ 本文件表(加 @app)。要在数据层出成品文案(「和昨天差不多」「今日在高低区间的 59%」)就直接在 .df 里出:
{
"version": 1,
"actions": [
{ "id": "resp", "action": "request", "params": { "url": "https://api.example.com/today?lang=${@app.language}", "formatType": "json" } },
{ "op": "set", "props": { "key": "_b", "value": "1" } },
{ "op": "set", "props": { "key": "delta", "value": "${resp.delta}" } },
{ "op": "set", "props": { "key": "summary", "value": "$[if::(gt::(${delta},0),${@i18n.up},${@i18n.flat})]" } },
{ "action": "resultFilter", "params": { "keys": ["delta", "summary"] } }
],
"i18n": {
"zh-CN": { "up": "比昨天高", "flat": "和昨天差不多" },
"en-US": { "up": "Higher than yesterday", "flat": "About the same as yesterday" }
}
}
一句话:任何 flow 文件都可以有顶层 i18n 表,文案写 ${@i18n.key},语言分支读 ${@app.language}。「今天」这类纯文案放取数层还是渲染层由你自决。check 对 .df 的表做与 .af 相同的 G8 检查。
两件事要知道:
- 数据缓存的键含语言。切完语言,每个组件在新语言下没有落盘数据:先出骨架,取到再显示;离线切语言就没有该语言的数据可显示(走空态 / 错误态)。这是有意取舍。
- 主题(明暗)与语言不同:它是纯渲染参数,切主题只重渲、不取数,数据缓存也不分明暗;颜色靠
浅|深双分支。
A 表在哪一层生效(scope 合并)
| 场景 | 能看见哪张 A 表 |
|---|---|
.rcn 里的 ${@i18n.key} |
页表 ⊕ 该 .rcn 的 rc.i18n(同名 key 以 .rcn 自己那份为准) |
.xpage 节点上的 ${@i18n.key}(params / props.text / menu[].label) |
页表(.xpage 顶层 i18n);.rcn 表的 key 在这里看不到 |
.af 事件流 |
宿主表 ⊕ 该 .af 的 i18n;.xwidget 的事件流没有宿主表,只有自己那份 |
.df 取数流 |
宿主页表 ⊕ 该 .df 的 i18n;.xwidget 的取数流没有宿主表,只有自己那份 |
.xform |
只有自己那张表 |
推论:画在组件和页面上的文案,住在各块 .rcn 的 rc.i18n 里最稳;取数层拼出的成品句子住 .df 自己的表。
A 表细则
- 扁平两层:
{ "zh-CN": { "title": "…" }, "en-US": { "title": "…" } }。不要套values外层(check会拦)。 - key 只用
[A-Za-z0-9_],值只能是字符串。 - key 不能动态拼:
${@i18n.k_${n}}求不出来。求不出的引用在 RCN / XPage 上原样保留字面串,屏幕上就会出现${@i18n.xxx}。 - 插值靠拆多个 key,不要在译文里放占位符再自己替换:
"${@i18n.pre}${n}${@i18n.post}"。 - 复数写
key_one/key_other,在正文里用$[if::(…)]挑。 - 门可以是任意合法 BCP-47;
zh-CN与en-US是必需门(加上manifest.lang那门),其余门缺 key 只出提醒。 - 刻意留空的译文在同级写一份声明,免得被当成漏译:
.rcn写在rc.i18nEmptyOk,其余文件写在顶层i18nEmptyOk,值是 key 数组。
最小双语示例(逐块)
manifest.json —— B 表
{
"lang": "zh-CN", // 基准语言:下面所有裸字段属于这一门
"title": "GitHub 示例",
"subtitle": "某个用户最近的公开动态",
"category": "developer", // 枚举 key,平台自带词表,不用翻
"i18n": {
"en-US": { "title": "GitHub Sample", "subtitle": "A user's recent public activity" }
}
}
可翻的字段只有 title / subtitle / category / description 四个。category 用枚举 key 就不必写译文。
.xwidget —— B 表
{
"version": 2,
"title": "个股",
"sub": "价格 · 两个月形状",
"i18n": { "en-US": { "title": "Stock", "sub": "Price and its 2-month shape" } },
"layout": 22
}
只认 title 与 sub 两个字段。
router.json —— B 表(旁挂在每条路由上)
{
"routes": [
{ "path": "/", "entry": "html/home/index.html" },
{ "path": "/detail", "entry": "html/detail/index.html",
"title": "个股详情",
"i18n": { "en-US": { "title": "Stock detail" } } }
]
}
不要在 router.json 顶层写词表,也不要在 title 里写 ${@i18n.key} —— 路由标题不求值,标题栏会直接露出模板串,check 一律报错。
.rcn —— A 表
{
"rc": {
"cells": [
{ "id": "t", "type": "txt", "text": "${@i18n.title}", "fontSize": "18pt",
"textColor": "#1A1A1A|#FFFFFF", "x": "12pt", "y": "16pt", "w": "-1", "h": "-1" },
{ "id": "u", "type": "txt", "text": "${temp}${@i18n.deg_post}", "fontSize": "13pt",
"textColor": "#6B7280|#9AA0A6", "x": "12pt", "y": "44pt", "w": "-1", "h": "-1" }
],
"i18n": {
"zh-CN": { "title": "全球速览", "deg_post": "度" },
"en-US": { "title": "World Markets", "deg_post": "" }
},
"i18nEmptyOk": ["deg_post"]
}
}
deg_post 在英文下刻意为空(英文不带那个后缀),i18nEmptyOk 声明这件事,免得被当成漏译。
.xpage —— A 表在顶层,文案落到各块 .rcn
{
"type": "page",
"id": "demo",
"i18n": { "zh-CN": { "empty": "还没有数据" }, "en-US": { "empty": "Nothing yet" } },
"root": { "…": "…" }
}
页表会与它引到的 .rcn / .af 的表合并。节点自己的属性里别写 ${@i18n.key} —— 见规则表。
.af —— A 表在顶层
{
"version": 1,
"i18n": { "zh-CN": { "done": "已保存" }, "en-US": { "done": "Saved" } },
"actions": [
{ "action": "ui.toast", "params": { "message": "${@i18n.done}", "type": "success" } }
]
}
.df 取数流的表与此同形,示例见上「取数流:按语言取数,也可以出文案」。
H5 页 —— 自带一张表
桥不提供翻译能力,html 页自己维护词表,语言从 data-lang 读:
<script>
const T = {
"zh-CN": { title: "我的记录", empty: "还没有记录" },
"en-US": { title: "My log", empty: "Nothing yet" }
};
const pick = () => T[document.documentElement.dataset.lang] || T["en-US"];
function render() {
document.getElementById("h").textContent = pick().title;
}
addEventListener("languagechange", render); // 切语言不重载页面,只派发事件
render();
</script>
也可以 const info = await xbridge.appInfo(); 读 info.data.language(必须 await;桥回的是信封,结果在 data)。详见 numable docs bridge。
规则(违反 = 返工)
| 规则 | 检查方式 | 违反时的现象 | 修法 |
|---|---|---|---|
A 表必须是扁平对象,不带 values 外层 |
check G8 |
引用求不出来,屏幕上出现 ${@i18n.xxx} 字面串 |
改成 {locale:{key:值}} |
正文引用的 key 在必需门(zh-CN / en-US / manifest.lang)里必须存在 |
check G8 |
那一门下屏幕上露出模板串 | 补 key;确属刻意为空的写 i18nEmptyOk |
manifest.lang 必须是合法 BCP-47 |
check G8 |
兜底链走歪,裸字段被归到错误的门 | 写 zh-CN / en-US 这类标准标签 |
router.json 不得有顶层 i18n 词表;routes[].title 不得引 ${@i18n.key} |
check G8 |
标题栏直接露出模板串 | 改成旁挂 routes[].i18n |
| 各门 key 集合应当一致;表里有、正文没引的 key 是多余的 | check G8(W) |
某个语言下少一句;或译文表里堆着早已删掉的 key | 补齐 / 删掉 |
routes[].title 要有非基准门的覆盖 |
check G8(W) |
英文环境下标题栏显示中文 | 补 routes[].i18n["en-US"].title |
| 用户可见的字段里不要写死中文 | check G8b(W) |
英文用户看到中文 | 提取成 ${@i18n.key} |
manifest.i18n["en-US"].title 与 .subtitle 必须有(基准语言本身是 en-* 的包除外) |
check G19 |
商店与安装面板在英文下显示中文 | 补上 |
manifest.i18n 的门必须是 BCP-47、每门必须是对象;可翻字段只有 title / subtitle / category / description |
check G21 |
写在白名单外的字段永远不会被读 | 按白名单写 |
| 各门的包名显示宽度 ≤ 24(全角算 2、半角算 1) | check G21 |
组件宫格里两行放不下,标题被截断 | 缩短 title(建议 ≤ 16) |
.xwidget.i18n 必须是对象,且有 en-US 的 title / sub 覆盖 |
check G20(W) |
英文环境下组件标题显示中文 | 补 B 表 |
XPage 节点层用到的 ${@i18n.key} 必须在 .xpage 顶层 i18n 表里有 |
check G35 |
缺 key 时那一处渲成空白 | 把 key 补进页表;画在 Canvas 里的文案仍住 .rcn 的 rc.i18n |
| A 表的空串是「刻意为空」,B 表的空串是「缺」 | 人审 | 写反了:A 表把没翻完的留空 → 英文下那句真的空着;B 表以为留空能兜底 → 兜到别的门 | A 表漏译就补全,刻意为空写进 i18nEmptyOk;B 表缺译文就删掉那个键,别写 "" |
出错怎么办
| 现象 | 最可能的原因 | 先做什么 |
|---|---|---|
屏幕上出现 ${@i18n.xxx} 字面串或空白 |
key 不在该载体读的表里(.rcn 读 rc 表、XPage 节点读页表)/ 表形状不对 |
跑 numable check 看 G8 / G35;把 key 补进对应的表 |
| 标题栏出现模板串 | routes[].title 写成了 ${@i18n.} |
改成旁挂 routes[].i18n |
| 英文下组件标题是中文 | .xwidget 缺 B 表 |
补 i18n["en-US"].title/sub,看 G20 |
| 英文下某句变成了空白 | A 表里那门写了 "" 而并非刻意 |
补译文;确属刻意就写 i18nEmptyOk |
| 切语言后组件先出骨架、过一会才出数据;离线时空态 | 数据缓存按语言分开,新语言下没有落盘数据,取到再显示;离线就没有该语言的数据可显示 | 预期行为;联网后下拉刷新 |
| 切语言后组件文案没变 | 文案写死在 .df 或 .rcn 里,没走 ${@i18n.key};或数据本来就不随语言变 |
顶层加 i18n 表、文案改 ${@i18n.key};要按语言取数读 ${@app.language} |
| 日文系统下看到的是中文 | 包只有 zh-CN 一门,而 manifest.lang 是 zh-CN |
补 en-US 门(链上英语在基准语言之前) |
相关
numable docs layout——manifest.json的全部字段与基准语言langnumable docs page——router.json的字段与三种页型numable docs rcn——.rcn里怎么写${@i18n.key}与双分支颜色