From 3df73f055192b96bb835d7fab3d6cc286dbca19e Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Tue, 29 Sep 2026 07:08:48 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20(locale):=20A=C3=B1ade=20Lc::with?= =?UTF-8?q?=5Fnumber()=20para=20plurales?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `with_arg()` sólo interpola texto, y el selector de plural de Fluent (`{ $n -> [one] ... *[other] ... }`) sólo aplica las reglas de plural cuando el argumento es un FluentValue::Number. --- src/locale/lc.rs | 65 ++++++++++++++++++++++++++++++++++++++++++------ tests/locale.rs | 17 ++++++++++++- 2 files changed, 74 insertions(+), 8 deletions(-) diff --git a/src/locale/lc.rs b/src/locale/lc.rs index c54ed4fb..551a8f4f 100644 --- a/src/locale/lc.rs +++ b/src/locale/lc.rs @@ -12,6 +12,8 @@ use std::collections::HashMap; use std::fmt; +// **< LcKind >************************************************************************************* + // Tipo de localización a aplicar. // // * `None` - No se aplica ninguna localización. @@ -25,6 +27,31 @@ enum LcKind { Translate(CowStr), } +// **< LcArg >************************************************************************************** + +// Valor de un argumento de la traducción. +// +// `Text` resuelve a `FluentValue::String` para interpolar cualquier texto; pero no aplica el +// selector de plural de Fluent (`{ $n -> [one] ... *[other] ... }`). Para eso está `Number`, que +// resuelve a `FluentValue::Number` y es el único tipo con el que Fluent consulta las reglas de +// plural del idioma. +#[derive(Clone, Debug)] +enum LcArg { + Text(CowStr), + Number(u32), +} + +impl<'a> From<&'a LcArg> for fluent_templates::fluent_bundle::FluentValue<'a> { + fn from(arg: &'a LcArg) -> Self { + match arg { + LcArg::Text(s) => s.as_ref().into(), + LcArg::Number(n) => (*n).into(), + } + } +} + +// **< Lc >***************************************************************************************** + /// Crea instancias para traducir *textos localizados*. /// /// Cada instancia puede representar: @@ -81,7 +108,7 @@ pub struct Lc { op: LcKind, #[default(&LOCALES_PAGETOP)] locales: &'static Locales, - args: Vec<(CowStr, CowStr)>, + args: Vec<(CowStr, LcArg)>, } impl fmt::Debug for Lc { @@ -141,22 +168,46 @@ impl Lc { // **< Lc BUILDER >***************************************************************************** /// Añade un argumento `{$arg}` => `value` a la traducción. + /// + /// `value` se interpreta siempre como texto. Si se necesita un argumento numérico que además + /// deba seleccionar plural (`{ $n -> [one] ... *[other] ... }`), hay que utilizar en su lugar + /// [`with_number()`](Self::with_number). pub fn with_arg(mut self, arg: impl Into, value: impl Into) -> Self { - self.args.push((arg.into(), value.into())); + self.args.push((arg.into(), LcArg::Text(value.into()))); self } - /// Añade varios argumentos a la traducción de una vez (p. ej. usando la macro + /// Añade varios argumentos de texto a la traducción de una vez (p. ej. usando la macro /// [`util::kv!`](crate::util::kv) o también `vec![("k", "v")]`, incluso un array de duplas u - /// otras colecciones). + /// otras colecciones). Aplica sólo a texto, como [`with_arg()`](Self::with_arg). pub fn with_args(mut self, args: I) -> Self where I: IntoIterator, K: Into, V: Into, { - self.args - .extend(args.into_iter().map(|(k, v)| (k.into(), v.into()))); + self.args.extend( + args.into_iter() + .map(|(k, v)| (k.into(), LcArg::Text(v.into()))), + ); + self + } + + /// Añade un argumento numérico `{$arg}` => `n` a la traducción. + /// + /// A diferencia de [`with_arg()`](Self::with_arg), `n` se interpola como un número real, el + /// único tipo en el que Fluent aplica las reglas de plural del idioma para que un selector + /// `{ $n -> [one] ... *[other] ... }` elija correctamente entre singular y plural. + /// + /// # Ejemplo + /// + /// ```rust,no_run + /// # use pagetop::prelude::*; + /// // "1 año" / "3 años", según el idioma y el valor de `n`. + /// let text = Lc::l("years_count").with_number("n", 3).get(); + /// ``` + pub fn with_number(mut self, arg: impl Into, n: u32) -> Self { + self.args.push((arg.into(), LcArg::Number(n))); self } @@ -211,7 +262,7 @@ impl Lc { } else { let mut args = HashMap::with_capacity(self.args.len()); for (k, v) in self.args.iter() { - args.insert(k.clone(), v.as_ref().into()); + args.insert(k.clone(), v.into()); } self.locales .try_lookup_with_args(language.langid(), key.as_ref(), &args) diff --git a/tests/locale.rs b/tests/locale.rs index 11f08def..ca55fd92 100644 --- a/tests/locale.rs +++ b/tests/locale.rs @@ -43,12 +43,27 @@ async fn translation_with_plural_and_select() { assert!(translation.contains("añadido 3 nuevas fotos de él")); } +// `with_arg()` only interpolates text: `photoCount` as a string ("3") can never select the `[one]` +// plural branch, whatever its value. Only `with_number()` produces a real numeric argument that +// Fluent's plural rules can match against. +#[pagetop::test] +async fn translation_with_number_selects_the_singular_plural_branch() { + setup().await; + + let lc = Lc::l("test_shared_photos") + .with_arg("userName", "Roberto") + .with_number("photoCount", 1) + .with_arg("userGender", "male"); + let translation = lc.lookup(&Locale::resolve("es-ES")).unwrap(); + assert!(translation.contains("ha añadido una nueva foto de él")); +} + #[pagetop::test] async fn check_fallback_language() { setup().await; let lc = Lc::l("test_hello_world"); - let translation = lc.lookup(&Locale::resolve("xx-YY")); // Retrocede a "en-US". + let translation = lc.lookup(&Locale::resolve("xx-YY")); // Falls back to "en-US". assert_eq!(translation, Some("Hello world!".to_string())); }