SmartKitio

الويب

يونيكود ولماذا ينكسر النص

يبدو النص سهلاً حتى يظهر اسم عميل كـ «Ã©»، أو تفشل كلمة مرور فيها إيموجي في التحقق، أو يختلف عدّاد تغريدة عن تطبيقك. هذه الأعطال نادراً ما تأتي من «أحرف غريبة» — بل من خلط ثلاث أفكار: كيف يسمّي Unicode الأحرف، وكيف تخزّن الترميزات بايتات، وماذا يدرك الإنسان كحرف واحد على الشاشة. الرمز المرئي ليس بايتاً واحداً وليس دائماً فهرساً واحداً في سلسلة JavaScript.

5 دقائق قراءة · آخر تحديث 2026-10-04

لقطة مقرّبة لحروف مطبوعة وطباعة تُوحي بترميز النص
صورة عبر Unsplash · رخصة Unsplash

يونيكود يعيّن أرقاماً؛ والترميزات تخزّن بايتات

معيار Unicode يعيّن نقطة ترميز — رقماً يُكتب مثل U+0041 للحرف A اللاتيني الكبير — لكل حرف مجرد في فهرسه. الفهرس ليس خطاً: الخطوط تربط نقاط الترميز برموز. UTF-8 وUTF-16 وUTF-32 مخططات ترميز تحوّل تسلسلات نقاط الترميز إلى بايتات للملفات وHTTP وقواعد البيانات. تظهر الأخطاء حين يعامل البرنامج بايتات UTF-8 كأحرف Latin-1، أو حين يفترض نظامان ترميزات مختلفة دون إعلانها.

نفس الحرف، تمثيلات مختلفة

المنظورمثال للحرف é (U+00E9)
نقطة الترميزU+00E9 (حرف واحد معيّن)
بايتات UTF-8 (hex)C3 A9 (بايتان)
وحدات ترميز UTF-16 (hex)00 E9 (وحدة واحدة في BMP)
طول سلسلة JavaScript1 (وحدة UTF-16 واحدة لـ é في BMP)

UTF-8 عملياً

UTF-8 بعرض متغيّر: حروف ASCII بايت واحد؛ كثير من الكتابات الأوروبية بايتان؛ معظم الإيموجي والكتابات النادرة أربعة. هو الترميز السائد على الويب (HTTP وJSON وHTML). حين يقول نظام «Unicode» في 2026، يقصد غالباً «UTF-8 على السلك ما لم يُذكر غير ذلك».

text
Character: é
UTF-8 bytes: 0xC3 0xA9
Mis-decoded as Latin-1: à ©  → often shown as "é" (mojibake)

UTF-16 وسلاسل JavaScript

سلاسل JavaScript تسلسلات من وحدات ترميز UTF-16، لا نقاط Unicode. الأحرف خارج Basic Multilingual Plane (فوق U+FFFF) — كثير من الإيموجي وحروف Han نادرة — تأخذ وحدتين UTF-16 تُسمى surrogate pair. لذا String.length يعدّ وحدات ترميز لا أحرفاً مرئية، والفهرسة بـ str[i] قد تقطع زوج surrogate وتنتج محارف تالفة.

javascript
const smile = "😀";
console.log(smile.length); // 2 (surrogate pair)
console.log(smile[0] === smile[1]); // false
console.log([...smile].length); // 1 (iterates code points)
console.log(Array.from(smile).length); // 1

الانتشار وArray.from يتكرّران على نقاط ترميز Unicode. للحدود في الإنتاج، فضّل Intl.Segmenter أو مكتبة واعية بالرسوم عند عدّ ما يراه المستخدمون.

تجمّعات الرسوم: ما يعدّه المستخدمون

قد يجمع الحرف المرئي عدة نقاط ترميز. حرف أساسي مع حركة حادة مركبة (e + ◌́) قد يُعرض كـ é لكنه يبقى نقطتين. أعلام الإيموجي أزواج من حرفي مؤشر إقليمي؛ معدّلات لون البشرة تلحق بإيموجي الأشخاص؛ سلاسل ZWJ تربط إيموجي العائلة. Unicode يحدّد حدود extended grapheme cluster لـ«ما يشبه حرفاً واحداً» في التحرير وحركة المؤشر.

رمز واحد، نقاط ترميز متعددة

مرئينقاط الترميز (مفهومياً)فخ JS .length
é (مركّب مسبقاً)U+00E91 وحدة ترميز
é (e + حركة حادة مركبة)U+0065 U+03012 وحدة ترميز
👨‍👩‍👧‍👦 (عائلة ZWJ)عدة نقاط + ZWJ U+200D.length ≫ 1
🇫🇷 (علم)مؤشران إقليميان.length === 4
1️⃣ (رقم keycap)U+0031 U+FE0F U+20E3.length === 3
👍🏽 (لون بشرة)U+1F44D U+1F3FD.length === 4
javascript
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
const text = "e\u0301"; // e + combining acute
console.log(text.length); // 2
console.log([...segmenter.segment(text)].length); // 1 grapheme
javascript
const bio = "Hi 👨‍👩‍👧‍👦!"; // family ZWJ sequence
console.log(bio.length); // 11 UTF-16 code units
console.log([...bio].length); // 8 Unicode code points
console.log([...new Intl.Segmenter("en", { granularity: "grapheme" }).segment(bio)].length); // 7 graphemes (letters, space, family emoji, !)
// A "max 10 characters" rule using .length rejects this bio even though it looks short.

النص المشوّه (mojibake) وإصلاح أخطاء الترميز

mojibake (نص مشوّش) يعني عادة أن بايتات فُسّرت بمفكّك خاطئ. الحالة الكلاسيكية: بايتات UTF-8 قُرئت كـ Windows-1252 أو ISO-8859-1. الإصلاح إعادة قراءة تدفق البايت الأصلي بالترميز الذي أُنتج به، ثم تحويل إلى UTF-8 — لا استبدال «Ã©» بـ «é» يدوياً في سلسلة فقدت معلومات.

مسار نص سليم
  1. بايتات داخلة

    رفع ملف أو جسم HTTP أو BLOB في قاعدة البيانات

  2. اكتشاف أو إعلان الترميز

    BOM أو charset في Content-Type أو بيانات وصفية

  3. فك إلى Unicode

    نقاط ترميز في الذاكرة بلغتك

  4. معالجة

    بحث وتحقق وعرض — على نص Unicode

  5. ترميز عند الإخراج

    تقريباً دائماً UTF-8 لواجهات الويب

حوّل عند الحواف؛ احتفظ بـ Unicode في الوسط.

الإيموجي وحدود الطول

حدود المنتج — «140 حرفاً»، مقاطع SMS، VARCHAR في قاعدة البيانات — تختار قاعدة عدّ. عدّ Twitter التاريخي استخدم وحدات ترميز موزونة؛ SMS يستخدم GSM-7 أو مقاطع UCS-2؛ قواعد البيانات قد تعدّ بايتات. رسالة غنية بالإيموجي قد تتجاوز ميزانية بايتات بينما تبدو قصيرة بصرياً. وثّق أي مقياس يعرضه العداد: بايتات، وحدات UTF-16، نقاط ترميز، أو رسوم.

  • حدود التخزين (bytea في PostgreSQL مقابل طول char) تهتم غالباً بالبايتات في UTF-8.
  • maxlength في JavaScript على المدخلات يعدّ غالباً وحدات UTF-16.
  • لوائح أو فوترة قد تعدّ رسوماً أو صفحات ترميز legacy.
  • Base64 يوسّع UTF-8 الثنائي — ترميز النص للروابط ليس مثل عدّ الأحرف.

التطبيع: حين تفشل المساواة

نفس السلسلة المرئية قد يكون لها تمثيلات Unicode متعددة. NFC وNFD يختاران تسلسلات مركّبة أو مفكّكة قياسية. كلمات المرور وأسماء المستخدمين يجب مقارنتها بعد تطبيع متفق عليه — وإلا تحرم مستخدمين ينتج IME لديهم تسلسلاً مختلفاً لكن مكافئاً.

قاعدة عملية: طبّع إلى NFC للتخزين والمقارنة ما لم يحدّد المعيار غير ذلك (بعض سياقات التشفير تمنع التطبيع).

طي الحالة والlocale

I التركية المنقطة وغير المنقطة، وß الألمانية، ومتغيّرات sigma اليونانية تبيّن لماذا toLowerCase() وحده استراتيجية مساواة ضعيفة. case folding في Unicode والفرز الواعي بالسياق المحلي ينتميان لفهارس البحث وسياسات أسماء المستخدمين، لا إلى حيل نصية ad hoc في كود التطبيق.

Surrogates وتسلسلات غير صالحة

UTF-8 يسمح فقط بتسلسلات بايت سليمة؛ surrogates منفردة غير قانونية في UTF-8 لكنها قد تظهر عند سوء التعامل مع بيانات UTF-16. محلّلات تستبدل بصمت U+FFFD قد تخفي فساد بيانات — سجّل وارفض إدخالاً غير صالح عند حدود الثقة حين تهم السلامة.

قائمة تحقق للمطوّرين

  1. أعلن charset=utf-8 على استجابات HTML وJSON.
  2. لا تفترض بايتاً واحداً لكل حرف في أحجام المخازن المؤقتة.
  3. لا تستخدم طول السلسلة وحده لحدود مواجهة المستخدم.
  4. استخدم Intl.Segmenter أو ICU لحدود تحرير واعية بالرسوم.
  5. اختبر بعلامات مركبة وأعلام إيموجي وسلاسل ZWJ في مجموعات QA.
  6. عند دمج CSV قديم أو تصدير Windows، اسأل أي ترميز أنشأ الملف.

عدادات الأحرف والكلمات في SmartKitio يجب قراءتها مع وضع العدّ الموثّق — قارن بنتائج منصتك قبل فرض حدود في الإنتاج.

البيانات الثنائية ليست نصاً

Base64 وhex تمثّل بايتات، لا سلاسل Unicode. فك Base64 ينتج بايتات لا بد من فكها بـ UTF-8 (أو charset صحيح) قبل معاملة النتيجة كنص. تمرير ثنائي عبر واجهات string دون خطوة ترميز صريحة يعيد mojibake في طبقة أخرى — شائع في مسارات الرفع ومرفقات البريد.

ما يجب تذكّره

  • Unicode يسمّي الأحرف كنقاط ترميز؛ UTF-8 (وغيره) يخزّن تلك النقاط بايتات.
  • JavaScript .length يعدّ وحدات UTF-16 — أزواج surrogate وتسلسلات مركبة تكسر حدوداً ساذجة.
  • المستخدمون يعدّون تجمّعات رسوم؛ استخدم Intl.Segmenter أو ICU حين تقول قواعد المنتج «أحرف».
  • mojibake غالباً فك ترميز خاطئ عند حد — أصلح المسار لا السلسلة بالبحث والاستبدال.
  • طبّع (غالباً NFC) قبل مقارنة كلمات مرور وأسماء مستخدمين من IME مختلفة.

قراءة إضافية

  1. Unicode Standard — Fundamental concepts
  2. Unicode — UTF-8 FAQ
  3. MDN — UTF-16 characters, Unicode code points, and grapheme clusters
  4. MDN — Intl.Segmenter

مقالات ذات صلة