Numable 创作文档同一份内容也在终端里:`numable docs <主题>`English

i18n —— 多语言(A 表 / B 表)

读者:做工具的用户,和替他干活的 AI。两者读同一份。

它是什么

包里有两类需要翻译的字符串,它们的写法完全不同,混用不会报错、只会静默不生效:

一句话判据:

宿主读来直接显示的字段一律 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 → 表里其余门

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 表细则

最小双语示例(逐块)

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 门(链上英语在基准语言之前)

相关