rust_widgets 2.9.0

Pure Rust cross-platform native GUI library with hardware-adaptive rendering, 180 widgets, touch/gesture support, i18n, and SVG-pipeline-accurate output
// SPDX-FileCopyrightText: Copyright (c) 2026 Mike Li/Mikewolfli/Wei Li(mikewolfli@163.com)
// SPDX-License-Identifier: MIT

//! i18n global - global functions and static instances
use crate::compat::{MiniToString, Mutex, MutexGuard, String, Vec};
use crate::i18n::manager::I18nManager;
use crate::i18n::options::{InitOptions, InitReport};
use crate::i18n::types::TranslationFile;
/// Embedded English translations as a compile-time fallback.
const EMBEDDED_EN_JSON: &str = include_str!("../../language/en.json");
/// Global i18n manager instance using Mutex for thread-safe access
pub(crate) static GLOBAL_I18N: Mutex<Option<I18nManager>> = Mutex::new(None);

#[cfg(test)]
std::thread_local! {
    /// Explicit [`init`] calls made by the **current test thread**.
    ///
    /// A process-global assertion cannot prove "this call did not initialise i18n":
    /// [`translate`] lazily populates `GLOBAL_I18N` by design, so any test that renders a
    /// translated label concurrently puts a manager in the slot, and the `is_none()` assertion
    /// fails for a reason unrelated to the switch under test. A per-thread counter observes only
    /// the calls the test's own thread made.
    static INIT_CALLS: core::cell::Cell<usize> = const { core::cell::Cell::new(0) };
}

/// How many times [`init`] ran on the calling thread (test-only).
#[cfg(test)]
pub(crate) fn init_call_count() -> usize {
    INIT_CALLS.with(core::cell::Cell::get)
}

/// Initialize the i18n system, loading the embedded English translations.
pub fn init() {
    #[cfg(test)]
    INIT_CALLS.with(|calls| calls.set(calls.get() + 1));
    let mut guard = crate::compat::lock(&GLOBAL_I18N);
    ensure_initialized(&mut guard);
}

/// Builds the embedded catalogue into `slot` when it is empty.
///
/// # Why this is a helper and not four lines inside `init`
///
/// [`translate`] calls it too, and the contract it enforces is the reason: **a label
/// lookup never returns a raw key**. A host that forgot `init` used to get
/// `dialog.font.select_font` painted inside the dialog — every `tr!` call site
/// showed its identifier, which reads as a broken label, not as a missing catalogue
/// entry. Loading the compiled-in English catalogue on first use makes the default
/// build correct with no setup, and an explicit `init_with_options` still replaces
/// it (including the active language) when the host supplies real translations.
///
/// Returns `false` only when the embedded JSON cannot be parsed, which is a build
/// error rather than a runtime condition; callers then leave the key untouched.
fn ensure_initialized(slot: &mut Option<I18nManager>) -> bool {
    if slot.is_some() {
        return true;
    }
    let Ok(translation_file) = serde_json::from_str::<TranslationFile>(EMBEDDED_EN_JSON) else {
        log::error!("[i18n] Failed to parse embedded en.json");
        return false;
    };
    let mut manager = I18nManager::new();
    let language = translation_file.language.clone();
    manager.set_language(&language);
    manager.inject_translations(language, translation_file);
    log::info!(
        "[i18n] Loaded embedded English translations ({} messages)",
        manager.translation_count()
    );
    *slot = Some(manager);
    true
}
/// Initialize the i18n system with options
pub fn init_with_options(options: InitOptions) -> InitReport {
    let mut report = InitReport::new();
    let mut manager = I18nManager::new();
    manager.set_language(&options.language);
    let diagnostics = options.diagnostics;

    // Embedded English is the compile-time fallback that `init` also loads. It
    // must be available through this entry point too, so a locale that only
    // partially translates still falls back to the source language. Files loaded
    // below override it when they declare the same language, so file-provided
    // translations keep their priority.
    match serde_json::from_str::<TranslationFile>(EMBEDDED_EN_JSON) {
        Ok(translation_file) => {
            let language = translation_file.language.clone();
            manager.inject_translations(language, translation_file);
        }
        Err(e) => {
            report.errors.push(format!("Failed to parse embedded en.json: {e}"));
        }
    }

    // Load translations from directory if specified
    if let Some(dir) = options.preload_dir {
        match std::fs::read_dir(&dir) {
            Ok(entries) => {
                for entry in entries {
                    let entry = match entry {
                        Ok(entry) => entry,
                        Err(e) => {
                            report.errors.push(format!(
                                "Failed to read an entry in translation directory '{dir}': {e}"
                            ));
                            continue;
                        }
                    };
                    let path = entry.path();
                    if path.extension().is_some_and(|ext| ext == "json") {
                        let path_str = match path.to_str() {
                            Some(p) => p,
                            None => {
                                report.errors.push(format!("Non-UTF-8 path: {path:?}"));
                                continue;
                            }
                        };
                        match manager.load_translations(path_str) {
                            Ok(()) => {
                                report.files_loaded += 1;
                                if diagnostics {
                                    log::info!("[i18n] Loaded translations from: {path:?}");
                                }
                            }
                            Err(e) => {
                                report.errors.push(format!("Failed to load {path:?}: {e}"));
                            }
                        }
                    }
                }
            }
            Err(e) => {
                report.errors.push(format!("Translation directory '{dir}' could not be read: {e}"));
            }
        }
    }

    report.translations_count = manager.translation_count();
    let mut guard = crate::compat::lock(&GLOBAL_I18N);
    *guard = Some(manager);
    report
}
/// Translate a key to the current language
pub fn translate(key: &str) -> String {
    let mut out = String::new();
    translate_into(key, &mut out);
    out
}
/// Translate a key into `out` without allocating.
///
/// The zero-allocation counterpart of [`translate`]: the resolved message is appended to the
/// caller's buffer. `translate` itself must allocate because its signature returns `String`;
/// a draw loop that reuses one buffer should call this instead.
pub fn translate_into(key: &str, out: &mut String) {
    let mut guard = crate::compat::lock(&GLOBAL_I18N);
    if !ensure_initialized(&mut guard) {
        out.push_str(key);
        return;
    }
    match *guard {
        Some(ref mut manager) => manager.translate_into(key, out),
        None => out.push_str(key),
    }
}
/// Translate a key with context and plural count into `out` without allocating.
pub fn translate_into_with_context(key: &str, context: Option<&str>, count: u32, out: &mut String) {
    let mut guard = crate::compat::lock(&GLOBAL_I18N);
    if !ensure_initialized(&mut guard) {
        out.push_str(key);
        return;
    }
    match *guard {
        Some(ref mut manager) => manager.translate_with_context_into(key, context, count, out),
        None => out.push_str(key),
    }
}
/// Translate a key with optional context and plural count
pub fn translate_with_context(key: &str, context: Option<&str>, count: u32) -> String {
    let mut guard = crate::compat::lock(&GLOBAL_I18N);
    if !ensure_initialized(&mut guard) {
        return key.to_string();
    }
    match *guard {
        Some(ref mut manager) => manager.translate_with_context(key, context, count),
        None => key.to_string(),
    }
}
/// Get the global i18n manager
pub fn get_manager() -> MutexGuard<'static, Option<I18nManager>> {
    crate::compat::lock(&GLOBAL_I18N)
}
/// Check and reload all modified translation files
pub fn check_and_reload_all() -> Vec<crate::i18n::types::ReloadEvent> {
    let mut guard = get_manager();
    if let Some(ref mut manager) = *guard {
        manager.check_and_reload()
    } else {
        Vec::new()
    }
}

/// Serializes tests that mutate the process-global i18n state.
///
/// `GLOBAL_I18N` is a process-wide static, and Rust runs `#[test]` functions on
/// parallel threads. Tests that call `init()` / `init_with_options()` /
/// `reset` race with each other; holding this lock keeps them sequential.
#[cfg(test)]
pub(crate) fn global_i18n_test_lock() -> std::sync::MutexGuard<'static, ()> {
    static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
    LOCK.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn test_init_loads_english() {
        let _lock = global_i18n_test_lock();
        init();
        let count = {
            let mgr = get_manager();
            mgr.as_ref().map(|m| m.translation_count()).unwrap_or(0)
        };
        assert!(count > 0, "init should load English translations");
        // Reset global state so other tests don't see loaded translations
        // (drop the manager guard first, then acquire the lock again for reset)
        *crate::compat::lock(&GLOBAL_I18N) = None;
    }
}