🧑‍💻 Mejora y simplifica localización de módulos

This commit is contained in:
Manuel Cillero 2023-06-04 00:43:28 +02:00
parent d0add7c7ab
commit 520d3bb20b
21 changed files with 265 additions and 325 deletions

View file

@ -17,6 +17,5 @@ pub(crate) use all::common_components;
mod renderable;
pub use renderable::{IsRenderable, Renderable};
mod basic;
pub use basic::{Html, COMPONENT_HTML};
pub use basic::{Text, COMPONENT_TEXT};
mod l10n;
pub use l10n::{L10n, COMPONENT_L10N};

View file

@ -1,143 +0,0 @@
use crate::core::component::{AnyComponent, ComponentTrait, RenderContext};
use crate::html::{html, Markup, PreEscaped};
use crate::locale::{translate, Locale, Locales};
use crate::{define_handle, fn_builder, paste, Handle};
use std::collections::HashMap;
macro_rules! basic_components {
( $($COMPONENT_HANDLE:ident: $Component:ty => $TypeValue:ty),* ) => { $( paste! {
define_handle!($COMPONENT_HANDLE);
pub enum [< $Component Op >] {
None,
Value($TypeValue),
Translated(&'static str, &'static Locales),
Escaped(&'static str, &'static Locales),
}
pub struct $Component {
op: [< $Component Op >],
args: HashMap<&'static str, String>,
}
impl Default for $Component {
fn default() -> Self {
$Component {
op: [< $Component Op >]::None,
args: HashMap::new(),
}
}
}
impl ComponentTrait for $Component {
fn new() -> Self {
$Component::default()
}
fn handle(&self) -> Handle {
$COMPONENT_HANDLE
}
fn default_render(&self, rcx: &mut RenderContext) -> Markup {
match self.op() {
[< $Component Op >]::None => html! {},
[< $Component Op >]::Value(value) => html! { (value) },
[< $Component Op >]::Translated(key, locales) => html! {
(translate(
key,
Locale::Using(
rcx.langid(),
locales,
&self.args().iter().fold(HashMap::new(), |mut args, (key, value)| {
args.insert(key.to_string(), value.to_owned().into());
args
})
)
))
},
[< $Component Op >]::Escaped(key, locales) => html! {
(PreEscaped(translate(
key,
Locale::Using(
rcx.langid(),
locales,
&self.args().iter().fold(HashMap::new(), |mut args, (key, value)| {
args.insert(key.to_string(), value.to_owned().into());
args
})
)
)))
},
}
}
fn as_ref_any(&self) -> &dyn AnyComponent {
self
}
fn as_mut_any(&mut self) -> &mut dyn AnyComponent {
self
}
}
impl $Component {
pub fn n(value: $TypeValue) -> Self {
$Component {
op: [< $Component Op >]::Value(value),
..Default::default()
}
}
pub fn t(key: &'static str, locales: &'static Locales) -> Self {
$Component {
op: [< $Component Op >]::Translated(key, locales),
..Default::default()
}
}
pub fn e(key: &'static str, locales: &'static Locales) -> Self {
$Component {
op: [< $Component Op >]::Escaped(key, locales),
..Default::default()
}
}
// $Component BUILDER.
#[fn_builder]
pub fn alter_op(&mut self, op: [< $Component Op >]) -> &mut Self {
self.op = op;
self
}
#[fn_builder]
pub fn alter_arg(&mut self, arg: &'static str, value: String) -> &mut Self {
self.args.insert(arg, value);
self
}
pub fn clear_args(&mut self) -> &mut Self {
self.args.drain();
self
}
// $Component GETTERS.
pub fn op(&self) -> &[< $Component Op >] {
&self.op
}
pub fn args(&self) -> &HashMap<&str, String> {
&self.args
}
}
} )* };
}
basic_components!(
COMPONENT_HTML: Html => Markup,
COMPONENT_TEXT: Text => &'static str
);

View file

@ -0,0 +1,141 @@
use crate::core::component::{AnyComponent, ComponentTrait, RenderContext};
use crate::html::{html, Markup, PreEscaped};
use crate::locale::Locales;
use crate::{define_handle, fn_builder, Handle};
use fluent_templates::Loader;
use std::collections::HashMap;
define_handle!(COMPONENT_L10N);
pub enum L10nOp {
None,
Value(Markup),
Translated(&'static str, &'static Locales),
Escaped(&'static str, &'static Locales),
}
pub struct L10n {
op: L10nOp,
args: HashMap<&'static str, String>,
}
impl Default for L10n {
fn default() -> Self {
L10n {
op: L10nOp::None,
args: HashMap::new(),
}
}
}
impl ComponentTrait for L10n {
fn new() -> Self {
L10n::default()
}
fn handle(&self) -> Handle {
COMPONENT_L10N
}
fn default_render(&self, rcx: &mut RenderContext) -> Markup {
match self.op() {
L10nOp::None => html! {},
L10nOp::Value(value) => html! { (value) },
L10nOp::Translated(key, locales) => html! {
(locales
.lookup_with_args(
rcx.langid(),
key,
&self.args().iter().fold(HashMap::new(), |mut args, (key, value)| {
args.insert(key.to_string(), value.to_owned().into());
args
})
)
.unwrap_or(key.to_string())
)
},
L10nOp::Escaped(key, locales) => html! {
(PreEscaped(locales
.lookup_with_args(
rcx.langid(),
key,
&self.args().iter().fold(HashMap::new(), |mut args, (key, value)| {
args.insert(key.to_string(), value.to_owned().into());
args
})
)
.unwrap_or(key.to_string())
))
},
}
}
fn as_ref_any(&self) -> &dyn AnyComponent {
self
}
fn as_mut_any(&mut self) -> &mut dyn AnyComponent {
self
}
}
impl L10n {
pub fn text(text: &'static str) -> Self {
L10n {
op: L10nOp::Value(html! { (text) }),
..Default::default()
}
}
pub fn html(html: Markup) -> Self {
L10n {
op: L10nOp::Value(html),
..Default::default()
}
}
pub fn t(key: &'static str, locales: &'static Locales) -> Self {
L10n {
op: L10nOp::Translated(key, locales),
..Default::default()
}
}
pub fn e(key: &'static str, locales: &'static Locales) -> Self {
L10n {
op: L10nOp::Escaped(key, locales),
..Default::default()
}
}
// L10n BUILDER.
#[fn_builder]
pub fn alter_op(&mut self, op: L10nOp) -> &mut Self {
self.op = op;
self
}
#[fn_builder]
pub fn alter_arg(&mut self, arg: &'static str, value: String) -> &mut Self {
self.args.insert(arg, value);
self
}
pub fn clear_args(&mut self) -> &mut Self {
self.args.drain();
self
}
// L10n GETTERS.
pub fn op(&self) -> &L10nOp {
&self.op
}
pub fn args(&self) -> &HashMap<&str, String> {
&self.args
}
}

View file

@ -1,5 +1,6 @@
use super::ThemeStaticRef;
use crate::core::component::L10n;
use crate::core::hook::HookAction;
use crate::util::single_type_name;
use crate::{server, Handle};
@ -17,12 +18,12 @@ pub trait BaseModule {
pub trait ModuleTrait: BaseModule + Send + Sync {
fn handle(&self) -> Handle;
fn name(&self) -> String {
self.single_name().to_owned()
fn name(&self) -> L10n {
L10n::text(self.single_name())
}
fn description(&self) -> Option<String> {
None
fn description(&self) -> L10n {
L10n::default()
}
fn theme(&self) -> Option<ThemeStaticRef> {

View file

@ -1,9 +1,7 @@
//! Localización (L10n).
//!
//! Proporciona soporte a [Fluent](https://www.projectfluent.org/), un conjunto de especificaciones
//! para la localización de aplicaciones, así como implementaciones y buenas prácticas originalmente
//! desarrolladas por Mozilla.
//!
//! PageTop usa el conjunto de especificaciones [Fluent](https://www.projectfluent.org/) para la
//! localización de aplicaciones.
//!
//! # Sintaxis Fluent (FTL)
//!
@ -72,43 +70,23 @@
//!
//! # Cómo aplicar la localización en tu código
//!
//! Una vez hayas creado tu directorio de recursos FTL, sólo tienes que usar la poderosa macro
//! Una vez hayas creado tu directorio de recursos FTL usa la macro
//! [`define_locale!`](crate::define_locale) para integrarlos en tu módulo o aplicación.
//!
//! Y podrás usar las funciones globales [`_t()`] o [`_e()`] para traducir tus textos:
//!
//! ```
//! use pagetop::prelude::*;
//!
//! define_locale!(LOCALE_SAMPLE, "static/locales");
//!
//! fn demo() {
//! println!("* {}", _t("hello-world", Locale::From(&LOCALE_SAMPLE)));
//! println!("* {}", _t("hello-user", Locale::With(&LOCALE_SAMPLE, &args!["userName" => "Julia"])));
//!
//! let args = args![
//! "userName" => "Roberto",
//! "photoCount" => 3,
//! "userGender" => "male"
//! ];
//! println!("* {}\n", _t("shared-photos", Locale::With(&LOCALE_SAMPLE, &args)));
//! }
//! ```
//! Aunque preferirás usar normalmente los componentes básicos [Text](crate::core::component::Text)
//! y [Html](crate::core::component::Html) para incluir, en respuestas a las peticiones web, textos
//! y contenidos opcionalmente traducibles según el contexto de renderizado.
//! Y utiliza el componente [L10n](crate::core::component::L10n) para incluir, en respuestas a las
//! peticiones web, textos y contenidos opcionalmente traducibles según el contexto de renderizado.
use crate::html::{Markup, PreEscaped};
use crate::{args, config, trace, LazyStatic};
pub(crate) use unic_langid::{langid, LanguageIdentifier};
pub use fluent_templates;
pub(crate) use fluent_templates::StaticLoader as Locales;
use fluent_templates::fluent_bundle::FluentValue;
use fluent_templates::Loader;
pub(crate) use unic_langid::{langid, LanguageIdentifier};
use std::collections::HashMap;
@ -127,7 +105,7 @@ static FALLBACK_LANGID: LazyStatic<LanguageIdentifier> = LazyStatic::new(|| lang
/// Almacena el Identificador de Idioma Unicode
/// ([Unicode Language Identifier](https://unicode.org/reports/tr35/tr35.html#Unicode_language_identifier))
/// global para la aplicación a partir de `SETTINGS.app.language`.
pub static DEFAULT_LANGID: LazyStatic<&LanguageIdentifier> =
pub(crate) static DEFAULT_LANGID: LazyStatic<&LanguageIdentifier> =
LazyStatic::new(|| langid_for(config::SETTINGS.app.language.as_str()));
pub fn langid_for(language: &str) -> &LanguageIdentifier {
@ -144,31 +122,3 @@ pub fn langid_for(language: &str) -> &LanguageIdentifier {
}
}
}
pub enum Locale<'a> {
From(&'a Locales),
With(&'a Locales, &'a HashMap<String, FluentValue<'a>>),
Using(
&'a LanguageIdentifier,
&'a Locales,
&'a HashMap<String, FluentValue<'a>>,
),
}
pub fn _t(key: &str, locale: Locale) -> String {
translate(key, locale)
}
pub fn _e(key: &str, locale: Locale) -> Markup {
PreEscaped(translate(key, locale))
}
#[inline]
pub(crate) fn translate(key: &str, locale: Locale) -> String {
match locale {
Locale::From(locales) => locales.lookup(&DEFAULT_LANGID, key),
Locale::With(locales, args) => locales.lookup_with_args(&DEFAULT_LANGID, key, args),
Locale::Using(langid, locales, args) => locales.lookup_with_args(langid, key, args),
}
.unwrap_or(key.to_string())
}

View file

@ -3,7 +3,7 @@ pub use error403::ERROR_403;
mod error404;
pub use error404::ERROR_404;
use crate::core::component::Text;
use crate::core::component::L10n;
use crate::response::{page::Page, ResponseError};
use crate::server::http::{header::ContentType, StatusCode};
use crate::server::{HttpRequest, HttpResponse};
@ -32,7 +32,7 @@ impl fmt::Display for FatalError {
FatalError::AccessDenied(request) => {
let error_page = Page::new(request.clone());
if let Ok(page) = error_page
.with_title(Text::n("Error FORBIDDEN"))
.with_title(L10n::text("Error FORBIDDEN"))
.with_this_in("region-content", error403::Error403)
.with_template("error")
.render()
@ -46,7 +46,7 @@ impl fmt::Display for FatalError {
FatalError::NotFound(request) => {
let error_page = Page::new(request.clone());
if let Ok(page) = error_page
.with_title(Text::n("Error RESOURCE NOT FOUND"))
.with_title(L10n::text("Error RESOURCE NOT FOUND"))
.with_this_in("region-content", error404::Error404)
.with_template("error")
.render()

View file

@ -11,8 +11,8 @@ use unic_langid::CharacterDirection;
use std::collections::HashMap;
type PageTitle = OneComponent<Text>;
type PageDescription = OneComponent<Text>;
type PageTitle = OneComponent<L10n>;
type PageDescription = OneComponent<L10n>;
#[rustfmt::skip]
pub struct Page {
@ -60,13 +60,13 @@ impl Page {
}
#[fn_builder]
pub fn alter_title(&mut self, title: Text) -> &mut Self {
pub fn alter_title(&mut self, title: L10n) -> &mut Self {
self.title.set(title);
self
}
#[fn_builder]
pub fn alter_description(&mut self, description: Text) -> &mut Self {
pub fn alter_description(&mut self, description: L10n) -> &mut Self {
self.description.set(description);
self
}