Text

Fluent translations

Project Fluent translations (.ftl): messages formatted by locale with each language's plural forms, selectors on gender or anything else, terms and attributes, fallback from one locale to the next, and translation files checked and compared.

Version 1.0.0 · 5 functions · 4 crates

#translations#i18n#localization#locale

Functions

What use fluent.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use fluent_format, fluent_format_file from fluent (Quillo 0.6).

fluent_format(source: str, locale: str, id: str, args: any) → stror an error

The message id of the FTL text source formatted for locale with args ({ count: 3, name: "Ana" }): numbers choose plural forms by the locale's rules. id may name an attribute, "login.placeholder". An error value when the message is missing.

fluent_format_file(path: str, locales: list, id: str, args: any) → anyor an error

The message id from translation files: path has {locale} in it ("locales/{locale}.ftl"), and each of locales is tried in order (["fr-CA", "fr", "en"]) until one has the message; files that do not exist are skipped. { text, locale }, the locale it came from.

fluent_messages(source: str) → listor an error

The messages an FTL text defines, in order, with their attributes as "id.attribute": for checking a translation is complete.

fluent_check(source: str) → any

Whether an FTL text parses: { valid, errors, messages }, each error { line, kind }, for checking translators' files on upload.

fluent_missing(reference: str, translation: str) → anyor an error

What a translation lacks: the messages and attributes of reference (the source language's FTL) not in translation, and those translation has that reference does not: { missing, extra }.

Get it

$ quillo install fluent               # fluent.qwer into this folder$ quillo install fluent --example     # and its example, fluent.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/fluent.qwer$ curl -O https://quillolang.org/plugins/files/fluent.qwe$ curl -O https://quillolang.org/plugins/files/fluent_sample_en.ftl$ curl -O https://quillolang.org/plugins/files/fluent_sample_fr.ftl$ curl -O https://quillolang.org/plugins/files/fluent_sample_pl.ftl

Then, in your program, all of it or only what you call:

main.qwe
use fluent.qweruse fluent_format, fluent_format_file from fluent      // Quillo 0.6: only these

It uses these crates, which cargo fetches the first time the plugin is built:

[dependencies]
fluent-bundle = "=0.16.0"fluent-syntax = "=0.12.0"unic-langid = "0.9"serde_json = { version = "1", features = ["preserve_order"] }

Example

fluent.qwe
// A shop in English, French and Polish with Project Fluent's .ftl files: plural forms by each language's rules, a gender selector, attributes, and fallback to English for what a translation lacks.////   quillo run fluent.qwe      then open http://localhost:8080/cartuse "fluent.qwer"let FILES = "fluent_sample_{locale}.ftl"fn tr(locale, id, args) = fluent_format_file(FILES, [locale, "en"], id, args)?.textroute GET /cart {    {        en: [tr("en", "cart-items", { count: 1 }), tr("en", "cart-items", { count: 3 })],        fr: [tr("fr", "cart-items", { count: 0 }), tr("fr", "cart-items", { count: 2 })],        // Polish has three forms: 1, 2-4 (and 22-24...), and 5 and up.        pl: [tr("pl", "cart-items", { count: 1 }), tr("pl", "cart-items", { count: 3 }), tr("pl", "cart-items", { count: 5 }), tr("pl", "cart-items", { count: 22 })],        welcome_fr: tr("fr", "welcome", { name: "Ana" }),        photos: tr("en", "shared-photos", { user: "Ana", count: 3, gender: "female" }),        placeholder: tr("fr", "login.placeholder", {}),        // Polish has no login message yet: it comes from English.        fallback: fluent_format_file(FILES, ["pl", "en"], "login.title", {})?,        missing_pl: fluent_missing(read_file("fluent_sample_en.ftl"), read_file("fluent_sample_pl.ftl"))?,        inline: fluent_format("hello = Hello, { $who }!", "en", "hello", { who: "world" })?,        broken: fluent_check("hello = Hello, { $who\nbye = Bye"),    }}
$ quillo run fluent.qwe$ curl 'localhost:8080/cart'

Source

fluent.qwer
//! Project Fluent translations (.ftl files, as Firefox uses): messages formatted by locale with plurals ("one", "few", "many"...), selectors on gender or anything else, terms and attributes, falling back from "fr-CA" to "fr" to "en"; and translation files checked and compared.//! version: 1.0.0//!//! [dependencies]//! fluent-bundle = "=0.16.0"//! fluent-syntax = "=0.12.0"//! unic-langid = "0.9"//! serde_json = { version = "1", features = ["preserve_order"] }use fluent_bundle::{FluentArgs, FluentBundle, FluentResource, FluentValue};use fluent_syntax::ast::Entry;use serde_json::{json, Value};use unic_langid::LanguageIdentifier;const MAX_FILE: u64 = 10 * 1024 * 1024;fn resource(source: &str, name: &str) -> Result<FluentResource, String> {    FluentResource::try_new(source.to_string()).map_err(|(_, errs)| format!("{}: {}", name, errs.iter().map(|e| format!("{:?}", e.kind)).collect::<Vec<_>>().join("; ")))}fn args_of(args: &Value) -> FluentArgs<'static> {    let mut a = FluentArgs::new();    if let Some(o) = args.as_object() {        for (k, v) in o {            match v {                Value::Number(n) => a.set(k.clone(), FluentValue::from(n.as_f64().unwrap_or(0.0))),                Value::String(s) => a.set(k.clone(), FluentValue::from(s.clone())),                Value::Bool(b) => a.set(k.clone(), FluentValue::from(if *b { "true" } else { "false" })),                Value::Null => {}                other => a.set(k.clone(), FluentValue::from(other.to_string())),            }        }    }    a}fn locale(l: &str) -> Result<LanguageIdentifier, String> {    l.trim().parse().map_err(|_| format!("`{}` is not a locale (\"en\", \"fr-CA\")", l))}// The message `id` ("welcome", or "login.placeholder" for an attribute) formatted, or None when the bundle lacks it.fn format(bundle: &FluentBundle<FluentResource>, id: &str, args: &Value) -> Result<Option<String>, String> {    let (mid, attr) = match id.split_once('.') { Some((m, a)) => (m, Some(a)), None => (id, None) };    let Some(msg) = bundle.get_message(mid) else { return Ok(None) };    let pattern = match attr {        Some(a) => match msg.get_attribute(a) { Some(at) => at.value(), None => return Ok(None) },        None => match msg.value() { Some(p) => p, None => return Err(format!("`{}` has no value, only attributes", mid)) },    };    let fa = args_of(args);    let mut errors = vec![];    let s = bundle.format_pattern(pattern, Some(&fa), &mut errors);    Ok(Some(s.to_string()))}fn bundle_for(source: &str, loc: &str, name: &str) -> Result<FluentBundle<FluentResource>, String> {    let mut b = FluentBundle::new(vec![locale(loc)?]);    b.set_use_isolating(false);    b.add_resource(resource(source, name)?).map_err(|e| format!("{}: {:?}", name, e))?;    Ok(b)}/// The message `id` of the FTL text `source` formatted for `locale` with `args` (`{ count: 3, name: "Ana" }`): numbers choose plural forms by the locale's rules. `id` may name an attribute, `"login.placeholder"`. An error value when the message is missing.pub fn fluent_format(source: &str, locale: &str, id: &str, args: serde_json::Value) -> Result<String, String> {    let b = bundle_for(source, locale, "the FTL")?;    format(&b, id, &args)?.ok_or_else(|| format!("no message `{}`", id))}/// The message `id` from translation files: `path` has `{locale}` in it (`"locales/{locale}.ftl"`), and each of `locales` is tried in order (`["fr-CA", "fr", "en"]`) until one has the message; files that do not exist are skipped. `{ text, locale }`, the locale it came from.pub fn fluent_format_file(path: &str, locales: Vec<String>, id: &str, args: serde_json::Value) -> Result<Value, String> {    if !path.contains("{locale}") { return Err("the path holds `{locale}`, as in \"locales/{locale}.ftl\"".into()) }    for l in &locales {        let file = path.replace("{locale}", l);        let Ok(f) = std::fs::File::open(&file) else { continue };        let mut text = String::new();        std::io::Read::read_to_string(&mut std::io::Read::take(f, MAX_FILE), &mut text).map_err(|e| format!("{}: {}", file, e))?;        let b = bundle_for(&text, l, &file)?;        if let Some(s) = format(&b, id, &args)? { return Ok(json!({ "text": s, "locale": l })) }    }    Err(format!("no message `{}` for {}", id, locales.join(", ")))}/// The messages an FTL text defines, in order, with their attributes as `"id.attribute"`: for checking a translation is complete.pub fn fluent_messages(source: &str) -> Result<Vec<String>, String> {    let r = resource(source, "the FTL")?;    let mut out = Vec::new();    for e in r.entries() {        if let Entry::Message(m) = e {            out.push(m.id.name.to_string());            for a in &m.attributes { out.push(format!("{}.{}", m.id.name, a.id.name)) }        }    }    Ok(out)}/// Whether an FTL text parses: `{ valid, errors, messages }`, each error `{ line, kind }`, for checking translators' files on upload.pub fn fluent_check(source: &str) -> Value {    match FluentResource::try_new(source.to_string()) {        Ok(r) => json!({ "valid": true, "errors": [], "messages": r.entries().filter(|e| matches!(e, Entry::Message(_))).count() }),        Err((r, errs)) => {            let errors: Vec<Value> = errs.iter().map(|e| {                let line = source[..e.pos.start.min(source.len())].matches('\n').count() + 1;                json!({ "line": line, "kind": format!("{:?}", e.kind) })            }).collect();            json!({ "valid": false, "errors": errors, "messages": r.entries().filter(|e| matches!(e, Entry::Message(_))).count() })        }    }}/// What a translation lacks: the messages and attributes of `reference` (the source language's FTL) not in `translation`, and those `translation` has that `reference` does not: `{ missing, extra }`.pub fn fluent_missing(reference: &str, translation: &str) -> Result<Value, String> {    let a = fluent_messages(reference)?;    let b = fluent_messages(translation)?;    Ok(json!({        "missing": a.iter().filter(|x| !b.contains(x)).collect::<Vec<_>>(),        "extra": b.iter().filter(|x| !a.contains(x)).collect::<Vec<_>>(),    }))}
Language detection lang.qwer · 1.0.0Which of 69 languages a text is in, its script and how sure: route tickets, tag posts, pick a spell-checker.i18nlanguagetextsupport6 views · 6 downloadsTypography typography.qwer · 1.0.0Typed text set as a typographer would, by language: curly quotes (“ ”, « », „ “) and apostrophes, en and em dashes, ellipses, ×, ©, French spaces before ; : ! ?, and no widows; in plain text or HTML, code left alone.writingtexthtmllocale7 views · 10 downloadsLanguages languages.qwer · 1.0.0ISO 639 language codes and names in English and in the language itself, two- and three-letter codes read, and the best of your translations picked from Accept-Language.i18nformslocaleheaders7 views · 9 downloadsHijri calendar hijri.qwer · 1.0.0Islamic (Hijri) dates: a Gregorian date in Umm al-Qura, civil or observational reckoning, written in English and Arabic; a Hijri date back to the Gregorian; a month's first and last days; and the year's holy days.datescalendarlocalei18n7 views · 7 downloadsWorld calendars world_calendars.qwer · 1.0.0Dates in the world's other calendars, each way and with month names: Hebrew, Persian, Ethiopian, Coptic, Indian national, Thai Buddhist, Japanese eras (令和8年) and Taiwan's Minguo; and the Jewish holidays of a year.datescalendarlocalei18n8 views · 6 downloadsAccept-Language accept_language.qwer · 1.0.0The visitor's language from the Accept-Language header: read with its weights, and the best of your site's languages picked, en-GB matching en.i18nlanguagesheaderslocalization8 views · 8 downloads
Thread
87Useful? Like it, or add to its thread below.

Thread 0 posts

On Fluent translations: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.