Skip to content

Commit

Permalink
move from a map to a sequence for plurals, map will be used for subkeys
Browse files Browse the repository at this point in the history
  • Loading branch information
Baptistemontan committed Aug 31, 2023
1 parent 7d727e5 commit f30ff34
Show file tree
Hide file tree
Showing 9 changed files with 489 additions and 236 deletions.
108 changes: 58 additions & 50 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,71 +284,66 @@ You may need to display different messages depending on a count, for exemple one

```json
{
"click_count": {
"0": "You have not clicked yet",
"1": "You clicked once",
"_": "You clicked {{ count }} times"
}
"click_count": [
{
"count": "0",
"value": "You have not clicked yet"
},
{
"count": "1",
"value": "You clicked once"
},
{
"count": "_",
"value": "You clicked {{ count }} times"
}
]
}
```

When using plurals, variable name `count` is reserved and takes as a value `T: Fn() -> Into<N> + Clone + 'static` where `N` is the specified type.
By default `N` is `i64` but you can change that with the key `type`:
By default `N` is `i64` but you can change that by specifying the type as the **first** value in the sequence:

```json
{
"money_in_da_bank": {
"type": "f32",
"0.0": "You are broke",
"..0.0": "You owe money",
"_": "You have {{ count }}€"
}
"money_count": [
"f32",
{
"count": "0.0",
"value": "You are broke"
},
{
"count": "..0.0",
"value": "You owe money"
},
{
"count": "_",
"value": "You have {{ count }}€"
}
]
}
```

The supported types are `i8`, `i16`, `i32`, `i64`, `u8`, `u16`, `u32`, `u64`, `f32` and `f64` and he `type` key must be the first.
The supported types are `i8`, `i16`, `i32`, `i64`, `u8`, `u16`, `u32`, `u64`, `f32` and `f64`.

You can also supply a range:

```json
{
"click_count": {
"0": "You have not clicked yet",
"1": "You clicked once",
"2..=10": "You clicked {{ count }} times",
"11..": "You clicked <b>a lot</b>"
}
}
```
As seen above you can supply a range: `s..e`, `..e`, `s..`, `s..=e`, `..=e` or even `..` ( `..` will considered fallback `_`)

The resulting code looks something like this:

```rust
match N::from(count()) {
0 => // render "You have not clicked yet",
1 => // render "You clicked once",
2..=20 => // render "You clicked beetween 2 and 20 times"
_ => // render "You clicked {{ count }} times"
}
```

But this exemple will not compile, because the resulting match statement will not cover the full `i64` range (even if your count is not a `i64`, it is till converted to one and need to match the full range), so you will either need to introduce a fallback, or the missing range: `"..0": "You clicked a negative amount ??"`, or set `type` to a unsigned like `u64`.
Because it expand to a match statement, a compilation error will be produced if the full range of `N` is not covered.

Because floats (`f32` and `f64`) are not accepted in match statements so it can't be known if the full range is covered, therefore floats must have a fallback (`"_"`) at the end.
But floats (`f32` and `f64`) are not accepted in match statements it expand to a `if-else` chain, therefore must and by a `else` block, so a fallback `_` or `..` is required.

Those plurals:

```json
{
"money_in_da_bank": {
"type": "f32",
"0.0": "You are broke",
"..0.0": "You owe money",
"_": "You have {{ count }}€"
}
}
```

Would generate code similar to this:
The plural above would generate code similar to this:

```rust
let plural_count = f32::from(count());
Expand All @@ -365,24 +360,37 @@ If one locale use plurals for a key, another locale does not need to use it, but

You are not required to use the `count` variable in the locale, but it must be provided.

If multiple locales use plurals for the same key, the count `type` must be the same.
If multiple locales use plurals for the same key, the count type must be the same.

(PS: Floats are generaly not a good idea for money.)

You can also have multiple conditions:
You can also have multiple conditions by either separate them by `|` or put them in a sequence:

```json
{
"click_count": {
"type": "u32",
"0 | 5": "You clicked 0 or 5 times",
"1": "You clicked once",
"2..=10 | 20": "You clicked {{ count }} times",
"11..": "You clicked <b>a lot</b>"
}
"click_count": [
"u32",
{
"count": "0 | 5",
"value": "You clicked 0 or 5 times"
},
{
"count": "1",
"value": "You clicked once"
},
{
"count": ["2..=10", "20"],
"value": "You clicked {{ count }} times"
},
{
"value": "You clicked <b>a lot</b>"
}
]
}
```

Fallback can omit the `count` key.

### Namespaces

Being constrained to put every translation in one unique file can make the locale file overly big, and keys must be unique making things even more complex. To avoid this situation you can introduce namespaces in the config file (i18n.json):
Expand Down
29 changes: 22 additions & 7 deletions examples/counter_plurals/locales/en.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
{
"click_to_change_lang": "Click to change language",
"click_count": {
"0": "You have not clicked yet",
"1": "You clicked once",
"2..20": "You clicked {{ count }} times",
"20..": "You clicked a lot",
"..0": "You clicked a negative amount ??"
},
"click_count": [
{
"count": "0",
"value": "You have not clicked yet"
},
{
"count": "1",
"value": "You clicked once"
},
{
"count": "2..20",
"value": "You clicked {{ count }} times"
},
{
"count": "20..",
"value": "You clicked a lot"
},
{
"count": "..0",
"value": "You clicked a negative amount ??"
}
],
"click_to_inc": "Click to increment the counter"
}
17 changes: 11 additions & 6 deletions leptos_i18n_macro/src/load_locales/locale.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,18 +241,23 @@ impl<'a: 'de, 'de> serde::de::Visitor<'de> for LocaleSeed<'a> {
A: serde::de::MapAccess<'de>,
{
let mut keys = HashMap::new();
let locale = self.locale_name.name.as_str();
let locale_name = self.locale_name.name.as_str();
let namespace = self.namespace;

while let Some(key) = map.next_key_seed(KeySeed::LocaleKey { locale, namespace })? {
while let Some(locale_key) = map.next_key_seed(KeySeed::LocaleKey {
locale: locale_name,
namespace,
})? {
let parsed_value_seed = ParsedValueSeed {
in_plural: false,
locale,
locale_key: &key.name,
namespace,
base: super::SeedBase {
locale_name,
locale_key: &locale_key.name,
namespace,
},
};
let value = map.next_value_seed(parsed_value_seed)?;
keys.insert(key, value);
keys.insert(locale_key, value);
}

Ok(keys)
Expand Down
7 changes: 7 additions & 0 deletions leptos_i18n_macro/src/load_locales/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ use quote::{format_ident, quote};

use self::locale::{BuildersKeys, BuildersKeysInner, LocalesOrNamespaces, Namespace};

#[derive(Debug, Clone, Copy)]
pub struct SeedBase<'a> {
pub locale_name: &'a str,
pub locale_key: &'a str,
pub namespace: Option<&'a str>,
}

pub fn load_locales(cfg_file_path: Option<impl AsRef<Path>>) -> Result<TokenStream> {
let cfg_file = ConfigFile::new(cfg_file_path)?;

Expand Down
40 changes: 21 additions & 19 deletions leptos_i18n_macro/src/load_locales/parsed_value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use quote::{format_ident, quote, ToTokens};
use super::{
key::Key,
plural::{PluralType, Plurals},
SeedBase,
};

#[derive(Debug, Clone, PartialEq)]
Expand Down Expand Up @@ -235,9 +236,7 @@ impl ToTokens for ParsedValue {
#[derive(Debug, Clone, Copy)]
pub struct ParsedValueSeed<'a> {
pub in_plural: bool,
pub locale: &'a str,
pub locale_key: &'a str,
pub namespace: Option<&'a str>,
pub base: SeedBase<'a>,
}

impl<'de> serde::de::DeserializeSeed<'de> for ParsedValueSeed<'_> {
Expand All @@ -261,63 +260,63 @@ impl<'de> serde::de::Visitor<'de> for ParsedValueSeed<'_> {
Ok(ParsedValue::new(v))
}

fn visit_map<A>(mut self, map: A) -> std::result::Result<Self::Value, A::Error>
fn visit_seq<A>(mut self, map: A) -> std::result::Result<Self::Value, A::Error>
where
A: serde::de::MapAccess<'de>,
A: serde::de::SeqAccess<'de>,
{
// nested plurals are not allowed, the code technically supports it,
// but it's pointless and probably nobody will ever needs it.
if std::mem::replace(&mut self.in_plural, true) {
let msg = match self.namespace {
let msg = match self.base.namespace {
Some(namespace) => format!(
"in locale {:?} at namespace {:?} at key {:?}: nested plurals are not allowed",
self.locale, namespace, self.locale_key
self.base.locale_name, namespace, self.base.locale_key
),
None => format!(
"in locale {:?} at key {:?}: nested plurals are not allowed",
self.locale, self.locale_key
self.base.locale_name, self.base.locale_key
),
};
return Err(serde::de::Error::custom(msg));
}
let plurals = Plurals::from_serde_map(map, self)?;
let plurals = Plurals::from_serde_seq(map, self)?;

let (invalid_fallback, fallback_count, should_have_fallback) =
plurals.check_deserialization();

if invalid_fallback {
let msg = match self.namespace {
let msg = match self.base.namespace {
Some(namespace) => format!(
"in locale {:?} at namespace {:?} at key {:?}: fallback is only allowed in last position",
self.locale, namespace, self.locale_key
self.base.locale_name, namespace, self.base.locale_key
),
None => format!(
"in locale {:?} at key {:?}: fallback is only allowed in last position",
self.locale, self.locale_key
self.base.locale_name, self.base.locale_key
),
};
Err(serde::de::Error::custom(msg))
} else if fallback_count > 1 {
let msg = match self.namespace {
let msg = match self.base.namespace {
Some(namespace) => format!(
"in locale {:?} at namespace {:?} at key {:?}: multiple fallbacks are not allowed",
self.locale, namespace, self.locale_key
self.base.locale_name, namespace, self.base.locale_key
),
None => format!(
"in locale {:?} at key {:?}: multiple fallbacks are not allowed",
self.locale, self.locale_key
self.base.locale_name, self.base.locale_key
),
};
Err(serde::de::Error::custom(msg))
} else if fallback_count == 0 && should_have_fallback {
let msg = match self.namespace {
let msg = match self.base.namespace {
Some(namespace) => format!(
"in locale {:?} at namespace {:?} at key {:?}: for plural type {:?} a fallback is required",
self.locale, namespace, self.locale_key, plurals.get_type()
self.base.locale_name, namespace, self.base.locale_key, plurals.get_type()
),
None => format!(
"in locale {:?} at key {:?}: for plural type {:?} a fallback is required",
self.locale, self.locale_key, plurals.get_type()
self.base.locale_name, self.base.locale_key, plurals.get_type()
),
};
Err(serde::de::Error::custom(msg))
Expand All @@ -327,7 +326,10 @@ impl<'de> serde::de::Visitor<'de> for ParsedValueSeed<'_> {
}

fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(formatter, "either a string or a map of string:string")
write!(
formatter,
"either a string, a sequence of plurals or a map of subkeys"
)
}
}

Expand Down
Loading

0 comments on commit f30ff34

Please sign in to comment.