Skip to main content

es_fluent_cli/commands/sync/
mod.rs

1//! Sync command for synchronizing missing translations across locales.
2//!
3//! This module provides functionality to sync missing translation keys
4//! from the fallback language to other locales, preserving existing translations.
5
6mod locale;
7mod merge;
8
9use crate::commands::{DryRunSummary, WorkspaceArgs, WorkspaceCrates};
10use crate::core::{CliError, LocaleNotFoundError, SyncMissingKey};
11use crate::ftl::collect_all_available_locales;
12use crate::utils::ui;
13use clap::Parser;
14use std::collections::HashSet;
15
16/// Arguments for the sync command.
17#[derive(Debug, Parser)]
18pub struct SyncArgs {
19    #[command(flatten)]
20    pub workspace: WorkspaceArgs,
21
22    /// Specific locale(s) to sync to (can be specified multiple times).
23    #[arg(short, long)]
24    pub locale: Vec<String>,
25
26    /// Sync to all locales (excluding the fallback language).
27    #[arg(long)]
28    pub all: bool,
29
30    /// Dry run - show what would be synced without making changes.
31    #[arg(long)]
32    pub dry_run: bool,
33}
34
35/// Run the sync command.
36pub fn run_sync(args: SyncArgs) -> Result<(), CliError> {
37    let workspace = WorkspaceCrates::discover(args.workspace)?;
38
39    ui::print_sync_header();
40
41    let crates = workspace.crates;
42
43    if crates.is_empty() {
44        ui::print_no_crates_found();
45        return Ok(());
46    }
47
48    let target_locales: Option<HashSet<String>> = if args.all {
49        None // Will sync to all locales
50    } else if args.locale.is_empty() {
51        ui::print_no_locales_specified();
52        return Ok(());
53    } else {
54        Some(args.locale.iter().cloned().collect())
55    };
56
57    // Validate that specified locales exist
58    if let Some(ref targets) = target_locales {
59        let all_available_locales = collect_all_available_locales(&crates)?;
60
61        for locale in targets {
62            if !all_available_locales.contains(locale) {
63                let mut available: Vec<String> = all_available_locales.into_iter().collect();
64                available.sort();
65                ui::print_locale_not_found(locale, &available);
66                return Err(CliError::LocaleNotFound(LocaleNotFoundError {
67                    locale: locale.clone(),
68                    available: available.join(", "),
69                }));
70            }
71        }
72    }
73
74    let mut total_keys_added = 0;
75    let mut total_locales_affected = 0;
76    let mut all_synced_keys: Vec<SyncMissingKey> = Vec::new();
77
78    let pb = ui::create_progress_bar(crates.len() as u64, "Syncing crates...");
79
80    for krate in &crates {
81        pb.set_message(format!("Syncing {}", krate.name));
82
83        let results = locale::sync_crate(krate, target_locales.as_ref(), args.dry_run)?;
84
85        for result in results {
86            if result.keys_added > 0 {
87                total_locales_affected += 1;
88                total_keys_added += result.keys_added;
89
90                pb.suspend(|| {
91                    if args.dry_run {
92                        ui::print_would_add_keys(result.keys_added, &result.locale, &krate.name);
93                        if let Some(diff) = &result.diff_info {
94                            diff.print();
95                        }
96                    } else {
97                        ui::print_added_keys(result.keys_added, &result.locale);
98                        for key in &result.added_keys {
99                            ui::print_synced_key(key);
100                            all_synced_keys.push(SyncMissingKey {
101                                key: key.clone(),
102                                target_locale: result.locale.clone(),
103                                source_locale: "fallback".to_string(),
104                            });
105                        }
106                    }
107                });
108            }
109        }
110        pb.inc(1);
111    }
112    pb.finish_and_clear();
113
114    if total_keys_added == 0 {
115        ui::print_all_in_sync();
116        Ok(())
117    } else if args.dry_run {
118        DryRunSummary::Sync {
119            keys: total_keys_added,
120            locales: total_locales_affected,
121        }
122        .print();
123        Ok(())
124    } else {
125        ui::print_sync_summary(total_keys_added, total_locales_affected);
126        Ok(())
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use crate::ftl::extract_message_keys;
133    use fluent_syntax::parser;
134
135    #[test]
136    fn test_extract_message_keys() {
137        let content = r#"hello = Hello
138world = World"#;
139        let resource = parser::parse(content.to_string()).unwrap();
140        let keys = extract_message_keys(&resource);
141
142        assert!(keys.contains("hello"));
143        assert!(keys.contains("world"));
144        assert_eq!(keys.len(), 2);
145    }
146}