sysd_manager_translating/
lib.rs

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