gettext-rs-0.4.1/.gitignore010064400007650000024000000004231330117773000140320ustar0000000000000000# Compiled files *.o *.so *.rlib *.dll # Executables *.exe .idea # Generated by Cargo **/target/ # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock Cargo.lock gettext-rs-0.4.1/.travis.yml010064400007650000024000000037271321413515100141570ustar0000000000000000sudo: required language: rust dist: trusty services: - docker matrix: include: - os: linux rust: stable env: TARGET=x86_64-unknown-linux-gnu DOCKER=linux64 NO_ADD=1 - os: linux rust: stable env: TARGET=i686-unknown-linux-gnu DOCKER=linux32 - os: linux rust: stable env: TARGET=x86_64-unknown-linux-musl DOCKER=musl - os: linux rust: stable env: TARGET=x86_64-unknown-linux-gnu DOCKER=linux64-gettext NO_ADD=1 GETTEXT_SYSTEM=1 - os: linux rust: stable env: TARGET=i686-unknown-linux-gnu DOCKER=linux32-gettext GETTEXT_SYSTEM=1 - os: linux rust: stable env: TARGET=x86_64-unknown-linux-gnu DOCKER=linux64-build NO_ADD=1 GETTEXT_DIR=/result - os: linux rust: stable env: TARGET=x86_64-unknown-linux-gnu DOCKER=linux64-build NO_ADD=1 GETTEXT_BIN_DIR=/result/bin GETTEXT_LIB_DIR=/result/lib GETTEXT_INCLUDE_DIR=/result/include - os: osx rust: stable env: TARGET=x86_64-apple-darwin NO_ADD=1 HOMEBREW=1 GETTEXT_DIR=/usr/local/opt/gettext - os: osx rust: stable env: TARGET=x86_64-apple-darwin NO_ADD=1 - os: linux rust: beta env: TARGET=x86_64-unknown-linux-gnu DOCKER=linux64 NO_ADD=1 - os: linux rust: nightly env: TARGET=x86_64-unknown-linux-gnu DOCKER=linux64 NO_ADD=1 install: - if [ -z "$NO_ADD" ]; then rustup target add $TARGET; fi - if [ -n "$HOMEBREW" ]; then brew install gettext; fi script: - cargo generate-lockfile - cargo generate-lockfile --manifest-path systest/Cargo.toml - if [ -z "$DOCKER" ]; then sh ci/run.sh; else mkdir .cargo target; docker build -t rust -f ci/Dockerfile-$DOCKER ci; docker run -w /src -v `pwd`:/src:ro -v `pwd`/target:/src/target -v `rustc --print sysroot`:/usr/local:ro -e TARGET -e NO_RUN -e GETTEXT_SYSTEM -e CARGO_TARGET_DIR=/src/target -it rust sh ci/run.sh; fi gettext-rs-0.4.1/Cargo.toml.orig010064400007650000024000000012701333546322000147310ustar0000000000000000[package] name = "gettext-rs" description = "GNU Gettext FFI binding for Rust" version = "0.4.1" authors = ["Konstantin Salikhov "] repository = "https://github.com/Koka/gettext-rs" documentation = "http://koka.github.io/gettext-rs/gettextrs/" homepage = "https://github.com/Koka/gettext-rs" readme = "README.md" keywords = ["gettext", "binding", "ffi", "i18n", "l10n"] license = "MIT" exclude = [ "systest/*", "ci/*", "gettext-sys/*", ] [lib] name = "gettextrs" [features] gettext-system = ["gettext-sys/gettext-system"] [dependencies.gettext-sys] version = "0.19.8" path = "gettext-sys" [dependencies] locale_config = "0.2" [workspace] members = ["systest"] gettext-rs-0.4.1/Cargo.toml0000644000000022030000000000000111760ustar00# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g. crates.io) dependencies # # If you believe there's an error in this file please file an # issue against the rust-lang/cargo repository. If you're # editing this file be aware that the upstream Cargo.toml # will likely look very different (and much more reasonable) [package] name = "gettext-rs" version = "0.4.1" authors = ["Konstantin Salikhov "] exclude = ["systest/*", "ci/*", "gettext-sys/*"] description = "GNU Gettext FFI binding for Rust" homepage = "https://github.com/Koka/gettext-rs" documentation = "http://koka.github.io/gettext-rs/gettextrs/" readme = "README.md" keywords = ["gettext", "binding", "ffi", "i18n", "l10n"] license = "MIT" repository = "https://github.com/Koka/gettext-rs" [lib] name = "gettextrs" [dependencies.gettext-sys] version = "0.19.8" [dependencies.locale_config] version = "0.2" [features] gettext-system = ["gettext-sys/gettext-system"] gettext-rs-0.4.1/LICENSE.txt010064400007650000024000000021011321413515100136520ustar0000000000000000The MIT License (MIT) Copyright (c) 2016 Konstantin V. Salikhov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. gettext-rs-0.4.1/README.md010064400007650000024000000071511333546316000133300ustar0000000000000000# gettext-rs GNU [Gettext](https://www.gnu.org/software/gettext/) FFI binding for Rust [![https://travis-ci.org/Koka/gettext-rs](https://travis-ci.org/Koka/gettext-rs.svg?branch=master)](https://travis-ci.org/Koka/gettext-rs) gettext-rs: [![https://crates.io/crates/gettext-rs](https://meritbadge.herokuapp.com/gettext-rs?v2)](https://crates.io/crates/gettext-rs) [![Docs](https://docs.rs/gettext-rs/badge.svg)](https://docs.rs/gettext-rs) gettext-sys: [![https://crates.io/crates/gettext-sys](https://meritbadge.herokuapp.com/gettext-sys?v2)](https://crates.io/crates/gettext-sys) [![Docs](https://docs.rs/gettext-sys/badge.svg)](https://docs.rs/gettext-sys) Usage: ```rust use gettextrs::*; setlocale(LocaleCategory::LcAll, "en_US.UTF-8"); bindtextdomain("hellorust", "/usr/local/share/locale"); textdomain("hellorust"); println!("Translated: {}", gettext("Hello, world!")); println!("Singular: {}", ngettext("One thing", "Multiple things", 1)); println!("Plural: {}", ngettext("One thing", "Multiple things", 2)); println!("With context: {}", pgettext("This is the context", "Hello, world!")); println!("Plural with context: {}", npgettext("This is the context", "One thing", "Multiple things", 2)); ``` Alternatively, you can initialize the locale and text domain using the `TextDomain` builder. By default, a translation of the specified text domain in current language is searched in the system's data paths. See `TextDomain`'s documentation for other options. ```rust use gettextrs::TextDomain; TextDomain::new("hellorust") .init() .unwrap(); ``` ## Manual configuration gettext.rs's build script will by default build its own version of gettext and statically link against that. If that is not what you want the build script can be configured via environment variables: GETTEXT_SYSTEM - If specified gettext-sys uses the gettext that is part of glibc. This only works on linux and Windows + GNU (e.g. [MSYS2](http://www.msys2.org/)). On Windows, you will need to install `gettext-devel`. You can also activate the `gettext-system` feature from your `Cargo.toml` configuration: ``` toml gettext-rs = { git = "https://github.com/Koka/gettext-rs", features = ["gettext-system"] } ``` GETTEXT_DIR - If specified, a directory that will be used to find gettext installation. It's expected that under this directory the include folder has header files, the bin folder has gettext binary and a lib folder has the runtime libraries. GETTEXT_LIB_DIR - If specified, a directory that will be used to find gettext libraries. Overrides the lib folder implied by GETTEXT_DIR (if specified). GETTEXT_INCLUDE_DIR - If specified, a directory that will be used to find gettext header files. Overrides the include folder implied by GETTEXT_DIR (if specified). GETTEXT_BIN_DIR - If specified, a directory that will be used to find gettext binaries. Overrides the bin folder implied by GETTEXT_DIR (if specified). GETTEXT_STATIC - If specified, gettext libraries will be statically rather than dynamically linked. For target-specific configuration, each of these environment variables can be prefixed by an upper-cased target, for example, X86_64_UNKNOWN_LINUX_GNU_GETTEXT_DIR. This can be useful in cross compilation contexts. Note: on Windows + GNU, if you want to build `gettext-rs` with its own static version of `getttext`, install the following packages first: ``` pacman --noconfirm -S base-devel mingw-w64-x86_64-gcc libxml2-devel tar ``` The build is quite long. You can speed things up by setting (e.g. for 4 cores): ``` export NUM_JOBS=5 ``` This doesn't work on AppVeyor ATM. Use `SET GETTEXT_SYSTEM=true` instead. gettext-rs-0.4.1/src/lib.rs010064400007650000024000000202501333546316000137470ustar0000000000000000//! # gettext C library FFI binding for Rust //! //! Usage: //! //! ``` //! use gettextrs::*; //! //! setlocale(LocaleCategory::LcAll, "en_US.UTF-8"); //! //! bindtextdomain("hellorust", "/usr/local/share/locale"); //! textdomain("hellorust"); //! //! println!("Translated: {}", gettext("Hello, world!")); //! println!("Singular: {}", ngettext("One thing", "Multiple things", 1)); //! println!("Plural: {}", ngettext("One thing", "Multiple things", 2)); //! ``` //! //! Alternatively, you can initialize the locale and text domain using the [`TextDomain`] builder. //! By default, a translation of the specified text domain in current language is searched in //! the system's data paths. See [`TextDomain`]'s documentation for other options. //! //! ```no_run //! use gettextrs::TextDomain; //! //! TextDomain::new("hellorust") //! .init() //! .unwrap(); //! ``` //! //! [`TextDomain`]: struct.TextDomain.html extern crate locale_config; extern crate gettext_sys as ffi; use std::ffi::CString; use std::ffi::CStr; use std::os::raw::c_ulong; mod text_domain; pub use text_domain::{TextDomain, TextDomainError}; /// Locale category enum ported from locale.h #[derive(Debug, PartialEq)] pub enum LocaleCategory { /// Character classification and case conversion. LcCType = 0, /// Non-monetary numeric formats. LcNumeric = 1, /// Date and time formats. LcTime = 2, /// Collation order. LcCollate = 3, /// Monetary formats. LcMonetary = 4, /// Formats of informative and diagnostic messages and interactive responses. LcMessages = 5, /// For all. LcAll = 6, /// Paper size. LcPaper = 7, /// Name formats. LcName = 8, /// Address formats and location information. LcAddress = 9, /// Telephone number formats. LcTelephone = 10, /// Measurement units (Metric or Other). LcMeasurement = 11, /// Metadata about the locale information. LcIdentification = 12, } /// Translate msgid to localized message from default domain pub fn gettext>>(s: T) -> String { unsafe { CStr::from_ptr(ffi::gettext(CString::new(s).unwrap().as_ptr())) .to_string_lossy() .into_owned() } } /// Translate msgid to localized message from specified domain pub fn dgettext>>(domain: T, s: T) -> String { unsafe { CStr::from_ptr(ffi::dgettext(CString::new(domain).unwrap().as_ptr(), CString::new(s).unwrap().as_ptr())) .to_string_lossy() .into_owned() } } /// Translate msgid to localized message from specified domain using custom locale category pub fn dcgettext>>(domain: T, s: T, category: LocaleCategory) -> String { unsafe { CStr::from_ptr(ffi::dcgettext(CString::new(domain).unwrap().as_ptr(), CString::new(s).unwrap().as_ptr(), category as i32)) .to_string_lossy() .into_owned() } } /// Translate msgid to localized message from default domain (with plural support) pub fn ngettext>>(singular: T, plural : T, n : u32) -> String { unsafe { CStr::from_ptr(ffi::ngettext(CString::new(singular).unwrap().as_ptr(), CString::new(plural).unwrap().as_ptr(), n as c_ulong)) .to_string_lossy() .into_owned() } } /// Translate msgid to localized message from specified domain (with plural support) pub fn dngettext>>(domain: T, singular: T, plural: T, n : u32) -> String { unsafe { CStr::from_ptr(ffi::dngettext(CString::new(domain).unwrap().as_ptr(), CString::new(singular).unwrap().as_ptr(), CString::new(plural).unwrap().as_ptr(), n as c_ulong)) .to_string_lossy() .into_owned() } } /// Translate msgid to localized message from specified domain using custom locale category (with plural support) pub fn dcngettext>>(domain: T, singular: T, plural: T, n : u32, category: LocaleCategory) -> String { unsafe { CStr::from_ptr(ffi::dcngettext(CString::new(domain).unwrap().as_ptr(), CString::new(singular).unwrap().as_ptr(), CString::new(plural).unwrap().as_ptr(), n as c_ulong, category as i32)) .to_string_lossy() .into_owned() } } /// Switch to specific text domain pub fn textdomain>>(domain: T) -> String { unsafe { CStr::from_ptr(ffi::textdomain(CString::new(domain).unwrap().as_ptr())) .to_string_lossy() .into_owned() } } /// Bind text domain to some directory containing gettext MO files pub fn bindtextdomain>>(domain: T, dir: T) -> String { unsafe { CStr::from_ptr(ffi::bindtextdomain(CString::new(domain).unwrap().as_ptr(), CString::new(dir).unwrap().as_ptr())) .to_string_lossy() .into_owned() } } /// Set current locale for translations pub fn setlocale>>(category: LocaleCategory, locale: T) -> Option { let c = CString::new(locale).unwrap(); unsafe { let ret = ffi::setlocale(category as i32, c.as_ptr()); if ret.is_null() { None } else { Some(CStr::from_ptr(ret).to_string_lossy().into_owned()) } } } pub fn bind_textdomain_codeset>>(domain: T, codeset: T) -> String { unsafe { CStr::from_ptr(ffi::bind_textdomain_codeset(CString::new(domain).unwrap().as_ptr(), CString::new(codeset).unwrap().as_ptr())) .to_string_lossy() .into_owned() } } static CONTEXT_SEPARATOR: u8 = b'\x04'; fn build_context_id(ctx: &Vec, s: &Vec) -> String { let mut text: Vec = vec![]; text.extend(ctx.iter().cloned()); text.push(CONTEXT_SEPARATOR); text.extend(s.iter().cloned()); CString::new(text).unwrap().to_string_lossy().into_owned() } /// Translate msgid to localized message from default domain (with context support) pub fn pgettext>>(ctx: T, s: T) -> String { let msgid = s.into(); let text = build_context_id(&ctx.into(), &msgid); let trans = gettext(text); if trans.contains(CONTEXT_SEPARATOR as char) { return gettext(msgid); //return CString::new(msgid).unwrap().to_string_lossy().into_owned(); } trans } /// Translate msgid to localized message from default domain (with plural support and context /// support) pub fn npgettext>>(ctx: T, singular: T, plural: T, n: u32) -> String { let ctx = ctx.into(); let singular_msgid = singular.into(); let plural_msgid = plural.into(); let singular_ctx = build_context_id(&ctx, &singular_msgid); let plural_ctx = build_context_id(&ctx, &plural_msgid); let trans = ngettext(singular_ctx, plural_ctx, n); if trans.contains(CONTEXT_SEPARATOR as char) { return ngettext(singular_msgid, plural_msgid, n); } trans } #[cfg(test)] mod tests { use super::*; #[test] fn smoke_test() { setlocale(LocaleCategory::LcAll, "en_US.UTF-8"); bindtextdomain("hellorust", "/usr/local/share/locale"); textdomain("hellorust"); assert_eq!("Hello, world!", gettext("Hello, world!")); } #[test] fn plural_test() { setlocale(LocaleCategory::LcAll, "en_US.UTF-8"); bindtextdomain("hellorust", "/usr/local/share/locale"); textdomain("hellorust"); 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"); textdomain("hellorust"); 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"); textdomain("hellorust"); assert_eq!("Hello, world!", npgettext("context", "Hello, world!", "Hello, worlds!", 1)); assert_eq!("Hello, worlds!", npgettext("context", "Hello, world!", "Hello, worlds!", 2)); } } gettext-rs-0.4.1/src/text_domain.rs010064400007650000024000000364231330104250300155100ustar0000000000000000use locale_config::{LanguageRange, Locale}; use std::default::Default; use std::env; use std::fmt; use std::fs; use std::path::PathBuf; use super::{bind_textdomain_codeset, bindtextdomain, setlocale, textdomain, LocaleCategory}; #[derive(Debug, PartialEq)] pub enum TextDomainError { /// The locale is malformed. InvalidLocale(String), /// The translation for the requested language could not be found or the search path is empty. TranslationNotFound(String), } /// A text domain initializer builder which finds the path to bind by searching translations /// in the system data paths and optionally in user specified paths. /// /// `TextDomain` takes care of calling the [`setlocale`], [`bindtextdomain`], /// [`bind_textdomain_codeset`] and [`textdomain`]. /// /// /// # Defaults /// /// - Current user's locale is selected by default. You can override this behaviour by calling /// [`locale`]. /// - [`LocaleCategory::LcMessages`] is used when calling [`setlocale`]. Use [`locale_category`] /// to override. /// - [`bind_textdomain_codeset`] is not invoked by default. Use [`codeset`] to specify a `codeset`. /// - System data paths are searched by default (see below for details). Use /// [`skip_system_data_paths`] to limit the search to user provided paths. /// /// # Text domain path binding /// /// A translation file for the text domain is searched in the following paths (in order): /// /// 1. Paths added using the [`prepend`] function. /// 1. Paths from the `XDG_DATA_DIRS` environment variable, except if the function /// [`skip_system_data_paths`] was invoked. If `XDG_DATA_DIRS` is not defined, current path is used. /// 1. Paths added using the [`push`] function. /// /// For each `path` in the search paths, the following subdirectories are scanned: /// `path/locale/lang*/LC_MESSAGES` (where `lang` is the language part of the selected locale). /// The first `path` containing a file matching `name.mo` is used for the call to /// [`bindtextdomain`]. /// /// # Examples /// /// Basic usage: /// /// ```no_run /// use gettextrs::TextDomain; /// /// TextDomain::new("my_textdomain") /// .init() /// .unwrap(); /// ``` /// /// Use the translation in current language under the `target` directory if available, otherwise /// search system defined paths: /// /// ```no_run /// use gettextrs::TextDomain; /// /// TextDomain::new("my_textdomain") /// .prepend("target") /// .init() /// .unwrap(); /// ``` /// /// Scan the `target` directory only, force locale to `fr_FR` and handle errors: /// /// ```no_run /// use gettextrs::{TextDomain, TextDomainError}; /// /// let init_msg = match TextDomain::new("my_textdomain") /// .skip_system_data_paths() /// .push("target") /// .locale("fr_FR") /// .init() /// { /// Ok(locale) => { /// format!("translation found, `setlocale` returned {:?}", locale) /// } /// Err(TextDomainError::TranslationNotFound(lang)) => { /// format!("translation not found for {}", lang) /// } /// Err(TextDomainError::InvalidLocale(locale)) => { /// format!("invalid locale {}", locale) /// } /// }; /// println!("Textdomain init result: {}", init_msg); /// ``` /// /// [`setlocale`]: fn.setlocale.html /// [`bindtextdomain`]: fn.bindtextdomain.html /// [`bind_textdomain_codeset`]: fn.bind_textdomain_codeset.html /// [`textdomain`]: fn.textdomain.html /// [`LocaleCategory::LcMessages`]: enum.LocaleCategory.html#variant.LcMessages /// [`locale`]: struct.TextDomain.html#method.locale /// [`locale_category`]: struct.TextDomain.html#method.locale_category /// [`codeset`]: struct.TextDomain.html#method.codeset /// [`skip_system_data_paths`]: struct.TextDomain.html#method.skip_system_data_paths /// [`prepend`]: struct.TextDomain.html#method.prepend /// [`push`]: struct.TextDomain.html#method.push #[derive(Default)] pub struct TextDomain { name: Option, locale: Option, locale_category: Option, codeset: Option, pre_paths: Vec, post_paths: Vec, skip_system_data_paths: bool, } impl TextDomain { /// Creates a new builder instance of `TextDomain` with the specified `name`. /// /// # Examples /// /// ```no_run /// use gettextrs::TextDomain; /// /// let text_domain = TextDomain::new("my_textdomain"); /// ``` pub fn new>(name: S) -> TextDomain { TextDomain { name: Some(name.into()), locale_category: Some(LocaleCategory::LcMessages), ..TextDomain::default() } } /// Override the `locale` for the `TextDomain`. Default is to use current locale. /// /// # Examples /// /// ```no_run /// use gettextrs::TextDomain; /// /// let text_domain = TextDomain::new("my_textdomain") /// .locale("fr_FR.UTF-8"); /// ``` pub fn locale(mut self, locale: &str) -> Self { self.locale = Some(locale.to_owned()); self } /// Override the `locale_category`. Default is [`LocaleCategory::LcMessages`]. /// /// # Examples /// /// ```no_run /// use gettextrs::{LocaleCategory, TextDomain}; /// /// let text_domain = TextDomain::new("my_textdomain") /// .locale_category(LocaleCategory::LcAll); /// ``` /// /// [`LocaleCategory::LcMessages`]: enum.LocaleCategory.html#variant.LcMessages pub fn locale_category(mut self, locale_category: LocaleCategory) -> Self { self.locale_category = Some(locale_category); self } /// Define the `codeset` that will be used for calling [`bind_textdomain_codeset`]. /// The default is to **not** call [`bind_textdomain_codeset`]. /// /// # Examples /// /// ```no_run /// use gettextrs::TextDomain; /// /// let text_domain = TextDomain::new("my_textdomain") /// .codeset("UTF-8"); /// ``` /// /// [`bind_textdomain_codeset`]: fn.bind_textdomain_codeset.html pub fn codeset>(mut self, codeset: S) -> Self { self.codeset = Some(codeset.into()); self } /// Prepend the given `path` to the search paths. /// /// # Examples /// /// ```no_run /// use gettextrs::TextDomain; /// /// let text_domain = TextDomain::new("my_textdomain") /// .prepend("~/.local/share"); /// ``` pub fn prepend>(mut self, path: P) -> Self { self.pre_paths.push(path.into()); self } /// Push the given `path` to the end of the search paths. /// /// # Examples /// /// ```no_run /// use gettextrs::TextDomain; /// /// let text_domain = TextDomain::new("my_textdomain") /// .push("test"); /// ``` pub fn push>(mut self, path: P) -> Self { self.post_paths.push(path.into()); self } /// Don't search translation in the system data paths. /// /// # Examples /// /// ```no_run /// use gettextrs::TextDomain; /// /// let text_domain = TextDomain::new("my_textdomain") /// .push("test") /// .skip_system_data_paths(); /// ``` pub fn skip_system_data_paths(mut self) -> Self { self.skip_system_data_paths = true; self } /// Search for translations in the search paths and initialize the `locale` and `textdomain`. /// Return an `Option` with the locale (i.e. the result from the call to [`setlocale`]) if: /// /// - a translation of the text domain in the requested language was found, /// - the locale is valid. /// /// # Examples /// /// ```no_run /// use gettextrs::TextDomain; /// /// TextDomain::new("my_textdomain") /// .init() /// .unwrap(); /// ``` /// /// [`TextDomainError`]: enum.TextDomainError.html /// [`setlocale`]: fn.setlocale.html pub fn init(mut self) -> Result, 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)); } match LanguageRange::new(&req_locale) { Ok(lang_range) => (req_locale.clone(), lang_range.into()), Err(_) => { // try again as unix language tag match LanguageRange::from_unix(&req_locale) { Ok(lang_range) => (req_locale.clone(), lang_range.into()), Err(_) => { return Err(TextDomainError::InvalidLocale(req_locale.clone())); } } } } } None => { // `setlocale` accepts an empty string for current locale ("".to_owned(), Locale::current()) } }; let lang = norm_locale.as_ref().splitn(2, "-").collect::>()[0].to_owned(); let name = self.name.take().unwrap(); let locale_category = self.locale_category.take().unwrap(); let mut codeset = self.codeset.take(); let mo_rel_path = PathBuf::from("LC_MESSAGES").join(&format!("{}.mo", &name)); // Get paths from system data dirs if requested so let sys_data_paths_str = if !self.skip_system_data_paths { env::var("XDG_DATA_DIRS").unwrap_or(".".to_owned()) } else { "".to_owned() }; let sys_data_dirs_iter = env::split_paths(&sys_data_paths_str); // Chain search paths and search for the translation mo file self.pre_paths .into_iter() .chain(sys_data_dirs_iter) .chain(self.post_paths.into_iter()) .find(|path| { let locale_path = path.join("locale"); locale_path.is_dir() && { // path contains a `locale` directory // search for sub directories matching `lang*` // and see if we can find a translation file for the `textdomain` // under `path/locale/lang*/LC_MESSAGES/` let locale_path = &locale_path; fs::read_dir(locale_path) .ok() .map_or(false, |mut entry_iter| { entry_iter .by_ref() .find(|entry_res| { entry_res.as_ref().ok().map_or(false, |entry| { entry.file_type().ok().map_or(false, |file_type| { file_type.is_dir() && entry.file_name().to_str().map_or( false, |entry_name| { entry_name.starts_with(&lang) && locale_path .join(entry_name) .join(&mo_rel_path) .exists() }, ) }) }) }) .is_some() }) } }) .map_or( Err(TextDomainError::TranslationNotFound(lang)), |path| { let result = setlocale(locale_category, req_locale); bindtextdomain( name.clone(), path.join("locale").to_str().unwrap().to_owned(), ); if let Some(codeset) = codeset.take() { bind_textdomain_codeset(name.clone(), codeset); } textdomain(name); Ok(result) }, ) } } 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("name", &self.name.as_ref().unwrap()) .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.as_ref().unwrap()) .field("codeset", &self.codeset) .field("pre_paths", &self.pre_paths); if !self.skip_system_data_paths { debug_struct.field( "using system data paths", &env::var("XDG_DATA_DIRS").unwrap_or("".to_owned()), ); } debug_struct.field("post_paths", &self.post_paths).finish() } } #[cfg(test)] mod tests { use super::{LocaleCategory, TextDomain, TextDomainError}; #[test] fn errors() { assert_eq!( Some(TextDomainError::InvalidLocale("(°_°)".to_owned())), TextDomain::new("test").locale("(°_°)").init().err() ); assert_eq!( Some(TextDomainError::TranslationNotFound("en".to_owned())), TextDomain::new("0_0").locale("en_US").init().err() ); } #[test] fn attributes() { let text_domain = TextDomain::new("test"); assert_eq!(Some("test".to_owned()), text_domain.name); assert!(text_domain.locale.is_none()); assert_eq!(Some(LocaleCategory::LcMessages), text_domain.locale_category); assert!(text_domain.codeset.is_none()); 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!( Some(LocaleCategory::LcAll), text_domain.locale_category ); let text_domain = text_domain.codeset("UTF-8"); assert_eq!(Some("UTF-8".to_owned()), 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); assert_eq!( Some(TextDomainError::TranslationNotFound("en".to_owned())), TextDomain::new("0_0").locale("en_US").init().err() ); // accept locale, but fail to find translation } }