feat(14-05): add tenderRadar i18n namespace (DE + EN) with key-parity guard
Introduces the tenderRadar top-level namespace in de.json/en.json covering page, results, filter (incl. Bundesland/CPV division labels), savedSearch, detail, coverage, settings, sourceConfig, rssFeeds and emailAlerts groups. EN translations authored with consistent Vergabe-domain terminology (Ausschreibung->tender, Vergabestelle->contracting authority, Frist-> deadline, Auftragswert->estimated value). tenderRadar-parity.spec.ts enforces recursively-flattened de/en key-set equality so no follow-up edit can silently add a string to only one locale. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -571,5 +571,258 @@
|
||||
"wrongPassword": "Falsches Passwort. PFX/P12-Datei konnte nicht entschluesselt werden.",
|
||||
"unknownFormat": "Unbekanntes Format. Die Datei konnte nicht als Zertifikat erkannt werden."
|
||||
}
|
||||
},
|
||||
"tenderRadar": {
|
||||
"page": {
|
||||
"title": "Ausschreibungs-Radar",
|
||||
"loading": "Lädt…"
|
||||
},
|
||||
"results": {
|
||||
"colTitle": "Titel",
|
||||
"colBuyer": "Auftraggeber",
|
||||
"colDeadline": "Frist",
|
||||
"colValue": "Wert",
|
||||
"colPublished": "Veröffentlicht",
|
||||
"colTriage": "Triage",
|
||||
"noValue": "keine Wertangabe",
|
||||
"error": "Ausschreibungen konnten nicht geladen werden.",
|
||||
"emptyTitle": "Keine Ausschreibungen gefunden.",
|
||||
"emptyBody": "Passen Sie die Filter an oder deaktivieren Sie „nur noch offene\".",
|
||||
"markUnread": "Als ungelesen markieren",
|
||||
"markRead": "Als gelesen markieren",
|
||||
"readLabel": "Gelesen",
|
||||
"unreadLabel": "Ungelesen",
|
||||
"removeFavorite": "Favorit entfernen",
|
||||
"setFavorite": "Favorit setzen",
|
||||
"favoriteOn": "★ Favorit",
|
||||
"favoriteOff": "☆ Favorit",
|
||||
"showingRange": "Zeige {from}–{to} von {total}",
|
||||
"prevPage": "← Zurück",
|
||||
"nextPage": "Weiter →"
|
||||
},
|
||||
"filter": {
|
||||
"sortPublished": "Veröffentlichungsdatum",
|
||||
"sortDeadline": "Abgabefrist",
|
||||
"sortValue": "Auftragswert",
|
||||
"searchLabel": "Suche",
|
||||
"searchPlaceholder": "Titel oder Auftraggeber…",
|
||||
"sortLabel": "Sortierung",
|
||||
"openOnlyLabel": "Nur noch offene",
|
||||
"favOnlyLabel": "Nur Favoriten/Merkliste",
|
||||
"deadlineFromLabel": "Abgabefrist von",
|
||||
"deadlineToLabel": "Abgabefrist bis",
|
||||
"plzLabel": "PLZ",
|
||||
"plzPlaceholder": "z. B. 10",
|
||||
"bundeslandLabel": "Bundesland",
|
||||
"bundeslandAll": "Alle Bundesländer",
|
||||
"cpvLabel": "Branche (CPV)",
|
||||
"cpvPlaceholder": "z. B. Bau, IT, Gesundheit…",
|
||||
"cpvRemoveAria": "Division {code} entfernen",
|
||||
"valueMinLabel": "Auftragswert von (€)",
|
||||
"valueMinPlaceholder": "min.",
|
||||
"valueMaxLabel": "Auftragswert bis (€)",
|
||||
"valueMaxPlaceholder": "max.",
|
||||
"includeNullValueLabel": "Ohne Wertangabe einschließen",
|
||||
"bundeslandOptions": {
|
||||
"DE1": "Baden-Württemberg",
|
||||
"DE2": "Bayern",
|
||||
"DE3": "Berlin",
|
||||
"DE4": "Brandenburg",
|
||||
"DE5": "Bremen",
|
||||
"DE6": "Hamburg",
|
||||
"DE7": "Hessen",
|
||||
"DE8": "Mecklenburg-Vorpommern",
|
||||
"DE9": "Niedersachsen",
|
||||
"DEA": "Nordrhein-Westfalen",
|
||||
"DEB": "Rheinland-Pfalz",
|
||||
"DEC": "Saarland",
|
||||
"DED": "Sachsen",
|
||||
"DEE": "Sachsen-Anhalt",
|
||||
"DEF": "Schleswig-Holstein",
|
||||
"DEG": "Thüringen"
|
||||
},
|
||||
"cpvDivisions": {
|
||||
"03": "Landwirtschaftliche, fischereiwirtschaftliche, forstwirtschaftliche und verwandte Erzeugnisse",
|
||||
"09": "Mineralölerzeugnisse, Brennstoffe, Elektrizität und andere Energiequellen",
|
||||
"14": "Bergbauerzeugnisse, Grundstoffe und verwandte Erzeugnisse",
|
||||
"15": "Nahrungsmittel, Getränke, Tabak und verwandte Erzeugnisse",
|
||||
"16": "Land- und forstwirtschaftliche Maschinen",
|
||||
"18": "Bekleidung, Schuhe, Reiseartikel und Zubehör",
|
||||
"19": "Leder- und Textilwaren, Kunststoff- und Gummierzeugnisse",
|
||||
"22": "Druckerzeugnisse und verwandte Erzeugnisse",
|
||||
"24": "Chemische Erzeugnisse",
|
||||
"30": "Büromaschinen, Datenverarbeitungsgeräte, -ausrüstung und -material, ausgenommen Möbel und Softwarepakete",
|
||||
"31": "Elektrische Maschinen, Geräte, Ausrüstungen und Verbrauchsartikel; Beleuchtung",
|
||||
"32": "Rundfunk-, Fernseh-, Kommunikations-, Telekommunikations- und verwandte Geräte",
|
||||
"33": "Medizinische Apparate, pharmazeutische Erzeugnisse und Körperpflegeprodukte",
|
||||
"34": "Transportausrüstung und Transportzubehör",
|
||||
"35": "Sicherheits-, Feuerlösch-, Polizei- und Verteidigungsausrüstung",
|
||||
"37": "Musikinstrumente, Sportartikel, Spiele, Spielzeug, Kunsthandwerk, Kunstmaterial und Zubehör",
|
||||
"38": "Laborgeräte, optische und Präzisionsgeräte (ausgenommen Gläser)",
|
||||
"39": "Möbel (einschließlich Büromöbel), Einrichtungsgegenstände, Haushaltsgeräte (ausgenommen Beleuchtung) und Reinigungsmittel",
|
||||
"41": "Gesammeltes und gereinigtes Wasser",
|
||||
"42": "Industriemaschinen",
|
||||
"43": "Maschinen für Bergbau, Steinbruch, Bauwesen",
|
||||
"44": "Baustoffe und zugehörige Artikel",
|
||||
"45": "Bauarbeiten",
|
||||
"48": "Softwarepaket und Informationssysteme",
|
||||
"50": "Reparatur- und Wartungsdienste",
|
||||
"51": "Installationsdienste (ausgenommen Software)",
|
||||
"55": "Beherbergungs-, Gaststätten- und Handelsdienstleistungen",
|
||||
"60": "Transportdienstleistungen (ohne Abfalltransport)",
|
||||
"63": "Neben- und Hilfstätigkeiten im Verkehr; Reisebüros",
|
||||
"64": "Post- und Telekommunikationsdienste",
|
||||
"65": "Versorgungsbetriebe",
|
||||
"66": "Finanz- und Versicherungsdienstleistungen",
|
||||
"70": "Dienstleistungen im Zusammenhang mit Immobilien",
|
||||
"71": "Architektur-, Bau-, Ingenieur- und Prüfdienste",
|
||||
"72": "IT-Dienste: Beratung, Software-Entwicklung, Internet und Hilfestellung",
|
||||
"73": "Forschungs- und Entwicklungsdienste sowie zugehörige Beratung",
|
||||
"75": "Öffentliche Verwaltung, Verteidigung und Sozialversicherung",
|
||||
"76": "Dienstleistungen im Zusammenhang mit der Öl- und Gasindustrie",
|
||||
"77": "Landwirtschaftliche, forstwirtschaftliche, gartenbauliche Dienste, Aquakultur und Imkerei",
|
||||
"79": "Unternehmensbezogene Dienstleistungen: Recht, Marketing, Consulting, Personal, Druck und Sicherheit",
|
||||
"80": "Dienstleistungen des Bildungswesens und der Ausbildung",
|
||||
"85": "Dienste im Gesundheits- und Sozialwesen",
|
||||
"90": "Abwasser- und Abfallbeseitigung, Reinigung und Umweltschutzdienste",
|
||||
"92": "Dienstleistungen der Freizeitgestaltung, der Kultur und des Sports",
|
||||
"98": "Sonstige Gemeinschafts-, Sozial- und Individualdienstleistungen"
|
||||
}
|
||||
},
|
||||
"savedSearch": {
|
||||
"title": "Suchprofile",
|
||||
"saveAsLabel": "Aktuelle Filter speichern als",
|
||||
"namePlaceholder": "Name des Suchprofils…",
|
||||
"save": "Speichern",
|
||||
"cancel": "Abbrechen",
|
||||
"loading": "Lädt…",
|
||||
"empty": "Noch keine Suchprofile gespeichert.",
|
||||
"loadTitle": "Suchprofil laden",
|
||||
"instantAlertTitle": "Sofort-Alert per E-Mail bei neuen Treffern dieses Profils",
|
||||
"instantAlertLabel": "Sofort-Alert",
|
||||
"instantAlertAria": "Sofort-Alert für {name}",
|
||||
"renameTitle": "Umbenennen",
|
||||
"renameAria": "{name} umbenennen",
|
||||
"renameConfirmAria": "{name} umbenennen bestätigen",
|
||||
"renameCancelAria": "Umbenennen abbrechen",
|
||||
"deleteTitle": "Löschen",
|
||||
"deleteAria": "{name} löschen",
|
||||
"errorLoad": "Suchprofile konnten nicht geladen werden.",
|
||||
"errorSave": "Suchprofil konnte nicht gespeichert werden.",
|
||||
"errorRename": "Suchprofil konnte nicht umbenannt werden.",
|
||||
"errorDelete": "Suchprofil konnte nicht gelöscht werden.",
|
||||
"errorToggleAlert": "Sofort-Alert konnte nicht umgeschaltet werden."
|
||||
},
|
||||
"detail": {
|
||||
"dialogAria": "Ausschreibungsdetails",
|
||||
"title": "Ausschreibungsdetails",
|
||||
"close": "Schließen",
|
||||
"loading": "Lädt…",
|
||||
"error": "Ausschreibung konnte nicht geladen werden.",
|
||||
"fieldTitle": "Titel",
|
||||
"fieldBuyer": "Auftraggeber",
|
||||
"fieldRegion": "Region / PLZ / Bundesland",
|
||||
"fieldCpv": "CPV-Codes",
|
||||
"noCpv": "keine CPV-Codes angegeben",
|
||||
"fieldDeadline": "Abgabefrist",
|
||||
"noDeadline": "keine Frist angegeben",
|
||||
"fieldValue": "Geschätzter Auftragswert",
|
||||
"noValue": "keine Wertangabe",
|
||||
"fieldProcedureType": "Verfahrensart",
|
||||
"fieldStatus": "Status",
|
||||
"fieldPublished": "Veröffentlicht",
|
||||
"mirrorNotice": "Vergabeunterlagen werden nicht lokal gespiegelt — der folgende Link führt direkt zum Original auf dem Quellportal.",
|
||||
"sourceLink": "{portal}: Zur Ausschreibung auf dem Quellportal →",
|
||||
"noSourceLink": "{portal}: Kein Quell-Link verfügbar.",
|
||||
"sourceLinkFallback": "Zur Ausschreibung auf dem Quellportal →",
|
||||
"noSourceLinkFallback": "Kein Quell-Link verfügbar."
|
||||
},
|
||||
"coverage": {
|
||||
"label": "Datenabdeckung:",
|
||||
"text": "Aktuell werden nur EU-weite Oberschwellen-Ausschreibungen aus der zentralen DÖE-Quelle erfasst. Unterschwellen-Vergaben einzelner Vergabestellen fehlen noch — eine kurze Trefferliste ist daher kein Fehler.",
|
||||
"denylistLabel": "Manuell beobachten:",
|
||||
"denylistText": "Diese Portale lassen laut ihren Nutzungsbedingungen keinen automatisierten Zugriff zu und müssen daher manuell geprüft werden:"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Ausschreibungs-Radar — Einstellungen",
|
||||
"rssSectionTitle": "RSS-Feeds",
|
||||
"rssSectionBody": "Öffentliche, plattformweite RSS-Quellen (z. B. service.bund.de oder eine subreport-elvis-Kommunalfeed) — gilt für alle Mandanten gleich, nicht pro Mandant konfigurierbar.",
|
||||
"emailSectionTitle": "E-Mail-Alerts",
|
||||
"emailSectionBody": "Postfach, in das Vergabeportale Ihre Benachrichtigungsmails schicken — pro Mandant konfigurierbar, getrennt vom DKV-Rechnungspostfach. Aus diesen Mails erkannte Ausschreibungen sind nur für Ihren Mandanten sichtbar.",
|
||||
"notificationsSectionTitle": "Benachrichtigungen",
|
||||
"digestIntervalLabel": "Digest-Intervall",
|
||||
"digestDaily": "Täglich",
|
||||
"digestWeekly": "Wöchentlich",
|
||||
"digestOff": "Aus",
|
||||
"digestHelp": "Fasst neue Treffer aus Ihren Suchprofilen in einer täglichen oder wöchentlichen Sammel-Mail zusammen. „Aus\" deaktiviert den Digest — Sofort-Alerts pro Suchprofil bleiben unabhängig davon nutzbar.",
|
||||
"saveSuccess": "Einstellung gespeichert.",
|
||||
"errorLoad": "Benachrichtigungseinstellung konnte nicht geladen werden",
|
||||
"errorSave": "Benachrichtigungseinstellung konnte nicht gespeichert werden"
|
||||
},
|
||||
"sourceConfig": {
|
||||
"errorLoad": "Konfiguration konnte nicht geladen werden",
|
||||
"errorSave": "Einstellungen konnten nicht gespeichert werden",
|
||||
"validationError": "Intervall muss zwischen {min} und {max} Minuten liegen.",
|
||||
"sourceLabel": "Quelle",
|
||||
"lastIngestedLabel": "Zuletzt erfasster Tag",
|
||||
"neverIngested": "Noch keine Erfassung",
|
||||
"pollIntervalLabel": "Abrufintervall (Minuten) *",
|
||||
"minutesSuffix": "Min.",
|
||||
"pollIntervalHelp": "Cron-Taktfrequenz ({min}–{max} Min.) — der eigentliche DÖE-Abruf ist zusätzlich über den Tages-Cursor gedrosselt.",
|
||||
"activeLabel": "Aktiv",
|
||||
"saving": "Speichert...",
|
||||
"save": "Speichern",
|
||||
"saveSuccess": "Einstellungen gespeichert."
|
||||
},
|
||||
"rssFeeds": {
|
||||
"errorLoad": "RSS-Feeds konnten nicht geladen werden",
|
||||
"errorAdd": "RSS-Feed konnte nicht gespeichert werden",
|
||||
"errorRemove": "RSS-Feed konnte nicht entfernt werden",
|
||||
"validationRequired": "URL und Bezeichnung sind erforderlich.",
|
||||
"empty": "Noch keine RSS-Feeds hinterlegt.",
|
||||
"activeLabel": "aktiv",
|
||||
"inactiveLabel": "inaktiv",
|
||||
"removeAria": "RSS-Feed \"{label}\" entfernen",
|
||||
"removing": "Entfernt...",
|
||||
"remove": "Entfernen",
|
||||
"urlLabel": "Feed-URL *",
|
||||
"urlPlaceholder": "https://www.service.bund.de/.../RSSGenerator_Ausschreibungen.xml",
|
||||
"labelLabel": "Bezeichnung *",
|
||||
"labelPlaceholder": "z. B. service-bund",
|
||||
"adding": "Wird hinzugefügt...",
|
||||
"add": "Feed hinzufügen"
|
||||
},
|
||||
"emailAlerts": {
|
||||
"notSaved": "Noch nicht gespeichert — Postfach-Konfiguration ausfüllen und speichern.",
|
||||
"protocolLabel": "Protokoll *",
|
||||
"protocolImap": "IMAP",
|
||||
"protocolExchange": "Exchange",
|
||||
"hostLabelExchange": "EWS-Endpunkt-URL",
|
||||
"hostLabelImap": "IMAP-Server",
|
||||
"hostPlaceholderExchange": "https://mail.firma.de/EWS/Exchange.asmx",
|
||||
"hostPlaceholderImap": "imap.firma.de",
|
||||
"portLabel": "Port *",
|
||||
"encryptionLabel": "Verschlüsselung *",
|
||||
"encryptionNone": "Keine",
|
||||
"encryptionStarttls": "STARTTLS",
|
||||
"encryptionSslTls": "SSL-TLS",
|
||||
"folderLabel": "Ordner",
|
||||
"folderPlaceholderExchange": "Inbox",
|
||||
"folderPlaceholderImap": "INBOX",
|
||||
"domainLabel": "Windows-Domäne",
|
||||
"domainPlaceholder": "CONTOSO",
|
||||
"senderFilterLabel": "Absenderfilter",
|
||||
"senderFilterPlaceholder": "alerts@vergabeportal.de",
|
||||
"usernameLabel": "Benutzername",
|
||||
"passwordLabel": "Passwort",
|
||||
"passwordPlaceholder": "••••••••",
|
||||
"showPassword": "Passwort anzeigen",
|
||||
"hidePassword": "Passwort verbergen",
|
||||
"activeLabel": "Aktiv",
|
||||
"saving": "Wird gespeichert...",
|
||||
"save": "Speichern",
|
||||
"saveSuccess": "Einstellungen gespeichert.",
|
||||
"errorSave": "Einstellungen konnten nicht gespeichert werden"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -571,5 +571,258 @@
|
||||
"wrongPassword": "Wrong password. Could not decrypt the PFX/P12 file.",
|
||||
"unknownFormat": "Unknown format. The file could not be recognized as a certificate."
|
||||
}
|
||||
},
|
||||
"tenderRadar": {
|
||||
"page": {
|
||||
"title": "Tender Radar",
|
||||
"loading": "Loading…"
|
||||
},
|
||||
"results": {
|
||||
"colTitle": "Title",
|
||||
"colBuyer": "Contracting Authority",
|
||||
"colDeadline": "Deadline",
|
||||
"colValue": "Value",
|
||||
"colPublished": "Published",
|
||||
"colTriage": "Triage",
|
||||
"noValue": "no value stated",
|
||||
"error": "Could not load tenders.",
|
||||
"emptyTitle": "No tenders found.",
|
||||
"emptyBody": "Adjust the filters or disable \"open only\".",
|
||||
"markUnread": "Mark as unread",
|
||||
"markRead": "Mark as read",
|
||||
"readLabel": "Read",
|
||||
"unreadLabel": "Unread",
|
||||
"removeFavorite": "Remove favorite",
|
||||
"setFavorite": "Set favorite",
|
||||
"favoriteOn": "★ Favorite",
|
||||
"favoriteOff": "☆ Favorite",
|
||||
"showingRange": "Showing {from}–{to} of {total}",
|
||||
"prevPage": "← Back",
|
||||
"nextPage": "Next →"
|
||||
},
|
||||
"filter": {
|
||||
"sortPublished": "Publication date",
|
||||
"sortDeadline": "Deadline",
|
||||
"sortValue": "Estimated value",
|
||||
"searchLabel": "Search",
|
||||
"searchPlaceholder": "Title or contracting authority…",
|
||||
"sortLabel": "Sort by",
|
||||
"openOnlyLabel": "Open only",
|
||||
"favOnlyLabel": "Favorites only",
|
||||
"deadlineFromLabel": "Deadline from",
|
||||
"deadlineToLabel": "Deadline to",
|
||||
"plzLabel": "Postal code",
|
||||
"plzPlaceholder": "e.g. 10",
|
||||
"bundeslandLabel": "Federal state",
|
||||
"bundeslandAll": "All federal states",
|
||||
"cpvLabel": "Sector (CPV)",
|
||||
"cpvPlaceholder": "e.g. construction, IT, health…",
|
||||
"cpvRemoveAria": "Remove division {code}",
|
||||
"valueMinLabel": "Estimated value from (€)",
|
||||
"valueMinPlaceholder": "min.",
|
||||
"valueMaxLabel": "Estimated value to (€)",
|
||||
"valueMaxPlaceholder": "max.",
|
||||
"includeNullValueLabel": "Include tenders without a stated value",
|
||||
"bundeslandOptions": {
|
||||
"DE1": "Baden-Württemberg",
|
||||
"DE2": "Bavaria",
|
||||
"DE3": "Berlin",
|
||||
"DE4": "Brandenburg",
|
||||
"DE5": "Bremen",
|
||||
"DE6": "Hamburg",
|
||||
"DE7": "Hesse",
|
||||
"DE8": "Mecklenburg-Western Pomerania",
|
||||
"DE9": "Lower Saxony",
|
||||
"DEA": "North Rhine-Westphalia",
|
||||
"DEB": "Rhineland-Palatinate",
|
||||
"DEC": "Saarland",
|
||||
"DED": "Saxony",
|
||||
"DEE": "Saxony-Anhalt",
|
||||
"DEF": "Schleswig-Holstein",
|
||||
"DEG": "Thuringia"
|
||||
},
|
||||
"cpvDivisions": {
|
||||
"03": "Agricultural, farming, fishing, forestry and related products",
|
||||
"09": "Petroleum products, fuel, electricity and other sources of energy",
|
||||
"14": "Mining, basic metals and related products",
|
||||
"15": "Food, beverages, tobacco and related products",
|
||||
"16": "Agricultural machinery",
|
||||
"18": "Clothing, footwear, luggage articles and accessories",
|
||||
"19": "Leather and textile fabrics, plastic and rubber materials",
|
||||
"22": "Printed matter and related products",
|
||||
"24": "Chemical products",
|
||||
"30": "Office and computing machinery, equipment and supplies except furniture and software packages",
|
||||
"31": "Electrical machinery, apparatus, equipment and consumables; lighting",
|
||||
"32": "Radio, television, communication, telecommunication and related equipment",
|
||||
"33": "Medical equipment, pharmaceutical products and personal care products",
|
||||
"34": "Transport equipment and auxiliary products to transportation",
|
||||
"35": "Security, fire-fighting, police and defence equipment",
|
||||
"37": "Musical instruments, sport goods, games, toys, handicraft, art materials and accessories",
|
||||
"38": "Laboratory, optical and precision equipment (excl. glasses)",
|
||||
"39": "Furniture (including office furniture), furnishings, domestic appliances (excl. lighting) and cleaning products",
|
||||
"41": "Collected and purified water",
|
||||
"42": "Industrial machinery",
|
||||
"43": "Machinery for mining, quarrying, construction equipment",
|
||||
"44": "Construction structures and materials; auxiliary products to construction",
|
||||
"45": "Construction work",
|
||||
"48": "Software package and information systems",
|
||||
"50": "Repair and maintenance services",
|
||||
"51": "Installation services (except software)",
|
||||
"55": "Hotel, restaurant and retail trade services",
|
||||
"60": "Transport services (excl. waste transport)",
|
||||
"63": "Supporting and auxiliary transport services; travel agency services",
|
||||
"64": "Postal and telecommunications services",
|
||||
"65": "Public utilities",
|
||||
"66": "Financial and insurance services",
|
||||
"70": "Real estate services",
|
||||
"71": "Architectural, construction, engineering and inspection services",
|
||||
"72": "IT services: consulting, software development, Internet and support",
|
||||
"73": "Research and development services and related consultancy services",
|
||||
"75": "Administration, defence and social security services",
|
||||
"76": "Services related to the oil and gas industry",
|
||||
"77": "Agricultural, forestry, horticultural, aquacultural and apicultural services",
|
||||
"79": "Business services: law, marketing, consulting, recruitment, printing and security",
|
||||
"80": "Education and training services",
|
||||
"85": "Health and social work services",
|
||||
"90": "Sewage, refuse, cleaning and environmental services",
|
||||
"92": "Recreational, cultural and sporting services",
|
||||
"98": "Other community, social and personal services"
|
||||
}
|
||||
},
|
||||
"savedSearch": {
|
||||
"title": "Saved Searches",
|
||||
"saveAsLabel": "Save current filters as",
|
||||
"namePlaceholder": "Name of the saved search…",
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"loading": "Loading…",
|
||||
"empty": "No saved searches yet.",
|
||||
"loadTitle": "Load saved search",
|
||||
"instantAlertTitle": "Instant email alert for new matches of this search",
|
||||
"instantAlertLabel": "Instant alert",
|
||||
"instantAlertAria": "Instant alert for {name}",
|
||||
"renameTitle": "Rename",
|
||||
"renameAria": "Rename {name}",
|
||||
"renameConfirmAria": "Confirm rename of {name}",
|
||||
"renameCancelAria": "Cancel rename",
|
||||
"deleteTitle": "Delete",
|
||||
"deleteAria": "Delete {name}",
|
||||
"errorLoad": "Could not load saved searches.",
|
||||
"errorSave": "Could not save the search.",
|
||||
"errorRename": "Could not rename the search.",
|
||||
"errorDelete": "Could not delete the search.",
|
||||
"errorToggleAlert": "Could not toggle the instant alert."
|
||||
},
|
||||
"detail": {
|
||||
"dialogAria": "Tender details",
|
||||
"title": "Tender details",
|
||||
"close": "Close",
|
||||
"loading": "Loading…",
|
||||
"error": "Could not load the tender.",
|
||||
"fieldTitle": "Title",
|
||||
"fieldBuyer": "Contracting Authority",
|
||||
"fieldRegion": "Region / Postal code / Federal state",
|
||||
"fieldCpv": "CPV codes",
|
||||
"noCpv": "no CPV codes stated",
|
||||
"fieldDeadline": "Deadline",
|
||||
"noDeadline": "no deadline stated",
|
||||
"fieldValue": "Estimated Value",
|
||||
"noValue": "no value stated",
|
||||
"fieldProcedureType": "Procedure Type",
|
||||
"fieldStatus": "Status",
|
||||
"fieldPublished": "Published",
|
||||
"mirrorNotice": "Tender documents are not mirrored locally — the link below leads directly to the original on the source portal.",
|
||||
"sourceLink": "{portal}: View tender on source portal →",
|
||||
"noSourceLink": "{portal}: No source link available.",
|
||||
"sourceLinkFallback": "View tender on source portal →",
|
||||
"noSourceLinkFallback": "No source link available."
|
||||
},
|
||||
"coverage": {
|
||||
"label": "Data coverage:",
|
||||
"text": "Currently only EU-wide above-threshold tenders from the central DÖE source are ingested. Below-threshold tenders from individual contracting authorities are not yet included — a short result list is therefore not a defect.",
|
||||
"denylistLabel": "Manual watch:",
|
||||
"denylistText": "These portals do not permit automated access under their terms of use and must therefore be checked manually:"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Tender Radar — Settings",
|
||||
"rssSectionTitle": "RSS Feeds",
|
||||
"rssSectionBody": "Public, platform-wide RSS sources (e.g. service.bund.de or a subreport-elvis municipal feed) — applies equally to all tenants, not configurable per tenant.",
|
||||
"emailSectionTitle": "Email Alerts",
|
||||
"emailSectionBody": "Mailbox that tender portals send their notification emails to — configurable per tenant, separate from the DKV invoice mailbox. Tenders detected from these emails are only visible to your tenant.",
|
||||
"notificationsSectionTitle": "Notifications",
|
||||
"digestIntervalLabel": "Digest Interval",
|
||||
"digestDaily": "Daily",
|
||||
"digestWeekly": "Weekly",
|
||||
"digestOff": "Off",
|
||||
"digestHelp": "Summarizes new matches from your saved searches in a daily or weekly digest email. \"Off\" disables the digest — instant alerts per saved search remain usable independently.",
|
||||
"saveSuccess": "Setting saved.",
|
||||
"errorLoad": "Could not load notification setting",
|
||||
"errorSave": "Could not save notification setting"
|
||||
},
|
||||
"sourceConfig": {
|
||||
"errorLoad": "Could not load configuration",
|
||||
"errorSave": "Could not save settings",
|
||||
"validationError": "Interval must be between {min} and {max} minutes.",
|
||||
"sourceLabel": "Source",
|
||||
"lastIngestedLabel": "Last Ingested Day",
|
||||
"neverIngested": "Not yet ingested",
|
||||
"pollIntervalLabel": "Poll Interval (Minutes) *",
|
||||
"minutesSuffix": "min.",
|
||||
"pollIntervalHelp": "Cron tick frequency ({min}–{max} min.) — the actual DÖE poll is additionally throttled by the day cursor.",
|
||||
"activeLabel": "Active",
|
||||
"saving": "Saving...",
|
||||
"save": "Save",
|
||||
"saveSuccess": "Settings saved."
|
||||
},
|
||||
"rssFeeds": {
|
||||
"errorLoad": "Could not load RSS feeds",
|
||||
"errorAdd": "Could not save the RSS feed",
|
||||
"errorRemove": "Could not remove the RSS feed",
|
||||
"validationRequired": "URL and label are required.",
|
||||
"empty": "No RSS feeds configured yet.",
|
||||
"activeLabel": "active",
|
||||
"inactiveLabel": "inactive",
|
||||
"removeAria": "Remove RSS feed \"{label}\"",
|
||||
"removing": "Removing...",
|
||||
"remove": "Remove",
|
||||
"urlLabel": "Feed URL *",
|
||||
"urlPlaceholder": "https://www.service.bund.de/.../RSSGenerator_Ausschreibungen.xml",
|
||||
"labelLabel": "Label *",
|
||||
"labelPlaceholder": "e.g. service-bund",
|
||||
"adding": "Adding...",
|
||||
"add": "Add Feed"
|
||||
},
|
||||
"emailAlerts": {
|
||||
"notSaved": "Not yet saved — fill in and save the mailbox configuration.",
|
||||
"protocolLabel": "Protocol *",
|
||||
"protocolImap": "IMAP",
|
||||
"protocolExchange": "Exchange",
|
||||
"hostLabelExchange": "EWS Endpoint URL",
|
||||
"hostLabelImap": "IMAP Server",
|
||||
"hostPlaceholderExchange": "https://mail.company.com/EWS/Exchange.asmx",
|
||||
"hostPlaceholderImap": "imap.company.com",
|
||||
"portLabel": "Port *",
|
||||
"encryptionLabel": "Encryption *",
|
||||
"encryptionNone": "None",
|
||||
"encryptionStarttls": "STARTTLS",
|
||||
"encryptionSslTls": "SSL-TLS",
|
||||
"folderLabel": "Folder",
|
||||
"folderPlaceholderExchange": "Inbox",
|
||||
"folderPlaceholderImap": "INBOX",
|
||||
"domainLabel": "Windows Domain",
|
||||
"domainPlaceholder": "CONTOSO",
|
||||
"senderFilterLabel": "Sender Filter",
|
||||
"senderFilterPlaceholder": "alerts@tenderportal.com",
|
||||
"usernameLabel": "Username",
|
||||
"passwordLabel": "Password",
|
||||
"passwordPlaceholder": "••••••••",
|
||||
"showPassword": "Show password",
|
||||
"hidePassword": "Hide password",
|
||||
"activeLabel": "Active",
|
||||
"saving": "Saving...",
|
||||
"save": "Save",
|
||||
"saveSuccess": "Settings saved.",
|
||||
"errorSave": "Could not save settings"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import de from './de.json';
|
||||
import en from './en.json';
|
||||
|
||||
/**
|
||||
* tenderRadar-parity.spec — Plan 14-05 (CONFIG-03, D-10). Structural
|
||||
* de/en key-set equality guard for the `tenderRadar` i18n namespace: fails
|
||||
* listing any key present in one locale but missing in the other. Does NOT
|
||||
* assert on translation content — only that both locales define the exact
|
||||
* same set of (recursively-flattened) keys, so a future edit can never add
|
||||
* a string to one locale and silently forget the other.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Recursively flattens a nested message object into dot-joined leaf key
|
||||
* paths (e.g. `{ filter: { plzLabel: '...' } }` -> `['filter.plzLabel']`).
|
||||
*/
|
||||
function flattenKeys(obj: unknown, prefix = ''): string[] {
|
||||
if (obj === null || typeof obj !== 'object') {
|
||||
return [prefix];
|
||||
}
|
||||
return Object.entries(obj as Record<string, unknown>).flatMap(([key, value]) =>
|
||||
flattenKeys(value, prefix ? `${prefix}.${key}` : key),
|
||||
);
|
||||
}
|
||||
|
||||
describe('tenderRadar i18n namespace — de/en key parity', () => {
|
||||
it('de.json and en.json both define a tenderRadar namespace', () => {
|
||||
expect(de).toHaveProperty('tenderRadar');
|
||||
expect(en).toHaveProperty('tenderRadar');
|
||||
});
|
||||
|
||||
it('has an identical (recursively-flattened) key set in de and en', () => {
|
||||
const deKeys = flattenKeys(de.tenderRadar).sort();
|
||||
const enKeys = flattenKeys(en.tenderRadar).sort();
|
||||
|
||||
const missingInEn = deKeys.filter((key) => !enKeys.includes(key));
|
||||
const missingInDe = enKeys.filter((key) => !deKeys.includes(key));
|
||||
|
||||
expect(missingInEn, `Keys present in de.json but missing in en.json: ${missingInEn.join(', ')}`).toEqual([]);
|
||||
expect(missingInDe, `Keys present in en.json but missing in de.json: ${missingInDe.join(', ')}`).toEqual([]);
|
||||
expect(deKeys).toEqual(enKeys);
|
||||
});
|
||||
|
||||
it('every leaf value under tenderRadar is a non-empty string in both locales', () => {
|
||||
const checkLeaves = (obj: unknown, path = '') => {
|
||||
if (obj === null || typeof obj !== 'object') {
|
||||
expect(typeof obj, `${path} should be a string`).toBe('string');
|
||||
expect((obj as string).length, `${path} should not be empty`).toBeGreaterThan(0);
|
||||
return;
|
||||
}
|
||||
for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
|
||||
checkLeaves(value, path ? `${path}.${key}` : key);
|
||||
}
|
||||
};
|
||||
checkLeaves(de.tenderRadar);
|
||||
checkLeaves(en.tenderRadar);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user