✨ (locale): Añade Lc::with_number() para plurales

`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.
This commit is contained in:
Manuel Cillero 2026-09-29 07:08:48 +02:00
parent 5d199ef10d
commit 3df73f0551
2 changed files with 74 additions and 8 deletions

View file

@ -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<CowStr>, value: impl Into<CowStr>) -> 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<I, K, V>(mut self, args: I) -> Self
where
I: IntoIterator<Item = (K, V)>,
K: Into<CowStr>,
V: Into<CowStr>,
{
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<CowStr>, 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)

View file

@ -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()));
}