1use crate::commands::{CommandInfo, Occurence, COMMANDS};
2use crate::parser::{FragmentContent, ParsedString};
3use serde::{Deserialize, Serialize};
4use std::collections::{BTreeMap, HashMap};
5
6#[derive(Debug, PartialEq, Copy, Clone)]
7pub enum Dialect {
8 NEWGRF,
9 GAMESCRIPT,
10 OPENTTD,
11}
12
13#[derive(Deserialize, Debug)]
14pub struct LanguageConfig {
15 pub dialect: Dialect,
16 pub cases: Vec<String>,
17 pub genders: Vec<String>,
18 pub plural_count: usize,
19}
20
21#[derive(Debug, PartialEq)]
22pub enum Severity {
23 Error, Warning, }
26
27#[derive(Serialize, Debug, PartialEq)]
28pub struct ValidationError {
29 pub severity: Severity,
30 pub pos_begin: Option<usize>, pub pos_end: Option<usize>,
32 pub message: String,
33 pub suggestion: Option<String>,
34}
35
36#[derive(Serialize, Debug)]
37pub struct ValidationResult {
38 pub errors: Vec<ValidationError>,
39 pub normalized: Option<String>,
40}
41
42impl Dialect {
43 pub fn allow_cases(&self) -> bool {
44 *self != Self::GAMESCRIPT
45 }
46
47 pub fn allow_genders(&self) -> bool {
48 *self != Self::GAMESCRIPT
49 }
50
51 pub fn as_str(&self) -> &'static str {
52 match self {
53 Self::NEWGRF => "newgrf",
54 Self::GAMESCRIPT => "game-script",
55 Self::OPENTTD => "openttd",
56 }
57 }
58}
59
60impl TryFrom<&str> for Dialect {
61 type Error = String;
62
63 fn try_from(value: &str) -> Result<Self, Self::Error> {
64 match value {
65 "newgrf" => Ok(Dialect::NEWGRF),
66 "game-script" => Ok(Dialect::GAMESCRIPT),
67 "openttd" => Ok(Dialect::OPENTTD),
68 _ => Err(String::from("Unknown dialect")),
69 }
70 }
71}
72
73impl<'de> Deserialize<'de> for Dialect {
74 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
75 where
76 D: serde::Deserializer<'de>,
77 {
78 let string = String::deserialize(deserializer)?;
79 let value = Dialect::try_from(string.as_str());
80 value.map_err(|_| {
81 serde::de::Error::unknown_variant(
82 string.as_str(),
83 &["game-script", "newgrf", "openttd"],
84 )
85 })
86 }
87}
88
89impl Serialize for Dialect {
90 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
91 where
92 S: serde::Serializer,
93 {
94 serializer.serialize_str(self.as_str())
95 }
96}
97
98impl Serialize for Severity {
99 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
100 where
101 S: serde::Serializer,
102 {
103 serializer.serialize_str(match self {
104 Self::Error => "error",
105 Self::Warning => "warning",
106 })
107 }
108}
109
110pub fn validate_base(config: &LanguageConfig, base: &String) -> ValidationResult {
119 let mut base = match ParsedString::parse(&base) {
120 Err(err) => {
121 return ValidationResult {
122 errors: vec![ValidationError {
123 severity: Severity::Error,
124 pos_begin: Some(err.pos_begin),
125 pos_end: err.pos_end,
126 message: err.message,
127 suggestion: None,
128 }],
129 normalized: None,
130 };
131 }
132 Ok(parsed) => parsed,
133 };
134 let errs = validate_string(&config, &base, None);
135 if errs.iter().any(|e| e.severity == Severity::Error) {
136 ValidationResult {
137 errors: errs,
138 normalized: None,
139 }
140 } else {
141 sanitize_whitespace(&mut base);
142 normalize_string(&config.dialect, &mut base);
143 ValidationResult {
144 errors: errs,
145 normalized: Some(base.compile()),
146 }
147 }
148}
149
150pub fn validate_translation(
161 config: &LanguageConfig,
162 base: &String,
163 case: &String,
164 translation: &String,
165) -> ValidationResult {
166 let base = match ParsedString::parse(&base) {
167 Err(_) => {
168 return ValidationResult {
169 errors: vec![ValidationError {
170 severity: Severity::Error,
171 pos_begin: None,
172 pos_end: None,
173 message: String::from("Base language text is invalid."),
174 suggestion: Some(String::from("This is a bug; wait until it is fixed.")),
175 }],
176 normalized: None,
177 };
178 }
179 Ok(parsed) => parsed,
180 };
181 if case != "default" {
182 if !config.dialect.allow_cases() {
183 return ValidationResult {
184 errors: vec![ValidationError {
185 severity: Severity::Error,
186 pos_begin: None,
187 pos_end: None,
188 message: String::from("No cases allowed."),
189 suggestion: None,
190 }],
191 normalized: None,
192 };
193 } else if !config.cases.contains(&case) {
194 return ValidationResult {
195 errors: vec![ValidationError {
196 severity: Severity::Error,
197 pos_begin: None,
198 pos_end: None,
199 message: format!("Unknown case '{}'.", case),
200 suggestion: Some(format!("Known cases are: '{}'", config.cases.join("', '"))),
201 }],
202 normalized: None,
203 };
204 }
205 }
206 let mut translation = match ParsedString::parse(&translation) {
207 Err(err) => {
208 return ValidationResult {
209 errors: vec![ValidationError {
210 severity: Severity::Error,
211 pos_begin: Some(err.pos_begin),
212 pos_end: err.pos_end,
213 message: err.message,
214 suggestion: None,
215 }],
216 normalized: None,
217 };
218 }
219 Ok(parsed) => parsed,
220 };
221 let errs = validate_string(&config, &translation, Some(&base));
222 if errs.iter().any(|e| e.severity == Severity::Error) {
223 ValidationResult {
224 errors: errs,
225 normalized: None,
226 }
227 } else {
228 sanitize_whitespace(&mut translation);
229 normalize_string(&config.dialect, &mut translation);
230 ValidationResult {
231 errors: errs,
232 normalized: Some(translation.compile()),
233 }
234 }
235}
236
237fn remove_ascii_ctrl(t: &mut String) {
238 *t = t.replace(|c| char::is_ascii_control(&c), " ");
239}
240
241fn remove_trailing_blanks(t: &mut String) {
242 t.truncate(t.trim_end().len());
243}
244
245fn sanitize_whitespace(parsed: &mut ParsedString) {
248 let mut is_eol = true;
249 for i in (0..parsed.fragments.len()).rev() {
250 let mut is_nl = false;
251 match &mut parsed.fragments[i].content {
252 FragmentContent::Text(t) => {
253 remove_ascii_ctrl(t);
254 if is_eol {
255 remove_trailing_blanks(t);
256 }
257 }
258 FragmentContent::Choice(c) => {
259 for t in &mut c.choices {
260 remove_ascii_ctrl(t);
261 }
262 }
263 FragmentContent::Command(c) => {
264 is_nl = c.name.is_empty();
265 }
266 _ => (),
267 }
268 is_eol = is_nl;
269 }
270}
271
272struct StringSignature {
273 parameters: HashMap<usize, (&'static CommandInfo<'static>, usize)>,
274 nonpositional_count: BTreeMap<String, (Occurence, usize)>,
275 }
277
278fn get_signature(
279 dialect: &Dialect,
280 base: &ParsedString,
281) -> Result<StringSignature, Vec<ValidationError>> {
282 let mut errors = Vec::new();
283 let mut signature = StringSignature {
284 parameters: HashMap::new(),
285 nonpositional_count: BTreeMap::new(),
286 };
287
288 let mut pos = 0;
289 for fragment in &base.fragments {
290 if let FragmentContent::Command(cmd) = &fragment.content {
291 if let Some(info) = COMMANDS
292 .into_iter()
293 .find(|ci| ci.name == cmd.name && ci.dialects.contains(&dialect))
294 {
295 if info.parameters.is_empty() {
296 if let Some(index) = cmd.index {
297 errors.push(ValidationError {
298 severity: Severity::Error,
299 pos_begin: Some(fragment.pos_begin),
300 pos_end: Some(fragment.pos_end),
301 message: format!(
302 "Command '{{{}}}' cannot have a position reference.",
303 cmd.name
304 ),
305 suggestion: Some(format!("Remove '{}:'.", index)),
306 });
307 }
308 let norm_name = String::from(info.get_norm_name());
309 if let Some(existing) = signature.nonpositional_count.get_mut(&norm_name) {
310 existing.1 += 1;
311 } else {
312 signature
313 .nonpositional_count
314 .insert(norm_name, (info.occurence.clone(), 1));
315 }
316 } else {
317 if let Some(index) = cmd.index {
318 pos = index;
319 }
320 if let Some(existing) = signature.parameters.get_mut(&pos) {
321 existing.1 += 1;
322 } else {
323 signature.parameters.insert(pos, (info, 1));
324 }
325 pos += 1;
326 }
327 } else {
328 errors.push(ValidationError {
329 severity: Severity::Error,
330 pos_begin: Some(fragment.pos_begin),
331 pos_end: Some(fragment.pos_end),
332 message: format!("Unknown string command '{{{}}}'.", cmd.name),
333 suggestion: None,
334 });
335 }
336 }
337 }
338
339 if errors.is_empty() {
340 Ok(signature)
341 } else {
342 Err(errors)
343 }
344}
345
346fn validate_string(
347 config: &LanguageConfig,
348 test: &ParsedString,
349 base: Option<&ParsedString>,
350) -> Vec<ValidationError> {
351 let signature: StringSignature;
352 match get_signature(&config.dialect, base.unwrap_or(test)) {
353 Ok(sig) => signature = sig,
354 Err(msgs) => {
355 if base.is_some() {
356 return vec![ValidationError {
357 severity: Severity::Error,
358 pos_begin: None,
359 pos_end: None,
360 message: String::from("Base language text is invalid."),
361 suggestion: Some(String::from("This is a bug; wait until it is fixed.")),
362 }];
363 } else {
364 return msgs;
365 }
366 }
367 }
368
369 let mut errors = Vec::new();
370 let mut positional_count: HashMap<usize, usize> = HashMap::new();
371 let mut nonpositional_count: BTreeMap<String, (Occurence, usize)> = BTreeMap::new();
372 let mut pos = 0;
373 let mut front = 0;
374 for fragment in &test.fragments {
375 match &fragment.content {
376 FragmentContent::Command(cmd) => {
377 let opt_expected = signature
378 .parameters
379 .get(&cmd.index.unwrap_or(pos))
380 .map(|v| (*v).0);
381 let opt_info =
382 opt_expected
383 .filter(|ex| ex.get_norm_name() == cmd.name)
384 .or(COMMANDS.into_iter().find(|ci| {
385 ci.name == cmd.name && ci.dialects.contains(&config.dialect)
386 }));
387 if let Some(info) = opt_info {
388 if let Some(c) = &cmd.case {
389 if !config.dialect.allow_cases() {
390 errors.push(ValidationError {
391 severity: Severity::Error,
392 pos_begin: Some(fragment.pos_begin),
393 pos_end: Some(fragment.pos_end),
394 message: String::from("No case selections allowed."),
395 suggestion: Some(format!("Remove '.{}'.", c)),
396 });
397 } else if !info.allow_case {
398 errors.push(ValidationError {
399 severity: Severity::Error,
400 pos_begin: Some(fragment.pos_begin),
401 pos_end: Some(fragment.pos_end),
402 message: format!(
403 "No case selection allowed for '{{{}}}'.",
404 cmd.name
405 ),
406 suggestion: Some(format!("Remove '.{}'.", c)),
407 });
408 } else if !config.cases.contains(&c) {
409 errors.push(ValidationError {
410 severity: Severity::Error,
411 pos_begin: Some(fragment.pos_begin),
412 pos_end: Some(fragment.pos_end),
413 message: format!("Unknown case '{}'.", c),
414 suggestion: Some(format!(
415 "Known cases are: '{}'",
416 config.cases.join("', '")
417 )),
418 });
419 }
420 }
421
422 if info.parameters.is_empty() {
423 if let Some(index) = cmd.index {
424 errors.push(ValidationError {
425 severity: Severity::Error,
426 pos_begin: Some(fragment.pos_begin),
427 pos_end: Some(fragment.pos_end),
428 message: format!(
429 "Command '{{{}}}' cannot have a position reference.",
430 cmd.name
431 ),
432 suggestion: Some(format!("Remove '{}:'.", index)),
433 });
434 }
435
436 let norm_name = String::from(info.get_norm_name());
437 if let Some(existing) = nonpositional_count.get_mut(&norm_name) {
438 existing.1 += 1;
439 } else {
440 nonpositional_count.insert(norm_name, (info.occurence.clone(), 1));
441 }
442 } else {
443 if let Some(index) = cmd.index {
444 pos = index;
445 }
446
447 if let Some(expected) = opt_expected {
448 if expected.get_norm_name() == info.get_norm_name() {
449 if let Some(existing) = positional_count.get_mut(&pos) {
450 *existing += 1;
451 } else {
452 positional_count.insert(pos, 1);
453 }
454 } else {
455 errors.push(ValidationError {
456 severity: Severity::Error,
457 pos_begin: Some(fragment.pos_begin),
458 pos_end: Some(fragment.pos_end),
459 message: format!(
460 "Expected '{{{}:{}}}', found '{{{}}}'.",
461 pos, expected.name, cmd.name
462 ),
463 suggestion: None,
464 })
465 }
466 } else {
467 errors.push(ValidationError {
468 severity: Severity::Error,
469 pos_begin: Some(fragment.pos_begin),
470 pos_end: Some(fragment.pos_end),
471 message: format!(
472 "There is no parameter in position {}, found '{{{}}}'.",
473 pos, cmd.name
474 ),
475 suggestion: None,
476 });
477 }
478
479 pos += 1;
480 }
481 } else {
482 errors.push(ValidationError {
483 severity: Severity::Error,
484 pos_begin: Some(fragment.pos_begin),
485 pos_end: Some(fragment.pos_end),
486 message: format!("Unknown string command '{{{}}}'.", cmd.name),
487 suggestion: None,
488 });
489 }
490 front = 2;
491 }
492 FragmentContent::Gender(g) => {
493 if !config.dialect.allow_genders() || config.genders.len() < 2 {
494 errors.push(ValidationError {
495 severity: Severity::Error,
496 pos_begin: Some(fragment.pos_begin),
497 pos_end: Some(fragment.pos_end),
498 message: String::from("No gender definitions allowed."),
499 suggestion: Some(String::from("Remove '{G=...}'.")),
500 });
501 } else if front == 2 {
502 errors.push(ValidationError {
503 severity: Severity::Warning,
504 pos_begin: Some(fragment.pos_begin),
505 pos_end: Some(fragment.pos_end),
506 message: String::from("Gender definitions must be at the front."),
507 suggestion: Some(String::from(
508 "Move '{G=...}' to the front of the translation.",
509 )),
510 });
511 } else if front == 1 {
512 errors.push(ValidationError {
513 severity: Severity::Warning,
514 pos_begin: Some(fragment.pos_begin),
515 pos_end: Some(fragment.pos_end),
516 message: String::from("Duplicate gender definition."),
517 suggestion: Some(String::from("Remove the second '{G=...}'.")),
518 });
519 } else {
520 front = 1;
521 if !config.genders.contains(&g.gender) {
522 errors.push(ValidationError {
523 severity: Severity::Error,
524 pos_begin: Some(fragment.pos_begin),
525 pos_end: Some(fragment.pos_end),
526 message: format!("Unknown gender '{}'.", g.gender),
527 suggestion: Some(format!(
528 "Known genders are: '{}'",
529 config.genders.join("', '")
530 )),
531 });
532 }
533 }
534 }
535 FragmentContent::Choice(cmd) => {
536 let opt_ref_pos = match cmd.name.as_str() {
537 "P" => {
538 if pos == 0 {
539 None
540 } else {
541 Some(pos - 1)
542 }
543 }
544 "G" => Some(pos),
545 _ => panic!(),
546 };
547 let opt_ref_pos = cmd.indexref.or(opt_ref_pos);
548 if cmd.name == "G" && (!config.dialect.allow_genders() || config.genders.len() < 2)
549 {
550 errors.push(ValidationError {
551 severity: Severity::Error,
552 pos_begin: Some(fragment.pos_begin),
553 pos_end: Some(fragment.pos_end),
554 message: String::from("No gender choices allowed."),
555 suggestion: Some(String::from("Remove '{G ...}'.")),
556 });
557 } else if cmd.name == "P" && config.plural_count < 2 {
558 errors.push(ValidationError {
559 severity: Severity::Error,
560 pos_begin: Some(fragment.pos_begin),
561 pos_end: Some(fragment.pos_end),
562 message: String::from("No plural choices allowed."),
563 suggestion: Some(String::from("Remove '{P ...}'.")),
564 });
565 } else {
566 match cmd.name.as_str() {
567 "P" => {
568 if cmd.choices.len() != config.plural_count {
569 errors.push(ValidationError {
570 severity: Severity::Error,
571 pos_begin: Some(fragment.pos_begin),
572 pos_end: Some(fragment.pos_end),
573 message: format!(
574 "Expected {} plural choices, found {}.",
575 config.plural_count,
576 cmd.choices.len()
577 ),
578 suggestion: None,
579 });
580 }
581 }
582 "G" => {
583 if cmd.choices.len() != config.genders.len() {
584 errors.push(ValidationError {
585 severity: Severity::Error,
586 pos_begin: Some(fragment.pos_begin),
587 pos_end: Some(fragment.pos_end),
588 message: format!(
589 "Expected {} gender choices, found {}.",
590 config.genders.len(),
591 cmd.choices.len()
592 ),
593 suggestion: None,
594 });
595 }
596 }
597 _ => panic!(),
598 };
599
600 if let Some(ref_info) = opt_ref_pos
601 .and_then(|ref_pos| signature.parameters.get(&ref_pos).map(|v| v.0))
602 {
603 let ref_pos = opt_ref_pos.unwrap();
604 let ref_norm_name = ref_info.get_norm_name();
605 let ref_subpos = match cmd.name.as_str() {
606 "P" => cmd
607 .indexsubref
608 .or(ref_info.def_plural_subindex)
609 .unwrap_or(0),
610 "G" => cmd.indexsubref.unwrap_or(0),
611 _ => panic!(),
612 };
613 if let Some(par_info) = ref_info.parameters.get(ref_subpos) {
614 match cmd.name.as_str() {
615 "P" => {
616 if !par_info.allow_plural {
617 errors.push(ValidationError{
618 severity: Severity::Error,
619 pos_begin: Some(fragment.pos_begin),
620 pos_end: Some(fragment.pos_end),
621 message: format!(
622 "'{{{}}}' references position '{}:{}', but '{{{}:{}}}' does not allow plurals.",
623 cmd.name, ref_pos, ref_subpos, ref_pos, ref_norm_name
624 ),
625 suggestion: None,
626 });
627 }
628 }
629 "G" => {
630 if !par_info.allow_gender {
631 errors.push(ValidationError{
632 severity: Severity::Error,
633 pos_begin: Some(fragment.pos_begin),
634 pos_end: Some(fragment.pos_end),
635 message: format!(
636 "'{{{}}}' references position '{}:{}', but '{{{}:{}}}' does not allow genders.",
637 cmd.name, ref_pos, ref_subpos, ref_pos, ref_norm_name
638 ),
639 suggestion: None,
640 });
641 }
642 }
643 _ => panic!(),
644 };
645 } else {
646 errors.push(ValidationError{
647 severity: Severity::Error,
648 pos_begin: Some(fragment.pos_begin),
649 pos_end: Some(fragment.pos_end),
650 message: format!(
651 "'{{{}}}' references position '{}:{}', but '{{{}:{}}}' only has {} subindices.",
652 cmd.name, ref_pos, ref_subpos, ref_pos, ref_norm_name, ref_info.parameters.len()
653 ),
654 suggestion: None,
655 });
656 }
657 } else {
658 errors.push(ValidationError {
659 severity: Severity::Error,
660 pos_begin: Some(fragment.pos_begin),
661 pos_end: Some(fragment.pos_end),
662 message: format!(
663 "'{{{}}}' references position '{}', which has no parameter.",
664 cmd.name,
665 opt_ref_pos
666 .and_then(|v| isize::try_from(v).ok())
667 .unwrap_or(-1)
668 ),
669 suggestion: if cmd.indexref.is_none() {
670 Some(String::from("Add a position reference."))
671 } else {
672 None
673 },
674 });
675 }
676 }
677 front = 2;
678 }
679 FragmentContent::Text(_) => {
680 front = 2;
681 }
682 }
683 }
684
685 for (pos, (info, ex_count)) in &signature.parameters {
686 let norm_name = info.get_norm_name();
687 let found_count = positional_count.get(pos).cloned().unwrap_or(0);
688 if info.occurence != Occurence::ANY && found_count == 0 {
689 errors.push(ValidationError {
690 severity: Severity::Error,
691 pos_begin: None,
692 pos_end: None,
693 message: format!("String command '{{{}:{}}}' is missing.", pos, norm_name),
694 suggestion: None,
695 });
696 } else if info.occurence == Occurence::EXACT && *ex_count != found_count {
697 errors.push(ValidationError {
698 severity: Severity::Warning,
699 pos_begin: None,
700 pos_end: None,
701 message: format!(
702 "String command '{{{}:{}}}': expected {} times, found {} times.",
703 pos, norm_name, ex_count, found_count
704 ),
705 suggestion: None,
706 });
707 }
708 }
709
710 for (norm_name, (occurence, ex_count)) in &signature.nonpositional_count {
711 let found_count = nonpositional_count.get(norm_name).map(|v| v.1).unwrap_or(0);
712 if *occurence != Occurence::ANY && found_count == 0 {
713 errors.push(ValidationError {
714 severity: Severity::Warning,
715 pos_begin: None,
716 pos_end: None,
717 message: format!("String command '{{{}}}' is missing.", norm_name),
718 suggestion: None,
719 });
720 } else if *occurence == Occurence::EXACT && *ex_count != found_count {
721 errors.push(ValidationError {
722 severity: Severity::Warning,
723 pos_begin: None,
724 pos_end: None,
725 message: format!(
726 "String command '{{{}}}': expected {} times, found {} times.",
727 norm_name, ex_count, found_count
728 ),
729 suggestion: None,
730 });
731 }
732 }
733 for (norm_name, (occurence, _)) in &nonpositional_count {
734 if *occurence != Occurence::ANY && signature.nonpositional_count.get(norm_name).is_none() {
735 errors.push(ValidationError {
736 severity: Severity::Warning,
737 pos_begin: None,
738 pos_end: None,
739 message: format!("String command '{{{}}}' is unexpected.", norm_name),
740 suggestion: Some(String::from("Remove this command.")),
741 });
742 }
743 }
744
745 errors
746}
747
748fn normalize_string(dialect: &Dialect, parsed: &mut ParsedString) {
749 let mut parameters = HashMap::new();
750
751 let mut pos = 0;
752 for fragment in &mut parsed.fragments {
753 match &mut fragment.content {
754 FragmentContent::Command(cmd) => {
755 if let Some(info) = COMMANDS
756 .into_iter()
757 .find(|ci| ci.name == cmd.name && ci.dialects.contains(&dialect))
758 {
759 if let Some(norm_name) = info.norm_name {
760 cmd.name = String::from(norm_name);
762 }
763 if !info.parameters.is_empty() {
764 if let Some(index) = cmd.index {
765 pos = index;
766 } else {
767 cmd.index = Some(pos);
769 }
770 parameters.insert(pos, info);
771 pos += 1;
772 }
773 }
774 }
775 FragmentContent::Choice(cmd) => {
776 match cmd.name.as_str() {
777 "P" => {
778 if cmd.indexref.is_none() && pos > 0 {
779 cmd.indexref = Some(pos - 1);
781 }
782 }
783 "G" => {
784 if cmd.indexref.is_none() {
785 cmd.indexref = Some(pos);
787 }
788 }
789 _ => panic!(),
790 };
791 }
792 _ => (),
793 }
794 }
795
796 for fragment in &mut parsed.fragments {
797 if let FragmentContent::Choice(cmd) = &mut fragment.content {
798 if let Some(ref_info) = cmd.indexref.and_then(|pos| parameters.get(&pos)) {
799 if cmd.indexsubref == ref_info.def_plural_subindex.or(Some(0)) {
800 cmd.indexsubref = None;
802 }
803 }
804 }
805 }
806}
807
808#[cfg(test)]
809mod tests {
810 use super::*;
811
812 #[test]
813 fn test_sanitize() {
814 let mut s1 = String::from("");
815 let mut s2 = String::from(" a b c ");
816 let mut s3 = String::from("\0a\tb\rc\r\n");
817 let mut s4 = String::from("abc\u{b3}");
818 remove_ascii_ctrl(&mut s1);
819 remove_ascii_ctrl(&mut s2);
820 remove_ascii_ctrl(&mut s3);
821 remove_ascii_ctrl(&mut s4);
822 assert_eq!(s1, String::from(""));
823 assert_eq!(s2, String::from(" a b c "));
824 assert_eq!(s3, String::from(" a b c "));
825 assert_eq!(s4, String::from("abc\u{b3}"));
826 remove_trailing_blanks(&mut s1);
827 remove_trailing_blanks(&mut s2);
828 remove_trailing_blanks(&mut s3);
829 remove_trailing_blanks(&mut s4);
830 assert_eq!(s1, String::from(""));
831 assert_eq!(s2, String::from(" a b c"));
832 assert_eq!(s3, String::from(" a b c"));
833 assert_eq!(s4, String::from("abc\u{b3}"));
834 }
835
836 #[test]
837 fn test_signature_empty() {
838 let parsed = ParsedString::parse("").unwrap();
839 let sig = get_signature(&Dialect::OPENTTD, &parsed).unwrap();
840 assert!(sig.parameters.is_empty());
841 assert!(sig.nonpositional_count.is_empty());
842 }
843
844 #[test]
845 fn test_signature_pos() {
846 let parsed = ParsedString::parse("{P a b}{RED}{NUM}{NBSP}{MONO_FONT}{5:STRING.foo}{RED}{2:STRING3.bar}{RAW_STRING}{3:RAW_STRING}{G c d}").unwrap();
847 let sig = get_signature(&Dialect::OPENTTD, &parsed).unwrap();
848 assert_eq!(sig.parameters.len(), 4);
849 assert_eq!(sig.parameters.get(&0).unwrap().0.name, "NUM");
850 assert_eq!(sig.parameters.get(&0).unwrap().1, 1);
851 assert_eq!(sig.parameters.get(&5).unwrap().0.name, "STRING");
852 assert_eq!(sig.parameters.get(&5).unwrap().1, 1);
853 assert_eq!(sig.parameters.get(&2).unwrap().0.name, "STRING3");
854 assert_eq!(sig.parameters.get(&2).unwrap().1, 1);
855 assert_eq!(sig.parameters.get(&3).unwrap().0.name, "RAW_STRING");
856 assert_eq!(sig.parameters.get(&3).unwrap().1, 2);
857 assert_eq!(sig.nonpositional_count.len(), 3);
858 assert_eq!(
859 sig.nonpositional_count.get("RED"),
860 Some(&(Occurence::NONZERO, 2))
861 );
862 assert_eq!(
863 sig.nonpositional_count.get("MONO_FONT"),
864 Some(&(Occurence::EXACT, 1))
865 );
866 assert_eq!(
867 sig.nonpositional_count.get("NBSP"),
868 Some(&(Occurence::ANY, 1))
869 );
870 }
871
872 #[test]
873 fn test_signature_dialect() {
874 let parsed = ParsedString::parse("{RAW_STRING}").unwrap();
875
876 let sig = get_signature(&Dialect::OPENTTD, &parsed).unwrap();
877 assert_eq!(sig.parameters.len(), 1);
878 assert_eq!(sig.parameters.get(&0).unwrap().0.name, "RAW_STRING");
879 assert_eq!(sig.parameters.get(&0).unwrap().1, 1);
880 assert_eq!(sig.nonpositional_count.len(), 0);
881
882 let err = get_signature(&Dialect::NEWGRF, &parsed).err().unwrap();
883 assert_eq!(err.len(), 1);
884 assert_eq!(
885 err[0],
886 ValidationError {
887 severity: Severity::Error,
888 pos_begin: Some(0),
889 pos_end: Some(12),
890 message: String::from("Unknown string command '{RAW_STRING}'."),
891 suggestion: None,
892 }
893 );
894 }
895
896 #[test]
897 fn test_signature_unknown() {
898 let parsed = ParsedString::parse("{FOOBAR}").unwrap();
899 let err = get_signature(&Dialect::OPENTTD, &parsed).err().unwrap();
900 assert_eq!(err.len(), 1);
901 assert_eq!(
902 err[0],
903 ValidationError {
904 severity: Severity::Error,
905 pos_begin: Some(0),
906 pos_end: Some(8),
907 message: String::from("Unknown string command '{FOOBAR}'."),
908 suggestion: None,
909 }
910 );
911 }
912
913 #[test]
914 fn test_signature_nonpos() {
915 let parsed = ParsedString::parse("{1:RED}").unwrap();
916 let err = get_signature(&Dialect::OPENTTD, &parsed).err().unwrap();
917 assert_eq!(err.len(), 1);
918 assert_eq!(
919 err[0],
920 ValidationError {
921 severity: Severity::Error,
922 pos_begin: Some(0),
923 pos_end: Some(7),
924 message: String::from("Command '{RED}' cannot have a position reference."),
925 suggestion: Some(String::from("Remove '1:'.")),
926 }
927 );
928 }
929
930 #[test]
931 fn test_validate_empty() {
932 let config = LanguageConfig {
933 dialect: Dialect::OPENTTD,
934 cases: vec![],
935 genders: vec![],
936 plural_count: 0,
937 };
938 let base = ParsedString::parse("").unwrap();
939
940 let val_base = validate_string(&config, &base, None);
941 assert_eq!(val_base.len(), 0);
942
943 let val_trans = validate_string(&config, &base, Some(&base));
944 assert_eq!(val_trans.len(), 0);
945 }
946
947 #[test]
948 fn test_validate_invalid() {
949 let config = LanguageConfig {
950 dialect: Dialect::OPENTTD,
951 cases: vec![],
952 genders: vec![],
953 plural_count: 0,
954 };
955 let base = ParsedString::parse("{FOOBAR}").unwrap();
956
957 let val_base = validate_string(&config, &base, None);
958 assert_eq!(val_base.len(), 1);
959 assert_eq!(
960 val_base[0],
961 ValidationError {
962 severity: Severity::Error,
963 pos_begin: Some(0),
964 pos_end: Some(8),
965 message: String::from("Unknown string command '{FOOBAR}'."),
966 suggestion: None,
967 }
968 );
969
970 let val_trans = validate_string(&config, &base, Some(&base));
971 assert_eq!(val_trans.len(), 1);
972 assert_eq!(
973 val_trans[0],
974 ValidationError {
975 severity: Severity::Error,
976 pos_begin: None,
977 pos_end: None,
978 message: String::from("Base language text is invalid."),
979 suggestion: Some(String::from("This is a bug; wait until it is fixed.")),
980 }
981 );
982 }
983
984 #[test]
985 fn test_validate_positional() {
986 let config = LanguageConfig {
987 dialect: Dialect::OPENTTD,
988 cases: vec![],
989 genders: vec![],
990 plural_count: 0,
991 };
992 let base = ParsedString::parse("{NUM}").unwrap();
993 let val_base = validate_string(&config, &base, None);
994 assert_eq!(val_base.len(), 0);
995
996 {
997 let trans = ParsedString::parse("{0:NUM}").unwrap();
998 let val_trans = validate_string(&config, &trans, Some(&base));
999 assert_eq!(val_trans.len(), 0);
1000 }
1001 {
1002 let trans = ParsedString::parse("{FOOBAR}{NUM}").unwrap();
1003 let val_trans = validate_string(&config, &trans, Some(&base));
1004 assert_eq!(val_trans.len(), 1);
1005 assert_eq!(
1006 val_trans[0],
1007 ValidationError {
1008 severity: Severity::Error,
1009 pos_begin: Some(0),
1010 pos_end: Some(8),
1011 message: String::from("Unknown string command '{FOOBAR}'."),
1012 suggestion: None,
1013 }
1014 );
1015 }
1016 {
1017 let trans = ParsedString::parse("{1:NUM}").unwrap();
1018 let val_trans = validate_string(&config, &trans, Some(&base));
1019 assert_eq!(val_trans.len(), 2);
1020 assert_eq!(
1021 val_trans[0],
1022 ValidationError {
1023 severity: Severity::Error,
1024 pos_begin: Some(0),
1025 pos_end: Some(7),
1026 message: String::from("There is no parameter in position 1, found '{NUM}'."),
1027 suggestion: None,
1028 }
1029 );
1030 assert_eq!(
1031 val_trans[1],
1032 ValidationError {
1033 severity: Severity::Error,
1034 pos_begin: None,
1035 pos_end: None,
1036 message: String::from("String command '{0:NUM}' is missing."),
1037 suggestion: None,
1038 }
1039 );
1040 }
1041 {
1042 let trans = ParsedString::parse("{COMMA}").unwrap();
1043 let val_trans = validate_string(&config, &trans, Some(&base));
1044 assert_eq!(val_trans.len(), 2);
1045 assert_eq!(
1046 val_trans[0],
1047 ValidationError {
1048 severity: Severity::Error,
1049 pos_begin: Some(0),
1050 pos_end: Some(7),
1051 message: String::from("Expected '{0:NUM}', found '{COMMA}'."),
1052 suggestion: None,
1053 }
1054 );
1055 assert_eq!(
1056 val_trans[1],
1057 ValidationError {
1058 severity: Severity::Error,
1059 pos_begin: None,
1060 pos_end: None,
1061 message: String::from("String command '{0:NUM}' is missing."),
1062 suggestion: None,
1063 }
1064 );
1065 }
1066 {
1067 let trans = ParsedString::parse("{0:NUM}{0:NUM}").unwrap();
1068 let val_trans = validate_string(&config, &trans, Some(&base));
1069 assert_eq!(val_trans.len(), 1);
1070 assert_eq!(
1071 val_trans[0],
1072 ValidationError {
1073 severity: Severity::Warning,
1074 pos_begin: None,
1075 pos_end: None,
1076 message: String::from(
1077 "String command '{0:NUM}': expected 1 times, found 2 times."
1078 ),
1079 suggestion: None,
1080 }
1081 );
1082 }
1083 }
1084
1085 #[test]
1086 fn test_validate_front() {
1087 let config = LanguageConfig {
1088 dialect: Dialect::OPENTTD,
1089 cases: vec![],
1090 genders: vec![String::from("a"), String::from("b")],
1091 plural_count: 0,
1092 };
1093 let base = ParsedString::parse("{BIG_FONT}foo{NUM}").unwrap();
1094 let val_base = validate_string(&config, &base, None);
1095 assert_eq!(val_base.len(), 0);
1096
1097 {
1098 let trans = ParsedString::parse("{G=a}{BIG_FONT}bar{NUM}").unwrap();
1099 let val_trans = validate_string(&config, &trans, Some(&base));
1100 assert_eq!(val_trans.len(), 0);
1101 }
1102 {
1103 let trans = ParsedString::parse("{G=a}{G=a}{BIG_FONT}bar{NUM}").unwrap();
1104 let val_trans = validate_string(&config, &trans, Some(&base));
1105 assert_eq!(val_trans.len(), 1);
1106 assert_eq!(
1107 val_trans[0],
1108 ValidationError {
1109 severity: Severity::Warning,
1110 pos_begin: Some(5),
1111 pos_end: Some(10),
1112 message: String::from("Duplicate gender definition."),
1113 suggestion: Some(String::from("Remove the second '{G=...}'.")),
1114 }
1115 );
1116 }
1117 {
1118 let trans = ParsedString::parse("{BIG_FONT}{G=a}bar{NUM}").unwrap();
1119 let val_trans = validate_string(&config, &trans, Some(&base));
1120 assert_eq!(val_trans.len(), 1);
1121 assert_eq!(
1122 val_trans[0],
1123 ValidationError {
1124 severity: Severity::Warning,
1125 pos_begin: Some(10),
1126 pos_end: Some(15),
1127 message: String::from("Gender definitions must be at the front."),
1128 suggestion: Some(String::from(
1129 "Move '{G=...}' to the front of the translation."
1130 )),
1131 }
1132 );
1133 }
1134 {
1135 let trans = ParsedString::parse("foo{BIG_FONT}bar{NUM}").unwrap();
1136 let val_trans = validate_string(&config, &trans, Some(&base));
1137 assert_eq!(val_trans.len(), 0);
1138 }
1139 {
1140 let trans = ParsedString::parse("foo{G=a}bar{NUM}").unwrap();
1141 let val_trans = validate_string(&config, &trans, Some(&base));
1142 assert_eq!(val_trans.len(), 2);
1143 assert_eq!(
1144 val_trans[0],
1145 ValidationError {
1146 severity: Severity::Warning,
1147 pos_begin: Some(3),
1148 pos_end: Some(8),
1149 message: String::from("Gender definitions must be at the front."),
1150 suggestion: Some(String::from(
1151 "Move '{G=...}' to the front of the translation."
1152 )),
1153 }
1154 );
1155 assert_eq!(
1156 val_trans[1],
1157 ValidationError {
1158 severity: Severity::Warning,
1159 pos_begin: None,
1160 pos_end: None,
1161 message: String::from("String command '{BIG_FONT}' is missing."),
1162 suggestion: None,
1163 }
1164 );
1165 }
1166 }
1167
1168 #[test]
1169 fn test_validate_position_references() {
1170 let config = LanguageConfig {
1171 dialect: Dialect::OPENTTD,
1172 cases: vec![String::from("x"), String::from("y")],
1173 genders: vec![String::from("a"), String::from("b")],
1174 plural_count: 2,
1175 };
1176 let base = ParsedString::parse("{RED}{NUM}{STRING3}").unwrap();
1177 let val_base = validate_string(&config, &base, None);
1178 assert_eq!(val_base.len(), 0);
1179
1180 {
1181 let trans = ParsedString::parse("{RED}{1:STRING.x}{0:NUM}").unwrap();
1182 let val_trans = validate_string(&config, &trans, Some(&base));
1183 assert_eq!(val_trans.len(), 0);
1184 }
1185 {
1186 let trans = ParsedString::parse("{2:RED}{1:STRING.z}{0:NUM.x}").unwrap();
1187 let val_trans = validate_string(&config, &trans, Some(&base));
1188 assert_eq!(val_trans.len(), 3);
1189 assert_eq!(
1190 val_trans[0],
1191 ValidationError {
1192 severity: Severity::Error,
1193 pos_begin: Some(0),
1194 pos_end: Some(7),
1195 message: String::from("Command '{RED}' cannot have a position reference."),
1196 suggestion: Some(String::from("Remove '2:'.")),
1197 }
1198 );
1199 assert_eq!(
1200 val_trans[1],
1201 ValidationError {
1202 severity: Severity::Error,
1203 pos_begin: Some(7),
1204 pos_end: Some(19),
1205 message: String::from("Unknown case 'z'."),
1206 suggestion: Some(String::from("Known cases are: 'x', 'y'")),
1207 }
1208 );
1209 assert_eq!(
1210 val_trans[2],
1211 ValidationError {
1212 severity: Severity::Error,
1213 pos_begin: Some(19),
1214 pos_end: Some(28),
1215 message: String::from("No case selection allowed for '{NUM}'."),
1216 suggestion: Some(String::from("Remove '.x'.")),
1217 }
1218 );
1219 }
1220 {
1221 let trans = ParsedString::parse("{RED}{NUM}{G i j}{P i j}{STRING.y}").unwrap();
1222 let val_trans = validate_string(&config, &trans, Some(&base));
1223 assert_eq!(val_trans.len(), 0);
1224 }
1225 {
1226 let trans = ParsedString::parse("{RED}{NUM}{G 0 i j}{P 1 i j}{STRING.y}").unwrap();
1227 let val_trans = validate_string(&config, &trans, Some(&base));
1228 assert_eq!(val_trans.len(), 2);
1229 assert_eq!(
1230 val_trans[0],
1231 ValidationError {
1232 severity: Severity::Error,
1233 pos_begin: Some(10),
1234 pos_end: Some(19),
1235 message: String::from(
1236 "'{G}' references position '0:0', but '{0:NUM}' does not allow genders."
1237 ),
1238 suggestion: None,
1239 }
1240 );
1241 assert_eq!(
1242 val_trans[1],
1243 ValidationError {
1244 severity: Severity::Error,
1245 pos_begin: Some(19),
1246 pos_end: Some(28),
1247 message: String::from(
1248 "'{P}' references position '1:0', but '{1:STRING}' does not allow plurals."
1249 ),
1250 suggestion: None,
1251 }
1252 );
1253 }
1254 {
1255 let trans = ParsedString::parse("{RED}{NUM}{G 1:1 i j}{P 1:3 i j}{STRING.y}").unwrap();
1256 let val_trans = validate_string(&config, &trans, Some(&base));
1257 assert_eq!(val_trans.len(), 0);
1258 }
1259 {
1260 let trans = ParsedString::parse("{RED}{NUM}{G 1:4 i j}{P 1:4 i j}{STRING.y}").unwrap();
1261 let val_trans = validate_string(&config, &trans, Some(&base));
1262 assert_eq!(val_trans.len(), 2);
1263 assert_eq!(
1264 val_trans[0],
1265 ValidationError {
1266 severity: Severity::Error,
1267 pos_begin: Some(10),
1268 pos_end: Some(21),
1269 message: String::from(
1270 "'{G}' references position '1:4', but '{1:STRING}' only has 4 subindices."
1271 ),
1272 suggestion: None,
1273 }
1274 );
1275 assert_eq!(
1276 val_trans[1],
1277 ValidationError {
1278 severity: Severity::Error,
1279 pos_begin: Some(21),
1280 pos_end: Some(32),
1281 message: String::from(
1282 "'{P}' references position '1:4', but '{1:STRING}' only has 4 subindices."
1283 ),
1284 suggestion: None,
1285 }
1286 );
1287 }
1288 {
1289 let trans = ParsedString::parse("{RED}{NUM}{G 2 i j}{P 2 i j}{STRING.y}").unwrap();
1290 let val_trans = validate_string(&config, &trans, Some(&base));
1291 assert_eq!(val_trans.len(), 2);
1292 assert_eq!(
1293 val_trans[0],
1294 ValidationError {
1295 severity: Severity::Error,
1296 pos_begin: Some(10),
1297 pos_end: Some(19),
1298 message: String::from("'{G}' references position '2', which has no parameter."),
1299 suggestion: None,
1300 }
1301 );
1302 assert_eq!(
1303 val_trans[1],
1304 ValidationError {
1305 severity: Severity::Error,
1306 pos_begin: Some(19),
1307 pos_end: Some(28),
1308 message: String::from("'{P}' references position '2', which has no parameter."),
1309 suggestion: None,
1310 }
1311 );
1312 }
1313 {
1314 let trans = ParsedString::parse("{RED}{P i j}{NUM}{STRING.y}{G i j}").unwrap();
1315 let val_trans = validate_string(&config, &trans, Some(&base));
1316 assert_eq!(val_trans.len(), 2);
1317 assert_eq!(
1318 val_trans[0],
1319 ValidationError {
1320 severity: Severity::Error,
1321 pos_begin: Some(5),
1322 pos_end: Some(12),
1323 message: String::from(
1324 "'{P}' references position '-1', which has no parameter."
1325 ),
1326 suggestion: Some(String::from("Add a position reference.")),
1327 }
1328 );
1329 assert_eq!(
1330 val_trans[1],
1331 ValidationError {
1332 severity: Severity::Error,
1333 pos_begin: Some(27),
1334 pos_end: Some(34),
1335 message: String::from("'{G}' references position '2', which has no parameter."),
1336 suggestion: Some(String::from("Add a position reference.")),
1337 }
1338 );
1339 }
1340 }
1341
1342 #[test]
1343 fn test_validate_nochoices() {
1344 let config = LanguageConfig {
1345 dialect: Dialect::OPENTTD,
1346 cases: vec![],
1347 genders: vec![],
1348 plural_count: 1,
1349 };
1350 let base = ParsedString::parse("{NUM}{STRING3}").unwrap();
1351 let val_base = validate_string(&config, &base, None);
1352 assert_eq!(val_base.len(), 0);
1353
1354 {
1355 let trans = ParsedString::parse("{G=a}{NUM}{P a}{G a}{STRING}").unwrap();
1356 let val_trans = validate_string(&config, &trans, Some(&base));
1357 assert_eq!(val_trans.len(), 3);
1358 assert_eq!(
1359 val_trans[0],
1360 ValidationError {
1361 severity: Severity::Error,
1362 pos_begin: Some(0),
1363 pos_end: Some(5),
1364 message: String::from("No gender definitions allowed."),
1365 suggestion: Some(String::from("Remove '{G=...}'.")),
1366 }
1367 );
1368 assert_eq!(
1369 val_trans[1],
1370 ValidationError {
1371 severity: Severity::Error,
1372 pos_begin: Some(10),
1373 pos_end: Some(15),
1374 message: String::from("No plural choices allowed."),
1375 suggestion: Some(String::from("Remove '{P ...}'.")),
1376 }
1377 );
1378 assert_eq!(
1379 val_trans[2],
1380 ValidationError {
1381 severity: Severity::Error,
1382 pos_begin: Some(15),
1383 pos_end: Some(20),
1384 message: String::from("No gender choices allowed."),
1385 suggestion: Some(String::from("Remove '{G ...}'.")),
1386 }
1387 );
1388 }
1389 }
1390
1391 #[test]
1392 fn test_validate_gschoices() {
1393 let config = LanguageConfig {
1394 dialect: Dialect::GAMESCRIPT,
1395 cases: vec![String::from("x"), String::from("y")],
1396 genders: vec![String::from("a"), String::from("b")],
1397 plural_count: 2,
1398 };
1399 let base = ParsedString::parse("{NUM}{STRING3}").unwrap();
1400 let val_base = validate_string(&config, &base, None);
1401 assert_eq!(val_base.len(), 0);
1402
1403 {
1404 let trans = ParsedString::parse("{G=a}{NUM}{P a b}{G a b}{STRING.x}").unwrap();
1405 let val_trans = validate_string(&config, &trans, Some(&base));
1406 assert_eq!(val_trans.len(), 3);
1407 assert_eq!(
1408 val_trans[0],
1409 ValidationError {
1410 severity: Severity::Error,
1411 pos_begin: Some(0),
1412 pos_end: Some(5),
1413 message: String::from("No gender definitions allowed."),
1414 suggestion: Some(String::from("Remove '{G=...}'.")),
1415 }
1416 );
1417 assert_eq!(
1418 val_trans[1],
1419 ValidationError {
1420 severity: Severity::Error,
1421 pos_begin: Some(17),
1422 pos_end: Some(24),
1423 message: String::from("No gender choices allowed."),
1424 suggestion: Some(String::from("Remove '{G ...}'.")),
1425 }
1426 );
1427 assert_eq!(
1428 val_trans[2],
1429 ValidationError {
1430 severity: Severity::Error,
1431 pos_begin: Some(24),
1432 pos_end: Some(34),
1433 message: String::from("No case selections allowed."),
1434 suggestion: Some(String::from("Remove '.x'.")),
1435 }
1436 );
1437 }
1438 }
1439
1440 #[test]
1441 fn test_validate_choices() {
1442 let config = LanguageConfig {
1443 dialect: Dialect::OPENTTD,
1444 cases: vec![String::from("x"), String::from("y")],
1445 genders: vec![String::from("a"), String::from("b")],
1446 plural_count: 2,
1447 };
1448 let base = ParsedString::parse("{NUM}{STRING3}").unwrap();
1449 let val_base = validate_string(&config, &base, None);
1450 assert_eq!(val_base.len(), 0);
1451
1452 {
1453 let trans = ParsedString::parse("{G=a}{NUM}{P a b}{G a b}{STRING.x}").unwrap();
1454 let val_trans = validate_string(&config, &trans, Some(&base));
1455 assert_eq!(val_trans.len(), 0);
1456 }
1457 {
1458 let trans = ParsedString::parse("{G=c}{NUM}{P a b c}{G a b c}{STRING.z}").unwrap();
1459 let val_trans = validate_string(&config, &trans, Some(&base));
1460 assert_eq!(val_trans.len(), 4);
1461 assert_eq!(
1462 val_trans[0],
1463 ValidationError {
1464 severity: Severity::Error,
1465 pos_begin: Some(0),
1466 pos_end: Some(5),
1467 message: String::from("Unknown gender 'c'."),
1468 suggestion: Some(String::from("Known genders are: 'a', 'b'")),
1469 }
1470 );
1471 assert_eq!(
1472 val_trans[1],
1473 ValidationError {
1474 severity: Severity::Error,
1475 pos_begin: Some(10),
1476 pos_end: Some(19),
1477 message: String::from("Expected 2 plural choices, found 3."),
1478 suggestion: None,
1479 }
1480 );
1481 assert_eq!(
1482 val_trans[2],
1483 ValidationError {
1484 severity: Severity::Error,
1485 pos_begin: Some(19),
1486 pos_end: Some(28),
1487 message: String::from("Expected 2 gender choices, found 3."),
1488 suggestion: None,
1489 }
1490 );
1491 assert_eq!(
1492 val_trans[3],
1493 ValidationError {
1494 severity: Severity::Error,
1495 pos_begin: Some(28),
1496 pos_end: Some(38),
1497 message: String::from("Unknown case 'z'."),
1498 suggestion: Some(String::from("Known cases are: 'x', 'y'")),
1499 }
1500 );
1501 }
1502 }
1503
1504 #[test]
1505 fn test_validate_nonpositional() {
1506 let config = LanguageConfig {
1507 dialect: Dialect::OPENTTD,
1508 cases: vec![],
1509 genders: vec![],
1510 plural_count: 0,
1511 };
1512 let base = ParsedString::parse("{RED}{NBSP}{}{GREEN}{NBSP}{}{RED}{TRAIN}").unwrap();
1513 let val_base = validate_string(&config, &base, None);
1514 assert_eq!(val_base.len(), 0);
1515
1516 {
1517 let trans = ParsedString::parse("{RED}{}{GREEN}{}{RED}{TRAIN}").unwrap();
1518 let val_trans = validate_string(&config, &trans, Some(&base));
1519 assert_eq!(val_trans.len(), 0);
1520 }
1521 {
1522 let trans = ParsedString::parse("{RED}{}{GREEN}{NBSP}{RED}{NBSP}{GREEN}{}{RED}{TRAIN}")
1523 .unwrap();
1524 let val_trans = validate_string(&config, &trans, Some(&base));
1525 assert_eq!(val_trans.len(), 0);
1526 }
1527 {
1528 let trans =
1529 ParsedString::parse("{RED}{}{RED}{TRAIN}{BLUE}{TRAIN}{RIGHT_ARROW}{SHIP}").unwrap();
1530 let val_trans = validate_string(&config, &trans, Some(&base));
1531 assert_eq!(val_trans.len(), 4);
1532 assert_eq!(
1533 val_trans[0],
1534 ValidationError {
1535 severity: Severity::Warning,
1536 pos_begin: None,
1537 pos_end: None,
1538 message: String::from("String command '{GREEN}' is missing."),
1539 suggestion: None,
1540 }
1541 );
1542 assert_eq!(
1543 val_trans[1],
1544 ValidationError {
1545 severity: Severity::Warning,
1546 pos_begin: None,
1547 pos_end: None,
1548 message: String::from(
1549 "String command '{TRAIN}': expected 1 times, found 2 times."
1550 ),
1551 suggestion: None,
1552 }
1553 );
1554 assert_eq!(
1555 val_trans[2],
1556 ValidationError {
1557 severity: Severity::Warning,
1558 pos_begin: None,
1559 pos_end: None,
1560 message: String::from("String command '{BLUE}' is unexpected."),
1561 suggestion: Some(String::from("Remove this command.")),
1562 }
1563 );
1564 assert_eq!(
1565 val_trans[3],
1566 ValidationError {
1567 severity: Severity::Warning,
1568 pos_begin: None,
1569 pos_end: None,
1570 message: String::from("String command '{SHIP}' is unexpected."),
1571 suggestion: Some(String::from("Remove this command.")),
1572 }
1573 );
1574 }
1575 }
1576
1577 #[test]
1578 fn test_normalize_cmd() {
1579 let mut parsed =
1580 ParsedString::parse("{RED}{NBSP}{2:RAW_STRING}{0:STRING5}{COMMA}").unwrap();
1581 normalize_string(&Dialect::OPENTTD, &mut parsed);
1582 let result = parsed.compile();
1583 assert_eq!(result, "{RED}{NBSP}{2:STRING}{0:STRING}{1:COMMA}");
1584 }
1585
1586 #[test]
1587 fn test_normalize_ref() {
1588 let mut parsed = ParsedString::parse("{RED}{NBSP}{P a b}{2:STRING}{P 1 a b}{G 0:1 a b}{0:STRING}{G 0 a b}{P 0:1 a b}{COMMA}{P a b}{G a b}").unwrap();
1589 normalize_string(&Dialect::OPENTTD, &mut parsed);
1590 let result = parsed.compile();
1591 assert_eq!(result, "{RED}{NBSP}{P a b}{2:STRING}{P 1 a b}{G 0:1 a b}{0:STRING}{G 0 a b}{P 0:1 a b}{1:COMMA}{P 1 a b}{G 2 a b}");
1592 }
1593
1594 #[test]
1595 fn test_normalize_subref() {
1596 let mut parsed = ParsedString::parse(
1597 "{NUM}{P 0:0 a b}{G 1:0 a b}{G 1:1 a b}{STRING}{P 1:2 a b}{CARGO_LONG}{P 2:1 a b}",
1598 )
1599 .unwrap();
1600 normalize_string(&Dialect::OPENTTD, &mut parsed);
1601 let result = parsed.compile();
1602 assert_eq!(
1603 result,
1604 "{0:NUM}{P 0 a b}{G 1 a b}{G 1:1 a b}{1:STRING}{P 1:2 a b}{2:CARGO_LONG}{P 2 a b}"
1605 );
1606 }
1607}