1extern crate locale_config;
81
82extern crate gettext_sys as ffi;
83
84use std::ffi::CStr;
85use std::ffi::CString;
86use std::io;
87use std::os::raw::c_ulong;
88use std::path::PathBuf;
89
90mod text_domain;
91pub use text_domain::{TextDomain, TextDomainError};
92pub mod getters;
93
94#[derive(Debug, PartialEq, Clone, Copy)]
96pub enum LocaleCategory {
97 LcCType = 0,
99 LcNumeric = 1,
101 LcTime = 2,
103 LcCollate = 3,
105 LcMonetary = 4,
107 LcMessages = 5,
109 LcAll = 6,
111 LcPaper = 7,
113 LcName = 8,
115 LcAddress = 9,
117 LcTelephone = 10,
119 LcMeasurement = 11,
121 LcIdentification = 12,
123}
124
125pub fn gettext<T: Into<String>>(msgid: T) -> String {
138 let msgid = CString::new(msgid.into()).expect("`msgid` contains an internal 0 byte");
139 unsafe {
140 CStr::from_ptr(ffi::gettext(msgid.as_ptr()))
141 .to_str()
142 .expect("gettext() returned invalid UTF-8")
143 .to_owned()
144 }
145}
146
147pub fn dgettext<T, U>(domainname: T, msgid: U) -> String
161where
162 T: Into<String>,
163 U: Into<String>,
164{
165 let domainname =
166 CString::new(domainname.into()).expect("`domainname` contains an internal 0 byte");
167 let msgid = CString::new(msgid.into()).expect("`msgid` contains an internal 0 byte");
168 unsafe {
169 CStr::from_ptr(ffi::dgettext(domainname.as_ptr(), msgid.as_ptr()))
170 .to_str()
171 .expect("dgettext() returned invalid UTF-8")
172 .to_owned()
173 }
174}
175
176pub fn dcgettext<T, U>(domainname: T, msgid: U, category: LocaleCategory) -> String
189where
190 T: Into<String>,
191 U: Into<String>,
192{
193 let domainname =
194 CString::new(domainname.into()).expect("`domainname` contains an internal 0 byte");
195 let msgid = CString::new(msgid.into()).expect("`msgid` contains an internal 0 byte");
196 unsafe {
197 CStr::from_ptr(ffi::dcgettext(
198 domainname.as_ptr(),
199 msgid.as_ptr(),
200 category as i32,
201 ))
202 .to_str()
203 .expect("dcgettext() returned invalid UTF-8")
204 .to_owned()
205 }
206}
207
208pub fn ngettext<T, S>(msgid: T, msgid_plural: S, n: u32) -> String
221where
222 T: Into<String>,
223 S: Into<String>,
224{
225 let msgid = CString::new(msgid.into()).expect("`msgid` contains an internal 0 byte");
226 let msgid_plural =
227 CString::new(msgid_plural.into()).expect("`msgid_plural` contains an internal 0 byte");
228 unsafe {
229 CStr::from_ptr(ffi::ngettext(
230 msgid.as_ptr(),
231 msgid_plural.as_ptr(),
232 n as c_ulong,
233 ))
234 .to_str()
235 .expect("ngettext() returned invalid UTF-8")
236 .to_owned()
237 }
238}
239
240pub fn dngettext<T, U, V>(domainname: T, msgid: U, msgid_plural: V, n: u32) -> String
253where
254 T: Into<String>,
255 U: Into<String>,
256 V: Into<String>,
257{
258 let domainname =
259 CString::new(domainname.into()).expect("`domainname` contains an internal 0 byte");
260 let msgid = CString::new(msgid.into()).expect("`msgid` contains an internal 0 byte");
261 let msgid_plural =
262 CString::new(msgid_plural.into()).expect("`msgid_plural` contains an internal 0 byte");
263 unsafe {
264 CStr::from_ptr(ffi::dngettext(
265 domainname.as_ptr(),
266 msgid.as_ptr(),
267 msgid_plural.as_ptr(),
268 n as c_ulong,
269 ))
270 .to_str()
271 .expect("dngettext() returned invalid UTF-8")
272 .to_owned()
273 }
274}
275
276pub fn dcngettext<T, U, V>(
290 domainname: T,
291 msgid: U,
292 msgid_plural: V,
293 n: u32,
294 category: LocaleCategory,
295) -> String
296where
297 T: Into<String>,
298 U: Into<String>,
299 V: Into<String>,
300{
301 let domainname =
302 CString::new(domainname.into()).expect("`domainname` contains an internal 0 byte");
303 let msgid = CString::new(msgid.into()).expect("`msgid` contains an internal 0 byte");
304 let msgid_plural =
305 CString::new(msgid_plural.into()).expect("`msgid_plural` contains an internal 0 byte");
306 unsafe {
307 CStr::from_ptr(ffi::dcngettext(
308 domainname.as_ptr(),
309 msgid.as_ptr(),
310 msgid_plural.as_ptr(),
311 n as c_ulong,
312 category as i32,
313 ))
314 .to_str()
315 .expect("dcngettext() returned invalid UTF-8")
316 .to_owned()
317 }
318}
319
320pub fn textdomain<T: Into<Vec<u8>>>(domainname: T) -> Result<Vec<u8>, io::Error> {
336 let domainname = CString::new(domainname).expect("`domainname` contains an internal 0 byte");
337 unsafe {
338 let result = ffi::textdomain(domainname.as_ptr());
339 if result.is_null() {
340 Err(io::Error::last_os_error())
341 } else {
342 Ok(CStr::from_ptr(result).to_bytes().to_owned())
343 }
344 }
345}
346
347pub fn bindtextdomain<T, U>(domainname: T, dirname: U) -> Result<PathBuf, io::Error>
362where
363 T: Into<Vec<u8>>,
364 U: Into<PathBuf>,
365{
366 let domainname = CString::new(domainname).expect("`domainname` contains an internal 0 byte");
367 let dirname = dirname.into().into_os_string();
368
369 #[cfg(windows)]
370 {
371 use std::ffi::OsString;
372 use std::os::windows::ffi::{OsStrExt, OsStringExt};
373
374 let mut dirname: Vec<u16> = dirname.encode_wide().collect();
375 if dirname.contains(&0) {
376 panic!("`dirname` contains an internal 0 byte");
377 }
378 dirname.push(0);
380 unsafe {
381 let mut ptr = ffi::wbindtextdomain(domainname.as_ptr(), dirname.as_ptr());
382 if ptr.is_null() {
383 Err(io::Error::last_os_error())
384 } else {
385 let mut result = vec![];
386 while *ptr != 0_u16 {
387 result.push(*ptr);
388 ptr = ptr.offset(1);
389 }
390 Ok(PathBuf::from(OsString::from_wide(&result)))
391 }
392 }
393 }
394
395 #[cfg(not(windows))]
396 {
397 use std::ffi::OsString;
398 use std::os::unix::ffi::OsStringExt;
399
400 let dirname = dirname.into_vec();
401 let dirname = CString::new(dirname).expect("`dirname` contains an internal 0 byte");
402 unsafe {
403 let result = ffi::bindtextdomain(domainname.as_ptr(), dirname.as_ptr());
404 if result.is_null() {
405 Err(io::Error::last_os_error())
406 } else {
407 let result = CStr::from_ptr(result);
408 Ok(PathBuf::from(OsString::from_vec(
409 result.to_bytes().to_vec(),
410 )))
411 }
412 }
413 }
414}
415
416pub fn setlocale<T: Into<Vec<u8>>>(category: LocaleCategory, locale: T) -> Option<Vec<u8>> {
431 let c = CString::new(locale).expect("`locale` contains an internal 0 byte");
432 unsafe {
433 let ret = ffi::setlocale(category as i32, c.as_ptr());
434 if ret.is_null() {
435 None
436 } else {
437 Some(CStr::from_ptr(ret).to_bytes().to_owned())
438 }
439 }
440}
441
442pub fn bind_textdomain_codeset<T, U>(domainname: T, codeset: U) -> Result<Option<String>, io::Error>
461where
462 T: Into<Vec<u8>>,
463 U: Into<String>,
464{
465 let domainname = CString::new(domainname).expect("`domainname` contains an internal 0 byte");
466 let codeset = CString::new(codeset.into()).expect("`codeset` contains an internal 0 byte");
467 unsafe {
468 let result = ffi::bind_textdomain_codeset(domainname.as_ptr(), codeset.as_ptr());
469 if result.is_null() {
470 let error = io::Error::last_os_error();
471 if let Some(0) = error.raw_os_error() {
472 return Ok(None);
473 } else {
474 return Err(error);
475 }
476 } else {
477 let result = CStr::from_ptr(result)
478 .to_str()
479 .expect("`bind_textdomain_codeset()` returned non-UTF-8 string")
480 .to_owned();
481 Ok(Some(result))
482 }
483 }
484}
485
486static CONTEXT_SEPARATOR: char = '\x04';
487
488fn build_context_id(ctxt: &str, msgid: &str) -> String {
489 format!("{}{}{}", ctxt, CONTEXT_SEPARATOR, msgid)
490}
491
492fn panic_on_zero_in_ctxt(msgctxt: &str) {
493 if msgctxt.contains('\0') {
494 panic!("`msgctxt` contains an internal 0 byte");
495 }
496}
497
498pub fn pgettext<T, U>(msgctxt: T, msgid: U) -> String
507where
508 T: Into<String>,
509 U: Into<String>,
510{
511 let msgctxt = msgctxt.into();
512 panic_on_zero_in_ctxt(&msgctxt);
513
514 let msgid = msgid.into();
515 let text = build_context_id(&msgctxt, &msgid);
516
517 let translation = gettext(text);
518 if translation.contains(CONTEXT_SEPARATOR as char) {
519 return gettext(msgid);
520 }
521
522 translation
523}
524
525pub fn npgettext<T, U, V>(msgctxt: T, msgid: U, msgid_plural: V, n: u32) -> String
535where
536 T: Into<String>,
537 U: Into<String>,
538 V: Into<String>,
539{
540 let msgctxt = msgctxt.into();
541 panic_on_zero_in_ctxt(&msgctxt);
542
543 let singular_msgid = msgid.into();
544 let plural_msgid = msgid_plural.into();
545 let singular_ctxt = build_context_id(&msgctxt, &singular_msgid);
546 let plural_ctxt = build_context_id(&msgctxt, &plural_msgid);
547
548 let translation = ngettext(singular_ctxt, plural_ctxt, n);
549 if translation.contains(CONTEXT_SEPARATOR as char) {
550 return ngettext(singular_msgid, plural_msgid, n);
551 }
552
553 translation
554}
555
556#[cfg(test)]
557mod tests {
558 use super::*;
559
560 #[test]
561 fn smoke_test() {
562 setlocale(LocaleCategory::LcAll, "en_US.UTF-8");
563
564 bindtextdomain("hellorust", "/usr/local/share/locale").unwrap();
565 textdomain("hellorust").unwrap();
566
567 assert_eq!("Hello, world!", gettext("Hello, world!"));
568 }
569
570 #[test]
571 fn plural_test() {
572 setlocale(LocaleCategory::LcAll, "en_US.UTF-8");
573
574 bindtextdomain("hellorust", "/usr/local/share/locale").unwrap();
575 textdomain("hellorust").unwrap();
576
577 assert_eq!(
578 "Hello, world!",
579 ngettext("Hello, world!", "Hello, worlds!", 1)
580 );
581 assert_eq!(
582 "Hello, worlds!",
583 ngettext("Hello, world!", "Hello, worlds!", 2)
584 );
585 }
586
587 #[test]
588 fn context_test() {
589 setlocale(LocaleCategory::LcAll, "en_US.UTF-8");
590
591 bindtextdomain("hellorust", "/usr/local/share/locale").unwrap();
592 textdomain("hellorust").unwrap();
593
594 assert_eq!("Hello, world!", pgettext("context", "Hello, world!"));
595 }
596
597 #[test]
598 fn plural_context_test() {
599 setlocale(LocaleCategory::LcAll, "en_US.UTF-8");
600
601 bindtextdomain("hellorust", "/usr/local/share/locale").unwrap();
602 textdomain("hellorust").unwrap();
603
604 assert_eq!(
605 "Hello, world!",
606 npgettext("context", "Hello, world!", "Hello, worlds!", 1)
607 );
608 assert_eq!(
609 "Hello, worlds!",
610 npgettext("context", "Hello, world!", "Hello, worlds!", 2)
611 );
612 }
613
614 #[test]
615 #[should_panic(expected = "`msgid` contains an internal 0 byte")]
616 fn gettext_panics() {
617 gettext("input string\0");
618 }
619
620 #[test]
621 #[should_panic(expected = "`domainname` contains an internal 0 byte")]
622 fn dgettext_panics_on_zero_in_domainname() {
623 dgettext("hello\0world!", "hi");
624 }
625
626 #[test]
627 #[should_panic(expected = "`msgid` contains an internal 0 byte")]
628 fn dgettext_panics_on_zero_in_msgid() {
629 dgettext("hello world", "another che\0ck");
630 }
631
632 #[test]
633 #[should_panic(expected = "`domainname` contains an internal 0 byte")]
634 fn dcgettext_panics_on_zero_in_domainname() {
635 dcgettext("a diff\0erent input", "hello", LocaleCategory::LcAll);
636 }
637
638 #[test]
639 #[should_panic(expected = "`msgid` contains an internal 0 byte")]
640 fn dcgettext_panics_on_zero_in_msgid() {
641 dcgettext("world", "yet \0 another\0 one", LocaleCategory::LcMessages);
642 }
643
644 #[test]
645 #[should_panic(expected = "`msgid` contains an internal 0 byte")]
646 fn ngettext_panics_on_zero_in_msgid() {
647 ngettext("singular\0form", "plural form", 10);
648 }
649
650 #[test]
651 #[should_panic(expected = "`msgid_plural` contains an internal 0 byte")]
652 fn ngettext_panics_on_zero_in_msgid_plural() {
653 ngettext("singular form", "plural\0form", 0);
654 }
655
656 #[test]
657 #[should_panic(expected = "`domainname` contains an internal 0 byte")]
658 fn dngettext_panics_on_zero_in_domainname() {
659 dngettext("do\0main", "one", "many", 0);
660 }
661
662 #[test]
663 #[should_panic(expected = "`msgid` contains an internal 0 byte")]
664 fn dngettext_panics_on_zero_in_msgid() {
665 dngettext("domain", "just a\0 single one", "many", 100);
666 }
667
668 #[test]
669 #[should_panic(expected = "`msgid_plural` contains an internal 0 byte")]
670 fn dngettext_panics_on_zero_in_msgid_plural() {
671 dngettext("d", "1", "many\0many\0many more", 10000);
672 }
673
674 #[test]
675 #[should_panic(expected = "`domainname` contains an internal 0 byte")]
676 fn dcngettext_panics_on_zero_in_domainname() {
677 dcngettext(
678 "doma\0in",
679 "singular",
680 "plural",
681 42,
682 LocaleCategory::LcCType,
683 );
684 }
685
686 #[test]
687 #[should_panic(expected = "`msgid` contains an internal 0 byte")]
688 fn dcngettext_panics_on_zero_in_msgid() {
689 dcngettext("domain", "\0ne", "plural", 13, LocaleCategory::LcNumeric);
690 }
691
692 #[test]
693 #[should_panic(expected = "`msgid_plural` contains an internal 0 byte")]
694 fn dcngettext_panics_on_zero_in_msgid_plural() {
695 dcngettext("d-o-m-a-i-n", "one", "a\0few", 0, LocaleCategory::LcTime);
696 }
697
698 #[test]
699 #[should_panic(expected = "`domainname` contains an internal 0 byte")]
700 fn textdomain_panics_on_zero_in_domainname() {
701 textdomain("this is \0 my domain").unwrap();
702 }
703
704 #[test]
705 #[should_panic(expected = "`domainname` contains an internal 0 byte")]
706 fn bindtextdomain_panics_on_zero_in_domainname() {
707 bindtextdomain("\0bind this", "/usr/share/locale").unwrap();
708 }
709
710 #[test]
711 #[should_panic(expected = "`dirname` contains an internal 0 byte")]
712 fn bindtextdomain_panics_on_zero_in_dirname() {
713 bindtextdomain("my_domain", "/opt/locales\0").unwrap();
714 }
715
716 #[test]
717 #[should_panic(expected = "`locale` contains an internal 0 byte")]
718 fn setlocale_panics_on_zero_in_locale() {
719 setlocale(LocaleCategory::LcCollate, "en_\0US");
720 }
721
722 #[test]
723 #[should_panic(expected = "`domainname` contains an internal 0 byte")]
724 fn bind_textdomain_codeset_panics_on_zero_in_domainname() {
725 bind_textdomain_codeset("doma\0in", "UTF-8").unwrap();
726 }
727
728 #[test]
729 #[should_panic(expected = "`codeset` contains an internal 0 byte")]
730 fn bind_textdomain_codeset_panics_on_zero_in_codeset() {
731 bind_textdomain_codeset("name", "K\0I8-R").unwrap();
732 }
733
734 #[test]
735 #[should_panic(expected = "`msgctxt` contains an internal 0 byte")]
736 fn pgettext_panics_on_zero_in_msgctxt() {
737 pgettext("context\0", "string");
738 }
739
740 #[test]
741 #[should_panic(expected = "`msgid` contains an internal 0 byte")]
742 fn pgettext_panics_on_zero_in_msgid() {
743 pgettext("ctx", "a message\0to be translated");
744 }
745
746 #[test]
747 #[should_panic(expected = "`msgctxt` contains an internal 0 byte")]
748 fn npgettext_panics_on_zero_in_msgctxt() {
749 npgettext("c\0tx", "singular", "plural", 0);
750 }
751
752 #[test]
753 #[should_panic(expected = "`msgid` contains an internal 0 byte")]
754 fn npgettext_panics_on_zero_in_msgid() {
755 npgettext("ctx", "sing\0ular", "many many more", 135626);
756 }
757
758 #[test]
759 #[should_panic(expected = "`msgid_plural` contains an internal 0 byte")]
760 fn npgettext_panics_on_zero_in_msgid_plural() {
761 npgettext("context", "uno", "one \0fewer", 10585);
762 }
763}