Skip to content

Commit

Permalink
Merge pull request #2 from BaptisteGelez/plurals
Browse files Browse the repository at this point in the history
Add support for plurals
  • Loading branch information
justinas authored Nov 1, 2018
2 parents e37bc31 + a229c16 commit 3367b8c
Show file tree
Hide file tree
Showing 10 changed files with 484 additions and 148 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
- [x] Parsing MO files (10.3)
- [x] Parsing metadata (6.2)
- [x] Supporting encodings other than UTF-8
- [ ] Parsing the plural expression? (11.2.6)
- [x] Parsing the plural expression (11.2.6)
- [ ] Correct pathfinding? (11.2.3)
1 change: 0 additions & 1 deletion rustfmt.toml

This file was deleted.

121 changes: 60 additions & 61 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,15 @@
//! ```
// https://pascalhertleif.de/artikel/good-practices-for-writing-rust-libraries/
#![deny(missing_docs, missing_debug_implementations,
trivial_casts, trivial_numeric_casts, unused_import_braces)]

#![cfg_attr(feature="clippy", feature(plugin))]
#![cfg_attr(feature="clippy", plugin(clippy))]
#![deny(
missing_docs,
missing_debug_implementations,
trivial_casts,
trivial_numeric_casts,
unused_import_braces
)]
#![cfg_attr(feature = "clippy", feature(plugin))]
#![cfg_attr(feature = "clippy", plugin(clippy))]

mod metadata;
mod parser;
Expand All @@ -51,8 +55,8 @@ use std::collections::HashMap;
use std::io::Read;
use std::ops::Deref;

pub use parser::{Error, ParseOptions};
use plurals::Resolver::{self, Function};
pub use parser::{default_resolver, Error, ParseOptions};
use plurals::*;

fn key_with_context(context: &str, key: &str) -> String {
let mut result = context.to_owned();
Expand All @@ -63,7 +67,7 @@ fn key_with_context(context: &str, key: &str) -> String {

/// Catalog represents a set of translation strings
/// parsed out of one MO file.
#[derive(Debug)]
#[derive(Clone, Debug)]
pub struct Catalog {
strings: HashMap<String, Message>,
resolver: Resolver,
Expand All @@ -74,13 +78,7 @@ impl Catalog {
fn new() -> Self {
Catalog {
strings: HashMap::new(),
resolver: Function(Box::new(|n| {
if n != 1 {
1
} else {
0
}
})),
resolver: Resolver::Function(default_resolver),
}
}

Expand All @@ -100,7 +98,6 @@ impl Catalog {
/// let file = File::open("french.mo").unwrap();
/// let catalog = Catalog::parse(file).unwrap();
/// ```
pub fn parse<R: Read>(reader: R) -> Result<Self, parser::Error> {
ParseOptions::new().parse(reader)
}
Expand All @@ -116,7 +113,10 @@ impl Catalog {
/// Returns the singular translation of `msg_id` from the given catalog
/// or `msg_id` itself if a translation does not exist.
pub fn gettext<'a>(&'a self, msg_id: &'a str) -> &'a str {
self.strings.get(msg_id).and_then(|msg| msg.get_translated(0)).unwrap_or(msg_id)
self.strings
.get(msg_id)
.and_then(|msg| msg.get_translated(0))
.unwrap_or(msg_id)
}

/// Returns the plural translation of `msg_id` from the given catalog
Expand All @@ -129,9 +129,9 @@ impl Catalog {
let form_no = self.resolver.resolve(n);

match self.strings.get(msg_id) {
Some(msg) => {
msg.get_translated(form_no).unwrap_or_else(|| [msg_id, msg_id_plural][form_no])
}
Some(msg) => msg
.get_translated(form_no)
.unwrap_or_else(|| [msg_id, msg_id_plural][form_no]),
None if n == 1 => msg_id,
None if n != 1 => msg_id_plural,
_ => unreachable!(),
Expand All @@ -144,7 +144,10 @@ impl Catalog {
// TODO: DRY gettext/pgettext
pub fn pgettext<'a>(&'a self, msg_context: &'a str, msg_id: &'a str) -> &'a str {
let key = key_with_context(msg_context, &msg_id);
self.strings.get(&key).and_then(|msg| msg.get_translated(0)).unwrap_or(msg_id)
self.strings
.get(&key)
.and_then(|msg| msg.get_translated(0))
.unwrap_or(msg_id)
}

/// Returns the plural translation of `msg_id`
Expand All @@ -155,26 +158,27 @@ impl Catalog {
///
/// Currently, the only supported plural formula is `n != 1`.
// TODO: DRY ngettext/npgettext
pub fn npgettext<'a>(&'a self,
msg_context: &'a str,
msg_id: &'a str,
msg_id_plural: &'a str,
n: u64)
-> &'a str {
pub fn npgettext<'a>(
&'a self,
msg_context: &'a str,
msg_id: &'a str,
msg_id_plural: &'a str,
n: u64,
) -> &'a str {
let key = key_with_context(msg_context, &msg_id);
let form_no = self.resolver.resolve(n);
match self.strings.get(&key) {
Some(msg) => {
msg.get_translated(form_no).unwrap_or_else(|| [msg_id, msg_id_plural][form_no])
}
Some(msg) => msg
.get_translated(form_no)
.unwrap_or_else(|| [msg_id, msg_id_plural][form_no]),
None if n == 1 => msg_id,
None if n != 1 => msg_id_plural,
_ => unreachable!(),
}
}
}

#[derive(Debug, Eq, PartialEq)]
#[derive(Clone, Debug, Eq, PartialEq)]
struct Message {
id: String,
context: Option<String>,
Expand Down Expand Up @@ -245,45 +249,40 @@ fn catalog_pgettext() {
#[test]
fn catalog_npgettext() {
let mut cat = Catalog::new();
cat.insert(Message::new("Text", Some("unit test"), vec!["Tekstas", "Tekstai"]));
cat.insert(Message::new(
"Text",
Some("unit test"),
vec!["Tekstas", "Tekstai"],
));

assert_eq!(cat.npgettext("unit test", "Text", "Texts", 1), "Tekstas");
assert_eq!(cat.npgettext("unit test", "Text", "Texts", 0), "Tekstai");
assert_eq!(cat.npgettext("unit test", "Text", "Texts", 2), "Tekstai");

assert_eq!(cat.npgettext("integration test", "Text", "Texts", 1),
"Text");
assert_eq!(cat.npgettext("integration test", "Text", "Texts", 0),
"Texts");
assert_eq!(cat.npgettext("integration test", "Text", "Texts", 2),
"Texts");
assert_eq!(
cat.npgettext("integration test", "Text", "Texts", 1),
"Text"
);
assert_eq!(
cat.npgettext("integration test", "Text", "Texts", 0),
"Texts"
);
assert_eq!(
cat.npgettext("integration test", "Text", "Texts", 2),
"Texts"
);
}

#[cfg(test)]
fn lithuanian_plural(n: u64) -> usize {
if (n % 10) == 1 && (n % 100) != 11 {
0
} else if ((n % 10) >= 2) && ((n % 100) < 10 || (n % 100) >= 20) {
1
} else {
2
}
}

#[test]
fn catalog_ngettext_resolver() {
let mut cat = Catalog::new();
cat.insert(Message::new("Garlic", None, vec!["Česnakas", "Česnakai", "Česnakų"]));
// https://localization-guide.readthedocs.org/en/latest/l10n/pluralforms.html
cat.resolver = Resolver::Function(Box::new(lithuanian_plural));
fn test_complex_plural() {
let reader: &[u8] = include_bytes!("../test_cases/complex_plural.mo");
let cat = parser::parse_catalog(reader, ParseOptions::new()).unwrap();

assert_eq!(cat.ngettext("Garlic", "Garlics", 0), "Česnakų");
assert_eq!(cat.ngettext("Garlic", "Garlics", 1), "Česnakas");
for i in 2..9 {
assert_eq!(cat.ngettext("Garlic", "Garlics", i), "Česnakai");
}
for i in 10..20 {
assert_eq!(cat.ngettext("Garlic", "Garlics", i), "Česnakų");
assert_eq!(cat.ngettext("Test", "Tests", 0), "Plural 2");
assert_eq!(cat.ngettext("Test", "Tests", 1), "Singular");
assert_eq!(cat.ngettext("Test", "Tests", 2), "Plural 1");
for i in 3..20 {
assert_eq!(cat.ngettext("Test", "Tests", i), "Plural 2");
}
assert_eq!(cat.ngettext("Garlic", "Garlics", 21), "Česnakas");
}
50 changes: 49 additions & 1 deletion src/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,32 @@ pub struct MetadataMap<'a>(HashMap<&'a str, &'a str>);
impl<'a> MetadataMap<'a> {
/// Returns a string that indicates the character set.
pub fn charset(&self) -> Option<&'a str> {
self.get("Content-Type").and_then(|x| x.split("charset=").skip(1).next())
self.get("Content-Type")
.and_then(|x| x.split("charset=").skip(1).next())
}

/// Returns the number of different plurals and the boolean
/// expression to determine the form to use depending on
/// the number of elements.
///
/// Defaults to `n_plurals = 2` and `plural = n!=1` (as in English).
pub fn plural_forms(&self) -> (Option<usize>, Option<&'a str>) {
self.get("Plural-Forms")
.map(|f| f.split(';').fold((None, None), |(n_pl, pl), prop| {
match prop.chars().position(|c| c == '=') {
Some(index) => {
let (name, value) = prop.split_at(index);
let value = value[1..value.len()].trim();
match name.trim() {
"n_plurals" => (usize::from_str_radix(value, 10).ok(), pl),
"plural" => (n_pl, Some(value)),
_ => (n_pl, pl)
}
},
None => (n_pl, pl)
}
}))
.unwrap_or((None, None))
}
}

Expand Down Expand Up @@ -52,3 +77,26 @@ fn test_metadatamap_charset() {
assert_eq!(map.charset().unwrap(), "utf-42");
}
}

#[test]
fn test_metadatamap_plural() {
{
let mut map = MetadataMap(HashMap::new());
assert_eq!(map.plural_forms(), (None, None));

map.insert("Plural-Forms", "");
assert_eq!(map.plural_forms(), (None, None));
// n_plural
map.insert("Plural-Forms", "n_plurals=42");
assert_eq!(map.plural_forms(), (Some(42), None));
// plural is specified
map.insert("Plural-Forms", "n_plurals=2; plural=n==12");
assert_eq!(map.plural_forms(), (Some(2), Some("n==12")));
// plural before n_plurals
map.insert("Plural-Forms", "plural=n==12; n_plurals=2");
assert_eq!(map.plural_forms(), (Some(2), Some("n==12")));
// with spaces
map.insert("Plural-Forms", " n_plurals = 42 ; plural = n > 10 ");
assert_eq!(map.plural_forms(), (Some(42), Some("n > 10")));
}
}
Loading

0 comments on commit 3367b8c

Please sign in to comment.