Add multilingual audit CI pipeline + extract mandelblog_content_guard

This commit is contained in:
2026-03-29 20:49:42 +02:00
parent 643aca26d0
commit e3bafd3a73
104 changed files with 3372 additions and 6 deletions

View File

@@ -0,0 +1,368 @@
from __future__ import annotations
from collections.abc import Iterable
SYSTEM_STRING_SPECS = {
"plan_badge": {
"sources": ("PLAN",),
"issue_type": "generic_badge_label",
"translations": {
"en": "Package",
"fr": "FORFAIT",
"es": "Paquete",
"ru": "Пакет",
},
"canonical_by_locale": {
"de": ("PLAN",),
"nl": ("PLAN",),
"it": ("PIANO",),
},
"contexts": {
"en": {
"badge": "Package",
"label": "Package",
"title": "Package",
"heading": "Package",
"rendered": "Package",
},
"fr": {
"badge": "FORFAIT",
"label": "FORFAIT",
"title": "FORFAIT",
"heading": "FORFAIT",
"rendered": "FORFAIT",
},
"es": {
"badge": "Paquete",
"label": "Paquete",
"title": "Paquete",
"heading": "Paquete",
"rendered": "Paquete",
},
"ru": {
"badge": "Пакет",
"label": "Пакет",
"title": "Пакет",
"heading": "Пакет",
"rendered": "Пакет",
},
},
},
"services_badge": {
"sources": ("SERVICES",),
"issue_type": "generic_badge_label",
"translations": {
"en": "Services",
"fr": "PRESTATIONS",
"pt": "SERVIÇOS",
},
"contexts": {
"en": {
"badge": "Services",
"label": "Services",
"title": "Services",
"heading": "Services",
"rendered": "Services",
},
"fr": {
"badge": "PRESTATIONS",
"label": "PRESTATIONS",
"title": "PRESTATIONS",
"heading": "PRESTATIONS",
"rendered": "PRESTATIONS",
},
"pt": {
"badge": "SERVIÇOS",
"label": "SERVIÇOS",
"title": "SERVIÇOS",
"heading": "SERVIÇOS",
"rendered": "SERVIÇOS",
},
},
},
"response_time": {
"sources": ("Reaktionszeit",),
"issue_type": "foreign_ui_label",
"translations": {
"en": "Response time",
"fr": "Temps de réponse",
"es": "Tiempo de respuesta",
"it": "Tempo di risposta",
"ru": "Время ответа",
},
},
"average_delivery": {
"sources": ("Durchschnittliche Lieferung",),
"issue_type": "foreign_ui_label",
"translations": {
"en": "Average delivery time",
"fr": "Délai moyen de livraison",
"es": "Plazo medio de entrega",
"it": "Tempo medio di consegna",
"ru": "Средний срок запуска",
},
},
"without_commitment": {
"sources": ("Unverbindlich",),
"issue_type": "foreign_ui_label",
"translations": {
"en": "No obligation",
"fr": "Sans engagement",
"es": "Sin compromiso",
"it": "Senza impegno",
"pt": "Sem compromisso",
"ru": "Без обязательств",
},
},
"transparent_label": {
"sources": ("Transparent",),
"issue_type": "foreign_ui_label",
"translations": {
"en": "Clear",
"fr": "Clair",
"es": "Transparente",
"it": "Chiaro",
"pt": "Transparente",
"ru": "Прозрачно",
},
"contexts": {
"en": {
"badge": "Clear",
"label": "Clear",
"metric": "Clear",
"stat": "Clear",
"rendered": "Clear",
},
"fr": {
"badge": "Clair",
"label": "Clair",
"metric": "Clair",
"stat": "Clair",
"rendered": "Clair",
},
"es": {
"badge": "Transparente",
"label": "Transparente",
"metric": "Transparente",
"stat": "Transparente",
"rendered": "Transparente",
},
"it": {
"badge": "Chiaro",
"label": "Chiaro",
"metric": "Chiaro",
"stat": "Chiaro",
"rendered": "Chiaro",
},
"pt": {
"badge": "Clara",
"label": "Clara",
"metric": "Investimento claro",
"stat": "Investimento claro",
"rendered": "Investimento claro",
},
"ru": {
"badge": "Прозрачно",
"label": "Прозрачно",
"metric": "Прозрачно",
"stat": "Прозрачно",
"rendered": "Прозрачно",
},
},
},
"weeks_1_2": {
"sources": ("1-2 Wochen",),
"issue_type": "weak_marketing_copy",
"translations": {
"fr": "1 à 2 semaines",
"es": "1-2 semanas",
"it": "1-2 settimane",
"pt": "1 a 2 semanas",
},
"contexts": {
"fr": {
"metric": "1 à 2 semaines",
"stat": "1 à 2 semaines",
},
"es": {
"metric": "1-2 semanas",
"stat": "1-2 semanas",
},
"it": {
"metric": "1-2 settimane",
"stat": "1-2 settimane",
},
"pt": {
"metric": "1 a 2 semanas",
"stat": "1 a 2 semanas",
},
},
},
"weeks_2_4": {
"sources": ("2-4 Wochen",),
"issue_type": "foreign_ui_label",
"translations": {
"fr": "2 à 4 semaines",
},
"contexts": {
"fr": {
"metric": "2 à 4 semaines",
"stat": "2 à 4 semaines",
},
},
},
"days_label": {
"sources": ("Tages",),
"issue_type": "weak_marketing_copy",
"translations": {
"fr": "jours",
"pt": "dias",
},
},
"customer_reviews": {
"sources": ("Kundenschätzung",),
"issue_type": "foreign_ui_label",
"translations": {
"en": "Customer rating",
"fr": "Avis clients",
"es": "Valoración de clientes",
"it": "Valutazione clienti",
"pt": "Avaliação dos clientes",
"ru": "Оценка клиентов",
},
},
"editable_label": {
"sources": ("Bearbeitbar",),
"issue_type": "foreign_ui_label",
"translations": {
"en": "Editable",
"fr": "Modifiable",
"es": "Editable",
"it": "Modificabile",
"pt": "Editável",
"ru": "Редактируемо",
},
},
"core_pages_label": {
"sources": ("Startseite + Kernseiten",),
"issue_type": "foreign_ui_label",
"translations": {
"pt": "Página inicial + páginas essenciais",
},
},
"detailed_page_structure": {
"sources": ("Detailliertes Seitenlayout",),
"issue_type": "foreign_ui_label",
"translations": {
"fr": "Structure détaillée des pages",
"es": "Estructura detallada de páginas",
"it": "Struttura dettagliata delle pagine",
"pt": "Estrutura detalhada das páginas",
"ru": "Детальная структура страниц",
},
},
"business_process_cta": {
"sources": ("Geschäftsprozess besprechen",),
"issue_type": "foreign_ui_label",
"translations": {
"fr": "Échanger sur votre processus métier",
"es": "Hablar sobre el proceso del negocio",
"pt": "Falar sobre o processo do negócio",
},
},
"multilingual_rollout": {
"sources": ("Mehrsprachige Einführung", "Mehrsprachiger Rollout-Plan"),
"issue_type": "foreign_ui_label",
"translations": {
"fr": "Déploiement multilingue",
"it": "Lancio multilingue",
"ru": "Многоязычный запуск",
},
},
"customization_integrations": {
"sources": ("Anpassung & Integrationen",),
"issue_type": "foreign_ui_label",
"translations": {
"fr": "Personnalisation & intégrations",
"es": "Personalización e integraciones",
"it": "Personalizzazioni e integrazioni",
"pt": "Personalização e integrações",
"ru": "Настройка и интеграции",
},
},
"transparent_investment": {
"sources": ("Transparente Investition",),
"issue_type": "foreign_ui_label",
"translations": {
"de": "Transparente Investition",
"en": "Transparent pricing",
"fr": "Investissement transparent",
"es": "Inversión transparente",
"it": "Investimento trasparente",
"pt": "Investimento transparente",
"ru": "Прозрачный бюджет",
},
},
}
def build_system_vocabulary(locale_code: str, keys: Iterable[str] | None = None) -> dict[str, str]:
vocabulary: dict[str, str] = {}
selected_keys = tuple(keys or SYSTEM_STRING_SPECS.keys())
for key in selected_keys:
spec = SYSTEM_STRING_SPECS[key]
target = spec.get("translations", {}).get(locale_code)
if not target:
continue
for source in spec["sources"]:
vocabulary[source] = target
return vocabulary
def build_contextual_system_vocabulary(locale_code: str, keys: Iterable[str] | None = None) -> dict[str, dict[str, str]]:
contextual: dict[str, dict[str, str]] = {}
selected_keys = tuple(keys or SYSTEM_STRING_SPECS.keys())
for key in selected_keys:
spec = SYSTEM_STRING_SPECS[key]
locale_contexts = spec.get("contexts", {}).get(locale_code, {})
if not locale_contexts:
continue
source = spec["sources"][0]
for context_name, replacement in locale_contexts.items():
contextual.setdefault(context_name, {})[source] = replacement
return contextual
def build_system_rewrite_candidates(keys: Iterable[str] | None = None) -> dict[str, str]:
candidates: dict[str, str] = {}
selected_keys = tuple(keys or SYSTEM_STRING_SPECS.keys())
for key in selected_keys:
spec = SYSTEM_STRING_SPECS[key]
for source in spec["sources"]:
candidates[source] = spec["issue_type"]
return candidates
def all_system_sources() -> set[str]:
sources: set[str] = set()
for spec in SYSTEM_STRING_SPECS.values():
sources.update(spec["sources"])
return sources
def is_canonical_system_string(locale_code: str, source: str) -> bool:
for spec in SYSTEM_STRING_SPECS.values():
if source in spec.get("canonical_by_locale", {}).get(locale_code, ()):
return True
if locale_code == "de":
return source in all_system_sources()
replacement = system_string_replacement(locale_code, source)
return bool(replacement and replacement == source)
def system_string_replacement(locale_code: str, source: str) -> str:
for spec in SYSTEM_STRING_SPECS.values():
if source not in spec["sources"]:
continue
return spec.get("translations", {}).get(locale_code, "")
return ""