rust 58 lines · 8 steps

A custom Accept-Language extractor in Axum

Build a reusable Axum extractor that reads a client's Accept-Language header and resolves it to a supported translation.

Explained by highlit
1use axum::{
2 extract::{FromRef, FromRequestParts, State},
3 http::{header::ACCEPT_LANGUAGE, request::Parts, StatusCode},
4 response::{IntoResponse, Response},
5 Json,
6};
7use serde_json::json;
8use std::{collections::HashMap, sync::Arc};
9 
10#[derive(Clone)]
11pub struct Translations {
12 tables: Arc<HashMap<String, HashMap<String, String>>>,
13 fallback: String,
14}
15 
16impl Translations {
17 pub fn message(&self, lang: &str, key: &str) -> String {
18 self.tables
19 .get(lang)
20 .and_then(|t| t.get(key))
21 .or_else(|| self.tables.get(&self.fallback).and_then(|t| t.get(key)))
22 .cloned()
23 .unwrap_or_else(|| key.to_string())
24 }
25}
26 
27pub struct PreferredLang(pub String);
28 
29impl<S> FromRequestParts<S> for PreferredLang
30where
31 Translations: FromRef<S>,
32 S: Send + Sync,
33{
34 type Rejection = std::convert::Infallible;
35 
36 async fn from_request_parts(parts: &Parts, state: &S) -> Result<Self, Self::Rejection> {
37 let translations = Translations::from_ref(state);
38 let chosen = parts
39 .headers
40 .get(ACCEPT_LANGUAGE)
41 .and_then(|v| v.to_str().ok())
42 .into_iter()
43 .flat_map(|raw| raw.split(','))
44 .map(|tag| tag.trim().split(';').next().unwrap_or("").trim())
45 .map(|tag| tag.split('-').next().unwrap_or(tag).to_lowercase())
46 .find(|tag| translations.tables.contains_key(tag))
47 .unwrap_or_else(|| translations.fallback.clone());
48 Ok(PreferredLang(chosen))
49 }
50}
51 
52pub async fn get_widget(
53 PreferredLang(lang): PreferredLang,
54 State(translations): State<Translations>,
55) -> Response {
56 let body = json!({ "error": translations.message(&lang, "widget.not_found") });
57 (StatusCode::NOT_FOUND, Json(body)).into_response()
58}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Implementing FromRequestParts lets you inject derived request data as a first-class handler parameter.
  2. 2Bounding on FromRef keeps an extractor decoupled from the concrete application state type.
  3. 3Chaining iterator adapters parses and normalizes header lists into a single resolved value cleanly.

Related explainers

Share this explainer

Here's the card — post it anywhere.

A custom Accept-Language extractor in Axum — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code