diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 65f5d85d1..f4b9ef4a1 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -12,8 +12,8 @@ "vscode": { "extensions": [ "rust-lang.rust-analyzer", - "bungcip.better-toml", - "serayuzgur.crates", + "tamasfe.even-better-toml", + "fill-labs.dependi", "statiolake.vscode-rustfmt" ] } diff --git a/.vscode/i18n-ally-custom-framework.yml b/.vscode/i18n-ally-custom-framework.yml new file mode 100644 index 000000000..7cca0a3b7 --- /dev/null +++ b/.vscode/i18n-ally-custom-framework.yml @@ -0,0 +1,7 @@ +languageIds: + - rust + +usageMatchRegex: + - "[^\\w\\d]t!\\([\\s\\n\\r]*['\"]({key})['\"]" + +monopoly: true \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 6a9e639cc..b1150f58a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,4 +7,7 @@ "--message-format=json", "--all-features", ], + "i18n-ally.localesPaths": [ + "locales" + ], } \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 2a7ec478d..6ea0bad0c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,7 @@ log = "0.4.17" byte-unit = "4.0.18" zip = { version = "0.6", default-features = false, features = ["deflate"] } anyhow = "1.0.72" +rust-i18n = "3.1.2" [patch.crates-io] native-tls = { git = "https://github.com/skyline-rs/rust-native-tls", branch = "switch-timeout-panic" } @@ -64,6 +65,11 @@ plugin-dependencies = [ { name = "libparam_hook.nro", url = "https://github.com/ultimate-research/params-hook-plugin/releases/download/v0.1.1/libparam_hook.nro" }, ] +[package.metadata.i18n] +load-path = "locales" +default-locale = "en_us" +available-locales = ["en_us", "fr"] + [features] outside_training_mode = [] layout_arc_from_file = [] diff --git a/locales/en_us.json b/locales/en_us.json new file mode 100644 index 000000000..7c6465f62 --- /dev/null +++ b/locales/en_us.json @@ -0,0 +1,113 @@ +{ + "common": { + "Welcome!": "welcome!", + "plugin_title": "Training Modpack", + "hold_button": "Hold %{button}", + "open_menu": "Open Menu", + "language": "Language", + "modpack_menu": "Modpack Menu", + "welcome": "Welcome!", + "yes": "yes", + "no": "no", + "on": "on", + "off": "off" + }, + "buttons": { + "start": "Start", + "a": "a", + "b": "b", + "x": "x", + "y": "y", + "l": "l", + "r": "r", + "z": "z", + "dpad_Down": "dpad down", + "dpad_Left": "dpad left", + "dpad_Right": "dpad right", + "dpad_Up": "dpad up", + "pro": { + "l": "pro l", + "r": "pro r; gcc z", + "zl_gcc_l": "pro zl; gcc l", + "zr_gcc_r": "pro zr; gcc r" + }, + "analog_stick": { + "up": "up", + "down": "down", + "left": "left", + "right": "right" + } + }, + "menus": { + "mash_settings": { + "tab_name": "Mash Settings", + "mash_toggles": { + "title": "Mash Toggles", + "description": "Actions to be performed as soon as possible" + }, + "followup_toggles": { + "title": "Followup Toggles", + "description": "Actions to be performed after a mash option" + }, + "mash_triggers": { + "title": "Mash Triggers", + "description": "Configure what causes the cpu to perform a mash option" + }, + "attack_angle": { + "title": "Attack Angle", + "description": "For attacks that can be angled, such as some forward tilts" + }, + "throw_options": { + "title": "Throw Options", + "description": "Throw to be performed when a grab is landed" + }, + "throw_delay": { + "title": "Throw Delay", + "description": "How many frames to delay the throw option" + }, + "pummel_delay": { + "title": "Pummel Delay", + "description": "How many frames after a grab to wait before starting to pummel" + }, + "falling_aerials": { + "title": "Falling Aerials", + "description": "Should aerials be performed when rising or when falling" + }, + "full_hop": { + "title": "Full Hop", + "description": "Should the cpu perform a full hop or a short hop" + }, + "aerial_delay": { + "title": "Aerial Delay", + "description": "How long to delay a mash aerial attack" + }, + "fast_fall": { + "title": "Fast Fall", + "description": "Should the cpu fastfall during a jump" + }, + "fast_fall_delay": { + "title": "Fast Fall Delay", + "description": "How many frames the cpu should delay their fastfall" + }, + "oos_offset": { + "title": "OoS Offset", + "description": "How many times the cpu shield can be hit before performing a mash option" + }, + "reaction_time": { + "title": "Reaction Time", + "description": "How many frames to delay before performing a mash option" + } + }, + "misc_settings": { + "tab_name": "Misc Settings", + "language": { + "title": "Language", + "help_text": "Language: Select your preferred language", + "locales": { + "en_us": "English (US)", + "fr": "French" + } + } + } + } +} diff --git a/locales/fr.json b/locales/fr.json new file mode 100644 index 000000000..efa48cd0d --- /dev/null +++ b/locales/fr.json @@ -0,0 +1,99 @@ +{ + "common": { + "plugin_title": "Modpack d'Entraînement'", + "hold_button": "Maintenir %{button}", + "open_menu": "Ouvrir le Menu", + "language": "Langue", + "modpack_menu": "Menu du Modpack", + "yes": "Oui", + "no": "Non", + "on": "Activé", + "off": "Désactivé" + }, + "buttons": { + "start": "Start", + "a": "A", + "b": "B", + "x": "X", + "y": "Y", + "l": "L", + "r": "R", + "z": "z", + "dpad_Up": "dpad haut", + "dpad_Down": "dpad bas", + "dpad_Left": "dpad gauche", + "dpad_Right": "dpad droite", + "pro": { + "l": "pro l", + "r": "pro r; gcc z", + "zl_gcc_l": "pro zl; gcc l", + "zr_gcc_r": "pro zr; gcc r" + }, + "analog_stick": { + "up": "haut", + "down": "bas", + "left": "gauche", + "right": "droite" + } + }, + "menus": { + "mash_settings": { + "mash_toggles": { + "title": "Options de Mashing", + "description": "Mashing à effectuer dès que possible" + }, + "followup_toggles": { + "title": "Options de Followups", + "description": "Followups à effectuer dès que possible" + }, + "mash_triggers": { + "title": "Déclencheurs de Mashing", + "description": "Configurer les actions qui feront réagir le CPU." + }, + "attack_angle": { + "title": "Angle d'Attaque", + "description": "Modifie l'angle des attaques qui peuvent être orientées, par ex. le F-Tilt de Falco." + }, + "throw_options": { + "title": "Options de Grabs", + "description": "Définit dans quelle direction le CPU doit faire un grab." + }, + "throw_delay": { + "title": "Délai de Grab", + "description": "De combien de frames retarder le grab." + }, + "pummel_delay": { + "title": "Délai de Pummel", + "description": "Combien de frames attendre avant de pummel pendant un grab." + }, + "falling_aerials": { + "title": "Aerials descendants", + "description": "Définit si les Aerials doivent se faire pendant la montée ou la descente d'un saut." + }, + "full_hop": { + "title": "Saut", + "description": "Définit si le CPU doit faire un Saut ou un Demi-Saut" + }, + "aerial_delay": { + "title": "Délai d'Aerial", + "description": "De combien de frames retarder l'Aerial." + }, + "fast_fall": { + "title": "Fast Fall", + "description": "Définit si le CPU doit Fast Fall pendant son saut." + }, + "fast_fall_delay": { + "title": "Délai de Fast Fall", + "description": "De combien de frames retarder le Fast Fall." + }, + "oos_offset": { + "title": "Coups avant Out of Shield", + "description": "Combien de fois le shield doit être touché avant que l'option Out of Shield se lance." + }, + "reaction_time": { + "title": "Temps de réaction", + "description": "De combien de frames retarder l'option de mashing." + } + } + } +} diff --git a/src/common/localization.rs b/src/common/localization.rs new file mode 100644 index 000000000..118d773aa --- /dev/null +++ b/src/common/localization.rs @@ -0,0 +1,85 @@ +use crate::common::MENU; +use crate::logging::*; +use crate::training::ui::notifications::notification; +use training_mod_consts::Locale; +use training_mod_sync::*; + +#[repr(u8)] +#[derive(Debug)] +pub enum ModLanguageId { + English, + French, +} + +impl From for ModLanguageId { + fn from(byte: u8) -> Self { + match byte { + 0 => Self::English, + 1 => Self::French, + _ => Self::English, + } + } +} + +impl From<&str> for ModLanguageId { + fn from(locale_code: &str) -> Self { + match locale_code { + "en_us" => Self::English, + "fr" => Self::French, + _ => Self::English, + } + } +} + +impl From for ModLanguageId { + fn from(locale_code: Locale) -> Self { + match locale_code { + Locale::ENGLISH_US => Self::English, + Locale::FRENCH => Self::French, + _ => Self::English, + } + } +} + +impl ModLanguageId { + pub fn get_locale_code(&self) -> &str { + match self { + ModLanguageId::English => "en_us", + ModLanguageId::French => "fr", + } + } +} + +pub fn init() { + info!("Initializing localization"); + handle_language_change(); + info!( + "Initialized localization with {:#?}", + ModLanguageId::from(read(&MENU).selected_locale) + ); +} + +pub fn set_language() { + let locale: ModLanguageId = ModLanguageId::from(read(&MENU).selected_locale); + info!("Setting language to {:?}", locale); + + let locale_code = locale.get_locale_code(); + + if rust_i18n::available_locales!().contains(&locale_code) { + rust_i18n::set_locale(locale_code); + + notification("Language".to_string(), locale_code.to_string(), 360); + } else { + info!("{} not found using en_us instead.", locale_code); + rust_i18n::set_locale("en_us"); + } +} + +pub fn handle_language_change() { + let has_locale_changed = + *ModLanguageId::from(read(&MENU).selected_locale).get_locale_code() != *rust_i18n::locale(); + + if has_locale_changed { + set_language(); + } +} diff --git a/src/common/mod.rs b/src/common/mod.rs index cfbb5320c..baef6bd6a 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -15,6 +15,7 @@ pub mod dev_config; pub mod dialog; pub mod events; pub mod input; +pub mod localization; pub mod menu; pub mod offsets; pub mod raygun_printer; diff --git a/src/lib.rs b/src/lib.rs index 0c1728c58..5ddb55813 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,6 +16,10 @@ clippy::missing_transmute_annotations )] +#[macro_use] +extern crate rust_i18n; +i18n!(fallback = "en_us"); + use std::fs; use std::path::PathBuf; @@ -79,7 +83,11 @@ pub fn main() { let mut event_queue = lock_write(&EVENT_QUEUE); (*event_queue).push(Event::smash_open()); drop(event_queue); - notification("Training Modpack".to_string(), "Welcome!".to_string(), 60); + notification( + t!("common.plugin_title").to_string(), + "Welcome!".to_string(), + 60, + ); hitbox_visualizer::hitbox_visualization(); hazard_manager::hazard_manager(); @@ -126,16 +134,18 @@ pub fn main() { info!("Skipping version check because we are using an emulator"); } - notification("Training Modpack".to_string(), "Welcome!".to_string(), 60); + localization::init(); + notification( - "Open Menu".to_string(), + t!("common.open_menu").to_string(), if read(&MENU).menu_open_start_press == OnOff::ON { - "Hold Start".to_string() + t!("common.hold_button", button = t!("buttons.start")).to_string() } else { DEFAULT_OPEN_MENU_CONFIG.to_string() }, 120, ); + notification( "Save State".to_string(), read(&MENU).save_state_save.to_string(), diff --git a/src/training/mod.rs b/src/training/mod.rs index e63e8d325..b29d5dcfe 100644 --- a/src/training/mod.rs +++ b/src/training/mod.rs @@ -4,8 +4,8 @@ use crate::common::button_config; use crate::common::consts::{BuffOption, FighterId, MENU}; use crate::common::offsets::*; use crate::common::{ - dev_config, get_module_accessor, is_operation_cpu, is_training_mode, menu, PauseMenu, - FIGHTER_MANAGER_ADDR, ITEM_MANAGER_ADDR, STAGE_MANAGER_ADDR, TRAINING_MENU_ADDR, + dev_config, get_module_accessor, is_operation_cpu, is_training_mode, localization, menu, + PauseMenu, FIGHTER_MANAGER_ADDR, ITEM_MANAGER_ADDR, STAGE_MANAGER_ADDR, TRAINING_MENU_ADDR, }; use crate::hitbox_visualizer; use crate::input::*; @@ -154,6 +154,7 @@ fn once_per_frame_per_fighter(module_accessor: &mut BattleObjectModuleAccessor, shield::get_command_flag_cat(module_accessor); directional_influence::get_command_flag_cat(module_accessor); reset::check_reset(module_accessor); + localization::handle_language_change(); } /** diff --git a/src/training/ui/menu.rs b/src/training/ui/menu.rs index d8ce8f0cb..0a32fb49e 100644 --- a/src/training/ui/menu.rs +++ b/src/training/ui/menu.rs @@ -109,7 +109,7 @@ unsafe fn render_submenu_page(app: &mut App, root_pane: &Pane) { && col == tab.submenus.state.selected_col().unwrap(); // Set Pane Visibility - title_text.set_text_string(submenu.title); + title_text.set_text_string(&t!(submenu.title)); // In the actual 'layout.arc' file, every icon image is stacked // into a single container pane, with each image directly on top of another. @@ -146,7 +146,7 @@ unsafe fn render_submenu_page(app: &mut App, root_pane: &Pane) { .find_pane_by_name_recursive("FooterTxt") .unwrap() .as_textbox() - .set_text_string(submenu.help_text); + .set_text_string(&t!(submenu.help_text)); title_bg_material.set_white_res_color(BG_LEFT_ON_WHITE_COLOR); title_bg_material.set_black_res_color(BG_LEFT_ON_BLACK_COLOR); @@ -223,7 +223,7 @@ unsafe fn render_toggle_page(app: &mut App, root_pane: &Pane) { } } - title_text.set_text_string(toggle.title); + title_text.set_text_string(&t!(toggle.title)); if use_check_icon { menu_button @@ -491,7 +491,9 @@ pub unsafe fn draw(root_pane: &Pane) { if let Some(quit_button) = root_pane.find_pane_by_name_recursive("TrModTitle") { for quit_txt_s in &["set_txt_00", "set_txt_01"] { if let Some(quit_txt) = quit_button.find_pane_by_name_recursive(quit_txt_s) { - quit_txt.as_textbox().set_text_string("Modpack Menu"); + quit_txt + .as_textbox() + .set_text_string(&t!("common.modpack_menu")); } } } @@ -600,7 +602,7 @@ pub unsafe fn draw(root_pane: &Pane) { help_pane.set_default_material_colors(); help_pane.set_color(255, 255, 0, 255); } - help_pane.set_text_string(tab_titles[idx]); + help_pane.set_text_string(&t!(tab_titles[idx])); }); // Save Defaults Keyhelp diff --git a/training_mod_consts/src/lib.rs b/training_mod_consts/src/lib.rs index 8242abed5..94d0276fc 100644 --- a/training_mod_consts/src/lib.rs +++ b/training_mod_consts/src/lib.rs @@ -95,6 +95,7 @@ pub struct TrainingModpackMenu { pub tech_hide: OnOff, pub update_policy: UpdatePolicy, pub lra_reset: OnOff, + pub selected_locale: Locale, } #[repr(C)] @@ -203,6 +204,7 @@ pub static BASE_MENU: TrainingModpackMenu = TrainingModpackMenu { tech_hide: OnOff::OFF, update_policy: UpdatePolicy::default(), lra_reset: OnOff::ON, + selected_locale: Locale::default(), }; pub static DEFAULTS_MENU: RwLock = RwLock::new(BASE_MENU); @@ -214,92 +216,92 @@ pub unsafe fn create_app<'a>() -> App<'a> { // Mash Tab let mut mash_tab_submenus: Vec = Vec::new(); mash_tab_submenus.push(Action::to_submenu( - "Mash Toggles", + "menus.mash_settings.mash_toggles.title", "mash_state", - "Action to be performed as soon as possible", + "menus.mash_settings.mash_toggles.description", ToggleMultiple, )); mash_tab_submenus.push(Action::to_submenu( - "Followup Toggles", + "menus.mash_settings.follow_up.title", "follow_up", - "Actions to be performed after a Mash Option", + "menus.mash_settings.follow_up.description", ToggleMultiple, )); mash_tab_submenus.push(MashTrigger::to_submenu( - "Mash Triggers", + "menus.mash_settings.mash_triggers.title", "mash_triggers", - "Configure what causes the CPU to perform a Mash Option", + "menus.mash_settings.mash_triggers.description", ToggleSingle, )); mash_tab_submenus.push(AttackAngle::to_submenu( - "Attack Angle", + "menus.mash_settings.attack_angle.title", "attack_angle", - "For attacks that can be angled, such as some forward tilts", + "menus.mash_settings.attack_angle.description", ToggleMultiple, )); mash_tab_submenus.push(ThrowOption::to_submenu( - "Throw Options", + "menus.mash_settings.throw_options.title", "throw_state", - "Throw to be performed when a grab is landed", + "menus.mash_settings.throw_options.description", ToggleMultiple, )); mash_tab_submenus.push(MedDelay::to_submenu( - "Throw Delay", + "menus.mash_settings.throw_delay.title", "throw_delay", - "How many frames to delay the throw option", + "menus.mash_settings.throw_delay.description", ToggleMultiple, )); mash_tab_submenus.push(MedDelay::to_submenu( - "Pummel Delay", + "menus.mash_settings.pummel_delay.title", "pummel_delay", - "How many frames after a grab to wait before starting to pummel", + "menus.mash_settings.pummel_delay.description", ToggleMultiple, )); mash_tab_submenus.push(BoolFlag::to_submenu( - "Falling Aerials", + "menus.mash_settings.falling_aerials.title", "falling_aerials", - "Should aerials be performed when rising or when falling", + "menus.mash_settings.falling_aerials.description", ToggleMultiple, )); mash_tab_submenus.push(BoolFlag::to_submenu( - "Full Hop", + "menus.mash_settings.full_hop.title", "full_hop", - "Should the CPU perform a ful hop or a short hop when jumping", + "menus.mash_settings.full_hop.description", ToggleMultiple, )); mash_tab_submenus.push(Delay::to_submenu( - "Aerial Delay", + "menus.mash_settings.aerial_delay.title", "aerial_delay", - "How long to delay an aerial attack", + "menus.mash_settings.aerial_delay.description", ToggleMultiple, )); mash_tab_submenus.push(BoolFlag::to_submenu( - "Fast Fall", + "menus.mash_settings.fast_fall.title", "fast_fall", - "Should the CPU fastfall during a jump", + "menus.mash_settings.fast_fall.description", ToggleMultiple, )); mash_tab_submenus.push(Delay::to_submenu( - "Fast Fall Delay", + "menus.mash_settings.fast_fall_delay.title", "fast_fall_delay", - "How many frames the CPU should delay their fastfall", + "menus.mash_settings.fast_fall_delay.description", ToggleMultiple, )); mash_tab_submenus.push(Delay::to_submenu( - "OoS Offset", + "menus.mash_settings.oos_offset.title", "oos_offset", - "How many times the CPU shield can be hit before performing a Mash option", + "menus.mash_settings.oos_offset.description", ToggleMultiple, )); mash_tab_submenus.push(Delay::to_submenu( - "Reaction Time", + "menus.mash_settings.reaction_time.title", "reaction_time", - "How many frames to delay before performing a Mash option", + "menus.mash_settings.reaction_time.description", ToggleMultiple, )); let mash_tab = Tab { id: "mash", - title: "Mash Settings", + title: "menus.mash_settings.title", submenus: StatefulTable::with_items(NX_SUBMENU_ROWS, NX_SUBMENU_COLUMNS, mash_tab_submenus), }; overall_menu.tabs.push(mash_tab); @@ -717,9 +719,15 @@ pub unsafe fn create_app<'a>() -> App<'a> { "Reset Training Room when pressing L+R+A", ToggleSingle, )); + misc_tab_submenus.push(Locale::to_submenu( + "menus.misc_settings.language.title", + "selected_locale", + "menus.misc_settings.language.help_text", + ToggleSingle, + )); let misc_tab = Tab { id: "misc", - title: "Misc Settings", + title: "menus.misc_settings.tab_name", submenus: StatefulTable::with_items(NX_SUBMENU_ROWS, NX_SUBMENU_COLUMNS, misc_tab_submenus), }; overall_menu.tabs.push(misc_tab); diff --git a/training_mod_consts/src/options.rs b/training_mod_consts/src/options.rs index 0df898efb..e77f509f1 100644 --- a/training_mod_consts/src/options.rs +++ b/training_mod_consts/src/options.rs @@ -1141,3 +1141,28 @@ byteflags! { } impl_submenutrait!(InputDisplay); + +byteflags! { + pub struct Locale { + pub ENGLISH_US = "English (US)", + pub FRENCH = "French", + } +} + +impl_submenutrait!(Locale); + +impl Locale { + pub const fn default() -> Locale { + Locale::ENGLISH_US + } +} + +impl From for Locale { + fn from(id: u8) -> Locale { + match id { + 0 => Locale::ENGLISH_US, + 1 => Locale::FRENCH, + _ => Locale::ENGLISH_US, + } + } +}