diff --git a/Cargo.lock b/Cargo.lock index b89f8b4a..02eaf72f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -522,6 +522,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + [[package]] name = "either" version = "1.13.0" @@ -534,6 +545,70 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +[[package]] +name = "encoding" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec" +dependencies = [ + "encoding-index-japanese", + "encoding-index-korean", + "encoding-index-simpchinese", + "encoding-index-singlebyte", + "encoding-index-tradchinese", +] + +[[package]] +name = "encoding-index-japanese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-korean" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-simpchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-singlebyte" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-tradchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding_index_tests" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" + [[package]] name = "env_filter" version = "0.1.2" @@ -625,6 +700,15 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "fluent-langneg" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7b2da3cb6583f7e5f98d3e0e1f9ff70451398037445c8e89a0dc51594cf1736" +dependencies = [ + "icu_locid", +] + [[package]] name = "fnv" version = "1.0.7" @@ -670,6 +754,16 @@ dependencies = [ "wasi", ] +[[package]] +name = "gettext" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ebb594e753d5997e4be036e5a8cf048ab9414352870fb45c779557bbc9ba971" +dependencies = [ + "byteorder", + "encoding", +] + [[package]] name = "gettext-rs" version = "0.2.1" @@ -735,6 +829,18 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", +] + [[package]] name = "indexmap" version = "2.5.0" @@ -933,6 +1039,12 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "litemap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" + [[package]] name = "lock_api" version = "0.4.12" @@ -1262,7 +1374,11 @@ version = "0.2.1" dependencies = [ "cfg-if", "errno", + "fluent-langneg", + "gettext", "libc", + "sys-locale", + "tr", ] [[package]] @@ -1301,9 +1417,13 @@ version = "0.2.1" dependencies = [ "chrono", "clap", + "fluent-langneg", + "gettext", "gettext-rs", "libc", "plib", + "sys-locale", + "tr", ] [[package]] @@ -1912,6 +2032,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sys-locale" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e801cf239ecd6ccd71f03d270d67dd53d13e90aab208bf4b8fe4ad957ea949b0" +dependencies = [ + "libc", +] + [[package]] name = "sysinfo" version = "0.31.4" @@ -2059,12 +2188,31 @@ dependencies = [ "time-core", ] +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", +] + [[package]] name = "topological-sort" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d" +[[package]] +name = "tr" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a592877f7cb2e5a6327b8c908fa6d51098482b1c87d478abdd783e61218f8151" +dependencies = [ + "gettext", + "lazy_static", +] + [[package]] name = "tracing" version = "0.1.40" @@ -2516,6 +2664,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + [[package]] name = "zerocopy" version = "0.7.35" diff --git a/Cargo.toml b/Cargo.toml index 8697a0a2..cbb97092 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,9 @@ libc = "0.2" regex = "1.10" gettext-rs = { path = "./gettext-rs" } errno = "0.3" +tr = { version = "0.1.10", default-features = false, features = ["gettext"]} +gettext = "0.4.0" +sys-locale = "0.3.1" [workspace.lints] diff --git a/datetime/Cargo.toml b/datetime/Cargo.toml index 6c85caf0..c74310ef 100644 --- a/datetime/Cargo.toml +++ b/datetime/Cargo.toml @@ -13,6 +13,10 @@ clap.workspace = true gettext-rs.workspace = true chrono.workspace = true libc.workspace = true +tr.workspace = true +gettext = "0.4.0" +sys-locale = "0.3.1" +fluent-langneg = "0.14.1" [lints] workspace = true diff --git a/datetime/cal.rs b/datetime/cal.rs index b075909a..ca792d03 100644 --- a/datetime/cal.rs +++ b/datetime/cal.rs @@ -13,15 +13,14 @@ use chrono::Datelike; use clap::Parser; -use gettextrs::{bind_textdomain_codeset, gettext, setlocale, textdomain, LocaleCategory}; -use plib::PROJECT_NAME; +use tr::tr; #[derive(Parser)] -#[command(version, about = gettext("cal - print a calendar"))] +#[command(version, about = tr!("cal - print a calendar"))] struct Args { #[arg( value_parser = clap::value_parser!(u32).range(1..), - help = gettext( + help = tr!( "Specify the month to be displayed, represented as a decimal integer from 1 (January) to 12 (December)" ) )] @@ -29,7 +28,7 @@ struct Args { #[arg( value_parser = clap::value_parser!(u32).range(1..), - help = gettext( + help = tr!( "Specify the year for which the calendar is displayed, represented as a decimal integer from 1 to 9999" ) )] @@ -38,23 +37,23 @@ struct Args { fn print_month(month: u32, year: u32) { let month_name = match month { - 1 => gettext("January"), - 2 => gettext("February"), - 3 => gettext("March"), - 4 => gettext("April"), - 5 => gettext("May"), - 6 => gettext("June"), - 7 => gettext("July"), - 8 => gettext("August"), - 9 => gettext("September"), - 10 => gettext("October"), - 11 => gettext("November"), - 12 => gettext("December"), + 1 => tr!("January"), + 2 => tr!("February"), + 3 => tr!("March"), + 4 => tr!("April"), + 5 => tr!("May"), + 6 => tr!("June"), + 7 => tr!("July"), + 8 => tr!("August"), + 9 => tr!("September"), + 10 => tr!("October"), + 11 => tr!("November"), + 12 => tr!("December"), _ => unreachable!(), }; println!("{} {}", month_name, year); - println!("{}", gettext("Su Mo Tu We Th Fr Sa")); + println!("{}", tr!("Su Mo Tu We Th Fr Sa")); let mut day = 1; let mut weekday = 1; @@ -94,13 +93,11 @@ fn print_year(year: u32) { } fn main() -> Result<(), Box> { + plib::initialize_i18n!()?; + // parse command line arguments let mut args = Args::parse(); - setlocale(LocaleCategory::LcAll, ""); - textdomain(PROJECT_NAME)?; - bind_textdomain_codeset(PROJECT_NAME, "UTF-8")?; - // If no arguments are provided, display the current month if args.month.is_none() && args.year.is_none() { let now = chrono::Local::now(); @@ -116,7 +113,7 @@ fn main() -> Result<(), Box> { let year = match args.year { Some(year) => { if year > 9999 { - return Err(gettext("year must be between 1 and 9999").into()); + return Err(tr!("year must be between 1 and 9999").into()); } year } @@ -125,7 +122,7 @@ fn main() -> Result<(), Box> { if let Some(month) = args.month { if month > 12 { - return Err(gettext("month must be between 1 and 12").into()); + return Err(tr!("month must be between 1 and 12").into()); } print_month(month, year); } else { diff --git a/plib/Cargo.toml b/plib/Cargo.toml index 62b8b1b5..41c1d9bf 100644 --- a/plib/Cargo.toml +++ b/plib/Cargo.toml @@ -11,6 +11,10 @@ rust-version.workspace = true cfg-if = "1.0" libc.workspace = true errno.workspace = true +gettext = "0.4.0" +sys-locale = "0.3.1" +fluent-langneg = "0.14.1" +tr.workspace = true [lints] workspace = true diff --git a/plib/src/i18n.rs b/plib/src/i18n.rs new file mode 100644 index 00000000..27036e36 --- /dev/null +++ b/plib/src/i18n.rs @@ -0,0 +1,97 @@ +use std::ffi::CString; + +/// Initialize localization catalog. +pub fn initialize_catalog() -> Result, Box> { + // TODO: load from available mo files + let available_locales = &["en-US", "en-AU"]; + + let mut requested_locales: Vec = sys_locale::get_locales().into_iter().collect(); + + let lc_all = match std::env::var("LC_ALL") { + Ok(var) => Some(var), + Err(std::env::VarError::NotPresent) => None, + // TODO: a proper error message + Err(error) => return Err(error.into()), + }; + + let lc_messages = match std::env::var("LC_MESSAGES") { + Ok(var) => Some(var), + Err(std::env::VarError::NotPresent) => None, + // TODO: a proper error message + Err(error) => return Err(error.into()), + }; + + let mut insert_highest_priority = |var: &Option| { + if let Some(var) = &var { + let mut move_locale = None; + for (i, locale) in requested_locales.iter().enumerate() { + if locale == var { + if i != 0 { + move_locale = Some(i); + } + } + } + if let Some(i) = move_locale { + let locale = requested_locales.remove(i); + requested_locales.insert(0, locale); + } else { + requested_locales.insert(0, var.clone()); + } + } + }; + + insert_highest_priority(&lc_messages); + insert_highest_priority(&lc_all); + + let default_locale: fluent_langneg::LanguageIdentifier = "en-US".parse().unwrap(); + let available_locales = fluent_langneg::convert_vec_str_to_langids_lossy(available_locales); + let locale = fluent_langneg::negotiate_languages( + &fluent_langneg::convert_vec_str_to_langids_lossy(requested_locales), + &available_locales, + Some(&default_locale), + // Using Lookup strategy because gettext only allows one fallback :'( + fluent_langneg::NegotiationStrategy::Lookup, + ) + .into_iter() + .next() + .expect("expected at least one locale to be present (at least the default)"); + + println!("Using locale {locale}"); + + + if let Some(lc_all) = lc_all { + let lc_all_c = CString::new(lc_all)?.as_ptr(); + unsafe { + libc::setlocale(libc::LC_ALL, lc_all_c); + } + } else { + let lc_messages_c = CString::new(locale.to_string())?.as_ptr(); + unsafe { + libc::setlocale(libc::LC_MESSAGES, lc_messages_c); + } + } + + Ok(if locale != &default_locale { + // TODO: do we need to load if en-US, as it's the fallback? + let f = std::fs::File::open(format!("messages.{locale}.mo"))?; + let catalog = gettext::Catalog::parse(f)?; + Some(catalog) + } else { + None + }) +} + +/// Macro to initialize +#[macro_export] +macro_rules! initialize_i18n { + () => { + match plib::i18n::initialize_catalog() { + Ok(Some(catalog)) => { + tr::set_translator!(catalog); + Ok(()) + }, + Ok(None) => Ok(()), + Err(error) => Err(error), + } + }; +} \ No newline at end of file diff --git a/plib/src/lib.rs b/plib/src/lib.rs index 81f71675..b48d9304 100644 --- a/plib/src/lib.rs +++ b/plib/src/lib.rs @@ -9,6 +9,7 @@ pub mod curuser; pub mod group; +pub mod i18n; pub mod io; pub mod lzw; pub mod modestr;