es_fluent_cli_helpers/
generate.rs

1//! FTL file generation functionality.
2
3use es_fluent_core::registry::FtlTypeInfo;
4use std::path::PathBuf;
5
6pub use es_fluent_generate::FluentParseMode;
7pub use es_fluent_generate::error::FluentGenerateError;
8
9/// Error type for FTL generation.
10#[derive(Debug, thiserror::Error)]
11pub enum GeneratorError {
12    /// Failed to read i18n.toml configuration.
13    #[error("Configuration error: {0}")]
14    Config(#[from] es_fluent_toml::I18nConfigError),
15
16    /// Failed to detect crate name.
17    #[error("Failed to detect crate name: {0}")]
18    CrateName(String),
19
20    /// Failed to generate FTL files.
21    #[error("Generation error: {0}")]
22    Generate(#[from] FluentGenerateError),
23}
24
25/// Builder for generating FTL files from registered types.
26///
27/// Uses the `inventory` crate to collect all types registered via
28/// `#[derive(EsFluent)]`, `#[derive(EsFluentKv)]`, or `#[derive(EsFluentThis)]`.
29#[derive(bon::Builder)]
30pub struct EsFluentGenerator {
31    /// The parse mode (Conservative preserves existing translations, Aggressive overwrites).
32    /// Defaults to Conservative.
33    #[builder(default)]
34    mode: FluentParseMode,
35
36    /// Override the crate name (defaults to auto-detect from Cargo.toml).
37    #[builder(into)]
38    crate_name: Option<String>,
39
40    /// Override the output path (defaults to reading from i18n.toml).
41    #[builder(into)]
42    output_path: Option<PathBuf>,
43
44    /// Override the assets directory (defaults to reading from i18n.toml).
45    #[builder(into)]
46    assets_dir: Option<PathBuf>,
47
48    /// Dry run (don't write changes).
49    #[builder(default)]
50    dry_run: bool,
51}
52
53/// Command line arguments for the generator.
54#[derive(clap::Parser)]
55pub struct GeneratorArgs {
56    #[command(subcommand)]
57    action: Action,
58}
59
60#[derive(clap::Subcommand)]
61enum Action {
62    /// Generate FTL files
63    Generate {
64        /// Parse mode
65        #[arg(long, default_value_t = FluentParseMode::default())]
66        mode: FluentParseMode,
67        /// Dry run (don't write changes)
68        #[arg(long)]
69        dry_run: bool,
70    },
71    /// Clean FTL files (remove orphans)
72    Clean {
73        /// Clean all locales
74        #[arg(long)]
75        all: bool,
76        /// Dry run (don't write changes)
77        #[arg(long)]
78        dry_run: bool,
79    },
80}
81
82impl EsFluentGenerator {
83    /// Runs the generator based on command line arguments.
84    pub fn run_cli(self) -> Result<bool, GeneratorError> {
85        use clap::Parser;
86        let args = GeneratorArgs::parse();
87
88        match args.action {
89            Action::Generate { mode, dry_run } => {
90                let mut generator = self;
91                generator.mode = mode;
92                generator.dry_run = dry_run;
93                generator.generate()
94            },
95            Action::Clean { all, dry_run } => self.clean(all, dry_run),
96        }
97    }
98
99    // --- Resolution helpers (DRY) ---
100
101    /// Resolve the crate name, using override or auto-detection.
102    fn resolve_crate_name(&self) -> Result<String, GeneratorError> {
103        self.crate_name
104            .clone()
105            .map_or_else(Self::detect_crate_name, Ok)
106    }
107
108    /// Resolve the output path for the fallback locale.
109    fn resolve_output_path(&self) -> Result<PathBuf, GeneratorError> {
110        if let Some(path) = &self.output_path {
111            return Ok(path.clone());
112        }
113        let config = es_fluent_toml::I18nConfig::read_from_manifest_dir()?;
114        Ok(config.assets_dir.join(&config.fallback_language))
115    }
116
117    /// Resolve the assets directory.
118    fn resolve_assets_dir(&self) -> Result<PathBuf, GeneratorError> {
119        if let Some(path) = &self.assets_dir {
120            return Ok(path.clone());
121        }
122        let config = es_fluent_toml::I18nConfig::read_from_manifest_dir()?;
123        Ok(config.assets_dir)
124    }
125
126    /// Resolve the paths to clean based on configuration.
127    fn resolve_clean_paths(&self, all_locales: bool) -> Result<Vec<PathBuf>, GeneratorError> {
128        if !all_locales {
129            return Ok(vec![self.resolve_output_path()?]);
130        }
131
132        let assets_dir = self.resolve_assets_dir()?;
133        let mut paths: Vec<PathBuf> = std::fs::read_dir(&assets_dir)
134            .ok()
135            .map(|entries| {
136                entries
137                    .filter_map(|e| e.ok())
138                    .filter(|e| e.path().is_dir())
139                    .map(|e| e.path())
140                    .collect()
141            })
142            .unwrap_or_else(|| self.output_path.clone().into_iter().collect());
143
144        // Sort paths to ensure deterministic ordering across filesystems
145        paths.sort();
146
147        Ok(paths)
148    }
149
150    /// Generates FTL files from all registered types.
151    pub fn generate(&self) -> Result<bool, GeneratorError> {
152        let crate_name = self.resolve_crate_name()?;
153        let output_path = self.resolve_output_path()?;
154        let type_infos = collect_type_infos(&crate_name);
155
156        tracing::info!(
157            "Generating FTL files for {} types in crate '{}'",
158            type_infos.len(),
159            crate_name
160        );
161
162        let changed = es_fluent_generate::generate(
163            &crate_name,
164            output_path,
165            type_infos,
166            self.mode.clone(),
167            self.dry_run,
168        )?;
169
170        Ok(changed)
171    }
172
173    /// Cleans FTL files by removing orphan keys while preserving existing translations.
174    pub fn clean(&self, all_locales: bool, dry_run: bool) -> Result<bool, GeneratorError> {
175        let crate_name = self.resolve_crate_name()?;
176        let paths = self.resolve_clean_paths(all_locales)?;
177        let type_infos = collect_type_infos(&crate_name);
178
179        let mut any_changed = false;
180        for output_path in paths {
181            if !dry_run {
182                tracing::info!(
183                    "Cleaning FTL files for {} types in crate '{}' at {}",
184                    type_infos.len(),
185                    crate_name,
186                    output_path.display()
187                );
188            }
189
190            if es_fluent_generate::clean::clean(
191                &crate_name,
192                output_path,
193                type_infos.clone(),
194                dry_run,
195            )? {
196                any_changed = true;
197            }
198        }
199
200        Ok(any_changed)
201    }
202
203    /// Auto-detects the crate name from Cargo.toml using cargo_metadata.
204    fn detect_crate_name() -> Result<String, GeneratorError> {
205        let manifest_dir = std::env::var("CARGO_MANIFEST_DIR")
206            .map_err(|_| GeneratorError::CrateName("CARGO_MANIFEST_DIR not set".to_string()))?;
207        let manifest_path = PathBuf::from(&manifest_dir).join("Cargo.toml");
208
209        cargo_metadata::MetadataCommand::new()
210            .exec()
211            .ok()
212            .and_then(|metadata| {
213                metadata
214                    .packages
215                    .iter()
216                    .find(|pkg| pkg.manifest_path == manifest_path)
217                    .map(|pkg| pkg.name.to_string())
218            })
219            .or_else(|| std::env::var("CARGO_PKG_NAME").ok())
220            .ok_or_else(|| GeneratorError::CrateName("Could not determine crate name".to_string()))
221    }
222}
223
224/// Collect all registered type infos for a given crate.
225fn collect_type_infos(crate_name: &str) -> Vec<FtlTypeInfo> {
226    let crate_ident = crate_name.replace('-', "_");
227    es_fluent_core::registry::get_all_ftl_type_infos()
228        .into_iter()
229        .filter(|info| {
230            info.module_path == crate_ident
231                || info.module_path.starts_with(&format!("{}::", crate_ident))
232        })
233        .collect()
234}