hypersig_i18n/axum.rs
1//! Sending a visitor to the page in their language, from an axum [`Router`].
2
3use axum::Router;
4use axum::http::{HeaderMap, HeaderValue, header};
5use axum::response::{IntoResponse, Redirect};
6use axum::routing::get;
7use hypersig::Route;
8
9use crate::{Locale, negotiate};
10
11/// Adds the redirect from `/` to the page in the visitor's language to an axum [`Router`].
12pub trait LocaleRouterExt<St>: Sized {
13 /// Answers `GET /` with a redirect to `home` in the locale the visitor's browser asks for.
14 ///
15 /// The locale comes from [`negotiate`] over the `Accept-Language` header.
16 /// The redirect is a `307 Temporary Redirect` with `Vary: Accept-Language`,
17 /// because another browser may be sent to another locale.
18 ///
19 /// ```
20 /// # use axum::Router;
21 /// # use hypersig::{Page, Route, Segment, Signals};
22 /// # use hypersig_i18n::{Locale, LocaleRouterExt};
23 /// # use serde::{Deserialize, Serialize};
24 /// # #[derive(Segment, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Debug)]
25 /// # #[serde(rename_all = "lowercase")]
26 /// # enum Lang { En, Ja }
27 /// # struct ByLang<T> { en: T, ja: T }
28 /// # impl Locale for Lang {
29 /// # const ALL: &'static [Self] = &[Self::En, Self::Ja];
30 /// # type Text<T> = ByLang<T>;
31 /// # fn pick<T>(self, t: ByLang<T>) -> T { match self { Self::En => t.en, Self::Ja => t.ja } }
32 /// # }
33 /// # #[derive(Signals, Serialize, Deserialize)]
34 /// # struct Home {}
35 /// # async fn home((): (), _: Lang) -> Option<Page<Home>> { None }
36 /// hypersig::routes! {
37 /// fn pages(Router<()>);
38 /// const HOME: Route<Lang> = "/{lang}" => home;
39 /// }
40 ///
41 /// let _app: Router = pages(Router::new()).redirect_to_locale(HOME);
42 /// ```
43 #[must_use]
44 fn redirect_to_locale<L: Locale + Send + Sync>(self, home: Route<L>) -> Self;
45}
46
47impl<St: Clone + Send + Sync + 'static> LocaleRouterExt<St> for Router<St> {
48 fn redirect_to_locale<L: Locale + Send + Sync>(self, home: Route<L>) -> Self {
49 self.route(
50 "/",
51 get(move |headers: HeaderMap| async move {
52 let accept = headers
53 .get(header::ACCEPT_LANGUAGE)
54 .and_then(|value| value.to_str().ok())
55 .unwrap_or_default();
56 let url = home.url(negotiate::<L>(accept));
57 (
58 [(header::VARY, HeaderValue::from_static("accept-language"))],
59 Redirect::temporary(url.as_str()),
60 )
61 .into_response()
62 }),
63 )
64 }
65}