Files
kshitijk4poor 0df6c56546 fix(desktop): the language picker only offers languages it can show
The backend's `i18n.languages` lists every language it translates
anywhere. Seventeen are bundled for core messages and the TUI, but the
Desktop bundles only nine. The other eight (af, ga, hu, it, ko, pt, tr,
uk) were added to the Desktop picker anyway, with no Desktop strings
behind them. Picking one saved `display.language` and the UI stayed in
English: a Korean user picking 한국어 saw no change.

Each `i18n.languages` row now carries `surfaces`: the surfaces some
layer has strings for. These come from the bundled
`locales/<lang>[.<surface>].yaml`, the user overlay and plugin packs,
which already record their (lang, surface) pairs. A layer that loaded no
strings (an empty, comment-only or unparseable file, or an empty pack)
does not count.
`agent.i18n.language_surfaces()` builds the map from those records. The
overlay cache now stores the pairs instead of bare ids, and
`overlay_languages` derives from it.

The Desktop registers a backend language it does not bundle only when
`desktop` is among its surfaces. A language pack with a Desktop catalog
(hermes-lang-ko) and a user overlay `<lang>.desktop.yaml` both still
show up. Rows from older backends (no `surfaces`) are kept as before. A
saved `display.language` the Desktop cannot render is left as it is for
core and the TUI; the Desktop falls back to its OS-locale inference, as
it already does for any unrenderable value, and does not save over it.

Introduced in 9bcbe7b5df (#126296).
2026-11-01 18:12:56 +05:30

81 lines
3.2 KiB
Python

"""Language identity for the bundled locales — endonym and script direction — plus the picker list.
One Python table; ``apps/shared/src/i18n.ts`` ``LOCALE_ENDONYMS`` / ``RTL_LOCALES`` must agree for the
ids both sides bundle (the desktop lane's test checks the relationship). Pack-only languages take
their endonym/rtl from the pack registration (``PluginContext.register_locale(endonym=..., rtl=...)``)
and fall back to the bare id.
"""
from __future__ import annotations
from typing import Iterable, TypedDict
class LanguageOption(TypedDict):
id: str
endonym: str
rtl: bool
source: str
surfaces: list[str]
# id -> (endonym, rtl). Keep sorted by id; ``en`` is listed first by ``language_options`` regardless.
BUNDLED_LANGUAGE_INFO: dict[str, tuple[str, bool]] = {
"af": ("Afrikaans", False),
"ar": ("العربية", True),
"de": ("Deutsch", False),
"en": ("English", False),
"es": ("Español", False),
"fr": ("Français", False),
"ga": ("Gaeilge", False),
"hu": ("Magyar", False),
"it": ("Italiano", False),
"ja": ("日本語", False),
"ko": ("한국어", False),
"pt": ("Português", False),
"ru": ("Русский", False),
"tr": ("Türkçe", False),
"uk": ("Українська", False),
"zh": ("简体中文", False),
"zh-hant": ("繁體中文", False),
}
BUNDLED_SOURCE = "bundled"
OVERLAY_SOURCE = "overlay"
def describe_language(lang: str, *, pack: dict | None, overlay: bool, surfaces: Iterable[str]) -> LanguageOption:
"""One picker row. Precedence for endonym/rtl: bundled table → pack metadata → bare id / LTR.
``source`` names the highest layer that supplies the language (``plugin:<name>`` > ``overlay`` >
``bundled``) so a picker can say where a language came from. ``surfaces`` are the surfaces some
layer has strings for (``agent.i18n.language_surfaces``), so a renderer can leave out a language it
would only show in English."""
bundled = BUNDLED_LANGUAGE_INFO.get(lang)
endonym, rtl = bundled if bundled else ((pack or {}).get("endonym") or lang, bool((pack or {}).get("rtl")))
if pack is not None:
source = str(pack.get("source") or "plugin")
elif overlay and bundled is None:
source = OVERLAY_SOURCE
else:
source = BUNDLED_SOURCE
return {"id": lang, "endonym": endonym, "rtl": rtl, "source": source, "surfaces": list(surfaces)}
def language_options() -> list[LanguageOption]:
"""``[{"id", "endonym", "rtl", "source", "surfaces"}, ...]`` for every supported language, ``en`` first then sorted
by id — the list ``i18n.languages`` serves and every switcher renders (endonym only, no flags)."""
from agent import i18n, i18n_layers
from hermes_constants import get_hermes_home
home = get_hermes_home()
overlay_langs = i18n_layers.overlay_languages(home)
surfaces = i18n.language_surfaces(str(home))
return [
describe_language(lang, pack=i18n_layers.pack_info(lang), overlay=lang in overlay_langs,
surfaces=surfaces.get(lang, ()))
for lang in i18n.supported_languages()
]
__all__ = ["BUNDLED_LANGUAGE_INFO", "LanguageOption", "describe_language", "language_options"]