gettext-rs-0.7.0/.cargo_vcs_info.json0000644000000001120000000000000131370ustar { "git": { "sha1": "81ded1d817c4d384ff19df72ba20829a6642e345" } } gettext-rs-0.7.0/CHANGELOG.md000064400000000000000000000063110000000000000135250ustar 00000000000000# Changelog ## 0.7.0 - 2021-04-25 ### Changed - If `XDG_DATA_DIRS` environment variable is not set, or is empty, `TextDomain` builder defaults to "/usr/local/share/:/usr/share/" instead of current directory. This is in line with XDG base directory specification. (Alexander Batischev) ## 0.6.0 - 2021-03-03 ### Added - Documentation on when functions will panic (Alexander Batischev) - Functions to query gettext configuration. These are wrappers over `textdomain(NULL)`, `bindtextdomain(domain, NULL)`, and `bind_textdomain_codeset(domain, NULL)` (Alexander Batischev) - `impl Error` for `TextDomainError`, which makes it easier to use with `?` operator (Alexander Batischev) ### Changed - **Users are required to configure gettext for UTF-8, either explicitly with `bind_textdomain_codeset()`, or implicitly with `setlocale()` if they understand the consequences.** Failure to do this might lead to panics and/or garbled data (Alexander Batischev) - Functions now require more specific types, like `Into` and `Into` (Alexander Batischev) - Functions that take multiple strings no longer require all strings to be of exact same type (i.e. you can mix `String` and `&str`) (Alexander Batischev) - Functions that configure gettext (`textdomain()`, `bindtextdomain()`, `bind_textdomain_codeset()`) now return a `Result` to indicate failures (Alexander Batischev) - Macros now behave like `format!(gettext(…), …)`, which is more useful than previous behaviour of `gettext(format!(…, …))` (Rasmus Thomsen) - On Windows, `TextDomain` now uses `wbindtextdomain` (Alexander Batischev) - Bump `gettext-sys` dependency to 0.21 (Alexander Batischev) ### Fixed - `CString`s being dropped while their pointers are in use (Alexander Batischev) ### Removed - `Default` instance for `TextDomain`. Default-constructed instance was useless since it would panic on `init()` as `name` is not set (Alexander Batischev) ## 0.5.0 - 2020-09-01 ### Changed - Bump `locale_config` dependency to 0.3 (Josh Stone) ## 0.4.4 - 2019-09-22 ### Added - Macros that do `gettext(format!(…, …))` (Rasmus Thomsen) ## 0.4.3 - 2019-08-16 ### Added - `Clone` and `Copy` impls for `LocaleCategory` (Cecile Tonglet) ## 0.4.2 - 2019-07-26 ### Changed - Bump `gettext-sys` dependency to 0.19.9 (Konstantin V. Salikhov) ## 0.4.1 - 2018-08-17 ### Added - `pgettext` and `npgettext` functions that support contexts (Daniel García Moreno) ## 0.4.0 - 2018-05-23 ### Added - `TextDomain` builder that abstracts over `setlocale`, `textdomain`, `bindtextdomain`, and `bind_textdomain_codeset` (François Laignel) ### Changed - `setlocale` now returns `Option` to indicate that the requested locale couldn't be installed (Brian Olsen) ### Removed - Raw FFI bindings are now in gettext-sys crate (Brian Olsen) ## 0.3.0 - 2016-02-04 ### Changed - Main module renamed from `gettext_rs` to `gettextrs` (Konstantin V. Salikhov) ## 0.2.0 - 2016-02-03 ### Added - Support for plurals (`d*gettext`, `n*gettext`) (Konstantin V. Salikhov) - Bindings for `bind_textdomain_codeset` (Konstantin V. Salikhov) ## 0.1.0 - 2016-02-02 Initial release (Konstantin V. Salikhov, Faizaan). gettext-rs-0.7.0/Cargo.toml0000644000000022440000000000000111450ustar # 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] edition = "2015" name = "gettext-rs" version = "0.7.0" authors = ["Konstantin Salikhov ", "Alexander Batischev "] description = "Safe bindings for gettext" homepage = "https://github.com/Koka/gettext-rs" documentation = "http://docs.rs/gettext-rs/" 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.21.0" [dependencies.locale_config] version = "0.3" [dev-dependencies.lazy_static] version = "1" [features] gettext-system = ["gettext-sys/gettext-system"] gettext-rs-0.7.0/Cargo.toml.orig000064400000000000000000000012410000000000000146000ustar 00000000000000[package] name = "gettext-rs" description = "Safe bindings for gettext" version = "0.7.0" authors = ["Konstantin Salikhov ", "Alexander Batischev "] repository = "https://github.com/Koka/gettext-rs" documentation = "http://docs.rs/gettext-rs/" homepage = "https://github.com/Koka/gettext-rs" readme = "README.md" keywords = ["gettext", "binding", "ffi", "i18n", "l10n"] license = "MIT" edition = "2015" [lib] name = "gettextrs" [features] gettext-system = ["gettext-sys/gettext-system"] [dependencies.gettext-sys] version = "0.21.0" path = "../gettext-sys" [dependencies] locale_config = "0.3" [dev-dependencies] lazy_static = "1" gettext-rs-0.7.0/LICENSE.txt000064400000000000000000000021010000000000000135300ustar 00000000000000The 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.7.0/README.md000064400000000000000000000104420000000000000131730ustar 00000000000000# gettext-rs Safe bindings for gettext. Please see [documentation](https://docs.rs/gettext-rs) for details. ## Licensing This crate depends on `gettext-sys`, which compiles GNU gettext on platforms that don't have a native gettext implementation. GNU gettext is licensed under LGPL, and is linked statically, which means **you have to abide by LGPL**. If you don't want or can't do that, there are two ways out: 1. if you use glibc or musl libc, enable `gettext-system` feature (see below); 2. dynamically link to GNU gettext library you obtained by some other means, like a package manager. For details, see environment variables in `gettext-sys` documentation. ## Usage (If you know how to use gettext and just want a gist of this crate's API, [skip to the next section](#complete-api-example)). To internationalize your program with gettext, you have to do four things: 1. wrap translatable strings into calls to appropriate gettext functions; 2. use tools to extract those strings into a so-called "PO template file"; 3. translate the strings in the template, getting a so-called "message catalog" as a result; 4. compile the message catalog from the plain-text, human-readable PO format to the binary MO format, and install them into a well-known location like _/usr/local/share/locale_. This crate only covers the first step, the markup. To extract messages, use `xtr` (`cargo install xtr`). To translate, you can use desktop tools like [Poedit][], sites like [Crowdin][], or any text editor. To compile from PO to MO, use `msgfmt` tool from gettext-tools. The way you install files highly depend on your distribution method, so it's not covered here either. [Poedit]: https://poedit.net [Crowdin]: https://crowdin.com The best resource on gettext is [GNU gettext manual][]. This crate has the same API, so you should have an easy time transferring the advice from that manual to this crate. In a pitch, you can also glance at the manpages for C functions which this crate wraps. [GNU gettext manual]: https://www.gnu.org/software/gettext/manual/index.html ## Complete API example ```rust use gettextrs::*; fn main() -> Result<(), Box> { // Specify the name of the .mo file to use. textdomain("hellorust")?; // Ask gettext for UTF-8 strings. THIS CRATE CAN'T HANDLE NON-UTF-8 DATA! bind_textdomain_codeset("hellorust", "UTF-8")?; // You could also use `TextDomain` builder which calls `textdomain` and // other functions for you: // // TextDomain::new("hellorust").init()?; // `gettext()` simultaneously marks a string for translation and translates // it at runtime. println!("Translated: {}", gettext("Hello, world!")); // gettext supports plurals, i.e. you can have different messages depending // on the number of items the message mentions. This even works for // languages that have more than one plural form, like Russian or Czech. println!("Singular: {}", ngettext("One thing", "Multiple things", 1)); println!("Plural: {}", ngettext("One thing", "Multiple things", 2)); // gettext de-duplicates strings, i.e. the same string used multiple times // will have a single entry in the PO and MO files. However, the same words // might have different meaning depending on the context. To distinguish // between different contexts, gettext accepts an additional string: println!("With context: {}", pgettext("This is the context", "Hello, world!")); println!( "Plural with context: {}", npgettext("This is the context", "One thing", "Multiple things", 2)); Ok(()) } ``` ## Features - `gettext-system`: if enabled, _asks_ the crate to use the gettext implementation that's part of glibc or musl libc. This only works on: * Linux with glibc or musl libc; * Windows + GNU (e.g. [MSYS2](http://www.msys2.org/)) with `gettext-devel` installed e.g. using: ``` pacman --noconfirm -S base-devel mingw-w64-x86_64-gcc libxml2-devel tar ``` If none of those conditions hold, the crate will proceed to building and statically linking its own copy of GNU gettext! This enables `gettext-system` feature of the underlying `gettext-sys` crate. ## Environment variables This crate doesn't use any. See also the documentation for the underlying `gettext-sys` crate. gettext-rs-0.7.0/src/getters.rs000064400000000000000000000111140000000000000145230ustar 00000000000000//! Query gettext configuration. //! //! There are just a few settings in gettext. The only required one is the message domain, set //! using [`textdomain`][::textdomain]; the other two are the path where translations are searched //! for, and the encoding to which the messages should be converted. //! //! The underlying C API uses the same functions both as setters and as getters: to get the current //! value, you just pass `NULL` as an argument. This is ergonomic in C, but not in Rust: wrapping //! everything in `Option`s is a tad ugly. That's why this crate provides getters as separate //! functions. They're in a module of their own to prevent them from clashing with any functions //! that the underlying C API might gain in the future. extern crate gettext_sys as ffi; use std::ffi::{CStr, CString}; use std::io; use std::path::PathBuf; use std::ptr; /// Get currently set message domain. /// /// If you want to *set* the domain, rather than getting its current value, use /// [`textdomain`][::textdomain]. /// /// For more information, see [textdomain(3)][]. /// /// [textdomain(3)]: https://www.man7.org/linux/man-pages/man3/textdomain.3.html pub fn current_textdomain() -> Result, io::Error> { unsafe { let result = ffi::textdomain(ptr::null()); if result.is_null() { Err(io::Error::last_os_error()) } else { Ok(CStr::from_ptr(result).to_bytes().to_owned()) } } } /// Get base directory for the given domain. /// /// If you want to *set* the directory, rather than querying its current value, use /// [`bindtextdomain`][::bindtextdomain]. /// /// For more information, see [bindtextdomain(3)][]. /// /// [bindtextdomain(3)]: https://www.man7.org/linux/man-pages/man3/bindtextdomain.3.html /// /// # Panics /// /// Panics if `domainname` contains an internal 0 byte, as such values can't be passed to the /// underlying C API. pub fn domain_directory>>(domainname: T) -> Result { let domainname = CString::new(domainname).expect("`domainname` contains an internal 0 byte"); #[cfg(windows)] { use std::ffi::OsString; use std::os::windows::ffi::OsStringExt; unsafe { let mut ptr = ffi::wbindtextdomain(domainname.as_ptr(), ptr::null()); 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; unsafe { let result = ffi::bindtextdomain(domainname.as_ptr(), ptr::null()); 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(), ))) } } } } /// Get encoding of translated messages for given domain. /// /// Returns `None` if encoding is not set. /// /// If you want to *set* an encoding, rather than get the current one, use /// [`bind_textdomain_codeset`][::bind_textdomain_codeset]. /// /// For more information, see [bind_textdomain_codeset(3)][]. /// /// [bind_textdomain_codeset(3)]: https://www.man7.org/linux/man-pages/man3/bind_textdomain_codeset.3.html /// /// # Panics /// /// Panics if: /// * `domainname` contains an internal 0 byte, as such values can't be passed to the underlying /// C API; /// * the result is not in UTF-8 (which shouldn't happen as the results should always be ASCII, as /// they're just codeset names). pub fn textdomain_codeset>>(domainname: T) -> Result, io::Error> { let domainname = CString::new(domainname).expect("`domainname` contains an internal 0 byte"); unsafe { let result = ffi::bind_textdomain_codeset(domainname.as_ptr(), ptr::null()); 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)) } } } gettext-rs-0.7.0/src/lib.rs000064400000000000000000000610610000000000000136220ustar 00000000000000//! # Safe Rust bindings for gettext. //! //! Usage: //! //! ```rust,no_run //! use gettextrs::*; //! //! fn main() -> Result<(), Box> { //! // Specify the name of the .mo file to use. //! textdomain("hellorust")?; //! // Ask gettext for UTF-8 strings. THIS CRATE CAN'T HANDLE NON-UTF-8 DATA! //! bind_textdomain_codeset("hellorust", "UTF-8")?; //! //! // You could also use `TextDomain` builder which calls `textdomain` and //! // other functions for you: //! // //! // TextDomain::new("hellorust").init()?; //! //! // `gettext()` simultaneously marks a string for translation and translates //! // it at runtime. //! println!("Translated: {}", gettext("Hello, world!")); //! //! // gettext supports plurals, i.e. you can have different messages depending //! // on the number of items the message mentions. This even works for //! // languages that have more than one plural form, like Russian or Czech. //! println!("Singular: {}", ngettext("One thing", "Multiple things", 1)); //! println!("Plural: {}", ngettext("One thing", "Multiple things", 2)); //! //! // gettext de-duplicates strings, i.e. the same string used multiple times //! // will have a single entry in the PO and MO files. However, the same words //! // might have different meaning depending on the context. To distinguish //! // between different contexts, gettext accepts an additional string: //! println!("With context: {}", pgettext("This is the context", "Hello, world!")); //! println!( //! "Plural with context: {}", //! npgettext("This is the context", "One thing", "Multiple things", 2)); //! //! Ok(()) //! } //! ``` //! //! ## UTF-8 is required //! //! By default, gettext converts results to the locale's codeset. Rust, on the other hand, always //! encodes strings to UTF-8. The best way to bridge this gap is to ask gettext to convert strings //! to UTF-8: //! //! ```rust,no_run //! # use gettextrs::*; //! # fn main() -> Result<(), Box> { //! bind_textdomain_codeset("hellorust", "UTF-8")?; //! # Ok(()) //! # } //! ``` //! //! ...or using [`TextDomain`] builder: //! //! ```rust,no_run //! # use gettextrs::*; //! # fn main() -> Result<(), Box> { //! TextDomain::new("hellorust") //! .codeset("UTF-8") // Optional, the builder does this by default //! .init()?; //! # Ok(()) //! # } //! ``` //! //! This crate doesn't do this for you because the encoding is a global setting; changing it can //! affect other gettext calls in your program, like calls in C or C++ parts of your binary. //! //! If you don't do this, calls to `gettext()` and other functions might panic when they encounter //! something that isn't UTF-8. They can also garble data as they interpret the other encoding as //! UTF-8. //! //! Another thing you could do is change the locale, e.g. `setlocale(LocaleCategory::LcAll, //! "fr_FR.UTF-8")`, but that would also hard-code the language, defeating the purpose of gettext: //! if you know the language in advance, you could just write all your strings in that language and //! be done with that. 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; /// Locale category enum ported from locale.h. #[derive(Debug, PartialEq, Clone, Copy)] 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 the default domain. /// /// For more information, see [gettext(3)][]. /// /// [gettext(3)]: https://www.man7.org/linux/man-pages/man3/gettext.3.html /// /// # Panics /// /// Panics if: /// /// * `msgid` contains an internal 0 byte, as such values can't be passed to the underlying C API; /// * the result is not in UTF-8 (see [this note](./index.html#utf-8-is-required)). pub fn gettext>(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() } } /// Translate msgid to localized message from the specified domain. /// /// For more information, see [dgettext(3)][]. /// /// [dgettext(3)]: https://www.man7.org/linux/man-pages/man3/dgettext.3.html /// /// # Panics /// /// Panics if: /// /// * `domainname` or `msgid` contain an internal 0 byte, as such values can't be passed to the /// underlying C API; /// * the result is not in UTF-8 (see [this note](./index.html#utf-8-is-required)). pub fn dgettext(domainname: T, msgid: U) -> String where T: Into, U: Into, { 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() } } /// Translate msgid to localized message from the specified domain using custom locale category. /// /// For more information, see [dcgettext(3)][]. /// /// [dcgettext(3)]: https://www.man7.org/linux/man-pages/man3/dcgettext.3.html /// /// # Panics /// /// Panics if: /// * `domainname` or `msgid` contain an internal 0 byte, as such values can't be passed to the /// underlying C API; /// * the result is not in UTF-8 (see [this note](./index.html#utf-8-is-required)). pub fn dcgettext(domainname: T, msgid: U, category: LocaleCategory) -> String where T: Into, U: Into, { 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() } } /// Translate msgid to localized message from the default domain (with plural support). /// /// For more information, see [ngettext(3)][]. /// /// [ngettext(3)]: https://www.man7.org/linux/man-pages/man3/ngettext.3.html /// /// # Panics /// /// Panics if: /// * `msgid` or `msgid_plural` contain an internal 0 byte, as such values can't be passed to the /// underlying C API; /// * the result is not in UTF-8 (see [this note](./index.html#utf-8-is-required)). pub fn ngettext(msgid: T, msgid_plural: S, n: u32) -> String where T: Into, S: Into, { 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() } } /// Translate msgid to localized message from the specified domain (with plural support). /// /// For more information, see [dngettext(3)][]. /// /// [dngettext(3)]: https://www.man7.org/linux/man-pages/man3/dngettext.3.html /// /// # Panics /// /// Panics if: /// * `domainname`, `msgid`, or `msgid_plural` contain an internal 0 byte, as such values can't be /// passed to the underlying C API; /// * the result is not in UTF-8 (see [this note](./index.html#utf-8-is-required)). pub fn dngettext(domainname: T, msgid: U, msgid_plural: V, n: u32) -> String where T: Into, U: Into, V: Into, { 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() } } /// Translate msgid to localized message from the specified domain using custom locale category /// (with plural support). /// /// For more information, see [dcngettext(3)][]. /// /// [dcngettext(3)]: https://www.man7.org/linux/man-pages/man3/dcngettext.3.html /// /// # Panics /// /// Panics if: /// * `domainname`, `msgid`, or `msgid_plural` contain an internal 0 byte, as such values can't be /// passed to the underlying C API; /// * the result is not in UTF-8 (see [this note](./index.html#utf-8-is-required)). pub fn dcngettext( domainname: T, msgid: U, msgid_plural: V, n: u32, category: LocaleCategory, ) -> String where T: Into, U: Into, V: Into, { 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() } } /// Switch to the specific text domain. /// /// Returns the current domain, after possibly changing it. (There's no trailing 0 byte in the /// return value.) /// /// If you want to *get* current domain, rather than set it, use [`getters::current_textdomain`]. /// /// For more information, see [textdomain(3)][]. /// /// [textdomain(3)]: https://www.man7.org/linux/man-pages/man3/textdomain.3.html /// /// # Panics /// /// Panics if `domainname` contains an internal 0 byte, as such values can't be passed to the /// underlying C API. pub fn textdomain>>(domainname: T) -> Result, 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()) } } } /// Specify the directory that contains MO files for the given domain. /// /// Returns the current directory for given domain, after possibly changing it. /// /// If you want to *get* domain directory, rather than set it, use [`getters::domain_directory`]. /// /// For more information, see [bindtextdomain(3)][]. /// /// [bindtextdomain(3)]: https://www.man7.org/linux/man-pages/man3/bindtextdomain.3.html /// /// # Panics /// /// Panics if `domainname` or `dirname` contain an internal 0 byte, as such values can't be passed /// to the underlying C API. pub fn bindtextdomain(domainname: T, dirname: U) -> Result where T: Into>, U: Into, { 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 = dirname.encode_wide().collect(); if dirname.contains(&0) { panic!("`dirname` contains an internal 0 byte"); } // Trailing zero to mark the end of the C string. 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(), ))) } } } } /// Set current locale. /// /// Returns an opaque string that describes the locale set. You can pass that string into /// `setlocale()` later to set the same local again. `None` means the call failed (the underlying /// API doesn't provide any details). /// /// For more information, see [setlocale(3)][]. /// /// [setlocale(3)]: https://www.man7.org/linux/man-pages/man3/setlocale.3.html /// /// # Panics /// /// Panics if `locale` contains an internal 0 byte, as such values can't be passed to the /// underlying C API. pub fn setlocale>>(category: LocaleCategory, locale: T) -> Option> { 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()) } } } /// Set encoding of translated messages. /// /// Returns the current charset for given domain, after possibly changing it. `None` means no /// codeset has been set. /// /// If you want to *get* current encoding, rather than set it, use [`getters::textdomain_codeset`]. /// /// For more information, see [bind_textdomain_codeset(3)][]. /// /// [bind_textdomain_codeset(3)]: https://www.man7.org/linux/man-pages/man3/bind_textdomain_codeset.3.html /// /// # Panics /// /// Panics if: /// * `domainname` or `codeset` contain an internal 0 byte, as such values can't be passed to the /// underlying C API; /// * the result is not in UTF-8 (which shouldn't happen as the results should always be ASCII, as /// they're just codeset names). pub fn bind_textdomain_codeset(domainname: T, codeset: U) -> Result, io::Error> where T: Into>, U: Into, { 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"); } } /// Translate msgid to localized message from the default domain (with context support). /// /// # Panics /// /// Panics if: /// * `msgctxt` or `msgid` contain an internal 0 byte, as such values can't be passed to the /// underlying C API; /// * the result is not in UTF-8 (see [this note](./index.html#utf-8-is-required)). pub fn pgettext(msgctxt: T, msgid: U) -> String where T: Into, U: Into, { 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 } /// Translate msgid to localized message from the default domain (with plural support and context /// support). /// /// # Panics /// /// Panics if: /// * `msgctxt`, `msgid`, or `msgid_plural` contain an internal 0 byte, as such values can't be /// passed to the underlying C API; /// * the result is not in UTF-8 (see [this note](./index.html#utf-8-is-required)). pub fn npgettext(msgctxt: T, msgid: U, msgid_plural: V, n: u32) -> String where T: Into, U: Into, V: Into, { 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); } } gettext-rs-0.7.0/src/macros.rs000064400000000000000000000171270000000000000143440ustar 00000000000000//! Macros that translate the message and then replace placeholders in it. /// This is an implementation detail for counting arguments in the gettext macros. Don't call this directly. #[macro_export] #[doc(hidden)] macro_rules! count_args { () => { 0 }; ($_e: expr $(, $rest: expr)*) => { 1 + $crate::count_args!($($rest),*) } } /// This is an implementation detail for replacing arguments in the gettext macros. Don't call this directly. #[macro_export] #[doc(hidden)] macro_rules! freplace { ($format:expr, $($args:expr),+ $(,)?) => {{ let mut parts = $format.split("{}"); debug_assert_eq!(parts.clone().count() - 1, $crate::count_args!($($args),*), "Argument count has to match number of format directives ({{}})"); let mut output = parts.next().unwrap_or_default().to_string(); $( output += &format!("{}{}", $args, &parts.next().expect("Argument count has to match number of format directives ({})")); )* output }}; } /// Like [`gettext`], but allows for formatting. /// /// It calls [`gettext`] on `msgid`, and then replaces each occurrence of `{}` with the next value /// out of `args`. /// /// [`gettext`]: fn.gettext.html #[macro_export] macro_rules! gettext { ($msgid:expr, $($args:expr),+ $(,)?) => {{ let format = $crate::gettext($msgid); $crate::freplace!(format, $($args),*) }}; } /// Like [`dgettext`], but allows for formatting. /// /// It calls [`dgettext`] on `domainname` and `msgid`, and then replaces each occurrence of `{}` /// with the next value out of `args`. /// /// [`dgettext`]: fn.dgettext.html #[macro_export] macro_rules! dgettext { ($domainname:expr, $msgid:expr, $($args:expr),+ $(,)?) => {{ let format = $crate::dgettext($domainname, $msgid); $crate::freplace!(format, $($args),*) }}; } /// Like [`dcgettext`], but allows for formatting. /// /// It calls [`dcgettext`] on `domainname`, `category`, and `msgid`, and then replaces each /// occurrence of `{}` with the next value out of `args`. /// /// [`dcgettext`]: fn.dcgettext.html #[macro_export] macro_rules! dcgettext { ($domainname:expr, $category:expr, $msgid:expr, $($args:expr),+ $(,)?) => {{ let format = $crate::dcgettext($domainname, $msgid, $category); $crate::freplace!(format, $($args),*) }}; } /// Like [`ngettext`], but allows for formatting. /// /// It calls [`ngettext`] on `msgid`, `msgid_plural`, and `n`, and then replaces each occurrence of /// `{}` with the next value out of `args`. /// /// [`ngettext`]: fn.ngettext.html #[macro_export] macro_rules! ngettext { ($msgid:expr, $msgid_plural:expr, $n:expr, $($args:expr),+ $(,)?) => {{ let format = $crate::ngettext($msgid, $msgid_plural, $n); $crate::freplace!(format, $($args),*) }} } /// Like [`dngettext`], but allows for formatting. /// /// It calls [`dngettext`] on `domainname`, `msgid`, `msgid_plural`, and `n`, and then replaces /// each occurrence of `{}` with the next value out of `args`. /// /// [`dngettext`]: fn.dngettext.html #[macro_export] macro_rules! dngettext { ($domainname:expr, $msgid:expr, $msgid_plural:expr, $n:expr, $($args:expr),+ $(,)?) => {{ let format = $crate::dngettext($domainname, $msgid, $msgid_plural, $n); $crate::freplace!(format, $($args),*) }} } /// Like [`dcngettext`], but allows for formatting. /// /// It calls [`dcngettext`] on `domainname`, `category`, `msgid`, `msgid_plural`, and `n`, and then /// replaces each occurrence of `{}` with the next value out of `args`. /// /// [`dcngettext`]: fn.dcngettext.html #[macro_export] macro_rules! dcngettext { ($domainname:expr, $category:expr, $msgid:expr, $msgid_plural:expr, $n:expr, $($args:expr),+ $(,)?) => {{ let format = $crate::dcngettext($domainname, $msgid, $msgid_plural, $n, $category); $crate::freplace!(format, $($args),*) }} } /// Like [`pgettext`], but allows for formatting. /// /// It calls [`pgettext`] on `msgctxt` and `msgid`, and then replaces each occurrence of `{}` with /// the next value out of `args`. /// /// [`pgettext`]: fn.pgettext.html #[macro_export] macro_rules! pgettext { ($msgctxt:expr, $msgid:expr, $($args:expr),+ $(,)?) => {{ let format = $crate::pgettext($msgctxt, $msgid); $crate::freplace!(format, $($args),*) }} } /// Like [`npgettext`], but allows for formatting. /// /// It calls [`npgettext`] on `msgctxt`, `msgid`, `msgid_plural`, and `n`, and then replaces each /// occurrence of `{}` with the next value out of `args`. /// /// [`npgettext`]: fn.npgettext.html #[macro_export] macro_rules! npgettext { ($msgctxt:expr, $msgid:expr, $msgid_plural:expr, $n:expr, $($args:expr),+ $(,)?) => {{ let format = $crate::npgettext($msgctxt, $msgid, $msgid_plural, $n); $crate::freplace!(format, $($args),*) }} } #[cfg(test)] mod test { use crate::*; #[test] fn test_gettext_macro() { setlocale(LocaleCategory::LcAll, "en_US.UTF-8"); bindtextdomain("hellorust", "/usr/local/share/locale").unwrap(); textdomain("hellorust").unwrap(); assert_eq!(gettext!("Hello, {}!", "world"), "Hello, world!"); assert_eq!( gettext!("Hello, {} {}!", "small", "world"), "Hello, small world!" ); } #[test] fn test_ngettext_macro() { setlocale(LocaleCategory::LcAll, "en_US.UTF-8"); bindtextdomain("hellorust", "/usr/local/share/locale").unwrap(); textdomain("hellorust").unwrap(); assert_eq!( ngettext!("Singular {}!", "Multiple {}!", 2, "Worlds"), "Multiple Worlds!" ); } #[test] fn test_pgettext_macro() { 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 test_npgettext_macro() { setlocale(LocaleCategory::LcAll, "en_US.UTF-8"); bindtextdomain("hellorust", "/usr/local/share/locale").unwrap(); textdomain("hellorust").unwrap(); assert_eq!( "Multiple Worlds!", npgettext!("context", "Singular {}!", "Multiple {}!", 2, "Worlds") ); } #[test] fn test_dgettext_macro() { setlocale(LocaleCategory::LcAll, "en_US.UTF-8"); bindtextdomain("hellorust", "/usr/local/share/locale").unwrap(); assert_eq!( "Hello, world!", dgettext!("hellorust", "Hello, {}!", "world") ); } #[test] fn test_dcgettext_macro() { setlocale(LocaleCategory::LcAll, "en_US.UTF-8"); bindtextdomain("hellorust", "/usr/local/share/locale").unwrap(); assert_eq!( "Hello, world!", dcgettext!("hellorust", LocaleCategory::LcAll, "Hello, {}!", "world") ); } #[test] fn test_dcngettext_macro() { setlocale(LocaleCategory::LcAll, "en_US.UTF-8"); bindtextdomain("hellorust", "/usr/local/share/locale").unwrap(); assert_eq!( "Singular World", dcngettext!( "hellorust", LocaleCategory::LcAll, "Singular {}", "Multiple {}", 1, "World" ) ) } #[test] fn test_dngettext_macro() { setlocale(LocaleCategory::LcAll, "en_US.UTF-8"); bindtextdomain("hellorust", "/usr/local/share/locale").unwrap(); assert_eq!( "Singular World!", dngettext!("hellrust", "Singular {}!", "Multiple {}!", 1, "World") ) } } gettext-rs-0.7.0/src/text_domain.rs000064400000000000000000000413420000000000000153670ustar 00000000000000//! A builder for gettext configuration. 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}; /// Errors that might come up after running the builder. #[derive(Debug)] 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), /// The call to `textdomain()` failed. TextDomainCallFailed(std::io::Error), /// The call to `bindtextdomain()` failed. BindTextDomainCallFailed(std::io::Error), /// The call to `bind_textdomain_codeset()` failed. 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), } } } /// A builder to configure gettext. /// /// It searches translations in the system data paths and optionally in the user-specified paths, /// and binds them to the given domain. `TextDomain` takes care of calling [`setlocale`], /// [`bindtextdomain`], [`bind_textdomain_codeset`], and [`textdomain`] for you. /// /// # Defaults /// /// - [`bind_textdomain_codeset`] is called by default to set UTF-8. You can use [`codeset`] to /// override this, but please bear in mind that [other functions in this crate require /// UTF-8](./index.html#utf-8-is-required). /// - 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. /// - 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 set, or is empty, the default /// of "/usr/local/share/:/usr/share/" 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 `domainname.mo` is used for the call to /// [`bindtextdomain`]. /// /// # Examples /// /// Basic usage: /// /// ```no_run /// use gettextrs::TextDomain; /// /// # fn main() -> Result<(), Box> { /// TextDomain::new("my_textdomain").init()?; /// # Ok(()) /// # } /// ``` /// /// Use the translation in current language under the `target` directory if available, otherwise /// search system defined paths: /// /// ```no_run /// use gettextrs::TextDomain; /// /// # fn main() -> Result<(), Box> { /// TextDomain::new("my_textdomain") /// .prepend("target") /// .init()?; /// # Ok(()) /// # } /// ``` /// /// 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(error) => { /// format!("an error occurred: {}", error) /// } /// }; /// 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 pub struct TextDomain { domainname: String, locale: Option, locale_category: LocaleCategory, codeset: String, pre_paths: Vec, post_paths: Vec, skip_system_data_paths: bool, } impl TextDomain { /// Creates a new instance of `TextDomain` for the specified `domainname`. /// /// # Examples /// /// ```no_run /// use gettextrs::TextDomain; /// /// let text_domain = TextDomain::new("my_textdomain"); /// ``` pub fn new>(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, } } /// 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 = locale_category; self } /// Define the `codeset` that will be used for calling [`bind_textdomain_codeset`]. The default /// is "UTF-8". /// /// **Warning:** [other functions in this crate require UTF-8](./index.html#utf-8-is-required). /// /// # Examples /// /// ```no_run /// use gettextrs::TextDomain; /// /// let text_domain = TextDomain::new("my_textdomain") /// .codeset("KOI8-R"); /// ``` /// /// [`bind_textdomain_codeset`]: fn.bind_textdomain_codeset.html pub fn codeset>(mut self, codeset: S) -> Self { self.codeset = 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 for translations 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, initialize the locale, set up the text domain /// and ask gettext to convert messages to UTF-8. /// /// Returns an `Option` with the opaque string that describes the locale set (i.e. the result /// of [`setlocale`]) if: /// /// - a translation of the text domain in the requested language was found; and /// - the locale is valid. /// /// # Examples /// /// ```no_run /// use gettextrs::TextDomain; /// /// # fn main() -> Result<(), Box> { /// TextDomain::new("my_textdomain").init()?; /// # Ok(()) /// # } /// ``` /// /// [`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.as_bytes().to_owned())); } 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 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)); // Get paths from system data dirs if requested so 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); // 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"); if !locale_path.is_dir() { return false; } // 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/` 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()), 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); // accept locale, but fail to find translation match TextDomain::new("0_0").locale("en_US").init().err() { Some(TextDomainError::TranslationNotFound(message)) => assert_eq!(message, "en"), _ => panic!(), }; } } gettext-rs-0.7.0/tests/getters.rs000064400000000000000000000033610000000000000151030ustar 00000000000000extern crate gettextrs; #[macro_use] extern crate lazy_static; use gettextrs::{getters::*, *}; use std::sync::Mutex; lazy_static! { // "Current text domain" is a global resource which all tests modify. This mutex serializes // access to that resource. static ref TEXTDOMAIN_MUTEX: Mutex<()> = Mutex::new(()); } #[test] fn test_current_textdomain() { let _lock = TEXTDOMAIN_MUTEX.lock().unwrap(); textdomain("just_testing").unwrap(); assert_eq!(current_textdomain().unwrap(), "just_testing".as_bytes()); textdomain("test_current_textdomain").unwrap(); assert_eq!( current_textdomain().unwrap(), "test_current_textdomain".as_bytes() ); } #[test] fn test_domain_directory() { use std::path::PathBuf; static TEXTDOMAIN: &'static str = "test_domain_directory"; { let _lock = TEXTDOMAIN_MUTEX.lock().unwrap(); textdomain(TEXTDOMAIN).unwrap(); } bindtextdomain(TEXTDOMAIN, "/tmp").unwrap(); assert_eq!(domain_directory(TEXTDOMAIN).unwrap(), PathBuf::from("/tmp")); let path = "/some/nonexistent path (hopefully)"; bindtextdomain(TEXTDOMAIN, path).unwrap(); assert_eq!(domain_directory(TEXTDOMAIN).unwrap(), PathBuf::from(path)); } #[test] fn test_textdomain_codeset() { static TEXTDOMAIN: &'static str = "test_textdomain_codeset"; { let _lock = TEXTDOMAIN_MUTEX.lock().unwrap(); textdomain(TEXTDOMAIN).unwrap(); } bind_textdomain_codeset(TEXTDOMAIN, "C").unwrap(); assert_eq!( textdomain_codeset(TEXTDOMAIN).unwrap(), Some("C".to_string()) ); bind_textdomain_codeset(TEXTDOMAIN, "UTF-8").unwrap(); assert_eq!( textdomain_codeset(TEXTDOMAIN).unwrap(), Some("UTF-8".to_string()) ); }