Files
voculinux/tests/test_system_language.py
T
688d9e9bd2 feat(language): start a first run from the language the system points at (#796)
* feat(language): start a first run from the language the system points at

Nothing asked the system what language the user speaks, so whisper.cpp always
began on auto-detect and VOSK on a hardcoded en-us. Both are wrong for anyone
who does not speak English, and neither improves on its own because nothing
ever suggests setting a language.

Seed the language on first run only, so a saved choice — including a deliberate
"auto" — is never touched. The keyboard layout is consulted before the locale:
running the desktop in English while speaking something else is common among
the people who install a Linux dictation tool, which makes the interface
language a weak signal for the spoken one. A "us" layout is the default on many
installs and says little, so the locale gets a say before it is accepted.

localectl is used rather than setxkbmap, which reports nothing useful on
Wayland. Detection failures leave the packaged default in place.

Closes #777

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TKVa2qTLQYMCHffjp97yqT

* fix(language): prefer LANGUAGE, keep cs/ca, map nb_NO

LANG was consulted before LANGUAGE, so a gettext preference list lost to
the fallback locale. C/POSIX detection used startswith("c"), which
rejected Czech and Catalan. nb_NO normalised to nb_no and missed the
nb→no catalogue map, so Norwegian fell through to en-us.

Tests cover LANG+LANGUAGE, LANGUAGE=cs, and nb_NO.

* fix(language): walk LANGUAGE prefs until one is supported

split(":")[0] dropped later supported preferences, so first-run could
stay on auto when LANGUAGE started with an unsupported tag. Try each
preference through the catalogue map. Annotate _seed_language_from_system.

* style(language): keep _language_from_locale_env on one line for black

* fix(language): fall through unmapped LANGUAGE to LANG

When every LANGUAGE preference missed the catalogue, the helper returned
before LANG was examined, so first-run could stay on auto. Skip unmapped
env vars and keep looking. Regression: LANGUAGE=xx:yy with LANG=pl_PL → pl.

---------

Co-authored-by: Kacper Paczos <23406555+kacperpaczos@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-09-07 14:11:40 -07:00

175 lines
5.6 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"),
("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"),
("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"