1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2025-08-09 04:22:45 +03:00

i18n: translator structure, to hold translations

This commit is contained in:
Quentin Gliech
2023-09-28 14:46:47 +02:00
parent 396104e8a4
commit 5f8411e88c
16 changed files with 753 additions and 32 deletions

255
Cargo.lock generated
View File

@@ -1414,6 +1414,17 @@ dependencies = [
"winapi",
]
[[package]]
name = "displaydoc"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.33",
]
[[package]]
name = "dotenvy"
version = "0.15.7"
@@ -1629,6 +1640,17 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6"
[[package]]
name = "fixed_decimal"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5287d527037d0f35c8801880361eb38bb9bce194805350052c2a79538388faeb"
dependencies = [
"displaydoc",
"smallvec",
"writeable",
]
[[package]]
name = "flume"
version = "0.10.14"
@@ -2206,6 +2228,121 @@ dependencies = [
"cc",
]
[[package]]
name = "icu_list"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "880479cefad9670d4e07b832435539ceaa78b813bddc11fa504a0fd88871bea8"
dependencies = [
"displaydoc",
"icu_list_data",
"icu_locid_transform",
"icu_provider",
"regex-automata 0.2.0",
"writeable",
]
[[package]]
name = "icu_list_data"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f0874b78501946c343c1cfb07fcae7ceceddba47b906d94af5b2730433de7a0"
[[package]]
name = "icu_locid"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56b72c6de0121c00da9828eb3e2603041d563788289bb15feba7c3331de71b5f"
dependencies = [
"displaydoc",
"litemap",
"tinystr",
"writeable",
"zerovec",
]
[[package]]
name = "icu_locid_transform"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49464337ec1e96a409e34be1d7215bdc20159193df2eb2a89fb94403996092ac"
dependencies = [
"displaydoc",
"icu_locid",
"icu_locid_transform_data",
"icu_provider",
"tinystr",
"zerovec",
]
[[package]]
name = "icu_locid_transform_data"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce5d8151979828a4645be945302e05c903cbb5c4a86a936965f7605bd5142e06"
[[package]]
name = "icu_plurals"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce4d79d7c6240f9f27761ef9ac49d5c770f9c34a70a69c89310a763deb9e24d7"
dependencies = [
"displaydoc",
"fixed_decimal",
"icu_locid",
"icu_locid_transform",
"icu_plurals_data",
"icu_provider",
"zerovec",
]
[[package]]
name = "icu_plurals_data"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "587cb66266fc618b5593ab11a5fa8c44391e10006acf979ae176e6bd4ead6c3f"
[[package]]
name = "icu_provider"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5d3810a06fce5c900f8ace41b72abf8f6308f77c9e7647211aa5f121c0c9f43"
dependencies = [
"displaydoc",
"icu_locid",
"icu_provider_macros",
"stable_deref_trait",
"tinystr",
"writeable",
"yoke",
"zerofrom",
"zerovec",
]
[[package]]
name = "icu_provider_adapters"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bb43949fa871c2c79828575058971af57b0ac5e58c9e4fdb69f1f4a2f3f4e17"
dependencies = [
"icu_locid",
"icu_locid_transform",
"icu_provider",
"tinystr",
"zerovec",
]
[[package]]
name = "icu_provider_macros"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca9be8af0b117ccf1516251daab4c9137c012646a211c2a02d2f568ea3cd0df4"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.33",
]
[[package]]
name = "id-arena"
version = "2.2.1"
@@ -2558,6 +2695,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "litemap"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a1a2647d5b7134127971a6de0d533c49de2159167e7f259c427195f87168a1"
[[package]]
name = "lock_api"
version = "0.4.10"
@@ -2859,12 +3002,20 @@ dependencies = [
name = "mas-i18n"
version = "0.2.0"
dependencies = [
"camino",
"icu_list",
"icu_locid",
"icu_locid_transform",
"icu_plurals",
"icu_provider",
"icu_provider_adapters",
"pad",
"pest",
"pest_derive",
"serde",
"serde_json",
"thiserror",
"writeable",
]
[[package]]
@@ -4356,6 +4507,15 @@ dependencies = [
"regex-syntax 0.6.29",
]
[[package]]
name = "regex-automata"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9368763f5a9b804326f3af749e16f9abf378d227bcdee7634b13d8f17793782"
dependencies = [
"memchr",
]
[[package]]
name = "regex-automata"
version = "0.3.8"
@@ -5428,6 +5588,18 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
[[package]]
name = "synstructure"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "285ba80e733fac80aa4270fbcdf83772a79b80aa35c97075320abfee4a915b06"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.33",
"unicode-xid",
]
[[package]]
name = "target-lexicon"
version = "0.12.11"
@@ -5543,6 +5715,16 @@ dependencies = [
"time-core",
]
[[package]]
name = "tinystr"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b07bb54ef1f8ff27564b08b861144d3b8d40263efe07684f64987f4c0d044e3e"
dependencies = [
"displaydoc",
"zerovec",
]
[[package]]
name = "tinyvec"
version = "1.6.0"
@@ -6763,6 +6945,12 @@ dependencies = [
"url",
]
[[package]]
name = "writeable"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0af0c3d13faebf8dda0b5256fa7096a2d5ccb662f7b9f54a40fe201077ab1c2"
[[package]]
name = "yaml-rust"
version = "0.4.5"
@@ -6778,12 +6966,79 @@ version = "1.0.0-rc.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1367295b8f788d371ce2dbc842c7b709c73ee1364d30351dd300ec2203b12377"
[[package]]
name = "yoke"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61e38c508604d6bbbd292dadb3c02559aa7fff6b654a078a36217cad871636e4"
dependencies = [
"serde",
"stable_deref_trait",
"yoke-derive",
"zerofrom",
]
[[package]]
name = "yoke-derive"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5e19fb6ed40002bab5403ffa37e53e0e56f914a4450c8765f533018db1db35f"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.33",
"synstructure",
]
[[package]]
name = "zerofrom"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "655b0814c5c0b19ade497851070c640773304939a6c0fd5f5fb43da0696d05b7"
dependencies = [
"zerofrom-derive",
]
[[package]]
name = "zerofrom-derive"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6a647510471d372f2e6c2e6b7219e44d8c574d24fdc11c610a61455782f18c3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.33",
"synstructure",
]
[[package]]
name = "zeroize"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9"
[[package]]
name = "zerovec"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1194130c5b155bf8ae50ab16c86ab758cd695cf9ad176d2f870b744cbdbb572e"
dependencies = [
"yoke",
"zerofrom",
"zerovec-derive",
]
[[package]]
name = "zerovec-derive"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acabf549809064225ff8878baedc4ce3732ac3b07e7c7ce6e5c2ccdbc485c324"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.33",
]
[[package]]
name = "zstd"
version = "0.11.2+zstd.1.5.2"

View File

@@ -47,6 +47,8 @@ version = "1.0.48"
# Logging and tracing
[workspace.dependencies.tracing]
version = "0.1.37"
[workspace.dependencies.tracing-subscriber]
version = "0.3.17"
# URL manipulation
[workspace.dependencies.url]

View File

@@ -32,7 +32,7 @@ zeroize = "1.6.0"
tracing.workspace = true
tracing-appender = "0.2.2"
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
tracing-subscriber = { workspace = true, features = ["env-filter"] }
tracing-opentelemetry = "0.21.0"
opentelemetry = { version = "0.20.0", features = ["trace", "metrics", "rt-tokio"] }
opentelemetry-http = { version = "0.9.0", features = ["tokio", "hyper"] }

View File

@@ -81,7 +81,7 @@ oauth2-types = { path = "../oauth2-types" }
[dev-dependencies]
insta = "1.32.0"
tracing-subscriber = "0.3.17"
tracing-subscriber.workspace = true
cookie_store = "0.20.0"
[features]

View File

@@ -8,9 +8,17 @@ homepage.workspace = true
repository.workspace = true
[dependencies]
camino = "1.1.6"
icu_list = { version = "1.3.0", features = ["compiled_data", "std"] }
icu_locid = { version = "1.3.0", features = ["std"] }
icu_locid_transform = { version = "1.3.0", features = ["compiled_data", "std"] }
icu_plurals = { version = "1.3.0", features = ["compiled_data", "std"] }
icu_provider = { version = "1.3.0", features = ["std"] }
icu_provider_adapters = { version = "1.3.0", features = ["std"] }
pad = "0.1.6"
pest = "2.7.3"
pest_derive = "2.7.3"
serde.workspace = true
serde_json.workspace = true
thiserror.workspace = true
thiserror.workspace = true
writeable = "0.5.3"

View File

@@ -17,3 +17,9 @@
pub mod sprintf;
pub mod translations;
mod translator;
pub use self::{
sprintf::{Argument, ArgumentList, Message},
translator::Translator,
};

View File

@@ -24,11 +24,13 @@ pub struct List {
}
impl List {
/// Get an argument by its index.
#[must_use]
pub fn get_by_index(&self, index: usize) -> Option<&Value> {
self.arguments.get(index)
}
/// Get an argument by its name.
#[must_use]
pub fn get_by_name(&self, name: &str) -> Option<&Value> {
self.name_index
@@ -58,6 +60,7 @@ impl<A: Into<Argument>> FromIterator<A> for List {
}
}
/// A single argument value.
pub struct Argument {
name: Option<String>,
value: Value,

View File

@@ -110,7 +110,7 @@ impl std::fmt::Display for TypeSpecifier {
}
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ArgumentReference {
Indexed(usize),
Named(String),
@@ -125,7 +125,7 @@ impl std::fmt::Display for ArgumentReference {
}
}
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PaddingSpecifier {
Zero,
Char(char),
@@ -156,7 +156,7 @@ impl PaddingSpecifier {
}
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Placeholder {
pub type_specifier: TypeSpecifier,
pub requested_argument: Option<ArgumentReference>,
@@ -211,7 +211,7 @@ impl std::fmt::Display for Placeholder {
}
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Message {
parts: Vec<Part>,
}
@@ -253,7 +253,7 @@ impl<'de> Deserialize<'de> for Message {
}
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Part {
Percent,
Text(String),

View File

@@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
#![allow(unused_macros)]
mod argument;
mod formatter;
mod message;
@@ -70,8 +72,12 @@ macro_rules! sprintf {
}};
}
#[allow(unused_imports)]
pub(crate) use arg_list;
#[allow(unused_imports)]
pub(crate) use arg_list_inner;
#[allow(unused_imports)]
pub(crate) use sprintf;
#[derive(Debug, Error)]
#[error(transparent)]

View File

@@ -14,41 +14,34 @@
use std::{collections::BTreeMap, ops::Deref};
use icu_plurals::PluralCategory;
use serde::{Deserialize, Serialize};
use crate::sprintf::Message;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PluralCategory {
Zero,
One,
Two,
Few,
Many,
Other,
}
impl PluralCategory {
fn as_str(self) -> &'static str {
match self {
Self::Zero => "zero",
Self::One => "one",
Self::Two => "two",
Self::Few => "few",
Self::Many => "many",
Self::Other => "other",
}
fn plural_category_as_str(category: PluralCategory) -> &'static str {
match category {
PluralCategory::Zero => "zero",
PluralCategory::One => "one",
PluralCategory::Two => "two",
PluralCategory::Few => "few",
PluralCategory::Many => "many",
PluralCategory::Other => "other",
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
enum TranslationTree {
pub enum TranslationTree {
Message(Message),
Children(BTreeMap<String, TranslationTree>),
}
impl TranslationTree {
/// Get a message from the tree by key.
///
/// Returns `None` if the requested key is not found.
#[must_use]
pub fn message(&self, key: &str) -> Option<&Message> {
let keys = key.split('.');
let node = self.walk_path(keys)?;
@@ -56,6 +49,12 @@ impl TranslationTree {
Some(message)
}
/// Get a pluralized message from the tree by key and plural category.
///
/// If the key doesn't have plural variants, this will return the message
/// itself. Returns the "other" category if the requested category is
/// not found. Returns `None` if the requested key is not found.
#[must_use]
pub fn pluralize(&self, key: &str, category: PluralCategory) -> Option<&Message> {
let keys = key.split('.');
let node = self.walk_path(keys)?;
@@ -65,7 +64,7 @@ impl TranslationTree {
TranslationTree::Children(tree) => tree,
};
if let Some(node) = subtree.get(category.as_str()) {
if let Some(node) = subtree.get(plural_category_as_str(category)) {
let message = node.as_message()?;
Some(message)
} else {

View File

@@ -0,0 +1,423 @@
// Copyright 2023 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{collections::HashMap, fs::File, str::FromStr};
use camino::{Utf8Path, Utf8PathBuf};
use icu_list::{ListError, ListFormatter, ListLength};
use icu_locid::{Locale, ParserError};
use icu_locid_transform::fallback::LocaleFallbacker;
use icu_plurals::{PluralRules, PluralsError};
use icu_provider::{
data_key, fallback::LocaleFallbackConfig, DataError, DataErrorKind, DataKey, DataLocale,
DataRequest, DataRequestMetadata,
};
use icu_provider_adapters::fallback::LocaleFallbackProvider;
use thiserror::Error;
use writeable::Writeable;
use crate::{sprintf::Message, translations::TranslationTree};
/// Fake data key for errors
const DATA_KEY: DataKey = data_key!("mas/translations@1");
/// Error type for loading translations
#[derive(Debug, Error)]
#[error("Failed to load translations")]
pub enum LoadError {
Io(#[from] std::io::Error),
Deserialize(#[from] serde_json::Error),
InvalidLocale(#[from] ParserError),
InvalidFileName(Utf8PathBuf),
}
/// A translator for a set of translations.
#[derive(Debug)]
pub struct Translator {
translations: HashMap<DataLocale, TranslationTree>,
plural_provider: LocaleFallbackProvider<icu_plurals::provider::Baked>,
list_provider: LocaleFallbackProvider<icu_list::provider::Baked>,
fallbacker: LocaleFallbacker,
}
impl Translator {
/// Create a new translator from a set of translations.
#[must_use]
pub fn new(translations: HashMap<DataLocale, TranslationTree>) -> Self {
let fallbacker = LocaleFallbacker::new().static_to_owned();
let plural_provider = LocaleFallbackProvider::new_with_fallbacker(
icu_plurals::provider::Baked,
fallbacker.clone(),
);
let list_provider = LocaleFallbackProvider::new_with_fallbacker(
icu_list::provider::Baked,
fallbacker.clone(),
);
Self {
translations,
plural_provider,
list_provider,
fallbacker,
}
}
/// Load a set of translations from a directory.
///
/// The directory should contain one JSON file per locale, with the locale
/// being the filename without the extension, e.g. `en-US.json`.
///
/// # Parameters
///
/// * `path` - The path to load from.
///
/// # Errors
///
/// Returns an error if the directory cannot be read, or if any of the files
/// cannot be parsed.
pub fn load_from_path(path: &Utf8Path) -> Result<Self, LoadError> {
let mut translations = HashMap::new();
let dir = path.read_dir_utf8()?;
for entry in dir {
let entry = entry?;
let path = entry.into_path();
let Some(name) = path.file_stem() else {
return Err(LoadError::InvalidFileName(path));
};
let locale: Locale = Locale::from_str(name)?;
let mut file = File::open(path)?;
let content = serde_json::from_reader(&mut file)?;
translations.insert(locale.into(), content);
}
Ok(Self::new(translations))
}
/// Get a message from the tree by key, with locale fallback.
///
/// Returns the message and the locale it was found in.
/// If the message is not found, returns `None`.
///
/// # Parameters
///
/// * `locale` - The locale to use.
/// * `key` - The key to look up, which is a dot-separated path.
#[must_use]
pub fn message_with_fallback(
&self,
locale: DataLocale,
key: &str,
) -> Option<(&Message, DataLocale)> {
let mut iter = self
.fallbacker
.for_config(LocaleFallbackConfig::default())
.fallback_for(locale);
loop {
let locale = iter.get();
if let Ok(message) = self.message(locale, key) {
return Some((message, iter.take()));
}
// Stop if we hit the `und` locale
if locale.is_und() {
return None;
}
iter.step();
}
}
/// Get a message from the tree by key.
///
/// # Parameters
///
/// * `locale` - The locale to use.
/// * `key` - The key to look up, which is a dot-separated path.
///
/// # Errors
///
/// Returns an error if the requested locale is not found, or if the
/// requested key is not found.
pub fn message(&self, locale: &DataLocale, key: &str) -> Result<&Message, DataError> {
let request = DataRequest {
locale,
metadata: DataRequestMetadata::default(),
};
let tree = self
.translations
.get(locale)
.ok_or(DataErrorKind::MissingLocale.with_req(DATA_KEY, request))?;
let message = tree
.message(key)
.ok_or(DataErrorKind::MissingDataKey.with_req(DATA_KEY, request))?;
Ok(message)
}
/// Get a plural message from the tree by key, with locale fallback.
///
/// Returns the message and the locale it was found in.
/// If the message is not found, returns `None`.
///
/// # Parameters
///
/// * `locale` - The locale to use.
/// * `key` - The key to look up, which is a dot-separated path.
/// * `count` - The count to use for pluralization.
#[must_use]
pub fn plural_with_fallback(
&self,
locale: DataLocale,
key: &str,
count: usize,
) -> Option<(&Message, DataLocale)> {
let mut iter = self
.fallbacker
.for_config(LocaleFallbackConfig::default())
.fallback_for(locale);
loop {
let locale = iter.get();
if let Ok(message) = self.plural(locale, key, count) {
return Some((message, iter.take()));
}
// Stop if we hit the `und` locale
if locale.is_und() {
return None;
}
iter.step();
}
}
/// Get a plural message from the tree by key.
///
/// # Parameters
///
/// * `locale` - The locale to use.
/// * `key` - The key to look up, which is a dot-separated path.
/// * `count` - The count to use for pluralization.
///
/// # Errors
///
/// Returns an error if the requested locale is not found, or if the
/// requested key is not found.
pub fn plural(
&self,
locale: &DataLocale,
key: &str,
count: usize,
) -> Result<&Message, PluralsError> {
let plurals = PluralRules::try_new_cardinal_unstable(&self.plural_provider, locale)?;
let category = plurals.category_for(count);
let request = DataRequest {
locale,
metadata: DataRequestMetadata::default(),
};
let tree = self
.translations
.get(locale)
.ok_or(DataErrorKind::MissingLocale.with_req(DATA_KEY, request))?;
let message = tree
.pluralize(key, category)
.ok_or(DataErrorKind::MissingDataKey.with_req(DATA_KEY, request))?;
Ok(message)
}
/// Format a list of items with the "and" conjunction.
///
/// # Parameters
///
/// * `locale` - The locale to use.
/// * `items` - The items to format.
///
/// # Errors
///
/// Returns an error if the requested locale is not found.
pub fn and_list<'a, W: Writeable + 'a, I: Iterator<Item = W> + Clone + 'a>(
&'a self,
locale: &DataLocale,
items: I,
) -> Result<String, ListError> {
let formatter = ListFormatter::try_new_and_with_length_unstable(
&self.list_provider,
locale,
ListLength::Wide,
)?;
let list = formatter.format_to_string(items);
Ok(list)
}
/// Format a list of items with the "or" conjunction.
///
/// # Parameters
///
/// * `locale` - The locale to use.
/// * `items` - The items to format.
///
/// # Errors
///
/// Returns an error if the requested locale is not found.
pub fn or_list<'a, W: Writeable + 'a, I: Iterator<Item = W> + Clone + 'a>(
&'a self,
locale: &DataLocale,
items: I,
) -> Result<String, ListError> {
let formatter = ListFormatter::try_new_or_with_length_unstable(
&self.list_provider,
locale,
ListLength::Wide,
)?;
let list = formatter.format_to_string(items);
Ok(list)
}
}
#[cfg(test)]
mod tests {
use camino::Utf8PathBuf;
use icu_locid::locale;
use crate::{sprintf::arg_list, translator::Translator};
fn translator() -> Translator {
let root: Utf8PathBuf = env!("CARGO_MANIFEST_DIR").parse().unwrap();
let test_data = root.join("test_data");
Translator::load_from_path(&test_data).unwrap()
}
#[test]
fn test_message() {
let translator = translator();
let message = translator.message(&locale!("en").into(), "hello").unwrap();
let formatted = message.format(&arg_list!()).unwrap();
assert_eq!(formatted, "Hello!");
let message = translator.message(&locale!("fr").into(), "hello").unwrap();
let formatted = message.format(&arg_list!()).unwrap();
assert_eq!(formatted, "Bonjour !");
let message = translator
.message(&locale!("en-US").into(), "hello")
.unwrap();
let formatted = message.format(&arg_list!()).unwrap();
assert_eq!(formatted, "Hey!");
// Try the fallback chain
let result = translator.message(&locale!("en-US").into(), "goodbye");
assert!(result.is_err());
let (message, locale) = translator
.message_with_fallback(locale!("en-US").into(), "goodbye")
.unwrap();
let formatted = message.format(&arg_list!()).unwrap();
assert_eq!(formatted, "Goodbye!");
assert_eq!(locale, locale!("en").into());
}
#[test]
fn test_plurals() {
let translator = translator();
let message = translator
.plural(&locale!("en").into(), "active_sessions", 1)
.unwrap();
let formatted = message.format(&arg_list!(count = 1)).unwrap();
assert_eq!(formatted, "1 active session.");
let message = translator
.plural(&locale!("en").into(), "active_sessions", 2)
.unwrap();
let formatted = message.format(&arg_list!(count = 2)).unwrap();
assert_eq!(formatted, "2 active sessions.");
// In english, zero is plural
let message = translator
.plural(&locale!("en").into(), "active_sessions", 0)
.unwrap();
let formatted = message.format(&arg_list!(count = 0)).unwrap();
assert_eq!(formatted, "0 active sessions.");
let message = translator
.plural(&locale!("fr").into(), "active_sessions", 1)
.unwrap();
let formatted = message.format(&arg_list!(count = 1)).unwrap();
assert_eq!(formatted, "1 session active.");
let message = translator
.plural(&locale!("fr").into(), "active_sessions", 2)
.unwrap();
let formatted = message.format(&arg_list!(count = 2)).unwrap();
assert_eq!(formatted, "2 sessions actives.");
// In french, zero is singular
let message = translator
.plural(&locale!("fr").into(), "active_sessions", 0)
.unwrap();
let formatted = message.format(&arg_list!(count = 0)).unwrap();
assert_eq!(formatted, "0 session active.");
// Try the fallback chain
let result = translator.plural(&locale!("en-US").into(), "active_sessions", 1);
assert!(result.is_err());
let (message, locale) = translator
.plural_with_fallback(locale!("en-US").into(), "active_sessions", 1)
.unwrap();
let formatted = message.format(&arg_list!(count = 1)).unwrap();
assert_eq!(formatted, "1 active session.");
assert_eq!(locale, locale!("en").into());
}
#[test]
fn test_list() {
let translator = translator();
let list = translator
.and_list(&locale!("en").into(), ["one", "two", "three"].iter())
.unwrap();
assert_eq!(list, "one, two, and three");
let list = translator
.and_list(&locale!("fr").into(), ["un", "deux", "trois"].iter())
.unwrap();
assert_eq!(list, "un, deux et trois");
let list = translator
.or_list(&locale!("en").into(), ["one", "two", "three"].iter())
.unwrap();
assert_eq!(list, "one, two, or three");
let list = translator
.or_list(&locale!("fr").into(), ["un", "deux", "trois"].iter())
.unwrap();
assert_eq!(list, "un, deux ou trois");
}
}

View File

@@ -0,0 +1,3 @@
{
"hello": "Hey!"
}

View File

@@ -0,0 +1,8 @@
{
"hello": "Hello!",
"goodbye": "Goodbye!",
"active_sessions": {
"one": "%(count)d active session.",
"other": "%(count)d active sessions."
}
}

View File

@@ -0,0 +1,8 @@
{
"hello": "Bonjour !",
"goodbye": "Au revoir !",
"active_sessions": {
"one": "%(count)d session active.",
"other": "%(count)d sessions actives."
}
}

View File

@@ -18,4 +18,4 @@ hyper = { version = "0.14.27", features = ["tcp", "client", "http1"] }
serde.workspace = true
tokio = { version = "1.32.0", features = ["macros", "rt-multi-thread", "fs", "io-util"] }
tracing.workspace = true
tracing-subscriber = "0.3.17"
tracing-subscriber.workspace = true

View File

@@ -27,7 +27,7 @@ anyhow.workspace = true
rustls-pemfile = "1.0.3"
tokio = { version = "1.32.0", features = ["net", "rt", "macros", "signal", "time", "rt-multi-thread"] }
tokio-test = "0.4.3"
tracing-subscriber = "0.3.17"
tracing-subscriber.workspace = true
[[example]]
name = "demo"