All checks were successful
- Fix platform-grouped merchant sidebar menu with core items at root level - Add merchant store management (detail page, create store, team page) - Fix store settings 500 error by removing dead stripe/API tab - Move onboarding translations to module-owned locale files - Fix onboarding banner i18n with server-side rendering + context inheritance - Refactor login language selectors to use languageSelector() function (LANG-002) - Move HTTPException handling to global exception handler in merchant routes (API-003) - Add language selector to all login pages and portal headers - Fix customer module: drop order stats from customer model, add to orders module - Fix admin menu config visibility for super admin platform context - Fix storefront auth and layout issues - Add missing i18n translations for onboarding steps (en/fr/de/lb) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
355 lines
12 KiB
Python
355 lines
12 KiB
Python
# tests/unit/utils/test_i18n.py
|
|
"""
|
|
Unit tests for i18n utilities.
|
|
|
|
Tests cover:
|
|
- Language configuration
|
|
- Translation loading
|
|
- Translation lookup
|
|
- Accept-Language parsing
|
|
- Language resolution
|
|
- Jinja2 integration
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from app.utils.i18n import (
|
|
DEFAULT_LANGUAGE,
|
|
LANGUAGE_FLAGS,
|
|
LANGUAGE_NAMES,
|
|
LANGUAGE_NAMES_EN,
|
|
SUPPORTED_LANGUAGES,
|
|
TranslationContext,
|
|
clear_translation_cache,
|
|
create_translation_context,
|
|
get_jinja2_globals,
|
|
get_language_choices,
|
|
get_language_info,
|
|
get_locales_path,
|
|
get_nested_value,
|
|
is_rtl_language,
|
|
load_translations,
|
|
parse_accept_language,
|
|
resolve_store_dashboard_language,
|
|
resolve_storefront_language,
|
|
t,
|
|
translate,
|
|
)
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.utils
|
|
class TestLanguageConfiguration:
|
|
"""Test language configuration constants"""
|
|
|
|
def test_supported_languages(self):
|
|
"""Test SUPPORTED_LANGUAGES contains expected languages"""
|
|
assert "en" in SUPPORTED_LANGUAGES
|
|
assert "fr" in SUPPORTED_LANGUAGES
|
|
assert "de" in SUPPORTED_LANGUAGES
|
|
assert "lb" in SUPPORTED_LANGUAGES
|
|
|
|
def test_default_language_is_french(self):
|
|
"""Test default language is French (Luxembourg context)"""
|
|
assert DEFAULT_LANGUAGE == "fr"
|
|
|
|
def test_language_names_defined(self):
|
|
"""Test all languages have names defined"""
|
|
for lang in SUPPORTED_LANGUAGES:
|
|
assert lang in LANGUAGE_NAMES
|
|
assert lang in LANGUAGE_NAMES_EN
|
|
assert lang in LANGUAGE_FLAGS
|
|
|
|
def test_get_locales_path_exists(self):
|
|
"""Test locales path is accessible"""
|
|
path = get_locales_path()
|
|
assert path is not None
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.utils
|
|
class TestTranslationLoading:
|
|
"""Test translation loading functionality"""
|
|
|
|
def test_load_translations_english(self):
|
|
"""Test loading English translations"""
|
|
clear_translation_cache()
|
|
translations = load_translations("en")
|
|
assert isinstance(translations, dict)
|
|
|
|
def test_load_translations_french(self):
|
|
"""Test loading French translations"""
|
|
clear_translation_cache()
|
|
translations = load_translations("fr")
|
|
assert isinstance(translations, dict)
|
|
|
|
def test_load_translations_unsupported(self):
|
|
"""Test loading unsupported language falls back to default"""
|
|
clear_translation_cache()
|
|
translations = load_translations("xx") # Non-existent
|
|
# Should fall back to French (default)
|
|
assert isinstance(translations, dict)
|
|
|
|
def test_clear_translation_cache(self):
|
|
"""Test clearing translation cache"""
|
|
load_translations("en")
|
|
clear_translation_cache()
|
|
# Should not raise
|
|
load_translations("en")
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.utils
|
|
class TestNestedValue:
|
|
"""Test get_nested_value function"""
|
|
|
|
def test_get_nested_value_simple(self):
|
|
"""Test getting simple key"""
|
|
data = {"key": "value"}
|
|
result = get_nested_value(data, "key")
|
|
assert result == "value"
|
|
|
|
def test_get_nested_value_nested(self):
|
|
"""Test getting nested key"""
|
|
data = {"level1": {"level2": {"level3": "value"}}}
|
|
result = get_nested_value(data, "level1.level2.level3")
|
|
assert result == "value"
|
|
|
|
def test_get_nested_value_missing(self):
|
|
"""Test getting missing key returns key path"""
|
|
data = {"key": "value"}
|
|
result = get_nested_value(data, "missing.key")
|
|
assert result == "missing.key"
|
|
|
|
def test_get_nested_value_with_default(self):
|
|
"""Test getting missing key with default"""
|
|
data = {"key": "value"}
|
|
result = get_nested_value(data, "missing.key", "default")
|
|
assert result == "default"
|
|
|
|
def test_get_nested_value_non_string(self):
|
|
"""Test getting non-string value returns default"""
|
|
data = {"key": {"nested": "obj"}}
|
|
result = get_nested_value(data, "key", "default")
|
|
assert result == "default"
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.utils
|
|
class TestTranslate:
|
|
"""Test translate function"""
|
|
|
|
def test_translate_with_language(self):
|
|
"""Test translate with specified language"""
|
|
result = translate("common.save", language="en")
|
|
# Should return something (either translation or key)
|
|
assert result is not None
|
|
|
|
def test_translate_default_language(self):
|
|
"""Test translate uses default language when not specified"""
|
|
result = translate("common.save")
|
|
assert result is not None
|
|
|
|
def test_translate_missing_key(self):
|
|
"""Test translate returns key when not found"""
|
|
result = translate("nonexistent.key.path")
|
|
assert result == "nonexistent.key.path"
|
|
|
|
def test_translate_with_interpolation(self):
|
|
"""Test translate with variable interpolation"""
|
|
# Create a translation that uses variables
|
|
result = translate("test.key", language="en", name="John")
|
|
# Should return something (translation with vars or key)
|
|
assert result is not None
|
|
|
|
def test_t_alias(self):
|
|
"""Test t() is alias for translate()"""
|
|
result1 = translate("common.save", language="en")
|
|
result2 = t("common.save", language="en")
|
|
assert result1 == result2
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.utils
|
|
class TestTranslationContext:
|
|
"""Test TranslationContext class"""
|
|
|
|
def test_translation_context_init(self):
|
|
"""Test TranslationContext initialization"""
|
|
ctx = TranslationContext("en")
|
|
assert ctx.language == "en"
|
|
|
|
def test_translation_context_default_language(self):
|
|
"""Test TranslationContext uses default when not specified"""
|
|
ctx = TranslationContext()
|
|
assert ctx.language == DEFAULT_LANGUAGE
|
|
|
|
def test_translation_context_callable(self):
|
|
"""Test TranslationContext is callable"""
|
|
ctx = TranslationContext("en")
|
|
result = ctx("common.save")
|
|
assert result is not None
|
|
|
|
def test_translation_context_set_language(self):
|
|
"""Test set_language changes language"""
|
|
ctx = TranslationContext("en")
|
|
ctx.set_language("fr")
|
|
assert ctx.language == "fr"
|
|
|
|
def test_translation_context_set_unsupported(self):
|
|
"""Test set_language rejects unsupported language"""
|
|
ctx = TranslationContext("en")
|
|
ctx.set_language("xx")
|
|
assert ctx.language == "en" # Should not change
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.utils
|
|
class TestJinja2Integration:
|
|
"""Test Jinja2 integration functions"""
|
|
|
|
def test_create_translation_context(self):
|
|
"""Test create_translation_context factory"""
|
|
ctx = create_translation_context("de")
|
|
assert isinstance(ctx, TranslationContext)
|
|
assert ctx.language == "de"
|
|
|
|
def test_get_jinja2_globals(self):
|
|
"""Test get_jinja2_globals returns required globals"""
|
|
globals = get_jinja2_globals("en")
|
|
|
|
assert "_" in globals
|
|
assert "t" in globals
|
|
assert "SUPPORTED_LANGUAGES" in globals
|
|
assert "DEFAULT_LANGUAGE" in globals
|
|
assert "LANGUAGE_NAMES" in globals
|
|
assert "LANGUAGE_FLAGS" in globals
|
|
assert "current_language" in globals
|
|
assert globals["current_language"] == "en"
|
|
|
|
def test_get_jinja2_globals_default_language(self):
|
|
"""Test get_jinja2_globals uses default when not specified"""
|
|
globals = get_jinja2_globals()
|
|
assert globals["current_language"] == DEFAULT_LANGUAGE
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.utils
|
|
class TestLanguageResolution:
|
|
"""Test language resolution functions"""
|
|
|
|
def test_resolve_store_dashboard_user_preferred(self):
|
|
"""Test store dashboard prefers user's language"""
|
|
result = resolve_store_dashboard_language("en", "fr")
|
|
assert result == "en"
|
|
|
|
def test_resolve_store_dashboard_store_fallback(self):
|
|
"""Test store dashboard falls back to store setting"""
|
|
result = resolve_store_dashboard_language(None, "de")
|
|
assert result == "de"
|
|
|
|
def test_resolve_store_dashboard_default(self):
|
|
"""Test store dashboard uses default when nothing set"""
|
|
result = resolve_store_dashboard_language(None, None)
|
|
assert result == DEFAULT_LANGUAGE
|
|
|
|
def test_resolve_storefront_customer_preferred(self):
|
|
"""Test storefront prefers customer's language"""
|
|
result = resolve_storefront_language("de", "fr", "en", None)
|
|
assert result == "de"
|
|
|
|
def test_resolve_storefront_session(self):
|
|
"""Test storefront uses session language"""
|
|
result = resolve_storefront_language(None, "de", "fr", None)
|
|
assert result == "de"
|
|
|
|
def test_resolve_storefront_store(self):
|
|
"""Test storefront uses store default"""
|
|
result = resolve_storefront_language(None, None, "en", None)
|
|
assert result == "en"
|
|
|
|
def test_resolve_storefront_browser(self):
|
|
"""Test storefront uses browser language"""
|
|
result = resolve_storefront_language(None, None, None, "de")
|
|
assert result == "de"
|
|
|
|
def test_resolve_storefront_enabled_filter(self):
|
|
"""Test storefront enabled_languages filters automatic fallback but not explicit choices"""
|
|
# Explicit user choice (customer_preferred) should be respected
|
|
# even if not in store's enabled list
|
|
result = resolve_storefront_language("de", None, None, None, ["en", "fr"])
|
|
assert result == "de"
|
|
|
|
# But automatic fallback (browser, store default) should be filtered
|
|
result = resolve_storefront_language(None, None, None, "de", ["en", "fr"])
|
|
assert result in ["en", "fr", DEFAULT_LANGUAGE]
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.utils
|
|
class TestAcceptLanguageParsing:
|
|
"""Test Accept-Language header parsing"""
|
|
|
|
def test_parse_accept_language_simple(self):
|
|
"""Test parsing simple Accept-Language"""
|
|
result = parse_accept_language("fr")
|
|
assert result == "fr"
|
|
|
|
def test_parse_accept_language_with_region(self):
|
|
"""Test parsing Accept-Language with region"""
|
|
result = parse_accept_language("fr-FR")
|
|
assert result == "fr"
|
|
|
|
def test_parse_accept_language_multiple(self):
|
|
"""Test parsing multiple languages with quality"""
|
|
result = parse_accept_language("de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7")
|
|
assert result == "de" # Highest quality supported
|
|
|
|
def test_parse_accept_language_none(self):
|
|
"""Test parsing None returns None"""
|
|
result = parse_accept_language(None)
|
|
assert result is None
|
|
|
|
def test_parse_accept_language_unsupported(self):
|
|
"""Test parsing unsupported language returns None"""
|
|
result = parse_accept_language("zh-CN")
|
|
assert result is None
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.utils
|
|
class TestUtilityFunctions:
|
|
"""Test utility functions"""
|
|
|
|
def test_get_language_choices(self):
|
|
"""Test get_language_choices returns tuples"""
|
|
choices = get_language_choices()
|
|
assert len(choices) == len(SUPPORTED_LANGUAGES)
|
|
for code, name in choices:
|
|
assert code in SUPPORTED_LANGUAGES
|
|
assert name == LANGUAGE_NAMES[code]
|
|
|
|
def test_get_language_info_supported(self):
|
|
"""Test get_language_info for supported language"""
|
|
info = get_language_info("en")
|
|
assert info["code"] == "en"
|
|
assert info["name"] == "English"
|
|
assert info["name_en"] == "English"
|
|
assert "flag" in info
|
|
|
|
def test_get_language_info_unsupported(self):
|
|
"""Test get_language_info for unsupported language"""
|
|
info = get_language_info("xx")
|
|
# Should fallback to default
|
|
assert info["code"] == DEFAULT_LANGUAGE
|
|
|
|
def test_is_rtl_language_false(self):
|
|
"""Test is_rtl_language returns False for LTR languages"""
|
|
for lang in SUPPORTED_LANGUAGES:
|
|
assert is_rtl_language(lang) is False
|
|
|
|
def test_is_rtl_language_arabic(self):
|
|
"""Test is_rtl_language returns True for RTL languages"""
|
|
assert is_rtl_language("ar") is True
|
|
assert is_rtl_language("he") is True
|