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:

  1. selected locale catalog
  2. default locale catalog
  3. 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.