whitaker_common/i18n/helpers.rs
1//! Helper functions for localization workflows.
2//!
3//! This module provides high-level conveniences for constructing localizers,
4//! rendering locale-aware phrases, and safely resolving diagnostic message sets
5//! with fallback support.
6
7use std::env;
8
9use log::debug;
10
11use super::{
12 Arguments, DiagnosticMessageSet, Localizer, MessageKey, resolve_localizer, resolve_message_set,
13};
14
15/// Construct a [`Localizer`] for `lint_name` using workspace configuration.
16///
17/// The helper inspects `DYLINT_LOCALE` and the optional configuration locale,
18/// logging the chosen source before returning the resolved [`Localizer`].
19///
20/// # Examples
21///
22/// ```
23/// use whitaker_common::i18n::get_localizer_for_lint;
24///
25/// let localizer = get_localizer_for_lint("demo-lint", None);
26/// assert_eq!(localizer.locale(), "en-GB");
27/// ```
28#[must_use]
29pub fn get_localizer_for_lint(lint_name: &str, configuration_locale: Option<&str>) -> Localizer {
30 let environment_locale =
31 env::var_os("DYLINT_LOCALE").and_then(|value| value.into_string().ok());
32 let selection = resolve_localizer(None, environment_locale, configuration_locale);
33
34 selection.log_outcome(lint_name);
35 selection.into_localizer()
36}
37
38/// Render a locale-aware description of the number of predicate branches.
39///
40/// Welsh and Scottish Gaelic require bespoke noun mutations, so the helper
41/// centralizes those rules for reuse across diagnostics, UI tests, and
42/// localization suites.
43///
44/// # Examples
45///
46/// ```
47/// use whitaker_common::i18n::branch_phrase;
48///
49/// assert_eq!(branch_phrase("en-GB", 2), "2 branches");
50/// assert_eq!(branch_phrase("cy", 3), "tair cangen");
51/// assert_eq!(branch_phrase("gd", 1), "1 meur");
52/// ```
53#[must_use]
54pub fn branch_phrase(locale: &str, branches: usize) -> String {
55 match locale
56 .split_once('-')
57 .map(|(lang, _)| lang)
58 .unwrap_or(locale)
59 {
60 "cy" => welsh_branch_phrase(branches),
61 "gd" => gaelic_branch_phrase(branches),
62 _ => english_branch_phrase(branches),
63 }
64}
65
66fn english_branch_phrase(branches: usize) -> String {
67 match branches {
68 1 => String::from("1 branch"),
69 _ => format!("{branches} branches"),
70 }
71}
72
73fn gaelic_branch_phrase(branches: usize) -> String {
74 match branches {
75 1 => String::from("1 meur"),
76 _ => format!("{branches} meuran"),
77 }
78}
79
80fn welsh_branch_phrase(branches: usize) -> String {
81 match branches {
82 0 => String::from("dim canghennau"),
83 1 => String::from("un gangen"),
84 2 => String::from("dwy gangen"),
85 3 => String::from("tair cangen"),
86 6 => String::from("chwe changen"),
87 4 | 5 => format!("{branches} cangen"),
88 _ => format!("{branches} canghennau"),
89 }
90}
91
92/// Report localization failures by discarding the message.
93///
94/// Use this helper with [`safe_resolve_message_set`] when a lint only needs
95/// deterministic fallback strings and does not want to surface missing
96/// localization details as bug reports.
97///
98/// # Examples
99///
100/// ```
101/// use whitaker_common::i18n::{
102/// Arguments, DiagnosticMessageSet, Localizer, MessageKey, MessageResolution,
103/// noop_reporter, safe_resolve_message_set,
104/// };
105///
106/// let localizer = Localizer::new(Some("en-GB"));
107/// let args: Arguments<'static> = Arguments::default();
108/// let resolution = MessageResolution {
109/// lint_name: "demo-lint",
110/// key: MessageKey::new("missing-key"),
111/// args: &args,
112/// };
113/// let fallback = DiagnosticMessageSet::new(
114/// "Fallback primary".into(),
115/// "Fallback note".into(),
116/// "Fallback help".into(),
117/// );
118///
119/// let messages = safe_resolve_message_set(&localizer, resolution, noop_reporter, || fallback);
120/// assert_eq!(messages.primary(), "Fallback primary");
121/// ```
122pub fn noop_reporter(_message: String) {}
123
124/// Resolve a diagnostic message set while logging localization failures.
125///
126/// When lookups fail the helper invokes the supplied bug reporter, records the
127/// failure in the lint's debug log, and returns deterministic fallback
128/// messages.
129///
130/// # Examples
131///
132/// ```
133/// use whitaker_common::i18n::testing::RecordingEmitter;
134/// use whitaker_common::i18n::{
135/// Arguments, DiagnosticMessageSet, Localizer, MessageKey, MessageResolution,
136/// safe_resolve_message_set,
137/// };
138/// use fluent_templates::fluent_bundle::FluentValue;
139/// use std::borrow::Cow;
140///
141/// let mut args: Arguments<'static> = Arguments::default();
142/// args.insert(Cow::Borrowed("subject"), FluentValue::from("demo"));
143///
144/// let resolution = MessageResolution {
145/// lint_name: "demo-lint",
146/// key: MessageKey::new("missing-key"),
147/// args: &args,
148/// };
149/// let fallback = DiagnosticMessageSet::new(
150/// "Fallback primary".into(),
151/// "Fallback note".into(),
152/// "Fallback help".into(),
153/// );
154/// let localizer = Localizer::new(Some("en-GB"));
155/// let emitter = RecordingEmitter::default();
156///
157/// let messages = safe_resolve_message_set(
158/// &localizer,
159/// resolution,
160/// |message| emitter.record(message),
161/// || fallback.clone(),
162/// );
163///
164/// assert_eq!(messages.primary(), "Fallback primary");
165/// let recorded = emitter.recorded_messages();
166/// assert!(recorded[0].contains("missing-key"));
167/// ```
168#[must_use]
169pub fn safe_resolve_message_set(
170 localizer: &Localizer,
171 resolution: MessageResolution<'_>,
172 report_bug: impl FnOnce(String),
173 fallback: impl FnOnce() -> DiagnosticMessageSet,
174) -> DiagnosticMessageSet {
175 match resolve_message_set(localizer, resolution.key, resolution.args) {
176 Ok(messages) => messages.strip_isolating_marks(),
177 Err(error) => {
178 debug!(
179 target: resolution.lint_name,
180 "localization error for key `{}` in locale `{}`: {error}; using fallback",
181 resolution.key,
182 localizer.locale(),
183 );
184
185 report_bug(format!(
186 "Localization error for `{}` key `{}` in locale `{}`: {error}",
187 resolution.lint_name,
188 resolution.key,
189 localizer.locale(),
190 ));
191
192 fallback().strip_isolating_marks()
193 }
194 }
195}
196
197/// Parameters supplied to [`safe_resolve_message_set`].
198#[derive(Clone, Copy)]
199pub struct MessageResolution<'a> {
200 /// Target lint identifier used for logging and error context.
201 pub lint_name: &'a str,
202 /// Fluent message key describing the diagnostic entry point.
203 pub key: MessageKey<'a>,
204 /// Fluent argument map supplied to the lookup.
205 pub args: &'a Arguments<'a>,
206}
207
208#[cfg(test)]
209mod tests {
210 use super::branch_phrase;
211 use crate::i18n::FALLBACK_LOCALE;
212
213 #[test]
214 fn renders_english_branch_phrase() {
215 assert_eq!(branch_phrase(FALLBACK_LOCALE, 0), "0 branches");
216 assert_eq!(branch_phrase(FALLBACK_LOCALE, 1), "1 branch");
217 assert_eq!(branch_phrase(FALLBACK_LOCALE, 4), "4 branches");
218 }
219
220 #[test]
221 fn renders_gaelic_branch_phrase() {
222 assert_eq!(branch_phrase("gd", 1), "1 meur");
223 assert_eq!(branch_phrase("gd", 2), "2 meuran");
224 assert_eq!(branch_phrase("gd", 3), "3 meuran");
225 }
226
227 #[test]
228 fn renders_welsh_branch_phrase() {
229 assert_eq!(branch_phrase("cy", 0), "dim canghennau");
230 assert_eq!(branch_phrase("cy", 1), "un gangen");
231 assert_eq!(branch_phrase("cy", 2), "dwy gangen");
232 assert_eq!(branch_phrase("cy", 3), "tair cangen");
233 assert_eq!(branch_phrase("cy", 6), "chwe changen");
234 assert_eq!(branch_phrase("cy", 11), "11 canghennau");
235 }
236}