oxygengine_core/
localization.rs1use 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 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}