lingora_core/domain/
locale.rs1use 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#[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 pub fn language(&self) -> &Language {
29 &self.0.language
30 }
31
32 pub fn script(&self) -> Option<&Script> {
34 self.0.script.as_ref()
35 }
36
37 pub fn region(&self) -> Option<&Region> {
39 self.0.region.as_ref()
40 }
41
42 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 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}