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}