formatjs_intl is the application-level Rust runtime. It combines message
descriptors, translation catalogs, ICU4X locale negotiation, and a shared
compiled-message cache.
Installation#
[dependencies]
formatjs_intl = "0.1"
formatjs_icu_messageformat = "0.1"
serde_json = "1"
The second dependency provides Value and Values for runtime arguments.
serde_json loads optional precompiled catalogs.
Create a catalog and format a message#
use formatjs_icu_messageformat::{Value, Values};
use formatjs_intl::{Intl, IntlCache, MessageCatalog, MessageDescriptor, message_descriptor};
use std::{collections::HashMap, sync::Arc};
const TASKS: MessageDescriptor = message_descriptor!(
id: "tasks.count",
default_message: "{count, plural, one {# task} other {# tasks}}",
description: "Task count"
);
fn main() -> Result<(), formatjs_intl::Error> {
let mut catalog = MessageCatalog::new();
catalog.insert("en", HashMap::new())?;
catalog.insert(
"fr",
HashMap::from([(
"tasks.count".to_owned(),
"{count, plural, one {# tâche} other {# tâches}}".to_owned(),
)]),
)?;
let catalog = Arc::new(catalog);
let cache = Arc::new(IntlCache::new());
let intl = Intl::try_new(["fr-CA", "fr"], "en", catalog, cache)?;
let values: Values =
HashMap::from([("count".to_owned(), Value::from(2_i64))]);
assert_eq!(intl.locale().to_string(), "fr");
assert_eq!(intl.format_message_to_string(TASKS, &values)?, "2 tâches");
Ok(())
}
The default locale must have a catalog, but that catalog may be empty. Message lookup falls back in this order:
- selected locale catalog
- default locale catalog
- descriptor
default_message
Request-scoped locales#
Build MessageCatalog and IntlCache once, then create a lightweight Intl
for each request:
let intl = Intl::try_new(
requested_locales,
"en",
catalog.clone(),
cache.clone(),
)?;
Requested locales must already be ordered by preference. ICU4X performs locale
fallback against available catalogs, such as fr-CA to fr. Parsing an HTTP
Accept-Language header remains the responsibility of the application or web
framework adapter.
Descriptors and IDs#
message_descriptor! accepts compile-time string literals:
const GREETING: MessageDescriptor = message_descriptor!(
default_message: "Hello, {name}!",
description: "Greeting shown after sign-in"
);
Without an explicit id, it generates
[sha512:contenthash:base64:10] from default_message and description. The
native FormatJS CLI recognizes the same macro and emits the same ID:
formatjs extract "src/**/*.rs" --out-file messages.json
Use id: "greeting" when a stable semantic ID is preferred.
Inline formatting#
format_message! creates an extractable descriptor and formats it in one
expression. values is optional:
use formatjs_intl::format_message;
let title = format_message!(
&intl,
default_message: "Approve to continue",
description: "Approval card title",
);
let greeting = format_message!(
&intl,
default_message: "Hello, {name}!",
description: "Greeting shown after sign-in",
values: &values,
);
The macro returns String. It uses normal catalog and default-message fallback.
If cache infrastructure prevents formatting, it reports the error through
with_on_error and returns default_message verbatim. Use
format_message_to_string when explicit Result handling is required.
Shared cache#
IntlCache stores parsed messages by source string. Share one Arc<IntlCache>
across request-scoped Intl values to avoid reparsing translations and default
messages.
Precompiled catalogs#
Compile translations to FormatJS AST JSON to remove runtime message parsing:
formatjs compile translations/fr.json --out-file translations/fr.compiled.json --ast
Deserialize that output and insert it directly:
use formatjs_intl::{MessageCatalog, PrecompiledMessages};
let messages: PrecompiledMessages =
serde_json::from_str(include_str!("../translations/fr.compiled.json"))?;
let mut catalog = MessageCatalog::new();
catalog.insert_precompiled("fr", messages)?;
These messages are ready to format when inserted and bypass both runtime
parsing and IntlCache.
See the complete formatjs_intl API.