#[cfg(feature = "gettext")]
use std::{
ffi::{CStr, CString},
sync::OnceLock,
};
#[cfg(feature = "gettext")]
pub(crate) mod check;
#[cfg(feature = "gettext")]
static TEXT_DOMAIN: OnceLock<&'static CStr> = OnceLock::new();
#[cfg(feature = "gettext")]
pub(crate) fn textdomain(domain: &'static CStr) {
use libc::{CODESET, LC_ALL, nl_langinfo, setlocale};
let utf8 = c"UTF-8";
unsafe {
if setlocale(LC_ALL, CString::default().as_ptr()).is_null() {
return;
};
if CStr::from_ptr(nl_langinfo(CODESET)) != utf8 {
return;
}
if gettext_sys::bind_textdomain_codeset(domain.as_ptr(), utf8.as_ptr()).is_null() {
return;
}
}
TEXT_DOMAIN.set(domain).expect("only set the locale once")
}
#[cfg(feature = "gettext")]
pub(crate) mod display {
pub struct Wrap<T: ?Sized>(pub T);
pub trait Convert {
fn display(&self) -> String;
}
pub trait Reference {
fn display(&self) -> &str;
}
impl<T: std::fmt::Display + ?Sized> Convert for Wrap<T> {
fn display(&self) -> String {
self.0.to_string()
}
}
impl<T: std::fmt::Display + AsRef<str> + ?Sized> Reference for &Wrap<T> {
fn display(&self) -> &str {
self.0.as_ref()
}
}
}
#[cfg(feature = "gettext")]
pub(crate) fn gettext(text: &'static CStr) -> &'static str {
unsafe {
CStr::from_ptr(gettext_sys::dgettext(
TEXT_DOMAIN
.get()
.map_or(std::ptr::null(), |domain| domain.as_ptr()),
text.as_ptr(),
))
}
.to_str()
.expect("translation files are corrupted")
}
#[cfg(feature = "gettext")]
macro_rules! cstr {
($lit:literal) => {{
const CS: &'static std::ffi::CStr =
match std::ffi::CStr::from_bytes_until_nul(concat!($lit, "\0").as_bytes()) {
Ok(x) => x,
Err(_) => panic!("string literal did not pass CStr checks"),
};
CS
}};
}
#[cfg(feature = "gettext")]
macro_rules! xlat {
($text: literal) => {{
const _: () = $crate::gettext::check::check_keys($text, &[]);
$crate::gettext::gettext(cstr!($text))
}};
($text: literal $(, $id: ident = $val: expr)* $(,)?) => {{
#[allow(unused)]
use $crate::gettext::display::{Convert, Reference, Wrap};
use std::ops::Deref;
const _: () = $crate::gettext::check::check_keys(
$text,
&[$(stringify!($id)),*]
);
let result = $crate::gettext::gettext(cstr!($text));
$(
let result = result.replace(
concat!("{", stringify!($id), "}"),
(&&Wrap(&$val)).display().deref(),
);
)*
result
}};
}
#[cfg(not(feature = "gettext"))]
macro_rules! xlat {
($text: literal) => { $text };
($text: literal $(, $id: ident = $val: expr)* $(,)?) => {{
format!($text $(,$id = $val)*)
}};
}
#[cfg(feature = "gettext")]
macro_rules! xlat_write {
($f: expr, $fmt: literal $(, $id: ident = $val: expr)* $(,)?) => {
write!($f, "{}", xlat!($fmt $(, $id = $val)*))
};
}
#[cfg(feature = "gettext")]
macro_rules! xlat_println {
($fmt: literal $(, $id: ident = $val: expr)* $(,)?) => {
println_ignore_io_error!("{}", xlat!($fmt $(, $id = $val)*))
};
}
#[cfg(not(feature = "gettext"))]
macro_rules! xlat_write {
($f: expr, $fmt: literal $(, $id: ident = $val: expr)* $(,)?) => {
write!($f, $fmt $(, $id = $val)*)
};
}
#[cfg(not(feature = "gettext"))]
macro_rules! xlat_println {
($fmt: literal $(, $id: ident = $val: expr)* $(,)?) => {
println_ignore_io_error!($fmt $(, $id = $val)*)
};
}
#[cfg(feature = "gettext")]
mod gettext_sys {
#[cfg_attr(target_os = "freebsd", link(name = "intl"))]
unsafe extern "C" {
pub fn dgettext(
domain: *const libc::c_char,
msgid: *const libc::c_char,
) -> *mut libc::c_char;
pub fn bind_textdomain_codeset(
domain: *const libc::c_char,
codeset: *const libc::c_char,
) -> *mut libc::c_char;
}
}
#[cfg(test)]
mod test {
#[test]
#[cfg(feature = "gettext")]
fn it_works() {
use super::*;
textdomain(c"libc");
let input = c"Hello World";
assert_eq!(gettext(input), input.to_str().unwrap());
assert_eq!(gettext(input).as_ptr(), input.to_str().unwrap().as_ptr());
if std::env::var("LANG").unwrap_or_default().starts_with("nl") {
assert_eq!(xlat!("Operation not permitted"), "Actie is niet toegestaan");
}
}
#[test]
fn var_subst() {
assert_eq!(
xlat!("{hello} {world}", world = "world", hello = "hello"),
"hello world"
);
assert_eq!(xlat!("five = {five}", five = 5), "five = 5");
}
#[test]
#[cfg(feature = "gettext")]
fn str_optimized() {
use super::display::{Reference, Wrap};
#[allow(unused_imports)]
use super::display::Convert;
let foo: &str = "foo";
let addr = foo.as_ptr();
assert_eq!((&&Wrap(&foo)).display().as_ptr(), addr);
assert_eq!((&&Wrap(foo)).display().as_ptr(), addr);
let foo: String = "foo".to_string();
let addr = foo.as_ptr();
assert_eq!((&&Wrap(&foo)).display().as_ptr(), addr);
assert_eq!((&&Wrap(foo)).display().as_ptr(), addr);
let foo: Box<str> = "foo".to_string().into_boxed_str();
let addr = foo.as_ptr();
assert_eq!((&&Wrap(&foo)).display().as_ptr(), addr);
assert_eq!((&&Wrap(foo)).display().as_ptr(), addr);
use crate::common::SudoString;
let foo: SudoString = SudoString::new("foo".to_string()).unwrap();
let addr = foo.as_str().as_ptr();
assert_eq!((&&Wrap(&foo)).display().as_ptr(), addr);
assert_eq!((&&Wrap(foo)).display().as_ptr(), addr);
}
}