Skip to content

Cyrillic layouts fail to load on every locale but ru: b345f519e renamed one of seven locale files #2268

Description

@AndrewKirkovski

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions