render the Persian API docs right-to-left

Swagger UI and Redoc lay every description out left-to-right, because they were
built for English. Our text is Persian, and in an LTR block the sentence-final
full stop jumps to the start of the line, table columns come out reversed, and
every English word inside a Persian sentence lands in the wrong place. The
screenshot of /api/docs/ showed all three.

This is not fixable by rewriting the text, because the base direction of the
block is what is wrong. Both views now render through a thin template of ours
that extends the drf-spectacular one via {{ block.super }} and adds scoped CSS:
right-to-left for the blocks that carry rendered Markdown, and an explicit
left-to-right override for code and pre, so that "Authorization: Bearer <token>"
and every JSON sample stay readable. Table cells align right, list bullets move
to the right margin, and the API title keeps its LTR treatment since it is
English.

Writing the description in English instead would have fixed only the header. The
per-endpoint descriptions are Persian across the whole codebase and were broken
the same way, so the shell fixes the entire page rather than one block of it,
and keeps the repo's convention of Persian documentation.

The upstream templates are untouched, so a drf-spectacular upgrade cannot be
broken by this.

Two tests cover it, because this failure is invisible to every other kind of
check: nothing errors, nothing 500s, the page simply reads wrong to a human.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-23 01:45:37 +03:30
parent ae0375c26c
commit ac3999ea92
6 changed files with 140 additions and 4 deletions

View File

@@ -13,7 +13,7 @@
| **آدرسِ زنده** | `https://api.math.tadrisino2.ir` · [سلامت](https://api.math.tadrisino2.ir/api/health) |
| **استک** | Python 3.11+ · Django 5.2 · DRF · SQLite · Qdrant (RAG) |
| **API** | ۵۶ اندپوینت · مستنداتِ زنده در `/api/docs/` |
| **تست** | ۴۸۱ تست، همه سبز (`python manage.py test`) |
| **تست** | ۴۸۳ تست، همه سبز (`python manage.py test`) |
| **محتوا** | ۸ پایه · ۵۸ فصل · ۲۶۳ نود · ۲٬۶۱۷ درس · ۲٬۴۶۳ سوال |
---

View File

@@ -237,7 +237,9 @@ ROOT_URLCONF = 'config.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
# قالب‌های سطحِ پروژه (که به هیچ اپی تعلق ندارند) — فعلاً فقط
# پوسته‌ی راست‌به‌چپِ صفحه‌های مستنداتِ API.
'DIRS': [BASE_DIR / 'templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [

View File

@@ -450,3 +450,27 @@ class CorsDevPortTests(TestCase):
# و پذیرشِ هر پورتِ localhost را واقعاً خطرناک می‌کرد.
from django.conf import settings
self.assertFalse(settings.CORS_ALLOW_CREDENTIALS)
class ApiDocsRtlShellTests(TestCase):
"""
صفحه‌های مستنداتِ API باید پوسته‌ی راست‌به‌چپِ خودمان را داشته باشند.
بدونِ آن، Swagger متنِ فارسی را در یک بلوکِ چپ‌به‌راست می‌چیند و نتیجه‌اش
پریدنِ نقطه‌ی جمله به ابتدای خط و برعکس شدنِ ستون‌های جدول است. این تست
هست چون خرابی‌اش فقط با چشم دیده می‌شود: هیچ خطایی نمی‌دهد و هیچ تستِ
دیگری نمی‌گیردش.
"""
def test_swagger_ui_carries_the_rtl_shell(self):
html = self.client.get('/api/docs/').content.decode()
self.assertIn('direction: rtl', html)
# کد باید صریحاً چپ‌به‌راست بماند، وگرنه «Authorization: Bearer ...»
# در بلوکِ RTL جابه‌جا می‌شود.
self.assertIn('direction: ltr', html)
# قالبِ اصلیِ drf-spectacular باید دست‌نخورده زیرش باشد.
self.assertIn('swagger-ui-bundle', html)
def test_redoc_carries_the_rtl_shell(self):
html = self.client.get('/api/redoc/').content.decode()
self.assertIn('direction: rtl', html)

View File

@@ -68,8 +68,13 @@ urlpatterns = [
# OpenAPI schema + Swagger UI + Redoc
path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
path('api/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),
# قالبِ خودمان فقط یک پوسته‌ی راست‌به‌چپ روی قالبِ drf-spectacular است؛
# بدونش متنِ فارسی در یک بلوکِ LTR رندر می‌شود و نقطه‌ی جمله و ترتیبِ
# ستون‌های جدول جابه‌جا می‌شوند. توضیح در خودِ قالب.
path('api/docs/', SpectacularSwaggerView.as_view(
url_name='schema', template_name='tadrisino/swagger_ui.html'), name='swagger-ui'),
path('api/redoc/', SpectacularRedocView.as_view(
url_name='schema', template_name='tadrisino/redoc.html'), name='redoc'),
# همه زیرِ /api/ جمع می‌شوند؛ هر اپ مسیرهای خودش را نگه می‌دارد، پس
# ترتیبِ این فهرست اهمیتی ندارد و تداخلی بینشان نیست.

View File

@@ -0,0 +1,36 @@
{% extends "drf_spectacular/redoc.html" %}
{# همان مشکلِ راست‌به‌چپِ swagger_ui.html، این بار در Redoc. توضیحِ کامل آنجاست. #}
{% block head %}
{{ block.super }}
<style>
[data-role="redoc-description"],
[data-role="redoc-summary"],
.redoc-markdown {
direction: rtl;
text-align: right;
}
/* کد و نمونه‌ها همیشه چپ‌به‌راست */
[data-role="redoc-description"] code,
[data-role="redoc-description"] pre,
.redoc-markdown code,
.redoc-markdown pre {
direction: ltr;
text-align: left;
unicode-bidi: embed;
}
[data-role="redoc-description"] table,
.redoc-markdown table {
direction: rtl;
}
[data-role="redoc-description"] th,
[data-role="redoc-description"] td,
.redoc-markdown th,
.redoc-markdown td {
text-align: right;
}
</style>
{% endblock head %}

View File

@@ -0,0 +1,69 @@
{% extends "drf_spectacular/swagger_ui.html" %}
{# Swagger UI هر متنی را چپ‌به‌راست می‌چیند، چون برای انگلیسی نوشته شده. متنِ
ما فارسی است، و در یک بلوکِ LTR نتیجه‌اش این می‌شود که نقطه‌ی پایانِ جمله
به ابتدای خط می‌پرد، ترتیبِ ستون‌های جدول برعکس می‌شود، و هر کلمه‌ی
انگلیسیِ وسطِ جمله جای خودش را گم می‌کند.
راهِ حل، تغییرِ *متن* نیست — جهتِ خودِ بلوک باید راست‌به‌چپ شود. اینجا فقط
بخش‌هایی که Markdown فارسی رندر می‌کنند RTL می‌شوند؛ ساختارِ خودِ Swagger
(مسیرها، متدها، نامِ فیلدها) دست‌نخورده می‌ماند چون همه انگلیسی‌اند.
قالبِ اصلی دست نمی‌خورد: این فایل با {{ block.super }} رویش سوار می‌شود،
پس به‌روزرسانیِ drf-spectacular چیزی را خراب نمی‌کند. #}
{% block head %}
{{ block.super }}
<style>
/* بلوک‌هایی که متنِ فارسی دارند */
.swagger-ui .info .description,
.swagger-ui .opblock-description-wrapper .renderedMarkdown,
.swagger-ui .response-col_description .renderedMarkdown,
.swagger-ui .markdown {
direction: rtl;
text-align: right;
}
/* ...ولی کد همیشه چپ‌به‌راست است: مسیر، هدر، JSON و نامِ فیلدها در RTL
به‌هم می‌ریزند (مثلاً «Authorization: Bearer <token>» جابه‌جا می‌شود). */
.swagger-ui .info .description code,
.swagger-ui .info .description pre,
.swagger-ui .opblock-description-wrapper .renderedMarkdown code,
.swagger-ui .opblock-description-wrapper .renderedMarkdown pre,
.swagger-ui .markdown code,
.swagger-ui .markdown pre {
direction: ltr;
text-align: left;
unicode-bidi: embed;
}
/* جدول‌ها: سرستون و سلول هر دو باید از راست شروع شوند، وگرنه ستونِ اول
ته جدول می‌افتد. */
.swagger-ui .info .description table,
.swagger-ui .markdown table {
direction: rtl;
}
.swagger-ui .info .description th,
.swagger-ui .info .description td,
.swagger-ui .markdown th,
.swagger-ui .markdown td {
text-align: right;
}
/* فهرست‌ها: بولت باید سمتِ راست باشد. */
.swagger-ui .info .description ul,
.swagger-ui .info .description ol,
.swagger-ui .markdown ul,
.swagger-ui .markdown ol {
padding-right: 2em;
padding-left: 0;
}
/* عنوانِ API و نشان‌های نسخه انگلیسی‌اند و باید چپ‌به‌راست بمانند. */
.swagger-ui .info .title,
.swagger-ui .info hgroup.main {
direction: ltr;
text-align: left;
}
</style>
{% endblock head %}