# keyboards reference

Build and edit a drawn keyboard without HTML inputs or a system keyboard. Import from `@controller-plus/sdk/keyboards` or `/sdk/src/keyboards.mjs`. Most games should use the [keyboard surface part](custom-controllers.md#keyboard); these functions support custom renderers.

## Exports

| Full signature | Parameters, defaults and return |
| --- | --- |
| `LAYOUTS` | Frozen array of 65 frozen metadata records `{ id, name, nativeName, script, direction, needsCandidates? }`. `direction` is `ltr` or `rtl`; Chinese sets `needsCandidates: true`. No row data here. |
| `layoutFor(language): string` | Resolve a BCP 47-style tag to a layout ID; case-insensitive, underscores accepted. Non-string, malformed or unsupported scripts fall back to `en`. |
| `keyRows(layoutId = 'en', { charset = 'text', shift = false, page = 'letters' } = {})` | Return `{ id, direction, rows: Key[][], pages: string[] }`. `charset`: `letters`, `text`, `digits`. Page names remain `letters`/`symbols`, even for vowel/kana pages. |
| `applyKey(state, key, { maxLength = 48 } = {}): TextState` | Return a copied serializable editor state. Enforce a grapheme limit, compose letters and update shift state. Does not mutate input state. |
| `textOf(state): string` | Committed `text` plus any active Hangul syllable. Missing state yields `''`. |

`Key` is `{ kind: 'char', text, alt?: string[] }` or `{ kind: 'shift' | 'delete' | 'enter' | 'page' | 'language' | 'space', width?: number }`. `alt` contains alternate characters for a renderer to expose. `width` is a relative key-width hint. The functions do not draw UI or emit events. They have no deliberate error class; malformed argument shapes can produce normal JavaScript errors.

`TextState` is `{ text: string, composing?: { shift?: true, hangul?: { initial?: string, medial?: string, final?: string } } }`. Store and restore it as JSON. Read `textOf(state)` for the visible string rather than only `state.text`.

## Language selection

Exact IDs win, then script-aware language matching. Examples: `tr-TR → tr`, `sr-Latn → sr-Latn`, `sr-Cyrl → sr`, `uz → uz-Latn`, `zh-Hant → zh`. Unsupported requested scripts use English. Aliases: `nb`/`nn → no`, `fil → tl`, `iw → he`, `in → id`, `sh → sr-Latn` without a script override. Extensions/private-use subtags do not override a script.

| Script/group | Layout IDs and languages |
| --- | --- |
| Latin | `en` English; `tr` Turkish; `de` German; `fr` French; `es` Spanish; `pt` Portuguese; `it` Italian; `nl` Dutch; `sv` Swedish; `no` Norwegian; `da` Danish; `fi` Finnish; `pl` Polish; `cs` Czech; `sk` Slovak; `hu` Hungarian; `ro` Romanian; `hr` Croatian; `sl` Slovenian; `sr-Latn` Serbian Latin; `lt` Lithuanian; `lv` Latvian; `et` Estonian; `is` Icelandic; `ga` Irish; `cy` Welsh; `mt` Maltese; `vi` Vietnamese; `id` Indonesian; `ms` Malay; `tl` Filipino; `sw` Swahili; `ha` Hausa; `yo` Yoruba; `az` Azerbaijani; `uz-Latn` Uzbek Latin. |
| Cyrillic | `ru` Russian; `uk` Ukrainian; `be` Belarusian; `bg` Bulgarian; `sr` Serbian; `mk` Macedonian; `kk` Kazakh; `ky` Kyrgyz; `mn` Mongolian. |
| Greek, Armenian, Georgian | `el` Greek; `hy` Armenian; `ka` Georgian. |
| Right to left | `he` Hebrew; `ar` Arabic; `fa` Persian; `ur` Urdu. |
| Indic | `hi` Hindi; `mr` Marathi; `bn` Bengali; `ta` Tamil; `te` Telugu; `gu` Gujarati; `pa` Punjabi Gurmukhi; `kn` Kannada; `ml` Malayalam. |
| Other | `th` Thai; `ko` Korean; `ja` Japanese kana; `zh` Chinese pinyin letters. |

## Pages, case and composition

Digits are a 3×4 phone-style pad: 1–9, delete, 0, enter. Text mode adds space, punctuation and a symbols page. Letters mode omits general punctuation, but retains Indic vowel pages and Japanese katakana. Page and language changes are caller-owned: `applyKey` commits composition but does not choose a new page/layout.

Shift lasts until a letter is accepted. `keyRows(..., { shift: true })` supplies locale-aware casing and alternatives, including Turkish dotted/dotless I and German ẞ. Read `state.composing?.shift` when redrawing.

Hangul composes syllables and compound vowels/finals. Delete removes one component while a syllable is active, then whole graphemes from committed text. Kana supports hiragana, katakana, voicing and small kana, without kanji conversion. Indic dependent signs attach only to an appropriate base and script; orphan marks are ignored. Chinese provides pinyin letters and tone alternatives, **not** a Hanzi candidate engine. Do not promise general system-IME support.

`enter`, `page` and `language` commit composition and clear shift. Enter adds no newline. Space adds a space. `applyKey` defaults to 48 graphemes; invalid nonnumeric limits fall back to 48, negative limits become zero. Surface keyboards separately clamp their own limits to 64 or 24 slots. Without `Intl.Segmenter`, editing counts/removes Unicode code points rather than complete graphemes.

The browser surface renderer chooses among the game's language, the player's language and English. Its current touch UI does not expose every alternate returned by `keyRows().rows[].alt`; availability in the data API is not a guarantee of a long-press menu on every client.

## Editing example

<!-- example: keyboards -->
```js
import { LAYOUTS, layoutFor, keyRows, applyKey, textOf } from '@controller-plus/sdk/keyboards';
let state = { text: '' };
for (const text of ['ㅎ', 'ㅏ', 'ㄴ']) state = applyKey(state, { kind: 'char', text });
console.log(textOf(state)); // 한
state = applyKey(state, { kind: 'enter' });
console.log(JSON.stringify(state)); // {"text":"한"}
const layout = layoutFor('tr-TR');
const board = keyRows(layout, { charset: 'text', shift: true });
console.log(board.id, board.pages, board.rows.length, LAYOUTS.length);
state = applyKey(state, { kind: 'delete' }, { maxLength: 24 });
console.log(textOf(state)); // empty
```
