es_fluent_cli/commands/sync/
mod.rs1mod 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#[derive(Debug, Parser)]
18pub struct SyncArgs {
19 #[command(flatten)]
20 pub workspace: WorkspaceArgs,
21
22 #[arg(short, long)]
24 pub locale: Vec<String>,
25
26 #[arg(long)]
28 pub all: bool,
29
30 #[arg(long)]
32 pub dry_run: bool,
33}
34
35pub 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 } 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 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}