Compare commits

...

2 Commits

13 changed files with 164 additions and 100 deletions

2
Cargo.lock generated
View File

@ -2709,6 +2709,8 @@ dependencies = [
"libadwaita", "libadwaita",
"pango", "pango",
"sgf", "sgf",
"sys-locale",
"thiserror",
"tokio", "tokio",
] ]

View File

@ -8735,6 +8735,14 @@ rec {
name = "cairo-rs"; name = "cairo-rs";
packageId = "cairo-rs"; packageId = "cairo-rs";
} }
{
name = "fluent";
packageId = "fluent";
}
{
name = "fluent-ergonomics";
packageId = "fluent-ergonomics";
}
{ {
name = "gio"; name = "gio";
packageId = "gio"; packageId = "gio";
@ -8867,6 +8875,10 @@ rec {
name = "icu_provider"; name = "icu_provider";
packageId = "icu_provider"; packageId = "icu_provider";
} }
{
name = "intl-memoizer";
packageId = "intl-memoizer";
}
{ {
name = "serde"; name = "serde";
packageId = "serde 1.0.193"; packageId = "serde 1.0.193";
@ -8880,6 +8892,10 @@ rec {
name = "sys-locale"; name = "sys-locale";
packageId = "sys-locale"; packageId = "sys-locale";
} }
{
name = "thiserror";
packageId = "thiserror";
}
{ {
name = "unic-langid"; name = "unic-langid";
packageId = "unic-langid"; packageId = "unic-langid";

View File

@ -17,6 +17,21 @@
let let
pkgs = import nixpkgs { system = "x86_64-linux"; }; pkgs = import nixpkgs { system = "x86_64-linux"; };
pkgs-unstable = import unstable { system = "x86_64-linux"; }; pkgs-unstable = import unstable { system = "x86_64-linux"; };
cargo_nix = pkgs.callPackage ./Cargo.nix {
nixpkgs = pkgs;
};
l10n-codegen-rust = pkgs.stdenv.mkDerivation {
name = "l10n-codegen-rust";
src = ./.;
installPhase = ''
mkdir -p $out/bin
cp ${cargo_nix.workspaceMembers.l10n.build}/bin/codegen-rust $out/bin/l10n-codegen-rust
'';
};
in in
pkgs.mkShell { pkgs.mkShell {
name = "ld-tools-devshell"; name = "ld-tools-devshell";
@ -45,6 +60,8 @@
pkgs.udev pkgs.udev
pkgs.wasm-pack pkgs.wasm-pack
typeshare.packages."x86_64-linux".default typeshare.packages."x86_64-linux".default
l10n-codegen-rust
]; ];
LIBCLANG_PATH="${pkgs.llvmPackages.libclang.lib}/lib"; LIBCLANG_PATH="${pkgs.llvmPackages.libclang.lib}/lib";
ENV = "dev"; ENV = "dev";
@ -86,17 +103,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;
l10n = cargo_nix.workspaceMembers.l10n.build;
l10n-codegen-rust = pkgs.stdenv.mkDerivation {
name = "l10n-codegen-rust";
src = ./.;
buildInputs = [ l10n ];
installPhase = ''
mkdir -p $out/bin
cp ${l10n}/bin/codegen-rust $out/bin/l10n-codegen-rust
'';
};
xdg-test = cargo_nix.workspaceMembers.xdg-test.build; xdg-test = cargo_nix.workspaceMembers.xdg-test.build;
all = pkgs.symlinkJoin { all = pkgs.symlinkJoin {
@ -107,8 +113,6 @@
file-service file-service
fitnesstrax fitnesstrax
# kifu-gtk # kifu-gtk
l10n
l10n-codegen-rust
xdg-test xdg-test
]; ];
}; };

View File

@ -13,8 +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" } fluent-ergonomics = { path = "../../fluent-ergonomics" }
fluent = { version = "0.16" }
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" ] }
@ -23,6 +23,8 @@ kifu-core = { path = "../core" }
l10n = { path = "../../l10n" } l10n = { path = "../../l10n" }
pango = { version = "*" } pango = { version = "*" }
sgf = { path = "../../sgf" } sgf = { path = "../../sgf" }
sys-locale = { version = "0.3" }
thiserror = { version = "1" }
tokio = { version = "1.26", features = [ "full" ] } tokio = { version = "1.26", features = [ "full" ] }
[build-dependencies] [build-dependencies]

View File

@ -1,7 +1,29 @@
use std::{path::PathBuf, env, process, fs::File, io::Write};
fn generate_message_types(dest: &PathBuf) {
println!("message types");
let output: Vec<u8> = process::Command::new("l10n-codegen-rust")
.arg("messages/en.yaml")
.output()
.unwrap()
.stdout;
let mut file = File::create(dest).unwrap();
file.write(&output).unwrap();
}
fn main() { fn main() {
glib_build_tools::compile_resources( glib_build_tools::compile_resources(
&["resources"], &["resources"],
"gresources.xml", "gresources.xml",
"com.luminescent-dreams.kifu-gtk.gresource", "com.luminescent-dreams.kifu-gtk.gresource",
); );
// println!("OUT_DIR={}", env::var("OUT_DIR").unwrap());
let mut message_path = PathBuf::from(env::var("OUT_DIR").unwrap());
message_path.push("messages.rs");
generate_message_types(&message_path);
println!("cargo:rustc-env=KIFU_GTK_MESSAGES={}", message_path.to_string_lossy());
} }

View File

@ -1,7 +1,7 @@
nothing-here = Nenio estas ĉi tie nothing-here = Nenio estas ĉi tie
hello = Saluton, ${name} 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.
[other] Estas {count} ludojn en la datumbazo. *[other] Estas ${count} ludojn en la datumbazo.
} }

View File

@ -1,15 +1,18 @@
mod messages; use async_std::task::yield_now;
use kifu_core::{Core, CoreRequest, Observable};
use l10n::{NonEmptyList, L10N};
use std::{rc::Rc, sync::Arc};
use tokio::runtime::Runtime;
mod messages {
include!(env!("KIFU_GTK_MESSAGES"));
}
pub mod ui; pub mod ui;
mod view_models; mod view_models;
mod views; mod views;
use async_std::task::yield_now;
use kifu_core::{Core, CoreRequest, CoreResponse, Observable};
use std::{rc::Rc, sync::Arc};
use tokio::runtime::Runtime;
#[derive(Clone)] #[derive(Clone)]
pub struct CoreApi { pub struct CoreApi {
pub rt: Arc<Runtime>, pub rt: Arc<Runtime>,
@ -41,6 +44,45 @@ where
result result
} }
/// AppContext is our global store for things that need to be available everywhere. Developers
/// should never pass this around directly, but should instead pass it around by traits. This is to
/// provide systems such as:
///
/// - Feature Flags
/// - L10N
pub struct AppContext {
pub l10n: L10N,
}
#[derive(Debug)]
pub enum AppContextInitError {}
impl AppContext {
pub fn new() -> Result<Self, AppContextInitError> {
let mut locale_list = NonEmptyList::new("en-US");
let user_locale = sys_locale::get_locale().unwrap();
println!("user locale: {}", user_locale);
if locale_list.find(|l| *l == user_locale).is_none() {
locale_list.push(&user_locale);
}
let mut l10n = l10n::L10N::new("messages".into());
l10n.set_locales(locale_list);
Ok(Self { l10n })
}
}
pub trait ProvidesL10N {
fn l10n(&self) -> &L10N;
}
impl ProvidesL10N for AppContext {
fn l10n(&self) -> &L10N {
&self.l10n
}
}
/// LocalObserver creates a task on the current thread which watches the specified observer for notifications and calls the handler function with each one. /// LocalObserver creates a task on the current thread which watches the specified observer for notifications and calls the handler function with each one.
/// ///
/// The LocalObserver starts a task which listens for notifications during the constructor. When the observer goes out of scope, it will make a point of aborting the task. This combination means that anything which uses the observer can create it, hold on to a reference of it, and then drop it when done, and not have to do anything else with the observer object. /// The LocalObserver starts a task which listens for notifications during the constructor. When the observer goes out of scope, it will make a point of aborting the task. This combination means that anything which uses the observer can create it, hold on to a reference of it, and then drop it when done, and not have to do anything else with the observer object.

View File

@ -3,8 +3,9 @@ use kifu_core::{Config, ConfigOption, Core, CoreRequest, CoreResponse, DatabaseP
use kifu_gtk::{ use kifu_gtk::{
perftrace, perftrace,
ui::{AppWindow, ConfigurationPage, Home, PlayingField}, ui::{AppWindow, ConfigurationPage, Home, PlayingField},
CoreApi, AppContext, CoreApi,
}; };
use l10n::NonEmptyList;
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
const APP_ID_DEV: &str = "com.luminescent-dreams.kifu-gtk.dev"; const APP_ID_DEV: &str = "com.luminescent-dreams.kifu-gtk.dev";
@ -104,7 +105,9 @@ fn main() {
app.connect_activate({ app.connect_activate({
let runtime = runtime.clone(); let runtime = runtime.clone();
move |app| { move |app| {
let app_window = AppWindow::new(app);
let ctx = AppContext::new().unwrap();
let app_window = AppWindow::new(app, &ctx);
let api = CoreApi { let api = CoreApi {
rt: runtime.clone(), rt: runtime.clone(),

View File

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

View File

@ -2,9 +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 l10n::{L10N, NonEmptyList};
use std::path::PathBuf; use std::path::PathBuf;
use crate::messages; use crate::{ProvidesL10N, messages};
mod chat; mod chat;
pub use chat::Chat; pub use chat::Chat;
@ -40,7 +40,7 @@ pub struct AppWindow {
} }
impl AppWindow { impl AppWindow {
pub fn new(app: &adw::Application) -> Self { pub fn new(app: &adw::Application, ctx: &impl ProvidesL10N) -> Self {
let window = adw::ApplicationWindow::builder() let window = adw::ApplicationWindow::builder()
.application(app) .application(app)
.width_request(800) .width_request(800)
@ -78,9 +78,8 @@ 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(l10n.tr(messages::NothingHere)).build(), &adw::StatusPage::builder().title(ctx.l10n().tr(messages::NothingHere)).build(),
)); ));
let layout = gtk::Box::builder() let layout = gtk::Box::builder()

View File

@ -22,11 +22,13 @@ 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 { pub fn new(elem: A) -> Self {
Self(vec![elem]) Self(vec![elem])
} }
fn from_iter(iter: impl IntoIterator<Item = A>) -> Result<NonEmptyList<A>, NonEmptyListError> { pub 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))
@ -35,9 +37,21 @@ impl<A> NonEmptyList<A> {
} }
} }
pub fn push(&mut self, item: A) {
self.0.push(item);
}
pub fn find(&self, f: impl Fn(&A) -> bool) -> Option<&A> {
self.0.iter().find(|item| f(*item))
}
fn first(&self) -> &A { fn first(&self) -> &A {
&self.0[0] &self.0[0]
} }
fn iter<'a>(&'a self) -> impl Iterator<Item = &'a A> {
self.0.iter()
}
} }
impl<A> Deref for NonEmptyList<A> { impl<A> Deref for NonEmptyList<A> {
@ -68,7 +82,7 @@ pub enum FileLoadError {
FileNotFound, FileNotFound,
#[error("The Fluent file is malformed")] #[error("The Fluent file is malformed")]
FluentParseError, FluentParseError(String),
#[error("An unknown IO error was found")] #[error("An unknown IO error was found")]
IOError(std::io::Error), IOError(std::io::Error),
@ -156,7 +170,13 @@ impl L10N {
self.message_bundles.push(bundle); self.message_bundles.push(bundle);
Ok(()) Ok(())
} }
Err((_, _error)) => Err(FileLoadError::FluentParseError), Err((_, errors)) => Err(FileLoadError::FluentParseError(
errors
.into_iter()
.map(|err| err.to_string())
.collect::<Vec<String>>()
.join("\n"),
)),
} }
} }
@ -178,7 +198,13 @@ impl L10N {
.iter() .iter()
.map(|locale| Locale::try_from_bytes(locale.as_bytes())) .map(|locale| Locale::try_from_bytes(locale.as_bytes()))
.collect::<Result<Vec<Locale>, icu::locid::Error>>()?; .collect::<Result<Vec<Locale>, icu::locid::Error>>()?;
for locale in locales.iter() {
self.load_messages_from_file(locale.to_string()).unwrap();
}
self.locales = NonEmptyList(locales); self.locales = NonEmptyList(locales);
Ok(()) Ok(())
} }
@ -200,15 +226,21 @@ impl L10N {
// } // }
pub fn tr(&self, message: impl Message) -> String { pub fn tr(&self, message: impl Message) -> String {
println!("message: {}", message.msgid()); for bundle in self.message_bundles.iter().rev() {
let msg = self.message_bundles[0] let msg = bundle
.get_message(message.msgid()) .get_message(message.msgid())
.and_then(|msg| msg.value()) .and_then(|msg| msg.value());
.unwrap(); match msg {
Some(msg) => {
let mut errors = vec![]; let mut errors = vec![];
self.message_bundles[0] return self.message_bundles[0]
.format_pattern(msg, message.args().as_ref(), &mut errors) .format_pattern(msg, message.args().as_ref(), &mut errors)
.to_string() .to_string();
}
None => continue,
}
}
unreachable!("The message {} is missing", message.msgid());
} }
pub fn format_date_time_utc( pub fn format_date_time_utc(