1use crate::formats::parse_custom_format;
2use crate::transformers::custom_format_to_resource;
3use crate::ui;
4
5use langcodec::{Codec, ReadOptions, converter};
6use rayon::prelude::*;
7
8#[derive(Debug, Clone, PartialEq, clap::ValueEnum)]
10pub enum ConflictStrategy {
11 First,
13 Last,
15 Skip,
17}
18
19pub fn run_merge_command(
21 inputs: Vec<String>,
22 output: String,
23 strategy: ConflictStrategy,
24 lang: Option<String>,
25 source_language_override: Option<String>,
26 version_override: Option<String>,
27 strict: bool,
28) {
29 if inputs.is_empty() {
30 eprintln!(
31 "{}",
32 ui::status_line_stderr(
33 ui::Tone::Error,
34 "Error: At least one input file is required."
35 )
36 );
37 std::process::exit(1);
38 }
39
40 println!(
42 "{}",
43 ui::status_line_stdout(
44 ui::Tone::Info,
45 &format!("Reading {} input files...", inputs.len()),
46 )
47 );
48 let read_results: Vec<Result<Codec, String>> = inputs
49 .par_iter()
50 .map(|input| read_input_to_codec(input, lang.clone(), strict))
51 .collect();
52
53 let mut input_codecs: Vec<Codec> = Vec::with_capacity(read_results.len());
54 for (idx, res) in read_results.into_iter().enumerate() {
55 match res {
56 Ok(c) => input_codecs.push(c),
57 Err(e) => {
58 println!(
59 "{}",
60 ui::status_line_stdout(
61 ui::Tone::Error,
62 &format!("Error reading input file {}/{}", idx + 1, inputs.len()),
63 )
64 );
65 eprintln!("{}", e);
66 std::process::exit(1);
67 }
68 }
69 }
70
71 let mut codec = Codec::from_codecs(input_codecs);
73
74 println!(
78 "{}",
79 ui::status_line_stdout(ui::Tone::Info, "Merging resources...")
80 );
81 let conflict_strategy = match strategy {
82 ConflictStrategy::First => langcodec::types::ConflictStrategy::First,
83 ConflictStrategy::Last => langcodec::types::ConflictStrategy::Last,
84 ConflictStrategy::Skip => langcodec::types::ConflictStrategy::Skip,
85 };
86
87 let merge_count = codec.merge_resources(&conflict_strategy);
88 println!(
89 "{}",
90 ui::status_line_stdout(
91 ui::Tone::Success,
92 &format!("Merged {} language groups", merge_count),
93 )
94 );
95
96 println!(
97 "{}",
98 ui::status_line_stdout(ui::Tone::Info, "Writing merged output...")
99 );
100 match converter::infer_format_from_path(output.clone()) {
101 Some(format) => {
102 println!(
103 "{}",
104 ui::status_line_stdout(
105 ui::Tone::Info,
106 &format!("Converting resources to format: {:?}", format),
107 )
108 );
109 let source_language = source_language_override
113 .filter(|s| !s.trim().is_empty())
114 .unwrap_or_else(|| {
115 codec
116 .resources
117 .first()
118 .and_then(|r| {
119 r.metadata
120 .custom
121 .get("source_language")
122 .cloned()
123 .filter(|s| !s.trim().is_empty())
124 })
125 .unwrap_or_else(|| {
126 codec
127 .resources
128 .first()
129 .map(|r| r.metadata.language.clone())
130 .unwrap_or("en".to_string())
131 })
132 });
133
134 println!(
135 "{}",
136 ui::status_line_stdout(
137 ui::Tone::Accent,
138 &format!("Setting metadata.source_language to: {}", source_language),
139 )
140 );
141
142 let version = version_override.unwrap_or_else(|| {
144 codec
145 .resources
146 .first()
147 .and_then(|r| r.metadata.custom.get("version").cloned())
148 .unwrap_or_else(|| "1.0".to_string())
149 });
150
151 println!(
152 "{}",
153 ui::status_line_stdout(
154 ui::Tone::Accent,
155 &format!("Setting metadata.version to: {}", version),
156 )
157 );
158
159 codec.iter_mut().for_each(|r| {
160 r.metadata
161 .custom
162 .insert("source_language".to_string(), source_language.clone());
163 r.metadata
164 .custom
165 .insert("version".to_string(), version.clone());
166 });
167
168 if let Err(e) = converter::convert_resources_to_format(codec.resources, &output, format)
169 {
170 println!(
171 "{}",
172 ui::status_line_stdout(ui::Tone::Error, "Error converting resources to format")
173 );
174 eprintln!("Error converting to {}: {}", output, e);
175 std::process::exit(1);
176 }
177 }
178 None => {
179 if codec.resources.len() == 1 {
180 println!(
181 "{}",
182 ui::status_line_stdout(
183 ui::Tone::Info,
184 "Writing single resource to output file",
185 )
186 );
187 if let Some(resource) = codec.resources.first()
188 && let Err(e) = Codec::write_resource_to_file(resource, &output)
189 {
190 println!(
191 "{}",
192 ui::status_line_stdout(ui::Tone::Error, "Error writing output file")
193 );
194 eprintln!("Error writing to {}: {}", output, e);
195 std::process::exit(1);
196 }
197 } else {
198 println!(
199 "{}",
200 ui::status_line_stdout(ui::Tone::Error, "Error writing output file")
201 );
202 eprintln!("Error writing to {}: multiple resources", output);
203 std::process::exit(1);
204 }
205 }
206 }
207
208 println!(
209 "{}",
210 ui::status_line_stdout(
211 ui::Tone::Success,
212 &format!("Successfully merged {} files into {}", inputs.len(), output),
213 )
214 );
215}
216
217fn read_input_to_resources(
219 input: &str,
220 lang: Option<String>,
221 strict: bool,
222) -> Result<Vec<langcodec::Resource>, String> {
223 if strict {
224 if input.ends_with(".json") || input.ends_with(".yaml") || input.ends_with(".yml") {
225 crate::validation::validate_custom_format_file(input)
226 .map_err(|e| format!("Failed to validate {}: {}", input, e))?;
227
228 let file_content = std::fs::read_to_string(input)
229 .map_err(|e| format!("Error reading file {}: {}", input, e))?;
230
231 crate::formats::validate_custom_format_content(input, &file_content)
232 .map_err(|e| format!("Invalid custom format {}: {}", input, e))?;
233
234 let resources = custom_format_to_resource(
235 input.to_string(),
236 parse_custom_format("json-language-map")
237 .map_err(|e| format!("Failed to parse custom format: {}", e))?,
238 )
239 .map_err(|e| format!("Failed to convert custom format {}: {}", input, e))?;
240
241 return Ok(resources);
242 }
243
244 let mut local_codec = Codec::new();
245 local_codec
246 .read_file_by_extension_with_options(
247 input,
248 &ReadOptions::new()
249 .with_language_hint(lang)
250 .with_strict(true),
251 )
252 .map_err(|e| format!("Error reading {}: {}", input, e))?;
253 return Ok(local_codec.resources);
254 }
255
256 {
258 let mut local_codec = Codec::new();
259 if let Ok(()) = local_codec.read_file_by_extension(input, lang.clone()) {
260 return Ok(local_codec.resources);
261 }
262 }
263
264 if input.ends_with(".json") || input.ends_with(".yaml") || input.ends_with(".yml") {
266 crate::validation::validate_custom_format_file(input)
268 .map_err(|e| format!("Failed to validate {}: {}", input, e))?;
269
270 let file_content = std::fs::read_to_string(input)
272 .map_err(|e| format!("Error reading file {}: {}", input, e))?;
273
274 crate::formats::validate_custom_format_content(input, &file_content)
276 .map_err(|e| format!("Invalid custom format {}: {}", input, e))?;
277
278 let resources = custom_format_to_resource(
280 input.to_string(),
281 parse_custom_format("json-language-map")
282 .map_err(|e| format!("Failed to parse custom format: {}", e))?,
283 )
284 .map_err(|e| format!("Failed to convert custom format {}: {}", input, e))?;
285
286 return Ok(resources);
287 }
288
289 Err(format!("Error reading {}: unsupported format", input))
290}
291
292fn read_input_to_codec(input: &str, lang: Option<String>, strict: bool) -> Result<Codec, String> {
294 let resources = read_input_to_resources(input, lang, strict)?;
295 Ok(Codec { resources })
296}