moonlangtag

    Pure MoonBit BCP 47 language tag parser, canonicalizer, validator, and RFC 4647 matcher

    bcp47
    rfc5646
    rfc4647
    language-tag
    accept-language
    Download zip
    Version
    0.1.0
    License
    MIT
    Last updated
    13 hours ago
    Downloads
    2

    #MoonLangTag

    纯 MoonBit 的 BCP 47 语言标签库:解析、校验、规范化、构建与匹配。

    MoonLangTag 实现 RFC 5646 文法(常规、私有、祖父标签)、RFC 5646 §4.5 规范化、RFC 4647 basic filtering / lookup,以及 Accept-Language 协商。它不下载 IANA 语言子标签注册表,也不做 IDNA、URI 解析或翻译记忆库。

    • GitHub:https://github.com/LuoYunxin1/moonlangtag
    • 包名:LuoYunxin1/moonlangtag
    • 版本:0.1.0

    #安装

    moon add LuoYunxin1/moonlangtag

    尚未发布到 mooncakes.io 时,请依赖 Git 仓库。可运行入口:

    moon run cmd/demo --target wasm-gc

    #三个示例

    cmd/demo/main.mbt 中的验收场景一致。

    #1. 电商目录:把货架标签写成规范 locale

    输入 EN-latn-us,去掉被 suppress 的 Latn,按推荐大小写得到 en-US,再写入商品 locale。

    let storefront = @langtag.canonicalize_text("EN-latn-us").unwrap()
    // storefront == "en-US"

    #2. CMS:拒绝残缺扩展并给出列定位

    保存语言配置时输入 en-u。解析失败,诊断指向第 1 行第 4 列的 LANG013,CMS 拒绝提交。

    let (bad, diags) = @langtag.parse_with_diagnostics("en-u")
    // bad is None
    // diags[0].format() == "1:4: error LANG013: extension singleton is missing subtags"

    #3. API 网关:按 Accept-Language 选出已打包译文

    请求头 fr-CH, fr;q=0.9, en;q=0.8,候选 en-US / fr-FR / defr-CH 没有精确包,退到 fr 后选中 fr-FR

    let chosen = @langtag.negotiate(
    "fr-CH, fr;q=0.9, en;q=0.8",
    ["en-US", "fr-FR", "de"],
    ).unwrap()
    // chosen == "fr-FR"

    Demo 预期输出:

    CATALOG: EN-latn-us -> en-US CMS: 1:4: error LANG013: extension singleton is missing subtags API: negotiated fr-FR MoonLangTag acceptance: 3 scenarios passed

    #API 入口

    let tag = @langtag.parse("zh-yue-Hant-CN").unwrap()
    let (built, _) = @langtag.builder("hy")
    .with_script("Latn")
    .with_region("IT")
    .with_variant("arevela")
    .build_canonical()
    let hits = @langtag.basic_filter("de-DE", ["de-DE", "de-DE-1996", "de"])

    示例假定调用包将本地库导入为 @langtag;可运行配置见 cmd/demo/moon.pkg

    #测试

    moon check --target wasm-gc --deny-warn moon test --target wasm-gc --deny-warn moon run cmd/demo --target wasm-gc

    #边界

    • 不是完整 IANA 子标签注册表校验器
    • 不是 CLDR 显示名或 collation
    • 不是 IDNA / Punycode / URI 解析
    • preferred-value 与 suppress-script 表是文档化子集

    #License

    MIT

    Diagnostic

    pub(all) struct Diagnostic {
    code : String
    severity : Severity
    message : String
    span : Span
    } derive(Eq,
    Debug
    )

    Diagnostic::format

    fn Diagnostic::format(self : Diagnostic) -> String

    Extension

    pub(all) struct Extension {
    singleton : String
    parts : Array[String]
    } derive(Eq,
    Debug
    )

    LanguageRange

    pub(all) struct LanguageRange {
    range : String
    q_milli : Int
    span : Span
    } derive(Eq,
    Debug
    )

    LanguageTag

    pub(all) struct LanguageTag {
    kind : TagKind
    language : String
    extlang : Array[String]
    script : String
    region : String
    variants : Array[String]
    extensions : Array[Extension]
    private_use : Array[String]
    grandfathered : String
    } derive(Eq,
    Debug
    )

    Immutable BCP 47 language tag. Arrays returned by accessors are copies.

    LanguageTag::extension_subtags

    fn LanguageTag::extension_subtags(self : LanguageTag) -> Array[Extension]

    LanguageTag::extlang_subtags

    fn LanguageTag::extlang_subtags(self : LanguageTag) -> Array[String]

    LanguageTag::has_region

    fn LanguageTag::has_region(self : LanguageTag) -> Bool

    LanguageTag::has_script

    fn LanguageTag::has_script(self : LanguageTag) -> Bool

    LanguageTag::private_use_subtags

    fn LanguageTag::private_use_subtags(self : LanguageTag) -> Array[String]

    LanguageTag::variant_subtags

    fn LanguageTag::variant_subtags(self : LanguageTag) -> Array[String]

    Severity

    pub(all) enum Severity {
    Error
    Warning
    } derive(Eq,
    Debug
    )

    Span

    pub(all) struct Span {
    start : Int
    end : Int
    line : Int
    column : Int
    } derive(Eq,
    Debug
    )

    Half-open UTF-16 offsets; line is always 1 for a single language tag.

    TagBuilder

    pub(all) struct TagBuilder {
    language : String
    extlang : Array[String]
    script : String
    region : String
    variants : Array[String]
    extensions : Array[Extension]
    private_use : Array[String]
    }

    TagBuilder::build

    TagBuilder::build_canonical

    fn TagBuilder::build_canonical(self : TagBuilder) -> (LanguageTag?, Array[Diagnostic])

    TagBuilder::with_extension

    fn TagBuilder::with_extension(self : TagBuilder, singleton : String, parts : Array[String]) -> TagBuilder

    TagBuilder::with_extlang

    fn TagBuilder::with_extlang(self : TagBuilder, extlang : String) -> TagBuilder

    TagBuilder::with_private_use

    fn TagBuilder::with_private_use(self : TagBuilder, part : String) -> TagBuilder

    TagBuilder::with_region

    fn TagBuilder::with_region(self : TagBuilder, region : String) -> TagBuilder

    TagBuilder::with_script

    fn TagBuilder::with_script(self : TagBuilder, script : String) -> TagBuilder

    TagBuilder::with_variant

    fn TagBuilder::with_variant(self : TagBuilder, variant : String) -> TagBuilder

    TagKind

    pub(all) enum TagKind {
    Regular
    PrivateUse
    Grandfathered
    } derive(Eq,
    Debug
    )

    basic_filter

    fn basic_filter(range : String, tags : Array[String]) -> Array[String]

    RFC 4647 basic filtering. The wildcard range * matches every well-formed tag.

    builder

    fn builder(language : String) -> TagBuilder

    canonicalize

    fn canonicalize(tag : LanguageTag) -> LanguageTag

    RFC 5646 4.5 canonicalization: preferred values, suppress-script, extension order, case.

    canonicalize_text

    fn canonicalize_text(input : String) -> String?

    format

    fn format(tag : LanguageTag) -> String

    Format a parsed tag using BCP 47 recommended capitalization.

    is_well_formed

    fn is_well_formed(input : String) -> Bool

    lookup

    fn lookup(requested : Array[String], available : Array[String]) -> String?

    RFC 4647 lookup: try each requested range, truncating subtags until a match exists.

    negotiate

    fn negotiate(header : String, available : Array[String]) -> String?

    Choose the best available tag for an Accept-Language header.

    parse

    fn parse(input : String) -> LanguageTag?

    parse_accept_language

    fn parse_accept_language(header : String) -> (Array[LanguageRange], Array[Diagnostic])

    Parse RFC 4647 / HTTP Accept-Language lists such as fr-CH, fr;q=0.9, en;q=0.8, *;q=0.1.

    parse_with_diagnostics

    fn parse_with_diagnostics(input : String) -> (LanguageTag?, Array[Diagnostic])

    Parse a BCP 47 language tag. Grammar errors yield None and diagnostics.