yew_template/
i18n.rs

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                    // Ignore the text if it's only a variable
22                    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    // Delete template.pot if it hasn't been modified too recently (otherwise keep the data)
53    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    // Append new translatables
69    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    // Make sure the file is in .gitignore
81    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        // Read all PO files in the locale_directory
96        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}