From 5a7c50d64eca2ebcacba045fb7cae0f3ee4670a2 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 18 May 2025 18:38:05 +0200 Subject: [PATCH] uucore: add functions to manage translations --- .../workspace.wordlist.txt | 2 + Cargo.lock | 148 ++++++++- Cargo.toml | 5 + fuzz/Cargo.lock | 188 ++++++++++- src/uucore/Cargo.toml | 12 +- src/uucore/src/lib/lib.rs | 1 + src/uucore/src/lib/mods.rs | 1 + src/uucore/src/lib/mods/locale.rs | 303 ++++++++++++++++++ 8 files changed, 645 insertions(+), 15 deletions(-) create mode 100644 src/uucore/src/lib/mods/locale.rs diff --git a/.vscode/cspell.dictionaries/workspace.wordlist.txt b/.vscode/cspell.dictionaries/workspace.wordlist.txt index d917d30a4..bbdb82519 100644 --- a/.vscode/cspell.dictionaries/workspace.wordlist.txt +++ b/.vscode/cspell.dictionaries/workspace.wordlist.txt @@ -25,6 +25,7 @@ getrandom globset indicatif itertools +langid lscolors mdbook memchr @@ -46,6 +47,7 @@ termsize termwidth textwrap thiserror +unic ureq walkdir winapi diff --git a/Cargo.lock b/Cargo.lock index 63ec9c208..27e2b2af8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -181,7 +181,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash", + "rustc-hash 2.1.1", "shlex", "syn", ] @@ -857,6 +857,17 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "dlv-list" version = "0.5.2" @@ -994,6 +1005,50 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "fluent" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb74634707bebd0ce645a981148e8fb8c7bccd4c33c652aeffd28bf2f96d555a" +dependencies = [ + "fluent-bundle", + "unic-langid", +] + +[[package]] +name = "fluent-bundle" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe0a21ee80050c678013f82edf4b705fe2f26f1f9877593d13198612503f493" +dependencies = [ + "fluent-langneg", + "fluent-syntax", + "intl-memoizer", + "intl_pluralrules", + "rustc-hash 1.1.0", + "self_cell 0.10.3", + "smallvec", + "unic-langid", +] + +[[package]] +name = "fluent-langneg" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ad0989667548f06ccd0e306ed56b61bd4d35458d54df5ec7587c0e8ed5e94" +dependencies = [ + "unic-langid", +] + +[[package]] +name = "fluent-syntax" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a530c4694a6a8d528794ee9bbd8ba0122e779629ac908d15ad5a7ae7763a33d" +dependencies = [ + "thiserror 1.0.69", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1242,6 +1297,25 @@ dependencies = [ "libc", ] +[[package]] +name = "intl-memoizer" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe22e020fce238ae18a6d5d8c502ee76a52a6e880d99477657e6acc30ec57bda" +dependencies = [ + "type-map", + "unic-langid", +] + +[[package]] +name = "intl_pluralrules" +version = "7.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972" +dependencies = [ + "unic-langid", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -2049,6 +2123,12 @@ dependencies = [ "trim-in-place", ] +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc-hash" version = "2.1.1" @@ -2111,6 +2191,15 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "self_cell" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14e4d63b804dc0c7ec4a1e52bcb63f02c7ac94476755aa579edac21e01f915d" +dependencies = [ + "self_cell 1.2.0", +] + [[package]] name = "self_cell" version = "1.2.0" @@ -2438,6 +2527,16 @@ dependencies = [ "crunchy", ] +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "toml_datetime" version = "0.6.8" @@ -2461,12 +2560,39 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc" +[[package]] +name = "type-map" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deb68604048ff8fa93347f02441e4487594adc20bb8a084f9e564d2b827a0a9f" +dependencies = [ + "rustc-hash 1.1.0", +] + [[package]] name = "typenum" version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +[[package]] +name = "unic-langid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05" +dependencies = [ + "unic-langid-impl", +] + +[[package]] +name = "unic-langid-impl" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce1bf08044d4b7a94028c93786f8566047edc11110595914de93362559bc658" +dependencies = [ + "tinystr", +] + [[package]] name = "unicode-ident" version = "1.0.18" @@ -3278,7 +3404,7 @@ dependencies = [ "nix", "rand 0.9.1", "rayon", - "self_cell", + "self_cell 1.2.0", "tempfile", "thiserror 2.0.12", "unicode-width 0.2.0", @@ -3582,6 +3708,8 @@ dependencies = [ "digest", "dns-lookup", "dunce", + "fluent", + "fluent-bundle", "glob", "hex", "iana-time-zone", @@ -3602,6 +3730,7 @@ dependencies = [ "tempfile", "thiserror 2.0.12", "time", + "unic-langid", "utmp-classic", "uucore_procs", "walkdir", @@ -4080,6 +4209,21 @@ dependencies = [ "syn", ] +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" + +[[package]] +name = "zerovec" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +dependencies = [ + "zerofrom", +] + [[package]] name = "zip" version = "4.0.0" diff --git a/Cargo.toml b/Cargo.toml index a4c9d3200..260d458d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -362,6 +362,11 @@ sm3 = "0.4.2" crc32fast = "1.4.2" digest = "0.10.7" +# Fluent dependencies +fluent-bundle = "0.15.3" +fluent = "0.16.1" +unic-langid = "0.9.6" + uucore = { version = "0.0.30", package = "uucore", path = "src/uucore" } uucore_procs = { version = "0.0.30", package = "uucore_procs", path = "src/uucore_procs" } uu_ls = { version = "0.0.30", path = "src/uu/ls" } diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 06faf0f5f..c48d6aa56 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -436,6 +436,17 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "dlv-list" version = "0.5.2" @@ -479,6 +490,50 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fluent" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb74634707bebd0ce645a981148e8fb8c7bccd4c33c652aeffd28bf2f96d555a" +dependencies = [ + "fluent-bundle", + "unic-langid", +] + +[[package]] +name = "fluent-bundle" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe0a21ee80050c678013f82edf4b705fe2f26f1f9877593d13198612503f493" +dependencies = [ + "fluent-langneg", + "fluent-syntax", + "intl-memoizer", + "intl_pluralrules", + "rustc-hash", + "self_cell 0.10.3", + "smallvec", + "unic-langid", +] + +[[package]] +name = "fluent-langneg" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ad0989667548f06ccd0e306ed56b61bd4d35458d54df5ec7587c0e8ed5e94" +dependencies = [ + "unic-langid", +] + +[[package]] +name = "fluent-syntax" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a530c4694a6a8d528794ee9bbd8ba0122e779629ac908d15ad5a7ae7763a33d" +dependencies = [ + "thiserror 1.0.69", +] + [[package]] name = "fnv" version = "1.0.7" @@ -560,6 +615,25 @@ dependencies = [ "cc", ] +[[package]] +name = "intl-memoizer" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe22e020fce238ae18a6d5d8c502ee76a52a6e880d99477657e6acc30ec57bda" +dependencies = [ + "type-map", + "unic-langid", +] + +[[package]] +name = "intl_pluralrules" +version = "7.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972" +dependencies = [ + "unic-langid", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -963,6 +1037,12 @@ dependencies = [ "trim-in-place", ] +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustix" version = "1.0.7" @@ -982,6 +1062,15 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" +[[package]] +name = "self_cell" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14e4d63b804dc0c7ec4a1e52bcb63f02c7ac94476755aa579edac21e01f915d" +dependencies = [ + "self_cell 1.2.0", +] + [[package]] name = "self_cell" version = "1.2.0" @@ -1067,6 +1156,12 @@ dependencies = [ "digest", ] +[[package]] +name = "smallvec" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" + [[package]] name = "strsim" version = "0.11.1" @@ -1107,13 +1202,33 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.12", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -1136,18 +1251,55 @@ dependencies = [ "crunchy", ] +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "trim-in-place" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc" +[[package]] +name = "type-map" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deb68604048ff8fa93347f02441e4487594adc20bb8a084f9e564d2b827a0a9f" +dependencies = [ + "rustc-hash", +] + [[package]] name = "typenum" version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +[[package]] +name = "unic-langid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05" +dependencies = [ + "unic-langid-impl", +] + +[[package]] +name = "unic-langid-impl" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce1bf08044d4b7a94028c93786f8566047edc11110595914de93362559bc658" +dependencies = [ + "tinystr", +] + [[package]] name = "unicode-ident" version = "1.0.18" @@ -1213,7 +1365,7 @@ dependencies = [ "clap", "nix", "rust-ini", - "thiserror", + "thiserror 2.0.12", "uucore", ] @@ -1225,7 +1377,7 @@ dependencies = [ "num-bigint", "num-traits", "onig", - "thiserror", + "thiserror 2.0.12", "uucore", ] @@ -1245,7 +1397,7 @@ dependencies = [ "clap", "num-bigint", "num-traits", - "thiserror", + "thiserror 2.0.12", "uucore", ] @@ -1263,9 +1415,9 @@ dependencies = [ "nix", "rand 0.9.1", "rayon", - "self_cell", + "self_cell 1.2.0", "tempfile", - "thiserror", + "thiserror 2.0.12", "unicode-width", "uucore", ] @@ -1276,7 +1428,7 @@ version = "0.0.30" dependencies = [ "clap", "memchr", - "thiserror", + "thiserror 2.0.12", "uucore", ] @@ -1306,7 +1458,7 @@ dependencies = [ "clap", "libc", "nix", - "thiserror", + "thiserror 2.0.12", "unicode-width", "uucore", ] @@ -1326,6 +1478,8 @@ dependencies = [ "data-encoding-macro", "digest", "dunce", + "fluent", + "fluent-bundle", "glob", "hex", "iana-time-zone", @@ -1341,7 +1495,8 @@ dependencies = [ "sha2", "sha3", "sm3", - "thiserror", + "thiserror 2.0.12", + "unic-langid", "uucore_procs", "wild", "winapi-util", @@ -1660,3 +1815,18 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" + +[[package]] +name = "zerovec" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +dependencies = [ + "zerofrom", +] diff --git a/src/uucore/Cargo.toml b/src/uucore/Cargo.toml index 746e24f46..be2db18e5 100644 --- a/src/uucore/Cargo.toml +++ b/src/uucore/Cargo.toml @@ -31,7 +31,6 @@ wild = "2.2.1" glob = { workspace = true, optional = true } iana-time-zone = { workspace = true, optional = true } itertools = { workspace = true, optional = true } -thiserror = { workspace = true, optional = true } time = { workspace = true, optional = true, features = [ "formatting", "local-offset", @@ -60,6 +59,11 @@ bigdecimal = { workspace = true, optional = true } num-traits = { workspace = true, optional = true } selinux = { workspace = true, optional = true } +# Fluent dependencies +fluent-bundle = { workspace = true } +fluent = { workspace = true } +unic-langid = { workspace = true } +thiserror = { workspace = true } [target.'cfg(unix)'.dependencies] walkdir = { workspace = true, optional = true } nix = { workspace = true, features = ["fs", "uio", "zerocopy", "signal"] } @@ -87,7 +91,7 @@ default = [] # * non-default features backup-control = [] colors = [] -checksum = ["data-encoding", "thiserror", "sum"] +checksum = ["data-encoding", "sum"] encoding = ["data-encoding", "data-encoding-macro", "z85"] entries = ["libc"] extendedbigdecimal = ["bigdecimal", "num-traits"] @@ -114,7 +118,7 @@ proc-info = ["tty", "walkdir"] quoting-style = [] ranges = [] ringbuffer = [] -selinux = ["dep:selinux", "thiserror"] +selinux = ["dep:selinux"] signals = [] sum = [ "digest", @@ -136,4 +140,4 @@ version-cmp = [] wide = [] custom-tz-fmt = ["chrono", "chrono-tz", "iana-time-zone"] tty = [] -uptime = ["chrono", "libc", "windows-sys", "utmpx", "utmp-classic", "thiserror"] +uptime = ["chrono", "libc", "windows-sys", "utmpx", "utmp-classic"] diff --git a/src/uucore/src/lib/lib.rs b/src/uucore/src/lib/lib.rs index dbf3924aa..b1a9363f7 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -27,6 +27,7 @@ pub use crate::mods::error; #[cfg(feature = "fs")] pub use crate::mods::io; pub use crate::mods::line_ending; +pub use crate::mods::locale; pub use crate::mods::os; pub use crate::mods::panic; pub use crate::mods::posix; diff --git a/src/uucore/src/lib/mods.rs b/src/uucore/src/lib/mods.rs index a5570e8e2..7af54ff5a 100644 --- a/src/uucore/src/lib/mods.rs +++ b/src/uucore/src/lib/mods.rs @@ -9,6 +9,7 @@ pub mod error; #[cfg(feature = "fs")] pub mod io; pub mod line_ending; +pub mod locale; pub mod os; pub mod panic; pub mod posix; diff --git a/src/uucore/src/lib/mods/locale.rs b/src/uucore/src/lib/mods/locale.rs new file mode 100644 index 000000000..bcc9fb2db --- /dev/null +++ b/src/uucore/src/lib/mods/locale.rs @@ -0,0 +1,303 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. +// spell-checker:ignore unic_langid + +use crate::error::UError; +use fluent::{FluentArgs, FluentBundle, FluentResource}; +use std::collections::HashMap; +use std::fs; +use std::path::{Path, PathBuf}; +use std::str::FromStr; +use std::sync::OnceLock; +use thiserror::Error; +use unic_langid::LanguageIdentifier; + +#[derive(Error, Debug)] +pub enum LocalizationError { + #[error("I/O error loading '{path}': {source}")] + Io { + source: std::io::Error, + path: PathBuf, + }, + #[error("Parse error: {0}")] + Parse(String), + #[error("Bundle error: {0}")] + Bundle(String), +} + +impl From for LocalizationError { + fn from(error: std::io::Error) -> Self { + LocalizationError::Io { + source: error, + path: PathBuf::from(""), + } + } +} + +// Add a generic way to convert LocalizationError to UError +impl UError for LocalizationError { + fn code(&self) -> i32 { + 1 + } +} + +pub const DEFAULT_LOCALE: &str = "en-US"; + +// A struct to handle localization +struct Localizer { + bundle: FluentBundle, +} + +impl Localizer { + fn new(bundle: FluentBundle) -> Self { + Self { bundle } + } + + fn format(&self, id: &str, args: Option<&FluentArgs>, default: &str) -> String { + match self.bundle.get_message(id).and_then(|m| m.value()) { + Some(value) => { + let mut errs = Vec::new(); + self.bundle + .format_pattern(value, args, &mut errs) + .to_string() + } + None => default.to_string(), + } + } +} + +// Global localizer stored in thread-local OnceLock +thread_local! { + static LOCALIZER: OnceLock = const { OnceLock::new() }; +} + +// Initialize localization with a specific locale and config +fn init_localization( + locale: &LanguageIdentifier, + config: &LocalizationConfig, +) -> Result<(), LocalizationError> { + let bundle = create_bundle(locale, config)?; + LOCALIZER.with(|lock| { + let loc = Localizer::new(bundle); + lock.set(loc) + .map_err(|_| LocalizationError::Bundle("Localizer already initialized".into())) + })?; + Ok(()) +} + +// Create a bundle for a locale with fallback chain +fn create_bundle( + locale: &LanguageIdentifier, + config: &LocalizationConfig, +) -> Result, LocalizationError> { + // Create a new bundle with requested locale + let mut bundle = FluentBundle::new(vec![locale.clone()]); + + // Try to load the requested locale + let mut locales_to_try = vec![locale.clone()]; + locales_to_try.extend_from_slice(&config.fallback_locales); + + // Try each locale in the chain + let mut tried_paths = Vec::new(); + + for try_locale in locales_to_try { + let locale_path = config.get_locale_path(&try_locale); + tried_paths.push(locale_path.clone()); + + if let Ok(ftl_file) = fs::read_to_string(&locale_path) { + let resource = FluentResource::try_new(ftl_file).map_err(|_| { + LocalizationError::Parse(format!( + "Failed to parse localization resource for {}", + try_locale + )) + })?; + + bundle.add_resource(resource).map_err(|_| { + LocalizationError::Bundle(format!( + "Failed to add resource to bundle for {}", + try_locale + )) + })?; + + return Ok(bundle); + } + } + + let paths_str = tried_paths + .iter() + .map(|p| p.to_string_lossy().to_string()) + .collect::>() + .join(", "); + + Err(LocalizationError::Io { + source: std::io::Error::new(std::io::ErrorKind::NotFound, "No localization files found"), + path: PathBuf::from(paths_str), + }) +} + +fn get_message_internal(id: &str, args: Option, default: &str) -> String { + LOCALIZER.with(|lock| { + lock.get() + .map(|loc| loc.format(id, args.as_ref(), default)) + .unwrap_or_else(|| default.to_string()) + }) +} + +/// Retrieves a localized message by its identifier. +/// +/// Looks up a message with the given ID in the current locale bundle and returns +/// the localized text. If the message ID is not found, returns the provided default text. +/// +/// # Arguments +/// +/// * `id` - The message identifier in the Fluent resources +/// * `default` - Default text to use if the message ID isn't found +/// +/// # Returns +/// +/// A `String` containing either the localized message or the default text +/// +/// # Examples +/// +/// ``` +/// use uucore::locale::get_message; +/// +/// // Get a localized greeting or fall back to English +/// let greeting = get_message("greeting", "Hello, World!"); +/// println!("{}", greeting); +/// ``` +pub fn get_message(id: &str, default: &str) -> String { + get_message_internal(id, None, default) +} + +/// Retrieves a localized message with variable substitution. +/// +/// Looks up a message with the given ID in the current locale bundle, +/// substitutes variables from the provided arguments map, and returns the +/// localized text. If the message ID is not found, returns the provided default text. +/// +/// # Arguments +/// +/// * `id` - The message identifier in the Fluent resources +/// * `ftl_args` - Key-value pairs for variable substitution in the message +/// * `default` - Default text to use if the message ID isn't found +/// +/// # Returns +/// +/// A `String` containing either the localized message with variable substitution or the default text +/// +/// # Examples +/// +/// ``` +/// use uucore::locale::get_message_with_args; +/// use std::collections::HashMap; +/// +/// // For a Fluent message like: "Hello, { $name }! You have { $count } notifications." +/// let mut args = HashMap::new(); +/// args.insert("name".to_string(), "Alice".to_string()); +/// args.insert("count".to_string(), "3".to_string()); +/// +/// let message = get_message_with_args( +/// "notification", +/// args, +/// "Hello! You have notifications." +/// ); +/// println!("{}", message); +/// ``` +pub fn get_message_with_args(id: &str, ftl_args: HashMap, default: &str) -> String { + let args = ftl_args.into_iter().collect(); + get_message_internal(id, Some(args), default) +} + +// Configuration for localization +#[derive(Clone)] +struct LocalizationConfig { + locales_dir: PathBuf, + fallback_locales: Vec, +} + +impl LocalizationConfig { + // Create a new config with a specific locales directory + fn new>(locales_dir: P) -> Self { + Self { + locales_dir: locales_dir.as_ref().to_path_buf(), + fallback_locales: vec![], + } + } + + // Set fallback locales + fn with_fallbacks(mut self, fallbacks: Vec) -> Self { + self.fallback_locales = fallbacks; + self + } + + // Get path for a specific locale + fn get_locale_path(&self, locale: &LanguageIdentifier) -> PathBuf { + self.locales_dir.join(format!("{}.ftl", locale)) + } +} + +// Function to detect system locale from environment variables +fn detect_system_locale() -> Result { + let locale_str = std::env::var("LANG") + .unwrap_or_else(|_| DEFAULT_LOCALE.to_string()) + .split('.') + .next() + .unwrap_or(DEFAULT_LOCALE) + .to_string(); + + LanguageIdentifier::from_str(&locale_str) + .map_err(|_| LocalizationError::Parse(format!("Failed to parse locale: {}", locale_str))) +} + +/// Sets up localization using the system locale (or default) and project paths. +/// +/// This function initializes the localization system based on the system's locale +/// preferences (via the LANG environment variable) or falls back to the default locale +/// if the system locale cannot be determined or is invalid. +/// +/// # 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 localization files cannot be read +/// * The files contain invalid syntax +/// * The bundle cannot be initialized properly +/// +/// # Examples +/// +/// ``` +/// use uucore::locale::setup_localization; +/// +/// // Initialize localization using files in the "locales" directory +/// match setup_localization("./locales") { +/// Ok(_) => println!("Localization initialized successfully"), +/// Err(e) => eprintln!("Failed to initialize localization: {}", e), +/// } +/// ``` +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") + }); + + let locales_dir = PathBuf::from(p); + let fallback_locales = vec![ + LanguageIdentifier::from_str(DEFAULT_LOCALE) + .expect("Default locale should always be valid"), + ]; + + let config = LocalizationConfig::new(locales_dir).with_fallbacks(fallback_locales); + + init_localization(&locale, &config)?; + Ok(()) +}