GEML 图解 · profile · geml-translator/v1(GEP-0010,草案;已注册) · English

GEML 图解 · geml-translator

一份译文是源文档沿语言轴的投影,和 .md 沿格式轴的投影是同一个形状:译文文件里只有 embed,每个用 translate-to= 说要哪种语言,处理器把源的散文投影过去,其余字节一个不动。id、顺序、非散文内容只有一个家,所以漂移不是被检测出来,而是不可能发生。这一页把 GEP-0010 的规则、profile 放行的那一个键、翻译器必须保留的东西、以及参考实现今天能做到哪一步,全部列出来。

看板

10 条规则,各自的出处和状态

GEP 草案 GEP-0010 定义,profile 已在注册表里,规范未收。实测 工具今天这样做。文档缺口 该有而没有的文档。

GEP 草案 7实测 2文档缺口 1
规则出处状态
1译文是沿语言轴的投影,复用 GEP-0006 沿格式轴的形状。译文文件只含 embed,别的什么都没有;块 id、顺序、非散文字节只有一个家,漂移不可能。GEP-0010 SummaryGEP 草案
2profile geml-translator/v1 只放行一个属性键:embed 上的 translate-to。§8.6.1 允许 profile 放行属性键,所以不需要改规范。profiles.ts · §8.6GEP 草案
3translate-to 一个键两个位置:meta 上是文档默认,embed 上覆盖;none 保留一块不译(「不写」是继承,不是「不译」,所以要有自己的拼法)。不叫 lang:code {lang=} 说正文已经是什么,这里说要做什么。GEP-0010GEP 草案
4translator= 保留、不发货:只有一个引擎时选引擎的键会「解析、无事发生、看着像支持」。处理器怎么拿到翻译器是实现定义,拿不到也合规。GEP-0010GEP 草案
5前提:embed 必须铺满源,从 meta 结束到文件尾。实测过:GEML-spec.geml 的 16 个 ## 节零缝隙铺满 L5–L1389,52 行的译文投影出 1349 行 Markdown,与源 1345 行逐字节相同。第一个标题前的段落不属于任何可寻址单元,会静默丢失;geml list 的行范围可机械证明覆盖,缝隙是待定义的诊断。GEP-0010GEP 草案
6翻译器必须保留:verbatim 行内原子(code span、行内数学);每个引用及其目标(标签可译,目标不可);每个 id、class、属性键,和命名事物的属性值;块结构。不许部分输出:失败、超时、不可用就用源语言。GEP-0010GEP 草案
7整块一次翻译,不是逐文本节点:实测 MANIFESTO.geml 133 次调用里 57 个字符串短于 25 字符,35% 的散文块被拆碎送出。用占位符替换不可动的 span,三条性质:目标语言里惰性、允许移动、恢复必须校验(每个占位符回来恰一次),否则整块回退源语言。GEP-0010GEP 草案
8词表:meta glossary = "#id" 指向一张 hidden 表,由投影层应用而不是问引擎;表在译文里不在源里,因为定下的译法是译文的属性。三条现有规则完成全部工作:hidden 标志、meta 不装表所以用引用、表是普通块。GEP-0010GEP 草案
9参考实现:CLI 的 --to md/html 没有翻译器,输出源文并加 note「translate-to 未应用」;真正翻译在 viewer 里由浏览器内置 Translator 完成(translate-browser.js);解析器侧 translate.ts 提供 resolveTarget / translateInlines / translateBlocks / glossaryFrom。声明 profile 的文档 0 诊断,未声明 → unknown-attribute warning。geml-parser/src · viewer实测
10文档缺口:spec/profiles/ 下没有 geml-translator 目录,profiles README 索引表也没列它,只有 profiles.ts 里的注册和 GEP-0010 正文。README 说「索引表和注册表是同一张表说了两遍」,此处已不成立。spec/profiles文档缺口
译文

一份译文只有 embed

左边是完整的译文文件;右边是 check 结果和没有 profile 时的对照。

meta 默认 · embed 覆盖 · none 保留 GEP 草案 check 实测
tr.geml
=== meta
title = "translated"
profile = "geml-translator/v1"
translate-to = "zh"                      %% 文档默认
===

=== embed {src=a.geml#budget}               %% 继承 zh
===
=== embed {src=a.geml#tbl translate-to=none}  %% 表不译
===

%% 没有别的东西:id、顺序、表格、代码都住在 a.geml 里
GEP 里的原型
=== meta
title    = "发布"
profile  = "geml-translator/v1"
lang     = "zh-cn"
source   = "PUBLISHING.geml"
===
=== embed {src=PUBLISHING.geml#topology translate-to=zh-cn}
===
=== embed {src=PUBLISHING.geml#prereq translate-to=zh-cn}
===
geml check --root .
tr.geml
ok: no diagnostics

tr-noprofile.geml(同样的 embed,没有 profile 行)
warning: unknown attribute `translate-to` for block type `embed` (line 4)

geml tr.geml --to md(CLI 导出)
note: `translate-to=zh` was not applied: this export has no translator,
      so the source text stands
位置translate-to 的意思
meta文档默认:没写的 embed 继承它
embed覆盖默认
embed,值 none这一块不译。「不写」是继承,所以「不译」需要自己的拼法
translator=保留。一个引擎时不该写;auto 同理,在只有一个成员的集合里选
为什么不叫 lang
code {lang=} 命名编程语言,是关于正文是什么的陈述;这里命名自然语言,是关于要对正文做什么的指令。两个值空间、两种词性,在一个「名字只有一个意思」的格式里不能共用一个键。translate-to 是动词,读不成另外两个。
放行
§8.6:profile 只放行名字。声明了且处理器认识,键不再报 warning;处理器怎么拿到翻译器(浏览器内置、OS 服务、MCP 工具、shell 出去的 CLI)是实现定义的,一个也拿不到仍然合规。
CLI 今天
参考实现的 CLI 导出没有翻译器,所以 --to md 输出源文并留一条 note 说明没有应用;真正的翻译在 viewer 里由浏览器 Translator 做。文档不会假装被翻译了。
前提

embed 必须铺满源

这不是「embed 能用」,而是「被嵌的 id 必须从 meta 结束覆盖到文件尾」。可以机械检查。

GEP 实测:16 节铺满 1385 行;一段落在标题前就静默丢失 GEP 草案
GEML-spec.geml · 16 个 ## 节零缝隙
灰格:L1–4,源的 H1 和「English | 中文」切换行,正好是译文自己要写的两样东西。蓝格:16 个 ## 节,L5–L1389,零缝隙。52 行译文投影出 1349 行 Markdown,与源从第一节起 1345 行逐字节相同,嵌套嵌入也一样。
反例:第一个标题前有一段
红格:meta 之后、第一个标题之前的段落,geml list 报 meta 在 L1–3、标题从 L7 起,中间的行不属于任何单元。两个标题都嵌了,译文 ok: no diagnostics,投影里就是没有那一段。没人警告,因为没人被要求。
规则
要求为什么
被嵌的 id 从 meta 结束铺到文件尾embed 只投影可寻址单元;不可寻址的散文投不过去、也翻不了
可用 geml list 的行范围证明覆盖缝隙是一条等待定义的诊断,不是设计缺陷
源里没人能寻址的散文,包进 text 块§3 已经给了它一个家;这条提案只是让不用的代价可见
命名标题的 embed 取整节裸段落、嵌套块一起来,不只标题行(见第 5 页)
压力在哪
GEP:这对源文档施加了温和的压力,而压力正该在那里——没人能寻址的散文,也没人能嵌入、翻译或块编辑它。
保留

翻译器必须原样保留什么,以及怎么送整块

规范说不了怎么翻译,只能说什么必须活下来。§4 的插值早有先例:code span 和行内数学里不替换。

四类不动的东西,一条不许半截的规则,一个占位符协议 GEP 草案
一句源文送进翻译器之前

Run geml check before you [publish](#pub); see §8 and $x^2$.

整块一次送出,不可动的 span 换成占位符:

Run ⟦1⟧ before you ⟦2⟧; see §8 and ⟦3⟧.

翻译器可以移动占位符(目标语言把它放它该在的地方),回来后每个占位符必须恰好出现一次,再还原成 code span、链接、数学。少一个、多一个、坏一个,整块回退源语言。
必须保留
什么规则
verbatim 行内原子code span、行内数学:绕着译,不穿过去
引用及其目标[[#id]]、[t](#id)、[^fn]、链接 href:标签可译,目标不可,否则 §8.2(5) 把译文变成构建错误
id、class、属性键以及命名事物而非陈述的属性值:format=、src=、translate-to=
块结构同样的块、同样的顺序、同样的 id
不许部分输出:失败、超时、不可用 → 该块用源语言。半句译文比不译更糟。
为什么整块
参考实现曾按行内 text 节点逐个送:MANIFESTO.geml 133 次调用 9798 字符,57 个字符串短于 25 字符," / " 送了四次,34 个散文块里 12 个(35%)被拆成碎片,最多七片。拼回去的结果一眼可见:全角括号一片开、半角一片关。拆分保证了原子不动,代价是从不送出一个完整句子。
占位符三性质
在任何目标语言里惰性;允许被移动;恢复必须校验。校验失败就走「不许部分输出」那条:句子里有一个洞,正是那条已经禁止的东西。
词表

词表钉住引擎每次都会重新决定的术语

结构不会漂,词汇会。翻译器按块调用、不记前事,一个术语出现八次就被决定八次。

meta 引用一张 hidden 表,投影层应用 GEP 草案
GEML · 译文文件
=== meta
profile      = "geml-translator/v1"
translate-to = "zh-cn"
source       = "MANIFESTO.geml"
glossary     = "#not-translated-terms"    %% meta 装不了表,所以是引用
===

=== table {#not-translated-terms hidden}
| term | zh-cn |
|---|---|
| Doc-as-a-Base | 文档即真相之源 |
| Single Source of Truth | 单一事实来源 |
===

=== embed {src=MANIFESTO.geml}
===
三条现有规则
hidden§4 给「进模型不显示的结构化内容」定义的标志;渲染器必须省略它。实测:这份文档 check 干净,Markdown 投影里没有那张表
glossary = "#id"§4:meta 不支持数组、日期、嵌套表,所以 meta 键是引用,表是普通块——数据源在这个语言里本来就是这个形状
在译文里定下的译法是译文的属性,源不知道也不该在意读者的语言争论过哪个词。译文仍是一个文件
依据

探针与出处

check 用仓库内当前构建,1.9.2;铺满和拆分的数字来自 GEP-0010 正文自己的实测。

探针 / 来源结果对应
tr.geml(有 profile)0 诊断译文、看板 2、9
tr-noprofile.geml1 warning unknown attribute translate-to看板 2
geml cli.ts:830–848导出无翻译器时输出源文并加 note看板 9
GEP-0010 · The prerequisite16 节铺满 L5–L1389,1345 行逐字节相同;标题前段落静默丢失铺满源
GEP-0010 · Translate a block133 次调用、57 个短串、35% 块被拆保留
ls spec/profiles无 geml-translator 目录;profiles README 无索引行看板 10