Format multilingual audit extraction for CI lint

This commit is contained in:
2026-03-29 21:25:01 +02:00
parent c516d72c8a
commit 51b2fd574c
19 changed files with 172 additions and 43 deletions

View File

@@ -20,7 +20,9 @@ from mandelblog_content_guard.mixins import MultilingualValidationMixin
@register_snippet @register_snippet
class LocalizedFooterContent(MultilingualValidationMixin, TranslatableMixin, models.Model): class LocalizedFooterContent(
MultilingualValidationMixin, TranslatableMixin, models.Model
):
title = models.CharField(max_length=120, default="Footer content") title = models.CharField(max_length=120, default="Footer content")
site = models.ForeignKey( site = models.ForeignKey(
Site, on_delete=models.CASCADE, related_name="localized_footer_contents" Site, on_delete=models.CASCADE, related_name="localized_footer_contents"

View File

@@ -77,5 +77,7 @@ CONTENT_GUARD_REWRITE_BACKEND = None
if "test" in sys.argv: if "test" in sys.argv:
MIGRATION_MODULES = globals().get("MIGRATION_MODULES", {}).copy() MIGRATION_MODULES = globals().get("MIGRATION_MODULES", {}).copy()
MIGRATION_MODULES["template_engine"] = "mandelstudio.test_migrations.template_engine" MIGRATION_MODULES["template_engine"] = (
"mandelstudio.test_migrations.template_engine"
)
TEST_RUNNER = "django.test.runner.DiscoverRunner" TEST_RUNNER = "django.test.runner.DiscoverRunner"

View File

@@ -1,2 +1,5 @@
from importlib import import_module from importlib import import_module
Migration = import_module("ocyan.plugin.template_engine.engine.migrations.0001_initial").Migration
Migration = import_module(
"ocyan.plugin.template_engine.engine.migrations.0001_initial"
).Migration

View File

@@ -1,2 +1,5 @@
from importlib import import_module from importlib import import_module
Migration = import_module("ocyan.plugin.template_engine.engine.migrations.0002_templateenginesitesettings").Migration
Migration = import_module(
"ocyan.plugin.template_engine.engine.migrations.0002_templateenginesitesettings"
).Migration

View File

@@ -1,2 +1,5 @@
from importlib import import_module from importlib import import_module
Migration = import_module("ocyan.plugin.template_engine.engine.migrations.0003_templateenginesitesettings_nav_items").Migration
Migration = import_module(
"ocyan.plugin.template_engine.engine.migrations.0003_templateenginesitesettings_nav_items"
).Migration

View File

@@ -1,2 +1,5 @@
from importlib import import_module from importlib import import_module
Migration = import_module("ocyan.plugin.template_engine.engine.migrations.0004_alter_basehomepage_body_alter_basestandardpage_body").Migration
Migration = import_module(
"ocyan.plugin.template_engine.engine.migrations.0004_alter_basehomepage_body_alter_basestandardpage_body"
).Migration

View File

@@ -1,2 +1,5 @@
from importlib import import_module from importlib import import_module
Migration = import_module("ocyan.plugin.template_engine.engine.migrations.0005_templateenginesitesettings_header_variant_and_more").Migration
Migration = import_module(
"ocyan.plugin.template_engine.engine.migrations.0005_templateenginesitesettings_header_variant_and_more"
).Migration

View File

@@ -1,2 +1,5 @@
from importlib import import_module from importlib import import_module
Migration = import_module("ocyan.plugin.template_engine.engine.migrations.0006_templateenginesitesettings_footer_dynamic_fields").Migration
Migration = import_module(
"ocyan.plugin.template_engine.engine.migrations.0006_templateenginesitesettings_footer_dynamic_fields"
).Migration

View File

@@ -1,2 +1,5 @@
from importlib import import_module from importlib import import_module
Migration = import_module("ocyan.plugin.template_engine.engine.migrations.0007_templateenginesitesettings_header_cta_fields").Migration
Migration = import_module(
"ocyan.plugin.template_engine.engine.migrations.0007_templateenginesitesettings_header_cta_fields"
).Migration

View File

@@ -1,2 +1,5 @@
from importlib import import_module from importlib import import_module
Migration = import_module("ocyan.plugin.template_engine.engine.migrations.0008_templateenginesitesettings_footer_bottom_links_and_more").Migration
Migration = import_module(
"ocyan.plugin.template_engine.engine.migrations.0008_templateenginesitesettings_footer_bottom_links_and_more"
).Migration

View File

@@ -1,2 +1,5 @@
from importlib import import_module from importlib import import_module
Migration = import_module("ocyan.plugin.template_engine.engine.migrations.0009_alter_basehomepage_body_alter_basestandardpage_body_and_more").Migration
Migration = import_module(
"ocyan.plugin.template_engine.engine.migrations.0009_alter_basehomepage_body_alter_basestandardpage_body_and_more"
).Migration

View File

@@ -1,2 +1,5 @@
from importlib import import_module from importlib import import_module
Migration = import_module("ocyan.plugin.template_engine.engine.migrations.0010_enginepage_and_more").Migration
Migration = import_module(
"ocyan.plugin.template_engine.engine.migrations.0010_enginepage_and_more"
).Migration

View File

@@ -1,2 +1,5 @@
from importlib import import_module from importlib import import_module
Migration = import_module("ocyan.plugin.template_engine.engine.migrations.0011_alter_basehomepage_body_alter_basestandardpage_body_and_more").Migration
Migration = import_module(
"ocyan.plugin.template_engine.engine.migrations.0011_alter_basehomepage_body_alter_basestandardpage_body_and_more"
).Migration

View File

@@ -1,2 +1,5 @@
from importlib import import_module from importlib import import_module
Migration = import_module("ocyan.plugin.template_engine.engine.migrations.0012_alter_basehomepage_body_alter_basestandardpage_body_and_more").Migration
Migration = import_module(
"ocyan.plugin.template_engine.engine.migrations.0012_alter_basehomepage_body_alter_basestandardpage_body_and_more"
).Migration

View File

@@ -1,2 +1,5 @@
from importlib import import_module from importlib import import_module
Migration = import_module("ocyan.plugin.template_engine.engine.migrations.0013_engineblockpreset").Migration
Migration = import_module(
"ocyan.plugin.template_engine.engine.migrations.0013_engineblockpreset"
).Migration

View File

@@ -1,2 +1,5 @@
from importlib import import_module from importlib import import_module
Migration = import_module("ocyan.plugin.template_engine.engine.migrations.0014_alter_basehomepage_body_alter_basestandardpage_body_and_more").Migration
Migration = import_module(
"ocyan.plugin.template_engine.engine.migrations.0014_alter_basehomepage_body_alter_basestandardpage_body_and_more"
).Migration

View File

@@ -13,7 +13,10 @@ def _ensure_navitem_table(apps, schema_editor):
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("template_engine", "0014_alter_basehomepage_body_alter_basestandardpage_body_and_more"), (
"template_engine",
"0014_alter_basehomepage_body_alter_basestandardpage_body_and_more",
),
] ]
operations = [ operations = [

View File

@@ -1,2 +1,5 @@
from importlib import import_module from importlib import import_module
Migration = import_module("ocyan.plugin.template_engine.engine.migrations.0016_alter_basehomepage_body_alter_basestandardpage_body_and_more").Migration
Migration = import_module(
"ocyan.plugin.template_engine.engine.migrations.0016_alter_basehomepage_body_alter_basestandardpage_body_and_more"
).Migration

View File

@@ -9,39 +9,66 @@ from django.test import SimpleTestCase, override_settings
from mandelblog_content_guard.agents import get_language_agent from mandelblog_content_guard.agents import get_language_agent
from mandelblog_content_guard.ai import rewrite_ai_output from mandelblog_content_guard.ai import rewrite_ai_output
from mandelblog_content_guard.system_strings import build_system_rewrite_candidates, build_system_vocabulary from mandelblog_content_guard.system_strings import (
build_system_rewrite_candidates,
build_system_vocabulary,
)
from mandelblog_content_guard.types import split_issues from mandelblog_content_guard.types import split_issues
from mandelblog_content_guard.validators.multilingual import extract_visible_rendered_text, validate_text_nodes from mandelblog_content_guard.validators.multilingual import (
extract_visible_rendered_text,
validate_text_nodes,
)
class ContentGuardRuleTests(SimpleTestCase): class ContentGuardRuleTests(SimpleTestCase):
def test_mixed_language_detection_blocks(self): def test_mixed_language_detection_blocks(self):
issues = validate_text_nodes( issues = validate_text_nodes(
"pt", "pt",
[("body.hero_text", 'Poiché l\'input "Unverbindliche Erstberatung" è in tedesco')], [
(
"body.hero_text",
'Poiché l\'input "Unverbindliche Erstberatung" è in tedesco',
)
],
) )
blocking, _warnings = split_issues(issues) blocking, _warnings = split_issues(issues)
self.assertTrue(blocking) self.assertTrue(blocking)
self.assertTrue(any(issue.issue_type == "known_bad_pattern" for issue in blocking)) self.assertTrue(
any(issue.issue_type == "known_bad_pattern" for issue in blocking)
)
def test_cta_mismatch_detection_blocks(self): def test_cta_mismatch_detection_blocks(self):
issues = validate_text_nodes("en", [("body.cta_text", "Plan kennismaking")]) issues = validate_text_nodes("en", [("body.cta_text", "Plan kennismaking")])
blocking, _warnings = split_issues(issues) blocking, _warnings = split_issues(issues)
self.assertTrue(any(issue.issue_type == "cta_language_mismatch" for issue in blocking)) self.assertTrue(
any(issue.issue_type == "cta_language_mismatch" for issue in blocking)
)
def test_form_validation_blocks_wrong_language(self): def test_form_validation_blocks_wrong_language(self):
issues = validate_text_nodes("ru", [("body.form.label", "Correo electrónico")]) issues = validate_text_nodes("ru", [("body.form.label", "Correo electrónico")])
blocking, _warnings = split_issues(issues) blocking, _warnings = split_issues(issues)
self.assertTrue(any(issue.issue_type in {"known_bad_pattern", "form_language_mismatch"} for issue in blocking)) self.assertTrue(
any(
issue.issue_type in {"known_bad_pattern", "form_language_mismatch"}
for issue in blocking
)
)
@override_settings(CONTENT_GUARD_BLOCK_MEDIUM=True) @override_settings(CONTENT_GUARD_BLOCK_MEDIUM=True)
def test_medium_can_be_blocked_in_strict_mode(self): def test_medium_can_be_blocked_in_strict_mode(self):
issues = validate_text_nodes( issues = validate_text_nodes(
"en", "en",
[("body.summary", "le la les et avec pour vous une pas des extra words to trigger heuristic")], [
(
"body.summary",
"le la les et avec pour vous une pas des extra words to trigger heuristic",
)
],
) )
blocking, _warnings = split_issues(issues) blocking, _warnings = split_issues(issues)
self.assertTrue(any(issue.issue_type == "language_heuristic" for issue in blocking)) self.assertTrue(
any(issue.issue_type == "language_heuristic" for issue in blocking)
)
def test_language_agent_registry(self): def test_language_agent_registry(self):
agent = get_language_agent("pt") agent = get_language_agent("pt")
@@ -56,7 +83,9 @@ class ContentGuardRuleTests(SimpleTestCase):
def test_portuguese_agent_contextual_badge_rewrite(self): def test_portuguese_agent_contextual_badge_rewrite(self):
agent = get_language_agent("pt") agent = get_language_agent("pt")
self.assertEqual(agent.rewrite("SERVICES", "body.cards[0].badge"), "SERVIÇOS") self.assertEqual(agent.rewrite("SERVICES", "body.cards[0].badge"), "SERVIÇOS")
self.assertEqual(agent.rewrite("Transparent", "body.metrics[0].label"), "Investimento claro") self.assertEqual(
agent.rewrite("Transparent", "body.metrics[0].label"), "Investimento claro"
)
def test_french_agent_contextual_badge_rewrite(self): def test_french_agent_contextual_badge_rewrite(self):
agent = get_language_agent("fr") agent = get_language_agent("fr")
@@ -66,8 +95,12 @@ class ContentGuardRuleTests(SimpleTestCase):
def test_german_agent_normalizes_non_system_copy(self): def test_german_agent_normalizes_non_system_copy(self):
agent = get_language_agent("de") agent = get_language_agent("de")
self.assertEqual(agent.rewrite("New", "body.cards[0].badge"), "Neu") self.assertEqual(agent.rewrite("New", "body.cards[0].badge"), "Neu")
self.assertEqual(agent.rewrite("Intakegespräch", "body.stats[0].label"), "Erstgespräch") self.assertEqual(
self.assertEqual(agent.rewrite("Was du bekommst", "body.heading"), "Was Sie erhalten") agent.rewrite("Intakegespräch", "body.stats[0].label"), "Erstgespräch"
)
self.assertEqual(
agent.rewrite("Was du bekommst", "body.heading"), "Was Sie erhalten"
)
self.assertEqual( self.assertEqual(
agent.rewrite("Sales-ready mit skalierbarem Stack", "body.cards[0].text"), agent.rewrite("Sales-ready mit skalierbarem Stack", "body.cards[0].text"),
"Verkaufsbereit mit skalierbarer Architektur", "Verkaufsbereit mit skalierbarer Architektur",
@@ -91,9 +124,16 @@ class ContentGuardRuleTests(SimpleTestCase):
def test_portuguese_rewrite_candidates_are_detected(self): def test_portuguese_rewrite_candidates_are_detected(self):
issues = validate_text_nodes( issues = validate_text_nodes(
"pt", "pt",
[("body.hero_text", "Siti web e negozi online che sono rapidamente online e facili da gestire")], [
(
"body.hero_text",
"Siti web e negozi online che sono rapidamente online e facili da gestire",
)
],
)
self.assertTrue(
any(issue.issue_type == "mixed_locale_heading" for issue in issues)
) )
self.assertTrue(any(issue.issue_type == "mixed_locale_heading" for issue in issues))
def test_french_foreign_ui_label_is_detected(self): def test_french_foreign_ui_label_is_detected(self):
issues = validate_text_nodes( issues = validate_text_nodes(
@@ -105,9 +145,14 @@ class ContentGuardRuleTests(SimpleTestCase):
def test_de_canonical_system_strings_are_not_rewrite_candidates(self): def test_de_canonical_system_strings_are_not_rewrite_candidates(self):
issues = validate_text_nodes( issues = validate_text_nodes(
"de", "de",
[("body.metric_label", "Durchschnittliche Lieferung"), ("body.badge", "PLAN")], [
("body.metric_label", "Durchschnittliche Lieferung"),
("body.badge", "PLAN"),
],
)
self.assertFalse(
any(issue.bad_value == "Durchschnittliche Lieferung" for issue in issues)
) )
self.assertFalse(any(issue.bad_value == "Durchschnittliche Lieferung" for issue in issues))
self.assertFalse(any(issue.bad_value == "PLAN" for issue in issues)) self.assertFalse(any(issue.bad_value == "PLAN" for issue in issues))
def test_extract_visible_rendered_text_ignores_hidden_script_and_style(self): def test_extract_visible_rendered_text_ignores_hidden_script_and_style(self):
@@ -131,21 +176,40 @@ class ContentGuardRuleTests(SimpleTestCase):
def test_system_strings_are_centralized_for_fr_and_pt(self): def test_system_strings_are_centralized_for_fr_and_pt(self):
self.assertEqual(build_system_vocabulary("fr")["PLAN"], "FORFAIT") self.assertEqual(build_system_vocabulary("fr")["PLAN"], "FORFAIT")
self.assertEqual(build_system_vocabulary("fr")["Reaktionszeit"], "Temps de réponse") self.assertEqual(
build_system_vocabulary("fr")["Reaktionszeit"], "Temps de réponse"
)
self.assertEqual(build_system_vocabulary("pt")["Transparent"], "Transparente") self.assertEqual(build_system_vocabulary("pt")["Transparent"], "Transparente")
self.assertEqual(build_system_vocabulary("fr")["Transparente Investition"], "Investissement transparent") self.assertEqual(
self.assertEqual(build_system_vocabulary("pt")["Transparente Investition"], "Investimento transparente") build_system_vocabulary("fr")["Transparente Investition"],
self.assertEqual(build_system_rewrite_candidates()["Durchschnittliche Lieferung"], "foreign_ui_label") "Investissement transparent",
)
self.assertEqual(
build_system_vocabulary("pt")["Transparente Investition"],
"Investimento transparente",
)
self.assertEqual(
build_system_rewrite_candidates()["Durchschnittliche Lieferung"],
"foreign_ui_label",
)
class AuditLocalesCommandTests(SimpleTestCase): class AuditLocalesCommandTests(SimpleTestCase):
@mock.patch("mandelblog_content_guard.management.commands.audit_locales.audit_locales") @mock.patch(
"mandelblog_content_guard.management.commands.audit_locales.audit_locales"
)
def test_json_output(self, audit_locales_mock): def test_json_output(self, audit_locales_mock):
run = mock.Mock() run = mock.Mock()
run.pk = 12 run.pk = 12
run.total_urls_checked = 2 run.total_urls_checked = 2
run.issues_found = 1 run.issues_found = 1
run.summary = {"en": {"total_urls_checked": 2, "issues_found": 1, "by_severity": {"block": 1}}} run.summary = {
"en": {
"total_urls_checked": 2,
"issues_found": 1,
"by_severity": {"block": 1},
}
}
issue = mock.Mock( issue = mock.Mock(
url="/en/contact/", url="/en/contact/",
title="Contact", title="Contact",
@@ -166,16 +230,29 @@ class AuditLocalesCommandTests(SimpleTestCase):
self.assertEqual(payload["run_id"], 12) self.assertEqual(payload["run_id"], 12)
self.assertEqual(payload["issues"]["en"][0]["bad_value"], "Correo electrónico") self.assertEqual(payload["issues"]["en"][0]["bad_value"], "Correo electrónico")
@mock.patch("mandelblog_content_guard.management.commands.audit_locales.audit_locales") @mock.patch(
"mandelblog_content_guard.management.commands.audit_locales.audit_locales"
)
def test_rewrite_flags_are_forwarded(self, audit_locales_mock): def test_rewrite_flags_are_forwarded(self, audit_locales_mock):
run = mock.Mock() run = mock.Mock()
run.pk = 13 run.pk = 13
run.total_urls_checked = 1 run.total_urls_checked = 1
run.issues_found = 0 run.issues_found = 0
run.summary = {"pt": {"total_urls_checked": 1, "issues_found": 0, "issues_fixed": 0, "by_severity": {"block": 0, "warn": 0, "log": 0}}} run.summary = {
"pt": {
"total_urls_checked": 1,
"issues_found": 0,
"issues_fixed": 0,
"by_severity": {"block": 0, "warn": 0, "log": 0},
}
}
run.issues.all.return_value.order_by.return_value = [] run.issues.all.return_value.order_by.return_value = []
audit_locales_mock.return_value = run audit_locales_mock.return_value = run
out = StringIO() out = StringIO()
call_command("audit_locales", "--locale", "pt", "--rewrite", "--dry-run", stdout=out) call_command(
audit_locales_mock.assert_called_once_with(["pt"], fix=False, rewrite=True, dry_run=True) "audit_locales", "--locale", "pt", "--rewrite", "--dry-run", stdout=out
)
audit_locales_mock.assert_called_once_with(
["pt"], fix=False, rewrite=True, dry_run=True
)