es_fluent_cli/utils/
discovery.rs

1use crate::core::{CrateInfo, WorkspaceInfo};
2use anyhow::{Context as _, Result};
3use cargo_metadata::MetadataCommand;
4use std::path::{Path, PathBuf};
5
6/// Discovers workspace information including root, target dir, and all crates with i18n.toml.
7/// This is used by the monolithic temp crate approach for efficient inventory collection.
8pub fn discover_workspace(root_dir: &Path) -> Result<WorkspaceInfo> {
9    let root_dir = root_dir
10        .canonicalize()
11        .context("Failed to canonicalize root directory")?;
12
13    let metadata = MetadataCommand::new()
14        .current_dir(&root_dir)
15        .no_deps()
16        .exec()
17        .context("Failed to get cargo metadata")?;
18
19    let workspace_root: PathBuf = metadata.workspace_root.clone().into();
20    let target_dir: PathBuf = metadata.target_directory.clone().into();
21
22    let mut crates = Vec::new();
23
24    for package in metadata.workspace_packages() {
25        let manifest_dir: PathBuf = package.manifest_path.parent().unwrap().into();
26
27        let i18n_config_path = manifest_dir.join("i18n.toml");
28        if !i18n_config_path.exists() {
29            continue;
30        }
31
32        let i18n_config = es_fluent_toml::I18nConfig::read_from_path(&i18n_config_path)
33            .with_context(|| format!("Failed to read {}", i18n_config_path.display()))?;
34
35        let ftl_output_dir = manifest_dir
36            .join(&i18n_config.assets_dir)
37            .join(&i18n_config.fallback_language);
38
39        let src_dir = manifest_dir.join("src");
40        let has_lib_rs = src_dir.join("lib.rs").exists();
41
42        crates.push(CrateInfo {
43            name: package.name.to_string(),
44            manifest_dir,
45            src_dir,
46            i18n_config_path,
47            ftl_output_dir,
48            has_lib_rs,
49            fluent_features: i18n_config
50                .fluent_feature
51                .as_ref()
52                .map(|f| f.as_vec())
53                .unwrap_or_default(),
54        });
55    }
56
57    // Sort by name for consistent ordering
58    crates.sort_by(|a, b| a.name.cmp(&b.name));
59
60    Ok(WorkspaceInfo {
61        root_dir: workspace_root,
62        target_dir,
63        crates,
64    })
65}
66
67/// Discovers all crates in a workspace (or single crate) that have i18n.toml.
68/// This is a convenience wrapper around discover_workspace that returns just the crates.
69pub fn discover_crates(root_dir: &Path) -> Result<Vec<CrateInfo>> {
70    discover_workspace(root_dir).map(|ws| ws.crates)
71}
72
73/// Counts the number of FTL resources (message keys) for a specific crate.
74pub fn count_ftl_resources(ftl_output_dir: &Path, crate_name: &str) -> usize {
75    let ftl_file = ftl_output_dir.join(format!("{}.ftl", crate_name));
76
77    if !ftl_file.exists() {
78        return 0;
79    }
80
81    let Ok(content) = std::fs::read_to_string(&ftl_file) else {
82        return 0;
83    };
84
85    // Count lines that start with a message identifier
86    // (not comments, not blank, not starting with whitespace)
87    content
88        .lines()
89        .filter(|line| {
90            let trimmed = line.trim();
91            !trimmed.is_empty()
92                && !trimmed.starts_with('#')
93                && !line.starts_with(' ')
94                && !line.starts_with('\t')
95                && trimmed.contains('=')
96        })
97        .count()
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn test_count_ftl_resources_empty() {
106        let temp = tempfile::tempdir().unwrap();
107        assert_eq!(count_ftl_resources(temp.path(), "test-crate"), 0);
108    }
109
110    #[test]
111    fn test_count_ftl_resources_nonexistent() {
112        assert_eq!(
113            count_ftl_resources(Path::new("/nonexistent/path"), "test-crate"),
114            0
115        );
116    }
117}