Skip to main content

futu_core/localization/
store.rs

1use 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    /// Load a C++ language `.ini` text file.
16    ///
17    /// Lines shaped as `numeric_key=value` start or replace one entry. A line
18    /// without a numeric key is appended to the previous entry with a newline,
19    /// matching C++ multiline behavior.
20    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}