التدويل
نص من اليمين إلى اليسار ومختلط الاتجاهات، وتشكيل الحروف العربية، وترجمة لوحة التركيز.
يعرض Text.js العبرية والعربية والفارسية وغيرها من الكتابات من اليمين إلى اليسار بالترتيب الصحيح، مع محاذاتها إلى شبكة الخلايا، في DevTools والطرفيات ومرايا DOM. وينفّذ خوارزمية Unicode ثنائية الاتجاه (UAX #9، Unicode 18.0) ويجتاز مجموعات اختبارات المطابقة الرسمية: جميع حالات BidiTest.txt البالغ عددها 770,241 (كل مدخل في كل اتجاه للفقرة) وجميع حالات BidiCharacterTest.txt البالغ عددها 91,707.
import { Box, Text } from "@textjs/react";export default function Page() { return ( <Box flexDirection="column" padding={1}> <Text>שלום עולם! זהו טקסט בעברית.</Text> <Text>مرحبا بالعالم! هذا نص عربي.</Text> <Text>Order #1234 (<Text color="accent">שלום</Text>) total: 25.50 ₪</Text> <Text>user <Text dir="auto" color="success">שלום!</Text> replied 3 times</Text> </Box> );}الاتجاه
تختار كل فقرة (كل سطر في <Text> يفصله \n) اتجاهها الأساسي من أول محرف قوي فيها، كما يفعل dir="auto" في HTML. تُحاذى الفقرات من اليمين إلى اليسار إلى اليمين افتراضيًا.
<Text>שלום</Text> {/* auto: RTL, right-aligned */}<Text dir="rtl">hello!</Text> {/* "!hello", right-aligned */}<Text dir="ltr">שלום!</Text> {/* LTR paragraph: "םולש!" reads correctly left of the "!" */}<Text align="left">שלום</Text> {/* RTL order, pinned to the left edge */}align | فقرة من اليسار إلى اليمين | فقرة من اليمين إلى اليسار |
|---|---|---|
"start" (الافتراضي) | اليسار | اليمين |
"end" | اليمين | اليسار |
"left" / "center" / "right" | كما هو مكتوب | كما هو مكتوب |
في مكوّن <Text> متداخل، يعزل dir مقطع span مثل <bdi> في HTML: يُخطّط محتواه على حدة ولا يمكنه تغيير ترتيب النص المحيط به. استخدمه للأسماء والعناوين وغيرها من النصوص التي يقدّمها المستخدم ويكون اتجاهها مجهولًا.
<Text>user <Text dir="auto">{name}</Text> replied 3 times</Text>// name = "שלום!" → user !םולש replied 3 times (isolated: "!" ends the name)// without dir → user םולש! replied 3 times ("!" resolves with the Latin text around it)آلية العرض
- الالتفاف بالترتيب المنطقي. تنكسر الأسطر تمامًا كما في النص من اليسار إلى اليمين، بحسب عرض الخلايا.
- إعادة ترتيب كل سطر. تحدد الخوارزمية مستويات التضمين للفقرة بأكملها (التضمينات والعوازل الصريحة، والأنواع الضعيفة والمحايدة، والأقواس المزدوجة)، ثم يُعاد ترتيب كل سطر ملتف على حدة (القاعدتان L1–L2). تنعكس الأقواس وغيرها من المحارف القابلة للعكس في المستويات من اليمين إلى اليسار (L4)، لذا يبقى
(abc)كما هو(abc)داخل العبرية. - تشكيل الحروف العربية. تأخذ الحروف أشكالها السياقية (المنفصلة، والأولية، والوسطية، والنهائية) من كتل أشكال العرض العربية، وتُحسب بالترتيب المنطقي داخل كل مقطع ذي مستوى واحد في السطر (يُحتسب ZWJ و ZWNJ في موضعهما). يشغل كل حرف خلية واحدة، وتبقى الأشكال المتصلة عبر أي حد لمقطع span (مؤشر، أو تغيير لون)، ولا تحتاج وحدة التحكم إلى تشكيلها.
- تثبيت الترتيب. تحمل الخلايا الآن الأشكال الرسومية بترتيب العرض. ستطبّق المتصفحات وكثير من الطرفيات خوارزمية الاتجاهين عليها مرة ثانية، لذا تثبّت برامج التشغيل الترتيب:
| المخرج | آلية تثبيت الترتيب |
|---|---|
DevTools (CssConsoleDriver) | تُحاط مقاطع span التي تحتوي على محارف من اليمين إلى اليسار بعلامتي LRO … PDF (U+202D / U+202C). تحذف آلية تنقية %c في DevTools الخاصيتين direction و unicode-bidi. |
الطرفيات (AnsiDriver، text run) | تُحاط الصفوف ذات النص من اليمين إلى اليسار بعلامتي LRO … PDF للطرفيات التي تطبّق خوارزمية الاتجاهين لكل سطر. ينتقل renderToTerminal إلى الوضع الصريح ECMA-48 BDSM (CSI 8 l، الذي تلتزم به الطرفيات المستندة إلى VTE) مرة عند الدخول ويعود إلى الوضع الضمني (CSI 8 h) عند الخروج؛ وتبدأ إطارات renderToAnsi ذات العرض لمرة واحدة التي تحتوي على نص من اليمين إلى اليسار بالتسلسل CSI 8 l. مرّر bidi: "none" لإخراج الخلايا كما هي. |
مرايا DOM (<ConsoleMirror>) | تستخدم المرآة direction: ltr (حتى لا تقلبها صفحة مضيفة ذات اتجاه من اليمين إلى اليسار)، وتحصل مقاطع span التي تحتوي على نص من اليمين إلى اليسار على unicode-bidi: bidi-override. |
تمرّ <Box title> و <Box bottomTitle> و <Divider title> بالخطوات نفسها (ترسم drawLogicalText(buffer, x, y, text, …) أي سطر منفرد بهذه الطريقة)، لذا تظهر تسمياتها مرتبة ومتّصلة.
يتجاوز النص الخالي من محارف اليمين إلى اليسار كل ذلك. يُكتشف نموذج النص الذي يتجه بالكامل من اليسار إلى اليمين أثناء تقسيم وحداته الكتابية (مقارنة واحدة لكل وحدة كتابية) ويُرسم عبر المسار دون تغيير؛ وتكون إطارات وحدة التحكم متطابقة بايتًا ببايت.
عروض الخلايا
مجموعة الخطوط المضمّنة هي "SF Mono", Menlo, "Cascadia Mono", Consolas, monospace. لا يحتوي أي من هذه الخطوط على أشكال رسومية عبرية أو عربية، لذا تستخدم المتصفحات خطًا بديلًا لكل محرف. في macOS يكون البديل Courier New، وهو ثابت العرض بمقدار 0.6 em: 7.2012 px لكل شكل رسومي بحجم 12 px مقابل 7.2246 px في Menlo. إنه أضيق بنسبة 0.3%، وهو ما يكفي لانحراف صف كامل من العبرية بنحو نصف خلية.
يقيس CssConsoleDriver كل مقطع span من اليمين إلى اليسار باستخدام لوحة رسم بخط وحدة التحكم (بنفس تشكيل Blink والخطوط البديلة التي تستخدمها DevTools)، ثم يضيف إليه حشوًا بمقدار الفرق الدقيق بوحدة LayoutUnit (1/64 px). يصبح عرض مقطع span مساويًا لعرض خلايا الشبكة التي يغطيها (تُحسب الوحدة الكتابية العريضة، مثل الرمز التعبيري، بخليتين). يُقلّص مقطع span بمقدار نصف خلية على الأكثر، لذا يفيض الخط البديل ذو العرض النسبي (Times New Roman بجوار Cascadia Mono على Windows) بدلًا من السماح لمقطع span التالي بالتداخل مع أشكاله الرسومية. أُجري القياس في واجهة DevTools الفعلية (Chromium 151) على إطار 64×21 يضم أسطرًا عبرية وعربية ومختلطة وأسطرًا عبرية تتضمن رموزًا تعبيرية، وعناوين مربعات من اليمين إلى اليسار (bun bench/devtools/bidi-shot.ts):
| الإطار | انحراف الحافة اليمنى عبر الصفوف |
|---|---|
| خلايا مسجّلة دون تثبيت | 14.5 px (تعيد DevTools الترتيب والتشكيل) |
| ترتيب مثبّت، دون ضبط العرض | 2.22 px |
| برنامج التشغيل المضمّن | 0.156 px |
العينة الضابطة: الخلايا نفسها، مع استبدال كل شكل رسومي من اليمين إلى اليسار بالحرف x | 1.67 px |
يطابق الإطار المضمّن العينة الضابطة اللاتينية بالكامل في كل صف باستثناء صف الرموز التعبيرية، حيث يتفوّق عليها: تُضبط الرموز التعبيرية مع مقطع span المحيط بها من اليمين إلى اليسار (0.03 px)، بينما لا يُضبط صف الرموز التعبيرية من اليسار إلى اليمين في العينة الضابطة (1.66 px).
ترسم DevTools كل مقطع span ذي %c باستخدام contain: paint وتحاذي كلًا منها إلى بكسلات الجهاز على حدة، لذا عندما تلتقي خلفيتان عند موضع كسري قد لا ينتمي البكسل بينهما إلى أي منهما، تاركًا خطًا رفيعًا بعرض 1 px. في Chrome و Edge، يمدّد مقطع span ذو الخلفية خلفيته بمقدار 1 px تحت جار معتم إلى اليمين، ويضيف مقطع span المضبوط ذلك البكسل إلى حشوه. لا يظهر أي خط رفيع في الإطارات الأربعة أعلاه.
مرّر measureText: false لإيقاف ضبط العرض، أو وفّر دالة القياس الخاصة بك (text, bold, italic) => px.
الإدخال والتحرير
يحتفظ <Input> و <Textarea> بمؤشر منطقي. تتحرك مفاتيح الأسهم عبر النص بترتيب التخزين، لذا يتحرك ← بصريًا إلى اليمين في النص من اليمين إلى اليسار. يستقر المؤشر الكتلي على المحرف المنطقي الذي يغطيه، أينما تضعه إعادة الترتيب. داخل مقطع من اليمين إلى اليسار، تكون تلك الخلية إلى يسار المحرف السابق. التحديد نطاق منطقي أيضًا، ولذلك قد يغطي تمييزه خلايا غير متجاورة في الأسطر مختلطة الاتجاه. يتبع التمرير الأفقي في <Input> أحادي السطر العمود المنطقي، لذا قد تؤدي قيمة طويلة من اليمين إلى اليسار إلى تمرير المؤشر خارج العرض. يُرسم المؤشر الواقع على مسافة أو في نهاية النص كمسافة غير قابلة للكسر ومعزولة، فيبقى محايدًا ولا ينضم إلى الأرقام على أي من جانبيه.
القيود
- فواصل الفقرات. تنقسم الأسطر عند
\n. ينهي U+2029 وغيره من فواصل الفقرات فقرة ثنائية الاتجاه، لكنها لا تبدأ سطرًا جديدًا. - علامات الجدولة. تتوسع علامات الجدولة إلى مسافات قبل تشغيل خوارزمية الاتجاهين، لذا تعمل كمسافات بيضاء (WS)، لا كفواصل مقاطع (S).
- تشكيل الحروف. تُشكّل الحروف ذات أشكال العرض فقط: العربية والفارسية والأردية وغيرها من الحروف في U+FB50–U+FDFF و U+FE70–U+FEFF. تبقى اللام والألف حرفين متصلين في خليتين، لا وصلة ذات شكل رسومي واحد. تحتفظ السريانية و N'Ko وغيرها من الكتابات المتصلة بأشكالها الاسمية.
- النسخ. يكون النص المنسوخ من وحدة التحكم بترتيب العرض (مع علامتي
LRO/PDF)، لا بالترتيب المنطقي. - الطرفيات. قد تعيد الطرفيات التي تتجاهل كلًا من BDSM و
LROوتطبّق خوارزمية الاتجاهين الخاصة بها (مثل تفعيل الاتجاهين في iTerm2) ترتيب الصفوف من اليمين إلى اليسار مرة أخرى. - مرايا DOM. لا يضبط
<ConsoleMirror>العروض، لذا قد تنحرف المقاطع من اليمين إلى اليسار فيه بما يصل إلى 0.3% من عرضها.
ترجمة لوحة التركيز
يوجّه <FocusPad> من @textjs/react/dom (الذي يعرضه <TextCanvas> أيضًا) الزوار إلى فتح وحدة التحكم والنقر على الصفحة والضغط على مفتاح. نصه باللغة الإنجليزية افتراضيًا. لترجمته، مرّر messages مع السلاسل النصية التي تريد استبدالها. تحتفظ المفاتيح التي تتركها دون تحديد بنصها الإنجليزي المدرج في defaultFocusPadMessages.
import { FocusPad, type FocusPadMessages } from "@textjs/react/dom";const ja: Partial<FocusPadMessages> = { openConsole: "コンソールを開く", step: (n, total) => `${total}ステップ中${n}ステップ目`, then: (action) => `次にここをクリックして、${action}。`, shortcutOpens: (shortcut) => <>{shortcut}でコンソールが開きます。</>,};<FocusPad root={root} hints={[{ keys: "↑ ↓", label: "件数" }]} action="↑ ↓を押します" messages={ja} />;السلسلة النصية التي تحتوي على قيمة هي دالة تعيد الجملة كاملة، لذا يمكن للترجمة وضع القيمة في أي موضع فيها. تُعدّ step و pressShortcut و then و defaultAction و shortcutOpens دوال. تستقبل step رقم الخطوة وعدد الخطوات. تستقبل then الخاصية action، أو نص defaultAction عند غياب action. تستقبل defaultAction مفاتيح التلميح الأول، أو null عند غياب التلميحات. تستقبل pressShortcut مفاتيح الاختصار لقارئات الشاشة، وتستقبل shortcutOpens الاختصار على هيئة عناصر <kbd>.
تشمل messages أيضًا التسميات التي تعلنها قارئات الشاشة وأداة التحكم في حجم وحدة التحكم (sizeControl). يقبل <TextCanvas> الخاصية messages نفسها. ويقبلها <ViewportControl> أيضًا ويستخدم مفاتيح أداة التحكم في الحجم. لا تُترجم رموز المفاتيح مثل ⌘⌥J و Ctrl+Shift+J و Enter.
واجهة برمجة التطبيقات
import { reorderLine, paragraphDirection, resolveLevels, bidiClass, shapeArabicCodePoints } from "@textjs/core";reorderLine("שלום (abc) 123!"); // "!123 (abc) םולש": one line in display orderreorderLine("مرحبا", "auto", { shape: true }); // presentation forms, display orderparagraphDirection("123 שלום"); // "rtl" (rules P2–P3)تتيح resolveLevels(types, codePoints, direction) و applyLineRules و reorderIndices الوصول إلى الخوارزمية نفسها: القواعد P1–I2 و L1 و L2.