diff --git a/src/vocalinux/utils/system_language.py b/src/vocalinux/utils/system_language.py index b1e15dc..de7a12a 100644 --- a/src/vocalinux/utils/system_language.py +++ b/src/vocalinux/utils/system_language.py @@ -29,6 +29,7 @@ _LAYOUT_TO_LANGUAGE = { "gb": "en-us", "cz": "cs", "se": "sv", + "si": "sl", "dk": "da", "gr": "el", "ua": "uk", diff --git a/src/vocalinux/utils/vosk_model_info.py b/src/vocalinux/utils/vosk_model_info.py index 02766b0..305f4e1 100644 --- a/src/vocalinux/utils/vosk_model_info.py +++ b/src/vocalinux/utils/vosk_model_info.py @@ -153,6 +153,11 @@ SUPPORTED_LANGUAGES = { "whisper": "ru", "vosk": "vosk-model-small-ru-0.22", }, + "sl": { + "name": "Slovenian", + "whisper": "sl", + "vosk": None, + }, "es": { "name": "Spanish", "whisper": "es", diff --git a/tests/test_language_shortcuts.py b/tests/test_language_shortcuts.py index 6b5d8a7..4c751ce 100644 --- a/tests/test_language_shortcuts.py +++ b/tests/test_language_shortcuts.py @@ -60,12 +60,14 @@ def test_normalize_language_shortcuts_keeps_valid_bindings() -> None: [ {"shortcut": "alt+d", "language": "de"}, {"shortcut": "ctrl+alt+g", "language": "hu"}, + {"shortcut": "ctrl+alt+s", "language": "sl"}, {"shortcut": "f5", "language": "auto"}, ] ) assert entries == [ {"shortcut": "alt+d", "language": "de"}, {"shortcut": "ctrl+alt+g", "language": "hu"}, + {"shortcut": "ctrl+alt+s", "language": "sl"}, {"shortcut": "f5", "language": "auto"}, ] @@ -1023,3 +1025,50 @@ def test_language_shortcut_managers_share_the_evdev_device_layer() -> None: assert isinstance(german.backend_instance, EvdevKeyboardBackend) assert isinstance(french.backend_instance, EvdevKeyboardBackend) assert french.backend_instance._hub is german.backend_instance._hub + + +@pytest.mark.parametrize( + "engine,offered", + [("whisper_cpp", True), ("whisper", True), ("remote_api", True), ("vosk", False)], +) +def test_settings_language_picker_offers_slovenian_on_whisper_engines( + settings_dialog: Any, dialog_class: type[Any], engine: str, offered: bool +) -> None: + """Slovenian (#970) is selectable wherever Whisper runs; VOSK has no model for it.""" + dialog = MagicMock() + dialog.engine_combo.get_active_text.return_value = settings_dialog.ENGINE_DISPLAY_NAMES[engine] + + with patch.object(settings_dialog, "_is_vosk_model_downloaded", return_value=False): + dialog_class._populate_language_options(dialog) + + appended = [c.args[0] for c in dialog.language_combo.append.call_args_list] + assert ("sl" in appended) is offered + assert "sk" not in appended + + +def test_slovenian_shortcut_row_persists_through_config( + dialog_class: type[Any], tmp_path: Any +) -> None: + """A row picked as Slovenian is saved and read back, not dropped (#970).""" + config_dir = tmp_path / "vocalinux" + config_dir.mkdir() + with ( + patch("vocalinux.ui.config_manager.CONFIG_DIR", str(config_dir)), + patch("vocalinux.ui.config_manager.CONFIG_FILE", str(config_dir / "config.json")), + patch("vocalinux.utils.system_language.detect_system_language", return_value=None), + ): + dialog = _dialog_stub() + dialog.config_manager = ConfigManager() + dialog.config_manager.save_settings = dialog.config_manager.save_config + picker, entry = MagicMock(), MagicMock() + picker.get_active_id.return_value = "sl" + entry.get_text.return_value = "ctrl+alt+s" + refs = {"language_picker": picker, "shortcut_entry": entry, "row": MagicMock()} + dialog._language_shortcut_rows = [refs] + dialog._report_language_shortcut_rejections = MagicMock() + + dialog_class._persist_language_shortcuts(dialog) + + assert ConfigManager().get_language_shortcuts() == [ + {"shortcut": "ctrl+alt+s", "language": "sl"} + ] diff --git a/tests/test_main.py b/tests/test_main.py index a4191b9..a5f03c3 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -104,7 +104,7 @@ class TestMainModule(unittest.TestCase): self.assertEqual(LANGUAGE_CHOICES, tuple(SUPPORTED_LANGUAGES.keys())) # Spot-check newly exposed languages (issue #565 and catalog expansion). - for lang in ["auto", "en-us", "hu", "ja", "ko", "ar", "nl", "pl", "uk"]: + for lang in ["auto", "en-us", "hu", "ja", "ko", "ar", "nl", "pl", "sl", "uk"]: self.assertIn(lang, LANGUAGE_CHOICES) with patch("sys.argv", ["vocalinux", "--language", lang]): args = parse_arguments() diff --git a/tests/test_speech_recognition.py b/tests/test_speech_recognition.py index a49d65e..74329c2 100644 --- a/tests/test_speech_recognition.py +++ b/tests/test_speech_recognition.py @@ -305,6 +305,18 @@ class TestSpeechRecognition(unittest.TestCase): self.assertIsNone(hu["vosk"]) self.assertNotIn("hu", VOSK_MODEL_INFO["small"]["languages"]) + def test_slovenian_is_whisper_only_in_catalog(self) -> None: + """Slovenian (issue #970) is exposed for Whisper engines, not VOSK or Slovak.""" + from vocalinux.utils.vosk_model_info import SUPPORTED_LANGUAGES + + slovenian = SUPPORTED_LANGUAGES["sl"] + self.assertEqual(slovenian["name"], "Slovenian") + self.assertEqual(slovenian["whisper"], "sl") + self.assertIsNone(slovenian["vosk"]) + for size in ("small", "medium", "large"): + self.assertNotIn("sl", VOSK_MODEL_INFO[size]["languages"]) + self.assertNotIn("sk", SUPPORTED_LANGUAGES) + def test_resolve_whisper_language_maps_english_variants(self): """Catalog ids like en-us/en-in must become Whisper's 'en' code.""" from unittest.mock import patch @@ -314,6 +326,7 @@ class TestSpeechRecognition(unittest.TestCase): self.assertEqual(resolve_whisper_language("en-us"), "en") self.assertEqual(resolve_whisper_language("en-in"), "en") self.assertEqual(resolve_whisper_language("hu"), "hu") + self.assertEqual(resolve_whisper_language("sl"), "sl") self.assertEqual(resolve_whisper_language("ja"), "ja") self.assertIsNone(resolve_whisper_language("auto")) # Unknown catalog ids pass through unchanged. diff --git a/tests/test_system_language.py b/tests/test_system_language.py index 2736c56..55c7cd3 100644 --- a/tests/test_system_language.py +++ b/tests/test_system_language.py @@ -24,6 +24,8 @@ def isolated_config(tmp_path, monkeypatch): ("de", "de"), ("cz", "cs"), ("se", "sv"), + ("si", "sl"), + ("sk", None), ("us", "en-us"), ("gb", "en-us"), ("xyz", None), @@ -48,6 +50,8 @@ def test_layouts_map_onto_catalogue_entries(layout, expected): ("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), diff --git a/web/src/app/languages/page.tsx b/web/src/app/languages/page.tsx index 19c3166..1611df2 100644 --- a/web/src/app/languages/page.tsx +++ b/web/src/app/languages/page.tsx @@ -216,6 +216,14 @@ const supportedLanguages = [ description: "Russian language speech-to-text transcription", engines: [...ALL_ENGINES], }, + { + code: "sl", + name: "Slovenian", + nativeName: "Slovenščina", + flag: "🇸🇮", + description: "Slovenian dictation with multilingual Whisper models", + engines: [...WHISPER_ENGINES], + }, { code: "es", name: "Spanish", @@ -314,6 +322,7 @@ export const metadata: Metadata = buildPageMetadata({ "Italian voice recognition", "Portuguese voice typing", "Russian speech recognition", + "Slovenian voice typing", "Chinese dictation", "Hindi voice typing", "Linux multilingual dictation",