1use crate::prelude::*;
37
38mod formats;
39mod lines;
40mod matching;
41mod places;
42mod source;
43mod tag;
44mod texts;
45
46use alloc::collections::{BTreeMap, BTreeSet};
47use core::fmt;
48
49use serde::de::{Deserializer, MapAccess, Visitor};
50use serde::{Deserialize, Serialize};
51
52use crate::dialogue::{DialogueLimits, Tokens};
53use crate::error::{ContentError, codes};
54use crate::font::CellWidths;
55use crate::format::AssetKind;
56use crate::import_settings::is_settings_file;
57use crate::names::Names;
58use crate::ron::{parse_ron, parse_ron_implicit_some};
59use crate::tip::placeholders;
60use places::UiValue;
61
62pub use formats::LangPackRon;
63pub use lines::{LineEntry, LineText, Orphan};
64pub use matching::best_match;
65pub use places::{Overflow, Place, Places};
66pub use source::{LineKind, SourceLine, SourceText, names_key};
67pub use tag::{LangCode, TagError};
68pub use texts::{TextAt, TextPart};
69
70#[deprecated(note = "a game's source language is `Lang::source_lang()`")]
72pub const ENGLISH: &str = tag::DEFAULT_SOURCE;
73
74const UI_FILE: &str = "ui.ron";
76
77pub(crate) const INFO_FILE: &str = "lang.ron";
79
80const DATA_FILE: &str = "data.ron";
82
83const DIALOGUE_DIR: &str = "dialogue/";
85
86const DIALOGUE_EXTENSION: &str = ".ron";
88
89pub fn text_width(text: &str, widths: &CellWidths) -> usize {
94 text.chars().map(|c| widths.of_char(c)).sum()
95}
96
97#[derive(Debug, Clone, PartialEq, Eq)]
101#[non_exhaustive]
102pub struct SourceLang {
103 pub code: LangCode,
105 pub name: String,
107 pub wide_font: Option<String>,
110 pub wide_punctuation: bool,
114}
115
116impl SourceLang {
117 pub fn new(code: LangCode, name: impl Into<String>) -> Self {
120 Self {
121 code,
122 name: name.into(),
123 wide_font: None,
124 wide_punctuation: false,
125 }
126 }
127
128 #[must_use]
131 pub fn with_wide_font(self, set: impl Into<String>) -> Self {
132 Self {
133 wide_font: Some(set.into()),
134 ..self
135 }
136 }
137
138 #[must_use]
141 pub fn with_wide_punctuation(self, on: bool) -> Self {
142 Self {
143 wide_punctuation: on,
144 ..self
145 }
146 }
147
148 pub fn widths(&self, base: &CellWidths) -> CellWidths {
151 base.clone()
152 .in_lang(self.wide_font.as_deref(), self.wide_punctuation)
153 }
154}
155
156impl Default for SourceLang {
157 fn default() -> Self {
159 Self::new(LangCode::default(), "English")
160 }
161}
162
163#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
165pub enum MadeBy {
166 Machine,
168 Human(String),
170}
171
172#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
174#[serde(deny_unknown_fields)]
175pub struct LangInfo {
176 pub name: String,
178 pub made_by: MadeBy,
180 #[serde(default)]
186 pub wide_font: Option<String>,
187 #[serde(default)]
191 pub wide_punctuation: bool,
192}
193
194impl LangInfo {
195 pub fn widths(&self, base: &CellWidths) -> CellWidths {
199 base.clone()
200 .in_lang(self.wide_font.as_deref(), self.wide_punctuation)
201 }
202}
203
204#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
206#[serde(deny_unknown_fields)]
207pub struct Entry {
208 pub key: String,
210 pub source: String,
212 pub text: String,
214}
215
216#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
218pub struct LangPack {
219 pub info: LangInfo,
221 ui: BTreeMap<String, Entry>,
223 data: BTreeMap<String, Entry>,
225 lines: BTreeMap<String, LineEntry>,
227}
228
229impl LangPack {
230 pub fn entry(&self, key: &str) -> Option<&Entry> {
232 self.ui.get(key).or_else(|| self.data.get(key))
233 }
234}
235
236#[derive(Debug, Clone, Default, PartialEq, Eq)]
239pub struct LangStatus {
240 pub missing: Vec<String>,
242 pub stale: Vec<String>,
244 pub orphans: Vec<Orphan>,
247}
248
249#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
253#[serde(transparent)]
254pub struct English(pub BTreeMap<String, String>);
255
256impl AssetKind for English {
257 const NAME: &'static str = "english";
258}
259
260#[derive(Debug, Clone, Default, PartialEq, Eq)]
262pub struct Lang {
263 written_in: SourceLang,
265 ui: BTreeMap<String, String>,
267 source: SourceText,
269 packs: BTreeMap<LangCode, LangPack>,
271 places: Places,
274}
275
276impl Lang {
277 pub fn new(ui: BTreeMap<String, String>, packs: BTreeMap<LangCode, LangPack>) -> Self {
281 Self::with_source(ui, SourceText::default(), packs)
282 }
283
284 pub fn with_source(
289 ui: BTreeMap<String, String>,
290 source: SourceText,
291 packs: BTreeMap<LangCode, LangPack>,
292 ) -> Self {
293 Self {
294 written_in: SourceLang::default(),
295 ui,
296 source,
297 packs,
298 places: BTreeMap::new(),
299 }
300 }
301
302 #[must_use]
304 pub fn with_source_lang(self, source: SourceLang) -> Self {
305 Self {
306 written_in: source,
307 ..self
308 }
309 }
310
311 pub fn source_lang(&self) -> &SourceLang {
313 &self.written_in
314 }
315
316 pub fn source(&self) -> &SourceText {
318 &self.source
319 }
320
321 pub fn has(&self, key: &str) -> bool {
323 self.ui.contains_key(key)
324 }
325
326 pub fn source_text(&self, key: &str) -> Option<&str> {
329 let line = || self.source.lines.get(key).map(|line| &line.text);
330 self.ui
331 .get(key)
332 .or_else(|| self.source.data.get(key))
333 .or_else(line)
334 .map(String::as_str)
335 }
336
337 #[deprecated(note = "renamed `source_text`: the source language needn't be English")]
339 pub fn english(&self, key: &str) -> Option<&str> {
340 self.source_text(key)
341 }
342
343 pub fn pack(&self, code: &LangCode) -> Option<&LangPack> {
345 self.packs.get(code)
346 }
347
348 pub fn widths(&self, code: &LangCode, base: &CellWidths) -> CellWidths {
352 if let Some(pack) = self.pack(code) {
353 pack.info.widths(base)
354 } else if *code == self.written_in.code {
355 self.written_in.widths(base)
356 } else {
357 base.clone()
358 }
359 }
360
361 pub fn codes(&self) -> impl Iterator<Item = &LangCode> {
363 self.packs.keys()
364 }
365
366 fn packs_along<'a>(
370 &'a self,
371 code: &LangCode,
372 ) -> impl Iterator<Item = (&'a LangCode, &'a LangPack)> {
373 let source = self.written_in.code.as_str();
374 tag::ancestry(code.as_str())
375 .filter(move |tag| *tag != source)
376 .filter_map(|tag| self.packs.get_key_value(tag))
377 }
378
379 pub fn chain(&self, code: &LangCode) -> Vec<LangCode> {
385 let mut chain: Vec<LangCode> = self.packs_along(code).map(|(c, _)| c.clone()).collect();
386 chain.push(self.written_in.code.clone());
387 chain
388 }
389
390 pub fn text<'a>(&'a self, code: &LangCode, key: &'a str) -> &'a str {
394 let Some(source) = self.ui.get(key) else {
395 return key;
396 };
397 self.packs_along(code)
398 .find_map(|(_, pack)| pack.ui.get(key).filter(|entry| entry.source == *source))
399 .map_or(source, |entry| &entry.text)
400 }
401
402 pub fn status(&self, code: &LangCode) -> Option<LangStatus> {
405 if *code == self.written_in.code {
406 return Some(LangStatus::default());
407 }
408 let pack = self.packs.get(code)?;
409 let mut status = LangStatus::default();
410 let mut check = |key: &String, english: &String, source: Option<&String>| match source {
411 None => status.missing.push(key.clone()),
412 Some(source) if source != english => status.stale.push(key.clone()),
413 Some(_) => {}
414 };
415 for (key, english) in &self.ui {
416 check(key, english, pack.ui.get(key).map(|e| &e.source));
417 }
418 for (key, english) in &self.source.data {
419 check(key, english, pack.data.get(key).map(|e| &e.source));
420 }
421 for (key, line) in &self.source.lines {
422 check(key, &line.text, pack.lines.get(key).map(|e| &e.source));
423 }
424 status.orphans = pack.orphans(&self.source);
425 Some(status)
426 }
427}
428
429struct Pairs<V>(Vec<(String, V)>);
432
433impl<'de, V: Deserialize<'de>> Deserialize<'de> for Pairs<V> {
434 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
435 struct PairsVisitor<V>(core::marker::PhantomData<V>);
436
437 impl<'de, V: Deserialize<'de>> Visitor<'de> for PairsVisitor<V> {
438 type Value = Pairs<V>;
439
440 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
441 f.write_str("a map of key to text")
442 }
443
444 fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Pairs<V>, A::Error> {
445 let mut pairs = Vec::new();
446 while let Some(pair) = map.next_entry()? {
447 pairs.push(pair);
448 }
449 Ok(Pairs(pairs))
450 }
451 }
452
453 deserializer.deserialize_map(PairsVisitor(core::marker::PhantomData))
454 }
455}
456
457fn key_ok(key: &str) -> bool {
460 !key.is_empty()
461 && key
462 .bytes()
463 .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_' || b == b'.')
464}
465
466pub fn english_from_source(
471 file: &str,
472 source: &str,
473) -> Result<BTreeMap<String, String>, Vec<ContentError>> {
474 english_and_places_from_source(file, source).map(|(english, _)| english)
475}
476
477pub fn english_and_places_from_source(
482 file: &str,
483 source: &str,
484) -> Result<(BTreeMap<String, String>, Places), Vec<ContentError>> {
485 let Pairs(pairs) =
486 parse_ron_implicit_some::<Pairs<UiValue>>(file, source).map_err(|e| vec![e])?;
487 let mut errors = Vec::new();
488 let mut english = BTreeMap::new();
489 let mut places = BTreeMap::new();
490 for (key, UiValue { text, place }) in pairs {
491 let at = |message: &str| ContentError::new(file, format!("\"{key}\": {message}"));
492 if !key_ok(&key) {
493 errors.push(at("a key must be lowercase letters, digits, `_` and `.`"));
494 }
495 if english.contains_key(&key) {
496 errors.push(at("the key is used twice").with_code(codes::DUPLICATE));
497 }
498 if place.max_cells == Some(0) {
499 errors.push(at("max_cells must be at least 1"));
500 }
501 if place.note.as_ref().is_some_and(|n| n.trim().is_empty()) {
502 errors.push(at("a note must not be empty"));
503 }
504 if place != Place::default() {
505 places.insert(key.clone(), place);
506 }
507 english.insert(key, text);
508 }
509 if errors.is_empty() {
510 Ok((english, places))
511 } else {
512 Err(errors)
513 }
514}
515
516#[derive(Clone, Copy)]
520pub struct PackRules<'a> {
521 pub data_problem: fn(key: &str, text: &str) -> Option<String>,
526 pub tokens: &'a dyn Tokens,
528 pub limits: DialogueLimits,
530 pub widths: &'a CellWidths,
533 pub variants: &'a [&'a str],
537}
538
539impl fmt::Debug for PackRules<'_> {
540 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
541 f.debug_struct("PackRules")
542 .field("limits", &self.limits)
543 .field("widths", &self.widths)
544 .field("variants", &self.variants)
545 .finish_non_exhaustive()
546 }
547}
548
549fn data_text_problem(
553 key: &str,
554 text: &str,
555 data_problem: fn(&str, &str) -> Option<String>,
556) -> Option<String> {
557 if !key.starts_with(source::NAMES_PREFIX) {
558 return data_problem(key, text);
559 }
560 if text.trim().is_empty() {
561 return Some("a name must not be empty".to_owned());
562 }
563 text.contains(['{', '}'])
564 .then(|| "a name can't hold a brace".to_owned())
565}
566
567pub type Source<'a> = (&'a str, &'a str);
569
570#[derive(Debug, Clone, Default)]
572pub struct PackSources<'a> {
573 pub info: Source<'a>,
575 pub ui: Source<'a>,
577 pub data: Option<Source<'a>>,
579 pub dialogue: Vec<Source<'a>>,
581}
582
583fn entries(
592 (file, source): Source<'_>,
593 english: &BTreeMap<String, String>,
594 no_key: &str,
595 data_problem: fn(&str, &str) -> Option<String>,
596 errors: &mut Vec<ContentError>,
597) -> BTreeMap<String, Entry> {
598 let entries = parse_ron::<Vec<Entry>>(file, source)
599 .map_err(|e| errors.push(e))
600 .unwrap_or_default();
601 let mut by_key = BTreeMap::new();
602 for entry in entries {
603 let at = |message: &str| ContentError::new(file, format!("\"{}\": {message}", entry.key));
604 if !english.contains_key(&entry.key) {
605 errors.push(at(no_key));
606 }
607 if by_key.contains_key(&entry.key) {
608 errors.push(at("the key is used twice").with_code(codes::DUPLICATE));
609 }
610 let names = |text| placeholders(text).into_iter().collect::<BTreeSet<_>>();
611 if names(&entry.text) != names(&entry.source) {
612 errors.push(at(
613 "the text and its source must have the same placeholders",
614 ));
615 }
616 if let Some(problem) = data_text_problem(&entry.key, &entry.text, data_problem) {
617 errors.push(at(&problem));
618 }
619 by_key.insert(entry.key.clone(), entry);
620 }
621 by_key
622}
623
624pub fn pack_from_sources(
628 info: Source<'_>,
629 ui: Source<'_>,
630 english: &BTreeMap<String, String>,
631 rules: &PackRules<'_>,
632) -> Result<LangPack, Vec<ContentError>> {
633 let sources = PackSources {
634 info,
635 ui,
636 ..PackSources::default()
637 };
638 pack_from_files(&sources, english, None, rules)
639}
640
641pub fn pack_from_files(
654 sources: &PackSources<'_>,
655 english: &BTreeMap<String, String>,
656 data: Option<&SourceText>,
657 rules: &PackRules<'_>,
658) -> Result<LangPack, Vec<ContentError>> {
659 pack_from_files_in(&LangCode::default(), sources, english, data, rules)
660}
661
662fn pack_from_files_in(
664 source: &LangCode,
665 sources: &PackSources<'_>,
666 english: &BTreeMap<String, String>,
667 data: Option<&SourceText>,
668 rules: &PackRules<'_>,
669) -> Result<LangPack, Vec<ContentError>> {
670 let mut errors = Vec::new();
671 let (info_file, info_source) = sources.info;
672 let info = parse_ron::<LangInfo>(info_file, info_source)
673 .map_err(|e| errors.push(e))
674 .ok();
675 if info.as_ref().is_some_and(|i| i.name.trim().is_empty()) {
676 errors.push(ContentError::new(info_file, "the name must not be empty"));
677 }
678 if info
679 .as_ref()
680 .and_then(|i| i.wide_font.as_deref())
681 .is_some_and(|set| set.trim().is_empty())
682 {
683 errors.push(ContentError::new(
684 info_file,
685 "wide_font must not be empty: leave it out for no wide set of its own",
686 ));
687 }
688 let widths = info
690 .as_ref()
691 .map_or_else(|| rules.widths.clone(), |i| i.widths(rules.widths));
692 let rules = &PackRules {
693 widths: &widths,
694 ..*rules
695 };
696 let problem = rules.data_problem;
697 let no_key = format!("no such key in the source language ({source})");
698 let ui = entries(sources.ui, english, &no_key, problem, &mut errors);
699 let mut pack_data = BTreeMap::new();
700 let mut pack_lines = BTreeMap::new();
701 if let Some(data) = data {
702 if let Some(file) = sources.data {
703 let no_key = "no such key in the data";
704 pack_data = entries(file, &data.data, no_key, problem, &mut errors);
705 }
706 let name = |(id, english): (&String, &String)| {
708 let entry = pack_data.get(&names_key(id));
709 let name = entry.filter(|e| e.source == *english);
710 (id.clone(), name.map_or(english, |e| &e.text).clone())
711 };
712 let names = Names {
713 names: data.names.names.iter().map(name).collect(),
714 };
715 for &file in &sources.dialogue {
716 lines::add_lines(file, data, &names, rules, &mut pack_lines, &mut errors);
717 }
718 }
719 match info {
720 Some(info) if errors.is_empty() => Ok(LangPack {
721 info,
722 ui,
723 data: pack_data,
724 lines: pack_lines,
725 }),
726 _ => Err(errors),
727 }
728}
729
730#[derive(Debug, Clone, Copy)]
732pub struct LangFile<'a> {
733 pub path: &'a str,
735 pub source: Option<&'a str>,
737}
738
739fn is_dialogue_file(name: &str) -> bool {
742 name.strip_prefix(DIALOGUE_DIR)
743 .and_then(|file| file.strip_suffix(DIALOGUE_EXTENSION))
744 .is_some_and(|stem| !stem.is_empty() && !stem.contains('/'))
745}
746
747fn pack_code(dir: &str) -> Result<LangCode, String> {
751 match LangCode::parse(dir) {
752 Ok(code) if code.as_str() == dir => Ok(code),
753 Ok(code) => Err(format!(
754 "name a language's folder by its tag as BCP 47 writes it: {code}"
755 )),
756 Err(e) => Err(format!(
757 "name a language's folder by a BCP 47 tag (en, pt-BR, zh-Hans): {e}"
758 )),
759 }
760}
761
762pub fn from_files(
764 files: &[LangFile<'_>],
765 data: Option<SourceText>,
766 display: impl Fn(&str) -> String,
767 rules: &PackRules<'_>,
768) -> Result<Lang, Vec<ContentError>> {
769 from_files_in(&SourceLang::default(), files, data, display, rules)
770}
771
772pub fn from_files_in(
783 source: &SourceLang,
784 files: &[LangFile<'_>],
785 data: Option<SourceText>,
786 display: impl Fn(&str) -> String,
787 rules: &PackRules<'_>,
788) -> Result<Lang, Vec<ContentError>> {
789 let mut errors = Vec::new();
790 let find = |path: &str| files.iter().find(|f| f.path == path);
791 let read = |path: &str, errors: &mut Vec<ContentError>| {
792 let source = find(path).and_then(|f| f.source);
793 if source.is_none() {
794 errors.push(ContentError::not_found(display(path)));
795 }
796 source
797 };
798 let mut dirs = BTreeSet::new();
799 for file in files {
800 if is_settings_file(file.path) {
801 continue;
802 }
803 let known = match file.path.split_once('/') {
804 Some((dir, name)) if dir == source.code.as_str() => name == UI_FILE,
805 Some((dir, name)) => {
806 dirs.insert(dir);
807 [UI_FILE, INFO_FILE, DATA_FILE].contains(&name) || is_dialogue_file(name)
808 }
809 None => false,
810 };
811 if !known {
812 errors.push(ContentError::new(
813 display(file.path),
814 "not a file a language has",
815 ));
816 }
817 }
818 let english_path = format!("{}/{UI_FILE}", source.code);
819 let english = read(&english_path, &mut errors)
820 .map(|source| english_and_places_from_source(&display(&english_path), source));
821 let (english, places) = match english {
822 Some(Ok((english, places))) => (Some(english), places),
823 Some(Err(e)) => {
824 errors.extend(e);
825 (None, BTreeMap::new())
826 }
827 None => (None, BTreeMap::new()),
828 };
829 let mut packs = BTreeMap::new();
830 for dir in dirs {
831 let code = match pack_code(dir) {
832 Ok(code) => code,
833 Err(message) => {
834 errors.push(ContentError::new(display(dir), message));
835 continue;
836 }
837 };
838 let info_path = format!("{dir}/{INFO_FILE}");
839 let ui_path = format!("{dir}/{UI_FILE}");
840 let data_path = format!("{dir}/{DATA_FILE}");
841 let info = read(&info_path, &mut errors);
842 let ui = read(&ui_path, &mut errors);
843 let data_source = find(&data_path).and_then(|_| read(&data_path, &mut errors));
846 let dialogue_dir = format!("{dir}/{DIALOGUE_DIR}");
847 let dialogue: Vec<(String, &str)> = files
848 .iter()
849 .filter(|f| f.path.starts_with(&dialogue_dir))
850 .filter_map(|f| Some((display(f.path), read(f.path, &mut errors)?)))
851 .collect();
852 let (Some(info), Some(ui), Some(english)) = (info, ui, &english) else {
853 continue;
854 };
855 let (info_name, ui_name, data_name) =
856 (display(&info_path), display(&ui_path), display(&data_path));
857 let sources = PackSources {
858 info: (&info_name, info),
859 ui: (&ui_name, ui),
860 data: data_source.map(|source| (data_name.as_str(), source)),
861 dialogue: dialogue.iter().map(|(n, s)| (n.as_str(), *s)).collect(),
862 };
863 match pack_from_files_in(&source.code, &sources, english, data.as_ref(), rules) {
864 Ok(pack) => {
865 packs.insert(code, pack);
866 }
867 Err(e) => errors.extend(e),
868 }
869 }
870 match english {
871 Some(english) if errors.is_empty() => {
872 Ok(Lang::with_source(english, data.unwrap_or_default(), packs)
873 .with_places(places)
874 .with_source_lang(source.clone()))
875 }
876 _ => Err(errors),
877 }
878}
879
880#[cfg(test)]
881mod chain_tests;
882#[cfg(test)]
883mod pack_tests;
884
885#[cfg(test)]
886mod tests {
887 use super::*;
888
889 #[test]
890 fn english_is_read_through_the_registry() {
891 use crate::bundle::{Bundle, Layer};
892 use crate::format::Formats;
893 use crate::ron::Ron;
894
895 static FILES: &[(&str, &[u8])] = &[
896 ("bad.ron", b"{ \"a\": 1 }"),
897 ("en.ron", b"{ \"title.name\": \"Demo\" }"),
898 ];
899 let bundle = Bundle::new(vec![Layer::table("files", FILES)]);
900 let mut formats = Formats::standard();
901 formats.register(Ron::<English>::new()).unwrap();
902 let english = formats.load::<English>(&bundle, "en.ron").unwrap();
903 assert_eq!(
904 english.0.get("title.name").map(String::as_str),
905 Some("Demo")
906 );
907 let errors = formats.load::<English>(&bundle, "bad.ron").unwrap_err();
908 assert!(
909 errors[0].to_string().starts_with("files/bad.ron:1:"),
910 "{errors:?}"
911 );
912 }
913
914 const ENGLISH_SOURCE: &str = r#"{
915 "a.one": "One",
916 "a.two": "Two {count}",
917 "a.three": "Three",
918 }"#;
919
920 const INFO: &str = r#"(name: "Test", made_by: Machine)"#;
921
922 const RULES: PackRules<'static> = PackRules {
924 data_problem: |_, _| None,
925 tokens: &crate::dialogue::NoTokens,
926 limits: crate::dialogue::tests::LIMITS,
927 widths: &CellWidths::NARROW,
928 variants: &[],
929 };
930
931 fn english() -> BTreeMap<String, String> {
932 english_from_source("en/ui.ron", ENGLISH_SOURCE).unwrap()
933 }
934
935 fn pack(ui: &str) -> Result<LangPack, Vec<String>> {
936 pack_from_sources(("xx/lang.ron", INFO), ("xx/ui.ron", ui), &english(), &RULES)
937 .map_err(|errors| errors.iter().map(ToString::to_string).collect())
938 }
939
940 fn code(code: &str) -> LangCode {
941 LangCode::new(code).unwrap()
942 }
943
944 fn lang() -> Lang {
947 let ui = r#"[
948 (key: "a.one", source: "One", text: "Un"),
949 (key: "a.two", source: "Two of {count}", text: "{count} deux"),
950 ]"#;
951 let packs = BTreeMap::from([(code("xx"), pack(ui).unwrap())]);
952 Lang::new(english(), packs)
953 }
954
955 #[test]
956 fn each_language_has_its_own_widths_the_source_too() {
957 let base = CellWidths::wide(['直']);
958 let lang = lang();
959 assert_eq!(SourceLang::default().widths(&base), base);
961 assert_eq!(lang.widths(&LangCode::default(), &base), base);
962 let source = SourceLang::new(code("zh-Hans"), "中文")
963 .with_wide_font("zh-Hans")
964 .with_wide_punctuation(true);
965 assert_eq!(source.wide_font.as_deref(), Some("zh-Hans"));
966 assert!(source.wide_punctuation);
967 let lang = lang.with_source_lang(source);
968 let chinese = lang.widths(&code("zh-Hans"), &base);
969 assert_eq!(chinese, base.clone().in_lang(Some("zh-Hans"), true));
970 assert!(chinese.is_wide('“') && chinese.is_wide('直'));
971 assert_eq!(lang.widths(&code("xx"), &base), base);
973 assert_eq!(lang.widths(&code("en"), &base), base);
974 }
975
976 #[test]
977 #[expect(deprecated)]
978 fn english_is_the_default_sources_tag() {
979 assert_eq!(ENGLISH, LangCode::default().as_str());
980 assert_eq!(ENGLISH, SourceLang::default().code.as_str());
981 }
982
983 #[test]
984 #[expect(deprecated)]
985 fn english_of_a_key_is_its_source_text() {
986 let lang = lang();
987 for key in ["a.one", "a.nope"] {
988 assert_eq!(lang.english(key), lang.source_text(key), "{key}");
989 }
990 }
991
992 #[test]
993 fn english_loads_as_a_map() {
994 let english = english();
995 assert_eq!(english.len(), 3);
996 assert_eq!(english["a.two"], "Two {count}");
997 }
998
999 #[test]
1000 fn bad_english_keys_are_reported_with_file_and_key() {
1001 let source = r#"{"A.b": "x", "": "y", "a b": "z", "ok_1.x": "fine", "ok_1.x": "again"}"#;
1002 let errors = english_from_source("en/ui.ron", source).unwrap_err();
1003 let errors: Vec<String> = errors.iter().map(ToString::to_string).collect();
1004 let must = "a key must be lowercase letters, digits, `_` and `.`";
1005 assert_eq!(
1006 errors,
1007 [
1008 format!("en/ui.ron: \"A.b\": {must}"),
1009 format!("en/ui.ron: \"\": {must}"),
1010 format!("en/ui.ron: \"a b\": {must}"),
1011 "en/ui.ron: \"ok_1.x\": the key is used twice".to_owned(),
1012 ]
1013 );
1014 let codes: Vec<_> = english_from_source("en/ui.ron", source)
1015 .unwrap_err()
1016 .iter()
1017 .map(|e| e.code)
1018 .collect();
1019 assert_eq!(codes, [None, None, None, Some(codes::DUPLICATE)]);
1020 }
1021
1022 #[test]
1023 fn english_that_is_not_a_map_is_a_syntax_error() {
1024 let errors = english_from_source("en/ui.ron", "[1, 2]").unwrap_err();
1025 assert_eq!(errors.len(), 1);
1026 assert_eq!(errors[0].line, Some(1));
1027 let errors = english_from_source("en/ui.ron", "{\"a\": \"b\", \"c\": 3}").unwrap_err();
1028 assert_eq!(errors.len(), 1);
1029 }
1030
1031 #[test]
1033 fn english_that_is_not_a_map_says_what_was_expected() {
1034 use serde::de::IntoDeserializer;
1035 use serde::de::value::{BoolDeserializer, Error};
1036 let wrong: BoolDeserializer<Error> = true.into_deserializer();
1037 let error = Pairs::<UiValue>::deserialize(wrong).err().unwrap();
1038 assert_eq!(
1039 error.to_string(),
1040 "invalid type: boolean `true`, expected a map of key to text"
1041 );
1042 }
1043
1044 #[test]
1045 fn a_pack_loads_its_info_and_entries() {
1046 let p = pack(r#"[(key: "a.one", source: "One", text: "Un")]"#).unwrap();
1047 assert_eq!(p.info.name, "Test");
1048 assert_eq!(p.info.made_by, MadeBy::Machine);
1049 assert_eq!(p.entry("a.one").unwrap().text, "Un");
1050 assert_eq!(p.entry("a.two"), None);
1051 let human = r#"(name: "Test", made_by: Human("A. Translator"))"#;
1052 let p = pack_from_sources(("l", human), ("u", "[]"), &english(), &RULES).unwrap();
1053 assert_eq!(p.info.made_by, MadeBy::Human("A. Translator".to_owned()));
1054 }
1055
1056 #[test]
1057 fn an_unknown_key_fails_with_file_and_key() {
1058 let errors = pack(r#"[(key: "a.nope", source: "x", text: "y")]"#).unwrap_err();
1059 assert_eq!(
1060 errors,
1061 ["xx/ui.ron: \"a.nope\": no such key in the source language (en)"]
1062 );
1063 }
1064
1065 #[test]
1066 fn different_placeholders_fail_with_file_and_key() {
1067 let message = "the text and its source must have the same placeholders";
1068 for text in ["deux", "{n} deux", "{count} {n}"] {
1069 let ui = format!(r#"[(key: "a.two", source: "Two {{count}}", text: "{text}")]"#);
1070 assert_eq!(
1071 pack(&ui).unwrap_err(),
1072 [format!("xx/ui.ron: \"a.two\": {message}")],
1073 "{text}"
1074 );
1075 }
1076 let ui = r#"[(key: "a.two", source: "{a} {b}", text: "{b} {a} {b}")]"#;
1078 assert!(pack(ui).is_ok());
1079 }
1080
1081 #[test]
1082 fn placeholders_are_checked_against_the_source_not_todays_english() {
1083 let ui = r#"[(key: "a.two", source: "Two", text: "Deux")]"#;
1085 assert!(pack(ui).is_ok());
1086 }
1087
1088 #[test]
1089 fn a_key_used_twice_in_a_pack_fails() {
1090 let ui = r#"[
1091 (key: "a.one", source: "One", text: "Un"),
1092 (key: "a.one", source: "One", text: "Une"),
1093 ]"#;
1094 assert_eq!(
1095 pack(ui).unwrap_err(),
1096 ["xx/ui.ron: \"a.one\": the key is used twice"]
1097 );
1098 let errors =
1099 pack_from_sources(("xx/lang.ron", INFO), ("xx/ui.ron", ui), &english(), &RULES)
1100 .unwrap_err();
1101 assert_eq!(errors[0].code, Some(codes::DUPLICATE));
1102 }
1103
1104 #[test]
1105 fn every_problem_in_a_pack_is_reported() {
1106 let errors = pack_from_sources(
1107 ("xx/lang.ron", r#"(name: " ", made_by: Machine)"#),
1108 (
1109 "xx/ui.ron",
1110 r#"[(key: "a.nope", source: "{a}", text: "b")]"#,
1111 ),
1112 &english(),
1113 &RULES,
1114 )
1115 .unwrap_err();
1116 let errors: Vec<String> = errors.iter().map(ToString::to_string).collect();
1117 assert_eq!(errors.len(), 3, "{errors:?}");
1118 assert_eq!(errors[0], "xx/lang.ron: the name must not be empty");
1119 let errors =
1121 pack_from_sources(("l.ron", "("), ("u.ron", "["), &english(), &RULES).unwrap_err();
1122 let files: Vec<&str> = errors.iter().map(|e| e.file.as_str()).collect();
1123 assert_eq!(files, ["l.ron", "u.ron"]);
1124 assert!(pack_from_sources(("l.ron", "("), ("u.ron", "[]"), &english(), &RULES).is_err());
1126 }
1127
1128 #[test]
1129 fn lookup_uses_the_pack_and_falls_back_to_english() {
1130 let lang = lang();
1131 let xx = code("xx");
1132 assert_eq!(lang.text(&xx, "a.one"), "Un");
1133 assert_eq!(lang.text(&xx, "a.two"), "Two {count}");
1135 assert_eq!(lang.text(&xx, "a.three"), "Three");
1136 assert_eq!(lang.text(&LangCode::default(), "a.one"), "One");
1138 assert_eq!(lang.text(&code("zz"), "a.one"), "One");
1139 assert_eq!(lang.text(&xx, "a.nope"), "a.nope");
1141 assert!(lang.has("a.one"));
1142 assert!(!lang.has("a.nope"));
1143 assert_eq!(lang.source_text("a.one"), Some("One"));
1144 assert_eq!(lang.source_text("a.nope"), None);
1145 }
1146
1147 #[test]
1148 fn status_lists_missing_and_stale_keys() {
1149 let lang = lang();
1150 let status = lang.status(&code("xx")).unwrap();
1151 assert_eq!(status.missing, ["a.three"]);
1152 assert_eq!(status.stale, ["a.two"]);
1153 assert_eq!(
1154 lang.status(&LangCode::default()),
1155 Some(LangStatus::default())
1156 );
1157 assert_eq!(lang.status(&code("zz")), None);
1158 assert_eq!(lang.codes().collect::<Vec<_>>(), [&code("xx")]);
1159 assert!(lang.pack(&code("xx")).is_some());
1160 assert!(lang.pack(&LangCode::default()).is_none());
1161 }
1162
1163 fn files(files: &[(&str, &str)]) -> Result<Lang, Vec<String>> {
1165 let files: Vec<LangFile<'_>> = files
1166 .iter()
1167 .map(|&(path, source)| LangFile {
1168 path,
1169 source: Some(source),
1170 })
1171 .collect();
1172 from_files(&files, None, |path| format!("lang/{path}"), &RULES)
1173 .map_err(|errors| errors.iter().map(ToString::to_string).collect())
1174 }
1175
1176 const UI: &str = r#"[(key: "a.one", source: "One", text: "Un")]"#;
1177
1178 #[test]
1179 fn files_make_english_and_a_pack_per_directory() {
1180 let lang = files(&[
1181 ("en/ui.ron", ENGLISH_SOURCE),
1182 ("xx/lang.ron", INFO),
1183 ("xx/ui.ron", UI),
1184 ("yy/ui.ron", "[]"),
1185 ("yy/lang.ron", INFO),
1186 ])
1187 .unwrap();
1188 assert_eq!(lang.codes().collect::<Vec<_>>(), [&code("xx"), &code("yy")]);
1189 assert_eq!(lang.text(&code("xx"), "a.one"), "Un");
1190 assert_eq!(lang.text(&code("yy"), "a.one"), "One");
1191 let lang = files(&[("en/ui.ron", ENGLISH_SOURCE)]).unwrap();
1193 assert_eq!(lang.codes().count(), 0);
1194 assert!(lang.has("a.one"));
1195 }
1196
1197 #[test]
1198 fn a_missing_file_is_reported() {
1199 assert_eq!(
1200 files(&[]).unwrap_err(),
1201 ["lang/en/ui.ron: file not found in asset bundle"]
1202 );
1203 assert_eq!(
1204 files(&[("en/ui.ron", ENGLISH_SOURCE), ("xx/ui.ron", UI)]).unwrap_err(),
1205 ["lang/xx/lang.ron: file not found in asset bundle"]
1206 );
1207 assert_eq!(
1208 files(&[("en/ui.ron", ENGLISH_SOURCE), ("xx/lang.ron", INFO)]).unwrap_err(),
1209 ["lang/xx/ui.ron: file not found in asset bundle"]
1210 );
1211 let binary = [LangFile {
1213 path: "en/ui.ron",
1214 source: None,
1215 }];
1216 let errors = from_files(&binary, None, str::to_owned, &RULES).unwrap_err();
1217 assert_eq!(errors.len(), 1);
1218 assert_eq!(errors[0].file, "en/ui.ron");
1219 }
1220
1221 #[test]
1222 fn a_settings_file_among_the_languages_is_no_error() {
1223 let read = files(&[
1224 ("_folder.meta", "()"),
1225 ("en/ui.ron", ENGLISH_SOURCE),
1226 ("en/ui.ron.meta", "()"),
1227 ("xx/_folder.meta", "()"),
1228 ("xx/lang.ron", INFO),
1229 ("xx/ui.ron", UI),
1230 ]);
1231 assert!(read.is_ok(), "{read:?}");
1232 }
1233
1234 #[test]
1235 fn a_file_no_language_has_is_reported() {
1236 let stray = "not a file a language has";
1237 assert_eq!(
1238 files(&[
1239 ("en/ui.ron", ENGLISH_SOURCE),
1240 ("en/lang.ron", INFO),
1241 ("notes.txt", ""),
1242 ("xx/lang.ron", INFO),
1243 ("xx/ui.ron", UI),
1244 ("xx/data.ron", "[]"),
1246 ("xx/dialogue/ch01.ron", "[]"),
1247 ("en/data.ron", "[]"),
1249 ("xx/dialogue/ch01.dlg", ""),
1250 ("xx/dialogue/.ron", "[]"),
1251 ("xx/dialogue/a/b.ron", "[]"),
1252 ("xx/notes/ch01.ron", "[]"),
1253 ])
1254 .unwrap_err(),
1255 [
1256 format!("lang/en/lang.ron: {stray}"),
1257 format!("lang/notes.txt: {stray}"),
1258 format!("lang/en/data.ron: {stray}"),
1259 format!("lang/xx/dialogue/ch01.dlg: {stray}"),
1260 format!("lang/xx/dialogue/.ron: {stray}"),
1261 format!("lang/xx/dialogue/a/b.ron: {stray}"),
1262 format!("lang/xx/notes/ch01.ron: {stray}"),
1263 ]
1264 );
1265 }
1266
1267 #[test]
1268 fn a_badly_named_directory_is_reported() {
1269 assert_eq!(
1270 files(&[
1271 ("en/ui.ron", ENGLISH_SOURCE),
1272 ("JA/lang.ron", INFO),
1273 ("JA/ui.ron", UI),
1274 ])
1275 .unwrap_err(),
1276 ["lang/JA: name a language's folder by its tag as BCP 47 writes it: ja"]
1277 );
1278 }
1279
1280 #[test]
1281 fn pack_errors_are_reported_for_every_pack() {
1282 let bad = r#"[(key: "a.nope", source: "x", text: "y")]"#;
1283 assert_eq!(
1284 files(&[
1285 ("en/ui.ron", ENGLISH_SOURCE),
1286 ("xx/lang.ron", INFO),
1287 ("xx/ui.ron", bad),
1288 ("yy/lang.ron", INFO),
1289 ("yy/ui.ron", bad),
1290 ])
1291 .unwrap_err(),
1292 [
1293 "lang/xx/ui.ron: \"a.nope\": no such key in the source language (en)",
1294 "lang/yy/ui.ron: \"a.nope\": no such key in the source language (en)",
1295 ]
1296 );
1297 }
1298
1299 #[test]
1300 fn with_english_broken_the_packs_are_not_checked() {
1301 let errors = files(&[
1302 ("en/ui.ron", r#"{"Bad Key": "x"}"#),
1303 ("xx/lang.ron", INFO),
1304 ("xx/ui.ron", UI),
1305 ])
1306 .unwrap_err();
1307 assert_eq!(errors.len(), 1, "{errors:?}");
1308 assert!(errors[0].starts_with("lang/en/ui.ron: \"Bad Key\""));
1309 }
1310
1311 #[test]
1312 fn width_counts_characters_and_a_wide_one_twice() {
1313 let narrow = &CellWidths::NARROW;
1314 assert_eq!(text_width("", narrow), 0);
1315 assert_eq!(text_width("abc", narrow), 3);
1316 assert_eq!(text_width("né!", narrow), 3);
1317 assert_eq!(text_width("あA", narrow), 2);
1318 let wide = &CellWidths::wide(['あ', 'い']);
1319 assert_eq!(text_width("あA", wide), 3);
1320 assert_eq!(text_width("あいう", wide), 5);
1321 }
1322}