Cookbook / Requests and responses

Language negotiation

Pick the best supported language per request from Accept-Language, a query parameter or a cookie.

Locales chooses one of your supported languages for every request: an explicit choice first (a ?lang= parameter or a cookie from your language switcher), then the best match for the browser's Accept-Language, then your default.

src/main.rs
use axeon::locale::Locales;
use axeon::Server;

fn main() {
    let mut app = Server::new();

    // The first language is the default.
    app.middleware(Locales::new(["en", "zh-CN", "fr"]).query_param("lang").cookie("lang"));

    app.get("/hello", |req| async move {
        match req.locale().map(|locale| locale.as_str()) {
            Some("zh-CN") => "你好",
            Some("fr") => "Bonjour",
            _ => "Hello",
        }
    });

    app.listen("127.0.0.1:3000").expect("server failed to start");
}
Terminal
curl -si http://127.0.0.1:3000/hello -H 'accept-language: fr-CA,fr;q=0.9,en;q=0.5'    # Bonjour
curl -s  http://127.0.0.1:3000/hello -H 'accept-language: zh'                          # 你好 (zh matches zh-CN)
curl -s  "http://127.0.0.1:3000/hello?lang=fr" -H 'accept-language: zh-CN'             # Bonjour (explicit wins)
curl -s  http://127.0.0.1:3000/hello -H 'accept-language: de'                          # Hello (the default)

Notes#

  • Matching follows RFC 4647: zh-Hant-TW falls back to zh-Hant, then zh; a request for zh matches a supported zh-CN; and ranges with q=0 are never chosen.
  • The middleware sets Content-Language (unless the handler did) and Vary: Accept-Language, so caches keep the languages apart.
  • If you let a cookie or a query parameter choose the language and put a shared cache in front, also vary on what you read: Vary: Cookie for a cookie. URLs with different ?lang= values are already different cache entries.
  • req.locale() returns the chosen tag exactly as you gave it to Locales::new, so you can use it as a key into your translation tables.