use mf2::native::{Error as NativeError, LocaleSource, NativeI18n};
use mf2::{
BidiStrategy, CatalogFile, Compiled, Corpus, Dir, ErrorSink, Formatter, Message, PartSink,
Registry, Sink,
};
static REGISTRY: Registry = Registry::EMPTY;
struct Only;
impl Message for Only {
fn write(&self, f: &Formatter<'_>, out: &mut dyn Sink, errs: &mut dyn ErrorSink) {
f.write(Compiled::ID, &[], out, errs);
}
fn parts(&self, f: &Formatter<'_>, out: &mut dyn PartSink, errs: &mut dyn ErrorSink) {
f.parts(Compiled::ID, &[], out, errs);
}
}
fn corpus(
messages: &[(&'static str, &str)],
embed: bool,
) -> (&'static Corpus, Vec<(&'static str, Vec<u8>)>) {
let mut hash = None;
let mut locales = Vec::new();
let mut files = Vec::new();
let mut raw = Vec::new();
for &(tag, source) in messages {
let compiled = mf2::compile_str(source, tag).expect("the test message compiles");
let h = compiled.catalog.manifest_hash();
assert_eq!(*hash.get_or_insert(h), h, "one manifest");
let bytes: &'static [u8] = Box::leak(compiled.catalog.as_bytes().to_vec().into());
let name: &'static str =
Box::leak(format!("{tag}.{}.mf2b", mf2_catalog::content_hash(bytes)).into_boxed_str());
locales.push((tag, Dir::Ltr));
files.push(CatalogFile::new(tag, name, embed.then_some(bytes)));
raw.push((name, bytes.to_vec()));
}
let corpus = Corpus::new(
messages[0].0,
hash.unwrap_or_default(),
Box::leak(locales.into()),
®ISTRY,
Box::leak(files.into()),
)
.with_language_matching(mf2::LanguageMatching::cldr());
(Box::leak(Box::new(corpus)), raw)
}
#[test]
fn embedded_catalogs_format_and_switch_locale() {
let (corpus, raw) = corpus(&[("en", "Welcome"), ("fr", "Bienvenue")], true);
let mut i18n = NativeI18n::embedded(corpus).expect("embedded catalogs load");
i18n.set_locale("FR_ca").expect("fr-CA falls back to fr");
assert_eq!(i18n.locale(), "fr");
assert_eq!(i18n.locale_source(), LocaleSource::Explicit);
assert_eq!(i18n.format(&Only), "Bienvenue");
let en = format!("en.{}.mf2b", mf2_catalog::content_hash(&raw[0].1));
assert_eq!(i18n.catalog_file_name("en"), Some(en.as_str()));
assert_eq!(i18n.available_locales().collect::<Vec<_>>(), ["en", "fr"]);
}
#[test]
fn an_unsupported_locale_is_an_error_and_changes_nothing() {
let (corpus, _) = corpus(&[("en", "Welcome")], true);
let mut i18n = NativeI18n::embedded(corpus).expect("embedded catalog loads");
let before = (i18n.locale(), i18n.locale_source());
assert!(matches!(
i18n.set_locale("xx"),
Err(NativeError::UnknownLocale(tag)) if tag == "xx"
));
assert_eq!((i18n.locale(), i18n.locale_source()), before);
}
#[test]
fn placeholders_are_not_isolated_unless_asked() {
let (corpus, _) = corpus(&[("en", ".local $x = {|Bob|} {{Hi {$x}}}")], true);
let mut i18n = NativeI18n::embedded(corpus).expect("embedded catalog loads");
assert_eq!(i18n.bidi(), BidiStrategy::None);
assert_eq!(i18n.format(&Only), "Hi Bob");
i18n.set_bidi(BidiStrategy::Default);
assert_eq!(i18n.format(&Only), "Hi \u{2068}Bob\u{2069}");
}
#[test]
fn a_files_corpus_is_not_embedded() {
let (corpus, _) = corpus(&[("en", "Welcome")], false);
assert!(matches!(
NativeI18n::embedded(corpus),
Err(NativeError::NotEmbedded(tag)) if tag == "en"
));
}
#[test]
fn external_catalogs_load_from_a_directory() {
let (corpus, raw) = corpus(&[("en", "Welcome")], false);
let root = std::env::temp_dir().join(format!("mf2-native-test-{}", std::process::id()));
std::fs::create_dir_all(&root).expect("temporary directory is created");
for (name, bytes) in &raw {
std::fs::write(root.join(name), bytes).expect("catalog is written");
}
let loaded = NativeI18n::from_directory(corpus, &root);
let missing = NativeI18n::from_directory(corpus, root.join("nowhere"));
std::fs::remove_dir_all(&root).expect("temporary directory is removed");
assert_eq!(loaded.expect("catalog loads").format(&Only), "Welcome");
assert!(matches!(missing, Err(NativeError::Io { .. })));
}
struct TempDir(std::path::PathBuf);
impl TempDir {
fn with(label: &str, files: &[(&str, &[u8])]) -> TempDir {
let root = std::env::temp_dir().join(format!("mf2-native-{label}-{}", std::process::id()));
std::fs::create_dir_all(&root).expect("temporary directory is created");
for (name, bytes) in files {
std::fs::write(root.join(name), bytes).expect("catalog is written");
}
TempDir(root)
}
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
#[test]
fn a_renamed_catalog_from_a_text_only_rebuild_is_refused() {
let (old, old_raw) = corpus(&[("en", "Welcome")], false);
let (new, new_raw) = corpus(&[("en", "Welcome back")], false);
assert_eq!(
old.manifest_hash(),
new.manifest_hash(),
"a text-only rebuild"
);
let (new_name, new_bytes) = &new_raw[0];
let (old_name, old_bytes) = &old_raw[0];
assert_ne!(new_name, old_name);
let fresh = TempDir::with("fresh", &[(*new_name, new_bytes.as_slice())]);
let loaded = NativeI18n::from_directory(new, &fresh.0).expect("the new build's catalog loads");
assert_eq!(loaded.format(&Only), "Welcome back");
let renamed = TempDir::with("renamed", &[(*new_name, old_bytes.as_slice())]);
let refused = NativeI18n::from_directory(new, &renamed.0);
let old_hash = mf2_catalog::content_hash(old_bytes);
match refused {
Err(NativeError::ContentMismatch { path, actual }) => {
assert!(path.ends_with(new_name), "the path names the file read");
assert_eq!(actual, old_hash, "the hash of the bytes read");
}
Err(other) => panic!("refused, but as {other:?}"),
Ok(i18n) => panic!(
"a catalog from another build loaded and prints {:?}",
i18n.format(&Only)
),
}
}
#[test]
fn a_catalog_for_another_locale_is_rejected() {
let (good, _) = corpus(&[("en", "Welcome")], true);
let bytes = good.catalogs()[0].bytes();
let files: &'static [CatalogFile] =
Box::leak(Box::new([CatalogFile::new("de", "de.mf2b", bytes)]));
let wrong = Box::leak(Box::new(Corpus::new(
"de",
good.manifest_hash(),
&[("de", Dir::Ltr)],
®ISTRY,
files,
)));
assert!(matches!(
NativeI18n::embedded(wrong),
Err(NativeError::LocaleMismatch { expected, actual }) if expected == "de" && actual == "en"
));
}
#[test]
fn jiffs_instants_and_civil_dates_are_date_arguments() {
use jiff::civil::date;
use jiff::tz::{Offset, TimeZone};
use mf2::{ArgValue, IntoArg};
let shown = |value: ArgValue| format!("{value:?}");
let ts = jiff::Timestamp::new(1, 500_900_000).expect("a timestamp");
let one_and_a_half = "DateTime(DateTimeValue(1970-01-01T00:00:01.500 offset 0))";
assert_eq!(shown(ts.into_arg()), one_and_a_half);
assert_eq!(shown((&ts).into_arg()), one_and_a_half);
let before = jiff::Timestamp::new(0, -1_500_000).expect("a timestamp");
assert_eq!(
shown(before.into_arg()),
"DateTime(DateTimeValue(1969-12-31T23:59:59.998 offset 0))"
);
let dt = date(2026, 9, 29).at(12, 30, 5, 123_456_789);
assert_eq!(
shown(dt.into_arg()),
"DateTime(DateTimeValue(2026-09-29T12:30:05.123))"
);
assert_eq!(
shown(date(2026, 9, 29).into_arg()),
"DateTime(DateTimeValue(2026-09-29T00:00:00.000))"
);
let paris = dt
.in_tz("Europe/Paris")
.expect("the bundled database has Paris");
let in_paris = "DateTime(DateTimeValue(2026-09-29T12:30:05.123 offset 7200 Europe/Paris))";
assert_eq!(shown(paris.clone().into_arg()), in_paris);
assert_eq!(shown(ArgValue::from(&paris)), in_paris);
let fixed = dt
.to_zoned(TimeZone::fixed(Offset::constant(-5)))
.expect("a fixed offset");
assert_eq!(
shown(fixed.into_arg()),
"DateTime(DateTimeValue(2026-09-29T12:30:05.123 offset -18000))"
);
}