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}