mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-11 04:52:13 +00:00
Format::parse reads what a Format writes: its digits, its decimal and grouping separators (the system's included) and the locale's Latin marks in an Arabic-Indic format, plus every digit set. parse_number is Format::from_tag(locale).parse, so the decimal marks come from CLDR's data alone and the hand-made table is gone. A lone . in a comma locale is the decimal mark when it can't be grouping (1.5, 0.250, 1234.5, .5); 1.234 and 1.234.567 stay grouped in German. The same holds for , in a point locale. Percent signs and units are documented as the caller's to strip.
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),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. - 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 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 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. - 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. - 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. - 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 isen-US;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-and-u-rg-. 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.