diff --git a/src/core/component/context.rs b/src/core/component/context.rs index 59dd1f1a..4f2cdf18 100644 --- a/src/core/component/context.rs +++ b/src/core/component/context.rs @@ -4,7 +4,7 @@ use crate::core::theme::{ChildrenInRegions, RegionRef, TemplateRef, ThemeRef}; use crate::core::TypeInfo; use crate::html::{html, Markup}; use crate::html::{Assets, Favicon, JavaScript, StyleSheet}; -use crate::locale::{LangId, LangMatch, LanguageIdentifier}; +use crate::locale::{LangId, LangMatch, LanguageIdentifier, DEFAULT_LANGID, FALLBACK_LANGID}; use crate::service::HttpRequest; use crate::{builder_fn, join}; @@ -49,7 +49,7 @@ pub enum ContextError { /// /// `Contextual` extiende [`LangId`] para establecer el idioma del documento y añade métodos para: /// -/// - Almacenar la **petición HTTP** de origen. +/// - Almacenar la **solicitud HTTP** de origen. /// - Seleccionar el **tema** y la **plantilla** de renderizado. /// - Administrar **recursos** del documento como el icono [`Favicon`], las hojas de estilo /// [`StyleSheet`] o los scripts [`JavaScript`] mediante [`ContextOp`]. @@ -81,7 +81,7 @@ pub trait Contextual: LangId { #[builder_fn] fn with_langid(self, language: &impl LangId) -> Self; - /// Almacena la petición HTTP de origen en el contexto. + /// Almacena la solicitud HTTP de origen en el contexto. #[builder_fn] fn with_request(self, request: Option) -> Self; @@ -107,7 +107,7 @@ pub trait Contextual: LangId { // **< Contextual GETTERS >********************************************************************* - /// Devuelve una referencia a la petición HTTP asociada, si existe. + /// Devuelve una referencia a la solicitud HTTP asociada, si existe. fn request(&self) -> Option<&HttpRequest>; /// Devuelve el tema que se usará para renderizar el documento. @@ -161,7 +161,7 @@ pub trait Contextual: LangId { /// /// # Ejemplos /// -/// Crea un nuevo contexto asociado a una petición HTTP: +/// Crea un nuevo contexto asociado a una solicitud HTTP: /// /// ```rust /// # use pagetop::prelude::*; @@ -223,13 +223,26 @@ impl Default for Context { } impl Context { - /// Crea un nuevo contexto asociado a una petición HTTP. + /// Crea un nuevo contexto asociado a una solicitud HTTP. /// /// El contexto inicializa el idioma, el tema y la plantilla por defecto, sin favicon ni otros /// recursos cargados. #[rustfmt::skip] pub fn new(request: Option) -> Self { - let langid = LangMatch::from_request(request.as_ref()).langid(); + // 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, diff --git a/src/global.rs b/src/global.rs index a40669df..6726a3ef 100644 --- a/src/global.rs +++ b/src/global.rs @@ -9,6 +9,7 @@ include_config!(SETTINGS: Settings => [ "app.name" => "PageTop App", "app.description" => "Developed with the amazing PageTop framework.", "app.theme" => "Basic", + "app.language" => "", "app.startup_banner" => "Slant", "app.welcome" => true, @@ -48,14 +49,14 @@ pub struct App { pub description: String, /// Tema predeterminado. pub theme: String, - /// Idioma predeterminado de la aplicación. + /// Idioma por defecto para la aplicación. /// - /// Si queda en `None`, el idioma de renderizado se decide intentando usar el asignado con - /// [`Contextual::with_langid()`](crate::core::component::Contextual::with_langid) en el - /// contexto del documento. Si no se ha establecido, prueba el recibido en la cabecera - /// `Accept-Language` enviada por el navegador. Y si ninguno aplica, emplea el idioma de - /// respaldo (`"en-US"`). - pub language: Option, + /// Si no está definido o no es válido, [`LangId`](crate::locale::LangId) determinará el idioma + /// efectivo para el renderizado en este orden: primero intentará usar el establecido mediante + /// [`Contextual::with_langid()`](crate::core::component::Contextual::with_langid); si no se ha + /// definido explícitamente, probará el indicado en la cabecera `Accept-Language` del navegador; + /// y, si ninguno aplica, se empleará el idioma de respaldo ("en-US"). + pub language: String, /// Banner ASCII mostrado al inicio: *"Off"* (desactivado), *"Slant"*, *"Small"*, *"Speed"* o /// *"Starwars"*. pub startup_banner: String, diff --git a/src/html/attr_l10n.rs b/src/html/attr_l10n.rs index 6323701c..a92e8344 100644 --- a/src/html/attr_l10n.rs +++ b/src/html/attr_l10n.rs @@ -18,7 +18,7 @@ use crate::{builder_fn, AutoDefault}; /// Some("¡Hola mundo!".to_string()) /// ); /// -/// // Japonés no disponible, traduce al idioma de respaldo (`"en-US"`). +/// // Japonés no disponible, traduce al idioma de respaldo ("en-US"). /// assert_eq!( /// hello.lookup(&LangMatch::resolve("ja-JP")), /// Some("Hello world!".to_string()) diff --git a/src/locale.rs b/src/locale.rs index 11db7252..0c1ad345 100644 --- a/src/locale.rs +++ b/src/locale.rs @@ -90,7 +90,6 @@ //! traducir textos con [`L10n`]. use crate::html::{Markup, PreEscaped}; -use crate::service::HttpRequest; use crate::{global, hm, AutoDefault}; pub use fluent_templates; @@ -123,16 +122,15 @@ 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. -static FALLBACK_LANGID: LazyLock = LazyLock::new(|| langid!("en-US")); +pub(crate) 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, se deja sin definir (`None`) y se delega en -// [`LangMatch::default()`] o [`LangId::langid()`] la aplicación del idioma de respaldo. -pub(crate) static DEFAULT_LANGID: LazyLock> = LazyLock::new(|| { - LangMatch::resolve(global::SETTINGS.app.language.as_deref().unwrap_or("")).as_option() -}); +// de idioma no es válido o no está disponible, se usa [`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. /// @@ -170,7 +168,7 @@ pub trait LangId { /// /// 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"`): +/// respaldo ("en-US"): /// /// ```rust /// # use pagetop::prelude::*; @@ -183,15 +181,15 @@ pub enum LangMatch { /// Cuando el identificador de idioma es una cadena vacía. Unspecified, /// Si encuentra un [`LanguageIdentifier`] en la lista de idiomas soportados por PageTop que - /// coincide exactamente con el identificador de idioma (p. ej. `"es-ES"`), o con el - /// identificador del idioma base (p. ej. `"es"`). + /// coincide exactamente con el identificador de idioma (p. ej. "es-ES"), o con el identificador + /// del idioma base (p. ej. "es"). Found(&'static LanguageIdentifier), /// Si el identificador de idioma no está entre los soportados por PageTop. Unsupported(String), } impl Default for LangMatch { - /// Resuelve al idioma por defecto y, si no está disponible, al idioma de respaldo (`"en-US"`). + /// 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)) } @@ -224,34 +222,6 @@ impl LangMatch { Self::Unsupported(language.to_string()) } - /// Crea un [`LangMatch`] a partir de una petición HTTP. - /// - /// El orden de resolución del idioma es el siguiente: - /// - /// 1. Idioma por defecto de la aplicación, si se ha definido en la configuración global - /// ([`global::SETTINGS.app.language`]). - /// 2. Si no hay idioma por defecto válido, se intenta extraer el idioma de la cabecera HTTP - /// `Accept-Language` usando [`LangMatch::resolve`]. - /// 3. Si no hay cabecera o el valor no es legible, se devuelve [`LangMatch::Unspecified`]. - /// - /// Este método **no aplica** idioma de respaldo. Para obtener siempre un [`LanguageIdentifier`] - /// válido (aplicando idioma por defecto y, en último término, el de respaldo), utiliza - /// [`LangId::langid()`] sobre el valor devuelto. - pub fn from_request(request: Option<&HttpRequest>) -> Self { - // 1) Se usa `DEFAULT_LANGID` si la aplicación tiene un idioma por defecto válido. - if let Some(default) = *DEFAULT_LANGID { - return LangMatch::Found(default); - } - // 2) Sin idioma por defecto, se evalúa la cabecera `Accept-Language` de la petición HTTP. - request - .and_then(|req| req.headers().get("Accept-Language")) - .and_then(|value| value.to_str().ok()) - // Aplica `resolve()` para devolver `Found`, `Unspecified` o `Unsupported`. - .map(LangMatch::resolve) - // 3) Si no hay cabecera o no puede leerse, se considera no especificado. - .unwrap_or(LangMatch::Unspecified) - } - /// Devuelve el [`LanguageIdentifier`] si el idioma fue reconocido. /// /// Solo retorna `Some` si la variante es [`LangMatch::Found`]. En cualquier otro caso (por diff --git a/src/response/page.rs b/src/response/page.rs index 90ce84e1..c4534f4c 100644 --- a/src/response/page.rs +++ b/src/response/page.rs @@ -101,7 +101,7 @@ pub struct Page { impl Page { /// Crea una nueva instancia de página. /// - /// La petición HTTP se guardará en el contexto de renderizado de la página para poder ser + /// La solicitud HTTP se guardará en el contexto de renderizado de la página para poder ser /// recuperada por los componentes si es necesario. #[rustfmt::skip] pub fn new(request: HttpRequest) -> Self { @@ -211,7 +211,7 @@ impl Page { /// Devuelve una referencia mutable al [`Context`] de la página. /// /// El [`Context`] actúa como intermediario para muchos métodos de `Page` (idioma, tema, - /// *layout*, recursos, petición HTTP, etc.). Resulta especialmente útil cuando un componente + /// *layout*, recursos, solicitud HTTP, etc.). Resulta especialmente útil cuando un componente /// o un tema necesita recibir el contexto como parámetro. pub fn context(&mut self) -> &mut Context { &mut self.context @@ -289,10 +289,6 @@ impl Page { } } -/// Permite a [`Page`] actuar como proveedor de idioma usando el [`Context`] de la página. -/// -/// Resulta útil para usar [`Page`] directamente como fuente de traducción en [`L10n::lookup()`] o -/// [`L10n::using()`]. impl LangId for Page { fn langid(&self) -> &'static LanguageIdentifier { self.context.langid() diff --git a/src/util.rs b/src/util.rs index 1cb21ae7..bfb50ec0 100644 --- a/src/util.rs +++ b/src/util.rs @@ -18,6 +18,7 @@ pub use indoc::{concatdoc, formatdoc, indoc}; // **< MACROS ÚTILES >****************************************************************************** +#[macro_export] /// Macro para construir una colección de pares clave-valor. /// /// ```rust @@ -30,7 +31,6 @@ pub use indoc::{concatdoc, formatdoc, indoc}; /// "userGender" => "male", /// ]; /// ``` -#[macro_export] macro_rules! hm { ( $($key:expr => $value:expr),* $(,)? ) => {{ let mut a = std::collections::HashMap::new();