Skip to main content

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}