Skip to main content

lingora_core/domain/
locale.rs

1use std::{
2    path::{Component, Path},
3    str::FromStr,
4};
5
6use icu_locale_core::{
7    LanguageIdentifier,
8    subtags::{Language, Region, Script},
9};
10use serde::{Deserialize, Serialize};
11
12use crate::{domain::LanguageRoot, error::LingoraError};
13
14/// A BCP 47 language tag (locale identifier) used throughout Lingora.
15#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
16pub struct Locale(LanguageIdentifier);
17
18pub trait HasLocale {
19    fn locale(&self) -> &Locale;
20
21    fn language_root(&self) -> LanguageRoot {
22        LanguageRoot::from(self.locale())
23    }
24}
25
26impl Locale {
27    /// The primary language subtag (e.g. `en`, `fr`, `zh`)
28    pub fn language(&self) -> &Language {
29        &self.0.language
30    }
31
32    /// The script subtag, if present (e.g. `Latn`, `Cyrl`, `Hans`)
33    pub fn script(&self) -> Option<&Script> {
34        self.0.script.as_ref()
35    }
36
37    /// The region subtag, if present (e.g. `GB`, `CA`, `US`)
38    pub fn region(&self) -> Option<&Region> {
39        self.0.region.as_ref()
40    }
41
42    /// Returns `true` if the locale includes any variant subtags (e.g. `ca-valencia`)
43    pub fn has_variants(&self) -> bool {
44        !self.0.variants.is_empty()
45    }
46}
47
48impl Default for Locale {
49    fn default() -> Self {
50        let locale = sys_locale::get_locale().unwrap_or("en".into());
51        Locale(LanguageIdentifier::from_str(&locale).unwrap())
52    }
53}
54
55impl FromStr for Locale {
56    type Err = LingoraError;
57
58    fn from_str(s: &str) -> Result<Self, Self::Err> {
59        // Call for BCP47 validation...
60        let _tags = icu_locale_core::Locale::try_from_str(s)
61            .map_err(|e| LingoraError::InvalidLocale(format!("{e}: '{s}'")))?;
62
63        let locale = s
64            .parse::<LanguageIdentifier>()
65            .map_err(|e| LingoraError::InvalidLocale(format!("{e}: '{s}'")))?;
66
67        Ok(Locale(locale))
68    }
69}
70
71impl TryFrom<&Path> for Locale {
72    type Error = LingoraError;
73
74    fn try_from(value: &Path) -> Result<Self, Self::Error> {
75        let locale_from_osstr = |c: &std::ffi::OsStr| Locale::from_str(&c.to_string_lossy()).ok();
76
77        let locale_from_path_segment = |c: Component| match c {
78            Component::Normal(name) => locale_from_osstr(name),
79            _ => None,
80        };
81
82        let invalid_locale = || {
83            LingoraError::InvalidLocale(format!(
84                "No valid locale found in path: {}",
85                value.display()
86            ))
87        };
88
89        value
90            .file_stem()
91            .and_then(locale_from_osstr)
92            .or_else(|| {
93                value
94                    .parent()?
95                    .components()
96                    .rev()
97                    .filter_map(locale_from_path_segment)
98                    .next()
99            })
100            .ok_or_else(invalid_locale)
101    }
102}
103
104impl TryFrom<&std::ffi::OsStr> for Locale {
105    type Error = LingoraError;
106
107    fn try_from(value: &std::ffi::OsStr) -> Result<Self, Self::Error> {
108        let as_str = value.to_str().ok_or(LingoraError::InvalidLocale(
109            value.to_string_lossy().to_string(),
110        ))?;
111        Locale::from_str(as_str)
112    }
113}
114
115impl Ord for Locale {
116    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
117        self.0.total_cmp(&other.0)
118    }
119}
120
121impl PartialOrd for Locale {
122    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
123        Some(self.cmp(other))
124    }
125}
126
127impl std::fmt::Display for Locale {
128    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129        self.0.fmt(f)
130    }
131}
132
133#[cfg(test)]
134mod test {
135    use pretty_assertions::assert_eq;
136
137    use super::*;
138
139    #[test]
140    fn default_locale_is_system_locale() {
141        let locale = sys_locale::get_locale().unwrap_or("en".into());
142        let expected_locale = Locale(LanguageIdentifier::from_str(&locale).unwrap());
143        assert_eq!(Locale::default(), expected_locale);
144    }
145
146    #[test]
147    fn is_created_from_valid_str() {
148        let locale = Locale::from_str("en-GB").unwrap();
149        assert_eq!(
150            locale,
151            Locale(LanguageIdentifier::from_str("en-GB").unwrap())
152        );
153    }
154
155    #[test]
156    fn is_not_created_from_invalid_str() {
157        let error = Locale::from_str("this-is-not-valid").unwrap_err();
158        assert!(matches!(error, LingoraError::InvalidLocale(_)));
159    }
160}