diff --git a/crate/multiworld-gui/src/main.rs b/crate/multiworld-gui/src/main.rs index f3767c6..4c241ff 100644 --- a/crate/multiworld-gui/src/main.rs +++ b/crate/multiworld-gui/src/main.rs @@ -116,6 +116,10 @@ use { Kind as Frontend, }, github::Repo, + localization::{ + Locale, + Message::*, + }, ws::{ ServerError, latest::{ @@ -380,6 +384,7 @@ enum Message { SetExistingRoomSelection(RoomFormatter), SetFrontend(Frontend), SetLobbyView(LobbyView), + SetLocale(Locale), SetNewRoomName(String), SetPassword(String), SetRoomView(RoomView), @@ -438,6 +443,7 @@ struct State { update_state: UpdateState, send_all_path: String, send_all_world: String, + locale: Locale, } impl State { @@ -686,6 +692,7 @@ impl Application for State { update_state: UpdateState::Pending, send_all_path: String::default(), send_all_world: String::default(), + locale: config.locale.unwrap_or_default(), frontend, config_error, persistent_state_error, persistent_state, }, cmd(future::ok(Message::CheckForUpdates))) } @@ -1418,6 +1425,15 @@ impl Application for State { Message::SetCreateNewRoom(new_val) => if let SessionState::Lobby { ref mut create_new_room, .. } = self.server_connection { *create_new_room = new_val }, Message::SetExistingRoomSelection(name) => if let SessionState::Lobby { ref mut existing_room_selection, .. } = self.server_connection { *existing_room_selection = Some(name) }, Message::SetFrontend(new_frontend) => self.frontend.kind = new_frontend, + Message::SetLocale(new_locale) => { + self.locale = new_locale; + return cmd(async move { + let mut config = Config::load().await?; + config.locale = Some(new_locale); + config.save().await?; + Ok(Message::Nop) + }) + }, Message::SetNewRoomName(name) => if let SessionState::Lobby { ref mut new_room_name, .. } = self.server_connection { *new_room_name = name }, Message::SetPassword(new_password) => if let SessionState::Lobby { ref mut password, .. } = self.server_connection { *password = new_password }, Message::SetSendAllPath(new_path) => self.send_all_path = new_path, @@ -1535,7 +1551,7 @@ impl Application for State { col = col.push( Row::new() .push("1. ") - .push(Button::new("Open Project64").on_press(Message::LaunchProject64)) + .push(Button::new(Text::new(self.locale.message(OpenPj64Button))).on_press(Message::LaunchProject64)) .align_items(iced::Alignment::Center)); col = col.push("2. In Project64's Debugger menu, select Scripts\n3. In the Scripts window, select ootrmw.js and click Run\n4. Wait until the Output area says “Connected to multiworld app”. (This should take less than 5 seconds.) You can then close the Scripts window.") } else { @@ -1647,6 +1663,7 @@ impl Application for State { .push(Button::new("Sign in with racetime.gg").on_press(Message::SetLobbyView(LobbyView::Login { provider: login::Provider::RaceTime, no_midos_house_account: false }))) .push(Button::new("Sign in with Discord").on_press(Message::SetLobbyView(LobbyView::Login { provider: login::Provider::Discord, no_midos_house_account: false }))); } + col = col.push(PickList::new(all::().collect::>(), Some(self.locale), Message::SetLocale)).into(); col.spacing(8) } SessionState::Lobby { view: LobbyView::Login { provider, no_midos_house_account: true }, wrong_password: false, .. } => Column::new() diff --git a/crate/multiworld-installer/src/main.rs b/crate/multiworld-installer/src/main.rs index 1bad805..211493a 100644 --- a/crate/multiworld-installer/src/main.rs +++ b/crate/multiworld-installer/src/main.rs @@ -66,6 +66,10 @@ use { config::Config, frontend::Kind as Emulator, //TODO rename to Frontend? github::Repo, + localization::{ + Locale, + Message::*, + }, }, }; #[cfg(target_os = "linux")] use { @@ -192,6 +196,7 @@ enum Message { SetEmulator(Emulator), SetInstallEmulator(bool), SetOpenEmulator(bool), + SetLocale(Locale), } fn cmd(future: impl Future> + Send + 'static) -> Command { @@ -232,6 +237,12 @@ struct Pj64ConfigDebugger { enum Page { Error(Arc, bool), Elevated, + SelectLocale { + emulator: Option, + install_emulator: Option, + emulator_path: Option, + multiworld_path: Option, + }, SelectEmulator { emulator: Option, install_emulator: Option, @@ -291,6 +302,7 @@ struct State { create_emulator_desktop_shortcut: bool, // Page::AskLaunch open_emulator: bool, + locale: Locale, } impl Application for State { @@ -299,25 +311,25 @@ impl Application for State { type Theme = Theme; type Flags = Args; - fn new(Args { mut emulator }: Args) -> (Self, Command) { + fn new(Args { mut emulator , locale}: Args) -> (Self, Command) { if let Ok(only_emulator) = all().filter(Emulator::is_supported).exactly_one() { emulator.get_or_insert(only_emulator); } + let page = match emulator { + Some(_) => Page::SelectEmulator { emulator, install_emulator: None, emulator_path: None, multiworld_path: None }, + None => Page::SelectLocale { emulator, install_emulator: None, emulator_path: None, multiworld_path: None } + }; (Self { http_client: reqwest::Client::builder() .user_agent(concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"))) .use_rustls_tls() .https_only(true) .build().expect("failed to build HTTP client"), - page: Page::SelectEmulator { - install_emulator: None, - emulator_path: None, - multiworld_path: None, - emulator, - }, + page, create_multiworld_desktop_shortcut: true, create_emulator_desktop_shortcut: true, open_emulator: true, + locale: locale.unwrap_or_default(), }, if emulator.is_some() { cmd(future::ok(Message::Continue)) } else { @@ -348,7 +360,8 @@ impl Application for State { fn update(&mut self, msg: Message) -> Command { match msg { Message::Back => self.page = match self.page { - Page::Error(_, _) | Page::Elevated | Page::SelectEmulator { .. } => unreachable!(), + Page::Error(_, _) | Page::Elevated | Page::SelectLocale { .. } => unreachable!(), + Page::SelectEmulator { emulator, install_emulator, ref emulator_path, ref multiworld_path } => Page::SelectLocale { emulator, install_emulator, emulator_path: emulator_path.clone(), multiworld_path: multiworld_path.clone() }, Page::EmulatorWarning { emulator, install_emulator, ref emulator_path, ref multiworld_path } => Page::SelectEmulator { emulator: Some(emulator), install_emulator, emulator_path: emulator_path.clone(), multiworld_path: multiworld_path.clone() }, Page::LocateEmulator { emulator, install_emulator, ref emulator_path, ref multiworld_path } => Page::SelectEmulator { emulator: Some(emulator), install_emulator: Some(install_emulator), emulator_path: Some(emulator_path.clone()), multiworld_path: multiworld_path.clone() }, Page::AskBizHawkUpdate { ref emulator_path, ref multiworld_path } => Page::LocateEmulator { emulator: Emulator::BizHawk, install_emulator: false, emulator_path: emulator_path.clone(), multiworld_path: multiworld_path.clone() }, @@ -405,6 +418,9 @@ impl Application for State { Message::ConfigWriteFailed => if let Page::InstallMultiworld { ref mut config_write_failed, .. } = self.page { *config_write_failed = true }, Message::Continue => match self.page { Page::Error(_, _) | Page::Elevated | Page::Project64EmError { .. } => unreachable!(), + Page::SelectLocale { emulator, install_emulator, ref emulator_path, ref multiworld_path } => { + self.page = Page::SelectEmulator { emulator, install_emulator, emulator_path: emulator_path.clone(), multiworld_path: multiworld_path.clone() } + } Page::SelectEmulator { emulator, install_emulator, ref emulator_path, ref multiworld_path } => { let emulator = emulator.expect("emulator must be selected to continue here"); match emulator { @@ -416,13 +432,18 @@ impl Application for State { #[cfg(target_os = "windows")] Emulator::Pj64V3 | Emulator::Pj64V4 if !is_elevated() => { // Project64 installation and plugin installation both require admin permissions (UAC) self.page = Page::Elevated; + let locale = self.locale; return cmd(async move { - let arg = match emulator { + let emulator_arg = match emulator { Emulator::Pj64V3 => "--emulator=pj64v3", Emulator::Pj64V4 => "--emulator=pj64v4", _ => unreachable!(), }; - tokio::task::spawn_blocking(move || Ok::<_, Error>(runas::Command::new(env::current_exe()?).arg(arg).gui(true).status().at_command("runas")?.check("runas")?)).await??; + let locale_arg = match locale { + Locale::EN => "--locale=en", + Locale::FR => "--locale=fr", + }; + tokio::task::spawn_blocking(move || Ok::<_, Error>(runas::Command::new(env::current_exe()?).arg(emulator_arg).arg(locale_arg).gui(true).status().at_command("runas")?.check("runas")?)).await??; Ok(Message::Exit) }) } @@ -764,6 +785,7 @@ impl Application for State { _ => unreachable!(), }; self.page = Page::InstallMultiworld { emulator, emulator_path: emulator_path.clone(), multiworld_path: multiworld_path.clone(), config_write_failed: false }; + let locale = self.locale; match emulator { Emulator::Dummy => unreachable!(), Emulator::EverDrive => { @@ -771,6 +793,7 @@ impl Application for State { return cmd(async move { let mut new_mw_config = Config::load().await?; new_mw_config.default_frontend = Some(Emulator::EverDrive); + new_mw_config.locale = Some(locale); new_mw_config.save().await?; let multiworld_path = PathBuf::from(multiworld_path.expect("multiworld app path must be set for Project64")); fs::create_dir_all(multiworld_path.parent().ok_or(Error::Root)?).await?; @@ -794,6 +817,7 @@ impl Application for State { Emulator::BizHawk => return cmd(async move { let mut new_mw_config = Config::load().await?; new_mw_config.default_frontend = Some(Emulator::BizHawk); + new_mw_config.locale = Some(locale); new_mw_config.save().await?; let emulator_dir = PathBuf::from(emulator_path.expect("emulator path must be set for BizHawk")); let external_tools_dir = emulator_dir.join("ExternalTools"); @@ -849,6 +873,7 @@ impl Application for State { let mut new_mw_config = Config::load().await?; new_mw_config.default_frontend = Some(Emulator::Pj64V3); new_mw_config.pj64_script_path = Some(script_path); + new_mw_config.locale = Some(locale); new_mw_config.save().await?; let config_path = emulator_dir.join("Config"); fs::create_dir(&config_path).await.exist_ok()?; @@ -938,6 +963,7 @@ impl Application for State { Message::SetCreateEmulatorDesktopShortcut(create_desktop_shortcut) => self.create_emulator_desktop_shortcut = create_desktop_shortcut, Message::SetCreateMultiworldDesktopShortcut(create_desktop_shortcut) => self.create_multiworld_desktop_shortcut = create_desktop_shortcut, Message::SetEmulator(new_emulator) => if let Page::SelectEmulator { ref mut emulator, .. } = self.page { *emulator = Some(new_emulator) }, + Message::SetLocale(new_locale) => self.locale = new_locale, Message::SetInstallEmulator(new_install_emulator) => if let Page::LocateEmulator { ref mut install_emulator, .. } = self.page { *install_emulator = new_install_emulator }, Message::SetOpenEmulator(open_emulator) => self.open_emulator = open_emulator, } @@ -975,10 +1001,24 @@ impl Application for State { None, ), Page::Elevated => ( - Text::new("The installer has been reopened with admin permissions. Please continue there.").into(), + Text::new(self.locale.message(InstallerReopenUAC)).into(), false, None, ), + Page::SelectLocale { .. } => ( + { + let mut col = Column::new(); + col = col.push("Select language"); + col = col.push(PickList::new(all::().collect::>(), Some(self.locale), Message::SetLocale)); + col.spacing(8).into() + }, + false, + Some({ + let mut row = Row::new(); + row = row.push(Text::new("Continue")); + (Into::>::into(row.spacing(8)), true) + }) + ), Page::SelectEmulator { emulator, .. } => ( { let mut col = Column::new(); @@ -991,7 +1031,7 @@ impl Application for State { col = col.push(Button::new(Text::new("See platform support status")).on_press(Message::PlatformSupport)); col.spacing(8).into() }, - false, + true, Some({ let mut row = Row::new(); #[cfg(target_os = "windows")] if matches!(emulator, Some(Emulator::Pj64V3 | Emulator::Pj64V4)) && !is_elevated() { @@ -1170,6 +1210,8 @@ impl Application for State { struct Args { #[clap(long, value_enum)] emulator: Option, + #[clap(long, value_enum)] + locale: Option, } #[derive(Debug, thiserror::Error)] diff --git a/crate/multiworld/src/config.rs b/crate/multiworld/src/config.rs index 5e405ee..7c2798f 100644 --- a/crate/multiworld/src/config.rs +++ b/crate/multiworld/src/config.rs @@ -10,7 +10,10 @@ use { Serialize, }, url::Url, - crate::frontend::Kind as Frontend, + crate::{ + frontend::Kind as Frontend, + localization::Locale, + }, }; #[cfg(unix)] use xdg::BaseDirectories; #[cfg(windows)] use directories::ProjectDirs; @@ -27,6 +30,7 @@ pub struct Config { #[serde(default)] pub refresh_tokens: BTreeMap, pub pj64_script_path: Option, + pub locale: Option, #[serde(default = "default_websocket_hostname")] pub websocket_hostname: String, } @@ -112,6 +116,7 @@ impl Default for Config { refresh_tokens: BTreeMap::default(), pj64_script_path: None, websocket_hostname: default_websocket_hostname(), + locale: None, } } } diff --git a/crate/multiworld/src/lib.rs b/crate/multiworld/src/lib.rs index 2956795..c423002 100644 --- a/crate/multiworld/src/lib.rs +++ b/crate/multiworld/src/lib.rs @@ -78,6 +78,7 @@ use { #[cfg(feature = "sqlx")] use sqlx::PgPool; pub mod config; +pub mod localization; pub mod frontend; pub mod github; pub mod ws; diff --git a/crate/multiworld/src/localization.rs b/crate/multiworld/src/localization.rs new file mode 100644 index 0000000..6c6307a --- /dev/null +++ b/crate/multiworld/src/localization.rs @@ -0,0 +1,58 @@ +use { + enum_iterator::Sequence, serde::{ + Deserialize, + Serialize, + }, std::{borrow::Cow, fmt} +}; + +#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, clap::ValueEnum, Sequence)] +#[clap(rename_all = "lower")] +pub enum Locale { + #[default] + EN, + FR, +} + +impl Locale { + + pub fn message(&self, message: Message) -> Cow<'static, str> { + match message { + Message::InstallerReopenUAC => { //used in installer + match self { + Locale::EN => Cow::Borrowed("The installer has been reopened with admin permissions. Please continue there."), + Locale::FR => Cow::Borrowed("L'installateur a été ré-ouvert avec les permissions administrateur. Veuillez continuer dans la nouvelle fenêtre."), + } + }, + Message::OpenPj64Button => { // used in gui + match self { + Locale::EN => Cow::Borrowed("Open Project64"), + Locale::FR => Cow::Borrowed("Ouvrir Project64"), + } + }, + // TODO find a way to translate formatted text somehow + //Message::AMessageWithParameter(my_int, my_string) => { + // match self { + // Locale::EN => format!("My integer is: {} and my string is: {}",my_int,my_string).as_str(), + // Locale::FR => format!("My integer is: {} and my string is: {}",my_int,my_string).as_str(), + // } + //} + } + } +} + +impl fmt::Display for Locale { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + // add local formating for display + Self::EN => write!(f, "English"), + Self::FR => write!(f, "Français"), + } + } +} + +pub enum Message { + InstallerReopenUAC, + OpenPj64Button, + //AMessageWithParameter(i32,String), +} + diff --git a/ootr-multiworld.sln b/ootr-multiworld.sln new file mode 100644 index 0000000..5c6deca --- /dev/null +++ b/ootr-multiworld.sln @@ -0,0 +1,36 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "crate", "crate", "{188837CE-D73F-4BFE-A160-2790D33D7AE1}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "multiworld-bizhawk", "multiworld-bizhawk", "{FA858C4F-74A6-4080-B07E-29A5EDD7C077}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "OotrMultiworld", "OotrMultiworld", "{E035DE8F-0231-4398-8869-85DF5E7BBBB3}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OotrMultiworld", "crate\multiworld-bizhawk\OotrMultiworld\src\OotrMultiworld.csproj", "{50118B58-42C6-451E-AD9D-79D1366FA4D1}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {50118B58-42C6-451E-AD9D-79D1366FA4D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {50118B58-42C6-451E-AD9D-79D1366FA4D1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {50118B58-42C6-451E-AD9D-79D1366FA4D1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {50118B58-42C6-451E-AD9D-79D1366FA4D1}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {FA858C4F-74A6-4080-B07E-29A5EDD7C077} = {188837CE-D73F-4BFE-A160-2790D33D7AE1} + {E035DE8F-0231-4398-8869-85DF5E7BBBB3} = {FA858C4F-74A6-4080-B07E-29A5EDD7C077} + {50118B58-42C6-451E-AD9D-79D1366FA4D1} = {E035DE8F-0231-4398-8869-85DF5E7BBBB3} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {6BF49C3B-4303-4D20-88FE-2515BB62EB21} + EndGlobalSection +EndGlobal