futu_core/localization/
store.rs1use std::collections::BTreeMap;
2
3use super::LanguageId;
4
5#[derive(Debug, Clone, Default)]
6pub struct LanguageStore {
7 texts: BTreeMap<LanguageId, BTreeMap<u32, String>>,
8}
9
10impl LanguageStore {
11 pub fn new() -> Self {
12 Self::default()
13 }
14
15 pub fn load_ini(&mut self, language: LanguageId, text: &str) {
21 self.load_ini_count(language, text);
22 }
23
24 pub fn load_ini_count(&mut self, language: LanguageId, text: &str) -> usize {
25 let mut last_key: Option<u32> = None;
26 let mut loaded = 0usize;
27 let mut lines = text.split('\n').peekable();
28 while let Some(raw_line) = lines.next() {
29 if raw_line.is_empty() && lines.peek().is_none() {
30 break;
31 }
32 let line = raw_line.strip_suffix('\r').unwrap_or(raw_line);
33 if let Some((key_text, value)) = line.split_once('=')
34 && let Ok(key) = key_text.parse::<u32>()
35 {
36 self.texts
37 .entry(language)
38 .or_default()
39 .insert(key, decode_escaped_text(value));
40 last_key = Some(key);
41 loaded += 1;
42 continue;
43 }
44
45 if let Some(key) = last_key
46 && let Some(existing) = self
47 .texts
48 .get_mut(&language)
49 .and_then(|texts| texts.get_mut(&key))
50 {
51 existing.push('\n');
52 existing.push_str(line);
53 }
54 }
55 loaded
56 }
57
58 pub fn translate_key(
59 &self,
60 language: LanguageId,
61 key: u32,
62 fallback: Option<&str>,
63 allow_english_fallback: bool,
64 ) -> Option<String> {
65 if let Some(text) = self.find_non_empty(language, key) {
66 return Some(text.to_string());
67 }
68 if allow_english_fallback
69 && language != LanguageId::English
70 && let Some(text) = self.find_non_empty(LanguageId::English, key)
71 {
72 return Some(text.to_string());
73 }
74 fallback.map(ToOwned::to_owned)
75 }
76
77 pub fn translate_key_with_template(
78 &self,
79 language: LanguageId,
80 key: u32,
81 fallback: Option<&str>,
82 allow_english_fallback: bool,
83 template: &BTreeMap<String, String>,
84 ) -> Option<String> {
85 let text = self.translate_key(language, key, fallback, allow_english_fallback)?;
86 Some(replace_placeholders(text, template))
87 }
88
89 pub(super) fn find_non_empty(&self, language: LanguageId, key: u32) -> Option<&str> {
90 self.texts
91 .get(&language)
92 .and_then(|texts| texts.get(&key))
93 .map(String::as_str)
94 .filter(|value| !value.is_empty())
95 }
96}
97
98fn decode_escaped_text(text: &str) -> String {
99 let mut decoded = String::with_capacity(text.len());
100 let mut chars = text.chars();
101 while let Some(ch) = chars.next() {
102 if ch != '\\' {
103 decoded.push(ch);
104 continue;
105 }
106
107 match chars.next() {
108 Some('r') => decoded.push('\r'),
109 Some('n') => decoded.push('\n'),
110 Some('t') => decoded.push('\t'),
111 Some('\\') => decoded.push('\\'),
112 Some(other) => {
113 decoded.push('\\');
114 decoded.push(other);
115 }
116 None => decoded.push('\\'),
117 }
118 }
119 decoded
120}
121
122pub(super) fn replace_placeholders(
123 mut text: String,
124 template: &BTreeMap<String, String>,
125) -> String {
126 for (key, value) in template {
127 text = text.replace(&format!("{{{{{key}}}}}"), value);
128 text = text.replace(&format!("{{{key}}}"), value);
129 }
130 text
131}