mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-02 00:37:37 +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),trnandtrn_decimal(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. - Isolation: when a message or a value inserted into it has a right-to-left letter (bidi
class R or AL), or a value has a bidi control (LRE, RLE, PDF, LRO, RLO, LRI, RLI, FSI, PDI,
LRM, RLM, ALM),
fmtwraps each inserted value in a first-strong isolate (FSI…PDI), andtrn,file_sizeandmeasurewrap{n}in a left-to-right one (LRI…PDI), so a Latin file name in an Arabic sentence, a Hebrew name in an English one, or an ASCII file name with a right-to-left override can't reorder the words around it. Embeddings, overrides and isolates a value opens are closed inside its isolate, and a stray PDF or PDI in it is dropped. Wrap numbers, paths and codes inltrbefore passing them; a value that already is one isolate keeps it. Other text comes out without marks, as iffmtdidn't isolate at all. egui draws the marks as nothing. For text that leaves the interface (file names, logs, terminals, text parsed later), usefmt_plainorstrip_isolates. - 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 ICU4X's CLDR rules;
plural_form_countsholds the counts craft-i18n's checker expects.trn_decimalpicks the form from the digits shown (ICU4X plural operands), so English1.0is plural and French1,5singular. A count in a category integers never reach takes thefewform if there is one (Russian and Ukrainian decimals,1,5 файла), else the last. Rules are per language, sosr-latnandzh-hanttakesr's andzh's. CLDR'sptis Brazilian, soptusespt-PT's rules (0 is plural) andpt-brpt's. A language CLDR has no plural rules for shows English forms. - 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. - Reading numbers:
Format::parse(text)reads what the format writes (its digits, decimal and grouping separators, the system's included) and what people type or paste: any Unicode decimal digits (ASCII, Arabic-Indic, Persian, Devanagari, Bengali, Thai, full-width, …) from one set;٫as a decimal mark everywhere; grouping by the locale's other mark of.and,, spaces (NBSP, narrow NBSP),',’or٬, in threes or the Indian 2-2-3; a leading-,−or+; bidi marks ignored. A lone.in a comma locale is decimal when it can't be grouping (1.5,0.250,1234.5), while1.234stays 1234 in German; the same holds for,in a point locale. Percent signs and units (50 %,12 px) are not read: strip them first. Other input that doesn't fit is an error, never a guess.parse_number(text, locale)is the same forFormat::from_tag(locale). - 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. - Sizes and units:
file_size(lang, bytes, style)writes1.5 MBor512 bytes;FileSizepicks steps of 1024 named KB (BINARY), 1024 named KiB (IEC) or 1000 named KB (DECIMAL), the decimals per unit, no decimals from a given value up, the smallest and largest unit (below the smallest, sizes round up), andBor words for bytes.measure(lang, value, decimals, "{n} px")writes any other measure. Unit names are rows in contextunit, keyed by the English template ({n} MB→{n} Mo,{n}%→{n} %) and read withget, so a plainBrow (bold) is never used; bytes in words are the@pluralrow{n} byte|{n} bytes. Numbers are written byI18n::format. - Numbers:
Formatwrites numbers for aRegionon ICU4X's decimal formatter:integer(1.234.567, Indian12,34,567),number(value, decimals)andpercent(ratio, decimals)(50 %, Turkish%50). It follows the region, not the interface language, with the locale's digits (١٬٢٣٤٫٥in Egypt,۱٬۲۳۴in Iran) or, withDigits::Latin,0–9. With Latin digits, a decimal or grouping separator the system sets (Windows' and macOS's own settings) replaces the locale's.I18n::formatis the formattrn,file_sizeandmeasureuse: the system region's untilI18n::set_format(the user's region setting or digit choice), and again afterI18n::reset_format. A count intrnis therefore grouped and in the region's digits (1.234 Dateien,١٬٢٣٤ ملفًا); for names written into documents or files (Copy 1234), usetrn_plain, which writes0–9without grouping or isolate marks. - CLDR data: decimal symbols, digits and plural rules (
data/numbers.postcard, about 10 KB) and percent patterns (data/locales.rs) cover every CLDR locale;data/generate.shrebuilds both from CLDR. The ICU4X crates are pinned to one minor version (~2.3), the one the data is generated for. The ICU4X code and data add about 90 KB to a stripped release build and 80 KB to wasm. - Formatting region:
Regionholds how numbers, times and measurements are written, apart from the interface language: the number and time locales (BCP 47), metric or US measurement, A4 or Letter, and the decimal and grouping separators (or no grouping) and clock when the system sets them; otherwise the number locale's CLDR data decides.Region::system()reads it once: Windows' regional format with the user's changes (HKCU\Control Panel\International), Apple's current locale (en_PL,en_GB@rg=uszzzz),LC_ALLor elseLC_NUMERIC,LC_TIME,LC_MEASUREMENTandLC_PAPEReach on its own overLANGon Unix (Cthere is English on A4, metric, with a.decimal and no grouping;LANG=Cgives way to the preferred language), the preferred language on the web and Android.Region::from_settingtakes the app's saved choice (autoor a tag likepl-PL). Paper and measurement follow CLDR's territory lists; tags may carry-u-ms-,-u-hc-,-u-rg-and-u-nu-(Apple's@numbers=), and a chosen digit system stays in the number locale. Ported from WordCraft'sRegion. - 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).