Speculative work on adding Message support to localization
This commit is contained in:
parent
a9d29e6518
commit
822e88a8ce
|
@ -2694,6 +2694,7 @@ dependencies = [
|
||||||
"gtk4",
|
"gtk4",
|
||||||
"image 0.24.7",
|
"image 0.24.7",
|
||||||
"kifu-core",
|
"kifu-core",
|
||||||
|
"l10n",
|
||||||
"libadwaita",
|
"libadwaita",
|
||||||
"pango",
|
"pango",
|
||||||
"sgf",
|
"sgf",
|
||||||
|
@ -2716,7 +2717,7 @@ dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"chrono-tz",
|
"chrono-tz",
|
||||||
"fixed_decimal",
|
"fixed_decimal",
|
||||||
"fluent-ergonomics",
|
"fluent",
|
||||||
"icu",
|
"icu",
|
||||||
"icu_locid",
|
"icu_locid",
|
||||||
"icu_provider",
|
"icu_provider",
|
||||||
|
|
|
@ -18,6 +18,7 @@ glib = { version = "0.18" }
|
||||||
gtk = { version = "0.7", package = "gtk4", features = [ "v4_8" ] }
|
gtk = { version = "0.7", package = "gtk4", features = [ "v4_8" ] }
|
||||||
image = { version = "0.24" }
|
image = { version = "0.24" }
|
||||||
kifu-core = { path = "../core" }
|
kifu-core = { path = "../core" }
|
||||||
|
l10n = { path = "../../l10n" }
|
||||||
pango = { version = "*" }
|
pango = { version = "*" }
|
||||||
sgf = { path = "../../sgf" }
|
sgf = { path = "../../sgf" }
|
||||||
tokio = { version = "1.26", features = [ "full" ] }
|
tokio = { version = "1.26", features = [ "full" ] }
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
mod messages;
|
||||||
|
|
||||||
pub mod ui;
|
pub mod ui;
|
||||||
|
|
||||||
mod view_models;
|
mod view_models;
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,7 +9,7 @@ edition = "2021"
|
||||||
chrono = { version = "0.4" }
|
chrono = { version = "0.4" }
|
||||||
chrono-tz = { version = "0.8" }
|
chrono-tz = { version = "0.8" }
|
||||||
fixed_decimal = { version = "0.5.5", features = [ "ryu" ] }
|
fixed_decimal = { version = "0.5.5", features = [ "ryu" ] }
|
||||||
fluent-ergonomics = { path = "../fluent-ergonomics" }
|
fluent = { version = "0.16" }
|
||||||
icu = { version = "1" }
|
icu = { version = "1" }
|
||||||
icu_locid = { version = "1" }
|
icu_locid = { version = "1" }
|
||||||
icu_provider = { version = "1" }
|
icu_provider = { version = "1" }
|
||||||
|
|
137
l10n/src/lib.rs
137
l10n/src/lib.rs
|
@ -1,40 +1,118 @@
|
||||||
use std::ops::Deref;
|
use std::{ops::Deref, path::Path};
|
||||||
|
|
||||||
use chrono::{Datelike, NaiveDate, Timelike};
|
use chrono::{Datelike, NaiveDate, Timelike};
|
||||||
use chrono_tz::{Tz};
|
use chrono_tz::{Tz};
|
||||||
use fixed_decimal::{FixedDecimal};
|
use fixed_decimal::{FixedDecimal};
|
||||||
use icu::{
|
use icu::{
|
||||||
datetime::options::length,
|
datetime::options::length,
|
||||||
decimal::{FixedDecimalFormatter},
|
decimal::{FixedDecimalFormatter},
|
||||||
|
locid::Locale,
|
||||||
};
|
};
|
||||||
use icu_locid::{Locale};
|
|
||||||
use icu_provider::DataLocale;
|
use icu_provider::DataLocale;
|
||||||
use sys_locale::get_locale;
|
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 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 {
|
pub struct L10N {
|
||||||
locale: Locale,
|
locales: NonEmptyList<Locale>,
|
||||||
zone: chrono_tz::Tz,
|
zone: chrono_tz::Tz,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for L10N {
|
impl Default for L10N {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let lc = get_locale().unwrap();
|
let english = "en-US".parse::<Locale>().unwrap();
|
||||||
let locale = Locale::try_from_bytes(lc.as_bytes()).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;
|
let zone = chrono_tz::UTC;
|
||||||
Self { locale, zone }
|
Self { locales, zone }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl L10N {
|
impl L10N {
|
||||||
fn set_locale(&mut self, locale: String) {
|
pub fn load_messages_from_file(&mut self, locale: String, path: &Path) -> Result<(), L10NError>{
|
||||||
let locale = Locale::try_from_bytes(locale.as_bytes()).unwrap();
|
unimplemented!()
|
||||||
self.locale = locale;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
self.zone = zone;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,11 +120,16 @@ impl L10N {
|
||||||
// know yet what form the message should take. Forming an adapter around fluent_ergonomics or
|
// 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
|
// 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.
|
// 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!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format_date_time_utc(
|
pub fn format_date_time_utc(
|
||||||
&self,
|
&self,
|
||||||
time: DateTime,
|
time: DateTime,
|
||||||
date_style: length::Date,
|
date_style: length::Date,
|
||||||
|
@ -55,7 +138,7 @@ impl L10N {
|
||||||
let time: DateTime = time.with_timezone(&chrono_tz::UTC).into();
|
let time: DateTime = time.with_timezone(&chrono_tz::UTC).into();
|
||||||
let options = length::Bag::from_date_time_style(date_style, time_style);
|
let options = length::Bag::from_date_time_style(date_style, time_style);
|
||||||
let formatter = icu::datetime::DateTimeFormatter::try_new(
|
let formatter = icu::datetime::DateTimeFormatter::try_new(
|
||||||
&DataLocale::from(&self.locale),
|
&DataLocale::from(self.locales.first()),
|
||||||
options.into(),
|
options.into(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -63,7 +146,7 @@ impl L10N {
|
||||||
formatter.format_to_string(&icu_time.to_any()).unwrap()
|
formatter.format_to_string(&icu_time.to_any()).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format_date_time_local(
|
pub fn format_date_time_local(
|
||||||
&self,
|
&self,
|
||||||
time: DateTime,
|
time: DateTime,
|
||||||
date_style: length::Date,
|
date_style: length::Date,
|
||||||
|
@ -72,7 +155,7 @@ impl L10N {
|
||||||
let time: DateTime = time.with_timezone(&self.zone).into();
|
let time: DateTime = time.with_timezone(&self.zone).into();
|
||||||
let options = length::Bag::from_date_time_style(date_style, time_style);
|
let options = length::Bag::from_date_time_style(date_style, time_style);
|
||||||
let formatter = icu::datetime::DateTimeFormatter::try_new(
|
let formatter = icu::datetime::DateTimeFormatter::try_new(
|
||||||
&DataLocale::from(&self.locale),
|
&DataLocale::from(self.locales.first()),
|
||||||
options.into(),
|
options.into(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.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(
|
let formatter = icu::datetime::DateFormatter::try_new_with_length(
|
||||||
&DataLocale::from(&self.locale),
|
&DataLocale::from(self.locales.first()),
|
||||||
date_style,
|
date_style,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -139,9 +222,9 @@ impl L10N {
|
||||||
formatter.format_to_string(&icu_date.to_any()).unwrap()
|
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(
|
let fdf = FixedDecimalFormatter::try_new(
|
||||||
&self.locale.clone().into(),
|
&self.locales.first().clone().into(),
|
||||||
Default::default()
|
Default::default()
|
||||||
).expect("locale should be present");
|
).expect("locale should be present");
|
||||||
|
|
||||||
|
@ -152,7 +235,7 @@ impl L10N {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
struct DateTime(chrono::DateTime<Tz>);
|
pub struct DateTime(chrono::DateTime<Tz>);
|
||||||
|
|
||||||
impl Deref for DateTime {
|
impl Deref for DateTime {
|
||||||
type Target = chrono::DateTime<Tz>;
|
type Target = chrono::DateTime<Tz>;
|
||||||
|
@ -192,7 +275,7 @@ mod tests {
|
||||||
let mut l10n = L10N::default();
|
let mut l10n = L10N::default();
|
||||||
// Make sure we know the locale before the test begins. Some systems, such as my own, are
|
// Make sure we know the locale before the test begins. Some systems, such as my own, are
|
||||||
// not actually in English.
|
// 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.set_timezone(chrono_tz::US::Eastern);
|
||||||
l10n
|
l10n
|
||||||
}
|
}
|
||||||
|
@ -223,7 +306,7 @@ mod tests {
|
||||||
"January 2, 2006, 10:04:05\u{202f}AM"
|
"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!(
|
assert_eq!(
|
||||||
l10n.format_date_time_utc(now.clone(), length::Date::Long, length::Time::Medium),
|
l10n.format_date_time_utc(now.clone(), length::Date::Long, length::Time::Medium),
|
||||||
"2006-Januaro-02 10:04:05"
|
"2006-Januaro-02 10:04:05"
|
||||||
|
@ -242,7 +325,7 @@ mod tests {
|
||||||
"January 2, 2006, 5:04:05\u{202f}AM"
|
"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!(
|
assert_eq!(
|
||||||
l10n.format_date_time_local(now.clone(), length::Date::Long, length::Time::Medium),
|
l10n.format_date_time_local(now.clone(), length::Date::Long, length::Time::Medium),
|
||||||
"2006-Januaro-02 05:04:05"
|
"2006-Januaro-02 05:04:05"
|
||||||
|
@ -259,7 +342,7 @@ mod tests {
|
||||||
"January 2, 2006"
|
"January 2, 2006"
|
||||||
);
|
);
|
||||||
|
|
||||||
l10n.set_locale("eo-EO".to_owned());
|
l10n.set_locales(NonEmptyList::from_iter(vec!["eo-EO", "en-US"]).unwrap());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
l10n.format_date(today.clone(), length::Date::Long),
|
l10n.format_date(today.clone(), length::Date::Long),
|
||||||
"2006-Januaro-02"
|
"2006-Januaro-02"
|
||||||
|
@ -279,7 +362,7 @@ mod tests {
|
||||||
"15,000.4",
|
"15,000.4",
|
||||||
);
|
);
|
||||||
|
|
||||||
l10n.set_locale("de-DE".to_owned());
|
l10n.set_locales(NonEmptyList::from_iter(vec!["de-DE", "en-US"]).unwrap());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
l10n.format_f64(100.4, FloatPrecision::Floating),
|
l10n.format_f64(100.4, FloatPrecision::Floating),
|
||||||
"100,4",
|
"100,4",
|
||||||
|
|
Loading…
Reference in New Issue