Skip to main content

whitaker_common/i18n/
diagnostics.rs

1//! Resolve localized diagnostic strings (primary/note/help) from Fluent bundles.
2//!
3//! # Examples
4//!
5//! ```
6//! # use whitaker_common::i18n::{
7//! #     Arguments, Localizer, MessageKey, resolve_message_set,
8//! # };
9//! # fn demo(localizer: &Localizer) -> Result<(), whitaker_common::i18n::I18nError> {
10//! #     let args = Arguments::default();
11//! let messages = resolve_message_set(
12//!     localizer,
13//!     MessageKey::new("my-lint.message"),
14//!     &args,
15//! )?;
16//! #     assert!(!messages.primary().is_empty());
17//! #     Ok(())
18//! # }
19//! ```
20
21use super::{Arguments, I18nError, Localizer};
22use std::fmt;
23
24/// Identifier for a Fluent message within a localization bundle.
25#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
26pub struct MessageKey<'a>(&'a str);
27
28impl<'a> MessageKey<'a> {
29    /// Construct a new message key wrapper.
30    #[must_use]
31    pub const fn new(value: &'a str) -> Self {
32        Self(value)
33    }
34}
35
36impl<'a> AsRef<str> for MessageKey<'a> {
37    fn as_ref(&self) -> &str {
38        self.0
39    }
40}
41
42impl fmt::Display for MessageKey<'_> {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        f.write_str(self.0)
45    }
46}
47
48/// Identifier for a Fluent attribute attached to a message.
49#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
50pub struct AttrKey<'a>(&'a str);
51
52impl<'a> AttrKey<'a> {
53    /// Construct a new attribute key wrapper.
54    #[must_use]
55    pub const fn new(value: &'a str) -> Self {
56        Self(value)
57    }
58}
59
60impl<'a> AsRef<str> for AttrKey<'a> {
61    fn as_ref(&self) -> &str {
62        self.0
63    }
64}
65
66impl fmt::Display for AttrKey<'_> {
67    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68        f.write_str(self.0)
69    }
70}
71
72/// Lookup trait used by lint crates to resolve translated diagnostic strings.
73pub trait BundleLookup {
74    /// Resolve the primary message for `key` using `args`.
75    fn message(&self, key: MessageKey<'_>, args: &Arguments<'_>) -> Result<String, I18nError>;
76
77    /// Resolve an attribute message for `key.attribute` using `args`.
78    fn attribute(
79        &self,
80        key: MessageKey<'_>,
81        attribute: AttrKey<'_>,
82        args: &Arguments<'_>,
83    ) -> Result<String, I18nError>;
84}
85
86impl BundleLookup for Localizer {
87    fn message(&self, key: MessageKey<'_>, args: &Arguments<'_>) -> Result<String, I18nError> {
88        self.message_with_args(key.as_ref(), args)
89    }
90
91    fn attribute(
92        &self,
93        key: MessageKey<'_>,
94        attribute: AttrKey<'_>,
95        args: &Arguments<'_>,
96    ) -> Result<String, I18nError> {
97        self.attribute_with_args(key.as_ref(), attribute.as_ref(), args)
98    }
99}
100
101/// Container holding the standard trio of lint diagnostic messages.
102#[derive(Clone, Debug, PartialEq, Eq)]
103pub struct DiagnosticMessageSet {
104    primary: String,
105    note: String,
106    help: String,
107}
108
109impl DiagnosticMessageSet {
110    /// Construct a new set of lint diagnostic strings.
111    #[must_use]
112    ///
113    /// # Examples
114    ///
115    /// ```
116    /// # use whitaker_common::i18n::DiagnosticMessageSet;
117    /// let messages = DiagnosticMessageSet::new(
118    ///     "primary".into(),
119    ///     "note".into(),
120    ///     "help".into(),
121    /// );
122    /// assert_eq!(messages.primary(), "primary");
123    /// ```
124    pub fn new(primary: String, note: String, help: String) -> Self {
125        Self {
126            primary,
127            note,
128            help,
129        }
130    }
131
132    /// Access the primary lint diagnostic.
133    #[must_use]
134    ///
135    /// # Examples
136    ///
137    /// ```
138    /// # use whitaker_common::i18n::DiagnosticMessageSet;
139    /// # let messages = DiagnosticMessageSet::new(
140    /// #     "primary".into(),
141    /// #     "note".into(),
142    /// #     "help".into(),
143    /// # );
144    /// assert_eq!(messages.primary(), "primary");
145    /// ```
146    pub fn primary(&self) -> &str {
147        &self.primary
148    }
149
150    /// Access the note attached to the diagnostic.
151    #[must_use]
152    ///
153    /// # Examples
154    ///
155    /// ```
156    /// # use whitaker_common::i18n::DiagnosticMessageSet;
157    /// # let messages = DiagnosticMessageSet::new(
158    /// #     "primary".into(),
159    /// #     "note".into(),
160    /// #     "help".into(),
161    /// # );
162    /// assert_eq!(messages.note(), "note");
163    /// ```
164    pub fn note(&self) -> &str {
165        &self.note
166    }
167
168    /// Access the help text attached to the diagnostic.
169    #[must_use]
170    ///
171    /// # Examples
172    ///
173    /// ```
174    /// # use whitaker_common::i18n::DiagnosticMessageSet;
175    /// # let messages = DiagnosticMessageSet::new(
176    /// #     "primary".into(),
177    /// #     "note".into(),
178    /// #     "help".into(),
179    /// # );
180    /// assert_eq!(messages.help(), "help");
181    /// ```
182    pub fn help(&self) -> &str {
183        &self.help
184    }
185
186    /// Remove Unicode isolating marks inserted by Fluent placeholders.
187    pub(crate) fn strip_isolating_marks(self) -> Self {
188        fn strip(mut text: String) -> String {
189            const ISOLATING_MARKS: [char; 2] = ['\u{2068}', '\u{2069}'];
190            text.retain(|ch| !ISOLATING_MARKS.contains(&ch));
191            text
192        }
193
194        Self {
195            primary: strip(self.primary),
196            note: strip(self.note),
197            help: strip(self.help),
198        }
199    }
200}
201
202/// Resolve the primary, note, and help messages for a lint diagnostic.
203const NOTE_ATTR: AttrKey<'static> = AttrKey::new("note");
204const HELP_ATTR: AttrKey<'static> = AttrKey::new("help");
205
206#[must_use = "Use the resolved localization messages when emitting diagnostics"]
207pub fn resolve_message_set(
208    lookup: &impl BundleLookup,
209    key: MessageKey<'_>,
210    args: &Arguments<'_>,
211) -> Result<DiagnosticMessageSet, I18nError> {
212    let primary = lookup.message(key, args)?;
213    let note = lookup.attribute(key, NOTE_ATTR, args)?;
214    let help = lookup.attribute(key, HELP_ATTR, args)?;
215
216    Ok(DiagnosticMessageSet::new(primary, note, help))
217}