Set up message generation using fluent-ergonomics
This commit is contained in:
parent
9699c63e50
commit
35c034b04b
|
@ -2718,10 +2718,12 @@ dependencies = [
|
|||
"chrono-tz",
|
||||
"fixed_decimal",
|
||||
"fluent",
|
||||
"fluent-ergonomics",
|
||||
"icu",
|
||||
"icu_locid",
|
||||
"icu_provider",
|
||||
"sys-locale",
|
||||
"unic-langid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
welcome = Welcome
|
||||
games-in-database = {count ->
|
||||
[one] There is one game in the database
|
||||
[other] There are {count} games in the database
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
welcome = Bonvenon
|
||||
games-in-database = {count ->
|
||||
[one] Estas unu ludon en la datumbazo.
|
||||
[other] Estas {count} ludojn en la datumbazo.
|
||||
}
|
|
@ -1,21 +1,5 @@
|
|||
use l10n::{FluentArgs, FluentValue, Message};
|
||||
|
||||
const ENGLISH_MESSAGES: &'static str = "
|
||||
welcome = Welcome
|
||||
games-in-database = {count ->
|
||||
[one] There is one game in the database
|
||||
[other] There are {count} games in the database
|
||||
}
|
||||
";
|
||||
|
||||
const ESPERANTO_MESSAGES: &'static str = "
|
||||
welcome = Bonvenon
|
||||
games-in-database = {count ->
|
||||
[one] Estas unu ludon en la datumbazo.
|
||||
[other] Estas {count} ludojn en la datumbazo.
|
||||
}
|
||||
";
|
||||
|
||||
enum Phrase {
|
||||
Welcome,
|
||||
GamesInDatabase(i32),
|
||||
|
|
|
@ -10,7 +10,10 @@ chrono = { version = "0.4" }
|
|||
chrono-tz = { version = "0.8" }
|
||||
fixed_decimal = { version = "0.5.5", features = [ "ryu" ] }
|
||||
fluent = { version = "0.16" }
|
||||
fluent-ergonomics = { path = "../fluent-ergonomics" }
|
||||
icu = { version = "1" }
|
||||
icu_locid = { version = "1" }
|
||||
icu_provider = { version = "1" }
|
||||
sys-locale = { version = "0.3" }
|
||||
unic-langid = { version = "*" }
|
||||
|
||||
|
|
160
l10n/src/lib.rs
160
l10n/src/lib.rs
|
@ -1,19 +1,17 @@
|
|||
use std::{ops::Deref, path::Path};
|
||||
use chrono::{Datelike, NaiveDate, Timelike};
|
||||
use chrono_tz::{Tz};
|
||||
use fixed_decimal::{FixedDecimal};
|
||||
use icu::{
|
||||
datetime::options::length,
|
||||
decimal::{FixedDecimalFormatter},
|
||||
locid::Locale,
|
||||
};
|
||||
use chrono_tz::Tz;
|
||||
use fixed_decimal::FixedDecimal;
|
||||
use fluent::{FluentBundle, FluentResource};
|
||||
use fluent_ergonomics::FluentErgo;
|
||||
use icu::{datetime::options::length, decimal::FixedDecimalFormatter, locid::Locale};
|
||||
use icu_provider::DataLocale;
|
||||
use std::{collections::HashMap, ops::Deref, path::Path};
|
||||
use sys_locale::get_locale;
|
||||
|
||||
// Re-exports. I'm doing these so that clients of this library don't have to go tracking down
|
||||
// additional structures
|
||||
pub use fluent::{FluentValue, FluentArgs};
|
||||
pub use fixed_decimal::FloatPrecision;
|
||||
pub use fluent::{FluentArgs, FluentValue};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum NonEmptyListError {
|
||||
|
@ -22,12 +20,12 @@ pub enum NonEmptyListError {
|
|||
|
||||
pub struct NonEmptyList<A>(Vec<A>);
|
||||
|
||||
impl <A> NonEmptyList<A> {
|
||||
impl<A> NonEmptyList<A> {
|
||||
fn new(elem: A) -> Self {
|
||||
Self(vec![elem])
|
||||
}
|
||||
|
||||
fn from_iter(iter: impl IntoIterator<Item = A>) -> Result<NonEmptyList<A> ,NonEmptyListError> {
|
||||
fn from_iter(iter: impl IntoIterator<Item = A>) -> Result<NonEmptyList<A>, NonEmptyListError> {
|
||||
let lst = iter.into_iter().collect::<Vec<A>>();
|
||||
if lst.len() > 0 {
|
||||
Ok(NonEmptyList(lst))
|
||||
|
@ -39,7 +37,6 @@ impl <A> NonEmptyList<A> {
|
|||
fn first(&self) -> &A {
|
||||
&self.0[0]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl<A> Deref for NonEmptyList<A> {
|
||||
|
@ -50,7 +47,7 @@ impl<A> Deref for NonEmptyList<A> {
|
|||
}
|
||||
|
||||
pub enum L10NError {
|
||||
UnparsableLocale
|
||||
UnparsableLocale,
|
||||
}
|
||||
|
||||
impl From<icu::locid::Error> for L10NError {
|
||||
|
@ -68,28 +65,60 @@ impl From<icu::locid::Error> for L10NError {
|
|||
// Nobody wants to generate all of that code, though I have done so in the past, and manually
|
||||
// generating that code could be useful for illustration. I think I'm going to want to do code
|
||||
// generation from the source strings file, and then compile the enumeration into the code.
|
||||
// However, I have not found a mechanism in Fluent to identify all of the placeholders within a
|
||||
// message, so I'm not even sure that I can automate this code generation.
|
||||
pub trait Message {
|
||||
fn msgid(&self) -> &str;
|
||||
fn args(&self) -> Option<FluentArgs>;
|
||||
}
|
||||
|
||||
pub struct L10N {
|
||||
messages_root: std::path::PathBuf,
|
||||
messages: FluentErgo,
|
||||
|
||||
locales: NonEmptyList<Locale>,
|
||||
zone: chrono_tz::Tz,
|
||||
}
|
||||
|
||||
impl Default for L10N {
|
||||
fn default() -> Self {
|
||||
impl L10N {
|
||||
fn new(messages_root: std::path::PathBuf) -> Self {
|
||||
let english = "en-US".parse::<Locale>().unwrap();
|
||||
let sys_locale = get_locale().and_then(|locale_str| locale_str.parse::<Locale>().ok()).unwrap_or(english);
|
||||
let sys_locale = get_locale()
|
||||
.and_then(|locale_str| locale_str.parse::<Locale>().ok())
|
||||
.unwrap_or(english.clone());
|
||||
let locales = NonEmptyList::new(sys_locale.clone());
|
||||
let zone = chrono_tz::UTC;
|
||||
Self { locales, zone }
|
||||
}
|
||||
}
|
||||
|
||||
impl L10N {
|
||||
pub fn load_messages_from_file(&mut self, locale: String, path: &Path) -> Result<(), L10NError>{
|
||||
/*
|
||||
let mut source_message_path = messages_root.clone();
|
||||
source_message_path.push("en-US.ftl");
|
||||
let english_phrases = FluentResource::try_new
|
||||
*/
|
||||
|
||||
let messages = {
|
||||
let mut english_messages = messages_root.clone();
|
||||
english_messages.push("en-US.ftl");
|
||||
|
||||
let langid: unic_langid::LanguageIdentifier = english.to_string().parse().unwrap();
|
||||
let mut messages = FluentErgo::new(&[langid.clone()]);
|
||||
let _ = messages.add_from_file(langid, &english_messages);
|
||||
|
||||
messages
|
||||
};
|
||||
|
||||
Self {
|
||||
messages_root,
|
||||
messages,
|
||||
locales,
|
||||
zone,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_messages_from_file(
|
||||
&mut self,
|
||||
locale: String,
|
||||
path: &Path,
|
||||
) -> Result<(), L10NError> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
|
@ -107,7 +136,10 @@ impl L10N {
|
|||
// changed any time the list of locales gets changed. Also, the system can just run through the
|
||||
// entire list of fallbacks.
|
||||
pub fn set_locales(&mut self, locales: NonEmptyList<&str>) -> Result<(), L10NError> {
|
||||
let locales = locales.iter().map(|locale| Locale::try_from_bytes(locale.as_bytes())).collect::<Result<Vec<Locale>, icu::locid::Error>>()?;
|
||||
let locales = locales
|
||||
.iter()
|
||||
.map(|locale| Locale::try_from_bytes(locale.as_bytes()))
|
||||
.collect::<Result<Vec<Locale>, icu::locid::Error>>()?;
|
||||
self.locales = NonEmptyList(locales);
|
||||
Ok(())
|
||||
}
|
||||
|
@ -125,8 +157,8 @@ impl L10N {
|
|||
// parameters. In an ideal world, neither of these can be incorrect. Messages are all checked
|
||||
// at compile time, as are their parameters. That implies an enumeration, with one element per
|
||||
// message, and with each element knowing its parameters.
|
||||
pub fn format_message(&self) -> String {
|
||||
unimplemented!()
|
||||
pub fn messages(&self) -> FluentErgo {
|
||||
self.messages.clone()
|
||||
}
|
||||
|
||||
pub fn format_date_time_utc(
|
||||
|
@ -225,8 +257,9 @@ impl L10N {
|
|||
pub fn format_f64(&self, value: f64, precision: FloatPrecision) -> String {
|
||||
let fdf = FixedDecimalFormatter::try_new(
|
||||
&self.locales.first().clone().into(),
|
||||
Default::default()
|
||||
).expect("locale should be present");
|
||||
Default::default(),
|
||||
)
|
||||
.expect("locale should be present");
|
||||
|
||||
let number = FixedDecimal::try_from_f64(value, precision).unwrap();
|
||||
|
||||
|
@ -270,9 +303,10 @@ impl From<DateTime> for icu::calendar::DateTime<icu::calendar::Gregorian> {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use fluent::fluent_args;
|
||||
|
||||
fn ref_l10n() -> L10N {
|
||||
let mut l10n = L10N::default();
|
||||
let mut l10n = L10N::new(std::path::PathBuf::from("./test_files"));
|
||||
// Make sure we know the locale before the test begins. Some systems, such as my own, are
|
||||
// not actually in English.
|
||||
l10n.set_locales(NonEmptyList::from_iter(vec!["en-US"]).unwrap());
|
||||
|
@ -353,23 +387,79 @@ mod tests {
|
|||
fn it_formats_a_number_according_to_locale() {
|
||||
let mut l10n = ref_l10n();
|
||||
|
||||
assert_eq!(
|
||||
l10n.format_f64(100.4, FloatPrecision::Floating),
|
||||
"100.4",
|
||||
);
|
||||
assert_eq!(l10n.format_f64(100.4, FloatPrecision::Floating), "100.4",);
|
||||
assert_eq!(
|
||||
l10n.format_f64(15000.4, FloatPrecision::Floating),
|
||||
"15,000.4",
|
||||
);
|
||||
|
||||
l10n.set_locales(NonEmptyList::from_iter(vec!["de-DE", "en-US"]).unwrap());
|
||||
assert_eq!(
|
||||
l10n.format_f64(100.4, FloatPrecision::Floating),
|
||||
"100,4",
|
||||
);
|
||||
assert_eq!(l10n.format_f64(100.4, FloatPrecision::Floating), "100,4",);
|
||||
assert_eq!(
|
||||
l10n.format_f64(15000.4, FloatPrecision::Floating),
|
||||
"15.000,4",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_can_load_message_files() {
|
||||
let mut l10n = ref_l10n();
|
||||
let messages = l10n.messages();
|
||||
|
||||
let args = fluent_args![
|
||||
"name" => "Savanni"
|
||||
];
|
||||
assert_eq!(
|
||||
messages.tr("welcome", Some(&args)).unwrap(),
|
||||
"Hello, Savanni"
|
||||
);
|
||||
|
||||
let args = fluent_args![
|
||||
"count" => 1
|
||||
];
|
||||
assert_eq!(
|
||||
messages.tr("games-in-database", Some(&args)).unwrap(),
|
||||
"There is one game in the database"
|
||||
);
|
||||
|
||||
let args = fluent_args![
|
||||
"count" => 2
|
||||
];
|
||||
assert_eq!(
|
||||
messages.tr("games-in-database", Some(&args)).unwrap(),
|
||||
"There are 2 games in the database"
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
#[test]
|
||||
fn it_can_change_languages_on_locale_change() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn phrases_can_be_translated() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn phrases_can_fall_back() {
|
||||
}
|
||||
*/
|
||||
|
||||
/* Not really a unit test, more of a test to see what I could introspect within a fluent
|
||||
* message. I was hoping that attributes would give me placeholder names, but that doesn't seem
|
||||
* to be the case.
|
||||
#[test]
|
||||
fn messages() {
|
||||
let langid_en = "en-US".parse().expect("Parsing failed.");
|
||||
let resource = FluentResource::try_new(MESSAGES.to_owned()).unwrap();
|
||||
let mut bundle = FluentBundle::new(vec![langid_en]);
|
||||
bundle.add_resource(&resource).unwrap();
|
||||
|
||||
let msg = bundle.get_message("welcome").expect("message should exist");
|
||||
for attr in msg.attributes() {
|
||||
println!("attr: {:?}", attr);
|
||||
}
|
||||
assert!(false);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
welcome = Hello, {$name}
|
||||
games-in-database = {$count ->
|
||||
[one] There is one game in the database
|
||||
*[other] There are {$count} games in the database
|
||||
}
|
Loading…
Reference in New Issue