diff --git a/Cargo.lock b/Cargo.lock index 2da41fe2cd8..ffd2bd63ed8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -77,7 +77,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -88,7 +88,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -465,7 +465,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1015,7 +1015,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1124,6 +1124,12 @@ dependencies = [ "thiserror 2.0.19", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.2.0" @@ -2029,7 +2035,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2422,6 +2428,12 @@ dependencies = [ "hex", ] +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quote" version = "1.0.47" @@ -2660,7 +2672,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2669,6 +2681,18 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + [[package]] name = "same-file" version = "1.0.6" @@ -2893,7 +2917,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2989,7 +3013,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2999,7 +3023,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -4573,6 +4597,7 @@ dependencies = [ "procfs", "rustc-hash", "rustix", + "rusty-fork", "selinux", "sha1", "sha2", @@ -4646,6 +4671,15 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + [[package]] name = "walkdir" version = "2.5.0" @@ -4748,7 +4782,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index fa9543d607f..4fb4d85f8fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -477,6 +477,7 @@ terminal_size = "0.4.0" textwrap = { version = "0.16.1", features = ["terminal_size"] } thiserror = "2.0.3" time = { version = "0.3.36" } +rusty-fork = { version = "0.3.0" } unicode-width = "0.2.0" unit-prefix = "0.5" utmp-classic = "0.1.6" diff --git a/src/uu/cat/locales/en-US.ftl b/src/uu/cat/locales/en-US.ftl index bf81d6d7ffb..229ef49eb71 100644 --- a/src/uu/cat/locales/en-US.ftl +++ b/src/uu/cat/locales/en-US.ftl @@ -18,5 +18,4 @@ cat-help-ignored-u = (ignored) cat-error-unknown-filetype = unknown filetype: { $ft_debug } cat-error-is-directory = Is a directory cat-error-input-file-is-output-file = input file is output file -cat-error-too-many-symbolic-links = Too many levels of symbolic links cat-error-no-such-device-or-address = No such device or address diff --git a/src/uu/cat/locales/fr-FR.ftl b/src/uu/cat/locales/fr-FR.ftl index 2316544ce03..259a8980e3f 100644 --- a/src/uu/cat/locales/fr-FR.ftl +++ b/src/uu/cat/locales/fr-FR.ftl @@ -18,5 +18,4 @@ cat-help-ignored-u = (ignoré) cat-error-unknown-filetype = type de fichier inconnu : { $ft_debug } cat-error-is-directory = Est un répertoire cat-error-input-file-is-output-file = le fichier d'entrée est le fichier de sortie -cat-error-too-many-symbolic-links = Trop de niveaux de liens symboliques cat-error-no-such-device-or-address = Aucun appareil ou adresse de ce type diff --git a/src/uu/cat/src/cat.rs b/src/uu/cat/src/cat.rs index cfd236d5b8e..6b758fc6cab 100644 --- a/src/uu/cat/src/cat.rs +++ b/src/uu/cat/src/cat.rs @@ -93,7 +93,7 @@ enum CatError { NoSuchDeviceOrAddress, #[error("{}", translate!("cat-error-input-file-is-output-file"))] OutputIsInput, - #[error("{}", translate!("cat-error-too-many-symbolic-links"))] + #[error("{}", translate!("uio-err-too-many-symlinks"))] TooManySymlinks, } diff --git a/src/uu/du/src/du.rs b/src/uu/du/src/du.rs index 34c779554f3..37476f15153 100644 --- a/src/uu/du/src/du.rs +++ b/src/uu/du/src/du.rs @@ -642,9 +642,8 @@ fn du_regular( // Check symlink depth limit if current_symlink_depth > MAX_SYMLINK_DEPTH { - print_tx.send(Err(std::io::Error::new( - std::io::ErrorKind::InvalidData, - "Too many levels of symbolic links", + print_tx.send(Err(std::io::Error::other( + translate!("uio-err-too-many-symlinks") ).map_err_context( || translate!("du-error-cannot-access", "path" => entry_path.quote()), )))?; diff --git a/src/uu/mv/src/mv.rs b/src/uu/mv/src/mv.rs index 59353a5aa88..d41d0865649 100644 --- a/src/uu/mv/src/mv.rs +++ b/src/uu/mv/src/mv.rs @@ -379,8 +379,7 @@ fn parse_paths(files: &[OsString], opts: &Options) -> Vec { fn handle_two_paths(source: &Path, target: &Path, opts: &Options) -> UResult<()> { // `mv` never follows a symlink source, so the guard must not either. if backup_would_destroy_source(source, target, &opts.suffix, opts.backup, false) { - return Err(io::Error::new( - io::ErrorKind::NotFound, + return Err(io::Error::other( translate!("mv-error-backup-might-destroy-source", "target" => target.quote(), "source" => source.quote()), ) .into()); diff --git a/src/uucore/Cargo.toml b/src/uucore/Cargo.toml index 3ec6c75c28c..289d74e83f9 100644 --- a/src/uucore/Cargo.toml +++ b/src/uucore/Cargo.toml @@ -94,6 +94,7 @@ thiserror = { workspace = true } [dev-dependencies] tempfile = { workspace = true } +rusty-fork = { workspace = true } [target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies] selinux = { workspace = true, optional = true } diff --git a/src/uucore/locales/en-US.ftl b/src/uucore/locales/en-US.ftl index f408b4ee69e..2dad731e517 100644 --- a/src/uucore/locales/en-US.ftl +++ b/src/uucore/locales/en-US.ftl @@ -32,6 +32,26 @@ error-file-not-found = No such file or directory error-invalid-argument = Invalid argument error-is-a-directory = { $file }: Is a directory +uio-err-not-found = No such file or directory +uio-err-permission-denied = Permission denied +uio-err-conn-refused = Connection refused +uio-err-conn-reset = Connection reset +uio-err-conn-abort = Connection aborted +uio-err-not-connected = Not connected +uio-err-addr-in-use = Address in use +uio-err-addr-not-avail = Address not available +uio-err-broken-pipe = Broken pipe +uio-err-already-exists = Already exists +uio-err-would-block = Would block +uio-err-invalid-input = Invalid input +uio-err-invalid-data = Invalid data +uio-err-timed-out = Timed out +uio-err-write-zero = Write zero +uio-err-interrupted = Interrupted +uio-err-unexpected-eof = Unexpected end of file +uio-err-is-a-directory = Is a directory +uio-err-too-many-symlinks = Too many levels of symbolic links + # Common actions action-copying = copying action-moving = moving diff --git a/src/uucore/locales/fr-FR.ftl b/src/uucore/locales/fr-FR.ftl index 507a536cc6e..a7572390340 100644 --- a/src/uucore/locales/fr-FR.ftl +++ b/src/uucore/locales/fr-FR.ftl @@ -32,6 +32,26 @@ error-file-not-found = Aucun fichier ou répertoire de ce type error-invalid-argument = Argument invalide error-is-a-directory = { $file }: Est un répertoire +uio-err-not-found = Aucun fichier ou répertoire de ce type +uio-err-permission-denied = Permission refusée +uio-err-conn-refused = Connexion refusée +uio-err-conn-reset = Connexion réinitialisée +uio-err-conn-abort = Connexion interrompue +uio-err-not-connected = Déconnecté +# uio-err-addr-in-use = ? +# uio-err-addr-not-avail = ? +# uio-err-broken-pipe = ? +# uio-err-already-exists = ? +# uio-err-would-block = ? +# uio-err-invalid-input = ? +# uio-err-invalid-data = ? +# uio-err-timed-out = ? +# uio-err-write-zero = ? +# uio-err-interrupted = ? +# uio-err-unexpected-eof = ? +uio-err-is-a-directory = Est un répertoire +uio-err-too-many-symlinks = Trop de niveaux de liens symboliques + # Actions communes action-copying = copie action-moving = déplacement diff --git a/src/uucore/src/lib/features/checksum/validate.rs b/src/uucore/src/lib/features/checksum/validate.rs index d10d9588e06..59e662a5354 100644 --- a/src/uucore/src/lib/features/checksum/validate.rs +++ b/src/uucore/src/lib/features/checksum/validate.rs @@ -595,20 +595,14 @@ fn get_input_file(filename: &OsStr) -> UResult> { match File::open(filename) { Ok(f) => { if f.metadata()?.is_dir() { - Err(io::Error::other( - translate!("error-is-a-directory", "file" => filename.maybe_quote()), - ) - .into()) + Err(io::Error::from(io::ErrorKind::IsADirectory) + .map_err_context(|| filename.maybe_quote().to_string())) } else { Ok(Box::new(f)) } } - Err(_) => Err(io::Error::other(format!( - "{}: {}", - filename.maybe_quote(), - translate!("error-file-not-found") - )) - .into()), + Err(_) => Err(io::Error::from(io::ErrorKind::NotFound) + .map_err_context(|| filename.maybe_quote().to_string())), } } diff --git a/src/uucore/src/lib/features/fs.rs b/src/uucore/src/lib/features/fs.rs index a15bf0f2b2f..52a58a238b4 100644 --- a/src/uucore/src/lib/features/fs.rs +++ b/src/uucore/src/lib/features/fs.rs @@ -17,7 +17,7 @@ use std::fs; use std::fs::read_dir; use std::hash::Hash; use std::io::Stdin; -use std::io::{Error, ErrorKind, Result as IOResult}; +use std::io::{Error, Result as IOResult}; #[cfg(any(unix, all(target_os = "wasi", target_env = "p2")))] use std::os::fd::AsFd; #[cfg(unix)] @@ -38,6 +38,8 @@ use windows_sys::Win32::System::IO::DeviceIoControl; #[cfg(windows)] use windows_sys::Win32::System::Ioctl::FSCTL_SET_SPARSE; +use crate::translate; + /// Used to check if the `mode` has its `perm` bit set. /// /// This macro expands to `mode & perm != 0`. @@ -451,10 +453,7 @@ pub fn canonicalize>( path_to_follow.push(part.as_os_str()); } if !visited_files.insert((file_info, path_to_follow)) { - return Err(Error::new( - ErrorKind::InvalidInput, - "Too many levels of symbolic links", - )); // TODO use ErrorKind::FilesystemLoop when stable + return Err(Error::other(translate!("uio-err-too-many-symlinks"))); // TODO use ErrorKind::FilesystemLoop when stable } } result.pop(); diff --git a/src/uucore/src/lib/features/perms.rs b/src/uucore/src/lib/features/perms.rs index 3c00f5696ee..111843674a5 100644 --- a/src/uucore/src/lib/features/perms.rs +++ b/src/uucore/src/lib/features/perms.rs @@ -633,7 +633,7 @@ impl ChownExecutor { if let Some(error) = e.io_error() { strip_errno(error) } else { - "Too many levels of symbolic links".into() + translate!("uio-err-too-many-symlinks") } ); } else { diff --git a/src/uucore/src/lib/mods/clap_localization.rs b/src/uucore/src/lib/mods/clap_localization.rs index 0249089c142..e28caff6491 100644 --- a/src/uucore/src/lib/mods/clap_localization.rs +++ b/src/uucore/src/lib/mods/clap_localization.rs @@ -677,28 +677,31 @@ mod tests { } } - #[test] - fn test_french_localization() { - use crate::locale::{get_message, setup_localization}; - use std::env; + use rusty_fork::rusty_fork_test; + rusty_fork_test! { + #[test] + fn test_french_localization() { + use crate::locale::{get_message, setup_localization}; + use std::env; - let original_lang = env::var_os("LANG").unwrap_or_default(); + let original_lang = env::var_os("LANG").unwrap_or_default(); - unsafe { - env::set_var("LANG", "fr_FR.UTF-8"); - } + unsafe { + env::set_var("LANG", "fr_FR.UTF-8"); + } - if setup_localization("test").is_ok() { - assert_eq!(get_message("common-error"), "erreur"); - assert_eq!(get_message("common-usage"), "Utilisation"); - assert_eq!(get_message("common-tip"), "conseil"); - } + if setup_localization("test").is_ok() { + assert_eq!(get_message("common-error"), "erreur"); + assert_eq!(get_message("common-usage"), "Utilisation"); + assert_eq!(get_message("common-tip"), "conseil"); + } - unsafe { - if original_lang.is_empty() { - env::remove_var("LANG"); - } else { - env::set_var("LANG", original_lang); + unsafe { + if original_lang.is_empty() { + env::remove_var("LANG"); + } else { + env::set_var("LANG", original_lang); + } } } } diff --git a/src/uucore/src/lib/mods/error.rs b/src/uucore/src/lib/mods/error.rs index 6149f527667..6e91a4ddfb5 100644 --- a/src/uucore/src/lib/mods/error.rs +++ b/src/uucore/src/lib/mods/error.rs @@ -62,6 +62,8 @@ use std::{ sync::atomic::{AtomicI32, Ordering}, }; +use crate::translate; + static EXIT_CODE: AtomicI32 = AtomicI32::new(0); /// Get the last exit code set with [`set_exit_code`]. @@ -409,46 +411,34 @@ impl Display for UIoError { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { use std::io::ErrorKind::*; - let message; - let message = if self.inner.raw_os_error().is_some() { - // These are errors that come directly from the OS. - // We want to normalize their messages across systems, - // and we want to strip the "(os error X)" suffix. - match self.inner.kind() { - NotFound => "No such file or directory", - PermissionDenied => "Permission denied", - ConnectionRefused => "Connection refused", - ConnectionReset => "Connection reset", - ConnectionAborted => "Connection aborted", - NotConnected => "Not connected", - AddrInUse => "Address in use", - AddrNotAvailable => "Address not available", - BrokenPipe => "Broken pipe", - AlreadyExists => "Already exists", - WouldBlock => "Would block", - InvalidInput => "Invalid input", - InvalidData => "Invalid data", - TimedOut => "Timed out", - WriteZero => "Write zero", - Interrupted => "Interrupted", - UnexpectedEof => "Unexpected end of file", - IsADirectory => "Is a directory", - _ => { - // TODO: When the new error variants - // (https://github.com/rust-lang/rust/issues/86442) - // are stabilized, we should add them to the match statement. - message = strip_errno(&self.inner); - &message - } + // These are errors that come directly from the OS. + // We want to normalize their messages across systems, + // and we want to strip the "(os error X)" suffix. + let message = match self.inner.kind() { + NotFound => translate!("uio-err-not-found"), + PermissionDenied => translate!("uio-err-permission-denied"), + ConnectionRefused => translate!("uio-err-conn-refused"), + ConnectionReset => translate!("uio-err-conn-reset"), + ConnectionAborted => translate!("uio-err-conn-abort"), + NotConnected => translate!("uio-err-not-connected"), + AddrInUse => translate!("uio-err-addr-in-use"), + AddrNotAvailable => translate!("uio-err-addr-not-avail"), + BrokenPipe => translate!("uio-err-broken-pipe"), + AlreadyExists => translate!("uio-err-already-exists"), + WouldBlock => translate!("uio-err-would-block"), + InvalidInput => translate!("uio-err-invalid-input"), + InvalidData => translate!("uio-err-invalid-data"), + TimedOut => translate!("uio-err-timed-out"), + WriteZero => translate!("uio-err-write-zero"), + Interrupted => translate!("uio-err-interrupted"), + UnexpectedEof => translate!("uio-err-unexpected-eof"), + IsADirectory => translate!("uio-err-is-a-directory"), + _ => { + // TODO: When the new error variants + // (https://github.com/rust-lang/rust/issues/86442) + // are stabilized, we should add them to the match statement. + strip_errno(&self.inner) } - } else { - // These messages don't need as much normalization, and the above - // messages wouldn't always be a good substitute. - // For example, ErrorKind::NotFound doesn't necessarily mean it was - // a file that was not found. - // There are also errors with entirely custom messages. - message = self.inner.to_string(); - &message }; if let Some(ctx) = &self.context { write!(f, "{ctx}: {message}") @@ -804,6 +794,8 @@ mod tests { use nix::errno::Errno; use std::io::ErrorKind; + let _ = crate::locale::setup_localization("test"); + for (nix_error, expected_error_kind) in [ (Errno::EACCES, ErrorKind::PermissionDenied), (Errno::ENOENT, ErrorKind::NotFound), diff --git a/src/uucore/src/lib/mods/locale.rs b/src/uucore/src/lib/mods/locale.rs index 358088e04b8..5589bf34f5c 100644 --- a/src/uucore/src/lib/mods/locale.rs +++ b/src/uucore/src/lib/mods/locale.rs @@ -40,6 +40,8 @@ pub enum LocalizationError { LocalesDirNotFound(String), #[error("Path resolution error: {0}")] PathResolution(String), + #[error("No util provided")] + NoUtilProvided, } impl From for LocalizationError { @@ -120,6 +122,15 @@ thread_local! { ) )] static LOCALIZER: OnceLock = const { OnceLock::new() }; + // Avoid duplicated and high-cost localizer setup + #[cfg_attr( + target_os = "android", + expect( + clippy::missing_const_for_thread_local, + reason = "https://github.com/rust-lang/rust-clippy/issues/13422" + ) + )] + static LOCALIZER_IS_SET: Cell = const { Cell::new(false) }; } /// Helper function to find the uucore locales directory from a utility's locales directory @@ -204,11 +215,11 @@ fn create_bundle( } /// Initialize localization with common strings in addition to utility-specific strings -fn init_localization( +fn build_localizer_from_locales_dir( locale: &LanguageIdentifier, locales_dir: &Path, util_name: &str, -) -> Result<(), LocalizationError> { +) -> Result { let default_locale = LanguageIdentifier::from_str(DEFAULT_LOCALE) .expect("Default locale should always be valid"); @@ -233,11 +244,36 @@ fn init_localization( } }; - LOCALIZER.with(|lock| { - lock.set(loc) - .map_err(|_| LocalizationError::Bundle("Localizer already initialized".into())) - })?; - Ok(()) + Ok(loc) +} + +fn build_localizer_from_embedded( + locale: &LanguageIdentifier, + util_name: &str, +) -> Result { + let default_locale = LanguageIdentifier::from_str(DEFAULT_LOCALE) + .expect("Default locale should always be valid"); + let localizer = { + #[cfg(target_os = "wasi")] + { + let english_bundle = create_wasi_bundle_from_embedded(&default_locale, util_name)?; + if locale == &default_locale { + Localizer::new(english_bundle) + } else if let Ok(localized) = create_wasi_bundle_from_embedded(locale, util_name) { + Localizer::new(localized).with_fallback(english_bundle) + } else { + Localizer::new(english_bundle) + } + } + + #[cfg(not(target_os = "wasi"))] + { + let _ = locale; + let english_bundle = create_english_bundle_from_embedded(&default_locale, util_name)?; + Localizer::new(english_bundle) + } + }; + Ok(localizer) } /// Helper function to parse FluentResource from content string @@ -361,6 +397,9 @@ fn create_wasi_bundle_from_embedded( } fn get_message_internal(id: &str, args: Option) -> String { + // Disable lazy setup evaluation for tests. + #[cfg(not(test))] + let _ = lazy_thread_setup_localization(); LOCALIZER.with(|lock| { lock.get() .map_or_else(|| id.to_string(), |loc| loc.format(id, args.as_ref())) // Return the key ID if localizer not initialized @@ -443,6 +482,13 @@ fn detect_system_locale() -> Result { }) } +#[inline(always)] +fn detect_system_locale_or_default() -> LanguageIdentifier { + detect_system_locale().unwrap_or_else(|_| { + LanguageIdentifier::from_str(DEFAULT_LOCALE).expect("Default locale should always be valid") + }) +} + /// Sets up localization using the system locale with English fallback. /// Always loads common strings in addition to utility-specific strings. /// @@ -480,58 +526,49 @@ fn detect_system_locale() -> Result { /// Err(e) => eprintln!("Failed to initialize localization: {e}"), /// } /// ``` -pub fn setup_localization(p: &str) -> Result<(), LocalizationError> { - // Avoid duplicated and high-cost localizer setup - thread_local! { - #[cfg_attr( - target_os = "android", - expect( - clippy::missing_const_for_thread_local, - reason = "https://github.com/rust-lang/rust-clippy/issues/13422" - ) - )] - static LOCALIZER_IS_SET: Cell = const { Cell::new(false) }; - } +pub fn setup_localization(util_name: &str) -> Result<(), LocalizationError> { + setup_localization_inner(Some(util_name)) +} + +/// Called by precaution when trying to get a message, in case we are in +/// a separate thread and the thread_local LOCALIZER is not created yet. +pub fn lazy_thread_setup_localization() -> Result<(), LocalizationError> { + setup_localization_inner(None) +} + +fn setup_localization_inner(util_name: Option<&str>) -> Result<(), LocalizationError> { + // Cache as much info as possible to avoid having to re-do the work when + // initializing data again for a thread. + static UTIL_NAME_AND_LOCALE_DIR: OnceLock<(String, Result)> = + OnceLock::new(); + static LOCALE: OnceLock = OnceLock::new(); + if LOCALIZER_IS_SET.with(Cell::get) { return Ok(()); } - let locale = detect_system_locale().unwrap_or_else(|_| { - LanguageIdentifier::from_str(DEFAULT_LOCALE).expect("Default locale should always be valid") - }); + let (util_name, locales_dir) = if let Some(util_name) = util_name { + UTIL_NAME_AND_LOCALE_DIR.get_or_init(|| (util_name.to_string(), get_locales_dir(util_name))) + } else { + UTIL_NAME_AND_LOCALE_DIR + .get() + .ok_or(LocalizationError::NoUtilProvided)? + }; + let locale = LOCALE.get_or_init(detect_system_locale_or_default); // Load common strings along with utility-specific strings - if let Ok(locales_dir) = get_locales_dir(p) { + let localizer = if let Ok(locales_dir) = locales_dir { // Load both utility-specific and common strings - init_localization(&locale, &locales_dir, p)?; + build_localizer_from_locales_dir(locale, locales_dir, util_name)? } else { // No locales directory found, use embedded locales - let default_locale = LanguageIdentifier::from_str(DEFAULT_LOCALE) - .expect("Default locale should always be valid"); - - #[cfg(target_os = "wasi")] - let localizer = { - let english_bundle = create_wasi_bundle_from_embedded(&default_locale, p)?; - if locale == default_locale { - Localizer::new(english_bundle) - } else if let Ok(localized) = create_wasi_bundle_from_embedded(&locale, p) { - Localizer::new(localized).with_fallback(english_bundle) - } else { - Localizer::new(english_bundle) - } - }; - - #[cfg(not(target_os = "wasi"))] - let localizer = { - let english_bundle = create_english_bundle_from_embedded(&default_locale, p)?; - Localizer::new(english_bundle) - }; + build_localizer_from_embedded(locale, util_name)? + }; - LOCALIZER.with(|lock| { - lock.set(localizer) - .map_err(|_| LocalizationError::Bundle("Localizer already initialized".into())) - })?; - } + LOCALIZER.with(|lock| { + lock.set(localizer) + .map_err(|_| LocalizationError::Bundle("Localizer already initialized".into())) + })?; LOCALIZER_IS_SET.with(|f| f.set(true)); Ok(()) } @@ -679,13 +716,13 @@ pub use translate; #[cfg(test)] mod tests { use super::*; + use rusty_fork::rusty_fork_test; use std::env; use std::fs; use std::path::PathBuf; use tempfile::TempDir; /// Test-specific helper function to create a bundle from test directory only - #[cfg(test)] fn create_test_bundle( locale: &LanguageIdentifier, test_locales_dir: &Path, @@ -709,7 +746,6 @@ mod tests { } /// Test-specific initialization function for test directories - #[cfg(test)] fn init_test_localization( locale: &LanguageIdentifier, test_locales_dir: &Path, @@ -806,653 +842,679 @@ invalid-syntax = This is { $missing temp_dir } - #[test] - fn test_create_bundle_success() { - let temp_dir = create_test_locales_dir(); - let locale = LanguageIdentifier::from_str("en-US").unwrap(); + rusty_fork_test! { + #[test] + fn test_create_bundle_success() { + let temp_dir = create_test_locales_dir(); + let locale = LanguageIdentifier::from_str("en-US").unwrap(); - let result = create_test_bundle(&locale, temp_dir.path()); - assert!(result.is_ok()); + let result = create_test_bundle(&locale, temp_dir.path()); + assert!(result.is_ok()); - let bundle = result.unwrap(); - assert!(bundle.get_message("greeting").is_some()); + let bundle = result.unwrap(); + assert!(bundle.get_message("greeting").is_some()); + } } - #[test] - fn test_create_bundle_file_not_found() { - let temp_dir = TempDir::new().unwrap(); - let locale = LanguageIdentifier::from_str("de-DE").unwrap(); - - let result = create_test_bundle(&locale, temp_dir.path()); - assert!(result.is_err()); + rusty_fork_test! { + #[test] + fn test_create_bundle_file_not_found() { + let temp_dir = TempDir::new().unwrap(); + let locale = LanguageIdentifier::from_str("de-DE").unwrap(); - if let Err(LocalizationError::LocalesDirNotFound(_)) = result { - // Expected - no localization strings found - } else { - panic!("Expected LocalesDirNotFound error"); - } - } + let result = create_test_bundle(&locale, temp_dir.path()); + assert!(result.is_err()); - #[test] - fn test_create_bundle_invalid_syntax() { - let temp_dir = create_test_locales_dir(); - let locale = LanguageIdentifier::from_str("es-ES").unwrap(); - - let result = create_test_bundle(&locale, temp_dir.path()); - - // The result should be an error due to invalid syntax - match result { - Err(LocalizationError::ParseResource { - error: _parser_err, - snippet: _, - }) => { - // Expected ParseResource variant - test passes - } - Ok(_) => { - panic!("Expected ParseResource error, but bundle was created successfully"); - } - Err(other) => { - panic!("Expected ParseResource error, but got: {other:?}"); + if let Err(LocalizationError::LocalesDirNotFound(_)) = result { + // Expected - no localization strings found + } else { + panic!("Expected LocalesDirNotFound error"); } } } - /// Regression test: fallback bundle is correctly constructed on missing - /// utility-specific locales. - /// - /// Before the fix, `create_bundle` returned `Ok` whenever common uucore - /// strings were loaded — even if the utility-specific locale file was - /// missing. This prevented `init_localization` from falling back to - /// embedded locales, so utility-specific message keys (e.g. - /// `wc-error-failed-to-print-result`) were returned verbatim instead of - /// being translated. - /// - /// After the fix, `create_bundle` requires the utility locale file to - /// have been loaded (`util_loaded`) and returns `Err` otherwise, allowing - /// the embedded-locale fallback path to kick in. - /// - /// https://github.com/uutils/coreutils/issues/11854 - #[test] - fn test_create_bundle_returns_err_when_util_locale_missing() { - // Build a temporary directory structure that mimics the repo layout - // so `find_uucore_locales_dir` can walk up and find common strings: - // - // /uu/fake_util/locales/ <- locales_dir passed to create_bundle - // /uucore/locales/en-US.ftl <- common strings (common-error) - let temp_dir = TempDir::new().expect("Failed to create temp directory"); - let temp_root = temp_dir.path(); - - fs::create_dir_all(temp_root.join("uu").join("fake_util").join("locales")) - .expect("Failed to create fake util locales dir"); - fs::create_dir_all(temp_root.join("uucore").join("locales")) - .expect("Failed to create fake uucore locales dir"); - - fs::write( - temp_root.join("uucore").join("locales").join("en-US.ftl"), - "common-error = error\n", - ) - .expect("Failed to write en-US.ftl"); - - let locales_dir = temp_root.join("uu").join("fake_util").join("locales"); - let locale = LanguageIdentifier::from_str(DEFAULT_LOCALE).unwrap(); - - // "fake_util" doesn't exist under src/uu/, so get_locales_dir fails - // and no utility-specific strings are loaded. Common strings ARE - // loaded from the temp uucore locales dir above. - let result = create_bundle(&locale, &locales_dir, "fake_util"); + rusty_fork_test! { + #[test] + fn test_create_bundle_invalid_syntax() { + let temp_dir = create_test_locales_dir(); + let locale = LanguageIdentifier::from_str("es-ES").unwrap(); - assert!( - result.is_err(), - "create_bundle should return Err when the utility locale file is missing, \ - even if common strings were loaded" - ); + let result = create_test_bundle(&locale, temp_dir.path()); - match result { - Err(LocalizationError::LocalesDirNotFound(msg)) => { - assert!( - msg.contains("fake_util"), - "error message should mention the utility name, got: {msg}" - ); + // The result should be an error due to invalid syntax + match result { + Err(LocalizationError::ParseResource { + error: _parser_err, + snippet: _, + }) => { + // Expected ParseResource variant - test passes + } + Ok(_) => { + panic!("Expected ParseResource error, but bundle was created successfully"); + } + Err(other) => { + panic!("Expected ParseResource error, but got: {other:?}"); + } } - Err(other) => panic!("Expected LocalesDirNotFound error, got: {other:?}"), - Ok(_) => panic!("Expected error, but create_bundle returned Ok"), } } - #[test] - fn test_localizer_format_primary_bundle() { - let temp_dir = create_test_locales_dir(); - let en_bundle: FluentBundle<&'static FluentResource> = create_test_bundle( - &LanguageIdentifier::from_str("en-US").unwrap(), - temp_dir.path(), - ) - .unwrap(); - - let localizer = Localizer::new(en_bundle); - let result = localizer.format("greeting", None); - assert_eq!(result, "Hello, world!"); - } - - #[test] - fn test_localizer_format_with_args() { - use fluent::FluentArgs; - let temp_dir = create_test_locales_dir(); - let en_bundle = create_test_bundle( - &LanguageIdentifier::from_str("en-US").unwrap(), - temp_dir.path(), - ) - .unwrap(); - - let localizer = Localizer::new(en_bundle); - let mut args = FluentArgs::new(); - args.set("name", "Alice"); + rusty_fork_test! { + /// Regression test: fallback bundle is correctly constructed on missing + /// utility-specific locales. + /// + /// Before the fix, `create_bundle` returned `Ok` whenever common uucore + /// strings were loaded — even if the utility-specific locale file was + /// missing. This prevented `init_localization` from falling back to + /// embedded locales, so utility-specific message keys (e.g. + /// `wc-error-failed-to-print-result`) were returned verbatim instead of + /// being translated. + /// + /// After the fix, `create_bundle` requires the utility locale file to + /// have been loaded (`util_loaded`) and returns `Err` otherwise, allowing + /// the embedded-locale fallback path to kick in. + /// + /// https://github.com/uutils/coreutils/issues/11854 + #[test] + fn test_create_bundle_returns_err_when_util_locale_missing() { + // Build a temporary directory structure that mimics the repo layout + // so `find_uucore_locales_dir` can walk up and find common strings: + // + // /uu/fake_util/locales/ <- locales_dir passed to create_bundle + // /uucore/locales/en-US.ftl <- common strings (common-error) + let temp_dir = TempDir::new().expect("Failed to create temp directory"); + let temp_root = temp_dir.path(); + + fs::create_dir_all(temp_root.join("uu").join("fake_util").join("locales")) + .expect("Failed to create fake util locales dir"); + fs::create_dir_all(temp_root.join("uucore").join("locales")) + .expect("Failed to create fake uucore locales dir"); + + fs::write( + temp_root.join("uucore").join("locales").join("en-US.ftl"), + "common-error = error\n", + ) + .expect("Failed to write en-US.ftl"); - let result = localizer.format("welcome", Some(&args)); - assert_eq!(result, "Welcome, Alice!"); - } + let locales_dir = temp_root.join("uu").join("fake_util").join("locales"); + let locale = LanguageIdentifier::from_str(DEFAULT_LOCALE).unwrap(); - #[test] - fn test_localizer_fallback_to_english() { - let temp_dir = create_test_locales_dir(); - let fr_bundle = create_test_bundle( - &LanguageIdentifier::from_str("fr-FR").unwrap(), - temp_dir.path(), - ) - .unwrap(); - let en_bundle = create_test_bundle( - &LanguageIdentifier::from_str("en-US").unwrap(), - temp_dir.path(), - ) - .unwrap(); - - let localizer = Localizer::new(fr_bundle).with_fallback(en_bundle); + // "fake_util" doesn't exist under src/uu/, so get_locales_dir fails + // and no utility-specific strings are loaded. Common strings ARE + // loaded from the temp uucore locales dir above. + let result = create_bundle(&locale, &locales_dir, "fake_util"); - // This message exists in French - let result1 = localizer.format("greeting", None); - assert_eq!(result1, "Bonjour, le monde!"); + assert!( + result.is_err(), + "create_bundle should return Err when the utility locale file is missing, \ + even if common strings were loaded" + ); - // This message only exists in English, should fallback - let result2 = localizer.format("missing-in-other", None); - assert_eq!(result2, "This message only exists in English"); + match result { + Err(LocalizationError::LocalesDirNotFound(msg)) => { + assert!( + msg.contains("fake_util"), + "error message should mention the utility name, got: {msg}" + ); + } + Err(other) => panic!("Expected LocalesDirNotFound error, got: {other:?}"), + Ok(_) => panic!("Expected error, but create_bundle returned Ok"), + } + } } - #[test] - fn test_localizer_format_message_not_found() { - let temp_dir = create_test_locales_dir(); - let en_bundle = create_test_bundle( - &LanguageIdentifier::from_str("en-US").unwrap(), - temp_dir.path(), - ) - .unwrap(); + rusty_fork_test! { + #[test] + fn test_localizer_format_primary_bundle() { + let temp_dir = create_test_locales_dir(); + let en_bundle: FluentBundle<&'static FluentResource> = create_test_bundle( + &LanguageIdentifier::from_str("en-US").unwrap(), + temp_dir.path(), + ) + .unwrap(); - let localizer = Localizer::new(en_bundle); - let result = localizer.format("nonexistent-message", None); - assert_eq!(result, "nonexistent-message"); + let localizer = Localizer::new(en_bundle); + let result = localizer.format("greeting", None); + assert_eq!(result, "Hello, world!"); + } } - #[test] - fn test_init_localization_english_only() { - // Run in a separate thread to avoid conflicts with other tests - std::thread::spawn(|| { + rusty_fork_test! { + #[test] + fn test_localizer_format_with_args() { + use fluent::FluentArgs; let temp_dir = create_test_locales_dir(); - let locale = LanguageIdentifier::from_str("en-US").unwrap(); + let en_bundle = create_test_bundle( + &LanguageIdentifier::from_str("en-US").unwrap(), + temp_dir.path(), + ) + .unwrap(); - let result = init_test_localization(&locale, temp_dir.path()); - assert!(result.is_ok()); + let localizer = Localizer::new(en_bundle); + let mut args = FluentArgs::new(); + args.set("name", "Alice"); - // Test that we can get messages - let message = get_message("greeting"); - assert_eq!(message, "Hello, world!"); - }) - .join() - .unwrap(); + let result = localizer.format("welcome", Some(&args)); + assert_eq!(result, "Welcome, Alice!"); + } } - #[test] - fn test_init_localization_with_fallback() { - std::thread::spawn(|| { + rusty_fork_test! { + #[test] + fn test_localizer_fallback_to_english() { let temp_dir = create_test_locales_dir(); - let locale = LanguageIdentifier::from_str("fr-FR").unwrap(); + let fr_bundle = create_test_bundle( + &LanguageIdentifier::from_str("fr-FR").unwrap(), + temp_dir.path(), + ) + .unwrap(); + let en_bundle = create_test_bundle( + &LanguageIdentifier::from_str("en-US").unwrap(), + temp_dir.path(), + ) + .unwrap(); - let result = init_test_localization(&locale, temp_dir.path()); - assert!(result.is_ok()); + let localizer = Localizer::new(fr_bundle).with_fallback(en_bundle); - // Test French message - let message1 = get_message("greeting"); - assert_eq!(message1, "Bonjour, le monde!"); + // This message exists in French + let result1 = localizer.format("greeting", None); + assert_eq!(result1, "Bonjour, le monde!"); - // Test fallback to English - let message2 = get_message("missing-in-other"); - assert_eq!(message2, "This message only exists in English"); - }) - .join() - .unwrap(); + // This message only exists in English, should fallback + let result2 = localizer.format("missing-in-other", None); + assert_eq!(result2, "This message only exists in English"); + } } - #[test] - fn test_init_localization_invalid_locale_falls_back_to_english() { - std::thread::spawn(|| { + rusty_fork_test! { + #[test] + fn test_localizer_format_message_not_found() { let temp_dir = create_test_locales_dir(); - let locale = LanguageIdentifier::from_str("de-DE").unwrap(); // No German file - - let result = init_test_localization(&locale, temp_dir.path()); - assert!(result.is_ok()); + let en_bundle = create_test_bundle( + &LanguageIdentifier::from_str("en-US").unwrap(), + temp_dir.path(), + ) + .unwrap(); - // Should use English as primary since German failed to load - let message = get_message("greeting"); - assert_eq!(message, "Hello, world!"); - }) - .join() - .unwrap(); + let localizer = Localizer::new(en_bundle); + let result = localizer.format("nonexistent-message", None); + assert_eq!(result, "nonexistent-message"); + } } - #[test] - fn test_init_localization_already_initialized() { - std::thread::spawn(|| { - let temp_dir = create_test_locales_dir(); - let locale = LanguageIdentifier::from_str("en-US").unwrap(); + rusty_fork_test! { + #[test] + fn test_init_localization_english_only() { + // Run in a separate thread to avoid conflicts with other tests + std::thread::spawn(|| { + let temp_dir = create_test_locales_dir(); + let locale = LanguageIdentifier::from_str("en-US").unwrap(); + + let result = init_test_localization(&locale, temp_dir.path()); + assert!(result.is_ok()); + + // Test that we can get messages + let message = get_message("greeting"); + assert_eq!(message, "Hello, world!"); + }) + .join() + .unwrap(); + } + } - // Initialize once - let result1 = init_test_localization(&locale, temp_dir.path()); - assert!(result1.is_ok()); + rusty_fork_test! { + #[test] + fn test_init_localization_with_fallback() { + std::thread::spawn(|| { + let temp_dir = create_test_locales_dir(); + let locale = LanguageIdentifier::from_str("fr-FR").unwrap(); + + let result = init_test_localization(&locale, temp_dir.path()); + assert!(result.is_ok()); + + // Test French message + let message1 = get_message("greeting"); + assert_eq!(message1, "Bonjour, le monde!"); + + // Test fallback to English + let message2 = get_message("missing-in-other"); + assert_eq!(message2, "This message only exists in English"); + }) + .join() + .unwrap(); + } + } - // Try to initialize again - should fail - let result2 = init_test_localization(&locale, temp_dir.path()); - assert!(result2.is_err()); + rusty_fork_test! { + #[test] + fn test_init_localization_invalid_locale_falls_back_to_english() { + std::thread::spawn(|| { + let temp_dir = create_test_locales_dir(); + let locale = LanguageIdentifier::from_str("de-DE").unwrap(); // No German file + + let result = init_test_localization(&locale, temp_dir.path()); + assert!(result.is_ok()); + + // Should use English as primary since German failed to load + let message = get_message("greeting"); + assert_eq!(message, "Hello, world!"); + }) + .join() + .unwrap(); + } + } - match result2 { - Err(LocalizationError::Bundle(msg)) => { - assert!(msg.contains("already initialized")); + rusty_fork_test! { + #[test] + fn test_init_localization_already_initialized() { + std::thread::spawn(|| { + let temp_dir = create_test_locales_dir(); + let locale = LanguageIdentifier::from_str("en-US").unwrap(); + + // Initialize once + let result1 = init_test_localization(&locale, temp_dir.path()); + assert!(result1.is_ok()); + + // Try to initialize again - should fail + let result2 = init_test_localization(&locale, temp_dir.path()); + assert!(result2.is_err()); + + match result2 { + Err(LocalizationError::Bundle(msg)) => { + assert!(msg.contains("already initialized")); + } + _ => panic!("Expected Bundle error"), } - _ => panic!("Expected Bundle error"), - } - }) - .join() - .unwrap(); + }) + .join() + .unwrap(); + } } - #[test] - fn test_get_message() { - std::thread::spawn(|| { - let temp_dir = create_test_locales_dir(); - let locale = LanguageIdentifier::from_str("fr-FR").unwrap(); + rusty_fork_test! { + #[test] + fn test_get_message() { + std::thread::spawn(|| { + let temp_dir = create_test_locales_dir(); + let locale = LanguageIdentifier::from_str("fr-FR").unwrap(); - init_test_localization(&locale, temp_dir.path()).unwrap(); + init_test_localization(&locale, temp_dir.path()).unwrap(); - let message = get_message("greeting"); - assert_eq!(message, "Bonjour, le monde!"); - }) - .join() - .unwrap(); + let message = get_message("greeting"); + assert_eq!(message, "Bonjour, le monde!"); + }) + .join() + .unwrap(); + } } - #[test] - fn test_get_message_with_args() { - use fluent::FluentArgs; - std::thread::spawn(|| { - let temp_dir = create_test_locales_dir(); - let locale = LanguageIdentifier::from_str("en-US").unwrap(); + rusty_fork_test! { + #[test] + fn test_get_message_with_args() { + use fluent::FluentArgs; + std::thread::spawn(|| { + let temp_dir = create_test_locales_dir(); + let locale = LanguageIdentifier::from_str("en-US").unwrap(); - init_test_localization(&locale, temp_dir.path()).unwrap(); + init_test_localization(&locale, temp_dir.path()).unwrap(); - let mut args = FluentArgs::new(); - args.set("name".to_string(), "Bob".to_string()); + let mut args = FluentArgs::new(); + args.set("name".to_string(), "Bob".to_string()); - let message = get_message_with_args("welcome", args); - assert_eq!(message, "Welcome, Bob!"); - }) - .join() - .unwrap(); + let message = get_message_with_args("welcome", args); + assert_eq!(message, "Welcome, Bob!"); + }) + .join() + .unwrap(); + } } - #[test] - fn test_get_message_with_args_pluralization() { - use fluent::FluentArgs; - std::thread::spawn(|| { - let temp_dir = create_test_locales_dir(); - let locale = LanguageIdentifier::from_str("en-US").unwrap(); - - init_test_localization(&locale, temp_dir.path()).unwrap(); - - // Test singular - let mut args1 = FluentArgs::new(); - args1.set("count", 1); - let message1 = get_message_with_args("count-items", args1); - assert_eq!(message1, "You have 1 item"); - - // Test plural - let mut args2 = FluentArgs::new(); - args2.set("count", 5); - let message2 = get_message_with_args("count-items", args2); - assert_eq!(message2, "You have 5 items"); - }) - .join() - .unwrap(); + rusty_fork_test! { + #[test] + fn test_get_message_with_args_pluralization() { + use fluent::FluentArgs; + std::thread::spawn(|| { + let temp_dir = create_test_locales_dir(); + let locale = LanguageIdentifier::from_str("en-US").unwrap(); + + init_test_localization(&locale, temp_dir.path()).unwrap(); + + // Test singular + let mut args1 = FluentArgs::new(); + args1.set("count", 1); + let message1 = get_message_with_args("count-items", args1); + assert_eq!(message1, "You have 1 item"); + + // Test plural + let mut args2 = FluentArgs::new(); + args2.set("count", 5); + let message2 = get_message_with_args("count-items", args2); + assert_eq!(message2, "You have 5 items"); + }) + .join() + .unwrap(); + } } - #[test] - fn test_thread_local_isolation() { - use std::thread; - - let temp_dir = create_test_locales_dir(); - - // Initialize in main thread with French - let temp_path_main = temp_dir.path().to_path_buf(); - let main_handle = thread::spawn(move || { - let locale = LanguageIdentifier::from_str("fr-FR").unwrap(); - init_test_localization(&locale, &temp_path_main).unwrap(); - let main_message = get_message("greeting"); - assert_eq!(main_message, "Bonjour, le monde!"); - }); - main_handle.join().unwrap(); - - // Test in a different thread - should not be initialized - let temp_path = temp_dir.path().to_path_buf(); - let handle = thread::spawn(move || { - // This thread should have its own uninitialized LOCALIZER - let thread_message = get_message("greeting"); - assert_eq!(thread_message, "greeting"); // Returns ID since not initialized - - // Initialize in this thread with English - let en_locale = LanguageIdentifier::from_str("en-US").unwrap(); - init_test_localization(&en_locale, &temp_path).unwrap(); - let thread_message_after_init = get_message("greeting"); - assert_eq!(thread_message_after_init, "Hello, world!"); - }); - - handle.join().unwrap(); - - // Test another thread to verify French doesn't persist across threads - let final_handle = thread::spawn(move || { - // Should be uninitialized again - let final_message = get_message("greeting"); - assert_eq!(final_message, "greeting"); - }); - final_handle.join().unwrap(); - } + rusty_fork_test! { + #[test] + fn test_thread_local_isolation() { + use std::thread; - #[test] - fn test_japanese_localization() { - use fluent::FluentArgs; - std::thread::spawn(|| { let temp_dir = create_test_locales_dir(); - let locale = LanguageIdentifier::from_str("ja-JP").unwrap(); - let result = init_test_localization(&locale, temp_dir.path()); - assert!(result.is_ok()); - - // Test Japanese greeting - let message = get_message("greeting"); - assert_eq!(message, "こんにちは、世界!"); - - // Test Japanese with arguments - let mut args = FluentArgs::new(); - args.set("name".to_string(), "田中".to_string()); - let welcome = get_message_with_args("welcome", args); - assert_eq!(welcome, "ようこそ、田中さん!"); - - // Test Japanese count (no pluralization) - let mut count_args = FluentArgs::new(); - count_args.set("count".to_string(), "5".to_string()); - let count_message = get_message_with_args("count-items", count_args); - assert_eq!(count_message, "5個のアイテムがあります"); - }) - .join() - .unwrap(); + // Initialize in main thread with French + let temp_path_main = temp_dir.path().to_path_buf(); + let main_handle = thread::spawn(move || { + let locale = LanguageIdentifier::from_str("fr-FR").unwrap(); + init_test_localization(&locale, &temp_path_main).unwrap(); + let main_message = get_message("greeting"); + assert_eq!(main_message, "Bonjour, le monde!"); + }); + main_handle.join().unwrap(); + + // Test in a different thread - should not be initialized + let temp_path = temp_dir.path().to_path_buf(); + let handle = thread::spawn(move || { + // This thread should have its own uninitialized LOCALIZER + let thread_message = get_message("greeting"); + assert_eq!(thread_message, "greeting"); // Returns ID since not initialized + + // Initialize in this thread with English + let en_locale = LanguageIdentifier::from_str("en-US").unwrap(); + init_test_localization(&en_locale, &temp_path).unwrap(); + let thread_message_after_init = get_message("greeting"); + assert_eq!(thread_message_after_init, "Hello, world!"); + }); + + handle.join().unwrap(); + + // Test another thread to verify French doesn't persist across threads + let final_handle = thread::spawn(move || { + // Should be uninitialized again + let final_message = get_message("greeting"); + assert_eq!(final_message, "greeting"); + }); + final_handle.join().unwrap(); + } } - #[test] - fn test_arabic_localization() { - use fluent::FluentArgs; - std::thread::spawn(|| { - let temp_dir = create_test_locales_dir(); - let locale = LanguageIdentifier::from_str("ar-SA").unwrap(); - - let result = init_test_localization(&locale, temp_dir.path()); - assert!(result.is_ok()); - - // Test Arabic greeting (RTL text) - let message = get_message("greeting"); - assert_eq!(message, "أهلاً بالعالم!"); - - // Test Arabic with arguments - let mut args = FluentArgs::new(); - args.set("name", "أحمد".to_string()); - let welcome = get_message_with_args("welcome", args); - assert_eq!(welcome, "أهلاً وسهلاً، أحمد!"); - - // Test Arabic pluralization (zero case) - let mut args_zero = FluentArgs::new(); - args_zero.set("count", 0); - let message_zero = get_message_with_args("count-items", args_zero); - assert_eq!(message_zero, "لديك لا عناصر"); - - // Test Arabic pluralization (one case) - let mut args_one = FluentArgs::new(); - args_one.set("count", 1); - let message_one = get_message_with_args("count-items", args_one); - assert_eq!(message_one, "لديك عنصر واحد"); - - // Test Arabic pluralization (two case) - let mut args_two = FluentArgs::new(); - args_two.set("count", 2); - let message_two = get_message_with_args("count-items", args_two); - assert_eq!(message_two, "لديك عنصران"); - - // Test Arabic pluralization (few case - 3-10) - let mut args_few = FluentArgs::new(); - args_few.set("count", 5); - let message_few = get_message_with_args("count-items", args_few); - assert_eq!(message_few, "لديك 5 عناصر"); - - // Test Arabic pluralization (other case - 11+) - let mut args_many = FluentArgs::new(); - args_many.set("count", 15); - let message_many = get_message_with_args("count-items", args_many); - assert_eq!(message_many, "لديك 15 عنصر"); - }) - .join() - .unwrap(); + rusty_fork_test! { + #[test] + fn test_japanese_localization() { + use fluent::FluentArgs; + std::thread::spawn(|| { + let temp_dir = create_test_locales_dir(); + let locale = LanguageIdentifier::from_str("ja-JP").unwrap(); + + let result = init_test_localization(&locale, temp_dir.path()); + assert!(result.is_ok()); + + // Test Japanese greeting + let message = get_message("greeting"); + assert_eq!(message, "こんにちは、世界!"); + + // Test Japanese with arguments + let mut args = FluentArgs::new(); + args.set("name".to_string(), "田中".to_string()); + let welcome = get_message_with_args("welcome", args); + assert_eq!(welcome, "ようこそ、田中さん!"); + + // Test Japanese count (no pluralization) + let mut count_args = FluentArgs::new(); + count_args.set("count".to_string(), "5".to_string()); + let count_message = get_message_with_args("count-items", count_args); + assert_eq!(count_message, "5個のアイテムがあります"); + }) + .join() + .unwrap(); + } } - #[test] - fn test_arabic_localization_with_macro() { - std::thread::spawn(|| { - let temp_dir = create_test_locales_dir(); - let locale = LanguageIdentifier::from_str("ar-SA").unwrap(); - - let result = init_test_localization(&locale, temp_dir.path()); - assert!(result.is_ok()); - - // Test Arabic greeting (RTL text) - let message = translate!("greeting"); - assert_eq!(message, "أهلاً بالعالم!"); - - // Test Arabic with arguments - let welcome = translate!("welcome", "name" => "أحمد"); - assert_eq!(welcome, "أهلاً وسهلاً، أحمد!"); - - // Test Arabic pluralization (zero case) - let message_zero = translate!("count-items", "count" => 0); - assert_eq!(message_zero, "لديك لا عناصر"); - - // Test Arabic pluralization (one case) - let message_one = translate!("count-items", "count" => 1); - assert_eq!(message_one, "لديك عنصر واحد"); - - // Test Arabic pluralization (two case) - let message_two = translate!("count-items", "count" => 2); - assert_eq!(message_two, "لديك عنصران"); - - // Test Arabic pluralization (few case - 3-10) - let message_few = translate!("count-items", "count" => 5); - assert_eq!(message_few, "لديك 5 عناصر"); - - // Test Arabic pluralization (other case - 11+) - let message_many = translate!("count-items", "count" => 15); - assert_eq!(message_many, "لديك 15 عنصر"); - }) - .join() - .unwrap(); + rusty_fork_test! { + #[test] + fn test_arabic_localization() { + use fluent::FluentArgs; + std::thread::spawn(|| { + let temp_dir = create_test_locales_dir(); + let locale = LanguageIdentifier::from_str("ar-SA").unwrap(); + + let result = init_test_localization(&locale, temp_dir.path()); + assert!(result.is_ok()); + + // Test Arabic greeting (RTL text) + let message = get_message("greeting"); + assert_eq!(message, "أهلاً بالعالم!"); + + // Test Arabic with arguments + let mut args = FluentArgs::new(); + args.set("name", "أحمد".to_string()); + let welcome = get_message_with_args("welcome", args); + assert_eq!(welcome, "أهلاً وسهلاً، أحمد!"); + + // Test Arabic pluralization (zero case) + let mut args_zero = FluentArgs::new(); + args_zero.set("count", 0); + let message_zero = get_message_with_args("count-items", args_zero); + assert_eq!(message_zero, "لديك لا عناصر"); + + // Test Arabic pluralization (one case) + let mut args_one = FluentArgs::new(); + args_one.set("count", 1); + let message_one = get_message_with_args("count-items", args_one); + assert_eq!(message_one, "لديك عنصر واحد"); + + // Test Arabic pluralization (two case) + let mut args_two = FluentArgs::new(); + args_two.set("count", 2); + let message_two = get_message_with_args("count-items", args_two); + assert_eq!(message_two, "لديك عنصران"); + + // Test Arabic pluralization (few case - 3-10) + let mut args_few = FluentArgs::new(); + args_few.set("count", 5); + let message_few = get_message_with_args("count-items", args_few); + assert_eq!(message_few, "لديك 5 عناصر"); + + // Test Arabic pluralization (other case - 11+) + let mut args_many = FluentArgs::new(); + args_many.set("count", 15); + let message_many = get_message_with_args("count-items", args_many); + assert_eq!(message_many, "لديك 15 عنصر"); + }) + .join() + .unwrap(); + } } - #[test] - fn test_mixed_script_fallback() { - std::thread::spawn(|| { - let temp_dir = create_test_locales_dir(); - let locale = LanguageIdentifier::from_str("ar-SA").unwrap(); - - let result = init_test_localization(&locale, temp_dir.path()); - assert!(result.is_ok()); - - // Test Arabic message exists - let arabic_message = get_message("greeting"); - assert_eq!(arabic_message, "أهلاً بالعالم!"); - - // Test fallback to English for missing message - let fallback_message = get_message("missing-in-other"); - assert_eq!(fallback_message, "This message only exists in English"); - }) - .join() - .unwrap(); + rusty_fork_test! { + #[test] + fn test_arabic_localization_with_macro() { + std::thread::spawn(|| { + let temp_dir = create_test_locales_dir(); + let locale = LanguageIdentifier::from_str("ar-SA").unwrap(); + + let result = init_test_localization(&locale, temp_dir.path()); + assert!(result.is_ok()); + + // Test Arabic greeting (RTL text) + let message = translate!("greeting"); + assert_eq!(message, "أهلاً بالعالم!"); + + // Test Arabic with arguments + let welcome = translate!("welcome", "name" => "أحمد"); + assert_eq!(welcome, "أهلاً وسهلاً، أحمد!"); + + // Test Arabic pluralization (zero case) + let message_zero = translate!("count-items", "count" => 0); + assert_eq!(message_zero, "لديك لا عناصر"); + + // Test Arabic pluralization (one case) + let message_one = translate!("count-items", "count" => 1); + assert_eq!(message_one, "لديك عنصر واحد"); + + // Test Arabic pluralization (two case) + let message_two = translate!("count-items", "count" => 2); + assert_eq!(message_two, "لديك عنصران"); + + // Test Arabic pluralization (few case - 3-10) + let message_few = translate!("count-items", "count" => 5); + assert_eq!(message_few, "لديك 5 عناصر"); + + // Test Arabic pluralization (other case - 11+) + let message_many = translate!("count-items", "count" => 15); + assert_eq!(message_many, "لديك 15 عنصر"); + }) + .join() + .unwrap(); + } } - #[test] - fn test_unicode_directional_isolation_disabled() { - use fluent::FluentArgs; - std::thread::spawn(|| { - let temp_dir = create_test_locales_dir(); - let locale = LanguageIdentifier::from_str("ar-SA").unwrap(); - - init_test_localization(&locale, temp_dir.path()).unwrap(); - - // Test that Latin script names are NOT isolated in RTL context - // since we disabled Unicode directional isolation - let mut args = FluentArgs::new(); - args.set("name".to_string(), "John Smith".to_string()); - let message = get_message_with_args("welcome", args); - - // The Latin name should NOT be wrapped in directional isolate characters - assert!(!message.contains("\u{2068}John Smith\u{2069}")); - assert_eq!(message, "أهلاً وسهلاً، John Smith!"); - }) - .join() - .unwrap(); + rusty_fork_test! { + #[test] + fn test_mixed_script_fallback() { + std::thread::spawn(|| { + let temp_dir = create_test_locales_dir(); + let locale = LanguageIdentifier::from_str("ar-SA").unwrap(); + + let result = init_test_localization(&locale, temp_dir.path()); + assert!(result.is_ok()); + + // Test Arabic message exists + let arabic_message = get_message("greeting"); + assert_eq!(arabic_message, "أهلاً بالعالم!"); + + // Test fallback to English for missing message + let fallback_message = get_message("missing-in-other"); + assert_eq!(fallback_message, "This message only exists in English"); + }) + .join() + .unwrap(); + } } - #[test] - fn test_parse_resource_error_includes_snippet() { - let temp_dir = create_test_locales_dir(); - let locale = LanguageIdentifier::from_str("es-ES").unwrap(); - - let result = create_test_bundle(&locale, temp_dir.path()); - assert!(result.is_err()); - - if let Err(LocalizationError::ParseResource { - error: _err, - snippet, - }) = result - { - // The snippet should contain exactly the invalid text from es-ES.ftl - assert!( - snippet.contains("This is { $missing"), - "snippet was `{snippet}` but did not include the invalid text" - ); - } else { - panic!("Expected LocalizationError::ParseResource with snippet"); + rusty_fork_test! { + #[test] + fn test_unicode_directional_isolation_disabled() { + use fluent::FluentArgs; + std::thread::spawn(|| { + let temp_dir = create_test_locales_dir(); + let locale = LanguageIdentifier::from_str("ar-SA").unwrap(); + + init_test_localization(&locale, temp_dir.path()).unwrap(); + + // Test that Latin script names are NOT isolated in RTL context + // since we disabled Unicode directional isolation + let mut args = FluentArgs::new(); + args.set("name".to_string(), "John Smith".to_string()); + let message = get_message_with_args("welcome", args); + + // The Latin name should NOT be wrapped in directional isolate characters + assert!(!message.contains("\u{2068}John Smith\u{2069}")); + assert_eq!(message, "أهلاً وسهلاً، John Smith!"); + }) + .join() + .unwrap(); } } - #[test] - fn test_localization_error_from_io_error() { - let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found"); - let loc_error = LocalizationError::from(io_error); + rusty_fork_test! { + #[test] + fn test_parse_resource_error_includes_snippet() { + let temp_dir = create_test_locales_dir(); + let locale = LanguageIdentifier::from_str("es-ES").unwrap(); + + let result = create_test_bundle(&locale, temp_dir.path()); + assert!(result.is_err()); - match loc_error { - LocalizationError::Io { source: _, path } => { - assert_eq!(path, PathBuf::from("")); + if let Err(LocalizationError::ParseResource { + error: _err, + snippet, + }) = result + { + // The snippet should contain exactly the invalid text from es-ES.ftl + assert!( + snippet.contains("This is { $missing"), + "snippet was `{snippet}` but did not include the invalid text" + ); + } else { + panic!("Expected LocalizationError::ParseResource with snippet"); } - _ => panic!("Expected IO error variant"), } } - #[test] - fn test_localization_error_uerror_impl() { - let error = LocalizationError::Bundle("some error".to_string()); - assert_eq!(error.code(), 1); - } + rusty_fork_test! { + #[test] + fn test_localization_error_from_io_error() { + let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found"); + let loc_error = LocalizationError::from(io_error); - #[test] - fn test_get_message_not_initialized() { - std::thread::spawn(|| { - let message = get_message("greeting"); - assert_eq!(message, "greeting"); // Should return the ID itself - }) - .join() - .unwrap(); + match loc_error { + LocalizationError::Io { source: _, path } => { + assert_eq!(path, PathBuf::from("")); + } + _ => panic!("Expected IO error variant"), + } + } } - #[test] - fn test_detect_system_locale_from_lang_env() { - // Test locale parsing logic directly instead of relying on environment variables - // which can have race conditions in multi-threaded test environments - - // Test parsing logic with UTF-8 encoding - let locale_with_encoding = "fr-FR.UTF-8"; - let parsed = locale_with_encoding.split('.').next().unwrap(); - let lang_id = LanguageIdentifier::from_str(parsed).unwrap(); - assert_eq!(lang_id.to_string(), "fr-FR"); - - // Test parsing logic without encoding - let locale_without_encoding = "es-ES"; - let lang_id = LanguageIdentifier::from_str(locale_without_encoding).unwrap(); - assert_eq!(lang_id.to_string(), "es-ES"); - - // Test that DEFAULT_LOCALE is valid - let default_lang_id = LanguageIdentifier::from_str(DEFAULT_LOCALE).unwrap(); - assert_eq!(default_lang_id.to_string(), "en-US"); + rusty_fork_test! { + #[test] + fn test_localization_error_uerror_impl() { + let error = LocalizationError::Bundle("some error".to_string()); + assert_eq!(error.code(), 1); + } } - #[test] - fn test_detect_system_locale_no_lang_env() { - // Save current LANG value - let original_lang = env::var("LANG").ok(); - - // Remove LANG environment variable - unsafe { - env::remove_var("LANG"); + rusty_fork_test! { + #[test] + fn test_get_message_not_initialized() { + std::thread::spawn(|| { + let message = get_message("greeting"); + assert_eq!(message, "greeting"); // Should return the ID itself + }) + .join() + .unwrap(); } + } - let result = detect_system_locale(); - assert!(result.is_ok()); - assert_eq!(result.unwrap().to_string(), "en-US"); - - // Restore original LANG value - if let Some(val) = original_lang { - unsafe { - env::set_var("LANG", val); - } - } else { - {} // Was already unset + rusty_fork_test! { + #[test] + fn test_detect_system_locale_from_lang_env() { + // Test locale parsing logic directly instead of relying on environment variables + // which can have race conditions in multi-threaded test environments + + // Test parsing logic with UTF-8 encoding + let locale_with_encoding = "fr-FR.UTF-8"; + let parsed = locale_with_encoding.split('.').next().unwrap(); + let lang_id = LanguageIdentifier::from_str(parsed).unwrap(); + assert_eq!(lang_id.to_string(), "fr-FR"); + + // Test parsing logic without encoding + let locale_without_encoding = "es-ES"; + let lang_id = LanguageIdentifier::from_str(locale_without_encoding).unwrap(); + assert_eq!(lang_id.to_string(), "es-ES"); + + // Test that DEFAULT_LOCALE is valid + let default_lang_id = LanguageIdentifier::from_str(DEFAULT_LOCALE).unwrap(); + assert_eq!(default_lang_id.to_string(), "en-US"); } } - #[test] - fn test_setup_localization_success() { - std::thread::spawn(|| { + rusty_fork_test! { + #[test] + fn test_detect_system_locale_no_lang_env() { // Save current LANG value let original_lang = env::var("LANG").ok(); + + // Remove LANG environment variable unsafe { - env::set_var("LANG", "en-US.UTF-8"); // Use English since we have embedded resources for "test" + env::remove_var("LANG"); } - let result = setup_localization("test"); + let result = detect_system_locale(); assert!(result.is_ok()); - - // Test that we can get messages (should use embedded English for "test" utility) - let message = get_message("test-about"); - // Since we're using embedded resources, we should get the expected message - assert!(!message.is_empty()); + assert_eq!(result.unwrap().to_string(), "en-US"); // Restore original LANG value if let Some(val) = original_lang { @@ -1460,123 +1522,161 @@ invalid-syntax = This is { $missing env::set_var("LANG", val); } } else { - unsafe { - env::remove_var("LANG"); - } + {} // Was already unset } - }) - .join() - .unwrap(); + } } - #[test] - fn test_setup_localization_falls_back_to_english() { - std::thread::spawn(|| { - // Save current LANG value - let original_lang = env::var("LANG").ok(); - unsafe { - env::set_var("LANG", "de-DE.UTF-8"); // German file doesn't exist, should fallback - } + rusty_fork_test! { + #[test] + fn test_setup_localization_success() { + std::thread::spawn(|| { + // Save current LANG value + let original_lang = env::var("LANG").ok(); + unsafe { + env::set_var("LANG", "en-US.UTF-8"); // Use English since we have embedded resources for "test" + } - let result = setup_localization("test"); - assert!(result.is_ok()); + let result = setup_localization("test"); + assert!(result.is_ok()); - // Should fall back to English embedded resources - let message = get_message("test-about"); - assert!(!message.is_empty()); // Should get something, not just the key + // Test that we can get messages (should use embedded English for "test" utility) + let message = get_message("test-about"); + // Since we're using embedded resources, we should get the expected message + assert!(!message.is_empty()); - // Restore original LANG value - if let Some(val) = original_lang { - unsafe { - env::set_var("LANG", val); + // Restore original LANG value + if let Some(val) = original_lang { + unsafe { + env::set_var("LANG", val); + } + } else { + unsafe { + env::remove_var("LANG"); + } } - } else { + }) + .join() + .unwrap(); + } + } + + rusty_fork_test! { + #[test] + fn test_setup_localization_falls_back_to_english() { + std::thread::spawn(|| { + // Save current LANG value + let original_lang = env::var("LANG").ok(); unsafe { - env::remove_var("LANG"); + env::set_var("LANG", "de-DE.UTF-8"); // German file doesn't exist, should fallback } - } - }) - .join() - .unwrap(); - } - #[test] - fn test_setup_localization_fallback_to_embedded() { - std::thread::spawn(|| { - // Force English locale for this test - unsafe { - env::set_var("LANG", "en-US"); - } + let result = setup_localization("test"); + assert!(result.is_ok()); - // Test with a utility name that has embedded locales - // This should fall back to embedded English when filesystem files aren't found - let result = setup_localization("test"); - if let Err(e) = &result { - eprintln!("Setup localization failed: {e}"); - } - assert!(result.is_ok()); + // Should fall back to English embedded resources + let message = get_message("test-about"); + assert!(!message.is_empty()); // Should get something, not just the key - // Verify we can get messages (using embedded English) - let message = get_message("test-about"); - assert_eq!(message, "Check file types and compare values."); // Should use embedded English - }) - .join() - .unwrap(); + // Restore original LANG value + if let Some(val) = original_lang { + unsafe { + env::set_var("LANG", val); + } + } else { + unsafe { + env::remove_var("LANG"); + } + } + }) + .join() + .unwrap(); + } } - #[test] - fn test_error_display() { - let io_error = LocalizationError::Io { - source: std::io::Error::new(std::io::ErrorKind::NotFound, "File not found"), - path: PathBuf::from("/test/path.ftl"), - }; - let error_string = format!("{io_error}"); - assert!(error_string.contains("I/O error loading")); - assert!(error_string.contains("/test/path.ftl")); + rusty_fork_test! { + #[test] + fn test_setup_localization_fallback_to_embedded() { + std::thread::spawn(|| { + // Force English locale for this test + unsafe { + env::set_var("LANG", "en-US"); + } - let bundle_error = LocalizationError::Bundle("Bundle creation failed".to_string()); - let bundle_string = format!("{bundle_error}"); - assert!(bundle_string.contains("Bundle error: Bundle creation failed")); + // Test with a utility name that has embedded locales + // This should fall back to embedded English when filesystem files aren't found + let result = setup_localization("test"); + if let Err(e) = &result { + eprintln!("Setup localization failed: {e}"); + } + assert!(result.is_ok()); + + // Verify we can get messages (using embedded English) + let message = get_message("test-about"); + assert_eq!(message, "Check file types and compare values."); // Should use embedded English + }) + .join() + .unwrap(); + } } - #[test] - fn test_clap_localization_fallbacks() { - std::thread::spawn(|| { - // Test the scenario where localization isn't properly initialized - // and we need fallbacks for clap error handling - - // First, test when localizer is not initialized - let error_msg = get_message("common-error"); - assert_eq!(error_msg, "common-error"); // Should return key when not initialized - - let tip_msg = get_message("common-tip"); - assert_eq!(tip_msg, "common-tip"); // Should return key when not initialized - - // Now initialize with setup_localization - let result = setup_localization("comm"); - if result.is_err() { - // If setup fails (e.g., no embedded locales for comm), try with a known utility - let _ = setup_localization("test"); - } + rusty_fork_test! { + #[test] + fn test_error_display() { + let io_error = LocalizationError::Io { + source: std::io::Error::new(std::io::ErrorKind::NotFound, "File not found"), + path: PathBuf::from("/test/path.ftl"), + }; + let error_string = format!("{io_error}"); + assert!(error_string.contains("I/O error loading")); + assert!(error_string.contains("/test/path.ftl")); + + let bundle_error = LocalizationError::Bundle("Bundle creation failed".to_string()); + let bundle_string = format!("{bundle_error}"); + assert!(bundle_string.contains("Bundle error: Bundle creation failed")); + } + } + + rusty_fork_test! { + #[test] + fn test_clap_localization_fallbacks() { + std::thread::spawn(|| { + // Test the scenario where localization isn't properly initialized + // and we need fallbacks for clap error handling + + // First, test when localizer is not initialized + let error_msg = get_message("common-error"); + assert_eq!(error_msg, "common-error"); // Should return key when not initialized + + let tip_msg = get_message("common-tip"); + assert_eq!(tip_msg, "common-tip"); // Should return key when not initialized + + // Now initialize with setup_localization + let result = setup_localization("comm"); + if result.is_err() { + // If setup fails (e.g., no embedded locales for comm), try with a known utility + let _ = setup_localization("test"); + } - // Test that common strings are available after initialization - let error_after_init = get_message("common-error"); - // Should either be translated or return the key (but not panic) - assert!(!error_after_init.is_empty()); + // Test that common strings are available after initialization + let error_after_init = get_message("common-error"); + // Should either be translated or return the key (but not panic) + assert!(!error_after_init.is_empty()); - let tip_after_init = get_message("common-tip"); - assert!(!tip_after_init.is_empty()); + let tip_after_init = get_message("common-tip"); + assert!(!tip_after_init.is_empty()); - // Test that clap error keys work with fallbacks - let unknown_arg_key = get_message("clap-error-unexpected-argument"); - assert!(!unknown_arg_key.is_empty()); + // Test that clap error keys work with fallbacks + let unknown_arg_key = get_message("clap-error-unexpected-argument"); + assert!(!unknown_arg_key.is_empty()); - // Test usage key fallback - let usage_key = get_message("common-usage"); - assert!(!usage_key.is_empty()); - }) - .join() - .unwrap(); + // Test usage key fallback + let usage_key = get_message("common-usage"); + assert!(!usage_key.is_empty()); + }) + .join() + .unwrap(); + } } } diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index 9ad4ef75c4d..373373fc06d 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -4157,7 +4157,7 @@ fn test_copy_dir_preserve_permissions_inaccessible_file() { // V V V V ucmd.args(&["-p", "-R", "d1", "d2"]) .fails_with_code(1) - .stderr_only("cp: cannot open 'd1/f' for reading: permission denied\n"); + .stderr_only("cp: cannot open 'd1/f' for reading: Permission denied\n"); assert!(at.dir_exists("d2")); assert!(!at.file_exists("d2/f"));