Testing and conformance

How dxn_i18n is tested - the Unicode MessageFormat Working Group's conformance suite, the feature test groups and CI - and how to test your own messages, plural forms and custom functions.

The library’s behavior is pinned by the Unicode MessageFormat Working Group’s conformance suite plus a test file per feature. Your own messages deserve tests too: a plural form missing for one language is invisible until a user hits it.

#The conformance suite

crates/dxn_i18n/tests/conformance/ holds the Working Group’s JSON test files, copied unchanged from unicode-org/message-format-wg (test/tests/) at commit 16372a6a02f41728be472a5921ba7c91774edeac, under the Unicode License. tests/conformance.rs runs every file: 17 files with 462 test cases in this copy. It checks the formatted text and the types of the errors (unresolved-variable, bad-operand, syntax-error and so on), for syntax, the data model, pattern selection, fallbacks, bidi isolation, the u: options and each default function.

FilesCases
syntax.json, syntax-errors.json247
data-model-errors.json23
pattern-selection.json, fallback.json30
bidi.json, u-options.json37
functions/ (number, integer, offset, percent, currency, date, time, datetime, string)125

The suite’s own test functions (:test:function, :test:select, :test:format) are implemented in the runner as a custom FunctionRegistry, following the suite’s README. The runner skips no case. All pass, and a case that fails fails the whole the_working_groups_suite_passes test with a list of every failure. To update, copy newer files over the directory, change the commit in tests/conformance/README.md and run cargo test -p dxn_i18n --test conformance.

#Feature tests and CI

Tests are grouped by feature, one file in crates/dxn_i18n/tests/ each:

FileCovers
syntax.rsParsing, escapes, options, attributes, markup, data-model rules (8 tests).
formatting.rsDeclarations, fallbacks, markup and parts, bidi isolation (7).
selection.rsPlural, ordinal, exact, text and multi-selector matching (7).
functions.rsOptions of the default functions and custom functions (6).
translator.rsNested JSON, load errors, the lookup chain, user preferences (6).
conformance.rsThe suite above (1 test running all cases).

The README’s and rustdoc’s examples run as 5 doc tests. Run one group with cargo test -p dxn_i18n --test selection. CI (.github/workflows/ci.yml) runs on Linux, macOS and Windows: cargo fmt --check, a check that no UI framework is in the dependency tree, cargo clippy --all-targets --all-features -- -D warnings, cargo test --workspace --all-features, cargo check for wasm32-unknown-unknown, and cargo package --workspace --list. Separate jobs build the rustdoc with missing-docs as an error and check the minimum Rust version, 1.97.

#Test your own messages

The pattern in the library’s tests: format with a known locale and assert that no errors came out, so a bad option or a missing argument fails the test instead of passing with a fallback in the text.

use dxn_i18n::{Args, Message, MessageFormatter};
use dxn_intl::parse_locale;

fn render(tag: &str, source: &str, args: Args) -> String {
    let message = Message::parse(source).expect("the message parses");
    let formatted = MessageFormatter::new(parse_locale(tag).expect("a language tag")).format(&message, &args);
    assert!(formatted.errors.is_empty(), "{source}: {:?}", formatted.errors);
    formatted.text
}

assert_eq!(render("en", "Hi {$name}", Args::new().with("name", "Ana")), "Hi Ana");

Put calls like this in #[test] functions. For bundles, load the real resource files and render every message you care about, with Translator::try_format (Errors and fallbacks); the cookbook has a test that compares languages.

#Plural forms, per language

A table-driven test documents which forms a language needs and catches a missing one:

use dxn_i18n::{Args, Message};
use dxn_intl::parse_locale;

let artikli = Message::parse(
    ".input {$n :number} .match $n 0 {{nema artikala}} one {{{$n} artikl}} few {{{$n} artikla}} * {{{$n} artikala}}",
)?;
let hr = parse_locale("hr")?;
for (n, expected) in [(0, "nema artikala"), (1, "1 artikl"), (3, "3 artikla"), (5, "5 artikala"), (21, "21 artikl"), (22, "22 artikla")] {
    assert_eq!(artikli.format(&hr, &Args::new().with("n", n)), expected);
}

#Custom functions

Register the function on a FunctionRegistry, build a MessageFormatter with it and assert both the text and errors.is_empty(), as in Custom functions. Also assert the failure paths: pass the wrong kind of operand and check for FormatError::BadOperand.

Next: Limitations.