Add internationalization support with English and French translations

- Introduced a new i18n module for managing translations using JSON files.
- Added English (en.json) and French (fr.json) translation files for UI elements.
- Implemented lazy initialization of the translator to load translations on demand.
- Added unit tests for translation lookup, fallback behavior, and available languages detection.
This commit is contained in:
claudi 2026-03-10 14:32:38 +01:00
parent fd0482ed2d
commit 7daec731ca
11 changed files with 1184 additions and 280 deletions

60
tests/unit/test_i18n.py Normal file
View file

@ -0,0 +1,60 @@
"""Unit tests for i18n translation helper."""
import json
from pathlib import Path
from webdrop_bridge.utils import i18n
class TestI18n:
"""Tests for translation lookup and fallback behavior."""
def test_tr_lazy_initialization_uses_english_defaults(self):
"""Translator should lazily initialize and resolve known keys."""
# Force a fresh singleton state for this test.
i18n._translator = i18n.Translator() # type: ignore[attr-defined]
assert i18n.tr("settings.title") == "Settings"
def test_initialize_with_language_falls_back_to_english(self, tmp_path: Path):
"""Missing keys in selected language should fall back to English."""
translations = tmp_path / "translations"
translations.mkdir(parents=True, exist_ok=True)
(translations / "en.json").write_text(
json.dumps(
{
"greeting": "Hello {name}",
"settings.title": "Settings",
}
),
encoding="utf-8",
)
(translations / "de.json").write_text(
json.dumps(
{
"settings.title": "Einstellungen",
}
),
encoding="utf-8",
)
i18n._translator = i18n.Translator() # type: ignore[attr-defined]
i18n.initialize("de", translations)
assert i18n.tr("settings.title") == "Einstellungen"
assert i18n.tr("greeting", name="Alex") == "Hello Alex"
def test_get_available_languages_reads_translation_files(self, tmp_path: Path):
"""Available languages should be discovered from JSON files."""
translations = tmp_path / "translations"
translations.mkdir(parents=True, exist_ok=True)
(translations / "en.json").write_text("{}", encoding="utf-8")
(translations / "fr.json").write_text("{}", encoding="utf-8")
i18n._translator = i18n.Translator() # type: ignore[attr-defined]
i18n.initialize("en", translations)
available = i18n.get_available_languages()
assert "en" in available
assert "fr" in available

View file

@ -34,7 +34,7 @@ class TestSettingsDialogInitialization:
"""Test dialog can be created."""
dialog = SettingsDialog(sample_config)
qtbot.addWidget(dialog)
assert dialog is not None
assert dialog.windowTitle() == "Settings"
@ -42,51 +42,58 @@ class TestSettingsDialogInitialization:
"""Test dialog has all required tabs."""
dialog = SettingsDialog(sample_config)
qtbot.addWidget(dialog)
assert dialog.tabs is not None
assert dialog.tabs.count() == 6 # Web Source, Paths, URLs, Logging, Window, Profiles
assert dialog.tabs.count() == 7 # General + previous 6 tabs
def test_dialog_has_general_tab(self, qtbot, sample_config):
"""Test General tab exists."""
dialog = SettingsDialog(sample_config)
qtbot.addWidget(dialog)
assert dialog.tabs.tabText(0) == "General"
def test_dialog_has_web_source_tab(self, qtbot, sample_config):
"""Test Web Source tab exists."""
dialog = SettingsDialog(sample_config)
qtbot.addWidget(dialog)
assert dialog.tabs.tabText(0) == "Web Source"
assert dialog.tabs.tabText(1) == "Web Source"
def test_dialog_has_paths_tab(self, qtbot, sample_config):
"""Test Paths tab exists."""
dialog = SettingsDialog(sample_config)
qtbot.addWidget(dialog)
assert dialog.tabs.tabText(1) == "Paths"
assert dialog.tabs.tabText(2) == "Paths"
def test_dialog_has_urls_tab(self, qtbot, sample_config):
"""Test URLs tab exists."""
dialog = SettingsDialog(sample_config)
qtbot.addWidget(dialog)
assert dialog.tabs.tabText(2) == "URLs"
assert dialog.tabs.tabText(3) == "URLs"
def test_dialog_has_logging_tab(self, qtbot, sample_config):
"""Test Logging tab exists."""
dialog = SettingsDialog(sample_config)
qtbot.addWidget(dialog)
assert dialog.tabs.tabText(3) == "Logging"
assert dialog.tabs.tabText(4) == "Logging"
def test_dialog_has_window_tab(self, qtbot, sample_config):
"""Test Window tab exists."""
dialog = SettingsDialog(sample_config)
qtbot.addWidget(dialog)
assert dialog.tabs.tabText(4) == "Window"
assert dialog.tabs.tabText(5) == "Window"
def test_dialog_has_profiles_tab(self, qtbot, sample_config):
"""Test Profiles tab exists."""
dialog = SettingsDialog(sample_config)
qtbot.addWidget(dialog)
assert dialog.tabs.tabText(5) == "Profiles"
assert dialog.tabs.tabText(6) == "Profiles"
class TestPathsTab:
@ -96,7 +103,7 @@ class TestPathsTab:
"""Test paths are loaded from configuration."""
dialog = SettingsDialog(sample_config)
qtbot.addWidget(dialog)
items = [dialog.paths_list.item(i).text() for i in range(dialog.paths_list.count())]
assert len(items) == 2
# Paths are normalized (backslashes on Windows)
@ -107,7 +114,7 @@ class TestPathsTab:
"""Test Add Path button exists."""
dialog = SettingsDialog(sample_config)
qtbot.addWidget(dialog)
assert dialog.tabs.currentWidget() is not None
@ -118,7 +125,7 @@ class TestURLsTab:
"""Test URLs are loaded from configuration."""
dialog = SettingsDialog(sample_config)
qtbot.addWidget(dialog)
items = [dialog.urls_list.item(i).text() for i in range(dialog.urls_list.count())]
assert len(items) == 2
assert "http://example.com" in items
@ -132,14 +139,14 @@ class TestLoggingTab:
"""Test log level is set from configuration."""
dialog = SettingsDialog(sample_config)
qtbot.addWidget(dialog)
assert dialog.log_level_combo.currentText() == "INFO"
def test_log_levels_available(self, qtbot, sample_config):
"""Test all log levels are available."""
dialog = SettingsDialog(sample_config)
qtbot.addWidget(dialog)
levels = [dialog.log_level_combo.itemText(i) for i in range(dialog.log_level_combo.count())]
assert "DEBUG" in levels
assert "INFO" in levels
@ -155,21 +162,21 @@ class TestWindowTab:
"""Test window width is set from configuration."""
dialog = SettingsDialog(sample_config)
qtbot.addWidget(dialog)
assert dialog.width_spin.value() == 800
def test_window_height_set_from_config(self, qtbot, sample_config):
"""Test window height is set from configuration."""
dialog = SettingsDialog(sample_config)
qtbot.addWidget(dialog)
assert dialog.height_spin.value() == 600
def test_window_width_has_min_max(self, qtbot, sample_config):
"""Test window width spinbox has min/max."""
dialog = SettingsDialog(sample_config)
qtbot.addWidget(dialog)
assert dialog.width_spin.minimum() == 400
assert dialog.width_spin.maximum() == 5000
@ -177,7 +184,7 @@ class TestWindowTab:
"""Test window height spinbox has min/max."""
dialog = SettingsDialog(sample_config)
qtbot.addWidget(dialog)
assert dialog.height_spin.minimum() == 300
assert dialog.height_spin.maximum() == 5000
@ -189,7 +196,7 @@ class TestProfilesTab:
"""Test profiles list is initialized."""
dialog = SettingsDialog(sample_config)
qtbot.addWidget(dialog)
assert dialog.profiles_list is not None
@ -200,9 +207,9 @@ class TestConfigDataRetrieval:
"""Test retrieving configuration data from dialog."""
dialog = SettingsDialog(sample_config)
qtbot.addWidget(dialog)
config_data = dialog.get_config_data()
assert config_data["app_name"] == "WebDrop Bridge"
assert config_data["log_level"] == "INFO"
assert config_data["window_width"] == 800
@ -212,7 +219,7 @@ class TestConfigDataRetrieval:
"""Test get_config_data returns valid configuration data."""
dialog = SettingsDialog(sample_config)
qtbot.addWidget(dialog)
# All default values are valid
config_data = dialog.get_config_data()
assert config_data is not None
@ -222,14 +229,14 @@ class TestConfigDataRetrieval:
"""Test get_config_data returns modified values."""
dialog = SettingsDialog(sample_config)
qtbot.addWidget(dialog)
# Modify values
dialog.width_spin.setValue(1024)
dialog.height_spin.setValue(768)
dialog.log_level_combo.setCurrentText("DEBUG")
config_data = dialog.get_config_data()
assert config_data["window_width"] == 1024
assert config_data["window_height"] == 768
assert config_data["log_level"] == "DEBUG"
@ -242,7 +249,7 @@ class TestApplyConfigData:
"""Test applying config data updates paths."""
dialog = SettingsDialog(sample_config)
qtbot.addWidget(dialog)
new_config = {
"app_name": "Test",
"app_version": "1.0.0",
@ -255,9 +262,9 @@ class TestApplyConfigData:
"window_height": 600,
"enable_logging": True,
}
dialog._apply_config_data(new_config)
items = [dialog.paths_list.item(i).text() for i in range(dialog.paths_list.count())]
assert "/new/path" in items
assert "/another/path" in items
@ -266,7 +273,7 @@ class TestApplyConfigData:
"""Test applying config data updates URLs."""
dialog = SettingsDialog(sample_config)
qtbot.addWidget(dialog)
new_config = {
"app_name": "Test",
"app_version": "1.0.0",
@ -279,9 +286,9 @@ class TestApplyConfigData:
"window_height": 600,
"enable_logging": True,
}
dialog._apply_config_data(new_config)
items = [dialog.urls_list.item(i).text() for i in range(dialog.urls_list.count())]
assert "http://new.com" in items
assert "http://test.org" in items
@ -290,7 +297,7 @@ class TestApplyConfigData:
"""Test applying config data updates window size."""
dialog = SettingsDialog(sample_config)
qtbot.addWidget(dialog)
new_config = {
"app_name": "Test",
"app_version": "1.0.0",
@ -303,8 +310,8 @@ class TestApplyConfigData:
"window_height": 1024,
"enable_logging": True,
}
dialog._apply_config_data(new_config)
assert dialog.width_spin.value() == 1280
assert dialog.height_spin.value() == 1024