Skip to main content

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}