mod: update Russian to English translations in templates and forms for improved accessibility
HYPN0 Build and Push Docker Image / build-and-push (push) Successful in 31s

This commit is contained in:
2026-09-27 21:12:57 +03:00
parent e7f348d95e
commit dc1897cc62
7 changed files with 270 additions and 120 deletions
+14 -14
View File
@@ -6,25 +6,25 @@ class HalftoneGenerateForm(forms.Form):
Форма валидации входных параметров для генерации гипнотического SVG.
"""
SHAPE_CHOICES = [
("circle", "Круг"),
("ring", "Кольцо"),
("triangle", "Треугольник"),
("square", "Квадрат"),
("diamond", "Ромб"),
("hexagon", "Шестиугольник"),
("star", "Звезда"),
("cross", "Крестик"),
("line", "Линия"),
("snowflake", "Снежинка"),
("binary", "Матрица (0/1)"),
("heart", "Сердечко"),
("circle", "Circle"),
("ring", "Ring"),
("triangle", "Triangle"),
("square", "Square"),
("diamond", "Diamond"),
("hexagon", "Hexagon"),
("star", "Star"),
("cross", "Cross"),
("line", "Line"),
("snowflake", "Snowflake"),
("binary", "Binary (0/1)"),
("heart", "Heart"),
]
image = forms.ImageField(
required=True,
error_messages={
"required": "Покорми Гипножабу изображением!",
"invalid_image": "Гипножаба не может переварить этот файл. Нужна картинка (PNG, JPG, WEBP).",
"required": "Feed the Hypnotoad with an image!",
"invalid_image": "Hypnotoad cannot digest this file. Image required (PNG, JPG, WEBP).",
},
)
shape = forms.ChoiceField(
+7 -7
View File
@@ -596,13 +596,13 @@ class TbBlogPost(models.Model):
def blog_nav_title(self) -> str:
"""Детерминированный выбор названия раздела блога для навигации на основе ID статьи."""
nav_titles = (
"Блог Мозгоклюя",
"Записки Гипножабы",
"Дневник Мозговых Перцепторов",
"Хроники психо-резонанса",
"Мемуары астрального архива",
"Манифесты волновой матрицы",
"Бортовой журнал транс-погружения",
"Brain Slug Blog",
"Hypnotoad Field Notes",
"Neural Perceptor Diary",
"Psycho-Resonance Chronicles",
"Astral Archive Memoirs",
"Wave Matrix Manifestos",
"Deep Trance Flight Log",
)
if not self.id:
return nav_titles[0]
+1 -1
View File
@@ -47,7 +47,7 @@ HYPNO_GENITIVES = [
HYPNO_POSTFIXES = [
"#42", "v3.14", "(Ultra)", "(Phase 7)", "(Alpha)", "(Omega)", "(Protocol 09)", "(Revision 14)",
"(Extract)", "(Layer 9)", "(Ward 106)", "(Hangar 18)", "(Unfinished Opus)", "(Secret Project)",
"[Classified]", "(DOOP-66)", "(Sub-Level 4)", "(MK-Ultra)", "С90"
"[Classified]", "(DOOP-66)", "(Sub-Level 4)", "(MK-Ultra)", "C90"
]
+65 -65
View File
@@ -232,7 +232,7 @@ class HalftoneViewTests(BaseMediaTestCase):
self.assertContains(response, 'X-CSRFToken')
self.assertContains(response, "handleFileChange")
self.assertContains(response, "imagePreviewUrl")
self.assertContains(response, "Гипножаба облизывается на:")
self.assertContains(response, "Hypnotoad salivates over:")
# Проверяем, что csrf_token не пустой в hx-headers
content = response.content.decode("utf-8")
self.assertNotIn('hx-headers=\'{"X-CSRFToken": ""}\'', content)
@@ -291,7 +291,7 @@ class HalftoneViewTests(BaseMediaTestCase):
def test_generate_post_no_image(self):
response = self.client.post(reverse("hypn0_site:generate"), data={"shape": "circle"})
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Покорми Гипножабу")
self.assertContains(response, "Feed the Hypnotoad")
def test_generate_post_success(self):
buf = io.BytesIO()
@@ -366,8 +366,8 @@ class PublishViewTests(BaseMediaTestCase):
},
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Мозговые слизняки протестуют!")
self.assertContains(response, "Подчиниться и опубликовать")
self.assertContains(response, "Brain Slugs are rioting!")
self.assertContains(response, "Submit and Publish")
self.assertEqual(TbHypn0Item.objects.count(), 0)
def test_publish_with_cookie_success(self):
@@ -389,8 +389,8 @@ class PublishViewTests(BaseMediaTestCase):
},
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Уже в галерее транса")
self.assertContains(response, "идет прорастание психо-паутины...")
self.assertContains(response, "In the Trance Gallery")
self.assertContains(response, "psychic neural web expanding…")
# Проверяем запись в БД
self.assertEqual(TbHypn0Item.objects.count(), 1)
@@ -419,7 +419,7 @@ class PublishViewTests(BaseMediaTestCase):
data={"svg_content": ""},
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Нет данных SVG для публикации")
self.assertContains(response, "No SVG data for publication")
self.assertEqual(TbHypn0Item.objects.count(), 0)
@@ -522,11 +522,11 @@ class GalleryDetailAndDownloadTests(BaseMediaTestCase):
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Астральный Транс Сознания #42")
self.assertContains(response, f"#{self.item.s_hash_id}")
self.assertContains(response, "Векторная анатомия")
self.assertContains(response, "Синтез психо-сетки")
self.assertContains(response, "Астральный паспорт")
self.assertContains(response, "Копировать SVG-код")
self.assertContains(response, "Скачать .svg")
self.assertContains(response, "Vector Anatomy")
self.assertContains(response, "Psych-Grid Synthesis")
self.assertContains(response, "Astral Passport")
self.assertContains(response, "Copy SVG code")
self.assertContains(response, "Download .svg")
# Проверка инкремента просмотров
self.item.refresh_from_db()
@@ -557,7 +557,7 @@ class GalleryDetailAndDownloadTests(BaseMediaTestCase):
client = Client()
response = client.post(reverse("hypn0_site:gallery_vote", kwargs={"hash_id": self.item.s_hash_id}))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Подчинитесь воле Гипножабы!")
self.assertContains(response, "Surrender to Hypnotoad!")
class GalleryRandomAndNavigationTests(BaseMediaTestCase):
@@ -699,8 +699,8 @@ class UnconsciousMatrixTests(BaseMediaTestCase):
item = self.items[0]
response = self.client.get(reverse("hypn0_site:gallery_detail", kwargs={"hash_id": item.s_hash_id}))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Матрица бессознательного")
self.assertContains(response, "5 узлов")
self.assertContains(response, "Matrix of the Unconscious")
self.assertContains(response, "5 nodes")
self.assertContains(response, f"#{item.s_hash_id}")
self.assertContains(response, "btn-nav-prev")
self.assertContains(response, "btn-nav-next")
@@ -782,7 +782,7 @@ class GalleryFreshFloorTests(BaseMediaTestCase):
response = self.client.get(reverse("hypn0_site:index"))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Плеск бессознательного")
self.assertContains(response, "Incubator of the Unconscious")
self.assertContains(response, "FRESH STREAM")
self.assertContains(response, f"gallery-card-{item.s_hash_id}")
self.assertContains(response, f"¤{item.s_hash_id}")
@@ -935,12 +935,12 @@ class GalleryArchiveTests(BaseMediaTestCase):
url = reverse("hypn0_site:gallery_archive")
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Галерея транса")
self.assertContains(response, "Trance Gallery")
self.assertContains(response, "ARCHIVE")
self.assertContains(response, "Все волны")
self.assertContains(response, "Инкубатор")
self.assertContains(response, "Одобрено")
self.assertContains(response, "Золотой фонд")
self.assertContains(response, "All waves")
self.assertContains(response, "Incubator")
self.assertContains(response, "Approved")
self.assertContains(response, "Hall of Fame")
# Все 3 картины присутствуют в выдаче
self.assertContains(response, f"gallery-card-{self.item_fresh.s_hash_id}")
self.assertContains(response, f"gallery-card-{self.item_curated.s_hash_id}")
@@ -1013,21 +1013,21 @@ class GalleryArchiveTests(BaseMediaTestCase):
resp_p1 = self.client.get(url, data={"floor": "all", "sort": "likes"})
self.assertEqual(resp_p1.status_code, 200)
self.assertEqual(len(resp_p1.context["page_obj"]), 16)
self.assertContains(resp_p1, "Фаза 1 из 2")
self.assertContains(resp_p1, "<title>Галерея транса — Каталог анимированных картин | HypnoSVG</title>")
self.assertContains(resp_p1, "Phase 1 of&nbsp;2")
self.assertContains(resp_p1, "<title>Trance Gallery — Animated SVG Catalog | HypnoSVG</title>")
# Проверяем сохранение query-параметров в ссылках пагинатора
self.assertContains(resp_p1, "floor=all&sort=likes&page=2")
resp_p2 = self.client.get(url, data={"floor": "all", "sort": "likes", "page": 2})
self.assertEqual(resp_p2.status_code, 200)
self.assertEqual(len(resp_p2.context["page_obj"]), 2)
self.assertContains(resp_p2, "Фаза 2 из 2")
self.assertContains(resp_p2, "<title>Галерея транса — Каталог анимированных картин (Фаза 2) | HypnoSVG</title>")
self.assertContains(resp_p2, "Phase 2 of&nbsp;2")
self.assertContains(resp_p2, "<title>Trance Gallery — Animated SVG Catalog (Phase 2) | HypnoSVG</title>")
self.assertContains(resp_p2, "floor=all&sort=likes&page=1")
# Проверяем Title при фильтрации по этажу
resp_curated = self.client.get(url, data={"floor": "curated"})
self.assertContains(resp_curated, "<title>Одобрено Мозговым Слизнем | HypnoSVG</title>")
self.assertContains(resp_curated, "<title>Approved by Brain Slug | HypnoSVG</title>")
class RescoreCommandTests(BaseMediaTestCase):
@@ -1311,12 +1311,12 @@ class BlogPostModelAndAdminTests(BaseMediaTestCase):
# Проверяем, что отображается один из динамических заголовков секции
content = response.content.decode("utf-8")
possible_headings = (
"Сигналы из параллельного континуума",
"Психо-артефакты ноосферы",
"Заметки астрального консилиума",
"Рефлекторные отклики мозговых слизней",
"Публикации из глубин подсознания",
"Материалы хроник гипноза",
"Signals from&nbsp;Parallel Continuum",
"Noosphere Psycho-Artifacts",
"Astral Council Field Notes",
"Reflexive Responses of&nbsp;Brain Slugs",
"Dispatches from&nbsp;Subconscious Depths",
"Materials from&nbsp;Hypno Chronicles",
)
self.assertTrue(any(heading in content for heading in possible_headings))
self.assertContains(response, "Хроники гипно-транса: Урок 1")
@@ -1345,7 +1345,7 @@ class BlogPostModelAndAdminTests(BaseMediaTestCase):
# Проверка 1-й страницы ленты блога
response = self.client.get("/blog")
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Хроники гипноза")
self.assertContains(response, "Hypno Chronicles")
self.assertContains(response, "Тестовая депеша 11")
self.assertNotContains(response, "Черновик ноосферы")
self.assertContains(response, "page=2")
@@ -1388,7 +1388,7 @@ class BlogPostModelAndAdminTests(BaseMediaTestCase):
resp_parent = self.client.get(f"/blog/{series_parent.slug}")
self.assertEqual(resp_parent.status_code, 200)
self.assertContains(resp_parent, "Цикл: Основы векторного гипноза")
self.assertContains(resp_parent, "Материалы и главы этого цикла")
self.assertContains(resp_parent, "Chapters in This Series")
self.assertContains(resp_parent, "Глава 1: Первичная модуляция")
self.assertContains(resp_parent, "Глава 2: Фазовый сдвиг матрицы")
@@ -1396,31 +1396,31 @@ class BlogPostModelAndAdminTests(BaseMediaTestCase):
resp_ch1 = self.client.get(f"/blog/{ch1.slug}")
self.assertEqual(resp_ch1.status_code, 200)
self.assertContains(resp_ch1, "Глава 1: Первичная модуляция")
self.assertContains(resp_ch1, "Материал входит в серию:")
self.assertContains(resp_ch1, "Part of&nbsp;the&nbsp;series:")
self.assertContains(resp_ch1, self.item.s_title)
self.assertContains(resp_ch1, f"#{self.item.s_hash_id}")
self.assertContains(resp_ch1, f"/gallery/{self.item.s_hash_id}")
self.assertContains(resp_ch1, "Скачать SVG")
self.assertContains(resp_ch1, "Вес:")
self.assertContains(resp_ch1, "Следующая глава цикла")
self.assertContains(resp_ch1, "Download SVG")
self.assertContains(resp_ch1, "Weight:")
self.assertContains(resp_ch1, "Next chapter in series")
self.assertContains(resp_ch1, "Глава 2: Фазовый сдвиг матрицы")
# Проверка хлебных крошек
self.assertContains(resp_ch1, 'aria-label="Хлебные крошки"')
self.assertContains(resp_ch1, 'aria-label="Breadcrumbs"')
self.assertContains(resp_ch1, ch1.blog_nav_title)
self.assertContains(resp_ch1, "Глава 1 из 2")
self.assertContains(resp_ch1, "Chapter 1 of 2")
self.assertContains(resp_ch1, 'aria-current="page"')
# Проверка JSON-LD BreadcrumbList
self.assertContains(resp_ch1, '"@type": "BreadcrumbList"')
self.assertContains(resp_ch1, '"name": "Главная"')
self.assertContains(resp_ch1, '"name": "Хроники гипноза"')
self.assertContains(resp_ch1, '"name": "Home"')
self.assertContains(resp_ch1, '"name": "Hypno Chronicles"')
self.assertContains(resp_ch1, series_parent.s_title)
# 3. Проверка второй главы (ссылка назад на главу 1)
resp_ch2 = self.client.get(f"/blog/{ch2.slug}")
self.assertEqual(resp_ch2.status_code, 200)
self.assertContains(resp_ch2, "Предыдущая глава цикла")
self.assertContains(resp_ch2, "Previous chapter in series")
self.assertContains(resp_ch2, "Глава 1: Первичная модуляция")
# 4. Проверка доступа по числовому ID
@@ -1437,10 +1437,10 @@ class BlogPostModelAndAdminTests(BaseMediaTestCase):
"""Проверка отдачи кастомной страницы 404 в режиме DEBUG=False."""
response = self.client.get("/totally-non-existent-hypno-route-404")
self.assertEqual(response.status_code, 404)
self.assertContains(response, "Гипноузел не&nbsp;обнаружен", status_code=404)
self.assertContains(response, "СБОЙ МАТРИЦЫ // КОД?&nbsp;404", status_code=404)
self.assertContains(response, "Hypno-Node Not Found", status_code=404)
self.assertContains(response, "MATRIX ANOMALY // CODE&nbsp;404", status_code=404)
self.assertContains(response, "return-btn", status_code=404)
self.assertContains(response, "Стираем остатки несущес&shy;твующей страницы", status_code=404)
self.assertContains(response, "Erasing traces of the phantom page", status_code=404)
@override_settings(DEBUG=True)
def test_debug_error_preview_routing(self):
@@ -1448,23 +1448,23 @@ class BlogPostModelAndAdminTests(BaseMediaTestCase):
# 1. Индексный список страниц ошибок
resp_list = self.client.get("/_error/")
self.assertEqual(resp_list.status_code, 200)
self.assertContains(resp_list, "Центр отладки страниц ошибок")
self.assertContains(resp_list, "Error Pages Debug Center")
self.assertContains(resp_list, "/_error/404")
self.assertContains(resp_list, "готов к просмотру")
self.assertContains(resp_list, "ready to preview")
# 2. Проверка каждого кода ошибки
expected_checks = [
("400", 400, "Сбой ментального протокола"),
("401", 401, "Требуется ментальная идентификация"),
("403", 403, "Отказано бюрократом 24-го уровня"),
("404", 404, "Гипноузел не&nbsp;обнаружен"),
("413", 413, "Перегрузка сенсорных каналов"),
("429", 429, "Слишком много мыслей в&nbsp;секунду"),
("500", 500, "Критический перегрев неокортекса"),
("502", 502, "Контейнер потерял сознание"),
("503", 503, "Плановый сеанс гипнотерапии"),
("504", 504, "Разрыв астральной связи"),
("under_reconstruction", 200, "Отдел мозга на&nbsp;реконструкции"),
("400", 400, "Perceptual Protocol Mismatch"),
("401", 401, "Subconscious Identity Required"),
("403", 403, "Cognitive Lockdown Active"),
("404", 404, "Hypno-Node Not Found"),
("413", 413, "Sensory Channel Saturation"),
("429", 429, "Too Many Thoughts per Second"),
("500", 500, "Introspection Neural Lock"),
("502", 502, "Uplink to Hippocampus Lost"),
("503", 503, "Scheduled Hypnotherapy in Progress"),
("504", 504, "Astral Synapse Timed Out"),
("under_reconstruction", 200, "Brain Sector Under Reconstruction"),
]
for code, expected_status, text_fragment in expected_checks:
@@ -1478,12 +1478,12 @@ class BlogPostModelAndAdminTests(BaseMediaTestCase):
# 3. Предпросмотр с суффиксом .html
resp_404_html = self.client.get("/_error/404.html")
self.assertEqual(resp_404_html.status_code, 404)
self.assertContains(resp_404_html, "Гипноузел не&nbsp;обнаружен", status_code=404)
self.assertContains(resp_404_html, "Hypno-Node Not Found", status_code=404)
# 4. Несуществующий шаблон ошибки в dev-режиме
resp_non_existent = self.client.get("/_error/non-existent-code-999")
self.assertEqual(resp_non_existent.status_code, 404)
self.assertContains(resp_non_existent, "Шаблон для ошибки 'non-existent-code-999' не найден", status_code=404)
self.assertContains(resp_non_existent, "Template for error 'non-existent-code-999' not found", status_code=404)
def test_custom_django_error_handlers(self):
"""Проверка прямого вызова стандартных Django-обработчиков ошибок."""
@@ -1495,22 +1495,22 @@ class BlogPostModelAndAdminTests(BaseMediaTestCase):
req400 = factory.get("/bad-request")
resp400 = error_400(req400)
self.assertEqual(resp400.status_code, 400)
self.assertIn("Сбой ментального протокола", resp400.content.decode("utf-8"))
self.assertIn("Perceptual Protocol Mismatch", resp400.content.decode("utf-8"))
req403 = factory.get("/forbidden")
resp403 = error_403(req403)
self.assertEqual(resp403.status_code, 403)
self.assertIn("Отказано бюрократом 24-го уровня", resp403.content.decode("utf-8"))
self.assertIn("Cognitive Lockdown Active", resp403.content.decode("utf-8"))
req404 = factory.get("/not-found")
resp404 = error_404(req404)
self.assertEqual(resp404.status_code, 404)
self.assertIn("Гипноузел не&nbsp;обнаружен", resp404.content.decode("utf-8"))
self.assertIn("Hypno-Node Not Found", resp404.content.decode("utf-8"))
req500 = factory.get("/server-error")
resp500 = error_500(req500)
self.assertEqual(resp500.status_code, 500)
self.assertIn("Критический перегрев неокортекса", resp500.content.decode("utf-8"))
self.assertIn("Introspection Neural Lock", resp500.content.decode("utf-8"))
def test_s_title_plain_property(self):
"""Проверка очистки HTML-тегов и мнемоник в s_title_plain для моделей TbHypn0Item и TbBlogPost."""
+10 -10
View File
@@ -196,14 +196,14 @@ def debug_error_preview(request: HttpRequest, code: str = "") -> HttpResponse:
break
except TemplateDoesNotExist:
pass
status_mark = "готов к просмотру" if exists else "шаблон не создан"
status_mark = "ready to preview" if exists else "template not found"
links.append(f"<li><a href='/_error/{c}'><strong>/{c}</strong></a> ({status_mark})</li>")
html = f"""<!doctype html>
<html lang="ru">
<html lang="en">
<head>
<meta charset="utf-8">
<title>Dev: Предпросмотр страниц ошибок // HypnoSVG</title>
<title>Dev: Error Pages Preview // HypnoSVG</title>
<style>
body {{ font-family: ui-monospace, monospace; background: #09090b; color: #d9f99d; padding: 2rem; line-height: 1.6; }}
a {{ color: #a3e635; text-decoration: none; border-bottom: 1px dotted #a3e635; }}
@@ -215,8 +215,8 @@ def debug_error_preview(request: HttpRequest, code: str = "") -> HttpResponse:
</head>
<body>
<div class="card">
<h2>Центр отладки страниц ошибок (DEBUG=True)</h2>
<p>Выберите код для инспекции верстки и интерактивных скриптов перекалибровки:</p>
<h2>Error Pages Debug Center (DEBUG=True)</h2>
<p>Select an error code to inspect layout and recalibration scripts:</p>
<ul>
{''.join(links)}
</ul>
@@ -244,7 +244,7 @@ def debug_error_preview(request: HttpRequest, code: str = "") -> HttpResponse:
if not found_template:
return HttpResponse(
f"<h1>Шаблон для ошибки '{raw_name}' не найден</h1><p>Проверены: {', '.join(template_candidates)}</p><p><a href='/_error/'>← Вернуться к списку</a></p>",
f"<h1>Template for error '{raw_name}' not found</h1><p>Checked: {', '.join(template_candidates)}</p><p><a href='/_error/'>← Back to index</a></p>",
status=404,
content_type="text/html; charset=utf-8",
)
@@ -281,7 +281,7 @@ def generate(request: HttpRequest) -> HttpResponse:
form = HalftoneGenerateForm(request.POST, request.FILES)
if not form.is_valid():
error_msg = next(iter(form.errors.values()))[0] if form.errors else "Ошибка параметров генерации"
error_msg = next(iter(form.errors.values()))[0] if form.errors else "Generation parameter error"
return render(request, "block/preview.html", {"error": error_msg})
image_file = form.cleaned_data["image"]
@@ -313,7 +313,7 @@ def generate(request: HttpRequest) -> HttpResponse:
return render(
request,
"block/preview.html",
{"error": f"Сбой в матрице гипноза: {str(e)}"},
{"error": f"Glitch in hypno matrix: {str(e)}"},
)
return render(
@@ -338,7 +338,7 @@ def publish(request: HttpRequest) -> HttpResponse:
return render(
request,
"block/publish_status.html",
{"error": "Нет данных SVG для публикации. Попробуйте сгенерировать заново."},
{"error": "No SVG data for publication. Please try generating again."},
)
visitor_uuid = request.COOKIES.get("hypn0_vid")
@@ -411,7 +411,7 @@ def publish(request: HttpRequest) -> HttpResponse:
return render(
request,
"block/publish_status.html",
{"error": f"Сбой фиксации в трансе: {str(e)}"},
{"error": f"Failed to record in trance: {str(e)}"},
)
return render(