From 39b94ec5841ce2ce1fb1e8d988213121864229bc Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Sat, 19 Jul 2025 00:13:49 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=91=E2=80=8D=F0=9F=92=BB=20Mejora=20us?= =?UTF-8?q?o=20y=20doc.=20de=20la=20API=20de=20localizaci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/locale.rs | 127 +++++++++++++++++++++++++++++++++++------------- tests/locale.rs | 10 ++-- 2 files changed, 98 insertions(+), 39 deletions(-) diff --git a/src/locale.rs b/src/locale.rs index 064481f..ab1f5ef 100644 --- a/src/locale.rs +++ b/src/locale.rs @@ -13,9 +13,9 @@ //! //! # Recursos Fluent //! -//! Por defecto, las traducciones se organizan en el directorio *src/locale*, con subdirectorios -//! para cada [Identificador de Idioma Unicode](https://docs.rs/unic-langid/) válido. Podríamos -//! tener una estructura como esta: +//! 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: //! //! ```text //! src/locale/ @@ -29,12 +29,12 @@ //! ├── es-MX/ //! │ ├── default.ftl //! │ └── main.ftl -//! └── fr/ +//! └── fr-FR/ //! ├── default.ftl //! └── main.ftl //! ``` //! -//! Ejemplo de un archivo *src/locale/en-US/main.ftl*: +//! Ejemplo de un archivo en `src/locale/en-US/main.ftl` //! //! ```text //! hello-world = Hello world! @@ -50,7 +50,7 @@ //! }. //! ``` //! -//! Y su archivo equivalente para español *src/locale/es-ES/main.ftl*: +//! Y su archivo equivalente para español en `src/locale/es-ES/main.ftl`: //! //! ```text //! hello-world = Hola mundo! @@ -69,10 +69,11 @@ //! //! # Cómo aplicar la localización en tu código //! -//! Una vez creado el directorio con los recursos FTL, basta con utilizar la macro +//! Una vez creado el directorio con los recursos FTL, basta con usar la macro //! [`include_locales!`](crate::include_locales) para integrarlos en la aplicación. //! -//! Si los recursos se encuentran en el directorio `"src/locale"`, sólo hay que declarar: +//! Si los recursos se encuentran en el directorio por defecto `src/locale` del *crate*, sólo hay +//! que declarar: //! //! ```rust //! use pagetop::prelude::*; @@ -80,11 +81,14 @@ //! include_locales!(LOCALES_SAMPLE); //! ``` //! -//! Pero si están ubicados en otro directorio, entonces se pueden incluir usando: +//! Si están ubicados en otro directorio se puede usar la forma: //! //! ```rust,ignore //! include_locales!(LOCALES_SAMPLE from "ruta/a/las/traducciones"); //! ``` +//! +//! Y *voilà*, sólo queda operar con los idiomas soportados por `PageTop` usando [`LangMatch`] y +//! traducir textos con [`L10n`]. use crate::html::{Markup, PreEscaped}; use crate::{global, hm, AutoDefault}; @@ -128,28 +132,69 @@ static FALLBACK_LANGID: LazyLock = LazyLock::new(|| langid!( pub(crate) static DEFAULT_LANGID: LazyLock<&LanguageIdentifier> = LazyLock::new(|| LangMatch::langid_or_fallback(&global::SETTINGS.app.language)); -/// Comprueba si el idioma está soportado por `PageTop`. +/// Operaciones con los idiomas soportados por `PageTop`. /// -/// Útil para transformar un código de idioma en un [`LanguageIdentifier`] válido para `PageTop`. +/// Utiliza [`LangMatch`] para transformar un código de idioma en un [`LanguageIdentifier`] +/// soportado por `PageTop`. +/// +/// # Ejemplos +/// +/// ```rust +/// use pagetop::prelude::*; +/// +/// // Coincidencia exacta. +/// let lang = LangMatch::resolve("es-ES"); +/// assert_eq!(lang.as_langid().to_string(), "es-ES"); +/// +/// // Coincidencia parcial (con el idioma base). +/// let lang = LangMatch::resolve("es-EC"); +/// assert_eq!(lang.as_langid().to_string(), "es-ES"); // Porque "es-EC" no está soportado. +/// +/// // Idioma no especificado. +/// let lang = LangMatch::resolve(""); +/// assert_eq!(lang, LangMatch::Unspecified); +/// +/// // Idioma no soportado. +/// let lang = LangMatch::resolve("ja-JP"); +/// assert_eq!(lang, LangMatch::Unsupported("ja-JP".to_string())); +/// ``` +/// +/// Las siguientes instrucciones devuelven siempre un [`LanguageIdentifier`] válido, ya sea porque +/// resuelven un idioma soportado o porque aplican el idioma por defecto o de respaldo: +/// +/// ```rust +/// use pagetop::prelude::*; +/// +/// // Idioma por defecto si no resuelve. +/// let lang = LangMatch::resolve("it-IT"); +/// let langid = lang.as_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)] pub enum LangMatch { /// Cuando el código del idioma es una cadena vacía. - Empty, - /// Si encuentra un [`LanguageIdentifier`] que coincide exactamente o retrocediendo al idioma - /// base. + Unspecified, + /// Si encuentra un [`LanguageIdentifier`] en la lista de idiomas soportados por `PageTop` que + /// coincide exactamente con el código del idioma (p.ej. "es-ES"), o con el código del idioma + /// base (p.ej. "es"). Found(&'static LanguageIdentifier), /// Si el código del idioma no está entre los soportados por `PageTop`. - Unknown(String), + Unsupported(String), } impl LangMatch { - /// Resuelve `language` y devuelve el [`LangMatch`] apropiado. + /// Resuelve `language` y devuelve la variante [`LangMatch`] apropiada. pub fn resolve(language: impl AsRef) -> Self { let language = language.as_ref().trim(); // Rechaza cadenas vacías. if language.is_empty() { - return Self::Empty; + return Self::Unspecified; } // Intenta aplicar coincidencia exacta con el código completo (p.ej. "es-MX"). @@ -164,28 +209,40 @@ impl LangMatch { } } - // Devuelve desconocido si el idioma no está soportado. - Self::Unknown(language.to_string()) + // En otro caso indica que el idioma no está soportado. + Self::Unsupported(language.to_string()) } - /// Devuelve siempre un [`LanguageIdentifier`] válido. + /// Devuelve el idioma de la variante de la instancia, o el idioma por defecto si no está + /// soportado. /// - /// Si `language` está vacío o es desconocido, devuelve el idioma de respaldo ("en-US"). - #[inline] - pub fn langid_or_fallback(language: impl AsRef) -> &'static LanguageIdentifier { - match Self::resolve(language) { - Self::Found(l) => l, - _ => &FALLBACK_LANGID, - } - } - - /// Devuelve un [`LanguageIdentifier`] válido para la instancia. - /// - /// Si `language` está vacío o es desconocido, devuelve el idioma de respaldo ("en-US"). + /// Siempre devuelve un [`LanguageIdentifier`] válido. #[inline] pub fn as_langid(&self) -> &'static LanguageIdentifier { match self { LangMatch::Found(l) => l, + _ => &DEFAULT_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) -> &'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) -> &'static LanguageIdentifier { + match Self::resolve(language) { + Self::Found(l) => l, _ => &FALLBACK_LANGID, } } @@ -243,7 +300,9 @@ enum L10nOp { /// - Una clave para traducir un texto de las traducciones por defecto de `PageTop` (`l()`). /// - Una clave para traducir de un conjunto concreto de traducciones (`t()`). /// -/// Los argumentos dinámicos se añaden mediante `with_arg()` o `with_args()`. +/// # Ejemplo +/// +/// Los argumentos dinámicos se añaden usando `with_arg()` o `with_args()`. /// /// ```rust /// use pagetop::prelude::*; @@ -261,7 +320,7 @@ enum L10nOp { /// /// ```rust,ignore /// // Traducción con clave, conjunto de traducciones y código de idioma a usar. -/// let bye = L10n::t("goodbye", &LOCALES_CUSTOM).using(LangMatch::langid_or_fallback("it")); +/// let bye = L10n::t("goodbye", &LOCALES_CUSTOM).using(LangMatch::langid_or_default("it")); /// ``` #[derive(AutoDefault)] pub struct L10n { diff --git a/tests/locale.rs b/tests/locale.rs index c723cd4..420914d 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::langid_or_fallback("es-ES")); + let translation = l10n.using(LangMatch::langid_or_default("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::langid_or_fallback("es-ES")); + let translation = l10n.using(LangMatch::langid_or_default("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::langid_or_fallback("es-ES")).unwrap(); + let translation = l10n.using(LangMatch::langid_or_default("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::langid_or_fallback("xx-YY")); // Fallback a "en-US". + let translation = l10n.using(LangMatch::langid_or_fallback("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::langid_or_fallback("en-US")); + let translation = l10n.using(LangMatch::langid_or_default("en-US")); assert_eq!(translation, None); }