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