Accept-Language
The 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.
Version 1.0.0 · 3 functions · 0 crates
Functions
What use accept_language.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use accept_language_parse, accept_language_pick from accept_language (Quillo 0.6).
accept_language_parse(header: str) → listThe header's languages, best first: [{ tag, q }]. "fr-CH, fr;q=0.9, en;q=0.8, *;q=0.5" gives fr-CH 1, fr 0.9, en 0.8, * 0.5.
accept_language_pick(header: str, supported: list, default: str) → strThe best of supported (your site's languages, as you spell them) for the header, or default when none fits (or the header is empty). Wanted languages are tried in order of weight; for each, an exact match beats a broader one (en for en-GB), which beats a narrower or sibling one (pt-BR for pt). * takes the first supported.
accept_language_rank(header: str, supported: list) → listEvery supported language with how much the visitor wants it, best first: [{ tag, q }], q 0 for those the header does not mention. For a language menu with the likely ones on top.
Get it
$ quillo install accept_language # accept_language.qwer into this folder$ quillo install accept_language --example # and its example, accept_language.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/accept_language.qwer$ curl -O https://quillolang.org/plugins/files/accept_language.qweThen, in your program, all of it or only what you call:
use accept_language.qweruse accept_language_parse, accept_language_pick from accept_language // Quillo 0.6: only theseIt uses these crates, which cargo fetches the first time the plugin is built:
Example
// The home page in the visitor's language, picked from the browser's Accept-Language header,// with a language menu ordered by what the visitor reads.//// quillo run accept_language.qwe then curl -H "Accept-Language: nl-BE,nl;q=0.9,en;q=0.8" localhost:8080/use "accept_language.qwer"let SITE = ["en", "nl", "fr", "pt-BR", "de"]let HELLO = { en: "Welcome", nl: "Welkom", fr: "Bienvenue", "pt-BR": "Bem-vindo", de: "Willkommen" }route GET / { let lang = accept_language_pick(request.headers["accept-language"] ?? "", SITE, "en") { lang, greeting: HELLO[lang], menu: accept_language_rank(request.headers["accept-language"] ?? "", SITE) | map((l) => l.tag) }}route GET /cases = { belgian: accept_language_pick("nl-BE,nl;q=0.9,en;q=0.8", SITE, "en"), portuguese: accept_language_pick("pt-PT,pt;q=0.8", SITE, "en"), swiss_german: accept_language_pick("gsw;q=1, de-CH;q=0.8", SITE, "en"), refused: accept_language_pick("fr;q=0, it", SITE, "en"), weights: accept_language_parse("fr-CH, fr;q=0.9, en;q=0.8, *;q=0.5, de;q=bad"),}$ quillo run accept_language.qwe$ curl 'localhost:8080/'$ curl 'localhost:8080/cases'Source
//! The visitor's language from the browser's Accept-Language header: the header read with its q-weights, and the best of the languages your site has picked, matching `en-GB` to `en` and `pt` to `pt-BR` as people expect.//! version: 1.0.0//!//! [dependencies]use serde_json::{json, Value};/// The header's languages with their weights, best first, ties in the order given; q=0 entries (refused) left out, malformed ones skipped.fn parse(header: &str) -> Vec<(String, f64)> { let mut out: Vec<(String, f64, usize)> = Vec::new(); for (i, part) in header.split(',').take(100).enumerate() { let mut bits = part.split(';'); let tag = bits.next().unwrap_or("").trim(); if tag.is_empty() || tag.len() > 35 || !tag.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '*') { continue; } let mut q = 1.0; for b in bits { if let Some(v) = b.trim().strip_prefix("q=").or_else(|| b.trim().strip_prefix("Q=")) { q = v.trim().parse::<f64>().unwrap_or(-1.0); } } if (0.0..=1.0).contains(&q) && q > 0.0 { out.push((tag.replace('_', "-"), q, i)); } } out.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal).then(a.2.cmp(&b.2))); out.into_iter().map(|(t, q, _)| (t, q)).collect()}/// How well a supported tag fits a wanted one: 3 the same, 2 the wanted is more precise (`en-GB` wanted, `en` there), 1 the supported is (`pt` wanted, `pt-BR` there), 0 not at all.fn fit(wanted: &str, have: &str) -> u8 { let (w, h) = (wanted.to_ascii_lowercase(), have.to_ascii_lowercase()); if w == h { 3 } else if w.starts_with(&format!("{}-", h)) { 2 } else if h.starts_with(&format!("{}-", w)) { 1 } else if w.split('-').next() == h.split('-').next() { // Same language, other region: en-GB wanted, en-US there. 1 } else { 0 }}/// The header's languages, best first: `[{ tag, q }]`. `"fr-CH, fr;q=0.9, en;q=0.8, *;q=0.5"` gives fr-CH 1, fr 0.9, en 0.8, * 0.5.pub fn accept_language_parse(header: &str) -> Vec<Value> { parse(header).into_iter().map(|(t, q)| json!({ "tag": t, "q": q })).collect()}/// The best of `supported` (your site's languages, as you spell them) for the header, or `default` when none fits (or the header is empty)./// Wanted languages are tried in order of weight; for each, an exact match beats a broader one (`en` for `en-GB`), which beats a narrower or sibling one (`pt-BR` for `pt`). `*` takes the first supported.pub fn accept_language_pick(header: &str, supported: Vec<String>, default: &str) -> String { for (want, _) in parse(header) { if want == "*" { if let Some(s) = supported.first() { return s.clone(); } continue; } let best = supported.iter().map(|s| (fit(&want, s), s)).filter(|(f, _)| *f > 0).max_by_key(|(f, _)| *f); if let Some((_, s)) = best { return s.clone(); } } default.to_string()}/// Every supported language with how much the visitor wants it, best first: `[{ tag, q }]`, `q` 0 for those the header does not mention. For a language menu with the likely ones on top.pub fn accept_language_rank(header: &str, supported: Vec<String>) -> Vec<Value> { let wanted = parse(header); let mut scored: Vec<(String, f64, u8, usize)> = supported .iter() .enumerate() .map(|(i, s)| { let (q, f) = wanted.iter().filter_map(|(w, q)| { let f = if w == "*" { 1 } else { fit(w, s) }; if f > 0 { Some((*q, f)) } else { None } }).fold((0.0, 0u8), |best, x| if x.0 > best.0 || (x.0 == best.0 && x.1 > best.1) { x } else { best }); (s.clone(), q, f, i) }) .collect(); scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal).then(b.2.cmp(&a.2)).then(a.3.cmp(&b.3))); scored.into_iter().map(|(t, q, _, _)| json!({ "tag": t, "q": q })).collect()}Related
fluent.qwer · 1.0.0Project 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.8 views · 7 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.7 views · 9 downloadsDomains domains.qwer · 1.0.0Domain names read by the Public Suffix List: registrable domain, subdomain and suffix, same-site checks for redirects and cookies, and international names to punycode and back.7 views · 7 downloadsHTTP caching http_cache.qwer · 1.0.0HTTP caching as RFC 9111 says: Cache-Control read and written, ETags made, conditional requests answered 304, and a stored response's freshness, age and time to live in a browser or a CDN.7 views · 6 downloadsThread 0 posts
On Accept-Language: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.