Speculative work on adding Message support to localization

This commit is contained in:
Savanni D'Gerinel 2024-03-01 10:07:44 -05:00
parent a9d29e6518
commit 822e88a8ce
6 changed files with 158 additions and 29 deletions

3
Cargo.lock generated
View File

@ -2694,6 +2694,7 @@ dependencies = [
"gtk4",
"image 0.24.7",
"kifu-core",
"l10n",
"libadwaita",
"pango",
"sgf",
@ -2716,7 +2717,7 @@ dependencies = [
"chrono",
"chrono-tz",
"fixed_decimal",
"fluent-ergonomics",
"fluent",
"icu",
"icu_locid",
"icu_provider",

View File

@ -18,6 +18,7 @@ glib = { version = "0.18" }
gtk = { version = "0.7", package = "gtk4", features = [ "v4_8" ] }
image = { version = "0.24" }
kifu-core = { path = "../core" }
l10n = { path = "../../l10n" }
pango = { version = "*" }
sgf = { path = "../../sgf" }
tokio = { version = "1.26", features = [ "full" ] }

View File

@ -1,3 +1,5 @@
mod messages;
pub mod ui;
mod view_models;

42
kifu/gtk/src/messages.rs Normal file
View File

@ -0,0 +1,42 @@
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),
}
impl Message for Phrase {
fn msgid(&self) -> &str {
match self {
Phrase::Welcome => "welcome",
Phrase::GamesInDatabase(_) => "games-in-database",
}
}
fn args(&self) -> Option<FluentArgs> {
match self {
Phrase::Welcome => None,
Phrase::GamesInDatabase(val) => {
let mut args = FluentArgs::new();
args.set("count", FluentValue::from(val));
Some(args)
}
}
}
}

View File

@ -9,7 +9,7 @@ edition = "2021"
chrono = { version = "0.4" }
chrono-tz = { version = "0.8" }
fixed_decimal = { version = "0.5.5", features = [ "ryu" ] }
fluent-ergonomics = { path = "../fluent-ergonomics" }
fluent = { version = "0.16" }
icu = { version = "1" }
icu_locid = { version = "1" }
icu_provider = { version = "1" }

View File

@ -1,40 +1,118 @@
use std::ops::Deref;
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 icu_locid::{Locale};
use icu_provider::DataLocale;
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;
#[derive(Clone, Debug)]
#[derive(Debug)]
pub enum NonEmptyListError {
BuildFromEmptyContainer,
}
pub struct NonEmptyList<A>(Vec<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> {
let lst = iter.into_iter().collect::<Vec<A>>();
if lst.len() > 0 {
Ok(NonEmptyList(lst))
} else {
Err(NonEmptyListError::BuildFromEmptyContainer)
}
}
fn first(&self) -> &A {
&self.0[0]
}
}
impl<A> Deref for NonEmptyList<A> {
type Target = Vec<A>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
pub enum L10NError {
UnparsableLocale
}
impl From<icu::locid::Error> for L10NError {
fn from(_: icu::locid::Error) -> L10NError {
L10NError::UnparsableLocale
}
}
// Potential Message structure.
//
// Let's assume the application has an enumeration that implements Message. For each element of the
// enumeration, there should be some boilerplate code that returns the message ID and the arguments
// as a FluentArgs.
//
// 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.
pub trait Message {
fn msgid(&self) -> &str;
fn args(&self) -> Option<FluentArgs>;
}
pub struct L10N {
locale: Locale,
locales: NonEmptyList<Locale>,
zone: chrono_tz::Tz,
}
impl Default for L10N {
fn default() -> Self {
let lc = get_locale().unwrap();
let locale = Locale::try_from_bytes(lc.as_bytes()).unwrap();
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 locales = NonEmptyList::new(sys_locale.clone());
let zone = chrono_tz::UTC;
Self { locale, zone }
Self { locales, zone }
}
}
impl L10N {
fn set_locale(&mut self, locale: String) {
let locale = Locale::try_from_bytes(locale.as_bytes()).unwrap();
self.locale = locale;
pub fn load_messages_from_file(&mut self, locale: String, path: &Path) -> Result<(), L10NError>{
unimplemented!()
}
fn set_timezone(&mut self, zone: Tz) {
// Now, whenever the user changes the locales, the list of messages has to change. How do we
// automatically set up the messages? Theoretically they all need to be reloaded, and I've
// already split how the messages get loaded from how the locales are specified.
//
// But, FluentErgo does that, too. It already has the concept of being constructed with a list
// of languages and then having each language bundle manually loaded afterwards.
//
// Problem: be able to change the preferred list of locales and automatically have a new
// FluentBundle which has all relevant translations loaded.
//
// One solution is that all bundles get loaded at startup time, and the bundle list gets
// 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>>()?;
self.locales = NonEmptyList(locales);
Ok(())
}
pub fn set_timezone(&mut self, zone: Tz) {
self.zone = zone;
}
@ -42,11 +120,16 @@ impl L10N {
// know yet what form the message should take. Forming an adapter around fluent_ergonomics or
// even around fluent itself. I would want for the message to be statically typed, but then I
// don't know what can be the data type that gets passed in here.
fn format_message(&self) -> String {
//
// Formatting a message requires identifying the message and passing it any relevant
// 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!()
}
fn format_date_time_utc(
pub fn format_date_time_utc(
&self,
time: DateTime,
date_style: length::Date,
@ -55,7 +138,7 @@ impl L10N {
let time: DateTime = time.with_timezone(&chrono_tz::UTC).into();
let options = length::Bag::from_date_time_style(date_style, time_style);
let formatter = icu::datetime::DateTimeFormatter::try_new(
&DataLocale::from(&self.locale),
&DataLocale::from(self.locales.first()),
options.into(),
)
.unwrap();
@ -63,7 +146,7 @@ impl L10N {
formatter.format_to_string(&icu_time.to_any()).unwrap()
}
fn format_date_time_local(
pub fn format_date_time_local(
&self,
time: DateTime,
date_style: length::Date,
@ -72,7 +155,7 @@ impl L10N {
let time: DateTime = time.with_timezone(&self.zone).into();
let options = length::Bag::from_date_time_style(date_style, time_style);
let formatter = icu::datetime::DateTimeFormatter::try_new(
&DataLocale::from(&self.locale),
&DataLocale::from(self.locales.first()),
options.into(),
)
.unwrap();
@ -123,9 +206,9 @@ impl L10N {
}
*/
fn format_date(&self, date: NaiveDate, date_style: length::Date) -> String {
pub fn format_date(&self, date: NaiveDate, date_style: length::Date) -> String {
let formatter = icu::datetime::DateFormatter::try_new_with_length(
&DataLocale::from(&self.locale),
&DataLocale::from(self.locales.first()),
date_style,
)
.unwrap();
@ -139,9 +222,9 @@ impl L10N {
formatter.format_to_string(&icu_date.to_any()).unwrap()
}
fn format_f64(&self, value: f64, precision: FloatPrecision) -> String {
pub fn format_f64(&self, value: f64, precision: FloatPrecision) -> String {
let fdf = FixedDecimalFormatter::try_new(
&self.locale.clone().into(),
&self.locales.first().clone().into(),
Default::default()
).expect("locale should be present");
@ -152,7 +235,7 @@ impl L10N {
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
struct DateTime(chrono::DateTime<Tz>);
pub struct DateTime(chrono::DateTime<Tz>);
impl Deref for DateTime {
type Target = chrono::DateTime<Tz>;
@ -192,7 +275,7 @@ mod tests {
let mut l10n = L10N::default();
// Make sure we know the locale before the test begins. Some systems, such as my own, are
// not actually in English.
l10n.set_locale("en-US".to_owned());
l10n.set_locales(NonEmptyList::from_iter(vec!["en-US"]).unwrap());
l10n.set_timezone(chrono_tz::US::Eastern);
l10n
}
@ -223,7 +306,7 @@ mod tests {
"January 2, 2006, 10:04:05\u{202f}AM"
);
l10n.set_locale("eo-EO".to_owned());
l10n.set_locales(NonEmptyList::from_iter(vec!["eo-EO", "en-US"]).unwrap());
assert_eq!(
l10n.format_date_time_utc(now.clone(), length::Date::Long, length::Time::Medium),
"2006-Januaro-02 10:04:05"
@ -242,7 +325,7 @@ mod tests {
"January 2, 2006, 5:04:05\u{202f}AM"
);
l10n.set_locale("eo-EO".to_owned());
l10n.set_locales(NonEmptyList::from_iter(vec!["eo-EO", "en-US"]).unwrap());
assert_eq!(
l10n.format_date_time_local(now.clone(), length::Date::Long, length::Time::Medium),
"2006-Januaro-02 05:04:05"
@ -259,7 +342,7 @@ mod tests {
"January 2, 2006"
);
l10n.set_locale("eo-EO".to_owned());
l10n.set_locales(NonEmptyList::from_iter(vec!["eo-EO", "en-US"]).unwrap());
assert_eq!(
l10n.format_date(today.clone(), length::Date::Long),
"2006-Januaro-02"
@ -279,7 +362,7 @@ mod tests {
"15,000.4",
);
l10n.set_locale("de-DE".to_owned());
l10n.set_locales(NonEmptyList::from_iter(vec!["de-DE", "en-US"]).unwrap());
assert_eq!(
l10n.format_f64(100.4, FloatPrecision::Floating),
"100,4",