1use std::collections::BTreeMap;
11use std::io::Read;
12use std::sync::OnceLock;
13
14const EN_API_LANG_GZ: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/lang_en_api.ini.gz"));
15const EN_STATIC_LANG_GZ: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/lang_en_static.ini.gz"));
16const ZH_CN_API_LANG_GZ: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/lang_zh_cn_api.ini.gz"));
17const ZH_CN_STATIC_LANG_GZ: &[u8] =
18 include_bytes!(concat!(env!("OUT_DIR"), "/lang_zh_cn_static.ini.gz"));
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
25pub enum LanguageId {
26 SimplifiedChinese = 0,
27 English = 2,
28}
29
30pub fn normalize_language_id(language_id: i32) -> LanguageId {
32 match language_id {
33 0 => LanguageId::SimplifiedChinese,
34 2 => LanguageId::English,
35 _ => LanguageId::English,
36 }
37}
38
39#[derive(Debug, Clone, Default)]
40pub struct LanguageStore {
41 texts: BTreeMap<LanguageId, BTreeMap<u32, String>>,
42}
43
44impl LanguageStore {
45 pub fn new() -> Self {
46 Self::default()
47 }
48
49 pub fn load_ini(&mut self, language: LanguageId, text: &str) {
55 let mut last_key: Option<u32> = None;
56 for raw_line in text.split('\n') {
57 let line = raw_line.strip_suffix('\r').unwrap_or(raw_line);
58 if let Some((key_text, value)) = line.split_once('=')
59 && let Ok(key) = key_text.parse::<u32>()
60 {
61 self.texts
62 .entry(language)
63 .or_default()
64 .insert(key, decode_escaped_text(value));
65 last_key = Some(key);
66 continue;
67 }
68
69 if let Some(key) = last_key
70 && let Some(existing) = self
71 .texts
72 .get_mut(&language)
73 .and_then(|texts| texts.get_mut(&key))
74 {
75 existing.push('\n');
76 existing.push_str(line);
77 }
78 }
79 }
80
81 pub fn translate_key(
82 &self,
83 language: LanguageId,
84 key: u32,
85 fallback: Option<&str>,
86 allow_english_fallback: bool,
87 ) -> Option<String> {
88 if let Some(text) = self.find_non_empty(language, key) {
89 return Some(text.to_string());
90 }
91 if allow_english_fallback
92 && language != LanguageId::English
93 && let Some(text) = self.find_non_empty(LanguageId::English, key)
94 {
95 return Some(text.to_string());
96 }
97 fallback.map(ToOwned::to_owned)
98 }
99
100 pub fn translate_key_with_template(
101 &self,
102 language: LanguageId,
103 key: u32,
104 fallback: Option<&str>,
105 allow_english_fallback: bool,
106 template: &BTreeMap<String, String>,
107 ) -> Option<String> {
108 let text = self.translate_key(language, key, fallback, allow_english_fallback)?;
109 Some(replace_placeholders(text, template))
110 }
111
112 fn find_non_empty(&self, language: LanguageId, key: u32) -> Option<&str> {
113 self.texts
114 .get(&language)
115 .and_then(|texts| texts.get(&key))
116 .map(String::as_str)
117 .filter(|value| !value.is_empty())
118 }
119}
120
121pub fn translate_embedded_key(language: LanguageId, key: u32) -> Option<String> {
122 if let Some(text) = embedded_store(language).translate_key(language, key, None, false) {
123 return Some(text);
124 }
125 if language != LanguageId::English {
126 return embedded_store(LanguageId::English).translate_key(
127 LanguageId::English,
128 key,
129 None,
130 false,
131 );
132 }
133 None
134}
135
136pub fn translate_embedded_key_with_template(
137 language: LanguageId,
138 key: u32,
139 template: &BTreeMap<String, String>,
140) -> Option<String> {
141 if let Some(text) =
142 embedded_store(language).translate_key_with_template(language, key, None, false, template)
143 {
144 return Some(text);
145 }
146 if language != LanguageId::English {
147 return embedded_store(LanguageId::English).translate_key_with_template(
148 LanguageId::English,
149 key,
150 None,
151 false,
152 template,
153 );
154 }
155 None
156}
157
158fn embedded_store(language: LanguageId) -> &'static LanguageStore {
159 static ZH_CN_STORE: OnceLock<LanguageStore> = OnceLock::new();
160 static EN_STORE: OnceLock<LanguageStore> = OnceLock::new();
161
162 match language {
163 LanguageId::SimplifiedChinese => {
164 ZH_CN_STORE.get_or_init(|| load_embedded_store(LanguageId::SimplifiedChinese))
165 }
166 LanguageId::English => EN_STORE.get_or_init(|| load_embedded_store(LanguageId::English)),
167 }
168}
169
170fn load_embedded_store(language: LanguageId) -> LanguageStore {
171 let mut store = LanguageStore::new();
172 match language {
175 LanguageId::SimplifiedChinese => {
176 load_compressed_ini(&mut store, LanguageId::SimplifiedChinese, ZH_CN_API_LANG_GZ);
177 load_compressed_ini(
178 &mut store,
179 LanguageId::SimplifiedChinese,
180 ZH_CN_STATIC_LANG_GZ,
181 );
182 }
183 LanguageId::English => {
184 load_compressed_ini(&mut store, LanguageId::English, EN_API_LANG_GZ);
185 load_compressed_ini(&mut store, LanguageId::English, EN_STATIC_LANG_GZ);
186 }
187 }
188 store
189}
190
191fn load_compressed_ini(store: &mut LanguageStore, language: LanguageId, bytes: &[u8]) {
192 let mut decoder = flate2::read::GzDecoder::new(bytes);
193 let mut text = String::new();
194 if let Err(error) = decoder.read_to_string(&mut text) {
195 tracing::error!(
196 ?language,
197 %error,
198 "embedded C++ language pack decode failed; skipping this pack"
199 );
200 return;
201 }
202 store.load_ini(language, &text);
203}
204
205fn decode_escaped_text(text: &str) -> String {
206 let mut decoded = String::with_capacity(text.len());
207 let mut chars = text.chars();
208 while let Some(ch) = chars.next() {
209 if ch != '\\' {
210 decoded.push(ch);
211 continue;
212 }
213
214 match chars.next() {
215 Some('r') => decoded.push('\r'),
216 Some('n') => decoded.push('\n'),
217 Some('t') => decoded.push('\t'),
218 Some('\\') => decoded.push('\\'),
219 Some(other) => {
220 decoded.push('\\');
221 decoded.push(other);
222 }
223 None => decoded.push('\\'),
224 }
225 }
226 decoded
227}
228
229fn replace_placeholders(mut text: String, template: &BTreeMap<String, String>) -> String {
230 for (key, value) in template {
231 text = text.replace(&format!("{{{{{key}}}}}"), value);
232 text = text.replace(&format!("{{{key}}}"), value);
233 }
234 text
235}
236
237#[cfg(test)]
238mod tests;