diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index 6c76790..0000000 --- a/.github/workflows/main.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Rust - -on: - push: - branches: [ $default-branch ] - pull_request: - branches: [ $default-branch ] - -env: - CARGO_TERM_COLOR: always - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - - name: Rustfmt - uses: actions-rs/cargo@v1 - with: - command: fmt - args: --all -- --check - - - name: Run tests - uses: actions-rs/cargo@v1 - with: - command: test - args: --all-features - - - name: Build - uses: actions-rs/cargo@v1 - with: - command: build - args: --release diff --git a/Cargo.toml b/Cargo.toml index 41ecdaf..7b6d16e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,10 @@ needle = "0.1.1" threadpool = "1.0" sudo = "0.6.0" gabi = "0.2.6" +i18n-embed = { version = "0.13.4", features = ["fluent-system", "desktop-requester"]} +i18n-embed-fl = "0.6.4" +once_cell = "1.16.0" +rust-embed = "6.4.2" [target.'cfg(target_os = "windows")'.dependencies] proc-maps = "0.3.1" diff --git a/i18n.toml b/i18n.toml new file mode 100644 index 0000000..e630543 --- /dev/null +++ b/i18n.toml @@ -0,0 +1,5 @@ + +fallback_language = "en" + +[fluent] +assets_dir = "i18n" diff --git a/i18n/de/game_cheetah.ftl b/i18n/de/game_cheetah.ftl new file mode 100644 index 0000000..73c8d31 --- /dev/null +++ b/i18n/de/game_cheetah.ftl @@ -0,0 +1,52 @@ +process-label = Prozesse: +no-processes-label = +filter-processes-hint = Prozesse filtern +default-label = Standard +name-label = Name: +value-label = Wert: +search-description-label = Suchbeschreibung +search-value-label = Suche nach { $valuetype } + +found-one-result-label = 1 Vorkommen gefunden +found-results-label = { $results } Vorkommen gefunden + +no-results-label = Keine Vorkommen + +undo-button = Rückgängig +initial-search-button = Erste Suche +update-button = Aktualisieren +clear-button = Löschen +close-button = Schließen +hide-results-button = Ergebnisse verstecken +show-results-button = Ergebnisse zeigen + +generic-error-label = +invalid-input-error = Eingabe ungültig +invalid-number-error = Zahl ungültig +conversion-error = Fehler beim Konvertieren { $valuetype }: { $message } + +guess-value-item = Unklar (2-8 Bytes) +short-value-item = Short (2 Bytes) +int-value-item = Int (4 Bytes) +int64-value-item = Int64 (8 Bytes) +float-value-item = Float (4 Bytes) +double-value-item = Double (8 Bytes) + +guess-descr = Unklar +short-descr = Short +int-descr = Int +int64-descr = Int64 +float-descr = Float +double-descr = Double + +address-heading = Addresse +value-heading = Wert +freezed-heading = Eingefroren + +pid-heading = Pid +name-heading = Name +memory-heading = Speicher +command-heading = Kommando + +update-numbers-progress = Aktualisiere { $current }/{ $total }… +search-memory-progress = Suche { $current }/{ $total }… diff --git a/i18n/en/game_cheetah.ftl b/i18n/en/game_cheetah.ftl new file mode 100644 index 0000000..1c0dbe4 --- /dev/null +++ b/i18n/en/game_cheetah.ftl @@ -0,0 +1,52 @@ +process-label = Processes: +no-processes-label = +filter-processes-hint = Filter processes +default-label = default +name-label = Name: +value-label = Value: +search-description-label = Search description +search-value-label = Search for { $valuetype } value + +found-one-result-label = found one result. +found-results-label = found { $results } results. + +no-results-label = No results found. + +undo-button = Undo +initial-search-button = Initial search +update-button = Update +clear-button = Clear +close-button = Close +hide-results-button = Hide Results +show-results-button = Show Results + +generic-error-label = +invalid-input-error = Invalid input +invalid-number-error = Invalid number +conversion-error = Error converting { $valuetype }: { $message } + +guess-value-item = guess value (2-8 bytes) +short-value-item = short (2 bytes) +int-value-item = int (4 bytes) +int64-value-item = int64 (8 bytes) +float-value-item = float (4 bytes) +double-value-item = double (8 bytes) + +guess-descr = Guess +short-descr = short +int-descr = int +int64-descr = int64 +float-descr = float +double-descr = double + +address-heading = Address +value-heading = Value +freezed-heading = Freezed + +pid-heading = Pid +name-heading = Name +memory-heading = Memory +command-heading = Command + +update-numbers-progress = Update { $current }/{ $total }… +search-memory-progress = Search { $current }/{ $total }… diff --git a/src/app.rs b/src/app.rs index 578e036..f5951aa 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,5 +1,6 @@ use egui::{Color32, RichText}; use egui_extras::{Column, TableBuilder}; +use i18n_embed_fl::fl; use process_memory::*; use std::{ cmp::max, @@ -25,9 +26,9 @@ impl GameCheetahEngine { ui.spacing_mut().item_spacing = egui::Vec2::splat(20.0); ui.horizontal(|ui| { ui.spacing_mut().item_spacing = egui::Vec2::splat(5.0); - + let i = ui.add( - egui::TextEdit::singleline(&mut self.process_filter).hint_text("Filter processes"), + egui::TextEdit::singleline(&mut self.process_filter).hint_text(fl!(crate::LANGUAGE_LOADER, "filter-processes-hint")), ); if ui.memory(|m| m.focus().is_none()) { ui.memory_mut(|m| m.request_focus(i.id)); @@ -39,7 +40,7 @@ impl GameCheetahEngine { self.process_filter.clear(); } - if ui.button("Close").clicked() { + if ui.button(fl!(crate::LANGUAGE_LOADER, "close-button")).clicked() { self.show_process_window = false; } }); @@ -56,16 +57,16 @@ impl GameCheetahEngine { table .header(20.0, |mut header| { header.col(|ui| { - ui.heading("Pid"); + ui.heading(fl!(crate::LANGUAGE_LOADER, "pid-heading")); }); header.col(|ui| { - ui.heading("Name"); + ui.heading(fl!(crate::LANGUAGE_LOADER, "name-heading")); }); header.col(|ui| { - ui.heading("Memory"); + ui.heading(fl!(crate::LANGUAGE_LOADER, "memory-heading")); }); header.col(|ui| { - ui.heading("Command"); + ui.heading(fl!(crate::LANGUAGE_LOADER, "command-heading")); }); }) .body(|mut body| { @@ -126,13 +127,19 @@ impl eframe::App for GameCheetahEngine { ui.spacing_mut().item_spacing = egui::Vec2::splat(12.0); ui.horizontal(|ui| { ui.spacing_mut().item_spacing = egui::Vec2::splat(5.0); - ui.label("Process:"); + ui.label(fl!( + crate::LANGUAGE_LOADER, + "process-label" + )); if ui .button(if self.pid != 0 { format!("{} ({})", self.process_name, self.pid) } else { - "".to_string() + fl!( + crate::LANGUAGE_LOADER, + "no-processes-label" + ) }) .clicked() { @@ -151,7 +158,7 @@ impl eframe::App for GameCheetahEngine { .unwrap_or_default(); self.searches.clear(); self.searches - .push(Box::new(SearchContext::new("default".to_string()))); + .push(Box::new(SearchContext::new( fl!(crate::LANGUAGE_LOADER, "default-label")))); self.process_filter.clear(); } }); @@ -207,11 +214,11 @@ impl GameCheetahEngine { if self.searches.len() > 1 { ui.horizontal(|ui| { ui.spacing_mut().item_spacing = egui::Vec2::splat(5.0); - ui.label("Name:"); + ui.label(fl!(crate::LANGUAGE_LOADER, "name-label")); if let Some(search_context) = self.searches.get_mut(search_index) { ui.add( egui::TextEdit::singleline(&mut search_context.description) - .hint_text("Search description") + .hint_text(fl!(crate::LANGUAGE_LOADER, "search-description-label")) .interactive(matches!(search_context.searching, SearchMode::None)), ); } @@ -220,14 +227,17 @@ impl GameCheetahEngine { ui.horizontal(|ui| { ui.spacing_mut().item_spacing = egui::Vec2::splat(5.0); - ui.label("Value:"); + ui.label(fl!(crate::LANGUAGE_LOADER, "value-label")); if let Some(search_context) = self.searches.get_mut(search_index) { let re = ui.add( egui::TextEdit::singleline(&mut search_context.search_value_text) - .hint_text(format!( - "Search for {} value", - search_context.search_type.get_description_text() - )) + .hint_text( + fl!( + crate::LANGUAGE_LOADER, + "search-value-label", + valuetype = search_context.search_type.get_description_text() + ), + ) .interactive(matches!(search_context.searching, SearchMode::None)), ); @@ -274,7 +284,7 @@ impl GameCheetahEngine { if ui .add_enabled( !search_context.old_results.is_empty(), - egui::Button::new("Undo"), + egui::Button::new(fl!(crate::LANGUAGE_LOADER, "undo-button")), ) .clicked() { @@ -316,7 +326,7 @@ impl GameCheetahEngine { .from_string(&search_context.search_value_text) .is_err() { - ui.label(RichText::new("Invalid number").color(Color32::from_rgb(200, 0, 0))); + ui.label(RichText::new(fl!(crate::LANGUAGE_LOADER, "invalid-number-error")).color(Color32::from_rgb(200, 0, 0))); } if !matches!( @@ -339,12 +349,12 @@ impl GameCheetahEngine { { let len = self.searches.get(search_index).unwrap().search_results; if len <= 0 { - if ui.button("Initial search").clicked() { + if ui.button(fl!(crate::LANGUAGE_LOADER, "initial-search-button")).clicked() { self.initial_search(search_index); return; } if len == 0 { - ui.label("No results found.".to_string()); + ui.label(fl!(crate::LANGUAGE_LOADER, "no-results-label")); } } else { let auto_show_treshold = 20; @@ -354,30 +364,30 @@ impl GameCheetahEngine { ui.spacing_mut().item_spacing = egui::Vec2::splat(5.0); - if ui.button("Update").clicked() { + if ui.button(fl!(crate::LANGUAGE_LOADER, "update-button")).clicked() { self.filter_searches(search_index); return; } - if ui.button("Clear").clicked() { + if ui.button(fl!(crate::LANGUAGE_LOADER, "clear-button")).clicked() { search_context.clear_results(&self.freeze_sender); return; } if len >= auto_show_treshold { if self.show_results { - if ui.button("Hide Results").clicked() { + if ui.button(fl!(crate::LANGUAGE_LOADER, "hide-results-button")).clicked() { self.show_results = false; return; } - } else if ui.button("Show Results").clicked() { + } else if ui.button(fl!(crate::LANGUAGE_LOADER, "show-results-button")).clicked() { self.show_results = true; return; } } if len == 1 { - ui.label(format!("found {len} result.")); + ui.label(fl!(crate::LANGUAGE_LOADER, "found-one-result-label")); } else { - ui.label(format!("found {len} results.")); + ui.label(fl!(crate::LANGUAGE_LOADER, "found-results-label", results=len)); } }); @@ -400,13 +410,13 @@ impl GameCheetahEngine { table .header(20.0, |mut header| { header.col(|ui| { - ui.heading("Address"); + ui.heading(fl!(crate::LANGUAGE_LOADER, "address-heading")); }); header.col(|ui| { - ui.heading("Value"); + ui.heading(fl!(crate::LANGUAGE_LOADER, "value-heading")); }); header.col(|ui| { - ui.heading("Freezed"); + ui.heading(fl!(crate::LANGUAGE_LOADER, "freezed-heading")); }); }) .body(|body| { @@ -454,15 +464,13 @@ impl GameCheetahEngine { "Error converting {:?}: {}", result.search_type, err ); - self.error_text = format!( - "Error converting {:?}: {}", - result.search_type, err - ); + self.error_text = fl!(crate::LANGUAGE_LOADER, "conversion-error", valuetype = result.search_type.get_short_description_text(), message = err); } } } } else { - ui.label(""); + + ui.label(fl!(crate::LANGUAGE_LOADER, "generic-error-label")); } } }); @@ -525,16 +533,13 @@ impl GameCheetahEngine { match search_context.searching { SearchMode::None => {} SearchMode::Percent => { - ui.label(format!( - "Update {}/{}…", - current_bytes, search_context.total_bytes - )); + ui.label(fl!(crate::LANGUAGE_LOADER, "update-numbers-progress", current=current_bytes, total=search_context.total_bytes)); } SearchMode::Memory => { let bb = gabi::BytesConfig::default(); - let current_bytes_out = bb.bytes(current_bytes as u64); - let total_bytes_out = bb.bytes(search_context.total_bytes as u64); - ui.label(format!("Search {current_bytes_out}/{total_bytes_out}")); + let current_bytes_out = bb.bytes(current_bytes as u64).to_string(); + let total_bytes_out = bb.bytes(search_context.total_bytes as u64).to_string(); + ui.label(fl!(crate::LANGUAGE_LOADER, "search-memory-progress", current=current_bytes_out, total=total_bytes_out)); } } diff --git a/src/lib.rs b/src/lib.rs index 9b44df5..e8ccb20 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -36,3 +36,21 @@ impl Message { } } } + + +use rust_embed::RustEmbed; +#[derive(RustEmbed)] +#[folder = "i18n"] // path to the compiled localization resources +struct Localizations; + +use i18n_embed::{ + fluent::{fluent_language_loader, FluentLanguageLoader}, + DesktopLanguageRequester, +}; +use once_cell::sync::Lazy; +pub static LANGUAGE_LOADER: Lazy = Lazy::new(|| { + let loader = fluent_language_loader!(); + let requested_languages = DesktopLanguageRequester::requested_languages(); + let _result = i18n_embed::select(&loader, &Localizations, &requested_languages); + loader +}); diff --git a/src/search_type.rs b/src/search_type.rs index ee851e8..d47d250 100644 --- a/src/search_type.rs +++ b/src/search_type.rs @@ -1,3 +1,5 @@ +use i18n_embed_fl::fl; + use crate::SearchValue; #[derive(Debug, PartialEq, Clone, Copy)] @@ -10,15 +12,16 @@ pub enum SearchType { Double, } + impl SearchType { - pub fn get_description_text(&self) -> &str { + pub fn get_description_text(&self) -> String { match self { - SearchType::Guess => "guess value (2-8 bytes)", - SearchType::Short => "short (2 bytes)", - SearchType::Int => "int (4 bytes)", - SearchType::Int64 => "int64 (4 bytes)", - SearchType::Float => "float (4 bytes)", - SearchType::Double => "double (8 bytes)", + SearchType::Guess => fl!(crate::LANGUAGE_LOADER, "guess-value-item"), + SearchType::Short => fl!(crate::LANGUAGE_LOADER, "short-value-item"), + SearchType::Int => fl!(crate::LANGUAGE_LOADER, "int-value-item"), + SearchType::Int64 => fl!(crate::LANGUAGE_LOADER, "int64-value-item"), + SearchType::Float => fl!(crate::LANGUAGE_LOADER, "float-value-item"), + SearchType::Double => fl!(crate::LANGUAGE_LOADER, "double-value-item"), } } @@ -33,45 +36,45 @@ impl SearchType { } } - pub fn get_short_description_text(&self) -> &str { + pub fn get_short_description_text(&self) -> String { match self { - SearchType::Guess => "Guess", - SearchType::Short => "short", - SearchType::Int => "int", - SearchType::Int64 => "int64", - SearchType::Float => "float", - SearchType::Double => "double", + SearchType::Guess => fl!(crate::LANGUAGE_LOADER, "guess-descr"), + SearchType::Short => fl!(crate::LANGUAGE_LOADER, "short-descr"), + SearchType::Int => fl!(crate::LANGUAGE_LOADER, "int-descr"), + SearchType::Int64 => fl!(crate::LANGUAGE_LOADER, "int64-descr"), + SearchType::Float => fl!(crate::LANGUAGE_LOADER, "float-descr"), + SearchType::Double => fl!(crate::LANGUAGE_LOADER, "double-descr") } } - pub fn from_string(&self, txt: &str) -> Result { + pub fn from_string(&self, txt: &str) -> Result { match self { SearchType::Short => { let parsed = txt.parse::(); match parsed { Ok(f) => Ok(SearchValue(SearchType::Short, i16::to_le_bytes(f).to_vec())), - Err(_) => Err("Invalid input"), + Err(_) => Err(fl!(crate::LANGUAGE_LOADER, "invalid-input-error")), } } SearchType::Int => { let parsed = txt.parse::(); match parsed { Ok(f) => Ok(SearchValue(SearchType::Int, i32::to_le_bytes(f).to_vec())), - Err(_) => Err("Invalid input"), + Err(_) => Err(fl!(crate::LANGUAGE_LOADER, "invalid-input-error")), } } SearchType::Int64 => { let parsed = txt.parse::(); match parsed { Ok(f) => Ok(SearchValue(SearchType::Int64, i64::to_le_bytes(f).to_vec())), - Err(_) => Err("Invalid input"), + Err(_) => Err(fl!(crate::LANGUAGE_LOADER, "invalid-input-error")), } } SearchType::Float => { let parsed = txt.parse::(); match parsed { Ok(f) => Ok(SearchValue(SearchType::Float, f32::to_le_bytes(f).to_vec())), - Err(_) => Err("Invalid input"), + Err(_) => Err(fl!(crate::LANGUAGE_LOADER, "invalid-input-error")), } } SearchType::Double => { @@ -81,7 +84,7 @@ impl SearchType { SearchType::Double, f64::to_le_bytes(f).to_vec(), )), - Err(_) => Err("Invalid input"), + Err(_) => Err(fl!(crate::LANGUAGE_LOADER, "invalid-input-error")), } } SearchType::Guess => {