Skip to main content

bible_io_references/
language.rs

1//! Language identifiers and bundled parsing-support introspection.
2
3use std::{error::Error, fmt, str::FromStr};
4
5/// A language understood by the reference parser.
6///
7/// [`Language::Auto`] searches all bundled languages. The remaining variants
8/// represent concrete languages, including languages for which callers may
9/// register their own aliases even when this crate does not bundle book names.
10#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
11#[repr(u8)]
12pub enum Language {
13    /// Search every language with bundled book aliases.
14    #[default]
15    Auto,
16    /// Arabic (`ar`).
17    Arabic,
18    /// Chinese (`zh`).
19    Chinese,
20    /// English (`en`).
21    English,
22    /// Esperanto (`eo`), available for custom aliases.
23    Esperanto,
24    /// Finnish (`fi`), available for custom aliases.
25    Finnish,
26    /// French (`fr`).
27    French,
28    /// German (`de`).
29    German,
30    /// Greek (`el`), available for custom aliases.
31    Greek,
32    /// Hebrew (`he`).
33    Hebrew,
34    /// Hindi (`hi`).
35    Hindi,
36    /// Indonesian (`id`).
37    Indonesian,
38    /// Korean (`ko`).
39    Korean,
40    /// Portuguese (`pt`).
41    Portuguese,
42    /// Romanian (`ro`), available for custom aliases.
43    Romanian,
44    /// Russian (`ru`).
45    Russian,
46    /// Spanish (`es`).
47    Spanish,
48    /// Tagalog (`tl`).
49    Tagalog,
50    /// Vietnamese (`vi`), available for custom aliases.
51    Vietnamese,
52}
53
54impl Language {
55    /// Every language and parser mode, in stable declaration order.
56    pub const ALL: [Self; 19] = [
57        Self::Auto,
58        Self::Arabic,
59        Self::Chinese,
60        Self::English,
61        Self::Esperanto,
62        Self::Finnish,
63        Self::French,
64        Self::German,
65        Self::Greek,
66        Self::Hebrew,
67        Self::Hindi,
68        Self::Indonesian,
69        Self::Korean,
70        Self::Portuguese,
71        Self::Romanian,
72        Self::Russian,
73        Self::Spanish,
74        Self::Tagalog,
75        Self::Vietnamese,
76    ];
77
78    /// Languages and parser modes backed by bundled book aliases.
79    pub const SUPPORTED: [Self; 14] = [
80        Self::Auto,
81        Self::Arabic,
82        Self::Chinese,
83        Self::English,
84        Self::French,
85        Self::German,
86        Self::Hebrew,
87        Self::Hindi,
88        Self::Indonesian,
89        Self::Korean,
90        Self::Portuguese,
91        Self::Russian,
92        Self::Spanish,
93        Self::Tagalog,
94    ];
95
96    /// The human-readable English name of this language.
97    #[must_use]
98    pub const fn display_name(self) -> &'static str {
99        match self {
100            Self::Auto => "Auto",
101            Self::Arabic => "Arabic",
102            Self::Chinese => "Chinese",
103            Self::English => "English",
104            Self::Esperanto => "Esperanto",
105            Self::Finnish => "Finnish",
106            Self::French => "French",
107            Self::German => "German",
108            Self::Greek => "Greek",
109            Self::Hebrew => "Hebrew",
110            Self::Hindi => "Hindi",
111            Self::Indonesian => "Indonesian",
112            Self::Korean => "Korean",
113            Self::Portuguese => "Portuguese",
114            Self::Romanian => "Romanian",
115            Self::Russian => "Russian",
116            Self::Spanish => "Spanish",
117            Self::Tagalog => "Tagalog",
118            Self::Vietnamese => "Vietnamese",
119        }
120    }
121
122    /// The canonical ISO 639-1 code, or `"auto"` for auto-detection.
123    #[must_use]
124    pub const fn code(self) -> &'static str {
125        match self {
126            Self::Auto => "auto",
127            Self::Arabic => "ar",
128            Self::Chinese => "zh",
129            Self::English => "en",
130            Self::Esperanto => "eo",
131            Self::Finnish => "fi",
132            Self::French => "fr",
133            Self::German => "de",
134            Self::Greek => "el",
135            Self::Hebrew => "he",
136            Self::Hindi => "hi",
137            Self::Indonesian => "id",
138            Self::Korean => "ko",
139            Self::Portuguese => "pt",
140            Self::Romanian => "ro",
141            Self::Russian => "ru",
142            Self::Spanish => "es",
143            Self::Tagalog => "tl",
144            Self::Vietnamese => "vi",
145        }
146    }
147
148    /// The ISO 639-2 identifier accepted for this language.
149    #[must_use]
150    pub const fn identifier_prefix(self) -> &'static str {
151        match self {
152            Self::Auto => "auto",
153            Self::Arabic => "arb",
154            Self::Chinese => "zho",
155            Self::English => "eng",
156            Self::Esperanto => "epo",
157            Self::Finnish => "fin",
158            Self::French => "fra",
159            Self::German => "deu",
160            Self::Greek => "ell",
161            Self::Hebrew => "heb",
162            Self::Hindi => "hin",
163            Self::Indonesian => "ind",
164            Self::Korean => "kor",
165            Self::Portuguese => "por",
166            Self::Romanian => "ron",
167            Self::Russian => "rus",
168            Self::Spanish => "spa",
169            Self::Tagalog => "tgl",
170            Self::Vietnamese => "vie",
171        }
172    }
173
174    /// Supplemental identifiers in addition to the name and ISO codes.
175    #[must_use]
176    pub const fn aliases(self) -> &'static [&'static str] {
177        match self {
178            Self::Auto => &["all", "global"],
179            Self::Tagalog => &["fil"],
180            _ => &[],
181        }
182    }
183
184    /// Every accepted identifier for this language.
185    ///
186    /// This includes the lowercase variant name, display name, two-letter code,
187    /// three-letter identifier, and supplemental aliases.
188    pub fn all_aliases(self) -> impl Iterator<Item = &'static str> {
189        [
190            self.variant_name(),
191            self.display_name(),
192            self.code(),
193            self.identifier_prefix(),
194        ]
195        .into_iter()
196        .chain(self.aliases().iter().copied())
197    }
198
199    /// Whether the crate bundles parsing aliases for this language or mode.
200    #[must_use]
201    pub const fn is_parsing_supported(self) -> bool {
202        matches!(
203            self,
204            Self::Auto
205                | Self::Arabic
206                | Self::Chinese
207                | Self::English
208                | Self::French
209                | Self::German
210                | Self::Hebrew
211                | Self::Hindi
212                | Self::Indonesian
213                | Self::Korean
214                | Self::Portuguese
215                | Self::Russian
216                | Self::Spanish
217                | Self::Tagalog
218        )
219    }
220
221    /// Whether this is the auto-detection parser mode rather than a language.
222    #[must_use]
223    pub const fn is_auto(self) -> bool {
224        matches!(self, Self::Auto)
225    }
226
227    const fn variant_name(self) -> &'static str {
228        match self {
229            Self::Auto => "auto",
230            Self::Arabic => "arabic",
231            Self::Chinese => "chinese",
232            Self::English => "english",
233            Self::Esperanto => "esperanto",
234            Self::Finnish => "finnish",
235            Self::French => "french",
236            Self::German => "german",
237            Self::Greek => "greek",
238            Self::Hebrew => "hebrew",
239            Self::Hindi => "hindi",
240            Self::Indonesian => "indonesian",
241            Self::Korean => "korean",
242            Self::Portuguese => "portuguese",
243            Self::Romanian => "romanian",
244            Self::Russian => "russian",
245            Self::Spanish => "spanish",
246            Self::Tagalog => "tagalog",
247            Self::Vietnamese => "vietnamese",
248        }
249    }
250
251    fn matches_identifier(self, identifier: &str) -> bool {
252        self.all_aliases()
253            .any(|alias| alias.eq_ignore_ascii_case(identifier))
254    }
255}
256
257impl fmt::Display for Language {
258    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
259        formatter.write_str(self.display_name())
260    }
261}
262
263impl FromStr for Language {
264    type Err = ParseLanguageError;
265
266    fn from_str(value: &str) -> Result<Self, Self::Err> {
267        let normalized = value.trim();
268        if normalized.is_empty() {
269            return Err(ParseLanguageError::new(value));
270        }
271
272        let prefix = normalized
273            .split(['-', '_'])
274            .next()
275            .expect("a non-empty string always has a first segment");
276
277        Self::ALL
278            .into_iter()
279            .find(|language| {
280                language.matches_identifier(normalized) || language.matches_identifier(prefix)
281            })
282            .ok_or_else(|| ParseLanguageError::new(value))
283    }
284}
285
286impl TryFrom<&str> for Language {
287    type Error = ParseLanguageError;
288
289    fn try_from(value: &str) -> Result<Self, Self::Error> {
290        value.parse()
291    }
292}
293
294impl AsRef<str> for Language {
295    fn as_ref(&self) -> &str {
296        self.code()
297    }
298}
299
300/// Returned when a language name, code, alias, or locale tag is not known.
301#[derive(Clone, Debug, Eq, PartialEq)]
302pub struct ParseLanguageError {
303    input: String,
304}
305
306impl ParseLanguageError {
307    fn new(input: &str) -> Self {
308        Self {
309            input: input.to_owned(),
310        }
311    }
312
313    /// The unmodified input that failed to parse.
314    #[must_use]
315    pub fn input(&self) -> &str {
316        &self.input
317    }
318}
319
320impl fmt::Display for ParseLanguageError {
321    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
322        if self.input.trim().is_empty() {
323            formatter.write_str("language must be a non-empty string")
324        } else {
325            write!(formatter, "unknown language: {}", self.input)
326        }
327    }
328}
329
330impl Error for ParseLanguageError {}
331
332#[cfg(test)]
333#[path = "../tests/unit/language.rs"]
334mod tests;