Set up message generation using fluent-ergonomics

This commit is contained in:
Savanni D'Gerinel 2024-03-05 08:50:54 -05:00
parent 9699c63e50
commit 35c034b04b
7 changed files with 147 additions and 53 deletions

2
Cargo.lock generated
View File

@ -2718,10 +2718,12 @@ dependencies = [
"chrono-tz",
"fixed_decimal",
"fluent",
"fluent-ergonomics",
"icu",
"icu_locid",
"icu_provider",
"sys-locale",
"unic-langid",
]
[[package]]

View File

@ -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
}

View File

@ -0,0 +1,5 @@
welcome = Bonvenon
games-in-database = {count ->
[one] Estas unu ludon en la datumbazo.
[other] Estas {count} ludojn en la datumbazo.
}

View File

@ -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),

View File

@ -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 = "*" }

View File

@ -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);
}
*/
}

View File

@ -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
}