Fluent FTL syntax, message resolution and JavaScript Intl localization in MoonBit
moon add chenliyi-cly/moonfluent@0.2.1import {
"chenliyi-cly/moonfluent" @ftl,
}
pkgtype(kind: "executable")moon 0.1.20261028 (e1e1f15 2026-10-28)
moonc v0.10.18+6c83a7f62 (2026-10-28)
moonrun 0.1.20261028 (e1e1f15 2026-10-28)import {
"chenliyi-cly/moonfluent/runtime",
"chenliyi-cly/moonfluent/intl",
}
options(is_main: true)///|
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
}moon run examples/runtime_ui --target js
moon run examples/runtime_invoice --target js
moon run examples/runtime_reports --target js| 入口 | 用途 |
|---|---|
| 根包 @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 |
| 目标 | 语法与求值核心 | 默认本地化后端 |
|---|---|---|
| JavaScript(Node 24,完整 Intl) | 支持 | 真实 NumberFormat、DateTimeFormat、PluralRules |
| wasm-gc / wasm / native | 支持,核心与测试后端可运行 | 必须由应用提供 Provider;intl.provider() 返回 unsupported |
moon run examples/editorfn 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("保留了一个诊断和后续有效消息")
}moon run examples/translationfn 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)
}moon run examples/build_indexfn 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()}")
}
}| 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 语法或运行时校验。 |
moon version --all
moon check
moon test
moon run examples/editor
moon run examples/translation
moon run examples/build_index
moon package --listpub suberror EditError {
InvalidEdit(Int)
UnorderedOrOverlappingEdit(Int)
StaleEdit(Int)
}pub suberror SerializationError {
SerializationError(String)
}pub suberror SourceError {
InvalidOffset(Int)
InvalidPosition(Int, Int)
InvalidSpan(Int, Int)
}fn Catalog::build(resource : Node, max_depth? : Int, max_nodes? : Int) -> Result[Catalog, 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)pub(all) struct Definition {
kind : SymbolKind
name : String
node : Node
attributes : Array[String]
has_value : Bool
}pub(all) struct DuplicateDefinition {
kind : SymbolKind
name : String
first_span : Span
duplicate_span : Span
} derive(Eq)pub(all) struct MissingReference {
reference : Reference
reason : MissingReferenceReason
} derive(Eq)fn MissingReferenceReason::not_equal(x : MissingReferenceReason, y : MissingReferenceReason) -> Boolfn Node::rename_symbol(self : Node, kind : SymbolKind, old_name : String, new_name : String, max_depth? : Int, max_nodes? : Int) -> Result[Node, CatalogError]pub(all) struct Reference {
kind : SymbolKind
name : String
attribute : String?
span : Span
} derive(Eq)fn SourceMap::from_scalar(self : SourceMap, offset : Int, unit? : OffsetUnit) -> Int raise SourceErrorfn SourceMap::offset(self : SourceMap, position : Position, unit? : OffsetUnit) -> Int raise SourceErrorfn SourceMap::position(self : SourceMap, offset : Int, unit? : OffsetUnit) -> Position raise SourceErrorfn SourceMap::to_scalar(self : SourceMap, offset : Int, unit? : OffsetUnit) -> Int raise SourceErrorpub(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)pub(all) enum TreeError {
InvalidLimits(Int, Int)
DepthLimitExceeded(Int)
NodeLimitExceeded(Int)
ChildCountMismatch(String, Int, Int)
} derive(Eq)fn valid_symbol_id(name : String) -> BoolInstall
Download zipFluent FTL syntax, message resolution and JavaScript Intl localization in MoonBit