mirror of
https://github.com/Second-Hand-Friends/kleinanzeigen-bot.git
synced 2026-03-12 10:31:50 +01:00
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:
committed by
Sebastian Thomschke
parent
a6d2d2dc5a
commit
6bd5ba98d2
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user