oxygengine_core/
localization.rs

1use crate::{
2    app::AppBuilder,
3    assets::{
4        asset::AssetId, database::AssetsDatabase, protocols::localization::LocalizationAsset,
5    },
6    ecs::{
7        pipeline::{PipelineBuilder, PipelineBuilderError},
8        Universe,
9    },
10};
11use pest::{iterators::Pair, Parser};
12use std::{collections::HashMap, fmt::Write};
13
14#[allow(clippy::upper_case_acronyms)]
15mod parser {
16    #[derive(Parser)]
17    #[grammar = "localization.pest"]
18    pub(super) struct SentenceParser;
19}
20
21#[derive(Default)]
22pub struct Localization {
23    default_language: Option<String>,
24    current_language: Option<String>,
25    /// { text id: { language: text format } }
26    map: HashMap<String, HashMap<String, String>>,
27}
28
29impl Localization {
30    pub fn default_language(&self) -> Option<&str> {
31        self.default_language.as_deref()
32    }
33
34    pub fn set_default_language(&mut self, value: Option<String>) {
35        self.default_language = value;
36    }
37
38    pub fn current_language(&self) -> Option<&str> {
39        self.current_language.as_deref()
40    }
41
42    pub fn set_current_language(&mut self, value: Option<String>) {
43        self.current_language = value.clone();
44        if self.default_language.is_none() && value.is_some() {
45            self.default_language = value;
46        }
47    }
48
49    pub fn add_text(&mut self, id: &str, language: &str, text_format: &str) {
50        if let Some(map) = self.map.get_mut(id) {
51            map.insert(language.to_owned(), text_format.to_owned());
52        } else {
53            let mut map = HashMap::new();
54            map.insert(language.to_owned(), text_format.to_owned());
55            self.map.insert(id.to_owned(), map);
56        }
57    }
58
59    pub fn remove_text(&mut self, id: &str, language: &str) -> bool {
60        let (empty, removed) = if let Some(map) = self.map.get_mut(id) {
61            let removed = map.remove(language).is_some();
62            let empty = map.is_empty();
63            (empty, removed)
64        } else {
65            (false, false)
66        };
67        if empty {
68            self.map.remove(id);
69        }
70        removed
71    }
72
73    pub fn remove_text_all(&mut self, id: &str) -> bool {
74        self.map.remove(id).is_some()
75    }
76
77    pub fn remove_language(&mut self, lang: &str) {
78        for map in self.map.values_mut() {
79            map.remove(lang);
80        }
81    }
82
83    pub fn find_text_format(&self, id: &str) -> Option<&str> {
84        if let Some(current) = &self.current_language {
85            if let Some(default) = &self.default_language {
86                if let Some(map) = self.map.get(id) {
87                    return map
88                        .get(current)
89                        .or_else(|| map.get(default))
90                        .or(None)
91                        .as_ref()
92                        .map(|v| v.as_str());
93                }
94            }
95        }
96        None
97    }
98
99    pub fn format_text(&self, id: &str, params: &[(&str, &str)]) -> Result<String, String> {
100        if let Some(text_format) = self.find_text_format(id) {
101            match parser::SentenceParser::parse(parser::Rule::sentence, text_format) {
102                Ok(mut ast) => {
103                    let pair = ast.next().unwrap();
104                    match pair.as_rule() {
105                        parser::Rule::sentence => Ok(Self::parse_sentence_inner(pair, params)),
106                        _ => unreachable!(),
107                    }
108                }
109                Err(error) => Err(error.to_string()),
110            }
111        } else {
112            Err(format!("There is no text format for id: {}", id))
113        }
114    }
115
116    fn parse_sentence_inner(pair: Pair<parser::Rule>, params: &[(&str, &str)]) -> String {
117        let mut result = String::new();
118        for p in pair.into_inner() {
119            match p.as_rule() {
120                parser::Rule::text => result.push_str(&p.as_str().replace("\\|", "|")),
121                parser::Rule::identifier => {
122                    let ident = p.as_str();
123                    if let Some((_, v)) = params.iter().find(|(id, _)| id == &ident) {
124                        result.push_str(v);
125                    } else {
126                        write!(result, "{{@{}}}", ident).unwrap();
127                    }
128                }
129                _ => {}
130            }
131        }
132        result
133    }
134}
135
136#[macro_export]
137macro_rules! localization_format_text {
138    ($res:expr, $text:expr, $( $id:ident => $value:expr ),*) => {
139        $crate::localization::Localization::format_text(
140            &$res,
141            $text,
142            &[ $( (stringify!($id), &$value.to_string()) ),* ]
143        )
144    }
145}
146
147#[derive(Default)]
148pub struct LocalizationSystemCache {
149    language_table: HashMap<AssetId, String>,
150}
151
152pub type LocalizationSystemResources<'a> = (
153    &'a AssetsDatabase,
154    &'a mut Localization,
155    &'a mut LocalizationSystemCache,
156);
157
158pub fn localization_system(universe: &mut Universe) {
159    let (assets, mut localization, mut cache) =
160        universe.query_resources::<LocalizationSystemResources>();
161
162    for id in assets.lately_loaded_protocol("locals") {
163        let id = *id;
164        let asset = assets
165            .asset_by_id(id)
166            .expect("trying to use not loaded localization asset");
167        let asset = asset
168            .get::<LocalizationAsset>()
169            .expect("trying to use non-localization asset");
170        for (k, v) in &asset.dictionary {
171            localization.add_text(k, &asset.language, v);
172        }
173        cache.language_table.insert(id, asset.language.clone());
174    }
175    for id in assets.lately_unloaded_protocol("locals") {
176        if let Some(name) = cache.language_table.remove(id) {
177            localization.remove_language(&name);
178        }
179    }
180}
181
182pub fn bundle_installer<PB, PMS>(
183    builder: &mut AppBuilder<PB>,
184    _: (),
185) -> Result<(), PipelineBuilderError>
186where
187    PB: PipelineBuilder,
188{
189    builder.install_resource(Localization::default());
190    builder.install_resource(LocalizationSystemCache::default());
191    builder.install_system::<LocalizationSystemResources>(
192        "localization",
193        localization_system,
194        &[],
195    )?;
196    Ok(())
197}