* 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>
175 lines
5.6 KiB
Python
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"
|