Skip to main content

sdl_keybridge/
localizer.rs

1//! Localization trait and UI-style enums.
2//!
3//! The core crate ships the [`KeyLocalizer`] trait and a [`MultiLocalizer`]
4//! that aggregates the locale modules enabled via Cargo features. Each
5//! locale module implements the trait and is wired in through the
6//! internal `locales` module.
7
8use std::borrow::Cow;
9
10use crate::keymod::KeyMod;
11
12/// How a key should be rendered for display.
13///
14/// - `Textual`  — words like `"Up"`, `"Haut"`, `"Command"`.
15/// - `Symbolic` — single-glyph icons like `↑`, `⌘`, `⇧`.
16///
17/// Consumers pick the style that fits their UI.
18#[derive(Copy, Clone, Eq, PartialEq, Debug, Default)]
19pub enum LabelStyle {
20    #[default]
21    Textual,
22    Symbolic,
23}
24
25/// Host operating system — required to pick the correct glyph and label
26/// for modifier keys (e.g. `LGUI` → `⌘` on macOS, `⊞` on Windows,
27/// `◇` / `Super` on Linux).
28#[derive(Copy, Clone, Eq, PartialEq, Debug)]
29pub enum Platform {
30    Mac,
31    Windows,
32    Linux,
33    ChromeOS,
34    Android,
35}
36
37impl Platform {
38    /// Short id suffix used in modifier `key_id`s — e.g.
39    /// `"mod_gui_mac"` maps to `Platform::Mac`.
40    pub const fn id(self) -> &'static str {
41        match self {
42            Platform::Mac => "mac",
43            Platform::Windows => "win",
44            Platform::Linux => "linux",
45            Platform::ChromeOS => "chromeos",
46            Platform::Android => "android",
47        }
48    }
49}
50
51/// Logical modifier — platform-agnostic. The mapping to physical
52/// `LCTRL` / `RCTRL` / `LGUI` / `LALT` / `MODE` / `LEVEL5` bits is
53/// done inside [`resolve`](crate::resolve()) and
54/// [`modifier_label`](crate::modifier_label).
55#[derive(Copy, Clone, Eq, PartialEq, Debug)]
56pub enum Modifier {
57    Ctrl,
58    Shift,
59    Alt,
60    Gui,
61    AltGr,
62}
63
64impl Modifier {
65    /// Stable id prefix used in localizer lookups — e.g. `"mod_ctrl"`.
66    pub const fn key_id_prefix(self) -> &'static str {
67        match self {
68            Modifier::Ctrl => "mod_ctrl",
69            Modifier::Shift => "mod_shift",
70            Modifier::Alt => "mod_alt",
71            Modifier::Gui => "mod_gui",
72            Modifier::AltGr => "mod_altgr",
73        }
74    }
75
76    /// The `KeyMod` bitmask that represents *either side* of this modifier.
77    pub const fn mask(self) -> KeyMod {
78        match self {
79            Modifier::Ctrl => KeyMod::CTRL,
80            Modifier::Shift => KeyMod::SHIFT,
81            Modifier::Alt => KeyMod::ALT,
82            Modifier::Gui => KeyMod::GUI,
83            Modifier::AltGr => KeyMod::new(KeyMod::MODE.raw() | KeyMod::RALT.raw()),
84        }
85    }
86}
87
88/// The crate's single extension point — translate stable ids
89/// (`"key_escape"`, `"mod_gui_mac"`, …) into display strings.
90///
91/// # Stable id contract
92///
93/// The id passed to [`translate`](Self::translate) is one of:
94///
95/// - `NamedKey::key_id()` — for non-printable keys, see
96///   [`crate::NamedKey`] for the full list (`"key_escape"`,
97///   `"key_arrow_up"`, `"key_kp_7"`, …).
98/// - `"mod_<name>_<platform>"` — for held-modifier labels emitted by
99///   [`crate::modifier_label`]: `"mod_ctrl"` / `"mod_shift"` /
100///   `"mod_alt"` / `"mod_gui"` / `"mod_altgr"`, optionally suffixed
101///   with `_mac` / `_win` / `_linux` / `_chromeos` / `_android`.
102///
103/// New ids may be added in future versions; existing ids will never be
104/// renamed.
105///
106/// # Style awareness
107///
108/// Translations may differ based on [`LabelStyle`] — e.g. `Symbolic`
109/// returns `↑` while `Textual` returns `"Haut"` (French). When your
110/// translation is the same for both styles, return it for both.
111///
112/// # Fallback chain
113///
114/// A localizer answering for only a subset of ids (or only one locale)
115/// must return `None` for everything else. The crate's runtime walks
116/// this chain on every public-API call:
117///
118/// 1. Your custom localizer for `(key_id, locale, style)`.
119/// 2. The compiled-in locale module for `locale`.
120/// 3. The English (`"en"`) module.
121/// 4. The raw id as a last resort.
122///
123/// # Implementing a custom localizer
124///
125/// Most callers use [`MultiLocalizer`] which already aggregates every
126/// locale shipped via Cargo features. Implement your own only to
127/// override or extend specific labels.
128///
129/// ```
130/// use std::borrow::Cow;
131/// use sdl_keybridge::{KeyLocalizer, LabelStyle, MultiLocalizer};
132///
133/// /// Renames every macOS GUI label to "⌘ Cmd" for our brand UI.
134/// /// Falls through (returns `None`) for everything else so the runtime
135/// /// chain still serves the rest from MultiLocalizer.
136/// struct BrandedLocalizer;
137///
138/// impl KeyLocalizer for BrandedLocalizer {
139///     fn translate(
140///         &self,
141///         key_id: &str,
142///         _locale: &str,
143///         _style: LabelStyle,
144///     ) -> Option<Cow<'static, str>> {
145///         match key_id {
146///             "mod_gui_mac" => Some(Cow::Borrowed("⌘ Cmd")),
147///             _ => None,
148///         }
149///     }
150/// }
151///
152/// // Pass it where you'd normally pass MultiLocalizer.
153/// let _ = BrandedLocalizer;
154/// let _fallback = MultiLocalizer::new();
155/// ```
156///
157/// # Implementing a single-locale localizer
158///
159/// To add a brand-new locale (rather than override one), follow the
160/// `CONTRIBUTING.md` recipe: copy `src/locales/en.rs` and wire it up
161/// behind a Cargo feature. That gets the locale into [`MultiLocalizer`]
162/// for free, which is almost always what users want.
163///
164/// You can also implement [`KeyLocalizer`] yourself for an unsupported
165/// locale — return `Some(_)` only when `locale` matches your code:
166///
167/// ```
168/// use std::borrow::Cow;
169/// use sdl_keybridge::{KeyLocalizer, LabelStyle};
170///
171/// struct EsperantoLocalizer;
172///
173/// impl KeyLocalizer for EsperantoLocalizer {
174///     fn translate(&self, key_id: &str, locale: &str, style: LabelStyle)
175///         -> Option<Cow<'static, str>>
176///     {
177///         if locale != "eo" { return None; }
178///         use LabelStyle::*;
179///         let s = match (key_id, style) {
180///             ("key_escape", Textual)    => "Eskapi",
181///             ("key_return", Textual)    => "Enen",
182///             ("key_space",  Textual)    => "Spaco",
183///             _ => return None,
184///         };
185///         Some(Cow::Borrowed(s))
186///     }
187/// }
188/// ```
189pub trait KeyLocalizer {
190    /// Translate a stable id into a display string, or `None` if this
191    /// localizer has no opinion on `(key_id, locale, style)`.
192    ///
193    /// Returning `None` lets the runtime fallback chain take over —
194    /// see the trait-level docs for the chain order.
195    fn translate(&self, key_id: &str, locale: &str, style: LabelStyle)
196        -> Option<Cow<'static, str>>;
197}
198
199/// Aggregates all locale modules compiled into the crate and dispatches
200/// calls by locale code. Use this as the default `KeyLocalizer` when you
201/// don't need custom overrides.
202#[derive(Copy, Clone, Debug, Default)]
203pub struct MultiLocalizer {
204    _private: (),
205}
206
207impl MultiLocalizer {
208    pub const fn new() -> Self {
209        Self { _private: () }
210    }
211}
212
213impl KeyLocalizer for MultiLocalizer {
214    fn translate(
215        &self,
216        key_id: &str,
217        locale: &str,
218        style: LabelStyle,
219    ) -> Option<Cow<'static, str>> {
220        crate::locales::translate(locale, key_id, style)
221    }
222}
223
224/// Helper used by the public API: try the user-supplied localizer first,
225/// then fall back to the compiled locale set, finally to English.
226pub(crate) fn translate_for(
227    key_id: &str,
228    locale: &str,
229    style: LabelStyle,
230    localizer: &impl KeyLocalizer,
231) -> Option<Cow<'static, str>> {
232    if let Some(s) = localizer.translate(key_id, locale, style) {
233        return Some(s);
234    }
235    if let Some(s) = crate::locales::translate(locale, key_id, style) {
236        return Some(s);
237    }
238    crate::locales::translate("en", key_id, style)
239}