1 //! # gettext C library FFI binding for Rust
2 //!
3 //! Usage:
4 //!
5 //! ```
6 //! use gettextrs::*;
7 //!
8 //! setlocale(LocaleCategory::LcAll, "en_US.UTF-8");
9 //!
10 //! bindtextdomain("hellorust", "/usr/local/share/locale");
11 //! textdomain("hellorust");
12 //!
13 //! println!("Translated: {}", gettext("Hello, world!"));
14 //! println!("Singular: {}", ngettext("One thing", "Multiple things", 1));
15 //! println!("Plural: {}", ngettext("One thing", "Multiple things", 2));
16 //! ```
17 //!
18 //! Alternatively, you can initialize the locale and text domain using the [`TextDomain`] builder.
19 //! By default, a translation of the specified text domain in current language is searched in
20 //! the system's data paths. See [`TextDomain`]'s documentation for other options.
21 //!
22 //! ```no_run
23 //! use gettextrs::TextDomain;
24 //!
25 //! TextDomain::new("hellorust")
26 //!            .init()
27 //!            .unwrap();
28 //! ```
29 //!
30 //! [`TextDomain`]: struct.TextDomain.html
31 
32 extern crate locale_config;
33 
34 extern crate gettext_sys as ffi;
35 
36 use std::ffi::CString;
37 use std::ffi::CStr;
38 use std::os::raw::c_ulong;
39 
40 mod text_domain;
41 pub use text_domain::{TextDomain, TextDomainError};
42 
43 /// Locale category enum ported from locale.h
44 #[derive(Debug, PartialEq)]
45 pub enum LocaleCategory {
46     /// Character classification and case conversion.
47     LcCType = 0,
48     /// Non-monetary numeric formats.
49     LcNumeric = 1,
50     /// Date and time formats.
51     LcTime = 2,
52     /// Collation order.
53     LcCollate = 3,
54     /// Monetary formats.
55     LcMonetary = 4,
56     /// Formats of informative and diagnostic messages and interactive responses.
57     LcMessages = 5,
58     /// For all.
59     LcAll = 6,
60     /// Paper size.
61     LcPaper = 7,
62     /// Name formats.
63     LcName = 8,
64     /// Address formats and location information.
65     LcAddress = 9,
66     /// Telephone number formats.
67     LcTelephone = 10,
68     /// Measurement units (Metric or Other).
69     LcMeasurement = 11,
70     /// Metadata about the locale information.
71     LcIdentification = 12,
72 }
73 
74 /// Translate msgid to localized message from default domain
gettext<T: Into<Vec<u8>>>(s: T) -> String75 pub fn gettext<T: Into<Vec<u8>>>(s: T) -> String {
76     unsafe {
77         CStr::from_ptr(ffi::gettext(CString::new(s).unwrap().as_ptr()))
78             .to_string_lossy()
79             .into_owned()
80     }
81 }
82 
83 /// Translate msgid to localized message from specified domain
dgettext<T: Into<Vec<u8>>>(domain: T, s: T) -> String84 pub fn dgettext<T: Into<Vec<u8>>>(domain: T, s: T) -> String {
85     unsafe {
86         CStr::from_ptr(ffi::dgettext(CString::new(domain).unwrap().as_ptr(), CString::new(s).unwrap().as_ptr()))
87             .to_string_lossy()
88             .into_owned()
89     }
90 }
91 
92 /// Translate msgid to localized message from specified domain using custom locale category
dcgettext<T: Into<Vec<u8>>>(domain: T, s: T, category: LocaleCategory) -> String93 pub fn dcgettext<T: Into<Vec<u8>>>(domain: T, s: T, category: LocaleCategory) -> String {
94     unsafe {
95         CStr::from_ptr(ffi::dcgettext(CString::new(domain).unwrap().as_ptr(), CString::new(s).unwrap().as_ptr(), category as i32))
96             .to_string_lossy()
97             .into_owned()
98     }
99 }
100 
101 /// Translate msgid to localized message from default domain (with plural support)
ngettext<T: Into<Vec<u8>>>(singular: T, plural : T, n : u32) -> String102 pub fn ngettext<T: Into<Vec<u8>>>(singular: T, plural : T, n : u32) -> String {
103     unsafe {
104         CStr::from_ptr(ffi::ngettext(CString::new(singular).unwrap().as_ptr(), CString::new(plural).unwrap().as_ptr(), n as c_ulong))
105             .to_string_lossy()
106             .into_owned()
107     }
108 }
109 
110 /// Translate msgid to localized message from specified domain (with plural support)
dngettext<T: Into<Vec<u8>>>(domain: T, singular: T, plural: T, n : u32) -> String111 pub fn dngettext<T: Into<Vec<u8>>>(domain: T, singular: T, plural: T, n : u32) -> String {
112     unsafe {
113         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))
114             .to_string_lossy()
115             .into_owned()
116     }
117 }
118 
119 /// Translate msgid to localized message from specified domain using custom locale category (with plural support)
dcngettext<T: Into<Vec<u8>>>(domain: T, singular: T, plural: T, n : u32, category: LocaleCategory) -> String120 pub fn dcngettext<T: Into<Vec<u8>>>(domain: T, singular: T, plural: T, n : u32, category: LocaleCategory) -> String {
121     unsafe {
122         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))
123             .to_string_lossy()
124             .into_owned()
125     }
126 }
127 
128 /// Switch to specific text domain
textdomain<T: Into<Vec<u8>>>(domain: T) -> String129 pub fn textdomain<T: Into<Vec<u8>>>(domain: T) -> String {
130     unsafe {
131         CStr::from_ptr(ffi::textdomain(CString::new(domain).unwrap().as_ptr()))
132             .to_string_lossy()
133             .into_owned()
134     }
135 }
136 
137 /// Bind text domain to some directory containing gettext MO files
bindtextdomain<T: Into<Vec<u8>>>(domain: T, dir: T) -> String138 pub fn bindtextdomain<T: Into<Vec<u8>>>(domain: T, dir: T) -> String {
139     unsafe {
140         CStr::from_ptr(ffi::bindtextdomain(CString::new(domain).unwrap().as_ptr(),
141                                                    CString::new(dir).unwrap().as_ptr()))
142             .to_string_lossy()
143             .into_owned()
144     }
145 }
146 
147 /// Set current locale for translations
setlocale<T: Into<Vec<u8>>>(category: LocaleCategory, locale: T) -> Option<String>148 pub fn setlocale<T: Into<Vec<u8>>>(category: LocaleCategory, locale: T) -> Option<String> {
149     let c = CString::new(locale).unwrap();
150     unsafe {
151         let ret = ffi::setlocale(category as i32, c.as_ptr());
152         if ret.is_null() {
153             None
154         } else {
155             Some(CStr::from_ptr(ret).to_string_lossy().into_owned())
156         }
157     }
158 }
159 
bind_textdomain_codeset<T: Into<Vec<u8>>>(domain: T, codeset: T) -> String160 pub fn bind_textdomain_codeset<T: Into<Vec<u8>>>(domain: T, codeset: T) -> String {
161     unsafe {
162         CStr::from_ptr(ffi::bind_textdomain_codeset(CString::new(domain).unwrap().as_ptr(),
163                                                    CString::new(codeset).unwrap().as_ptr()))
164             .to_string_lossy()
165             .into_owned()
166     }
167 }
168 
169 static CONTEXT_SEPARATOR: u8 = b'\x04';
170 
build_context_id(ctx: &Vec<u8>, s: &Vec<u8>) -> String171 fn build_context_id(ctx: &Vec<u8>, s: &Vec<u8>) -> String {
172     let mut text: Vec<u8> = vec![];
173     text.extend(ctx.iter().cloned());
174     text.push(CONTEXT_SEPARATOR);
175     text.extend(s.iter().cloned());
176     CString::new(text).unwrap().to_string_lossy().into_owned()
177 }
178 
179 /// Translate msgid to localized message from default domain (with context support)
pgettext<T: Into<Vec<u8>>>(ctx: T, s: T) -> String180 pub fn pgettext<T: Into<Vec<u8>>>(ctx: T, s: T) -> String {
181     let msgid = s.into();
182     let text = build_context_id(&ctx.into(), &msgid);
183 
184     let trans = gettext(text);
185     if trans.contains(CONTEXT_SEPARATOR as char) {
186         return gettext(msgid);
187         //return CString::new(msgid).unwrap().to_string_lossy().into_owned();
188     }
189 
190     trans
191 }
192 
193 /// Translate msgid to localized message from default domain (with plural support and context
194 /// support)
npgettext<T: Into<Vec<u8>>>(ctx: T, singular: T, plural: T, n: u32) -> String195 pub fn npgettext<T: Into<Vec<u8>>>(ctx: T, singular: T, plural: T, n: u32) -> String {
196     let ctx = ctx.into();
197     let singular_msgid = singular.into();
198     let plural_msgid = plural.into();
199     let singular_ctx = build_context_id(&ctx, &singular_msgid);
200     let plural_ctx = build_context_id(&ctx, &plural_msgid);
201 
202     let trans = ngettext(singular_ctx, plural_ctx, n);
203     if trans.contains(CONTEXT_SEPARATOR as char) {
204         return ngettext(singular_msgid, plural_msgid, n);
205     }
206 
207     trans
208 }
209 
210 #[cfg(test)]
211 mod tests {
212     use super::*;
213 
214     #[test]
smoke_test()215     fn smoke_test() {
216         setlocale(LocaleCategory::LcAll, "en_US.UTF-8");
217 
218         bindtextdomain("hellorust", "/usr/local/share/locale");
219         textdomain("hellorust");
220 
221         assert_eq!("Hello, world!", gettext("Hello, world!"));
222     }
223 
224     #[test]
plural_test()225     fn plural_test() {
226         setlocale(LocaleCategory::LcAll, "en_US.UTF-8");
227 
228         bindtextdomain("hellorust", "/usr/local/share/locale");
229         textdomain("hellorust");
230 
231         assert_eq!("Hello, world!", ngettext("Hello, world!", "Hello, worlds!", 1));
232         assert_eq!("Hello, worlds!", ngettext("Hello, world!", "Hello, worlds!", 2));
233     }
234 
235     #[test]
context_test()236     fn context_test() {
237         setlocale(LocaleCategory::LcAll, "en_US.UTF-8");
238 
239         bindtextdomain("hellorust", "/usr/local/share/locale");
240         textdomain("hellorust");
241 
242         assert_eq!("Hello, world!", pgettext("context", "Hello, world!"));
243     }
244 
245     #[test]
plural_context_test()246     fn plural_context_test() {
247         setlocale(LocaleCategory::LcAll, "en_US.UTF-8");
248 
249         bindtextdomain("hellorust", "/usr/local/share/locale");
250         textdomain("hellorust");
251 
252         assert_eq!("Hello, world!", npgettext("context", "Hello, world!", "Hello, worlds!", 1));
253         assert_eq!("Hello, worlds!", npgettext("context", "Hello, world!", "Hello, worlds!", 2));
254     }
255 }
256