1use std::{
8 cmp::Ordering,
9 fmt::Write as _,
10 fs, io,
11 path::{Path, PathBuf},
12};
13
14use clap::*;
15
16use crate::util;
17
18mod pseudo;
19mod scraper;
20
21#[derive(Args, Debug)]
22pub struct L10nArgs {
23 #[arg(short, long, default_value = "")]
25 input: String,
26
27 #[arg(short, long, default_value = "")]
29 output: String,
30
31 #[arg(short, long, default_value = "")]
35 package: String,
36
37 #[arg(long, default_value = "")]
41 manifest_path: String,
42
43 #[arg(long, action)]
47 no_deps: bool,
48
49 #[arg(long, action)]
53 no_local: bool,
54
55 #[arg(long, action)]
59 no_pkg: bool,
60
61 #[arg(long, action)]
63 clean_deps: bool,
64
65 #[arg(long, action)]
67 clean_template: bool,
68
69 #[arg(long, action)]
71 clean: bool,
72
73 #[arg(short, long, default_value = "")]
75 macros: String,
76
77 #[arg(long, default_value = "")]
83 pseudo: String,
84 #[arg(long, default_value = "")]
86 pseudo_m: String,
87 #[arg(long, default_value = "")]
89 pseudo_w: String,
90
91 #[arg(long, action)]
93 check: bool,
94
95 #[arg(short, long, action)]
97 verbose: bool,
98}
99
100pub fn run(args: L10nArgs) {
101 run_impl(args, false);
102}
103fn run_impl(mut args: L10nArgs, is_local_scrap_recursion: bool) {
104 if !args.package.is_empty() && !args.manifest_path.is_empty() {
105 fatal!("only one of --package --manifest-path must be set")
106 }
107
108 let mut input = String::new();
109 let mut output = args.output.replace('\\', "/");
110
111 if !args.input.is_empty() {
112 input = args.input.replace('\\', "/");
113
114 if !input.contains('*') && PathBuf::from(&input).is_dir() {
115 input = format!("{}/**/*.rs", input.trim_end_matches('/'));
116 }
117 }
118 if !args.package.is_empty() {
119 if let Some(m) = crate::util::manifest_path_from_package(&args.package) {
120 args.manifest_path = m;
121 } else {
122 fatal!("package `{}` not found in workspace", args.package);
123 }
124 }
125
126 if !args.manifest_path.is_empty() {
127 if !Path::new(&args.manifest_path).exists() {
128 fatal!("{input} does not exist")
129 }
130
131 if let Some(path) = args.manifest_path.replace('\\', "/").strip_suffix("/Cargo.toml") {
132 if output.is_empty() {
133 output = format!("{path}/l10n");
134 }
135 if input.is_empty() {
136 input = format!("{path}/src/**/*.rs");
137 }
138 } else {
139 fatal!("expected path to Cargo.toml manifest file");
140 }
141 }
142
143 if args.check {
144 args.clean = false;
145 args.clean_deps = false;
146 args.clean_template = false;
147 } else if args.clean {
148 args.clean_deps = true;
149 args.clean_template = true;
150 }
151
152 if args.verbose && !is_local_scrap_recursion {
153 println!(
154 "input: `{input}`\noutput: `{output}`\nclean_deps: {}\nclean_template: {}",
155 args.clean_deps, args.clean_template
156 );
157 }
158
159 if !input.is_empty() {
160 if output.is_empty() {
161 fatal!("--output is required for --input")
162 }
163
164 if !args.no_pkg {
166 if args.check {
167 println!(r#"checking "{input}".."#);
168 } else {
169 println!(r#"scraping "{input}".."#);
170 }
171
172 let custom_macro_names: Vec<&str> = args.macros.split(',').map(|n| n.trim()).collect();
173 let mut template = scraper::scrape_fluent_text(&input, &custom_macro_names);
176 if !args.check {
177 match template.entries.len() {
178 0 => println!("did not find any entry"),
179 1 => println!("found 1 entry"),
180 n => println!("found {n} entries"),
181 }
182 }
183
184 if !template.entries.is_empty() || !template.notes.is_empty() {
185 if let Err(e) = util::check_or_create_dir_all(args.check, &output) {
186 fatal!("cannot create dir `{output}`, {e}");
187 }
188
189 template.sort();
190
191 let r = template.write(|file, contents| {
192 let mut output = PathBuf::from(&output);
193 output.push("template");
194
195 if args.clean_template {
196 debug_assert!(!args.check);
197 if args.verbose {
198 println!("removing `{}` to clean template", output.display());
199 }
200 if let Err(e) = fs::remove_dir_all(&output) {
201 if !matches!(e.kind(), io::ErrorKind::NotFound) {
202 error!("cannot remove `{}`, {e}", output.display());
203 }
204 }
205 }
206 util::check_or_create_dir_all(args.check, &output)?;
207 output.push(format!("{}.ftl", if file.is_empty() { "_" } else { file }));
208 util::check_or_write(args.check, output, contents, args.verbose)
209 });
210 if let Err(e) = r {
211 fatal!("error writing template files, {e}");
212 }
213 }
214 }
215
216 let l10n_dir = Path::new(&output);
218 if args.clean_deps {
219 for entry in glob::glob(&format!("{}/*/deps", l10n_dir.display()))
220 .unwrap_or_else(|e| fatal!("cannot cleanup deps in `{}`, {e}", l10n_dir.display()))
221 {
222 let dir = entry.unwrap_or_else(|e| fatal!("cannot cleanup deps, {e}"));
223 if args.verbose {
224 println!("removing `{}` to clean deps", dir.display());
225 }
226 if let Err(e) = std::fs::remove_dir_all(&dir) {
227 if !matches!(e.kind(), io::ErrorKind::NotFound) {
228 error!("cannot remove `{}`, {e}", dir.display());
229 }
230 }
231 }
232 }
233
234 let mut local = vec![];
236 if !args.no_deps {
237 let mut count = 0;
238 let (workspace_root, deps) = util::dependencies(&args.manifest_path);
239 for dep in deps {
240 if dep.version.pre.as_str() == "local" && dep.manifest_path.starts_with(&workspace_root) {
241 local.push(dep);
242 continue;
243 }
244
245 let dep_l10n = dep.manifest_path.with_file_name("l10n");
246 let dep_l10n_reader = match fs::read_dir(&dep_l10n) {
247 Ok(d) => d,
248 Err(e) => {
249 if !matches!(e.kind(), io::ErrorKind::NotFound) {
250 error!("cannot read `{}`, {e}", dep_l10n.display());
251 }
252 continue;
253 }
254 };
255
256 let mut any = false;
257
258 let mut l10n_dir = |lang: Option<&std::ffi::OsStr>| {
260 any = true;
261 let dir = l10n_dir.join(lang.unwrap()).join("deps");
262
263 let ignore_file = dir.join(".gitignore");
264
265 if !ignore_file.exists() {
266 (|| -> io::Result<()> {
268 util::check_or_create_dir_all(args.check, &dir)?;
269
270 let mut ignore = "# Dependency localization files\n".to_owned();
271
272 let output = Path::new(&output);
273 let custom_output = if output != Path::new(&args.manifest_path).with_file_name("l10n") {
274 format!(
275 " --output \"{}\"",
276 output.strip_prefix(std::env::current_dir().unwrap()).unwrap_or(output).display()
277 )
278 .replace('\\', "/")
279 } else {
280 String::new()
281 };
282 if !args.package.is_empty() {
283 writeln!(
284 &mut ignore,
285 "# Call `cargo zng l10n --package {}{custom_output} --no-pkg --no-local --clean-deps` to update",
286 args.package
287 )
288 .unwrap();
289 } else {
290 let path = Path::new(&args.manifest_path);
291 let path = path.strip_prefix(std::env::current_dir().unwrap()).unwrap_or(path);
292 writeln!(
293 &mut ignore,
294 "# Call `cargo zng l10n --manifest-path \"{}\" --no-pkg --no-local --clean-deps` to update",
295 path.display()
296 )
297 .unwrap();
298 }
299 writeln!(&mut ignore).unwrap();
300 writeln!(&mut ignore, "*").unwrap();
301 writeln!(&mut ignore, "!.gitignore").unwrap();
302
303 if let Err(e) = fs::write(&ignore_file, ignore.as_bytes()) {
304 fatal!("cannot write `{}`, {e}", ignore_file.display())
305 }
306
307 Ok(())
308 })()
309 .unwrap_or_else(|e| fatal!("cannot create `{}`, {e}", l10n_dir.display()));
310 }
311
312 let dir = dir.join(&dep.name).join(dep.version.to_string());
313 let _ = util::check_or_create_dir_all(args.check, &dir);
314
315 dir
316 };
317
318 let mut reexport_deps = vec![];
320
321 for dep_l10n_entry in dep_l10n_reader {
322 let dep_l10n_entry = match dep_l10n_entry {
323 Ok(e) => e.path(),
324 Err(e) => {
325 error!("cannot read `{}` entry, {e}", dep_l10n.display());
326 continue;
327 }
328 };
329 if dep_l10n_entry.is_dir() {
330 let output_dir = l10n_dir(dep_l10n_entry.file_name());
332 let _ = util::check_or_create_dir_all(args.check, &output_dir);
333
334 let lang_dir_reader = match fs::read_dir(&dep_l10n_entry) {
335 Ok(d) => d,
336 Err(e) => {
337 error!("cannot read `{}`, {e}", dep_l10n_entry.display());
338 continue;
339 }
340 };
341
342 for lang_entry in lang_dir_reader {
343 let lang_entry = match lang_entry {
344 Ok(e) => e.path(),
345 Err(e) => {
346 error!("cannot read `{}` entry, {e}", dep_l10n_entry.display());
347 continue;
348 }
349 };
350
351 if lang_entry.is_dir() {
352 if lang_entry.file_name().map(|n| n == "deps").unwrap_or(false) {
353 reexport_deps.push((&dep, lang_entry));
354 }
355 } else if lang_entry.is_file() && lang_entry.extension().map(|e| e == "ftl").unwrap_or(false) {
356 let _ = util::check_or_create_dir_all(args.check, &output_dir);
357 let to = output_dir.join(lang_entry.file_name().unwrap());
358 if let Err(e) = util::check_or_copy(args.check, &lang_entry, &to, args.verbose) {
359 error!("cannot copy `{}` to `{}`, {e}", lang_entry.display(), to.display());
360 continue;
361 }
362 }
363 }
364 }
365 }
366
367 reexport_deps.sort_by(|a, b| match a.0.name.cmp(&b.0.name) {
368 Ordering::Equal => b.0.version.cmp(&a.0.version),
369 o => o,
370 });
371
372 for (_, deps) in reexport_deps {
373 let target = l10n_dir(deps.parent().and_then(|p| p.file_name()));
375
376 for entry in glob::glob(&deps.join("*/*/*.ftl").display().to_string()).unwrap() {
378 let entry = entry.unwrap_or_else(|e| fatal!("cannot read `{}` entry, {e}", deps.display()));
379 let target = target.join(entry.strip_prefix(&deps).unwrap());
380 if !target.exists() && entry.is_file() {
381 if let Err(e) = util::check_or_copy(args.check, &entry, &target, args.verbose) {
382 error!("cannot copy `{}` to `{}`, {e}", entry.display(), target.display());
383 }
384 }
385 }
386 }
387
388 count += any as u32;
389 }
390 println!("found {count} dependencies with localization");
391 }
392
393 if !args.no_local {
395 for dep in local {
396 run_impl(
397 L10nArgs {
398 input: String::new(),
399 output: output.clone(),
400 package: String::new(),
401 manifest_path: dep.manifest_path.display().to_string(),
402 no_deps: true,
403 no_local: true,
404 no_pkg: false,
405 clean_deps: false,
406 clean_template: false,
407 clean: false,
408 macros: args.macros.clone(),
409 pseudo: String::new(),
410 pseudo_m: String::new(),
411 pseudo_w: String::new(),
412 check: args.check,
413 verbose: args.verbose,
414 },
415 true,
416 )
417 }
418 }
419 }
420
421 if !args.pseudo.is_empty() {
422 pseudo::pseudo(&args.pseudo, args.check, args.verbose);
423 }
424 if !args.pseudo_m.is_empty() {
425 pseudo::pseudo_mirr(&args.pseudo_m, args.check, args.verbose);
426 }
427 if !args.pseudo_w.is_empty() {
428 pseudo::pseudo_wide(&args.pseudo_w, args.check, args.verbose);
429 }
430}