es_fluent_cli/commands/
format.rs1use crate::commands::{WorkspaceArgs, WorkspaceCrates};
7use crate::core::{CliError, CrateInfo, FormatError, FormatReport};
8use crate::utils::{get_all_locales, ui};
9use anyhow::{Context as _, Result};
10use clap::Parser;
11use es_fluent_toml::I18nConfig;
12use fluent_syntax::parser;
13use std::fs;
14use std::path::{Path, PathBuf};
15
16#[derive(Debug, Parser)]
18pub struct FormatArgs {
19 #[command(flatten)]
20 pub workspace: WorkspaceArgs,
21
22 #[arg(long)]
24 pub all: bool,
25
26 #[arg(long)]
28 pub dry_run: bool,
29}
30
31#[derive(Debug)]
33pub struct FormatResult {
34 pub path: PathBuf,
36 pub changed: bool,
38 pub error: Option<String>,
40 pub diff_info: Option<(String, String)>,
42}
43
44impl FormatResult {
45 fn error(path: &Path, msg: impl Into<String>) -> Self {
47 Self {
48 path: path.to_path_buf(),
49 changed: false,
50 error: Some(msg.into()),
51 diff_info: None,
52 }
53 }
54
55 fn unchanged(path: &Path) -> Self {
57 Self {
58 path: path.to_path_buf(),
59 changed: false,
60 error: None,
61 diff_info: None,
62 }
63 }
64
65 fn changed(path: &Path, diff: Option<(String, String)>) -> Self {
67 Self {
68 path: path.to_path_buf(),
69 changed: true,
70 error: None,
71 diff_info: diff,
72 }
73 }
74}
75
76pub fn run_format(args: FormatArgs) -> Result<(), CliError> {
78 let workspace = WorkspaceCrates::discover(args.workspace)?;
79
80 if !workspace.print_discovery(ui::print_format_header) {
81 ui::print_no_crates_found();
82 return Ok(());
83 }
84
85 let mut total_formatted = 0;
86 let mut total_unchanged = 0;
87 let mut errors: Vec<FormatError> = Vec::new();
88
89 let pb = ui::create_progress_bar(workspace.crates.len() as u64, "Formatting crates...");
90
91 for krate in &workspace.crates {
92 pb.set_message(format!("Formatting {}", krate.name));
93 let results = format_crate(krate, args.all, args.dry_run)?;
94
95 for result in results {
96 if let Some(error) = result.error {
97 errors.push(FormatError {
98 path: result.path,
99 help: error,
100 });
101 } else if result.changed {
102 total_formatted += 1;
103 pb.suspend(|| {
104 let display_path = std::env::current_dir()
105 .ok()
106 .and_then(|cwd| result.path.strip_prefix(&cwd).ok())
107 .unwrap_or(&result.path);
108
109 if args.dry_run {
110 ui::print_would_format(display_path);
111 if let Some((old, new)) = &result.diff_info {
112 ui::print_diff(old, new);
113 }
114 } else {
115 ui::print_formatted(display_path);
116 }
117 });
118 } else {
119 total_unchanged += 1;
120 }
121 }
122 pb.inc(1);
123 }
124 pb.finish_and_clear();
125
126 if errors.is_empty() {
127 if args.dry_run && total_formatted > 0 {
128 ui::print_format_dry_run_summary(total_formatted);
129 } else {
130 ui::print_format_summary(total_formatted, total_unchanged);
131 }
132 Ok(())
133 } else {
134 Err(CliError::Format(FormatReport {
135 formatted_count: total_formatted,
136 error_count: errors.len(),
137 errors,
138 }))
139 }
140}
141
142fn format_crate(
144 krate: &CrateInfo,
145 all_locales: bool,
146 check_only: bool,
147) -> Result<Vec<FormatResult>> {
148 let config = I18nConfig::read_from_path(&krate.i18n_config_path)
149 .with_context(|| format!("Failed to read {}", krate.i18n_config_path.display()))?;
150
151 let assets_dir = krate.manifest_dir.join(&config.assets_dir);
152
153 let locales: Vec<String> = if all_locales {
154 get_all_locales(&assets_dir)?
156 } else {
157 vec![config.fallback_language.clone()]
158 };
159
160 let mut results = Vec::new();
161
162 for locale in &locales {
163 let locale_dir = assets_dir.join(locale);
164 if !locale_dir.exists() {
165 continue;
166 }
167
168 let ftl_file = locale_dir.join(format!("{}.ftl", krate.name));
170 if ftl_file.exists() {
171 let ftl_file = fs::canonicalize(&ftl_file).unwrap_or(ftl_file);
172 let result = format_ftl_file(&ftl_file, check_only);
173 results.push(result);
174 }
175 }
176
177 Ok(results)
178}
179
180fn format_ftl_file(path: &Path, check_only: bool) -> FormatResult {
182 let content = match fs::read_to_string(path) {
183 Ok(c) => c,
184 Err(e) => return FormatResult::error(path, format!("Failed to read file: {}", e)),
185 };
186
187 if content.trim().is_empty() {
188 return FormatResult::unchanged(path);
189 }
190
191 let resource = match parser::parse(content.clone()) {
192 Ok(res) => res,
193 Err((res, _errors)) => res, };
195
196 let formatted = es_fluent_generate::formatting::sort_ftl_resource(&resource);
198 let formatted_content = format!("{}\n", formatted.trim_end());
199
200 if content == formatted_content {
201 return FormatResult::unchanged(path);
202 }
203
204 if !check_only && let Err(e) = fs::write(path, &formatted_content) {
206 return FormatResult::error(path, format!("Failed to write file: {}", e));
207 }
208
209 let diff = if check_only {
210 Some((content, formatted_content))
211 } else {
212 None
213 };
214
215 FormatResult::changed(path, diff)
216}