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

@@ -33,18 +33,15 @@ kleinanzeigen_bot/__init__.py:
load_ads:
"Searching for ad config files...": "Suche nach Anzeigendateien..."
" -> found %s": "-> %s gefunden"
"ad config file": "Anzeigendatei"
"Start fetch task for the ad(s) with id(s):": "Starte Abrufaufgabe für die Anzeige(n) mit ID(s):"
" -> SKIPPED: inactive ad [%s]": " -> ÜBERSPRUNGEN: inaktive Anzeige [%s]"
" -> SKIPPED: ad [%s] is not in list of given ids.": " -> ÜBERSPRUNGEN: Anzeige [%s] ist nicht in der Liste der angegebenen IDs."
" -> SKIPPED: ad [%s] is not new. already has an id assigned.": " -> ÜBERSPRUNGEN: Anzeige [%s] ist nicht neu. Eine ID wurde bereits zugewiesen."
"Category [%s] unknown. Using category [%s] with ID [%s] instead.": "Kategorie [%s] unbekannt. Verwende stattdessen Kategorie [%s] mit ID [%s]."
"Loaded %s": "%s geladen"
"ad": "Anzeige"
load_config:
" -> found %s": "-> %s gefunden"
"category": "Kategorie"
"config": "Konfiguration"
"Config file %s does not exist. Creating it with default values...": "Konfigurationsdatei %s existiert nicht. Erstelle sie mit Standardwerten..."
@@ -53,7 +50,6 @@ kleinanzeigen_bot/__init__.py:
"Already logged in as [%s]. Skipping login.": "Bereits eingeloggt als [%s]. Überspringe Anmeldung."
"Opening login page...": "Öffne Anmeldeseite..."
"# Captcha present! Please solve the captcha.": "# Captcha vorhanden! Bitte lösen Sie das Captcha."
"Logging in as [%s]...": "Anmeldung als [%s]..."
handle_after_login_logic:
"# Device verification message detected. Please follow the instruction displayed in the Browser.": "# Nachricht zur Geräteverifizierung erkannt. Bitte den Anweisungen im Browser folgen."
@@ -63,7 +59,6 @@ kleinanzeigen_bot/__init__.py:
delete_ads:
"Processing %s/%s: '%s' from [%s]...": "Verarbeite %s/%s: '%s' von [%s]..."
"DONE: Deleted %s": "FERTIG: %s gelöscht"
"ad": "Anzeige"
delete_ad:
"Deleting ad '%s' if already present...": "Lösche Anzeige '%s', falls bereits vorhanden..."
@@ -74,7 +69,6 @@ kleinanzeigen_bot/__init__.py:
"Processing %s/%s: '%s' from [%s]...": "Verarbeite %s/%s: '%s' von [%s]..."
"Skipping because ad is reserved": "Überspringen, da Anzeige reserviert ist"
"DONE: (Re-)published %s": "FERTIG: %s (erneut) veröffentlicht"
"ad": "Anzeige"
publish_ad:
"Publishing ad '%s'...": "Veröffentliche Anzeige '%s'..."
@@ -92,11 +86,9 @@ kleinanzeigen_bot/__init__.py:
__upload_images:
" -> found %s": "-> %s gefunden"
"image": "Bild"
" -> uploading image [%s]": " -> Lade Bild [%s] hoch"
__check_ad_republication:
"Changes detected in ad [%s], will republish": "Änderungen in Anzeige [%s] erkannt, wird erneut veröffentlicht"
" -> SKIPPED: ad [%s] was last published %d days ago. republication is only required every %s days": " -> ÜBERSPRUNGEN: Anzeige [%s] wurde zuletzt vor %d Tagen veröffentlicht. Erneute Veröffentlichung ist erst nach %s Tagen erforderlich"
__set_special_attributes:
@@ -116,12 +108,10 @@ kleinanzeigen_bot/__init__.py:
"Starting download of not yet downloaded ads...": "Starte den Download noch nicht heruntergeladener Anzeigen..."
"The ad with id %d has already been saved.": "Die Anzeige mit der ID %d wurde bereits gespeichert."
"%s were downloaded from your profile.": "%s wurden aus Ihrem Profil heruntergeladen."
"new ad": "neue Anzeige"
"Starting download of ad(s) with the id(s):": "Starte Download der Anzeige(n) mit den ID(s):"
"Downloaded ad with id %d": "Anzeige mit der ID %d heruntergeladen"
"The page with the id %d does not exist!": "Die Seite mit der ID %d existiert nicht!"
"%s found.": "%s gefunden."
"ad": "Anzeige"
parse_args:
"Use --help to display available options.": "Mit --help können die verfügbaren Optionen angezeigt werden."
@@ -134,8 +124,6 @@ kleinanzeigen_bot/__init__.py:
"DONE: No ads to delete found.": "FERTIG: Keine zu löschnenden Anzeigen gefunden."
"You provided no ads selector. Defaulting to \"new\".": "Es wurden keine Anzeigen-Selektor angegeben. Es wird \"new\" verwendet."
"Unknown command: %s": "Unbekannter Befehl: %s"
"%s found.": "%s gefunden."
" -> effective ad meta:": " -> effektive Anzeigen-Metadaten:"
fill_login_data_and_send:
"Logging in as [%s]...": "Anmeldung als [%s]..."
@@ -158,22 +146,20 @@ kleinanzeigen_bot/extract.py:
"No image area found. Continuing without downloading images.": "Keine Bildbereiche gefunden. Fahre ohne Bilder-Download fort."
extract_ad_id_from_ad_url:
"The ad ID could not be extracted from the given URL %s": "Die Anzeigen-ID konnte nicht aus der angegebenen URL extrahiert werden: %s"
"The ad ID could not be extracted from the given URL %s": "Die Anzeigen-ID konnte nicht aus der angegebenen URL %s extrahiert werden"
extract_own_ads_urls:
"There are currently no ads on your profile!": "Derzeit gibt es keine Anzeigen auf deinem Profil!"
"It looks like you have many ads!": "Es scheint viele Anzeigen zu geben!"
"It looks like all your ads fit on one overview page.": "Alle Anzeigen scheinen auf eine Übersichtsseite zu passen."
"Last ad overview page explored.": "Letzte Anzeigenübersichtsseite gesichtet."
"There are currently no ads on your profile!": "Es gibt derzeit keine Anzeigen in deinem Profil!"
"It looks like you have many ads!": "Es sieht so aus, als hättest du viele Anzeigen!"
"It looks like all your ads fit on one overview page.": "Es sieht so aus, als würden alle deine Anzeigen auf eine Übersichtsseite passen."
"Last ad overview page explored.": "Letzte Übersichtsseite erkundet."
naviagte_to_ad_page:
"There is no ad under the given ID.": "Es gibt keine Anzeige unter der angegebenen ID."
"A popup appeared!": "Ein Popup ist erschienen!"
_extract_ad_page_info:
"Extracting information from ad with title \"%s\"": "Extrahiere Informationen aus der Anzeige mit dem Titel \"%s\""
"NEXT button in image gallery somehow missing, aborting image fetching.": "WEITER-Button in der Bildergalerie fehlt, breche Bildabruf ab."
"No image area found. Continuing without downloading images.": "Keine Bildbereiche gefunden. Fahre ohne Bilder-Download fort."
"Extracting information from ad with title \"%s\"": "Extrahiere Informationen aus Anzeige mit Titel \"%s\""
_extract_contact_from_ad_page:
"No street given in the contact.": "Keine Straße in den Kontaktdaten angegeben."
@@ -191,8 +177,6 @@ kleinanzeigen_bot/utils/error_handlers.py:
#################################################
on_sigint:
"Aborted on user request.": "Auf Benutzeranfrage abgebrochen."
handle_error:
"%s: %s": "%s: %s"
on_exception:
"%s: %s": "%s: %s"
@@ -201,7 +185,6 @@ kleinanzeigen_bot/utils/dicts.py:
#################################################
load_dict_if_exists:
"Loading %s[%s]...": "Lade %s[%s]..."
"Loading %s[%s.%s]...": "Lade %s[%s.%s]..."
" from ": " von "
"Unsupported file type. The filename \"%s\" must end with *.json, *.yaml, or *.yml": "Nicht unterstützter Dateityp. Der Dateiname \"%s\" muss mit *.json, *.yaml oder *.yml enden"
save_dict:
@@ -214,9 +197,6 @@ kleinanzeigen_bot/utils/web_scraping_mixin.py:
#################################################
create_browser_session:
"Creating Browser session...": "Erstelle Browser-Sitzung..."
"Closing Browser session...": "Schließe Browser-Sitzung..."
"Installed browser could not be detected": "Installierter Browser konnte nicht erkannt werden"
"Installed browser for OS %s could not be detected": "Installierter Browser für Betriebssystem %s konnte nicht erkannt werden"
"Using existing browser process at %s:%s": "Verwende existierenden Browser-Prozess unter %s:%s"
"New Browser session is %s": "Neue Browser-Sitzung ist %s"
" -> Browser binary location: %s": " -> Browser-Programmpfad: %s"
@@ -224,10 +204,7 @@ kleinanzeigen_bot/utils/web_scraping_mixin.py:
" -> Browser user data dir: %s": " -> Browser-Benutzerdatenverzeichnis: %s"
" -> Custom Browser argument: %s": " -> Benutzerdefiniertes Browser-Argument: %s"
" -> Setting chrome prefs [%s]...": " -> Setze Chrome-Einstellungen [%s]..."
" -> Opening [%s]...": " -> Öffne [%s]..."
" -> Adding Browser extension: [%s]": " -> Füge Browser-Erweiterung hinzu: [%s]"
" -> HTTP %s [%s]...": " -> HTTP %s [%s]..."
" => skipping, [%s] is already open": " => überspringe, [%s] ist bereits geöffnet"
web_check:
"Unsupported attribute: %s": "Nicht unterstütztes Attribut: %s"

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)