mirror of
https://github.com/storytold/craft-libs.git
synced 2026-10-31 15:49:54 +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);
problems.iter().for_each(|p| log::warn!("translation catalog: {p}; that string shows in English"));
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 and returned byI18n::newfor the app to log; craft-i18n's CI rejects them before they ship. - 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 its shared catalog's
@id language.namerow, else its code; English is "English". - Right to left:
rtlis true forar,ckb,dv,fa,he,ps,sd,ug,urandyi; 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. - 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. - Search (
Folder):Folder::new(locale).fold(text)gives a key for case- and accent-insensitive matching (keep_accents()makes accents count) and a map back to the original text, soFolded::findreturns the byte range to highlight. Compatibility forms fold to their plain letters (fi, full-widthA),ßtoss,ςtoσ, digits of every script to ASCII (Layer ١٢findslayer 12); ignoring accents, Persian and Arabic letter forms match (کandك,یandي,ةandه,أandا); invisible format characters (soft hyphens, joiners, bidi isolates) are dropped. Turkish and Azerbaijani foldItoıwhen accents count; ignoring accents,I,i,İandıall match. - Sorting (
Collator, featurecollate): ICU4X collation for the locales craft-i18n ships, Persian and Hindi, with digits compared by value (Layer 2beforeLayer 10) and-u-co-pinyin/-u-co-strokefor Chinese. The data (754 KB) is regenerated bydata/generate-collation.sh; the feature adds about 1 MB to a release binary or wasm module (320 KB gzipped).