sysd_manager_translating/
lib.rs

1use constcat::concat;
2use log::error;
3use log::info;
4use std::{
5    collections::HashSet,
6    fs::{self, read_to_string},
7    io,
8    path::PathBuf,
9    process::Command,
10};
11
12use crate::error::TransError;
13pub mod error;
14
15pub const MAIN_PROG: &str = "sysd-manager";
16pub const PO_DIR: &str = "./po";
17pub const DESKTOP_DIR: &str = "./data/applications";
18pub const DESKTOP_FILE: &str = "io.github.plrigaux.sysd-manager.desktop";
19pub const DESKTOP_FILE_PATH: &str = concat!(DESKTOP_DIR, "/", DESKTOP_FILE);
20
21pub const METAINFO_DIR: &str = "./data/metainfo";
22pub const METAINFO_FILE: &str = "io.github.plrigaux.sysd-manager.metainfo.xml";
23pub const METAINFO_FILE_PATH: &str = concat!(METAINFO_DIR, "/", METAINFO_FILE);
24
25pub const PACK_FILE_DIR: &str = "target/loc";
26
27/// Making the PO Template File
28/// https://www.gnu.org/software/gettext/manual/html_node/xgettext-Invocation.html
29pub fn xgettext(potfiles_file_path: &str, output_pot_file: &str) {
30    let mut command = Command::new("xgettext");
31
32    for preset in glib_preset() {
33        command.arg(preset);
34    }
35
36    let output = command
37        .arg(format!("--files-from={potfiles_file_path}"))
38        .arg(format!("--output={output_pot_file}"))
39        .arg("--verbose")
40        .output()
41        .unwrap();
42
43    display_output("XGETTEXT", output);
44}
45
46fn display_output(id: &str, output: std::process::Output) {
47    info!("{id}: {:?}", output.status);
48    info!("{id}: {}", String::from_utf8_lossy(&output.stdout));
49    if !output.status.success() {
50        error!("{id}: {}", String::from_utf8_lossy(&output.stderr));
51    }
52}
53
54/// Creating a New PO File
55/// https://www.gnu.org/software/gettext/manual/html_node/msginit-Invocation.html
56pub fn msginit(input_pot_file: &str, output_file: &str, lang: &str) {
57    let mut command = Command::new("msginit");
58
59    let output = command
60        .arg(format!("--input={input_pot_file}"))
61        .arg(format!("--output-file={output_file}"))
62        .arg(format!("--locale={lang}"))
63        .output()
64        .expect("command msginit ok");
65
66    display_output("MSGINIT", output);
67}
68
69//   /usr/bin/msgmerge --update --quiet  --lang=pt_BR --previous pt_BR.po hello-rust.pot
70// rm -f pt_BR.gmo && /usr/bin/msgmerge --for-msgfmt -o pt_BR.1po pt_BR.po hello-rust.pot && /usr/bin/msgfmt -c --statistics --verbose -o pt_BR.gmo pt_BR.1po && rm -f pt_BR.1po
71// pt_BR.1po: 2 translated messages.
72
73/// https://www.gnu.org/software/gettext/manual/html_node/msgmerge-Invocation.html
74pub fn msgmerge(input_pot_file: &str, output_file: &str) {
75    let mut command = Command::new("msgmerge");
76
77    let output = command
78        .arg("-o")
79        .arg(output_file)
80        .arg(output_file)
81        .arg(input_pot_file)
82        .arg("--verbose")
83        .output()
84        .unwrap();
85
86    display_output("MSGMERGE", output);
87}
88
89pub fn generate_mo() -> Result<(), TransError> {
90    for path in fs::read_dir(PO_DIR)?
91        .filter_map(|r| r.ok())
92        .filter_map(|dir_entry| {
93            let p = dir_entry.path();
94            if p.extension().is_some_and(|this_ext| this_ext == "po") {
95                Some(p)
96            } else {
97                None
98            }
99        })
100    {
101        info!("PO file {:?} lang {:?}", path, path.file_stem());
102
103        if let Some(po_file) = path.to_str() {
104            if let Some(lang) = path.file_stem().and_then(|s| s.to_str()) {
105                msgfmt(po_file, lang, MAIN_PROG)?;
106            }
107        }
108    }
109
110    Ok(())
111}
112
113pub fn lingas_from_files() -> Result<HashSet<String>, TransError> {
114    let linguas: HashSet<_> = fs::read_dir(PO_DIR)?
115        .filter_map(|r| r.ok())
116        .filter_map(|dir_entry| {
117            let p = dir_entry.path();
118            if p.extension().is_some_and(|this_ext| this_ext == "po") {
119                Some(p)
120            } else {
121                None
122            }
123        })
124        .filter_map(|path| {
125            path.file_stem()
126                .and_then(|s| s.to_str())
127                .map(|s| s.to_string())
128        })
129        .collect();
130
131    Ok(linguas)
132}
133
134pub fn lingas_from_lingua_file() -> Result<HashSet<String>, TransError> {
135    let mut linguas_file = PathBuf::from(PO_DIR);
136    linguas_file.push("LINGUAS");
137
138    let mut linguas = HashSet::new();
139
140    for line in read_to_string(linguas_file)?
141        .lines()
142        .map(|s| s.trim())
143        .filter(|s| !s.starts_with("#"))
144    {
145        for lang in line.split_whitespace() {
146            linguas.insert(lang.to_owned());
147        }
148    }
149
150    Ok(linguas)
151}
152
153pub fn generate_desktop() -> Result<(), TransError> {
154    fs::create_dir_all(PACK_FILE_DIR)?;
155    let out_file = format!("{}/{}", PACK_FILE_DIR, DESKTOP_FILE);
156
157    let mut command = Command::new("msgfmt");
158    let output = command
159        .arg("--check")
160        .arg("--statistics")
161        .arg("--verbose")
162        .arg("--desktop")
163        .arg(format!("--template={}", DESKTOP_FILE_PATH))
164        .arg("-d")
165        .arg(PO_DIR)
166        .arg("-o")
167        .arg(out_file)
168        .output()?;
169
170    display_output("MSGFMT", output);
171
172    Ok(())
173}
174
175pub fn generate_metainfo() -> Result<(), TransError> {
176    fs::create_dir_all(PACK_FILE_DIR)?;
177
178    let out_file = format!("{}/{}", PACK_FILE_DIR, METAINFO_FILE);
179
180    let mut command = Command::new("msgfmt");
181    let output = command
182        .arg("--check")
183        .arg("--statistics")
184        .arg("--verbose")
185        .arg("--xml")
186        .arg(format!("--template={}", METAINFO_FILE_PATH))
187        .arg("-d")
188        .arg(PO_DIR)
189        .arg("-o")
190        .arg(out_file)
191        .output()?;
192
193    display_output("MSGFMT", output);
194
195    Ok(())
196}
197
198// /usr/bin/msgfmt -c --statistics --verbose -o pt_BR.gmo pt_BR.1po && rm -f pt_BR.1po
199/// Generates a binary message catalog from a textual translation description.
200/// https://www.gnu.org/software/gettext/manual/html_node/msgfmt-Invocation.html
201pub fn msgfmt(po_file: &str, lang: &str, domain_name: &str) -> io::Result<()> {
202    let mut command = Command::new("msgfmt");
203
204    let out_dir = format!("target/locale/{lang}/LC_MESSAGES");
205
206    fs::create_dir_all(&out_dir)?;
207
208    let output = command
209        .arg("--check")
210        .arg("--statistics")
211        .arg("--verbose")
212        .arg("-o")
213        .arg(format!("{out_dir}/{domain_name}.mo"))
214        .arg(po_file)
215        .output()
216        .unwrap();
217
218    display_output("MSGFMT", output);
219
220    Ok(())
221}
222
223fn glib_preset() -> Vec<&'static str> {
224    let v = vec![
225        "--from-code=UTF-8",
226        "--add-comments",
227        // # https://developer.gnome.org/glib/stable/glib-I18N.html
228        "--keyword=_",
229        "--keyword=N_",
230        "--keyword=C_:1c,2",
231        "--keyword=NC_:1c,2",
232        "--keyword=g_dcgettext:2",
233        "--keyword=g_dngettext:2,3",
234        "--keyword=g_dpgettext2:2c,3",
235        "--flag=N_:1:pass-c-format",
236        "--flag=C_:2:pass-c-format",
237        "--flag=NC_:2:pass-c-format",
238        "--flag=g_dngettext:2:pass-c-format",
239        "--flag=g_strdup_printf:1:c-format",
240        "--flag=g_string_printf:2:c-format",
241        "--flag=g_string_append_printf:2:c-format",
242        "--flag=g_error_new:3:c-format",
243        "--flag=g_set_error:4:c-format",
244        "--flag=g_markup_printf_escaped:1:c-format",
245        "--flag=g_log:3:c-format",
246        "--flag=g_print:1:c-format",
247        "--flag=g_printerr:1:c-format",
248        "--flag=g_printf:1:c-format",
249        "--flag=g_fprintf:2:c-format",
250        "--flag=g_sprintf:2:c-format",
251        "--flag=g_snprintf:3:c-format",
252    ];
253    v
254}