#[cfg(feature = "serde")]
mod serde_impl;
extern crate phf;
use std::{
error::Error,
fmt::{Debug, Display, Formatter},
str::{self, FromStr},
};
struct LanguageData {
code_3: [u8; 3],
code_1: Option<[u8; 2]>,
#[cfg(feature = "english_names")]
name_en: &'static str,
#[cfg(feature = "local_names")]
autonym: Option<&'static str>,
}
#[rustfmt::skip]
mod isotable;
pub use isotable::Language;
use isotable::{OVERVIEW, THREE_TO_THREE, TWO_TO_THREE};
#[cfg(any(feature = "list_languages", test))]
pub fn languages() -> impl Iterator<Item = Language> {
OVERVIEW.iter().enumerate().filter_map(|(idx, _)| Language::from_usize(idx))
}
impl Language {
pub fn to_639_3(&self) -> &'static str {
unsafe { str::from_utf8_unchecked(&OVERVIEW[*self as usize].code_3) }
}
pub fn to_639_1(&self) -> Option<&'static str> {
unsafe {
OVERVIEW[*self as usize]
.code_1
.as_ref()
.map(|s| str::from_utf8_unchecked(s))
}
}
#[cfg(feature = "english_names")]
pub fn to_name(&self) -> &'static str {
OVERVIEW[*self as usize].name_en
}
#[cfg(feature = "english_names")]
pub fn from_name(engl_name: &str) -> Option<Self> {
OVERVIEW
.iter()
.enumerate()
.find(|(_, it)| it.name_en == engl_name)
.and_then(|(idx, _)| Language::from_usize(idx))
}
#[cfg(all(feature = "english_names", feature = "lowercase_names"))]
pub fn from_name_lowercase(engl_name: &str) -> Option<Self> {
OVERVIEW
.iter()
.enumerate()
.find(|(_, it)| {
it.name_en.to_ascii_lowercase().as_str() == engl_name
})
.and_then(|(idx, _)| Language::from_usize(idx))
}
#[cfg(feature = "english_names")]
pub fn match_names<F>(matcher: F) -> impl Iterator<Item = Self>
where
F: Fn(&str) -> bool + 'static,
{
OVERVIEW.iter().enumerate().filter_map(move |(idx, it)| {
match matcher(it.name_en) {
true => Language::from_usize(idx),
false => None,
}
})
}
#[cfg(feature = "local_names")]
pub fn to_autonym(&self) -> Option<&'static str> {
OVERVIEW[*self as usize].autonym
}
#[cfg(feature = "local_names")]
pub fn from_autonym(autonym: &str) -> Option<Self> {
OVERVIEW
.iter()
.enumerate()
.find(|(_, it)| it.autonym == Some(autonym))
.and_then(|(idx, _)| Language::from_usize(idx))
}
#[cfg(feature = "local_names")]
pub fn match_autonyms<F>(matcher: F) -> impl Iterator<Item = Self>
where
F: Fn(&str) -> bool + 'static,
{
OVERVIEW.iter().enumerate().filter_map(move |(idx, it)| {
it.autonym.and_then(|autonym| match matcher(autonym) {
true => Language::from_usize(idx),
false => None,
})
})
}
pub fn from_639_1(code: &str) -> Option<Language> {
if code.len() != 2 {
return None;
}
TWO_TO_THREE
.get(code)
.copied()
.and_then(|raw_lang| Language::from_usize(raw_lang as usize))
}
pub fn from_639_3(code: &str) -> Option<Language> {
if code.len() != 3 {
return None;
}
THREE_TO_THREE
.get(code)
.copied()
.and_then(|raw_lang| Language::from_usize(raw_lang as usize))
}
pub fn from_locale(locale: &str) -> Option<Language> {
if locale.len() < 3 {
return None;
}
locale.split('_').next().and_then(Language::from_639_1)
}
}
#[allow(clippy::derivable_impls)]
impl Default for Language {
fn default() -> Self {
Language::Und
}
}
impl Debug for Language {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
write!(f, "{}", self.to_639_3())
}
}
impl Display for Language {
#[cfg(all(feature = "local_names", feature = "english_names"))]
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
write!(
f,
"{} ({})",
self.to_name(),
self.to_autonym().unwrap_or("missing autonym")
)
}
#[cfg(all(feature = "local_names", not(feature = "english_names")))]
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
write!(f, "{}", self.to_autonym().unwrap_or("missing autonym"))
}
#[cfg(all(not(feature = "local_names"), feature = "english_names"))]
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
write!(f, "{}", self.to_name())
}
#[cfg(all(not(feature = "local_names"), not(feature = "english_names")))]
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
write!(f, "{}", self.to_639_3())
}
}
#[derive(Debug)]
pub struct ParseLanguageError(String);
impl Display for ParseLanguageError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "'{}' is not a valid ISO 639-1 or 639-3 code.", self.0)
}
}
impl Error for ParseLanguageError {}
impl FromStr for Language {
type Err = ParseLanguageError;
#[cfg(any(
not(feature = "english_names"),
not(feature = "lowercase_names")
))]
fn from_str(s: &str) -> Result<Self, ParseLanguageError> {
match Language::from_639_3(s).or_else(|| Language::from_639_1(s)) {
Some(l) => Ok(l),
None => Err(ParseLanguageError(s.to_owned())),
}
}
#[cfg(all(
feature = "english_names",
feature = "lowercase_names",
not(feature = "local_names")
))]
fn from_str(s: &str) -> Result<Self, ParseLanguageError> {
match Language::from_639_3(s)
.or_else(|| Language::from_639_1(s))
.or_else(|| Language::from_name_lowercase(s))
{
Some(l) => Ok(l),
None => Err(ParseLanguageError(s.to_owned())),
}
}
#[cfg(all(
feature = "english_names",
feature = "lowercase_names",
feature = "local_names"
))]
fn from_str(s: &str) -> Result<Self, ParseLanguageError> {
match Language::from_639_3(s)
.or_else(|| Language::from_639_1(s))
.or_else(|| Language::from_name_lowercase(s))
.or_else(|| Language::from_autonym(s))
{
Some(l) => Ok(l),
None => Err(ParseLanguageError(s.to_owned())),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "serde")]
extern crate serde_json;
use std::fmt::Write;
#[test]
fn invalid_locale_gives_none() {
assert!(Language::from_locale("foo").is_none());
assert!(Language::from_locale("deu_DEU.UTF-8").is_none());
assert!(Language::from_locale("___").is_none());
assert!(Language::from_locale("ƤƤ_ƶƶ.UTF-8").is_none());
}
#[test]
fn test_valid_locales_are_correctly_decoded() {
assert_eq!(Language::from_locale("de_DE.UTF-8"), Some(Language::Deu));
assert_eq!(Language::from_locale("en_GB.UTF-8"), Some(Language::Eng));
}
#[test]
fn test_std_fmt() {
let mut t = String::new();
write!(t, "{}", Language::Deu).unwrap();
if cfg!(feature = "local_names") && cfg!(feature = "english_names") {
assert_eq!(t, "German (Deutsch)");
} else if cfg!(feature = "local_names") {
assert_eq!(t, "Deutsch");
} else if cfg!(feature = "english_names") {
assert_eq!(t, "German");
} else {
assert_eq!(t, "deu");
}
let mut t = String::new();
write!(t, "{:?}", Language::Deu).unwrap();
assert_eq!(t, "deu");
}
#[test]
#[cfg(feature = "local_names")]
fn test_iso639_3_to_autonym() {
assert_eq!(
Language::from_639_3("bul").unwrap().to_autonym(),
Some("Š±ŃŠ»Š³Š°ŃŃŠŗŠø")
);
assert_eq!(
Language::from_639_3("fra").unwrap().to_autonym(),
Some("franƧais")
);
}
#[test]
fn test_default() {
assert_eq!(Language::default(), Language::Und);
}
#[test]
#[cfg(feature = "serde")]
fn test_serde() {
fn to_json(code: &str) -> String {
format!(r#""{code}""#)
}
fn test_deserialize(language: Language, code: &str) {
assert_eq!(
serde_json::from_str::<Language>(&to_json(code)).unwrap(),
language
);
assert_eq!(
serde_json::from_value::<Language>(serde_json::json!(code))
.unwrap(),
language
);
}
for language in languages() {
assert_eq!(
serde_json::to_string(&language).unwrap(),
to_json(language.to_639_3())
);
test_deserialize(language, language.to_639_3());
if let Some(code) = language.to_639_1() {
test_deserialize(language, code)
}
assert_eq!(
serde_json::from_str::<Language>(
&serde_json::to_string(&language).unwrap()
)
.unwrap(),
language
);
}
assert_eq!(
serde_json::from_str::<Language>(&to_json("foo")).map_err(|e| e.to_string()),
Err("unknown variant `foo`, expected `any valid ISO 639-1 or 639-3 code` at line 1 column 5".to_string())
);
assert_eq!(
serde_json::from_str::<Language>("123").map_err(|e| e.to_string()),
Err("invalid type: integer `123`, expected borrowed str or bytes at line 1 column 3".to_string())
);
}
#[test]
fn test_ordering() {
assert!(Language::Deu < Language::Fra);
let fra = Language::Fra;
assert!(fra <= Language::Fra);
}
#[test]
#[cfg(feature = "list_languages")]
fn test_good_language_filtering() {
let languages = languages();
let languages_with_iso_639_1 =
languages.filter(|language| language.to_639_1().is_some());
for language in languages_with_iso_639_1 {
assert!(language.to_639_1().is_some());
}
}
#[test]
#[cfg(feature = "list_languages")]
fn test_wrong_language_filtering() {
let languages = languages();
let languages_with_iso_639_1 =
languages.filter(|language| language.to_639_1().is_none());
for language in languages_with_iso_639_1 {
assert!(language.to_639_1().is_none());
}
}
#[test]
fn test_from_str() {
assert_eq!(Language::from_str("deu").unwrap(), Language::Deu);
assert_eq!(Language::from_str("fr").unwrap(), Language::Fra);
assert!(Language::from_str("foo").is_err());
}
#[test]
#[cfg(feature = "english_names")]
fn test_from_str_full_features() {
assert_eq!(Language::from_str("es").unwrap().to_name(), "Spanish");
assert_eq!(Language::from_str("spa").unwrap().to_name(), "Spanish");
if cfg!(feature = "lowercase_names") {
assert_eq!(
Language::from_str("spanish").unwrap().to_name(),
"Spanish"
);
}
if cfg!(feature = "lowercase_names") && cfg!(feature = "local_names") {
assert_eq!(
Language::from_str("espaƱol").unwrap().to_name(),
"Spanish"
);
}
assert!(Language::from_str("Spanish").is_err());
}
}