diff --git a/tests/test_i18n.py b/tests/test_i18n.py new file mode 100644 index 00000000..dce0fb9d --- /dev/null +++ b/tests/test_i18n.py @@ -0,0 +1,208 @@ +"""Tests for the i18n internationalization module.""" + +from __future__ import annotations + +import json +import os +from pathlib import Path +from unittest.mock import patch + +import pytest + +import strix.i18n as mod +from strix.config.settings import Settings +from strix.i18n import ( + SUPPORTED_LANGUAGES, + _detect_language, + _load_locale, + _normalize_lang, + get_language, + get_language_directive, + set_language, + t, +) + + +@pytest.fixture(autouse=True) +def _reset_i18n_state(): + """Reset module-level state between tests.""" + mod._language = None + mod._locales.clear() + yield + mod._language = None + mod._locales.clear() + + +class TestSetLanguage: + def test_set_language_spanish(self): + set_language("es") + assert get_language() == "es" + + def test_set_language_english(self): + set_language("en") + assert get_language() == "en" + + def test_set_language_normalizes(self): + set_language("ES") + assert get_language() == "es" + + def test_set_language_none_resets(self): + set_language("es") + set_language(None) + # After reset, should fall back to detection + assert get_language() == "en" # default + + def test_set_language_unsupported_falls_back(self): + set_language("fr") + assert get_language() == "en" + + +class TestNormalizeLang: + def test_normalize_lowercase(self): + assert _normalize_lang("es") == "es" + + def test_normalize_uppercase(self): + assert _normalize_lang("ES") == "es" + + def test_normalize_strips(self): + assert _normalize_lang(" es ") == "es" + + def test_normalize_unsupported(self): + assert _normalize_lang("fr") == "en" + + def test_normalize_empty(self): + assert _normalize_lang("") == "en" + + +class TestDetectLanguage: + def test_default_is_english(self): + assert _detect_language() == "en" + + def test_explicit_set_takes_priority(self): + mod._language = "es" + assert _detect_language() == "es" + + def test_env_var_detected(self): + with patch.dict(os.environ, {"STRIX_LANGUAGE": "es"}): + assert _detect_language() == "es" + + def test_env_var_overrides_config(self): + with patch.dict(os.environ, {"STRIX_LANGUAGE": "es"}): + assert _detect_language() == "es" + + def test_system_locale_detected(self): + with patch.dict(os.environ, {"LANG": "es_ES.UTF-8"}, clear=False): + mod._language = None + assert _detect_language() == "es" + + +class TestLoadLocale: + def test_load_english(self): + locale = _load_locale("en") + assert "cli.description" in locale + assert isinstance(locale, dict) + + def test_load_spanish(self): + locale = _load_locale("es") + assert "cli.description" in locale + + def test_load_missing_returns_empty(self): + locale = _load_locale("nonexistent") + assert locale == {} + + def test_cached_after_first_load(self): + _load_locale("en") + assert "en" in mod._locales + + +class TestTranslationFunction: + def test_t_returns_english_by_default(self): + result = t("cli.description") + assert "Strix" in result + assert "Penetration" in result + + def test_t_returns_spanish_when_set(self): + set_language("es") + result = t("cli.description") + assert "Penetración" in result + + def test_t_falls_back_to_english_for_missing_key(self): + set_language("es") + # Use a key that exists in en but not es + result = t("cli.description") + # Should still return something (English fallback) + assert result != "cli.description" + + def test_t_returns_key_for_missing(self): + result = t("nonexistent.key.xyz") + assert result == "nonexistent.key.xyz" + + def test_t_interpolates_placeholders(self): + result = t("cli.scan_started", target="example.com") + assert "example.com" in result + + def test_t_interpolates_spanish(self): + set_language("es") + result = t("cli.scan_started", target="example.com") + assert "example.com" in result + assert "escaneo" in result.lower() + + def test_t_handles_missing_placeholder_gracefully(self): + # Should not raise, just return with missing placeholder + result = t("cli.scan_started") # target not provided + assert isinstance(result, str) + + +class TestLanguageDirective: + def test_english_returns_empty(self): + set_language("en") + directive = get_language_directive() + assert directive == "" + + def test_spanish_returns_directive(self): + set_language("es") + directive = get_language_directive() + assert "Spanish" in directive + assert "CVE" in directive # preservation rule + assert "CWE" in directive + + def test_directive_preserves_identifiers(self): + set_language("es") + directive = get_language_directive() + assert "CVE identifiers" in directive + assert "CWE identifiers" in directive + assert "CVSS scores" in directive + assert "Source code" in directive + assert "Shell commands" in directive + + +class TestLocaleKeyConsistency: + def test_all_en_keys_exist_in_es(self): + en = _load_locale("en") + es = _load_locale("es") + missing = set(en.keys()) - set(es.keys()) + assert missing == set(), f"Missing Spanish keys: {missing}" + + def test_locale_files_are_valid_json(self): + locales_dir = Path(__file__).parent.parent / "strix" / "locales" + for json_file in locales_dir.glob("*.json"): + data = json.loads(json_file.read_text(encoding="utf-8")) + assert isinstance(data, dict), f"{json_file.name} is not a dict" + + def test_supported_languages_match_files(self): + locales_dir = Path(__file__).parent.parent / "strix" / "locales" + for lang in SUPPORTED_LANGUAGES: + locale_file = locales_dir / f"{lang}.json" + assert locale_file.exists(), f"Missing locale file: {locale_file}" + + +class TestSettingsLanguageField: + def test_settings_has_language_field(self): + s = Settings() + assert hasattr(s, "language") + assert s.language == "en" + + def test_settings_language_from_env(self): + with patch.dict(os.environ, {"STRIX_LANGUAGE": "es"}): + s = Settings() + assert s.language == "es"