Compare commits
No commits in common. "137a88ad8e40976d93768cc2defde5acec0d6965" and "822e88a8ce494f9a2e290d5ef7c52c57136330ac" have entirely different histories.
137a88ad8e
...
822e88a8ce
|
@ -658,15 +658,6 @@ version = "0.9.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "convert_case"
|
|
||||||
version = "0.6.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
|
|
||||||
dependencies = [
|
|
||||||
"unicode-segmentation",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cookie"
|
name = "cookie"
|
||||||
version = "0.17.0"
|
version = "0.17.0"
|
||||||
|
@ -2710,13 +2701,6 @@ dependencies = [
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "kifu-l10n"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"messages-codegen",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kv-log-macro"
|
name = "kv-log-macro"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
|
@ -2734,12 +2718,10 @@ dependencies = [
|
||||||
"chrono-tz",
|
"chrono-tz",
|
||||||
"fixed_decimal",
|
"fixed_decimal",
|
||||||
"fluent",
|
"fluent",
|
||||||
"fluent-ergonomics",
|
|
||||||
"icu",
|
"icu",
|
||||||
"icu_locid",
|
"icu_locid",
|
||||||
"icu_provider",
|
"icu_provider",
|
||||||
"sys-locale",
|
"sys-locale",
|
||||||
"unic-langid",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -2917,17 +2899,6 @@ dependencies = [
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "messages-codegen"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"convert_case",
|
|
||||||
"quote",
|
|
||||||
"serde 1.0.193",
|
|
||||||
"serde_yaml",
|
|
||||||
"syn 2.0.48",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mime"
|
name = "mime"
|
||||||
version = "0.2.6"
|
version = "0.2.6"
|
||||||
|
@ -4183,19 +4154,6 @@ dependencies = [
|
||||||
"serde 1.0.193",
|
"serde 1.0.193",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serde_yaml"
|
|
||||||
version = "0.9.29"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a15e0ef66bf939a7c890a0bf6d5a733c70202225f9888a89ed5c62298b019129"
|
|
||||||
dependencies = [
|
|
||||||
"indexmap",
|
|
||||||
"itoa",
|
|
||||||
"ryu",
|
|
||||||
"serde 1.0.193",
|
|
||||||
"unsafe-libyaml",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sgf"
|
name = "sgf"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
@ -5123,12 +5081,6 @@ dependencies = [
|
||||||
"traitobject",
|
"traitobject",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unsafe-libyaml"
|
|
||||||
version = "0.2.10"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ab4c90930b95a82d00dc9e9ac071b4991924390d46cbd0dfe566148667605e4b"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "url"
|
name = "url"
|
||||||
version = "1.7.2"
|
version = "1.7.2"
|
||||||
|
@ -5610,10 +5562,6 @@ version = "0.5.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dad7bb64b8ef9c0aa27b6da38b452b0ee9fd82beaf276a87dd796fb55cbae14e"
|
checksum = "dad7bb64b8ef9c0aa27b6da38b452b0ee9fd82beaf276a87dd796fb55cbae14e"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "xdg-test"
|
|
||||||
version = "0.1.0"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yoke"
|
name = "yoke"
|
||||||
version = "0.7.3"
|
version = "0.7.3"
|
||||||
|
|
|
@ -20,10 +20,8 @@ members = [
|
||||||
"ifc",
|
"ifc",
|
||||||
"kifu/core",
|
"kifu/core",
|
||||||
"kifu/gtk",
|
"kifu/gtk",
|
||||||
"kifu/l10n",
|
|
||||||
"l10n",
|
"l10n",
|
||||||
"memorycache",
|
"memorycache",
|
||||||
"messages-codegen",
|
|
||||||
"nom-training",
|
"nom-training",
|
||||||
"result-extended",
|
"result-extended",
|
||||||
"screenplay",
|
"screenplay",
|
||||||
|
@ -31,5 +29,4 @@ members = [
|
||||||
"timezone-testing",
|
"timezone-testing",
|
||||||
"tree",
|
"tree",
|
||||||
"visions/server",
|
"visions/server",
|
||||||
"xdg-test",
|
|
||||||
]
|
]
|
||||||
|
|
|
@ -71,7 +71,6 @@
|
||||||
dashboard = attrs: { nativeBuildInputs = gtkNativeInputs; };
|
dashboard = attrs: { nativeBuildInputs = gtkNativeInputs; };
|
||||||
fitnesstrax = import ./fitnesstrax/app/override.nix { gtkNativeInputs = gtkNativeInputs; };
|
fitnesstrax = import ./fitnesstrax/app/override.nix { gtkNativeInputs = gtkNativeInputs; };
|
||||||
kifu-gtk = import ./kifu/gtk/override.nix { gtkNativeInputs = gtkNativeInputs; };
|
kifu-gtk = import ./kifu/gtk/override.nix { gtkNativeInputs = gtkNativeInputs; };
|
||||||
xdg-test = import ./xdg-test/override.nix { wrapGAppsHook4 = pkgs.wrapGAppsHook4; };
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -86,7 +85,6 @@
|
||||||
file-service = cargo_nix.workspaceMembers.file-service.build;
|
file-service = cargo_nix.workspaceMembers.file-service.build;
|
||||||
fitnesstrax = cargo_nix.workspaceMembers.fitnesstrax.build;
|
fitnesstrax = cargo_nix.workspaceMembers.fitnesstrax.build;
|
||||||
kifu-gtk = cargo_nix.workspaceMembers.kifu-gtk.build;
|
kifu-gtk = cargo_nix.workspaceMembers.kifu-gtk.build;
|
||||||
xdg-test = cargo_nix.workspaceMembers.xdg-test.build;
|
|
||||||
|
|
||||||
all = pkgs.symlinkJoin {
|
all = pkgs.symlinkJoin {
|
||||||
name = "all";
|
name = "all";
|
||||||
|
@ -96,7 +94,6 @@
|
||||||
file-service
|
file-service
|
||||||
fitnesstrax
|
fitnesstrax
|
||||||
kifu-gtk
|
kifu-gtk
|
||||||
xdg-test
|
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
welcome = Welcome
|
|
||||||
games-in-database = {count ->
|
|
||||||
[one] There is one game in the database
|
|
||||||
[other] There are {count} games in the database
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
welcome = Bonvenon
|
|
||||||
games-in-database = {count ->
|
|
||||||
[one] Estas unu ludon en la datumbazo.
|
|
||||||
[other] Estas {count} ludojn en la datumbazo.
|
|
||||||
}
|
|
|
@ -1,5 +1,21 @@
|
||||||
use l10n::{FluentArgs, FluentValue, Message};
|
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 {
|
enum Phrase {
|
||||||
Welcome,
|
Welcome,
|
||||||
GamesInDatabase(i32),
|
GamesInDatabase(i32),
|
||||||
|
|
|
@ -10,10 +10,7 @@ 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 = { version = "0.16" }
|
fluent = { version = "0.16" }
|
||||||
fluent-ergonomics = { path = "../fluent-ergonomics" }
|
|
||||||
icu = { version = "1" }
|
icu = { version = "1" }
|
||||||
icu_locid = { version = "1" }
|
icu_locid = { version = "1" }
|
||||||
icu_provider = { version = "1" }
|
icu_provider = { version = "1" }
|
||||||
sys-locale = { version = "0.3" }
|
sys-locale = { version = "0.3" }
|
||||||
unic-langid = { version = "*" }
|
|
||||||
|
|
||||||
|
|
158
l10n/src/lib.rs
158
l10n/src/lib.rs
|
@ -1,17 +1,19 @@
|
||||||
|
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 fluent::{FluentBundle, FluentResource};
|
use icu::{
|
||||||
use fluent_ergonomics::FluentErgo;
|
datetime::options::length,
|
||||||
use icu::{datetime::options::length, decimal::FixedDecimalFormatter, locid::Locale};
|
decimal::{FixedDecimalFormatter},
|
||||||
|
locid::Locale,
|
||||||
|
};
|
||||||
use icu_provider::DataLocale;
|
use icu_provider::DataLocale;
|
||||||
use std::{collections::HashMap, ops::Deref, path::Path};
|
|
||||||
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
|
// Re-exports. I'm doing these so that clients of this library don't have to go tracking down
|
||||||
// additional structures
|
// additional structures
|
||||||
|
pub use fluent::{FluentValue, FluentArgs};
|
||||||
pub use fixed_decimal::FloatPrecision;
|
pub use fixed_decimal::FloatPrecision;
|
||||||
pub use fluent::{FluentArgs, FluentValue};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum NonEmptyListError {
|
pub enum NonEmptyListError {
|
||||||
|
@ -20,12 +22,12 @@ pub enum NonEmptyListError {
|
||||||
|
|
||||||
pub struct NonEmptyList<A>(Vec<A>);
|
pub struct NonEmptyList<A>(Vec<A>);
|
||||||
|
|
||||||
impl<A> NonEmptyList<A> {
|
impl <A> NonEmptyList<A> {
|
||||||
fn new(elem: A) -> Self {
|
fn new(elem: A) -> Self {
|
||||||
Self(vec![elem])
|
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>>();
|
let lst = iter.into_iter().collect::<Vec<A>>();
|
||||||
if lst.len() > 0 {
|
if lst.len() > 0 {
|
||||||
Ok(NonEmptyList(lst))
|
Ok(NonEmptyList(lst))
|
||||||
|
@ -37,6 +39,7 @@ impl<A> NonEmptyList<A> {
|
||||||
fn first(&self) -> &A {
|
fn first(&self) -> &A {
|
||||||
&self.0[0]
|
&self.0[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<A> Deref for NonEmptyList<A> {
|
impl<A> Deref for NonEmptyList<A> {
|
||||||
|
@ -47,7 +50,7 @@ impl<A> Deref for NonEmptyList<A> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum L10NError {
|
pub enum L10NError {
|
||||||
UnparsableLocale,
|
UnparsableLocale
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<icu::locid::Error> for L10NError {
|
impl From<icu::locid::Error> for L10NError {
|
||||||
|
@ -65,60 +68,28 @@ 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
|
// 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
|
// 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.
|
// 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 {
|
pub trait Message {
|
||||||
fn msgid(&self) -> &str;
|
fn msgid(&self) -> &str;
|
||||||
fn args(&self) -> Option<FluentArgs>;
|
fn args(&self) -> Option<FluentArgs>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct L10N {
|
pub struct L10N {
|
||||||
messages_root: std::path::PathBuf,
|
|
||||||
messages: FluentErgo,
|
|
||||||
|
|
||||||
locales: NonEmptyList<Locale>,
|
locales: NonEmptyList<Locale>,
|
||||||
zone: chrono_tz::Tz,
|
zone: chrono_tz::Tz,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl L10N {
|
impl Default for L10N {
|
||||||
fn new(messages_root: std::path::PathBuf) -> Self {
|
fn default() -> Self {
|
||||||
let english = "en-US".parse::<Locale>().unwrap();
|
let english = "en-US".parse::<Locale>().unwrap();
|
||||||
let sys_locale = get_locale()
|
let sys_locale = get_locale().and_then(|locale_str| locale_str.parse::<Locale>().ok()).unwrap_or(english);
|
||||||
.and_then(|locale_str| locale_str.parse::<Locale>().ok())
|
|
||||||
.unwrap_or(english.clone());
|
|
||||||
let locales = NonEmptyList::new(sys_locale.clone());
|
let locales = NonEmptyList::new(sys_locale.clone());
|
||||||
let zone = chrono_tz::UTC;
|
let zone = chrono_tz::UTC;
|
||||||
|
Self { locales, zone }
|
||||||
/*
|
|
||||||
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(
|
impl L10N {
|
||||||
&mut self,
|
pub fn load_messages_from_file(&mut self, locale: String, path: &Path) -> Result<(), L10NError>{
|
||||||
locale: String,
|
|
||||||
path: &Path,
|
|
||||||
) -> Result<(), L10NError> {
|
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,10 +107,7 @@ impl L10N {
|
||||||
// changed any time the list of locales gets changed. Also, the system can just run through the
|
// changed any time the list of locales gets changed. Also, the system can just run through the
|
||||||
// entire list of fallbacks.
|
// entire list of fallbacks.
|
||||||
pub fn set_locales(&mut self, locales: NonEmptyList<&str>) -> Result<(), L10NError> {
|
pub fn set_locales(&mut self, locales: NonEmptyList<&str>) -> Result<(), L10NError> {
|
||||||
let locales = locales
|
let locales = locales.iter().map(|locale| Locale::try_from_bytes(locale.as_bytes())).collect::<Result<Vec<Locale>, icu::locid::Error>>()?;
|
||||||
.iter()
|
|
||||||
.map(|locale| Locale::try_from_bytes(locale.as_bytes()))
|
|
||||||
.collect::<Result<Vec<Locale>, icu::locid::Error>>()?;
|
|
||||||
self.locales = NonEmptyList(locales);
|
self.locales = NonEmptyList(locales);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -157,8 +125,8 @@ impl L10N {
|
||||||
// parameters. In an ideal world, neither of these can be incorrect. Messages are all checked
|
// 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
|
// at compile time, as are their parameters. That implies an enumeration, with one element per
|
||||||
// message, and with each element knowing its parameters.
|
// message, and with each element knowing its parameters.
|
||||||
pub fn messages(&self) -> FluentErgo {
|
pub fn format_message(&self) -> String {
|
||||||
self.messages.clone()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn format_date_time_utc(
|
pub fn format_date_time_utc(
|
||||||
|
@ -257,9 +225,8 @@ impl L10N {
|
||||||
pub 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.locales.first().clone().into(),
|
&self.locales.first().clone().into(),
|
||||||
Default::default(),
|
Default::default()
|
||||||
)
|
).expect("locale should be present");
|
||||||
.expect("locale should be present");
|
|
||||||
|
|
||||||
let number = FixedDecimal::try_from_f64(value, precision).unwrap();
|
let number = FixedDecimal::try_from_f64(value, precision).unwrap();
|
||||||
|
|
||||||
|
@ -303,10 +270,9 @@ impl From<DateTime> for icu::calendar::DateTime<icu::calendar::Gregorian> {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use fluent::fluent_args;
|
|
||||||
|
|
||||||
fn ref_l10n() -> L10N {
|
fn ref_l10n() -> L10N {
|
||||||
let mut l10n = L10N::new(std::path::PathBuf::from("./test_files"));
|
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_locales(NonEmptyList::from_iter(vec!["en-US"]).unwrap());
|
l10n.set_locales(NonEmptyList::from_iter(vec!["en-US"]).unwrap());
|
||||||
|
@ -387,79 +353,23 @@ mod tests {
|
||||||
fn it_formats_a_number_according_to_locale() {
|
fn it_formats_a_number_according_to_locale() {
|
||||||
let mut l10n = ref_l10n();
|
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!(
|
assert_eq!(
|
||||||
l10n.format_f64(15000.4, FloatPrecision::Floating),
|
l10n.format_f64(15000.4, FloatPrecision::Floating),
|
||||||
"15,000.4",
|
"15,000.4",
|
||||||
);
|
);
|
||||||
|
|
||||||
l10n.set_locales(NonEmptyList::from_iter(vec!["de-DE", "en-US"]).unwrap());
|
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!(
|
assert_eq!(
|
||||||
l10n.format_f64(15000.4, FloatPrecision::Floating),
|
l10n.format_f64(15000.4, FloatPrecision::Floating),
|
||||||
"15.000,4",
|
"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);
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
welcome = Hello, {$name}
|
|
||||||
games-in-database = {$count ->
|
|
||||||
[one] There is one game in the database
|
|
||||||
*[other] There are {$count} games in the database
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "messages-codegen"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
proc-macro = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
convert_case = { version = "0.6" }
|
|
||||||
quote = { version = "*" }
|
|
||||||
syn = { version = "*" }
|
|
||||||
serde_yaml = { version = "*" }
|
|
||||||
serde = { version = "*", features = [ "derive" ] }
|
|
|
@ -1,87 +0,0 @@
|
||||||
/* Given that fluent doesn't provide an ability to see all of the placeholders within a message,
|
|
||||||
* and that the placeholders may change depending on the values of others, I don't have a way to
|
|
||||||
* ensure that the translation code remains entirely connected to the source messages themselves.
|
|
||||||
*
|
|
||||||
* Given that I can't really enforce it, I can, perhaps, codegen it in a way that at least the two
|
|
||||||
* are right next to one another. A macro that generates the data structure and can write the
|
|
||||||
* source strings file.
|
|
||||||
*
|
|
||||||
* For overall file structure, I want the messages, L10N, and FluentErgo tied together.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use proc_macro::TokenStream;
|
|
||||||
use quote::quote;
|
|
||||||
use syn;
|
|
||||||
|
|
||||||
/* I want to write a macro that reads something like this:
|
|
||||||
* messages! {
|
|
||||||
* Welcome(name: String) => "Hello, ${name}",
|
|
||||||
* GamesInDatabase(count: usize) => "{$count ->
|
|
||||||
* [one] There is one game in the database
|
|
||||||
* *[other] There are ${count} games in the database
|
|
||||||
* }",
|
|
||||||
*
|
|
||||||
* It generates an enumeration with all of the named values (Welcome, GamesInDatabase), it
|
|
||||||
* generates corresponding structures (Welcome{ name: String }, GamesInDatabase{ count: usize }),
|
|
||||||
* and it generates two implementations. One is an implementation that can be used to write all of
|
|
||||||
* the strings after => into a file. The other is an implementation that can be imported into a
|
|
||||||
* program, but which *does not* contain the strings. That would look more like this:
|
|
||||||
*
|
|
||||||
* messages.rs:
|
|
||||||
*
|
|
||||||
* enum Messages {
|
|
||||||
* Welcome(Welcome),
|
|
||||||
* GamesInDatabase(GamesInDatabase),
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* struct Welcome{ name: String }
|
|
||||||
* struct GamesInDatabase{ count: usize }
|
|
||||||
*
|
|
||||||
* message_strings.ftl:
|
|
||||||
*
|
|
||||||
* welcome = "Hello, ${name}",
|
|
||||||
* games-in-database = "{$count ->
|
|
||||||
* [one] There is one game in the database
|
|
||||||
* *[other] There are ${count} games in the database
|
|
||||||
* }",
|
|
||||||
*
|
|
||||||
* messages.rs can be imported into the resulting program, and the names of the strings are thus a
|
|
||||||
* part of the program, but the strings themselves are still data files.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! messages {
|
|
||||||
($($name:ident($($arg:ident: $argtype:ty)*) => $message:literal,)+) => {
|
|
||||||
pub enum Messages {
|
|
||||||
$($name($name)),+
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Messages {
|
|
||||||
fn gen_strings() -> Vec<String> {
|
|
||||||
vec![
|
|
||||||
$(concat!(stringify!($name), " => ", $message).to_string(),)+
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$(pub struct $name {
|
|
||||||
$($arg: $argtype)*
|
|
||||||
})+
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
#[proc_macro_derive(Messages)]
|
|
||||||
pub fn messages_macro(input: TokenStream) -> TokenStream {
|
|
||||||
let syn::DeriveInput { ident, .. } = syn::parse_macro_input! {input};
|
|
||||||
|
|
||||||
let gen = quote! {
|
|
||||||
impl Messages for #ident {
|
|
||||||
fn hello() {
|
|
||||||
println!("Hello from {}", stringify!(#ident));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
gen.into()
|
|
||||||
}
|
|
|
@ -1,135 +0,0 @@
|
||||||
use convert_case::{Case, Casing};
|
|
||||||
use serde::Deserialize;
|
|
||||||
use serde_yaml;
|
|
||||||
use std::{collections::HashMap, fmt, fs::File};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
struct MessageJS {
|
|
||||||
#[serde(default)]
|
|
||||||
parameters: HashMap<String, String>,
|
|
||||||
content: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
||||||
enum Ty {
|
|
||||||
String,
|
|
||||||
Number,
|
|
||||||
Count,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for Ty {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
|
|
||||||
match self {
|
|
||||||
Ty::String => write!(f, "String")?,
|
|
||||||
Ty::Number => write!(f, "i32")?,
|
|
||||||
Ty::Count => write!(f, "usize")?,
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
||||||
struct Parameter {
|
|
||||||
name: String,
|
|
||||||
ty: Ty,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&str> for Parameter {
|
|
||||||
fn from(val: &str) -> Self {
|
|
||||||
Self {
|
|
||||||
name: "Nothing".to_owned(),
|
|
||||||
ty: Ty::String,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<(String, String)> for Parameter {
|
|
||||||
fn from((name, val): (String, String)) -> Self {
|
|
||||||
let ty = match val.as_ref() {
|
|
||||||
"string" => Ty::String,
|
|
||||||
"number" => Ty::Number,
|
|
||||||
"count" => Ty::Count,
|
|
||||||
_ => panic!("invalid value"),
|
|
||||||
};
|
|
||||||
Self { name, ty }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
struct Message {
|
|
||||||
name: String,
|
|
||||||
parameters: Vec<Parameter>,
|
|
||||||
content: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Message {
|
|
||||||
fn from_js(name: String, source: MessageJS) -> Self {
|
|
||||||
Self {
|
|
||||||
name,
|
|
||||||
parameters: source
|
|
||||||
.parameters
|
|
||||||
.into_iter()
|
|
||||||
.map(|param| Parameter::from(param))
|
|
||||||
.collect(),
|
|
||||||
content: source.content,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rust_code(&self) -> String {
|
|
||||||
if self.parameters.is_empty() {
|
|
||||||
format!(r"pub struct {}; ", self.name.to_case(Case::Pascal))
|
|
||||||
} else {
|
|
||||||
let mut struct_strs = vec![];
|
|
||||||
|
|
||||||
struct_strs.push(format!("pub struct {} {{", self.name.to_case(Case::Pascal)));
|
|
||||||
let mut parameters: Vec<String> = self
|
|
||||||
.parameters
|
|
||||||
.iter()
|
|
||||||
.map(|param| format!(" {}: {},", param.name, param.ty))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
struct_strs.append(&mut parameters);
|
|
||||||
struct_strs.push("}".to_owned());
|
|
||||||
struct_strs.push("".to_owned());
|
|
||||||
|
|
||||||
struct_strs.push(format!(
|
|
||||||
"impl Message for {} {{",
|
|
||||||
self.name.to_case(Case::Pascal)
|
|
||||||
));
|
|
||||||
struct_strs.push(format!(" pub fn localize(&self, bundle: &FluentBundle) -> String {{"));
|
|
||||||
struct_strs.push(" let mut args = FluentArgs::new();".to_owned());
|
|
||||||
let mut parameters: Vec<String> = self
|
|
||||||
.parameters
|
|
||||||
.iter()
|
|
||||||
.map(|param| format!(" args.set(\"{}\", self.{})", param.name, param.name))
|
|
||||||
.collect();
|
|
||||||
struct_strs.append(&mut parameters);
|
|
||||||
struct_strs.push("".to_owned());
|
|
||||||
struct_strs.push(format!(" let msg = bundle.get_message(\"{}\").unwrap();", self.name));
|
|
||||||
struct_strs.push(format!(" let mut errors = vec![]"));
|
|
||||||
struct_strs.push(format!(" msg.format_pattern(&msg.value().unwrap(), args, &mut errors)"));
|
|
||||||
struct_strs.push(" }".to_owned());
|
|
||||||
struct_strs.push("}".to_owned());
|
|
||||||
|
|
||||||
struct_strs.join("\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn content(&self) -> String {
|
|
||||||
format!("{} = {}", self.name, self.content.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let messages: HashMap<String, MessageJS> =
|
|
||||||
serde_yaml::from_reader(File::open("test-data/messages.yaml").unwrap()).unwrap();
|
|
||||||
|
|
||||||
let messages = messages
|
|
||||||
.into_iter()
|
|
||||||
.map(|(name, msg)| Message::from_js(name, msg));
|
|
||||||
for message in messages {
|
|
||||||
println!();
|
|
||||||
println!("{}", message.rust_code());
|
|
||||||
// println!("{}", message.content());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
welcome:
|
|
||||||
content: "Welcome to Kifu"
|
|
||||||
hello:
|
|
||||||
parameters:
|
|
||||||
name: string
|
|
||||||
content: "Hello, ${name}"
|
|
||||||
games-in-database:
|
|
||||||
parameters:
|
|
||||||
count: count
|
|
||||||
content: |
|
|
||||||
{$count ->
|
|
||||||
[one] There is one game in the database
|
|
||||||
*[other] There are ${count} games in the database
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "xdg-test"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[dependencies]
|
|
|
@ -1,7 +0,0 @@
|
||||||
{ wrapGAppsHook4 }:
|
|
||||||
attrs:
|
|
||||||
let
|
|
||||||
gsettingsDir = "${attrs.crateName}-${attrs.version}";
|
|
||||||
in {
|
|
||||||
nativeBuildInputs = [ wrapGAppsHook4 ];
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
use std::env;
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let data_dirs = env::var("XDG_DATA_DIRS");
|
|
||||||
|
|
||||||
println!("{:?}", data_dirs);
|
|
||||||
}
|
|
Loading…
Reference in New Issue