Compare commits

...

2 commits

6 changed files with 61 additions and 41 deletions

View file

@ -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, DEFAULT_LANGID, FALLBACK_LANGID};
use crate::locale::{LangId, LangMatch, LanguageIdentifier};
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 **solicitud HTTP** de origen.
/// - Almacenar la **petición 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 solicitud HTTP de origen en el contexto.
/// Almacena la petición HTTP de origen en el contexto.
#[builder_fn]
fn with_request(self, request: Option<HttpRequest>) -> Self;
@ -107,7 +107,7 @@ pub trait Contextual: LangId {
// **< Contextual GETTERS >*********************************************************************
/// Devuelve una referencia a la solicitud HTTP asociada, si existe.
/// Devuelve una referencia a la petición 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 solicitud HTTP:
/// Crea un nuevo contexto asociado a una petición HTTP:
///
/// ```rust
/// # use pagetop::prelude::*;
@ -223,26 +223,13 @@ impl Default for Context {
}
impl Context {
/// Crea un nuevo contexto asociado a una solicitud HTTP.
/// Crea un nuevo contexto asociado a una petición 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<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);
let langid = LangMatch::from_request(request.as_ref()).langid();
Context {
request,
langid,

View file

@ -9,7 +9,6 @@ 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,
@ -49,14 +48,14 @@ pub struct App {
pub description: String,
/// Tema predeterminado.
pub theme: String,
/// Idioma por defecto para la aplicación.
/// Idioma predeterminado de la aplicación.
///
/// 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,
/// 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<String>,
/// Banner ASCII mostrado al inicio: *"Off"* (desactivado), *"Slant"*, *"Small"*, *"Speed"* o
/// *"Starwars"*.
pub startup_banner: String,

View file

@ -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())

View file

@ -90,6 +90,7 @@
//! traducir textos con [`L10n`].
use crate::html::{Markup, PreEscaped};
use crate::service::HttpRequest;
use crate::{global, hm, AutoDefault};
pub use fluent_templates;
@ -122,15 +123,16 @@ 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
// idioma soportado por la aplicación.
pub(crate) static FALLBACK_LANGID: LazyLock<LanguageIdentifier> =
LazyLock::new(|| langid!("en-US"));
static FALLBACK_LANGID: LazyLock<LanguageIdentifier> = 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 usa [`FALLBACK_LANGID`].
pub(crate) static DEFAULT_LANGID: LazyLock<Option<&LanguageIdentifier>> =
LazyLock::new(|| LangMatch::resolve(&global::SETTINGS.app.language).as_option());
// 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<Option<&LanguageIdentifier>> = LazyLock::new(|| {
LangMatch::resolve(global::SETTINGS.app.language.as_deref().unwrap_or("")).as_option()
});
/// Representa la fuente de idioma (`LanguageIdentifier`) asociada a un recurso.
///
@ -168,7 +170,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::*;
@ -181,15 +183,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))
}
@ -222,6 +224,34 @@ 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

View file

@ -101,7 +101,7 @@ pub struct Page {
impl Page {
/// Crea una nueva instancia de página.
///
/// La solicitud HTTP se guardará en el contexto de renderizado de la página para poder ser
/// La petición 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, solicitud HTTP, etc.). Resulta especialmente útil cuando un componente
/// *layout*, recursos, petición 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,6 +289,10 @@ 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()

View file

@ -18,7 +18,6 @@ pub use indoc::{concatdoc, formatdoc, indoc};
// **< MACROS ÚTILES >******************************************************************************
#[macro_export]
/// Macro para construir una colección de pares clave-valor.
///
/// ```rust
@ -31,6 +30,7 @@ pub use indoc::{concatdoc, formatdoc, indoc};
/// "userGender" => "male",
/// ];
/// ```
#[macro_export]
macro_rules! hm {
( $($key:expr => $value:expr),* $(,)? ) => {{
let mut a = std::collections::HashMap::new();