1use std::{fmt::Formatter, hint::unreachable_unchecked, io::Error as IoError, num::ParseIntError, str::FromStr};
4
5use bevy_asset::{AssetLoader, LoadContext, ParseAssetPathError, io::Reader, ron, ron::de::SpannedError};
6use bevy_utils::HashMap;
7use nom::{
8 Err as NomErr, IResult, Parser,
9 branch::alt,
10 bytes::complete::{is_not, tag, take_while_m_n, take_while1},
11 character::complete::char,
12 combinator::{cut, eof, map_opt, map_res, value, verify},
13 error::{FromExternalError, ParseError},
14 multi::fold,
15 sequence::{delimited, preceded},
16};
17use nom_language::error::{VerboseError, convert_error};
18use serde::{
19 Deserialize, Deserializer, Serialize, Serializer,
20 de::{self, Visitor},
21};
22use thiserror::Error;
23
24use crate::def::{Locale, LocaleCollection, LocaleFmt};
25
26enum FmtFrag<'a> {
27 Literal(&'a str),
28 Escaped(char),
29 Index(usize),
30}
31
32#[inline]
34fn parse_unicode<'a, E: ParseError<&'a str> + FromExternalError<&'a str, ParseIntError>>(
35 input: &'a str,
36) -> IResult<&'a str, char, E> {
37 map_opt(
38 map_res(
39 preceded(
40 char('u'),
41 cut(delimited(
42 char('{'),
43 take_while_m_n(1, 6, |c: char| c.is_ascii_hexdigit()),
44 char('}'),
45 )),
46 ),
47 |hex| u32::from_str_radix(hex, 16),
48 ),
49 char::from_u32,
50 )
51 .parse(input)
52}
53
54#[inline]
56fn parse_escaped<'a, E: ParseError<&'a str> + FromExternalError<&'a str, ParseIntError>>(
57 input: &'a str,
58) -> IResult<&'a str, FmtFrag<'a>, E> {
59 preceded(
60 char('\\'),
61 cut(alt((
62 parse_unicode,
63 value('\n', char('n')),
64 value('\r', char('r')),
65 value('\t', char('t')),
66 value('\u{08}', char('b')),
67 value('\u{0C}', char('f')),
68 value('\\', char('\\')),
69 value('/', char('/')),
70 value('"', char('"')),
71 ))),
72 )
73 .map(FmtFrag::Escaped)
74 .parse(input)
75}
76
77#[inline]
79fn parse_index<'a, E: ParseError<&'a str> + FromExternalError<&'a str, ParseIntError>>(
80 input: &'a str,
81) -> IResult<&'a str, FmtFrag<'a>, E> {
82 map_res(
83 delimited(char('{'), cut(take_while1(|c: char| c.is_ascii_digit())), char('}')),
84 usize::from_str,
85 )
86 .map(FmtFrag::Index)
87 .parse(input)
88}
89
90#[inline]
92fn parse_brace<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, FmtFrag<'a>, E> {
93 alt((value('{', tag("{{")), value('}', tag("}}"))))
94 .map(FmtFrag::Escaped)
95 .parse(input)
96}
97
98#[inline]
100fn parse_literal<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, FmtFrag<'a>, E> {
101 verify(is_not("\\{}"), |s: &str| !s.is_empty())
102 .map(FmtFrag::Literal)
103 .parse(input)
104}
105
106fn parse<'a, E: ParseError<&'a str> + FromExternalError<&'a str, ParseIntError>>(
107 input: &'a str,
108) -> IResult<&'a str, LocaleFmt, E> {
109 cut((
110 fold(
111 0..,
112 alt((parse_literal, parse_brace, parse_index, parse_escaped)),
113 || (0, LocaleFmt::Unformatted(String::new())),
114 |(start, mut fmt), frag| match frag {
115 FmtFrag::Literal(lit) => match &mut fmt {
116 LocaleFmt::Unformatted(format) | LocaleFmt::Formatted { format, .. } => {
117 format.push_str(lit);
118 (start, fmt)
119 }
120 },
121 FmtFrag::Escaped(c) => match &mut fmt {
122 LocaleFmt::Unformatted(format) | LocaleFmt::Formatted { format, .. } => {
123 format.push(c);
124 (start, fmt)
125 }
126 },
127 FmtFrag::Index(i) => {
128 let (end, args) = match fmt {
129 LocaleFmt::Unformatted(format) => {
130 fmt = LocaleFmt::Formatted {
131 format,
132 args: Vec::new(),
133 };
134
135 let LocaleFmt::Formatted { format, args } = &mut fmt else {
137 unsafe { unreachable_unchecked() }
138 };
139 (format.len(), args)
140 }
141 LocaleFmt::Formatted {
142 ref format,
143 ref mut args,
144 } => (format.len(), args),
145 };
146
147 args.push((start..end, i));
148 (end, fmt)
149 }
150 },
151 ),
152 eof,
153 ))
154 .map(|((.., fmt), ..)| fmt)
155 .parse(input)
156}
157
158impl Serialize for LocaleFmt {
159 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
160 where
161 S: Serializer,
162 {
163 match self {
164 Self::Unformatted(raw) => serializer.serialize_str(raw),
165 Self::Formatted { format, args } => {
166 let mut out = String::new();
167
168 let mut last = 0;
169 for &(ref range, i) in args {
170 let start = range.start.min(format.len());
172 let end = range.end.min(format.len());
173 last = last.max(end);
174
175 out.push_str(&format[start..end]);
177 out.push('{');
178 out.push_str(&i.to_string());
179 out.push('}');
180 }
181 out.push_str(&format[last..]);
182
183 serializer.serialize_str(&out)
184 }
185 }
186 }
187}
188
189impl<'de> Deserialize<'de> for LocaleFmt {
190 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
191 where
192 D: Deserializer<'de>,
193 {
194 struct Parser;
195 impl Visitor<'_> for Parser {
196 type Value = LocaleFmt;
197
198 #[inline]
199 fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
200 write!(formatter, "a valid UTF-8 string")
201 }
202
203 #[inline]
204 fn visit_str<E>(self, input: &str) -> Result<Self::Value, E>
205 where
206 E: de::Error,
207 {
208 match parse::<VerboseError<&str>>(input) {
209 Ok(("", fmt)) => Ok(fmt),
210 Ok(..) => unreachable!("`cut(eof)` should've ruled out leftover data"),
211 Err(e) => Err(match e {
212 NomErr::Error(e) | NomErr::Failure(e) => E::custom(convert_error(input, e)),
213 NomErr::Incomplete(..) => unreachable!("only complete operations are used"),
214 }),
215 }
216 }
217 }
218
219 deserializer.deserialize_str(Parser)
220 }
221}
222
223impl FromStr for LocaleFmt {
224 type Err = VerboseError<String>;
225
226 #[inline]
227 fn from_str(input: &str) -> Result<Self, Self::Err> {
228 match parse::<VerboseError<&str>>(input) {
229 Ok(("", fmt)) => Ok(fmt),
230 Ok(..) => unreachable!("`cut(eof)` should've ruled out leftover data"),
231 Err(e) => Err(match e {
232 NomErr::Error(e) | NomErr::Failure(e) => e.into(),
233 NomErr::Incomplete(..) => unreachable!("only complete operations are used"),
234 }),
235 }
236 }
237}
238
239#[derive(Error, Debug)]
241pub enum LocaleError {
242 #[error(transparent)]
244 Io(#[from] IoError),
245 #[error(transparent)]
247 Ron(#[from] SpannedError),
248}
249
250pub struct LocaleLoader;
252impl AssetLoader for LocaleLoader {
253 type Asset = Locale;
254 type Settings = ();
255 type Error = LocaleError;
256
257 async fn load(
258 &self,
259 reader: &mut dyn Reader,
260 _: &Self::Settings,
261 _: &mut LoadContext<'_>,
262 ) -> Result<Self::Asset, Self::Error> {
263 Ok(Locale(ron::de::from_bytes::<HashMap<String, LocaleFmt>>(&{
264 let mut bytes = Vec::new();
265 reader.read_to_end(&mut bytes).await?;
266
267 bytes
268 })?))
269 }
270
271 #[inline]
272 fn extensions(&self) -> &[&str] {
273 &["locale.ron"]
274 }
275}
276
277#[derive(Error, Debug)]
279pub enum LocaleCollectionError {
280 #[error(transparent)]
282 Io(#[from] IoError),
283 #[error(transparent)]
285 Ron(#[from] SpannedError),
286 #[error(transparent)]
288 InvalidPath(#[from] ParseAssetPathError),
289 #[error("locale default '{0}' is defined, but is not available in `locales`")]
291 MissingDefault(String),
292}
293
294#[derive(Deserialize)]
295struct LocaleCollectionFile {
296 default: String,
297 languages: Vec<String>,
298}
299
300pub struct LocaleCollectionLoader;
302impl AssetLoader for LocaleCollectionLoader {
303 type Asset = LocaleCollection;
304 type Settings = ();
305 type Error = LocaleCollectionError;
306
307 async fn load(
308 &self,
309 reader: &mut dyn Reader,
310 _: &Self::Settings,
311 load_context: &mut LoadContext<'_>,
312 ) -> Result<Self::Asset, Self::Error> {
313 let file = ron::de::from_bytes::<LocaleCollectionFile>(&{
314 let mut bytes = Vec::new();
315 reader.read_to_end(&mut bytes).await?;
316
317 bytes
318 })?;
319
320 let mut asset = LocaleCollection {
321 default: file.default,
322 languages: HashMap::with_capacity(file.languages.len()),
323 };
324
325 for key in file.languages {
326 let path = load_context.asset_path().resolve_embed(&format!("locale_{key}.locale.ron"))?;
327 asset.languages.insert(key, load_context.load(path));
328 }
329
330 if !asset.languages.contains_key(&asset.default) {
331 return Err(LocaleCollectionError::MissingDefault(asset.default));
332 }
333
334 Ok(asset)
335 }
336
337 #[inline]
338 fn extensions(&self) -> &[&str] {
339 &["locales.ron"]
340 }
341}