use locale_config::{LanguageRange, Locale};
use std::env;
use std::error;
use std::fmt;
use std::fs;
use std::path::PathBuf;
use super::{bind_textdomain_codeset, bindtextdomain, setlocale, textdomain, LocaleCategory};
#[derive(Debug)]
pub enum TextDomainError {
InvalidLocale(String),
TranslationNotFound(String),
TextDomainCallFailed(std::io::Error),
BindTextDomainCallFailed(std::io::Error),
BindTextDomainCodesetCallFailed(std::io::Error),
}
impl fmt::Display for TextDomainError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use TextDomainError::*;
match self {
InvalidLocale(locale) => write!(f, r#"Locale "{}" is invalid."#, locale),
TranslationNotFound(language) => {
write!(f, "Translations not found for language {}.", language)
}
TextDomainCallFailed(inner) => write!(f, "The call to textdomain() failed: {}", inner),
BindTextDomainCallFailed(inner) => {
write!(f, "The call to bindtextdomain() failed: {}", inner)
}
BindTextDomainCodesetCallFailed(inner) => {
write!(f, "The call to bind_textdomain_codeset() failed: {}", inner)
}
}
}
}
impl error::Error for TextDomainError {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
use TextDomainError::*;
match self {
InvalidLocale(_) => None,
TranslationNotFound(_) => None,
TextDomainCallFailed(inner) => Some(inner),
BindTextDomainCallFailed(inner) => Some(inner),
BindTextDomainCodesetCallFailed(inner) => Some(inner),
}
}
}
pub struct TextDomain {
domainname: String,
locale: Option<String>,
locale_category: LocaleCategory,
codeset: String,
pre_paths: Vec<PathBuf>,
post_paths: Vec<PathBuf>,
skip_system_data_paths: bool,
}
impl TextDomain {
pub fn new<S: Into<String>>(domainname: S) -> TextDomain {
TextDomain {
domainname: domainname.into(),
locale: None,
locale_category: LocaleCategory::LcMessages,
codeset: "UTF-8".to_string(),
pre_paths: vec![],
post_paths: vec![],
skip_system_data_paths: false,
}
}
pub fn locale(mut self, locale: &str) -> Self {
self.locale = Some(locale.to_owned());
self
}
pub fn locale_category(mut self, locale_category: LocaleCategory) -> Self {
self.locale_category = locale_category;
self
}
pub fn codeset<S: Into<String>>(mut self, codeset: S) -> Self {
self.codeset = codeset.into();
self
}
pub fn prepend<P: Into<PathBuf>>(mut self, path: P) -> Self {
self.pre_paths.push(path.into());
self
}
pub fn push<P: Into<PathBuf>>(mut self, path: P) -> Self {
self.post_paths.push(path.into());
self
}
pub fn skip_system_data_paths(mut self) -> Self {
self.skip_system_data_paths = true;
self
}
pub fn init(mut self) -> Result<Option<Vec<u8>>, TextDomainError> {
let (req_locale, norm_locale) = match self.locale.take() {
Some(req_locale) => {
if req_locale == "C" || req_locale == "POSIX" {
return Ok(Some(req_locale.as_bytes().to_owned()));
}
match LanguageRange::new(&req_locale) {
Ok(lang_range) => (req_locale.clone(), lang_range.into()),
Err(_) => {
match LanguageRange::from_unix(&req_locale) {
Ok(lang_range) => (req_locale.clone(), lang_range.into()),
Err(_) => {
return Err(TextDomainError::InvalidLocale(req_locale.clone()));
}
}
}
}
}
None => {
("".to_owned(), Locale::current())
}
};
let lang = norm_locale.as_ref().splitn(2, "-").collect::<Vec<&str>>()[0].to_owned();
let domainname = self.domainname;
let locale_category = self.locale_category;
let codeset = self.codeset;
let mo_rel_path = PathBuf::from("LC_MESSAGES").join(&format!("{}.mo", &domainname));
let sys_data_paths_str = if !self.skip_system_data_paths {
get_system_data_paths()
} else {
"".to_owned()
};
let sys_data_dirs_iter = env::split_paths(&sys_data_paths_str);
self.pre_paths
.into_iter()
.chain(sys_data_dirs_iter)
.chain(self.post_paths.into_iter())
.find(|path| {
let locale_path = path.join("locale");
if !locale_path.is_dir() {
return false;
}
if let Ok(entry_iter) = fs::read_dir(&locale_path) {
return entry_iter
.filter_map(|entry_res| entry_res.ok())
.filter(|entry| {
matches!(
entry.file_type().map(|ft| ft.is_dir() || ft.is_symlink()),
Ok(true)
)
})
.any(|entry| {
if let Some(entry_name) = entry.file_name().to_str() {
return entry_name.starts_with(&lang)
&& locale_path.join(entry_name).join(&mo_rel_path).exists();
}
false
});
}
false
})
.map_or(Err(TextDomainError::TranslationNotFound(lang)), |path| {
let result = setlocale(locale_category, req_locale);
bindtextdomain(domainname.clone(), path.join("locale"))
.map_err(TextDomainError::BindTextDomainCallFailed)?;
bind_textdomain_codeset(domainname.clone(), codeset)
.map_err(TextDomainError::BindTextDomainCodesetCallFailed)?;
textdomain(domainname).map_err(TextDomainError::TextDomainCallFailed)?;
Ok(result)
})
}
}
fn get_system_data_paths() -> String {
static DEFAULT: &str = "/usr/local/share/:/usr/share/";
if let Ok(dirs) = env::var("XDG_DATA_DIRS") {
if dirs.is_empty() {
DEFAULT.to_owned()
} else {
dirs
}
} else {
DEFAULT.to_owned()
}
}
impl fmt::Debug for TextDomain {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
let mut debug_struct = fmt.debug_struct("TextDomain");
debug_struct
.field("domainname", &self.domainname)
.field(
"locale",
&match self.locale.as_ref() {
Some(locale) => locale.to_owned(),
None => {
let cur_locale = Locale::current();
cur_locale.as_ref().to_owned()
}
},
)
.field("locale_category", &self.locale_category)
.field("codeset", &self.codeset)
.field("pre_paths", &self.pre_paths);
if !self.skip_system_data_paths {
debug_struct.field("using system data paths", &get_system_data_paths());
}
debug_struct.field("post_paths", &self.post_paths).finish()
}
}
#[cfg(test)]
mod tests {
use super::{LocaleCategory, TextDomain, TextDomainError};
#[test]
fn errors() {
match TextDomain::new("test").locale("(°_°)").init().err() {
Some(TextDomainError::InvalidLocale(message)) => assert_eq!(message, "(°_°)"),
_ => panic!(),
};
match TextDomain::new("0_0").locale("en_US").init().err() {
Some(TextDomainError::TranslationNotFound(message)) => assert_eq!(message, "en"),
_ => panic!(),
};
}
#[test]
fn attributes() {
let text_domain = TextDomain::new("test");
assert_eq!("test".to_owned(), text_domain.domainname);
assert!(text_domain.locale.is_none());
assert_eq!(LocaleCategory::LcMessages, text_domain.locale_category);
assert_eq!(text_domain.codeset, "UTF-8");
assert!(text_domain.pre_paths.is_empty());
assert!(text_domain.post_paths.is_empty());
assert!(!text_domain.skip_system_data_paths);
let text_domain = text_domain.locale_category(LocaleCategory::LcAll);
assert_eq!(LocaleCategory::LcAll, text_domain.locale_category);
let text_domain = text_domain.codeset("ISO-8859-15");
assert_eq!("ISO-8859-15", text_domain.codeset);
let text_domain = text_domain.prepend("pre");
assert!(!text_domain.pre_paths.is_empty());
let text_domain = text_domain.push("post");
assert!(!text_domain.post_paths.is_empty());
let text_domain = text_domain.skip_system_data_paths();
assert!(text_domain.skip_system_data_paths);
let text_domain = TextDomain::new("test").locale("en_US");
assert_eq!(Some("en_US".to_owned()), text_domain.locale);
match TextDomain::new("0_0").locale("en_US").init().err() {
Some(TextDomainError::TranslationNotFound(message)) => assert_eq!(message, "en"),
_ => panic!(),
};
}
}