diff --git a/.github/workflows/check_locale_file.yml b/.github/workflows/check_locale_file.yml new file mode 100644 index 00000000..a0652299 --- /dev/null +++ b/.github/workflows/check_locale_file.yml @@ -0,0 +1,22 @@ +on: + pull_request: + push: + branches: + - main + +name: Check i18n locale file + +jobs: + check_locale: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install checker + # Build it with the dev profile as this is faster and the checker still works + run: | + cargo install --git https://github.com/topgrade-rs/topgrade_i18n_locale_file_checker --profile dev + + - name: Run the checker + run: topgrade_i18n_locale_file_checker ./locales/app.yml diff --git a/Cargo.lock b/Cargo.lock index 83a8a671..8dc535d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -90,6 +90,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + [[package]] name = "async-broadcast" version = "0.7.1" @@ -319,6 +325,16 @@ dependencies = [ "piper", ] +[[package]] +name = "bstr" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -539,6 +555,25 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.20" @@ -994,6 +1029,36 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "globset" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata 0.4.7", + "regex-syntax 0.8.4", +] + +[[package]] +name = "globwalk" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" +dependencies = [ + "bitflags 1.3.2", + "ignore", + "walkdir", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.5" @@ -1276,12 +1341,38 @@ dependencies = [ "utf8_iter", ] +[[package]] +name = "ignore" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b46810df39e66e925525d6e38ce1e7f6e1d208f72dc39757880fcb66e2c58af1" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata 0.4.7", + "same-file", + "walkdir", + "winapi-util", +] + [[package]] name = "indenter" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + [[package]] name = "indexmap" version = "2.2.6" @@ -1289,7 +1380,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.14.5", ] [[package]] @@ -1363,6 +1454,12 @@ dependencies = [ "libc", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -1500,6 +1597,15 @@ dependencies = [ "libc", ] +[[package]] +name = "normpath" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5831952a9476f2fed74b77d74182fa5ddc4d21c72ec45a333b250e3ed0272804" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "notify-rust" version = "4.11.0" @@ -1611,7 +1717,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" dependencies = [ "dlv-list", - "hashbrown", + "hashbrown 0.14.5", ] [[package]] @@ -1982,6 +2088,57 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316" +[[package]] +name = "rust-i18n" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dcd94370631e5658a0a23635f7f47e43d06a00ad948e0bb5de79b00d85b880c" +dependencies = [ + "globwalk", + "once_cell", + "regex", + "rust-i18n-macro", + "rust-i18n-support", + "smallvec", +] + +[[package]] +name = "rust-i18n-macro" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "355763801dcf287e777e42def7c578410783477b804b1107852119e0b2518396" +dependencies = [ + "glob", + "once_cell", + "proc-macro2", + "quote", + "rust-i18n-support", + "serde", + "serde_json", + "serde_yaml", + "syn 2.0.66", +] + +[[package]] +name = "rust-i18n-support" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "399801f4d955abf1c3ce3ce2215dc76bd40beb4ae39e3a84936b21a79ce2caa5" +dependencies = [ + "arc-swap", + "globwalk", + "lazy_static", + "normpath", + "once_cell", + "proc-macro2", + "regex", + "serde", + "serde_json", + "serde_yaml", + "toml 0.7.8", + "triomphe", +] + [[package]] name = "rust-ini" version = "0.21.0" @@ -2187,6 +2344,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_yaml" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" +dependencies = [ + "indexmap 1.9.3", + "ryu", + "serde", + "yaml-rust", +] + [[package]] name = "sha1" version = "0.10.6" @@ -2378,6 +2547,15 @@ dependencies = [ "syn 2.0.66", ] +[[package]] +name = "sys-locale" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e801cf239ecd6ccd71f03d270d67dd53d13e90aab208bf4b8fe4ad957ea949b0" +dependencies = [ + "libc", +] + [[package]] name = "tar" version = "0.4.41" @@ -2520,6 +2698,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.19.15", +] + [[package]] name = "toml" version = "0.8.14" @@ -2541,13 +2731,26 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.2.6", + "serde", + "serde_spanned", + "toml_datetime", + "winnow 0.5.40", +] + [[package]] name = "toml_edit" version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" dependencies = [ - "indexmap", + "indexmap 2.2.6", "toml_datetime", "winnow 0.5.40", ] @@ -2558,7 +2761,7 @@ version = "0.22.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" dependencies = [ - "indexmap", + "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", @@ -2588,6 +2791,7 @@ dependencies = [ "parselnk", "regex", "regex-split", + "rust-i18n", "rust-ini", "self_update", "semver", @@ -2595,10 +2799,11 @@ dependencies = [ "shell-words", "shellexpand", "strum", + "sys-locale", "tempfile", "thiserror", "tokio", - "toml", + "toml 0.8.14", "tracing", "tracing-subscriber", "walkdir", @@ -2713,6 +2918,17 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc" +[[package]] +name = "triomphe" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6631e42e10b40c0690bf92f404ebcfe6e1fdb480391d15f17cc8e96eeed5369" +dependencies = [ + "arc-swap", + "serde", + "stable_deref_trait", +] + [[package]] name = "try-lock" version = "0.2.5" @@ -3243,6 +3459,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "yoke" version = "0.7.4" diff --git a/Cargo.toml b/Cargo.toml index 676da655..1730a081 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,6 +52,8 @@ merge = "~0.1" regex-split = "~0.1" notify-rust = "~4.11" wildmatch = "2.3.0" +rust-i18n = "3.0.1" +sys-locale = "0.3.1" [package.metadata.generate-rpm] assets = [{ source = "target/release/topgrade", dest = "/usr/bin/topgrade" }] diff --git a/locales/app.yml b/locales/app.yml new file mode 100644 index 00000000..e04efbd0 --- /dev/null +++ b/locales/app.yml @@ -0,0 +1,303 @@ +_version: 2 + +"Current system locale is {system_locale}": + en: "Current system locale is %{system_locale}" +"Dry running: {program_name} {arguments}": + en: "Dry running: %{program_name} %{arguments}" +"in {directory}": + en: "in %{directory}" +"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" +"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" +#TODO: See if the \n breaks anything +"Topgrade upgraded to {version}:\n": + en: "Topgrade upgraded to %{version}:\n" +"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: {error}": + en: "Error detecting current distribution: %{error}" +"Error: {error}": + en: "Error: %{error}" +"Failed": + en: "Failed" +"pulling": + en: "pulling" +"Changed": + en: "Changed" +"Up-to-date": + en: "Up-to-date" +"Self update": + en: "Self update" + +# The following 2 strings are used in the same sentence +"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(<0.4.6) requires sudo installed to work with AUR packages": + en: "Aura(<0.4.6) 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" +"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 boxes": + en: "Vagrant boxes" +"No outdated boxes": + en: "No outdated boxes" +"Summary": + en: "Summary" +"Topgrade finished with errors": + en: "Topgrade finished with errors" +"Topgrade finished successfully": + en: "Topgrade finished successfully" +"Topgrade {version_str} Breaking Changes": + en: "Topgrade %{version_str} Breaking Changes" +"Path {path} expanded to {expanded}": + en: "Path %{path} expanded to %{expanded}" +"Path {path} doesn't exist": + en: "Path %{path} doesn't exist" +"Cannot find {binary_name} in PATH": + en: "Cannot find %{binary_name} in PATH" +"Failed to get a UTF-8 encoded hostname": + en: "Failed to get a UTF-8 encoded hostname" +"Failed to get hostname: {err}": + en: "Failed to get hostname: %{err}" +"{python} is a Python 2, skip.": + en: "%{python} is a Python 2, skip." +"{python} is a Python shim, skip.": + en: "%{python} is a Python shim, skip." +"{key} failed:": + en: "%{key} failed:" +"{step_name} failed": + en: "%{step_name} failed" +"DragonFly BSD Packages": + en: "DragonFly BSD Packages" +"DragonFly BSD Audit": + en: "DragonFly BSD Audit" +"FreeBSD Update": + en: "FreeBSD Update" +"FreeBSD Packages": + en: "FreeBSD Packages" +"FreeBSD Audit": + en: "FreeBSD Audit" +"System update": + en: "System update" +"needrestart will be ran by the package manager": + en: "needrestart will be ran by the package manager" +"Check for needed restarts": + en: "Check for needed restarts" +"Should not run in WSL": + en: "Should not run in WSL" +"Firmware upgrades": + en: "Firmware upgrades" +"Flatpak System Packages": + en: "Flatpak System Packages" +"Snapd socket does not exist": + en: "Snapd socket does not exist" +"You need to specify at least one container": + en: "You need to specify at least one container" +"Skipped in --yes": + en: "Skipped in --yes" +"Configuration update": + en: "Configuration update" +"Going to execute `waydroid upgrade`, which would STOP the running container, is this ok?": + en: "Going to execute `waydroid upgrade`, which would STOP the running container, is this ok?" +"Skip the Waydroid step because the user don't want to proceed": + en: "Skip the Waydroid step because the user don't want to proceed" +"macOS App Store": + en: "macOS App Store" +"macOS system update": + en: "macOS system update" +"OpenBSD Update": + en: "OpenBSD Update" +"OpenBSD Packages": + en: "OpenBSD Packages" +"`fisher` is not defined in `fish`": + en: "`fisher` is not defined in `fish`" +"`fish_plugins` path doesn't exist: {err}": + en: "`fish_plugins` path doesn't exist: %{err}" +"`fish_update_completions` is not available": + en: "`fish_update_completions` is not available" +"Desktop doest not appear to be gnome": + en: "Desktop doest not appear to be gnome" +"Gnome shell extensions are unregistered in DBus": + en: "Gnome shell extensions are unregistered in DBus" +"Gnome Shell extensions": + en: "Gnome Shell extensions" +"Not a custom brew for macOS": + en: "Not a custom brew for macOS" +"Guix Pull Failed, Skipping": + en: "Guix Pull Failed, Skipping" +"Nix-darwin on macOS must be upgraded via darwin-rebuild switch": + en: "Nix-darwin on macOS must be upgraded via darwin-rebuild switch" +"`nix upgrade-nix` can only be used on macOS or non-NixOS Linux": + en: "`nix upgrade-nix` can only be used on macOS or non-NixOS Linux" +"`nix upgrade-nix` cannot be run when Nix is installed in a profile": + en: "`nix upgrade-nix` cannot be run when Nix is installed in a profile" +"Nix (self-upgrade)": + en: "Nix (self-upgrade)" +"Pyenv is installed, but $PYENV_ROOT is not set correctly": + en: "Pyenv is installed, but $PYENV_ROOT is not set correctly" +"pyenv is not a git repository": + en: "pyenv is not a git repository" +"Bun Packages": + en: "Bun Packages" +"WSL not installed": + en: "WSL not installed" +"Update WSL": + en: "Update WSL" +"Could not find Topgrade installed in WSL": + en: "Could not find Topgrade installed in WSL" +"Consider installing PSWindowsUpdate as the use of Windows Update via USOClient is not supported.": + en: "Consider installing PSWindowsUpdate as the use of Windows Update via USOClient is not supported." +"USOClient not supported.": + en: "USOClient not supported." +"Connecting to {hostname}...": + en: "Connecting to %{hostname}..." +"Skipping powered off box {vagrant_box}": + en: "Skipping powered off box %{vagrant_box}" +"`{repo_tag}` for `{platform}`": + en: "`%{repo_tag}` for `%{platform}`" +"Containers": + en: "Containers" +"Emacs directory does not exist": + en: "Emacs directory does not exist" +"Error getting the composer directory: {error}": + en: "Error getting the composer directory: %{error}" +"Composer directory {composer_home} isn't a descendant of the user's home directory": + en: "Composer directory %{composer_home} isn't a descendant of the user's home directory" +"Composer": + en: "Composer" +"Error running `dotnet tool list`. This is expected when a dotnet runtime is installed but no SDK.": + en: "Error running `dotnet tool list`. This is expected when a dotnet runtime is installed but no SDK." +"No dotnet global tools installed": + en: "No dotnet global tools installed" +"Racket Package Manager": + en: "Racket Package Manager" +"GH failed": + en: "GH failed" +"GitHub CLI Extensions": + en: "GitHub CLI Extensions" +"Julia Packages": + en: "Julia Packages" +"Update ClamAV Database(FreshClam)": + en: "Update ClamAV Database(FreshClam)" +"Path {pattern} did not contain any git repositories": + en: "Path %{pattern} did not contain any git repositories" +"No repositories to pull": + en: "No repositories to pull" +"Git repositories": + en: "Git repositories" +"Would pull {repo}": + en: "Would pull %{repo}" +"Node Package Manager": + en: "Node Package Manager" +"Performant Node Package Manager": + en: "Performant Node Package Manager" +"Yarn Package Manager": + en: "Yarn Package Manager" +"Deno installed outside of .deno directory": + en: "Deno installed outside of .deno directory" +"The Ultimate vimrc": + en: "The Ultimate vimrc" +"vim binary might be actually nvim": + en: "vim binary might be actually nvim" +"`{process}` failed: {exit_satus}": + en: "`%{process}` failed: %{exit_satus}" +"`{process}` failed: {exit_satus} with {output}": + en: "`%{process}` failed: %{exit_satus} with %{output}" +"Unknown Linux Distribution": + en: "Unknown Linux Distribution" +'File "/etc/os-release" does not exist or is empty': + en: 'File "/etc/os-release" does not exist or is empty' +"Failed getting the system package manager": + en: "Failed getting the system package manager" +"A step failed": + en: "A step failed" +"Dry running": + en: "Dry running" +"Topgrade Upgraded": + en: "Topgrade Upgraded" +"OK": + en: "OK" +"FAILED": + en: "FAILED" +"IGNORED": + en: "IGNORED" +"SKIPPED": + en: "SKIPPED" + +# 'Y' and 'N' have to stay the same characters. Eg for German the translation +# would look sth like "(Y) Ja / (N) Nein" +"(Y)es/(N)o": + en: "(Y)es/(N)o" +# 'y', 'N', 's', 'q' have to stay the same throughout all translations. +# Eg German would look like "(y) Wiederholen / (N) Nein / (s) Konsole / (q) Beenden" +"Retry? (y)es/(N)o/(s)hell/(q)uit": + en: "Retry? (y)es/(N)o/(s)hell/(q)uit" +# 'R', 'S', 'Q' have to stay the same throughout all translations. Eg German would look like "\n(R) Neustarten\n(S) Konsole\n(Q) Beenden" +"\n(R)eboot\n(S)hell\n(Q)uit": + en: "\n(R)eboot\n(S)hell\n(Q)uit" +"Require sudo or counterpart but not found, skip": + en: "Require sudo or counterpart but not found, skip" +"sudo as user '{user}'": + en: "sudo as user '%{user}'" +"Updating aqua ...": + en: "Updating aqua ..." +"Updating aqua installed cli tools ...": + en: "Updating aqua installed cli tools ..." +"Updating Volta packages...": + en: "Updating Volta packages..." +"No packages installed with Volta": + en: "No packages installed with Volta" +"pyenv-update plugin is not installed": + en: "pyenv-update plugin is not installed" + \ No newline at end of file diff --git a/src/breaking_changes.rs b/src/breaking_changes.rs index 520db396..f758f963 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}, @@ -45,7 +46,7 @@ impl FromStr for Version { // They cannot be all 0s assert!( !(major == 0 && minor == 0 && patch == 0), - "Version numbers can not be all 0s" + "Version numbers cannot be all 0s" ); Ok(Self { @@ -118,12 +119,15 @@ 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!( + "{}", + t!("Topgrade {version_str} Breaking Changes", version_str = VERSION_STR) + ); 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 +163,7 @@ mod test { } #[test] - #[should_panic(expected = "Version numbers can not be all 0s")] + #[should_panic(expected = "Version numbers cannot be all 0s")] fn invalid_version() { let all_0 = "0.0.0"; all_0.parse::().unwrap(); diff --git a/src/config.rs b/src/config.rs index 93289558..69842617 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; @@ -25,6 +26,7 @@ use crate::sudo::SudoKind; use crate::utils::string_prepend_str; use tracing::{debug, error}; +// TODO: Add i18n to this. Tracking issue: https://github.com/topgrade-rs/topgrade/issues/859 pub static EXAMPLE_CONFIG: &str = include_str!("../config.example.toml"); /// Topgrade's default log level. @@ -650,14 +652,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!("Unable to read {}: {e}", include_path.display(),); 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!("Failed to deserialize {}: {e}", include_path.display(),); continue; } }; @@ -667,14 +669,17 @@ 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!("Failed to deserialize {}: {e}", config_path.display(),), } } 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 {path} expanded to {expanded}", path = path, expanded = expanded) + ); *path = expanded; } } @@ -712,6 +717,8 @@ impl ConfigFile { } // Command line arguments +// TODO: i18n of clap currently not easily possible. Waiting for https://github.com/clap-rs/clap/issues/380 +// Tracking issue for i18n: https://github.com/topgrade-rs/topgrade/issues/859 #[derive(Parser, Debug)] #[command(name = "topgrade", version)] pub struct CommandLineArgs { @@ -869,7 +876,7 @@ 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!("failed to load configuration: {e}"); ConfigFile::default() }) } else { diff --git a/src/error.rs b/src/error.rs index 96819f60..584c6da7 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,41 +1,98 @@ -use std::process::ExitStatus; +use std::{fmt::Display, process::ExitStatus}; +use rust_i18n::t; use thiserror::Error; #[derive(Error, Debug, PartialEq, Eq)] pub enum TopgradeError { - #[error("`{0}` failed: {1}")] ProcessFailed(String, ExitStatus), - #[error("`{0}` failed: {1}")] ProcessFailedWithOutput(String, ExitStatus, String), - #[error("Unknown Linux Distribution")] #[cfg(target_os = "linux")] UnknownLinuxDistribution, - #[error("File \"/etc/os-release\" does not exist or is empty")] #[cfg(target_os = "linux")] EmptyOSReleaseFile, - #[error("Failed getting the system package manager")] #[cfg(target_os = "linux")] FailedGettingPackageManager, } +impl Display for TopgradeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TopgradeError::ProcessFailed(process, exit_status) => { + write!( + f, + "{}", + t!( + "`{process}` failed: {exit_satus}", + process = process, + exit_status = exit_status + ) + ) + } + TopgradeError::ProcessFailedWithOutput(process, exit_status, output) => { + write!( + f, + "{}", + t!( + "`{process}` failed: {exit_satus} with {output}", + process = process, + exit_status = exit_status, + output = output + ) + ) + } + #[cfg(target_os = "linux")] + TopgradeError::UnknownLinuxDistribution => write!(f, "{}", t!("Unknown Linux Distribution")), + #[cfg(target_os = "linux")] + TopgradeError::EmptyOSReleaseFile => { + write!(f, "{}", t!("File \"/etc/os-release\" does not exist or is empty")) + } + #[cfg(target_os = "linux")] + TopgradeError::FailedGettingPackageManager => { + write!(f, "{}", t!("Failed getting the system package manager")) + } + } + } +} + #[derive(Error, Debug)] -#[error("A step failed")] pub struct StepFailed; -#[derive(Error, Debug)] -#[error("Dry running")] -pub struct DryRun(); +impl Display for StepFailed { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", t!("A step failed")) + } +} + +#[derive(Error, Debug)] +pub struct DryRun(); + +impl Display for DryRun { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", t!("Dry running")) + } +} #[derive(Error, Debug)] -#[error("{0}")] pub struct SkipStep(pub String); +impl Display for SkipStep { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + #[cfg(all(windows, feature = "self-update"))] #[derive(Error, Debug)] -#[error("Topgrade Upgraded")] pub struct Upgraded(pub ExitStatus); + +#[cfg(all(windows, feature = "self-update"))] +impl Display for Upgraded { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", t!("Topgrade Upgraded")) + } +} diff --git a/src/execution_context.rs b/src/execution_context.rs index 7408257a..a4cc8684 100644 --- a/src/execution_context.rs +++ b/src/execution_context.rs @@ -1,7 +1,7 @@ #![allow(dead_code)] use crate::executor::RunType; use crate::sudo::Sudo; -use crate::utils::{require_option, REQUIRE_SUDO}; +use crate::utils::{get_require_sudo_string, require_option}; use crate::{config::Config, executor::Executor}; use color_eyre::eyre::Result; use std::env::var; @@ -33,7 +33,7 @@ impl<'a> ExecutionContext<'a> { } pub fn execute_elevated(&self, command: &Path, interactive: bool) -> Result { - let sudo = require_option(self.sudo.as_ref(), REQUIRE_SUDO.to_string())?; + let sudo = require_option(self.sudo.as_ref(), get_require_sudo_string())?; Ok(sudo.execute_elevated(self, command, interactive)) } diff --git a/src/executor.rs b/src/executor.rs index 4bd813fe..7df3e719 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; @@ -209,17 +210,20 @@ pub struct DryCommand { impl DryCommand { fn dry_run(&self) { print!( - "Dry running: {} {}", - self.program.to_string_lossy(), - shell_words::join( - self.args - .iter() - .map(|a| String::from(a.to_string_lossy())) - .collect::>() + "{}", + t!( + "Dry running: {program_name} {arguments}", + program_name = self.program.to_string_lossy(), + arguments = shell_words::join( + self.args + .iter() + .map(|a| String::from(a.to_string_lossy())) + .collect::>() + ) ) ); match &self.directory { - Some(dir) => println!(" in {}", dir.to_string_lossy()), + Some(dir) => println!(" {}", t!("in {directory}", directory = dir.to_string_lossy())), None => println!(), }; } diff --git a/src/main.rs b/src/main.rs index 4805be19..1166615a 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}; @@ -54,6 +55,9 @@ pub(crate) static XDG_DIRS: Lazy = Lazy::new(|| Xdg::new().expect("No home #[cfg(windows)] pub(crate) static WINDOWS_DIRS: Lazy = Lazy::new(|| Windows::new().expect("No home directory")); +// Init and load the i18n files +i18n!("locales", fallback = "en"); + fn run() -> Result<()> { install_color_eyre()?; ctrlc::set_handler(); @@ -72,6 +76,11 @@ fn run() -> Result<()> { // and `Config::tracing_filter_directives()`. let reload_handle = install_tracing(&opt.tracing_filter_directives())?; + // Get current system locale and set it as the default locale + let system_locale = sys_locale::get_locale().unwrap_or("en".to_string()); + rust_i18n::set_locale(&system_locale); + debug!("Current system locale is {system_locale}"); + if let Some(shell) = opt.gen_completion { let cmd = &mut CommandLineArgs::command(); clap_complete::generate(shell, cmd, clap::crate_name!(), &mut io::stdout()); @@ -210,7 +219,7 @@ fn run() -> Result<()> { runner.execute(Step::System, "System update", || distribution.upgrade(&ctx))?; } Err(e) => { - println!("Error detecting current distribution: {e}"); + println!("{}", t!("Error detecting current distribution: {error}", error = e)); } } runner.execute(Step::ConfigUpdate, "config-update", || linux::run_config_update(&ctx))?; @@ -452,7 +461,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); @@ -476,7 +485,7 @@ fn run() -> Result<()> { } if config.keep_at_end() { - print_info("\n(R)eboot\n(S)hell\n(Q)uit"); + print_info(t!("\n(R)eboot\n(S)hell\n(Q)uit")); loop { match get_key() { Ok(Key::Char('s')) | Ok(Key::Char('S')) => { @@ -498,10 +507,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)), ) } @@ -536,7 +546,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!("{}", t!("Error: {error}", error = format!("{:?}", error))); } exit(1); } diff --git a/src/self_update.rs b/src/self_update.rs index d1d664d8..638fca8a 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!("{}", t!("Topgrade upgraded to {version}:\n", version = release.version)); if let Some(body) = &release.body { println!("{body}"); } } 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/containers.rs b/src/steps/containers.rs index 89871c58..bc99b310 100644 --- a/src/steps/containers.rs +++ b/src/steps/containers.rs @@ -1,187 +1,196 @@ -use std::fmt::{Display, Formatter}; -use std::path::Path; -use std::process::Command; - -use color_eyre::eyre::eyre; -use color_eyre::eyre::Context; -use color_eyre::eyre::Result; -use tracing::{debug, error, warn}; -use wildmatch::WildMatch; - -use crate::command::CommandExt; -use crate::error::{self, TopgradeError}; -use crate::terminal::print_separator; -use crate::{execution_context::ExecutionContext, utils::require}; - -// A string found in the output of docker for containers that weren't found in -// the docker registry. We use this to gracefully handle and skip containers -// that cannot be pulled, likely because they don't exist in the registry in -// the first place. This happens e.g. when the user tags an image locally -// themselves or when using docker-compose. -const NONEXISTENT_REPO: &str = "repository does not exist"; - -/// Uniquely identifies a `Container`. -#[derive(Debug)] -struct Container { - /// `Repository` and `Tag` - /// - /// format: `Repository:Tag`, e.g., `nixos/nix:latest`. - repo_tag: String, - /// Platform - /// - /// format: `OS/Architecture`, e.g., `linux/amd64`. - platform: String, -} - -impl Container { - /// Construct a new `Container`. - fn new(repo_tag: String, platform: String) -> Self { - Self { repo_tag, platform } - } -} - -impl Display for Container { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - // e.g., "`fedora:latest` for `linux/amd64`" - write!(f, "`{}` for `{}`", self.repo_tag, self.platform) - } -} - -/// Returns a Vector of all containers, with Strings in the format -/// "REGISTRY/[PATH/]CONTAINER_NAME:TAG" -/// -/// Containers specified in `ignored_containers` will be filtered out. -fn list_containers(crt: &Path, ignored_containers: Option<&Vec>) -> Result> { - let ignored_containers = ignored_containers.map(|patterns| { - patterns - .iter() - .map(|pattern| WildMatch::new(pattern)) - .collect::>() - }); - - debug!( - "Querying '{} image ls --format \"{{{{.Repository}}}}:{{{{.Tag}}}}/{{{{.ID}}}}\"' for containers", - crt.display() - ); - let output = Command::new(crt) - .args(["image", "ls", "--format", "{{.Repository}}:{{.Tag}} {{.ID}}"]) - .output_checked_with_utf8(|_| Ok(()))?; - - let mut retval = vec![]; - for line in output.stdout.lines() { - if line.starts_with("localhost") { - // Don't know how to update self-built containers - debug!("Skipping self-built container '{}'", line); - continue; - } - - if line.contains("") { - // Bogus/dangling container or intermediate layer - debug!("Skipping bogus container '{}'", line); - continue; - } - - if line.starts_with("vsc-") { - debug!("Skipping visual studio code dev container '{}'", line); - continue; - } - - debug!("Using container '{}'", line); - - // line is of format: `Repository:Tag ImageID`, e.g., `nixos/nix:latest d80fea9c32b4` - let split_res = line.split(' ').collect::>(); - assert_eq!(split_res.len(), 2); - let (repo_tag, image_id) = (split_res[0], split_res[1]); - - if let Some(ref ignored_containers) = ignored_containers { - if ignored_containers.iter().any(|pattern| pattern.matches(repo_tag)) { - debug!("Skipping ignored container '{}'", line); - continue; - } - } - - debug!( - "Querying '{} image inspect --format \"{{{{.Os}}}}/{{{{.Architecture}}}}\"' for container {}", - crt.display(), - image_id - ); - let inspect_output = Command::new(crt) - .args(["image", "inspect", image_id, "--format", "{{.Os}}/{{.Architecture}}"]) - .output_checked_with_utf8(|_| Ok(()))?; - let mut platform = inspect_output.stdout; - // truncate the tailing new line character - platform.truncate(platform.len() - 1); - assert!(platform.contains('/')); - - retval.push(Container::new(repo_tag.to_string(), platform)); - } - - Ok(retval) -} - -pub fn run_containers(ctx: &ExecutionContext) -> Result<()> { - // Check what runtime is specified in the config - let container_runtime = ctx.config().containers_runtime().to_string(); - let crt = require(container_runtime)?; - debug!("Using container runtime '{}'", crt.display()); - - print_separator("Containers"); - let mut success = true; - let containers = - list_containers(&crt, ctx.config().containers_ignored_tags()).context("Failed to list Docker containers")?; - debug!("Containers to inspect: {:?}", containers); - - for container in containers.iter() { - debug!("Pulling container '{}'", container); - let args = vec![ - "pull", - container.repo_tag.as_str(), - "--platform", - container.platform.as_str(), - ]; - let mut exec = ctx.run_type().execute(&crt); - - if let Err(e) = exec.args(&args).status_checked() { - error!("Pulling container '{}' failed: {}", container, e); - - // Find out if this is 'skippable' - // This is necessary e.g. for docker, because unlike podman docker doesn't tell from - // which repository a container originates (such as `docker.io`). This has the - // practical consequence that all containers, whether self-built, created by - // docker-compose or pulled from the docker hub, look exactly the same to us. We can - // only find out what went wrong by manually parsing the output of the command... - if match exec.output_checked_utf8() { - Ok(s) => s.stdout.contains(NONEXISTENT_REPO) || s.stderr.contains(NONEXISTENT_REPO), - Err(e) => match e.downcast_ref::() { - Some(TopgradeError::ProcessFailedWithOutput(_, _, stderr)) => stderr.contains(NONEXISTENT_REPO), - _ => false, - }, - } { - warn!("Skipping unknown container '{}'", container); - continue; - } - - success = false; - } - } - - if ctx.config().cleanup() { - // Remove dangling images - debug!("Removing dangling images"); - if let Err(e) = ctx - .run_type() - .execute(&crt) - .args(["image", "prune", "-f"]) - .status_checked() - { - error!("Removing dangling images failed: {}", e); - success = false; - } - } - - if success { - Ok(()) - } else { - Err(eyre!(error::StepFailed)) - } -} +use std::fmt::{Display, Formatter}; +use std::path::Path; +use std::process::Command; + +use color_eyre::eyre::eyre; +use color_eyre::eyre::Context; +use color_eyre::eyre::Result; +use tracing::{debug, error, warn}; +use wildmatch::WildMatch; + +use crate::command::CommandExt; +use crate::error::{self, TopgradeError}; +use crate::terminal::print_separator; +use crate::{execution_context::ExecutionContext, utils::require}; +use rust_i18n::t; + +// A string found in the output of docker for containers that weren't found in +// the docker registry. We use this to gracefully handle and skip containers +// that cannot be pulled, likely because they don't exist in the registry in +// the first place. This happens e.g. when the user tags an image locally +// themselves or when using docker-compose. +const NONEXISTENT_REPO: &str = "repository does not exist"; + +/// Uniquely identifies a `Container`. +#[derive(Debug)] +struct Container { + /// `Repository` and `Tag` + /// + /// format: `Repository:Tag`, e.g., `nixos/nix:latest`. + repo_tag: String, + /// Platform + /// + /// format: `OS/Architecture`, e.g., `linux/amd64`. + platform: String, +} + +impl Container { + /// Construct a new `Container`. + fn new(repo_tag: String, platform: String) -> Self { + Self { repo_tag, platform } + } +} + +impl Display for Container { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + // e.g., "`fedora:latest` for `linux/amd64`" + write!( + f, + "{}", + t!( + "`{repo_tag}` for `{platform}`", + repo_tag = self.repo_tag, + platform = self.platform + ) + ) + } +} + +/// Returns a Vector of all containers, with Strings in the format +/// "REGISTRY/[PATH/]CONTAINER_NAME:TAG" +/// +/// Containers specified in `ignored_containers` will be filtered out. +fn list_containers(crt: &Path, ignored_containers: Option<&Vec>) -> Result> { + let ignored_containers = ignored_containers.map(|patterns| { + patterns + .iter() + .map(|pattern| WildMatch::new(pattern)) + .collect::>() + }); + + debug!( + "Querying '{} image ls --format \"{{{{.Repository}}}}:{{{{.Tag}}}}/{{{{.ID}}}}\"' for containers", + crt.display() + ); + let output = Command::new(crt) + .args(["image", "ls", "--format", "{{.Repository}}:{{.Tag}} {{.ID}}"]) + .output_checked_with_utf8(|_| Ok(()))?; + + let mut retval = vec![]; + for line in output.stdout.lines() { + if line.starts_with("localhost") { + // Don't know how to update self-built containers + debug!("Skipping self-built container '{}'", line); + continue; + } + + if line.contains("") { + // Bogus/dangling container or intermediate layer + debug!("Skipping bogus container '{}'", line); + continue; + } + + if line.starts_with("vsc-") { + debug!("Skipping visual studio code dev container '{}'", line); + continue; + } + + debug!("Using container '{}'", line); + + // line is of format: `Repository:Tag ImageID`, e.g., `nixos/nix:latest d80fea9c32b4` + let split_res = line.split(' ').collect::>(); + assert_eq!(split_res.len(), 2); + let (repo_tag, image_id) = (split_res[0], split_res[1]); + + if let Some(ref ignored_containers) = ignored_containers { + if ignored_containers.iter().any(|pattern| pattern.matches(repo_tag)) { + debug!("Skipping ignored container '{}'", line); + continue; + } + } + + debug!( + "Querying '{} image inspect --format \"{{{{.Os}}}}/{{{{.Architecture}}}}\"' for container {}", + crt.display(), + image_id + ); + let inspect_output = Command::new(crt) + .args(["image", "inspect", image_id, "--format", "{{.Os}}/{{.Architecture}}"]) + .output_checked_with_utf8(|_| Ok(()))?; + let mut platform = inspect_output.stdout; + // truncate the tailing new line character + platform.truncate(platform.len() - 1); + assert!(platform.contains('/')); + + retval.push(Container::new(repo_tag.to_string(), platform)); + } + + Ok(retval) +} + +pub fn run_containers(ctx: &ExecutionContext) -> Result<()> { + // Check what runtime is specified in the config + let container_runtime = ctx.config().containers_runtime().to_string(); + let crt = require(container_runtime)?; + debug!("Using container runtime '{}'", crt.display()); + + print_separator(t!("Containers")); + let mut success = true; + let containers = + list_containers(&crt, ctx.config().containers_ignored_tags()).context("Failed to list Docker containers")?; + debug!("Containers to inspect: {:?}", containers); + + for container in containers.iter() { + debug!("Pulling container '{}'", container); + let args = vec![ + "pull", + container.repo_tag.as_str(), + "--platform", + container.platform.as_str(), + ]; + let mut exec = ctx.run_type().execute(&crt); + + if let Err(e) = exec.args(&args).status_checked() { + error!("Pulling container '{}' failed: {}", container, e); + + // Find out if this is 'skippable' + // This is necessary e.g. for docker, because unlike podman docker doesn't tell from + // which repository a container originates (such as `docker.io`). This has the + // practical consequence that all containers, whether self-built, created by + // docker-compose or pulled from the docker hub, look exactly the same to us. We can + // only find out what went wrong by manually parsing the output of the command... + if match exec.output_checked_utf8() { + Ok(s) => s.stdout.contains(NONEXISTENT_REPO) || s.stderr.contains(NONEXISTENT_REPO), + Err(e) => match e.downcast_ref::() { + Some(TopgradeError::ProcessFailedWithOutput(_, _, stderr)) => stderr.contains(NONEXISTENT_REPO), + _ => false, + }, + } { + warn!("Skipping unknown container '{}'", container); + continue; + } + + success = false; + } + } + + if ctx.config().cleanup() { + // Remove dangling images + debug!("Removing dangling images"); + if let Err(e) = ctx + .run_type() + .execute(&crt) + .args(["image", "prune", "-f"]) + .status_checked() + { + error!("Removing dangling images failed: {}", e); + success = false; + } + } + + if success { + Ok(()) + } else { + Err(eyre!(error::StepFailed)) + } +} diff --git a/src/steps/emacs.rs b/src/steps/emacs.rs index a6afa8c9..2567dc0e 100644 --- a/src/steps/emacs.rs +++ b/src/steps/emacs.rs @@ -4,6 +4,7 @@ use std::path::{Path, PathBuf}; use color_eyre::eyre::Result; use etcetera::base_strategy::BaseStrategy; +use rust_i18n::t; use crate::command::CommandExt; use crate::execution_context::ExecutionContext; @@ -74,9 +75,12 @@ impl Emacs { if let Some(doom) = &self.doom { Emacs::update_doom(doom, ctx)?; } - let init_file = require_option(self.directory.as_ref(), String::from("Emacs directory does not exist"))? - .join("init.el") - .require()?; + let init_file = require_option( + self.directory.as_ref(), + t!("Emacs directory does not exist").to_string(), + )? + .join("init.el") + .require()?; print_separator("Emacs"); diff --git a/src/steps/generic.rs b/src/steps/generic.rs index f16584c4..f25f0fae 100644 --- a/src/steps/generic.rs +++ b/src/steps/generic.rs @@ -8,6 +8,7 @@ use std::{fs, io::Write}; use color_eyre::eyre::eyre; use color_eyre::eyre::Context; use color_eyre::eyre::Result; +use rust_i18n::t; use semver::Version; use tempfile::tempfile_in; use tracing::{debug, error}; @@ -16,7 +17,7 @@ use crate::command::{CommandExt, Utf8Output}; use crate::execution_context::ExecutionContext; use crate::executor::ExecutorOutput; use crate::terminal::{print_separator, shell}; -use crate::utils::{self, check_is_python_2_or_shim, require, require_option, which, PathExt, REQUIRE_SUDO}; +use crate::utils::{self, check_is_python_2_or_shim, get_require_sudo_string, require, require_option, which, PathExt}; use crate::Step; use crate::HOME_DIR; use crate::{ @@ -130,7 +131,7 @@ pub fn run_rubygems(ctx: &ExecutionContext) -> Result<()> { .args(["update", "--system"]) .status_checked()?; } else { - let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?; + let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?; if !Path::new("/usr/lib/ruby/vendor_ruby/rubygems/defaults/operating_system.rb").exists() { ctx.run_type() .execute(sudo) @@ -159,7 +160,7 @@ pub fn run_haxelib_update(ctx: &ExecutionContext) -> Result<()> { let mut command = if directory_writable { ctx.run_type().execute(&haxelib) } else { - let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?; + let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?; let mut c = ctx.run_type().execute(sudo); c.arg(&haxelib); c @@ -229,8 +230,8 @@ pub fn run_aqua(ctx: &ExecutionContext) -> Result<()> { print_separator("Aqua"); if ctx.run_type().dry() { - println!("Updating aqua ..."); - println!("Updating aqua installed cli tools ..."); + println!("{}", t!("Updating aqua ...")); + println!("{}", t!("Updating aqua installed cli tools ...")); Ok(()) } else { ctx.run_type().execute(&aqua).arg("update-aqua").status_checked()?; @@ -364,7 +365,7 @@ pub fn run_vcpkg_update(ctx: &ExecutionContext) -> Result<()> { let mut command = if is_root_install { ctx.run_type().execute(&vcpkg) } else { - let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?; + let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?; let mut c = ctx.run_type().execute(sudo); c.arg(&vcpkg); c @@ -665,7 +666,7 @@ pub fn run_tlmgr_update(ctx: &ExecutionContext) -> Result<()> { let mut command = if directory_writable { ctx.run_type().execute(&tlmgr) } else { - let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?; + let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?; let mut c = ctx.run_type().execute(sudo); c.arg(&tlmgr); c @@ -722,19 +723,22 @@ pub fn run_composer_update(ctx: &ExecutionContext) -> Result<()> { let composer_home = Command::new(&composer) .args(["global", "config", "--absolute", "--quiet", "home"]) .output_checked_utf8() - .map_err(|e| (SkipStep(format!("Error getting the composer directory: {e}")))) + .map_err(|e| (SkipStep(t!("Error getting the composer directory: {error}", error = e).to_string()))) .map(|s| PathBuf::from(s.stdout.trim()))? .require()?; if !composer_home.is_descendant_of(&HOME_DIR) { - return Err(SkipStep(format!( - "Composer directory {} isn't a descendant of the user's home directory", - composer_home.display() - )) + return Err(SkipStep( + t!( + "Composer directory {composer_home} isn't a descendant of the user's home directory", + composer_home = composer_home.display() + ) + .to_string(), + ) .into()); } - print_separator("Composer"); + print_separator(t!("Composer")); if ctx.config().composer_self_update() { cfg_if::cfg_if! { @@ -746,7 +750,7 @@ pub fn run_composer_update(ctx: &ExecutionContext) -> Result<()> { }; if has_update { - let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?; + let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?; ctx.run_type() .execute(sudo) .arg(&composer) @@ -790,9 +794,10 @@ pub fn run_dotnet_upgrade(ctx: &ExecutionContext) -> Result<()> { { Ok(output) => output, Err(_) => { - return Err(SkipStep(String::from( - "Error running `dotnet tool list`. This is expected when a dotnet runtime is installed but no SDK.", - )) + return Err(SkipStep( + t!("Error running `dotnet tool list`. This is expected when a dotnet runtime is installed but no SDK.") + .to_string(), + ) .into()); } }; @@ -820,7 +825,7 @@ pub fn run_dotnet_upgrade(ctx: &ExecutionContext) -> Result<()> { .peekable(); if packages.peek().is_none() { - return Err(SkipStep(String::from("No dotnet global tools installed")).into()); + return Err(SkipStep(t!("No dotnet global tools installed").to_string()).into()); } print_separator(".NET"); @@ -831,7 +836,7 @@ pub fn run_dotnet_upgrade(ctx: &ExecutionContext) -> Result<()> { .execute(&dotnet) .args(["tool", "update", package_name, "--global"]) .status_checked() - .with_context(|| format!("Failed to update .NET package {package_name}"))?; + .with_context(|| format!("Failed to update .NET package {:?}", package_name))?; } Ok(()) @@ -860,7 +865,7 @@ pub fn run_helix_grammars(ctx: &ExecutionContext) -> Result<()> { pub fn run_raco_update(ctx: &ExecutionContext) -> Result<()> { let raco = require("raco")?; - print_separator("Racket Package Manager"); + print_separator(t!("Racket Package Manager")); ctx.run_type() .execute(raco) @@ -888,10 +893,10 @@ pub fn run_ghcli_extensions_upgrade(ctx: &ExecutionContext) -> Result<()> { let result = Command::new(&gh).args(["extensions", "list"]).output_checked_utf8(); if result.is_err() { debug!("GH result {:?}", result); - return Err(SkipStep(String::from("GH failed")).into()); + return Err(SkipStep(t!("GH failed").to_string()).into()); } - print_separator("GitHub CLI Extensions"); + print_separator(t!("GitHub CLI Extensions")); ctx.run_type() .execute(&gh) .args(["extension", "upgrade", "--all"]) @@ -901,7 +906,7 @@ pub fn run_ghcli_extensions_upgrade(ctx: &ExecutionContext) -> Result<()> { pub fn update_julia_packages(ctx: &ExecutionContext) -> Result<()> { let julia = require("julia")?; - print_separator("Julia Packages"); + print_separator(t!("Julia Packages")); ctx.run_type() .execute(julia) @@ -918,7 +923,7 @@ pub fn run_helm_repo_update(ctx: &ExecutionContext) -> Result<()> { let mut success = true; let mut exec = ctx.run_type().execute(helm); if let Err(e) = exec.arg("repo").arg("update").status_checked() { - error!("Updating repositories failed: {}", e); + error!("Updating repositories failed: {e}"); success = match exec.output_checked_utf8() { Ok(s) => s.stdout.contains(no_repo) || s.stderr.contains(no_repo), Err(e) => match e.downcast_ref::() { @@ -951,7 +956,7 @@ pub fn run_bob(ctx: &ExecutionContext) -> Result<()> { } pub fn run_certbot(ctx: &ExecutionContext) -> Result<()> { - let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?; + let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?; let certbot = require("certbot")?; print_separator("Certbot"); @@ -968,7 +973,7 @@ pub fn run_certbot(ctx: &ExecutionContext) -> Result<()> { /// doc: https://docs.clamav.net/manual/Usage/SignatureManagement.html#freshclam pub fn run_freshclam(ctx: &ExecutionContext) -> Result<()> { let freshclam = require("freshclam")?; - print_separator("Update ClamAV Database(FreshClam)"); + print_separator(t!("Update ClamAV Database(FreshClam)")); ctx.run_type().execute(freshclam).status_checked() } @@ -1001,7 +1006,7 @@ pub fn run_lensfun_update_data(ctx: &ExecutionContext) -> Result<()> { const EXIT_CODE_WHEN_NO_UPDATE: i32 = 1; if ctx.config().lensfun_use_sudo() { - let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?; + let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?; print_separator(SEPARATOR); ctx.run_type() .execute(sudo) diff --git a/src/steps/git.rs b/src/steps/git.rs index 7988aed4..2f0e17e2 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; @@ -100,16 +101,18 @@ pub fn run_git_pull(ctx: &ExecutionContext) -> Result<()> { // NOTE: this should be executed **before** skipping the Git step or the // user won't receive this warning in the cases where all the paths configured // are bad patterns. - repos - .bad_patterns - .iter() - .for_each(|pattern| print_warning(format!("Path {pattern} did not contain any git repositories"))); + repos.bad_patterns.iter().for_each(|pattern| { + print_warning(t!( + "Path {pattern} did not contain any git repositories", + pattern = pattern + )) + }); if repos.is_repos_empty() { - return Err(SkipStep(String::from("No repositories to pull")).into()); + return Err(SkipStep(t!("No repositories to pull").to_string()).into()); } - print_separator("Git repositories"); + print_separator(t!("Git repositories")); repos.pull_repos(ctx) } @@ -143,7 +146,7 @@ fn get_head_revision>(git: &Path, repo: P) -> Option { .output_checked_utf8() .map(|output| output.stdout.trim().to_string()) .map_err(|e| { - error!("Error getting revision for {}: {}", repo.as_ref().display(), e); + error!("Error getting revision for {}: {e}", repo.as_ref().display(),); e }) @@ -206,7 +209,7 @@ impl RepoStep { } Err(e) => match e.kind() { io::ErrorKind::NotFound => debug!("{} does not exist", path.as_ref().display()), - _ => error!("Error looking for {}: {}", path.as_ref().display(), e), + _ => error!("Error looking for {}: {e}", path.as_ref().display(),), }, } @@ -236,7 +239,7 @@ impl RepoStep { res.map(|output| output.stdout.lines().count() > 0) .map_err(|e| { - error!("Error getting remotes for {}: {}", repo.as_ref().display(), e); + error!("Error getting remotes for {}: {e}", repo.as_ref().display()); e }) .ok() @@ -264,7 +267,7 @@ impl RepoStep { } } Err(e) => { - error!("Error in path {}", e); + error!("Error in path {e}"); } } } @@ -273,7 +276,7 @@ impl RepoStep { self.bad_patterns.push(String::from(pattern)); } } else { - error!("Bad glob pattern: {}", pattern); + error!("Bad glob pattern: {pattern}"); } } @@ -296,7 +299,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); @@ -322,13 +325,18 @@ impl RepoStep { .wrap_err_with(|| format!("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 +353,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 +371,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}", repo = 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 +390,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/node.rs b/src/steps/node.rs index 51b56a6e..1fbaafa5 100644 --- a/src/steps/node.rs +++ b/src/steps/node.rs @@ -4,11 +4,12 @@ use std::os::unix::fs::MetadataExt; use std::path::PathBuf; use std::process::Command; -use crate::utils::{require_option, REQUIRE_SUDO}; +use crate::utils::{get_require_sudo_string, require_option}; use crate::HOME_DIR; use color_eyre::eyre::Result; #[cfg(target_os = "linux")] use nix::unistd::Uid; +use rust_i18n::t; use semver::Version; use tracing::debug; @@ -92,7 +93,7 @@ impl NPM { fn upgrade(&self, ctx: &ExecutionContext, use_sudo: bool) -> Result<()> { let args = ["update", self.global_location_arg()]; if use_sudo { - let sudo = require_option(ctx.sudo().clone(), REQUIRE_SUDO.to_string())?; + let sudo = require_option(ctx.sudo().clone(), get_require_sudo_string())?; ctx.run_type() .execute(sudo) .arg(&self.command) @@ -156,7 +157,7 @@ impl Yarn { let args = ["global", "upgrade"]; if use_sudo { - let sudo = require_option(ctx.sudo().clone(), REQUIRE_SUDO.to_string())?; + let sudo = require_option(ctx.sudo().clone(), get_require_sudo_string())?; ctx.run_type() .execute(sudo) .arg(self.yarn.as_ref().unwrap_or(&self.command)) @@ -214,7 +215,7 @@ fn should_use_sudo_yarn(yarn: &Yarn, ctx: &ExecutionContext) -> Result { pub fn run_npm_upgrade(ctx: &ExecutionContext) -> Result<()> { let npm = require("npm").map(|b| NPM::new(b, NPMVariant::Npm))?; - print_separator("Node Package Manager"); + print_separator(t!("Node Package Manager")); #[cfg(target_os = "linux")] { @@ -230,7 +231,7 @@ pub fn run_npm_upgrade(ctx: &ExecutionContext) -> Result<()> { pub fn run_pnpm_upgrade(ctx: &ExecutionContext) -> Result<()> { let pnpm = require("pnpm").map(|b| NPM::new(b, NPMVariant::Pnpm))?; - print_separator("Performant Node Package Manager"); + print_separator(t!("Performant Node Package Manager")); #[cfg(target_os = "linux")] { @@ -251,7 +252,7 @@ pub fn run_yarn_upgrade(ctx: &ExecutionContext) -> Result<()> { return Ok(()); } - print_separator("Yarn Package Manager"); + print_separator(t!("Yarn Package Manager")); #[cfg(target_os = "linux")] { @@ -269,7 +270,7 @@ pub fn deno_upgrade(ctx: &ExecutionContext) -> Result<()> { let deno_dir = HOME_DIR.join(".deno"); if !deno.canonicalize()?.is_descendant_of(&deno_dir) { - let skip_reason = SkipStep("Deno installed outside of .deno directory".to_string()); + let skip_reason = SkipStep(t!("Deno installed outside of .deno directory").to_string()); return Err(skip_reason.into()); } @@ -284,7 +285,7 @@ pub fn run_volta_packages_upgrade(ctx: &ExecutionContext) -> Result<()> { print_separator("Volta"); if ctx.run_type().dry() { - print_info("Updating Volta packages..."); + print_info(t!("Updating Volta packages...")); return Ok(()); } @@ -308,7 +309,7 @@ pub fn run_volta_packages_upgrade(ctx: &ExecutionContext) -> Result<()> { .collect(); if installed_packages.is_empty() { - print_info("No packages installed with Volta"); + print_info(t!("No packages installed with Volta")); return Ok(()); } diff --git a/src/steps/os/archlinux.rs b/src/steps/os/archlinux.rs index 2575c431..ac0ab8ab 100644 --- a/src/steps/os/archlinux.rs +++ b/src/steps/os/archlinux.rs @@ -4,6 +4,7 @@ use std::path::{Path, PathBuf}; use color_eyre::eyre; use color_eyre::eyre::Result; +use rust_i18n::t; use walkdir::WalkDir; use crate::command::CommandExt; @@ -310,7 +311,7 @@ impl ArchPackageManager for Aura { } else { let sudo = crate::utils::require_option( ctx.sudo().as_ref(), - "Aura(<0.4.6) requires sudo installed to work with AUR packages".into(), + t!("Aura(<0.4.6) requires sudo installed to work with AUR packages").to_string(), )?; let mut cmd = ctx.run_type().execute(sudo); @@ -383,7 +384,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..895ab254 100644 --- a/src/steps/os/dragonfly.rs +++ b/src/steps/os/dragonfly.rs @@ -1,14 +1,14 @@ use crate::command::CommandExt; use crate::execution_context::ExecutionContext; use crate::terminal::print_separator; -use crate::utils::{require_option, REQUIRE_SUDO}; +use crate::utils::{get_require_sudo_string, require_option}; use crate::Step; use color_eyre::eyre::Result; use std::process::Command; pub fn upgrade_packages(ctx: &ExecutionContext) -> Result<()> { - let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?; - print_separator("DragonFly BSD Packages"); + let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?; + print_separator(t!("DragonFly BSD Packages")); let mut cmd = ctx.run_type().execute(sudo); cmd.args(["/usr/local/sbin/pkg", "upgrade"]); if ctx.config().yes(Step::System) { @@ -18,9 +18,9 @@ pub fn upgrade_packages(ctx: &ExecutionContext) -> Result<()> { } pub fn audit_packages(ctx: &ExecutionContext) -> Result<()> { - let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?; + let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?; - print_separator("DragonFly BSD Audit"); + print_separator(t!("DragonFly BSD Audit")); #[allow(clippy::disallowed_methods)] if !Command::new(sudo) @@ -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/freebsd.rs b/src/steps/os/freebsd.rs index 60e9275e..57982711 100644 --- a/src/steps/os/freebsd.rs +++ b/src/steps/os/freebsd.rs @@ -1,14 +1,15 @@ use crate::command::CommandExt; use crate::execution_context::ExecutionContext; use crate::terminal::print_separator; -use crate::utils::{require_option, REQUIRE_SUDO}; +use crate::utils::{get_require_sudo_string, require_option}; use crate::Step; use color_eyre::eyre::Result; +use rust_i18n::t; use std::process::Command; pub fn upgrade_freebsd(ctx: &ExecutionContext) -> Result<()> { - let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?; - print_separator("FreeBSD Update"); + let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?; + print_separator(t!("FreeBSD Update")); ctx.run_type() .execute(sudo) .args(["/usr/sbin/freebsd-update", "fetch", "install"]) @@ -16,8 +17,8 @@ pub fn upgrade_freebsd(ctx: &ExecutionContext) -> Result<()> { } pub fn upgrade_packages(ctx: &ExecutionContext) -> Result<()> { - let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?; - print_separator("FreeBSD Packages"); + let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?; + print_separator(t!("FreeBSD Packages")); let mut command = ctx.run_type().execute(sudo); @@ -29,9 +30,9 @@ pub fn upgrade_packages(ctx: &ExecutionContext) -> Result<()> { } pub fn audit_packages(ctx: &ExecutionContext) -> Result<()> { - let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?; + let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?; - print_separator("FreeBSD Audit"); + print_separator(t!("FreeBSD Audit")); Command::new(sudo) .args(["/usr/sbin/pkg", "audit", "-Fr"]) diff --git a/src/steps/os/linux.rs b/src/steps/os/linux.rs index 7853d535..80430006 100644 --- a/src/steps/os/linux.rs +++ b/src/steps/os/linux.rs @@ -3,6 +3,7 @@ use std::process::Command; use color_eyre::eyre::Result; use ini::Ini; +use rust_i18n::t; use tracing::{debug, warn}; use crate::command::CommandExt; @@ -11,7 +12,7 @@ use crate::execution_context::ExecutionContext; use crate::steps::generic::is_wsl; use crate::steps::os::archlinux; use crate::terminal::{print_separator, prompt_yesno}; -use crate::utils::{require, require_option, which, PathExt, REQUIRE_SUDO}; +use crate::utils::{get_require_sudo_string, require, require_option, which, PathExt}; use crate::{Step, HOME_DIR}; static OS_RELEASE_PATH: &str = "/etc/os-release"; @@ -135,7 +136,7 @@ impl Distribution { } pub fn upgrade(self, ctx: &ExecutionContext) -> Result<()> { - print_separator("System update"); + print_separator(t!("System update")); match self { Distribution::Alpine => upgrade_alpine_linux(ctx), @@ -176,7 +177,7 @@ impl Distribution { } fn update_bedrock(ctx: &ExecutionContext) -> Result<()> { - let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?; + let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?; ctx.run_type().execute(sudo).args(["brl", "update"]); @@ -201,7 +202,7 @@ fn update_bedrock(ctx: &ExecutionContext) -> Result<()> { fn upgrade_alpine_linux(ctx: &ExecutionContext) -> Result<()> { let apk = require("apk")?; - let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?; + let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?; ctx.run_type().execute(sudo).arg(&apk).arg("update").status_checked()?; ctx.run_type().execute(sudo).arg(&apk).arg("upgrade").status_checked() @@ -209,7 +210,7 @@ fn upgrade_alpine_linux(ctx: &ExecutionContext) -> Result<()> { fn upgrade_chimera_linux(ctx: &ExecutionContext) -> Result<()> { let apk = require("apk")?; - let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?; + let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?; ctx.run_type().execute(sudo).arg(&apk).arg("update").status_checked()?; ctx.run_type().execute(sudo).arg(&apk).arg("upgrade").status_checked() @@ -217,7 +218,7 @@ fn upgrade_chimera_linux(ctx: &ExecutionContext) -> Result<()> { fn upgrade_wolfi_linux(ctx: &ExecutionContext) -> Result<()> { let apk = require("apk")?; - let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?; + let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?; ctx.run_type().execute(sudo).arg(&apk).arg("update").status_checked()?; ctx.run_type().execute(sudo).arg(&apk).arg("upgrade").status_checked() @@ -232,7 +233,7 @@ fn upgrade_redhat(ctx: &ExecutionContext) -> Result<()> { } }; - let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?; + let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?; let mut command = ctx.run_type().execute(sudo); command .arg(which("dnf").unwrap_or_else(|| Path::new("yum").to_path_buf())) @@ -255,7 +256,7 @@ fn upgrade_redhat(ctx: &ExecutionContext) -> Result<()> { } fn upgrade_nobara(ctx: &ExecutionContext) -> Result<()> { - let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?; + let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?; let pkg_manager = require("dnf")?; let mut update_command = ctx.run_type().execute(sudo); @@ -289,7 +290,7 @@ fn upgrade_nobara(ctx: &ExecutionContext) -> Result<()> { } fn upgrade_nilrt(ctx: &ExecutionContext) -> Result<()> { - let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?; + let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?; let opkg = require("opkg")?; ctx.run_type().execute(sudo).arg(&opkg).arg("update").status_checked()?; @@ -305,14 +306,14 @@ fn upgrade_fedora_immutable(ctx: &ExecutionContext) -> Result<()> { } fn upgrade_bedrock_strata(ctx: &ExecutionContext) -> Result<()> { - let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?; + let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?; ctx.run_type().execute(sudo).args(["brl", "update"]).status_checked()?; Ok(()) } fn upgrade_suse(ctx: &ExecutionContext) -> Result<()> { - let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?; + let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?; ctx.run_type() .execute(sudo) .args(["zypper", "refresh"]) @@ -335,7 +336,7 @@ fn upgrade_suse(ctx: &ExecutionContext) -> Result<()> { } fn upgrade_opensuse_tumbleweed(ctx: &ExecutionContext) -> Result<()> { - let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?; + let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?; ctx.run_type() .execute(sudo) .args(["zypper", "refresh"]) @@ -353,7 +354,7 @@ fn upgrade_opensuse_tumbleweed(ctx: &ExecutionContext) -> Result<()> { } fn upgrade_suse_micro(ctx: &ExecutionContext) -> Result<()> { - let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?; + let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?; let mut cmd = ctx.run_type().execute(sudo); cmd.arg("transactional-update"); if ctx.config().yes(Step::System) { @@ -366,7 +367,7 @@ fn upgrade_suse_micro(ctx: &ExecutionContext) -> Result<()> { } fn upgrade_openmandriva(ctx: &ExecutionContext) -> Result<()> { - let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?; + let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?; let mut command = ctx.run_type().execute(sudo); command.arg(which("dnf").unwrap()).arg("upgrade"); @@ -385,7 +386,7 @@ fn upgrade_openmandriva(ctx: &ExecutionContext) -> Result<()> { } fn upgrade_pclinuxos(ctx: &ExecutionContext) -> Result<()> { - let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?; + let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?; let mut command_update = ctx.run_type().execute(sudo); command_update.arg(which("apt-get").unwrap()).arg("update"); @@ -432,7 +433,7 @@ fn upgrade_vanilla(ctx: &ExecutionContext) -> Result<()> { } fn upgrade_void(ctx: &ExecutionContext) -> Result<()> { - let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?; + let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?; let mut command = ctx.run_type().execute(sudo); command.args(["xbps-install", "-Su", "xbps"]); if ctx.config().yes(Step::System) { @@ -453,7 +454,7 @@ fn upgrade_void(ctx: &ExecutionContext) -> Result<()> { fn upgrade_gentoo(ctx: &ExecutionContext) -> Result<()> { let run_type = ctx.run_type(); - let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?; + let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?; if let Some(layman) = which("layman") { run_type .execute(sudo) @@ -462,7 +463,7 @@ fn upgrade_gentoo(ctx: &ExecutionContext) -> Result<()> { .status_checked()?; } - println!("Syncing portage"); + println!("{}", t!("Syncing portage")); if let Some(ego) = which("ego") { // The Funtoo team doesn't reccomend running both ego sync and emerge --sync run_type.execute(sudo).arg(ego).arg("sync").status_checked()?; @@ -528,7 +529,7 @@ fn upgrade_debian(ctx: &ExecutionContext) -> Result<()> { return Ok(()); } - let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?; + let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?; if !is_nala { ctx.run_type() .execute(sudo) @@ -582,7 +583,7 @@ pub fn run_deb_get(ctx: &ExecutionContext) -> Result<()> { } fn upgrade_solus(ctx: &ExecutionContext) -> Result<()> { - let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?; + let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?; let mut cmd = ctx.run_type().execute(sudo); cmd.arg("eopkg"); if ctx.config().yes(Step::System) { @@ -691,7 +692,7 @@ pub fn run_packer_nu(ctx: &ExecutionContext) -> Result<()> { } fn upgrade_clearlinux(ctx: &ExecutionContext) -> Result<()> { - let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?; + let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?; let mut cmd = ctx.run_type().execute(sudo); cmd.args(["swupd", "update"]); if ctx.config().yes(Step::System) { @@ -703,7 +704,7 @@ fn upgrade_clearlinux(ctx: &ExecutionContext) -> Result<()> { } fn upgrade_exherbo(ctx: &ExecutionContext) -> Result<()> { - let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?; + let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?; ctx.run_type().execute(sudo).args(["cave", "sync"]).status_checked()?; ctx.run_type() @@ -732,7 +733,7 @@ fn upgrade_exherbo(ctx: &ExecutionContext) -> Result<()> { } fn upgrade_nixos(ctx: &ExecutionContext) -> Result<()> { - let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?; + let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?; let mut command = ctx.run_type().execute(sudo); command.args(["/run/current-system/sw/bin/nixos-rebuild", "switch", "--upgrade"]); @@ -758,7 +759,7 @@ fn upgrade_neon(ctx: &ExecutionContext) -> Result<()> { // seems rare // if that comes up we need to create a Distribution::PackageKit or some such - let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?; + let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?; let pkcon = which("pkcon").unwrap(); // pkcon ignores update with update and refresh provided together ctx.run_type() @@ -787,7 +788,7 @@ fn upgrade_neon(ctx: &ExecutionContext) -> Result<()> { /// alternative fn should_skip_needrestart() -> Result<()> { let distribution = Distribution::detect()?; - let msg = "needrestart will be ran by the package manager"; + let msg = t!("needrestart will be ran by the package manager"); if distribution.redhat_based() { return Err(SkipStep(String::from(msg)).into()); @@ -822,12 +823,12 @@ fn should_skip_needrestart() -> Result<()> { } pub fn run_needrestart(ctx: &ExecutionContext) -> Result<()> { - let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?; + let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?; let needrestart = require("needrestart")?; should_skip_needrestart()?; - print_separator("Check for needed restarts"); + print_separator(t!("Check for needed restarts")); ctx.run_type().execute(sudo).arg(needrestart).status_checked()?; @@ -838,10 +839,10 @@ pub fn run_fwupdmgr(ctx: &ExecutionContext) -> Result<()> { let fwupdmgr = require("fwupdmgr")?; if is_wsl()? { - return Err(SkipStep(String::from("Should not run in WSL")).into()); + return Err(SkipStep(t!("Should not run in WSL").to_string()).into()); } - print_separator("Firmware upgrades"); + print_separator(t!("Firmware upgrades")); ctx.run_type() .execute(&fwupdmgr) @@ -863,7 +864,7 @@ pub fn run_fwupdmgr(ctx: &ExecutionContext) -> Result<()> { pub fn run_flatpak(ctx: &ExecutionContext) -> Result<()> { let flatpak = require("flatpak")?; - let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?; + let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?; let cleanup = ctx.config().cleanup(); let yes = ctx.config().yes(Step::Flatpak); let run_type = ctx.run_type(); @@ -883,7 +884,7 @@ pub fn run_flatpak(ctx: &ExecutionContext) -> Result<()> { run_type.execute(&flatpak).args(&cleanup_args).status_checked()?; } - print_separator("Flatpak System Packages"); + print_separator(t!("Flatpak System Packages")); if ctx.config().flatpak_use_sudo() || std::env::var("SSH_CLIENT").is_ok() { let mut update_args = vec!["update", "--system"]; if yes { @@ -924,11 +925,11 @@ pub fn run_flatpak(ctx: &ExecutionContext) -> Result<()> { } pub fn run_snap(ctx: &ExecutionContext) -> Result<()> { - let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?; + let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?; let snap = require("snap")?; if !PathBuf::from("/var/snapd.socket").exists() && !PathBuf::from("/run/snapd.socket").exists() { - return Err(SkipStep(String::from("Snapd socket does not exist")).into()); + return Err(SkipStep(t!("Snapd socket does not exist").to_string()).into()); } print_separator("snap"); @@ -936,7 +937,7 @@ pub fn run_snap(ctx: &ExecutionContext) -> Result<()> { } pub fn run_pihole_update(ctx: &ExecutionContext) -> Result<()> { - let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?; + let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?; let pihole = require("pihole")?; Path::new("/opt/pihole/update.sh").require()?; @@ -970,7 +971,7 @@ pub fn run_distrobox_update(ctx: &ExecutionContext) -> Result<()> { ) { (r, Some(c)) => { if c.is_empty() { - return Err(SkipStep("You need to specify at least one container".to_string()).into()); + return Err(SkipStep(t!("You need to specify at least one container").to_string()).into()); } r.args(c) } @@ -985,7 +986,7 @@ pub fn run_distrobox_update(ctx: &ExecutionContext) -> Result<()> { } pub fn run_dkp_pacman_update(ctx: &ExecutionContext) -> Result<()> { - let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?; + let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?; let dkp_pacman = require("dkp-pacman")?; print_separator("Devkitpro pacman"); @@ -1008,20 +1009,20 @@ pub fn run_dkp_pacman_update(ctx: &ExecutionContext) -> Result<()> { } pub fn run_config_update(ctx: &ExecutionContext) -> Result<()> { - let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?; + let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?; if ctx.config().yes(Step::ConfigUpdate) { - return Err(SkipStep("Skipped in --yes".to_string()).into()); + return Err(SkipStep(t!("Skipped in --yes").to_string()).into()); } if let Ok(etc_update) = require("etc-update") { - print_separator("Configuration update"); + print_separator(t!("Configuration update")); ctx.run_type().execute(sudo).arg(etc_update).status_checked()?; } else if let Ok(pacdiff) = require("pacdiff") { if std::env::var("DIFFPROG").is_err() { require("vim")?; } - print_separator("Configuration update"); + print_separator(t!("Configuration update")); ctx.execute_elevated(&pacdiff, false)?.status_checked()?; } @@ -1045,7 +1046,7 @@ pub fn run_lure_update(ctx: &ExecutionContext) -> Result<()> { } pub fn run_waydroid(ctx: &ExecutionContext) -> Result<()> { - let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?; + let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?; let waydroid = require("waydroid")?; let status = ctx.run_type().execute(&waydroid).arg("status").output_checked_utf8()?; // example output of `waydroid status`: @@ -1069,17 +1070,20 @@ pub fn run_waydroid(ctx: &ExecutionContext) -> Result<()> { .stdout .lines() .find(|line| line.contains("Session:")) - .expect("the output of `waydroid status` should contain `Session:`"); + .unwrap_or_else(|| panic!("the output of `waydroid status` should contain `Session:`")); let is_container_running = session.contains("RUNNING"); let assume_yes = ctx.config().yes(Step::Waydroid); print_separator("Waydroid"); if is_container_running && !assume_yes { - let update_allowed = - prompt_yesno("Going to execute `waydroid upgrade`, which would STOP the running container, is this ok?")?; + let update_allowed = prompt_yesno(&t!( + "Going to execute `waydroid upgrade`, which would STOP the running container, is this ok?" + ))?; if !update_allowed { - return Err(SkipStep("Skip the Waydroid step because the user don't want to proceed".to_string()).into()); + return Err( + SkipStep(t!("Skip the Waydroid step because the user don't want to proceed").to_string()).into(), + ); } } ctx.run_type() @@ -1090,7 +1094,7 @@ pub fn run_waydroid(ctx: &ExecutionContext) -> Result<()> { } pub fn run_auto_cpufreq(ctx: &ExecutionContext) -> Result<()> { - let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?; + let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?; let auto_cpu_freq = require("auto-cpufreq")?; print_separator("auto-cpufreq"); diff --git a/src/steps/os/macos.rs b/src/steps/os/macos.rs index 72c4d3c0..c9325a06 100644 --- a/src/steps/os/macos.rs +++ b/src/steps/os/macos.rs @@ -1,9 +1,10 @@ use crate::command::CommandExt; use crate::execution_context::ExecutionContext; use crate::terminal::{print_separator, prompt_yesno}; -use crate::utils::{require_option, REQUIRE_SUDO}; +use crate::utils::{get_require_sudo_string, require_option}; 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; @@ -11,7 +12,7 @@ use tracing::debug; pub fn run_macports(ctx: &ExecutionContext) -> Result<()> { require("port")?; - let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?; + let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?; print_separator("MacPorts"); ctx.run_type() @@ -34,25 +35,25 @@ pub fn run_macports(ctx: &ExecutionContext) -> Result<()> { pub fn run_mas(ctx: &ExecutionContext) -> Result<()> { let mas = require("mas")?; - print_separator("macOS App Store"); + print_separator(t!("macOS App Store")); ctx.run_type().execute(mas).arg("upgrade").status_checked() } pub fn upgrade_macos(ctx: &ExecutionContext) -> Result<()> { - print_separator("macOS system update"); + print_separator(t!("macOS system update")); 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?").as_ref())?; if !answer { return Ok(()); } println!(); } else { - println!("No new software available."); + println!("{}", t!("No new software available.")); return Ok(()); } } @@ -115,7 +116,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 +195,8 @@ 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?").as_ref())?; if answer_uninstall { let _ = ctx .run_type() @@ -221,11 +223,12 @@ 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 { - let answer_install = prompt_yesno("Would you like to install it?")?; + let answer_install = prompt_yesno(t!("Would you like to install it?").as_ref())?; if answer_install { let _ = ctx .run_type() diff --git a/src/steps/os/openbsd.rs b/src/steps/os/openbsd.rs index cffb1843..4630aba6 100644 --- a/src/steps/os/openbsd.rs +++ b/src/steps/os/openbsd.rs @@ -1,12 +1,12 @@ use crate::command::CommandExt; use crate::execution_context::ExecutionContext; use crate::terminal::print_separator; -use crate::utils::{require_option, REQUIRE_SUDO}; +use crate::utils::{get_require_sudo_string, require_option}; use color_eyre::eyre::Result; pub fn upgrade_openbsd(ctx: &ExecutionContext) -> Result<()> { - let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?; - print_separator("OpenBSD Update"); + let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?; + print_separator(t!("OpenBSD Update")); ctx.run_type() .execute(sudo) .args(["/usr/sbin/sysupgrade", "-n"]) @@ -14,8 +14,8 @@ pub fn upgrade_openbsd(ctx: &ExecutionContext) -> Result<()> { } pub fn upgrade_packages(ctx: &ExecutionContext) -> Result<()> { - let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?; - print_separator("OpenBSD Packages"); + let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?; + print_separator(t!("OpenBSD Packages")); if ctx.config().cleanup() { ctx.run_type() diff --git a/src/steps/os/unix.rs b/src/steps/os/unix.rs index f66f2b6c..38cda5ab 100644 --- a/src/steps/os/unix.rs +++ b/src/steps/os/unix.rs @@ -15,6 +15,7 @@ use home; use ini::Ini; #[cfg(target_os = "linux")] use nix::unistd::Uid; +use rust_i18n::t; use semver::Version; use tracing::debug; @@ -27,7 +28,7 @@ use crate::executor::Executor; #[cfg(any(target_os = "linux", target_os = "macos"))] use crate::executor::RunType; use crate::terminal::print_separator; -use crate::utils::{require, require_option, PathExt, REQUIRE_SUDO}; +use crate::utils::{get_require_sudo_string, require, require_option, PathExt}; #[cfg(any(target_os = "linux", target_os = "macos"))] const INTEL_BREW: &str = "/usr/local/bin/brew"; @@ -101,19 +102,19 @@ pub fn run_fisher(ctx: &ExecutionContext) -> Result<()> { .args(["-c", "type -t fisher"]) .output_checked_utf8() .map(|_| ()) - .map_err(|_| SkipStep("`fisher` is not defined in `fish`".to_owned()))?; + .map_err(|_| SkipStep(t!("`fisher` is not defined in `fish`").to_string()))?; Command::new(&fish) .args(["-c", "echo \"$__fish_config_dir/fish_plugins\""]) .output_checked_utf8() .and_then(|output| Path::new(&output.stdout.trim()).require().map(|_| ())) - .map_err(|err| SkipStep(format!("`fish_plugins` path doesn't exist: {err}")))?; + .map_err(|err| SkipStep(t!("`fish_plugins` path doesn't exist: {err}", err = err).to_string()))?; Command::new(&fish) .args(["-c", "fish_update_completions"]) .output_checked_utf8() .map(|_| ()) - .map_err(|_| SkipStep("`fish_update_completions` is not available".to_owned()))?; + .map_err(|_| SkipStep(t!("`fish_update_completions` is not available").to_string()))?; print_separator("Fisher"); @@ -180,7 +181,7 @@ pub fn run_oh_my_fish(ctx: &ExecutionContext) -> Result<()> { pub fn run_pkgin(ctx: &ExecutionContext) -> Result<()> { let pkgin = require("pkgin")?; - let sudo = require_option(ctx.sudo().as_ref(), REQUIRE_SUDO.to_string())?; + let sudo = require_option(ctx.sudo().as_ref(), get_require_sudo_string())?; print_separator("Pkgin"); @@ -235,7 +236,7 @@ pub fn upgrade_gnome_extensions(ctx: &ExecutionContext) -> Result<()> { let gdbus = require("gdbus")?; require_option( var("XDG_CURRENT_DESKTOP").ok().filter(|p| p.contains("GNOME")), - "Desktop doest not appear to be gnome".to_string(), + t!("Desktop doest not appear to be gnome").to_string(), )?; let output = Command::new("gdbus") .args([ @@ -252,10 +253,10 @@ pub fn upgrade_gnome_extensions(ctx: &ExecutionContext) -> Result<()> { debug!("Checking for gnome extensions: {}", output); if !output.stdout.contains("org.gnome.Shell.Extensions") { - return Err(SkipStep(String::from("Gnome shell extensions are unregistered in DBus")).into()); + return Err(SkipStep(t!("Gnome shell extensions are unregistered in DBus").to_string()).into()); } - print_separator("Gnome Shell extensions"); + print_separator(t!("Gnome Shell extensions")); ctx.run_type() .execute(gdbus) @@ -297,7 +298,7 @@ pub fn run_brew_formula(ctx: &ExecutionContext, variant: BrewVariant) -> Result< #[cfg(target_os = "macos")] { if variant.is_path() && !BrewVariant::is_macos_custom(binary_name) { - return Err(SkipStep("Not a custom brew for macOS".to_string()).into()); + return Err(SkipStep(t!("Not a custom brew for macOS").to_string()).into()); } } @@ -310,8 +311,11 @@ pub fn run_brew_formula(ctx: &ExecutionContext, variant: BrewVariant) -> Result< let user = nix::unistd::User::from_uid(uid) .expect("failed to call getpwuid()") .expect("this user should exist"); - print_separator(format!("{} (sudo as user '{}')", variant.step_title(), user.name)); - let sudo = crate::utils::require_option(ctx.sudo().as_ref(), "sudo is needed to run the update".into())?; + + let sudo_as_user = t!("sudo as user '{user}'", user = user.name); + print_separator(format!("{} ({})", variant.step_title(), sudo_as_user)); + + let sudo = crate::utils::require_option(ctx.sudo().as_ref(), crate::utils::get_require_sudo_string())?; ctx.run_type() .execute(sudo) .current_dir("/tmp") // brew needs a writable current directory @@ -354,7 +358,7 @@ pub fn run_brew_formula(ctx: &ExecutionContext, variant: BrewVariant) -> Result< pub fn run_brew_cask(ctx: &ExecutionContext, variant: BrewVariant) -> Result<()> { let binary_name = require(variant.binary_name())?; if variant.is_path() && !BrewVariant::is_macos_custom(binary_name) { - return Err(SkipStep("Not a custom brew for macOS".to_string()).into()); + return Err(SkipStep(t!("Not a custom brew for macOS").to_string()).into()); } print_separator(format!("{} - Cask", variant.step_title())); let run_type = ctx.run_type(); @@ -409,7 +413,7 @@ pub fn run_guix(ctx: &ExecutionContext) -> Result<()> { if should_upgrade { return run_type.execute(&guix).args(["package", "-u"]).status_checked(); } - Err(SkipStep(String::from("Guix Pull Failed, Skipping")).into()) + Err(SkipStep(t!("Guix Pull Failed, Skipping").to_string()).into()) } pub fn run_nix(ctx: &ExecutionContext) -> Result<()> { @@ -429,41 +433,38 @@ pub fn run_nix(ctx: &ExecutionContext) -> Result<()> { #[cfg(target_os = "macos")] { if require("darwin-rebuild").is_ok() { - return Err(SkipStep(String::from( - "Nix-darwin on macOS must be upgraded via darwin-rebuild switch", - )) - .into()); + return Err( + SkipStep(t!("Nix-darwin on macOS must be upgraded via darwin-rebuild switch").to_string()).into(), + ); } } let run_type = ctx.run_type(); run_type.execute(nix_channel).arg("--update").status_checked()?; - let version: Result = match Command::new(&nix) - .arg("--version") - .output_checked_utf8()? + let mut get_version_cmd = ctx.run_type().execute(&nix); + get_version_cmd.arg("--version"); + let get_version_cmd_output = get_version_cmd.output_checked_utf8()?; + let get_version_cmd_first_line_stdout = get_version_cmd_output .stdout .lines() .next() - { - Some(item) => { - let parts: Vec<&str> = item.split_whitespace().collect(); - if parts.len() >= 3 { - Version::parse(parts[2]).map_err(|err| err.into()) - } else { - Err(SkipStep(String::from("Unexpected version format")).into()) - } - } - _ => return Err(SkipStep(String::from("Cannot find nix version")).into()), + .expect("nix --version gives an empty output"); + let splitted: Vec<&str> = get_version_cmd_first_line_stdout.split_whitespace().collect(); + let version = if splitted.len() >= 3 { + Version::parse(splitted[2]).expect("invalid version") + } else { + panic!("nix --version output format changed, file an issue to Topgrade!") }; debug!("Nix version: {:?}", version); - let mut packages: Vec<&str> = vec!["--all", "--impure"]; - - if !matches!(version, Ok(version) if version >= Version::new(2, 21, 0)) { - packages = vec![".*"]; - } + // Nix since 2.21.0 uses `--all --impure` rather than `.*` to upgrade all packages + let packages = if version >= Version::new(2, 21, 0) { + vec!["--all", "--impure"] + } else { + vec![".*"] + }; if Path::new(&manifest_json_path).exists() { run_type @@ -500,20 +501,16 @@ pub fn run_nix_self_upgrade(ctx: &ExecutionContext) -> Result<()> { } if !should_self_upgrade { - return Err(SkipStep(String::from( - "`nix upgrade-nix` can only be used on macOS or non-NixOS Linux", - )) - .into()); + return Err(SkipStep(t!("`nix upgrade-nix` can only be used on macOS or non-NixOS Linux").to_string()).into()); } if nix_profile_dir(&nix)?.is_none() { - return Err(SkipStep(String::from( - "`nix upgrade-nix` cannot be run when Nix is installed in a profile", - )) - .into()); + return Err( + SkipStep(t!("`nix upgrade-nix` cannot be run when Nix is installed in a profile").to_string()).into(), + ); } - print_separator("Nix (self-upgrade)"); + print_separator(t!("Nix (self-upgrade)")); let multi_user = fs::metadata(&nix)?.uid() == 0; debug!("Multi user nix: {}", multi_user); @@ -578,7 +575,6 @@ fn nix_profile_dir(nix: &Path) -> Result> { } debug!("Found Nix profile {profile_dir:?}"); - let user_env = profile_dir .canonicalize() .wrap_err_with(|| format!("Failed to canonicalize {profile_dir:?}"))?; @@ -675,15 +671,15 @@ pub fn run_pyenv(ctx: &ExecutionContext) -> Result<()> { .unwrap_or_else(|_| HOME_DIR.join(".pyenv")); if !pyenv_dir.exists() { - return Err(SkipStep("Pyenv is installed, but $PYENV_ROOT is not set correctly".to_string()).into()); + return Err(SkipStep(t!("Pyenv is installed, but $PYENV_ROOT is not set correctly").to_string()).into()); } if !pyenv_dir.join(".git").exists() { - return Err(SkipStep("pyenv is not a git repository".to_string()).into()); + return Err(SkipStep(t!("pyenv is not a git repository").to_string()).into()); } if !pyenv_dir.join("plugins").join("pyenv-update").exists() { - return Err(SkipStep("pyenv-update plugin is not installed".to_string()).into()); + return Err(SkipStep(t!("pyenv-update plugin is not installed").to_string()).into()); } ctx.run_type().execute(pyenv).arg("update").status_checked() @@ -755,7 +751,7 @@ pub fn run_sdkman(ctx: &ExecutionContext) -> Result<()> { pub fn run_bun_packages(ctx: &ExecutionContext) -> Result<()> { let bun = require("bun")?; - print_separator("Bun Packages"); + print_separator(t!("Bun Packages")); let mut package_json: PathBuf = var("BUN_INSTALL") .map(PathBuf::from) @@ -763,7 +759,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(()); } @@ -788,7 +784,7 @@ pub fn run_maza(ctx: &ExecutionContext) -> Result<()> { } pub fn reboot() -> Result<()> { - print!("Rebooting..."); + print!("{}", t!("Rebooting...")); cfg_if::cfg_if! { if #[cfg(target_os = "linux")] { diff --git a/src/steps/os/windows.rs b/src/steps/os/windows.rs index d752377a..c5ff220d 100644 --- a/src/steps/os/windows.rs +++ b/src/steps/os/windows.rs @@ -11,6 +11,7 @@ use crate::terminal::{print_separator, print_warning}; use crate::utils::{require, which}; use crate::{error::SkipStep, steps::git::RepoStep}; use crate::{powershell, Step}; +use rust_i18n::t; pub fn run_chocolatey(ctx: &ExecutionContext) -> Result<()> { let choco = require("choco")?; @@ -68,12 +69,12 @@ pub fn run_scoop(ctx: &ExecutionContext) -> Result<()> { pub fn update_wsl(ctx: &ExecutionContext) -> Result<()> { if !is_wsl_installed()? { - return Err(SkipStep("WSL not installed".to_string()).into()); + return Err(SkipStep(t!("WSL not installed").to_string()).into()); } let wsl = require("wsl")?; - print_separator("Update WSL"); + print_separator(t!("Update WSL")); let mut wsl_command = ctx.run_type().execute(wsl); wsl_command.args(["--update"]); @@ -126,7 +127,7 @@ fn upgrade_wsl_distribution(wsl: &Path, dist: &str, ctx: &ExecutionContext) -> R let topgrade = Command::new(wsl) .args(["-d", dist, "bash", "-lc", "which topgrade"]) .output_checked_utf8() - .map_err(|_| SkipStep(String::from("Could not find Topgrade installed in WSL")))? + .map_err(|_| SkipStep(t!("Could not find Topgrade installed in WSL").to_string()))? .stdout // The normal output from `which topgrade` appends a newline, so we trim it here. .trim_end() .to_owned(); @@ -175,7 +176,7 @@ fn upgrade_wsl_distribution(wsl: &Path, dist: &str, ctx: &ExecutionContext) -> R pub fn run_wsl_topgrade(ctx: &ExecutionContext) -> Result<()> { if !is_wsl_installed()? { - return Err(SkipStep("WSL not installed".to_string()).into()); + return Err(SkipStep(t!("WSL not installed").to_string()).into()); } let wsl = require("wsl")?; @@ -198,25 +199,25 @@ pub fn run_wsl_topgrade(ctx: &ExecutionContext) -> Result<()> { if ran { Ok(()) } else { - Err(SkipStep(String::from("Could not find Topgrade in any WSL disribution")).into()) + Err(SkipStep(t!("Could not find Topgrade in any WSL disribution").to_string()).into()) } } pub fn windows_update(ctx: &ExecutionContext) -> Result<()> { let powershell = powershell::Powershell::windows_powershell(); - print_separator("Windows Update"); + print_separator(t!("Windows Update")); if powershell.supports_windows_update() { println!("The installer will request to run as administrator, expect a prompt."); powershell.windows_update(ctx) } else { - print_warning( - "Consider installing PSWindowsUpdate Module as the use of Windows Update via USOClient is not supported.", - ); + print_warning(t!( + "Consider installing PSWindowsUpdate as the use of Windows Update via USOClient is not supported." + )); - Err(SkipStep("USOClient not supported.".to_string()).into()) + Err(SkipStep(t!("USOClient not supported.").to_string()).into()) } } diff --git a/src/steps/powershell.rs b/src/steps/powershell.rs index fbf482f0..7bd04036 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`. @@ -94,7 +95,8 @@ impl Powershell { #[cfg(windows)] pub fn windows_update(&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())?; + debug_assert!(self.supports_windows_update()); let accept_all = if ctx.config().accept_all_windows_updates() { diff --git a/src/steps/remote/ssh.rs b/src/steps/remote/ssh.rs index 6a9f244c..93c6d56f 100644 --- a/src/steps/remote/ssh.rs +++ b/src/steps/remote/ssh.rs @@ -1,54 +1,55 @@ -use color_eyre::eyre::Result; - -use crate::{ - command::CommandExt, error::SkipStep, execution_context::ExecutionContext, terminal::print_separator, utils, -}; - -fn prepare_async_ssh_command(args: &mut Vec<&str>) { - args.insert(0, "ssh"); - args.push("--keep"); -} - -pub fn ssh_step(ctx: &ExecutionContext, hostname: &str) -> Result<()> { - let ssh = utils::require("ssh")?; - - let topgrade = ctx.config().remote_topgrade_path(); - let mut args = vec!["-t", hostname]; - - if let Some(ssh_arguments) = ctx.config().ssh_arguments() { - args.extend(ssh_arguments.split_whitespace()); - } - - let env = format!("TOPGRADE_PREFIX={hostname}"); - args.extend(["env", &env, "$SHELL", "-lc", topgrade]); - - if ctx.config().run_in_tmux() && !ctx.run_type().dry() { - #[cfg(unix)] - { - 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()) - } - - #[cfg(not(unix))] - unreachable!("Tmux execution is only implemented in Unix"); - } 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()) - } else { - let mut args = vec!["-t", hostname]; - - if let Some(ssh_arguments) = ctx.config().ssh_arguments() { - args.extend(ssh_arguments.split_whitespace()); - } - - let env = format!("TOPGRADE_PREFIX={hostname}"); - args.extend(["env", &env, "$SHELL", "-lc", topgrade]); - - print_separator(format!("Remote ({hostname})")); - println!("Connecting to {hostname}..."); - - ctx.run_type().execute(ssh).args(&args).status_checked() - } -} +use color_eyre::eyre::Result; +use rust_i18n::t; + +use crate::{ + command::CommandExt, error::SkipStep, execution_context::ExecutionContext, terminal::print_separator, utils, +}; + +fn prepare_async_ssh_command(args: &mut Vec<&str>) { + args.insert(0, "ssh"); + args.push("--keep"); +} + +pub fn ssh_step(ctx: &ExecutionContext, hostname: &str) -> Result<()> { + let ssh = utils::require("ssh")?; + + let topgrade = ctx.config().remote_topgrade_path(); + let mut args = vec!["-t", hostname]; + + if let Some(ssh_arguments) = ctx.config().ssh_arguments() { + args.extend(ssh_arguments.split_whitespace()); + } + + let env = format!("TOPGRADE_PREFIX={hostname}"); + args.extend(["env", &env, "$SHELL", "-lc", topgrade]); + + if ctx.config().run_in_tmux() && !ctx.run_type().dry() { + #[cfg(unix)] + { + prepare_async_ssh_command(&mut args); + crate::tmux::run_command(ctx, hostname, &shell_words::join(args))?; + Err(SkipStep(String::from(t!("Remote Topgrade launched in Tmux"))).into()) + } + + #[cfg(not(unix))] + unreachable!("Tmux execution is only implemented in Unix"); + } 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(t!("Remote Topgrade launched in an external terminal"))).into()) + } else { + let mut args = vec!["-t", hostname]; + + if let Some(ssh_arguments) = ctx.config().ssh_arguments() { + args.extend(ssh_arguments.split_whitespace()); + } + + let env = format!("TOPGRADE_PREFIX={hostname}"); + args.extend(["env", &env, "$SHELL", "-lc", topgrade]); + + print_separator(format!("Remote ({hostname})")); + println!("{}", t!("Connecting to {hostname}...", hostname = hostname)); + + 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..f3c2397d 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}; @@ -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(); @@ -183,7 +184,11 @@ 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!( + "{}", + t!("Skipping powered off box {vagrant_box}", vagrant_box = vagrant_box) + )) + .into()); } else { print_separator(seperator); _poweron = Some(vagrant.temporary_power_on(vagrant_box, ctx)?); @@ -205,7 +210,7 @@ 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"]) @@ -227,7 +232,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 ad021eec..1914719c 100644 --- a/src/steps/tmux.rs +++ b/src/steps/tmux.rs @@ -16,6 +16,7 @@ use crate::{ utils::{which, PathExt}, }; +use rust_i18n::t; #[cfg(unix)] use std::os::unix::process::CommandExt as _; @@ -158,7 +159,7 @@ pub fn run_in_tmux(config: TmuxConfig) -> Result<()> { TmuxSessionMode::AttachIfNotInSession => { if is_inside_tmux { // Only attach to the newly-created session if we're not currently in a tmux session. - println!("Topgrade launched in a new tmux session"); + println!("{}", t!("Topgrade launched in a new tmux session")); return Ok(()); } else { tmux.build().args(["attach-client", "-t", &session]).exec() diff --git a/src/steps/vim.rs b/src/steps/vim.rs index dc507fd7..861982ca 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")) } } @@ -77,7 +78,7 @@ pub fn upgrade_ultimate_vimrc(ctx: &ExecutionContext) -> Result<()> { let python = require("python3")?; let update_plugins = config_dir.join("update_plugins.py").require()?; - print_separator("The Ultimate vimrc"); + print_separator(t!("The Ultimate vimrc")); ctx.run_type() .execute(&git) @@ -108,7 +109,7 @@ pub fn upgrade_vim(ctx: &ExecutionContext) -> Result<()> { let output = Command::new(&vim).arg("--version").output_checked_utf8()?; if !output.stdout.starts_with("VIM") { - return Err(SkipStep(String::from("vim binary might be actually nvim")).into()); + return Err(SkipStep(t!("vim binary might be actually nvim").to_string()).into()); } let vimrc = vimrc()?; diff --git a/src/steps/zsh.rs b/src/steps/zsh.rs index 35767e92..9800fda6 100644 --- a/src/steps/zsh.rs +++ b/src/steps/zsh.rs @@ -210,8 +210,7 @@ pub fn run_oh_my_zsh(ctx: &ExecutionContext) -> Result<()> { .unwrap_or_else(|e| { let default_path = oh_my_zsh.join("custom"); debug!( - "Running zsh returned {}. Using default path: {}", - e, + "Running zsh returned {e}. Using default path: {}", default_path.display() ); default_path diff --git a/src/terminal.rs b/src/terminal.rs index 04df3e06..39936464 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; @@ -144,7 +145,7 @@ impl Terminal { self.term .write_fmt(format_args!( "{} {}", - style(format!("{key} failed:")).red().bold(), + style(format!("{}", t!("{key} failed:", key = key))).red().bold(), message )) .ok(); @@ -174,10 +175,10 @@ impl Terminal { "{}: {}\n", key, match result { - StepResult::Success => format!("{}", style("OK").bold().green()), - StepResult::Failure => format!("{}", style("FAILED").bold().red()), - StepResult::Ignored => format!("{}", style("IGNORED").bold().yellow()), - StepResult::Skipped(reason) => format!("{}: {}", style("SKIPPED").bold().blue(), reason), + StepResult::Success => format!("{}", style(t!("OK")).bold().green()), + StepResult::Failure => format!("{}", style(t!("FAILED")).bold().red()), + StepResult::Ignored => format!("{}", style(t!("IGNORED")).bold().yellow()), + StepResult::Skipped(reason) => format!("{}: {}", style(t!("SKIPPED")).bold().blue(), reason), } )) .ok(); @@ -188,7 +189,7 @@ impl Terminal { self.term .write_fmt(format_args!( "{}", - style(format!("{question} (y)es/(N)o",)).yellow().bold() + style(format!("{question} {}", t!("(Y)es/(N)o"))).yellow().bold() )) .ok(); @@ -207,14 +208,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!("{}", t!("{step_name} failed", step_name = step_name)), None); } - let prompt_inner = style(format!("{}Retry? (y)es/(N)o/(s)hell/(q)uit", self.prefix)) + let prompt_inner = style(format!("{}{}", self.prefix, t!("Retry? (y)es/(N)o/(s)hell/(q)uit"))) .yellow() .bold(); @@ -224,7 +225,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 { diff --git a/src/utils.rs b/src/utils.rs index d16c4b0b..181ebddd 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; @@ -51,7 +52,11 @@ where debug!("Path {:?} exists", self.as_ref()); Ok(self) } else { - Err(SkipStep(format!("Path {:?} doesn't exist", self.as_ref())).into()) + Err(SkipStep(format!( + "{}", + t!("Path {path} doesn't exist", path = format!("{:?}", self.as_ref())) + )) + .into()) } } } @@ -92,9 +97,14 @@ pub fn require + Debug>(binary_name: T) -> Result { Ok(path) } Err(e) => match e { - which_crate::Error::CannotFindBinaryPath => { - Err(SkipStep(format!("Cannot find {:?} in PATH", &binary_name)).into()) - } + which_crate::Error::CannotFindBinaryPath => Err(SkipStep(format!( + "{}", + t!( + "Cannot find {binary_name} in PATH", + binary_name = format!("{:?}", &binary_name) + ) + )) + .into()), _ => { panic!("Detecting {:?} failed: {}", &binary_name, e); } @@ -123,7 +133,7 @@ pub fn hostname() -> Result { match nix::unistd::gethostname() { Ok(os_str) => Ok(os_str .into_string() - .map_err(|_| SkipStep("Failed to get a UTF-8 encoded hostname".into()))?), + .map_err(|_| SkipStep(t!("Failed to get a UTF-8 encoded hostname").into()))?), Err(e) => Err(e.into()), } } @@ -132,7 +142,7 @@ pub fn hostname() -> Result { pub fn hostname() -> Result { Command::new("hostname") .output_checked_utf8() - .map_err(|err| SkipStep(format!("Failed to get hostname: {err}")).into()) + .map_err(|err| SkipStep(t!("Failed to get hostname: {err}", err = err).to_string()).into()) .map(|output| output.stdout.trim().to_owned()) } @@ -191,7 +201,9 @@ pub mod merge_strategies { // Skip causes // TODO: Put them in a better place when we have more of them -pub const REQUIRE_SUDO: &str = "Require sudo or counterpart but not found, skip"; +pub fn get_require_sudo_string() -> String { + t!("Require sudo or counterpart but not found, skip").to_string() +} /// Return `Err(SkipStep)` if `python` is a Python 2 or shim. /// @@ -218,11 +230,11 @@ pub fn check_is_python_2_or_shim(python: PathBuf) -> Result { .parse::() .expect("Major version should be a valid number"); if major_version == 2 { - return Err(SkipStep(format!("{} is a Python 2, skip.", python.display())).into()); + return Err(SkipStep(t!("{python} is a Python 2, skip.", python = python.display()).to_string()).into()); } } else { // No version number, is a shim - return Err(SkipStep(format!("{} is a Python shim, skip.", python.display())).into()); + return Err(SkipStep(t!("{python} is a Python shim, skip.", python = python.display()).to_string()).into()); } Ok(python)