-
Notifications
You must be signed in to change notification settings - Fork 82
Localization support #232
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Localization support #232
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,8 @@ | ||
| pub mod error; | ||
| pub mod etag; | ||
| pub mod localization; | ||
| pub mod maintenance; | ||
| pub mod pagination; | ||
| pub mod preferences; | ||
| pub mod query; | ||
| pub mod response; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| use crate::preferences::{ClientPreferences, PreferenceManager}; | ||
| use crate::{tryo_result, tryo_state}; | ||
| use pointercrate_core::error::CoreError; | ||
| use pointercrate_core::localization::LocaleConfiguration; | ||
| use rocket::{ | ||
| request::{FromRequest, Outcome}, | ||
| Request, | ||
| }; | ||
| use unic_langid::subtags::Language; | ||
|
|
||
| pub const LOCALE_COOKIE_NAME: &str = "locale"; | ||
|
|
||
| pub struct ClientLocale(pub Language); | ||
|
|
||
| #[rocket::async_trait] | ||
| impl<'r> FromRequest<'r> for ClientLocale { | ||
| type Error = CoreError; | ||
|
|
||
| async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> { | ||
| let preference_manager = tryo_state!(request, PreferenceManager); | ||
| let preferences = ClientPreferences::from_cookies(request.cookies(), preference_manager); | ||
| let language = tryo_result!(preferences | ||
| .get(LOCALE_COOKIE_NAME) | ||
| .ok_or_else(|| CoreError::internal_server_error("locale set not registered with preference manager"))); | ||
| let lang_id = LocaleConfiguration::get().by_code(language); | ||
|
|
||
| Outcome::Success(ClientLocale(lang_id.language)) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| use std::collections::HashMap; | ||
|
|
||
| use crate::localization::LOCALE_COOKIE_NAME; | ||
| use pointercrate_core::error::CoreError; | ||
| use pointercrate_core::localization::LocaleConfiguration; | ||
| use rocket::{ | ||
| http::CookieJar, | ||
| request::{FromRequest, Outcome}, | ||
| Request, | ||
| }; | ||
|
|
||
| /// A request guard which stores the preferences sent from the client. | ||
| pub struct ClientPreferences<'k, 'v>(HashMap<&'k str, &'v str>); | ||
|
|
||
| impl<'k: 'v, 'v> ClientPreferences<'k, 'v> { | ||
| /// Retrieve a particular preference which was sent to us from the client. | ||
| /// | ||
| /// `T` must implement `From<ClientPreference>`, which [`String`] already | ||
| /// implements, in case the untouched cookie value is what needs to be handled. | ||
| pub fn get(&self, name: &'k str) -> Option<&'v str> { | ||
| self.0.get(name).map(|&s| s) | ||
| } | ||
|
|
||
| pub fn from_cookies(cookies: &'v CookieJar<'v>, preference_manager: &'k PreferenceManager) -> Self { | ||
| ClientPreferences( | ||
| preference_manager | ||
| .0 | ||
| .iter() | ||
| .map(|(name, default)| { | ||
| ( | ||
| name.as_ref(), | ||
| cookies | ||
| .get(&format!("preference-{}", name)) | ||
| .map(|cookie| cookie.value()) | ||
| .unwrap_or(default) | ||
| .as_ref(), | ||
| ) | ||
| }) | ||
| .collect(), | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| #[rocket::async_trait] | ||
| impl<'r> FromRequest<'r> for ClientPreferences<'r, 'r> { | ||
| type Error = CoreError; | ||
|
|
||
| async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> { | ||
| let preference_manager = match request.rocket().state::<PreferenceManager>() { | ||
| Some(preference_manager) => preference_manager, | ||
| _ => return Outcome::Success(ClientPreferences(HashMap::new())), // return an empty preferences hashmap if this instance doesnt support preferences | ||
| }; | ||
|
|
||
| let preferences = ClientPreferences::from_cookies(request.cookies(), preference_manager); | ||
|
|
||
| Outcome::Success(preferences) | ||
| } | ||
| } | ||
|
|
||
| /// A configuration state to manage all of your pointercrate instance's | ||
| /// client preferences. | ||
| #[derive(Default)] | ||
| pub struct PreferenceManager(HashMap<String, String>); | ||
|
|
||
| impl PreferenceManager { | ||
| /// Append a new preference to this [`PreferenceManager`]. `name` represents | ||
| /// the name of the cookie which stores the value of this preference. | ||
| /// | ||
| /// Note that the cookie name is prefixed with `"preference-"`, so creating a | ||
| /// preference with the `name` value as `"theme"` would result in the cookie | ||
| /// sent from the client being named `"preference-theme"`. | ||
| /// | ||
| /// If the cookie was not received, its value will default to `default`. | ||
| pub fn preference(mut self, name: impl Into<String>, default: impl Into<String>) -> Self { | ||
| self.0.insert(name.into(), default.into()); | ||
|
|
||
| self | ||
| } | ||
|
|
||
| /// Automatically register the preferences needed to store active locales. | ||
| /// | ||
| /// Requires the global localization context to have been set up via [`LocalesLoader::commit`], | ||
| /// otherwise will panic. | ||
| pub fn with_localization(self) -> Self { | ||
| self.preference(LOCALE_COOKIE_NAME, LocaleConfiguration::get().fallback.as_str()) | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| [package] | ||
| name = "pointercrate-core-macros" | ||
| version = "0.1.0" | ||
| authors.workspace = true | ||
| description.workspace = true | ||
| homepage.workspace = true | ||
| edition.workspace = true | ||
| repository.workspace = true | ||
|
|
||
| [lib] | ||
| proc-macro = true | ||
|
|
||
| [dependencies] | ||
| syn = { version = "2.0.101", features = ["full"] } | ||
| quote = "1.0.40" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| use proc_macro::TokenStream; | ||
| use quote::quote; | ||
| use syn::{parse2, parse_macro_input, parse_quote, ItemFn}; | ||
|
|
||
| /// A procedural macro for automatically wrapping a request handler inside a tokio::task_local! | ||
| /// [`LocalKey`] scope for `LANGUAGE`, with the value of the [`ClientLocale`] request guard. | ||
| /// | ||
| /// Use of this macro eliminates the need for writing and maintaining boilerplate code caused | ||
| /// by manually wrapping the request handler body inside a `LANGUAGE` scope, while also | ||
| /// having to take in a [`ClientLocale`] guard and handling that properly. | ||
| /// | ||
| /// This macro should be used for any endpoint whose request handler calls a translation | ||
| /// function at some point, so basically any page or API endpoint (API endpoints need | ||
| /// to be localized because errors are also translated) | ||
| #[proc_macro_attribute] | ||
| pub fn localized(_: TokenStream, input: TokenStream) -> TokenStream { | ||
| let mut f = parse_macro_input!(input as ItemFn); | ||
|
|
||
| // modify the request handler to automatically take in our [`ClientLocale`] request | ||
| // guard (defined in pointercrate-core-api/src/localization.rs) | ||
| f.sig | ||
| .inputs | ||
| .push(parse_quote! { __locale: pointercrate_core_api::localization::ClientLocale }); | ||
|
|
||
| let block = &f.block; | ||
| let block = quote! { | ||
| { | ||
| pointercrate_core::localization::LANGUAGE.scope(__locale.0, async { | ||
| #block | ||
| }).await | ||
| } | ||
| }; | ||
|
|
||
| f.block = parse2(block).unwrap(); | ||
|
|
||
| TokenStream::from(quote!(#f)) | ||
| } | ||
|
|
||
| /// Identical behaviour to `#[localized]`, but modified to support error catchers. | ||
| #[proc_macro_attribute] | ||
| pub fn localized_catcher(_: TokenStream, input: TokenStream) -> TokenStream { | ||
| let mut f = parse_macro_input!(input as ItemFn); | ||
|
|
||
| f.sig.inputs.push(parse_quote! { __request: &rocket::Request<'_> }); | ||
|
|
||
| let block = &f.block; | ||
| let block = quote! { | ||
| { | ||
| use rocket::request::FromRequest; | ||
|
|
||
| let __locale = match pointercrate_core_api::localization::ClientLocale::from_request(__request).await { | ||
| rocket::request::Outcome::Success(locale) => locale, | ||
| _ => return pointercrate_core_api::error::ErrorResponder::from(pointercrate_core::error::CoreError::internal_server_error("An error occurred while trying to extract requested locale. Check your locale fallbacks!")), | ||
| }; | ||
|
|
||
| pointercrate_core::localization::LANGUAGE.scope(__locale.0, async { | ||
| #block | ||
| }).await | ||
| } | ||
| }; | ||
|
|
||
| f.block = parse2(block).unwrap(); | ||
|
|
||
| TokenStream::from(quote!(#f)) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.