1use serde::Deserialize;
7use std::collections::HashMap;
8
9#[derive(Debug, Clone, PartialEq, Deserialize)]
11pub struct QuizUiStrings {
12 pub title: String,
13 pub configure: String,
14 pub categories: String,
15 pub select_all: String,
16 pub select_none: String,
17 pub difficulty: String,
18 pub all_levels: String,
19 pub beginner: String,
20 pub intermediate: String,
21 pub expert: String,
22 pub num_questions: String,
23 pub questions_available: String,
24 pub questions_selected: String,
25 pub start_quiz: String,
26 pub question_of: String,
27 pub correct_count: String,
28 pub correct: String,
29 pub wrong: String,
30 pub reference: String,
31 pub next_question: String,
32 pub see_results: String,
33 pub skip: String,
34 pub excellent: String,
35 pub good_job: String,
36 pub keep_learning: String,
37 pub try_again: String,
38 pub score_detail: String,
39 pub by_category: String,
40 pub by_difficulty: String,
41 pub try_again_btn: String,
42 pub back_to_editor: String,
43 pub powered_by: String,
44 pub questions_across: String,
45 pub polar_diagram: String,
46 pub cartesian_diagram: String,
47 pub symmetric: String,
48 pub asymmetric: String,
49 pub projector_narrow: String,
50 #[serde(default)]
51 pub heatmap: Option<String>,
52}
53
54#[derive(Debug, Clone, PartialEq, Deserialize)]
56pub struct QuestionLocale {
57 pub text: String,
58 pub options: Vec<String>,
59 pub explanation: String,
60}
61
62#[derive(Debug, Clone, PartialEq, Deserialize)]
64pub struct QuizLocale {
65 pub ui: QuizUiStrings,
66 pub categories: HashMap<String, String>,
67 pub questions: HashMap<String, QuestionLocale>,
68}
69
70const EN_JSON: &str = include_str!("../locales/en.json");
71const ZH_JSON: &str = include_str!("../locales/zh.json");
72const DE_JSON: &str = include_str!("../locales/de.json");
73const FR_JSON: &str = include_str!("../locales/fr.json");
74const ES_JSON: &str = include_str!("../locales/es.json");
75const IT_JSON: &str = include_str!("../locales/it.json");
76const RU_JSON: &str = include_str!("../locales/ru.json");
77const PT_BR_JSON: &str = include_str!("../locales/pt-BR.json");
78
79impl QuizLocale {
80 pub fn for_code(code: &str) -> Self {
83 let json = match code.to_lowercase().as_str() {
84 "en" => EN_JSON,
85 "zh" | "zh-cn" | "zh-hans" => ZH_JSON,
86 "de" => DE_JSON,
87 "fr" => FR_JSON,
88 "es" => ES_JSON,
89 "it" => IT_JSON,
90 "ru" => RU_JSON,
91 "pt" | "pt-br" => PT_BR_JSON,
92 _ => EN_JSON,
93 };
94 serde_json::from_str(json).expect("invalid quiz locale JSON")
95 }
96
97 pub fn question(&self, id: u32) -> Option<&QuestionLocale> {
99 self.questions.get(&id.to_string())
100 }
101
102 pub fn category_label(&self, cat: &crate::Category) -> &str {
104 self.categories
105 .get(cat.key())
106 .map(|s| s.as_str())
107 .unwrap_or(cat.label())
108 }
109
110 pub fn difficulty_label(&self, diff: &crate::Difficulty) -> &str {
112 match diff {
113 crate::Difficulty::Beginner => &self.ui.beginner,
114 crate::Difficulty::Intermediate => &self.ui.intermediate,
115 crate::Difficulty::Expert => &self.ui.expert,
116 }
117 }
118
119 pub fn format(template: &str, args: &[&dyn std::fmt::Display]) -> String {
121 let mut result = template.to_string();
122 for (i, arg) in args.iter().enumerate() {
123 result = result.replace(&format!("{{{}}}", i), &arg.to_string());
124 }
125 result
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 #[test]
134 fn test_english_locale_loads() {
135 let locale = QuizLocale::for_code("en");
136 assert_eq!(locale.ui.title, "Photometric Knowledge Quiz");
137 assert!(!locale.questions.is_empty());
138 }
139
140 #[test]
141 fn test_all_locales_load() {
142 for code in &["en", "zh", "de", "fr", "es", "it", "ru", "pt-BR"] {
143 let locale = QuizLocale::for_code(code);
144 assert!(
145 !locale.ui.title.is_empty(),
146 "Locale {} has empty title",
147 code
148 );
149 assert!(
150 !locale.categories.is_empty(),
151 "Locale {} has empty categories",
152 code
153 );
154 }
155 }
156
157 #[test]
158 fn test_english_has_all_questions() {
159 let locale = QuizLocale::for_code("en");
160 let all = crate::QuizBank::all_questions();
161 for q in &all {
162 assert!(
163 locale.question(q.id).is_some(),
164 "English locale missing question {}",
165 q.id
166 );
167 }
168 }
169
170 #[test]
171 fn test_format() {
172 let s = QuizLocale::format("{0} of {1} questions", &[&5, &10]);
173 assert_eq!(s, "5 of 10 questions");
174 }
175
176 #[test]
177 fn test_category_label_lookup() {
178 let locale = QuizLocale::for_code("en");
179 let label = locale.category_label(&crate::Category::EulumdatFormat);
180 assert_eq!(label, "EULUMDAT Format");
181 }
182}