Skip to main content

es_fluent_cli_helpers/
lib.rs

1#![doc = include_str!("../README.md")]
2
3mod cli;
4mod generate;
5
6use es_fluent_derive_core::{EsFluentError, write_metadata_result};
7use es_fluent_toml::I18nConfig;
8use std::path::Path;
9
10#[cfg(test)]
11pub(crate) static TEST_CWD_LOCK: std::sync::LazyLock<std::sync::Mutex<()>> =
12    std::sync::LazyLock::new(|| std::sync::Mutex::new(()));
13
14pub use cli::{ExpectedKey, InventoryData, write_inventory_for_crate};
15pub use generate::{EsFluentGenerator, FluentParseMode, GeneratorArgs};
16
17/// Type alias for compatibility
18pub type GeneratorError = EsFluentError;
19
20/// Run the FTL generation process for a crate.
21///
22/// This function:
23/// - Reads the i18n.toml configuration
24/// - Resolves output and assets paths
25/// - Runs the es-fluent generator
26/// - Writes the result status to result.json
27///
28/// Returns `true` if any FTL files were modified, `false` otherwise.
29pub fn run_generate(i18n_toml_path: &str, crate_name: &str) -> bool {
30    // Read config from parent crate's i18n.toml
31    let i18n_toml_path = Path::new(i18n_toml_path);
32    let i18n_dir = i18n_toml_path
33        .parent()
34        .expect("Failed to get i18n directory");
35    let config =
36        es_fluent_toml::I18nConfig::from_manifest_dir(i18n_dir).expect("Failed to read i18n.toml");
37    let output_path = I18nConfig::output_dir_from_manifest_dir(i18n_dir)
38        .expect("Failed to resolve output directory");
39    let assets_dir = config
40        .assets_dir_from_base(Some(i18n_dir))
41        .expect("Failed to resolve assets directory");
42
43    let changed = EsFluentGenerator::builder()
44        .output_path(output_path)
45        .assets_dir(assets_dir)
46        .manifest_dir(i18n_dir)
47        .crate_name(crate_name)
48        .build()
49        .run_cli()
50        .expect("Failed to run generator");
51
52    // Write result to JSON file for CLI to read
53    let result = serde_json::json!({ "changed": changed });
54    write_metadata_result(crate_name, &result).expect("Failed to write metadata result");
55    changed
56}
57
58/// Run the FTL generation process with explicit options (no CLI parsing).
59///
60/// This is used by the monolithic binary to avoid conflicting clap argument parsing.
61pub fn run_generate_with_options(
62    i18n_toml_path: &str,
63    crate_name: &str,
64    mode: FluentParseMode,
65    dry_run: bool,
66) -> bool {
67    let i18n_toml_path = Path::new(i18n_toml_path);
68    let i18n_dir = i18n_toml_path
69        .parent()
70        .expect("Failed to get i18n directory");
71    let _config =
72        es_fluent_toml::I18nConfig::from_manifest_dir(i18n_dir).expect("Failed to read i18n.toml");
73    let output_path = I18nConfig::output_dir_from_manifest_dir(i18n_dir)
74        .expect("Failed to resolve output directory");
75
76    let changed = EsFluentGenerator::builder()
77        .output_path(output_path)
78        .assets_dir(
79            I18nConfig::assets_dir_from_manifest_dir(i18n_dir)
80                .expect("Failed to resolve assets directory"),
81        )
82        .manifest_dir(i18n_dir)
83        .crate_name(crate_name)
84        .mode(mode)
85        .dry_run(dry_run)
86        .build()
87        .generate()
88        .expect("Failed to run generator");
89
90    let result = serde_json::json!({ "changed": changed });
91    write_metadata_result(crate_name, &result).expect("Failed to write metadata result");
92    changed
93}
94
95/// Run the inventory check process for a crate.
96///
97/// This writes the collected inventory data for the specified crate.
98pub fn run_check(crate_name: &str) {
99    write_inventory_for_crate(crate_name);
100}
101
102/// Run the FTL clean process with explicit options (no CLI parsing).
103///
104/// This is used by the monolithic binary to avoid conflicting clap argument parsing.
105pub fn run_clean_with_options(
106    i18n_toml_path: &str,
107    crate_name: &str,
108    all_locales: bool,
109    dry_run: bool,
110) -> bool {
111    let i18n_toml_path = Path::new(i18n_toml_path);
112    let i18n_dir = i18n_toml_path
113        .parent()
114        .expect("Failed to get i18n directory");
115    let config =
116        es_fluent_toml::I18nConfig::from_manifest_dir(i18n_dir).expect("Failed to read i18n.toml");
117    let output_path = I18nConfig::output_dir_from_manifest_dir(i18n_dir)
118        .expect("Failed to resolve output directory");
119    let assets_dir = config
120        .assets_dir_from_base(Some(i18n_dir))
121        .expect("Failed to resolve assets directory");
122
123    let changed = EsFluentGenerator::builder()
124        .output_path(output_path)
125        .assets_dir(assets_dir)
126        .manifest_dir(i18n_dir)
127        .crate_name(crate_name)
128        .dry_run(dry_run)
129        .build()
130        .clean(all_locales, dry_run)
131        .expect("Failed to run clean");
132
133    let result = serde_json::json!({ "changed": changed });
134    write_metadata_result(crate_name, &result).expect("Failed to write metadata result");
135    changed
136}
137
138/// Main entry point for the monolithic binary.
139///
140/// Parses command-line arguments and dispatches to the appropriate handler.
141/// This minimizes the code needed in the generated binary template.
142pub fn run() {
143    let args: Vec<String> = std::env::args().collect();
144
145    let command = args.get(1).map(|s| s.as_str()).unwrap_or("check");
146    let i18n_path = args.get(2).map(|s| s.as_str());
147
148    let target_crate = args
149        .iter()
150        .position(|s| s == "--crate")
151        .and_then(|i| args.get(i + 1))
152        .map(|s| s.as_str());
153
154    let mode_str = args
155        .iter()
156        .position(|s| s == "--mode")
157        .and_then(|i| args.get(i + 1))
158        .map(|s| s.as_str())
159        .unwrap_or("conservative");
160
161    let dry_run = args.iter().any(|s| s == "--dry-run");
162    let all_locales = args.iter().any(|s| s == "--all");
163
164    match command {
165        "generate" => {
166            let path = i18n_path.expect("Missing i18n.toml path");
167            let name = target_crate.expect("Missing --crate argument");
168            let mode = match mode_str {
169                "aggressive" => FluentParseMode::Aggressive,
170                _ => FluentParseMode::Conservative,
171            };
172            run_generate_with_options(path, name, mode, dry_run);
173        },
174        "clean" => {
175            let path = i18n_path.expect("Missing i18n.toml path");
176            let name = target_crate.expect("Missing --crate argument");
177            run_clean_with_options(path, name, all_locales, dry_run);
178        },
179        "check" => {
180            let name = target_crate.expect("Missing --crate argument");
181            run_check(name);
182        },
183        _ => {
184            eprintln!("Unknown command: {}", command);
185            std::process::exit(1);
186        },
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193    use tempfile::tempdir;
194
195    fn with_temp_cwd<T>(f: impl FnOnce(&Path) -> T) -> T {
196        let _guard = crate::TEST_CWD_LOCK.lock().expect("lock poisoned");
197        let original = std::env::current_dir().expect("cwd");
198        let temp = tempdir().expect("tempdir");
199        std::env::set_current_dir(temp.path()).expect("set cwd");
200        let result = f(temp.path());
201        std::env::set_current_dir(original).expect("restore cwd");
202        result
203    }
204
205    fn write_basic_manifest(manifest_dir: &Path) {
206        std::fs::create_dir_all(manifest_dir.join("i18n/en-US")).expect("mkdir en-US");
207        std::fs::create_dir_all(manifest_dir.join("i18n/fr")).expect("mkdir fr");
208        std::fs::write(
209            manifest_dir.join("i18n.toml"),
210            "fallback_language = \"en-US\"\nassets_dir = \"i18n\"\n",
211        )
212        .expect("write i18n.toml");
213    }
214
215    fn read_changed_result(base: &Path, crate_name: &str) -> bool {
216        let result_path = base.join("metadata").join(crate_name).join("result.json");
217        let content = std::fs::read_to_string(result_path).expect("read result json");
218        let value: serde_json::Value = serde_json::from_str(&content).expect("parse result json");
219        value["changed"].as_bool().expect("changed bool")
220    }
221
222    #[test]
223    fn run_generate_and_clean_with_options_write_metadata_result() {
224        with_temp_cwd(|cwd| {
225            write_basic_manifest(cwd);
226            let i18n_path = cwd.join("i18n.toml");
227
228            let changed = run_generate_with_options(
229                i18n_path.to_str().expect("path"),
230                "missing-crate",
231                FluentParseMode::Conservative,
232                false,
233            );
234            assert_eq!(changed, read_changed_result(cwd, "missing-crate"));
235
236            let clean_changed = run_clean_with_options(
237                i18n_path.to_str().expect("path"),
238                "missing-crate",
239                true,
240                true,
241            );
242            assert_eq!(clean_changed, read_changed_result(cwd, "missing-crate"));
243        });
244    }
245
246    #[test]
247    fn run_check_writes_inventory_json_for_requested_crate() {
248        with_temp_cwd(|cwd| {
249            run_check("unknown-crate");
250
251            let inventory_path = cwd.join("metadata/unknown-crate/inventory.json");
252            let content = std::fs::read_to_string(inventory_path).expect("read inventory");
253            let value: serde_json::Value = serde_json::from_str(&content).expect("parse json");
254            assert_eq!(
255                value["expected_keys"]
256                    .as_array()
257                    .expect("expected_keys")
258                    .len(),
259                0
260            );
261        });
262    }
263}