diff --git a/src/base/extension/welcome.rs b/src/base/extension/welcome.rs index 39edfd3..d8b1259 100644 --- a/src/base/extension/welcome.rs +++ b/src/base/extension/welcome.rs @@ -28,15 +28,12 @@ async fn homepage(request: HttpRequest) -> ResultPage { .with_title(L10n::l("welcome_page")) .with_theme("Basic") .with_assets(AssetsOp::AddStyleSheet(StyleSheet::from("/css/welcome.css"))) - .with_component(Html::with(move |cx| html! { + .with_component(Html::with(move |_| html! { div id="main-header" { header { - h1 - id="header-title" - aria-label=(L10n::l("welcome_aria").with_arg("app", app).to_markup(cx)) - { - span { (L10n::l("welcome_title").to_markup(cx)) } - (L10n::l("welcome_intro").with_arg("app", app).to_markup(cx)) + h1 id="header-title" aria-label=(L10n::l("welcome_aria").with_arg("app", app)) { + span { (L10n::l("welcome_title")) } + (L10n::l("welcome_intro").with_arg("app", app)) } } aside id="header-image" aria-hidden="true" { @@ -62,25 +59,20 @@ async fn homepage(request: HttpRequest) -> ResultPage { main id="main-content" { section class="content-body" { div id="poweredby-button" { - a - id="poweredby-link" - href="https://pagetop.cillero.es" - target="_blank" - rel="noreferrer" - { + a id="poweredby-link" href="https://pagetop.cillero.es" target="_blank" rel="noreferrer" { span {} span {} span {} - div id="poweredby-text" { (L10n::l("welcome_powered").to_markup(cx)) } + div id="poweredby-text" { (L10n::l("welcome_powered")) } } } div class="content-text" { - p { (L10n::l("welcome_text1").to_markup(cx)) } - p { (L10n::l("welcome_text2").to_markup(cx)) } + p { (L10n::l("welcome_text1")) } + p { (L10n::l("welcome_text2")) } div class="subcontent" { - h1 { span { (L10n::l("welcome_about").to_markup(cx)) } } - p { (L10n::l("welcome_pagetop").to_markup(cx)) } - p { (L10n::l("welcome_issues1").to_markup(cx)) } - p { (L10n::l("welcome_issues2").with_arg("app", app).to_markup(cx)) } + h1 { span { (L10n::l("welcome_about")) } } + p { (L10n::l("welcome_pagetop")) } + p { (L10n::l("welcome_issues1")) } + p { (L10n::l("welcome_issues2").with_arg("app", app)) } } } } @@ -93,7 +85,7 @@ async fn homepage(request: HttpRequest) -> ResultPage { viewBox="0 0 1614 1614" xmlns="http://www.w3.org/2000/svg" role="img" - aria-label=[L10n::l("pagetop_logo").using(cx)] + aria-label=(L10n::l("pagetop_logo")) preserveAspectRatio="xMidYMid slice" focusable="false" { @@ -105,8 +97,8 @@ async fn homepage(request: HttpRequest) -> ResultPage { div class="footer-links" { a href="https://crates.io/crates/pagetop" target="_blank" rel="noreferrer" { ("Crates.io") } a href="https://docs.rs/pagetop" target="_blank" rel="noreferrer" { ("Docs.rs") } - a href="https://git.cillero.es/manuelcillero/pagetop" target="_blank" rel="noreferrer" { (L10n::l("welcome_code").to_markup(cx)) } - em { (L10n::l("welcome_have_fun").to_markup(cx)) } + a href="https://git.cillero.es/manuelcillero/pagetop" target="_blank" rel="noreferrer" { (L10n::l("welcome_code")) } + em { (L10n::l("welcome_have_fun")) } } } } diff --git a/src/config.rs b/src/config.rs index 08067fe..5dc05a1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -240,7 +240,7 @@ pub static CONFIG_VALUES: LazyLock> = LazyLock::new( macro_rules! include_config { ( $SETTINGS_NAME:ident : $Settings_Type:ty => [ $( $k:literal => $v:expr ),* $(,)? ] ) => { #[doc = concat!( - "Referencia y valores por defecto de los ajustes de configuración para [`", + "Referencia a los ajustes de configuración deserializados de [`", stringify!($Settings_Type), "`]." )] diff --git a/src/core/theme/regions.rs b/src/core/theme/regions.rs index 22ab6f2..5e0186a 100644 --- a/src/core/theme/regions.rs +++ b/src/core/theme/regions.rs @@ -74,12 +74,12 @@ impl InRegion { /// use pagetop::prelude::*; /// /// // Banner global, en la región por defecto de cualquier página. - /// InRegion::Content.add(Child::with(Html::with(|_| + /// InRegion::Content.add(Child::with(Html::with( /// html! { ("🎉 ¡Bienvenido!") } /// ))); /// /// // Texto en la región "sidebar". - /// InRegion::Named("sidebar").add(Child::with(Html::with(|_| + /// InRegion::Named("sidebar").add(Child::with(Html::with( /// html! { ("Publicidad") } /// ))); /// ``` diff --git a/src/global.rs b/src/global.rs index 25a1781..f150ca6 100644 --- a/src/global.rs +++ b/src/global.rs @@ -9,7 +9,7 @@ include_config!(SETTINGS: Settings => [ "app.name" => "PageTop App", "app.description" => "Developed with the amazing PageTop framework.", "app.theme" => "Basic", - "app.language" => "", + "app.language" => "en-US", "app.startup_banner" => "Slant", // [dev] @@ -48,14 +48,7 @@ pub struct App { pub description: String, /// Tema predeterminado. pub theme: String, - /// Idioma por defecto para la aplicación. - /// - /// Si no se especifica un valor válido, normalmente se usará el idioma devuelto por la - /// implementación de [`LangId`](crate::locale::LangId) para [`Context`](crate::html::Context), - /// en el siguiente orden: primero, el idioma establecido explícitamente con - /// [`Context::with_langid`](crate::html::Context::with_langid); si no se ha definido, se usará - /// el indicado en la cabecera `Accept-Language` del navegador; y, si ninguno aplica, se - /// empleará el idioma de respaldo ("en-US"). + /// Idioma predeterminado (localización). pub language: String, /// Banner ASCII mostrado al inicio: *"Off"* (desactivado), *"Slant"*, *"Small"*, *"Speed"* o /// *"Starwars"*. diff --git a/src/html/context.rs b/src/html/context.rs index 8aaeb4a..456ca7a 100644 --- a/src/html/context.rs +++ b/src/html/context.rs @@ -3,7 +3,7 @@ use crate::core::theme::ThemeRef; use crate::core::TypeInfo; use crate::html::{html, Markup}; use crate::html::{Assets, Favicon, JavaScript, StyleSheet}; -use crate::locale::{LangId, LangMatch, LanguageIdentifier, DEFAULT_LANGID, FALLBACK_LANGID}; +use crate::locale::{LanguageIdentifier, DEFAULT_LANGID}; use crate::service::HttpRequest; use crate::{builder_fn, join}; @@ -69,9 +69,9 @@ impl Error for ErrorParam {} /// use pagetop::prelude::*; /// /// fn new_context(request: HttpRequest) -> Context { -/// Context::new(Some(request)) +/// Context::new(request) /// // Establece el idioma del documento a español. -/// .with_langid(&LangMatch::resolve("es-ES")) +/// .with_langid(LangMatch::langid_or_default("es-ES")) /// // Selecciona un tema (por su nombre corto). /// .with_theme("aliner") /// // Asigna un favicon. @@ -125,23 +125,9 @@ impl Context { /// cargados. #[rustfmt::skip] pub fn new(request: Option) -> Self { - // Se intenta DEFAULT_LANGID. - let langid = DEFAULT_LANGID - // Si es None evalúa la cadena de extracción desde la cabecera HTTP. - .or_else(|| { - request - // Se usa `as_ref()` sobre `Option` para no mover el valor. - .as_ref() - .and_then(|req| req.headers().get("Accept-Language")) - .and_then(|value| value.to_str().ok()) - .and_then(|language| LangMatch::resolve(language).as_option()) - }) - // Si todo falla, se recurre a &FALLBACK_LANGID. - .unwrap_or(&FALLBACK_LANGID); - Context { request, - langid, + langid : &DEFAULT_LANGID, theme : *DEFAULT_THEME, layout : "default", favicon : None, @@ -154,10 +140,10 @@ impl Context { // Context BUILDER ***************************************************************************** - /// Modifica la fuente de idioma del documento. + /// Modifica el identificador de idioma del documento. #[builder_fn] - pub fn with_langid(mut self, language: &impl LangId) -> Self { - self.langid = language.langid(); + pub fn with_langid(mut self, langid: &'static LanguageIdentifier) -> Self { + self.langid = langid; self } @@ -216,6 +202,11 @@ impl Context { self.request.as_ref() } + /// Devuelve el identificador de idioma asociado al documento. + pub fn langid(&self) -> &LanguageIdentifier { + self.langid + } + /// Devuelve el tema que se usará para renderizar el documento. pub fn theme(&self) -> ThemeRef { self.theme @@ -290,21 +281,3 @@ impl Context { } } } - -/// Permite a [`Context`](crate::html::Context) actuar como proveedor de idioma. -/// -/// Devuelve un [`LanguageIdentifier`] siguiendo este orden de prioridad: -/// -/// 1. Un idioma válido establecido explícitamente con [`Context::with_langid`]. -/// 2. El idioma por defecto configurado para la aplicación. -/// 3. Un idioma válido extraído de la cabecera `Accept-Language` del navegador. -/// 4. Y si ninguna de las opciones anteriores aplica, se usa el idioma de respaldo (`"en-US"`). -/// -/// Resulta útil para usar un contexto ([`Context`]) como fuente de traducción en -/// [`L10n::using`](crate::locale::L10n::using) o -/// [`L10n::to_markup`](crate::locale::L10n::to_markup). -impl LangId for Context { - fn langid(&self) -> &'static LanguageIdentifier { - self.langid - } -} diff --git a/src/html/opt_translated.rs b/src/html/opt_translated.rs index b15ea18..ba60a0f 100644 --- a/src/html/opt_translated.rs +++ b/src/html/opt_translated.rs @@ -1,5 +1,5 @@ use crate::html::Markup; -use crate::locale::{L10n, LangId}; +use crate::locale::{L10n, LanguageIdentifier}; use crate::{builder_fn, AutoDefault}; /// Cadena para traducir al renderizar ([`locale`](crate::locale)). @@ -16,18 +16,18 @@ use crate::{builder_fn, AutoDefault}; /// /// // Español disponible. /// assert_eq!( -/// hello.using(&LangMatch::resolve("es-ES")), +/// hello.using(LangMatch::langid_or_default("es-ES")), /// Some(String::from("¡Hola mundo!")) /// ); /// /// // Japonés no disponible, traduce al idioma de respaldo ("en-US"). /// assert_eq!( -/// hello.using(&LangMatch::resolve("ja-JP")), +/// hello.using(LangMatch::langid_or_fallback("ja-JP")), /// Some(String::from("Hello world!")) /// ); /// /// // Para incrustar en HTML escapado: -/// let markup = hello.to_markup(&LangMatch::resolve("es-ES")); +/// let markup = hello.escaped(LangMatch::langid_or_default("es-ES")); /// assert_eq!(markup.into_string(), "¡Hola mundo!"); /// ``` #[derive(AutoDefault, Clone, Debug)] @@ -50,16 +50,16 @@ impl OptionTranslated { // OptionTranslated GETTERS ******************************************************************** - /// Devuelve la traducción para `language`, si existe. - pub fn using(&self, language: &impl LangId) -> Option { - self.0.using(language) + /// Devuelve la traducción para `langid`, si existe. + pub fn using(&self, langid: &LanguageIdentifier) -> Option { + self.0.using(langid) } - /// Devuelve la traducción *escapada* como [`Markup`] para `language`, si existe. + /// Devuelve la traducción *escapada* como [`Markup`] para `langid`, si existe. /// /// Útil para incrustar el texto directamente en plantillas HTML sin riesgo de inyección de /// contenido. - pub fn to_markup(&self, language: &impl LangId) -> Markup { - self.0.to_markup(language) + pub fn escaped(&self, langid: &LanguageIdentifier) -> Markup { + self.0.escaped(langid) } } diff --git a/src/locale.rs b/src/locale.rs index d81243e..2a08ba6 100644 --- a/src/locale.rs +++ b/src/locale.rs @@ -90,7 +90,7 @@ //! Y *voilà*, sólo queda operar con los idiomas soportados por `PageTop` usando [`LangMatch`] y //! traducir textos con [`L10n`]. -use crate::html::{Markup, PreEscaped}; +use crate::html::{Markup, PreEscaped, Render}; use crate::{global, hm, AutoDefault}; pub use fluent_templates; @@ -112,10 +112,10 @@ use std::fmt; static LANGUAGES: LazyLock> = LazyLock::new(|| { hm![ "en" => ( langid!("en-US"), "english" ), - "en-gb" => ( langid!("en-GB"), "english_british" ), - "en-us" => ( langid!("en-US"), "english_united_states" ), + "en-GB" => ( langid!("en-GB"), "english_british" ), + "en-US" => ( langid!("en-US"), "english_united_states" ), "es" => ( langid!("es-ES"), "spanish" ), - "es-es" => ( langid!("es-ES"), "spanish_spain" ), + "es-ES" => ( langid!("es-ES"), "spanish_spain" ), ] }); @@ -123,23 +123,14 @@ static LANGUAGES: LazyLock> = LazyLock // // Se usa cuando el valor del identificador de idioma en las traducciones no corresponde con ningún // idioma soportado por la aplicación. -pub(crate) static FALLBACK_LANGID: LazyLock = - LazyLock::new(|| langid!("en-US")); +static FALLBACK_LANGID: LazyLock = LazyLock::new(|| langid!("en-US")); // Identificador de idioma **por defecto** para la aplicación. // // Se resuelve a partir de [`global::SETTINGS.app.language`](global::SETTINGS). Si el identificador // de idioma no es válido o no está disponible entonces resuelve como [`FALLBACK_LANGID`]. -pub(crate) static DEFAULT_LANGID: LazyLock> = - LazyLock::new(|| LangMatch::resolve(&global::SETTINGS.app.language).as_option()); - -/// Representa la fuente de idioma (`LanguageIdentifier`) asociada a un recurso. -/// -/// Este *trait* permite que distintas estructuras expongan su fuente de idioma de forma uniforme. -pub trait LangId { - /// Devuelve el identificador de idioma asociado al recurso. - fn langid(&self) -> &'static LanguageIdentifier; -} +pub(crate) static DEFAULT_LANGID: LazyLock<&LanguageIdentifier> = + LazyLock::new(|| LangMatch::langid_or_fallback(&global::SETTINGS.app.language)); /// Operaciones con los idiomas soportados por `PageTop`. /// @@ -153,11 +144,11 @@ pub trait LangId { /// /// // Coincidencia exacta. /// let lang = LangMatch::resolve("es-ES"); -/// assert_eq!(lang.langid().to_string(), "es-ES"); +/// assert_eq!(lang.as_langid().to_string(), "es-ES"); /// /// // Coincidencia parcial (con el idioma base). /// let lang = LangMatch::resolve("es-EC"); -/// assert_eq!(lang.langid().to_string(), "es-ES"); // Porque "es-EC" no está soportado. +/// assert_eq!(lang.as_langid().to_string(), "es-ES"); // Porque "es-EC" no está soportado. /// /// // Idioma no especificado. /// let lang = LangMatch::resolve(""); @@ -168,16 +159,21 @@ pub trait LangId { /// assert_eq!(lang, LangMatch::Unsupported(String::from("ja-JP"))); /// ``` /// -/// Con la siguiente instrucción siempre se obtiene un [`LanguageIdentifier`] válido, ya sea porque -/// resuelve un idioma soportado o porque se aplica el idioma por defecto o, en último caso, el de -/// respaldo ("en-US"): +/// Las siguientes líneas devuelven siempre un [`LanguageIdentifier`] válido, ya sea porque +/// resuelven un idioma soportado o porque aplican el idioma por defecto o de respaldo: /// /// ```rust /// use pagetop::prelude::*; /// -/// // Idioma por defecto o de respaldo si no resuelve. +/// // Idioma por defecto si no resuelve. /// let lang = LangMatch::resolve("it-IT"); -/// let langid = lang.langid(); +/// let langid = lang.as_langid(); +/// +/// // Idioma por defecto si no se encuentra. +/// let langid = LangMatch::langid_or_default("es-MX"); +/// +/// // Idioma de respaldo ("en-US") si no se encuentra. +/// let langid = LangMatch::langid_or_fallback("es-MX"); /// ``` #[derive(Clone, Debug, Eq, PartialEq)] pub enum LangMatch { @@ -191,13 +187,6 @@ pub enum LangMatch { Unsupported(String), } -impl Default for LangMatch { - /// Resuelve al idioma por defecto y, si no está disponible, al idioma de respaldo ("en-US"). - fn default() -> Self { - LangMatch::Found(DEFAULT_LANGID.unwrap_or(&FALLBACK_LANGID)) - } -} - impl LangMatch { /// Resuelve `language` y devuelve la variante [`LangMatch`] apropiada. pub fn resolve(language: impl AsRef) -> Self { @@ -209,13 +198,12 @@ impl LangMatch { } // Intenta aplicar coincidencia exacta con el código completo (p.ej. "es-MX"). - let lang = language.to_ascii_lowercase(); - if let Some(langid) = LANGUAGES.get(lang.as_str()).map(|(langid, _)| langid) { + if let Some(langid) = LANGUAGES.get(language).map(|(langid, _)| langid) { return Self::Found(langid); } // Si la variante regional no existe, retrocede al idioma base (p.ej. "es"). - if let Some((base_lang, _)) = lang.split_once('-') { + if let Some((base_lang, _)) = language.split_once('-') { if let Some(langid) = LANGUAGES.get(base_lang).map(|(langid, _)| langid) { return Self::Found(langid); } @@ -225,47 +213,37 @@ impl LangMatch { Self::Unsupported(String::from(language)) } - /// Devuelve el [`LanguageIdentifier`] si el idioma fue reconocido. + /// Devuelve el idioma de la variante de la instancia, o el idioma por defecto si no está + /// soportado. /// - /// Solo retorna `Some` si la variante es [`LangMatch::Found`]. En cualquier otro caso (por - /// ejemplo, si el identificador es vacío o no está soportado), devuelve `None`. - /// - /// Este método es útil cuando se desea acceder directamente al idioma reconocido sin aplicar el - /// idioma por defecto ni el de respaldo. - /// - /// # Ejemplo - /// - /// ```rust - /// use pagetop::prelude::*; - /// - /// let lang = LangMatch::resolve("es-ES").as_option(); - /// assert_eq!(lang.unwrap().to_string(), "es-ES"); - /// - /// let lang = LangMatch::resolve("jp-JP").as_option(); - /// assert!(lang.is_none()); - /// ``` + /// Siempre devuelve un [`LanguageIdentifier`] válido. #[inline] - pub fn as_option(&self) -> Option<&'static LanguageIdentifier> { - match self { - LangMatch::Found(l) => Some(l), - _ => None, - } - } -} - -/// Permite a [`LangMatch`] actuar como proveedor de idioma. -/// -/// Devuelve el [`LanguageIdentifier`] si la variante es [`LangMatch::Found`]; en caso contrario, -/// devuelve el idioma por defecto de la aplicación y, si tampoco está disponible, el idioma de -/// respaldo ("en-US"). -/// -/// Resulta útil para usar un valor de [`LangMatch`] como fuente de traducción en [`L10n::using`] o -/// [`L10n::to_markup`]. -impl LangId for LangMatch { - fn langid(&self) -> &'static LanguageIdentifier { + pub fn as_langid(&self) -> &'static LanguageIdentifier { match self { LangMatch::Found(l) => l, - _ => DEFAULT_LANGID.unwrap_or(&FALLBACK_LANGID), + _ => &DEFAULT_LANGID, + } + } + + /// Si `language` está vacío o no está soportado, devuelve el idioma por defecto. + /// + /// Siempre devuelve un [`LanguageIdentifier`] válido. + #[inline] + pub fn langid_or_default(language: impl AsRef) -> &'static LanguageIdentifier { + match Self::resolve(language) { + Self::Found(l) => l, + _ => &DEFAULT_LANGID, + } + } + + /// Si `language` está vacío o no está soportado, devuelve el idioma de respaldo ("en-US"). + /// + /// Siempre devuelve un [`LanguageIdentifier`] válido. + #[inline] + pub fn langid_or_fallback(language: impl AsRef) -> &'static LanguageIdentifier { + match Self::resolve(language) { + Self::Found(l) => l, + _ => &FALLBACK_LANGID, } } } @@ -319,7 +297,7 @@ enum L10nOp { /// Cada instancia puede representar: /// /// - Un texto puro (`n()`) que no requiere traducción. -/// - Una clave para traducir un texto de las traducciones predefinidas de `PageTop` (`l()`). +/// - Una clave para traducir un texto de las traducciones por defecto de `PageTop` (`l()`). /// - Una clave para traducir de un conjunto concreto de traducciones (`t()`). /// /// # Ejemplo @@ -341,8 +319,8 @@ enum L10nOp { /// También para traducciones a idiomas concretos. /// /// ```rust,ignore -/// // Traducción con clave, conjunto de traducciones y fuente de idioma. -/// let bye = L10n::t("goodbye", &LOCALES_CUSTOM).using(&LangMatch::resolve("it")); +/// // Traducción con clave, conjunto de traducciones e identificador de idioma a usar. +/// let bye = L10n::t("goodbye", &LOCALES_CUSTOM).using(LangMatch::langid_or_default("it")); /// ``` #[derive(AutoDefault, Clone)] pub struct L10n { @@ -361,8 +339,8 @@ impl L10n { } } - /// **l** = *“lookup”*. Crea una instancia para traducir usando una clave del conjunto de - /// traducciones predefinidas. + /// **l** = *“lookup”*. Crea una instancia para traducir usando una clave de la tabla de + /// traducciones por defecto. pub fn l(key: impl Into) -> Self { L10n { op: L10nOp::Translate(key.into()), @@ -370,8 +348,8 @@ impl L10n { } } - /// **t** = *“translate”*. Crea una instancia para traducir usando una clave de un conjunto de - /// traducciones específico. + /// **t** = *“translate”*. Crea una instancia para traducir usando una clave de una tabla de + /// traducciones específica. pub fn t(key: impl Into, locales: &'static Locales) -> Self { L10n { op: L10nOp::Translate(key.into()), @@ -399,47 +377,20 @@ impl L10n { self } - /// Resuelve la traducción usando el idioma por defecto o de respaldo de la aplicación. - /// - /// Devuelve `None` si no aplica o no encuentra una traducción válida. - /// - /// # Ejemplo - /// - /// ```rust - /// use pagetop::prelude::*; - /// - /// let text = L10n::l("greeting").with_arg("name", "Manuel").get(); - /// ``` + /// Resuelve la traducción usando el idioma por defecto de la aplicación. Devuelve `None` si no + /// aplica o no encuentra una traducción. pub fn get(&self) -> Option { - self.using(&LangMatch::default()) + self.using(&DEFAULT_LANGID) } - /// Resuelve la traducción usando la fuente de idioma proporcionada. - /// - /// Devuelve `None` si no aplica o no encuentra una traducción válida. - /// - /// # Ejemplo - /// - /// ```rust - /// use pagetop::prelude::*; - /// - /// struct ResourceLang; - /// - /// impl LangId for ResourceLang { - /// fn langid(&self) -> &'static LanguageIdentifier { - /// LangMatch::resolve("es-MX").langid() - /// } - /// } - /// - /// let r = ResourceLang; - /// let text = L10n::l("greeting").with_arg("name", "Usuario").using(&r); - /// ``` - pub fn using(&self, language: &impl LangId) -> Option { + /// Resuelve la traducción usando el [`LanguageIdentifier`] indicado. Devuelve `None` si no + /// aplica o no encuentra una traducción. + pub fn using(&self, langid: &LanguageIdentifier) -> Option { match &self.op { L10nOp::None => None, L10nOp::Text(text) => Some(text.to_owned()), L10nOp::Translate(key) => self.locales.try_lookup_with_args( - language.langid(), + langid, key, &self.args.iter().fold(HashMap::new(), |mut arg, (k, v)| { arg.insert(Cow::Owned(k.clone()), v.to_owned().into()); @@ -449,19 +400,16 @@ impl L10n { } } - /// Traduce el texto y lo devuelve como [`Markup`] usando la fuente de idioma proporcionada. - /// - /// Si no se encuentra una traducción válida, devuelve una cadena vacía. - /// - /// # Ejemplo - /// - /// ```rust - /// use pagetop::prelude::*; - /// - /// let html = L10n::l("welcome.message").to_markup(&LangMatch::resolve("es")); - /// ``` - pub fn to_markup(&self, language: &impl LangId) -> Markup { - PreEscaped(self.using(language).unwrap_or_default()) + /// Traduce y escapa con el [`LanguageIdentifier`] indicado, devolviendo [`Markup`]. + pub fn escaped(&self, langid: &LanguageIdentifier) -> Markup { + PreEscaped(self.using(langid).unwrap_or_default()) + } +} + +impl Render for L10n { + /// Traduce y escapa con el idioma por defecto, devolviendo [`Markup`]. + fn render(&self) -> Markup { + PreEscaped(self.get().unwrap_or_default()) } } @@ -475,3 +423,14 @@ impl fmt::Debug for L10n { .finish() } } + +impl fmt::Display for L10n { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let content = match &self.op { + L10nOp::None => String::new(), + L10nOp::Text(text) => text.clone(), + L10nOp::Translate(key) => self.get().unwrap_or_else(|| format!("??<{}>", key)), + }; + write!(f, "{content}") + } +} diff --git a/src/response/page.rs b/src/response/page.rs index c1815d0..0964509 100644 --- a/src/response/page.rs +++ b/src/response/page.rs @@ -9,7 +9,7 @@ use crate::core::component::{Child, ChildOp, ComponentTrait}; use crate::core::theme::{ChildrenInRegions, ThemeRef, CONTENT_REGION_NAME}; use crate::html::{html, AssetsOp, Context, Markup, DOCTYPE}; use crate::html::{ClassesOp, OptionClasses, OptionId, OptionTranslated}; -use crate::locale::{CharacterDirection, L10n, LangId, LanguageIdentifier}; +use crate::locale::{CharacterDirection, L10n, LanguageIdentifier}; use crate::service::HttpRequest; /// Representa una página HTML completa lista para renderizar. @@ -78,10 +78,10 @@ impl Page { self } - /// Modifica la fuente de idioma de la página ([`Context::with_langid`]). + /// Modifica el identificador de idioma de la página ([`Context::with_langid`]). #[builder_fn] - pub fn with_langid(mut self, language: &impl LangId) -> Self { - self.context.alter_langid(language); + pub fn with_langid(mut self, langid: &'static LanguageIdentifier) -> Self { + self.context.alter_langid(langid); self } @@ -147,14 +147,14 @@ impl Page { // Page GETTERS ******************************************************************************** - /// Devuelve el título traducido para el idioma de la página, si existe. + /// Devuelve el título traducido para el idioma activo, si existe. pub fn title(&mut self) -> Option { - self.title.using(&self.context) + self.title.using(self.context.langid()) } - /// Devuelve la descripción traducida para el idioma de la página, si existe. + /// Devuelve la descripción traducida para el idioma activo, si existe. pub fn description(&mut self) -> Option { - self.description.using(&self.context) + self.description.using(self.context.langid()) } /// Devuelve la lista de metadatos ``. diff --git a/tests/locale.rs b/tests/locale.rs index ef875d7..420914d 100644 --- a/tests/locale.rs +++ b/tests/locale.rs @@ -13,7 +13,7 @@ async fn translation_without_args() { let _app = service::test::init_service(Application::new().test()).await; let l10n = L10n::l("test-hello-world"); - let translation = l10n.using(&LangMatch::resolve("es-ES")); + let translation = l10n.using(LangMatch::langid_or_default("es-ES")); assert_eq!(translation, Some("¡Hola mundo!".to_string())); } @@ -22,7 +22,7 @@ async fn translation_with_args() { let _app = service::test::init_service(Application::new().test()).await; let l10n = L10n::l("test-hello-user").with_arg("userName", "Manuel"); - let translation = l10n.using(&LangMatch::resolve("es-ES")); + let translation = l10n.using(LangMatch::langid_or_default("es-ES")); assert_eq!(translation, Some("¡Hola, Manuel!".to_string())); } @@ -35,7 +35,7 @@ async fn translation_with_plural_and_select() { ("photoCount", "3"), ("userGender", "male"), ]); - let translation = l10n.using(&LangMatch::resolve("es-ES")).unwrap(); + let translation = l10n.using(LangMatch::langid_or_default("es-ES")).unwrap(); assert!(translation.contains("añadido 3 nuevas fotos de él")); } @@ -44,7 +44,7 @@ async fn check_fallback_language() { let _app = service::test::init_service(Application::new().test()).await; let l10n = L10n::l("test-hello-world"); - let translation = l10n.using(&LangMatch::resolve("xx-YY")); // Retrocede a "en-US". + let translation = l10n.using(LangMatch::langid_or_fallback("xx-YY")); // Retrocede a "en-US". assert_eq!(translation, Some("Hello world!".to_string())); } @@ -53,6 +53,6 @@ async fn check_unknown_key() { let _app = service::test::init_service(Application::new().test()).await; let l10n = L10n::l("non-existent-key"); - let translation = l10n.using(&LangMatch::resolve("en-US")); + let translation = l10n.using(LangMatch::langid_or_default("en-US")); assert_eq!(translation, None); }