gettext-0.4.0/.gitignore010064400017500001750000000000361337300215500134060ustar0000000000000000src/*.rs.bk target Cargo.lock gettext-0.4.0/.travis.yml010064400017500001750000000001741347544157000135450ustar0000000000000000language: rust rust: - 1.31.0 - stable - beta - nightly script: - cargo build --verbose - cargo test --verbose gettext-0.4.0/Cargo.toml.orig010064400017500001750000000005241347544157000143220ustar0000000000000000[package] name = "gettext" version = "0.4.0" authors = ["Justinas Stankevicius "] description = "An implementation of Gettext translation framework for Rust" license = "MIT" repository = "https://github.com/justinas/gettext" readme = "README.md" edition = "2018" [dependencies] byteorder = "1.3" encoding = "0.2.32" gettext-0.4.0/Cargo.toml0000644000000015770000000000000105700ustar00# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g. crates.io) dependencies # # If you believe there's an error in this file please file an # issue against the rust-lang/cargo repository. If you're # editing this file be aware that the upstream Cargo.toml # will likely look very different (and much more reasonable) [package] edition = "2018" name = "gettext" version = "0.4.0" authors = ["Justinas Stankevicius "] description = "An implementation of Gettext translation framework for Rust" readme = "README.md" license = "MIT" repository = "https://github.com/justinas/gettext" [dependencies.byteorder] version = "1.3" [dependencies.encoding] version = "0.2.32" gettext-0.4.0/LICENSE010066400017500001750000000021001275140174200124230ustar0000000000000000The MIT License (MIT) Copyright (c) 2016 Justinas Stankevicius Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. gettext-0.4.0/Makefile010066400017500001750000000002431275140174200130640ustar0000000000000000MO_FILES = $(patsubst %.po,%.mo,$(wildcard test_cases/*.po)) %.mo: %.po msgfmt -o $@ $< all: test_cases test_cases: $(MO_FILES) clean: rm -f test_cases/*.mo gettext-0.4.0/README.md010064400017500001750000000004321337277123100127040ustar0000000000000000# Gettext for Rust [Documentation (latest stable)](https://docs.rs/gettext/) ## Roadmap for now - [x] Parsing MO files (10.3) - [x] Parsing metadata (6.2) - [x] Supporting encodings other than UTF-8 - [x] Parsing the plural expression (11.2.6) - [ ] Correct pathfinding? (11.2.3) gettext-0.4.0/src/lib.rs010064400017500001750000000231551347544157000133430ustar0000000000000000//! This crate is a reimplementation //! of GNU gettext translation framework in Rust. //! It allows your Rust programs to parse out GNU MO files //! containing translations and use them in your user interface. //! //! It contains several differences from the official C implementation. //! Notably, this crate does not in any way depend on a global locale //! ([2.2](https://www.gnu.org/software/gettext/manual/gettext.html#Setting-the-GUI-Locale)) //! and does not enforce a directory structure //! for storing your translation catalogs //! ([11.2.3](https://www.gnu.org/software/gettext/manual/gettext.html#Locating-Catalogs)). //! Instead, the choice of translation catalog to use is explicitly made by the user. //! //! This crate is still in-progress //! and may not be on par with the original implementation feature-wise. //! //! For the exact feature parity see the roadmap in the //! [README](https://github.com/justinas/gettext#readme). //! //! # Example //! //! ```ignore //! //! use std::fs::File; //! use gettext::Catalog; //! //! fn main() { //! let f = File::open("french.mo").expect("could not open the catalog"); //! let catalog = Catalog::parse(f).expect("could not parse the catalog"); //! //! // Will print out the French translation //! // if it is found in the parsed file //! // or "Name" otherwise. //! println!("{}", catalog.gettext("Name")); //! } //! ``` // 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))] mod metadata; mod parser; mod plurals; use std::collections::HashMap; use std::io::Read; use std::ops::Deref; use crate::parser::default_resolver; pub use crate::parser::{Error, ParseOptions}; use crate::plurals::*; fn key_with_context(context: &str, key: &str) -> String { let mut result = context.to_owned(); result.push('\x04'); result.push_str(key); result } /// Catalog represents a set of translation strings /// parsed out of one MO file. #[derive(Clone, Debug)] pub struct Catalog { strings: HashMap, resolver: Resolver, } impl Catalog { /// Creates an empty catalog. /// /// All the translated strings will be the same as the original ones. pub fn empty() -> Self { Self::new() } /// Creates a new, empty gettext catalog. fn new() -> Self { Catalog { strings: HashMap::new(), resolver: Resolver::Function(default_resolver), } } /// 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. /// /// Calling this method is equivalent to calling /// `ParseOptions::new().parse(reader)`. /// /// # Examples /// /// ```ignore /// use gettext::Catalog; /// use std::fs::File; /// /// let file = File::open("french.mo").unwrap(); /// let catalog = Catalog::parse(file).unwrap(); /// ``` pub fn parse(reader: R) -> Result { ParseOptions::new().parse(reader) } fn insert(&mut self, msg: Message) { let key = match msg.context { Some(ref ctxt) => key_with_context(ctxt, &msg.id), None => msg.id.clone(), }; self.strings.insert(key, msg); } /// 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) } /// Returns the plural translation of `msg_id` from the given 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 { let form_no = self.resolver.resolve(n); let message = self.strings.get(msg_id); match message.and_then(|m| m.get_translated(form_no)) { Some(msg) => msg, None if n == 1 => msg_id, None if n != 1 => msg_id_plural, _ => unreachable!(), } } /// Returns the singular translation of `msg_id` /// in the context `msg_context` /// or `msg_id` itself if a translation does not exist. // 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) } /// Returns the plural translation of `msg_id` /// in the context `msg_context` /// 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. // 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 { let key = key_with_context(msg_context, &msg_id); let form_no = self.resolver.resolve(n); let message = self.strings.get(&key); match message.and_then(|m| m.get_translated(form_no)) { Some(msg) => msg, None if n == 1 => msg_id, None if n != 1 => msg_id_plural, _ => unreachable!(), } } } #[derive(Clone, Debug, Eq, PartialEq)] struct Message { id: String, context: Option, translated: Vec, } impl Message { fn new>(id: T, context: Option, translated: Vec) -> Self { Message { id: id.into(), context: context.map(Into::into), translated: translated.into_iter().map(Into::into).collect(), } } fn get_translated(&self, form_no: usize) -> Option<&str> { self.translated.get(form_no).map(|s| s.deref()) } } #[test] fn catalog_impls_send_sync() { fn check(_: T) {}; check(Catalog::new()); } #[test] fn catalog_insert() { let mut cat = Catalog::new(); cat.insert(Message::new("thisisid", None, vec![])); cat.insert(Message::new("anotherid", Some("context"), vec![])); let mut keys = cat.strings.keys().collect::>(); keys.sort(); assert_eq!(keys, &["context\x04anotherid", "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"])); assert_eq!(cat.gettext("Text"), "Tekstas"); assert_eq!(cat.gettext("Image"), "Image"); } #[test] fn catalog_ngettext() { let mut cat = Catalog::new(); { // n == 1, no translation assert_eq!(cat.ngettext("Text", "Texts", 1), "Text"); // n != 1, no translation assert_eq!(cat.ngettext("Text", "Texts", 0), "Texts"); assert_eq!(cat.ngettext("Text", "Texts", 2), "Texts"); } { cat.insert(Message::new("Text", None, vec!["Tekstas", "Tekstai"])); // n == 1, translation available assert_eq!(cat.ngettext("Text", "Texts", 1), "Tekstas"); // n != 1, translation available assert_eq!(cat.ngettext("Text", "Texts", 0), "Tekstai"); assert_eq!(cat.ngettext("Text", "Texts", 2), "Tekstai"); } } #[test] fn catalog_ngettext_not_enough_forms_in_message() { fn resolver(count: u64) -> usize { count as usize } let mut cat = Catalog::new(); cat.insert(Message::new("Text", None, vec!["Tekstas", "Tekstai"])); cat.resolver = Resolver::Function(resolver); assert_eq!(cat.ngettext("Text", "Texts", 0), "Tekstas"); assert_eq!(cat.ngettext("Text", "Texts", 1), "Tekstai"); assert_eq!(cat.ngettext("Text", "Texts", 2), "Texts"); } #[test] fn catalog_npgettext_not_enough_forms_in_message() { fn resolver(count: u64) -> usize { count as usize } let mut cat = Catalog::new(); cat.insert(Message::new( "Text", Some("ctx"), vec!["Tekstas", "Tekstai"], )); cat.resolver = Resolver::Function(resolver); assert_eq!(cat.npgettext("ctx", "Text", "Texts", 0), "Tekstas"); assert_eq!(cat.npgettext("ctx", "Text", "Texts", 1), "Tekstai"); assert_eq!(cat.npgettext("ctx", "Text", "Texts", 2), "Texts"); } #[test] fn catalog_pgettext() { let mut cat = Catalog::new(); cat.insert(Message::new("Text", Some("unit test"), vec!["Tekstas"])); 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( "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" ); } gettext-0.4.0/src/metadata.rs010064400017500001750000000067171347544157000143620ustar0000000000000000use std::collections::HashMap; use std::ops::{Deref, DerefMut}; use super::Error; use crate::Error::MalformedMetadata; #[derive(Debug)] 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()) } /// 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, 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)) } } impl<'a> Deref for MetadataMap<'a> { type Target = HashMap<&'a str, &'a str>; fn deref(&self) -> &Self::Target { &self.0 } } impl<'a> DerefMut for MetadataMap<'a> { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } pub fn parse_metadata(blob: &str) -> Result { let mut map = MetadataMap(HashMap::new()); for line in blob.split('\n').filter(|s| s != &"") { 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()); } Ok(map) } #[test] fn test_metadatamap_charset() { { let mut map = MetadataMap(HashMap::new()); assert!(map.charset().is_none()); map.insert("Content-Type", ""); assert!(map.charset().is_none()); map.insert("Content-Type", "abc"); assert!(map.charset().is_none()); map.insert("Content-Type", "text/plain; charset=utf-42"); 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"))); } } gettext-0.4.0/src/parser.rs010064400017500001750000000245601347544157000140720ustar0000000000000000use std::borrow::Cow; use std::default::Default; use std::error; use std::fmt; use std::io; use byteorder::{BigEndian, ByteOrder, LittleEndian}; use encoding::label::encoding_from_whatwg_label; use encoding::types::DecoderTrap::Strict; use encoding::types::EncodingRef; use crate::plurals::{Ast, Resolver}; use crate::{Catalog, Message}; use crate::metadata::parse_metadata; #[allow(non_upper_case_globals)] static utf8_encoding: EncodingRef = &encoding::codec::utf_8::UTF8Encoding; /// Represents an error encountered while parsing an MO file. #[derive(Debug)] pub enum Error { /// An incorrect magic number has been encountered BadMagic, /// An invalid byte sequence for the given encoding has been encountered DecodingError, /// An unexpected EOF occured Eof, /// An I/O error occured Io(io::Error), /// Incorrect syntax encountered while parsing the meta information MalformedMetadata, /// Meta information string was not the first string in the catalog MisplacedMetadata, /// Invalid Plural-Forms metadata PluralParsing, /// An unknown encoding was specified in the metadata UnknownEncoding, } use crate::Error::*; impl error::Error for Error { fn description(&self) -> &str { match *self { BadMagic => "bad magic number", DecodingError => "invalid byte sequence in a string", Eof => "unxpected end of file", Io(ref err) => err.description(), MalformedMetadata => "metadata syntax error", MisplacedMetadata => "misplaced metadata", UnknownEncoding => "unknown encoding specified", PluralParsing => "invalid plural expression", } } } impl fmt::Display for Error { fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { let self_err: &error::Error = self; write!(fmt, "{}", self_err.description()) } } impl From for Error { fn from(inner: io::Error) -> Error { Io(inner) } } impl From> for Error { fn from(_: Cow<'static, str>) -> Error { DecodingError } } /// ParseOptions allows setting options for parsing MO catalogs. /// /// # Examples /// ```ignore /// use std::fs::File; /// use encoding::all::ISO_8859_1; /// /// let file = File::open("french.mo").unwrap(); /// let catalog = ParseOptions::new().force_encoding(ISO_8859_1).parse(file).unwrap(); /// ``` #[allow(missing_debug_implementations)] #[derive(Default)] pub struct ParseOptions { force_encoding: Option, force_plural: Option usize>, } impl ParseOptions { /// Returns a new instance of ParseOptions with default options. pub fn new() -> Self { Default::default() } /// Tries to parse the catalog from the given reader using the specified options. pub fn parse(self, reader: R) -> Result { parse_catalog(reader, self) } /// Forces a use of a specific encoding /// when parsing strings from a catalog. /// If this option is not enabled, /// the parser tries to use the encoding specified in the metadata /// or UTF-8 if metadata is non-existent. pub fn force_encoding(mut self, encoding: EncodingRef) -> Self { self.force_encoding = Some(encoding); self } /// Forces a use of the given plural formula /// for deciding the proper plural form for a message. /// If this option is not enabled, /// the parser tries to use the plural formula specified in the metadata /// or `n != 1` if metadata is non-existent. pub fn force_plural(mut self, plural: fn(u64) -> usize) -> Self { self.force_plural = Some(plural); self } } /// According to the given magic number of a MO file, /// returns the function which reads a `u32` in the relevant endianness. fn get_read_u32_fn(magic: &[u8]) -> Option u32> { if magic == [0xde, 0x12, 0x04, 0x95] { Some(LittleEndian::read_u32) } else if magic == [0x95, 0x04, 0x12, 0xde] { Some(BigEndian::read_u32) } else { None } } pub fn parse_catalog<'a, R: io::Read>(mut file: R, opts: ParseOptions) -> Result { let mut contents = vec![]; let n = file.read_to_end(&mut contents)?; if n < 28 { return Err(Eof); } let read_u32 = get_read_u32_fn(&contents[0..4]).ok_or(BadMagic)?; // ignore hashing tables (bytes at 20..28) let num_strings = read_u32(&contents[8..12]) as usize; let mut off_otable = read_u32(&contents[12..16]) as usize; let mut off_ttable = read_u32(&contents[16..20]) as usize; if n < off_otable || n < off_ttable { return Err(Eof); } let mut catalog = Catalog::new(); if let Some(f) = opts.force_plural { catalog.resolver = Resolver::Function(f); } let mut encoding = opts.force_encoding.unwrap_or(utf8_encoding); for i in 0..num_strings { // Parse the original string if n < off_otable + 8 { return Err(Eof); } let len = read_u32(&contents[off_otable..off_otable + 4]) as usize; let off = read_u32(&contents[off_otable + 4..off_otable + 8]) as usize; // +1 compensates for the ending NUL byte which is not included in length if n < off + len + 1 { return Err(Eof); } let mut original = &contents[off..off + len + 1]; // check for context let context = match original.iter().position(|x| *x == 4) { Some(idx) => { let ctx = &original[..idx]; original = &original[idx + 1..]; Some(encoding.decode(ctx, Strict)?) } None => None, }; // extract msg_id singular, ignoring the plural let id = match original .iter() .position(|x| *x == 0) .map(|i| &original[..i]) { Some(b) => encoding.decode(b, Strict)?, None => return Err(Eof), }; if id == "" && i != 0 { return Err(MisplacedMetadata); } // Parse the translation strings if n < off_ttable + 8 { return Err(Eof); } let len = read_u32(&contents[off_ttable..off_ttable + 4]) as usize; let off = read_u32(&contents[off_ttable + 4..off_ttable + 8]) as usize; // +1 compensates for the ending NUL byte which is not included in length if n < off + len + 1 { return Err(Eof); } let translated = contents[off..off + len] .split(|x| *x == 0) .map(|b| encoding.decode(b, Strict)) .collect::, _>>()?; if id == "" { let map = parse_metadata(&*translated[0])?; if let (Some(c), None) = (map.charset(), opts.force_encoding) { encoding = encoding_from_whatwg_label(c).ok_or(UnknownEncoding)?; } if opts.force_plural.is_none() { if let Some(p) = map.plural_forms().1 { catalog.resolver = Ast::parse(p).map(Resolver::Expr)?; } } } catalog.insert(Message::new(id, context, translated)); off_otable += 8; off_ttable += 8; } Ok(catalog) } /// The default plural resolver. /// /// It will be used if not `Plural-Forms` header is found in the .mo file, and if /// `ParseOptions::force_plural` was not called. /// /// It is valid for English and similar languages: plural will be used for any quantity /// different of 1. pub fn default_resolver(n: u64) -> usize { if n == 1 { 0 } else { 1 } } #[test] fn test_get_read_u32_fn() { use std::mem; assert!(get_read_u32_fn(&[]).is_none()); assert!(get_read_u32_fn(&[0xde, 0x12, 0x04, 0x95, 0x00]).is_none()); { let le_ptr: *const (); let ret_ptr; unsafe { le_ptr = mem::transmute(LittleEndian::read_u32 as usize); ret_ptr = mem::transmute(get_read_u32_fn(&[0xde, 0x12, 0x04, 0x95]).unwrap()); } assert_eq!(le_ptr, ret_ptr); } { let be_ptr: *const (); let ret_ptr; unsafe { be_ptr = mem::transmute(BigEndian::read_u32 as usize); ret_ptr = mem::transmute(get_read_u32_fn(&[0x95, 0x04, 0x12, 0xde]).unwrap()); } assert_eq!(be_ptr, ret_ptr); } } #[test] fn test_parse_catalog() { macro_rules! assert_variant { ($value:expr, $variant:path) => { match $value { $variant => (), _ => panic!("Expected {:?}, got {:?}", $variant, $value), } }; } let fluff = [0; 24]; // zeros to pad our magic test cases to satisfy the length requirements { let mut reader = vec![1u8, 2, 3]; reader.extend(fluff.iter().cloned()); let err = parse_catalog(&reader[..], ParseOptions::new()).unwrap_err(); assert_variant!(err, Eof); } { let mut reader = vec![1u8, 2, 3, 4]; reader.extend(fluff.iter().cloned()); let err = parse_catalog(&reader[..], ParseOptions::new()).unwrap_err(); assert_variant!(err, BadMagic); } { let mut reader = vec![0x95, 0x04, 0x12, 0xde]; reader.extend(fluff.iter().cloned()); assert!(parse_catalog(&reader[..], ParseOptions::new()).is_ok()); } { let mut reader = vec![0xde, 0x12, 0x04, 0x95]; reader.extend(fluff.iter().cloned()); assert!(parse_catalog(&reader[..], ParseOptions::new()).is_ok()); } { let reader: &[u8] = include_bytes!("../test_cases/1.mo"); let catalog = parse_catalog(reader, ParseOptions::new()).unwrap(); assert_eq!(catalog.strings.len(), 1); assert_eq!( catalog.strings["this is context\x04Text"], Message::new("Text", Some("this is context"), vec!["Tekstas", "Tekstai"]) ); } { let reader: &[u8] = include_bytes!("../test_cases/2.mo"); let catalog = parse_catalog(reader, ParseOptions::new()).unwrap(); assert_eq!(catalog.strings.len(), 2); assert_eq!( catalog.strings["Image"], Message::new("Image", None, vec!["Nuotrauka", "Nuotraukos"]) ); } { let reader: &[u8] = include_bytes!("../test_cases/invalid_utf8.mo"); let err = parse_catalog(reader, ParseOptions::new()).unwrap_err(); assert_variant!(err, DecodingError); } } gettext-0.4.0/src/plurals.rs010064400017500001750000000251761347544157000142640ustar0000000000000000use crate::parser::Error; use self::Resolver::*; #[derive(Clone, Debug)] pub enum Resolver { /// A boolean expression /// Use Ast::parse to get an Ast Expr(Ast), /// A function Function(fn(u64) -> usize), } /// Finds the index of a pattern, outside of parenthesis fn index_of<'a>(src: &'a str, pat: &'static str) -> Option { src.chars() .fold( (None, 0, 0, 0), |(match_index, i, n_matches, paren_level), ch| { if let Some(x) = match_index { return (Some(x), i, n_matches, paren_level); } else { let new_par_lvl = match ch { '(' => paren_level + 1, ')' => paren_level - 1, _ => paren_level, }; if Some(ch) == pat.chars().nth(n_matches) { let length = n_matches + 1; if length == pat.len() && new_par_lvl == 0 { (Some(i - n_matches), i + 1, length, new_par_lvl) } else { (match_index, i + 1, length, new_par_lvl) } } else { (match_index, i + 1, 0, new_par_lvl) } } }, ) .0 } use self::Ast::*; #[derive(Clone, Debug, PartialEq)] pub enum Ast { /// A ternary expression /// x ? a : b /// /// the three Ast<'a> are respectively x, a and b. Ternary(Box, Box, Box), /// The n variable. N, /// Integer literals. Integer(u64), /// Binary operators. Op(Operator, Box, Box), /// ! operator. Not(Box), } #[derive(Clone, Debug, PartialEq)] pub enum Operator { Equal, NotEqual, GreaterOrEqual, SmallerOrEqual, Greater, Smaller, And, Or, Modulo, } impl Ast { fn resolve(&self, n: u64) -> usize { match *self { Ternary(ref cond, ref ok, ref nok) => { if cond.resolve(n) == 0 { nok.resolve(n) } else { ok.resolve(n) } } N => n as usize, Integer(x) => x as usize, Op(ref op, ref lhs, ref rhs) => match *op { Operator::Equal => (lhs.resolve(n) == rhs.resolve(n)) as usize, Operator::NotEqual => (lhs.resolve(n) != rhs.resolve(n)) as usize, Operator::GreaterOrEqual => (lhs.resolve(n) >= rhs.resolve(n)) as usize, Operator::SmallerOrEqual => (lhs.resolve(n) <= rhs.resolve(n)) as usize, Operator::Greater => (lhs.resolve(n) > rhs.resolve(n)) as usize, Operator::Smaller => (lhs.resolve(n) < rhs.resolve(n)) as usize, Operator::And => (lhs.resolve(n) != 0 && rhs.resolve(n) != 0) as usize, Operator::Or => (lhs.resolve(n) != 0 || rhs.resolve(n) != 0) as usize, Operator::Modulo => lhs.resolve(n) % rhs.resolve(n), }, Not(ref val) => match val.resolve(n) { 0 => 1, _ => 0, }, } } pub fn parse<'a>(src: &'a str) -> Result { Self::parse_parens(src.trim()) } fn parse_parens<'a>(src: &'a str) -> Result { if src.starts_with('(') { let end = src[1..src.len() - 1].chars().fold((1, 2), |(level, index), ch| { if level > 0 { if ch == ')' { (level - 1, index + 1) } else if ch == '(' { (level + 1, index + 1) } else { (level, index + 1) } } else { if ch == '(' { (level + 1, index + 1) } else { (level, index) } } }).1; if end == src.len() { Ast::parse(src[1..src.len() - 1].trim()) } else { Ast::parse_and(src.trim()) } } else { Ast::parse_and(src.trim()) } } fn parse_and<'a>(src: &'a str) -> Result { if let Some(i) = index_of(src, "&&") { Ok(Ast::Op( Operator::And, Box::new(Ast::parse(&src[0..i])?), Box::new(Ast::parse(&src[i + 2..])?), )) } else { Self::parse_or(src) } } fn parse_or<'a>(src: &'a str) -> Result { if let Some(i) = index_of(src, "||") { Ok(Ast::Op( Operator::Or, Box::new(Ast::parse(&src[0..i])?), Box::new(Ast::parse(&src[i + 2..])?), )) } else { Self::parse_ternary(src) } } fn parse_ternary<'a>(src: &'a str) -> Result { if let Some(i) = index_of(src, "?") { if let Some(l) = index_of(src, ":") { Ok(Ast::Ternary( Box::new(Ast::parse(&src[0..i])?), Box::new(Ast::parse(&src[i + 1..l])?), Box::new(Ast::parse(&src[l + 1..])?), )) } else { Err(Error::PluralParsing) } } else { Self::parse_ge(src) } } fn parse_ge<'a>(src: &'a str) -> Result { if let Some(i) = index_of(src, ">=") { Ok(Ast::Op( Operator::GreaterOrEqual, Box::new(Ast::parse(&src[0..i])?), Box::new(Ast::parse(&src[i + 2..])?), )) } else { Self::parse_gt(src) } } fn parse_gt<'a>(src: &'a str) -> Result { if let Some(i) = index_of(src, ">") { Ok(Ast::Op( Operator::Greater, Box::new(Ast::parse(&src[0..i])?), Box::new(Ast::parse(&src[i + 1..])?), )) } else { Self::parse_le(src) } } fn parse_le<'a>(src: &'a str) -> Result { if let Some(i) = index_of(src, "<=") { Ok(Ast::Op( Operator::SmallerOrEqual, Box::new(Ast::parse(&src[0..i])?), Box::new(Ast::parse(&src[i + 2..])?), )) } else { Self::parse_lt(src) } } fn parse_lt<'a>(src: &'a str) -> Result { if let Some(i) = index_of(src, "<") { Ok(Ast::Op( Operator::Smaller, Box::new(Ast::parse(&src[0..i])?), Box::new(Ast::parse(&src[i + 1..])?), )) } else { Self::parse_eq(src) } } fn parse_eq<'a>(src: &'a str) -> Result { if let Some(i) = index_of(src, "==") { Ok(Ast::Op( Operator::Equal, Box::new(Ast::parse(&src[0..i])?), Box::new(Ast::parse(&src[i + 2..])?), )) } else { Self::parse_neq(src) } } fn parse_neq<'a>(src: &'a str) -> Result { if let Some(i) = index_of(src, "!=") { Ok(Ast::Op( Operator::NotEqual, Box::new(Ast::parse(&src[0..i])?), Box::new(Ast::parse(&src[i + 2..])?), )) } else { Self::parse_mod(src) } } fn parse_mod<'a>(src: &'a str) -> Result { if let Some(i) = index_of(src, "%") { Ok(Ast::Op( Operator::Modulo, Box::new(Ast::parse(&src[0..i])?), Box::new(Ast::parse(&src[i + 1..])?), )) } else { Self::parse_not(src.trim()) } } fn parse_not<'a>(src: &'a str) -> Result { if index_of(src, "!") == Some(0) { Ok(Ast::Not(Box::new(Ast::parse(&src[1..])?))) } else { Self::parse_int(src.trim()) } } fn parse_int<'a>(src: &'a str) -> Result { if let Ok(x) = u64::from_str_radix(src, 10) { Ok(Ast::Integer(x)) } else { Self::parse_n(src.trim()) } } fn parse_n<'a>(src: &'a str) -> Result { if src == "n" { Ok(Ast::N) } else { Err(Error::PluralParsing) } } } impl Resolver { /// Returns the number of the correct plural form /// for `n` objects, as defined by the rule contained in this resolver. pub fn resolve(&self, n: u64) -> usize { match *self { Expr(ref ast) => ast.resolve(n), Function(ref f) => f(n), } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_expr_resolver() { assert_eq!(Expr(N).resolve(42), 42); } #[test] fn test_parser() { assert_eq!( Ast::parse("n == 42 ? n : 6 && n < 7").expect("Invalid plural"), Ast::Op( Operator::And, Box::new(Ast::Ternary( Box::new(Ast::Op( Operator::Equal, Box::new(Ast::N), Box::new(Ast::Integer(42)) )), Box::new(Ast::N), Box::new(Ast::Integer(6)) )), Box::new(Ast::Op( Operator::Smaller, Box::new(Ast::N), Box::new(Ast::Integer(7)) )) ) ); assert_eq!(Ast::parse("(n)").expect("Invalid plural"), Ast::N); assert_eq!( Ast::parse("(n == 1 || n == 2) ? 0 : 1").expect("Invalid plural"), Ast::Ternary( Box::new(Ast::Op( Operator::Or, Box::new(Ast::Op( Operator::Equal, Box::new(Ast::N), Box::new(Ast::Integer(1)) )), Box::new(Ast::Op( Operator::Equal, Box::new(Ast::N), Box::new(Ast::Integer(2)) )) )), Box::new(Ast::Integer(0)), Box::new(Ast::Integer(1)) ) ); let ru_plural = "((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3))"; assert!(Ast::parse(ru_plural).is_ok()); } } gettext-0.4.0/test_cases/1.mo010066400017500001750000000001431275140176600142630ustar0000000000000000$,8Sthis is contextTextTextsTekstasTekstaigettext-0.4.0/test_cases/1.po010066400017500001750000000001441275140176600142670ustar0000000000000000msgctxt "this is context" msgid "Text" msgid_plural "Texts" msgstr[0] "Tekstas" msgstr[1] "Tekstai" gettext-0.4.0/test_cases/2.mo010066400017500001750000000002351275140176600142660ustar0000000000000000,< P]xImageImagesthis is contextTextTextsNuotraukaNuotraukosTekstasTekstaigettext-0.4.0/test_cases/2.po010066400017500001750000000002661275140176600142750ustar0000000000000000msgctxt "this is context" msgid "Text" msgid_plural "Texts" msgstr[0] "Tekstas" msgstr[1] "Tekstai" msgid "Image" msgid_plural "Images" msgstr[0] "Nuotrauka" msgstr[1] "Nuotraukos" gettext-0.4.0/test_cases/complex_plural.mo010064400017500001750000000004221337277123100171440ustar0000000000000000,<P Q\TestTestsMIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=3; plural = n == 1 ? 0 : n == 2 ? 1 : 2; SingularPlural 1Plural 2gettext-0.4.0/test_cases/complex_plural.po010064400017500001750000000004371337277123100171550ustar0000000000000000msgid "" msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural = n == 1 ? 0 : n == 2 ? 1 : 2;\n" msgid "Test" msgid_plural "Tests" msgstr[0] "Singular" msgstr[1] "Plural 1" msgstr[2] "Plural 2" gettext-0.4.0/test_cases/cp1257_forced.mo010066400017500001750000000001101322055122200163410ustar0000000000000000$,8?Garlicesnakasgettext-0.4.0/test_cases/cp1257_forced.po010066400017500001750000000001111322055122200163450ustar0000000000000000# This file must be encoded in cp1257. msgid "Garlic" msgstr "esnakas" gettext-0.4.0/test_cases/cp1257_meta.mo010066400017500001750000000002131322055122200160310ustar0000000000000000,<PQ)XGarlicContent-Type: text/plain; charset=cp1257 esnakasgettext-0.4.0/test_cases/cp1257_meta.po010066400017500001750000000002121322055122200160330ustar0000000000000000# This file must be encoded in cp1257. msgid "" msgstr "" "Content-Type: text/plain; charset=cp1257\n" msgid "Garlic" msgstr "esnakas" gettext-0.4.0/test_cases/integration.mo010064400017500001750000000012631337277123100164450ustar0000000000000000Dl gC-]a good stringgood stringsctxta good stringgood stringsctxtexistentexistentProject-Id-Version: PACKAGE VERSION Report-Msgid-Bugs-To: POT-Creation-Date: 2016-02-09 12:43+0200 PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE Last-Translator: FULL NAME Language-Team: LANGUAGE Language: MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=2; plural=n!=1; gera eilutegeros eilutesgera eilute kontekstegeros eilutes konteksteegzistuojantis konteksteegzistuojantisgettext-0.4.0/test_cases/integration.po010064400017500001750000000026711337277123100164540ustar0000000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-02-09 12:43+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n!=1;\n" #: ../tests/lib.rs:12 msgid "non-existent" msgstr "" #: ../tests/lib.rs:13 msgid "existent" msgstr "egzistuojantis" #: ../tests/lib.rs:15 ../tests/lib.rs:17 msgid "a bad string" msgid_plural "bad strings" msgstr[0] "" msgstr[1] "" #: ../tests/lib.rs:19 ../tests/lib.rs:21 msgid "a good string" msgid_plural "good strings" msgstr[0] "gera eilute" msgstr[1] "geros eilutes" #: ../tests/lib.rs:24 msgctxt "ctxt" msgid "non-existent" msgstr "" #: ../tests/lib.rs:25 msgctxt "ctxt" msgid "existent" msgstr "egzistuojantis kontekste" #: ../tests/lib.rs:27 ../tests/lib.rs:29 msgctxt "ctxt" msgid "a bad string" msgid_plural "bad strings" msgstr[0] "" msgstr[1] "" #: ../tests/lib.rs:31 ../tests/lib.rs:33 msgctxt "ctxt" msgid "a good string" msgid_plural "good strings" msgstr[0] "gera eilute kontekste" msgstr[1] "geros eilutes kontekste" gettext-0.4.0/test_cases/invalid_utf8.mo010066400017500001750000000002361275140176600165220ustar0000000000000000,< P]xImageImagesthis is contextTextTextsNuotraukaNuotraukosTekstasTekstaigettext-0.4.0/test_cases/invalid_utf8.po010066400017500001750000000002721275140176600165250ustar0000000000000000msgctxt "this is context" msgid "Text" msgid_plural "Texts" msgstr[0] "Tekstas" msgstr[1] "Tekstai" msgid "Image" msgid_plural "Images" msgstr[0] "Nuotrauka\xFF" msgstr[1] "Nuotraukos" gettext-0.4.0/test_cases/lt_plural_forced.mo010064400017500001750000000001451337277123100174400ustar0000000000000000$,8GGarlicGarlicsČesnakasČesnakaiČesnakųgettext-0.4.0/test_cases/lt_plural_forced.po010064400017500001750000000001501337277123100174370ustar0000000000000000msgid "Garlic" msgid_plural "Garlics" msgstr[0] "Česnakas" msgstr[1] "Česnakai" msgstr[2] "Česnakų" gettext-0.4.0/tests/lib.rs010064400017500001750000000070371347544157000137170ustar0000000000000000use encoding::label::encoding_from_whatwg_label; use gettext::{Catalog, ParseOptions}; use std::fs::File; #[test] fn test_integration() { let f = File::open("test_cases/integration.mo").unwrap(); let catalog = Catalog::parse(f).unwrap(); assert_eq!(catalog.gettext("non-existent"), "non-existent"); assert_eq!(catalog.gettext("existent"), "egzistuojantis"); assert_eq!( catalog.ngettext("a bad string", "bad strings", 1), "a bad string" ); assert_eq!( catalog.ngettext("a bad string", "bad strings", 2), "bad strings" ); assert_eq!( catalog.ngettext("a good string", "good strings", 1), "gera eilute" ); assert_eq!( catalog.ngettext("a good string", "good strings", 2), "geros eilutes" ); assert_eq!(catalog.pgettext("ctxt", "non-existent"), "non-existent"); assert_eq!( catalog.pgettext("ctxt", "existent"), "egzistuojantis kontekste" ); assert_eq!( catalog.npgettext("ctxt", "a bad string", "bad strings", 1), "a bad string" ); assert_eq!( catalog.npgettext("ctxt", "a bad string", "bad strings", 2), "bad strings" ); assert_eq!( catalog.npgettext("ctxt", "a good string", "good strings", 1), "gera eilute kontekste" ); assert_eq!( catalog.npgettext("ctxt", "a good string", "good strings", 2), "geros eilutes kontekste" ); } #[test] fn test_cp1257() { // cp1257_meta { let reader: &[u8] = include_bytes!("../test_cases/cp1257_meta.mo"); let catalog = ParseOptions::new().parse(reader).unwrap(); assert_eq!(catalog.gettext("Garlic"), "Česnakas"); } // cp1257_forced { let reader: &[u8] = include_bytes!("../test_cases/cp1257_forced.mo"); for enc_name in &["cp1257", "windows-1257", "x-cp1257"] { let encoding = encoding_from_whatwg_label(enc_name).unwrap(); let catalog = ParseOptions::new() .force_encoding(encoding) .parse(reader) .unwrap(); assert_eq!(catalog.gettext("Garlic"), "Česnakas"); } } } #[test] fn test_lt_plural() { 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 } } // lt_plural_forced { let reader: &[u8] = include_bytes!("../test_cases/lt_plural_forced.mo"); let cat = ParseOptions::new() .force_plural(lithuanian_plural) .parse(reader) .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("Garlic", "Garlics", 21), "Česnakas"); } } #[test] fn test_complex_plural() { let reader: &[u8] = include_bytes!("../test_cases/complex_plural.mo"); let cat = ParseOptions::new().parse(reader).unwrap(); 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"); } } gettext-0.4.0/.cargo_vcs_info.json0000644000000001120000000000000125520ustar00{ "git": { "sha1": "396a80fe89b3c72c83699e9a482c914d3491f39a" } }