rust
58 lines · 7 steps
A locale extractor for Axum handlers
Parse the Accept-Language header into a supported locale and inject it as a custom Axum extractor.
Explained by
highlit
1use axum::{
2 extract::FromRequestParts,
3 http::{request::Parts, StatusCode, header::ACCEPT_LANGUAGE},
4};
5
6const SUPPORTED: &[&str] = &["en", "fr", "de", "es", "ja"];
7const DEFAULT_LOCALE: &str = "en";
8
9#[derive(Debug, Clone)]
10pub struct Locale(pub String);
11
12impl Locale {
13 fn negotiate(header: &str) -> String {
14 let mut ranked: Vec<(f32, &str)> = header
15 .split(',')
16 .filter_map(|part| {
17 let mut segments = part.trim().split(';');
18 let tag = segments.next()?.trim();
19 let primary = tag.split('-').next()?.to_ascii_lowercase();
20 if primary.is_empty() {
21 return None;
22 }
23 let quality = segments
24 .find_map(|s| s.trim().strip_prefix("q="))
25 .and_then(|q| q.parse::<f32>().ok())
26 .unwrap_or(1.0);
27 SUPPORTED
28 .iter()
29 .find(|s| **s == primary)
30 .map(|s| (quality, *s))
31 })
32 .collect();
33
34 ranked.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
35 ranked
36 .first()
37 .map(|(_, tag)| tag.to_string())
38 .unwrap_or_else(|| DEFAULT_LOCALE.to_string())
39 }
40}
41
42impl<S> FromRequestParts<S> for Locale
43where
44 S: Send + Sync,
45{
46 type Rejection = (StatusCode, &'static str);
47
48 async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
49 let locale = parts
50 .headers
51 .get(ACCEPT_LANGUAGE)
52 .and_then(|value| value.to_str().ok())
53 .map(Self::negotiate)
54 .unwrap_or_else(|| DEFAULT_LOCALE.to_string());
55
56 Ok(Locale(locale))
57 }
58}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Implementing FromRequestParts turns any parsing logic into a reusable handler argument.
- 2Accept-Language negotiation means ranking client preferences by q-value against a fixed supported set.
- 3Falling back to a default locale at every failure point keeps the extractor infallible in practice.
Related explainers
rust
use axum::{ body::Body, http::{Method, StatusCode, Uri}, response::{IntoResponse, Response},
Nesting routers and JSON fallbacks in Axum
routing
http
json-responses
Intermediate
7 steps
rust
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::thread; use std::time::Duration;
Graceful thread shutdown with an atomic flag
concurrency
atomics
memory-ordering
Advanced
7 steps
rust
#[derive(Debug, PartialEq)] enum State { FieldStart, InUnquoted,
Parsing a CSV line with a state machine
state machine
parsing
enums
Intermediate
9 steps
rust
use std::convert::TryFrom; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum HttpStatus {
Converting HTTP codes with TryFrom in Rust
enums
error-handling
trait-implementation
Intermediate
8 steps
rust
use axum::{ extract::{Request, State}, http::{HeaderValue, StatusCode}, middleware::Next,
API version headers with Axum middleware
middleware
http-headers
versioning
Intermediate
8 steps
rust
use axum::{ extract::Path, http::StatusCode, routing::{get, post},
Building a REST resource in Axum
rest-api
routing
serialization
Intermediate
9 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/a-locale-extractor-for-axum-handlers-explained-rust-18d3/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.