1use std::{collections::HashMap, path::Path, io::Read};
2use poreader::{PoParser, Message};
3use crate::*;
4
5pub struct Translatable {
6 original: String,
7 origin: (String, usize),
8 context: String,
9}
10
11fn context_from_path(path: &str) -> &str {
12 path.split('/').last().unwrap_or_default().trim_end_matches(".html")
13}
14
15impl Element {
16 pub(crate) fn get_translatables(&self, args: &Args) -> Vec<Translatable> {
17 let mut translatables = Vec::new();
18 for child in &self.children {
19 match &child.part {
20 HtmlPart::Text(text) => {
21 let text_parts = TextPart::parse(text, args);
23 if matches!(text_parts.as_slice(), &[TextPart::Expression(_)]) {
24 continue;
25 }
26
27 translatables.push(Translatable {
28 original: text.to_string(),
29 origin: (args.path.trim_start_matches("./").to_owned(), child.line),
30 context: context_from_path(&args.path).to_string(),
31 })
32 },
33 HtmlPart::Element(el) => translatables.append(&mut el.get_translatables(args)),
34 }
35 }
36 translatables
37 }
38}
39
40impl Translatable {
41 fn generate_pot_part(&self) -> String {
42 format!("#: {}:{}\nmsgctxt {:?}\nmsgid {:?}\nmsgstr \"\"", self.origin.0, self.origin.1, self.context, self.original)
43 }
44}
45
46pub(crate) fn generate_pot(root: &Element, args: &Args) {
47 let path = Path::new(&args.config.locale_directory);
48 if !path.exists() {
49 return;
50 }
51
52 let mut data = match std::fs::File::open(format!("{}template.pot", args.config.locale_directory)) {
54 Ok(mut file) => {
55 let metadata = file.metadata().unwrap();
56 let mut data = String::new();
57 file.read_to_string(&mut data).unwrap();
58 if metadata.modified().unwrap().elapsed().unwrap().as_secs() > 120 {
59 std::fs::remove_file(format!("{}template.pot", args.config.locale_directory)).unwrap();
60 String::new()
61 } else {
62 data
63 }
64 }
65 _ => String::new()
66 };
67
68 let translatables = root.get_translatables(args);
70 for translatable in translatables {
71 let pot_part = translatable.generate_pot_part();
72 if !data.contains(&pot_part) {
73 data.push('\n');
74 data.push_str(&pot_part);
75 data.push('\n');
76 }
77 }
78 std::fs::write(format!("{}template.pot", args.config.locale_directory), data).unwrap();
79
80 let gitignore_path = format!("{}.gitignore", args.config.locale_directory);
82 let path = Path::new(&gitignore_path);
83 if !path.exists() {
84 std::fs::write(gitignore_path, "template.pot\n").unwrap();
85 }
86}
87
88#[derive(Debug)]
89pub(crate) struct Catalog {
90 catalogs: HashMap<String, HashMap<(String, String), String>>,
91}
92
93impl Catalog {
94 pub(crate) fn new(locale_directory: &str) -> Self {
95 let mut catalogs = HashMap::new();
97 let read_dir = match std::fs::read_dir(locale_directory) {
98 Ok(read_dir) => read_dir,
99 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Self { catalogs },
100 Err(_) => abort_call_site!("Failed to read locale directory"),
101 };
102 for entry in read_dir {
103 let entry = entry.expect("Error while reading locale directory");
104 let path = entry.path();
105 if path.extension().map(|ext| ext != "po").unwrap_or(true) {
106 continue;
107 }
108
109 let locale = path.file_name().expect("no file stem").to_str().expect("cannot convert file stem").trim_end_matches(".po").to_string();
110 let locale = locale.replace('\\', "\\\\").replace('\"', "\\\"");
111 let file = std::fs::File::open(path).unwrap_or_else(|_| panic!("could not open the {locale} catalog"));
112 let parser = PoParser::new();
113 let reader = parser.parse(file).unwrap_or_else(|_| panic!("could not parse the {locale} catalog"));
114
115 let mut items = HashMap::new();
116 for unit in reader {
117 let Ok(unit) = unit else {
118 eprintln!("WARNING: Invalid unit in the {locale} catalog");
119 continue;
120 };
121 let context = unit.context().unwrap_or("").to_string();
122 if let Message::Simple { id, text: Some(text) } = unit.message() {
123 items.insert((context, id.to_owned()), text.to_owned());
124 }
125 }
126
127 catalogs.insert(locale.to_string(), items);
128 }
129
130 Self {
131 catalogs,
132 }
133 }
134
135 pub(crate) fn translate_text(&self, text: &str, args: &Args) -> Vec<(String, Vec<TextPart>)> {
136 let context = context_from_path(&args.path).to_string();
137 let context_and_text = (context.clone(), text.to_string());
138
139 let mut translations = Vec::new();
140 translations.push((String::new(), TextPart::parse(text, args)));
141 for (language, catalog) in &self.catalogs {
142 let Some(translated_text) = catalog.get(&context_and_text) else {
143 eprintln!("WARNING: Missing translation for text {text:?} with context {context:?} in language {language}");
144 continue;
145 };
146 let translated_parts = TextPart::parse(translated_text, args);
147 translations.push((language.to_owned(), translated_parts));
148 }
149
150 translations
151 }
152}