Speculative work on adding Message support to localization
This commit is contained in:
parent
a9d29e6518
commit
822e88a8ce
|
@ -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",
|
||||
|
|
|
@ -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" ] }
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
mod messages;
|
||||
|
||||
pub mod ui;
|
||||
|
||||
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-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" }
|
||||
|
|
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_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",
|
||||
|
|
Loading…
Reference in New Issue