🧑‍💻 Mejora uso y doc. de la API de localización

This commit is contained in:
Manuel Cillero 2025-07-19 00:13:49 +02:00
parent 4eb9a87344
commit 39b94ec584
2 changed files with 98 additions and 39 deletions

View file

@ -13,9 +13,9 @@
//! //!
//! # Recursos Fluent //! # Recursos Fluent
//! //!
//! Por defecto, las traducciones se organizan en el directorio *src/locale*, con subdirectorios //! Por defecto las traducciones están en el directorio `src/locale`, con subdirectorios para cada
//! para cada [Identificador de Idioma Unicode](https://docs.rs/unic-langid/) válido. Podríamos //! [Identificador de Idioma Unicode](https://unicode.org/reports/tr35/tr35.html#Unicode_language_identifier)
//! tener una estructura como esta: //! válido. Podríamos tener una estructura como esta:
//! //!
//! ```text //! ```text
//! src/locale/ //! src/locale/
@ -29,12 +29,12 @@
//! ├── es-MX/ //! ├── es-MX/
//! │ ├── default.ftl //! │ ├── default.ftl
//! │ └── main.ftl //! │ └── main.ftl
//! └── fr/ //! └── fr-FR/
//! ├── default.ftl //! ├── default.ftl
//! └── main.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 //! ```text
//! hello-world = Hello world! //! 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 //! ```text
//! hello-world = Hola mundo! //! hello-world = Hola mundo!
@ -69,10 +69,11 @@
//! //!
//! # Cómo aplicar la localización en tu código //! # 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. //! [`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 //! ```rust
//! use pagetop::prelude::*; //! use pagetop::prelude::*;
@ -80,11 +81,14 @@
//! include_locales!(LOCALES_SAMPLE); //! 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 //! ```rust,ignore
//! include_locales!(LOCALES_SAMPLE from "ruta/a/las/traducciones"); //! 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::html::{Markup, PreEscaped};
use crate::{global, hm, AutoDefault}; use crate::{global, hm, AutoDefault};
@ -128,28 +132,69 @@ static FALLBACK_LANGID: LazyLock<LanguageIdentifier> = LazyLock::new(|| langid!(
pub(crate) static DEFAULT_LANGID: LazyLock<&LanguageIdentifier> = pub(crate) static DEFAULT_LANGID: LazyLock<&LanguageIdentifier> =
LazyLock::new(|| LangMatch::langid_or_fallback(&global::SETTINGS.app.language)); 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)] #[derive(Clone, Debug, Eq, PartialEq)]
pub enum LangMatch { pub enum LangMatch {
/// Cuando el código del idioma es una cadena vacía. /// Cuando el código del idioma es una cadena vacía.
Empty, Unspecified,
/// Si encuentra un [`LanguageIdentifier`] que coincide exactamente o retrocediendo al idioma /// Si encuentra un [`LanguageIdentifier`] en la lista de idiomas soportados por `PageTop` que
/// base. /// 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), Found(&'static LanguageIdentifier),
/// Si el código del idioma no está entre los soportados por `PageTop`. /// Si el código del idioma no está entre los soportados por `PageTop`.
Unknown(String), Unsupported(String),
} }
impl LangMatch { impl LangMatch {
/// Resuelve `language` y devuelve el [`LangMatch`] apropiado. /// Resuelve `language` y devuelve la variante [`LangMatch`] apropiada.
pub fn resolve(language: impl AsRef<str>) -> Self { pub fn resolve(language: impl AsRef<str>) -> Self {
let language = language.as_ref().trim(); let language = language.as_ref().trim();
// Rechaza cadenas vacías. // Rechaza cadenas vacías.
if language.is_empty() { if language.is_empty() {
return Self::Empty; return Self::Unspecified;
} }
// Intenta aplicar coincidencia exacta con el código completo (p.ej. "es-MX"). // 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. // En otro caso indica que el idioma no está soportado.
Self::Unknown(language.to_string()) 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"). /// Siempre devuelve un [`LanguageIdentifier`] válido.
#[inline]
pub fn langid_or_fallback(language: impl AsRef<str>) -> &'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").
#[inline] #[inline]
pub fn as_langid(&self) -> &'static LanguageIdentifier { pub fn as_langid(&self) -> &'static LanguageIdentifier {
match self { match self {
LangMatch::Found(l) => l, 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<str>) -> &'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<str>) -> &'static LanguageIdentifier {
match Self::resolve(language) {
Self::Found(l) => l,
_ => &FALLBACK_LANGID, _ => &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 un texto de las traducciones por defecto de `PageTop` (`l()`).
/// - Una clave para traducir de un conjunto concreto de traducciones (`t()`). /// - 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 /// ```rust
/// use pagetop::prelude::*; /// use pagetop::prelude::*;
@ -261,7 +320,7 @@ enum L10nOp {
/// ///
/// ```rust,ignore /// ```rust,ignore
/// // Traducción con clave, conjunto de traducciones y código de idioma a usar. /// // 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)] #[derive(AutoDefault)]
pub struct L10n { pub struct L10n {

View file

@ -13,7 +13,7 @@ async fn translation_without_args() {
let _app = service::test::init_service(Application::new().test()).await; let _app = service::test::init_service(Application::new().test()).await;
let l10n = L10n::l("test-hello-world"); 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())); 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 _app = service::test::init_service(Application::new().test()).await;
let l10n = L10n::l("test-hello-user").with_arg("userName", "Manuel"); 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())); assert_eq!(translation, Some("¡Hola, Manuel!".to_string()));
} }
@ -35,7 +35,7 @@ async fn translation_with_plural_and_select() {
("photoCount", "3"), ("photoCount", "3"),
("userGender", "male"), ("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")); 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 _app = service::test::init_service(Application::new().test()).await;
let l10n = L10n::l("test-hello-world"); 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())); 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 _app = service::test::init_service(Application::new().test()).await;
let l10n = L10n::l("non-existent-key"); 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); assert_eq!(translation, None);
} }