fix: Clean up obsolete translations in German language file

- Remove unused translation entries from translations.de.yaml
- Improve translation test to better detect obsolete entries
- Add KNOWN_NEEDED_MODULES for special cases
- Add helper function _message_exists_in_code for better translation verification
- Improve error messages to show both original and translated text
- Fix import sorting in test file

This commit improves the maintainability of the translation system by
removing unused entries and enhancing the verification process.
This commit is contained in:
Jens Bergmann
2025-03-12 21:43:12 +01:00
committed by Sebastian Thomschke
parent a6d2d2dc5a
commit 6bd5ba98d2
2 changed files with 76 additions and 59 deletions

View File

@@ -16,12 +16,12 @@ The tests work by:
4. Verifying no unused translations exist
"""
import ast, os
from collections import defaultdict
from dataclasses import dataclass
from importlib.resources import files
from collections import defaultdict
from ruamel.yaml import YAML
import pytest
from ruamel.yaml import YAML
from kleinanzeigen_bot import resources
@@ -30,6 +30,9 @@ EXCLUDED_MESSAGES: dict[str, set[str]] = {
"kleinanzeigen_bot/__init__.py": {"############################################"}
}
# Special modules that are known to be needed even if not in messages_by_file
KNOWN_NEEDED_MODULES = {'getopt.py'}
# Type aliases for better readability
ModulePath = str
FunctionName = str
@@ -273,6 +276,46 @@ def _find_translation(translations: TranslationDict,
return has_translation
def _message_exists_in_code(code_messages: dict[str, MessageDict],
module: str,
function: str,
message: str) -> bool:
"""
Check if a message exists in the code at the given location.
This is the reverse of _find_translation - it checks if a translation's message
exists in the code messages.
Args:
code_messages: Dictionary of all code messages
module: Module path
function: Function name
message: Message to find in code
Returns:
True if message exists in the code, False otherwise
"""
# Special case for getopt.py
if module == 'getopt.py':
return bool(code_messages.get(module, {}).get(function, {}).get(message))
# Remove kleinanzeigen_bot/ prefix if present for code message lookup
module_path = module[len('kleinanzeigen_bot/'):] if module.startswith('kleinanzeigen_bot/') else module
module_path = f'kleinanzeigen_bot/{module_path}'
# Check if module exists in code messages
module_msgs = code_messages.get(module_path)
if not module_msgs:
return False
# Check if function exists in module messages
function_msgs = module_msgs.get(function)
if not function_msgs:
return False
# Check if message exists in any of the function's message sets
return any(message in msg_dict for msg_dict in function_msgs.values())
@pytest.mark.parametrize("lang", _get_available_languages())
def test_all_log_messages_have_translations(lang: str) -> None:
"""
@@ -325,54 +368,51 @@ def test_no_obsolete_translations(lang: str) -> None:
Test that all translations in each language YAML file are actually used in the code.
This test ensures there are no obsolete translations that should be removed.
The translations file has the structure:
module:
function:
"original message": "translated message"
"""
messages_by_file = _get_all_log_messages()
translations = _get_translations_for_language(lang)
obsolete_items: list[tuple[str, str, str]] = []
for module, module_trans in translations.items():
# Add kleinanzeigen_bot/ prefix if not present
module_with_prefix = f'kleinanzeigen_bot/{module}' if not module.startswith('kleinanzeigen_bot/') else module
# Remove .py extension for comparison if present
module_no_ext = module_with_prefix[:-3] if module_with_prefix.endswith('.py') else module_with_prefix
if module_no_ext not in messages_by_file:
# Skip obsolete module check since we know these modules are needed
if not isinstance(module_trans, dict):
continue
# Skip known needed modules
if module in KNOWN_NEEDED_MODULES:
continue
code_messages = messages_by_file[module_no_ext]
for function, function_trans in module_trans.items():
if not isinstance(function_trans, dict):
continue
if function not in code_messages and function != 'module':
# Skip obsolete function check since we know these functions are needed
continue
for original_message in function_trans.keys():
# Check if this message exists in the code
message_exists = _message_exists_in_code(messages_by_file, module, function, original_message)
for trans_message in function_trans:
if function == 'module' or trans_message in code_messages.get(function, {}):
continue
obsolete_items.append((module, function, trans_message))
if not message_exists:
obsolete_items.append((module, function, original_message))
# Fail the test if obsolete translations are found
if obsolete_items:
obsolete_str = f"\nPlease remove the following obsolete translations for language [{lang}]:\n"
by_module: defaultdict[str, defaultdict[str, set[str]]] = defaultdict(lambda: defaultdict(set))
obsolete_str = f"\nObsolete translations found for language [{lang}]:\n"
# Group by module and function for better readability
by_module: defaultdict[str, defaultdict[str, list[str]]] = defaultdict(lambda: defaultdict(list))
for module, function, message in obsolete_items:
if module not in by_module:
by_module[module] = defaultdict(set)
if function not in by_module[module]:
by_module[module][function] = set()
by_module[module][function].add(message)
by_module[module][function].append(message)
for module, functions in sorted(by_module.items()):
obsolete_str += f" {module}:\n"
for function, messages in sorted(functions.items()):
if function:
obsolete_str += f" {function}:\n"
obsolete_str += f" {function}:\n"
for message in sorted(messages):
obsolete_str += f' "{message}"\n'
obsolete_str += f' "{message}": "{translations[module][function][message]}"\n'
raise AssertionError(obsolete_str)