From 0bf31e236ac606821cc5bd83f2aeac2d7be5f22d Mon Sep 17 00:00:00 2001 From: Christopher Dryden Date: Fri, 13 Feb 2026 11:42:34 +0000 Subject: [PATCH 1/3] uucore: remove redundant setup_localization call in help template --- src/uucore/src/lib/lib.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/uucore/src/lib/lib.rs b/src/uucore/src/lib/lib.rs index 5b89829288e..295c5d636dd 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -287,9 +287,6 @@ pub fn localized_help_template_with_colors( ) -> clap::builder::StyledStr { use std::fmt::Write; - // Ensure localization is initialized for this utility - let _ = locale::setup_localization(util_name); - // Get the localized "Usage" label let usage_label = crate::locale::translate!("common-usage"); From c7f195de15958c1d5373d9f6dda4f772c82dc1a1 Mon Sep 17 00:00:00 2001 From: Christopher Dryden Date: Fri, 13 Feb 2026 13:47:27 +0000 Subject: [PATCH 2/3] uucore: defer locale loading until first translated message is needed --- src/uu/factor/src/factor.rs | 9 +- src/uu/shuf/src/shuf.rs | 14 +- src/uucore/src/lib/lib.rs | 9 +- src/uucore/src/lib/mods/clap_localization.rs | 49 ++++++- src/uucore/src/lib/mods/locale.rs | 137 +++++++------------ 5 files changed, 99 insertions(+), 119 deletions(-) diff --git a/src/uu/factor/src/factor.rs b/src/uu/factor/src/factor.rs index 898679893c9..08537c98231 100644 --- a/src/uu/factor/src/factor.rs +++ b/src/uu/factor/src/factor.rs @@ -15,7 +15,7 @@ use num_traits::FromPrimitive; use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError, set_exit_code}; use uucore::translate; -use uucore::{format_usage, show_error, show_warning}; +use uucore::{show_error, show_warning}; mod options { pub static EXPONENTS: &str = "exponents"; @@ -148,7 +148,7 @@ fn write_result_big_uint( #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?; + let matches = uucore::clap_localization::parse_deferred(uu_app, args)?; // If matches find --exponents flag than variable print_exponents is true and p^e output format will be used. let print_exponents = matches.get_flag(options::EXPONENTS); @@ -190,9 +190,6 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) - .about(translate!("factor-about")) - .override_usage(format_usage(&translate!("factor-usage"))) .infer_long_args(true) .disable_help_flag(true) .args_override_self(true) @@ -201,13 +198,11 @@ pub fn uu_app() -> Command { Arg::new(options::EXPONENTS) .short('h') .long(options::EXPONENTS) - .help(translate!("factor-help-exponents")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::HELP) .long(options::HELP) - .help(translate!("factor-help-help")) .action(ArgAction::Help), ) } diff --git a/src/uu/shuf/src/shuf.rs b/src/uu/shuf/src/shuf.rs index c7e5689ab40..60d0cb91034 100644 --- a/src/uu/shuf/src/shuf.rs +++ b/src/uu/shuf/src/shuf.rs @@ -21,7 +21,6 @@ use rand::{ use uucore::display::{OsWrite, Quotable}; use uucore::error::{FromIo, UResult, USimpleError, UUsageError}; -use uucore::format_usage; use uucore::translate; mod compat_random_source; @@ -68,7 +67,7 @@ mod options { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?; + let matches = uucore::clap_localization::parse_deferred(uu_app, args)?; let mode = if matches.get_flag(options::ECHO) { Mode::Echo( @@ -173,16 +172,12 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uu_app() -> Command { Command::new(uucore::util_name()) - .about(translate!("shuf-about")) .version(uucore::crate_version!()) - .help_template(uucore::localized_help_template(uucore::util_name())) - .override_usage(format_usage(&translate!("shuf-usage"))) .infer_long_args(true) .arg( Arg::new(options::ECHO) .short('e') .long(options::ECHO) - .help(translate!("shuf-help-echo")) .action(ArgAction::SetTrue) .overrides_with(options::ECHO) .conflicts_with(options::INPUT_RANGE), @@ -192,7 +187,6 @@ pub fn uu_app() -> Command { .short('i') .long(options::INPUT_RANGE) .value_name("LO-HI") - .help(translate!("shuf-help-input-range")) .value_parser(parse_range) .conflicts_with(options::FILE_OR_ARGS), ) @@ -202,7 +196,6 @@ pub fn uu_app() -> Command { .long(options::HEAD_COUNT) .value_name("COUNT") .action(ArgAction::Append) - .help(translate!("shuf-help-head-count")) .value_parser(u64::from_str), ) .arg( @@ -210,7 +203,6 @@ pub fn uu_app() -> Command { .short('o') .long(options::OUTPUT) .value_name("FILE") - .help(translate!("shuf-help-output")) .value_parser(ValueParser::path_buf()) .value_hint(clap::ValueHint::FilePath), ) @@ -218,7 +210,6 @@ pub fn uu_app() -> Command { Arg::new(options::RANDOM_SEED) .long(options::RANDOM_SEED) .value_name("STRING") - .help(translate!("shuf-help-random-seed")) .value_parser(ValueParser::string()) .value_hint(clap::ValueHint::Other) .conflicts_with(options::RANDOM_SOURCE), @@ -227,7 +218,6 @@ pub fn uu_app() -> Command { Arg::new(options::RANDOM_SOURCE) .long(options::RANDOM_SOURCE) .value_name("FILE") - .help(translate!("shuf-help-random-source")) .value_parser(ValueParser::path_buf()) .value_hint(clap::ValueHint::FilePath), ) @@ -235,7 +225,6 @@ pub fn uu_app() -> Command { Arg::new(options::REPEAT) .short('r') .long(options::REPEAT) - .help(translate!("shuf-help-repeat")) .action(ArgAction::SetTrue) .overrides_with(options::REPEAT), ) @@ -243,7 +232,6 @@ pub fn uu_app() -> Command { Arg::new(options::ZERO_TERMINATED) .short('z') .long(options::ZERO_TERMINATED) - .help(translate!("shuf-help-zero-terminated")) .action(ArgAction::SetTrue) .overrides_with(options::ZERO_TERMINATED), ) diff --git a/src/uucore/src/lib/lib.rs b/src/uucore/src/lib/lib.rs index 295c5d636dd..f25be4d213f 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -263,7 +263,7 @@ pub fn format_usage(s: &str) -> String { /// let app = Command::new("myutil") /// .help_template(localized_help_template("myutil")); /// ``` -pub fn localized_help_template(util_name: &str) -> clap::builder::StyledStr { +pub fn localized_help_template(_util_name: &str) -> clap::builder::StyledStr { use std::io::IsTerminal; // Determine if colors should be enabled - same logic as configure_localized_command @@ -276,15 +276,12 @@ pub fn localized_help_template(util_name: &str) -> clap::builder::StyledStr { && std::env::var("TERM").unwrap_or_default() != "dumb" }; - localized_help_template_with_colors(util_name, colors_enabled) + localized_help_template_with_colors(colors_enabled) } /// Create a localized help template with explicit color control /// This ensures color detection consistency between clap and our template -pub fn localized_help_template_with_colors( - util_name: &str, - colors_enabled: bool, -) -> clap::builder::StyledStr { +pub fn localized_help_template_with_colors(colors_enabled: bool) -> clap::builder::StyledStr { use std::fmt::Write; // Get the localized "Usage" label diff --git a/src/uucore/src/lib/mods/clap_localization.rs b/src/uucore/src/lib/mods/clap_localization.rs index cfc30ab22fd..ad1c044a8dd 100644 --- a/src/uucore/src/lib/mods/clap_localization.rs +++ b/src/uucore/src/lib/mods/clap_localization.rs @@ -516,13 +516,54 @@ pub fn configure_localized_command(mut cmd: Command) -> Command { // For help output (stdout), we check stdout TTY status let colors_enabled = should_use_color_for_stream(&std::io::stdout()); - cmd = cmd.help_template(crate::localized_help_template_with_colors( - crate::util_name(), - colors_enabled, - )); + cmd = cmd.help_template(crate::localized_help_template_with_colors(colors_enabled)); cmd } +/// Apply localized about, usage, help template, and per-arg help to a +/// `Command` using the naming convention `{name}-about`, `{name}-usage`, +/// `{name}-help-{arg_id}`. +pub fn localize_command(mut cmd: Command) -> Command { + let name = cmd.get_name().to_string(); + + cmd = cmd + .about(crate::locale::get_message(&format!("{name}-about"))) + .override_usage(crate::format_usage(&crate::locale::get_message(&format!( + "{name}-usage" + )))) + .help_template(crate::localized_help_template(&name)); + + let arg_ids: Vec = cmd + .get_arguments() + .map(|a| a.get_id().to_string()) + .collect(); + + for arg_id in arg_ids { + let help_key = format!("{name}-help-{arg_id}"); + let help_text = crate::locale::get_message(&help_key); + if help_text != help_key { + cmd = cmd.mut_arg(&arg_id, |a| a.help(help_text)); + } + } + + cmd +} + +/// Parse arguments with deferred translation loading. On the happy path, +/// no FTL files are loaded. `app_fn` should build a bare `Command` without +/// `translate!()` calls; translations are applied via `localize_command` +/// only when help or error output is needed. +pub fn parse_deferred(app_fn: F, args: impl crate::Args) -> UResult +where + F: Fn() -> Command, +{ + let args: Vec = args.collect(); + match app_fn().try_get_matches_from(args.iter().map(Clone::clone)) { + Ok(m) => Ok(m), + Err(_) => handle_clap_result(localize_command(app_fn()), args), + } +} + /* spell-checker: disable */ #[cfg(test)] mod tests { diff --git a/src/uucore/src/lib/mods/locale.rs b/src/uucore/src/lib/mods/locale.rs index b670f897620..39a49b3d3c0 100644 --- a/src/uucore/src/lib/mods/locale.rs +++ b/src/uucore/src/lib/mods/locale.rs @@ -108,6 +108,7 @@ impl Localizer { // Global localizer stored in thread-local OnceLock thread_local! { + static UTIL_NAME: OnceLock = const { OnceLock::new() }; static LOCALIZER: OnceLock = const { OnceLock::new() }; } @@ -181,41 +182,34 @@ fn create_bundle( ))) } } - -/// Initialize localization with common strings in addition to utility-specific strings -fn init_localization( - locale: &LanguageIdentifier, - locales_dir: &Path, - util_name: &str, -) -> Result<(), LocalizationError> { +fn create_localizer_from_util_name(util_name: &str) -> Localizer { let default_locale = LanguageIdentifier::from_str(DEFAULT_LOCALE) .expect("Default locale should always be valid"); + let locale = detect_system_locale().unwrap_or_else(|_| default_locale.clone()); - // Try to create a bundle that combines common and utility-specific strings - let english_bundle = create_bundle(&default_locale, locales_dir, util_name).or_else(|_| { - // Fallback to embedded utility-specific and common strings - create_english_bundle_from_embedded(&default_locale, util_name) - })?; + if let Ok(locales_dir) = get_locales_dir(util_name) { + let english_bundle = create_bundle(&default_locale, &locales_dir, util_name) + .or_else(|_| create_english_bundle_from_embedded(&default_locale, util_name)); - let loc = if locale == &default_locale { - // If requesting English, just use English as primary (no fallback needed) - Localizer::new(english_bundle) + match english_bundle { + Ok(eb) => { + if locale == default_locale { + Localizer::new(eb) + } else { + match create_bundle(&locale, &locales_dir, util_name) { + Ok(primary) => Localizer::new(primary).with_fallback(eb), + Err(_) => Localizer::new(eb), + } + } + } + Err(_) => Localizer::new(FluentBundle::new(vec![default_locale])), + } } else { - // Try to load the requested locale with common strings - if let Ok(primary_bundle) = create_bundle(locale, locales_dir, util_name) { - // Successfully loaded requested locale, load English as fallback - Localizer::new(primary_bundle).with_fallback(english_bundle) - } else { - // Failed to load requested locale, just use English as primary - Localizer::new(english_bundle) + match create_english_bundle_from_embedded(&default_locale, util_name) { + Ok(eb) => Localizer::new(eb), + Err(_) => Localizer::new(FluentBundle::new(vec![default_locale])), } - }; - - LOCALIZER.with(|lock| { - lock.set(loc) - .map_err(|_| LocalizationError::Bundle("Localizer already initialized".into())) - })?; - Ok(()) + } } /// Helper function to parse FluentResource from content string @@ -288,8 +282,24 @@ fn create_english_bundle_from_embedded( fn get_message_internal(id: &str, args: Option) -> String { 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 + // If already initialized (by tests or a previous get_message call), use it. + if let Some(loc) = lock.get() { + return loc.format(id, args.as_ref()); + } + // Only lazily initialize if setup_localization stored a util name. + // Otherwise return the key ID (e.g. in tests before init_test_localization). + let has_name = UTIL_NAME.with(|n| n.get().is_some()); + if has_name { + let loc = lock.get_or_init(|| { + UTIL_NAME.with(|name_lock| { + let name = name_lock.get().map_or("", String::as_str); + create_localizer_from_util_name(name) + }) + }); + loc.format(id, args.as_ref()) + } else { + id.to_string() + } }) } @@ -369,65 +379,14 @@ fn detect_system_locale() -> Result { }) } -/// Sets up localization using the system locale with English fallback. -/// Always loads common strings in addition to utility-specific strings. -/// -/// This function initializes the localization system based on the system's locale -/// preferences (via the LANG environment variable) or falls back to English -/// if the system locale cannot be determined or the locale file doesn't exist. -/// English is always loaded as a fallback. -/// -/// # Arguments -/// -/// * `p` - Path to the directory containing localization (.ftl) files -/// -/// # Returns -/// -/// * `Ok(())` if initialization succeeds -/// * `Err(LocalizationError)` if initialization fails -/// -/// # Errors -/// -/// Returns a `LocalizationError` if: -/// * The en-US.ftl file cannot be read (English is required) -/// * The files contain invalid Fluent syntax -/// * The bundle cannot be initialized properly -/// -/// # Examples -/// -/// ``` -/// use uucore::locale::setup_localization; -/// -/// // Initialize localization using files in the "locales" directory -/// // Make sure you have at least an "en-US.ftl" file in this directory -/// // Other locale files like "fr-FR.ftl" are optional -/// match setup_localization("./locales") { -/// Ok(_) => println!("Localization initialized successfully"), -/// Err(e) => eprintln!("Failed to initialize localization: {e}"), -/// } -/// ``` +// Just store the utility name. The actual FTL parsing and bundle +// creation are deferred until the first get_message() call. pub fn setup_localization(p: &str) -> Result<(), LocalizationError> { - let locale = detect_system_locale().unwrap_or_else(|_| { - LanguageIdentifier::from_str(DEFAULT_LOCALE).expect("Default locale should always be valid") - }); - - // Load common strings along with utility-specific strings - if let Ok(locales_dir) = get_locales_dir(p) { - // Load both utility-specific and common strings - init_localization(&locale, &locales_dir, p) - } else { - // No locales directory found, use embedded English with common strings directly - let default_locale = LanguageIdentifier::from_str(DEFAULT_LOCALE) - .expect("Default locale should always be valid"); - let english_bundle = create_english_bundle_from_embedded(&default_locale, p)?; - let localizer = Localizer::new(english_bundle); - - LOCALIZER.with(|lock| { - lock.set(localizer) - .map_err(|_| LocalizationError::Bundle("Localizer already initialized".into())) - })?; - Ok(()) - } + UTIL_NAME.with(|lock| { + lock.set(p.to_string()) + .map_err(|_| LocalizationError::Bundle("Localization already initialized".into())) + })?; + Ok(()) } #[cfg(not(debug_assertions))] From 7e70533807fc40a6d4fc4d7c8720dc876cfaf173 Mon Sep 17 00:00:00 2001 From: Christopher Dryden Date: Fri, 13 Feb 2026 14:08:56 +0000 Subject: [PATCH 3/3] shuf,factor: restore localized uu_app() for uudoc compatibility --- src/uu/factor/src/factor.rs | 6 +++++- src/uu/shuf/src/shuf.rs | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/uu/factor/src/factor.rs b/src/uu/factor/src/factor.rs index 08537c98231..e3dcaebf7da 100644 --- a/src/uu/factor/src/factor.rs +++ b/src/uu/factor/src/factor.rs @@ -148,7 +148,7 @@ fn write_result_big_uint( #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uucore::clap_localization::parse_deferred(uu_app, args)?; + let matches = uucore::clap_localization::parse_deferred(uu_app_base, args)?; // If matches find --exponents flag than variable print_exponents is true and p^e output format will be used. let print_exponents = matches.get_flag(options::EXPONENTS); @@ -188,6 +188,10 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { + uucore::clap_localization::localize_command(uu_app_base()) +} + +fn uu_app_base() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) .infer_long_args(true) diff --git a/src/uu/shuf/src/shuf.rs b/src/uu/shuf/src/shuf.rs index 60d0cb91034..1cc7def5152 100644 --- a/src/uu/shuf/src/shuf.rs +++ b/src/uu/shuf/src/shuf.rs @@ -67,7 +67,7 @@ mod options { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uucore::clap_localization::parse_deferred(uu_app, args)?; + let matches = uucore::clap_localization::parse_deferred(uu_app_base, args)?; let mode = if matches.get_flag(options::ECHO) { Mode::Echo( @@ -171,6 +171,10 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } pub fn uu_app() -> Command { + uucore::clap_localization::localize_command(uu_app_base()) +} + +fn uu_app_base() -> Command { Command::new(uucore::util_name()) .version(uucore::crate_version!()) .infer_long_args(true)