phrasebook 0.2.0

Fluent localization for Rust web frontends: message catalogs per language, arguments and plurals, a remembered language choice in the browser.
Documentation

phrasebook

Fluent localization for Rust web frontends: message catalogs per language, formatting with arguments and plurals, a fallback language, and the user's chosen language remembered in the browser.

An app declares its languages and catalogs once, in a Phrasebook, and hands its components a Phrases handle: the phrasebook open at the current language. Components look messages up with Phrases::text and Phrases::msg. A library ships its own Catalog under a prefix, which an app adds to its phrasebook and can override or translate, and which the library falls back to when the app provides nothing.

Lang, Catalog and Source are defined in their own crate, phrasebook-catalog, and re-exported here. A library's catalog names that crate's Catalog, which changes far more rarely than phrasebook, so it survives phrasebook's breaking releases: an app moves to a new phrasebook without waiting for every library it uses to move with it.

The design is in docs/design.md, including which dependencies appear in the API and what is deliberately left breaking-prone.

Features

std is on by default and is what everything but Lang, Catalog and Source needs; the other features enable it. On docs.rs, items that need a feature are marked with it.

Feature Module What it provides
(none) Lang, Catalog and Source
std (default) check Phrasebook, Phrases, Args, Value, args!, and the check test helpers
alloc nothing yet; enabled by std
browser browser the language remembered in localStorage, the browser's preferred languages, Phrases::store, and numbers formatted in the handle's language
yew023 yew023 yew 0.23: ImplicitClone and IntoPropValue impls, and the or_default and or_catalog helpers for components rendered without a provided handle; turns on phrasebook-catalog's yew023 for the impls on Lang
koine03 koine03 conversions between Lang and koine 0.3's Language, Country and Locale, koine's English names of languages (koine03::name), and a warning for declared languages koine does not know; turns on phrasebook-catalog's koine03 for the conversions

Fluent and unic-langid are used underneath but appear nowhere in the API, so upgrading them is never a breaking change. A dependency that is not yet 1.0 and does appear in the API is named after its version in its feature and module, so a later version can be added beside it.

yew023 does not enable browser; an app in the browser enables both.

yew023 and koine03 forward to phrasebook-catalog's features of the same names, because only the crate that defines Lang may implement yew's and koine's traits for it.

Example

use phrasebook::{Catalog, Lang, Phrasebook, Source, args};

const SV_SE: Lang = Lang::new("sv-SE");

static APP: Catalog = Catalog::new("app", &[
    Source::new(Lang::EN_US, "
title = Mafia
players = { $count ->
    [one] One player
   *[other] { $count } players
}
"),
    Source::new(SV_SE, "
title = Maffia
"),
]);

// The first declared language is the fallback.
let book = Phrasebook::builder()
    .language(Lang::EN_US, "English")
    .language(SV_SE, "Svenska")
    .catalog(&APP)
    .build();

let phrases = book.phrases(SV_SE);

assert_eq!(&*phrases.text("title"), "Maffia");
// Not translated yet: the fallback language, logged once.
assert_eq!(phrases.msg("players", &args!(count = 1)), "One player");
// Not defined anywhere: the id, so the hole is visible.
assert_eq!(&*phrases.text("missing"), "missing");

let phrases = phrases.with_lang(Lang::EN_US);
assert_eq!(phrases.msg("players", &args!(count = "4")), "4 players");

Pass numbers as numbers, not as strings formatted beforehand. With browser, in the browser, a number is formatted in the handle's language (12,345 in English, 12 345 in Swedish), honouring NUMBER()'s options in the message; a string is shown as written. A number that is not a quantity, such as a year, is passed as a string, or formatted with NUMBER($year, useGrouping: "false"). Phrases::number formats a number shown outside a message the same way.

In an app's tests, check turns a catalog's problems (syntax errors, duplicate ids, ids outside the prefix, messages that do not format, and untranslated or extra messages) into failures. check::all runs every check of a catalog in every language at once, naming the library catalogs the app translates, and Phrasebook::has_catalog asserts that a library's catalog was added. check::coverage compares the ids the code asks for with the ones a catalog defines, both ways.

With yew

The context type is Phrases itself. The app's root owns the handle as app state and provides it with yew's own ContextProvider<Phrases>; on a language change it provides phrases.with_lang(lang) (and, with browser, calls browser::set_document_lang and store). Components read it with the use_phrases hook, or keep a PhrasesContext in a struct component.

// The app's root, in `view`.
html! {
    <ContextProvider<Phrases> context={self.phrases.clone()}>
        <Main />
    </ContextProvider<Phrases>>
}

// An app's component.
let phrases = use_phrases();

// A library's component, with its own catalog `L10N`.
let phrases = use_phrases_with(&L10N);

// A struct component, in `create`; each later handle arrives as a
// message, for `self.l10n.set(phrases)` in `update`.
let l10n = PhrasesContext::new(ctx.link(), ctx.link().callback(Msg::Phrases));

A library's component falls back to its own catalog for messages the app lacks, or to the catalog alone when nothing is provided. The yew023 module documents both forms in full, struct components included.

To put markup where a translation places an argument, such as a user's name in <strong>, yew023::html_around renders the message around an element (Phrases::msg_around gives the text on either side).

Examples

examples/tour.rs prints what an app's catalog, a library's catalog with a backstop, plurals and the fallback language produce, in English and Swedish:

cargo run --example tour

examples/app is a small Yew app: the root owns and provides the handle, a picker switches and remembers the language, and a library-style component falls back to its own catalog. It is its own cargo workspace, so it adds nothing to the crate's builds, and is built and served with trunk:

cd examples/app && trunk serve