Initial commit of fluent-ergonomics
This commit is contained in:
parent
fe2faa61e0
commit
cb917e9a73
|
@ -0,0 +1 @@
|
||||||
|
use_nix
|
|
@ -0,0 +1,188 @@
|
||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 3
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "byteorder"
|
||||||
|
version = "1.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fluent"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9ebe7532e1e5146a909de9e019e31835a84b5dee3eeb234561e525844f3cf3bf"
|
||||||
|
dependencies = [
|
||||||
|
"fluent-bundle",
|
||||||
|
"unic-langid",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fluent-bundle"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "27ade33328521266c81cc0924523988f43ccd7359f64689a1b6e818afca3a646"
|
||||||
|
dependencies = [
|
||||||
|
"fluent-langneg",
|
||||||
|
"fluent-syntax",
|
||||||
|
"intl-memoizer",
|
||||||
|
"intl_pluralrules",
|
||||||
|
"rental",
|
||||||
|
"smallvec",
|
||||||
|
"unic-langid",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fluent-ergonomics"
|
||||||
|
version = "0.2.0"
|
||||||
|
dependencies = [
|
||||||
|
"fluent",
|
||||||
|
"fluent-syntax",
|
||||||
|
"unic-langid",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fluent-langneg"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fe5815efd5542e40841cd34ef9003822352b04c67a70c595c6758597c72e1f56"
|
||||||
|
dependencies = [
|
||||||
|
"unic-langid",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fluent-syntax"
|
||||||
|
version = "0.9.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ac0f7e83d14cccbf26e165d8881dcac5891af0d85a88543c09dd72ebd31d91ba"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fxhash"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "intl-memoizer"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9867e2d65d82936ef34217ed0f87b639a94384e93a0676158142c861c705391f"
|
||||||
|
dependencies = [
|
||||||
|
"type-map",
|
||||||
|
"unic-langid",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "intl_pluralrules"
|
||||||
|
version = "6.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d82c14d8eece42c03353e0ce86a4d3f97b1f1cef401e4d962dca6c6214a85002"
|
||||||
|
dependencies = [
|
||||||
|
"tinystr",
|
||||||
|
"unic-langid",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.33"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fb37d2df5df740e582f28f8560cf425f52bb267d872fe58358eadb554909f07a"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-xid",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rental"
|
||||||
|
version = "0.5.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cc89fe2acac36d212474d138aaf939c04a82df5b61d07011571ebce5aef81f2e"
|
||||||
|
dependencies = [
|
||||||
|
"rental-impl",
|
||||||
|
"stable_deref_trait",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rental-impl"
|
||||||
|
version = "0.5.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "475e68978dc5b743f2f40d8e0a8fdc83f1c5e78cbf4b8fa5e74e73beebc340de"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "smallvec"
|
||||||
|
version = "1.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "stable_deref_trait"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "1.0.82"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8daf5dd0bb60cbd4137b1b587d2fc0ae729bc07cf01cd70b36a1ed5ade3b9d59"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-xid",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tinystr"
|
||||||
|
version = "0.3.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29738eedb4388d9ea620eeab9384884fc3f06f586a2eddb56bedc5885126c7c1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "type-map"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9d2741b1474c327d95c1f1e3b0a2c3977c8e128409c572a33af2914e7d636717"
|
||||||
|
dependencies = [
|
||||||
|
"fxhash",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unic-langid"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "24d81136159f779c35b10655f45210c71cd5ca5a45aadfe9840a61c7071735ed"
|
||||||
|
dependencies = [
|
||||||
|
"unic-langid-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unic-langid-impl"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c43c61e94492eb67f20facc7b025778a904de83d953d8fcb60dd9adfd6e2d0ea"
|
||||||
|
dependencies = [
|
||||||
|
"tinystr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-xid"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
|
|
@ -0,0 +1,23 @@
|
||||||
|
[package]
|
||||||
|
name = "fluent-ergonomics"
|
||||||
|
authors = ["Savanni D'Gerinel <savanni@luminescent-dreams.com>"]
|
||||||
|
edition = "2018"
|
||||||
|
version = "0.2.0"
|
||||||
|
description = "An ergonomics wrapper around Fluent-RS"
|
||||||
|
license = "AGPL-3.0-or-later"
|
||||||
|
homepage = "https://github.com/luminescent-dreams/fluent-ergonomics"
|
||||||
|
repository = "https://github.com/luminescent-dreams/fluent-ergonomics"
|
||||||
|
categories = ["internationalization"]
|
||||||
|
|
||||||
|
include = [
|
||||||
|
"**/*.rs",
|
||||||
|
"Cargo.toml",
|
||||||
|
"readme.md",
|
||||||
|
"LICENSE",
|
||||||
|
"CODE_OF_CONDUCT.md",
|
||||||
|
]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
fluent = "0.11"
|
||||||
|
unic-langid = "0.8"
|
||||||
|
fluent-syntax = "^0.9"
|
|
@ -0,0 +1,5 @@
|
||||||
|
# Fluent-Ergonomics
|
||||||
|
|
||||||
|
This is a short-term wrapper around the [Fluent-RS](https://github.com/projectfluent/fluent-rs) library. The Fluent-RS maintainers acknowledge that the core has little in the way of ergonomics, and they have two different libraries that they are developing to provide those. No ergonomics library is mature at this time, and this one follows just my own pattens of use which may not be suitable for all users.
|
||||||
|
|
||||||
|
I do expect to deprecate this eventually.
|
|
@ -0,0 +1,21 @@
|
||||||
|
let
|
||||||
|
rust_overlay = import (builtins.fetchTarball "https://github.com/oxalica/rust-overlay/archive/master.tar.gz");
|
||||||
|
pkgs = import <pkgs-21.05> { overlays = [ rust_overlay ]; };
|
||||||
|
unstable = import <unstable> {};
|
||||||
|
rust = pkgs.rust-bin.stable."1.55.0".default.override {
|
||||||
|
extensions = [ "rust-src" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
in pkgs.mkShell {
|
||||||
|
name = "luminescent-dreams-core";
|
||||||
|
|
||||||
|
buildInputs = [
|
||||||
|
pkgs.openssl
|
||||||
|
rust
|
||||||
|
unstable.rust-analyzer
|
||||||
|
];
|
||||||
|
|
||||||
|
shellHook = ''
|
||||||
|
if [ -e ~/.nixpkgs/shellhook.sh ]; then . ~/.nixpkgs/shellhook.sh; fi
|
||||||
|
'';
|
||||||
|
}
|
|
@ -0,0 +1,368 @@
|
||||||
|
//! Provide a more ergonomic interface to the base Fluent library
|
||||||
|
//!
|
||||||
|
//! The Fluent class makes it easier to load translation bundles with language fallbacks and to go
|
||||||
|
//! through the most common steps of translating a message.
|
||||||
|
//!
|
||||||
|
use fluent::concurrent::FluentBundle;
|
||||||
|
use fluent::{FluentArgs, FluentError, FluentResource};
|
||||||
|
use fluent_syntax::parser::ParserError;
|
||||||
|
use std::collections::hash_map::Entry;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::error;
|
||||||
|
use std::fmt;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io;
|
||||||
|
use std::io::Read;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::string::FromUtf8Error;
|
||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
use unic_langid::LanguageIdentifier;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
/// All files must be UTF-8 encoded.
|
||||||
|
FileEncodingError(FromUtf8Error),
|
||||||
|
/// Fluent encountered an underlying error
|
||||||
|
FluentError(Vec<FluentError>),
|
||||||
|
/// Fluent encountered an underlying error while parsing the translation strings
|
||||||
|
FluentParserError(Vec<ParserError>),
|
||||||
|
/// There was an underlying IO error
|
||||||
|
IOError(io::Error),
|
||||||
|
/// No message could be found matching the specified message ID
|
||||||
|
NoMatchingMessage(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl error::Error for Error {
|
||||||
|
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
|
||||||
|
match self {
|
||||||
|
Error::FileEncodingError(error) => Some(error),
|
||||||
|
Error::NoMatchingMessage(_) => None,
|
||||||
|
Error::FluentParserError(_) => None,
|
||||||
|
Error::FluentError(_) => None,
|
||||||
|
Error::IOError(error) => Some(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Error {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Error::FileEncodingError(error) => {
|
||||||
|
write!(f, "Translation file has an encoding problem: {}", error)
|
||||||
|
}
|
||||||
|
Error::FluentError(errs) => write!(f, "Fluent Error: {:?}", errs),
|
||||||
|
Error::FluentParserError(errs) => write!(f, "Fluent Parser Error: {:?}", errs),
|
||||||
|
Error::IOError(error) => write!(f, "IO Error: {}", error),
|
||||||
|
Error::NoMatchingMessage(id) => write!(f, "No matching message for {}", id),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<(FluentResource, Vec<ParserError>)> for Error {
|
||||||
|
fn from(inp: (FluentResource, Vec<ParserError>)) -> Self {
|
||||||
|
let (_, error) = inp;
|
||||||
|
Error::FluentParserError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Vec<ParserError>> for Error {
|
||||||
|
fn from(error: Vec<ParserError>) -> Self {
|
||||||
|
Error::FluentParserError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Vec<FluentError>> for Error {
|
||||||
|
fn from(error: Vec<FluentError>) -> Self {
|
||||||
|
Error::FluentError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<io::Error> for Error {
|
||||||
|
fn from(error: io::Error) -> Self {
|
||||||
|
Error::IOError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<FromUtf8Error> for Error {
|
||||||
|
fn from(error: FromUtf8Error) -> Self {
|
||||||
|
Error::FileEncodingError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
pub struct FluentErgo {
|
||||||
|
languages: Vec<LanguageIdentifier>,
|
||||||
|
bundles: Arc<RwLock<HashMap<LanguageIdentifier, FluentBundle<FluentResource>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for FluentErgo {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "FluentErgo")
|
||||||
|
//write!(
|
||||||
|
//f,
|
||||||
|
//"FluentErgo {{ language: {:?}, units: {} }}",
|
||||||
|
//self.language, "whatever, for the moment"
|
||||||
|
//)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An Ergonomic class wrapping the Fluent library
|
||||||
|
impl FluentErgo {
|
||||||
|
/// Construct the class with a list of languages. The list must be sorted in the order that
|
||||||
|
/// language packs will be tested. The first language listed will be the first language
|
||||||
|
/// searched for any translation message.
|
||||||
|
///
|
||||||
|
/// Typically, I call this as
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let eo_id = "eo".parse::<unic_langid::LanguageIdentifier>().unwrap();
|
||||||
|
/// let en_id = "en-US".parse::<unic_langid::LanguageIdentifier>().unwrap();
|
||||||
|
///
|
||||||
|
/// let mut fluent = fluent_ergonomics::FluentErgo::new(&[eo_id, en_id]);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// This specifies that I want to first look up messages in the Esperanto list, then fall back
|
||||||
|
/// to the English specfications if no Esperanto specification is present.
|
||||||
|
///
|
||||||
|
/// Note that no language resources are loaded during construction. You must call
|
||||||
|
/// `add_from_text` or `add_from_file` to load language packs.
|
||||||
|
pub fn new(languages: &[LanguageIdentifier]) -> FluentErgo {
|
||||||
|
FluentErgo {
|
||||||
|
languages: Vec::from(languages),
|
||||||
|
bundles: Arc::new(RwLock::new(HashMap::new())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a list of translation strings from a string, which can be a constant hard-coded in the
|
||||||
|
/// application, loaded from a file, loaded from the internet, or wherever you like. `lang`
|
||||||
|
/// specifies which language the translation strings being provided.
|
||||||
|
///
|
||||||
|
/// You should not specify a language that you did not include in the constructor. You can, but
|
||||||
|
/// the translation function will only check those languages specified when this object was
|
||||||
|
/// constructed.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// * `FluentError`
|
||||||
|
/// * `FluentParserError`
|
||||||
|
///
|
||||||
|
pub fn add_from_text(&mut self, lang: LanguageIdentifier, text: String) -> Result<(), Error> {
|
||||||
|
let res = FluentResource::try_new(text)?;
|
||||||
|
let mut bundles = self.bundles.write().unwrap();
|
||||||
|
let entry = bundles.entry(lang.clone());
|
||||||
|
match entry {
|
||||||
|
Entry::Occupied(mut e) => {
|
||||||
|
let bundle = e.get_mut();
|
||||||
|
bundle.add_resource(res).map_err(|err| Error::from(err))
|
||||||
|
}
|
||||||
|
Entry::Vacant(e) => {
|
||||||
|
let mut bundle = FluentBundle::new(&[lang]);
|
||||||
|
bundle.add_resource(res).map_err(|err| Error::from(err))?;
|
||||||
|
e.insert(bundle);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Like `add_from_text`, but this will load the translation strings from a file.
|
||||||
|
///
|
||||||
|
/// Note that this will load the entire file into memory before passing it to Fluent. While I
|
||||||
|
/// think it is unlikely, it is possible that a translation file may be so big as to run the
|
||||||
|
/// computer out of memory.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// * `FluentError`
|
||||||
|
/// * `FluentParserError`
|
||||||
|
/// * `FileEncodingError` -- all files must be encoded in UTF-8. Most files saved from text
|
||||||
|
/// editors already do proper UTF-8 encoding, so this should rarely be a problem.
|
||||||
|
///
|
||||||
|
pub fn add_from_file(&mut self, lang: LanguageIdentifier, path: &Path) -> Result<(), Error> {
|
||||||
|
let mut v = Vec::new();
|
||||||
|
let mut f = File::open(path)?;
|
||||||
|
f.read_to_end(&mut v)?;
|
||||||
|
String::from_utf8(v)
|
||||||
|
.map_err(Error::FileEncodingError)
|
||||||
|
.and_then(|s| self.add_from_text(lang, s))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run a translation.
|
||||||
|
///
|
||||||
|
/// `msgid` is the translation identifier as specified in the translation strings. `args` is a
|
||||||
|
/// set of Fluent arguments to be interpolated into the strings.
|
||||||
|
///
|
||||||
|
/// This function will search language bundles in the order that they were specified in the
|
||||||
|
/// constructor. NoMatchingMessage will be returned only if the message identifier cannot be
|
||||||
|
/// found in any bundle.
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// length-without-label = {$value}
|
||||||
|
/// swimming = Swimming
|
||||||
|
/// units = Units
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// With this set of translation strings, `length-without-label`, `swimming`, and `units` are
|
||||||
|
/// all valid translation identifiers. See the documentation for `FluentBundle.get_message` for
|
||||||
|
/// more information.
|
||||||
|
///
|
||||||
|
/// A typical call with arguments would look like this:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use fluent::{FluentArgs, FluentValue};
|
||||||
|
///
|
||||||
|
/// let eo_id = "eo".parse::<unic_langid::LanguageIdentifier>().unwrap();
|
||||||
|
/// let en_id = "en-US".parse::<unic_langid::LanguageIdentifier>().unwrap();
|
||||||
|
///
|
||||||
|
/// let mut fluent = fluent_ergonomics::FluentErgo::new(&[eo_id, en_id]);
|
||||||
|
/// let mut args = FluentArgs::new();
|
||||||
|
/// args.insert("value", FluentValue::from("15"));
|
||||||
|
/// let r = fluent.tr("length-without-label", Some(&args));
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// * NoMatchingMessage -- this will be returned if the message identifier cannot be found in
|
||||||
|
/// any language bundle.
|
||||||
|
///
|
||||||
|
pub fn tr(&self, msgid: &str, args: Option<&FluentArgs>) -> Result<String, Error> {
|
||||||
|
let bundles = self.bundles.read().unwrap();
|
||||||
|
let result: Option<String> = self
|
||||||
|
.languages
|
||||||
|
.iter()
|
||||||
|
.map(|lang| {
|
||||||
|
let bundle = bundles.get(lang)?;
|
||||||
|
self.tr_(bundle, msgid, args)
|
||||||
|
})
|
||||||
|
.filter(|v| v.is_some())
|
||||||
|
.map(|v| v.unwrap())
|
||||||
|
.next();
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Some(r) => Ok(r),
|
||||||
|
_ => Err(Error::NoMatchingMessage(String::from(msgid))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tr_(
|
||||||
|
&self,
|
||||||
|
bundle: &FluentBundle<FluentResource>,
|
||||||
|
msgid: &str,
|
||||||
|
args: Option<&FluentArgs>,
|
||||||
|
) -> Option<String> {
|
||||||
|
let mut errors = vec![];
|
||||||
|
let pattern = bundle.get_message(msgid).and_then(|msg| msg.value);
|
||||||
|
let res = match pattern {
|
||||||
|
None => None,
|
||||||
|
Some(p) => {
|
||||||
|
let res = bundle.format_pattern(&p, args, &mut errors);
|
||||||
|
if errors.len() > 0 {
|
||||||
|
println!("Errors in formatting: {:?}", errors)
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(String::from(res))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match res {
|
||||||
|
Some(mut tr_string) => {
|
||||||
|
tr_string.retain(|v| v != '\u{2068}' && v != '\u{2069}');
|
||||||
|
Some(tr_string)
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::FluentErgo;
|
||||||
|
use fluent::{FluentArgs, FluentValue};
|
||||||
|
use unic_langid::LanguageIdentifier;
|
||||||
|
|
||||||
|
const EN_TRANSLATIONS: &'static str = "
|
||||||
|
preferences = Preferences
|
||||||
|
history = History
|
||||||
|
time_display = {$time} during the day
|
||||||
|
nested_display = nesting a time display: {time_display}
|
||||||
|
";
|
||||||
|
|
||||||
|
const EO_TRANSLATIONS: &'static str = "
|
||||||
|
history = Historio
|
||||||
|
";
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn translations() {
|
||||||
|
let en_id = "en-US".parse::<LanguageIdentifier>().unwrap();
|
||||||
|
let mut fluent = FluentErgo::new(&vec![en_id.clone()]);
|
||||||
|
fluent
|
||||||
|
.add_from_text(en_id, String::from(EN_TRANSLATIONS))
|
||||||
|
.expect("text should load");
|
||||||
|
assert_eq!(
|
||||||
|
fluent.tr("preferences", None).unwrap(),
|
||||||
|
String::from("Preferences")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn translation_fallback() {
|
||||||
|
let eo_id = "eo".parse::<LanguageIdentifier>().unwrap();
|
||||||
|
let en_id = "en".parse::<LanguageIdentifier>().unwrap();
|
||||||
|
let mut fluent = FluentErgo::new(&vec![eo_id.clone(), en_id.clone()]);
|
||||||
|
fluent
|
||||||
|
.add_from_text(en_id, String::from(EN_TRANSLATIONS))
|
||||||
|
.expect("text should load");
|
||||||
|
fluent
|
||||||
|
.add_from_text(eo_id, String::from(EO_TRANSLATIONS))
|
||||||
|
.expect("text should load");
|
||||||
|
assert_eq!(
|
||||||
|
fluent.tr("preferences", None).unwrap(),
|
||||||
|
String::from("Preferences")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
fluent.tr("history", None).unwrap(),
|
||||||
|
String::from("Historio")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn placeholder_insertion_should_strip_placeholder_markers() {
|
||||||
|
let en_id = "en".parse::<LanguageIdentifier>().unwrap();
|
||||||
|
let mut fluent = FluentErgo::new(&vec![en_id.clone()]);
|
||||||
|
fluent
|
||||||
|
.add_from_text(en_id, String::from(EN_TRANSLATIONS))
|
||||||
|
.expect("text should load");
|
||||||
|
let mut args = FluentArgs::new();
|
||||||
|
args.insert("time", FluentValue::from(String::from("13:00")));
|
||||||
|
assert_eq!(
|
||||||
|
fluent.tr("time_display", Some(&args)).unwrap(),
|
||||||
|
String::from("13:00 during the day")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn placeholder_insertion_should_strip_nested_placeholder_markers() {
|
||||||
|
let en_id = "en".parse::<LanguageIdentifier>().unwrap();
|
||||||
|
let mut fluent = FluentErgo::new(&vec![en_id.clone()]);
|
||||||
|
fluent
|
||||||
|
.add_from_text(en_id, String::from(EN_TRANSLATIONS))
|
||||||
|
.expect("text should load");
|
||||||
|
let mut args = FluentArgs::new();
|
||||||
|
args.insert("time", FluentValue::from(String::from("13:00")));
|
||||||
|
assert_eq!(
|
||||||
|
fluent.tr("nested_display", Some(&args)).unwrap(),
|
||||||
|
String::from("nesting a time display: 13:00 during the day")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_send() {
|
||||||
|
fn assert_send<T: Send>() {}
|
||||||
|
assert_send::<FluentErgo>();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sync() {
|
||||||
|
fn assert_sync<T: Sync>() {}
|
||||||
|
assert_sync::<FluentErgo>();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue