1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
use crate::snippet::Style;
use crate::{DiagnosticArg, DiagnosticMessage, FluentBundle};
use rustc_data_structures::sync::Lrc;
use rustc_error_messages::{
    fluent_bundle::resolver::errors::{ReferenceKind, ResolverError},
    FluentArgs, FluentError,
};
use std::borrow::Cow;

/// Convert diagnostic arguments (a rustc internal type that exists to implement
/// `Encodable`/`Decodable`) into `FluentArgs` which is necessary to perform translation.
///
/// Typically performed once for each diagnostic at the start of `emit_diagnostic` and then
/// passed around as a reference thereafter.
pub fn to_fluent_args<'iter, 'arg: 'iter>(
    iter: impl Iterator<Item = DiagnosticArg<'iter, 'arg>>,
) -> FluentArgs<'arg> {
    let mut args = if let Some(size) = iter.size_hint().1 {
        FluentArgs::with_capacity(size)
    } else {
        FluentArgs::new()
    };

    for (k, v) in iter {
        args.set(k.clone(), v.clone());
    }

    args
}

pub trait Translate {
    /// Return `FluentBundle` with localized diagnostics for the locale requested by the user. If no
    /// language was requested by the user then this will be `None` and `fallback_fluent_bundle`
    /// should be used.
    fn fluent_bundle(&self) -> Option<&Lrc<FluentBundle>>;

    /// Return `FluentBundle` with localized diagnostics for the default locale of the compiler.
    /// Used when the user has not requested a specific language or when a localized diagnostic is
    /// unavailable for the requested locale.
    fn fallback_fluent_bundle(&self) -> &FluentBundle;

    /// Convert `DiagnosticMessage`s to a string, performing translation if necessary.
    fn translate_messages(
        &self,
        messages: &[(DiagnosticMessage, Style)],
        args: &FluentArgs<'_>,
    ) -> Cow<'_, str> {
        Cow::Owned(
            messages.iter().map(|(m, _)| self.translate_message(m, args)).collect::<String>(),
        )
    }

    /// Convert a `DiagnosticMessage` to a string, performing translation if necessary.
    fn translate_message<'a>(
        &'a self,
        message: &'a DiagnosticMessage,
        args: &'a FluentArgs<'_>,
    ) -> Cow<'_, str> {
        trace!(?message, ?args);
        let (identifier, attr) = match message {
            DiagnosticMessage::Str(msg) | DiagnosticMessage::Eager(msg) => {
                return Cow::Borrowed(msg);
            }
            DiagnosticMessage::FluentIdentifier(identifier, attr) => (identifier, attr),
        };

        let translate_with_bundle = |bundle: &'a FluentBundle| -> Option<(Cow<'_, str>, Vec<_>)> {
            let message = bundle.get_message(identifier)?;
            let value = match attr {
                Some(attr) => message.get_attribute(attr)?.value(),
                None => message.value()?,
            };
            debug!(?message, ?value);

            let mut errs = vec![];
            let translated = bundle.format_pattern(value, Some(args), &mut errs);
            debug!(?translated, ?errs);
            Some((translated, errs))
        };

        self.fluent_bundle()
            .and_then(|bundle| translate_with_bundle(bundle))
            // If `translate_with_bundle` returns `None` with the primary bundle, this is likely
            // just that the primary bundle doesn't contain the message being translated, so
            // proceed to the fallback bundle.
            //
            // However, when errors are produced from translation, then that means the translation
            // is broken (e.g. `{$foo}` exists in a translation but `foo` isn't provided).
            //
            // In debug builds, assert so that compiler devs can spot the broken translation and
            // fix it..
            .inspect(|(_, errs)| {
                debug_assert!(
                    errs.is_empty(),
                    "identifier: {:?}, attr: {:?}, args: {:?}, errors: {:?}",
                    identifier,
                    attr,
                    args,
                    errs
                );
            })
            // ..otherwise, for end users, an error about this wouldn't be useful or actionable, so
            // just hide it and try with the fallback bundle.
            .filter(|(_, errs)| errs.is_empty())
            .or_else(|| translate_with_bundle(self.fallback_fluent_bundle()))
            .map(|(translated, errs)| {
                // Always bail out for errors with the fallback bundle.

                let mut help_messages = vec![];

                if !errs.is_empty() {
                    for error in &errs {
                        match error {
                            FluentError::ResolverError(ResolverError::Reference(
                                ReferenceKind::Message { id, .. },
                            )) if args.iter().any(|(arg_id, _)| arg_id == id) => {
                                help_messages.push(format!("Argument `{id}` exists but was not referenced correctly. Try using `{{${id}}}` instead"));
                            }
                            _ => {}
                        }
                    }

                    panic!(
                        "Encountered errors while formatting message for `{identifier}`\n\
                        help: {}\n\
                        attr: `{attr:?}`\n\
                        args: `{args:?}`\n\
                        errors: `{errs:?}`",
                        help_messages.join("\nhelp: ")
                    );
                }

                translated
            })
            .expect("failed to find message in primary or fallback fluent bundles")
    }
}