use anyhow; use std::collections::{HashMap, HashSet}; use std::iter::{repeat, IntoIterator}; use std::path::{Path, PathBuf}; use tokio::task::JoinSet; use eyeballs_api::api_model; use eyeballs_common::grit; fn schedule_translations( tasks: &mut JoinSet>, known: &mut HashSet, path: &Path, files: &Vec, ) { for file in files { match file { grit::IfFile::File(file) => { if known.insert(file.path.to_string()) { tasks.spawn(grit::parse_xlf(path.join(file.path.as_str()))); } } grit::IfFile::If { expr: _, file } => { schedule_translations(tasks, known, path, file); } } } } fn push_strings( strings: &mut Vec, file: &String, messages: Vec, ) { for message in messages { match message { grit::IfMessagePart::Message(message) => { let mut source = String::new(); let mut placeholders = Vec::::new(); let mut placeholder_offset = Vec::::new(); let translation_id = grit::get_message_id(&message); let mut offset: usize = 0; for text in message.content { match text { grit::TextPlaceholder::Text(text) => { source.push_str(text.as_str()); offset += text.len(); } grit::TextPlaceholder::Placeholder { name, content, example, } => { placeholders.push(api_model::LocalizationPlaceholder { id: name, content, example: example.unwrap_or_default(), }); placeholder_offset.push(offset); } } } strings.push(api_model::LocalizationString { id: message.name, file: file.to_string(), description: message.desc, meaning: message.meaning.unwrap_or_default(), source, placeholders, placeholder_offset, translation_id, translations: Vec::::new(), }); } grit::IfMessagePart::If { expr: _, message } => { push_strings(strings, file, message); } grit::IfMessagePart::Part(_) => { // There should be none of these as we use parse_grit_with_parts assert!(false); } } } } fn push_translation( string: &mut api_model::LocalizationString, language: &String, unit: grit::TranslationUnit, ) { let mut translation = String::new(); let mut placeholder_offset = Vec::::with_capacity(string.placeholders.len()); // Fill offset vec with zeros, it's not guaranteed that they will be in the same order // below so easier to index directly. placeholder_offset.extend(repeat(0).take(string.placeholders.len())); // There can be multiple placeholders with the same name, so when doing name lookup, // skip the previous hits. let mut placeholder_last = HashMap::::new(); let mut offset: usize = 0; for text in unit.target { match text { grit::TextPlaceholder::Text(text) => { translation.push_str(text.as_str()); offset += text.len(); } grit::TextPlaceholder::Placeholder { name, content: _, example: _, } => { let previous = placeholder_last.get(name.as_str()).map_or(0, |x| x + 1); if let Some(index) = string .placeholders .iter() .skip(previous) .position(|x| x.id == name) { placeholder_last.insert(name, previous + index); placeholder_offset[previous + index] = offset; } } } } string.translations.push(api_model::TranslationString { language: language.to_string(), translation, placeholder_offset, }) } pub async fn collect_strings( base: impl AsRef, grits: impl IntoIterator, ) -> anyhow::Result> { let mut grit_tasks = JoinSet::new(); for grit_name in grits { let grit_path = base.as_ref().join(grit_name.as_str()); grit_tasks.spawn(async move { let tmp = grit::parse_grit_with_parts(grit_path.as_path()).await; (grit_path, grit_name, tmp) }); } let mut parsed_grits = Vec::<(PathBuf, String, anyhow::Result)>::with_capacity(grit_tasks.len()); while let Some(res) = grit_tasks.join_next().await { parsed_grits.push(res?); } let mut strings = Vec::::new(); let mut translation_tasks = JoinSet::new(); let mut known_translations = HashSet::::new(); for (grit_path, grit_name, maybe_grit) in parsed_grits { let grit = maybe_grit?; schedule_translations( &mut translation_tasks, &mut known_translations, grit_path.parent().unwrap(), &grit.translations.file, ); let first_index = strings.len(); push_strings(&mut strings, &grit_name, grit.release.messages.messages); let mut id_to_string = HashMap::::with_capacity(strings.len() - first_index); for i in first_index..strings.len() { id_to_string.insert(strings[i].translation_id, i); } while let Some(res) = translation_tasks.join_next().await { let translation_file = res??; for unit in translation_file.units { if let Some(index) = id_to_string.get(&unit.id) { push_translation( &mut strings[*index], &translation_file.target_language, unit, ); } } } } Ok(strings) }