#[macro_use]
extern crate lazy_static;
extern crate regex;
#[cfg(target_os = "macos")]
#[macro_use]
extern crate objc;
use regex::Regex;
use std::borrow::{Borrow,Cow};
use std::cell::RefCell;
use std::convert::AsRef;
use std::fmt;
use std::sync::Mutex;
#[derive(Clone,Debug,Eq,Hash,PartialEq)]
pub struct LanguageRange<'a> {
language: Cow<'a, str>
}
lazy_static! {
static ref REGULAR_LANGUAGE_RANGE_REGEX: Regex = Regex::new(r"(?x) ^
(?P<language> (?:
[[:alpha:]]{2,3} (?: - [[:alpha:]]{3} ){0,3}
| \* ))
(?P<script> - (?: [[:alpha:]]{4} | \* ))?
(?P<region> - (?: [[:alpha:]]{2} | [[:digit:]]{3} | \* ))?
(?P<rest> (?: - (?: [[:alnum:]]{1,8} | \* ))*)
$ ").unwrap();
static ref LANGUAGE_RANGE_REGEX: Regex = Regex::new(r"(?x) ^
(?: [[:alpha:]]{1,8} | \* )
(?: - (?: [[:alnum:]]{1,8} | \* ))*
$ ").unwrap();
static ref UNIX_INVARIANT_REGEX: Regex = Regex::new(r"(?ix) ^
(?: c | posix )
(?: \. (?: [0-9a-zA-Z-]{1,20} ))?
$ ").unwrap();
static ref UNIX_TAG_REGEX: Regex = Regex::new(r"(?ix) ^
(?P<language> [[:alpha:]]{2,3} )
(?: _ (?P<region> [[:alpha:]]{2} | [[:digit:]]{3} ))?
(?: \. (?P<encoding> [0-9a-zA-Z-]{1,20} ))?
(?: @ (?P<variant> [[:alnum:]]{1,20} ))?
$ ").unwrap();
}
fn is_owned<'a, T: ToOwned + ?Sized>(c: &Cow<'a, T>) -> bool {
match *c {
Cow::Owned(_) => true,
Cow::Borrowed(_) => false,
}
}
fn canon_lower<'a>(o: Option<&'a str>) -> Cow<'a, str> {
match o {
None => Cow::Borrowed(""),
Some(s) =>
if s.chars().any(char::is_uppercase) {
Cow::Owned(s.to_ascii_lowercase())
} else {
Cow::Borrowed(s)
},
}
}
fn canon_script<'a>(o: Option<&'a str>) -> Cow<'a, str> {
assert!(o.map_or(true, |s| s.len() >= 2 && &s[0..1] == "-"));
match o {
None => Cow::Borrowed(""),
Some(s) =>
if s[1..2].chars().next().unwrap().is_uppercase() &&
s[2..].chars().all(char::is_lowercase) {
Cow::Borrowed(s)
} else {
Cow::Owned(String::from("-") +
s[1..2].to_ascii_uppercase().as_ref() +
s[2..].to_ascii_lowercase().as_ref())
},
}
}
fn canon_upper<'a>(o: Option<&'a str>) -> Cow<'a, str> {
assert!(o.map_or(true, |s| s.len() > 1 && &s[0..1] == "-"));
match o {
None => Cow::Borrowed(""),
Some(s) =>
if s.chars().any(char::is_lowercase) {
Cow::Owned(s.to_ascii_uppercase())
} else {
Cow::Borrowed(s)
},
}
}
impl<'a> LanguageRange<'a> {
pub fn new(lt: &'a str) -> Result<LanguageRange> {
if lt == "" {
return Ok(LanguageRange {
language: Cow::Borrowed(lt),
});
} else if let Some(caps) = REGULAR_LANGUAGE_RANGE_REGEX.captures(lt) {
let language = canon_lower(caps.name("language").map(|m| m.as_str()));
let script = canon_script(caps.name("script").map(|m| m.as_str()));
let region = canon_upper(caps.name("region").map(|m| m.as_str()));
let rest = canon_lower(caps.name("rest").map(|m| m.as_str()));
if is_owned(&language) ||
is_owned(&script) ||
is_owned(®ion) ||
is_owned(&rest)
{
return Ok(LanguageRange {
language: Cow::Owned(
language.into_owned() +
script.borrow() +
region.borrow() +
rest.borrow()),
});
} else {
return Ok(LanguageRange {
language: Cow::Borrowed(lt),
});
}
} else if LANGUAGE_RANGE_REGEX.is_match(lt) {
return Ok(LanguageRange {
language: canon_lower(Some(lt)),
});
} else {
return Err(Error::NotWellFormed);
}
}
pub fn invariant() -> LanguageRange<'static> {
LanguageRange { language: Cow::Borrowed("") }
}
pub fn into_static(self) -> LanguageRange<'static> {
LanguageRange {
language: Cow::Owned(self.language.into_owned())
}
}
pub fn to_shared(&'a self) -> Self {
LanguageRange {
language: Cow::Borrowed(self.language.borrow())
}
}
pub fn from_unix(s: &str) -> Result<LanguageRange<'static>> {
if let Some(caps) = UNIX_TAG_REGEX.captures(s) {
let src_variant = caps.name("variant").map(|m| m.as_str()).unwrap_or("").to_ascii_lowercase();
let mut res = caps.name("language").map(|m| m.as_str()).unwrap().to_ascii_lowercase();
let region = caps.name("region").map(|m| m.as_str()).unwrap_or("");
let mut script = "";
let mut variant = "";
let mut uvariant = "";
match src_variant.as_ref() {
"saaho" if res == "aa" => res = String::from("ssy"),
"arabic" => script = "Arab",
"cyrl" => script = "Cyrl",
"cyrillic" => script = "Cyrl",
"devanagari" => script = "Deva",
"hebrew" => script = "Hebr",
"iqtelif" => script = "Latn",
"latn" => script = "Latn",
"latin" => script = "Latn",
"shaw" => script = "Shaw",
"ijekavianlatin" => {
script = "Latn";
variant = "ijekavsk";
},
"ije" => variant = "ijekavsk",
"ijekavian" => variant = "ijekavsk",
"valencia" => variant = "valencia",
"euro" => {},
s if s.len() <= 8 => uvariant = &*s,
s => uvariant = &s[0..8], };
if script != "" {
res.push('-');
res.push_str(script);
}
if region != "" {
res.push('-');
res.push_str(&*region.to_ascii_uppercase());
}
if variant != "" {
res.push('-');
res.push_str(variant);
}
if uvariant != "" {
res.push_str("-u-va-");
res.push_str(uvariant);
}
return Ok(LanguageRange {
language: Cow::Owned(res)
});
} else if UNIX_INVARIANT_REGEX.is_match(s) {
return Ok(LanguageRange::invariant())
} else {
return Err(Error::NotWellFormed);
}
}
}
impl<'a> AsRef<str> for LanguageRange<'a> {
fn as_ref(&self) -> &str {
self.language.as_ref()
}
}
impl<'a> fmt::Display for LanguageRange<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.language.fmt(f)
}
}
#[derive(Clone,Debug,Eq,Hash,PartialEq)]
pub struct Locale {
inner: String,
}
lazy_static! {
static ref LOCALE_ELEMENT_REGEX: Regex = Regex::new(r"(?ix) ^
(?: (?P<category> [[:alpha:]]{1,20} ) = )?
(?P<tag> (?: [[:alnum:]] | - | \* )+ )
$ ").unwrap();
}
impl Locale {
pub fn user_default() -> Locale {
USER_LOCALE.clone()
}
pub fn global_default() -> Locale {
GLOBAL_LOCALE.lock().unwrap().clone()
}
pub fn set_global_default(lb: Locale) {
*GLOBAL_LOCALE.lock().unwrap() = lb;
}
pub fn current() -> Locale {
CURRENT_LOCALE.with(|l| l.borrow().clone())
}
pub fn set_current(lb: Locale) {
CURRENT_LOCALE.with(|l| *l.borrow_mut() = lb);
}
pub fn new(s: &str) -> Result<Locale> {
let mut i = s.split(',');
let mut res = Locale::from(
try!(LanguageRange::new(
i.next().unwrap()))); for t in i {
if let Some(caps) = LOCALE_ELEMENT_REGEX.captures(t) {
let tag = try!(LanguageRange::new(
try!(caps.name("tag").map(|m| m.as_str()).ok_or(Error::NotWellFormed))));
match caps.name("category").map(|m| m.as_str()) {
Some(cat) => res.add_category(cat.to_ascii_lowercase().as_ref(), &tag),
None => res.add(&tag),
}
} else {
return Err(Error::NotWellFormed);
}
}
return Ok(res);
}
pub fn invariant() -> Locale {
Locale::from(LanguageRange::invariant())
}
pub fn add(&mut self, tag: &LanguageRange) {
for i in self.inner.split(',') {
if i == tag.as_ref() {
return; }
}
self.inner.push_str(",");
self.inner.push_str(tag.as_ref());
}
pub fn add_category(&mut self, category: &str, tag: &LanguageRange) {
if self.inner.split(',').next().unwrap() == tag.as_ref() {
return; }
for i in self.inner.split(',') {
if i.starts_with(category) &&
i[category.len()..].starts_with("=") &&
&i[category.len() + 1..] == tag.as_ref() {
return; }
}
self.inner.push_str(",");
self.inner.push_str(category);
self.inner.push_str("=");
self.inner.push_str(tag.as_ref());
}
pub fn tags<'a>(&'a self) -> Tags<'a> {
Tags { tags: self.inner.split(","), }
}
pub fn tags_for<'a, 'c>(&'a self, category: &'c str) -> TagsFor<'a, 'c> {
let mut tags = self.inner.split(",");
while let Some(s) = tags.clone().next() {
if s.starts_with(category) && s[category.len()..].starts_with("=") {
return TagsFor {
src: self.inner.as_ref(),
tags: tags,
category: Some(category),
};
}
tags.next();
}
return TagsFor {
src: self.inner.as_ref(),
tags: self.inner.split(","),
category: None,
};
}
}
impl AsRef<str> for Locale {
fn as_ref(&self) -> &str {
self.inner.as_ref()
}
}
impl<'a> From<LanguageRange<'a>> for Locale {
fn from(t: LanguageRange<'a>) -> Locale {
Locale {
inner: t.language.into_owned(),
}
}
}
impl fmt::Display for Locale {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.inner.fmt(f)
}
}
pub struct Tags<'a> {
tags: std::str::Split<'a, &'static str>,
}
impl<'a> Iterator for Tags<'a> {
type Item = (Option<&'a str>, LanguageRange<'a>);
fn next(&mut self) -> Option<Self::Item> {
if let Some(s) = self.tags.next() {
if let Some(i) = s.find('=') {
return Some((
Some(&s[..i]),
LanguageRange { language: Cow::Borrowed(&s[i+1..]), }));
} else {
return Some((
None,
LanguageRange { language: Cow::Borrowed(s), }));
}
} else {
return None;
}
}
}
pub struct TagsFor<'a, 'c> {
src: &'a str,
tags: std::str::Split<'a, &'static str>,
category: Option<&'c str>,
}
impl<'a, 'c> Iterator for TagsFor<'a, 'c> {
type Item = LanguageRange<'a>;
fn next(&mut self) -> Option<Self::Item> {
if let Some(cat) = self.category {
while let Some(s) = self.tags.next() {
if s.starts_with(cat) && s[cat.len()..].starts_with("=") {
return Some(
LanguageRange { language: Cow::Borrowed(&s[cat.len()+1..]) });
}
}
self.category = None;
self.tags = self.src.split(",");
}
while let Some(s) = self.tags.next() {
if s.find('=').is_none() {
return Some(
LanguageRange{ language: Cow::Borrowed(s) });
}
}
return None;
}
}
lazy_static! {
static ref USER_LOCALE: Locale = system_locale();
static ref GLOBAL_LOCALE: Mutex<Locale> = Mutex::new(Locale::user_default());
}
thread_local!(
static CURRENT_LOCALE: RefCell<Locale> = RefCell::new(Locale::global_default())
);
mod cgi;
mod unix;
#[cfg(target_family = "windows")]
mod win32;
#[cfg(target_os = "emscripten")]
mod emscripten;
#[cfg(target_os = "macos")]
mod macos;
static INITIALISERS: &'static [fn() -> Option<Locale>] = &[
cgi::system_locale,
unix::system_locale,
#[cfg(target_family = "windows")] win32::system_locale,
#[cfg(target_os = "emscripten")] emscripten::system_locale,
#[cfg(target_os = "macos")] macos::system_locale,
];
fn system_locale() -> Locale {
for f in INITIALISERS {
if let Some(l) = f() {
return l;
}
}
return Locale::invariant();
}
#[derive(Copy,Clone,Debug,PartialEq,Eq)]
pub enum Error {
NotWellFormed,
__NonExhaustive,
}
impl ::std::fmt::Display for Error {
fn fmt(&self, out: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
use ::std::error::Error;
out.write_str(self.description())
}
}
impl ::std::error::Error for Error {
fn description(&self) -> &str {
match self {
&Error::NotWellFormed => "Language tag is not well-formed.",
&Error::__NonExhaustive => panic!("Placeholder error must not be instantiated!"),
}
}
}
type Result<T> = ::std::result::Result<T, Error>;
#[cfg(test)]
mod test {
use super::LanguageRange;
use super::Locale;
use super::is_owned;
use std::iter::FromIterator;
#[test]
fn simple_valid_lang_ranges() {
assert_eq!("en-US", LanguageRange::new("en-US").unwrap().as_ref());
assert_eq!("en-US", LanguageRange::new("EN-US").unwrap().as_ref());
assert_eq!("en", LanguageRange::new("en").unwrap().as_ref());
assert_eq!("eng-Latn-840", LanguageRange::new("eng-Latn-840").unwrap().as_ref());
assert_eq!("english", LanguageRange::new("English").unwrap().as_ref());
}
#[test]
fn wildcard_lang_ranges() {
assert_eq!("*", LanguageRange::new("*").unwrap().as_ref());
assert_eq!("zh-*", LanguageRange::new("zh-*").unwrap().as_ref());
assert_eq!("zh-*-CN", LanguageRange::new("zh-*-cn").unwrap().as_ref());
assert_eq!("en-*-simple-*", LanguageRange::new("En-*-Simple-*").unwrap().as_ref());
assert_eq!("zh-Hans-*", LanguageRange::new("zh-hans-*").unwrap().as_ref());
}
#[test]
fn complex_valid_lang_ranges() {
assert_eq!("de-DE-u-email-co-phonebk-x-linux",
LanguageRange::new("de-DE-u-email-co-phonebk-x-linux").unwrap().as_ref());
assert_eq!("vi-VN-u-fw-mon-hc-h24-ms-metric",
LanguageRange::new("vi-vn-u-fw-mon-hc-h24-ms-metric").unwrap().as_ref());
assert_eq!("sl-Cyrl-YU-rozaj-solba-1994-b-1234-a-foobar-x-b-1234-a-foobar",
LanguageRange::new("sl-Cyrl-YU-rozaj-solba-1994-b-1234-a-Foobar-x-b-1234-a-Foobar").unwrap().as_ref());
}
#[test]
fn invalid_lang_range_invalid_char() {
assert!(LanguageRange::new("not a range").is_err());
}
#[test]
fn invalid_lang_range_long_element() {
assert!(LanguageRange::new("de-DE-u-email-co-phonebook-x-linux").is_err());
}
#[test]
fn invalid_lang_range_leading_number() {
assert!(LanguageRange::new("840").is_err());
}
#[test]
fn invalid_lang_range_bad_asterisk() {
assert!(LanguageRange::new("e*-US").is_err());
assert!(LanguageRange::new("en-*s").is_err());
}
#[test]
fn normal_lang_range() {
assert!(!is_owned(&LanguageRange::new("en-US").unwrap().language));
assert!(!is_owned(&LanguageRange::new("en").unwrap().language));
assert!(!is_owned(&LanguageRange::new("zh-Hant-CN").unwrap().language));
assert!(!is_owned(&LanguageRange::new("cs-CZ-x-ds-002e").unwrap().language));
assert!(!is_owned(&LanguageRange::new("czech").unwrap().language));
}
#[test]
fn locale_simple() {
assert_eq!("en-US", Locale::new("en-US").unwrap().as_ref());
assert_eq!("zh-Hant", Locale::new("zh-hant").unwrap().as_ref());
assert_eq!("de-*", Locale::new("de-*").unwrap().as_ref());
assert!(Locale::new("invalid!").is_err());
assert!(Locale::new("hฤฑ-ฤฐN").is_err());
}
#[test]
fn locale_list() {
assert_eq!("cs-CZ,en-GB,en,*", Locale::new("cs-cz,en-gb,en,*").unwrap().as_ref());
assert_eq!("cs-CZ,engrish", Locale::new("cs-cz,engrish").unwrap().as_ref());
assert!(Locale::new("cs-cz,hฤฑ-ฤฐN").is_err());
}
#[test]
fn locale_category() {
assert_eq!("cs-CZ,messages=en-GB",
Locale::new("cs-CZ,messages=en-GB").unwrap().as_ref());
assert_eq!("zh-Hant,time=ja-JP,measurement=en-US",
Locale::new("zh-hant,TIME=ja-jp,meaSURement=en-US").unwrap().as_ref());
assert!(Locale::new("messages=pl").is_err());
assert!(Locale::new("numeric=de,fr-FR").is_err());
}
#[test]
fn locale_dups() {
assert_eq!("cs-CZ,en,de-AT", Locale::new("cs-CZ,en,de-AT,en").unwrap().as_ref());
assert_eq!("en-US,en", Locale::new("en-us,en-US,EN,eN-Us,en").unwrap().as_ref());
}
#[test]
fn locale_category_dups() {
assert_eq!("cs-CZ",
Locale::new("cs-CZ,messages=cs-CZ,time=cs-cz,collate=CS-cz").unwrap().as_ref());
assert_eq!("de-AT,en-AU",
Locale::new("de-AT,en-AU,messages=de-AT").unwrap().as_ref());
assert_eq!("de-AT,en-AU,messages=en-AU",
Locale::new("de-AT,en-AU,messages=en-AU").unwrap().as_ref());
assert_eq!("hi-IN,time=en-IN",
Locale::new("hi-IN,time=en-IN,TIME=EN-in,TiMe=En-iN").unwrap().as_ref());
}
#[test]
fn unix_tags() {
assert_eq!("cs-CZ", LanguageRange::from_unix("cs_CZ.UTF-8").unwrap().as_ref());
assert_eq!("sr-RS-ijekavsk", LanguageRange::from_unix("sr_RS@ijekavian").unwrap().as_ref());
assert_eq!("sr-Latn-ijekavsk", LanguageRange::from_unix("sr.UTF-8@ijekavianlatin").unwrap().as_ref());
assert_eq!("en-Arab", LanguageRange::from_unix("en@arabic").unwrap().as_ref());
assert_eq!("en-Arab", LanguageRange::from_unix("en.UTF-8@arabic").unwrap().as_ref());
assert_eq!("de-DE", LanguageRange::from_unix("DE_de.UTF-8@euro").unwrap().as_ref());
assert_eq!("ssy-ER", LanguageRange::from_unix("aa_ER@saaho").unwrap().as_ref());
assert!(LanguageRange::from_unix("foo_BAR").is_err());
assert!(LanguageRange::from_unix("en@arabic.UTF-8").is_err());
assert_eq!("", LanguageRange::from_unix("C").unwrap().as_ref());
assert_eq!("", LanguageRange::from_unix("C.UTF-8").unwrap().as_ref());
assert_eq!("", LanguageRange::from_unix("C.ISO-8859-1").unwrap().as_ref());
assert_eq!("", LanguageRange::from_unix("POSIX").unwrap().as_ref());
}
#[test]
fn category_tag_list() {
assert_eq!(
Vec::from_iter(Locale::new("cs-CZ,messages=en-GB,time=de-DE,collate=en-US").unwrap().tags()),
&[(None, LanguageRange::new("cs-CZ").unwrap()),
(Some("messages"), LanguageRange::new("en-GB").unwrap()),
(Some("time"), LanguageRange::new("de-DE").unwrap()),
(Some("collate"), LanguageRange::new("en-US").unwrap()),
]);
}
#[test]
fn tag_list_for() {
let locale = Locale::new("cs-CZ,messages=en-GB,time=de-DE,sk-SK,pl-PL").unwrap();
assert_eq!(
Vec::from_iter(locale.tags_for("messages")),
&[LanguageRange::new("en-GB").unwrap(),
LanguageRange::new("cs-CZ").unwrap(),
LanguageRange::new("sk-SK").unwrap(),
LanguageRange::new("pl-PL").unwrap(),
]);
assert_eq!(
Vec::from_iter(locale.tags_for("time")),
&[LanguageRange::new("de-DE").unwrap(),
LanguageRange::new("cs-CZ").unwrap(),
LanguageRange::new("sk-SK").unwrap(),
LanguageRange::new("pl-PL").unwrap(),
]);
assert_eq!(
Vec::from_iter(locale.tags_for("measurement")),
&[LanguageRange::new("cs-CZ").unwrap(),
LanguageRange::new("sk-SK").unwrap(),
LanguageRange::new("pl-PL").unwrap(),
]);
}
}