phrasebook 0.1.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.

The design is in docs/design.md.

Features

No feature is on by default. On docs.rs, items that need a feature are marked with it.

Feature Module What it provides
(none) Lang, Catalog, Source, Phrasebook, Phrases, Args, Value, args!, and the check test helpers
browser browser the language remembered in localStorage, the browser's preferred languages, and Phrases::store
yew023 yew023 yew 0.23: ImplicitClone and IntoPropValue impls, and the or_default and or_catalog helpers for components rendered without a provided handle
koine01 koine01 conversions between Lang and koine 0.1's Language, Country and Locale, a warning for declared languages koine does not know, and Builder::language_named_by_koine

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.

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
"),
]);

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");

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; an app that translates a library's messages in its own catalog checks parity with check::parity_with, naming the library catalogs.

With yew

phrasebook ships no components and no hooks: 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 calls store on it, with browser). Components read it with use_context::<Phrases>(), or ctx.link().context::<Phrases>(..) 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 = or_default(use_context::<Phrases>());

// A library's component, with its own catalog `L10N`.
let phrases = or_catalog(use_context::<Phrases>(), &L10N);

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.

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