رفتن به محتوای اصلی
← همه‌ی تصمیم‌ها

یک درخت کامپوننت، دو جهت

accepted

زمینه

این سایت فارسی (RTL) و انگلیسی (LTR) را از یک درخت کامپوننت واحد رندر می‌کند. ریسک اصلی این نیست که آینه‌کردن سخت است — Tailwind v4 یوتیلیتی‌های لاجیکال دارد و بیشتر موارد را پوشش می‌دهد. ریسک این است که اشتباه‌کردن در آن بی‌صدا است.

یک کلاس فیزیکی مثل ml-4 یا origin-right کامپایل می‌شود، از لینت رد می‌شود، از تایپ‌چک رد می‌شود، و در همان جهتی که نویسنده جلوی چشمش داشته کاملاً درست به نظر می‌رسد. فقط در جهت دیگر غلط است، آن هم فقط برای کسی که آن جهت را می‌خواند.

این را می‌دانم چون خودم قبلاً همینش را منتشر کرده‌ام. در یکی از پروژه‌های قبلی‌ام نوار پیشرفت این است:

<div className="h-full origin-right bg-linear-to-l from-accent-500 to-accent-300" />

با transform: scaleX(pct). هم origin-right و هم bg-linear-to-l برای RTL هاردکد شده‌اند. در LTR نوار از لبه‌ی اشتباه رشد می‌کند و گرادیان برعکس می‌افتد. هیچ خطایی هم نمی‌دهد. در همان کدبیس، انیمیشن @keyframes sweep یک مقدار فیزیکی -120% → 320% را جابه‌جا می‌کند و به همین دلیل در LTR برعکس اجرا می‌شود.

گزینه‌های بررسی‌شده

کامپوننت جدا برای هر جهت. صادقانه است و کار می‌کند. اما سطح هر تغییر آینده را دو برابر می‌کند و تضمین می‌دهد که دو نسخه به‌مرور از هم فاصله بگیرند.

کتابخانه‌ی میرورینگ. ویژگی‌های فیزیکی را زمان بیلد به لاجیکال تبدیل می‌کند. یک وابستگی اضافه می‌کند که مالک پایپ‌لاین CSS من می‌شود، و آن مواردی را که اصلاً معادل لاجیکال ندارند هم حل نمی‌کند.

ویژگی‌های لاجیکال همه‌جا، به‌علاوه‌ی یک توکن جهت برای شکاف‌ها. انضباط بیشتری می‌خواهد، وابستگی ندارد، و — مهم‌تر از همه — این انضباط را می‌شود با اسکریپت اجبار کرد، نه با حافظه.

تصمیم

یوتیلیتی لاجیکال به‌صورت پیش‌فرض. برای آن ویژگی‌هایی که واقعاً شکل لاجیکال ندارند، یک علامت جهت که فقط یک بار تعریف می‌شود:

:root { --dir: 1; --shadow-x: 18px; --grad-angle: 90deg; }
[dir="rtl"] { --dir: -1; --shadow-x: -18px; --grad-angle: 270deg; }

بعد هر مقدار فیزیکی از همین رد می‌شود — translateX(calc(var(--dir) * 12px)) و linear-gradient(calc(145deg * var(--dir)), …). چون --dir یک کاستوم پراپرتی است، جاوااسکریپت هم با getComputedStyle همان مقدار را می‌خواند؛ پس محاسبات اسلایدر و اسکرول هرگز از استایل‌شیت فاصله نمی‌گیرند.

این بخش آخر ایده‌ی تازه‌ای برای من نیست. در یک پروژه‌ی قبلی پالت رنگ را از کاستوم پراپرتی‌های CSS می‌خواندم تا <canvas> هرگز با استایل‌شیت اختلاف پیدا نکند. همان تکنیک است، این‌بار روی جهت به‌جای رنگ.

و بعد بخشی که واقعاً اهمیت دارد: scripts/check-direction.mjs بیلد را روی هر یوتیلیتی فیزیکی که معادل لاجیکال دارد می‌شکند، مگر اینکه صریحاً با rtl: یا ltr: نوشته شده باشد تا بگوید این عمدی است. اجرای آن روی همان کد نوار پیشرفت، چهار خطا می‌دهد.

هزینه‌هایی که پذیرفتم

  • گیت متن سورس را می‌خواند، پس کلاسی که زمان اجرا ساخته شود برایش نامرئی است. ترجیح می‌دهم رشته‌های کلاس را ثابت نگه دارم تا اینکه بررسی را برای پوشش موردی که لازم ندارم ضعیف کنم.
  • از راه فرار می‌شود سوءاستفاده کرد. rtl: گیت را ساکت می‌کند. این عمدی است — هدف این است که انتخاب فیزیکی در ریویو دیده شود، نه اینکه غیرممکن باشد.
  • هر false positive هزینه‌ی یک کامنت دارد. تا امروز موردی پیش نیامده، اما اگر بیاید راه‌حل صادقانه رفتن سراغ یک پارسر است، نه بازکردن استثنا.

نتیجه

صفر یوتیلیتی فیزیکی در کل کدبیس. سایت هر دو جهت را از یک درخت رندر می‌کند، و آن دسته باگی که در پروژه‌ی قبلی‌ام منتشر شد اینجا به main نمی‌رسد — نه چون دقیق‌ترم، بلکه چون بیلد قبولش نمی‌کند.

چه چیزی را عوض می‌کردم

گیت را قبل از کامپوننت‌ها می‌نوشتم، نه بعدشان. اول حدود ده‌دوازده کامپوننت نوشتم، بعد گیت را، و بعد مجبور شدم برگردم و چیزهایی را که پیدا کرد درست کنم. گیت سی دقیقه کار است و طرز نوشتن کامپوننت‌ها را عوض می‌کند — وقتی می‌دانی این بررسی وجود دارد، از همان اول سراغ ms- می‌روی، نه اینکه ml- بنویسی و بعد تمیزکاری کنی.

همچنین گسترشش می‌دادم تا scrollLeft را هم بگیرد؛ تنها مین جهت‌داری که هنوز پوشش داده نشده. Chrome و Firefox و Safari از نظر تاریخی سر علامت آن در RTL اختلاف داشتند، پس هر محاسبه‌ای که رویش نوشته شود دست‌کم در یکی از موتورها غلط است.