mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-09 20:12:57 +00:00
craft-i18n
Translations for the Craft apps: lookup with fallback, plurals, language names and the system locale, over the catalogs of craft-i18n (the data repository, same name).
The catalogs are a fully optional build input. An app's build.rs embeds them from
CRAFT_I18N_DIR as a static CRAFT_I18N: &[(locale, shared catalog, app catalog)], empty when the
variable is unset; the recipe is in craft-i18n's
docs/integration.md.
This crate never reads files.
Wiring an app
use std::cell::Cell;
use std::sync::LazyLock;
include!(concat!(env!("OUT_DIR"), "/craft_i18n.rs")); // pub static CRAFT_I18N: &craft_i18n::Embedded
pub static I18N: LazyLock<craft_i18n::I18n> = LazyLock::new(|| {
let offered: Vec<&str> = LANGUAGES.iter().map(|l| l.code).collect(); // the app's own list
let (i18n, problems) = craft_i18n::I18n::new(CRAFT_I18N, &offered); // parses nothing yet
problems.iter().for_each(|p| log::warn!("translation catalog: {p}"));
i18n
});
thread_local! { static CURRENT: Cell<craft_i18n::Lang> = const { Cell::new(craft_i18n::Lang::EN) }; }
macro_rules! tl { ($s:expr) => { $crate::I18N.t($crate::CURRENT.get(), $s) }; }
The app keeps its tl! macro, its language setting ("auto" → I18N.system_lang(), otherwise
I18N.from_code) and its picker (I18N.languages(), I18N.name(lang)).
Behaviour
- Offered locales:
I18n::newtakes the codes the app offers (its own language list), so an app never offers a language its interface isn't ready for (Arabic without right-to-left layout, say).languages(),from_code,match_tagandsystem_langonly return offered locales; an embedded locale that isn't offered still serves as a fallback (ptfor an offeredpt-br). - Fallback: locale by locale along the chain,
pt-br→pt→ the English source text. In each locale, app rows override shared ones, andtc/tidtry their context or@idrow before the plain row, sopt-br's plain row beatspt'smenurow. Only a region falls back (pt-br→pt,zh-hant-hk→zh-hant);sr-latnnever falls back to Cyrillicsr. A missing translation never errors. - Lookups:
t(plain),tc(with a context such asmenuor@msg, else plain),tid(an@idcommand id, else the English label),trn(a count,@pluralrows,{n}filled),get(one context along the locale chain, sopt-brcan return aptrow, but never the plain row or English: a key cap's name underkey, or "is there a translation?"), andfmtfor{name}placeholders, filled in one pass. - Bad rows (malformed, an unknown
@context, a wrong plural form count, a row defined twice, of which the first is kept) and bad locales (en,pseudo, a code embedded twice in any case) are skipped; craft-i18n's CI rejects them before they ship.I18n::newparses nothing: it returns the bad locales, and each locale's rows are parsed (and its bad rows logged withlog::warn!) on the first lookup that reaches it, including as a fallback.I18n::check_all()parses every locale and returns all problems (<locale>/<common|app>: line N: …), for a test. - Plurals: forms are listed in CLDR order (zero, one, two, few, many, other) for the categories
whole numbers 0 to 1000 reach, from
intl_pluralrules; a count in any other category takes the last form. CLDR'sptis Brazilian, soptusespt-PT's rules (0 is plural) andpt-brpt's.intl_pluralrulesis pinned to the version craft-i18n's checker uses, andplural_form_countsholds the same counts. - Names: a language's name comes from the app catalog's
@id language.namerow, else the shared catalog's, else its code; English is "English". - Scripts and direction: for any language tag,
scriptslists the ISO 15924 scripts its interface text is written in (ja→ Hani, Hira, Kana;sr-latn→ Latn;pa-PK→ Arab),directionis right to left when the main script is (Arabic, Hebrew, Syriac, Thaana), andhangives the regional forms of its Han characters for picking a CJK face:zh-CN/zh-SG/zh-Hans→ SC,zh-TW/zh-US/zh-Hant→ TC,zh-HK/zh-MO→ HK,ja→ JP,ko→ KR.I18n::rtlisdirectionof the language's code; putting text into display order is the app's job. - System locale:
system_langmatches the system's list once and caches it: on desktop UnixLANGUAGE,LC_ALL,LC_MESSAGESandLANG, read raw; elsewheresys-locale's (macOS and Windows preferred languages, the browser's on the web).fr_CA.UTF-8→fr,zh-TW→zh-hant,pt-PT→pt-brwhen that is the only Portuguese,C/POSIX→ English. A script is never crossed:sr_RS@latinandsr-Latn-RSpicksr-latn, neversr, whilesr-Cyrl-RSpickssr. A region that implies a script counts as that script:pa-PKpickspa-arab, not Gurmukhipa. - Pseudo locale (
pseudo, debug builds only, never inlanguages()): every lookup,getincluded, comes back as⟦…⟧, line by line, so text drawn without the marks never went through translation.looked_up()lists the source texts looked up so far, for an audit against a complete catalog.