diff --git a/locales/app.yml b/locales/app.yml new file mode 100644 index 00000000..a10cc334 --- /dev/null +++ b/locales/app.yml @@ -0,0 +1,222 @@ +_version: 2 + +"Dry running:": + en: "Dry running:" +"Rebooting...": + en: "Rebooting..." +"Plugins upgraded": + en: "Plugins upgraded" +"Would self-update": + en: "Would self-update" +"Pulling": + en: "Pulling" +"No Breaking changes": + en: "No Breaking changes" +"in": + en: "in" +"Dropping you to shell. Fix what you need and then exit the shell.": + en: "Dropping you to shell. Fix what you need and then exit the shell." +"Topgrade launched in a new tmux session": + en: "Topgrade launched in a new tmux session" +"Topgrade upgraded to ": + en: "Topgrade upgraded to " +"Topgrade is up-to-date": + en: "Topgrade is up-to-date" +"Updating modules...": + en: "Updating modules..." +"Powershell Modules Update": + en: "Powershell Modules Update" +"Powershell is not installed": + en: "Powershell is not installed" +"Error detecting current distribution:": + en: "Error detecting current distribution:" +"Error:": + en: "Error:" +"Failed": + en: "Failed" +"pulling": + en: "pulling" +"Failed to pull": + en: "Failed to pull" +"Changed": + en: "Changed" +"Up-to-date": + en: "Up-to-date" +"Self update": + en: "Self update" +"Would pull": + en: "Would pull" +"Only": + en: "Only" +"updated repositories will be shown...": + en: "updated repositories will be shown..." +"because it has no remotes": + en: "because it has no remotes" +"Skipping": + en: "Skipping" +"Aura requires sudo installed to work with AUR packages": + en: "Aura requires sudo installed to work with AUR packages" +"Pacman backup configuration files found:": + en: "Pacman backup configuration files found:" +"The package audit was successful, but vulnerable packages still remain on the system": + en: "The package audit was successful, but vulnerable packages still remain on the system" +"Syncing portage": + en: "Syncing portage" +"Finding available software": + en: "Finding available software" +"A system update is available. Do you wish to install it?": + en: "A system update is available. Do you wish to install it?" +"No new software available.": + en: "No new software available." +"No Xcode releases installed.": + en: "No Xcode releases installed." +"Would you like to move the former Xcode release to the trash?": + en: "Would you like to move the former Xcode release to the trash?" +"New Xcode release detected:": + en: "New Xcode release detected:" +"Would you like to install it?": + en: "Would you like to install it?" +"No global packages installed": + en: "No global packages installed" +"Connecting to": + en: "Connecting to" +"Remote Topgrade launched in Tmux": + en: "Remote Topgrade launched in Tmux" +"Remote Topgrade launched in an external terminal": + en: "Remote Topgrade launched in an external terminal" +"Collecting Vagrant boxes": + en: "Collecting Vagrant boxes" +"No Vagrant directories were specified in the configuration file": + en: "No Vagrant directories were specified in the configuration file" +"Vagrant output in": + en: "Vagrant output in" +"Vagrant line": + en: "Vagrant line" +"Error collecting vagrant boxes from": + en: "Error collecting vagrant boxes from" +"Skipping powered off box": + en: "Skipping powered off box" +"Vagrant boxes": + en: "Vagrant boxes" +"No outdated boxes": + en: "No outdated boxes" +"No home directory": + en: "No home directory" +"Version": + en: "Version" +"OS": + en: "OS" +"Binary path": + en: "Binary path" +"self-update Feature Enabled": + en: "self-update Feature Enabled" +"Configuration": + en: "Configuration" +"Summary": + en: "Summary" +"Failed to execute shell": + en: "Failed to execute shell" +"Failed to reboot": + en: "Failed to reboot" +"Topgrade finished with errors": + en: "Topgrade finished with errors" +"Topgrade finished successfully": + en: "Topgrade finished successfully" +"Topgrade version is not semantic": + en: "Topgrade version is not semantic" +"Topgrade version is not dot-separated numbers": + en: "Topgrade version is not dot-separated numbers" +"Version numbers can not be all 0s": + en: "Version numbers can not be all 0s" +"should be a valid version": + en: "should be a valid version" +"Breaking Changes": + en: "Breaking Changes" +"Stdout contained invalid UTF-8": + en: "Stdout contained invalid UTF-8" +"Stderr contained invalid UTF-8": + en: "Stderr contained invalid UTF-8" +"Failed to execute": + en: "Failed to execute" +"Command failed": + en: "Command failed" +"Executing command": + en: "Executing command" +"Configuration at": + en: "Configuration at" +"No configuration exists": + en: "No configuration exists" +"Unable to write the example configuration file to": + en: "Unable to write the example configuration file to %{location}: %{error}. Using blank config." +"Found additional (directory) configuration file at": + en: "Found additional (directory) configuration file at %{path}" +"No additional configuration directory exists, creating one": + en: "No additional configuration directory exists, creating one" +"Unable to read": + en: "Unable to read" +"Failed to deserialize": + en: "Failed to deserialize" +"Failed to compile regex": + en: "Failed to compile regex" +"Failed to deserialize an include section of": + en: "Failed to deserialize an include section of" +"Path expanded to": + en: "Path expanded %{path} to %{expanded}" +"Loaded configuration": + en: "Loaded configuration" +"Editor": + en: "Editor" +"Failed to open configuration file editor": + en: "Failed to open configuration file editor" +"Adding [misc] section to": + en: "Adding [misc] section to %{path}" +"Tried to auto-migrate the config file, unable to write to config file.\nPlease add \"[misc]\" section manually to the first line of the file.\nError": + en: "Tried to auto-migrate the config file, unable to write to config file.\nPlease add \"[misc]\" section manually to the first line of the file.\nError" +"failed to load configuration": + en: "failed to load configuration" +"Configuration directory {} does not exist": + en: "Configuration directory %{directory} does not exist" +"Failed to parse `tmux_arguments`": + en: "Failed to parse `tmux_arguments`" +"Running": + en: "Running" +"{key} already reported": + en: "%{key} already reported" +"Step": + en: "Step" +"failed": + en: "failed" +"Current exe in": + en: "Current exe in" +"Moving it to": + en: "Moving it to" +"exists. Topgrade was probably upgraded": + en: "exists. Topgrade was probably upgraded" +"Moved Topgrade back from": + en: "Moved Topgrade back from" +"to": + en: "to" +"Could not move Topgrade from {} back to {}: {}": + en: "Could not move Topgrade from %{temp_path} back to %{exe_path}: {error}" +"Failed to elevate permissions": + en: "Failed to elevate permissions" +"Desktop notification": + en: "Desktop notification" +"Error reading from terminal": + en: "Error reading from terminal" +"Quit from user input": + en: "Quit from user input" +"Path": + en: "Path" +"exists": + en: "exists" +"doesn't exist": + en: "doesn't exist" +"Detected": + en: "Detected" +"as": + en: "as" +"Cannot find": + en: "Cannot find" +"Detecting": + en: "Detecting" \ No newline at end of file diff --git a/src/breaking_changes.rs b/src/breaking_changes.rs index 520db396..cdc69044 100644 --- a/src/breaking_changes.rs +++ b/src/breaking_changes.rs @@ -11,6 +11,7 @@ use crate::WINDOWS_DIRS; use crate::XDG_DIRS; use color_eyre::eyre::Result; use etcetera::base_strategy::BaseStrategy; +use rust_i18n::t; use std::{ env::var, fs::{read_to_string, OpenOptions}, @@ -34,18 +35,19 @@ impl FromStr for Version { type Err = std::convert::Infallible; fn from_str(s: &str) -> Result { - const NOT_SEMVER: &str = "Topgrade version is not semantic"; - const NOT_NUMBER: &str = "Topgrade version is not dot-separated numbers"; + let not_semver = t!("Topgrade version is not semantic").to_string(); + let not_number = t!("Topgrade version is not dot-separated numbers").to_string(); let mut iter = s.split('.').take(3); - let major = iter.next().expect(NOT_SEMVER).parse().expect(NOT_NUMBER); - let minor = iter.next().expect(NOT_SEMVER).parse().expect(NOT_NUMBER); - let patch = iter.next().expect(NOT_SEMVER).parse().expect(NOT_NUMBER); + let major = iter.next().expect(¬_semver).parse().expect(¬_number); + let minor = iter.next().expect(¬_semver).parse().expect(¬_number); + let patch = iter.next().expect(¬_semver).parse().expect(¬_number); // They cannot be all 0s assert!( !(major == 0 && minor == 0 && patch == 0), - "Version numbers can not be all 0s" + "{}", + t!("Version numbers can not be all 0s") ); Ok(Self { @@ -102,7 +104,7 @@ pub(crate) fn should_skip() -> bool { /// True if this is the first execution of a major release. pub(crate) fn first_run_of_major_release() -> Result { - let version = VERSION_STR.parse::().expect("should be a valid version"); + let version = VERSION_STR.parse::().expect(&*t!("should be a valid version")); let keep_file = keep_file_path(); // disable this lint here as the current code has better readability @@ -118,12 +120,12 @@ pub(crate) fn first_run_of_major_release() -> Result { /// Print breaking changes to the user. pub(crate) fn print_breaking_changes() { - let header = format!("Topgrade {VERSION_STR} Breaking Changes"); + let header = format!("Topgrade {VERSION_STR} {}", t!("Breaking Changes")); print_separator(header); let contents = if BREAKINGCHANGES.is_empty() { - "No Breaking changes" + t!("No Breaking changes").to_string() } else { - BREAKINGCHANGES + BREAKINGCHANGES.to_string() }; println!("{contents}\n"); } @@ -159,7 +161,7 @@ mod test { } #[test] - #[should_panic(expected = "Version numbers can not be all 0s")] + #[should_panic(expected = "Version numbers can not be all 0s")] // TODO: Problems with i18n? fn invalid_version() { let all_0 = "0.0.0"; all_0.parse::().unwrap(); diff --git a/src/command.rs b/src/command.rs index 5c8bb804..ad9c012a 100644 --- a/src/command.rs +++ b/src/command.rs @@ -7,6 +7,7 @@ use std::process::{Command, ExitStatus, Output}; use color_eyre::eyre; use color_eyre::eyre::eyre; use color_eyre::eyre::Context; +use rust_i18n::t; use crate::error::TopgradeError; @@ -26,13 +27,15 @@ impl TryFrom for Utf8Output { fn try_from(Output { status, stdout, stderr }: Output) -> Result { let stdout = String::from_utf8(stdout).map_err(|err| { eyre!( - "Stdout contained invalid UTF-8: {}", + "{}: {}", + t!("Stdout contained invalid UTF-8"), String::from_utf8_lossy(err.as_bytes()) ) })?; let stderr = String::from_utf8(stderr).map_err(|err| { eyre!( - "Stderr contained invalid UTF-8: {}", + "{}: {}", + t!("Stderr contained invalid UTF-8"), String::from_utf8_lossy(err.as_bytes()) ) })?; @@ -47,13 +50,15 @@ impl TryFrom<&Output> for Utf8Output { fn try_from(Output { status, stdout, stderr }: &Output) -> Result { let stdout = String::from_utf8(stdout.to_vec()).map_err(|err| { eyre!( - "Stdout contained invalid UTF-8: {}", + "{}: {}", + t!("Stdout contained invalid UTF-8"), String::from_utf8_lossy(err.as_bytes()) ) })?; let stderr = String::from_utf8(stderr.to_vec()).map_err(|err| { eyre!( - "Stderr contained invalid UTF-8: {}", + "{}: {}", + t!("Stderr contained invalid UTF-8"), String::from_utf8_lossy(err.as_bytes()) ) })?; @@ -163,12 +168,12 @@ impl CommandExt for Command { #[allow(clippy::disallowed_methods)] let output = self .output() - .with_context(|| format!("Failed to execute `{command}`"))?; + .with_context(|| format!("{} `{command}`", t!("Failed to execute")))?; if succeeded(&output).is_ok() { Ok(output) } else { - let mut message = format!("Command failed: `{command}`"); + let mut message = format!("{}: `{command}`", t!("Command failed")); let stderr = String::from_utf8_lossy(&output.stderr); let stdout = String::from_utf8_lossy(&output.stdout); @@ -185,14 +190,14 @@ impl CommandExt for Command { let err = TopgradeError::ProcessFailedWithOutput(program, output.status, stderr.into_owned()); let ret = Err(err).with_context(|| message); - debug!("Command failed: {ret:?}"); + debug!("{}: {ret:?}", t!("Command failed")); ret } } fn status_checked_with(&mut self, succeeded: impl Fn(ExitStatus) -> Result<(), ()>) -> eyre::Result<()> { let command = log(self); - let message = format!("Failed to execute `{command}`"); + let message = format!("{} `{command}`", t!("Failed to execute")); // This is where we implement `status_checked`, which is what we prefer to use instead of // `status`, so we allow `Command::status` here. @@ -204,15 +209,15 @@ impl CommandExt for Command { } else { let (program, _) = get_program_and_args(self); let err = TopgradeError::ProcessFailed(program, status); - let ret = Err(err).with_context(|| format!("Command failed: `{command}`")); - debug!("Command failed: {ret:?}"); + let ret = Err(err).with_context(|| format!("{}: `{command}`", t!("Command failed"))); + debug!("{}: {ret:?}", t!("Command failed")); ret } } fn spawn_checked(&mut self) -> eyre::Result { let command = log(self); - let message = format!("Failed to execute `{command}`"); + let message = format!("{} `{command}`", t!("Failed to execute")); // This is where we implement `spawn_checked`, which is what we prefer to use instead of // `spawn`, so we allow `Command::spawn` here. @@ -241,6 +246,6 @@ fn format_program_and_args(cmd: &Command) -> String { fn log(cmd: &Command) -> String { let command = format_program_and_args(cmd); - debug!("Executing command `{command}`"); + debug!("{} `{command}`", t!("Executing command")); command } diff --git a/src/config.rs b/src/config.rs index c172c56d..9b46d54e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -15,6 +15,7 @@ use etcetera::base_strategy::BaseStrategy; use merge::Merge; use regex::Regex; use regex_split::RegexSplit; +use rust_i18n::t; use serde::Deserialize; use strum::{EnumIter, EnumString, IntoEnumIterator, VariantNames}; use which_crate::which; @@ -489,7 +490,7 @@ impl ConfigFile { // Search for the main config file for path in possible_config_paths.iter() { if path.exists() { - debug!("Configuration at {}", path.display()); + debug!("{} {}", t!("Configuration at"), path.display()); res.0.clone_from(path); break; } @@ -500,12 +501,15 @@ impl ConfigFile { // If no config file exists, create a default one in the config directory if !res.0.exists() && res.1.is_empty() { res.0.clone_from(&possible_config_paths[0]); - debug!("No configuration exists"); + debug!("{}", t!("No configuration exists")); write(&res.0, EXAMPLE_CONFIG).map_err(|e| { debug!( - "Unable to write the example configuration file to {}: {}. Using blank config.", - &res.0.display(), - e + "{}", + t!( + "Unable to write the example configuration file to", + location = &res.0.display(), + error = e + ), ); e })?; @@ -524,15 +528,18 @@ impl ConfigFile { let entry = entry?; if entry.file_type()?.is_file() { debug!( - "Found additional (directory) configuration file at {}", - entry.path().display() + "{}", + t!( + "Found additional (directory) configuration file at", + path = entry.path().display() + ) ); res.push(entry.path()); } } res.sort(); } else { - debug!("No additional configuration directory exists, creating one"); + debug!("{}", t!("No additional configuration directory exists, creating one")); fs::create_dir_all(&dir_to_search)?; } @@ -556,11 +563,11 @@ impl ConfigFile { */ for include in dir_include { let include_contents = fs::read_to_string(&include).map_err(|e| { - error!("Unable to read {}", include.display()); + error!("{} {}", t!("Unable to read"), include.display()); e })?; let include_contents_parsed = toml::from_str(include_contents.as_str()).map_err(|e| { - error!("Failed to deserialize {}", include.display()); + error!("{} {}", t!("Failed to deserialize"), include.display()); e })?; @@ -577,7 +584,7 @@ impl ConfigFile { } let mut contents_non_split = fs::read_to_string(&config_path).map_err(|e| { - error!("Unable to read {}", config_path.display()); + error!("{} {}", t!("Unable to read"), config_path.display()); e })?; @@ -585,12 +592,16 @@ impl ConfigFile { // To parse [include] sections in the order as they are written, // we split the file and parse each part as a separate file - let regex_match_include = Regex::new(r"^\s*\[include]").expect("Failed to compile regex"); + let regex_match_include = Regex::new(r"^\s*\[include]").expect(&*t!("Failed to compile regex")); let contents_split = regex_match_include.split_inclusive_left(contents_non_split.as_str()); for contents in contents_split { let config_file_include_only: ConfigFileIncludeOnly = toml::from_str(contents).map_err(|e| { - error!("Failed to deserialize an include section of {}", config_path.display()); + error!( + "{} {}", + t!("Failed to deserialize an include section of"), + config_path.display() + ); e })?; @@ -603,14 +614,14 @@ impl ConfigFile { let include_contents = match fs::read_to_string(&include_path) { Ok(c) => c, Err(e) => { - error!("Unable to read {}: {}", include_path.display(), e); + error!("{} {}: {}", t!("Unable to read"), include_path.display(), e); continue; } }; match toml::from_str::(&include_contents) { Ok(include_parsed) => result.merge(include_parsed), Err(e) => { - error!("Failed to deserialize {}: {}", include_path.display(), e); + error!("{} {}: {}", t!("Failed to deserialize"), include_path.display(), e); continue; } }; @@ -620,26 +631,26 @@ impl ConfigFile { match toml::from_str::(contents) { Ok(contents) => result.merge(contents), - Err(e) => error!("Failed to deserialize {}: {}", config_path.display(), e), + Err(e) => error!("{} {}: {}", t!("Failed to deserialize"), config_path.display(), e), } } if let Some(paths) = result.git.as_mut().and_then(|git| git.repos.as_mut()) { for path in paths.iter_mut() { let expanded = shellexpand::tilde::<&str>(&path.as_ref()).into_owned(); - debug!("Path {} expanded to {}", path, expanded); + debug!("{}", t!("Path expanded to", path = path, expanded = expanded)); *path = expanded; } } - debug!("Loaded configuration: {:?}", result); + debug!("{}: {:?}", "Loaded configuration", result); Ok(result) } fn edit() -> Result<()> { let config_path = Self::ensure()?.0; let editor = editor(); - debug!("Editor: {:?}", editor); + debug!("{}: {:?}", t!("Editor"), editor); let command = which(&editor[0])?; let args: Vec<&String> = editor.iter().skip(1).collect(); @@ -648,23 +659,24 @@ impl ConfigFile { .args(args) .arg(config_path) .status_checked() - .context("Failed to open configuration file editor") + .context(t!("Failed to open configuration file editor")) } /// [Misc] was added later, here we check if it is present in the config file and add it if not fn ensure_misc_is_present(contents: &mut String, path: &PathBuf) { if !contents.contains("[misc]") { - debug!("Adding [misc] section to {}", path.display()); + debug!("{}", t!("Adding [misc] section to ", path = path.display())); string_prepend_str(contents, "[misc]\n"); File::create(path) .and_then(|mut f| f.write_all(contents.as_bytes())) - .expect("Tried to auto-migrate the config file, unable to write to config file.\nPlease add \"[misc]\" section manually to the first line of the file.\nError"); + .expect(&*t!("Tried to auto-migrate the config file, unable to write to config file.\nPlease add \"[misc]\" section manually to the first line of the file.\nError")); } } } // Command line arguments +// TODO: i18n of clap currently not easily possible. Waiting for https://github.com/clap-rs/clap/issues/380 #[derive(Parser, Debug)] #[clap(name = "Topgrade", version)] pub struct CommandLineArgs { @@ -822,11 +834,17 @@ impl Config { ConfigFile::read(opt.config.clone()).unwrap_or_else(|e| { // Inform the user about errors when loading the configuration, // but fallback to the default config to at least attempt to do something - error!("failed to load configuration: {}", e); + error!("{}: {}", t!("failed to load configuration"), e); ConfigFile::default() }) } else { - debug!("Configuration directory {} does not exist", config_directory.display()); + debug!( + "{}", + t!( + "Configuration directory {} does not exist", + directory = config_directory.display() + ) + ); ConfigFile::default() }; @@ -1001,7 +1019,7 @@ impl Config { // // Caused by: // missing closing quote - .with_context(|| format!("Failed to parse `tmux_arguments`: `{args}`")) + .with_context(|| format!("{}: `{args}`", t!("Failed to parse `tmux_arguments`"))) } /// Prompt for a key before exiting diff --git a/src/error.rs b/src/error.rs index 96819f60..227b4049 100644 --- a/src/error.rs +++ b/src/error.rs @@ -24,7 +24,7 @@ pub enum TopgradeError { } #[derive(Error, Debug)] -#[error("A step failed")] +#[error("A step failed")] // TODO: How to add i18 to these? pub struct StepFailed; #[derive(Error, Debug)] diff --git a/src/executor.rs b/src/executor.rs index 5353f60d..e66efbbc 100644 --- a/src/executor.rs +++ b/src/executor.rs @@ -4,6 +4,7 @@ use std::path::Path; use std::process::{Child, Command, ExitStatus, Output}; use color_eyre::eyre::Result; +use rust_i18n::t; use tracing::debug; use crate::command::CommandExt; @@ -150,7 +151,7 @@ impl Executor { pub fn spawn(&mut self) -> Result { let result = match self { Executor::Wet(c) => { - debug!("Running {:?}", c); + debug!("{} {:?}", t!("Running"), c); c.spawn_checked().map(ExecutorChild::Wet)? } Executor::Dry(c) => { @@ -209,7 +210,8 @@ pub struct DryCommand { impl DryCommand { fn dry_run(&self) { print!( - "Dry running: {} {}", + "{} {} {}", + t!("Dry running:"), self.program.to_string_lossy(), shell_words::join( self.args @@ -219,7 +221,7 @@ impl DryCommand { ) ); match &self.directory { - Some(dir) => println!(" in {}", dir.to_string_lossy()), + Some(dir) => println!(" {} {}", t!("in"), dir.to_string_lossy()), None => println!(), }; } diff --git a/src/main.rs b/src/main.rs index e80e0df9..9052cf2f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,6 +18,7 @@ use etcetera::base_strategy::Windows; #[cfg(unix)] use etcetera::base_strategy::Xdg; use once_cell::sync::Lazy; +use rust_i18n::{i18n, t}; use tracing::debug; use self::config::{CommandLineArgs, Config, Step}; @@ -47,13 +48,16 @@ mod sudo; mod terminal; mod utils; -pub(crate) static HOME_DIR: Lazy = Lazy::new(|| home::home_dir().expect("No home directory")); +pub(crate) static HOME_DIR: Lazy = Lazy::new(|| home::home_dir().expect(&*t!("No home directory"))); #[cfg(unix)] -pub(crate) static XDG_DIRS: Lazy = Lazy::new(|| Xdg::new().expect("No home directory")); +pub(crate) static XDG_DIRS: Lazy = Lazy::new(|| Xdg::new().expect(&*t!("No home directory"))); #[cfg(windows)] pub(crate) static WINDOWS_DIRS: Lazy = Lazy::new(|| Windows::new().expect("No home directory")); +// Initialization of the i18n crate +i18n!("locales", fallback = "en"); + fn run() -> Result<()> { install_color_eyre()?; ctrlc::set_handler(); @@ -97,7 +101,7 @@ fn run() -> Result<()> { }; if opt.show_config_reference() { - print!("{}", config::EXAMPLE_CONFIG); + print!("{}", config::EXAMPLE_CONFIG); // TODO: Find a way to use a translated example config return Ok(()); } @@ -108,12 +112,16 @@ fn run() -> Result<()> { display_time(config.display_time()); set_desktop_notifications(config.notify_each_step()); - debug!("Version: {}", crate_version!()); - debug!("OS: {}", env!("TARGET")); + debug!("{}: {}", t!("Version"), crate_version!()); + debug!("{}: {}", t!("OS"), env!("TARGET")); debug!("{:?}", std::env::args()); - debug!("Binary path: {:?}", std::env::current_exe()); - debug!("self-update Feature Enabled: {:?}", cfg!(feature = "self-update")); - debug!("Configuration: {:?}", config); + debug!("{}: {:?}", t!("Binary path"), std::env::current_exe()); + debug!( + "{}: {:?}", + t!("self-update Feature Enabled"), + cfg!(feature = "self-update") + ); + debug!("{}: {:?}", t!("Configuration"), config); if config.run_in_tmux() && env::var("TOPGRADE_INSIDE_TMUX").is_err() { #[cfg(unix)] @@ -210,7 +218,7 @@ fn run() -> Result<()> { runner.execute(Step::System, "System update", || distribution.upgrade(&ctx))?; } Err(e) => { - println!("Error detecting current distribution: {e}"); + println!("{} {e}", t!("Error detecting current distribution:")); } } runner.execute(Step::ConfigUpdate, "config-update", || linux::run_config_update(&ctx))?; @@ -441,7 +449,7 @@ fn run() -> Result<()> { runner.execute(Step::Vagrant, "Vagrant boxes", || vagrant::upgrade_vagrant_boxes(&ctx))?; if !runner.report().data().is_empty() { - print_separator("Summary"); + print_separator(t!("Summary")); for (key, result) in runner.report().data() { print_result(key, result); @@ -465,14 +473,16 @@ fn run() -> Result<()> { } if config.keep_at_end() { + // TODO: Refactor this to make it easier to implement i18n + // Ie use the first letter from the translations, not a hardcoded literal print_info("\n(R)eboot\n(S)hell\n(Q)uit"); loop { match get_key() { Ok(Key::Char('s')) | Ok(Key::Char('S')) => { - run_shell().context("Failed to execute shell")?; + run_shell().context(t!("Failed to execute shell"))?; } Ok(Key::Char('r')) | Ok(Key::Char('R')) => { - reboot().context("Failed to reboot")?; + reboot().context(t!("Failed to reboot"))?; } Ok(Key::Char('q')) | Ok(Key::Char('Q')) => (), _ => { @@ -487,10 +497,11 @@ fn run() -> Result<()> { if !config.skip_notify() { notify_desktop( - format!( - "Topgrade finished {}", - if failed { "with errors" } else { "successfully" } - ), + if failed { + t!("Topgrade finished with errors") + } else { + t!("Topgrade finished successfully") + }, Some(Duration::from_secs(10)), ) } @@ -525,7 +536,7 @@ fn main() { // The `Debug` implementation of `eyre::Result` prints a multi-line // error message that includes all the 'causes' added with // `.with_context(...)` calls. - println!("Error: {error:?}"); + println!("{} {error:?}", t!("Error:")); } exit(1); } diff --git a/src/report.rs b/src/report.rs index 77e0d57a..f24b1fac 100644 --- a/src/report.rs +++ b/src/report.rs @@ -1,3 +1,4 @@ +use rust_i18n::t; use std::borrow::Cow; pub enum StepResult { @@ -34,7 +35,11 @@ impl<'a> Report<'a> { if let Some((key, success)) = result { let key = key.into(); - debug_assert!(!self.data.iter().any(|(k, _)| k == &key), "{key} already reported"); + debug_assert!( + !self.data.iter().any(|(k, _)| k == &key), + "{}", + t!("{key} already reported", key = key) + ); self.data.push((key, success)); } } diff --git a/src/runner.rs b/src/runner.rs index af26c725..73c5a188 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -5,6 +5,7 @@ use crate::report::{Report, StepResult}; use crate::terminal::print_error; use crate::{config::Step, terminal::should_retry}; use color_eyre::eyre::Result; +use rust_i18n::t; use std::borrow::Cow; use std::fmt::Debug; use tracing::debug; @@ -32,7 +33,7 @@ impl<'a> Runner<'a> { } let key = key.into(); - debug!("Step {:?}", key); + debug!("{} {:?}", t!("Step"), key); // alter the `func` to put it in a span let func = || { @@ -56,7 +57,7 @@ impl<'a> Runner<'a> { break; } Err(e) => { - debug!("Step {:?} failed: {:?}", key, e); + debug!("{} {:?} {}: {:?}", t!("Step"), key, t!("failed"), e); let interrupted = ctrlc::interrupted(); if interrupted { ctrlc::unset_interrupted(); diff --git a/src/self_renamer.rs b/src/self_renamer.rs index 4a26b672..82c3cbe0 100644 --- a/src/self_renamer.rs +++ b/src/self_renamer.rs @@ -15,7 +15,7 @@ impl SelfRenamer { let temp_path = tempdir.path().join("topgrade.exe"); let exe_path = current_exe()?; - debug!("Current exe in {:?}. Moving it to {:?}", exe_path, temp_path); + debug!("{} {:?}. {} {:?}", t!("Current exe in"), exe_path, t!("Moving it to"), temp_path); fs::rename(&exe_path, &temp_path)?; @@ -26,17 +26,15 @@ impl SelfRenamer { impl Drop for SelfRenamer { fn drop(&mut self) { if self.exe_path.exists() { - debug!("{:?} exists. Topgrade was probably upgraded", self.exe_path); + debug!("{:?} {}", self.exe_path, t!("exists. Topgrade was probably upgraded")); return; } match fs::rename(&self.temp_path, &self.exe_path) { - Ok(_) => debug!("Moved Topgrade back from {:?} to {:?}", self.temp_path, self.exe_path), + Ok(_) => debug!("{} {:?} {} {:?}", t!("Moved Topgrade back from"), self.temp_path, t!("to"), self.exe_path), Err(e) => error!( - "Could not move Topgrade from {} back to {}: {}", - self.temp_path.display(), - self.exe_path.display(), - e + "{}", + t!("Could not move Topgrade from {} back to {}: {}", temp_path=self.temp_path.display(), exe_path=self.exe_path.display(), error=e) ), } } diff --git a/src/self_update.rs b/src/self_update.rs index d1d664d8..fab251f4 100644 --- a/src/self_update.rs +++ b/src/self_update.rs @@ -5,6 +5,7 @@ use std::process::Command; use crate::config::Step; use color_eyre::eyre::{bail, Result}; +use rust_i18n::t; use self_update_crate::backends::github::Update; use self_update_crate::update::UpdateStatus; @@ -15,10 +16,10 @@ use crate::error::Upgraded; use crate::execution_context::ExecutionContext; pub fn self_update(ctx: &ExecutionContext) -> Result<()> { - print_separator("Self update"); + print_separator(t!("Self update")); if ctx.run_type().dry() { - println!("Would self-update"); + println!("{}", t!("Would self-update")); Ok(()) } else { let assume_yes = ctx.config().yes(Step::SelfUpdate); @@ -38,17 +39,17 @@ pub fn self_update(ctx: &ExecutionContext) -> Result<()> { .update_extended()?; if let UpdateStatus::Updated(release) = &result { - println!("\nTopgrade upgraded to {}:\n", release.version); + println!("\n{}{}:\n", t!("Topgrade upgraded to "), release.version); if let Some(body) = &release.body { - println!("{body}"); + println!("{body}"); // TODO: Any way to translate what I think are the release notes? } } else { - println!("Topgrade is up-to-date"); + println!("{}", t!("Topgrade is up-to-date")); } { if result.updated() { - print_info("Respawning..."); + print_info(t!("Respawning...")); let mut command = Command::new(current_exe?); command.args(env::args().skip(1)).env("TOPGRADE_NO_SELF_UPGRADE", ""); diff --git a/src/steps/git.rs b/src/steps/git.rs index 7988aed4..beedece9 100644 --- a/src/steps/git.rs +++ b/src/steps/git.rs @@ -20,6 +20,7 @@ use crate::terminal::print_separator; use crate::utils::{require, PathExt}; use crate::{error::SkipStep, terminal::print_warning, HOME_DIR}; use etcetera::base_strategy::BaseStrategy; +use rust_i18n::t; #[cfg(unix)] use crate::XDG_DIRS; @@ -296,7 +297,7 @@ impl RepoStep { let before_revision = get_head_revision(&self.git, &repo); if ctx.config().verbose() { - println!("{} {}", style("Pulling").cyan().bold(), repo.as_ref().display()); + println!("{} {}", style(t!("Pulling")).cyan().bold(), repo.as_ref().display()); } let mut command = AsyncCommand::new(&self.git); @@ -319,16 +320,21 @@ impl RepoStep { .await?; let result = output_checked_utf8(pull_output) .and_then(|_| output_checked_utf8(submodule_output)) - .wrap_err_with(|| format!("Failed to pull {}", repo.as_ref().display())); + .wrap_err_with(|| format!("{} {}", t!("Failed to pull"), repo.as_ref().display())); if result.is_err() { - println!("{} pulling {}", style("Failed").red().bold(), repo.as_ref().display()); + println!( + "{} {} {}", + style(t!("Failed")).red().bold(), + t!("pulling"), + repo.as_ref().display() + ); } else { let after_revision = get_head_revision(&self.git, repo.as_ref()); match (&before_revision, &after_revision) { (Some(before), Some(after)) if before != after => { - println!("{} {}", style("Changed").yellow().bold(), repo.as_ref().display()); + println!("{} {}", style(t!("Changed")).yellow().bold(), repo.as_ref().display()); Command::new(&self.git) .stdin(Stdio::null()) @@ -345,7 +351,7 @@ impl RepoStep { } _ => { if ctx.config().verbose() { - println!("{} {}", style("Up-to-date").green().bold(), repo.as_ref().display()); + println!("{} {}", style(t!("Up-to-date")).green().bold(), repo.as_ref().display()); } } } @@ -363,15 +369,16 @@ impl RepoStep { if ctx.run_type().dry() { self.repos .iter() - .for_each(|repo| println!("Would pull {}", repo.display())); + .for_each(|repo| println!("{} {}", t!("Would pull"), repo.display())); return Ok(()); } if !ctx.config().verbose() { println!( - "\n{} updated repositories will be shown...\n", - style("Only").green().bold() + "\n{} {}\n", + style(t!("Only")).green().bold(), + t!("updated repositories will be shown...") ); } @@ -381,9 +388,10 @@ impl RepoStep { .filter(|repo| match self.has_remotes(repo) { Some(false) => { println!( - "{} {} because it has no remotes", - style("Skipping").yellow().bold(), - repo.display() + "{} {} {}", + style(t!("Skipping")).yellow().bold(), + repo.display(), + t!("because it has no remotes") ); false } diff --git a/src/steps/kakoune.rs b/src/steps/kakoune.rs index d1b955aa..ba06c748 100644 --- a/src/steps/kakoune.rs +++ b/src/steps/kakoune.rs @@ -1,6 +1,7 @@ use crate::terminal::print_separator; use crate::utils::require; use color_eyre::eyre::Result; +use rust_i18n::t; use crate::execution_context::ExecutionContext; @@ -17,7 +18,7 @@ pub fn upgrade_kak_plug(ctx: &ExecutionContext) -> Result<()> { .args(["-ui", "dummy", "-e", UPGRADE_KAK]) .output()?; - println!("Plugins upgraded"); + println!("{}", t!("Plugins upgraded")); Ok(()) } diff --git a/src/steps/os/archlinux.rs b/src/steps/os/archlinux.rs index c1b21227..d750b132 100644 --- a/src/steps/os/archlinux.rs +++ b/src/steps/os/archlinux.rs @@ -291,7 +291,7 @@ impl ArchPackageManager for Aura { aur_update.status_checked()?; } else { - println!("Aura requires sudo installed to work with AUR packages") + println!(t!("Aura requires sudo installed to work with AUR packages")) } let mut pacman_update = ctx.run_type().execute(&self.sudo); @@ -355,7 +355,7 @@ pub fn show_pacnew() { .peekable(); if iter.peek().is_some() { - println!("\nPacman backup configuration files found:"); + println!("\n{}", t!("Pacman backup configuration files found:")); for entry in iter { println!("{}", entry.path().display()); diff --git a/src/steps/os/dragonfly.rs b/src/steps/os/dragonfly.rs index 527a86f9..0a4d4120 100644 --- a/src/steps/os/dragonfly.rs +++ b/src/steps/os/dragonfly.rs @@ -28,7 +28,9 @@ pub fn audit_packages(ctx: &ExecutionContext) -> Result<()> { .status()? .success() { - println!("The package audit was successful, but vulnerable packages still remain on the system"); + println!(t!( + "The package audit was successful, but vulnerable packages still remain on the system" + )); } Ok(()) } diff --git a/src/steps/os/linux.rs b/src/steps/os/linux.rs index f08ec71a..36ccec8d 100644 --- a/src/steps/os/linux.rs +++ b/src/steps/os/linux.rs @@ -451,7 +451,7 @@ fn upgrade_gentoo(ctx: &ExecutionContext) -> Result<()> { .status_checked()?; } - println!("Syncing portage"); + println!(t!("Syncing portage")); run_type .execute(sudo) .args(["emerge", "--sync"]) diff --git a/src/steps/os/macos.rs b/src/steps/os/macos.rs index 72c4d3c0..b89091f8 100644 --- a/src/steps/os/macos.rs +++ b/src/steps/os/macos.rs @@ -4,6 +4,7 @@ use crate::terminal::{print_separator, prompt_yesno}; use crate::utils::{require_option, REQUIRE_SUDO}; use crate::{utils::require, Step}; use color_eyre::eyre::Result; +use rust_i18n::t; use std::collections::HashSet; use std::fs; use std::process::Command; @@ -44,15 +45,19 @@ pub fn upgrade_macos(ctx: &ExecutionContext) -> Result<()> { let should_ask = !(ctx.config().yes(Step::System) || ctx.config().dry_run()); if should_ask { - println!("Finding available software"); + println!("{}", t!("Finding available software")); if system_update_available()? { - let answer = prompt_yesno("A system update is available. Do you wish to install it?")?; + let answer = prompt_yesno( + t!("A system update is available. Do you wish to install it?") + .to_string() + .as_ref(), + )?; if !answer { return Ok(()); } println!(); } else { - println!("No new software available."); + println!("{}", t!("No new software available.")); return Ok(()); } } @@ -72,7 +77,9 @@ fn system_update_available() -> Result { debug!("{:?}", output); - Ok(!output.stderr.contains("No new software available")) + Ok(!output + .stderr + .contains(t!("No new software available").to_string().as_str())) } pub fn run_sparkle(ctx: &ExecutionContext) -> Result<()> { @@ -115,7 +122,7 @@ pub fn update_xcodes(ctx: &ExecutionContext) -> Result<()> { .collect(); if releases_installed.is_empty() { - println!("No Xcode releases installed."); + println!("{}", t!("No Xcode releases installed.")); return Ok(()); } @@ -194,7 +201,11 @@ pub fn update_xcodes(ctx: &ExecutionContext) -> Result<()> { releases_regular_new_installed, ] { if should_ask && releases_new_installed.len() == 2 { - let answer_uninstall = prompt_yesno("Would you like to move the former Xcode release to the trash?")?; + let answer_uninstall = prompt_yesno( + t!("Would you like to move the former Xcode release to the trash?") + .to_string() + .as_str(), + )?; if answer_uninstall { let _ = ctx .run_type() @@ -221,7 +232,8 @@ pub fn process_xcodes_releases(releases_filtered: Vec, should_ask: bool, && !releases_filtered.is_empty() { println!( - "New Xcode release detected: {}", + "{} {}", + t!("New Xcode release detected:"), releases_filtered.last().cloned().unwrap_or_default() ); if should_ask { diff --git a/src/steps/os/unix.rs b/src/steps/os/unix.rs index 7e08d6a0..d9a7843b 100644 --- a/src/steps/os/unix.rs +++ b/src/steps/os/unix.rs @@ -13,6 +13,7 @@ use color_eyre::eyre::Context; use color_eyre::eyre::Result; use home; use ini::Ini; +use rust_i18n::t; use tracing::debug; #[cfg(target_os = "linux")] @@ -694,7 +695,7 @@ pub fn run_bun_packages(ctx: &ExecutionContext) -> Result<()> { package_json.push("install/global/package.json"); if !package_json.exists() { - println!("No global packages installed"); + println!("{}", t!("No global packages installed")); return Ok(()); } @@ -719,6 +720,6 @@ pub fn run_maza(ctx: &ExecutionContext) -> Result<()> { } pub fn reboot() -> Result<()> { - print!("Rebooting..."); + print!("{}", t!("Rebooting...")); Command::new("sudo").arg("reboot").status_checked() } diff --git a/src/steps/powershell.rs b/src/steps/powershell.rs index b5e5b67d..3989e019 100644 --- a/src/steps/powershell.rs +++ b/src/steps/powershell.rs @@ -4,6 +4,7 @@ use std::path::PathBuf; use std::process::Command; use color_eyre::eyre::Result; +use rust_i18n::t; use crate::command::CommandExt; use crate::execution_context::ExecutionContext; @@ -62,9 +63,9 @@ impl Powershell { } pub fn update_modules(&self, ctx: &ExecutionContext) -> Result<()> { - let powershell = require_option(self.path.as_ref(), String::from("Powershell is not installed"))?; + let powershell = require_option(self.path.as_ref(), t!("Powershell is not installed").to_string())?; - print_separator("Powershell Modules Update"); + print_separator(t!("Powershell Modules Update")); let mut cmd = vec!["Update-Module"]; @@ -76,7 +77,7 @@ impl Powershell { cmd.push("-Force") } - println!("Updating modules..."); + println!("{}", t!("Updating modules...")); ctx.run_type() .execute(powershell) // This probably doesn't need `shell_words::join`. diff --git a/src/steps/remote/ssh.rs b/src/steps/remote/ssh.rs index 6a9f244c..4c107c76 100644 --- a/src/steps/remote/ssh.rs +++ b/src/steps/remote/ssh.rs @@ -1,4 +1,5 @@ use color_eyre::eyre::Result; +use rust_i18n::t; use crate::{ command::CommandExt, error::SkipStep, execution_context::ExecutionContext, terminal::print_separator, utils, @@ -27,7 +28,7 @@ pub fn ssh_step(ctx: &ExecutionContext, hostname: &str) -> Result<()> { { prepare_async_ssh_command(&mut args); crate::tmux::run_command(ctx, hostname, &shell_words::join(args))?; - Err(SkipStep(String::from("Remote Topgrade launched in Tmux")).into()) + Err(SkipStep(String::from(t!("Remote Topgrade launched in Tmux"))).into()) } #[cfg(not(unix))] @@ -35,7 +36,7 @@ pub fn ssh_step(ctx: &ExecutionContext, hostname: &str) -> Result<()> { } else if ctx.config().open_remotes_in_new_terminal() && !ctx.run_type().dry() && cfg!(windows) { prepare_async_ssh_command(&mut args); ctx.run_type().execute("wt").args(&args).spawn()?; - Err(SkipStep(String::from("Remote Topgrade launched in an external terminal")).into()) + Err(SkipStep(String::from(t!("Remote Topgrade launched in an external terminal"))).into()) } else { let mut args = vec!["-t", hostname]; @@ -47,7 +48,7 @@ pub fn ssh_step(ctx: &ExecutionContext, hostname: &str) -> Result<()> { args.extend(["env", &env, "$SHELL", "-lc", topgrade]); print_separator(format!("Remote ({hostname})")); - println!("Connecting to {hostname}..."); + println!("{} {hostname}...", t!("Connecting to")); ctx.run_type().execute(ssh).args(&args).status_checked() } diff --git a/src/steps/remote/vagrant.rs b/src/steps/remote/vagrant.rs index dc022797..71705628 100644 --- a/src/steps/remote/vagrant.rs +++ b/src/steps/remote/vagrant.rs @@ -4,6 +4,7 @@ use std::{fmt::Display, rc::Rc, str::FromStr}; use color_eyre::eyre::Result; use regex::Regex; +use rust_i18n::t; use strum::EnumString; use tracing::{debug, error}; @@ -62,7 +63,7 @@ impl Vagrant { .arg("status") .current_dir(directory) .output_checked_utf8()?; - debug!("Vagrant output in {}: {}", directory, output); + debug!("{} {}: {}", t!("Vagrant output in"), directory, output); let boxes = output .stdout @@ -70,7 +71,7 @@ impl Vagrant { .skip(2) .take_while(|line| !(line.is_empty() || line.starts_with('\r'))) .map(|line| { - debug!("Vagrant line: {:?}", line); + debug!("{}: {:?}", t!("Vagrant line"), line); let mut elements = line.split_whitespace(); let name = elements.next().unwrap().to_string(); @@ -151,14 +152,14 @@ impl<'a> Drop for TemporaryPowerOn<'a> { pub fn collect_boxes(ctx: &ExecutionContext) -> Result> { let directories = utils::require_option( ctx.config().vagrant_directories(), - String::from("No Vagrant directories were specified in the configuration file"), + String::from(t!("No Vagrant directories were specified in the configuration file")), )?; let vagrant = Vagrant { path: utils::require("vagrant")?, }; print_separator("Vagrant"); - println!("Collecting Vagrant boxes"); + println!("{}", t!("Collecting Vagrant boxes")); let mut result = Vec::new(); @@ -167,7 +168,7 @@ pub fn collect_boxes(ctx: &ExecutionContext) -> Result> { Ok(mut boxes) => { result.append(&mut boxes); } - Err(e) => error!("Error collecting vagrant boxes from {}: {}", directory, e), + Err(e) => error!("{} {}: {}", t!("Error collecting vagrant boxes from"), directory, e), }; } @@ -183,7 +184,7 @@ pub fn topgrade_vagrant_box(ctx: &ExecutionContext, vagrant_box: &VagrantBox) -> let mut _poweron = None; if !vagrant_box.initial_status.powered_on() { if !(ctx.config().vagrant_power_on().unwrap_or(true)) { - return Err(SkipStep(format!("Skipping powered off box {vagrant_box}")).into()); + return Err(SkipStep(format!("{} {vagrant_box}", t!("Skipping powered off box"))).into()); } else { print_separator(seperator); _poweron = Some(vagrant.temporary_power_on(vagrant_box, ctx)?); @@ -205,12 +206,13 @@ pub fn topgrade_vagrant_box(ctx: &ExecutionContext, vagrant_box: &VagrantBox) -> pub fn upgrade_vagrant_boxes(ctx: &ExecutionContext) -> Result<()> { let vagrant = utils::require("vagrant")?; - print_separator("Vagrant boxes"); + print_separator(t!("Vagrant boxes")); let outdated = Command::new(&vagrant) .args(["box", "outdated", "--global"]) .output_checked_utf8()?; + // TODO: How to handle this with i18n in mind? let re = Regex::new(r"\* '(.*?)' for '(.*?)' is outdated").unwrap(); let mut found = false; @@ -227,7 +229,7 @@ pub fn upgrade_vagrant_boxes(ctx: &ExecutionContext) -> Result<()> { } if !found { - println!("No outdated boxes") + println!("{}", t!("No outdated boxes")) } else { ctx.run_type() .execute(&vagrant) diff --git a/src/steps/tmux.rs b/src/steps/tmux.rs index 444a49b5..a56b5252 100644 --- a/src/steps/tmux.rs +++ b/src/steps/tmux.rs @@ -14,6 +14,7 @@ use crate::{ utils::{which, PathExt}, }; +use rust_i18n::t; #[cfg(unix)] use std::os::unix::process::CommandExt as _; @@ -149,7 +150,7 @@ pub fn run_in_tmux(args: Vec) -> Result<()> { let err = tmux.build().args(["attach-session", "-t", &session]).exec(); Err(eyre!("{err}")).context("Failed to `execvp(3)` tmux") } else { - println!("Topgrade launched in a new tmux session"); + println!("{}", t!("Topgrade launched in a new tmux session")); Ok(()) } } diff --git a/src/steps/vim.rs b/src/steps/vim.rs index dc507fd7..1fdd75ff 100644 --- a/src/steps/vim.rs +++ b/src/steps/vim.rs @@ -10,6 +10,7 @@ use crate::{ execution_context::ExecutionContext, utils::{require, PathExt}, }; +use rust_i18n::t; use std::path::PathBuf; use std::{ io::{self, Write}, @@ -64,7 +65,7 @@ fn upgrade(command: &mut Executor, ctx: &ExecutionContext) -> Result<()> { if !status.success() { return Err(TopgradeError::ProcessFailed(command.get_program(), status).into()); } else { - println!("Plugins upgraded") + println!("{}", t!("Plugins upgraded")) } } diff --git a/src/sudo.rs b/src/sudo.rs index b51a283a..1f187dce 100644 --- a/src/sudo.rs +++ b/src/sudo.rs @@ -4,6 +4,7 @@ use std::path::PathBuf; use color_eyre::eyre::Context; use color_eyre::eyre::Result; +use rust_i18n::t; use serde::Deserialize; use strum::AsRefStr; @@ -86,7 +87,7 @@ impl Sudo { cmd.arg("-w"); } } - cmd.status_checked().wrap_err("Failed to elevate permissions") + cmd.status_checked().wrap_err(t!("Failed to elevate permissions")) } /// Execute a command with `sudo`. diff --git a/src/terminal.rs b/src/terminal.rs index 04df3e06..e82fb80d 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -11,6 +11,7 @@ use color_eyre::eyre::Context; use console::{style, Key, Term}; use lazy_static::lazy_static; use notify_rust::{Notification, Timeout}; +use rust_i18n::t; use tracing::{debug, error}; #[cfg(windows)] use which_crate::which; @@ -73,7 +74,7 @@ impl Terminal { } fn notify_desktop>(&self, message: P, timeout: Option) { - debug!("Desktop notification: {}", message.as_ref()); + debug!("{}: {}", t!("Desktop notification"), message.as_ref()); let mut notification = Notification::new(); notification .summary("Topgrade") @@ -144,7 +145,7 @@ impl Terminal { self.term .write_fmt(format_args!( "{} {}", - style(format!("{key} failed:")).red().bold(), + style(format!("{key} {}:", t!("failed"))).red().bold(), message )) .ok(); @@ -188,6 +189,7 @@ impl Terminal { self.term .write_fmt(format_args!( "{}", + // TODO: Implement i18n for this using the first character from the translated key style(format!("{question} (y)es/(N)o",)).yellow().bold() )) .ok(); @@ -207,13 +209,14 @@ impl Terminal { } if self.set_title { - self.term.set_title("Topgrade - Awaiting user"); + self.term.set_title(format!("Topgrade - {}", t!("Awaiting user"))); } if self.desktop_notification { - self.notify_desktop(format!("{step_name} failed"), None); + self.notify_desktop(format!("{step_name} {}", t!("failed")), None); } + // TODO: Implement i18n for this using the first character from the translated key let prompt_inner = style(format!("{}Retry? (y)es/(N)o/(s)hell/(q)uit", self.prefix)) .yellow() .bold(); @@ -224,7 +227,10 @@ impl Terminal { match self.term.read_key() { Ok(Key::Char('y')) | Ok(Key::Char('Y')) => break Ok(true), Ok(Key::Char('s')) | Ok(Key::Char('S')) => { - println!("\n\nDropping you to shell. Fix what you need and then exit the shell.\n"); + println!( + "\n\n{}\n", + t!("Dropping you to shell. Fix what you need and then exit the shell.") + ); if let Err(err) = run_shell().context("Failed to run shell") { self.term.write_fmt(format_args!("{err:?}\n{prompt_inner}")).ok(); } else { @@ -233,11 +239,11 @@ impl Terminal { } Ok(Key::Char('n')) | Ok(Key::Char('N')) | Ok(Key::Enter) => break Ok(false), Err(e) => { - error!("Error reading from terminal: {}", e); + error!("{}: {}", t!("Error reading from terminal"), e); break Ok(false); } Ok(Key::Char('q')) | Ok(Key::Char('Q')) => { - return Err(io::Error::from(io::ErrorKind::Interrupted)).context("Quit from user input") + return Err(io::Error::from(io::ErrorKind::Interrupted)).context(t!("Quit from user input")) } _ => (), } diff --git a/src/utils.rs b/src/utils.rs index d16c4b0b..8c4ffec5 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -5,6 +5,7 @@ use std::path::{Path, PathBuf}; use std::process::Command; use color_eyre::eyre::Result; +use rust_i18n::t; use tracing::{debug, error}; use tracing_subscriber::layer::SubscriberExt; @@ -34,10 +35,10 @@ where { fn if_exists(self) -> Option { if self.as_ref().exists() { - debug!("Path {:?} exists", self.as_ref()); + debug!("{} {:?} {}", t!("Path"), self.as_ref(), t!("exists")); Some(self) } else { - debug!("Path {:?} doesn't exist", self.as_ref()); + debug!("{} {:?} {}", t!("Path"), self.as_ref(), t!("doesn't exist")); None } } @@ -48,10 +49,10 @@ where fn require(self) -> Result { if self.as_ref().exists() { - debug!("Path {:?} exists", self.as_ref()); + debug!("{} {:?} {}", t!("Path"), self.as_ref(), t!("exists")); Ok(self) } else { - Err(SkipStep(format!("Path {:?} doesn't exist", self.as_ref())).into()) + Err(SkipStep(format!("{} {:?} {}", t!("Path"), self.as_ref(), t!("doesn't exist"))).into()) } } } @@ -59,16 +60,16 @@ where pub fn which + Debug>(binary_name: T) -> Option { match which_crate::which(&binary_name) { Ok(path) => { - debug!("Detected {:?} as {:?}", &path, &binary_name); + debug!("{} {:?} {} {:?}", t!("Detected"), &path, t!("as"), &binary_name); Some(path) } Err(e) => { match e { which_crate::Error::CannotFindBinaryPath => { - debug!("Cannot find {:?}", &binary_name); + debug!("{} {:?}", t!("Cannot find"), &binary_name); } _ => { - error!("Detecting {:?} failed: {}", &binary_name, e); + error!("{} {:?} {}: {}", t!("Detecting"), &binary_name, t!("failed"), e); } }