-
-
Notifications
You must be signed in to change notification settings - Fork 228
Derive i18n support #844
base: main
Are you sure you want to change the base?
Derive i18n support #844
Changes from all commits
f8af562
e9b1be5
9877915
2eb0fdf
7d11fd2
8336929
72dda5e
858ea69
465483d
39a1370
47cbb51
1874729
98ff019
07db0e7
8bd252e
c96ada6
d1a46fe
cdbc7d0
4c7300c
7e1f5e6
17f95e7
2c827f2
c3f5684
2658da3
7b89872
5ce38a3
7a11db6
355163a
62027c8
bbe5991
f3ec8c7
cc01d9b
afbc64f
b3714f4
569a79b
db5c3e2
129786d
ba50d10
ca2ca74
da0319b
da9f506
96c63c5
7663bf8
03b4979
08d34bd
39b0c4a
6872a72
92fadc9
98a4777
63ed5c9
073e92a
f944c86
c32b22d
426575e
81cce1a
1c315d6
6384ae2
7f82494
3f002ef
925d1b5
8f30ba5
b52aaf3
9909c36
6cda9f5
9e1f326
7c15904
5c35e12
2bbba70
b5dda4c
f9dfa60
70dca43
47b7c6c
e1b66c8
b21ba00
4a3b1d0
7a79974
2db3abe
cf707c9
a0821b3
3ad39b4
e70e93d
986ef3e
4f1e5c7
94f6a09
21202a2
de05a6b
22e72fe
68a77b0
15149ce
6703c24
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
//! Module for compile time checked localization | ||
//! | ||
//! # Example: | ||
//! | ||
//! [Fluent Translation List](https://projectfluent.org/) resource file `i18n/es-MX/basic.ftl`: | ||
//! | ||
//! ```ftl | ||
//! greeting = ¡Hola, { $name }! | ||
//! ``` | ||
//! | ||
//! Askama HTML template `templates/example.html`: | ||
//! | ||
//! ```html | ||
//! <h1>{{ localize("greeting", name: name) }}</h1> | ||
//! ``` | ||
//! | ||
//! Rust usage: | ||
//! ```ignore | ||
//! use askama::i18n::{langid, Locale}; | ||
//! use askama::Template; | ||
//! | ||
//! askama::i18n::load!(LOCALES); | ||
//! | ||
//! #[derive(Template)] | ||
//! #[template(path = "example.html")] | ||
//! struct ExampleTemplate<'a> { | ||
//! #[locale] | ||
//! loc: Locale<'a>, | ||
//! name: &'a str, | ||
//! } | ||
//! | ||
//! let template = ExampleTemplate { | ||
//! loc: Locale::new(langid!("es-MX"), &LOCALES), | ||
//! name: "Hilda", | ||
//! }; | ||
//! | ||
//! // "<h1>¡Hola, Hilda!</h1>" | ||
//! template.render().unwrap(); | ||
//! ``` | ||
|
||
use std::collections::HashMap; | ||
use std::iter::FromIterator; | ||
|
||
// Re-export conventiently as `askama::i18n::load!()`. | ||
// Proc-macro crates can only export macros from their root namespace. | ||
/// Load locales at compile time. See example above for usage. | ||
pub use askama_derive::i18n_load as load; | ||
|
||
pub use fluent_templates::{self, fluent_bundle::FluentValue, fs::langid, LanguageIdentifier}; | ||
use fluent_templates::{Loader, StaticLoader}; | ||
use parking_lot::const_mutex; | ||
|
||
pub struct Locale<'a> { | ||
loader: &'a StaticLoader, | ||
language: LanguageIdentifier, | ||
} | ||
Comment on lines
+53
to
+56
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it be possible to also support non-static loaders (e.g ArcLoader)? Could be a dyn type or parameterised by the type. Wondering as I have a use-case for this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the use case? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm adding some resource files at runtime: The app server can support running as one of a distinct set of 'sites' depending on a command line flag - this simplifies code & deployment. Each site has its own name, description, purpose, and other translatable values which are also used by the common translation resources. Because switching is done at runtime using a command-line flag, I can't use a static loader. For instance, the value of about = -snip-
.description = { site.short-name } is a free, open, online dictionary for { target-language } and { source-language }. depends on which site is being used. If the site is "isixhosa" and the user's language is en-ZA, it will use this value from the site-specific bundle: source-language = English If the site is "isixhosa" and the user's language is xh, then it will use the appropriate one from the site-specific bundle: source-language = IsiNgesi Finally, if the site is "isizulu" and the locale en-ZA, then it will use this value: source-language = English (It happens to be the same for the isixhosa site in en-ZA, but this isn't the case for all strings) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okay, I think that all sounds plausible, but I'd probably like to declare it out of scope for the initial PR. |
||
|
||
impl Locale<'_> { | ||
pub fn new(language: LanguageIdentifier, loader: &'static StaticLoader) -> Self { | ||
Self { loader, language } | ||
} | ||
|
||
pub fn translate<'a>( | ||
&self, | ||
msg_id: &str, | ||
args: impl IntoIterator<Item = (&'a str, FluentValue<'a>)>, | ||
) -> Option<String> { | ||
let args = HashMap::<&str, FluentValue<'_>>::from_iter(args); | ||
let args = match args.is_empty() { | ||
true => None, | ||
false => Some(&args), | ||
}; | ||
self.loader.lookup_complete(&self.language, msg_id, args) | ||
} | ||
} | ||
|
||
/// Similar to OnceCell, but it has an additional take() function, which can only be used once, | ||
/// and only if the instance was never dereferenced. | ||
/// | ||
/// The struct is only meant to be used by the [`i18n_load!()`] macro. | ||
/// Concurrent access will deliberately panic. | ||
/// | ||
/// Rationale: StaticLoader cannot be cloned. | ||
#[doc(hidden)] | ||
pub struct Unlazy<T>(parking_lot::Mutex<UnlazyEnum<T>>); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we need this? The stated rationale is that |
||
|
||
enum UnlazyEnum<T> { | ||
Generator(Option<fn() -> T>), | ||
Value(Box<T>), | ||
} | ||
|
||
impl<T> Unlazy<T> { | ||
pub const fn new(f: fn() -> T) -> Self { | ||
Self(const_mutex(UnlazyEnum::Generator(Some(f)))) | ||
} | ||
|
||
pub fn take(&self) -> T { | ||
let f = match &mut *self.0.try_lock().unwrap() { | ||
UnlazyEnum::Generator(f) => f.take(), | ||
_ => None, | ||
}; | ||
f.unwrap()() | ||
} | ||
} | ||
|
||
impl<T> std::ops::Deref for Unlazy<T> | ||
where | ||
Self: 'static, | ||
{ | ||
type Target = T; | ||
|
||
fn deref(&self) -> &Self::Target { | ||
let data = &mut *self.0.try_lock().unwrap(); | ||
let value: &T = match data { | ||
UnlazyEnum::Generator(f) => { | ||
*data = UnlazyEnum::Value(Box::new(f.take().unwrap()())); | ||
match data { | ||
UnlazyEnum::Value(value) => value, | ||
_ => unreachable!(), | ||
} | ||
} | ||
UnlazyEnum::Value(value) => value, | ||
}; | ||
|
||
// SAFETY: This transmutation is safe because once a value is assigned, | ||
// it won't be unassigned again, and Self has static lifetime. | ||
unsafe { std::mem::transmute(value) } | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please revert this change.