From cd6a953cc61ff78b8c091659e6960252225d4cc9 Mon Sep 17 00:00:00 2001 From: nawaaaaaAaar <198336470+nawaaaaaAaar@users.noreply.github.com> Date: Mon, 5 Oct 2026 09:11:50 +0000 Subject: [PATCH 1/2] Add German translation with key and placeholder checks --- .../rohlikcz/translations/de.json | 164 ++++++++++++++++++ tests/test_translations.py | 38 ++++ 2 files changed, 202 insertions(+) create mode 100644 custom_components/rohlikcz/translations/de.json create mode 100644 tests/test_translations.py diff --git a/custom_components/rohlikcz/translations/de.json b/custom_components/rohlikcz/translations/de.json new file mode 100644 index 0000000..05534f9 --- /dev/null +++ b/custom_components/rohlikcz/translations/de.json @@ -0,0 +1,164 @@ +{ + "config": { + "step": { + "user": { + "title": "Rohlík-Konto", + "description": "Wähle deinen Shop und gib die zugehörigen Zugangsdaten ein.", + "data": { + "site": "Shop", + "email": "E-Mail", + "password": "Passwort" + }, + "data_description": { + "site": "Der Shop der Rohlík-Gruppe, bei dem du ein Konto hast.", + "email": "Die E-Mail-Adresse deines Shop-Kontos", + "password": "Das Passwort deines Shop-Kontos" + } + }, + "analytics": { + "title": "Ausgabenanalyse", + "description": "Wähle die Auswertungen aus, die du verfolgen möchtest. Für jede ausgewählte Ebene werden Sensoren für dieses Jahr und den gesamten Zeitraum erstellt. Wenn du eine Option auswählst, wird dein Bestellverlauf einmalig heruntergeladen (dies kann einige Minuten dauern).", + "data": { + "analytics": "Analyseebenen", + "top_n": "Anzahl der Top-Artikel in Sensoren", + "hide_discontinued": "Nicht mehr angebotene Produkte ausblenden" + }, + "data_description": { + "top_n": "Wie viele Artikel mit den höchsten Ausgaben in den Sensorattributen angezeigt werden. Standard: 10.", + "hide_discontinued": "Nicht mehr angebotene Produkte aus den Top-Kategorien und Top-Artikeln ausschließen." + } + }, + "reauth_confirm": { + "title": "Rohlík-Konto erneut authentifizieren", + "description": "Die gespeicherten Zugangsdaten für {email} sind nicht mehr gültig. Gib das Passwort erneut ein.", + "data": { + "password": "Passwort" + } + }, + "reconfigure": { + "title": "Shop wechseln", + "description": "Wähle den Shop aus, zu dem das Konto {email} gehört, und gib das Passwort ein.", + "data": { + "site": "Shop", + "password": "Passwort" + } + } + }, + "error": { + "cannot_connect": "Verbindung zum Shop fehlgeschlagen", + "invalid_auth": "Ungültige E-Mail-Adresse oder ungültiges Passwort", + "unknown": "Unerwarteter Fehler" + }, + "abort": { + "already_configured": "Das Konto ist bereits eingerichtet", + "reauth_successful": "Erneute Authentifizierung erfolgreich", + "wrong_account": "Die Zugangsdaten gehören zu einem anderen Rohlík-Konto", + "reconfigure_successful": "Das Konto wurde zum ausgewählten Shop verschoben" + } + }, + "options": { + "step": { + "init": { + "title": "Ausgabenanalyse", + "description": "Wähle die Auswertungen aus, die du verfolgen möchtest. Änderungen werden nach dem Neuladen wirksam.", + "data": { + "analytics": "Analyseebenen", + "top_n": "Anzahl der Top-Artikel in Sensoren", + "hide_discontinued": "Nicht mehr angebotene Produkte ausblenden" + }, + "data_description": { + "top_n": "Wie viele Artikel mit den höchsten Ausgaben in den Sensorattributen angezeigt werden. Standard: 10.", + "hide_discontinued": "Nicht mehr angebotene Produkte aus den Top-Kategorien und Top-Artikeln ausschließen." + } + } + } + }, + "selector": { + "site": { + "options": { + "cz": "Rohlík.cz (Tschechien)", + "de": "Knuspr.de (Deutschland)", + "at": "Gurkerl.at (Österreich)", + "hu": "Kifli.hu (Ungarn)", + "ro": "Sezamo.ro (Rumänien)" + } + }, + "analytics": { + "options": { + "categories_l0": "Hauptkategorien (z. B. Getränke, Drogerie, Obst und Gemüse)", + "categories_l1": "Unterkategorien (z. B. Heißgetränke, Reinigungsmittel, Gemüse)", + "categories_l2": "Detaillierte Kategorien (z. B. Kaffee, Universalreiniger, Tomaten)", + "categories_l3": "Spezifischste Kategorien (z. B. Kaffeebohnen, Sprühreiniger)", + "per_item": "Ausgaben je Artikel (z. B. Tchibo Barista, Savo Spray)" + } + } + }, + "entity": { + "binary_sensor": { + "is_express_available": { + "name": "Expresslieferung verfügbar", + "state": {"on": "Ja", "off": "Nein"} + }, + "is_reusable": { + "name": "Mehrwegtaschen", + "state": {"on": "Ja", "off": "Nein"} + }, + "is_parent": { + "name": "Elternclub", + "state": {"on": "Ja", "off": "Nein"} + }, + "is_premium": { + "name": "Xtra-Mitgliedschaft", + "state": {"on": "Ja", "off": "Nein"} + }, + "is_ordered": { + "name": "Nächste Bestellung aufgegeben", + "state": {"on": "Ja", "off": "Nein"} + }, + "is_reserved": { + "name": "Lieferzeitfenster reserviert", + "state": {"on": "Ja", "off": "Nein"} + } + }, + "sensor": { + "first_delivery": {"name": "Nächste Lieferung"}, + "account_id": {"name": "Konto-ID"}, + "email": {"name": "E-Mail"}, + "phone": {"name": "Telefon"}, + "no_limit": {"name": "Bestellungen ohne Mindestbestellwert"}, + "free_express": {"name": "Kostenlose Expresslieferungen"}, + "credit_amount": {"name": "Guthaben"}, + "bags_amount": {"name": "Anzahl der Taschen", "unit_of_measurement": "Stk."}, + "premium_days": {"name": "Verbleibende Xtra-Tage"}, + "cart_price": {"name": "Warenkorbwert"}, + "updated": {"name": "Zuletzt aktualisiert"}, + "last_order": {"name": "Letzte Bestellung"}, + "next_order_since": {"name": "Beginn des Lieferzeitfensters"}, + "next_order_till": {"name": "Ende des Lieferzeitfensters"}, + "express_slot": {"name": "Express-Lieferzeitfenster"}, + "standard_slot": {"name": "Standard-Lieferzeitfenster"}, + "eco_slot": {"name": "Eco-Lieferzeitfenster"}, + "delivery_info": {"name": "Lieferinformationen"}, + "delivery_time": {"name": "Lieferzeit"}, + "monthly_spent": {"name": "Ausgaben diesen Monat"}, + "yearly_spent": {"name": "Ausgaben dieses Jahr"}, + "alltime_spent": {"name": "Ausgaben insgesamt"}, + "categories_l0_this_year": {"name": "Top-Kategorien dieses Jahr"}, + "categories_l0_all_time": {"name": "Top-Kategorien insgesamt"}, + "categories_this_year": {"name": "Kategorien dieses Jahr"}, + "categories_all_time": {"name": "Kategorien insgesamt"}, + "categories_l2_this_year": {"name": "Detaillierte Kategorien dieses Jahr"}, + "categories_l2_all_time": {"name": "Detaillierte Kategorien insgesamt"}, + "categories_l3_this_year": {"name": "Spezifische Kategorien dieses Jahr"}, + "categories_l3_all_time": {"name": "Spezifische Kategorien insgesamt"}, + "items_this_year": {"name": "Artikel dieses Jahr"}, + "items_all_time": {"name": "Artikel insgesamt"} + }, + "todo": { + "shopping_cart": {"name": "Warenkorb"} + }, + "calendar": { + "delivery_calendar": {"name": "Bestellkalender"} + } + } +} diff --git a/tests/test_translations.py b/tests/test_translations.py new file mode 100644 index 0000000..dd4f91f --- /dev/null +++ b/tests/test_translations.py @@ -0,0 +1,38 @@ +"""Offline translation key and interpolation checks, also runnable with unittest.""" + +import json +from pathlib import Path +from string import Formatter +import unittest + + +ROOT = Path(__file__).resolve().parents[1] / "custom_components/rohlikcz/translations" + + +def strings(document, path=()): + if isinstance(document, dict): + for key, value in document.items(): + yield from strings(value, (*path, key)) + else: + yield path, document + + +class TestGermanTranslation(unittest.TestCase): + def test_complete_string_tree(self): + english = dict(strings(json.loads((ROOT / "en.json").read_text(encoding="utf-8")))) + german = dict(strings(json.loads((ROOT / "de.json").read_text(encoding="utf-8")))) + self.assertEqual(english.keys(), german.keys()) + for path, value in german.items(): + with self.subTest(path=path): + self.assertIsInstance(value, str) + self.assertTrue(value.strip()) + + def test_placeholders_preserved(self): + formatter = Formatter() + english = dict(strings(json.loads((ROOT / "en.json").read_text(encoding="utf-8")))) + german = dict(strings(json.loads((ROOT / "de.json").read_text(encoding="utf-8")))) + for path, value in english.items(): + with self.subTest(path=path): + original = [(field, spec, conversion) for _, field, spec, conversion in formatter.parse(value) if field] + translated = [(field, spec, conversion) for _, field, spec, conversion in formatter.parse(german[path]) if field] + self.assertEqual(original, translated) From 91750e974ab78ca593b0b1085d9c38820db8c37e Mon Sep 17 00:00:00 2001 From: nawaaaaaAaar <198336470+nawaaaaaAaar@users.noreply.github.com> Date: Mon, 5 Oct 2026 09:45:40 +0000 Subject: [PATCH 2/2] Match translation formatting and name missing or extra keys --- .../rohlikcz/translations/de.json | 167 +++++++++++++----- tests/test_translations.py | 3 +- 2 files changed, 129 insertions(+), 41 deletions(-) diff --git a/custom_components/rohlikcz/translations/de.json b/custom_components/rohlikcz/translations/de.json index 05534f9..c28d1f6 100644 --- a/custom_components/rohlikcz/translations/de.json +++ b/custom_components/rohlikcz/translations/de.json @@ -97,68 +97,155 @@ "binary_sensor": { "is_express_available": { "name": "Expresslieferung verfügbar", - "state": {"on": "Ja", "off": "Nein"} + "state": { + "on": "Ja", + "off": "Nein" + } }, "is_reusable": { "name": "Mehrwegtaschen", - "state": {"on": "Ja", "off": "Nein"} + "state": { + "on": "Ja", + "off": "Nein" + } }, "is_parent": { "name": "Elternclub", - "state": {"on": "Ja", "off": "Nein"} + "state": { + "on": "Ja", + "off": "Nein" + } }, "is_premium": { "name": "Xtra-Mitgliedschaft", - "state": {"on": "Ja", "off": "Nein"} + "state": { + "on": "Ja", + "off": "Nein" + } }, "is_ordered": { "name": "Nächste Bestellung aufgegeben", - "state": {"on": "Ja", "off": "Nein"} + "state": { + "on": "Ja", + "off": "Nein" + } }, "is_reserved": { "name": "Lieferzeitfenster reserviert", - "state": {"on": "Ja", "off": "Nein"} + "state": { + "on": "Ja", + "off": "Nein" + } } }, "sensor": { - "first_delivery": {"name": "Nächste Lieferung"}, - "account_id": {"name": "Konto-ID"}, - "email": {"name": "E-Mail"}, - "phone": {"name": "Telefon"}, - "no_limit": {"name": "Bestellungen ohne Mindestbestellwert"}, - "free_express": {"name": "Kostenlose Expresslieferungen"}, - "credit_amount": {"name": "Guthaben"}, - "bags_amount": {"name": "Anzahl der Taschen", "unit_of_measurement": "Stk."}, - "premium_days": {"name": "Verbleibende Xtra-Tage"}, - "cart_price": {"name": "Warenkorbwert"}, - "updated": {"name": "Zuletzt aktualisiert"}, - "last_order": {"name": "Letzte Bestellung"}, - "next_order_since": {"name": "Beginn des Lieferzeitfensters"}, - "next_order_till": {"name": "Ende des Lieferzeitfensters"}, - "express_slot": {"name": "Express-Lieferzeitfenster"}, - "standard_slot": {"name": "Standard-Lieferzeitfenster"}, - "eco_slot": {"name": "Eco-Lieferzeitfenster"}, - "delivery_info": {"name": "Lieferinformationen"}, - "delivery_time": {"name": "Lieferzeit"}, - "monthly_spent": {"name": "Ausgaben diesen Monat"}, - "yearly_spent": {"name": "Ausgaben dieses Jahr"}, - "alltime_spent": {"name": "Ausgaben insgesamt"}, - "categories_l0_this_year": {"name": "Top-Kategorien dieses Jahr"}, - "categories_l0_all_time": {"name": "Top-Kategorien insgesamt"}, - "categories_this_year": {"name": "Kategorien dieses Jahr"}, - "categories_all_time": {"name": "Kategorien insgesamt"}, - "categories_l2_this_year": {"name": "Detaillierte Kategorien dieses Jahr"}, - "categories_l2_all_time": {"name": "Detaillierte Kategorien insgesamt"}, - "categories_l3_this_year": {"name": "Spezifische Kategorien dieses Jahr"}, - "categories_l3_all_time": {"name": "Spezifische Kategorien insgesamt"}, - "items_this_year": {"name": "Artikel dieses Jahr"}, - "items_all_time": {"name": "Artikel insgesamt"} + "first_delivery": { + "name": "Nächste Lieferung" + }, + "account_id": { + "name": "Konto-ID" + }, + "email": { + "name": "E-Mail" + }, + "phone": { + "name": "Telefon" + }, + "no_limit": { + "name": "Bestellungen ohne Mindestbestellwert" + }, + "free_express": { + "name": "Kostenlose Expresslieferungen" + }, + "credit_amount": { + "name": "Guthaben" + }, + "bags_amount": { + "name": "Anzahl der Taschen", + "unit_of_measurement": "Stk." + }, + "premium_days": { + "name": "Verbleibende Xtra-Tage" + }, + "cart_price": { + "name": "Warenkorbwert" + }, + "updated": { + "name": "Zuletzt aktualisiert" + }, + "last_order": { + "name": "Letzte Bestellung" + }, + "next_order_since": { + "name": "Beginn des Lieferzeitfensters" + }, + "next_order_till": { + "name": "Ende des Lieferzeitfensters" + }, + "express_slot": { + "name": "Express-Lieferzeitfenster" + }, + "standard_slot": { + "name": "Standard-Lieferzeitfenster" + }, + "eco_slot": { + "name": "Eco-Lieferzeitfenster" + }, + "delivery_info": { + "name": "Lieferinformationen" + }, + "delivery_time": { + "name": "Lieferzeit" + }, + "monthly_spent": { + "name": "Ausgaben diesen Monat" + }, + "yearly_spent": { + "name": "Ausgaben dieses Jahr" + }, + "alltime_spent": { + "name": "Ausgaben insgesamt" + }, + "categories_l0_this_year": { + "name": "Top-Kategorien dieses Jahr" + }, + "categories_l0_all_time": { + "name": "Top-Kategorien insgesamt" + }, + "categories_this_year": { + "name": "Kategorien dieses Jahr" + }, + "categories_all_time": { + "name": "Kategorien insgesamt" + }, + "categories_l2_this_year": { + "name": "Detaillierte Kategorien dieses Jahr" + }, + "categories_l2_all_time": { + "name": "Detaillierte Kategorien insgesamt" + }, + "categories_l3_this_year": { + "name": "Spezifische Kategorien dieses Jahr" + }, + "categories_l3_all_time": { + "name": "Spezifische Kategorien insgesamt" + }, + "items_this_year": { + "name": "Artikel dieses Jahr" + }, + "items_all_time": { + "name": "Artikel insgesamt" + } }, "todo": { - "shopping_cart": {"name": "Warenkorb"} + "shopping_cart": { + "name": "Warenkorb" + } }, "calendar": { - "delivery_calendar": {"name": "Bestellkalender"} + "delivery_calendar": { + "name": "Bestellkalender" + } } } } diff --git a/tests/test_translations.py b/tests/test_translations.py index dd4f91f..3d59e35 100644 --- a/tests/test_translations.py +++ b/tests/test_translations.py @@ -21,7 +21,8 @@ class TestGermanTranslation(unittest.TestCase): def test_complete_string_tree(self): english = dict(strings(json.loads((ROOT / "en.json").read_text(encoding="utf-8")))) german = dict(strings(json.loads((ROOT / "de.json").read_text(encoding="utf-8")))) - self.assertEqual(english.keys(), german.keys()) + self.assertEqual(set(english) - set(german), set(), "missing keys in de") + self.assertEqual(set(german) - set(english), set(), "extra keys in de") for path, value in german.items(): with self.subTest(path=path): self.assertIsInstance(value, str)