Jelajahi Sumber

app: locale realtime switching and reloading UI

darkfi 1 tahun lalu
induk
melakukan
af12bf8341

+ 4 - 0
bin/app/Cargo.toml

@@ -55,6 +55,10 @@ swash = { git = "https://github.com/valadaptive/swash", branch = "tight-bounds"
 zeno = { git = "https://github.com/valadaptive/zeno", branch = "tight-bounds" }
 peniko = "*"
 
+fluent = "0.17"
+unic-langid = { version = "0.9", features = ["unic-langid-macros"] }
+indoc = "2"
+
 [features]
 emulate-android = []
 enable-plugins = []

+ 43 - 3
bin/app/src/app/mod.rs

@@ -18,6 +18,7 @@
 
 use chrono::{NaiveDate, NaiveDateTime};
 use darkfi_serial::Encodable;
+use indoc::indoc;
 use sled_overlay::sled;
 use smol::Task;
 use std::sync::{Arc, Mutex as SyncMutex};
@@ -33,6 +34,7 @@ use crate::{
     scene::{Pimpl, SceneNode, SceneNodePtr, SceneNodeType},
     text::TextShaperPtr,
     ui::{chatview, Window},
+    util::i18n::I18nBabelFish,
     ExecutorPtr,
 };
 
@@ -86,6 +88,8 @@ impl App {
 
         let mut window = SceneNode::new("window", SceneNodeType::Window);
 
+        let i18n_fish = self.setup_locale(&mut window);
+
         let mut prop = Property::new("screen_size", PropertyType::Float32, PropertySubType::Pixel);
         prop.set_array_len(2);
         window.add_property(prop).unwrap();
@@ -120,13 +124,16 @@ impl App {
             self.tasks.lock().unwrap().push(setting_task);
         }
 
-        let window =
-            window.setup(|me| Window::new(me, self.render_api.clone(), setting_root.clone())).await;
+        let window = window
+            .setup(|me| {
+                Window::new(me, self.render_api.clone(), i18n_fish.clone(), setting_root.clone())
+            })
+            .await;
 
         self.sg_root.clone().link(window.clone());
         self.sg_root.clone().link(setting_root.clone());
 
-        schema::make(&self, window.clone()).await;
+        schema::make(&self, window.clone(), &i18n_fish).await;
 
         //settings::make(&self, window, self.ex.clone()).await;
 
@@ -135,6 +142,39 @@ impl App {
         Ok(None)
     }
 
+    fn setup_locale(&self, window: &mut SceneNode) -> I18nBabelFish {
+        let i18n_src = indoc! {"
+            hello-world = Hello, world!
+            channels-label = CHANNELS
+        "}
+        .to_owned();
+        // Will be managed by settings eventually
+        let i18n_fish = I18nBabelFish::new(i18n_src, "en-US");
+
+        // sys-locale = "0.3"
+        // fluent-langneg = "0.14"
+        /*
+        use fluent_langneg::{
+            negotiate_languages,
+            NegotiationStrategy,
+            convert_vec_str_to_langids_lossy,
+            LanguageIdentifier
+        };
+        let mut locales: Vec<_> = sys_locale::get_locales().collect();
+        let en_US = "en-US".to_string();
+        if !locales.contains(&en_US) {
+            locales.push(en_US);
+        }
+        info!(target: "app", "Locale: {:?}", locales);
+        */
+
+        let mut prop = Property::new("locale", PropertyType::Str, PropertySubType::Locale);
+        prop.set_defaults_str(vec!["en-US".to_string()]).unwrap();
+        window.add_property(prop).unwrap();
+
+        i18n_fish
+    }
+
     /// Begins the draw of the tree, and then starts the UI procs.
     pub async fn start(self: Arc<Self>, event_pub: GraphicsEventPublisherPtr, epoch: EpochIndex) {
         d!("Starting app epoch={epoch}");

+ 3 - 0
bin/app/src/app/node.rs

@@ -183,6 +183,9 @@ pub fn create_text(name: &str) -> SceneNode {
     let prop = Property::new("priority", PropertyType::Uint32, PropertySubType::Null);
     node.add_property(prop).unwrap();
 
+    let prop = Property::new("use_i18n", PropertyType::Bool, PropertySubType::Null);
+    node.add_property(prop).unwrap();
+
     let prop = Property::new("debug", PropertyType::Bool, PropertySubType::Null);
     node.add_property(prop).unwrap();
 

+ 18 - 7
bin/app/src/app/schema/chat.rs

@@ -40,7 +40,7 @@ use crate::{
         chatview, emoji_picker, Button, ChatEdit, ChatView, EmojiPicker, Layer, Shortcut, Text,
         VectorArt, VectorShape,
     },
-    util::unixtime,
+    util::{i18n::I18nBabelFish, unixtime},
 };
 
 use super::{ColorScheme, COLOR_SCHEME};
@@ -181,6 +181,7 @@ pub async fn make(
     window: SceneNodePtr,
     channel: &str,
     db: &sled::Db,
+    i18n_fish: &I18nBabelFish,
     emoji_meshes: emoji_picker::EmojiMeshesPtr,
     is_first_time: bool,
 ) {
@@ -361,7 +362,9 @@ pub async fn make(
     }
     node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
 
-    let node = node.setup(|me| Text::new(me, window_scale.clone(), app.render_api.clone())).await;
+    let node = node
+        .setup(|me| Text::new(me, window_scale.clone(), app.render_api.clone(), i18n_fish.clone()))
+        .await;
     layer_node.clone().link(node);
 
     // Create the emoji picker
@@ -744,7 +747,7 @@ pub async fn make(
     }
     node.set_property_u32(atom, Role::App, "z_index", 6).unwrap();
     node.set_property_u32(atom, Role::App, "priority", 3).unwrap();
-    //node.set_property_bool(atom, Role::App, "debug", true).unwrap();
+    node.set_property_bool(atom, Role::App, "debug", true).unwrap();
 
     let editz_text = PropertyStr::wrap(&node, Role::App, "text", 0).unwrap();
     let editz_select_text = node.get_property("select_text").unwrap();
@@ -1113,7 +1116,9 @@ pub async fn make(
     prop.clone().set_f32(atom, Role::App, 3, 1.).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 1).unwrap();
 
-    let node = node.setup(|me| Text::new(me, window_scale.clone(), app.render_api.clone())).await;
+    let node = node
+        .setup(|me| Text::new(me, window_scale.clone(), app.render_api.clone(), i18n_fish.clone()))
+        .await;
     cmd_layer_node.clone().link(node);
 
     // Create some text
@@ -1134,7 +1139,9 @@ pub async fn make(
     prop.clone().set_f32(atom, Role::App, 3, 1.).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 1).unwrap();
 
-    let node = node.setup(|me| Text::new(me, window_scale.clone(), app.render_api.clone())).await;
+    let node = node
+        .setup(|me| Text::new(me, window_scale.clone(), app.render_api.clone(), i18n_fish.clone()))
+        .await;
     cmd_layer_node.clone().link(node);
 
     // Create debug box
@@ -1281,7 +1288,9 @@ pub async fn make(
     prop.clone().set_f32(atom, Role::App, 3, 1.).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 1).unwrap();
 
-    let node = node.setup(|me| Text::new(me, window_scale.clone(), app.render_api.clone())).await;
+    let node = node
+        .setup(|me| Text::new(me, window_scale.clone(), app.render_api.clone(), i18n_fish.clone()))
+        .await;
     layer_node.clone().link(node);
 
     // Copy button
@@ -1451,7 +1460,9 @@ pub async fn make(
     prop.clone().set_f32(atom, Role::App, 3, 1.).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 1).unwrap();
 
-    let node = node.setup(|me| Text::new(me, window_scale.clone(), app.render_api.clone())).await;
+    let node = node
+        .setup(|me| Text::new(me, window_scale.clone(), app.render_api.clone(), i18n_fish.clone()))
+        .await;
     layer_node.clone().link(node);
 
     // Paste button

+ 12 - 5
bin/app/src/app/schema/menu.rs

@@ -25,6 +25,7 @@ use crate::{
     prop::{PropertyAtomicGuard, PropertyBool, PropertyFloat32, Role},
     scene::{SceneNodePtr, Slot},
     ui::{Button, Layer, ShapeVertex, Shortcut, Text, VectorArt, VectorShape},
+    util::i18n::I18nBabelFish,
 };
 
 use super::{ColorScheme, CHANNELS, COLOR_SCHEME};
@@ -60,7 +61,7 @@ mod ui_consts {
 
 use ui_consts::*;
 
-pub async fn make(app: &App, window: SceneNodePtr) {
+pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
     let window_scale = PropertyFloat32::wrap(
         &app.sg_root.clone().lookup_node("/setting/scale").unwrap(),
         Role::Internal,
@@ -131,7 +132,8 @@ pub async fn make(app: &App, window: SceneNodePtr) {
     prop.clone().set_f32(atom, Role::App, 3, 200.).unwrap();
     node.set_property_u32(atom, Role::App, "z_index", 1).unwrap();
     node.set_property_f32(atom, Role::App, "font_size", CHANNEL_LABEL_FONTSIZE).unwrap();
-    node.set_property_str(atom, Role::App, "text", "CHANNELS").unwrap();
+    node.set_property_bool(atom, Role::App, "use_i18n", true).unwrap();
+    node.set_property_str(atom, Role::App, "text", "channels-label").unwrap();
     //node.set_property_str(atom, Role::App, "text", "anon1").unwrap();
     //node.set_property_bool(atom, Role::App, "debug", true).unwrap();
     let prop = node.get_property("text_color").unwrap();
@@ -148,7 +150,9 @@ pub async fn make(app: &App, window: SceneNodePtr) {
     }
     node.set_property_u32(atom, Role::App, "z_index", 1).unwrap();
 
-    let node = node.setup(|me| Text::new(me, window_scale.clone(), app.render_api.clone())).await;
+    let node = node
+        .setup(|me| Text::new(me, window_scale.clone(), app.render_api.clone(), i18n_fish.clone()))
+        .await;
     layer_node.clone().link(node);
 
     let mut channel_y = CHANNEL_LABEL_LINESPACE;
@@ -225,8 +229,11 @@ pub async fn make(app: &App, window: SceneNodePtr) {
         set_normal_color(atom);
         node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
 
-        let node =
-            node.setup(|me| Text::new(me, window_scale.clone(), app.render_api.clone())).await;
+        let node = node
+            .setup(|me| {
+                Text::new(me, window_scale.clone(), app.render_api.clone(), i18n_fish.clone())
+            })
+            .await;
         layer_node.clone().link(node);
 
         // Create the button

+ 13 - 3
bin/app/src/app/schema/mod.rs

@@ -30,6 +30,7 @@ use crate::{
     scene::{SceneNodePtr, Slot},
     shape,
     ui::{emoji_picker, Image, Layer, Shortcut, VectorArt, VectorShape},
+    util::i18n::I18nBabelFish,
 };
 
 mod chat;
@@ -124,7 +125,7 @@ enum ColorScheme {
     PaperLight,
 }
 
-pub async fn make(app: &App, window: SceneNodePtr) {
+pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
     let mut cc = Compiler::new();
     cc.add_const_f32("NETSTATUS_ICON_SIZE", NETSTATUS_ICON_SIZE);
     cc.add_const_f32("SETTINGS_ICON_SIZE", SETTINGS_ICON_SIZE);
@@ -504,9 +505,18 @@ pub async fn make(app: &App, window: SceneNodePtr) {
     let chatdb_path = get_chatdb_path();
     let db = sled::open(chatdb_path).expect("cannot open sleddb");
     for channel in CHANNELS {
-        chat::make(app, window.clone(), channel, &db, emoji_meshes.clone(), is_first_time).await;
+        chat::make(
+            app,
+            window.clone(),
+            channel,
+            &db,
+            i18n_fish,
+            emoji_meshes.clone(),
+            is_first_time,
+        )
+        .await;
     }
-    menu::make(app, window.clone()).await;
+    menu::make(app, window.clone(), i18n_fish).await;
 
     // @@@ Debug stuff @@@
     //let chatview_node = app.sg_root.clone().lookup_node("/window/dev_chat_layer").unwrap();

+ 2 - 2
bin/app/src/main.rs

@@ -120,14 +120,14 @@ struct God {
 
 impl God {
     fn new() -> Self {
-        info!(target: "main", "Creating the app");
-
         // Abort the application on panic right away
         std::panic::set_hook(Box::new(panic_hook));
 
         text2::init_txt_ctx();
         logger::setup_logging();
 
+        info!(target: "main", "Creating the app");
+
         #[cfg(target_os = "android")]
         {
             use crate::android::get_appdata_path;

+ 1 - 0
bin/app/src/prop/mod.rs

@@ -73,6 +73,7 @@ pub enum PropertySubType {
     // Size of something in pixels
     Pixel = 2,
     ResourceId = 3,
+    Locale = 4,
 }
 
 #[derive(Debug, Copy, Clone, PartialEq)]

+ 8 - 1
bin/app/src/ui/layer.rs

@@ -26,7 +26,7 @@ use crate::{
     gfx::{GfxDrawCall, GfxDrawInstruction, Point, Rectangle, RenderApi},
     prop::{PropertyAtomicGuard, PropertyBool, PropertyRect, PropertyUint32, Role},
     scene::{Pimpl, SceneNodePtr, SceneNodeWeak},
-    util::unixtime,
+    util::{i18n::I18nBabelFish, unixtime},
     ExecutorPtr,
 };
 
@@ -314,4 +314,11 @@ impl UIObject for Layer {
         }
         false
     }
+
+    fn set_i18n(&self, i18n_fish: &I18nBabelFish) {
+        for child in self.get_children() {
+            let obj = get_ui_object3(&child);
+            obj.set_i18n(i18n_fish);
+        }
+    }
 }

+ 3 - 0
bin/app/src/ui/mod.rs

@@ -25,6 +25,7 @@ use crate::{
     gfx::{GfxDrawCall, Point, Rectangle},
     prop::{ModifyAction, PropertyAtomicGuard, PropertyPtr, Role},
     scene::{Pimpl, SceneNode as SceneNode3, SceneNodePtr, SceneNodeWeak},
+    util::i18n::I18nBabelFish,
     ExecutorPtr,
 };
 
@@ -106,6 +107,8 @@ pub trait UIObject: Sync {
     async fn handle_touch(&self, _phase: TouchPhase, _id: u64, _touch_pos: Point) -> bool {
         false
     }
+
+    fn set_i18n(&self, _i18n_fish: &I18nBabelFish) {}
 }
 
 pub struct DrawUpdate {

+ 21 - 1
bin/app/src/ui/text.rs

@@ -29,7 +29,7 @@ use crate::{
     },
     scene::{Pimpl, SceneNodeWeak},
     text2::{self, TEXT_CTX},
-    util::unixtime,
+    util::{i18n::I18nBabelFish, unixtime},
     ExecutorPtr,
 };
 
@@ -42,6 +42,7 @@ pub type TextPtr = Arc<Text>;
 pub struct Text {
     node: SceneNodeWeak,
     render_api: RenderApi,
+    i18n_fish: I18nBabelFish,
     tasks: SyncMutex<Vec<smol::Task<()>>>,
 
     dc_key: u64,
@@ -53,6 +54,7 @@ pub struct Text {
     font_size: PropertyFloat32,
     text_color: PropertyColor,
     lineheight: PropertyFloat32,
+    use_i18n: PropertyBool,
     debug: PropertyBool,
 
     window_scale: PropertyFloat32,
@@ -64,6 +66,7 @@ impl Text {
         node: SceneNodeWeak,
         window_scale: PropertyFloat32,
         render_api: RenderApi,
+        i18n_fish: I18nBabelFish,
     ) -> Pimpl {
         t!("Text::new()");
 
@@ -75,11 +78,13 @@ impl Text {
         let font_size = PropertyFloat32::wrap(node_ref, Role::Internal, "font_size", 0).unwrap();
         let text_color = PropertyColor::wrap(node_ref, Role::Internal, "text_color").unwrap();
         let lineheight = PropertyFloat32::wrap(node_ref, Role::Internal, "lineheight", 0).unwrap();
+        let use_i18n = PropertyBool::wrap(node_ref, Role::Internal, "use_i18n", 0).unwrap();
         let debug = PropertyBool::wrap(node_ref, Role::Internal, "debug", 0).unwrap();
 
         let self_ = Arc::new(Self {
             node,
             render_api,
+            i18n_fish,
             tasks: SyncMutex::new(vec![]),
             dc_key: OsRng.gen(),
 
@@ -90,6 +95,7 @@ impl Text {
             font_size,
             text_color,
             lineheight,
+            use_i18n,
             debug,
 
             window_scale,
@@ -106,6 +112,16 @@ impl Text {
         let text_color = self.text_color.get();
         let window_scale = self.window_scale.get();
 
+        let text = if self.use_i18n.get() {
+            if let Some(trans) = self.i18n_fish.tr(&text) {
+                trans
+            } else {
+                format!("tr err: {}", text)
+            }
+        } else {
+            text
+        };
+
         let layout = {
             let mut txt_ctx = TEXT_CTX.get().await;
             txt_ctx.make_layout(&text, text_color, font_size, lineheight, window_scale, None, &[])
@@ -185,6 +201,10 @@ impl UIObject for Text {
         *self.parent_rect.lock() = Some(parent_rect);
         self.get_draw_calls(parent_rect).await
     }
+
+    fn set_i18n(&self, i18n_fish: &I18nBabelFish) {
+        self.i18n_fish.set(i18n_fish);
+    }
 }
 
 impl Drop for Text {

+ 47 - 9
bin/app/src/ui/win.rs

@@ -27,14 +27,15 @@ use crate::{
         GraphicsEventMouseMoveSub, GraphicsEventMouseWheelSub, GraphicsEventPublisherPtr,
         GraphicsEventTouchSub, Point, Rectangle, RenderApi,
     },
-    prop::{PropertyAtomicGuard, PropertyDimension, PropertyFloat32, Role},
+    prop::{PropertyAtomicGuard, PropertyDimension, PropertyFloat32, PropertyStr, Role},
     scene::{Pimpl, SceneNodePtr, SceneNodeWeak},
-    util::unixtime,
+    util::{i18n::I18nBabelFish, unixtime},
     ExecutorPtr,
 };
 
 use super::{get_children_ordered, get_ui_object3, get_ui_object_ptr, OnModify};
 
+macro_rules! i { ($($arg:tt)*) => { info!(target: "ui::window", $($arg)*); } }
 macro_rules! d { ($($arg:tt)*) => { debug!(target: "ui::window", $($arg)*); } }
 macro_rules! t { ($($arg:tt)*) => { trace!(target: "ui::window", $($arg)*); } }
 
@@ -48,22 +49,26 @@ pub type WindowPtr = Arc<Window>;
 
 pub struct Window {
     node: SceneNodeWeak,
-
+    render_api: RenderApi,
+    i18n_fish: I18nBabelFish,
     tasks: SyncMutex<Vec<smol::Task<()>>>,
+
+    locale: PropertyStr,
     screen_size: PropertyDimension,
     scale: PropertyFloat32,
-    render_api: RenderApi,
 }
 
 impl Window {
     pub async fn new(
         node: SceneNodeWeak,
         render_api: RenderApi,
+        i18n_fish: I18nBabelFish,
         setting_root: SceneNodePtr,
     ) -> Pimpl {
         t!("Window::new()");
 
         let node_ref = &node.upgrade().unwrap();
+        let locale = PropertyStr::wrap(node_ref, Role::Internal, "locale", 0).unwrap();
         let screen_size = PropertyDimension::wrap(node_ref, Role::Internal, "screen_size").unwrap();
         let scale = PropertyFloat32::wrap(
             &setting_root.clone().lookup_node("/scale").unwrap(),
@@ -73,8 +78,16 @@ impl Window {
         )
         .unwrap();
 
-        let self_ =
-            Arc::new(Self { node, tasks: SyncMutex::new(vec![]), screen_size, scale, render_api });
+        let self_ = Arc::new(Self {
+            node,
+            render_api,
+            i18n_fish,
+            tasks: SyncMutex::new(vec![]),
+
+            locale,
+            screen_size,
+            scale,
+        });
 
         Pimpl::Window(self_)
     }
@@ -154,12 +167,16 @@ impl Window {
         let me2 = me.clone();
         let touch_task = ex.spawn(async move { while Self::process_touch(&me2, &ev_sub).await {} });
 
-        let redraw_fn = move |self_: Arc<Self>| async move {
+        async fn reload_locale(self_: Arc<Window>) {
+            self_.reload_locale().await;
+        }
+        async fn redraw(self_: Arc<Window>) {
             self_.draw().await;
-        };
+        }
 
         let mut on_modify = OnModify::new(ex.clone(), self.node.clone(), me.clone());
-        on_modify.when_change(self.scale.prop(), redraw_fn);
+        on_modify.when_change(self.locale.prop(), reload_locale);
+        on_modify.when_change(self.scale.prop(), redraw);
 
         let mut tasks = vec![
             resize_task,
@@ -454,4 +471,25 @@ impl Window {
 
         t!("Window::draw() - replaced draw call [timest={timest}, trace_id={trace_id}]");
     }
+
+    async fn reload_locale(&self) {
+        // todo: Load this from disk
+        let i18n_src = indoc::indoc! {"
+            hello-world = Hello, world!
+            channels-label = KANALLAR
+        "}
+        .to_owned();
+
+        let locale = self.locale.get();
+        i!("Changed locale to: {locale}");
+        assert_eq!(locale, "tr");
+        let i18n_fish = I18nBabelFish::new(i18n_src, &locale);
+        self.i18n_fish.set(&i18n_fish);
+        for child in self.get_children() {
+            let obj = get_ui_object3(&child);
+            obj.set_i18n(&i18n_fish);
+        }
+        // Just redraw everything lol
+        self.draw().await;
+    }
 }

+ 1 - 1
bin/app/src/util/i18n.rs

@@ -40,7 +40,7 @@ impl I18nBabelFish {
         Self { bundle: RwLock::new(Arc::new(bundle)) }
     }
 
-    pub fn set(&self, other: Self) {
+    pub fn set(&self, other: &Self) {
         let bundle = other.bundle.read();
         *self.bundle.write() = Arc::clone(&*bundle);
     }

+ 1 - 0
bin/app/src/util/mod.rs

@@ -20,6 +20,7 @@
 use colored::Colorize;
 use std::time::{SystemTime, UNIX_EPOCH};
 
+pub mod i18n;
 mod rt;
 pub use rt::{AsyncRuntime, ExecutorPtr};