Compare commits

...

3 commits

9 changed files with 221 additions and 138 deletions

View file

@ -28,12 +28,15 @@ async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
.with_title(L10n::l("welcome_page")) .with_title(L10n::l("welcome_page"))
.with_theme("Basic") .with_theme("Basic")
.with_assets(AssetsOp::AddStyleSheet(StyleSheet::from("/css/welcome.css"))) .with_assets(AssetsOp::AddStyleSheet(StyleSheet::from("/css/welcome.css")))
.with_component(Html::with(move |_| html! { .with_component(Html::with(move |cx| html! {
div id="main-header" { div id="main-header" {
header { header {
h1 id="header-title" aria-label=(L10n::l("welcome_aria").with_arg("app", app)) { h1
span { (L10n::l("welcome_title")) } id="header-title"
(L10n::l("welcome_intro").with_arg("app", app)) 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))
} }
} }
aside id="header-image" aria-hidden="true" { aside id="header-image" aria-hidden="true" {
@ -59,20 +62,25 @@ async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
main id="main-content" { main id="main-content" {
section class="content-body" { section class="content-body" {
div id="poweredby-button" { 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 {} span {} span {} span {}
div id="poweredby-text" { (L10n::l("welcome_powered")) } div id="poweredby-text" { (L10n::l("welcome_powered").to_markup(cx)) }
} }
} }
div class="content-text" { div class="content-text" {
p { (L10n::l("welcome_text1")) } p { (L10n::l("welcome_text1").to_markup(cx)) }
p { (L10n::l("welcome_text2")) } p { (L10n::l("welcome_text2").to_markup(cx)) }
div class="subcontent" { div class="subcontent" {
h1 { span { (L10n::l("welcome_about")) } } h1 { span { (L10n::l("welcome_about").to_markup(cx)) } }
p { (L10n::l("welcome_pagetop")) } p { (L10n::l("welcome_pagetop").to_markup(cx)) }
p { (L10n::l("welcome_issues1")) } p { (L10n::l("welcome_issues1").to_markup(cx)) }
p { (L10n::l("welcome_issues2").with_arg("app", app)) } p { (L10n::l("welcome_issues2").with_arg("app", app).to_markup(cx)) }
} }
} }
} }
@ -85,7 +93,7 @@ async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
viewBox="0 0 1614 1614" viewBox="0 0 1614 1614"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
role="img" role="img"
aria-label=(L10n::l("pagetop_logo")) aria-label=[L10n::l("pagetop_logo").using(cx)]
preserveAspectRatio="xMidYMid slice" preserveAspectRatio="xMidYMid slice"
focusable="false" focusable="false"
{ {
@ -97,8 +105,8 @@ async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
div class="footer-links" { div class="footer-links" {
a href="https://crates.io/crates/pagetop" target="_blank" rel="noreferrer" { ("Crates.io") } 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://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")) } 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")) } em { (L10n::l("welcome_have_fun").to_markup(cx)) }
} }
} }
} }

View file

@ -240,7 +240,7 @@ pub static CONFIG_VALUES: LazyLock<ConfigBuilder<DefaultState>> = LazyLock::new(
macro_rules! include_config { macro_rules! include_config {
( $SETTINGS_NAME:ident : $Settings_Type:ty => [ $( $k:literal => $v:expr ),* $(,)? ] ) => { ( $SETTINGS_NAME:ident : $Settings_Type:ty => [ $( $k:literal => $v:expr ),* $(,)? ] ) => {
#[doc = concat!( #[doc = concat!(
"Referencia a los ajustes de configuración deserializados de [`", "Referencia y valores por defecto de los ajustes de configuración para [`",
stringify!($Settings_Type), stringify!($Settings_Type),
"`]." "`]."
)] )]

View file

@ -74,12 +74,12 @@ impl InRegion {
/// use pagetop::prelude::*; /// use pagetop::prelude::*;
/// ///
/// // Banner global, en la región por defecto de cualquier página. /// // 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!") } /// html! { ("🎉 ¡Bienvenido!") }
/// ))); /// )));
/// ///
/// // Texto en la región "sidebar". /// // Texto en la región "sidebar".
/// InRegion::Named("sidebar").add(Child::with(Html::with( /// InRegion::Named("sidebar").add(Child::with(Html::with(|_|
/// html! { ("Publicidad") } /// html! { ("Publicidad") }
/// ))); /// )));
/// ``` /// ```

View file

@ -9,7 +9,7 @@ include_config!(SETTINGS: Settings => [
"app.name" => "PageTop App", "app.name" => "PageTop App",
"app.description" => "Developed with the amazing PageTop framework.", "app.description" => "Developed with the amazing PageTop framework.",
"app.theme" => "Basic", "app.theme" => "Basic",
"app.language" => "en-US", "app.language" => "",
"app.startup_banner" => "Slant", "app.startup_banner" => "Slant",
// [dev] // [dev]
@ -48,7 +48,14 @@ pub struct App {
pub description: String, pub description: String,
/// Tema predeterminado. /// Tema predeterminado.
pub theme: String, pub theme: String,
/// Idioma predeterminado (localización). /// 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").
pub language: String, pub language: String,
/// Banner ASCII mostrado al inicio: *"Off"* (desactivado), *"Slant"*, *"Small"*, *"Speed"* o /// Banner ASCII mostrado al inicio: *"Off"* (desactivado), *"Slant"*, *"Small"*, *"Speed"* o
/// *"Starwars"*. /// *"Starwars"*.

View file

@ -3,7 +3,7 @@ use crate::core::theme::ThemeRef;
use crate::core::TypeInfo; use crate::core::TypeInfo;
use crate::html::{html, Markup}; use crate::html::{html, Markup};
use crate::html::{Assets, Favicon, JavaScript, StyleSheet}; use crate::html::{Assets, Favicon, JavaScript, StyleSheet};
use crate::locale::{LanguageIdentifier, DEFAULT_LANGID}; use crate::locale::{LangId, LangMatch, LanguageIdentifier, DEFAULT_LANGID, FALLBACK_LANGID};
use crate::service::HttpRequest; use crate::service::HttpRequest;
use crate::{builder_fn, join}; use crate::{builder_fn, join};
@ -69,9 +69,9 @@ impl Error for ErrorParam {}
/// use pagetop::prelude::*; /// use pagetop::prelude::*;
/// ///
/// fn new_context(request: HttpRequest) -> Context { /// fn new_context(request: HttpRequest) -> Context {
/// Context::new(request) /// Context::new(Some(request))
/// // Establece el idioma del documento a español. /// // Establece el idioma del documento a español.
/// .with_langid(LangMatch::langid_or_default("es-ES")) /// .with_langid(&LangMatch::resolve("es-ES"))
/// // Selecciona un tema (por su nombre corto). /// // Selecciona un tema (por su nombre corto).
/// .with_theme("aliner") /// .with_theme("aliner")
/// // Asigna un favicon. /// // Asigna un favicon.
@ -125,9 +125,23 @@ impl Context {
/// cargados. /// cargados.
#[rustfmt::skip] #[rustfmt::skip]
pub fn new(request: Option<HttpRequest>) -> Self { pub fn new(request: Option<HttpRequest>) -> 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<HttpRequest>` 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 { Context {
request, request,
langid : &DEFAULT_LANGID, langid,
theme : *DEFAULT_THEME, theme : *DEFAULT_THEME,
layout : "default", layout : "default",
favicon : None, favicon : None,
@ -140,10 +154,10 @@ impl Context {
// Context BUILDER ***************************************************************************** // Context BUILDER *****************************************************************************
/// Modifica el identificador de idioma del documento. /// Modifica la fuente de idioma del documento.
#[builder_fn] #[builder_fn]
pub fn with_langid(mut self, langid: &'static LanguageIdentifier) -> Self { pub fn with_langid(mut self, language: &impl LangId) -> Self {
self.langid = langid; self.langid = language.langid();
self self
} }
@ -202,11 +216,6 @@ impl Context {
self.request.as_ref() 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. /// Devuelve el tema que se usará para renderizar el documento.
pub fn theme(&self) -> ThemeRef { pub fn theme(&self) -> ThemeRef {
self.theme self.theme
@ -281,3 +290,21 @@ 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
}
}

View file

@ -1,5 +1,5 @@
use crate::html::Markup; use crate::html::Markup;
use crate::locale::{L10n, LanguageIdentifier}; use crate::locale::{L10n, LangId};
use crate::{builder_fn, AutoDefault}; use crate::{builder_fn, AutoDefault};
/// Cadena para traducir al renderizar ([`locale`](crate::locale)). /// Cadena para traducir al renderizar ([`locale`](crate::locale)).
@ -16,18 +16,18 @@ use crate::{builder_fn, AutoDefault};
/// ///
/// // Español disponible. /// // Español disponible.
/// assert_eq!( /// assert_eq!(
/// hello.using(LangMatch::langid_or_default("es-ES")), /// hello.using(&LangMatch::resolve("es-ES")),
/// Some(String::from("¡Hola mundo!")) /// Some(String::from("¡Hola mundo!"))
/// ); /// );
/// ///
/// // Japonés no disponible, traduce al idioma de respaldo ("en-US"). /// // Japonés no disponible, traduce al idioma de respaldo ("en-US").
/// assert_eq!( /// assert_eq!(
/// hello.using(LangMatch::langid_or_fallback("ja-JP")), /// hello.using(&LangMatch::resolve("ja-JP")),
/// Some(String::from("Hello world!")) /// Some(String::from("Hello world!"))
/// ); /// );
/// ///
/// // Para incrustar en HTML escapado: /// // Para incrustar en HTML escapado:
/// let markup = hello.escaped(LangMatch::langid_or_default("es-ES")); /// let markup = hello.to_markup(&LangMatch::resolve("es-ES"));
/// assert_eq!(markup.into_string(), "¡Hola mundo!"); /// assert_eq!(markup.into_string(), "¡Hola mundo!");
/// ``` /// ```
#[derive(AutoDefault, Clone, Debug)] #[derive(AutoDefault, Clone, Debug)]
@ -50,16 +50,16 @@ impl OptionTranslated {
// OptionTranslated GETTERS ******************************************************************** // OptionTranslated GETTERS ********************************************************************
/// Devuelve la traducción para `langid`, si existe. /// Devuelve la traducción para `language`, si existe.
pub fn using(&self, langid: &LanguageIdentifier) -> Option<String> { pub fn using(&self, language: &impl LangId) -> Option<String> {
self.0.using(langid) self.0.using(language)
} }
/// Devuelve la traducción *escapada* como [`Markup`] para `langid`, si existe. /// Devuelve la traducción *escapada* como [`Markup`] para `language`, si existe.
/// ///
/// Útil para incrustar el texto directamente en plantillas HTML sin riesgo de inyección de /// Útil para incrustar el texto directamente en plantillas HTML sin riesgo de inyección de
/// contenido. /// contenido.
pub fn escaped(&self, langid: &LanguageIdentifier) -> Markup { pub fn to_markup(&self, language: &impl LangId) -> Markup {
self.0.escaped(langid) self.0.to_markup(language)
} }
} }

View file

@ -90,7 +90,7 @@
//! Y *voilà*, sólo queda operar con los idiomas soportados por `PageTop` usando [`LangMatch`] y //! Y *voilà*, sólo queda operar con los idiomas soportados por `PageTop` usando [`LangMatch`] y
//! traducir textos con [`L10n`]. //! traducir textos con [`L10n`].
use crate::html::{Markup, PreEscaped, Render}; use crate::html::{Markup, PreEscaped};
use crate::{global, hm, AutoDefault}; use crate::{global, hm, AutoDefault};
pub use fluent_templates; pub use fluent_templates;
@ -112,10 +112,10 @@ use std::fmt;
static LANGUAGES: LazyLock<HashMap<&str, (LanguageIdentifier, &str)>> = LazyLock::new(|| { static LANGUAGES: LazyLock<HashMap<&str, (LanguageIdentifier, &str)>> = LazyLock::new(|| {
hm![ hm![
"en" => ( langid!("en-US"), "english" ), "en" => ( langid!("en-US"), "english" ),
"en-GB" => ( langid!("en-GB"), "english_british" ), "en-gb" => ( langid!("en-GB"), "english_british" ),
"en-US" => ( langid!("en-US"), "english_united_states" ), "en-us" => ( langid!("en-US"), "english_united_states" ),
"es" => ( langid!("es-ES"), "spanish" ), "es" => ( langid!("es-ES"), "spanish" ),
"es-ES" => ( langid!("es-ES"), "spanish_spain" ), "es-es" => ( langid!("es-ES"), "spanish_spain" ),
] ]
}); });
@ -123,14 +123,23 @@ static LANGUAGES: LazyLock<HashMap<&str, (LanguageIdentifier, &str)>> = LazyLock
// //
// Se usa cuando el valor del identificador de idioma en las traducciones no corresponde con ningún // Se usa cuando el valor del identificador de idioma en las traducciones no corresponde con ningún
// idioma soportado por la aplicación. // idioma soportado por la aplicación.
static FALLBACK_LANGID: LazyLock<LanguageIdentifier> = LazyLock::new(|| langid!("en-US")); pub(crate) static FALLBACK_LANGID: LazyLock<LanguageIdentifier> =
LazyLock::new(|| langid!("en-US"));
// Identificador de idioma **por defecto** para la aplicación. // Identificador de idioma **por defecto** para la aplicación.
// //
// Se resuelve a partir de [`global::SETTINGS.app.language`](global::SETTINGS). Si el identificador // 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`]. // de idioma no es válido o no está disponible entonces resuelve como [`FALLBACK_LANGID`].
pub(crate) static DEFAULT_LANGID: LazyLock<&LanguageIdentifier> = pub(crate) static DEFAULT_LANGID: LazyLock<Option<&LanguageIdentifier>> =
LazyLock::new(|| LangMatch::langid_or_fallback(&global::SETTINGS.app.language)); 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;
}
/// Operaciones con los idiomas soportados por `PageTop`. /// Operaciones con los idiomas soportados por `PageTop`.
/// ///
@ -144,11 +153,11 @@ pub(crate) static DEFAULT_LANGID: LazyLock<&LanguageIdentifier> =
/// ///
/// // Coincidencia exacta. /// // Coincidencia exacta.
/// let lang = LangMatch::resolve("es-ES"); /// let lang = LangMatch::resolve("es-ES");
/// assert_eq!(lang.as_langid().to_string(), "es-ES"); /// assert_eq!(lang.langid().to_string(), "es-ES");
/// ///
/// // Coincidencia parcial (con el idioma base). /// // Coincidencia parcial (con el idioma base).
/// let lang = LangMatch::resolve("es-EC"); /// let lang = LangMatch::resolve("es-EC");
/// assert_eq!(lang.as_langid().to_string(), "es-ES"); // Porque "es-EC" no está soportado. /// assert_eq!(lang.langid().to_string(), "es-ES"); // Porque "es-EC" no está soportado.
/// ///
/// // Idioma no especificado. /// // Idioma no especificado.
/// let lang = LangMatch::resolve(""); /// let lang = LangMatch::resolve("");
@ -159,21 +168,16 @@ pub(crate) static DEFAULT_LANGID: LazyLock<&LanguageIdentifier> =
/// assert_eq!(lang, LangMatch::Unsupported(String::from("ja-JP"))); /// assert_eq!(lang, LangMatch::Unsupported(String::from("ja-JP")));
/// ``` /// ```
/// ///
/// Las siguientes líneas devuelven siempre un [`LanguageIdentifier`] válido, ya sea porque /// Con la siguiente instrucción siempre se obtiene un [`LanguageIdentifier`] válido, ya sea porque
/// resuelven un idioma soportado o porque aplican el idioma por defecto o de respaldo: /// resuelve un idioma soportado o porque se aplica el idioma por defecto o, en último caso, el de
/// respaldo ("en-US"):
/// ///
/// ```rust /// ```rust
/// use pagetop::prelude::*; /// use pagetop::prelude::*;
/// ///
/// // Idioma por defecto si no resuelve. /// // Idioma por defecto o de respaldo si no resuelve.
/// let lang = LangMatch::resolve("it-IT"); /// let lang = LangMatch::resolve("it-IT");
/// let langid = lang.as_langid(); /// let langid = lang.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)] #[derive(Clone, Debug, Eq, PartialEq)]
pub enum LangMatch { pub enum LangMatch {
@ -187,6 +191,13 @@ pub enum LangMatch {
Unsupported(String), 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 { impl LangMatch {
/// Resuelve `language` y devuelve la variante [`LangMatch`] apropiada. /// Resuelve `language` y devuelve la variante [`LangMatch`] apropiada.
pub fn resolve(language: impl AsRef<str>) -> Self { pub fn resolve(language: impl AsRef<str>) -> Self {
@ -198,12 +209,13 @@ impl LangMatch {
} }
// Intenta aplicar coincidencia exacta con el código completo (p.ej. "es-MX"). // Intenta aplicar coincidencia exacta con el código completo (p.ej. "es-MX").
if let Some(langid) = LANGUAGES.get(language).map(|(langid, _)| langid) { let lang = language.to_ascii_lowercase();
if let Some(langid) = LANGUAGES.get(lang.as_str()).map(|(langid, _)| langid) {
return Self::Found(langid); return Self::Found(langid);
} }
// Si la variante regional no existe, retrocede al idioma base (p.ej. "es"). // Si la variante regional no existe, retrocede al idioma base (p.ej. "es").
if let Some((base_lang, _)) = language.split_once('-') { if let Some((base_lang, _)) = lang.split_once('-') {
if let Some(langid) = LANGUAGES.get(base_lang).map(|(langid, _)| langid) { if let Some(langid) = LANGUAGES.get(base_lang).map(|(langid, _)| langid) {
return Self::Found(langid); return Self::Found(langid);
} }
@ -213,37 +225,47 @@ impl LangMatch {
Self::Unsupported(String::from(language)) Self::Unsupported(String::from(language))
} }
/// Devuelve el idioma de la variante de la instancia, o el idioma por defecto si no está /// Devuelve el [`LanguageIdentifier`] si el idioma fue reconocido.
/// soportado.
/// ///
/// Siempre devuelve un [`LanguageIdentifier`] válido. /// 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());
/// ```
#[inline] #[inline]
pub fn as_langid(&self) -> &'static LanguageIdentifier { 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 {
match self { match self {
LangMatch::Found(l) => l, LangMatch::Found(l) => l,
_ => &DEFAULT_LANGID, _ => DEFAULT_LANGID.unwrap_or(&FALLBACK_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<str>) -> &'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<str>) -> &'static LanguageIdentifier {
match Self::resolve(language) {
Self::Found(l) => l,
_ => &FALLBACK_LANGID,
} }
} }
} }
@ -297,7 +319,7 @@ enum L10nOp {
/// Cada instancia puede representar: /// Cada instancia puede representar:
/// ///
/// - Un texto puro (`n()`) que no requiere traducción. /// - Un texto puro (`n()`) que no requiere traducción.
/// - Una clave para traducir un texto de las traducciones por defecto de `PageTop` (`l()`). /// - Una clave para traducir un texto de las traducciones predefinidas de `PageTop` (`l()`).
/// - Una clave para traducir de un conjunto concreto de traducciones (`t()`). /// - Una clave para traducir de un conjunto concreto de traducciones (`t()`).
/// ///
/// # Ejemplo /// # Ejemplo
@ -319,8 +341,8 @@ enum L10nOp {
/// También para traducciones a idiomas concretos. /// También para traducciones a idiomas concretos.
/// ///
/// ```rust,ignore /// ```rust,ignore
/// // Traducción con clave, conjunto de traducciones e identificador de idioma a usar. /// // Traducción con clave, conjunto de traducciones y fuente de idioma.
/// let bye = L10n::t("goodbye", &LOCALES_CUSTOM).using(LangMatch::langid_or_default("it")); /// let bye = L10n::t("goodbye", &LOCALES_CUSTOM).using(&LangMatch::resolve("it"));
/// ``` /// ```
#[derive(AutoDefault, Clone)] #[derive(AutoDefault, Clone)]
pub struct L10n { pub struct L10n {
@ -339,8 +361,8 @@ impl L10n {
} }
} }
/// **l** = *“lookup”*. Crea una instancia para traducir usando una clave de la tabla de /// **l** = *“lookup”*. Crea una instancia para traducir usando una clave del conjunto de
/// traducciones por defecto. /// traducciones predefinidas.
pub fn l(key: impl Into<String>) -> Self { pub fn l(key: impl Into<String>) -> Self {
L10n { L10n {
op: L10nOp::Translate(key.into()), op: L10nOp::Translate(key.into()),
@ -348,8 +370,8 @@ impl L10n {
} }
} }
/// **t** = *“translate”*. Crea una instancia para traducir usando una clave de una tabla de /// **t** = *“translate”*. Crea una instancia para traducir usando una clave de un conjunto de
/// traducciones específica. /// traducciones específico.
pub fn t(key: impl Into<String>, locales: &'static Locales) -> Self { pub fn t(key: impl Into<String>, locales: &'static Locales) -> Self {
L10n { L10n {
op: L10nOp::Translate(key.into()), op: L10nOp::Translate(key.into()),
@ -377,20 +399,47 @@ impl L10n {
self self
} }
/// Resuelve la traducción usando el idioma por defecto de la aplicación. Devuelve `None` si no /// Resuelve la traducción usando el idioma por defecto o de respaldo de la aplicación.
/// aplica o no encuentra una traducció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();
/// ```
pub fn get(&self) -> Option<String> { pub fn get(&self) -> Option<String> {
self.using(&DEFAULT_LANGID) self.using(&LangMatch::default())
} }
/// Resuelve la traducción usando el [`LanguageIdentifier`] indicado. Devuelve `None` si no /// Resuelve la traducción usando la fuente de idioma proporcionada.
/// aplica o no encuentra una traducción. ///
pub fn using(&self, langid: &LanguageIdentifier) -> Option<String> { /// 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<String> {
match &self.op { match &self.op {
L10nOp::None => None, L10nOp::None => None,
L10nOp::Text(text) => Some(text.to_owned()), L10nOp::Text(text) => Some(text.to_owned()),
L10nOp::Translate(key) => self.locales.try_lookup_with_args( L10nOp::Translate(key) => self.locales.try_lookup_with_args(
langid, language.langid(),
key, key,
&self.args.iter().fold(HashMap::new(), |mut arg, (k, v)| { &self.args.iter().fold(HashMap::new(), |mut arg, (k, v)| {
arg.insert(Cow::Owned(k.clone()), v.to_owned().into()); arg.insert(Cow::Owned(k.clone()), v.to_owned().into());
@ -400,16 +449,19 @@ impl L10n {
} }
} }
/// Traduce y escapa con el [`LanguageIdentifier`] indicado, devolviendo [`Markup`]. /// Traduce el texto y lo devuelve como [`Markup`] usando la fuente de idioma proporcionada.
pub fn escaped(&self, langid: &LanguageIdentifier) -> Markup { ///
PreEscaped(self.using(langid).unwrap_or_default()) /// Si no se encuentra una traducción válida, devuelve una cadena vacía.
} ///
} /// # Ejemplo
///
impl Render for L10n { /// ```rust
/// Traduce y escapa con el idioma por defecto, devolviendo [`Markup`]. /// use pagetop::prelude::*;
fn render(&self) -> Markup { ///
PreEscaped(self.get().unwrap_or_default()) /// 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())
} }
} }
@ -423,14 +475,3 @@ impl fmt::Debug for L10n {
.finish() .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}")
}
}

View file

@ -9,7 +9,7 @@ use crate::core::component::{Child, ChildOp, ComponentTrait};
use crate::core::theme::{ChildrenInRegions, ThemeRef, CONTENT_REGION_NAME}; use crate::core::theme::{ChildrenInRegions, ThemeRef, CONTENT_REGION_NAME};
use crate::html::{html, AssetsOp, Context, Markup, DOCTYPE}; use crate::html::{html, AssetsOp, Context, Markup, DOCTYPE};
use crate::html::{ClassesOp, OptionClasses, OptionId, OptionTranslated}; use crate::html::{ClassesOp, OptionClasses, OptionId, OptionTranslated};
use crate::locale::{CharacterDirection, L10n, LanguageIdentifier}; use crate::locale::{CharacterDirection, L10n, LangId, LanguageIdentifier};
use crate::service::HttpRequest; use crate::service::HttpRequest;
/// Representa una página HTML completa lista para renderizar. /// Representa una página HTML completa lista para renderizar.
@ -78,10 +78,10 @@ impl Page {
self self
} }
/// Modifica el identificador de idioma de la página ([`Context::with_langid`]). /// Modifica la fuente de idioma de la página ([`Context::with_langid`]).
#[builder_fn] #[builder_fn]
pub fn with_langid(mut self, langid: &'static LanguageIdentifier) -> Self { pub fn with_langid(mut self, language: &impl LangId) -> Self {
self.context.alter_langid(langid); self.context.alter_langid(language);
self self
} }
@ -147,14 +147,14 @@ impl Page {
// Page GETTERS ******************************************************************************** // Page GETTERS ********************************************************************************
/// Devuelve el título traducido para el idioma activo, si existe. /// Devuelve el título traducido para el idioma de la página, si existe.
pub fn title(&mut self) -> Option<String> { pub fn title(&mut self) -> Option<String> {
self.title.using(self.context.langid()) self.title.using(&self.context)
} }
/// Devuelve la descripción traducida para el idioma activo, si existe. /// Devuelve la descripción traducida para el idioma de la página, si existe.
pub fn description(&mut self) -> Option<String> { pub fn description(&mut self) -> Option<String> {
self.description.using(self.context.langid()) self.description.using(&self.context)
} }
/// Devuelve la lista de metadatos `<meta name=...>`. /// Devuelve la lista de metadatos `<meta name=...>`.

View file

@ -13,7 +13,7 @@ async fn translation_without_args() {
let _app = service::test::init_service(Application::new().test()).await; let _app = service::test::init_service(Application::new().test()).await;
let l10n = L10n::l("test-hello-world"); let l10n = L10n::l("test-hello-world");
let translation = l10n.using(LangMatch::langid_or_default("es-ES")); let translation = l10n.using(&LangMatch::resolve("es-ES"));
assert_eq!(translation, Some("¡Hola mundo!".to_string())); 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 _app = service::test::init_service(Application::new().test()).await;
let l10n = L10n::l("test-hello-user").with_arg("userName", "Manuel"); let l10n = L10n::l("test-hello-user").with_arg("userName", "Manuel");
let translation = l10n.using(LangMatch::langid_or_default("es-ES")); let translation = l10n.using(&LangMatch::resolve("es-ES"));
assert_eq!(translation, Some("¡Hola, Manuel!".to_string())); assert_eq!(translation, Some("¡Hola, Manuel!".to_string()));
} }
@ -35,7 +35,7 @@ async fn translation_with_plural_and_select() {
("photoCount", "3"), ("photoCount", "3"),
("userGender", "male"), ("userGender", "male"),
]); ]);
let translation = l10n.using(LangMatch::langid_or_default("es-ES")).unwrap(); let translation = l10n.using(&LangMatch::resolve("es-ES")).unwrap();
assert!(translation.contains("añadido 3 nuevas fotos de él")); 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 _app = service::test::init_service(Application::new().test()).await;
let l10n = L10n::l("test-hello-world"); let l10n = L10n::l("test-hello-world");
let translation = l10n.using(LangMatch::langid_or_fallback("xx-YY")); // Retrocede a "en-US". let translation = l10n.using(&LangMatch::resolve("xx-YY")); // Retrocede a "en-US".
assert_eq!(translation, Some("Hello world!".to_string())); 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 _app = service::test::init_service(Application::new().test()).await;
let l10n = L10n::l("non-existent-key"); let l10n = L10n::l("non-existent-key");
let translation = l10n.using(LangMatch::langid_or_default("en-US")); let translation = l10n.using(&LangMatch::resolve("en-US"));
assert_eq!(translation, None); assert_eq!(translation, None);
} }