Sign in

    cldr-conformance

    CLDR conformance benchmark and differential tester for MoonBit i18n implementations

    cldr
    i18n
    l10n
    conformance
    unicode
    testing
    devtools
    Download zip
    Version
    0.1.0
    License
    Apache-2.0
    Last updated
    12 hours ago
    Downloads
    2

    #cldr-conformance

    ci

    CLDR conformance benchmark and differential tester for MoonBit i18n implementations.

    用 CLDR 官方测试数据,度量并对比 MoonBit 生态里各个 i18n 实现的一致性:谁符合标准、差在哪个维度、差多少。

    状态:M1 适配器契约、能力登记与 --impls 选择已完成。CLDR 官方数据可按不可变 tag 拉取、离线校验并幂等重跑,且已解析成 4714 条用例(数字 2625 / 日期 1752 / MF2 337)。 当前 CLI 可选择并列出适配器;用例执行、失败归因与报告按 开发方案.md §4.2 继续推进。

    #解决什么问题

    MoonBit 生态已有多个 i18n 实现(moonlocale、lampclaw/i18n、moon_l10n、moongettext 等),各自实现了复数规则、数字与日期格式化,但没有任何工具能回答:它们是否符合 CLDR 标准、彼此差在哪里。

    CLDR 官方公开了一致性测试数据(common/testData/,见 DATA-SOURCES.md),本项目把它变成可执行的基准。

    #范围(Scope)

    • 读取 CLDR 官方测试数据,产出可复现的一致率矩阵与失败归因
    • 通过可插拔适配器调用被测实现(一个适配器一个包)
    • 输出 JSON / Markdown / 静态 HTML 报告

    #非目标(Non-goals)

    • 不实现格式化算法——不写 LDML pattern 引擎、不写复数规则求值器
    • 不造新的 i18n 库——不做翻译目录、不做消息运行时
    • 不修改被测包源码——只通过公开 API 调用,缺陷以 issue/PR 反馈上游
    • 不追求"全绿"——unsupported 与 fail 严格区分并如实记录

    #当前结构

    cmd/fetch-data/ # 数据资产工具:--sync / --verify / 单文件下载(D2 已完成) cmd/conform/ # 主 CLI:登记适配器、解析 --impls 并清点用例;执行与报告后续接入 cmd/report-site/ # 报告 JSON → 静态 HTML(D16+) internal/source/ # 目录、下载、摘要、清单、离线校验、幂等同步(D2 已完成) internal/corpus/ # TSV / JSON 测试数据解析(D3–D4) internal/engine/ # 用例执行、归因、最小化(D5–D7) internal/adapter/ # 适配器 trait 对象登记、能力校验与选择(M1) adapters/ # 每个被测实现一个包,reference_null 用于引擎自测 fixtures/ # 官方测试数据快照 + SOURCES.json(来源与 SHA256)

    #快速开始

    moon check --target all moon test # 数据管线(默认已入库,可直接离线校验) moon run cmd/fetch-data -- --verify # 需要重新拉取时(幂等:已是最新的文件会跳过) moon run cmd/fetch-data -- --sync

    套件枚举(下面的 mbt check 代码块会被 moon test 真实执行):

    ///|
    test {
    let suites = @cldr-conformance.Suite::all()
    assert_eq(suites.length(), 3)
    assert_eq(suites[0].label(), "decimal")
    }

    #许可证

    代码 Apache-2.0;CLDR 测试数据为 Unicode License v3,数据基线、文件清单与第三方依赖许可证见 DATA-SOURCES.md。

    DatetimeAdapter

    pub(open) trait DatetimeAdapter {
    fn name(Self) -> String
    fn format_skeleton(Self, locale : String, calendar : String, skeleton : String) -> String raise
    fn format_datetime(Self, input : DatetimeFormatInput) -> String raise
    }

    日期格式化适配器。

    MessageFormat2Adapter

    pub(open) trait MessageFormat2Adapter {
    fn name(Self) -> String
    fn format_message(Self, input : MessageFormat2Input) -> MessageFormat2Result raise
    }

    MessageFormat 2 适配器。

    NumberAdapter

    pub(open) trait NumberAdapter {
    fn name(Self) -> String
    fn format_number(Self, locale : String, value : String, style~ : NumberStyle, length~ : NumberLength) -> String raise
    }

    数字格式化适配器。

    value 用 String 而非数值类型传入:生态内各 i18n 包的数值类型并不统一, 用字符串可以避免浮点转换引入额外舍入误差(见 DESIGN.md D2)。

    AdapterCapabilities

    pub struct AdapterCapabilities {
    capabilities : Array[AdapterCapability]
    } derive(Eq,
    Debug
    )

    适配器声明的一组能力。构造时按首次出现顺序去重。

    AdapterCapabilities::all

    AdapterCapabilities::contains

    fn AdapterCapabilities::contains(self : AdapterCapabilities, capability : AdapterCapability) -> Bool

    AdapterCapabilities::equal

    AdapterCapabilities::new

    AdapterCapabilities::not_equal

    AdapterCapability

    pub(all) enum AdapterCapability {
    Decimal
    DecimalScientific
    DecimalCompact
    DatetimeSkeleton
    DatetimeFormat
    MessageFormat2
    } derive(Eq,
    Debug
    )

    适配器可声明的能力。

    粒度按"一个库可能只做其中一部分"来切:
    • 日期骨架查询与日期格式化分开声明;
    • 数字套件内部再分 Decimal(普通/百分比)、DecimalScientific、DecimalCompact。

    这样"没有这个能力"会记为 unsupported(不进一致率分母), 而不是被算成"做错了"——见 可比性映射.md 的实测依据 (例如 zlhahaha/moonlocale 没有 scientific 与 compact 数字 API)。

    AdapterCapability::all

    所有可声明的能力,顺序稳定。

    AdapterCapability::equal

    AdapterCapability::label

    fn AdapterCapability::label(self : AdapterCapability) -> String

    能力在 CLI 和报告中的稳定标识。

    AdapterCapability::not_equal

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

    AdapterReport

    pub(all) struct AdapterReport {
    adapter_id : String
    summary : RunSummary
    by_suite : Array[SuiteSummary]
    by_reason : Array[ReasonCount]
    failures : Array[FailureDetail]
    errors : Array[ErrorDetail]
    } derive(Eq,
    Debug
    )

    单个适配器的报告。

    AdapterReport::equal

    AdapterReport::from_results

    fn AdapterReport::from_results(adapter_id : String, results : Array[CaseResult], limit? : Int) -> AdapterReport

    从执行结果构建单个适配器的报告。

    limit 为负时采用 failure_sample_limit();需要看全部样例时传一个很大的数 (CLI 的 --all-samples 就是这么用的)。

    AdapterReport::not_equal

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

    CaseOutcome

    pub(all) enum CaseOutcome {
    Pass
    Fail(reason~ : FailReason, expected~ : String, actual~ : String)
    Errored(detail~ : String)
    Unsupported(feature~ : String)
    NotInstalled
    } derive(Eq,
    Debug
    )

    单条用例的执行结果。

    Unsupported 与 Fail 严格区分:被测实现没有该能力不算失败, 单独统计,避免用缺失能力拉低一致率。

    CaseOutcome::equal

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

    CaseOutcome::is_failure

    fn CaseOutcome::is_failure(self : CaseOutcome) -> Bool

    是否属于真正的一致性失败(答错或抛异常)。 unsupported 与 not-installed 不算失败——没有能力不等于做错了(见 DESIGN.md D3)。

    CaseOutcome::label

    fn CaseOutcome::label(self : CaseOutcome) -> String

    结果的稳定标签(报告与 CLI 共用)。

    CaseOutcome::not_equal

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

    CaseOutcome::reason_label

    fn CaseOutcome::reason_label(self : CaseOutcome) -> String?

    失败原因的稳定标识;不是 Fail 时返回 None。

    CaseResult

    pub(all) struct CaseResult {
    suite : Suite
    adapter_id : String
    source : String
    location : String
    outcome : CaseOutcome
    elapsed_ms : Int
    } derive(Eq,
    Debug
    )

    单条用例在某个适配器上的执行结果,是报告的最小单元。

    CaseResult::equal

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

    CaseResult::not_equal

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

    CharDiff

    pub(all) struct CharDiff {
    index : Int
    expected_snippet : String
    actual_snippet : String
    expected_length : Int
    actual_length : Int
    } derive(Eq,
    Debug
    )

    字符级最小差异:首个不同码元的位置与两侧片段。

    报告要给的是"一眼看出差在哪",而不是让人自己 diff 两串长文本。

    CharDiff::equal

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

    CharDiff::not_equal

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

    CharDiff::to_repr

    DatetimeCase

    pub(all) struct DatetimeCase {
    locale : String
    calendar : String
    input : String
    expected : String
    semantic_skeleton : String?
    classical_skeleton : String?
    semantic_skeleton_length : String?
    zone_style : String?
    time_length : String?
    date_length : String?
    hour_cycle : String?
    year_style : String?
    date_time_format_type : String?
    index : Int
    } derive(Eq,
    Debug
    )

    一条「时间输入 → 格式化输出」用例:datetime.json 的一个元素。

    可选字段按上游原样保留(缺省即该项未被测试),刻意不做归一化: 报告要能说明「这条用例到底在测哪个能力」。

    DatetimeCase::equal

    DatetimeCase::format_input

    fn DatetimeCase::format_input(self : DatetimeCase) -> DatetimeFormatInput

    日期用例 → 适配器输入。不含期望值与来源信息,适配器在类型上就拿不到答案 (见 DESIGN.md D1「只做裁判,不做选手」)。

    DatetimeCase::not_equal

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

    DatetimeCorpus

    pub(all) struct DatetimeCorpus {
    skeletons : Array[SkeletonCase]
    formatted : Array[DatetimeCase]
    } derive(Eq,
    Debug
    )

    日期套件的用例集合:骨架表 + 格式化用例。

    DatetimeCorpus::calendars

    fn DatetimeCorpus::calendars(self : DatetimeCorpus) -> Array[String]

    涉及的日历(两个来源合并、去重后按字典序)。

    DatetimeCorpus::equal

    DatetimeCorpus::length

    fn DatetimeCorpus::length(self : DatetimeCorpus) -> Int

    DatetimeCorpus::locales

    fn DatetimeCorpus::locales(self : DatetimeCorpus) -> Array[String]

    涉及的语言(两个来源合并、去重后按字典序)。

    DatetimeCorpus::new

    fn DatetimeCorpus::new(skeletons : Array[SkeletonCase], formatted : Array[DatetimeCase]) -> DatetimeCorpus

    DatetimeCorpus::not_equal

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

    DatetimeFormatInput

    pub(all) struct DatetimeFormatInput {
    locale : String
    calendar : String
    input : String
    semantic_skeleton : String?
    classical_skeleton : String?
    semantic_skeleton_length : String?
    zone_style : String?
    time_length : String?
    date_length : String?
    hour_cycle : String?
    year_style : String?
    date_time_format_type : String?
    } derive(Eq,
    Debug
    )

    日期格式化的输入参数,不包含 CLDR 的期望结果和来源行号。

    DatetimeFormatInput::equal

    DatetimeFormatInput::not_equal

    DecimalCase

    pub(all) struct DecimalCase {
    locale : String
    style : NumberStyle
    length : NumberLength
    input : String
    expected : String
    line : Int
    } derive(Eq,
    Debug
    )

    一条数字格式化用例:decimals*.tsv 的一行。

    input 与 expected 都按字符串原样保存:CLDR 给的输入本身就是十进制字符串, 转成浮点会引入舍入误差,恰好污染本项目要度量的那个维度(见 DESIGN.md D2)。

    DecimalCase::equal

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

    DecimalCase::not_equal

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

    DecimalCorpus

    pub(all) struct DecimalCorpus {
    cases : Array[DecimalCase]
    } derive(Eq,
    Debug
    )

    数字套件的用例集合。

    DecimalCorpus::equal

    DecimalCorpus::length

    fn DecimalCorpus::length(self : DecimalCorpus) -> Int

    DecimalCorpus::locales

    fn DecimalCorpus::locales(self : DecimalCorpus) -> Array[String]

    涉及的语言,去重后按字典序排列(报告表头需要稳定顺序)。

    DecimalCorpus::new

    DecimalCorpus::not_equal

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

    DecimalCorpus::of_locale

    fn DecimalCorpus::of_locale(self : DecimalCorpus, locale : String) -> Array[DecimalCase]

    某个语言的用例(保持源文件顺序)。

    ErrorDetail

    pub(all) struct ErrorDetail {
    suite : Suite
    source : String
    location : String
    detail : String
    } derive(Eq,
    Debug
    )

    一条「抛异常」的明细。

    与答错分列存放:抛异常与答错是两类缺陷,混在一起会掩盖实现崩溃的问题。

    ErrorDetail::equal

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

    ErrorDetail::not_equal

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

    FailReason

    pub(all) enum FailReason {
    GroupingMismatch
    DigitSystemMismatch
    RoundingMismatch
    PluralCategoryMismatch
    BidiControlMissing
    CompactFormMismatch
    PatternSyntaxUnsupported
    ValueMismatch
    Other
    } derive(Eq,
    Debug
    )

    失败归因分类。

    这是本项目相对「简单字符串 diff」的核心增量:报告要给出可行动的结论, 而不是只丢一句 expected != actual(见 DESIGN.md)。

    FailReason::all

    全部归因类别,顺序即报告中的展示顺序(分组类在前、兜底在后)。

    FailReason::equal

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

    FailReason::label

    fn FailReason::label(self : FailReason) -> String

    失败标签的稳定标识(用于 JSON 与报告,不要随展示文案改动)。

    FailReason::not_equal

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

    FailureDetail

    pub(all) struct FailureDetail {
    suite : Suite
    source : String
    location : String
    reason : FailReason
    expected : String
    actual : String
    diff : CharDiff
    } derive(Eq,
    Debug
    )

    一条「答错」的明细:报告里最有价值的部分。

    FailureDetail::equal

    FailureDetail::not_equal

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

    MessageCase

    pub(all) struct MessageCase {
    file : String
    scenario : String
    index : Int
    description : String?
    src : String
    locale : String
    bidi_isolation : String?
    exp : String?
    expected_errors : Array[String]
    expects_parts : Bool
    tags : Array[String]
    params_json : String?
    } derive(Eq,
    Debug
    )

    一条 MF2 用例(messageFormat/tests/*.json 的一个 test)。

    上游用「文件级默认值 + 用例级覆盖」表达期望:例如 syntax-errors.json 的 defaultTestProperties 带 expErrors,其 133 个用例只写 src 就继承了它。 因此这里存的是合并后的结果,而不是用例的原始字段。

    MessageCase::equal

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

    MessageCase::format_input

    fn MessageCase::format_input(self : MessageCase) -> MessageFormat2Input

    MF2 用例 → 适配器输入。不含期望输出与期望错误。

    MessageCase::not_equal

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

    MessageCorpus

    pub(all) struct MessageCorpus {
    cases : Array[MessageCase]
    } derive(Eq,
    Debug
    )

    MF2 套件的用例集合。

    MessageCorpus::equal

    MessageCorpus::files

    fn MessageCorpus::files(self : MessageCorpus) -> Array[String]

    涉及的来源文件(去重后按字典序)。

    MessageCorpus::length

    fn MessageCorpus::length(self : MessageCorpus) -> Int

    MessageCorpus::new

    MessageCorpus::not_equal

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

    MessageCorpus::of_scenario

    fn MessageCorpus::of_scenario(self : MessageCorpus, scenario : String) -> Array[MessageCase]

    某个场景的用例(保持源文件顺序)。

    MessageCorpus::scenarios

    fn MessageCorpus::scenarios(self : MessageCorpus) -> Array[String]

    涉及的上游场景(去重后按字典序)。

    MessageFormat2Input

    pub(all) struct MessageFormat2Input {
    locale : String
    source : String
    bidi_isolation : String?
    tags : Array[String]
    params_json : String?
    } derive(Eq,
    Debug
    )

    MF2 格式化的输入参数,不包含 CLDR 的期望输出或期望错误。

    MessageFormat2Input::equal

    MessageFormat2Input::not_equal

    MessageFormat2Result

    pub(all) enum MessageFormat2Result {
    Formatted(String)
    Errors(Array[String])
    } derive(Eq,
    Debug
    )

    MF2 适配器的实际结果。错误按 CLDR 测试数据中的类别返回。

    MessageFormat2Result::equal

    MessageFormat2Result::not_equal

    NumberLength

    pub(all) enum NumberLength {
    Standard
    Short
    Long
    } derive(Eq,
    Debug
    )

    紧凑格式长度,对应 decimals.tsv 的 format_length 列(空值记为 Standard)。

    NumberLength::equal

    NumberLength::not_equal

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

    NumberStyle

    pub(all) enum NumberStyle {
    DecimalStyle
    PercentStyle
    ScientificStyle
    CurrencyStyle
    } derive(Eq,
    Debug
    )

    数字格式化样式,对应 decimals.tsv 的 number_format 列。

    NumberStyle::equal

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

    NumberStyle::not_equal

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

    ReasonCount

    pub(all) struct ReasonCount {
    reason : FailReason
    count : Int
    } derive(Eq,
    Debug
    )

    某个失败原因的计数。

    ReasonCount::equal

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

    ReasonCount::not_equal

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

    Report

    pub(all) struct Report {
    baseline : String
    adapters : Array[AdapterReport]
    summary : RunSummary
    } derive(Eq,
    Debug
    )

    一次运行的完整报告。

    Report::equal

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

    Report::from_results

    fn Report::from_results(baseline : String, adapter_ids : Array[String], results : Array[CaseResult], limit? : Int) -> Report

    从一次运行的全部结果构建报告。adapter_ids 决定顺序(即报告中的展示顺序)。

    Report::not_equal

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

    Report::to_json

    fn Report::to_json(self : Report) -> String

    渲染为 JSON(缩进 2 空格,键名稳定)。

    schema 顶层:baseline / summary / adapters[], 每个适配器含 adapter_id / summary / by_suite[] / by_reason[] / failures[](样例)/ errors[](样例)。

    Report::to_markdown

    fn Report::to_markdown(self : Report) -> String

    渲染为 Markdown:矩阵 + 归因分布 + 失败样例(带字符级差异)。

    Report::to_repr

    RunSummary

    pub(all) struct RunSummary {
    total : Int
    passed : Int
    failed : Int
    unsupported : Int
    not_installed : Int
    errored : Int
    } derive(Eq,
    Debug
    )

    一次运行的计数汇总。

    RunSummary::empty

    fn RunSummary::empty() -> RunSummary

    RunSummary::equal

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

    RunSummary::excluded

    fn RunSummary::excluded(self : RunSummary) -> Int

    不参与一致性统计的用例数(无能力 + 未选入)。

    RunSummary::failures

    fn RunSummary::failures(self : RunSummary) -> Int

    真正的一致性失败数 = 答错 + 抛异常。

    RunSummary::from_results

    fn RunSummary::from_results(results : Array[CaseResult]) -> RunSummary

    统计一批结果。

    RunSummary::is_clean

    fn RunSummary::is_clean(self : RunSummary) -> Bool

    是否没有任何一致性失败。

    RunSummary::not_equal

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

    RunSummary::pass_rate

    fn RunSummary::pass_rate(self : RunSummary) -> Double

    一致率 = pass /(总数 − unsupported − not-installed)。

    分母排除"没这个能力"与"未安装":它们不是失败,也不该把一致率算低 (见 DESIGN.md D3)。评审看的就是这个数字。

    RunSummary::pass_rate_text

    fn RunSummary::pass_rate_text(self : RunSummary) -> String

    一致率的展示形式(截断到整数百分比)。

    RunSummary::render

    fn RunSummary::render(self : RunSummary) -> String

    一行文本,供 CLI 与日志展示。

    SkeletonCase

    pub(all) struct SkeletonCase {
    locale : String
    calendar : String
    skeleton : String
    pattern : String
    line : Int
    } derive(Eq,
    Debug
    )

    一条「骨架 → 模式」用例:skeletons*.tsv 的一行。

    SkeletonCase::equal

    SkeletonCase::not_equal

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

    Suite

    pub(all) enum Suite {
    Decimal
    Datetime
    MessageFormat2
    } derive(Eq,
    Debug
    )

    一致性套件。每个套件对应 CLDR 官方测试数据中的一类(见 DATA-SOURCES.md)。

    Suite::all

    fn Suite::all() -> Array[Suite]

    全部套件,顺序即报告中的展示顺序。

    Suite::equal

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

    Suite::label

    fn Suite::label(self : Suite) -> String

    套件在 CLI 与报告中的稳定标识(不要随展示文案改动)。

    Suite::not_equal

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

    Suite::to_repr

    SuiteSummary

    pub(all) struct SuiteSummary {
    suite : Suite
    summary : RunSummary
    } derive(Eq,
    Debug
    )

    按套件拆分的小计。

    SuiteSummary::equal

    SuiteSummary::not_equal

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

    char_diff

    fn char_diff(expected : String, actual : String) -> CharDiff

    逐码元比较,找出首个差异位置,并给出该位置两侧的片段。

    窗口半径固定为 14 个码元:足够看清差异,又不至于把整串塞进报告。

    failure_sample_limit

    fn failure_sample_limit() -> Int

    每个适配器在报告里最多保留多少条明细。

    4714 条用例 × N 个适配器全量塞进 JSON 会有数 MB,而人看前几条就能定位问题; 计数与归因分布始终是完整的,不受这个上限影响。