finance_query/translation/
lang.rs1use crate::error::{FinanceError, Result};
4
5#[derive(Debug, Clone, PartialEq, Eq, Hash)]
10#[non_exhaustive]
11pub struct Lang {
12 primary: String,
13 script: Option<String>,
14 region: Option<String>,
15}
16
17impl Lang {
18 pub fn parse(tag: &str) -> Result<Self> {
23 let mut primary = None;
24 let mut script = None;
25 let mut region = None;
26 for (i, part) in tag.split(['-', '_']).enumerate() {
27 if i == 0 {
28 if !(2..=3).contains(&part.len()) || !part.chars().all(|c| c.is_ascii_alphabetic())
29 {
30 break;
31 }
32 primary = Some(part.to_ascii_lowercase());
33 } else if part.len() == 4 && part.chars().all(|c| c.is_ascii_alphabetic()) {
34 let mut s = part.to_ascii_lowercase();
35 s[..1].make_ascii_uppercase();
36 script = Some(s);
37 } else if part.len() == 2 && part.chars().all(|c| c.is_ascii_alphabetic()) {
38 region = Some(part.to_ascii_uppercase());
39 }
40 }
41 let primary = primary.ok_or_else(|| FinanceError::InvalidParameter {
42 param: "lang".into(),
43 reason: format!("'{tag}' is not a valid BCP 47 language tag"),
44 })?;
45 Ok(Self {
46 primary,
47 script,
48 region,
49 })
50 }
51
52 pub fn primary(&self) -> &str {
54 &self.primary
55 }
56
57 pub fn code(&self) -> String {
59 match self.effective_script() {
60 Some(script) => format!("{}-{}", self.primary, script),
61 None => self.primary.clone(),
62 }
63 }
64
65 pub fn is_english(&self) -> bool {
67 self.primary == "en"
68 }
69
70 fn effective_script(&self) -> Option<&str> {
73 if let Some(s) = &self.script {
74 return Some(s.as_str());
75 }
76 if self.primary == "zh" {
77 return match self.region.as_deref() {
78 Some("TW") | Some("HK") | Some("MO") => Some("Hant"),
79 _ => Some("Hans"),
80 };
81 }
82 None
83 }
84
85 #[cfg(any(feature = "translation-offline", test))]
88 pub(crate) fn joins_without_space(&self) -> bool {
89 matches!(self.primary.as_str(), "ja" | "zh" | "th" | "km" | "lo")
90 }
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96
97 #[test]
98 fn parses_simple_tags() {
99 let l = Lang::parse("ja").unwrap();
100 assert_eq!(l.primary(), "ja");
101 assert_eq!(l.code(), "ja");
102 assert!(!l.is_english());
103 }
104
105 #[test]
106 fn parses_region_and_underscore() {
107 assert_eq!(Lang::parse("de-DE").unwrap().code(), "de");
108 assert_eq!(Lang::parse("pt_BR").unwrap().code(), "pt");
109 assert_eq!(Lang::parse("ja-JP").unwrap().primary(), "ja");
110 }
111
112 #[test]
113 fn english_variants_are_english() {
114 for tag in ["en", "en-US", "en-GB", "en_AU"] {
115 assert!(Lang::parse(tag).unwrap().is_english());
116 }
117 }
118
119 #[test]
120 fn chinese_script_resolution() {
121 assert_eq!(Lang::parse("zh").unwrap().code(), "zh-Hans");
122 assert_eq!(Lang::parse("zh-CN").unwrap().code(), "zh-Hans");
123 assert_eq!(Lang::parse("zh-TW").unwrap().code(), "zh-Hant");
124 assert_eq!(Lang::parse("zh-HK").unwrap().code(), "zh-Hant");
125 assert_eq!(Lang::parse("zh-Hant-HK").unwrap().code(), "zh-Hant");
126 assert_eq!(Lang::parse("zh-Hans").unwrap().code(), "zh-Hans");
127 }
128
129 #[test]
130 fn invalid_tags_error() {
131 assert!(Lang::parse("").is_err());
132 assert!(Lang::parse("x").is_err());
133 assert!(Lang::parse("1234").is_err());
134 }
135
136 #[test]
137 fn cjk_join_detection() {
138 assert!(Lang::parse("ja").unwrap().joins_without_space());
139 assert!(Lang::parse("zh-TW").unwrap().joins_without_space());
140 assert!(!Lang::parse("ko").unwrap().joins_without_space());
141 assert!(!Lang::parse("de").unwrap().joins_without_space());
142 }
143}