Skip to main content

uucore/mods/
locale.rs

1// This file is part of the uutils coreutils package.
2//
3// For the full copyright and license information, please view the LICENSE
4// file that was distributed with this source code.
5// spell-checker:disable
6
7use crate::error::UError;
8
9use fluent::{FluentArgs, FluentBundle, FluentResource};
10use fluent_syntax::parser::ParserError;
11
12use std::cell::Cell;
13use std::fs;
14use std::path::{Path, PathBuf};
15use std::str::FromStr;
16use std::sync::OnceLock;
17
18use os_display::Quotable;
19use thiserror::Error;
20use unic_langid::LanguageIdentifier;
21
22#[derive(Error, Debug)]
23pub enum LocalizationError {
24    #[error("I/O error loading '{path}': {source}")]
25    Io {
26        source: std::io::Error,
27        path: PathBuf,
28    },
29    #[error("Parse-locale error: {0}")]
30    ParseLocale(String),
31    #[error("Resource parse error at '{snippet}': {error:?}")]
32    ParseResource {
33        #[source]
34        error: ParserError,
35        snippet: String,
36    },
37    #[error("Bundle error: {0}")]
38    Bundle(String),
39    #[error("Locales directory not found: {0}")]
40    LocalesDirNotFound(String),
41    #[error("Path resolution error: {0}")]
42    PathResolution(String),
43}
44
45impl From<std::io::Error> for LocalizationError {
46    fn from(error: std::io::Error) -> Self {
47        Self::Io {
48            source: error,
49            path: PathBuf::from("<unknown>"),
50        }
51    }
52}
53
54// Add a generic way to convert LocalizationError to UError
55impl UError for LocalizationError {
56    fn code(&self) -> i32 {
57        1
58    }
59}
60
61pub const DEFAULT_LOCALE: &str = "en-US";
62
63// Include embedded locale files as fallback
64include!(concat!(env!("OUT_DIR"), "/embedded_locales.rs"));
65
66// A struct to handle localization with optional English fallback
67struct Localizer {
68    primary_bundle: FluentBundle<&'static FluentResource>,
69    fallback_bundle: Option<FluentBundle<&'static FluentResource>>,
70}
71
72impl Localizer {
73    fn new(primary_bundle: FluentBundle<&'static FluentResource>) -> Self {
74        Self {
75            primary_bundle,
76            fallback_bundle: None,
77        }
78    }
79
80    fn with_fallback(mut self, fallback_bundle: FluentBundle<&'static FluentResource>) -> Self {
81        self.fallback_bundle = Some(fallback_bundle);
82        self
83    }
84
85    fn format(&self, id: &str, args: Option<&FluentArgs>) -> String {
86        // Try primary bundle first
87        if let Some(message) = self.primary_bundle.get_message(id).and_then(|m| m.value()) {
88            let mut errs = Vec::new();
89            return self
90                .primary_bundle
91                .format_pattern(message, args, &mut errs)
92                .to_string();
93        }
94
95        // Fall back to English bundle if available
96        if let Some(ref fallback) = self.fallback_bundle {
97            if let Some(message) = fallback.get_message(id).and_then(|m| m.value()) {
98                let mut errs = Vec::new();
99                return fallback
100                    .format_pattern(message, args, &mut errs)
101                    .to_string();
102            }
103        }
104
105        // Return the key ID if not found anywhere
106        id.to_string()
107    }
108}
109
110// Cache localizer. FluentResource cannot be shared between threads while FluentBundle can be shared
111static UUCORE_FLUENT: OnceLock<FluentResource> = OnceLock::new();
112static CHECKSUM_FLUENT: OnceLock<FluentResource> = OnceLock::new();
113static UTIL_FLUENT: OnceLock<FluentResource> = OnceLock::new();
114thread_local! {
115    static LOCALIZER: OnceLock<Localizer> = const { OnceLock::new() };
116}
117
118/// Helper function to find the uucore locales directory from a utility's locales directory
119fn find_uucore_locales_dir(utility_locales_dir: &Path) -> Option<PathBuf> {
120    // Normalize the path to get absolute path
121    let normalized_dir = utility_locales_dir
122        .canonicalize()
123        .unwrap_or_else(|_| utility_locales_dir.to_path_buf());
124
125    // Walk up: locales -> printenv -> uu -> src
126    let uucore_locales = normalized_dir
127        .parent()? // printenv
128        .parent()? // uu
129        .parent()? // src
130        .join("uucore")
131        .join("locales");
132
133    // Only return if the directory actually exists
134    uucore_locales.exists().then_some(uucore_locales)
135}
136
137/// Create a bundle that combines common and utility-specific strings
138fn create_bundle(
139    locale: &LanguageIdentifier,
140    locales_dir: &Path,
141    util_name: &str,
142) -> Result<FluentBundle<&'static FluentResource>, LocalizationError> {
143    let mut bundle: FluentBundle<&'static FluentResource> = FluentBundle::new(vec![locale.clone()]);
144
145    // Disable Unicode directional isolate characters
146    bundle.set_use_isolating(false);
147
148    let mut try_add_resource_from = |dir_opt: Option<PathBuf>| {
149        if let Some(resource) = dir_opt
150            .map(|dir| dir.join(format!("{locale}.ftl")))
151            .and_then(|locale_path| fs::read_to_string(locale_path).ok())
152            .and_then(|ftl| FluentResource::try_new(ftl).ok())
153        {
154            // use Box::leak to provide 'static lifetime for shared FluentBundle between threads
155            bundle.add_resource_overriding(Box::leak(Box::new(resource)));
156        }
157    };
158
159    // Load common strings from uucore locales directory
160    try_add_resource_from(find_uucore_locales_dir(locales_dir));
161    // Then, try to load utility-specific strings from the utility's locale directory
162    try_add_resource_from(get_locales_dir(util_name).ok());
163
164    // checksum binaries also require fluent files from the checksum_common crate
165    if [
166        "cksum",
167        "b2sum",
168        "md5sum",
169        "sha1sum",
170        "sha224sum",
171        "sha256sum",
172        "sha384sum",
173        "sha512sum",
174    ]
175    .contains(&util_name)
176    {
177        try_add_resource_from(get_locales_dir("checksum_common").ok());
178    }
179
180    // If we have at least one resource, return the bundle
181    if bundle.has_message("common-error") || bundle.has_message(&format!("{util_name}-about")) {
182        Ok(bundle)
183    } else {
184        Err(LocalizationError::LocalesDirNotFound(format!(
185            "No localization strings found for {locale} and utility {util_name}"
186        )))
187    }
188}
189
190/// Initialize localization with common strings in addition to utility-specific strings
191fn init_localization(
192    locale: &LanguageIdentifier,
193    locales_dir: &Path,
194    util_name: &str,
195) -> Result<(), LocalizationError> {
196    let default_locale = LanguageIdentifier::from_str(DEFAULT_LOCALE)
197        .expect("Default locale should always be valid");
198
199    // Try to create a bundle that combines common and utility-specific strings
200    let english_bundle: FluentBundle<&'static FluentResource> =
201        create_bundle(&default_locale, locales_dir, util_name).or_else(|_| {
202            // Fallback to embedded utility-specific and common strings
203            create_english_bundle_from_embedded(&default_locale, util_name)
204        })?;
205
206    let loc = if locale == &default_locale {
207        // If requesting English, just use English as primary (no fallback needed)
208        Localizer::new(english_bundle)
209    } else {
210        // Try to load the requested locale with common strings
211        if let Ok(primary_bundle) = create_bundle(locale, locales_dir, util_name) {
212            // Successfully loaded requested locale, load English as fallback
213            Localizer::new(primary_bundle).with_fallback(english_bundle)
214        } else {
215            // Failed to load requested locale, just use English as primary
216            Localizer::new(english_bundle)
217        }
218    };
219
220    LOCALIZER.with(|lock| {
221        lock.set(loc)
222            .map_err(|_| LocalizationError::Bundle("Localizer already initialized".into()))
223    })?;
224    Ok(())
225}
226
227/// Helper function to parse FluentResource from content string
228fn parse_fluent_resource(
229    content: &str,
230    cache: &'static OnceLock<FluentResource>,
231) -> Result<&'static FluentResource, LocalizationError> {
232    // global cache breaks unit tests
233    #[cfg(not(test))]
234    if let Some(res) = cache.get() {
235        return Ok(res);
236    }
237
238    let resource = FluentResource::try_new(content.to_string()).map_err(
239        |(_partial_resource, errs): (FluentResource, Vec<ParserError>)| {
240            if let Some(first_err) = errs.into_iter().next() {
241                let snippet = first_err
242                    .slice
243                    .clone()
244                    .and_then(|range| content.get(range))
245                    .unwrap_or("")
246                    .to_string();
247                LocalizationError::ParseResource {
248                    error: first_err,
249                    snippet,
250                }
251            } else {
252                LocalizationError::LocalesDirNotFound("Parse error without details".to_string())
253            }
254        },
255    )?;
256    // global cache breaks unit tests
257    if cfg!(not(test)) {
258        Ok(cache.get_or_init(|| resource))
259    } else {
260        Ok(Box::leak(Box::new(resource)))
261    }
262}
263
264/// Create a bundle from embedded English locale files with common uucore strings
265fn create_english_bundle_from_embedded(
266    locale: &LanguageIdentifier,
267    util_name: &str,
268) -> Result<FluentBundle<&'static FluentResource>, LocalizationError> {
269    // Only support English from embedded files
270    if *locale != "en-US" {
271        return Err(LocalizationError::LocalesDirNotFound(
272            "Embedded locales only support en-US".to_string(),
273        ));
274    }
275
276    let mut bundle: FluentBundle<&'static FluentResource> = FluentBundle::new(vec![locale.clone()]);
277    bundle.set_use_isolating(false);
278
279    // First, try to load common uucore strings
280    if let Some(uucore_content) = get_embedded_locale("uucore/en-US.ftl") {
281        let uucore_resource = parse_fluent_resource(uucore_content, &UUCORE_FLUENT)?;
282        bundle.add_resource_overriding(uucore_resource);
283    }
284
285    // Checksum algorithms need locale messages from checksum_common
286    if util_name.ends_with("sum") {
287        if let Some(uucore_content) = get_embedded_locale("checksum_common/en-US.ftl") {
288            let uucore_resource = parse_fluent_resource(uucore_content, &CHECKSUM_FLUENT)?;
289            bundle.add_resource_overriding(uucore_resource);
290        }
291    }
292
293    // Then, try to load utility-specific strings
294    let locale_key = format!("{util_name}/en-US.ftl");
295    if let Some(ftl_content) = get_embedded_locale(&locale_key) {
296        let resource = parse_fluent_resource(ftl_content, &UTIL_FLUENT)?;
297        bundle.add_resource_overriding(resource);
298    }
299
300    // Return the bundle if we have either common strings or utility-specific strings
301    if bundle.has_message("common-error") || bundle.has_message(&format!("{util_name}-about")) {
302        Ok(bundle)
303    } else {
304        Err(LocalizationError::LocalesDirNotFound(format!(
305            "No embedded locale found for {util_name} and no common strings found"
306        )))
307    }
308}
309
310/// Create a bundle from embedded locale files for any locale on WASI.
311/// Bypasses the global OnceLock cache (uses Box::leak) so it can be
312/// called for multiple locales in the same process.
313#[cfg(target_os = "wasi")]
314fn create_wasi_bundle_from_embedded(
315    locale: &LanguageIdentifier,
316    util_name: &str,
317) -> Result<FluentBundle<&'static FluentResource>, LocalizationError> {
318    let locale_str = locale.to_string();
319    let mut bundle: FluentBundle<&'static FluentResource> = FluentBundle::new(vec![locale.clone()]);
320    bundle.set_use_isolating(false);
321
322    let mut try_add = |key: &str| {
323        if let Some(content) = get_embedded_locale(key) {
324            if let Ok(resource) = FluentResource::try_new(content.to_string()) {
325                bundle.add_resource_overriding(Box::leak(Box::new(resource)));
326            }
327        }
328    };
329
330    try_add(&format!("uucore/{locale_str}.ftl"));
331    if util_name.ends_with("sum") {
332        try_add(&format!("checksum_common/{locale_str}.ftl"));
333    }
334    try_add(&format!("{util_name}/{locale_str}.ftl"));
335
336    if bundle.has_message("common-error") || bundle.has_message(&format!("{util_name}-about")) {
337        Ok(bundle)
338    } else {
339        Err(LocalizationError::LocalesDirNotFound(format!(
340            "No embedded locale found for {util_name}/{locale_str}"
341        )))
342    }
343}
344
345fn get_message_internal(id: &str, args: Option<FluentArgs>) -> String {
346    LOCALIZER.with(|lock| {
347        lock.get()
348            .map_or_else(|| id.to_string(), |loc| loc.format(id, args.as_ref())) // Return the key ID if localizer not initialized
349    })
350}
351
352/// Retrieves a localized message by its identifier.
353///
354/// Looks up a message with the given ID in the current locale bundle and returns
355/// the localized text. If the message ID is not found in the current locale,
356/// it will fall back to English. If the message is not found in English either,
357/// returns the message ID itself.
358///
359/// # Arguments
360///
361/// * `id` - The message identifier in the Fluent resources
362///
363/// # Returns
364///
365/// A `String` containing the localized message, or the message ID if not found
366///
367/// # Examples
368///
369/// ```
370/// use uucore::locale::get_message;
371///
372/// // Get a localized greeting (from .ftl files)
373/// let greeting = get_message("greeting");
374/// println!("{greeting}");
375/// ```
376pub fn get_message(id: &str) -> String {
377    get_message_internal(id, None)
378}
379
380/// Retrieves a localized message with variable substitution.
381///
382/// Looks up a message with the given ID in the current locale bundle,
383/// substitutes variables from the provided arguments map, and returns the
384/// localized text. If the message ID is not found in the current locale,
385/// it will fall back to English. If the message is not found in English either,
386/// returns the message ID itself.
387///
388/// # Arguments
389///
390/// * `id` - The message identifier in the Fluent resources
391/// * `ftl_args` - Key-value pairs for variable substitution in the message
392///
393/// # Returns
394///
395/// A `String` containing the localized message with variable substitution, or the message ID if not found
396///
397/// # Examples
398///
399/// ```
400/// use uucore::locale::get_message_with_args;
401/// use fluent::FluentArgs;
402///
403/// // For a Fluent message like: "Hello, { $name }! You have { $count } notifications."
404/// let mut args = FluentArgs::new();
405/// args.set("name".to_string(), "Alice".to_string());
406/// args.set("count".to_string(), 3);
407///
408/// let message = get_message_with_args("notification", args);
409/// println!("{message}");
410/// ```
411pub fn get_message_with_args(id: &str, ftl_args: FluentArgs) -> String {
412    get_message_internal(id, Some(ftl_args))
413}
414
415/// Function to detect system locale from environment variables
416fn detect_system_locale() -> Result<LanguageIdentifier, LocalizationError> {
417    let locale_str = std::env::var("LANG")
418        .unwrap_or_else(|_| DEFAULT_LOCALE.to_string())
419        .split('.')
420        .next()
421        .unwrap_or(DEFAULT_LOCALE)
422        .to_string();
423    LanguageIdentifier::from_str(&locale_str).map_err(|_| {
424        LocalizationError::ParseLocale(format!("Failed to parse locale: {locale_str}"))
425    })
426}
427
428/// Sets up localization using the system locale with English fallback.
429/// Always loads common strings in addition to utility-specific strings.
430///
431/// This function initializes the localization system based on the system's locale
432/// preferences (via the LANG environment variable) or falls back to English
433/// if the system locale cannot be determined or the locale file doesn't exist.
434/// English is always loaded as a fallback.
435///
436/// # Arguments
437///
438/// * `p` - Path to the directory containing localization (.ftl) files
439///
440/// # Returns
441///
442/// * `Ok(())` if initialization succeeds
443/// * `Err(LocalizationError)` if initialization fails
444///
445/// # Errors
446///
447/// Returns a `LocalizationError` if:
448/// * The en-US.ftl file cannot be read (English is required)
449/// * The files contain invalid Fluent syntax
450/// * The bundle cannot be initialized properly
451///
452/// # Examples
453///
454/// ```
455/// use uucore::locale::setup_localization;
456///
457/// // Initialize localization using files in the "locales" directory
458/// // Make sure you have at least an "en-US.ftl" file in this directory
459/// // Other locale files like "fr-FR.ftl" are optional
460/// match setup_localization("./locales") {
461///     Ok(_) => println!("Localization initialized successfully"),
462///     Err(e) => eprintln!("Failed to initialize localization: {e}"),
463/// }
464/// ```
465pub fn setup_localization(p: &str) -> Result<(), LocalizationError> {
466    // Avoid duplicated and high-cost localizer setup
467    thread_local! {
468        static LOCALIZER_IS_SET: Cell<bool> = const { Cell::new(false) };
469    }
470    if LOCALIZER_IS_SET.with(Cell::get) {
471        return Ok(());
472    }
473
474    let locale = detect_system_locale().unwrap_or_else(|_| {
475        LanguageIdentifier::from_str(DEFAULT_LOCALE).expect("Default locale should always be valid")
476    });
477
478    // Load common strings along with utility-specific strings
479    if let Ok(locales_dir) = get_locales_dir(p) {
480        // Load both utility-specific and common strings
481        init_localization(&locale, &locales_dir, p)?;
482    } else {
483        // No locales directory found, use embedded locales
484        let default_locale = LanguageIdentifier::from_str(DEFAULT_LOCALE)
485            .expect("Default locale should always be valid");
486
487        #[cfg(target_os = "wasi")]
488        let localizer = {
489            let english_bundle = create_wasi_bundle_from_embedded(&default_locale, p)?;
490            if locale == default_locale {
491                Localizer::new(english_bundle)
492            } else if let Ok(localized) = create_wasi_bundle_from_embedded(&locale, p) {
493                Localizer::new(localized).with_fallback(english_bundle)
494            } else {
495                Localizer::new(english_bundle)
496            }
497        };
498
499        #[cfg(not(target_os = "wasi"))]
500        let localizer = {
501            let english_bundle = create_english_bundle_from_embedded(&default_locale, p)?;
502            Localizer::new(english_bundle)
503        };
504
505        LOCALIZER.with(|lock| {
506            lock.set(localizer)
507                .map_err(|_| LocalizationError::Bundle("Localizer already initialized".into()))
508        })?;
509    }
510    LOCALIZER_IS_SET.with(|f| f.set(true));
511    Ok(())
512}
513
514#[cfg(not(debug_assertions))]
515fn resolve_locales_dir_from_exe_dir(exe_dir: &Path, p: &str) -> Option<PathBuf> {
516    // 1. <bindir>/locales/<prog>
517    let coreutils = exe_dir.join("locales").join(p);
518    if coreutils.is_dir() {
519        return Some(coreutils);
520    }
521
522    // 2. <prefix>/share/locales/<prog>
523    if let Some(prefix) = exe_dir.parent() {
524        let fhs = prefix.join("share").join("locales").join(p);
525        if fhs.is_dir() {
526            return Some(fhs);
527        }
528    }
529
530    // 3. <bindir>/<prog>   (legacy fall-back)
531    let fallback = exe_dir.join(p);
532    if fallback.is_dir() {
533        return Some(fallback);
534    }
535
536    None
537}
538
539/// Helper function to get the locales directory based on the build configuration
540fn get_locales_dir(p: &str) -> Result<PathBuf, LocalizationError> {
541    #[cfg(debug_assertions)]
542    {
543        // During development, use the project's locales directory
544        let manifest_dir = env!("CARGO_MANIFEST_DIR");
545        // from uucore path, load the locales directory from the program directory
546        let dev_path = PathBuf::from(manifest_dir)
547            .join("../uu")
548            .join(p)
549            .join("locales");
550
551        if dev_path.exists() {
552            return Ok(dev_path);
553        }
554
555        // Fallback for development if the expected path doesn't exist
556        let fallback_dev_path = PathBuf::from(manifest_dir).join(p);
557        if fallback_dev_path.exists() {
558            return Ok(fallback_dev_path);
559        }
560
561        Err(LocalizationError::LocalesDirNotFound(format!(
562            "Development locales directory not found at {} or {}",
563            dev_path.quote(),
564            fallback_dev_path.quote()
565        )))
566    }
567
568    #[cfg(not(debug_assertions))]
569    {
570        use std::env;
571        // In release builds, look relative to executable
572        let exe_path = env::current_exe().map_err(|e| {
573            LocalizationError::PathResolution(format!("Failed to get executable path: {e}"))
574        })?;
575
576        let exe_dir = exe_path.parent().ok_or_else(|| {
577            LocalizationError::PathResolution("Failed to get executable directory".to_string())
578        })?;
579
580        if let Some(dir) = resolve_locales_dir_from_exe_dir(exe_dir, p) {
581            return Ok(dir);
582        }
583
584        Err(LocalizationError::LocalesDirNotFound(format!(
585            "Release locales directory not found starting from {}",
586            exe_dir.quote()
587        )))
588    }
589}
590
591/// Macro for retrieving localized messages with optional arguments.
592///
593/// This macro provides a unified interface for both simple message retrieval
594/// and message retrieval with variable substitution. It accepts a message ID
595/// and optionally key-value pairs using the `"key" => value` syntax.
596///
597/// # Arguments
598///
599/// * `$id` - The message identifier string
600/// * Optional key-value pairs in the format `"key" => value`
601///
602/// # Examples
603///
604/// ```
605/// use uucore::translate;
606/// use fluent::FluentArgs;
607///
608/// // Simple message without arguments
609/// let greeting = translate!("greeting");
610///
611/// // Message with one argument
612/// let welcome = translate!("welcome", "name" => "Alice");
613///
614/// // Message with multiple arguments
615/// let username = "user name";
616/// let item_count = 2;
617/// let notification = translate!(
618///     "user-stats",
619///     "name" => username,
620///     "count" => item_count,
621///     "status" => "active"
622/// );
623/// ```
624#[macro_export]
625macro_rules! translate {
626    // Case 1: Message ID only (no arguments)
627    ($id:expr) => {
628        $crate::locale::get_message($id)
629    };
630
631    // Case 2: Message ID with key-value arguments
632    ($id:expr, $($key:expr => $value:expr),+ $(,)?) => {
633        {
634            let mut args = fluent::FluentArgs::new();
635            $(
636                let value_str = $value.to_string();
637                if let Ok(num_val) = value_str.parse::<i64>() {
638                    args.set($key, num_val);
639                } else if let Ok(float_val) = value_str.parse::<f64>() {
640                    args.set($key, float_val);
641                } else {
642                    // Keep as string if not a number
643                    args.set($key, value_str);
644                }
645            )+
646            $crate::locale::get_message_with_args($id, args)
647        }
648    };
649}
650
651// Re-export the macro for easier access
652pub use translate;
653
654#[cfg(test)]
655mod tests {
656    use super::*;
657    use std::env;
658    use std::fs;
659    use std::path::PathBuf;
660    use tempfile::TempDir;
661
662    /// Test-specific helper function to create a bundle from test directory only
663    #[cfg(test)]
664    fn create_test_bundle(
665        locale: &LanguageIdentifier,
666        test_locales_dir: &Path,
667    ) -> Result<FluentBundle<&'static FluentResource>, LocalizationError> {
668        let mut bundle: FluentBundle<&'static FluentResource> =
669            FluentBundle::new(vec![locale.clone()]);
670        bundle.set_use_isolating(false);
671
672        // Only load from the test directory - no common strings or utility-specific paths
673        let locale_path = test_locales_dir.join(format!("{locale}.ftl"));
674        if let Ok(ftl_content) = fs::read_to_string(&locale_path) {
675            let resource = parse_fluent_resource(&ftl_content, &UUCORE_FLUENT)?;
676            bundle.add_resource_overriding(resource);
677            return Ok(bundle);
678        }
679
680        Err(LocalizationError::LocalesDirNotFound(format!(
681            "No localization strings found for {locale} in {}",
682            test_locales_dir.quote()
683        )))
684    }
685
686    /// Test-specific initialization function for test directories
687    #[cfg(test)]
688    fn init_test_localization(
689        locale: &LanguageIdentifier,
690        test_locales_dir: &Path,
691    ) -> Result<(), LocalizationError> {
692        let default_locale = LanguageIdentifier::from_str(DEFAULT_LOCALE)
693            .expect("Default locale should always be valid");
694
695        // Create English bundle from test directory
696        let english_bundle = create_test_bundle(&default_locale, test_locales_dir)?;
697
698        let loc = if locale == &default_locale {
699            // If requesting English, just use English as primary
700            Localizer::new(english_bundle)
701        } else {
702            // Try to load the requested locale from test directory
703            if let Ok(primary_bundle) = create_test_bundle(locale, test_locales_dir) {
704                // Successfully loaded requested locale, load English as fallback
705                Localizer::new(primary_bundle).with_fallback(english_bundle)
706            } else {
707                // Failed to load requested locale, just use English as primary
708                Localizer::new(english_bundle)
709            }
710        };
711
712        LOCALIZER.with(|lock| {
713            lock.set(loc)
714                .map_err(|_| LocalizationError::Bundle("Localizer already initialized".into()))
715        })?;
716        Ok(())
717    }
718
719    /// Helper function to create a temporary directory with test locale files
720    fn create_test_locales_dir() -> TempDir {
721        let temp_dir = TempDir::new().expect("Failed to create temp directory");
722
723        // Create en-US.ftl
724        let en_content = r"
725greeting = Hello, world!
726welcome = Welcome, { $name }!
727count-items = You have { $count ->
728    [one] { $count } item
729   *[other] { $count } items
730}
731missing-in-other = This message only exists in English
732";
733
734        // Create fr-FR.ftl
735        let fr_content = r"
736greeting = Bonjour, le monde!
737welcome = Bienvenue, { $name }!
738count-items = Vous avez { $count ->
739    [one] { $count } élément
740   *[other] { $count } éléments
741}
742";
743
744        // Create ja-JP.ftl (Japanese)
745        let ja_content = r"
746greeting = こんにちは、世界!
747welcome = ようこそ、{ $name }さん!
748count-items = { $count }個のアイテムがあります
749";
750
751        // Create ar-SA.ftl (Arabic - Right-to-Left)
752        let ar_content = r"
753greeting = أهلاً بالعالم!
754welcome = أهلاً وسهلاً، { $name }!
755count-items = لديك { $count ->
756    [zero] لا عناصر
757    [one] عنصر واحد
758    [two] عنصران
759    [few] { $count } عناصر
760   *[other] { $count } عنصر
761}
762";
763
764        // Create es-ES.ftl with invalid syntax
765        let es_invalid_content = r"
766greeting = Hola, mundo!
767invalid-syntax = This is { $missing
768";
769
770        fs::write(temp_dir.path().join("en-US.ftl"), en_content)
771            .expect("Failed to write en-US.ftl");
772        fs::write(temp_dir.path().join("fr-FR.ftl"), fr_content)
773            .expect("Failed to write fr-FR.ftl");
774        fs::write(temp_dir.path().join("ja-JP.ftl"), ja_content)
775            .expect("Failed to write ja-JP.ftl");
776        fs::write(temp_dir.path().join("ar-SA.ftl"), ar_content)
777            .expect("Failed to write ar-SA.ftl");
778        fs::write(temp_dir.path().join("es-ES.ftl"), es_invalid_content)
779            .expect("Failed to write es-ES.ftl");
780
781        temp_dir
782    }
783
784    #[test]
785    fn test_create_bundle_success() {
786        let temp_dir = create_test_locales_dir();
787        let locale = LanguageIdentifier::from_str("en-US").unwrap();
788
789        let result = create_test_bundle(&locale, temp_dir.path());
790        assert!(result.is_ok());
791
792        let bundle = result.unwrap();
793        assert!(bundle.get_message("greeting").is_some());
794    }
795
796    #[test]
797    fn test_create_bundle_file_not_found() {
798        let temp_dir = TempDir::new().unwrap();
799        let locale = LanguageIdentifier::from_str("de-DE").unwrap();
800
801        let result = create_test_bundle(&locale, temp_dir.path());
802        assert!(result.is_err());
803
804        if let Err(LocalizationError::LocalesDirNotFound(_)) = result {
805            // Expected - no localization strings found
806        } else {
807            panic!("Expected LocalesDirNotFound error");
808        }
809    }
810
811    #[test]
812    fn test_create_bundle_invalid_syntax() {
813        let temp_dir = create_test_locales_dir();
814        let locale = LanguageIdentifier::from_str("es-ES").unwrap();
815
816        let result = create_test_bundle(&locale, temp_dir.path());
817
818        // The result should be an error due to invalid syntax
819        match result {
820            Err(LocalizationError::ParseResource {
821                error: _parser_err,
822                snippet: _,
823            }) => {
824                // Expected ParseResource variant - test passes
825            }
826            Ok(_) => {
827                panic!("Expected ParseResource error, but bundle was created successfully");
828            }
829            Err(other) => {
830                panic!("Expected ParseResource error, but got: {other:?}");
831            }
832        }
833    }
834
835    #[test]
836    fn test_localizer_format_primary_bundle() {
837        let temp_dir = create_test_locales_dir();
838        let en_bundle: FluentBundle<&'static FluentResource> = create_test_bundle(
839            &LanguageIdentifier::from_str("en-US").unwrap(),
840            temp_dir.path(),
841        )
842        .unwrap();
843
844        let localizer = Localizer::new(en_bundle);
845        let result = localizer.format("greeting", None);
846        assert_eq!(result, "Hello, world!");
847    }
848
849    #[test]
850    fn test_localizer_format_with_args() {
851        use fluent::FluentArgs;
852        let temp_dir = create_test_locales_dir();
853        let en_bundle = create_test_bundle(
854            &LanguageIdentifier::from_str("en-US").unwrap(),
855            temp_dir.path(),
856        )
857        .unwrap();
858
859        let localizer = Localizer::new(en_bundle);
860        let mut args = FluentArgs::new();
861        args.set("name", "Alice");
862
863        let result = localizer.format("welcome", Some(&args));
864        assert_eq!(result, "Welcome, Alice!");
865    }
866
867    #[test]
868    fn test_localizer_fallback_to_english() {
869        let temp_dir = create_test_locales_dir();
870        let fr_bundle = create_test_bundle(
871            &LanguageIdentifier::from_str("fr-FR").unwrap(),
872            temp_dir.path(),
873        )
874        .unwrap();
875        let en_bundle = create_test_bundle(
876            &LanguageIdentifier::from_str("en-US").unwrap(),
877            temp_dir.path(),
878        )
879        .unwrap();
880
881        let localizer = Localizer::new(fr_bundle).with_fallback(en_bundle);
882
883        // This message exists in French
884        let result1 = localizer.format("greeting", None);
885        assert_eq!(result1, "Bonjour, le monde!");
886
887        // This message only exists in English, should fallback
888        let result2 = localizer.format("missing-in-other", None);
889        assert_eq!(result2, "This message only exists in English");
890    }
891
892    #[test]
893    fn test_localizer_format_message_not_found() {
894        let temp_dir = create_test_locales_dir();
895        let en_bundle = create_test_bundle(
896            &LanguageIdentifier::from_str("en-US").unwrap(),
897            temp_dir.path(),
898        )
899        .unwrap();
900
901        let localizer = Localizer::new(en_bundle);
902        let result = localizer.format("nonexistent-message", None);
903        assert_eq!(result, "nonexistent-message");
904    }
905
906    #[test]
907    fn test_init_localization_english_only() {
908        // Run in a separate thread to avoid conflicts with other tests
909        std::thread::spawn(|| {
910            let temp_dir = create_test_locales_dir();
911            let locale = LanguageIdentifier::from_str("en-US").unwrap();
912
913            let result = init_test_localization(&locale, temp_dir.path());
914            assert!(result.is_ok());
915
916            // Test that we can get messages
917            let message = get_message("greeting");
918            assert_eq!(message, "Hello, world!");
919        })
920        .join()
921        .unwrap();
922    }
923
924    #[test]
925    fn test_init_localization_with_fallback() {
926        std::thread::spawn(|| {
927            let temp_dir = create_test_locales_dir();
928            let locale = LanguageIdentifier::from_str("fr-FR").unwrap();
929
930            let result = init_test_localization(&locale, temp_dir.path());
931            assert!(result.is_ok());
932
933            // Test French message
934            let message1 = get_message("greeting");
935            assert_eq!(message1, "Bonjour, le monde!");
936
937            // Test fallback to English
938            let message2 = get_message("missing-in-other");
939            assert_eq!(message2, "This message only exists in English");
940        })
941        .join()
942        .unwrap();
943    }
944
945    #[test]
946    fn test_init_localization_invalid_locale_falls_back_to_english() {
947        std::thread::spawn(|| {
948            let temp_dir = create_test_locales_dir();
949            let locale = LanguageIdentifier::from_str("de-DE").unwrap(); // No German file
950
951            let result = init_test_localization(&locale, temp_dir.path());
952            assert!(result.is_ok());
953
954            // Should use English as primary since German failed to load
955            let message = get_message("greeting");
956            assert_eq!(message, "Hello, world!");
957        })
958        .join()
959        .unwrap();
960    }
961
962    #[test]
963    fn test_init_localization_already_initialized() {
964        std::thread::spawn(|| {
965            let temp_dir = create_test_locales_dir();
966            let locale = LanguageIdentifier::from_str("en-US").unwrap();
967
968            // Initialize once
969            let result1 = init_test_localization(&locale, temp_dir.path());
970            assert!(result1.is_ok());
971
972            // Try to initialize again - should fail
973            let result2 = init_test_localization(&locale, temp_dir.path());
974            assert!(result2.is_err());
975
976            match result2 {
977                Err(LocalizationError::Bundle(msg)) => {
978                    assert!(msg.contains("already initialized"));
979                }
980                _ => panic!("Expected Bundle error"),
981            }
982        })
983        .join()
984        .unwrap();
985    }
986
987    #[test]
988    fn test_get_message() {
989        std::thread::spawn(|| {
990            let temp_dir = create_test_locales_dir();
991            let locale = LanguageIdentifier::from_str("fr-FR").unwrap();
992
993            init_test_localization(&locale, temp_dir.path()).unwrap();
994
995            let message = get_message("greeting");
996            assert_eq!(message, "Bonjour, le monde!");
997        })
998        .join()
999        .unwrap();
1000    }
1001
1002    #[test]
1003    fn test_get_message_with_args() {
1004        use fluent::FluentArgs;
1005        std::thread::spawn(|| {
1006            let temp_dir = create_test_locales_dir();
1007            let locale = LanguageIdentifier::from_str("en-US").unwrap();
1008
1009            init_test_localization(&locale, temp_dir.path()).unwrap();
1010
1011            let mut args = FluentArgs::new();
1012            args.set("name".to_string(), "Bob".to_string());
1013
1014            let message = get_message_with_args("welcome", args);
1015            assert_eq!(message, "Welcome, Bob!");
1016        })
1017        .join()
1018        .unwrap();
1019    }
1020
1021    #[test]
1022    fn test_get_message_with_args_pluralization() {
1023        use fluent::FluentArgs;
1024        std::thread::spawn(|| {
1025            let temp_dir = create_test_locales_dir();
1026            let locale = LanguageIdentifier::from_str("en-US").unwrap();
1027
1028            init_test_localization(&locale, temp_dir.path()).unwrap();
1029
1030            // Test singular
1031            let mut args1 = FluentArgs::new();
1032            args1.set("count", 1);
1033            let message1 = get_message_with_args("count-items", args1);
1034            assert_eq!(message1, "You have 1 item");
1035
1036            // Test plural
1037            let mut args2 = FluentArgs::new();
1038            args2.set("count", 5);
1039            let message2 = get_message_with_args("count-items", args2);
1040            assert_eq!(message2, "You have 5 items");
1041        })
1042        .join()
1043        .unwrap();
1044    }
1045
1046    #[test]
1047    fn test_thread_local_isolation() {
1048        use std::thread;
1049
1050        let temp_dir = create_test_locales_dir();
1051
1052        // Initialize in main thread with French
1053        let temp_path_main = temp_dir.path().to_path_buf();
1054        let main_handle = thread::spawn(move || {
1055            let locale = LanguageIdentifier::from_str("fr-FR").unwrap();
1056            init_test_localization(&locale, &temp_path_main).unwrap();
1057            let main_message = get_message("greeting");
1058            assert_eq!(main_message, "Bonjour, le monde!");
1059        });
1060        main_handle.join().unwrap();
1061
1062        // Test in a different thread - should not be initialized
1063        let temp_path = temp_dir.path().to_path_buf();
1064        let handle = thread::spawn(move || {
1065            // This thread should have its own uninitialized LOCALIZER
1066            let thread_message = get_message("greeting");
1067            assert_eq!(thread_message, "greeting"); // Returns ID since not initialized
1068
1069            // Initialize in this thread with English
1070            let en_locale = LanguageIdentifier::from_str("en-US").unwrap();
1071            init_test_localization(&en_locale, &temp_path).unwrap();
1072            let thread_message_after_init = get_message("greeting");
1073            assert_eq!(thread_message_after_init, "Hello, world!");
1074        });
1075
1076        handle.join().unwrap();
1077
1078        // Test another thread to verify French doesn't persist across threads
1079        let final_handle = thread::spawn(move || {
1080            // Should be uninitialized again
1081            let final_message = get_message("greeting");
1082            assert_eq!(final_message, "greeting");
1083        });
1084        final_handle.join().unwrap();
1085    }
1086
1087    #[test]
1088    fn test_japanese_localization() {
1089        use fluent::FluentArgs;
1090        std::thread::spawn(|| {
1091            let temp_dir = create_test_locales_dir();
1092            let locale = LanguageIdentifier::from_str("ja-JP").unwrap();
1093
1094            let result = init_test_localization(&locale, temp_dir.path());
1095            assert!(result.is_ok());
1096
1097            // Test Japanese greeting
1098            let message = get_message("greeting");
1099            assert_eq!(message, "こんにちは、世界!");
1100
1101            // Test Japanese with arguments
1102            let mut args = FluentArgs::new();
1103            args.set("name".to_string(), "田中".to_string());
1104            let welcome = get_message_with_args("welcome", args);
1105            assert_eq!(welcome, "ようこそ、田中さん!");
1106
1107            // Test Japanese count (no pluralization)
1108            let mut count_args = FluentArgs::new();
1109            count_args.set("count".to_string(), "5".to_string());
1110            let count_message = get_message_with_args("count-items", count_args);
1111            assert_eq!(count_message, "5個のアイテムがあります");
1112        })
1113        .join()
1114        .unwrap();
1115    }
1116
1117    #[test]
1118    fn test_arabic_localization() {
1119        use fluent::FluentArgs;
1120        std::thread::spawn(|| {
1121            let temp_dir = create_test_locales_dir();
1122            let locale = LanguageIdentifier::from_str("ar-SA").unwrap();
1123
1124            let result = init_test_localization(&locale, temp_dir.path());
1125            assert!(result.is_ok());
1126
1127            // Test Arabic greeting (RTL text)
1128            let message = get_message("greeting");
1129            assert_eq!(message, "أهلاً بالعالم!");
1130
1131            // Test Arabic with arguments
1132            let mut args = FluentArgs::new();
1133            args.set("name", "أحمد".to_string());
1134            let welcome = get_message_with_args("welcome", args);
1135            assert_eq!(welcome, "أهلاً وسهلاً، أحمد!");
1136
1137            // Test Arabic pluralization (zero case)
1138            let mut args_zero = FluentArgs::new();
1139            args_zero.set("count", 0);
1140            let message_zero = get_message_with_args("count-items", args_zero);
1141            assert_eq!(message_zero, "لديك لا عناصر");
1142
1143            // Test Arabic pluralization (one case)
1144            let mut args_one = FluentArgs::new();
1145            args_one.set("count", 1);
1146            let message_one = get_message_with_args("count-items", args_one);
1147            assert_eq!(message_one, "لديك عنصر واحد");
1148
1149            // Test Arabic pluralization (two case)
1150            let mut args_two = FluentArgs::new();
1151            args_two.set("count", 2);
1152            let message_two = get_message_with_args("count-items", args_two);
1153            assert_eq!(message_two, "لديك عنصران");
1154
1155            // Test Arabic pluralization (few case - 3-10)
1156            let mut args_few = FluentArgs::new();
1157            args_few.set("count", 5);
1158            let message_few = get_message_with_args("count-items", args_few);
1159            assert_eq!(message_few, "لديك 5 عناصر");
1160
1161            // Test Arabic pluralization (other case - 11+)
1162            let mut args_many = FluentArgs::new();
1163            args_many.set("count", 15);
1164            let message_many = get_message_with_args("count-items", args_many);
1165            assert_eq!(message_many, "لديك 15 عنصر");
1166        })
1167        .join()
1168        .unwrap();
1169    }
1170
1171    #[test]
1172    fn test_arabic_localization_with_macro() {
1173        std::thread::spawn(|| {
1174            let temp_dir = create_test_locales_dir();
1175            let locale = LanguageIdentifier::from_str("ar-SA").unwrap();
1176
1177            let result = init_test_localization(&locale, temp_dir.path());
1178            assert!(result.is_ok());
1179
1180            // Test Arabic greeting (RTL text)
1181            let message = translate!("greeting");
1182            assert_eq!(message, "أهلاً بالعالم!");
1183
1184            // Test Arabic with arguments
1185            let welcome = translate!("welcome", "name" => "أحمد");
1186            assert_eq!(welcome, "أهلاً وسهلاً، أحمد!");
1187
1188            // Test Arabic pluralization (zero case)
1189            let message_zero = translate!("count-items", "count" => 0);
1190            assert_eq!(message_zero, "لديك لا عناصر");
1191
1192            // Test Arabic pluralization (one case)
1193            let message_one = translate!("count-items", "count" => 1);
1194            assert_eq!(message_one, "لديك عنصر واحد");
1195
1196            // Test Arabic pluralization (two case)
1197            let message_two = translate!("count-items", "count" => 2);
1198            assert_eq!(message_two, "لديك عنصران");
1199
1200            // Test Arabic pluralization (few case - 3-10)
1201            let message_few = translate!("count-items", "count" => 5);
1202            assert_eq!(message_few, "لديك 5 عناصر");
1203
1204            // Test Arabic pluralization (other case - 11+)
1205            let message_many = translate!("count-items", "count" => 15);
1206            assert_eq!(message_many, "لديك 15 عنصر");
1207        })
1208        .join()
1209        .unwrap();
1210    }
1211
1212    #[test]
1213    fn test_mixed_script_fallback() {
1214        std::thread::spawn(|| {
1215            let temp_dir = create_test_locales_dir();
1216            let locale = LanguageIdentifier::from_str("ar-SA").unwrap();
1217
1218            let result = init_test_localization(&locale, temp_dir.path());
1219            assert!(result.is_ok());
1220
1221            // Test Arabic message exists
1222            let arabic_message = get_message("greeting");
1223            assert_eq!(arabic_message, "أهلاً بالعالم!");
1224
1225            // Test fallback to English for missing message
1226            let fallback_message = get_message("missing-in-other");
1227            assert_eq!(fallback_message, "This message only exists in English");
1228        })
1229        .join()
1230        .unwrap();
1231    }
1232
1233    #[test]
1234    fn test_unicode_directional_isolation_disabled() {
1235        use fluent::FluentArgs;
1236        std::thread::spawn(|| {
1237            let temp_dir = create_test_locales_dir();
1238            let locale = LanguageIdentifier::from_str("ar-SA").unwrap();
1239
1240            init_test_localization(&locale, temp_dir.path()).unwrap();
1241
1242            // Test that Latin script names are NOT isolated in RTL context
1243            // since we disabled Unicode directional isolation
1244            let mut args = FluentArgs::new();
1245            args.set("name".to_string(), "John Smith".to_string());
1246            let message = get_message_with_args("welcome", args);
1247
1248            // The Latin name should NOT be wrapped in directional isolate characters
1249            assert!(!message.contains("\u{2068}John Smith\u{2069}"));
1250            assert_eq!(message, "أهلاً وسهلاً، John Smith!");
1251        })
1252        .join()
1253        .unwrap();
1254    }
1255
1256    #[test]
1257    fn test_parse_resource_error_includes_snippet() {
1258        let temp_dir = create_test_locales_dir();
1259        let locale = LanguageIdentifier::from_str("es-ES").unwrap();
1260
1261        let result = create_test_bundle(&locale, temp_dir.path());
1262        assert!(result.is_err());
1263
1264        if let Err(LocalizationError::ParseResource {
1265            error: _err,
1266            snippet,
1267        }) = result
1268        {
1269            // The snippet should contain exactly the invalid text from es-ES.ftl
1270            assert!(
1271                snippet.contains("This is { $missing"),
1272                "snippet was `{snippet}` but did not include the invalid text"
1273            );
1274        } else {
1275            panic!("Expected LocalizationError::ParseResource with snippet");
1276        }
1277    }
1278
1279    #[test]
1280    fn test_localization_error_from_io_error() {
1281        let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found");
1282        let loc_error = LocalizationError::from(io_error);
1283
1284        match loc_error {
1285            LocalizationError::Io { source: _, path } => {
1286                assert_eq!(path, PathBuf::from("<unknown>"));
1287            }
1288            _ => panic!("Expected IO error variant"),
1289        }
1290    }
1291
1292    #[test]
1293    fn test_localization_error_uerror_impl() {
1294        let error = LocalizationError::Bundle("some error".to_string());
1295        assert_eq!(error.code(), 1);
1296    }
1297
1298    #[test]
1299    fn test_get_message_not_initialized() {
1300        std::thread::spawn(|| {
1301            let message = get_message("greeting");
1302            assert_eq!(message, "greeting"); // Should return the ID itself
1303        })
1304        .join()
1305        .unwrap();
1306    }
1307
1308    #[test]
1309    fn test_detect_system_locale_from_lang_env() {
1310        // Test locale parsing logic directly instead of relying on environment variables
1311        // which can have race conditions in multi-threaded test environments
1312
1313        // Test parsing logic with UTF-8 encoding
1314        let locale_with_encoding = "fr-FR.UTF-8";
1315        let parsed = locale_with_encoding.split('.').next().unwrap();
1316        let lang_id = LanguageIdentifier::from_str(parsed).unwrap();
1317        assert_eq!(lang_id.to_string(), "fr-FR");
1318
1319        // Test parsing logic without encoding
1320        let locale_without_encoding = "es-ES";
1321        let lang_id = LanguageIdentifier::from_str(locale_without_encoding).unwrap();
1322        assert_eq!(lang_id.to_string(), "es-ES");
1323
1324        // Test that DEFAULT_LOCALE is valid
1325        let default_lang_id = LanguageIdentifier::from_str(DEFAULT_LOCALE).unwrap();
1326        assert_eq!(default_lang_id.to_string(), "en-US");
1327    }
1328
1329    #[test]
1330    fn test_detect_system_locale_no_lang_env() {
1331        // Save current LANG value
1332        let original_lang = env::var("LANG").ok();
1333
1334        // Remove LANG environment variable
1335        unsafe {
1336            env::remove_var("LANG");
1337        }
1338
1339        let result = detect_system_locale();
1340        assert!(result.is_ok());
1341        assert_eq!(result.unwrap().to_string(), "en-US");
1342
1343        // Restore original LANG value
1344        if let Some(val) = original_lang {
1345            unsafe {
1346                env::set_var("LANG", val);
1347            }
1348        } else {
1349            {} // Was already unset
1350        }
1351    }
1352
1353    #[test]
1354    fn test_setup_localization_success() {
1355        std::thread::spawn(|| {
1356            // Save current LANG value
1357            let original_lang = env::var("LANG").ok();
1358            unsafe {
1359                env::set_var("LANG", "en-US.UTF-8"); // Use English since we have embedded resources for "test"
1360            }
1361
1362            let result = setup_localization("test");
1363            assert!(result.is_ok());
1364
1365            // Test that we can get messages (should use embedded English for "test" utility)
1366            let message = get_message("test-about");
1367            // Since we're using embedded resources, we should get the expected message
1368            assert!(!message.is_empty());
1369
1370            // Restore original LANG value
1371            if let Some(val) = original_lang {
1372                unsafe {
1373                    env::set_var("LANG", val);
1374                }
1375            } else {
1376                unsafe {
1377                    env::remove_var("LANG");
1378                }
1379            }
1380        })
1381        .join()
1382        .unwrap();
1383    }
1384
1385    #[test]
1386    fn test_setup_localization_falls_back_to_english() {
1387        std::thread::spawn(|| {
1388            // Save current LANG value
1389            let original_lang = env::var("LANG").ok();
1390            unsafe {
1391                env::set_var("LANG", "de-DE.UTF-8"); // German file doesn't exist, should fallback
1392            }
1393
1394            let result = setup_localization("test");
1395            assert!(result.is_ok());
1396
1397            // Should fall back to English embedded resources
1398            let message = get_message("test-about");
1399            assert!(!message.is_empty()); // Should get something, not just the key
1400
1401            // Restore original LANG value
1402            if let Some(val) = original_lang {
1403                unsafe {
1404                    env::set_var("LANG", val);
1405                }
1406            } else {
1407                unsafe {
1408                    env::remove_var("LANG");
1409                }
1410            }
1411        })
1412        .join()
1413        .unwrap();
1414    }
1415
1416    #[test]
1417    fn test_setup_localization_fallback_to_embedded() {
1418        std::thread::spawn(|| {
1419            // Force English locale for this test
1420            unsafe {
1421                env::set_var("LANG", "en-US");
1422            }
1423
1424            // Test with a utility name that has embedded locales
1425            // This should fall back to embedded English when filesystem files aren't found
1426            let result = setup_localization("test");
1427            if let Err(e) = &result {
1428                eprintln!("Setup localization failed: {e}");
1429            }
1430            assert!(result.is_ok());
1431
1432            // Verify we can get messages (using embedded English)
1433            let message = get_message("test-about");
1434            assert_eq!(message, "Check file types and compare values."); // Should use embedded English
1435        })
1436        .join()
1437        .unwrap();
1438    }
1439
1440    #[test]
1441    fn test_error_display() {
1442        let io_error = LocalizationError::Io {
1443            source: std::io::Error::new(std::io::ErrorKind::NotFound, "File not found"),
1444            path: PathBuf::from("/test/path.ftl"),
1445        };
1446        let error_string = format!("{io_error}");
1447        assert!(error_string.contains("I/O error loading"));
1448        assert!(error_string.contains("/test/path.ftl"));
1449
1450        let bundle_error = LocalizationError::Bundle("Bundle creation failed".to_string());
1451        let bundle_string = format!("{bundle_error}");
1452        assert!(bundle_string.contains("Bundle error: Bundle creation failed"));
1453    }
1454
1455    #[test]
1456    fn test_clap_localization_fallbacks() {
1457        std::thread::spawn(|| {
1458            // Test the scenario where localization isn't properly initialized
1459            // and we need fallbacks for clap error handling
1460
1461            // First, test when localizer is not initialized
1462            let error_msg = get_message("common-error");
1463            assert_eq!(error_msg, "common-error"); // Should return key when not initialized
1464
1465            let tip_msg = get_message("common-tip");
1466            assert_eq!(tip_msg, "common-tip"); // Should return key when not initialized
1467
1468            // Now initialize with setup_localization
1469            let result = setup_localization("comm");
1470            if result.is_err() {
1471                // If setup fails (e.g., no embedded locales for comm), try with a known utility
1472                let _ = setup_localization("test");
1473            }
1474
1475            // Test that common strings are available after initialization
1476            let error_after_init = get_message("common-error");
1477            // Should either be translated or return the key (but not panic)
1478            assert!(!error_after_init.is_empty());
1479
1480            let tip_after_init = get_message("common-tip");
1481            assert!(!tip_after_init.is_empty());
1482
1483            // Test that clap error keys work with fallbacks
1484            let unknown_arg_key = get_message("clap-error-unexpected-argument");
1485            assert!(!unknown_arg_key.is_empty());
1486
1487            // Test usage key fallback
1488            let usage_key = get_message("common-usage");
1489            assert!(!usage_key.is_empty());
1490        })
1491        .join()
1492        .unwrap();
1493    }
1494}
1495
1496#[cfg(all(test, not(debug_assertions)))]
1497mod fhs_tests {
1498    use super::*;
1499    use tempfile::TempDir;
1500
1501    #[test]
1502    fn resolves_fhs_share_locales_layout() {
1503        // 1. Set up a fake installation prefix in a temp directory
1504        let prefix = TempDir::new().unwrap(); // e.g.  /tmp/xyz
1505        let bin_dir = prefix.path().join("bin"); //        /tmp/xyz/bin
1506        let share_dir = prefix.path().join("share").join("locales").join("cut"); // /tmp/xyz/share/locales/cut
1507        std::fs::create_dir_all(&share_dir).unwrap();
1508        std::fs::create_dir_all(&bin_dir).unwrap();
1509
1510        // 2. Pretend the executable lives in <prefix>/bin
1511        let exe_dir = bin_dir.as_path();
1512
1513        // 3. Ask the helper to resolve the locales dir
1514        let result = resolve_locales_dir_from_exe_dir(exe_dir, "cut")
1515            .expect("should find locales via FHS path");
1516
1517        assert_eq!(result, share_dir);
1518    }
1519}