واجهة برمجة التطبيقات Intl.Collator
ترتيب ومقارنة السلاسل النصية بشكل صحيح عبر اللغات المختلفة
مقدمة
يبدو ترتيب السلاسل النصية في جافاسكريبت أمرًا بسيطًا حتى تواجه نصوصًا دولية. تستخدم المقارنة الافتراضية للسلاسل النصية قيم نقاط الترميز في يونيكود، مما ينتج عنه نتائج غير صحيحة للعديد من اللغات. توفر واجهة برمجة التطبيقات Intl.Collator مقارنة للسلاسل النصية تراعي اللغة المحلية وتحترم قواعد الترتيب الثقافية وتتعامل مع الأحرف الخاصة بشكل صحيح.
لماذا يفشل الترتيب الافتراضي
فكر في ترتيب قائمة بأسماء ألمانية:
const names = ["Zoe", "Ava", "Ärzte", "Änder"];
console.log(names.sort());
// ["Ava", "Zoe", "Änder", "Ärzte"]
هذه النتيجة خاطئة بالنسبة للمتحدثين بالألمانية. في اللغة الألمانية، يجب ترتيب الأحرف التي تحتوي على علامة الصوت المعدلة (umlaut) مثل ä بالقرب من الحرف الأساسي a، وليس في النهاية. تنبع المشكلة من مقارنة جافاسكريبت لقيم نقاط ترميز يونيكود، حيث يأتي Ä (U+00C4) بعد Z (U+005A).
تمتلك اللغات المختلفة قواعد ترتيب مختلفة. تقوم السويدية بترتيب ä في نهاية الأبجدية، بينما ترتبها الألمانية بالقرب من a، وتعامل الفرنسية الأحرف المشكلة بشكل مختلف. تتجاهل المقارنة الثنائية هذه الاتفاقيات الثقافية.
كيف يعمل تجميع السلاسل النصية
التجميع (Collation) هو عملية مقارنة وترتيب السلاسل النصية وفقًا لقواعد لغوية محددة. تحدد خوارزمية تجميع يونيكود كيفية مقارنة السلاسل النصية من خلال تحليل الأحرف والعلامات التشكيلية والحالة وعلامات الترقيم بشكل منفصل.
عند مقارنة سلسلتين نصيتين، تُرجع دالة التجميع رقمًا:
- قيمة سالبة: السلسلة الأولى تأتي قبل الثانية
- صفر: السلاسل متكافئة لمستوى الحساسية الحالي
- قيمة موجبة: السلسلة الأولى تأتي بعد الثانية
يعمل نمط المقارنة الثلاثي هذا مع Array.sort ويتيح تحكمًا دقيقًا في الاختلافات المهمة.
استخدام localeCompare للترتيب الأساسي المراعي للغة المحلية
توفر طريقة localeCompare مقارنة للسلاسل النصية تراعي اللغة المحلية:
const names = ["Zoe", "Ava", "Ärzte", "Änder"];
console.log(names.sort((a, b) => a.localeCompare(b, "de")));
// ["Ava", "Änder", "Ärzte", "Zoe"]
هذا ينتج ترتيبًا ألمانيًا صحيحًا. تحدد المعلمة الأولى اللغة المحلية، وتتعامل localeCompare مع القواعد الثقافية تلقائيًا.
يمكنك تمرير خيارات كمعلمة ثالثة:
const items = ["File10", "File2", "File1"];
console.log(items.sort((a, b) =>
a.localeCompare(b, "en", { numeric: true })
));
// ["File1", "File2", "File10"]
يتيح خيار numeric الترتيب الطبيعي حيث يأتي "2" قبل "10". بدونه، سيتم ترتيب "10" قبل "2" لأن "1" يأتي قبل "2".
مشكلة الأداء مع استخدام localeCompare بشكل متكرر
كل استدعاء لـ localeCompare يعالج إعدادات اللغة من الصفر. عند فرز مصفوفات كبيرة، يؤدي هذا إلى نفقات إضافية كبيرة:
// غير فعّال: يعالج إعدادات اللغة لكل مقارنة
const sorted = items.sort((a, b) => a.localeCompare(b, "de"));
فرز 1000 عنصر يتطلب تقريبًا 10000 مقارنة. كل مقارنة تعيد إنشاء تكوين اللغة، مما يضاعف تكلفة الأداء. تصبح هذه النفقات الإضافية ملحوظة في واجهات المستخدم ذات مجموعات البيانات الكبيرة.
استخدام Intl.Collator للمقارنة الفعّالة للسلاسل النصية
Intl.Collator ينشئ كائن مقارنة قابل لإعادة الاستخدام يعالج إعدادات اللغة مرة واحدة:
const collator = new Intl.Collator("de");
const sorted = items.sort((a, b) => collator.compare(a, b));
يخزن كائن collator تكوين اللغة وقواعد المقارنة. تستخدم طريقة compare هذه القواعد المحسوبة مسبقًا لكل مقارنة، مما يلغي النفقات الإضافية للتهيئة المتكررة.
تتراوح تحسينات الأداء من 60% إلى 80% عند فرز مصفوفات كبيرة مقارنة باستدعاءات localeCompare المتكررة.
الوصول إلى طريقة compare مباشرة
يمكنك تمرير طريقة compare مباشرة إلى sort:
const collator = new Intl.Collator("de");
const sorted = items.sort(collator.compare);
هذا يعمل لأن compare مرتبطة بكائن collator. تستقبل الطريقة سلسلتين نصيتين وتعيد نتيجة المقارنة، مما يطابق التوقيع الذي تتوقعه Array.sort.
فهم مستويات الحساسية
خيار sensitivity يتحكم في اختلافات الأحرف التي تهم أثناء المقارنة. هناك أربعة مستويات:
الحساسية الأساسية
الحساسية الأساسية تتجاهل علامات التشكيل وحالة الأحرف:
const collator = new Intl.Collator("en", { sensitivity: "base" });
console.log(collator.compare("a", "a")); // 0
console.log(collator.compare("a", "á")); // 0
console.log(collator.compare("a", "A")); // 0
console.log(collator.compare("a", "b")); // -1
فقط الأحرف الأساسية تختلف. يعمل هذا المستوى بشكل جيد للبحث التقريبي حيث قد لا يكتب المستخدمون علامات التشكيل بشكل صحيح.
حساسية التشكيل
حساسية التشكيل تأخذ بعين الاعتبار التشكيل ولكنها تتجاهل حالة الأحرف:
const collator = new Intl.Collator("en", { sensitivity: "accent" });
console.log(collator.compare("a", "a")); // 0
console.log(collator.compare("a", "á")); // -1
console.log(collator.compare("a", "A")); // 0
console.log(collator.compare("á", "A")); // 1
الأحرف المشكلة وغير المشكلة تختلف. الأحرف الكبيرة والصغيرة من نفس الحرف تتطابق.
حساسية حالة الأحرف
حساسية حالة الأحرف تأخذ بعين الاعتبار حالة الأحرف ولكنها تتجاهل التشكيل:
const collator = new Intl.Collator("en", { sensitivity: "case" });
console.log(collator.compare("a", "a")); // 0
console.log(collator.compare("a", "á")); // 0
console.log(collator.compare("a", "A")); // -1
console.log(collator.compare("á", "Á")); // -1
الاختلافات في حالة الأحرف مهمة ولكن يتم تجاهل التشكيل. هذا المستوى أقل شيوعًا في التطبيق العملي.
حساسية المتغيرات
حساسية المتغيرات تأخذ بعين الاعتبار جميع الاختلافات:
const collator = new Intl.Collator("en", { sensitivity: "variant" });
console.log(collator.compare("a", "a")); // 0
console.log(collator.compare("a", "á")); // -1
console.log(collator.compare("a", "A")); // -1
console.log(collator.compare("á", "Á")); // -1
هذا هو الإعداد الافتراضي للفرز. كل اختلاف في الأحرف ينتج نتيجة مقارنة مميزة.
اختيار الحساسية بناءً على حالة الاستخدام
تتطلب السيناريوهات المختلفة مستويات حساسية مختلفة:
- فرز القوائم: استخدم حساسية المتغيرات للحفاظ على ترتيب صارم
- البحث في المحتوى: استخدم الحساسية الأساسية للمطابقة بغض النظر عن التشكيل أو حالة الأحرف
- تصفية الخيارات: استخدم حساسية التشكيل عندما لا تهم حالة الأحرف
- البحث الحساس لحالة الأحرف: استخدم حساسية حالة الأحرف عندما لا يهم التشكيل
يوفر خيار الاستخدام إعدادات حساسية افتراضية للسيناريوهات الشائعة.
استخدام خيار الاستخدام لأوضاع الفرز والبحث
يحسن خيار الاستخدام سلوك المقارن للفرز أو البحث:
// محسن للفرز
const sortCollator = new Intl.Collator("en", { usage: "sort" });
// محسن للبحث
const searchCollator = new Intl.Collator("en", { usage: "search" });
استخدام الفرز يفترض حساسية المتغيرات افتراضيًا، مما يضمن أن كل اختلاف ينتج ترتيبًا متسقًا. استخدام البحث يحسن للعثور على التطابقات، عادة باستخدام حساسية أكثر مرونة.
للبحث غير الحساس لحالة الأحرف وغير الحساس للتشكيل:
const collator = new Intl.Collator("en", {
usage: "search",
sensitivity: "base"
});
const items = ["Apple", "Äpfel", "Banana"];
const matches = items.filter(item =>
collator.compare(item, "apple") === 0
);
console.log(matches); // ["Apple"]
يتيح هذا النمط المطابقة التقريبية حيث لا يحتاج المستخدمون إلى كتابة الأحرف بالضبط.
تمكين الفرز الرقمي للترتيب الطبيعي
يعامل خيار numeric الأرقام المضمنة كقيم رقمية:
const collator = new Intl.Collator("en", { numeric: true });
const files = ["File1", "File10", "File2"];
console.log(files.sort(collator.compare));
// ["File1", "File2", "File10"]
بدون الفرز الرقمي، سيتم ترتيب "File10" قبل "File2" لأن السلسلة "10" تبدأ بـ "1". يقوم الفرز الرقمي بتحليل تسلسلات الأرقام ومقارنتها رياضياً.
ينتج عن ذلك ترتيب طبيعي يتوافق مع توقعات البشر لأسماء الملفات وأرقام الإصدارات والقوائم المرقمة.
التعامل مع الأرقام العشرية مع الفرز الرقمي
الفرز الرقمي له قيود مع الأرقام العشرية:
const collator = new Intl.Collator("en", { numeric: true });
const values = ["1.5", "1.10", "1.2"];
console.log(values.sort(collator.compare));
// ["1.2", "1.5", "1.10"]
تتم معاملة النقطة العشرية كعلامة ترقيم، وليست جزءاً من الرقم. يتم فرز كل قسم بين علامات الترقيم بشكل منفصل. للفرز الرقمي العشري، قم بتحليل القيم إلى أرقام واستخدم المقارنة الرقمية.
التحكم في ترتيب الحالة باستخدام caseFirst
يحدد خيار caseFirst ما إذا كانت الأحرف الكبيرة أو الصغيرة تُفرز أولاً:
// الأحرف الكبيرة أولاً
const upperFirst = new Intl.Collator("en", { caseFirst: "upper" });
console.log(["a", "A", "b", "B"].sort(upperFirst.compare));
// ["A", "a", "B", "b"]
// الأحرف الصغيرة أولاً
const lowerFirst = new Intl.Collator("en", { caseFirst: "lower" });
console.log(["a", "A", "b", "B"].sort(lowerFirst.compare));
// ["a", "A", "b", "B"]
القيمة الافتراضية هي false، والتي تستخدم ترتيب اللغة الافتراضي. ليس لهذا الخيار أي تأثير عندما تكون الحساسية base أو accent لأن هذه المستويات تتجاهل حالة الأحرف.
تجاهل علامات الترقيم أثناء المقارنة
يتخطى خيار ignorePunctuation علامات الترقيم أثناء المقارنة:
const collator = new Intl.Collator("en", { ignorePunctuation: true });
console.log(collator.compare("hello", "he-llo")); // 0
console.log(collator.compare("hello", "hello!")); // 0
هذا الخيار افتراضي بقيمة true للغة التايلاندية وfalse للغات الأخرى. استخدمه عندما لا ينبغي أن تؤثر علامات الترقيم على ترتيب السلاسل أو مطابقتها.
تحديد أنواع الترتيب للقواعد الخاصة باللغات
تدعم بعض اللغات المحلية أنواعًا متعددة من الترتيب للفرز المتخصص:
// ترتيب الصينية بنظام بينيين
const pinyin = new Intl.Collator("zh-CN-u-co-pinyin");
// ترتيب دليل الهاتف الألماني
const phonebook = new Intl.Collator("de-DE-u-co-phonebk");
// تجميع الرموز التعبيرية
const emoji = new Intl.Collator("en-u-co-emoji");
يتم تحديد نوع الترتيب في سلسلة اللغة المحلية باستخدام بناء جملة امتداد يونيكود. تشمل الأنواع الشائعة:
pinyin: ترتيب الصينية حسب النطق المكتوب بالحروف اللاتينيةstroke: ترتيب الصينية حسب عدد الضرباتphonebk: ترتيب دليل الهاتف الألمانيtrad: قواعد الترتيب التقليدية للغات معينةemoji: تجميع الرموز التعبيرية حسب الفئة
تحقق من Intl.supportedValuesOf لمعرفة أنواع الترتيب المتاحة في بيئتك.
إعادة استخدام كائنات المقارنة عبر تطبيقك
قم بإنشاء كائنات المقارنة مرة واحدة وأعد استخدامها في جميع أنحاء تطبيقك:
// utils/collation.js
export const germanCollator = new Intl.Collator("de");
export const searchCollator = new Intl.Collator("en", {
sensitivity: "base"
});
export const numericCollator = new Intl.Collator("en", {
numeric: true
});
// في المكونات الخاصة بك
import { germanCollator } from "./utils/collation";
const sorted = names.sort(germanCollator.compare);
يعمل هذا النمط على تحسين الأداء والحفاظ على سلوك مقارنة متسق عبر قاعدة التعليمات البرمجية الخاصة بك.
فرز مصفوفات الكائنات حسب الخاصية
استخدم المقارن في دالة مقارنة تصل إلى خصائص الكائن:
const collator = new Intl.Collator("de");
const users = [
{ name: "Zoe" },
{ name: "Änder" },
{ name: "Ava" }
];
const sorted = users.sort((a, b) =>
collator.compare(a.name, b.name)
);
يعمل هذا النهج مع أي بنية كائن. استخرج السلاسل النصية للمقارنة ومررها إلى المقارن.
مقارنة أداء Intl.Collator مع localeCompare
يوفر Intl.Collator أداءً أفضل عند فرز مجموعات البيانات الكبيرة:
// أبطأ: يعيد إنشاء إعدادات اللغة المحلية لكل مقارنة
const slow = items.sort((a, b) => a.localeCompare(b, "de"));
// أسرع: يعيد استخدام إعدادات اللغة المحلية المحسوبة مسبقًا
const collator = new Intl.Collator("de");
const fast = items.sort(collator.compare);
بالنسبة للمصفوفات الصغيرة (أقل من 100 عنصر)، يكون الفرق ضئيلاً. بالنسبة للمصفوفات الكبيرة (آلاف العناصر)، يمكن أن يكون Intl.Collator أسرع بنسبة 60-80٪.
يوجد استثناء واحد في المتصفحات المستندة إلى V8 مثل Chrome. تمتلك localeCompare تحسينًا للسلاسل النصية التي تحتوي على ASCII فقط باستخدام جداول البحث. عند فرز سلاسل ASCII فقط، قد يكون أداء localeCompare مماثلاً لـ Intl.Collator.
معرفة متى تستخدم Intl.Collator مقابل localeCompare
استخدم Intl.Collator عندما:
- تقوم بفرز مصفوفات كبيرة (مئات أو آلاف العناصر)
- تقوم بالفرز بشكل متكرر (المستخدم يبدل ترتيب الفرز، القوائم الافتراضية)
- تبني أدوات مساعدة للمقارنة قابلة لإعادة الاستخدام
- الأداء مهم لحالة الاستخدام الخاصة بك
استخدم localeCompare عندما:
- تقوم بإجراء مقارنات لمرة واحدة
- فرز مصفوفات صغيرة (أقل من 100 عنصر)
- البساطة تفوق اعتبارات الأداء
- تحتاج إلى مقارنة مباشرة دون إعداد
كلا واجهتي البرمجة تدعمان نفس الخيارات وتنتجان نتائج متطابقة. الفرق يتعلق فقط بالأداء وتنظيم الكود.
التحقق من الخيارات المحلولة
تُرجع طريقة resolvedOptions الخيارات الفعلية المستخدمة بواسطة المقارن:
const collator = new Intl.Collator("de", { sensitivity: "base" });
console.log(collator.resolvedOptions());
// {
// locale: "de",
// usage: "sort",
// sensitivity: "base",
// ignorePunctuation: false,
// collation: "default",
// numeric: false,
// caseFirst: "false"
// }
هذا يساعد في تصحيح سلوك المقارنة وفهم القيم الافتراضية. قد تختلف اللغة المحلية المحلولة عن اللغة المحلية المطلوبة إذا كان النظام لا يدعم اللغة المحلية المحددة.
التحقق من دعم اللغة المحلية
تحقق من اللغات المحلية المدعومة في البيئة الحالية:
const supported = Intl.Collator.supportedLocalesOf(["de", "fr", "xx"]);
console.log(supported); // ["de", "fr"]
اللغات المحلية غير المدعومة ترجع إلى الإعداد الافتراضي للنظام. تساعد هذه الطريقة في اكتشاف متى تكون اللغة المحلية المطلوبة غير متوفرة.
دعم المتصفح والبيئة
تم دعم Intl.Collator على نطاق واسع منذ سبتمبر 2017. جميع المتصفحات الحديثة وإصدارات Node.js تدعمه. تعمل واجهة البرمجة بشكل متسق عبر البيئات المختلفة.
قد يكون لبعض أنواع المقارنة والخيارات دعم محدود في المتصفحات القديمة. اختبر الوظائف الحرجة أو تحقق من جداول التوافق في MDN إذا كنت تدعم البيئات القديمة.
أخطاء شائعة يجب تجنبها
لا تقم بإنشاء مُرتِّب (collator) جديد لكل مقارنة:
// خطأ: ينشئ المُرتِّب بشكل متكرر
items.sort((a, b) => new Intl.Collator("de").compare(a, b));
// صحيح: إنشاء مرة واحدة، وإعادة الاستخدام
const collator = new Intl.Collator("de");
items.sort(collator.compare);
لا تفترض أن الترتيب الافتراضي يعمل مع النصوص الدولية:
// خطأ: لا يعمل مع الأحرف غير ASCII
names.sort();
// صحيح: استخدم الترتيب المناسب للغة
names.sort(new Intl.Collator("de").compare);
لا تنس تحديد الحساسية للبحث:
// خطأ: حساسية المتغيرات تتطلب تطابقًا دقيقًا
const collator = new Intl.Collator("en");
items.filter(item => collator.compare(item, "apple") === 0);
// صحيح: حساسية أساسية للمطابقة التقريبية
const collator = new Intl.Collator("en", { sensitivity: "base" });
items.filter(item => collator.compare(item, "apple") === 0);
حالات الاستخدام العملية
استخدم Intl.Collator لـ:
- ترتيب المحتوى الذي ينشئه المستخدم (الأسماء، العناوين، العناوين البريدية)
- تنفيذ ميزات البحث والإكمال التلقائي
- بناء جداول البيانات ذات الأعمدة القابلة للترتيب
- إنشاء قوائم مفلترة وخيارات القوائم المنسدلة
- ترتيب أسماء الملفات وأرقام الإصدارات
- التنقل الأبجدي في قوائم جهات الاتصال
- واجهات التطبيق متعددة اللغات
أي واجهة تعرض نصًا مرتبًا للمستخدمين تستفيد من الترتيب المناسب للغة. هذا يضمن أن تطبيقك يبدو أصليًا وصحيحًا بغض النظر عن لغة المستخدم.