clap_i18n_richformatter/
lib.rs

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// Re-export the derive macro
22#[cfg(feature = "derive")]
23pub use clap_i18n_derive::clap_i18n;
24
25// Hidden module for internal use by derive macro (not shown in code completion)
26#[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    // Windows Terminal doesn't support bidirectional (BiDi) text, and renders the isolate characters incorrectly.
42    // This is a temporary workaround for https://github.com/microsoft/terminal/issues/16574
43    // TODO: this might break BiDi text, though we don't support any writing system depends on that.
44    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    /// `clap::Command::try_get_matches` i18n version
54    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    /// `clap::Command::get_matches` i18n version
61    fn get_matches_i18n(self) -> ArgMatches {
62        self.try_get_matches_i18n().map_err(|e| e.exit()).unwrap()
63    }
64}
65
66/// Richly formatted error context
67///
68/// This follows the [rustc diagnostic style guide](https://rustc-dev-guide.rust-lang.org/diagnostics.html#suggestion-style-guide).
69#[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}