diff --git a/dash-frontend/assets/lang/de.json b/dash-frontend/assets/lang/de.json index 32dbd1b..149d849 100644 --- a/dash-frontend/assets/lang/de.json +++ b/dash-frontend/assets/lang/de.json @@ -10,7 +10,6 @@ "APP_SETTINGS": { "HIDE_USERNAME": "Benutzernamen ausblenden", "OPAQUE_BACKGROUND": "Undurchsichtiger Hintergrund", - "WLX": {}, "LOOK_AND_FEEL": "Aussehen und Verhalten", "HIDE_GRAB_HELP": "Greif-Hilfe ausblenden", "ANIMATION_SPEED": "UI-Animationsgeschwindigkeit", @@ -89,7 +88,9 @@ "RESET_PLAYSPACE": "Spielbereich zurücksetzen", "RESET_PLAYSPACE_HELP": "Den Abstand des Spielbereichs zurücksetzen.", "BLOCK_POSES_ON_KBD_INTERACTION": "Posen beim Interagieren mit der Tastatur blockieren", - "BLOCK_POSES_ON_KBD_INTERACTION_HELP": "Verhindert, dass das Spiel Posen empfängt, wenn die Tastatur angefahren wird und „Spieleingabe blockieren“ aktiviert ist" + "BLOCK_POSES_ON_KBD_INTERACTION_HELP": "Verhindert, dass das Spiel Posen empfängt, wenn die Tastatur angefahren wird und „Spieleingabe blockieren“ aktiviert ist", + "LANGUAGE": "Sprache", + "REQUIRES_RESTART": "Erfordert Neustart" }, "HELLO": "Hallo!", "AUDIO": { diff --git a/dash-frontend/assets/lang/en.json b/dash-frontend/assets/lang/en.json index 524d0a5..b247103 100644 --- a/dash-frontend/assets/lang/en.json +++ b/dash-frontend/assets/lang/en.json @@ -63,6 +63,7 @@ "KEYBOARD_MIDDLE_CLICK": "Keyboard middle click", "KEYBOARD_MIDDLE_CLICK_HELP": "Modifier to use when typing\nwith purple laser", "KEYBOARD_SOUND_ENABLED": "Keyboard sounds", + "LANGUAGE": "Language", "LEFT_HANDED_MOUSE": "Left-handed mouse", "LEFT_HANDED_MOUSE_HELP": "Use this if mouse buttons are swapped", "LONG_PRESS_DURATION": "Long press duration", @@ -74,10 +75,10 @@ "OPTION": { "AUTO": "Automatic", "AUTO_HELP": "ScreenCopy GPU if supported,\notherwise PipeWire GPU.", - "EYE_PINCH": "Eye + pinch", - "HMD_PINCH": "HMD + pinch", "EYE_ONLY": "Eye only", + "EYE_PINCH": "Eye + pinch", "HMD_ONLY": "HMD only", + "HMD_PINCH": "HMD + pinch", "NONE": "None", "PIPEWIRE_HELP": "Fast GPU capture,\nstandard on all desktops.", "PW_FALLBACK_HELP": "Slow method with high CPU usage.\nTry in case PipeWire GPU doesn't work", @@ -85,6 +86,7 @@ "SCREENCOPY_HELP": "Slow, no screen share popups.\nWorks on: Hyprland, Niri, River, Sway" }, "POINTER_LERP_FACTOR": "Pointer smoothing", + "REQUIRES_RESTART": "Requires restart", "RESET_PLAYSPACE": "Reset playspace", "RESET_PLAYSPACE_HELP": "Clear the stage space offset.", "RESTART_SOFTWARE": "Restart software", @@ -148,8 +150,8 @@ "RESOLUTION": "Resolution" }, "PROCESS": { - "STOP": "Stop", - "FORCE_KILL": "Force-kill" + "FORCE_KILL": "Force-kill", + "STOP": "Stop" }, "PROCESS_LIST": "Process list", "REFRESH": "Refresh", diff --git a/dash-frontend/assets/lang/es.json b/dash-frontend/assets/lang/es.json index 5841c0b..b862203 100644 --- a/dash-frontend/assets/lang/es.json +++ b/dash-frontend/assets/lang/es.json @@ -10,7 +10,6 @@ "APP_SETTINGS": { "HIDE_USERNAME": "Ocultar nombre de usuario", "OPAQUE_BACKGROUND": "Fondo opaco", - "WLX": {}, "LOOK_AND_FEEL": "Apariencia y estilo", "HIDE_GRAB_HELP": "Ocultar ayuda para agarrar", "ANIMATION_SPEED": "Velocidad de animación de la IU", @@ -89,7 +88,9 @@ "RESET_PLAYSPACE": "Restablecer espacio de juego", "RESET_PLAYSPACE_HELP": "Borrar el desplazamiento del espacio de juego.", "BLOCK_POSES_ON_KBD_INTERACTION": "Bloquear poses al interactuar con el teclado", - "BLOCK_POSES_ON_KBD_INTERACTION_HELP": "Bloquea que el juego reciba poses cuando el teclado está sobre él y 'Bloquear entrada del juego' está habilitado" + "BLOCK_POSES_ON_KBD_INTERACTION_HELP": "Bloquea que el juego reciba poses cuando el teclado está sobre él y 'Bloquear entrada del juego' está habilitado", + "LANGUAGE": "Idioma", + "REQUIRES_RESTART": "Requiere reinicio" }, "HELLO": "¡Hola!", "AUDIO": { diff --git a/dash-frontend/assets/lang/it.json b/dash-frontend/assets/lang/it.json index 93023a4..51ab5c4 100644 --- a/dash-frontend/assets/lang/it.json +++ b/dash-frontend/assets/lang/it.json @@ -109,7 +109,9 @@ "RESET_PLAYSPACE": "Ripristina playspace", "RESET_PLAYSPACE_HELP": "Cancella l'offset dello spazio di gioco.", "BLOCK_POSES_ON_KBD_INTERACTION": "Blocca le pose durante l'interazione con la tastiera", - "BLOCK_POSES_ON_KBD_INTERACTION_HELP": "Impedisce al gioco di ricevere pose quando la tastiera è evidenziata e 'Blocca input di gioco' è abilitato" + "BLOCK_POSES_ON_KBD_INTERACTION_HELP": "Impedisce al gioco di ricevere pose quando la tastiera è evidenziata e 'Blocca input di gioco' è abilitato", + "LANGUAGE": "Lingua", + "REQUIRES_RESTART": "Richiede riavvio" }, "APPLICATION_LAUNCHER": "Lanciatore applicazioni", "APPLICATION_STARTED": "Applicazione avviata", diff --git a/dash-frontend/assets/lang/ja.json b/dash-frontend/assets/lang/ja.json index a2992ac..15322fc 100644 --- a/dash-frontend/assets/lang/ja.json +++ b/dash-frontend/assets/lang/ja.json @@ -10,7 +10,6 @@ "APP_SETTINGS": { "HIDE_USERNAME": "ユーザー名を非表示", "OPAQUE_BACKGROUND": "不透明な背景", - "WLX": {}, "LOOK_AND_FEEL": "外観", "HIDE_GRAB_HELP": "グリップ動作中にのヘルプを非表示", "ANIMATION_SPEED": "UIアニメーション速度", @@ -89,7 +88,9 @@ "RESET_PLAYSPACE": "プレイエリアをリセット", "RESET_PLAYSPACE_HELP": "プレイエリアのオフセットをクリアします。", "BLOCK_POSES_ON_KBD_INTERACTION": "キーボード操作時のポーズをブロック", - "BLOCK_POSES_ON_KBD_INTERACTION_HELP": "キーボードがホバーされ、「ゲーム入力をブロック」が有効になっている場合、ゲームがポーズを受信することをブロックします" + "BLOCK_POSES_ON_KBD_INTERACTION_HELP": "キーボードがホバーされ、「ゲーム入力をブロック」が有効になっている場合、ゲームがポーズを受信することをブロックします", + "LANGUAGE": "言語", + "REQUIRES_RESTART": "再起動が必要です" }, "HELLO": "こんにちは!", "AUDIO": { diff --git a/dash-frontend/assets/lang/pl.json b/dash-frontend/assets/lang/pl.json index b021dd8..9e86daa 100644 --- a/dash-frontend/assets/lang/pl.json +++ b/dash-frontend/assets/lang/pl.json @@ -5,7 +5,6 @@ "APP_SETTINGS": { "HIDE_USERNAME": "Ukryj nazwę użytkownika", "OPAQUE_BACKGROUND": "Nieprzezroczyste tło", - "WLX": {}, "LOOK_AND_FEEL": "Wygląd i działanie", "HIDE_GRAB_HELP": "Ukryj pomoc dotyczącą chwytania", "ANIMATION_SPEED": "Prędkość animacji UI", @@ -34,7 +33,6 @@ "XR_CLICK_SENSITIVITY": "Czułość kliknięć XR", "XR_CLICK_SENSITIVITY_RELEASE": "Czułość zwalniania XR", "CLICK_FREEZE_TIME_MS": "Czas zamrożenia po kliknięciu (ms)", - "MISC": "Różne", "XWAYLAND_BY_DEFAULT": "Uruchamiaj aplikacje domyślnie w trybie kompatybilności", "UPRIGHT_SCREEN_FIX": "Naprawa pozycji ekranu", "DOUBLE_CURSOR_FIX": "Naprawa podwójnego kursora", @@ -55,7 +53,7 @@ "CLEAR_SAVED_STATE": "Wyczyść zapisany stan", "CLEAR_PIPEWIRE_TOKENS": "Wyczyść tokeny PipeWire", "DELETE_ALL_CONFIGS": "Wyczyść konfigurację", - "RESTART_SOFTWARE": "Uruchom ponownie oprogramowanie", + "RESTART_SOFTWARE": "Restartuj WayVR", "CLEAR_SAVED_STATE_HELP": "Zresetuj zestawy i pozycje nakładek", "CLEAR_PIPEWIRE_TOKENS_HELP": "Zapytaj o wybór ekranu przy następnym uruchomieniu", "DELETE_ALL_CONFIGS_HELP": "Usuń wszystkie pliki konfiguracyjne z katalogu conf.d", @@ -72,19 +70,22 @@ "SCREENCOPY_GPU_HELP": "Szybkie działanie, brak wyskakujących okien z informacją o udostępnianiu ekranu.\nDziała na: Hyprland, Niri, River, Sway", "SCREENCOPY_HELP": "Wolne, bez wyskakujących okienek udostępniania ekranu.\nDziała na: Hyprland, Niri, River, Sway", "NONE": "Brak", - "HMD_PINCH": "HMD + szczyknięcie", + "HMD_PINCH": "HMD + ściśnięcie placami", "EYE_PINCH": "Ściśnięcie palcami + oko", "EYE_ONLY": "Tylko oko", "HMD_ONLY": "Tylko HMD" }, - "AUTOSTART_APPS": "Aplikacje do uruchomienia przy starcie", + "AUTOSTART_APPS": "Aplikacje auto-start", "HANDSFREE_POINTER": "Tryb bez użycia rąk", "HANDSFREE_POINTER_HELP": "Wejście do użycia, gdy kontrolery ruchu\nsą niedostępne. Lewy szczyptak to chwyt,\nprawy to kliknięcie.", "UI_GRADIENT_INTENSITY": "Intensywność gradientu UI", "RESET_PLAYSPACE": "Zresetuj przestrzeń gry", "RESET_PLAYSPACE_HELP": "Wyczyść przesunięcie przestrzeni gry.", "BLOCK_POSES_ON_KBD_INTERACTION": "Blokuj pozy podczas interakcji z klawiaturą", - "BLOCK_POSES_ON_KBD_INTERACTION_HELP": "Blokuje odbieranie póz przez grę, gdy kursor myszy znajduje się nad klawiaturą i włączona jest opcja 'Blokuj dane wejściowe z gry'" + "BLOCK_POSES_ON_KBD_INTERACTION_HELP": "Blokuje odbieranie póz przez grę, gdy kursor myszy znajduje się nad klawiaturą i włączona jest opcja 'Blokuj dane wejściowe z gry'", + "LANGUAGE": "Język", + "REQUIRES_RESTART": "Wymaga restartu", + "MISC": "Różne" }, "APPLICATION_LAUNCHER": "Uruchamiacz aplikacji", "APPLICATIONS": "Aplikacje", diff --git a/dash-frontend/assets/lang/zh_CN.json b/dash-frontend/assets/lang/zh_CN.json index 32996e4..81f6b04 100644 --- a/dash-frontend/assets/lang/zh_CN.json +++ b/dash-frontend/assets/lang/zh_CN.json @@ -109,7 +109,9 @@ "RESET_PLAYSPACE": "重置游戏空间", "RESET_PLAYSPACE_HELP": "清除舞台空间偏移。", "BLOCK_POSES_ON_KBD_INTERACTION": "与键盘交互时阻止姿势", - "BLOCK_POSES_ON_KBD_INTERACTION_HELP": "当键盘悬停且启用“阻止游戏输入”时,阻止游戏接收姿势" + "BLOCK_POSES_ON_KBD_INTERACTION_HELP": "当键盘悬停且启用“阻止游戏输入”时,阻止游戏接收姿势", + "LANGUAGE": "语言", + "REQUIRES_RESTART": "需要重启" }, "APPLICATION_LAUNCHER": "应用启动器", "APPLICATION_STARTED": "应用已启动", diff --git a/dash-frontend/src/frontend.rs b/dash-frontend/src/frontend.rs index 070d1ce..3c33435 100644 --- a/dash-frontend/src/frontend.rs +++ b/dash-frontend/src/frontend.rs @@ -18,6 +18,7 @@ use wgui::{ use wlx_common::{ audio, dash_interface::{BoxDashInterface, RecenterMode}, + locale::WayVRLangProvider, timestep::{self, Timestep}, }; @@ -81,8 +82,9 @@ pub struct FrontendUpdateResult { pub sounds_to_play: Vec, } -pub struct InitParams { +pub struct InitParams<'a, T> { pub interface: BoxDashInterface, + pub lang_provider: &'a WayVRLangProvider, pub has_monado: bool, } @@ -118,6 +120,7 @@ impl Frontend { let globals = WguiGlobals::new( assets, + params.lang_provider, wgui::globals::Defaults::default(), &WguiFontConfig { binaries: vec![&font_binary_regular, &font_binary_bold, &font_binary_light], diff --git a/dash-frontend/src/tab/settings.rs b/dash-frontend/src/tab/settings.rs index c678725..cdb8e97 100644 --- a/dash-frontend/src/tab/settings.rs +++ b/dash-frontend/src/tab/settings.rs @@ -10,14 +10,20 @@ use wgui::{ slider::ComponentSlider, tabs::ComponentTabs, }, + drawing, event::{CallbackDataCommon, EventAlterables}, globals::WguiGlobals, i18n::Translation, layout::{Layout, WidgetID}, log::LogErr, parser::{Fetchable, ParseDocumentParams, ParserState}, + renderer_vk::text::{FontWeight, TextStyle}, + taffy::{self, prelude::length}, task::Tasks, - widget::label::WidgetLabel, + widget::{ + div::WidgetDiv, + label::{WidgetLabel, WidgetLabelParams}, + }, windowing::context_menu::{self, Blueprint, ContextMenu, TickResult}, }; use wlx_common::{config::GeneralConfig, config_io::ConfigRoot, dash_interface::RecenterMode}; @@ -202,46 +208,48 @@ impl Tab for TabSettings { } } +// Sorted alphabetically #[allow(clippy::enum_variant_names)] #[derive(Clone, Copy, AsRefStr, EnumString)] enum SettingType { - UiAnimationSpeed, - UiGradientIntensity, - UiRoundMultiplier, - InvertScrollDirectionX, - InvertScrollDirectionY, - ScrollSpeed, - LongPressDuration, - NotificationsEnabled, - NotificationsSoundEnabled, - KeyboardSoundEnabled, - UprightScreenFix, - DoubleCursorFix, - SetsOnWatch, - HideGrabHelp, - XrClickSensitivity, - XrClickSensitivityRelease, AllowSliding, - ClickFreezeTimeMs, - FocusFollowsMouseMode, - LeftHandedMouse, BlockGameInput, BlockGameInputIgnoreWatch, BlockPosesOnKbdInteraction, - SpaceDragMultiplier, - UseSkybox, - UsePassthrough, - ScreenRenderDown, + CaptureMethod, + ClickFreezeTimeMs, + Clock12h, + DoubleCursorFix, + FocusFollowsMouseMode, + HandsfreePointer, + HideGrabHelp, + HideUsername, + InvertScrollDirectionX, + InvertScrollDirectionY, + KeyboardMiddleClick, + KeyboardSoundEnabled, + Language, + LeftHandedMouse, + LongPressDuration, + NotificationsEnabled, + NotificationsSoundEnabled, + OpaqueBackground, PointerLerpFactor, + ScreenRenderDown, + ScrollSpeed, + SetsOnWatch, + SpaceDragMultiplier, SpaceDragUnlocked, SpaceRotateUnlocked, - Clock12h, - HideUsername, - OpaqueBackground, + UiAnimationSpeed, + UiGradientIntensity, + UiRoundMultiplier, + UprightScreenFix, + UsePassthrough, + UseSkybox, + XrClickSensitivity, + XrClickSensitivityRelease, XwaylandByDefault, - CaptureMethod, - KeyboardMiddleClick, - HandsfreePointer, } impl SettingType { @@ -309,6 +317,9 @@ impl SettingType { Self::HandsfreePointer => { config.handsfree_pointer = wlx_common::config::HandsfreePointer::from_str(value).expect("Invalid enum value!") } + Self::Language => { + config.language = Some(wlx_common::locale::Language::from_str(value).expect("Invalid enum value!")) + } _ => panic!("Requested enum for non-enum SettingType"), } } @@ -318,6 +329,10 @@ impl SettingType { Self::CaptureMethod => Self::get_enum_title_inner(config.capture_method), Self::KeyboardMiddleClick => Self::get_enum_title_inner(config.keyboard_middle_click_mode), Self::HandsfreePointer => Self::get_enum_title_inner(config.handsfree_pointer), + Self::Language => match &config.language { + Some(lang) => Self::get_enum_title_inner(*lang), + None => Translation::from_translation_key("APP_SETTINGS.OPTION.AUTO"), + }, _ => panic!("Requested enum for non-enum SettingType"), } } @@ -341,70 +356,71 @@ impl SettingType { } /// Ok is translation, Err is raw text + /// `match` sorted alphabetically fn get_translation(self) -> Result<&'static str, &'static str> { match self { - Self::UiAnimationSpeed => Ok("APP_SETTINGS.ANIMATION_SPEED"), - Self::UiGradientIntensity => Ok("APP_SETTINGS.UI_GRADIENT_INTENSITY"), - Self::UiRoundMultiplier => Ok("APP_SETTINGS.ROUND_MULTIPLIER"), - Self::InvertScrollDirectionX => Ok("APP_SETTINGS.INVERT_SCROLL_DIRECTION_X"), - Self::InvertScrollDirectionY => Ok("APP_SETTINGS.INVERT_SCROLL_DIRECTION_Y"), - Self::ScrollSpeed => Ok("APP_SETTINGS.SCROLL_SPEED"), - Self::LongPressDuration => Ok("APP_SETTINGS.LONG_PRESS_DURATION"), - Self::NotificationsEnabled => Ok("APP_SETTINGS.NOTIFICATIONS_ENABLED"), - Self::NotificationsSoundEnabled => Ok("APP_SETTINGS.NOTIFICATIONS_SOUND_ENABLED"), - Self::KeyboardSoundEnabled => Ok("APP_SETTINGS.KEYBOARD_SOUND_ENABLED"), - Self::UprightScreenFix => Ok("APP_SETTINGS.UPRIGHT_SCREEN_FIX"), - Self::DoubleCursorFix => Ok("APP_SETTINGS.DOUBLE_CURSOR_FIX"), - Self::SetsOnWatch => Ok("APP_SETTINGS.SETS_ON_WATCH"), - Self::HideGrabHelp => Ok("APP_SETTINGS.HIDE_GRAB_HELP"), - Self::XrClickSensitivity => Ok("APP_SETTINGS.XR_CLICK_SENSITIVITY"), - Self::XrClickSensitivityRelease => Ok("APP_SETTINGS.XR_CLICK_SENSITIVITY_RELEASE"), Self::AllowSliding => Ok("APP_SETTINGS.ALLOW_SLIDING"), - Self::ClickFreezeTimeMs => Ok("APP_SETTINGS.CLICK_FREEZE_TIME_MS"), - Self::FocusFollowsMouseMode => Ok("APP_SETTINGS.FOCUS_FOLLOWS_MOUSE_MODE"), - Self::LeftHandedMouse => Ok("APP_SETTINGS.LEFT_HANDED_MOUSE"), Self::BlockGameInput => Ok("APP_SETTINGS.BLOCK_GAME_INPUT"), Self::BlockGameInputIgnoreWatch => Ok("APP_SETTINGS.BLOCK_GAME_INPUT_IGNORE_WATCH"), Self::BlockPosesOnKbdInteraction => Ok("APP_SETTINGS.BLOCK_POSES_ON_KBD_INTERACTION"), - Self::SpaceDragMultiplier => Ok("APP_SETTINGS.SPACE_DRAG_MULTIPLIER"), - Self::UseSkybox => Ok("APP_SETTINGS.USE_SKYBOX"), - Self::UsePassthrough => Ok("APP_SETTINGS.USE_PASSTHROUGH"), - Self::ScreenRenderDown => Ok("APP_SETTINGS.SCREEN_RENDER_DOWN"), + Self::CaptureMethod => Ok("APP_SETTINGS.CAPTURE_METHOD"), + Self::ClickFreezeTimeMs => Ok("APP_SETTINGS.CLICK_FREEZE_TIME_MS"), + Self::Clock12h => Ok("APP_SETTINGS.CLOCK_12H"), + Self::DoubleCursorFix => Ok("APP_SETTINGS.DOUBLE_CURSOR_FIX"), + Self::FocusFollowsMouseMode => Ok("APP_SETTINGS.FOCUS_FOLLOWS_MOUSE_MODE"), + Self::HandsfreePointer => Ok("APP_SETTINGS.HANDSFREE_POINTER"), + Self::HideGrabHelp => Ok("APP_SETTINGS.HIDE_GRAB_HELP"), + Self::HideUsername => Ok("APP_SETTINGS.HIDE_USERNAME"), + Self::InvertScrollDirectionX => Ok("APP_SETTINGS.INVERT_SCROLL_DIRECTION_X"), + Self::InvertScrollDirectionY => Ok("APP_SETTINGS.INVERT_SCROLL_DIRECTION_Y"), + Self::KeyboardMiddleClick => Ok("APP_SETTINGS.KEYBOARD_MIDDLE_CLICK"), + Self::KeyboardSoundEnabled => Ok("APP_SETTINGS.KEYBOARD_SOUND_ENABLED"), + Self::Language => Ok("APP_SETTINGS.LANGUAGE"), + Self::LeftHandedMouse => Ok("APP_SETTINGS.LEFT_HANDED_MOUSE"), + Self::LongPressDuration => Ok("APP_SETTINGS.LONG_PRESS_DURATION"), + Self::NotificationsEnabled => Ok("APP_SETTINGS.NOTIFICATIONS_ENABLED"), + Self::NotificationsSoundEnabled => Ok("APP_SETTINGS.NOTIFICATIONS_SOUND_ENABLED"), + Self::OpaqueBackground => Ok("APP_SETTINGS.OPAQUE_BACKGROUND"), Self::PointerLerpFactor => Ok("APP_SETTINGS.POINTER_LERP_FACTOR"), + Self::ScreenRenderDown => Ok("APP_SETTINGS.SCREEN_RENDER_DOWN"), + Self::ScrollSpeed => Ok("APP_SETTINGS.SCROLL_SPEED"), + Self::SetsOnWatch => Ok("APP_SETTINGS.SETS_ON_WATCH"), + Self::SpaceDragMultiplier => Ok("APP_SETTINGS.SPACE_DRAG_MULTIPLIER"), Self::SpaceDragUnlocked => Ok("APP_SETTINGS.SPACE_DRAG_UNLOCKED"), Self::SpaceRotateUnlocked => Ok("APP_SETTINGS.SPACE_ROTATE_UNLOCKED"), - Self::Clock12h => Ok("APP_SETTINGS.CLOCK_12H"), - Self::HideUsername => Ok("APP_SETTINGS.HIDE_USERNAME"), - Self::OpaqueBackground => Ok("APP_SETTINGS.OPAQUE_BACKGROUND"), + Self::UiAnimationSpeed => Ok("APP_SETTINGS.ANIMATION_SPEED"), + Self::UiGradientIntensity => Ok("APP_SETTINGS.UI_GRADIENT_INTENSITY"), + Self::UiRoundMultiplier => Ok("APP_SETTINGS.ROUND_MULTIPLIER"), + Self::UprightScreenFix => Ok("APP_SETTINGS.UPRIGHT_SCREEN_FIX"), + Self::UsePassthrough => Ok("APP_SETTINGS.USE_PASSTHROUGH"), + Self::UseSkybox => Ok("APP_SETTINGS.USE_SKYBOX"), + Self::XrClickSensitivity => Ok("APP_SETTINGS.XR_CLICK_SENSITIVITY"), + Self::XrClickSensitivityRelease => Ok("APP_SETTINGS.XR_CLICK_SENSITIVITY_RELEASE"), Self::XwaylandByDefault => Ok("APP_SETTINGS.XWAYLAND_BY_DEFAULT"), - Self::CaptureMethod => Ok("APP_SETTINGS.CAPTURE_METHOD"), - Self::KeyboardMiddleClick => Ok("APP_SETTINGS.KEYBOARD_MIDDLE_CLICK"), - Self::HandsfreePointer => Ok("APP_SETTINGS.HANDSFREE_POINTER"), } } + /// `match` sorted alphabetically fn get_tooltip(self) -> Option<&'static str> { match self { - Self::UprightScreenFix => Some("APP_SETTINGS.UPRIGHT_SCREEN_FIX_HELP"), - Self::DoubleCursorFix => Some("APP_SETTINGS.DOUBLE_CURSOR_FIX_HELP"), - Self::XrClickSensitivity => Some("APP_SETTINGS.XR_CLICK_SENSITIVITY_HELP"), - Self::XrClickSensitivityRelease => Some("APP_SETTINGS.XR_CLICK_SENSITIVITY_RELEASE_HELP"), - Self::FocusFollowsMouseMode => Some("APP_SETTINGS.FOCUS_FOLLOWS_MOUSE_MODE_HELP"), - Self::LeftHandedMouse => Some("APP_SETTINGS.LEFT_HANDED_MOUSE_HELP"), Self::BlockGameInput => Some("APP_SETTINGS.BLOCK_GAME_INPUT_HELP"), Self::BlockGameInputIgnoreWatch => Some("APP_SETTINGS.BLOCK_GAME_INPUT_IGNORE_WATCH_HELP"), Self::BlockPosesOnKbdInteraction => Some("APP_SETTINGS.BLOCK_POSES_ON_KBD_INTERACTION_HELP"), - Self::UseSkybox => Some("APP_SETTINGS.USE_SKYBOX_HELP"), - Self::UsePassthrough => Some("APP_SETTINGS.USE_PASSTHROUGH_HELP"), - Self::ScreenRenderDown => Some("APP_SETTINGS.SCREEN_RENDER_DOWN_HELP"), Self::CaptureMethod => Some("APP_SETTINGS.CAPTURE_METHOD_HELP"), - Self::KeyboardMiddleClick => Some("APP_SETTINGS.KEYBOARD_MIDDLE_CLICK_HELP"), + Self::DoubleCursorFix => Some("APP_SETTINGS.DOUBLE_CURSOR_FIX_HELP"), Self::HandsfreePointer => Some("APP_SETTINGS.HANDSFREE_POINTER_HELP"), + Self::KeyboardMiddleClick => Some("APP_SETTINGS.KEYBOARD_MIDDLE_CLICK_HELP"), + Self::LeftHandedMouse => Some("APP_SETTINGS.LEFT_HANDED_MOUSE_HELP"), + Self::ScreenRenderDown => Some("APP_SETTINGS.SCREEN_RENDER_DOWN_HELP"), + Self::UprightScreenFix => Some("APP_SETTINGS.UPRIGHT_SCREEN_FIX_HELP"), + Self::UsePassthrough => Some("APP_SETTINGS.USE_PASSTHROUGH_HELP"), + Self::UseSkybox => Some("APP_SETTINGS.USE_SKYBOX_HELP"), + Self::XrClickSensitivity => Some("APP_SETTINGS.XR_CLICK_SENSITIVITY_HELP"), + Self::XrClickSensitivityRelease => Some("APP_SETTINGS.XR_CLICK_SENSITIVITY_RELEASE_HELP"), _ => None, } } - //TODO: incorporate this fn requires_restart(self) -> bool { match self { Self::UiAnimationSpeed @@ -412,6 +428,7 @@ impl SettingType { | Self::UprightScreenFix | Self::DoubleCursorFix | Self::ScreenRenderDown + | Self::Language | Self::CaptureMethod => true, _ => false, } @@ -426,6 +443,42 @@ impl SettingType { } } +// creates a simple div with horizontal, centered flow +fn horiz_cell(layout: &mut Layout, parent: WidgetID) -> anyhow::Result { + let (pair, _) = layout.add_child( + parent, + WidgetDiv::create(), + taffy::Style { + flex_direction: taffy::FlexDirection::Row, + align_items: Some(taffy::AlignItems::Center), + gap: length(8.0), + ..Default::default() + }, + )?; + + Ok(pair.id) +} + +fn mount_requires_restart(layout: &mut Layout, parent: WidgetID) -> anyhow::Result<()> { + let content = Translation::from_translation_key("APP_SETTINGS.REQUIRES_RESTART"); + let label = WidgetLabel::create( + &mut layout.state.globals.get(), + WidgetLabelParams { + content, + style: TextStyle { + wrap: false, + color: Some(drawing::Color::new(1.0, 0.5, 0.5, 1.0)), + weight: Some(FontWeight::Bold), + size: Some(10.0), + ..Default::default() + }, + }, + ); + + layout.add_child(parent, label, Default::default())?; + Ok(()) +} + macro_rules! category { ($pe:expr, $root:expr, $translation:expr, $icon:expr) => {{ let id = $pe.idx.to_string(); @@ -464,9 +517,15 @@ macro_rules! checkbox { let checked = if *$setting.mut_bool($mp.config) { "1" } else { "0" }; params.insert(Rc::from("checked"), Rc::from(checked)); + let id_cell = horiz_cell($mp.layout, $root)?; + $mp .parser_state - .instantiate_template($mp.doc_params, "CheckBoxSetting", $mp.layout, $root, params)?; + .instantiate_template($mp.doc_params, "CheckBoxSetting", $mp.layout, id_cell, params)?; + + if $setting.requires_restart() { + mount_requires_restart($mp.layout, id_cell)?; + } let checkbox = $mp.parser_state.fetch_component_as::(&id)?; checkbox.on_toggle(Box::new({ @@ -502,9 +561,15 @@ macro_rules! slider_f32 { params.insert(Rc::from("max"), Rc::from($max.to_string())); params.insert(Rc::from("step"), Rc::from($step.to_string())); + let id_cell = horiz_cell($mp.layout, $root)?; + $mp .parser_state - .instantiate_template($mp.doc_params, "SliderSetting", $mp.layout, $root, params)?; + .instantiate_template($mp.doc_params, "SliderSetting", $mp.layout, id_cell, params)?; + + if $setting.requires_restart() { + mount_requires_restart($mp.layout, id_cell)?; + } let slider = $mp.parser_state.fetch_component_as::(&id)?; slider.on_value_changed(Box::new({ @@ -534,6 +599,8 @@ macro_rules! slider_i32 { params.insert(Rc::from("tooltip"), Rc::from(tooltip)); } + let id_cell = horiz_cell($mp.layout, $root)?; + let value = $setting.mut_i32($mp.config).to_string(); params.insert(Rc::from("value"), Rc::from(value)); params.insert(Rc::from("min"), Rc::from($min.to_string())); @@ -542,7 +609,11 @@ macro_rules! slider_i32 { $mp .parser_state - .instantiate_template($mp.doc_params, "SliderSetting", $mp.layout, $root, params)?; + .instantiate_template($mp.doc_params, "SliderSetting", $mp.layout, id_cell, params)?; + + if $setting.requires_restart() { + mount_requires_restart($mp.layout, id_cell)?; + } let slider = $mp.parser_state.fetch_component_as::(&id)?; slider.on_value_changed(Box::new({ @@ -556,7 +627,7 @@ macro_rules! slider_i32 { } macro_rules! dropdown { - ($mp:expr, $root:expr, $setting:expr, $options:expr) => { + ($mp:expr /* `MacroParams` struct */, $root:expr, $setting:expr, $options:expr) => { let id = $mp.idx.to_string(); $mp.idx += 1; @@ -572,9 +643,15 @@ macro_rules! dropdown { params.insert(Rc::from("tooltip"), Rc::from(tooltip)); } + let id_cell = horiz_cell($mp.layout, $root)?; + $mp .parser_state - .instantiate_template($mp.doc_params, "DropdownButton", $mp.layout, $root, params)?; + .instantiate_template($mp.doc_params, "DropdownButton", $mp.layout, id_cell, params)?; + + if $setting.requires_restart() { + mount_requires_restart($mp.layout, id_cell)?; + } let setting_str = $setting.as_ref(); let title = $setting.get_enum_title($mp.config); @@ -709,6 +786,7 @@ impl TabSettings { match name { TabNameEnum::LookAndFeel => { let c = category!(mp, root, "APP_SETTINGS.LOOK_AND_FEEL", "dashboard/palette.svg")?; + dropdown!(mp, c, SettingType::Language, wlx_common::locale::Language::VARIANTS); checkbox!(mp, c, SettingType::OpaqueBackground); checkbox!(mp, c, SettingType::HideUsername); checkbox!(mp, c, SettingType::HideGrabHelp); diff --git a/uidev/src/testbed/testbed_any.rs b/uidev/src/testbed/testbed_any.rs index ab182ea..fc3a02e 100644 --- a/uidev/src/testbed/testbed_any.rs +++ b/uidev/src/testbed/testbed_any.rs @@ -12,6 +12,7 @@ use wgui::{ layout::{Layout, LayoutParams, LayoutUpdateParams}, parser::{ParseDocumentParams, ParserState}, }; +use wlx_common::locale::WayVRLangProvider; pub struct TestbedAny { pub layout: Layout, @@ -28,8 +29,11 @@ impl TestbedAny { AssetPath::BuiltIn(&format!("gui/{name}.xml")) }; + let lang_provider = WayVRLangProvider::default(); + let globals = WguiGlobals::new( assets, + &lang_provider, wgui::globals::Defaults::default(), &WguiFontConfig::default(), PathBuf::new(), // cwd diff --git a/uidev/src/testbed/testbed_dashboard.rs b/uidev/src/testbed/testbed_dashboard.rs index 60922e1..a051ad6 100644 --- a/uidev/src/testbed/testbed_dashboard.rs +++ b/uidev/src/testbed/testbed_dashboard.rs @@ -1,7 +1,7 @@ use crate::testbed::{Testbed, TestbedUpdateParams}; use dash_frontend::frontend::{self, FrontendUpdateParams}; use wgui::layout::Layout; -use wlx_common::dash_interface_emulated::DashInterfaceEmulated; +use wlx_common::{dash_interface_emulated::DashInterfaceEmulated, locale::WayVRLangProvider}; pub struct TestbedDashboard { frontend: frontend::Frontend<()>, @@ -10,11 +10,13 @@ pub struct TestbedDashboard { impl TestbedDashboard { pub fn new() -> anyhow::Result { let interface = DashInterfaceEmulated::new(); + let lang_provider = WayVRLangProvider::default(); let frontend = frontend::Frontend::new( frontend::InitParams { interface: Box::new(interface), has_monado: true, + lang_provider: &lang_provider, }, &mut (), )?; diff --git a/uidev/src/testbed/testbed_generic.rs b/uidev/src/testbed/testbed_generic.rs index 1e4b25e..6388bf5 100644 --- a/uidev/src/testbed/testbed_generic.rs +++ b/uidev/src/testbed/testbed_generic.rs @@ -27,6 +27,7 @@ use wgui::{ window::{WguiWindow, WguiWindowParams, WguiWindowParamsExtra}, }, }; +use wlx_common::locale::WayVRLangProvider; #[derive(Clone)] pub enum TestbedTask { @@ -85,8 +86,11 @@ impl TestbedGeneric { } pub fn new(assets: Box) -> anyhow::Result { + let lang_provider = WayVRLangProvider::default(); + let globals = WguiGlobals::new( assets, + &lang_provider, wgui::globals::Defaults::default(), &WguiFontConfig::default(), PathBuf::new(), // cwd diff --git a/wayvr/src/config.rs b/wayvr/src/config.rs index da2a811..2a797e1 100644 --- a/wayvr/src/config.rs +++ b/wayvr/src/config.rs @@ -10,6 +10,7 @@ use wlx_common::{ SerializedWindowStates, }, config_io, + locale::Language, overlays::BackendAttribValue, }; @@ -143,6 +144,7 @@ pub struct AutoSettings { pub keyboard_middle_click_mode: AltModifier, pub autostart_apps: Vec, pub handsfree_pointer: HandsfreePointer, + pub language: Option, } fn get_settings_path() -> PathBuf { @@ -192,6 +194,7 @@ pub fn save_settings(config: &GeneralConfig) -> anyhow::Result<()> { keyboard_middle_click_mode: config.keyboard_middle_click_mode, autostart_apps: config.autostart_apps.clone(), handsfree_pointer: config.handsfree_pointer, + language: config.language, }; let json = serde_json::to_string_pretty(&conf).unwrap(); // want panic diff --git a/wayvr/src/overlays/dashboard.rs b/wayvr/src/overlays/dashboard.rs index fc67bba..c6ea206 100644 --- a/wayvr/src/overlays/dashboard.rs +++ b/wayvr/src/overlays/dashboard.rs @@ -17,6 +17,7 @@ use wgui::{ }; use wlx_common::{ dash_interface::{self, DashInterface, RecenterMode}, + locale::WayVRLangProvider, overlays::{BackendAttrib, BackendAttribValue}, }; use wlx_common::{ @@ -79,6 +80,7 @@ impl DashFrontend { let frontend = frontend::Frontend::new( frontend::InitParams { interface: Box::new(interface), + lang_provider: &WayVRLangProvider::from_config(&app.session.config), has_monado: matches!(app.xr_backend, XrBackend::OpenXR), }, app, diff --git a/wayvr/src/state.rs b/wayvr/src/state.rs index f2639af..0547549 100644 --- a/wayvr/src/state.rs +++ b/wayvr/src/state.rs @@ -7,6 +7,7 @@ use wgui::{ drawing, font_config::WguiFontConfig, gfx::WGfx, globals::WguiGlobals, parser::parse_color_hex, renderer_vk::context::SharedContext as WSharedContext, }; +use wlx_common::locale::WayVRLangProvider; use wlx_common::{ audio, config::GeneralConfig, @@ -138,6 +139,8 @@ impl AppState { let mut desktop_finder = DesktopFinder::new(); desktop_finder.refresh(); + let lang_provider = WayVRLangProvider::from_config(&session.config); + Ok(Self { session, tasks, @@ -153,6 +156,7 @@ impl AppState { anchor_grabbed: false, wgui_globals: WguiGlobals::new( assets, + &lang_provider, defaults, &WguiFontConfig::default(), get_config_file_path(&theme), diff --git a/wgui/src/assets.rs b/wgui/src/assets.rs index aa90837..e1fb451 100644 --- a/wgui/src/assets.rs +++ b/wgui/src/assets.rs @@ -3,6 +3,8 @@ use std::ffi::OsStr; use std::io::Read; use std::path::{Component, Path, PathBuf}; +use crate::i18n::LangsList; + #[derive(Debug, Clone, Copy)] pub enum AssetPath<'a> { WguiInternal(&'a str), // tied to internal wgui AssetProvider. Used internally @@ -83,6 +85,11 @@ impl Default for AssetPathOwned { } } +pub trait LangProvider { + fn langs_list(&self) -> &dyn LangsList; + fn forced_lang(&self) -> Option<&str>; +} + pub trait AssetProvider { fn load_from_path(&mut self, path: &str) -> anyhow::Result>; fn load_from_path_gzip(&mut self, path: &str) -> anyhow::Result> { diff --git a/wgui/src/globals.rs b/wgui/src/globals.rs index 0c3ab11..11c7fc5 100644 --- a/wgui/src/globals.rs +++ b/wgui/src/globals.rs @@ -10,7 +10,7 @@ use anyhow::Context; use regex::Regex; use crate::{ - assets::{AssetPath, AssetProvider}, + assets::{AssetPath, AssetProvider, LangProvider}, assets_internal, drawing, font_config::{WguiFontConfig, WguiFontSystem}, i18n::I18n, @@ -66,11 +66,12 @@ pub struct WguiGlobals(Rc>); impl WguiGlobals { pub fn new( mut assets_builtin: Box, + lang_provider: &dyn LangProvider, defaults: Defaults, font_config: &WguiFontConfig, asset_folder: PathBuf, ) -> anyhow::Result { - let i18n_builtin = I18n::new(&mut assets_builtin)?; + let i18n_builtin = I18n::new(assets_builtin.as_mut(), lang_provider)?; let assets_internal = Box::new(assets_internal::AssetInternal {}); Ok(Self(Rc::new(RefCell::new(Globals { diff --git a/wgui/src/i18n.rs b/wgui/src/i18n.rs index 3fc2eb7..e2384be 100644 --- a/wgui/src/i18n.rs +++ b/wgui/src/i18n.rs @@ -2,7 +2,7 @@ use std::{fmt::Display, rc::Rc, str}; use anyhow::Context; -use crate::assets::AssetProvider; +use crate::assets::{AssetProvider, LangProvider}; // a string which optionally has translation key in it // it will hopefully support dynamic language changing soon @@ -63,50 +63,76 @@ pub struct Locale { matched: String, } +pub trait LangsList { + fn all_locale(&self) -> &'static [&'static str]; // "en", "de", "es" (...) + fn default_lang(&self) -> &'static str; // "en" +} + impl Locale { - pub fn all_locale() -> &'static [&'static str] { - &["de", "en", "es", "ja", "it", "pl", "zh_CN"] - } - pub fn default_lang() -> &'static str { - "en" - } - fn match_locale<'o>(lang: &str, region: Option<&str>, all_locales: &[&'o str]) -> &'o str { + fn match_locale<'o>( + default_lang: &'static str, + lang: &str, + region: Option<&str>, + all_locales: &[&'o str], + ) -> &'o str { if let Some(region) = region { let locale_str = format!("{lang}_{region}"); if let Some(locale) = all_locales.iter().find(|&&l| l == locale_str) { return locale; } log::warn!("Unsupported locale \"{locale_str}\", trying \"{lang}\"."); - }; + } if let Some(locale) = all_locales.iter().find(|&&l| l == lang) { return locale; } - + let prefix = format!("{lang}_"); if let Some(locale) = all_locales.iter().find(|&&l| l.starts_with(&prefix)) { return locale; } - let locale = Self::default_lang(); - log::warn!("Unsupported language \"{lang}\", defaulting to \"{locale}\"."); - locale + log::warn!("Unsupported language \"{lang}\", defaulting to \"{default_lang}\"."); + default_lang } - pub fn new(lang: String, region: Option) -> Self { - let matched = Self::match_locale(&lang, region.as_deref(), Self::all_locale()).to_string(); + + pub fn new(langs_list: &dyn LangsList, lang: String, region: Option) -> Self { + let matched = Self::match_locale( + langs_list.default_lang(), + &lang, + region.as_deref(), + langs_list.all_locale(), + ) + .to_string(); Self { lang, region, matched } } - pub fn parse_str(locale: &str) -> Self { - let base = locale.split(|c| c == '.' || c == '@').next().unwrap_or(locale); - let parts: Vec<&str> = base.split(|c| c == '_' || c == '-').collect(); + + pub fn parse_str(langs_list: &dyn LangsList, locale: &str) -> Self { + let base = locale.split(['.', '@']).next().unwrap_or(locale); + let parts: Vec<&str> = base.split(['_', '-']).collect(); // Ensures the format is lang_REGION match parts.as_slice() { - [lang, region, ..] => Self::new(lang.to_lowercase(), Some(region.to_uppercase())), - [lang] if !lang.is_empty() => Self::new(lang.to_lowercase(), None), - _ => Self::new("en".to_string(), None), + [lang, region, ..] => Self::new(langs_list, lang.to_lowercase(), Some(region.to_uppercase())), + [lang] if !lang.is_empty() => Self::new(langs_list, lang.to_lowercase(), None), + _ => Self::new(langs_list, langs_list.default_lang().to_string(), None), } } - pub fn from_env() -> Self { + + pub fn from_env(lang_provider: &dyn LangProvider) -> Self { + let default_lang = lang_provider.langs_list().default_lang(); + + // check if forced language is set + if let Some(forced_lang) = lang_provider.forced_lang() { + let matched = + Self::match_locale(default_lang, forced_lang, None, lang_provider.langs_list().all_locale()).to_string(); + return Self { + lang: forced_lang.to_string(), + region: None, + matched, + }; + } + + // fallback to environment variables use std::env; let vars = ["LC_ALL", "LC_MESSAGES", "LANG"]; let full_locale = vars @@ -114,13 +140,10 @@ impl Locale { .find_map(|&v| env::var(v).ok()) .filter(|v| !v.is_empty() && v != "C" && v != "POSIX") .unwrap_or_else(|| { - log::warn!( - "LC_ALL/LC_MESSAGES/LANG is not set, defaulting to \"{}\"", - Self::default_lang() - ); - Self::default_lang().to_string() + log::warn!("LC_ALL/LC_MESSAGES/LANG is not set, defaulting to \"{default_lang}\""); + default_lang.to_string() }); - Self::parse_str(&full_locale) + Self::parse_str(lang_provider.langs_list(), &full_locale) } pub fn get_matched(&self) -> &str { &self.matched @@ -155,13 +178,13 @@ fn find_translation<'a>(translation: &str, mut val: &'a serde_json::Value) -> Op } impl I18n { - pub fn new(provider: &mut Box) -> anyhow::Result { - let locale = Locale::from_env(); + pub fn new(asset_provider: &mut dyn AssetProvider, lang_provider: &dyn LangProvider) -> anyhow::Result { + let locale = Locale::from_env(lang_provider); log::info!("Guessed system language: {locale}"); - let data_english = provider.load_from_path("lang/en.json")?; + let data_english = asset_provider.load_from_path("lang/en.json")?; let path = format!("lang/{}.json", locale.get_matched()); - let data_translated = provider + let data_translated = asset_provider .load_from_path(&path) .with_context(|| path.clone()) .context("Could not load translation file")?; @@ -189,7 +212,7 @@ impl I18n { }) } - pub fn get_locale(&self) -> &Locale { + pub const fn get_locale(&self) -> &Locale { &self.locale } @@ -202,7 +225,7 @@ impl I18n { } if let Some(translated_fallback) = find_translation(translation_key, &self.json_root_fallback) { - log::warn!("missing translation for key \"{translation_key}\", using \"en\" instead"); + log::warn!("missing translation for key \"{translation_key}\", using fallback instead"); return Rc::from(format_translated(translated_fallback, sections)); } diff --git a/wlx-common/src/config.rs b/wlx-common/src/config.rs index 580cd59..6086477 100644 --- a/wlx-common/src/config.rs +++ b/wlx-common/src/config.rs @@ -7,9 +7,7 @@ use strum::{AsRefStr, EnumProperty, EnumString, VariantArray}; use wayvr_ipc::packet_client::WvrProcessLaunchParams; use crate::{ - astr_containers::{AStrMap, AStrSet}, - overlays::{BackendAttribValue, ToastDisplayMethod, ToastTopic}, - windowing::OverlayWindowState, + astr_containers::{AStrMap, AStrSet}, locale::{self}, overlays::{BackendAttribValue, ToastDisplayMethod, ToastTopic}, windowing::OverlayWindowState }; pub type PwTokenMap = AStrMap; @@ -140,6 +138,8 @@ const fn def_max_height() -> u16 { 1440 } + + #[derive(Deserialize, Serialize)] pub struct GeneralConfig { #[serde(default = "def_theme_path")] @@ -151,6 +151,8 @@ pub struct GeneralConfig { pub color_faded: Option, pub color_background: Option, + pub language: Option, // auto-detected at runtime if unset + #[serde(default = "def_one")] #[serde(alias = "ui_animation_speed", alias = "animation_speed" /* old name */)] pub ui_animation_speed: f32, diff --git a/wlx-common/src/lib.rs b/wlx-common/src/lib.rs index a96fbba..698a2d2 100644 --- a/wlx-common/src/lib.rs +++ b/wlx-common/src/lib.rs @@ -8,6 +8,7 @@ pub mod dash_interface; pub mod dash_interface_emulated; pub mod desktop_finder; mod handle; +pub mod locale; pub mod overlays; pub mod timestep; pub mod windowing; diff --git a/wlx-common/src/locale.rs b/wlx-common/src/locale.rs new file mode 100644 index 0000000..7679c47 --- /dev/null +++ b/wlx-common/src/locale.rs @@ -0,0 +1,88 @@ +use std::rc::Rc; + +use serde::{Deserialize, Serialize}; +use strum::{AsRefStr, EnumProperty, EnumString, VariantArray}; + +use crate::config::GeneralConfig; + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, AsRefStr, EnumString, EnumProperty, VariantArray)] +pub enum Language { + #[strum(props(Text = "English"))] + English, + #[strum(props(Text = "Polski"))] + Polish, + #[strum(props(Text = "日本語"))] + Japanese, + #[strum(props(Text = "German"))] + German, + #[strum(props(Text = "Italiano"))] + Italian, + #[strum(props(Text = "简体中文"))] + ChineseSimplified, + #[strum(props(Text = "Español"))] + Spanish, +} + +impl Language { + pub const fn code(&self) -> &'static str { + match self { + Language::English => "en", + Language::Polish => "pl", + Language::Japanese => "ja", + Language::German => "de", + Language::Italian => "it", + Language::ChineseSimplified => "zh_CN", + Language::Spanish => "es", + } + } + + pub const fn get_default() -> Self { + Self::English + } + + pub const fn all_codes() -> &'static [&'static str] { + &["en", "pl", "ja", "de", "it", "zh_CN", "es"] + } +} + +pub struct WayVRLangsList {} + +impl wgui::i18n::LangsList for WayVRLangsList { + fn all_locale(&self) -> &'static [&'static str] { + Language::all_codes() + } + + fn default_lang(&self) -> &'static str { + Language::get_default().code() + } +} + +// static +const G_LANGS_LIST: WayVRLangsList = WayVRLangsList {}; + +#[derive(Default)] +pub struct WayVRLangProvider { + forced_lang: Option>, +} + +impl wgui::assets::LangProvider for WayVRLangProvider { + fn langs_list(&self) -> &dyn wgui::i18n::LangsList { + &G_LANGS_LIST + } + + fn forced_lang(&self) -> Option<&str> { + self.forced_lang.as_ref().map(|lang| lang.as_ref()) + } +} + +impl WayVRLangProvider { + pub fn from_config(config: &GeneralConfig) -> Self { + if let Some(lang) = &config.language { + return Self { + forced_lang: Some(lang.code().into()), + }; + } + + Self::default() + } +}