From 19c16d962f708deaa2376eba20618de10ef89212 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Thu, 4 Sep 2025 00:27:25 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20[locale]=20Mejora=20el=20uso=20d?= =?UTF-8?q?e=20`lookup`=20/=20`using`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/base/component/poweredby.rs | 2 +- src/html/attr_l10n.rs | 29 ++++++----- src/html/context.rs | 3 +- src/locale.rs | 86 +++++++++++++++++++-------------- src/response/page.rs | 4 +- tests/locale.rs | 10 ++-- 6 files changed, 75 insertions(+), 59 deletions(-) diff --git a/src/base/component/poweredby.rs b/src/base/component/poweredby.rs index afa8db7..bfe3835 100644 --- a/src/base/component/poweredby.rs +++ b/src/base/component/poweredby.rs @@ -31,7 +31,7 @@ impl Component for PoweredBy { span class="poweredby__copyright" { (c) "." } " " } span class="poweredby__pagetop" { - (L10n::l("poweredby_pagetop").with_arg("pagetop_link", LINK).to_markup(cx)) + (L10n::l("poweredby_pagetop").with_arg("pagetop_link", LINK).using(cx)) } } }) diff --git a/src/html/attr_l10n.rs b/src/html/attr_l10n.rs index cd5b389..3e8a4e4 100644 --- a/src/html/attr_l10n.rs +++ b/src/html/attr_l10n.rs @@ -4,7 +4,7 @@ use crate::{builder_fn, AutoDefault}; /// Texto para [traducir](crate::locale) en atributos HTML. /// -/// Encapsula un tipo [`L10n`] para manejar traducciones de forma segura. +/// Encapsula un [`L10n`] para manejar traducciones de forma segura en atributos. /// /// # Ejemplo /// @@ -16,19 +16,19 @@ use crate::{builder_fn, AutoDefault}; /// /// // Español disponible. /// assert_eq!( -/// hello.using(&LangMatch::resolve("es-ES")), +/// hello.lookup(&LangMatch::resolve("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.lookup(&LangMatch::resolve("ja-JP")), /// Some(String::from("Hello world!")) /// ); /// -/// // Para incrustar en HTML escapado: -/// let markup = hello.to_markup(&LangMatch::resolve("es-ES")); -/// assert_eq!(markup.into_string(), "¡Hola mundo!"); +/// // Uso típico en un atributo: +/// let title = hello.value(&LangMatch::resolve("es-ES")); +/// // Ejemplo: html! { a title=(title) { "Link" } } /// ``` #[derive(AutoDefault, Clone, Debug)] pub struct AttrL10n(L10n); @@ -51,15 +51,18 @@ impl AttrL10n { // AttrL10n GETTERS **************************************************************************** /// Devuelve la traducción para `language`, si existe. - pub fn using(&self, language: &impl LangId) -> Option { - self.0.using(language) + pub fn lookup(&self, language: &impl LangId) -> Option { + self.0.lookup(language) } - /// 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 - /// contenido. + /// Devuelve la traducción para `language` o una cadena vacía si no existe. + pub fn value(&self, language: &impl LangId) -> String { + self.0.lookup(language).unwrap_or_default() + } + + /// **Obsoleto desde la versión 0.4.0**: no recomendado para atributos HTML. + #[deprecated(since = "0.4.0", note = "For attributes use `lookup()` or `value()`")] pub fn to_markup(&self, language: &impl LangId) -> Markup { - self.0.to_markup(language) + self.0.using(language) } } diff --git a/src/html/context.rs b/src/html/context.rs index 9678786..8b7afba 100644 --- a/src/html/context.rs +++ b/src/html/context.rs @@ -306,8 +306,7 @@ impl Context { /// 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). +/// [`L10n::lookup()`](crate::locale::L10n::lookup) o [`L10n::using()`](crate::locale::L10n::using). impl LangId for Context { fn langid(&self) -> &'static LanguageIdentifier { self.langid diff --git a/src/locale.rs b/src/locale.rs index 43612bd..cf44dd8 100644 --- a/src/locale.rs +++ b/src/locale.rs @@ -13,7 +13,7 @@ //! //! # Recursos Fluent //! -//! Por defecto las traducciones están en el directorio `src/locale`, con subdirectorios para cada +//! Por defecto, las traducciones están en el directorio `src/locale`, con subdirectorios para cada //! [Identificador de Idioma Unicode](https://unicode.org/reports/tr35/tr35.html#Unicode_language_identifier) //! válido. Podríamos tener una estructura como esta: //! @@ -34,7 +34,7 @@ //! └── main.ftl //! ``` //! -//! Ejemplo de un archivo en `src/locale/en-US/main.ftl` +//! Ejemplo de un archivo en `src/locale/en-US/main.ftl`: //! //! ```text //! hello-world = Hello world! @@ -53,7 +53,7 @@ //! Y su archivo equivalente para español en `src/locale/es-ES/main.ftl`: //! //! ```text -//! hello-world = Hola mundo! +//! hello-world = ¡Hola, mundo! //! hello-user = ¡Hola, {$userName}! //! shared-photos = //! {$userName} {$photoCount -> @@ -81,7 +81,7 @@ //! include_locales!(LOCALES_SAMPLE); //! ``` //! -//! Si están ubicados en otro directorio se puede usar la forma: +//! Si están ubicados en otro directorio, se puede usar la forma: //! //! ```rust,ignore //! include_locales!(LOCALES_SAMPLE from "ruta/a/las/traducciones"); @@ -129,7 +129,7 @@ pub(crate) static FALLBACK_LANGID: LazyLock = // 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`]. +// 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()); @@ -155,7 +155,7 @@ pub trait LangId { /// let lang = LangMatch::resolve("es-ES"); /// assert_eq!(lang.langid().to_string(), "es-ES"); /// -/// // Coincidencia parcial (con el idioma base). +/// // Coincidencia parcial (retrocede al idioma base si no hay variante regional). /// let lang = LangMatch::resolve("es-EC"); /// assert_eq!(lang.langid().to_string(), "es-ES"); // Porque "es-EC" no está soportado. /// @@ -221,7 +221,7 @@ impl LangMatch { } } - // En otro caso indica que el idioma no está soportado. + // En caso contrario, indica que el idioma no está soportado. Self::Unsupported(String::from(language)) } @@ -241,7 +241,7 @@ impl LangMatch { /// let lang = LangMatch::resolve("es-ES").as_option(); /// assert_eq!(lang.unwrap().to_string(), "es-ES"); /// - /// let lang = LangMatch::resolve("jp-JP").as_option(); + /// let lang = LangMatch::resolve("ja-JP").as_option(); /// assert!(lang.is_none()); /// ``` #[inline] @@ -259,8 +259,8 @@ impl LangMatch { /// 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()`]. +/// Resulta útil para usar un valor de [`LangMatch`] como fuente de traducción en [`L10n::lookup()`] +/// o [`L10n::using()`]. impl LangId for LangMatch { fn langid(&self) -> &'static LanguageIdentifier { match self { @@ -271,10 +271,10 @@ impl LangId for LangMatch { } #[macro_export] -/// Define un conjunto de elementos de localización y textos de traducción local. +/// Incluye un conjunto de recursos **Fluent** y textos de traducción propios. macro_rules! include_locales { - // Se eliminan las marcas de aislamiento Unicode en los argumentos para mejorar la legibilidad y - // la compatibilidad en ciertos contextos de renderizado. + // Se desactiva la inserción de marcas de aislamiento Unicode (FSI/PDI) en los argumentos para + // mejorar la legibilidad y la compatibilidad en ciertos contextos de renderizado. ( $LOCALES:ident $(, $core_locales:literal)? ) => { $crate::locale::fluent_templates::static_loader! { static $LOCALES = { @@ -310,8 +310,8 @@ include_locales!(LOCALES_PAGETOP); enum L10nOp { #[default] None, - Text(String), - Translate(String), + Text(Cow<'static, str>), + Translate(Cow<'static, str>), } /// Crea instancias para traducir textos localizados. @@ -324,7 +324,7 @@ enum L10nOp { /// /// # Ejemplo /// -/// Los argumentos dinámicos se añaden usando `with_arg()` o `with_args()`. +/// Los argumentos dinámicos se añaden con `with_arg()` o `with_args()`. /// /// ```rust /// use pagetop::prelude::*; @@ -338,11 +338,11 @@ enum L10nOp { /// .get(); /// ``` /// -/// También para traducciones a idiomas concretos. +/// También sirve para traducciones contra un conjunto de recursos concreto. /// /// ```rust,ignore /// // Traducción con clave, conjunto de traducciones y fuente de idioma. -/// let bye = L10n::t("goodbye", &LOCALES_CUSTOM).using(&LangMatch::resolve("it")); +/// let bye = L10n::t("goodbye", &LOCALES_CUSTOM).lookup(&LangMatch::resolve("it")); /// ``` #[derive(AutoDefault, Clone)] pub struct L10n { @@ -354,7 +354,7 @@ pub struct L10n { impl L10n { /// **n** = *“native”*. Crea una instancia con una cadena literal sin traducción. - pub fn n(text: impl Into) -> Self { + pub fn n(text: impl Into>) -> Self { L10n { op: L10nOp::Text(text.into()), ..Default::default() @@ -363,7 +363,7 @@ impl L10n { /// **l** = *“lookup”*. Crea una instancia para traducir usando una clave del conjunto de /// traducciones predefinidas. - pub fn l(key: impl Into) -> Self { + pub fn l(key: impl Into>) -> Self { L10n { op: L10nOp::Translate(key.into()), ..Default::default() @@ -372,7 +372,7 @@ impl L10n { /// **t** = *“translate”*. Crea una instancia para traducir usando una clave de un conjunto de /// traducciones específico. - pub fn t(key: impl Into, locales: &'static Locales) -> Self { + pub fn t(key: impl Into>, locales: &'static Locales) -> Self { L10n { op: L10nOp::Translate(key.into()), locales, @@ -399,7 +399,8 @@ impl L10n { self } - /// Resuelve la traducción usando el idioma por defecto o de respaldo de la aplicación. + /// Resuelve la traducción usando el idioma por defecto o, si no procede, el de respaldo de la + /// aplicación. /// /// Devuelve `None` si no aplica o no encuentra una traducción válida. /// @@ -411,7 +412,7 @@ impl L10n { /// let text = L10n::l("greeting").with_arg("name", "Manuel").get(); /// ``` pub fn get(&self) -> Option { - self.using(&LangMatch::default()) + self.lookup(&LangMatch::default()) } /// Resuelve la traducción usando la fuente de idioma proporcionada. @@ -432,20 +433,27 @@ impl L10n { /// } /// /// let r = ResourceLang; - /// let text = L10n::l("greeting").with_arg("name", "Usuario").using(&r); + /// let text = L10n::l("greeting").with_arg("name", "Usuario").lookup(&r); /// ``` - pub fn using(&self, language: &impl LangId) -> Option { + pub fn lookup(&self, language: &impl LangId) -> 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(), - key, - &self.args.iter().fold(HashMap::new(), |mut arg, (k, v)| { - arg.insert(Cow::Owned(k.clone()), v.to_owned().into()); - arg - }), - ), + L10nOp::Text(text) => Some(text.clone().into_owned()), + L10nOp::Translate(key) => { + if self.args.is_empty() { + self.locales.try_lookup(language.langid(), key.as_ref()) + } else { + self.locales.try_lookup_with_args( + language.langid(), + key.as_ref(), + &self + .args + .iter() + .map(|(k, v)| (Cow::Owned(k.clone()), v.clone().into())) + .collect::>(), + ) + } + } } } @@ -458,10 +466,16 @@ impl L10n { /// ```rust /// use pagetop::prelude::*; /// - /// let html = L10n::l("welcome.message").to_markup(&LangMatch::resolve("es")); + /// let html = L10n::l("welcome.message").using(&LangMatch::resolve("es")); /// ``` + pub fn using(&self, language: &impl LangId) -> Markup { + PreEscaped(self.lookup(language).unwrap_or_default()) + } + + /// **Obsoleto desde la versión 0.4.0**: usar [`using()`](Self::using) en su lugar. + #[deprecated(since = "0.4.0", note = "Use `using()` instead")] pub fn to_markup(&self, language: &impl LangId) -> Markup { - PreEscaped(self.using(language).unwrap_or_default()) + self.using(language) } } diff --git a/src/response/page.rs b/src/response/page.rs index ea88e84..7ef5270 100644 --- a/src/response/page.rs +++ b/src/response/page.rs @@ -151,12 +151,12 @@ impl Page { /// Devuelve el título traducido para el idioma de la página, si existe. pub fn title(&mut self) -> Option { - self.title.using(&self.context) + self.title.lookup(&self.context) } /// Devuelve la descripción traducida para el idioma de la página, si existe. pub fn description(&mut self) -> Option { - self.description.using(&self.context) + self.description.lookup(&self.context) } /// Devuelve la lista de metadatos ``. diff --git a/tests/locale.rs b/tests/locale.rs index ef875d7..ee7ac27 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.lookup(&LangMatch::resolve("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.lookup(&LangMatch::resolve("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.lookup(&LangMatch::resolve("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.lookup(&LangMatch::resolve("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.lookup(&LangMatch::resolve("en-US")); assert_eq!(translation, None); }