Set up code and message generation for the Kifu localization strings

This commit is contained in:
Savanni D'Gerinel 2024-03-11 08:44:43 -04:00
parent 8ceca454b1
commit 15b1d4bfd6
11 changed files with 133 additions and 74 deletions

2
Cargo.lock generated
View File

@ -2697,6 +2697,8 @@ dependencies = [
"async-channel 2.1.1", "async-channel 2.1.1",
"async-std", "async-std",
"cairo-rs", "cairo-rs",
"fluent",
"fluent-ergonomics",
"gio", "gio",
"glib", "glib",
"glib-build-tools 0.17.10", "glib-build-tools 0.17.10",

View File

@ -13,6 +13,8 @@ adw = { version = "0.5", package = "libadwaita", features = [ "v1_2"
async-channel = { version = "2" } async-channel = { version = "2" }
async-std = { version = "1" } async-std = { version = "1" }
cairo-rs = { version = "0.18" } cairo-rs = { version = "0.18" }
fluent = { version = "0.16" }
fluent-ergonomics = { path = "../../fluent-ergonomics" }
gio = { version = "0.18" } gio = { version = "0.18" }
glib = { version = "0.18" } glib = { version = "0.18" }
gtk = { version = "0.7", package = "gtk4", features = [ "v4_8" ] } gtk = { version = "0.7", package = "gtk4", features = [ "v4_8" ] }
@ -25,13 +27,3 @@ tokio = { version = "1.26", features = [ "full" ] }
[build-dependencies] [build-dependencies]
glib-build-tools = "0.17" glib-build-tools = "0.17"
# [[bin]]
# name = "kifu-gtk"
# path = "src/main.rs"
# [[bin]]
# name = "screenplay"
# path = "src/bin/screenplay.rs"
# required-features = [ "screenplay" ]

View File

@ -4,11 +4,4 @@ fn main() {
"gresources.xml", "gresources.xml",
"com.luminescent-dreams.kifu-gtk.gresource", "com.luminescent-dreams.kifu-gtk.gresource",
); );
let messages = std::process::Command::new("codegen-rust")
.arg("messages/en.yaml")
.output()
.expect("Failed to execute command");
println!("{}", String::from_utf8(messages.stdout).unwrap());
} }

View File

@ -1,9 +1,14 @@
nothing-here:
content: "Nothing Here"
welcome: welcome:
content: "Welcome to Kifu" content: "Welcome to Kifu"
hello: hello:
parameters: parameters:
name: string name: string
content: "Hello, ${name}" content: "Hello, ${name}"
games-in-database: games-in-database:
parameters: parameters:
count: count count: count

View File

@ -1,5 +1,8 @@
welcome = Welcome nothing-here = Nothing Here
games-in-database = {count -> hello = Hello, ${name}
[one] There is one game in the database games-in-database = {$count ->
[other] There are {count} games in the database [one] There is one game in the database
*[other] There are ${count} games in the database
} }
welcome = Welcome to Kifu

View File

@ -1,3 +1,5 @@
nothing-here = Nenio estas ĉi tie
hello = Saluton, ${name}
welcome = Bonvenon welcome = Bonvenon
games-in-database = {count -> games-in-database = {count ->
[one] Estas unu ludon en la datumbazo. [one] Estas unu ludon en la datumbazo.

View File

@ -1,26 +1,43 @@
use l10n::{FluentArgs, FluentValue, Message}; // This file was autogenerated from l10n-codegen-rust. Edits will be lost
// on next generation.
use l10n::Message;
use fluent_ergonomics::FluentErgo;
use fluent::FluentArgs;
enum Phrase { pub struct Hello {
Welcome, name: String,
GamesInDatabase(i32),
} }
impl Message for Phrase { impl Message for Hello {
fn msgid(&self) -> &str { fn localize(&self, bundle: &FluentErgo) -> String {
match self { let mut args = FluentArgs::new();
Phrase::Welcome => "welcome", args.set("name", self.name.clone());
Phrase::GamesInDatabase(_) => "games-in-database", bundle.tr("hello", Some(&args)).unwrap()
} }
} }
fn args(&self) -> Option<FluentArgs> { pub struct NothingHere;
match self { impl Message for NothingHere {
Phrase::Welcome => None, fn localize(&self, bundle: &FluentErgo) -> String {
Phrase::GamesInDatabase(val) => { bundle.tr("nothing-here", None).unwrap()
let mut args = FluentArgs::new(); }
args.set("count", FluentValue::from(val)); }
Some(args)
} pub struct Welcome;
} impl Message for Welcome {
fn localize(&self, bundle: &FluentErgo) -> String {
bundle.tr("welcome", None).unwrap()
}
}
pub struct GamesInDatabase {
count: usize,
}
impl Message for GamesInDatabase {
fn localize(&self, bundle: &FluentErgo) -> String {
let mut args = FluentArgs::new();
args.set("count", self.count.clone());
bundle.tr("games-in-database", Some(&args)).unwrap()
} }
} }

View File

@ -2,6 +2,9 @@ use adw::prelude::*;
use gio::resources_lookup_data; use gio::resources_lookup_data;
use glib::IsA; use glib::IsA;
use gtk::STYLE_PROVIDER_PRIORITY_USER; use gtk::STYLE_PROVIDER_PRIORITY_USER;
use l10n::L10N;
use std::path::PathBuf;
use crate::messages;
mod chat; mod chat;
pub use chat::Chat; pub use chat::Chat;
@ -75,8 +78,9 @@ impl AppWindow {
header.pack_end(&hamburger); header.pack_end(&hamburger);
let content = adw::Bin::builder().css_classes(vec!["content"]).build(); let content = adw::Bin::builder().css_classes(vec!["content"]).build();
let l10n = L10N::new(PathBuf::from("resources"));
content.set_child(Some( content.set_child(Some(
&adw::StatusPage::builder().title("Nothing here").build(), &adw::StatusPage::builder().title(l10n.tr(messages::NothingHere)).build(),
)); ));
let layout = gtk::Box::builder() let layout = gtk::Box::builder()

View File

@ -77,39 +77,53 @@ impl Message {
fn rust_code(&self) -> String { fn rust_code(&self) -> String {
if self.parameters.is_empty() { if self.parameters.is_empty() {
format!(r"pub struct {}; ", self.name.to_case(Case::Pascal)) let mut struct_strs = vec![];
struct_strs.push(format!("pub struct {}; ", self.name.to_case(Case::Pascal)));
struct_strs.push(format!("impl Message for {} {{
fn localize(&self, bundle: &FluentErgo) -> String {{
bundle.tr(\"{}\", None).unwrap()
}}
}}",
self.name.to_case(Case::Pascal),
self.name,
));
struct_strs.join("\n")
} else { } else {
let mut struct_strs = vec![]; let mut struct_strs = vec![];
struct_strs.push(format!("pub struct {} {{", self.name.to_case(Case::Pascal))); let parameters: String = self
let mut parameters: Vec<String> = self
.parameters .parameters
.iter() .iter()
.map(|param| format!(" {}: {},", param.name, param.ty)) .map(|param| format!(" {}: {},", param.name, param.ty))
.collect(); .collect::<Vec<String>>().join("\n");
struct_strs.push(format!("pub struct {} {{
{}
}}", self.name.to_case(Case::Pascal),
parameters));
struct_strs.append(&mut parameters);
struct_strs.push("}".to_owned());
struct_strs.push("".to_owned()); struct_strs.push("".to_owned());
struct_strs.push(format!( let parameters: String = self
"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 .parameters
.iter() .iter()
.map(|param| format!(" args.set(\"{}\", self.{})", param.name, param.name)) .map(|param| format!(" args.set(\"{}\", self.{}.clone());", param.name, param.name))
.collect(); .collect::<Vec<String>>().join("\n");
struct_strs.append(&mut parameters);
struct_strs.push("".to_owned()); struct_strs.push(format!(
struct_strs.push(format!(" let msg = bundle.get_message(\"{}\").unwrap();", self.name)); "impl Message for {} {{
struct_strs.push(format!(" let mut errors = vec![]")); fn localize(&self, bundle: &FluentErgo) -> String {{
struct_strs.push(format!(" msg.format_pattern(&msg.value().unwrap(), args, &mut errors)")); let mut args = FluentArgs::new();
struct_strs.push(" }".to_owned()); {}
struct_strs.push("}".to_owned()); bundle.tr(\"{}\", Some(&args)).unwrap()
}}
}}",
self.name.to_case(Case::Pascal),
parameters,
self.name,
));
struct_strs.join("\n") struct_strs.join("\n")
} }
@ -127,6 +141,12 @@ fn main() {
let messages: HashMap<String, MessageJS> = let messages: HashMap<String, MessageJS> =
serde_yaml::from_reader(File::open(input_file).unwrap()).unwrap(); serde_yaml::from_reader(File::open(input_file).unwrap()).unwrap();
println!("// This file was autogenerated from l10n-codegen-rust. Edits will be lost
// on next generation.");
println!("use l10n::Message;");
println!("use fluent_ergonomics::FluentErgo;");
println!("use fluent::FluentArgs;");
let messages = messages let messages = messages
.into_iter() .into_iter()
.map(|(name, msg)| Message::from_js(name, msg)); .map(|(name, msg)| Message::from_js(name, msg));

View File

@ -1,3 +1,21 @@
fn main() { use std::{collections::HashMap, fs::File, env};
println!("msggen running"); use serde::Deserialize;
use serde_yaml;
#[derive(Debug, Clone, Deserialize)]
struct MessageJS {
#[serde(default)]
content: String,
}
fn main() {
let mut args = env::args();
let _ = args.next();
let input_file = args.next().unwrap();
let messages: HashMap<String, MessageJS> =
serde_yaml::from_reader(File::open(input_file).unwrap()).unwrap();
for (name, message) in messages {
println!("{} = {}", name, message.content);
}
} }

View File

@ -68,20 +68,19 @@ impl From<icu::locid::Error> for L10NError {
// However, I have not found a mechanism in Fluent to identify all of the placeholders within a // 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. // 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 localize(&self, bundle: &FluentErgo) -> String;
fn args(&self) -> Option<FluentArgs>;
} }
pub struct L10N { pub struct L10N {
messages_root: std::path::PathBuf, messages_root: std::path::PathBuf,
messages: FluentErgo, message_bundle: FluentErgo,
locales: NonEmptyList<Locale>, locales: NonEmptyList<Locale>,
zone: chrono_tz::Tz, zone: chrono_tz::Tz,
} }
impl L10N { impl L10N {
fn new(messages_root: std::path::PathBuf) -> Self { pub fn new(messages_root: std::path::PathBuf) -> 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()) .and_then(|locale_str| locale_str.parse::<Locale>().ok())
@ -95,7 +94,7 @@ impl L10N {
let english_phrases = FluentResource::try_new let english_phrases = FluentResource::try_new
*/ */
let messages = { let message_bundle = {
let mut english_messages = messages_root.clone(); let mut english_messages = messages_root.clone();
english_messages.push("en-US.ftl"); english_messages.push("en-US.ftl");
@ -108,7 +107,7 @@ impl L10N {
Self { Self {
messages_root, messages_root,
messages, message_bundle,
locales, locales,
zone, zone,
} }
@ -158,7 +157,11 @@ impl L10N {
// 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 messages(&self) -> FluentErgo {
self.messages.clone() self.message_bundle.clone()
}
pub fn tr(&self, message: impl Message) -> String {
message.localize(&self.message_bundle)
} }
pub fn format_date_time_utc( pub fn format_date_time_utc(