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
//! [<img alt="github" src="https://img.shields.io/badge/github-udoprog/phrasebook-8da0cb?style=for-the-badge&logo=github" height="20">](https://github.com/udoprog/phrasebook)
//! [<img alt="crates.io" src="https://img.shields.io/crates/v/phrasebook.svg?style=for-the-badge&color=fc8d62&logo=rust" height="20">](https://crates.io/crates/phrasebook)
//! [<img alt="docs.rs" src="https://img.shields.io/badge/docs.rs-phrasebook-66c2a5?style=for-the-badge&logoColor=white&logo=data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDUxMiA1MTIiPjxwYXRoIGZpbGw9IiNmNWY1ZjUiIGQ9Ik00ODguNiAyNTAuMkwzOTIgMjE0VjEwNS41YzAtMTUtOS4zLTI4LjQtMjMuNC0zMy43bC0xMDAtMzcuNWMtOC4xLTMuMS0xNy4xLTMuMS0yNS4zIDBsLTEwMCAzNy41Yy0xNC4xIDUuMy0yMy40IDE4LjctMjMuNCAzMy43VjIxNGwtOTYuNiAzNi4yQzkuMyAyNTUuNSAwIDI2OC45IDAgMjgzLjlWMzk0YzAgMTMuNiA3LjcgMjYuMSAxOS45IDMyLjJsMTAwIDUwYzEwLjEgNS4xIDIyLjEgNS4xIDMyLjIgMGwxMDMuOS01MiAxMDMuOSA1MmMxMC4xIDUuMSAyMi4xIDUuMSAzMi4yIDBsMTAwLTUwYzEyLjItNi4xIDE5LjktMTguNiAxOS45LTMyLjJWMjgzLjljMC0xNS05LjMtMjguNC0yMy40LTMzLjd6TTM1OCAyMTQuOGwtODUgMzEuOXYtNjguMmw4NS0zN3Y3My4zek0xNTQgMTA0LjFsMTAyLTM4LjIgMTAyIDM4LjJ2LjZsLTEwMiA0MS40LTEwMi00MS40di0uNnptODQgMjkxLjFsLTg1IDQyLjV2LTc5LjFsODUtMzguOHY3NS40em0wLTExMmwtMTAyIDQxLjQtMTAyLTQxLjR2LS42bDEwMi0zOC4yIDEwMiAzOC4ydi42em0yNDAgMTEybC04NSA0Mi41di03OS4xbDg1LTM4Ljh2NzUuNHptMC0xMTJsLTEwMiA0MS40LTEwMi00MS40di0uNmwxMDItMzguMiAxMDIgMzguMnYuNnoiPjwvcGF0aD48L3N2Zz4K" height="20">](https://docs.rs/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`].
//!
//! <br>
//!
//! ## 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.
//!
//! <br>
//!
//! ## Example
//!
//! ```rust
//! 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.
//!
//! <br>
//!
//! ## 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.
//!
//! ```rust,ignore
//! // 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.
//!
//! <br>
//!
//! ## 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:
//!
//! ```sh
//! 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]:
//!
//! ```sh
//! cd examples/app && trunk serve
//! ```
//!
//! [`Args`]: https://docs.rs/phrasebook/latest/phrasebook/struct.Args.html
//! [`args!`]: https://docs.rs/phrasebook/latest/phrasebook/macro.args.html
//! [`browser`]: https://docs.rs/phrasebook/latest/phrasebook/browser/index.html
//! [`Catalog`]: https://docs.rs/phrasebook/latest/phrasebook/struct.Catalog.html
//! [`check`]: https://docs.rs/phrasebook/latest/phrasebook/check/index.html
//! [`docs/design.md`]: https://github.com/udoprog/phrasebook/blob/main/docs/design.md
//! [`examples/app`]: https://github.com/udoprog/phrasebook/tree/main/examples/app
//! [`examples/tour.rs`]: https://github.com/udoprog/phrasebook/blob/main/examples/tour.rs
//! [`koine01`]: https://docs.rs/phrasebook/latest/phrasebook/koine01/index.html
//! [`Lang`]: https://docs.rs/phrasebook/latest/phrasebook/struct.Lang.html
//! [`or_catalog`]: https://docs.rs/phrasebook/latest/phrasebook/yew023/fn.or_catalog.html
//! [`or_default`]: https://docs.rs/phrasebook/latest/phrasebook/yew023/fn.or_default.html
//! [`Phrasebook`]: https://docs.rs/phrasebook/latest/phrasebook/struct.Phrasebook.html
//! [`Phrases::msg`]: https://docs.rs/phrasebook/latest/phrasebook/struct.Phrases.html#method.msg
//! [`Phrases::text`]: https://docs.rs/phrasebook/latest/phrasebook/struct.Phrases.html#method.text
//! [`Phrases`]: https://docs.rs/phrasebook/latest/phrasebook/struct.Phrases.html
//! [`Source`]: https://docs.rs/phrasebook/latest/phrasebook/struct.Source.html
//! [`Value`]: https://docs.rs/phrasebook/latest/phrasebook/struct.Value.html
//! [`yew023`]: https://docs.rs/phrasebook/latest/phrasebook/yew023/index.html
//! [trunk]: https://trunkrs.dev

#![cfg_attr(phrasebook_docsrs, feature(doc_cfg))]
#![deny(missing_docs)]

mod args;
mod catalog;
mod lang;
mod phrasebook;
mod phrases;

#[cfg(feature = "browser")]
#[cfg_attr(phrasebook_docsrs, doc(cfg(feature = "browser")))]
pub mod browser;

#[cfg(feature = "koine01")]
#[cfg_attr(phrasebook_docsrs, doc(cfg(feature = "koine01")))]
pub mod koine01;

pub mod check;

#[cfg(feature = "yew023")]
#[cfg_attr(phrasebook_docsrs, doc(cfg(feature = "yew023")))]
pub mod yew023;

pub use self::args::{Args, Value, args};
pub use self::catalog::{Catalog, Source};
pub use self::lang::{Lang, ParseLangError};
pub use self::phrasebook::{Builder, Language, Phrasebook};
pub use self::phrases::Phrases;

#[cfg(test)]
mod tests;