# sdl-keybridge
[](https://github.com/Le-Syl21/sdl-keybridge/actions/workflows/ci.yml)
[](https://crates.io/crates/sdl-keybridge)
[](https://docs.rs/sdl-keybridge)
[](#license)
**The universal Rosetta Stone for SDL keyboards.**
A static correspondence table that exposes, for every key press, all of
its parallel representations — physical scancode, logical keycode,
textual or symbolic glyph, localized label — across all the layout ×
platform combinations supported by SDL.
No other Rust crate combines **layout-awareness** (AZERTY/QWERTZ/JCUKEN),
**i18n of named keys** (Escape → Échap/Esc/エスケープ), and a
**cross-layout binding bridge** via scancode as a universal pivot.
## Philosophy: Rosette, not Champollion
This crate is a **static lookup table** (Rosette). It exposes parallel
data and individual labels. It **does not interpret** and **does not
package** presentation conventions.
Anything that *interprets* — formatting a `Ctrl+Shift+A` combo,
serializing a binding, rebasing a config from one layout to another — is
**Champollion territory**: the job of the consuming application. The
Rosette gives you the raw correspondence data; you pick the separator,
the ordering, the storage format.
## The four public functions
```rust
use sdl_keybridge::{
resolve, scancode_for, modifier_label, keycode_from_name,
KeyMod, Keycode, LabelStyle, Modifier, MultiLocalizer, Platform, Scancode,
};
let loc = MultiLocalizer::new();
// 1. Forward lookup — every parallel representation in one pass.
let r = resolve(
Scancode::A,
KeyMod::LSHIFT,
"linux/fr-t-k0-azerty", // layout id
"fr", // UI locale
LabelStyle::Textual,
&loc,
);
assert_eq!(r.character, Some('Q')); // Shift+A on AZERTY → 'Q'
// 2. Reverse lookup — find the scancode for a keycode in a layout.
let sc = scancode_for(Keycode::from('ф'), "linux/ru-t-k0-jcuken");
assert_eq!(sc, Some(Scancode::A));
// 3. Platform-aware modifier label.
let s = modifier_label(Modifier::Gui, Platform::Mac, "fr", LabelStyle::Textual, &loc);
assert_eq!(s.as_ref(), "Commande");
// 4. Parse a textual key name back into a keycode.
assert_eq!(keycode_from_name("Left Shift"), Some(Keycode::LSHIFT));
```
## Cross-layout binding bridge
Two lines of consumer code translate a binding from one layout to
another through scancode-as-pivot:
```rust
use sdl_keybridge::{resolve, scancode_for, KeyMod, Keycode, LabelStyle, MultiLocalizer};
# let keycode_ru = Keycode::from('ф');
let loc = MultiLocalizer::new();
let sc = scancode_for(keycode_ru, "windows/ru-t-k0-jcuken").unwrap();
let r = resolve(sc, KeyMod::NONE, "windows/fr-t-k0-azerty", "fr", LabelStyle::Textual, &loc);
// r.glyph_local is what the user sees on their French AZERTY keyboard.
```
## Combo formatting (consumer-side)
There is no `format_combo()` in this crate — the separator and ordering
are yours. Assemble the individual labels the way your UI expects:
```rust
use sdl_keybridge::{resolve, modifier_label, KeyMod, LabelStyle, MultiLocalizer, Modifier, Platform, Scancode};
# let loc = MultiLocalizer::new();
# let layout = "linux/fr-t-k0-azerty";
# let locale = "fr";
# let style = LabelStyle::Textual;
let ctrl = modifier_label(Modifier::Ctrl, Platform::Linux, locale, style, &loc);
let r = resolve(Scancode::A, KeyMod::LCTRL, layout, locale, style, &loc);
let combo = format!("{}+{}", ctrl, r.glyph_local); // "Ctrl+q" on AZERTY
```
## SDL2 + SDL3 compatibility
There are no `sdl2` / `sdl3` feature flags. The API takes primitive types
(`u32` for scancodes / keycodes, `u16` for the modifier bitmask), whose
numeric values are identical between SDL2 and SDL3 for every constant
exposed here. Use the same crate regardless of your SDL binding.
## Locales
26 locales available as individual Cargo features.
| `en` *(default)* | English | reference, 110 ids |
| `fr` | Français | full |
| `de` | Deutsch | full |
| `cs` | Čeština | near-full |
| `es` | Español | near-full |
| `fi` | Suomi | near-full |
| `it` | Italiano | near-full |
| `ja` | 日本語 | near-full |
| `ko` | 한국어 | near-full |
| `nl` | Nederlands | near-full |
| `pl` | Polski | near-full |
| `pt` | Português | near-full |
| `ru` | Русский | near-full |
| `sk` | Slovenčina | near-full |
| `sv` | Svenska | near-full |
| `tr` | Türkçe | near-full |
| `zh-hans` | 简体中文 | near-full |
| `zh-hant` | 繁體中文 | near-full |
| `ar` | العربية | partial (essentials; falls back to English) |
| `bn` | বাংলা | partial |
| `hi` | हिन्दी | partial |
| `id` | Bahasa Indonesia | partial |
| `sw` | Kiswahili | partial |
| `th` | ภาษาไทย | partial |
| `ur` | اردو | partial |
| `vi` | Tiếng Việt | partial |
Enable only what you need; use the aggregate `all-locales` feature to
pull them all in.
```toml
[dependencies]
sdl-keybridge = { version = "0.3", features = ["fr", "de", "ja"] }
```
### 🙏 Call for translators
The **partial** locales above only cover the most common keys — F-keys,
keypad labels, left/right modifier variants, and platform-aware
modifier names all fall back to English for these 8 languages. We kept
them partial rather than risk inaccurate translations in scripts we
cannot independently verify (Arabic, Devanagari, Bengali, Thai, etc.).
**If you are a native speaker**, opening a PR to complete any of these
locales is the single highest-leverage contribution you can make.
The recipe is in [CONTRIBUTING.md](CONTRIBUTING.md) — in short:
`cp src/locales/en.rs src/locales/<code>.rs`, translate the strings,
send a PR. Each locale is pure Rust (~120 lines, one `match` expression,
no TOML / no codegen).
The same offer stands for any locale not yet listed (Greek, Hebrew,
Icelandic, Kazakh, …). Adding a brand-new locale is ~5 lines of wiring
on top of the translated file.
## Layouts
v0.3 ships **601 layouts, all sourced from [Unicode CLDR 43](https://github.com/unicode-org/cldr/tree/release-43/keyboards)**
at build time — no hand-authored layout data in the crate:
| Android | 175 |
| ChromeOS | 55 |
| macOS | 137 |
| Windows | 209 |
| Platform-neutral (`und`) | 25 |
| **Total** | **601** |
Every glyph served by `resolve()` / `scancode_for()` comes from a
CLDR-maintained XML with the full chain of Unicode Consortium / vendor
review behind it. If you find a character that looks wrong, that's a
CLDR upstream issue — fix it there and refresh `data/cldr-43/` to get
the corrected version.
Layout ids are the exact CLDR locale tags prefixed by the platform dir:
- `windows/en-t-k0-windows` — US QWERTY, Windows
- `windows/en-GB-t-k0-windows` — UK QWERTY
- `windows/fr-t-k0-windows` — French AZERTY
- `windows/de-t-k0-windows` — German QWERTZ
- `windows/ru-t-k0-windows` — Russian ЙЦУКЕН
- `windows/ja-t-k0-windows` — Japanese JIS
- `osx/en-t-k0-osx-colemak` — Colemak (macOS)
- `windows/en-t-k0-windows-dvorak` — Dvorak (Windows)
- `android/*-t-k0-android*` — touch keyboards with long-press
ignored (we expose primary glyph)
- etc.
CLDR 44+ migrated to a new 3.0 format currently covering only ~8 niche
layouts; CLDR 43 remains the canonical legacy source until 3.0 catches
up.
Run `cargo run --example inspect --all-features -- ? scancode 0` for
the full list of layout ids in this build.
### Regenerating from fresh CLDR data
The build script reads `data/cldr-43/keyboards/**/*.xml`. To switch
source (e.g. drop in a newer CLDR release), replace that directory and
rebuild — the generated output regenerates automatically. Parser lives
in `build.rs`; ISO-to-scancode mapping is the `ISO_MAP` table at the
top of that file.
## What this crate will *not* do (non-goals)
- **Detect the current OS layout** — the caller provides the BCP 47 id.
No Rust solution reliably covers all five SDL platforms.
- **Dead keys / text composition** — a scancode + modifiers resolves to
*one* glyph. Composition (e.g. `^` + `e` → `ê`) is the OS/IME's job,
triggered by `SDL_StartTextInput`, not by us.
- **Package a `format_combo()`** — presentation conventions vary; the
consumer assembles the labels it receives.
- **Package a binding serializer** — config format is yours (JSON, INI,
RON, binary, …).
## License
Dual-licensed under either of
- Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
at your option.
### Contribution
Unless you explicitly state otherwise, any contribution intentionally
submitted for inclusion in the work by you, as defined in the
Apache-2.0 license, shall be dual licensed as above, without any
additional terms or conditions.