Files
gh-christianlouis-docuelevate/tests/test_i18n.py
copilot-swe-agent[bot] 08fabe154a fix(test): remove tests that check specific translation content
Remove three tests that asserted exact translated strings for German,
French, and Chinese locales. These tests broke whenever translation
files were updated externally.

Replace content-checking tests with behavioral assertions:
- Translated values are non-empty strings (not the raw key)
- Fallback and None-locale return the English translation
- Placeholder interpolation injects the kwarg value

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
2026-03-14 08:47:56 +00:00

543 lines
18 KiB
Python

"""Tests for the i18n (internationalization) and l10n (localization) utilities."""
from __future__ import annotations
import json
from datetime import date, datetime
from pathlib import Path
from unittest.mock import MagicMock
import pytest
from fastapi.testclient import TestClient
from app.utils.i18n import (
DEFAULT_LANGUAGE,
SUPPORTED_LANGUAGE_CODES,
SUPPORTED_LANGUAGES,
_parse_accept_language,
detect_language,
format_date,
format_datetime,
format_number,
get_language_info,
get_suggested_languages,
reload_translations,
translate,
)
# ---------------------------------------------------------------------------
# Translation file integrity
# ---------------------------------------------------------------------------
class TestTranslationFiles:
"""Verify that all translation JSON files are valid and complete."""
@pytest.fixture(autouse=True)
def _clear_cache(self) -> None:
"""Clear translation cache before each test."""
reload_translations()
@pytest.mark.unit
def test_all_translation_files_exist(self) -> None:
"""Every supported language must have a corresponding JSON file."""
translations_dir = Path(__file__).resolve().parent.parent / "frontend" / "translations"
for lang in SUPPORTED_LANGUAGES:
filepath = translations_dir / f"{lang['code']}.json"
assert filepath.is_file(), f"Missing translation file for {lang['code']}"
@pytest.mark.unit
def test_all_translation_files_are_valid_json(self) -> None:
"""All translation files must be parseable JSON."""
translations_dir = Path(__file__).resolve().parent.parent / "frontend" / "translations"
for lang in SUPPORTED_LANGUAGES:
filepath = translations_dir / f"{lang['code']}.json"
data = json.loads(filepath.read_text(encoding="utf-8"))
assert isinstance(data, dict), f"{lang['code']}.json must be a dict"
assert len(data) > 0, f"{lang['code']}.json must not be empty"
# ---------------------------------------------------------------------------
# Core translate() function
# ---------------------------------------------------------------------------
class TestTranslate:
"""Tests for the translate() function."""
@pytest.fixture(autouse=True)
def _clear_cache(self) -> None:
reload_translations()
@pytest.mark.unit
def test_translate_known_key_returns_non_empty_string(self) -> None:
"""A valid key should resolve to a non-empty string (not the key itself)."""
result = translate("nav.dashboard", "en")
assert isinstance(result, str)
assert result != ""
assert result != "nav.dashboard"
@pytest.mark.unit
def test_translate_non_english_locale_returns_value(self) -> None:
"""Non-English locales should return a non-empty translated value."""
for locale in ("de", "fr", "zh"):
result = translate("nav.dashboard", locale)
assert isinstance(result, str)
assert result != ""
assert result != "nav.dashboard"
@pytest.mark.unit
def test_translate_fallback_to_english(self) -> None:
"""Unknown locale falls back to English translation."""
result = translate("nav.dashboard", "xx")
english = translate("nav.dashboard", "en")
assert result == english
@pytest.mark.unit
def test_translate_missing_key_returns_key(self) -> None:
"""Missing key falls back to the key itself."""
result = translate("nonexistent.key", "en")
assert result == "nonexistent.key"
@pytest.mark.unit
def test_translate_none_locale_uses_default(self) -> None:
"""None locale defaults to English."""
result = translate("nav.dashboard", None)
english = translate("nav.dashboard", "en")
assert result == english
@pytest.mark.unit
def test_translate_with_kwargs(self) -> None:
"""Placeholders should be interpolated via kwargs."""
result = translate("footer.copyright", "en", year="2025")
assert "2025" in result
assert result != "footer.copyright"
@pytest.mark.unit
def test_translate_with_kwargs_german(self) -> None:
"""Placeholder interpolation in German."""
result = translate("language.changed", "de", language="English")
assert "English" in result
assert result != "language.changed"
# ---------------------------------------------------------------------------
# Accept-Language header parsing
# ---------------------------------------------------------------------------
class TestParseAcceptLanguage:
"""Tests for parsing the Accept-Language HTTP header."""
@pytest.mark.unit
def test_simple_language(self) -> None:
assert _parse_accept_language("de") == "de"
@pytest.mark.unit
def test_language_with_region(self) -> None:
assert _parse_accept_language("de-DE") == "de"
@pytest.mark.unit
def test_multiple_languages_quality(self) -> None:
result = _parse_accept_language("fr;q=0.9, de;q=1.0, en;q=0.8")
assert result == "de"
@pytest.mark.unit
def test_unsupported_language_fallback(self) -> None:
result = _parse_accept_language("xx, yy")
assert result is None
@pytest.mark.unit
def test_empty_header(self) -> None:
assert _parse_accept_language("") is None
@pytest.mark.unit
def test_complex_accept_language(self) -> None:
header = "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7"
result = _parse_accept_language(header)
assert result == "zh"
# ---------------------------------------------------------------------------
# Language detection
# ---------------------------------------------------------------------------
class TestDetectLanguage:
"""Tests for detecting language from request context."""
@pytest.mark.unit
def test_session_preference_takes_priority(self) -> None:
request = MagicMock()
request.session = {"preferred_language": "de"}
request.cookies = {}
request.headers = {}
assert detect_language(request) == "de"
@pytest.mark.unit
def test_cookie_fallback(self) -> None:
request = MagicMock()
request.session = {}
request.cookies = {"docuelevate_lang": "fr"}
request.headers = {}
assert detect_language(request) == "fr"
@pytest.mark.unit
def test_accept_language_fallback(self) -> None:
request = MagicMock()
request.session = {}
request.cookies = {}
request.headers = {"accept-language": "es-ES,es;q=0.9"}
assert detect_language(request) == "es"
@pytest.mark.unit
def test_default_fallback(self) -> None:
request = MagicMock()
request.session = {}
request.cookies = {}
request.headers = {}
assert detect_language(request) == DEFAULT_LANGUAGE
@pytest.mark.unit
def test_invalid_session_language_ignored(self) -> None:
request = MagicMock()
request.session = {"preferred_language": "invalid"}
request.cookies = {"docuelevate_lang": "it"}
request.headers = {}
assert detect_language(request) == "it"
# ---------------------------------------------------------------------------
# Localization helpers
# ---------------------------------------------------------------------------
class TestL10nFormatters:
"""Tests for locale-aware formatting functions."""
@pytest.mark.unit
def test_format_date_english(self) -> None:
d = date(2025, 3, 15)
result = format_date(d, "en")
assert "March" in result
assert "15" in result
assert "2025" in result
@pytest.mark.unit
def test_format_date_german(self) -> None:
d = date(2025, 3, 15)
result = format_date(d, "de")
assert "15." in result
assert "2025" in result
@pytest.mark.unit
def test_format_date_short(self) -> None:
d = date(2025, 3, 15)
result = format_date(d, "en", short=True)
assert result == "03/15/2025"
@pytest.mark.unit
def test_format_date_short_german(self) -> None:
d = date(2025, 3, 15)
result = format_date(d, "de", short=True)
assert result == "15.03.2025"
@pytest.mark.unit
def test_format_date_none(self) -> None:
assert format_date(None) == ""
@pytest.mark.unit
def test_format_datetime_none(self) -> None:
assert format_datetime(None) == ""
@pytest.mark.unit
def test_format_number_english(self) -> None:
result = format_number(1234567, "en")
assert result == "1,234,567"
@pytest.mark.unit
def test_format_number_german(self) -> None:
result = format_number(1234567, "de")
assert result == "1.234.567"
@pytest.mark.unit
def test_format_number_float_english(self) -> None:
result = format_number(1234.56, "en")
assert result == "1,234.56"
@pytest.mark.unit
def test_format_number_float_german(self) -> None:
result = format_number(1234.56, "de")
assert result == "1.234,56"
@pytest.mark.unit
def test_format_datetime_chinese(self) -> None:
dt = datetime(2025, 3, 15, 14, 30)
result = format_datetime(dt, "zh")
assert "2025" in result
assert "03" in result
assert "15" in result
# ---------------------------------------------------------------------------
# get_language_info()
# ---------------------------------------------------------------------------
class TestGetLanguageInfo:
"""Tests for get_language_info() utility."""
@pytest.mark.unit
def test_known_language(self) -> None:
info = get_language_info("de")
assert info is not None
assert info["name"] == "German"
assert info["native"] == "Deutsch"
@pytest.mark.unit
def test_unknown_language(self) -> None:
assert get_language_info("xx") is None
# ---------------------------------------------------------------------------
# get_suggested_languages()
# ---------------------------------------------------------------------------
class TestGetSuggestedLanguages:
"""Tests for get_suggested_languages() utility."""
@pytest.mark.unit
def test_returns_at_most_six(self) -> None:
"""Result must contain at most 6 languages."""
result = get_suggested_languages("en", "en,de;q=0.9,fr;q=0.8,es;q=0.7,it;q=0.6,pt;q=0.5,nl;q=0.4,zh;q=0.3")
assert len(result) <= 6
@pytest.mark.unit
def test_current_locale_is_first(self) -> None:
"""Currently active language must always be the first entry."""
result = get_suggested_languages("de", "")
assert result[0]["code"] == "de"
@pytest.mark.unit
def test_includes_browser_preference(self) -> None:
"""Languages from Accept-Language header should be included."""
result = get_suggested_languages("en", "fr;q=0.9,de;q=0.8")
codes = [lang["code"] for lang in result]
assert "fr" in codes
assert "de" in codes
@pytest.mark.unit
def test_fallback_to_popular_languages(self) -> None:
"""Popular languages fill remaining slots when no browser prefs given."""
result = get_suggested_languages("en", "")
codes = [lang["code"] for lang in result]
# en is current; popular fallbacks like zh, es, fr should be present
assert "en" in codes
# At least one other popular language should appear
popular = {"zh", "es", "ar", "fr", "de", "ja", "pt", "hi", "ko"}
assert popular & set(codes)
@pytest.mark.unit
def test_no_duplicates(self) -> None:
"""No language code should appear more than once."""
result = get_suggested_languages("fr", "fr;q=1.0,de;q=0.9")
codes = [lang["code"] for lang in result]
assert len(codes) == len(set(codes))
@pytest.mark.unit
def test_all_entries_are_valid_languages(self) -> None:
"""Every returned entry must be a dict with required language fields."""
result = get_suggested_languages("es", "ca;q=0.9")
for entry in result:
assert "code" in entry
assert "name" in entry
assert "native" in entry
assert "flag" in entry
@pytest.mark.unit
def test_unknown_locale_falls_back_gracefully(self) -> None:
"""An unsupported current_locale must not crash and still return results."""
result = get_suggested_languages("xx", "")
assert len(result) > 0 # popular fallbacks still returned
# ---------------------------------------------------------------------------
# SUPPORTED_LANGUAGES metadata
# ---------------------------------------------------------------------------
class TestSupportedLanguages:
"""Tests for language metadata constants."""
@pytest.mark.unit
def test_ten_languages_supported(self) -> None:
assert len(SUPPORTED_LANGUAGES) == 77
@pytest.mark.unit
def test_supported_codes_set(self) -> None:
expected = {
"en",
"de",
"fr",
"es",
"it",
"pt",
"nl",
"nb",
"no",
"da",
"sv",
"fi",
"is",
"ga",
"lb",
"ca",
"cy",
"fy",
"gl",
"li",
"vls",
"nds",
"pl",
"cs",
"sk",
"hu",
"sl",
"hr",
"ro",
"bg",
"el",
"et",
"lv",
"lt",
"sr",
"tr",
"uk",
"he",
"ar",
"fa",
"af",
"zh",
"zh-TW",
"ja",
"ko",
"vi",
"pa",
"kn",
"hi",
"bn",
"gu",
"ml",
"mr",
"ta",
"te",
"ur",
"si",
"ne",
"th",
"km",
"id",
"ms",
"jv",
"tl",
"mn",
"kk",
"uz",
"az",
"hy",
"ka",
"sw",
"am",
"ha",
"yo",
"ig",
"zu",
"eo",
}
assert SUPPORTED_LANGUAGE_CODES == expected
@pytest.mark.unit
def test_default_language_is_english(self) -> None:
assert DEFAULT_LANGUAGE == "en"
# ---------------------------------------------------------------------------
# API endpoint tests
# ---------------------------------------------------------------------------
class TestI18nAPI:
"""Tests for the i18n API endpoints."""
@pytest.mark.integration
def test_list_languages(self, client: TestClient) -> None:
"""GET /api/i18n/languages should return all supported languages."""
response = client.get("/api/i18n/languages")
assert response.status_code == 200
data = response.json()
assert "languages" in data
assert len(data["languages"]) == 77
assert data["default"] == "en"
# Verify each language has required fields
for lang in data["languages"]:
assert "code" in lang
assert "name" in lang
assert "native" in lang
assert "flag" in lang
@pytest.mark.integration
def test_set_language(self, client: TestClient) -> None:
"""POST /api/i18n/language should set language preference."""
response = client.post(
"/api/i18n/language",
json={"language": "de"},
)
assert response.status_code == 200
data = response.json()
assert data["language"] == "de"
# Verify cookie was set
assert "docuelevate_lang" in response.cookies
@pytest.mark.integration
def test_set_language_invalid_falls_back_to_default(self, client: TestClient) -> None:
"""Invalid language code should fall back to default."""
response = client.post(
"/api/i18n/language",
json={"language": "invalid"},
)
assert response.status_code == 200
data = response.json()
assert data["language"] == "en"
@pytest.mark.integration
def test_set_language_persists_in_cookie(self, client: TestClient) -> None:
"""Language setting should be persisted in a cookie."""
client.post("/api/i18n/language", json={"language": "fr"})
# Subsequent requests should detect the language from cookie
response = client.get("/api/i18n/languages")
data = response.json()
assert data["current"] == "fr"
@pytest.mark.integration
def test_base_html_uses_current_locale(self, client: TestClient) -> None:
"""The base template should set lang attribute to current locale."""
# Set language to German
client.post("/api/i18n/language", json={"language": "de"})
# Load homepage
response = client.get("/", follow_redirects=True)
assert response.status_code == 200
# The lang attribute should reflect the locale
assert 'lang="de"' in response.text or 'lang="en"' in response.text
@pytest.mark.integration
def test_language_selector_in_nav(self, client: TestClient) -> None:
"""The navigation should contain the language selector with flag and search."""
response = client.get("/", follow_redirects=True)
if response.status_code == 200:
# The selector renders a flag emoji (not the old fa-globe icon) and the
# setLanguage JS helper for switching languages.
assert "setLanguage" in response.text
# The search input for filtering all languages must be present.
assert "langSearch" in response.text