moonfluent

    Fluent FTL syntax, message resolution and JavaScript Intl localization in MoonBit

    fluent
    ftl
    localization
    parser
    runtime
    i18n
    Download zip
    Version
    0.2.1
    License
    Apache-2.0
    Last updated
    4 days ago
    Downloads
    37

    No dependencies

    #MoonFluent

    MoonFluent 是 MoonBit 的 Fluent FTL 语法与本地化运行时库,面向应用界面、通知/账单和批量报表,也保留编辑器、翻译维护及构建工具能力。它将 FTL 文本解析为带源位置的类型化 AST,在输入损坏时保留 Junk 和诊断,并提供规范化序列化、源码坐标映射、结构遍历、资源内符号索引与重命名。

    0.2.0 补齐应用运行时闭环:多资源 bundle、变量与术语作用域、消息/属性引用、选择表达式、NUMBER/DATETIME、自定义函数及恢复诊断。JavaScript 后端借助真实 Intl 格式化数字、日期并选择复数;可移植求值核心允许显式注入后端。参见完整语义范围与验收矩阵,不将非 JS 的核心测试冒充全目标 Intl 支持。

    #安装与导入

    在使用本库的 MoonBit 项目中添加指定版本依赖:

    moon add chenliyi-cly/moonfluent@0.2.1

    版本固定为 0.2.1。也可在源码仓库根目录直接运行示例和测试,不需要先安装已发布包。

    在调用方的可执行包 moon.pkg 中导入根包:

    import {
    "chenliyi-cly/moonfluent" @ftl,
    }

    pkgtype(kind: "executable")

    编译器要求 moonc >= 0.10.16。本项目 CI 使用 scripts/toolchain-lock.json 固定官方工具链和下载摘要,本次本地验证使用:

    moon 0.1.20261028 (e1e1f15 2026-10-28) moonc v0.10.18+6c83a7f62 (2026-10-28) moonrun 0.1.20261028 (e1e1f15 2026-10-28)

    默认目标为 wasm-gc。以下验证结论不扩展为所有历史工具链或所有目标后端的兼容承诺。

    #应用快速开始:真正生成本地化消息

    调用方 moon.pkg:

    import {
    "chenliyi-cly/moonfluent/runtime",
    "chenliyi-cly/moonfluent/intl",
    }
    options(is_main: true)

    调用方 main.mbt(使用 moon run main --target js,或在仓库运行下述示例):

    ///|
    fn main {
    let bundle = @runtime.Bundle::new("en-US", @intl.provider(), use_isolating=false)
    let errors = bundle.add_resource("hello = Hello { $name }!\nitems = { $count ->\n [one] One item\n *[other] { NUMBER($count) } items\n}\n")
    if !errors.is_empty() { abort("Invalid FTL") }
    let result = bundle.format("hello", arguments={"name": @runtime.Text("Alice")})
    println(result.value) // Hello Alice!
    println(bundle.format("items", arguments={"count": @runtime.number(1234)}).value)
    // 1,234 items
    }

    生产应用通常保留默认双向隔离,并检查 Formatted.errors。例子禁用隔离仅为了终端可读输出。模板和变量都不是可信 HTML,应由应用做上下文转义。

    #三个可执行运行时场景

    moon run examples/runtime_ui --target js moon run examples/runtime_invoice --target js moon run examples/runtime_reports --target js

    1. 界面翻译:中文 FTL、用户变量、品牌术语、控件属性和文件数量 → bundle 解析/引用/数字格式化 → 小陈,欢迎使用 MoonFluent。、打开 MoonFluent、已同步 1,234 个文件。
    2. 通知与账单:英文 FTL、客户名、类型化 USD 数字与 UTC epoch 毫秒 → NUMBER/DATETIME 和复数 → Invoice for Ada: $1,234.50、Due October 20, 2026、2 items。
    3. 批量报表:法语/英语资源链、记录数和缺失键 → 当前 locale 消息求值、整条消息回退、缺键诊断 → 2 lignes、Report complete 和可记录的错误,不把缺失引用当成功。

    这些示例在程序内断言输出及错误,CI 真实执行,不是截图或伪代码。三个原有语法工具示例也继续保留。

    #稳定公共入口

    入口用途
    根包 @ftl.parse、serialize_checked、Catalog、SourceMap可移植语法工具,原有 API 保持兼容
    @runtime.Bundle::new / add_resource组合 FTL 资源、重复策略和恢复式加载
    Bundle::format(id, arguments~, attribute~)生成字符串、locale 及类型化诊断数组
    @runtime.Text / Number / DateTime变量模型,后两者包含 Intl 选项;DateTime 单位为毫秒
    Bundle::add_function可信应用函数,以 Result 返回错误
    @runtime.Localizer::new / format调用方确定顺序的 locale bundle 回退
    @intl.provider()JS 的 Intl 后端;非 JS 显式 unsupported

    接口声明分别见 runtime/pkg.generated.mbti、intl/pkg.generated.mbti。不存在隐式网络请求、翻译脚本执行或全语言静态规则包。

    #目标平台

    目标语法与求值核心默认本地化后端
    JavaScript(Node 24,完整 Intl)支持真实 NumberFormat、DateTimeFormat、PluralRules
    wasm-gc / wasm / native支持,核心与测试后端可运行必须由应用提供 Provider;intl.provider() 返回 unsupported

    CI 在 Linux 执行 native;Windows 本机没有 C 编译器,只做 native check,不声称本地 native 可执行已运行。日期测试显式 UTC。宿主 ICU/时区差异与精确支持范围见语义矩阵。

    #场景一:编辑器容错诊断与位置转换

    翻译人员输入到一半时,资源不一定语法正确。parse 返回 Resource,将损坏部分保存在 Junk 中,并尽可能继续解析后面的有效条目。遍历 Annotation 可以获得错误码、参数、说明和位置。

    完整示例见 examples/editor/main.mbt;在仓库根目录运行:

    moon run examples/editor

    fn main {
    let source = "# 😀\nbad = { $ }\nvalid = Welcome\n"
    let root = @ftl.parse(source)
    let map = @ftl.SourceMap::new(source)
    let visits = match root.walk() {
    Ok(v) => v
    Err(_) => abort("遍历失败")
    }
    let mut errors = 0
    for visit in visits {
    match visit.node.kind {
    Annotation(code, _, message) => {
    let position = map.position(visit.node.span.start) catch {
    _ => abort("诊断位置无效")
    }
    println("\{code} at \{position.line}:\{position.character}: \{message}")
    errors += 1
    }
    _ => ()
    }
    }
    let catalog = match @ftl.Catalog::build(root) {
    Ok(c) => c
    Err(_) => abort("索引失败")
    }
    if errors != 1 ||
    catalog.find(@ftl.MessageSymbol, "valid").length() != 1 {
    abort("错误恢复结果不符合预期")
    }
    println("保留了一个诊断和后续有效消息")
    }

    此输入会报告 E0004,保留 valid 消息。行列从零开始。AST 的 span 是 Unicode scalar 偏移,而不是 UTF-16 下标;SourceMap.position 接收 scalar 偏移,默认输出 UTF-16 列,适合编辑器协议适配。to_scalar、from_scalar 支持 Scalar、Utf16、Utf8,并拒绝编码单元内部的非法边界。

    SourceMap 绑定一份精确源码快照。CRLF 的 CR 与 LF 都计入原始偏移;LF 才推进映射的行号。不要先替换换行,再使用旧 AST 的 span。

    #场景二:翻译维护中的结构化重命名

    批量字符串替换容易同时修改注释、正文和变量。rename_symbol 区分 message 与 term 命名空间,只修改指定定义及对应的消息或术语引用。

    完整示例见 examples/translation/main.mbt;运行:

    moon run examples/translation

    fn main {
    let input = "# old must remain a comment\nold = 你好 { $old }\nuse = { old } / old stays text\n"
    let root = @ftl.parse(input)
    let renamed = match root.rename_symbol(@ftl.MessageSymbol, "old", "welcome") {
    Ok(n) => n
    Err(_) => abort("重命名被拒绝")
    }
    let output = @ftl.serialize_checked(renamed) catch {
    _ => abort("输出无法保持严格语法往返")
    }
    if !output.contains("# old must remain a comment") ||
    !output.contains("{ $old }") ||
    !output.contains("{ welcome }") ||
    !output.contains("old stays text") {
    abort("修改了不应修改的内容")
    }
    let catalog = match @ftl.Catalog::build(@ftl.parse(output)) {
    Ok(c) => c
    Err(_) => abort("输出索引失败")
    }
    if catalog.find(@ftl.MessageSymbol, "welcome").length() != 1 ||
    !catalog.missing_references().is_empty() {
    abort("往返检查失败")
    }
    println(output)
    }

    输出保留注释里的 old、变量 $old 和正文中的 old,将定义及 { old } 引用改为 welcome。术语重命名使用 TermSymbol,传入的名字不带 -。

    有 Junk、目标定义缺失或重复、新名字冲突、名字不合法、遍历超限时,重命名返回 Err,而不是部分结果。它只处理当前资源,不会发现或修改其他文件中的引用,也不会重新计算输出位置。

    需要保留原排版时,可使用 SourceEdit 与 apply_edits 对原始源码做局部补丁:span 使用 scalar 单位,编辑按起点严格递增且不能重叠,expected 必须与当前片段一致。它是补丁工具,不是语法校验器;应用补丁后仍需重新解析、检查诊断。

    #场景三:构建阶段的资源索引与引用检查

    Catalog 按源码顺序保留定义和引用,不会默默覆盖重复键。仅含属性而没有值的消息可以通过属性引用访问,但不能据此认为裸消息引用一定有效。

    完整示例见 examples/build_index/main.mbt;运行:

    moon run examples/build_index

    fn main {
    let input = "-brand = Moon\nheadline = { -brand }\nheadline = Duplicate\nbutton =\n .label = Open\npage = { button.label } { missing }\n"
    let catalog = match @ftl.Catalog::build(@ftl.parse(input)) {
    Ok(c) => c
    Err(_) => abort("索引失败")
    }
    let duplicates = catalog.duplicates()
    let missing = catalog.missing_references()
    if duplicates.length() != 1 ||
    missing.length() != 1 ||
    catalog.references().length() != 3 {
    abort("索引结果不符合预期")
    }
    println("重复定义:\{duplicates[0].name}")
    println("资源内未解析引用:\{missing[0].reference.name}")
    for definition in catalog.definitions() {
    println("定义:\{definition.name},属性数:\{definition.attributes.length()}")
    }
    }

    结果为 5 个定义、3 个引用、1 个重复定义 headline、1 个资源内未解析引用 missing。

    资源内未解析不等于整个应用中不存在。 missing_references() 只在传入的一个 Resource 内查找,区分 UnknownDefinition、UnknownAttribute、MissingValue。此语法索引不处理外部文件;需要跨资源求值时使用下文 runtime.Bundle。变量、函数属于运行时输入,不纳入该静态引用列表。重复目标只要有一个满足访问条件,该引用就不列为 missing;重复或歧义必须另查 duplicates()。

    #序列化与校验边界

    API保证与限制
    parse / parse_entry恢复式解析;成功返回节点不等于没有语法错误。parse_entry 跳过前导有效注释,只取首个条目。
    serialize(root, with_junk=false)采用上游 canonical 排版,检查节点结构并限制递归;默认丢弃 Junk,不全面验证手建 AST 的名字与字面量词法。
    serialize(root, with_junk=true)额外原样输出 Junk 内容,但不会恢复整个文件的原始排版或证明输出有效。
    serialize_checked(root)拒绝 Junk 和遍历超限;规范化输出后重新解析,要求忽略 span 的整棵 AST 严格一致,否则抛出 SerializationError。
    valid_symbol_id检查不带术语前缀的 ASCII 标识符,不是整个资源验证器。
    Catalog::build建立资源内索引、记录 Junk 和重复定义,不代表资源已全面通过 Fluent 语法或运行时校验。

    对修改后的或手工构造的 AST,写出 FTL 时应使用 serialize_checked 的严格 roundtrip,不能以 serialize 未抛异常作为安全依据。这里的安全仅指语法结构可表示且没有往返损失,不包含运行时求值、跨文件引用完备性、HTML 转义或任意业务安全承诺。

    canonical 输出会调整空白、缩进、条目间距及换行布局,不是无损格式化。严格检查也可能拒绝语义近似却结构不同的手建树,例如序列化后合并了相邻文本节点。修改后请重新解析输出,再使用新源码的 span。

    AST 构造器公开,内部数组可以被调用方修改。请将解析结果和已索引 AST 当作只读快照;遍历、转换回调和索引使用期间不要改变其数组。to_json_ast、same_syntax 和派生 Eq 仅用于可信、无环且规模受控的树,不是防循环入口。

    #验证与复现

    在仓库根目录执行:

    moon version --all moon check moon test moon run examples/editor moon run examples/translation moon run examples/build_index moon package --list

    2026-10-16 的本地默认目标验证为 381 个测试全部通过,三个示例均运行成功。其中:

    • 39 个上游 fixture 对完整 AST JSON(含 span、诊断与恢复内容)做比较;
    • 同一组 39 个 fixture 分别检查两种 Junk 模式,共 78 组 serializer 期望比较,组织为 39 个测试;
    • 201 个额外输入与独立 Python 实现做完整 AST 差分;
    • 1 个 seeded 随机输入测试内部执行 2000 例,检查恢复、位置范围和输出可解析性,不是 2000 个独立测试,也不是 2000 例 Python 差分。

    其余测试覆盖公共 API、错误路径和边界。这些是有限输入上的证据,不是所有 FTL 输入或所有后端的认证。Python 仅用于重新生成 oracle,库及已生成测试运行不依赖 Python;生成说明见 tests/README.md 与 scripts/generate_oracle.py、scripts/generate_differential.py。生成脚本会重写测试文件,应在受控工作区执行并审查差异。

    完整矩阵入口为 python scripts/verify.py:本地 wasm-gc、wasm、js 的严格检查、构建、测试和三个语法示例均通过,另运行三个 JS 运行时示例,native 本地只做严格静态检查。Linux CI 使用 python3 scripts/verify.py --native 实际构建和运行四个目标;每次运行结果见仓库 Actions。具体证据与口径见 docs/competition/verification.md。

    #0.2.0 修复验证(2026-10-20)

    本机 wasm-gc、wasm 各 403/403,JavaScript 659/659 测试通过;三个语法示例跨三个目标运行,三个新增运行时示例在 JS 运行,严格 check/build 与 native check 通过。Windows 本机仍未运行 native 可执行。

    新增独立运行时语料 253 项:250 项 JS 上游字符串/错误数一致,3 项 Python 佐证的嵌套术语作用域合同。第三项保留本项目占位符拼写,不冒称全部与某个上游逐字相同。对照生成器使用 Node 24.14.0 / ICU 78.2 和哈希锁定的 Python 依赖;复现命令与三个差异见 oracle 说明 和 语义范围。非 JS 的额外 smoke test 不计作 Intl 运行时差分。

    当前手写生产 MoonBit 为 3434 物理行 / 2989 非空非注释行,排除测试、生成语料、示例和元数据。原有至少 21 个实质提交保留,新增提交对应实际修复,未重写历史。

    #进一步阅读与许可

    • pkg.generated.mbti:完整公共 API。
    • docs/architecture.md:分层、遍历顺序、坐标与错误模型。
    • SECURITY.md:输入信任边界、资源限制和报告建议。
    • AI_USAGE.md:AI 辅助开发范围与可核查依据。
    • CHANGELOG.md:版本能力和已知限制。
    • THIRD_PARTY.md、NOTICE、LICENSE:上游来源、修订和 Apache-2.0 归属。

    Copyright 2026 chenliyi。包含改编自 Mozilla 的代码,Copyright 2016 Mozilla Foundation。MoonFluent 是独立适配,不代表 Mozilla 或 Project Fluent 的背书。

    EditError

    pub suberror EditError {
    InvalidEdit(Int)
    UnorderedOrOverlappingEdit(Int)
    StaleEdit(Int)
    }

    SerializationError

    pub suberror SerializationError {
    SerializationError(String)
    }

    A structurally invalid AST, or an AST exceeding the serialization depth limit.

    SourceError

    pub suberror SourceError {
    InvalidOffset(Int)
    InvalidPosition(Int, Int)
    InvalidSpan(Int, Int)
    }

    Catalog

    pub struct Catalog {
    entries : Array[Definition]
    uses : Array[Reference]
    repeated : Array[DuplicateDefinition]
    junk : Array[Span]
    messages : Map[String, Array[Int]]
    terms : Map[String, Array[Int]]
    }

    Immutable index snapshot. Returned arrays are copies of index storage. Node payloads are shared, so callers must not mutate AST arrays after indexing.

    Catalog::build

    fn Catalog::build(resource : Node, max_depth? : Int, max_nodes? : Int) -> Result[Catalog, CatalogError]

    Build a source-ordered index, preserving all duplicate definitions rather than silently choosing a winner. Junk is recorded, not discarded. Definitions must be Resource children; nested definitions/resources are rejected.

    Catalog::definitions

    fn Catalog::definitions(self : Catalog) -> Array[Definition]

    Catalog::duplicates

    fn Catalog::duplicates(self : Catalog) -> Array[DuplicateDefinition]

    Catalog::find

    fn Catalog::find(self : Catalog, kind : SymbolKind, name : String) -> Array[Definition]

    Exact namespace lookup; returns every match in source order, including duplicates.

    Catalog::junk_spans

    fn Catalog::junk_spans(self : Catalog) -> Array[Span]

    Catalog::missing_references

    fn Catalog::missing_references(self : Catalog) -> Array[MissingReference]

    Resolve against this resource only. Attribute access requires that attribute; bare access requires a value. With duplicates, any matching definition suffices; ambiguity is reported separately by duplicates(). Junk may hide further uses.

    Catalog::references

    fn Catalog::references(self : Catalog) -> Array[Reference]

    CatalogError

    pub(all) enum CatalogError {
    Traversal(TreeError)
    ExpectedResource
    InvalidIdentifier(String)
    MalformedNode(String, Span)
    JunkPresent(Span)
    MissingDefinition(SymbolKind, String)
    AmbiguousDefinition(SymbolKind, String)
    RenameConflict(SymbolKind, String)
    } derive(Eq)

    CatalogError::equal

    CatalogError::not_equal

    fn CatalogError::not_equal(x : CatalogError, y : CatalogError) -> Bool

    Definition

    pub(all) struct Definition {
    kind : SymbolKind
    name : String
    node : Node
    attributes : Array[String]
    has_value : Bool
    }

    DuplicateDefinition

    pub(all) struct DuplicateDefinition {
    kind : SymbolKind
    name : String
    first_span : Span
    duplicate_span : Span
    } derive(Eq)

    DuplicateDefinition::equal

    DuplicateDefinition::not_equal

    MissingReference

    pub(all) struct MissingReference {
    reference : Reference
    reason : MissingReferenceReason
    } derive(Eq)

    MissingReference::equal

    MissingReference::not_equal

    fn MissingReference::not_equal(x : MissingReference, y : MissingReference) -> Bool

    MissingReferenceReason

    pub(all) enum MissingReferenceReason {
    UnknownDefinition
    UnknownAttribute
    MissingValue
    } derive(Eq)

    MissingReferenceReason::equal

    MissingReferenceReason::not_equal

    Node

    pub(all) struct Node {
    kind : Syntax
    span : Span
    } derive(Eq)

    Node::children

    fn Node::children(self : Node) -> Array[Node]

    Direct children in AST field order (including ids, comments and annotations). Returns a fresh array, not an alias of an AST-owned array.

    Node::equal

    fn Node::equal(Node, Node) -> Bool

    Node::new

    fn Node::new(kind : Syntax) -> Node

    Construct a synthetic node. Negative positions indicate no source origin.

    Node::not_equal

    fn Node::not_equal(x : Node, y : Node) -> Bool

    Node::rename_symbol

    fn Node::rename_symbol(self : Node, kind : SymbolKind, old_name : String, new_name : String, max_depth? : Int, max_nodes? : Int) -> Result[Node, CatalogError]

    Transactional, syntax-aware rename of one definition and all matching references, including attributes and nested term call arguments. Text, comments, variables, function names and the other namespace are untouched. Conflicts, invalid ids, missing/ambiguous sources, traversal limits and any Junk fail before edits. An identity rename still validates input. Spans remain source provenance, not recalculated offsets for a future serialization.

    Node::same_syntax

    fn Node::same_syntax(self : Node, other : Node) -> Bool

    Structural equality independent of source locations.

    Node::to_json_ast

    fn Node::to_json_ast(self : Node, with_spans? : Bool) -> Json

    Mozilla-compatible AST JSON, suitable for differential tests and tooling. Source offsets are omitted by default, not normalized to UTF-16 offsets.

    Node::transform

    fn Node::transform(self : Node, rewrite : (Node) -> Node, max_depth? : Int, max_nodes? : Int) -> Result[Node, TreeError]

    Bottom-up, left-to-right rewrite with original spans retained, even when the callback supplies a synthetic span. Newly returned subtrees are not revisited. The callback must not mutate input AST arrays. Limits are validated for the entire input before invoking any callback, so traversal errors have no effects.

    Node::type_name

    fn Node::type_name(self : Node) -> String

    Node::walk

    fn Node::walk(self : Node, max_depth? : Int, max_nodes? : Int) -> Result[Array[TreeVisit], TreeError]

    Preorder enumeration. Shared subtrees are visited once per occurrence, not deduplicated. No recursive call stack is used. Failure returns no partial list.

    Node::with_children

    fn Node::with_children(self : Node, children : Array[Node]) -> Result[Node, TreeError]

    Replace all direct children without changing constructor, optional-field presence, array lengths, scalar payloads or span. This is a structural primitive, not a Fluent grammar validator. Even leaves require zero children.

    Node::with_span

    fn Node::with_span(kind : Syntax, start : Int, end : Int) -> Node

    OffsetUnit

    pub(all) enum OffsetUnit {
    Scalar
    Utf16
    Utf8
    } derive(Eq)

    Position units are explicit: AST spans are Scalar; LSP uses Utf16.

    OffsetUnit::equal

    fn OffsetUnit::equal(OffsetUnit, OffsetUnit) -> Bool

    OffsetUnit::not_equal

    fn OffsetUnit::not_equal(x : OffsetUnit, y : OffsetUnit) -> Bool

    Position

    pub(all) struct Position {
    line : Int
    character : Int
    } derive(Eq)

    Position::equal

    fn Position::equal(Position, Position) -> Bool

    Position::not_equal

    fn Position::not_equal(x : Position, y : Position) -> Bool

    Reference

    pub(all) struct Reference {
    kind : SymbolKind
    name : String
    attribute : String?
    span : Span
    } derive(Eq)

    Only message/term references are resolution targets; variables and functions are external runtime inputs, and are deliberately excluded.

    Reference::equal

    fn Reference::equal(Reference, Reference) -> Bool

    Reference::not_equal

    fn Reference::not_equal(x : Reference, y : Reference) -> Bool

    SourceEdit

    pub(all) struct SourceEdit {
    span : Span
    expected : String
    replacement : String
    }

    A scalar-range patch against an exact source snapshot. Expected text prevents stale editor state from silently changing a different token.

    SourceMap

    pub struct SourceMap {
    chars : Array[Char]
    utf16 : Array[Int]
    utf8 : Array[Int]
    lines : Array[Int]
    }

    Immutable mapping over one exact source snapshot. Zero-based positions.

    SourceMap::from_scalar

    fn SourceMap::from_scalar(self : SourceMap, offset : Int, unit? : OffsetUnit) -> Int raise SourceError

    SourceMap::length

    fn SourceMap::length(self : SourceMap, unit? : OffsetUnit) -> Int

    SourceMap::line_count

    fn SourceMap::line_count(self : SourceMap) -> Int

    SourceMap::new

    fn SourceMap::new(source : String) -> SourceMap

    SourceMap::offset

    fn SourceMap::offset(self : SourceMap, position : Position, unit? : OffsetUnit) -> Int raise SourceError

    Inverse of position. Positions on newline codepoints are accepted, not beyond.

    SourceMap::position

    fn SourceMap::position(self : SourceMap, offset : Int, unit? : OffsetUnit) -> Position raise SourceError

    Map a raw offset to a line and character; CR remains a source character.

    SourceMap::slice

    fn SourceMap::slice(self : SourceMap, span : Span) -> String raise SourceError

    Slice scalar spans without accidentally using MoonBit's UTF-16 indexing.

    SourceMap::to_scalar

    fn SourceMap::to_scalar(self : SourceMap, offset : Int, unit? : OffsetUnit) -> Int raise SourceError

    Reject offsets inside a UTF-8 encoding or UTF-16 surrogate pair.

    Span

    pub(all) struct Span {
    start : Int
    end : Int
    } derive(Eq)

    Half-open Unicode scalar offsets, as used by Python fluent.syntax. Offsets count both code points of CRLF even though parsing treats it as EOL.

    Span::equal

    fn Span::equal(Span, Span) -> Bool

    Span::not_equal

    fn Span::not_equal(x : Span, y : Span) -> Bool

    SymbolKind

    pub(all) enum SymbolKind {
    MessageSymbol
    TermSymbol
    } derive(Eq)

    Message and term namespaces are distinct. Term names omit the leading '-'.

    SymbolKind::equal

    fn SymbolKind::equal(SymbolKind, SymbolKind) -> Bool

    SymbolKind::not_equal

    fn SymbolKind::not_equal(x : SymbolKind, y : SymbolKind) -> Bool

    Syntax

    pub(all) enum Syntax {
    Resource(Array[Node])
    Message(Node, Node?, Array[Node], Node?)
    Term(Node, Node, Array[Node], Node?)
    Pattern(Array[Node])
    TextElement(String)
    Placeable(Node)
    StringLiteral(String)
    NumberLiteral(String)
    MessageReference(Node, Node?)
    TermReference(Node, Node?, Node?)
    VariableReference(Node)
    FunctionReference(Node, Node)
    SelectExpression(Node, Array[Node])
    CallArguments(Array[Node], Array[Node])
    Attribute(Node, Node)
    Variant(Node, Node, Bool)
    NamedArgument(Node, Node)
    Identifier(String)
    Comment(String)
    GroupComment(String)
    ResourceComment(String)
    Junk(String, Array[Node])
    Annotation(String, Array[String?], String)
    } derive(Eq)

    Typed Fluent syntax, deliberately independent of runtime evaluation.

    Syntax::equal

    fn Syntax::equal(Syntax, Syntax) -> Bool

    Syntax::not_equal

    fn Syntax::not_equal(x : Syntax, y : Syntax) -> Bool

    TreeError

    pub(all) enum TreeError {
    InvalidLimits(Int, Int)
    DepthLimitExceeded(Int)
    NodeLimitExceeded(Int)
    ChildCountMismatch(String, Int, Int)
    } derive(Eq)

    Limits are checked before visiting a node. Root depth is zero; both limits must be nonnegative. Iterative algorithms also terminate on cyclic AST arrays.

    TreeError::equal

    fn TreeError::equal(TreeError, TreeError) -> Bool

    TreeError::not_equal

    fn TreeError::not_equal(x : TreeError, y : TreeError) -> Bool

    TreeVisit

    pub(all) struct TreeVisit {
    node : Node
    depth : Int
    }

    apply_edits

    fn apply_edits(source : String, edits : Array[SourceEdit]) -> String raise EditError

    Apply caller-ordered, nonoverlapping patches atomically. No partial result is returned on failure. Two edits at the same start are deliberately ambiguous.

    parse

    fn parse(source : String) -> Node

    Parse an FTL resource, preserving invalid entries as Junk with annotations. Default nesting is limited to 64, returning recoverable E9001 on excess.

    parse_entry

    fn parse_entry(source : String) -> Node

    Parse the first entry, ignoring preceding valid comments as upstream does.

    serialize

    fn serialize(root : Node, with_junk? : Bool) -> String raise SerializationError

    Canonical python-fluent serialization. Spans and Junk annotations are ignored; literal spellings are preserved rather than escaped or normalized again. The root must be a Resource. Invalid child types and excessive nesting raise SerializationError instead of silently dropping nodes or overflowing the stack.

    serialize_checked

    fn serialize_checked(resource : Node) -> String raise SerializationError

    Validate an entire editable resource by an actual syntax round trip. Unlike serialize's upstream-compatible structural renderer, this rejects invalid identifier/literal spellings, unrepresentable text, Junk and lossy tree shapes. Synthetic spans are ignored; returned FTL uses canonical whitespace, not the original formatting. Reparse the result before using its new source positions.

    valid_symbol_id

    fn valid_symbol_id(name : String) -> Bool

    Fluent identifier syntax, without the term sigil. ASCII letters start an id; subsequent characters may also be digits, '_' or '-'.