From 10a8a1136c5c9eb5f515d7af946fe131c37f3184 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Wed, 3 Dec 2025 22:55:24 +0100 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactoriza=20gesti=C3=B3n?= =?UTF-8?q?=20de=20idiomas=20en=20el=20contexto?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/component/context.rs | 19 +++----------- src/global.rs | 15 +++++------ src/locale.rs | 48 ++++++++++++++++++++++++++++------- 3 files changed, 49 insertions(+), 33 deletions(-) diff --git a/src/core/component/context.rs b/src/core/component/context.rs index 4f2cdf18..3a531312 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, DEFAULT_LANGID, FALLBACK_LANGID}; +use crate::locale::{LangId, LangMatch, LanguageIdentifier}; use crate::service::HttpRequest; use crate::{builder_fn, join}; @@ -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) -> 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); - + let langid = LangMatch::from_request(request.as_ref()).langid(); Context { request, langid, diff --git a/src/global.rs b/src/global.rs index 6726a3ef..a40669df 100644 --- a/src/global.rs +++ b/src/global.rs @@ -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, /// Banner ASCII mostrado al inicio: *"Off"* (desactivado), *"Slant"*, *"Small"*, *"Speed"* o /// *"Starwars"*. pub startup_banner: String, diff --git a/src/locale.rs b/src/locale.rs index 0c1ad345..11db7252 100644 --- a/src/locale.rs +++ b/src/locale.rs @@ -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> = 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, se usa [`FALLBACK_LANGID`]. -pub(crate) static DEFAULT_LANGID: LazyLock> = - 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> = 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