Translations for Rust
MessageFormat2 messages, formatted in Rust.
dxn_i18n parses and formats Unicode MessageFormat 2 messages, loads them from JSON resources and finds the right bundle for the user's locale and its fallbacks. Plurals, numbers, money and dates come from dXn.Intl on ICU4X's CLDR data, so every platform, the browser included, shows the same text. It depends on no UI framework.
[dependencies]
dxn_i18n = { git = "https://github.com/dXnSix/dXn.I18n", tag = "v0.1.1" }
dxn_intl = { git = "https://github.com/dXnSix/dXn.Intl", tag = "v0.1.1" }use dxn_i18n::{Args, Bundle, Translator};
use dxn_intl::parse_locale;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut hr = Bundle::new(parse_locale("hr")?);
hr.add_json(r#"{ "cart": { "summary":
".input {$count :number} .match $count 0 {{Košarica je prazna.}} one {{{$count} artikl.}} few {{{$count} artikla.}} * {{{$count} artikala.}}"
} }"#)?;
let mut translator = Translator::new(parse_locale("en")?);
translator.add_bundle(hr);
translator.set_locale(parse_locale("hr-HR")?);
for count in [0, 1, 3, 5] {
let args = Args::new().with("count", count);
println!("{}", translator.format("cart.summary", &args));
}
Ok(())
}- Messages
- Unicode MessageFormat 2
- Conformance
- 462 Working Group cases pass
- Locale data
- CLDR via ICU4X
- Rust
- 1.97+ (edition 2024)
- Targets
- native and wasm32
- License
- MIT OR Apache-2.0
What you get
What you get
The whole of MessageFormat 2
Simple and complex messages, .input and .local declarations, matchers with any number of selectors, markup, fallbacks and bidi isolation, checked against the data model when a message is parsed.
Read more →Plurals for every CLDR language
Cardinal and ordinal categories, exact numbers and text keys pick the variant, so Croatian gets its few form and Arabic its six. The category reads the digits actually shown.
Read more →Numbers, money and dates in the user's style
:number, :integer, :percent, :currency, :offset, :date, :time and :datetime with their options, plus the user's -u-hc-h23 clock, -u-nu- digits and calendar preferences.
Read more →JSON bundles and a lookup chain
Nested JSON makes ids like cart.summary. A Translator walks from the user's locale through region parents and fallback locales to the default, and formats values in the matching language.
Read more →Markup as parts
format_to_parts returns text, values, fallbacks and markup separately, so {#b}bold{/b} and links render in your UI without string surgery.
Read more →Failures still produce text
A placeholder that cannot be resolved shows a fallback such as {$name}, and try_format returns the typed errors for logs and tests. A message that fails to load never hides the rest of its language.
Read more →Quick tour
#Pick the plural form for the language
A matcher lists variants by key. 0 is an exact number, one and few are the language’s plural categories, and * is the fallback every matcher needs.
use dxn_i18n::{Args, Message};
use dxn_intl::parse_locale;
let message = Message::parse(
".input {$count :number}
.match $count
0 {{Your cart is empty.}}
one {{One item in your cart.}}
* {{{$count} items in your cart.}}",
)?;
let en = parse_locale("en")?;
assert_eq!(message.format(&en, &Args::new().with("count", 1200)), "1,200 items in your cart.");
See Selecting variants for ordinals, text keys and several selectors.
#Format in the user’s locale and preferences
The locale carries the user’s region and Unicode extensions. A de-CH user sees Swiss separators in a de message, and -u-hc-h23 turns every time into a 24-hour one.
use dxn_i18n::{Args, Bundle, Translator};
use dxn_intl::{DateTime, parse_locale};
let mut translator = Translator::new(parse_locale("en")?);
for (tag, json) in [("en", r#"{ "at": "At {$t :time}." }"#), ("de", r#"{ "price": "{$p :number}" }"#)] {
let mut bundle = Bundle::new(parse_locale(tag)?);
bundle.add_json(json)?;
translator.add_bundle(bundle);
}
translator.set_locale(parse_locale("de-CH")?);
assert_eq!(translator.format("price", &Args::new().with("p", 1234.5)), "1'234.5");
translator.set_locale(parse_locale("en-US-u-hc-h23")?);
let at = Args::new().with("t", DateTime::new(2026, 10, 27, 15, 45, 0)?);
assert_eq!(translator.format("at", &at), "At 15:45.");
See The Translator for the lookup chain and Dates and times.
#Render markup without string surgery
Translators put {#link} where the link belongs in their language. format_to_parts gives you the pieces, with markup options resolved.
use dxn_i18n::{Args, Bundle, Translator};
use dxn_intl::parse_locale;
let mut en = Bundle::new(parse_locale("en")?);
en.add("terms", "Read the {#link href=$url}terms{/link} first.")?;
let mut translator = Translator::new(parse_locale("en")?);
translator.add_bundle(en);
let args = Args::new().with("url", "https://example.com/terms");
let parts = translator.format_to_parts("terms", &args).expect("the message exists").text;
assert_eq!(parts.len(), 5);
// [Text("Read the "), Markup { kind: Open, name: "link", options: [("href", ..)] },
// Text("terms"), Markup { kind: Close, name: "link", .. }, Text(" first.")]
See Markup and parts for a renderer.
#Errors that stay on screen
Formatting never panics and never returns nothing. What cannot be resolved shows as a fallback, and the errors come back next to the text.
use dxn_i18n::{Args, FormatError, Message, MessageFormatter};
use dxn_intl::parse_locale;
let message = Message::parse("Hello {$name}, you have {$n :number} messages.")?;
let formatter = MessageFormatter::new(parse_locale("en")?);
let formatted = formatter.format(&message, &Args::new().with("n", "many"));
assert_eq!(formatted.text, "Hello {$name}, you have {$n} messages.");
assert_eq!(formatted.errors[0], FormatError::UnresolvedVariable("name".to_string()));
See Errors and fallbacks.
#Add your own function
A function is a plain fn that returns a value able to format itself and, if you like, to select a variant. Register it under a namespace.
use std::sync::Arc;
use dxn_i18n::{
Args, FunctionContext, FunctionError, FunctionRegistry, Message, MessageFormatter, Options,
ResolvedValue, TextValue, Value,
};
use dxn_intl::parse_locale;
fn shout(
_: &mut FunctionContext<'_>,
operand: Option<&Value>,
_: &Options,
) -> Result<Arc<dyn ResolvedValue>, FunctionError> {
let text = operand.and_then(Value::as_str).unwrap_or_default();
Ok(Arc::new(TextValue(text.to_uppercase())))
}
let mut functions = FunctionRegistry::default();
functions.register("my:shout", shout);
let formatter = MessageFormatter::new(parse_locale("en")?).with_functions(Arc::new(functions));
let message = Message::parse("Welcome, {$name :my:shout}!")?;
assert_eq!(formatter.format(&message, &Args::new().with("name", "ana")).text, "Welcome, ANA!");
See Custom functions.
Where next
Get started
Add the dependency and format your first plural message.
Continue →Message syntax
Placeholders, declarations and the parse errors you can get.
Continue →The Translator
How a message is found and which locale formats it.
Continue →Cookbook
Embedding translations, checking them in a test, sharing a translator.
Continue →