Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Store metadata in catalog #19

Open
wants to merge 19 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 104 additions & 16 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@
)]

mod error;
mod metadata;
/// Declare a public module named `metadata`.
/// This module contains code related to handling metadata associated with translation entries.
/// It provides functionality for managing key-value pairs of metadata.
pub mod metadata;
mod parser;
mod plurals;

Expand All @@ -57,6 +60,7 @@ use std::ops::Deref;
use crate::parser::default_resolver;
use crate::plurals::*;
pub use crate::{error::Error, parser::ParseOptions};
use metadata::MetadataMap;

fn key_with_context(context: &str, key: &str) -> String {
let mut result = context.to_owned();
Expand All @@ -69,8 +73,11 @@ fn key_with_context(context: &str, key: &str) -> String {
/// parsed out of one MO file.
#[derive(Clone, Debug)]
pub struct Catalog {
strings: HashMap<String, Message>,
/// Creates a public property to store the `Message` values from MO files
pub strings: HashMap<String, Message>,
resolver: Resolver,
/// Creates a public optional property to store the metadata from MO files
pub metadata: Option<MetadataMap>,
}

impl Catalog {
Expand All @@ -86,9 +93,15 @@ impl Catalog {
Catalog {
strings: HashMap::new(),
resolver: Resolver::Function(default_resolver),
metadata: None,
}
}

/// Merge another catalog.
pub fn merge(&mut self, catalog: &Catalog) {
self.strings.extend(catalog.strings.to_owned());
}

/// Parses a gettext catalog from the given binary MO file.
/// Returns the `Err` variant upon encountering an invalid file format
/// or invalid byte sequence in strings.
Expand Down Expand Up @@ -130,7 +143,7 @@ impl Catalog {
/// with the correct plural form for the number `n` of objects.
/// Returns msg_id if a translation does not exist and `n == 1`,
/// msg_id_plural otherwise.
pub fn ngettext<'a>(&'a self, msg_id: &'a str, msg_id_plural: &'a str, n: u64) -> &'a str {
pub fn ngettext<'a>(&'a self, msg_id: &'a str, msg_id_plural: &'a str, n: i64) -> &'a str {
let form_no = self.resolver.resolve(n);
let message = self.strings.get(msg_id);
match message.and_then(|m| m.get_translated(form_no)) {
Expand Down Expand Up @@ -164,7 +177,7 @@ impl Catalog {
msg_context: &str,
msg_id: &'a str,
msg_id_plural: &'a str,
n: u64,
n: i64,
) -> &'a str {
let key = key_with_context(msg_context, &msg_id);
let form_no = self.resolver.resolve(n);
Expand All @@ -179,18 +192,47 @@ impl Catalog {
}

#[derive(Clone, Debug, Eq, PartialEq)]
struct Message {
id: String,
context: Option<String>,
translated: Vec<String>,
/// `Message` represents a message that can be translated. It contains
/// the original string (ID), an optional plural form, an optional context
/// for disambiguation, and the translated strings for the message.
pub struct Message {
/// The original string to be translated, used as the key for looking up
/// translations.
pub id: String,
/// An optional context for the translation, used for disambiguation
/// when the same original string can have different translations
/// depending on its usage.
pub context: Option<String>,
/// Translated strings for the message. Contains one string for each
/// plural form in the target language.
pub translated: Vec<String>,
/// An optional plural form of the original string, used with ngettext.
pub plural: Option<String>,
}

impl Message {
/// Constructs a new `Message` instance with the given id, context and translated strings.
fn new<T: Into<String>>(id: T, context: Option<T>, translated: Vec<T>) -> Self {
Message {
id: id.into(),
context: context.map(Into::into),
translated: translated.into_iter().map(Into::into).collect(),
plural: None,
}
}
/// Constructs a new `Message` instance with the given id, context, translated strings,
/// and an optional plural form which will be used only when a plural form is available.
fn with_plural<T: Into<String>>(
id: T,
context: Option<T>,
translated: Vec<T>,
plural: Option<T>,
) -> Self {
Message {
id: id.into(),
context: context.map(Into::into),
translated: translated.into_iter().map(Into::into).collect(),
plural: plural.map(Into::into),
}
}

Expand All @@ -209,19 +251,53 @@ fn catalog_impls_send_sync() {
fn catalog_insert() {
let mut cat = Catalog::new();
cat.insert(Message::new("thisisid", None, vec![]));
cat.insert(Message::new("anotherid", Some("context"), vec![]));
cat.insert(Message::new("thisisid", Some("context"), vec![]));
cat.insert(Message::with_plural(
"anotherid",
None,
vec![],
Some("thisispluralid"),
));
cat.insert(Message::with_plural(
"anotherid",
Some("context"),
vec![],
Some("thisispluralid"),
));
let mut keys = cat.strings.keys().collect::<Vec<_>>();
keys.sort();
assert_eq!(keys, &["context\x04anotherid", "thisisid"])
assert_eq!(
keys,
&[
"anotherid",
"context\x04anotherid",
"context\x04thisisid",
"thisisid"
]
)
}

#[test]
fn catalog_gettext() {
let mut cat = Catalog::new();
cat.insert(Message::new("Text", None, vec!["Tekstas"]));
cat.insert(Message::new("Image", Some("context"), vec!["Paveikslelis"]));
cat.insert(Message::new("Text", Some("context"), vec!["Tekstas"]));
cat.insert(Message::with_plural(
"Image",
None,
vec!["Paveikslelis"],
Some("Images"),
));
cat.insert(Message::with_plural(
"Image",
Some("context"),
vec!["Paveikslelis"],
Some("Images"),
));
assert_eq!(cat.gettext("Text"), "Tekstas");
assert_eq!(cat.gettext("Image"), "Image");
assert_eq!(cat.gettext("context\x04Text"), "Tekstas");
assert_eq!(cat.gettext("Image"), "Paveikslelis");
assert_eq!(cat.gettext("context\x04Image"), "Paveikslelis");
}

#[test]
Expand All @@ -235,7 +311,12 @@ fn catalog_ngettext() {
assert_eq!(cat.ngettext("Text", "Texts", 2), "Texts");
}
{
cat.insert(Message::new("Text", None, vec!["Tekstas", "Tekstai"]));
cat.insert(Message::with_plural(
"Text",
None,
vec!["Tekstas", "Tekstai"],
Some("Texts"),
));
// n == 1, translation available
assert_eq!(cat.ngettext("Text", "Texts", 1), "Tekstas");
// n != 1, translation available
Expand Down Expand Up @@ -265,10 +346,11 @@ fn catalog_npgettext_not_enough_forms_in_message() {
}

let mut cat = Catalog::new();
cat.insert(Message::new(
cat.insert(Message::with_plural(
"Text",
Some("ctx"),
vec!["Tekstas", "Tekstai"],
Some("Texts"),
));
cat.resolver = Resolver::Function(resolver);
assert_eq!(cat.npgettext("ctx", "Text", "Texts", 0), "Tekstas");
Expand All @@ -279,18 +361,24 @@ fn catalog_npgettext_not_enough_forms_in_message() {
#[test]
fn catalog_pgettext() {
let mut cat = Catalog::new();
cat.insert(Message::new("Text", Some("unit test"), vec!["Tekstas"]));
cat.insert(Message::with_plural(
"Text",
Some("unit test"),
vec!["Tekstas"],
Some("Texts"),
));
assert_eq!(cat.pgettext("unit test", "Text"), "Tekstas");
assert_eq!(cat.pgettext("integration test", "Text"), "Text");
}

#[test]
fn catalog_npgettext() {
let mut cat = Catalog::new();
cat.insert(Message::new(
cat.insert(Message::with_plural(
"Text",
Some("unit test"),
vec!["Tekstas", "Tekstai"],
Some("Texts"),
));

assert_eq!(cat.npgettext("unit test", "Text", "Texts", 1), "Tekstas");
Expand Down
58 changes: 39 additions & 19 deletions src/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@ use std::ops::{Deref, DerefMut};
use super::Error;
use crate::Error::MalformedMetadata;

#[derive(Debug)]
pub struct MetadataMap<'a>(HashMap<&'a str, &'a str>);
#[derive(Debug, Clone)]

impl<'a> MetadataMap<'a> {
/// Define a struct called `MetadataMap` that represents a map of metadata.
/// It is a simple wrapper around a `HashMap` with `String` keys and `String` values.
/// This struct is used to store key-value pairs of metadata associated with a translation entry or other data.
pub struct MetadataMap(HashMap<String, String>);

impl MetadataMap {
/// Returns a string that indicates the character set.
pub fn charset(&self) -> Option<&'a str> {
pub fn charset(&self) -> Option<&str> {
self.get("Content-Type")
.and_then(|x| x.split("charset=").nth(1))
}
Expand All @@ -19,7 +23,7 @@ impl<'a> MetadataMap<'a> {
/// 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>) {
pub fn plural_forms(&self) -> (Option<usize>, Option<&str>) {
self.get("Plural-Forms")
.map(|f| {
f.split(';').fold((None, None), |(n_pl, pl), prop| {
Expand All @@ -41,27 +45,31 @@ impl<'a> MetadataMap<'a> {
}
}

impl<'a> Deref for MetadataMap<'a> {
type Target = HashMap<&'a str, &'a str>;
impl Deref for MetadataMap {
type Target = HashMap<String, String>;
fn deref(&self) -> &Self::Target {
&self.0
}
}

impl<'a> DerefMut for MetadataMap<'a> {
impl DerefMut for MetadataMap {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}

pub fn parse_metadata(blob: &str) -> Result<MetadataMap, Error> {
/// Parses the given metadata blob into a `MetadataMap`.
pub fn parse_metadata(blob: String) -> Result<MetadataMap, Error> {
let mut map = MetadataMap(HashMap::new());
for line in blob.split('\n').filter(|s| s != &"") {
for line in blob.split('\n').filter(|s| !s.is_empty()) {
let pos = match line.bytes().position(|b| b == b':') {
Some(p) => p,
None => return Err(MalformedMetadata),
};
map.insert(line[..pos].trim(), line[pos + 1..].trim());
map.insert(
line[..pos].trim().to_string(),
line[pos + 1..].trim().to_string(),
);
}
Ok(map)
}
Expand All @@ -71,11 +79,14 @@ fn test_metadatamap_charset() {
{
let mut map = MetadataMap(HashMap::new());
assert!(map.charset().is_none());
map.insert("Content-Type", "");
map.insert("Content-Type".to_string(), "".to_string());
assert!(map.charset().is_none());
map.insert("Content-Type", "abc");
map.insert("Content-Type".to_string(), "abc".to_string());
assert!(map.charset().is_none());
map.insert("Content-Type", "text/plain; charset=utf-42");
map.insert(
"Content-Type".to_string(),
"text/plain; charset=utf-42".to_string(),
);
assert_eq!(map.charset().unwrap(), "utf-42");
}
}
Expand All @@ -86,19 +97,28 @@ fn test_metadatamap_plural() {
let mut map = MetadataMap(HashMap::new());
assert_eq!(map.plural_forms(), (None, None));

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