1use std::error::Error;
2
3use clap::ArgMatches;
4use clap::builder::StyledStr;
5use clap::builder::Styles;
6use clap::builder::styling::Style;
7use clap::error::ContextKind;
8use clap::error::ContextValue;
9use clap::error::ErrorFormatter;
10use clap::error::ErrorKind;
11use i18n_embed::DefaultLocalizer;
12use i18n_embed::DesktopLanguageRequester;
13use i18n_embed::Localizer;
14
15use crate::lang::CLAP_I18N_LANGUAGE_LOADER;
16pub use crate::lang::ClapI18nLocalizations;
17use crate::lang::fl;
18
19mod lang;
20
21#[cfg(feature = "derive")]
23pub use clap_i18n_derive::clap_i18n;
24
25#[doc(hidden)]
27pub mod __private {
28 pub use crate::lang::get_translation;
29}
30
31const TAB: &str = " ";
32
33pub fn init_clap_rich_formatter_localizer() {
34 let localizer = DefaultLocalizer::new(&*CLAP_I18N_LANGUAGE_LOADER, &ClapI18nLocalizations);
35 let requested_languages = DesktopLanguageRequester::requested_languages();
36
37 if let Err(error) = localizer.select(&requested_languages) {
38 eprintln!("Error while loading languages for library_fluent {error}");
39 }
40
41 CLAP_I18N_LANGUAGE_LOADER.set_use_isolating(false);
45}
46
47pub trait CommandI18nExt {
48 fn try_get_matches_i18n(self) -> Result<ArgMatches, clap::error::Error<ClapI18nRichFormatter>>;
49 fn get_matches_i18n(self) -> ArgMatches;
50}
51
52impl CommandI18nExt for clap::Command {
53 fn try_get_matches_i18n(self) -> Result<ArgMatches, clap::error::Error<ClapI18nRichFormatter>> {
55 init_clap_rich_formatter_localizer();
56 self.try_get_matches()
57 .map_err(|e| e.apply::<ClapI18nRichFormatter>())
58 }
59
60 fn get_matches_i18n(self) -> ArgMatches {
62 self.try_get_matches_i18n().map_err(|e| e.exit()).unwrap()
63 }
64}
65
66#[non_exhaustive]
70pub struct ClapI18nRichFormatter;
71
72impl ErrorFormatter for ClapI18nRichFormatter {
73 fn format_error(error: &clap::error::Error<Self>) -> StyledStr {
74 use std::fmt::Write as _;
75 let styles = &Styles::default();
76 let valid = &styles.get_valid();
77
78 let mut styled = StyledStr::new();
79 start_error(&mut styled, styles);
80
81 if !write_dynamic_context(error, &mut styled, styles) {
82 if error.kind().as_str().is_some() {
83 styled.push_str(&translation_errorkind(error));
84 } else if let Some(source) = error.source() {
85 let _ = write!(styled, "{source}");
86 } else {
87 styled.push_str("unknown cause");
88 }
89 }
90
91 let mut suggested = false;
92 if let Some(valid) = error.get(ContextKind::SuggestedSubcommand) {
93 styled.push_str("\n");
94 if !suggested {
95 styled.push_str("\n");
96 suggested = true;
97 }
98 did_you_mean(&mut styled, styles, &fl!("clap-subcommand-context"), valid);
99 }
100 if let Some(valid) = error.get(ContextKind::SuggestedArg) {
101 styled.push_str("\n");
102 if !suggested {
103 styled.push_str("\n");
104 suggested = true;
105 }
106 did_you_mean(&mut styled, styles, &fl!("clap-argument-context"), valid);
107 }
108 if let Some(valid) = error.get(ContextKind::SuggestedValue) {
109 styled.push_str("\n");
110 if !suggested {
111 styled.push_str("\n");
112 suggested = true;
113 }
114 did_you_mean(&mut styled, styles, &fl!("clap-value-context"), valid);
115 }
116 let suggestions = error.get(ContextKind::Suggested);
117 if let Some(ContextValue::StyledStrs(suggestions)) = suggestions {
118 if !suggested {
119 styled.push_str("\n");
120 }
121 for suggestion in suggestions {
122 let _ = write!(
123 styled,
124 "\n{TAB}{valid}{}:{valid:#} ",
125 fl!("clap-tip-heading")
126 );
127 styled.push_str(&suggestion.ansi().to_string());
128 }
129 }
130
131 let usage = error.get(ContextKind::Usage);
132 if let Some(ContextValue::StyledStr(usage)) = usage {
133 put_usage(&mut styled, usage);
134 }
135
136 try_help(&mut styled, styles, Some("--help"));
137
138 styled
139 }
140}
141
142fn translation_errorkind(error: &clap::error::Error<ClapI18nRichFormatter>) -> String {
143 match error.kind() {
144 ErrorKind::InvalidValue => fl!("clap-errorkind-invalidvalue"),
145 ErrorKind::UnknownArgument => fl!("clap-errorkind-unknown-arg"),
146 ErrorKind::InvalidSubcommand => fl!("clap-errorkind-invalid-subcmd"),
147 ErrorKind::NoEquals => fl!("clap-errorkind-noeq"),
148 ErrorKind::ValueValidation => fl!("clap-errorkind-value-validation"),
149 ErrorKind::TooManyValues => fl!("clap-errorkind-too-many-values"),
150 ErrorKind::TooFewValues => fl!("clap-errorkind-too-few-values"),
151 ErrorKind::WrongNumberOfValues => fl!("clap-errorkind-wrong-number-of-values"),
152 ErrorKind::ArgumentConflict => fl!("clap-errorkind-arg-conflict"),
153 ErrorKind::MissingRequiredArgument => {
154 fl!("clap-errorkind-missing-required-arg")
155 }
156 ErrorKind::MissingSubcommand => fl!("clap-errorkind-missing-subcmd"),
157 ErrorKind::InvalidUtf8 => fl!("clap-errorkind-invalid-utf8"),
158 _ => unreachable!(),
159 }
160}
161
162fn start_error(styled: &mut StyledStr, styles: &Styles) {
163 use std::fmt::Write as _;
164 let error = &styles.get_error();
165 let _ = write!(styled, "{error}{}:{error:#} ", fl!("clap-error-heading"));
166}
167
168#[must_use]
169fn write_dynamic_context(
170 error: &clap::error::Error<ClapI18nRichFormatter>,
171 styled: &mut StyledStr,
172 styles: &Styles,
173) -> bool {
174 use std::fmt::Write as _;
175 let valid = styles.get_valid();
176 let invalid = styles.get_invalid();
177 let literal = styles.get_literal();
178
179 match error.kind() {
180 ErrorKind::ArgumentConflict => {
181 let mut prior_arg = error.get(ContextKind::PriorArg);
182 let mut arg_conflict = false;
183 let mut subcommand_conflict = false;
184 let mut arg = "".to_string();
185 if let Some(ContextValue::String(invalid_arg)) = error.get(ContextKind::InvalidArg) {
186 arg = format!("{invalid}{invalid_arg}{invalid:#}");
187 if Some(&ContextValue::String(invalid_arg.clone())) == prior_arg {
188 prior_arg = None;
189 let _ = write!(
190 styled,
191 "{}",
192 fl!("clap-dyn-errorkind-multipletimes", arg = arg.clone()),
193 );
194 } else {
195 arg_conflict = true;
196 }
197 } else if let Some(ContextValue::String(invalid_arg)) =
198 error.get(ContextKind::InvalidSubcommand)
199 {
200 arg = format!("{invalid}{invalid_arg}{invalid:#}");
201 subcommand_conflict = true;
202 } else {
203 styled.push_str(&translation_errorkind(error));
204 }
205
206 if let Some(prior_arg) = prior_arg {
207 match prior_arg {
208 ContextValue::Strings(values) => {
209 styled.push_str(":");
210 for v in values {
211 let _ = write!(styled, "\n{TAB}{invalid}{v}{invalid:#}",);
212 }
213 }
214 ContextValue::String(value) => {
215 let value = format!("{invalid}{value}{invalid:#}");
216
217 if arg_conflict {
218 let _ = write!(
219 styled,
220 "{}",
221 fl!("clap-dyn-errorkind-argconflict", arg = arg, arg2 = value)
222 );
223 } else if subcommand_conflict {
224 let _ = write!(
225 styled,
226 "{}",
227 fl!(
228 "clap-dyn-errorkind-subcmd-conflict",
229 arg = arg,
230 subcmd = value
231 )
232 );
233 }
234 }
235 _ => {
236 let _ = write!(styled, "{}", fl!("clap-dyn-errorkind-conflict-other"));
237 }
238 }
239 }
240
241 true
242 }
243 ErrorKind::NoEquals => {
244 let invalid_arg = error.get(ContextKind::InvalidArg);
245 if let Some(ContextValue::String(invalid_arg)) = invalid_arg {
246 let arg = format!("{invalid}{invalid_arg}{invalid:#}'");
247 let _ = write!(styled, "{}", fl!("clap-dyn-errorkind-no-eq", arg = arg));
248 true
249 } else {
250 false
251 }
252 }
253 ErrorKind::InvalidValue => {
254 let invalid_arg = error.get(ContextKind::InvalidArg);
255 let invalid_value = error.get(ContextKind::InvalidValue);
256 if let (
257 Some(ContextValue::String(invalid_arg)),
258 Some(ContextValue::String(invalid_value)),
259 ) = (invalid_arg, invalid_value)
260 {
261 let arg = format!("{invalid}{invalid_arg}{invalid:#}");
262 if invalid_value.is_empty() {
263 let _ = write!(
264 styled,
265 "{}",
266 fl!("clap-dyn-errorkind-required-arg-but-none", arg = arg),
267 );
268 } else {
269 let value = format!("{invalid}{invalid_value}{invalid:#}");
270 let _ = write!(
271 styled,
272 "{}",
273 fl!(
274 "clap-dyn-errorkind-value-validation",
275 invalid_value = value,
276 invalid_arg = arg
277 ),
278 );
279 }
280
281 let values = error.get(ContextKind::ValidValue);
282 write_values_list(
283 &fl!("clap-possible-value-context", multi = true.to_string()),
284 styled,
285 valid,
286 values,
287 );
288
289 true
290 } else {
291 false
292 }
293 }
294 ErrorKind::InvalidSubcommand => {
295 let invalid_sub = error.get(ContextKind::InvalidSubcommand);
296 if let Some(ContextValue::String(invalid_sub)) = invalid_sub {
297 let sub = format!("{invalid}{invalid_sub}{invalid:#}");
298 let _ = write!(
299 styled,
300 "{}",
301 fl!("clap-dyn-errorkind-unrecognized-subcmd", sub = sub),
302 );
303 true
304 } else {
305 false
306 }
307 }
308 ErrorKind::MissingRequiredArgument => {
309 let invalid_arg = error.get(ContextKind::InvalidArg);
310 if let Some(ContextValue::Strings(invalid_arg)) = invalid_arg {
311 let _ = write!(styled, "{}", fl!("clap-dyn-errorkind-not-provided"));
312 for v in invalid_arg {
313 let _ = write!(styled, "\n{TAB}{valid}{v}{valid:#}",);
314 }
315 true
316 } else {
317 false
318 }
319 }
320 ErrorKind::MissingSubcommand => {
321 let invalid_sub = error.get(ContextKind::InvalidSubcommand);
322 if let Some(ContextValue::String(invalid_sub)) = invalid_sub {
323 let sub = format!("{invalid}{invalid_sub}{invalid:#}");
324 let _ = write!(
325 styled,
326 "{}",
327 fl!("clap-dyn-errorkind-subcmd-not-provided", sub = sub),
328 );
329 let values = error.get(ContextKind::ValidSubcommand);
330 write_values_list(
331 &fl!("clap-subcommand-context", multi = true.to_string()),
332 styled,
333 valid,
334 values,
335 );
336
337 true
338 } else {
339 false
340 }
341 }
342 ErrorKind::InvalidUtf8 => false,
343 ErrorKind::TooManyValues => {
344 let invalid_arg = error.get(ContextKind::InvalidArg);
345 let invalid_value = error.get(ContextKind::InvalidValue);
346 if let (
347 Some(ContextValue::String(invalid_arg)),
348 Some(ContextValue::String(invalid_value)),
349 ) = (invalid_arg, invalid_value)
350 {
351 let value = format!("{invalid}{invalid_value}{invalid:#}");
352 let arg = format!("{literal}{invalid_arg}{literal:#}");
353 let _ = write!(
354 styled,
355 "{}",
356 fl!(
357 "clap-dyn-errorkind-too-many-values-no-more-expected",
358 value = value,
359 arg = arg
360 )
361 );
362 true
363 } else {
364 false
365 }
366 }
367 ErrorKind::TooFewValues => {
368 let invalid_arg = error.get(ContextKind::InvalidArg);
369 let actual_num_values = error.get(ContextKind::ActualNumValues);
370 let min_values = error.get(ContextKind::MinValues);
371 if let (
372 Some(ContextValue::String(invalid_arg)),
373 Some(ContextValue::Number(actual_num_values)),
374 Some(ContextValue::Number(min_values)),
375 ) = (invalid_arg, actual_num_values, min_values)
376 {
377 let min_values = format!("{valid}{min_values}{valid:#}");
378 let invalid_arg = format!("{literal}{invalid_arg}{literal:#}");
379 let actual_num_values_str = format!("{invalid}{actual_num_values}{invalid:#}");
380 let _ = write!(
381 styled,
382 "{}",
383 fl!(
384 "clap-dyn-errorkind-too-few-values",
385 min_values = min_values,
386 invalid_arg = invalid_arg,
387 actual_num_values = actual_num_values_str,
388 n = actual_num_values
389 )
390 );
391 true
392 } else {
393 false
394 }
395 }
396 ErrorKind::ValueValidation => {
397 let invalid_arg = error.get(ContextKind::InvalidArg);
398 let invalid_value = error.get(ContextKind::InvalidValue);
399 if let (
400 Some(ContextValue::String(invalid_arg)),
401 Some(ContextValue::String(invalid_value)),
402 ) = (invalid_arg, invalid_value)
403 {
404 let invalid_value = format!("{invalid}{invalid_value}{invalid:#}");
405 let invalid_arg = format!("{literal}{invalid_arg}{literal:#}");
406 let _ = write!(
407 styled,
408 "{}",
409 fl!(
410 "clap-dyn-errorkind-value-validation",
411 invalid_value = invalid_value,
412 invalid_arg = invalid_arg
413 )
414 );
415 if let Some(source) = error.source() {
416 let _ = write!(styled, ": {source}");
417 }
418 true
419 } else {
420 false
421 }
422 }
423 ErrorKind::WrongNumberOfValues => {
424 let invalid_arg = error.get(ContextKind::InvalidArg);
425 let actual_num_values = error.get(ContextKind::ActualNumValues);
426 let num_values = error.get(ContextKind::ExpectedNumValues);
427 if let (
428 Some(ContextValue::String(invalid_arg)),
429 Some(ContextValue::Number(actual_num_values)),
430 Some(ContextValue::Number(num_values)),
431 ) = (invalid_arg, actual_num_values, num_values)
432 {
433 let num_values = format!("{valid}{num_values}{valid:#}");
434 let invalid_arg = format!("{literal}{invalid_arg}{literal:#}");
435 let actual_num_values_str = format!("{invalid}{actual_num_values}{invalid:#}");
436
437 let _ = write!(
438 styled,
439 "{}",
440 fl!(
441 "clap-dyn-errorkind-wrong-number-of-values",
442 num_values = num_values,
443 invalid_arg = invalid_arg,
444 actual_num_values = actual_num_values_str,
445 n = actual_num_values
446 )
447 );
448 true
449 } else {
450 false
451 }
452 }
453 ErrorKind::UnknownArgument => {
454 let invalid_arg = error.get(ContextKind::InvalidArg);
455 if let Some(ContextValue::String(invalid_arg)) = invalid_arg {
456 let arg = format!("{invalid}{invalid_arg}{invalid:#}");
457 let _ = write!(
458 styled,
459 "{}",
460 fl!("clap-dyn-errorkind-unexpected-arg", arg = arg),
461 );
462 true
463 } else {
464 false
465 }
466 }
467 ErrorKind::DisplayHelp
468 | ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand
469 | ErrorKind::DisplayVersion
470 | ErrorKind::Io
471 | ErrorKind::Format => false,
472 _ => false,
473 }
474}
475
476fn write_values_list(
477 list_name: &str,
478 styled: &mut StyledStr,
479 valid: &Style,
480 possible_values: Option<&ContextValue>,
481) {
482 use std::fmt::Write as _;
483 if let Some(ContextValue::Strings(possible_values)) = possible_values
484 && !possible_values.is_empty()
485 {
486 let _ = write!(styled, "\n{TAB}[{list_name}: ");
487
488 for (idx, val) in possible_values.iter().enumerate() {
489 if idx > 0 {
490 styled.push_str(", ");
491 }
492 let _ = write!(styled, "{valid}{}{valid:#}", Escape(val));
493 }
494
495 styled.push_str("]");
496 }
497}
498
499fn put_usage(styled: &mut StyledStr, usage: &StyledStr) {
500 styled.push_str("\n\n");
501 styled.push_str(
502 &usage
503 .ansi()
504 .to_string()
505 .replacen("Usage:", &fl!("clap-usage-heading"), 1),
506 );
507}
508
509fn try_help(styled: &mut StyledStr, styles: &Styles, help: Option<&str>) {
510 if let Some(help) = help {
511 use std::fmt::Write as _;
512 let literal = &styles.get_literal();
513 let help = format!("{literal}{help}{literal:#}");
514 let _ = write!(styled, "\n\n{}\n", fl!("clap-help-tips", help = help));
515 } else {
516 styled.push_str("\n");
517 }
518}
519
520fn did_you_mean(styled: &mut StyledStr, styles: &Styles, context: &str, possibles: &ContextValue) {
521 use std::fmt::Write as _;
522
523 let valid = &styles.get_valid();
524 let _ = write!(styled, "{TAB}{valid}{}:{valid:#}", fl!("clap-tip-heading"));
525 if let ContextValue::String(possible) = possibles {
526 let possible = format!("{valid}{possible}{valid:#}");
527 let _ = write!(
528 styled,
529 " {}",
530 fl!(
531 "clap-similar-exists-single",
532 possible = possible,
533 context = context
534 )
535 );
536 } else if let ContextValue::Strings(possibles) = possibles {
537 if possibles.len() == 1 {
538 let possible = format!("{valid}{}{valid:#}", &possibles[0]);
539 let _ = write!(
540 styled,
541 " {}",
542 fl!(
543 "clap-similar-exists-single",
544 possible = possible,
545 context = context
546 )
547 );
548 } else {
549 let _ = write!(
550 styled,
551 " {} ",
552 fl!("clap-similar-exists-multi", context = context)
553 );
554
555 for (i, possible) in possibles.iter().enumerate() {
556 if i != 0 {
557 styled.push_str(", ");
558 }
559 let _ = write!(styled, "'{valid}{possible}{valid:#}'",);
560 }
561 }
562 }
563}
564
565struct Escape<'s>(&'s str);
566
567impl std::fmt::Display for Escape<'_> {
568 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
569 if self.0.contains(char::is_whitespace) {
570 std::fmt::Debug::fmt(self.0, f)
571 } else {
572 self.0.fmt(f)
573 }
574 }
575}