Skip to main content

egui_i18n/
lib.rs

1use once_cell::sync::Lazy;
2use std::collections::HashMap;
3use std::fs;
4use std::path::Path;
5use std::sync::RwLock;
6
7pub use self::vendor::classic::parse_translations;
8
9#[cfg(feature = "fluent")]
10pub use fluent;
11
12#[cfg(feature = "fluent")]
13pub use fluent_bundle;
14
15mod vendor;
16
17// ---------------------------------------------------------------------------
18// Global configuration
19// ---------------------------------------------------------------------------
20
21struct Config {
22  language: String,
23  fallback: String,
24  #[cfg(feature = "fluent")]
25  use_isolating: bool,
26}
27
28#[allow(clippy::derivable_impls)]
29impl Default for Config {
30  fn default() -> Self {
31    Self {
32      language: String::new(),
33      fallback: String::new(),
34      #[cfg(feature = "fluent")]
35      use_isolating: true,
36    }
37  }
38}
39
40// todo: migrate to egui context
41static CONFIG: Lazy<RwLock<Config>> = Lazy::new(|| RwLock::new(Config::default()));
42
43// ---------------------------------------------------------------------------
44// Language / fallback configuration
45// ---------------------------------------------------------------------------
46
47pub fn set_language(locale: &str) {
48  CONFIG.write().unwrap().language = locale.to_string();
49}
50
51pub fn get_language() -> String {
52  CONFIG.read().unwrap().language.clone()
53}
54
55pub fn set_fallback(locale: &str) {
56  CONFIG.write().unwrap().fallback = locale.to_string();
57}
58
59pub fn get_fallback() -> String {
60  CONFIG.read().unwrap().fallback.clone()
61}
62
63// ---------------------------------------------------------------------------
64// Fluent-only: isolating marks configuration
65// ---------------------------------------------------------------------------
66
67#[cfg(feature = "fluent")]
68pub fn set_use_isolating(value: bool) {
69  CONFIG.write().unwrap().use_isolating = value;
70}
71
72#[cfg(feature = "fluent")]
73pub fn get_use_isolating() -> bool {
74  CONFIG.read().unwrap().use_isolating
75}
76
77// ---------------------------------------------------------------------------
78// Loaded-language enumeration
79// ---------------------------------------------------------------------------
80
81#[cfg(feature = "fluent")]
82pub fn languages() -> Vec<String> {
83  vendor::fluent::languages()
84}
85
86#[cfg(not(feature = "fluent"))]
87pub fn languages() -> Vec<String> {
88  vendor::classic::languages()
89}
90
91// ---------------------------------------------------------------------------
92// Translation loading — from HashMap
93// ---------------------------------------------------------------------------
94
95/// Load translations from a plain key-value map.
96///
97/// Only available in `classic` mode. In `fluent` mode this function returns
98/// an error because a flat `HashMap` cannot represent Fluent syntax; use
99/// [`load_translations_from_text`] with raw `.ftl` content instead.
100#[cfg(not(feature = "fluent"))]
101pub fn load_translations_from_map(
102  language: impl AsRef<str>,
103  translations: HashMap<String, String>,
104) -> Result<(), String> {
105  vendor::classic::load_translations_from_map(language, translations);
106  Ok(())
107}
108
109#[cfg(feature = "fluent")]
110pub fn load_translations_from_map(
111  _language: impl AsRef<str>,
112  _translations: HashMap<String, String>,
113) -> Result<(), String> {
114  Err(
115    "load_translations_from_map is not supported in fluent mode; \
116     use load_translations_from_text with raw .ftl content instead"
117      .to_string(),
118  )
119}
120
121// ---------------------------------------------------------------------------
122// Translation loading — from text
123// ---------------------------------------------------------------------------
124
125#[cfg(feature = "fluent")]
126pub fn load_translations_from_text(
127  language: impl AsRef<str>,
128  content: impl AsRef<str>,
129) -> Result<(), String> {
130  vendor::fluent::load_translations_from_text(
131    language.as_ref(),
132    content.as_ref(),
133    get_use_isolating(),
134  )
135}
136
137#[cfg(not(feature = "fluent"))]
138pub fn load_translations_from_text(
139  language: impl AsRef<str>,
140  content: impl AsRef<str>,
141) -> Result<(), String> {
142  vendor::classic::load_translations_from_text(language, content)
143}
144
145// ---------------------------------------------------------------------------
146// Translation loading — from filesystem path
147// ---------------------------------------------------------------------------
148
149/// Load all `.egl` / `.ftl` translation files from a directory (or a single
150/// file). Each file's stem is used as the language identifier.
151pub fn load_translations_from_path(path: impl AsRef<str>) -> Result<(), String> {
152  let path_ref = Path::new(path.as_ref());
153  let mut files = vec![];
154
155  if path_ref.is_file() {
156    files.push(path_ref.to_path_buf());
157  } else {
158    let read_dir = match fs::read_dir(path_ref) {
159      Ok(v) => v,
160      Err(e) => return Err(format!("{:?}", e)),
161    };
162    for entry in read_dir {
163      let path_file = match entry {
164        Ok(dir_entry) => dir_entry.path(),
165        Err(e) => {
166          log::warn!("failed to read directory entry: {:?}", e);
167          continue;
168        },
169      };
170      let allowed = path_file
171        .extension()
172        .map(|ext| {
173          let ext = ext.to_string_lossy().to_lowercase();
174          ext == "egl" || ext == "ftl"
175        })
176        .unwrap_or(false);
177      if !allowed {
178        continue;
179      }
180      files.push(path_file);
181    }
182  }
183
184  for file in files {
185    let name = match file.file_stem() {
186      Some(v) => v.to_string_lossy().to_string(),
187      None => continue,
188    };
189    match fs::read_to_string(&file) {
190      Ok(content) => load_translations_from_text(name, content)?,
191      Err(e) => return Err(format!("{:?}", e)),
192    }
193  }
194  Ok(())
195}
196
197// ---------------------------------------------------------------------------
198// Translation execution
199// ---------------------------------------------------------------------------
200
201#[cfg(not(feature = "fluent"))]
202pub fn translate_classic(key: &str, args: &HashMap<&str, String>) -> String {
203  let language = get_language();
204  let fallback = get_fallback();
205  vendor::classic::translate(language, fallback, key, args)
206}
207
208#[cfg(feature = "fluent")]
209pub fn translate_fluent(key: &str, args: &crate::fluent::FluentArgs) -> String {
210  let language = get_language();
211  let fallback = get_fallback();
212  vendor::fluent::translate(language, fallback, key, args)
213}
214
215// ---------------------------------------------------------------------------
216// tr! macro
217// ---------------------------------------------------------------------------
218
219#[cfg(not(feature = "fluent"))]
220#[macro_export]
221macro_rules! tr {
222  ($key:expr, {$($name:ident: $val:expr),*}) => {{
223    let mut args = std::collections::HashMap::new();
224    $(
225      args.insert(stringify!($name), $val.to_string());
226    )*
227    $crate::translate_classic($key, &args)
228  }};
229  ($key:expr) => {{
230    $crate::translate_classic($key, &std::collections::HashMap::new())
231  }};
232}
233
234#[cfg(feature = "fluent")]
235#[macro_export]
236macro_rules! tr {
237  ($key:expr, {$($name:ident: $val:expr),*}) => {{
238    let mut args = $crate::fluent::FluentArgs::new();
239    $(
240      args.set(stringify!($name), $val);
241    )*
242    $crate::translate_fluent($key, &args)
243  }};
244  ($key:expr) => {{
245    $crate::translate_fluent($key, &$crate::fluent::FluentArgs::new())
246  }};
247}