extern crate locale_config;
extern crate gettext_sys as ffi;
use std::ffi::CStr;
use std::ffi::CString;
use std::io;
use std::os::raw::c_ulong;
use std::path::PathBuf;
mod macros;
mod text_domain;
pub use text_domain::{TextDomain, TextDomainError};
pub mod getters;
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum LocaleCategory {
LcCType = 0,
LcNumeric = 1,
LcTime = 2,
LcCollate = 3,
LcMonetary = 4,
LcMessages = 5,
LcAll = 6,
LcPaper = 7,
LcName = 8,
LcAddress = 9,
LcTelephone = 10,
LcMeasurement = 11,
LcIdentification = 12,
}
pub fn gettext<T: Into<String>>(msgid: T) -> String {
let msgid = CString::new(msgid.into()).expect("`msgid` contains an internal 0 byte");
unsafe {
CStr::from_ptr(ffi::gettext(msgid.as_ptr()))
.to_str()
.expect("gettext() returned invalid UTF-8")
.to_owned()
}
}
pub fn dgettext<T, U>(domainname: T, msgid: U) -> String
where
T: Into<String>,
U: Into<String>,
{
let domainname =
CString::new(domainname.into()).expect("`domainname` contains an internal 0 byte");
let msgid = CString::new(msgid.into()).expect("`msgid` contains an internal 0 byte");
unsafe {
CStr::from_ptr(ffi::dgettext(domainname.as_ptr(), msgid.as_ptr()))
.to_str()
.expect("dgettext() returned invalid UTF-8")
.to_owned()
}
}
pub fn dcgettext<T, U>(domainname: T, msgid: U, category: LocaleCategory) -> String
where
T: Into<String>,
U: Into<String>,
{
let domainname =
CString::new(domainname.into()).expect("`domainname` contains an internal 0 byte");
let msgid = CString::new(msgid.into()).expect("`msgid` contains an internal 0 byte");
unsafe {
CStr::from_ptr(ffi::dcgettext(
domainname.as_ptr(),
msgid.as_ptr(),
category as i32,
))
.to_str()
.expect("dcgettext() returned invalid UTF-8")
.to_owned()
}
}
pub fn ngettext<T, S>(msgid: T, msgid_plural: S, n: u32) -> String
where
T: Into<String>,
S: Into<String>,
{
let msgid = CString::new(msgid.into()).expect("`msgid` contains an internal 0 byte");
let msgid_plural =
CString::new(msgid_plural.into()).expect("`msgid_plural` contains an internal 0 byte");
unsafe {
CStr::from_ptr(ffi::ngettext(
msgid.as_ptr(),
msgid_plural.as_ptr(),
n as c_ulong,
))
.to_str()
.expect("ngettext() returned invalid UTF-8")
.to_owned()
}
}
pub fn dngettext<T, U, V>(domainname: T, msgid: U, msgid_plural: V, n: u32) -> String
where
T: Into<String>,
U: Into<String>,
V: Into<String>,
{
let domainname =
CString::new(domainname.into()).expect("`domainname` contains an internal 0 byte");
let msgid = CString::new(msgid.into()).expect("`msgid` contains an internal 0 byte");
let msgid_plural =
CString::new(msgid_plural.into()).expect("`msgid_plural` contains an internal 0 byte");
unsafe {
CStr::from_ptr(ffi::dngettext(
domainname.as_ptr(),
msgid.as_ptr(),
msgid_plural.as_ptr(),
n as c_ulong,
))
.to_str()
.expect("dngettext() returned invalid UTF-8")
.to_owned()
}
}
pub fn dcngettext<T, U, V>(
domainname: T,
msgid: U,
msgid_plural: V,
n: u32,
category: LocaleCategory,
) -> String
where
T: Into<String>,
U: Into<String>,
V: Into<String>,
{
let domainname =
CString::new(domainname.into()).expect("`domainname` contains an internal 0 byte");
let msgid = CString::new(msgid.into()).expect("`msgid` contains an internal 0 byte");
let msgid_plural =
CString::new(msgid_plural.into()).expect("`msgid_plural` contains an internal 0 byte");
unsafe {
CStr::from_ptr(ffi::dcngettext(
domainname.as_ptr(),
msgid.as_ptr(),
msgid_plural.as_ptr(),
n as c_ulong,
category as i32,
))
.to_str()
.expect("dcngettext() returned invalid UTF-8")
.to_owned()
}
}
pub fn textdomain<T: Into<Vec<u8>>>(domainname: T) -> Result<Vec<u8>, io::Error> {
let domainname = CString::new(domainname).expect("`domainname` contains an internal 0 byte");
unsafe {
let result = ffi::textdomain(domainname.as_ptr());
if result.is_null() {
Err(io::Error::last_os_error())
} else {
Ok(CStr::from_ptr(result).to_bytes().to_owned())
}
}
}
pub fn bindtextdomain<T, U>(domainname: T, dirname: U) -> Result<PathBuf, io::Error>
where
T: Into<Vec<u8>>,
U: Into<PathBuf>,
{
let domainname = CString::new(domainname).expect("`domainname` contains an internal 0 byte");
let dirname = dirname.into().into_os_string();
#[cfg(windows)]
{
use std::ffi::OsString;
use std::os::windows::ffi::{OsStrExt, OsStringExt};
let mut dirname: Vec<u16> = dirname.encode_wide().collect();
if dirname.contains(&0) {
panic!("`dirname` contains an internal 0 byte");
}
dirname.push(0);
unsafe {
let mut ptr = ffi::wbindtextdomain(domainname.as_ptr(), dirname.as_ptr());
if ptr.is_null() {
Err(io::Error::last_os_error())
} else {
let mut result = vec![];
while *ptr != 0_u16 {
result.push(*ptr);
ptr = ptr.offset(1);
}
Ok(PathBuf::from(OsString::from_wide(&result)))
}
}
}
#[cfg(not(windows))]
{
use std::ffi::OsString;
use std::os::unix::ffi::OsStringExt;
let dirname = dirname.into_vec();
let dirname = CString::new(dirname).expect("`dirname` contains an internal 0 byte");
unsafe {
let result = ffi::bindtextdomain(domainname.as_ptr(), dirname.as_ptr());
if result.is_null() {
Err(io::Error::last_os_error())
} else {
let result = CStr::from_ptr(result);
Ok(PathBuf::from(OsString::from_vec(
result.to_bytes().to_vec(),
)))
}
}
}
}
pub fn setlocale<T: Into<Vec<u8>>>(category: LocaleCategory, locale: T) -> Option<Vec<u8>> {
let c = CString::new(locale).expect("`locale` contains an internal 0 byte");
unsafe {
let ret = ffi::setlocale(category as i32, c.as_ptr());
if ret.is_null() {
None
} else {
Some(CStr::from_ptr(ret).to_bytes().to_owned())
}
}
}
pub fn bind_textdomain_codeset<T, U>(domainname: T, codeset: U) -> Result<Option<String>, io::Error>
where
T: Into<Vec<u8>>,
U: Into<String>,
{
let domainname = CString::new(domainname).expect("`domainname` contains an internal 0 byte");
let codeset = CString::new(codeset.into()).expect("`codeset` contains an internal 0 byte");
unsafe {
let result = ffi::bind_textdomain_codeset(domainname.as_ptr(), codeset.as_ptr());
if result.is_null() {
let error = io::Error::last_os_error();
if let Some(0) = error.raw_os_error() {
return Ok(None);
} else {
return Err(error);
}
} else {
let result = CStr::from_ptr(result)
.to_str()
.expect("`bind_textdomain_codeset()` returned non-UTF-8 string")
.to_owned();
Ok(Some(result))
}
}
}
static CONTEXT_SEPARATOR: char = '\x04';
fn build_context_id(ctxt: &str, msgid: &str) -> String {
format!("{}{}{}", ctxt, CONTEXT_SEPARATOR, msgid)
}
fn panic_on_zero_in_ctxt(msgctxt: &str) {
if msgctxt.contains('\0') {
panic!("`msgctxt` contains an internal 0 byte");
}
}
pub fn pgettext<T, U>(msgctxt: T, msgid: U) -> String
where
T: Into<String>,
U: Into<String>,
{
let msgctxt = msgctxt.into();
panic_on_zero_in_ctxt(&msgctxt);
let msgid = msgid.into();
let text = build_context_id(&msgctxt, &msgid);
let translation = gettext(text);
if translation.contains(CONTEXT_SEPARATOR as char) {
return gettext(msgid);
}
translation
}
pub fn npgettext<T, U, V>(msgctxt: T, msgid: U, msgid_plural: V, n: u32) -> String
where
T: Into<String>,
U: Into<String>,
V: Into<String>,
{
let msgctxt = msgctxt.into();
panic_on_zero_in_ctxt(&msgctxt);
let singular_msgid = msgid.into();
let plural_msgid = msgid_plural.into();
let singular_ctxt = build_context_id(&msgctxt, &singular_msgid);
let plural_ctxt = build_context_id(&msgctxt, &plural_msgid);
let translation = ngettext(singular_ctxt, plural_ctxt, n);
if translation.contains(CONTEXT_SEPARATOR as char) {
return ngettext(singular_msgid, plural_msgid, n);
}
translation
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn smoke_test() {
setlocale(LocaleCategory::LcAll, "en_US.UTF-8");
bindtextdomain("hellorust", "/usr/local/share/locale").unwrap();
textdomain("hellorust").unwrap();
assert_eq!("Hello, world!", gettext("Hello, world!"));
}
#[test]
fn plural_test() {
setlocale(LocaleCategory::LcAll, "en_US.UTF-8");
bindtextdomain("hellorust", "/usr/local/share/locale").unwrap();
textdomain("hellorust").unwrap();
assert_eq!(
"Hello, world!",
ngettext("Hello, world!", "Hello, worlds!", 1)
);
assert_eq!(
"Hello, worlds!",
ngettext("Hello, world!", "Hello, worlds!", 2)
);
}
#[test]
fn context_test() {
setlocale(LocaleCategory::LcAll, "en_US.UTF-8");
bindtextdomain("hellorust", "/usr/local/share/locale").unwrap();
textdomain("hellorust").unwrap();
assert_eq!("Hello, world!", pgettext("context", "Hello, world!"));
}
#[test]
fn plural_context_test() {
setlocale(LocaleCategory::LcAll, "en_US.UTF-8");
bindtextdomain("hellorust", "/usr/local/share/locale").unwrap();
textdomain("hellorust").unwrap();
assert_eq!(
"Hello, world!",
npgettext("context", "Hello, world!", "Hello, worlds!", 1)
);
assert_eq!(
"Hello, worlds!",
npgettext("context", "Hello, world!", "Hello, worlds!", 2)
);
}
#[test]
#[should_panic(expected = "`msgid` contains an internal 0 byte")]
fn gettext_panics() {
gettext("input string\0");
}
#[test]
#[should_panic(expected = "`domainname` contains an internal 0 byte")]
fn dgettext_panics_on_zero_in_domainname() {
dgettext("hello\0world!", "hi");
}
#[test]
#[should_panic(expected = "`msgid` contains an internal 0 byte")]
fn dgettext_panics_on_zero_in_msgid() {
dgettext("hello world", "another che\0ck");
}
#[test]
#[should_panic(expected = "`domainname` contains an internal 0 byte")]
fn dcgettext_panics_on_zero_in_domainname() {
dcgettext("a diff\0erent input", "hello", LocaleCategory::LcAll);
}
#[test]
#[should_panic(expected = "`msgid` contains an internal 0 byte")]
fn dcgettext_panics_on_zero_in_msgid() {
dcgettext("world", "yet \0 another\0 one", LocaleCategory::LcMessages);
}
#[test]
#[should_panic(expected = "`msgid` contains an internal 0 byte")]
fn ngettext_panics_on_zero_in_msgid() {
ngettext("singular\0form", "plural form", 10);
}
#[test]
#[should_panic(expected = "`msgid_plural` contains an internal 0 byte")]
fn ngettext_panics_on_zero_in_msgid_plural() {
ngettext("singular form", "plural\0form", 0);
}
#[test]
#[should_panic(expected = "`domainname` contains an internal 0 byte")]
fn dngettext_panics_on_zero_in_domainname() {
dngettext("do\0main", "one", "many", 0);
}
#[test]
#[should_panic(expected = "`msgid` contains an internal 0 byte")]
fn dngettext_panics_on_zero_in_msgid() {
dngettext("domain", "just a\0 single one", "many", 100);
}
#[test]
#[should_panic(expected = "`msgid_plural` contains an internal 0 byte")]
fn dngettext_panics_on_zero_in_msgid_plural() {
dngettext("d", "1", "many\0many\0many more", 10000);
}
#[test]
#[should_panic(expected = "`domainname` contains an internal 0 byte")]
fn dcngettext_panics_on_zero_in_domainname() {
dcngettext(
"doma\0in",
"singular",
"plural",
42,
LocaleCategory::LcCType,
);
}
#[test]
#[should_panic(expected = "`msgid` contains an internal 0 byte")]
fn dcngettext_panics_on_zero_in_msgid() {
dcngettext("domain", "\0ne", "plural", 13, LocaleCategory::LcNumeric);
}
#[test]
#[should_panic(expected = "`msgid_plural` contains an internal 0 byte")]
fn dcngettext_panics_on_zero_in_msgid_plural() {
dcngettext("d-o-m-a-i-n", "one", "a\0few", 0, LocaleCategory::LcTime);
}
#[test]
#[should_panic(expected = "`domainname` contains an internal 0 byte")]
fn textdomain_panics_on_zero_in_domainname() {
textdomain("this is \0 my domain").unwrap();
}
#[test]
#[should_panic(expected = "`domainname` contains an internal 0 byte")]
fn bindtextdomain_panics_on_zero_in_domainname() {
bindtextdomain("\0bind this", "/usr/share/locale").unwrap();
}
#[test]
#[should_panic(expected = "`dirname` contains an internal 0 byte")]
fn bindtextdomain_panics_on_zero_in_dirname() {
bindtextdomain("my_domain", "/opt/locales\0").unwrap();
}
#[test]
#[should_panic(expected = "`locale` contains an internal 0 byte")]
fn setlocale_panics_on_zero_in_locale() {
setlocale(LocaleCategory::LcCollate, "en_\0US");
}
#[test]
#[should_panic(expected = "`domainname` contains an internal 0 byte")]
fn bind_textdomain_codeset_panics_on_zero_in_domainname() {
bind_textdomain_codeset("doma\0in", "UTF-8").unwrap();
}
#[test]
#[should_panic(expected = "`codeset` contains an internal 0 byte")]
fn bind_textdomain_codeset_panics_on_zero_in_codeset() {
bind_textdomain_codeset("name", "K\0I8-R").unwrap();
}
#[test]
#[should_panic(expected = "`msgctxt` contains an internal 0 byte")]
fn pgettext_panics_on_zero_in_msgctxt() {
pgettext("context\0", "string");
}
#[test]
#[should_panic(expected = "`msgid` contains an internal 0 byte")]
fn pgettext_panics_on_zero_in_msgid() {
pgettext("ctx", "a message\0to be translated");
}
#[test]
#[should_panic(expected = "`msgctxt` contains an internal 0 byte")]
fn npgettext_panics_on_zero_in_msgctxt() {
npgettext("c\0tx", "singular", "plural", 0);
}
#[test]
#[should_panic(expected = "`msgid` contains an internal 0 byte")]
fn npgettext_panics_on_zero_in_msgid() {
npgettext("ctx", "sing\0ular", "many many more", 135626);
}
#[test]
#[should_panic(expected = "`msgid_plural` contains an internal 0 byte")]
fn npgettext_panics_on_zero_in_msgid_plural() {
npgettext("context", "uno", "one \0fewer", 10585);
}
}