summaryrefslogtreecommitdiff
path: root/server/src/trans.rs
blob: 6a18e277099ddb0fed4ef491f1aec5e8bdddee0e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
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<anyhow::Result<grit::TranslationFile>>,
    known: &mut HashSet<String>,
    path: &Path,
    files: &Vec<grit::IfFile>,
) {
    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<api_model::LocalizationString>,
    file: &String,
    messages: Vec<grit::IfMessagePart>,
) {
    for message in messages {
        match message {
            grit::IfMessagePart::Message(message) => {
                let mut source = String::new();
                let mut placeholders = Vec::<api_model::LocalizationPlaceholder>::new();
                let mut placeholder_offset = Vec::<usize>::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::<api_model::TranslationString>::new(),
                });
            }
            grit::IfMessagePart::If { expr: _, message } => {
                push_strings(strings, file, message);
            }
            grit::IfMessagePart::Part { file, messages } => {
                push_strings(strings, &file, messages);
            }
        }
    }
}

fn push_translation(
    string: &mut api_model::LocalizationString,
    language: &String,
    unit: grit::TranslationUnit,
) {
    let mut translation = String::new();
    let mut placeholder_offset = Vec::<usize>::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::<String, usize>::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<Path>,
    grits: impl IntoIterator<Item = String>,
) -> anyhow::Result<Vec<api_model::LocalizationString>> {
    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<grit::Grit>)>::with_capacity(grit_tasks.len());
    while let Some(res) = grit_tasks.join_next().await {
        parsed_grits.push(res?);
    }

    let mut strings = Vec::<api_model::LocalizationString>::new();
    let mut translation_tasks = JoinSet::new();
    let mut known_translations = HashSet::<String>::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::<i64, usize>::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)
}