The stock Belarusian layout does not load. On a be subtype the keyboard shows "VIEW LAYOUT ERROR DETAILS" instead of keys, and logcat reports:
E KeyboardLayoutSet: Failed to load element KeyboardLayoutElement(kind=Alphabet0, page=Base)
for keyboard layout set belarusian. Message: Illegal index=294 for name=morekeys_code_u0435 locale=be
at org.futo.inputmethod.keyboard.internal.KeyboardTextsTable.getText(KeyboardTextsTable.java:72)
at org.futo.inputmethod.keyboard.internal.KeyboardTextsSet.expandReference(KeyboardTextsSet.java:162)
at org.futo.inputmethod.v2keyboard.MoreKeysBuilder.insertMoreKeys(MoreKeysBuilder.kt:104)
at org.futo.inputmethod.v2keyboard.BaseKey.computeData(BaseKey.kt:461)
at org.futo.inputmethod.v2keyboard.LayoutEngine.build(LayoutEngine.kt:784)
Same for page=Shifted. There is no keyboard at all, only the error panel over a SWITCH LANGUAGE button.
To reproduce: build at b345f519e, add беларуская (Беларусь) (the be_BY subtype), switch to it.
Why
b345f519e "Treat Russian ie as a language long-press key" (2026-08-24, three commits past the 0.1.30 tag, so this is unreleased) renames one key in ru.json:
- "cyrillic_ie": [
+ "code_u0435": [
"ё"
],
Seven locale files define that entry. The rename touched one of them:
| file |
before |
after |
DEFAULT.json |
cyrillic_ie: [] |
unchanged |
be.json, kk.json, ky.json |
cyrillic_ie: ["ё"] |
unchanged |
mk.json, sr.json |
cyrillic_ie: ["ѐ"] |
unchanged |
ru.json |
cyrillic_ie: ["ё"] |
code_u0435: ["ё"] |
MoreKeysBuilder builds the name from the key's code point, so for е it asks for morekeys_code_u0435:
LongPressKey.LanguageKeys -> "!text/morekeys_$codeCharOrUnicode"
LanguageKeys is in the shipped default long press order, so every layout with a plain Cyrillic е on a letter row asks for it.
That name is now defined by ru.json alone. Two things follow.
The crash. generate.py orders NAMES by how many locales define each entry:
names_and_uses.sort(key=lambda x: -x[1])
A name defined by one locale sorts last, so morekeys_code_u0435 is index 294 of 295. DEFAULT.json defines only the old name, not this one, and transform_texts_dict_to_array ends with return texts[:last_idx], dropping trailing Nones. TEXTS_DEFAULT is therefore 294 long and stops one short of the index that is now needed. In getText the per-locale lookup is out of range, the index >= 0 && index < TEXTS_DEFAULT.length guard is 294 < 294, and it throws.
Before the rename the name was absent from sNameToIndexesMap entirely, so it took the escape hatch a few lines earlier and returned "" harmlessly:
if (indexObj == null) {
if (name.startsWith("actions_") || name.startsWith("qwertysyms_")
|| name.startsWith("morekeys_code_u") || name.startsWith("morekeys_misc_code_u")) {
return "";
}
Adding the name to NAMES is what routed it past that hatch and into the throw.
Two names for one key. The old name is not orphaned. KeySpecShortcuts.kt:81 maps е to listOf("е", "morekeys_cyrillic_ie"), and Cyrillic/mongolian.yaml:9 is ['э', '!text/morekeys_cyrillic_ie'], so be, kk and ky still resolve ё and mk and sr still resolve ѐ through that route. What the rename produced is two names for the same key, one of them reachable only on ru, which is what makes the choice of fix below matter.
Scope
Any Cyrillic layout with an е key, on any locale whose table is shorter than 295, which is every locale but ru. It is the key's code that selects the name, not its spelling in the YAML, so decoration does not exempt a layout.
All 28 stock layouts under assets/layouts/Cyrillic/ have an е key.
Registration comes from mapping.yaml (LayoutManager.kt:53), not from a layout's own languages: field, which is read only by KeyboardLayoutPreview.kt and DevLayoutList.kt. Of the 28, four are offered on ru: east_slavic, russian_student, russian_yavert and russian_yazhert. Those four work there and throw on the other six locales they register for (ba, be_BY, kk, ky, tg, uk). The remaining 24 are offered on no Russian locale at all, so they fail everywhere they are offered.
I reproduced the Belarusian one.
Fix
Add the new name to DEFAULT.json beside the old one:
"cyrillic_ie": [],
"code_u0435": [],
That is enough. Two locales defining the name instead of one moves it out of the
tail, and DEFAULT.json covering it stops the array being truncated short of it.
Every locale but ru then falls back to the empty entry instead of throwing. I
ran the generator both ways to check.
The tempting alternative, finishing the rename in the other six files, is worse.
morekeys_cyrillic_ie is still live in two places: KeySpecShortcuts.kt:81
maps е to listOf("е", "morekeys_cyrillic_ie"), and
Cyrillic/mongolian.yaml:9 is ['э', '!text/morekeys_cyrillic_ie']. I ran
both candidates through generate.py and resolved the two names the way
getText does:
|
morekeys_code_u0435 |
morekeys_cyrillic_ie |
| today |
ru gets ё, every other locale throws |
be/kk/ky ё, mk/sr ѐ |
add to DEFAULT.json |
ru gets ё, the rest get "" |
unchanged |
| finish the rename |
works for all seven |
"" everywhere, with a logged warning |
So the rename would silently empty the shortcut path that actually supplies ё
on be, kk and ky today, and Mongolian's э with it. The
KeySpecShortcuts route runs through getDefaultMoreKeysForKey whatever the
user's long press order is, where LanguageKeys can be removed, so it is the
one worth keeping intact.
Two hardening changes are worth making separately, since the same shape recurs
the next time a name ends up defined by a single locale:
- Stop truncating in
transform_texts_dict_to_array, so TEXTS_DEFAULT is
always NAMES.length long.
- Have
getText return "" rather than throwing when the index is past both
tables. Not null: expandReference does sb.append(getText(name)), and
StringBuilder.append((String) null) writes the literal "null", which would
parse into a long press key labeled null that types null. "" is what the
escape hatch above already returns.
The KDoc on Keyboard.languages still says the layout "will be displayed as an option for the specified languages", which reads as registration and is stale. The same sentence is in LayoutSpec.md.
Found while testing a Cyrillic layout of my own against be and uk.
The stock Belarusian layout does not load. On a
besubtype the keyboard shows "VIEW LAYOUT ERROR DETAILS" instead of keys, and logcat reports:Same for
page=Shifted. There is no keyboard at all, only the error panel over a SWITCH LANGUAGE button.To reproduce: build at
b345f519e, add беларуская (Беларусь) (thebe_BYsubtype), switch to it.Why
b345f519e"Treat Russian ie as a language long-press key" (2026-08-24, three commits past the 0.1.30 tag, so this is unreleased) renames one key inru.json:Seven locale files define that entry. The rename touched one of them:
DEFAULT.jsoncyrillic_ie: []be.json,kk.json,ky.jsoncyrillic_ie: ["ё"]mk.json,sr.jsoncyrillic_ie: ["ѐ"]ru.jsoncyrillic_ie: ["ё"]code_u0435: ["ё"]MoreKeysBuilderbuilds the name from the key's code point, so forеit asks formorekeys_code_u0435:LanguageKeysis in the shipped default long press order, so every layout with a plain Cyrillicеon a letter row asks for it.That name is now defined by
ru.jsonalone. Two things follow.The crash.
generate.pyordersNAMESby how many locales define each entry:A name defined by one locale sorts last, so
morekeys_code_u0435is index 294 of 295.DEFAULT.jsondefines only the old name, not this one, andtransform_texts_dict_to_arrayends withreturn texts[:last_idx], dropping trailingNones.TEXTS_DEFAULTis therefore 294 long and stops one short of the index that is now needed. IngetTextthe per-locale lookup is out of range, theindex >= 0 && index < TEXTS_DEFAULT.lengthguard is294 < 294, and it throws.Before the rename the name was absent from
sNameToIndexesMapentirely, so it took the escape hatch a few lines earlier and returned""harmlessly:Adding the name to
NAMESis what routed it past that hatch and into the throw.Two names for one key. The old name is not orphaned.
KeySpecShortcuts.kt:81mapsеtolistOf("е", "morekeys_cyrillic_ie"), andCyrillic/mongolian.yaml:9is['э', '!text/morekeys_cyrillic_ie'], sobe,kkandkystill resolveёandmkandsrstill resolveѐthrough that route. What the rename produced is two names for the same key, one of them reachable only onru, which is what makes the choice of fix below matter.Scope
Any Cyrillic layout with an
еkey, on any locale whose table is shorter than 295, which is every locale butru. It is the key's code that selects the name, not its spelling in the YAML, so decoration does not exempt a layout.All 28 stock layouts under
assets/layouts/Cyrillic/have anеkey.Registration comes from
mapping.yaml(LayoutManager.kt:53), not from a layout's ownlanguages:field, which is read only byKeyboardLayoutPreview.ktandDevLayoutList.kt. Of the 28, four are offered onru:east_slavic,russian_student,russian_yavertandrussian_yazhert. Those four work there and throw on the other six locales they register for (ba,be_BY,kk,ky,tg,uk). The remaining 24 are offered on no Russian locale at all, so they fail everywhere they are offered.I reproduced the Belarusian one.
Fix
Add the new name to
DEFAULT.jsonbeside the old one:That is enough. Two locales defining the name instead of one moves it out of the
tail, and
DEFAULT.jsoncovering it stops the array being truncated short of it.Every locale but
ruthen falls back to the empty entry instead of throwing. Iran the generator both ways to check.
The tempting alternative, finishing the rename in the other six files, is worse.
morekeys_cyrillic_ieis still live in two places:KeySpecShortcuts.kt:81maps
еtolistOf("е", "morekeys_cyrillic_ie"), andCyrillic/mongolian.yaml:9is['э', '!text/morekeys_cyrillic_ie']. I ranboth candidates through
generate.pyand resolved the two names the waygetTextdoes:morekeys_code_u0435morekeys_cyrillic_ierugetsё, every other locale throwsbe/kk/kyё,mk/srѐDEFAULT.jsonrugetsё, the rest get""""everywhere, with a logged warningSo the rename would silently empty the shortcut path that actually supplies
ёon
be,kkandkytoday, and Mongolian'sэwith it. TheKeySpecShortcutsroute runs throughgetDefaultMoreKeysForKeywhatever theuser's long press order is, where
LanguageKeyscan be removed, so it is theone worth keeping intact.
Two hardening changes are worth making separately, since the same shape recurs
the next time a name ends up defined by a single locale:
transform_texts_dict_to_array, soTEXTS_DEFAULTisalways
NAMES.lengthlong.getTextreturn""rather than throwing when the index is past bothtables. Not
null:expandReferencedoessb.append(getText(name)), andStringBuilder.append((String) null)writes the literal"null", which wouldparse into a long press key labeled
nullthat typesnull.""is what theescape hatch above already returns.
The KDoc on
Keyboard.languagesstill says the layout "will be displayed as an option for the specified languages", which reads as registration and is stale. The same sentence is inLayoutSpec.md.Found while testing a Cyrillic layout of my own against
beanduk.