Files
voculinux/tests/test_system_language.py
Jatin K MalikandGitHub 3abc106fdb feat(languages): add Slovenian (sl) to the supported languages (#995)
Slovenian was missing from SUPPORTED_LANGUAGES, so the Settings picker did not offer it and normalize_language_shortcuts dropped any shortcut bound to it. Add it as a Whisper-only entry (whisper code sl; alphacephei publishes no Slovenian VOSK model), map the Slovenian XKB layout si to it for follow-keyboard-layout, and list it on the website languages page.

Fixes #970
2026-10-20 23:29:47 -07:00

360 lines
13 KiB
Python

"""First run should start from the language the system points at (#777)."""
from unittest.mock import patch
import pytest
from vocalinux.ui import config_manager as cm
from vocalinux.utils import system_language as sl
from vocalinux.utils.vosk_model_info import SUPPORTED_LANGUAGES
@pytest.fixture
def isolated_config(tmp_path, monkeypatch):
monkeypatch.setattr(cm, "CONFIG_DIR", str(tmp_path))
monkeypatch.setattr(cm, "CONFIG_FILE", str(tmp_path / "config.json"))
monkeypatch.setattr(cm, "_shared_instance", None)
return tmp_path
@pytest.mark.parametrize(
"layout,expected",
[
("pl", "pl"),
("de", "de"),
("cz", "cs"),
("se", "sv"),
("si", "sl"),
("sk", None),
("us", "en-us"),
("gb", "en-us"),
("xyz", None),
],
)
def test_layouts_map_onto_catalogue_entries(layout, expected):
assert sl._language_for_layout(layout, SUPPORTED_LANGUAGES) == expected
@pytest.mark.parametrize(
"value,expected",
[
("pl_PL.UTF-8", "pl"),
("de_DE@euro", "de"),
("en_US.UTF-8", "en-us"),
("en_GB", "en-us"),
("en_IN", "en-in"),
("cs", "cs"),
("cs_CZ.UTF-8", "cs"),
("ca", "ca"),
("ca_ES.UTF-8", "ca"),
("nb_NO", "no"),
("nb_NO.UTF-8", "no"),
("nn_NO", "no"),
("sl_SI.UTF-8", "sl"),
("sk_SK.UTF-8", None),
("C", None),
("POSIX", None),
("C.UTF-8", None),
("xx_YY", None),
],
)
def test_locales_map_onto_catalogue_entries(value, expected):
assert sl._language_for_locale(value, SUPPORTED_LANGUAGES) == expected
def test_a_non_english_layout_beats_an_english_locale():
"""The reported case: desktop in English, keyboard in Polish, speaks Polish."""
with patch.object(sl, "detect_keyboard_layout", return_value="pl"):
detected = sl.detect_system_language(SUPPORTED_LANGUAGES, {"LANG": "en_US.UTF-8"})
assert detected == "pl"
def test_a_us_layout_defers_to_the_locale():
""" "us" is the default layout on many installs, so it carries little signal."""
with patch.object(sl, "detect_keyboard_layout", return_value="us"):
detected = sl.detect_system_language(SUPPORTED_LANGUAGES, {"LANG": "pl_PL.UTF-8"})
assert detected == "pl"
def test_a_us_layout_is_used_when_the_locale_says_nothing():
with patch.object(sl, "detect_keyboard_layout", return_value="us"):
detected = sl.detect_system_language(SUPPORTED_LANGUAGES, {"LANG": "C"})
assert detected == "en-us"
def test_nothing_decisive_returns_none():
with patch.object(sl, "detect_keyboard_layout", return_value=None):
assert sl.detect_system_language(SUPPORTED_LANGUAGES, {}) is None
def test_a_missing_localectl_does_not_raise():
with patch.object(sl.subprocess, "run", side_effect=FileNotFoundError):
assert sl.detect_keyboard_layout() is None
def test_first_run_starts_from_the_detected_language(isolated_config):
"""Without this the fresh config keeps the packaged "auto"."""
with patch.object(sl, "detect_keyboard_layout", return_value="pl"):
with patch.dict("os.environ", {"LANG": "en_US.UTF-8"}, clear=False):
manager = cm.ConfigManager()
assert manager.get("speech_recognition", "language") == "pl"
def test_first_run_keeps_the_default_when_detection_finds_nothing(isolated_config):
with patch.object(sl, "detect_keyboard_layout", return_value=None):
with patch.dict("os.environ", {"LANG": "C", "LC_ALL": "C"}, clear=True):
manager = cm.ConfigManager()
assert manager.get("speech_recognition", "language") == "auto"
def test_a_saved_language_is_never_overwritten(isolated_config):
"""A returning user who chose auto-detect keeps it."""
import json
with open(cm.CONFIG_FILE, "w") as handle:
json.dump({"speech_recognition": {"language": "auto"}}, handle)
with patch.object(sl, "detect_keyboard_layout", return_value="pl"):
manager = cm.ConfigManager()
assert manager.get("speech_recognition", "language") == "auto"
def test_language_env_beats_lang_when_both_set():
"""LANGUAGE is the gettext preference list; LANG must not mask it (#796)."""
with patch.object(sl, "detect_keyboard_layout", return_value="us"):
detected = sl.detect_system_language(
SUPPORTED_LANGUAGES,
{"LANG": "en_US.UTF-8", "LANGUAGE": "pl:en"},
)
assert detected == "pl"
def test_language_env_skips_unsupported_preferences():
"""An unsupported first LANGUAGE pref must not hide a later supported one."""
with patch.object(sl, "detect_keyboard_layout", return_value="us"):
detected = sl.detect_system_language(
SUPPORTED_LANGUAGES,
{"LANG": "en_US.UTF-8", "LANGUAGE": "xx:pl:en"},
)
assert detected == "pl"
def test_unmapped_language_env_falls_through_to_lang():
"""An unsupported LANGUAGE list must not hide LANG (#796)."""
with patch.object(sl, "detect_keyboard_layout", return_value="us"):
detected = sl.detect_system_language(
SUPPORTED_LANGUAGES,
{"LANGUAGE": "xx:yy", "LANG": "pl_PL.UTF-8"},
)
assert detected == "pl"
def test_language_env_czech_is_not_treated_as_c():
with patch.object(sl, "detect_keyboard_layout", return_value="us"):
detected = sl.detect_system_language(
SUPPORTED_LANGUAGES,
{"LANG": "en_US.UTF-8", "LANGUAGE": "cs"},
)
assert detected == "cs"
def test_nb_no_locale_maps_to_norwegian():
with patch.object(sl, "detect_keyboard_layout", return_value="us"):
detected = sl.detect_system_language(
SUPPORTED_LANGUAGES,
{"LANG": "nb_NO.UTF-8"},
)
assert detected == "no"
# --- Follow the active keyboard layout (#821) -------------------------------
@pytest.mark.parametrize(
"sources,current,expected",
[
# The bilingual case from the issue: same config, different active index.
("[('xkb', 'us'), ('xkb', 'fr')]", "uint32 0", "us"),
("[('xkb', 'us'), ('xkb', 'fr')]", "uint32 1", "fr"),
# A variant is still that layout.
("[('xkb', 'fr+oss')]", "uint32 0", "fr"),
# "uint32 1" must not read as index 32 via a bare \d+ search.
("[('xkb', 'us'), ('xkb', 'pl')]", "uint32 1", "pl"),
# A stale index outlives a removed source; GNOME falls back to the first.
("[('xkb', 'us'), ('xkb', 'fr')]", "uint32 9", "us"),
("[('xkb', 'us')]", "", "us"),
# An IBus engine is not an xkb layout and maps to nothing here.
("[('ibus', 'mozc-jp')]", "uint32 0", None),
("@a(ss) []", "uint32 0", None),
("", "uint32 0", None),
],
)
def test_active_source_parsing(sources, current, expected):
assert sl._parse_active_source(sources, current) == expected
def test_active_layout_reads_gsettings():
with patch.object(sl, "_gsettings_get", side_effect=["[('xkb', 'fr')]", "uint32 0"]):
assert sl.detect_active_keyboard_layout() == "fr"
def test_active_layout_is_none_without_gsettings():
with patch.object(sl, "_gsettings_get", return_value=None):
assert sl.detect_active_keyboard_layout() is None
def test_active_layout_language_maps_onto_catalogue():
with patch.object(sl, "_active_input_source", return_value=("xkb", "fr")):
assert sl.language_for_active_layout(SUPPORTED_LANGUAGES) == "fr"
def test_active_layout_falls_back_to_configured_layout():
"""Outside GNOME there is no readable active source; the configured one remains."""
with patch.object(sl, "_active_input_source", return_value=None):
with patch.object(sl, "detect_keyboard_layout", return_value="pl"):
assert sl.language_for_active_layout(SUPPORTED_LANGUAGES) == "pl"
def test_active_layout_language_is_none_when_unmappable():
with patch.object(sl, "_active_input_source", return_value=("xkb", "xyz")):
assert sl.language_for_active_layout(SUPPORTED_LANGUAGES) is None
def test_active_layout_language_is_none_without_any_layout():
with patch.object(sl, "_active_input_source", return_value=None):
with patch.object(sl, "detect_keyboard_layout", return_value=None):
assert sl.language_for_active_layout(SUPPORTED_LANGUAGES) is None
def test_an_active_ibus_engine_does_not_borrow_the_configured_layout():
"""Typing Japanese through mozc-jp must not resolve to the us layout's English."""
with patch.object(sl, "_active_input_source", return_value=("ibus", "mozc-jp")):
with patch.object(sl, "detect_keyboard_layout", return_value="us") as configured:
assert sl.language_for_active_layout(SUPPORTED_LANGUAGES) == "ja"
configured.assert_not_called()
def test_an_unmapped_ibus_engine_does_not_borrow_the_configured_layout():
"""An unknown IBus engine is still a real answer; leave it to auto-detect."""
with patch.object(sl, "_active_input_source", return_value=("ibus", "foo-engine")):
with patch.object(sl, "detect_keyboard_layout", return_value="us") as configured:
assert sl.language_for_active_layout(SUPPORTED_LANGUAGES) is None
configured.assert_not_called()
@pytest.mark.parametrize("engine_id", ["foo-us", "foo-gb", "foo-fr", "foo-de"])
def test_hyphen_suffix_does_not_false_map_layout_tokens(engine_id):
"""rsplit last token is not a language code: mozc-us is Japanese, not xkb us."""
with patch.object(sl, "_active_input_source", return_value=("ibus", engine_id)):
with patch.object(sl, "detect_keyboard_layout", return_value="us") as configured:
assert sl.language_for_active_layout(SUPPORTED_LANGUAGES) is None
configured.assert_not_called()
def test_ibus_mozc_us_maps_to_japanese():
"""Mozc's documented US-layout engine is still a Japanese IME."""
with patch.object(sl, "_active_input_source", return_value=("ibus", "mozc-us")):
with patch.object(sl, "detect_keyboard_layout", return_value="us") as configured:
assert sl.language_for_active_layout(SUPPORTED_LANGUAGES) == "ja"
configured.assert_not_called()
def test_ibus_hyphen_jp_and_kr_suffixes_still_map():
with patch.object(sl, "_active_input_source", return_value=("ibus", "foo-jp")):
assert sl.language_for_active_layout(SUPPORTED_LANGUAGES) == "ja"
with patch.object(sl, "_active_input_source", return_value=("ibus", "foo-kr")):
assert sl.language_for_active_layout(SUPPORTED_LANGUAGES) == "ko"
def test_ibus_m17n_hi_itrans_maps_to_hindi():
with patch.object(sl, "_active_input_source", return_value=("ibus", "m17n:hi:itrans")):
assert sl.language_for_active_layout(SUPPORTED_LANGUAGES) == "hi"
def test_ibus_libpinyin_maps_to_chinese():
with patch.object(sl, "_active_input_source", return_value=("ibus", "libpinyin")):
assert sl.language_for_active_layout(SUPPORTED_LANGUAGES) == "zh"
def test_active_source_prefers_mru_and_falls_back_to_the_index():
values = {
"mru-sources": "[('ibus', 'mozc-jp'), ('xkb', 'us')]",
"sources": "[('xkb', 'us')]",
"current": "uint32 0",
}
with patch.object(sl, "_gsettings_get", side_effect=lambda key: values[key]):
# The MRU answer wins even though it is unmappable.
assert tuple(sl._active_input_source()) == ("ibus", "mozc-jp")
values["mru-sources"] = "@a(ss) []"
with patch.object(sl, "_gsettings_get", side_effect=lambda key: values[key]):
assert tuple(sl._active_input_source()) == ("xkb", "us")
def test_no_active_source_without_gsettings():
with patch.object(sl, "_gsettings_get", return_value=None):
assert sl._active_input_source() is None
def test_active_layout_prefers_mru_over_stale_current_index():
"""GNOME's "current" index goes stale; the MRU list is what tracks a switch.
Observed live: after switching to the second layout, mru-sources led with
('xkb', 'fr') while current still read 0, which points at 'us' (#497, #738).
"""
values = {
"mru-sources": "[('xkb', 'fr'), ('xkb', 'us')]",
"sources": "[('xkb', 'us'), ('xkb', 'fr')]",
"current": "uint32 0",
}
with patch.object(sl, "_gsettings_get", side_effect=lambda key: values[key]):
assert sl.detect_active_keyboard_layout() == "fr"
def test_active_layout_falls_back_to_index_when_mru_is_empty():
"""A session that has never switched has no MRU list to go on."""
values = {
"mru-sources": "@a(ss) []",
"sources": "[('xkb', 'us'), ('xkb', 'pl')]",
"current": "uint32 1",
}
with patch.object(sl, "_gsettings_get", side_effect=lambda key: values[key]):
assert sl.detect_active_keyboard_layout() == "pl"
def test_active_layout_reports_nothing_for_an_ibus_mru_entry():
"""An IBus engine at the front of the MRU list must not fall through."""
values = {
"mru-sources": "[('ibus', 'mozc-jp'), ('xkb', 'us')]",
"sources": "[('xkb', 'us')]",
"current": "uint32 0",
}
with patch.object(sl, "_gsettings_get", side_effect=lambda key: values[key]):
assert sl.detect_active_keyboard_layout() is None
@pytest.mark.parametrize(
"value,expected",
[
("[('xkb', 'us'), ('xkb', 'fr')]", [("xkb", "us"), ("xkb", "fr")]),
("@a(ss) [('xkb', 'us')]", [("xkb", "us")]),
("@a(ss) []", []),
("not a list", []),
("", []),
(None, []),
],
)
def test_source_list_parsing(value, expected):
assert sl._parse_sources(value) == expected