أساسيات JavaScript

القوالب النصية والقوالب الموسومة

45 دقيقة الدرس 20 من 60

مقدمة في القوالب النصية

القوالب النصية التي قدمت في ES6 هي طريقة اقوى للتعامل مع النصوص في JavaScript. تستخدم احرف الفاصلة العكسية (`) بدلا من علامات الاقتباس المفردة (') او المزدوجة ("). توفر القوالب النصية ثلاث مزايا رئيسية على النصوص التقليدية: استيفاء التعبيرات ودعم الاسطر المتعددة والقدرة على انشاء قوالب موسومة. هذه الميزات تجعل معالجة النصوص انظف واسهل قراءة واكثر قوة بكثير. قبل القوالب النصية كان بناء النصوص الديناميكية يتطلب ضما محرجا باستخدام عامل +. القوالب النصية تلغي ذلك تماما وتفتح انماطا متقدمة مثل القوالب الموسومة التي تتيح لغات خاصة بالمجال وتوليد HTML امن وانظمة التدويل.

صيغة القوالب النصية

التغيير الاساسي هو المحدد. بدلا من تغليف النص بعلامات اقتباس مفردة او مزدوجة تغلفه بفاصلات عكسية. القالب النصي المحاط بفاصلات عكسية يتصرف مثل نص عادي بعدة طرق -- يمكنك تعيينه لمتغيرات وتمريره للدوال واستخدامه في اي مكان يتوقع فيه نص. القوة الحقيقية تاتي مما يمكنك تضمينه داخل تلك الفاصلات العكسية.

مثال: صيغة القالب النصي الاساسية

// النصوص التقليدية
const single = 'Hello, World!';
const double = "Hello, World!";

// القالب النصي
const template = `Hello, World!`;

// الثلاثة تنتج نفس النص
console.log(single === double);    // true
console.log(double === template);  // true

// القوالب النصية يمكن ان تحتوي على علامات اقتباس مفردة ومزدوجة بدون هروب
const message = `She said "it's a beautiful day" and smiled.`;
console.log(message);
// She said "it's a beautiful day" and smiled.

// مع النصوص التقليدية ستحتاج للهروب
const oldMessage = "She said \"it's a beautiful day\" and smiled.";
const oldMessage2 = 'She said "it\'s a beautiful day" and smiled.';
نصيحة احترافية: القوالب النصية مثالية عندما يحتوي نصك على علامات اقتباس مفردة ومزدوجة معا وهو امر شائع في توليد HTML والنصوص اللغوية الطبيعية. تتجنب فوضى احرف الهروب مما يجعل الكود اسهل في القراءة والصيانة.

استيفاء التعبيرات

الميزة الاكثر استخداما في القوالب النصية هي استيفاء التعبيرات. باستخدام صيغة ${expression} يمكنك تضمين اي تعبير JavaScript مباشرة داخل نص. يتم تقييم التعبير وتحويله لنص وادراجه في مكانه. هذا يحل محل نمط ضم النصوص الممل الذي كان الخيار الوحيد قبل ES6.

مثال: الاستيفاء الاساسي

const name = 'Alice';
const age = 28;

// طريقة الضم القديمة
const oldGreeting = 'Hello, my name is ' + name + ' and I am ' + age + ' years old.';

// استيفاء القالب النصي
const newGreeting = `Hello, my name is ${name} and I am ${age} years old.`;

console.log(oldGreeting); // Hello, my name is Alice and I am 28 years old.
console.log(newGreeting); // Hello, my name is Alice and I am 28 years old.

// اي تعبير يعمل داخل ${}
const price = 49.99;
const taxRate = 0.08;
console.log(`Total: $${(price * (1 + taxRate)).toFixed(2)}`);
// Total: $53.99

// التعبيرات الثلاثية
const isAdmin = true;
console.log(`Role: ${isAdmin ? 'Administrator' : 'User'}`);
// Role: Administrator

// استدعاءات الدوال
const items = ['apple', 'banana', 'cherry'];
console.log(`You have ${items.length} items: ${items.join(', ')}`);
// You have 3 items: apple, banana, cherry

التعبيرات مقابل التعليمات

داخل ${} يمكنك وضع اي تعبير -- اي شيء ينتج قيمة. يشمل ذلك العمليات الحسابية واستدعاءات الدوال والعوامل الثلاثية والوصول للخصائص واستدعاء الطرق وحتى قوالب نصية اخرى. لكن لا يمكنك وضع تعليمات مثل if وfor وwhile او تصريحات المتغيرات داخل اقواس الاستيفاء. اذا احتجت منطقا معقدا استخرجه في دالة واستدعها داخل الاستيفاء.

مثال: تعبيرات معقدة في الاستيفاء

const user = {
    firstName: 'John',
    lastName: 'Doe',
    scores: [85, 92, 78, 95, 88]
};

// الوصول للخصائص واستدعاء الطرق
console.log(`Full name: ${user.firstName} ${user.lastName}`);
// Full name: John Doe

// طرق المصفوفة داخل الاستيفاء
const average = user.scores.reduce((a, b) => a + b, 0) / user.scores.length;
console.log(`Average score: ${average.toFixed(1)}`);
// Average score: 87.6

// قوالب نصية متداخلة
console.log(`Best score: ${Math.max(...user.scores)} out of ${user.scores.length} tests`);
// Best score: 95 out of 5 tests

// تفكيك الكائنات والاستخدام الفوري
const formatUser = ({ firstName, lastName }) => `${lastName}, ${firstName}`;
console.log(`Formatted: ${formatUser(user)}`);
// Formatted: Doe, John

// التعبيرات المنطقية
const count = 0;
console.log(`${count || 'No'} messages found.`);
// No messages found.

// دمج القيم المعدومة
const nickname = null;
console.log(`Display name: ${nickname ?? 'Anonymous'}`);
// Display name: Anonymous
خطا شائع: تجنب وضع الكثير من المنطق داخل اقواس ${}. على الرغم من انك تستطيع تقنيا تضمين تعبيرات معقدة فان ذلك يضر بسهولة القراءة. اذا كان التعبير اطول من سطر واحد او صعب الفهم بنظرة سريعة استخرجه في متغير او دالة اولا ثم استوفِ النتيجة. ابقِ الاستيفاء بسيطا وسيكون الكود المحيط اسهل بكثير في الصيانة.

النصوص متعددة الاسطر

قبل القوالب النصية كان انشاء نصوص متعددة الاسطر في JavaScript يتطلب اما ضم النصوص مع اسطر جديدة او احرف الهروب. القوالب النصية تحافظ على فواصل الاسطر تماما كما تظهر في الكود المصدري. عندما تضغط Enter داخل قالب نصي يتضمن النص الناتج حرف السطر الجديد. هذا يجعل القوالب النصية مثالية لتوليد HTML وكتابة الرسائل المنسقة وانشاء كتل نصية.

مثال: النصوص متعددة الاسطر

// الطريقة القديمة: الضم مع \n
const oldPoem = 'Roses are red,\n' +
    'Violets are blue,\n' +
    'Template literals,\n' +
    'Are awesome for you.';

// القالب النصي: فقط اضغط Enter
const newPoem = `Roses are red,
Violets are blue,
Template literals,
Are awesome for you.`;

console.log(oldPoem === newPoem); // true

// متعدد الاسطر مع الاستيفاء
const name = 'Developer';
const date = new Date().toLocaleDateString();
const welcomeMessage = `
Welcome back, ${name}!
Today is ${date}.
You have 3 unread notifications.
`;
console.log(welcomeMessage);

// توليد مخرجات منسقة
const scores = [
    { name: 'Alice', score: 95 },
    { name: 'Bob', score: 87 },
    { name: 'Charlie', score: 92 }
];

const report = `
=== Score Report ===
${scores.map(s => `  ${s.name}: ${s.score}/100`).join('\n')}
===================
Total students: ${scores.length}
Average: ${(scores.reduce((a, s) => a + s.score, 0) / scores.length).toFixed(1)}
`;
console.log(report);
ملاحظة: كن حذرا مع المسافة البادئة في القوالب النصية متعددة الاسطر. المسافة البيضاء في بداية كل سطر مضمنة في النص. اذا اضفت مسافة بادئة للقوالب النصية داخل دوال او شروط تصبح المسافة البادئة الاضافية جزءا من النص. فكر في استخدام دالة مساعدة لازالة المسافة البادئة البيضاء او استخدم مكتبات مثل dedent لهذا الغرض.

مثال: مشكلة المسافة البادئة والحل

function generateHTML() {
    // المشكلة: المسافة البادئة جزء من النص
    const html = `
        <div>
            <h1>Title</h1>
            <p>Paragraph</p>
        </div>
    `;
    // النص يحتوي على 8 مسافات من المسافة البادئة في كل سطر!

    // الحل 1: البدء من العمود 0 (يبدو غريبا في الكود)
    const html2 =
`<div>
    <h1>Title</h1>
    <p>Paragraph</p>
</div>`;

    // الحل 2: قص كل سطر
    const html3 = `
        <div>
            <h1>Title</h1>
            <p>Paragraph</p>
        </div>
    `.split('\n').map(line => line.trim()).filter(Boolean).join('\n');

    return html3;
}

تداخل القوالب النصية

يمكنك تداخل القوالب النصية داخل بعضها البعض. بما ان استيفاء ${} يمكن ان يحتوي على اي تعبير والقالب النصي هو تعبير يمكنك تضمين قوالب نصية داخل قوالب نصية. هذا مفيد بشكل خاص للعرض الشرطي وتوليد نصوص معقدة بناء على البيانات. كل مستوى من التداخل يستخدم مجموعته الخاصة من الفاصلات العكسية ولا تتعارض مع بعضها.

مثال: القوالب النصية المتداخلة

const isLoggedIn = true;
const username = 'Alex';

// التداخل مع التعبير الثلاثي
const header = `<header>${isLoggedIn
    ? `<span>Welcome, ${username}</span><a href="/logout">Logout</a>`
    : `<a href="/login">Login</a><a href="/signup">Sign Up</a>`
}</header>`;
console.log(header);
// <header><span>Welcome, Alex</span><a href="/logout">Logout</a></header>

// توليد قائمة مع قوالب متداخلة
const items = [
    { name: 'Laptop', price: 999.99, inStock: true },
    { name: 'Mouse', price: 29.99, inStock: true },
    { name: 'Keyboard', price: 79.99, inStock: false }
];

const productList = `
<ul class="products">
${items.map(item => `    <li class="${item.inStock ? 'available' : 'sold-out'}">
        ${item.name} - $${item.price.toFixed(2)}
        ${item.inStock ? `<button>Add to Cart</button>` : `<span>Out of Stock</span>`}
    </li>`).join('\n')}
</ul>`;
console.log(productList);

// تداخل عميق لهياكل البيانات المعقدة
const sections = [
    {
        title: 'Frontend',
        skills: ['HTML', 'CSS', 'JavaScript', 'React']
    },
    {
        title: 'Backend',
        skills: ['Node.js', 'Python', 'SQL']
    }
];

const resume = `
${sections.map(section => `
<section>
    <h2>${section.title}</h2>
    <ul>
        ${section.skills.map(skill => `<li>${skill}</li>`).join('\n        ')}
    </ul>
</section>`).join('\n')}`;
console.log(resume);

القوالب الموسومة

القوالب الموسومة هي الميزة الاكثر تقدما وقوة في القوالب النصية. يتم انشاء قالب موسوم بوضع اسم دالة (تسمى "دالة الوسم") مباشرة قبل الفاصلة العكسية بدون مسافة بينهما. بدلا من تقييم القالب النصي الى نص تلقائيا تستقبل دالة الوسم اجزاء القالب ويمكنها معالجتها كما تريد. هذا يفتح الباب لمعالجة النصوص المخصصة وتوليد HTML الامن ومنع حقن SQL والتدويل و CSS-في-JS وانماط اخرى كثيرة.

كيف تعمل القوالب الموسومة

عندما تكتب tagFunction`string with ${expr}` لا يقيم JavaScript القالب النصي الى نص ببساطة. بدلا من ذلك يستدعي tagFunction بمعاملات خاصة. المعامل الاول هو مصفوفة اجزاء النص الثابتة (القطع بين الاستيفاءات). المعاملات المتبقية هي قيم الاستيفاء المقيمة. يمكن لدالة الوسم بعد ذلك دمج هذه القطع كما تريد وارجاع اي قيمة -- ليس فقط نصا.

مثال: فهم معاملات دالة الوسم

function inspect(strings, ...values) {
    console.log('Static strings:', strings);
    console.log('Interpolated values:', values);
    console.log('strings.length:', strings.length);
    console.log('values.length:', values.length);
    // strings.length دائما تساوي values.length + 1
}

const name = 'Alice';
const age = 30;

inspect`Hello, ${name}! You are ${age} years old.`;
// Static strings: ['Hello, ', '! You are ', ' years old.']
// Interpolated values: ['Alice', 30]
// strings.length: 3
// values.length: 2

// بدون استيفاء
inspect`Just a plain string`;
// Static strings: ['Just a plain string']
// Interpolated values: []
// strings.length: 1
// values.length: 0

// البدء او الانتهاء باستيفاء
inspect`${name} is here`;
// Static strings: ['', ' is here']
// Interpolated values: ['Alice']

inspect`Say hello to ${name}`;
// Static strings: ['Say hello to ', '']
// Interpolated values: ['Alice']
ملاحظة: مصفوفة strings دائما تحتوي على عنصر واحد اكثر من مصفوفة values. اذا بدا القالب باستيفاء يكون اول نص فارغا (''). اذا انتهى باستيفاء يكون اخر نص فارغا. هذه العلاقة المتسقة تسهل تشابك المصفوفتين.

بناء دالة وسم اساسية

ابسط دالة وسم تعيد بناء النص الاصلي بتشابك الاجزاء الثابتة والقيم. من هناك يمكنك تحويل القيم قبل ادراجها -- مثلا تحويلها لاحرف كبيرة او هروب الاحرف الخاصة او تطبيق التنسيق.

مثال: دوال وسم مخصصة

// اعادة بناء النص (السلوك الافتراضي)
function identity(strings, ...values) {
    let result = '';
    strings.forEach((str, i) => {
        result += str;
        if (i < values.length) {
            result += values[i];
        }
    });
    return result;
}

const name = 'World';
console.log(identity`Hello, ${name}!`); // Hello, World!

// تمييز القيم المستوفاة
function highlight(strings, ...values) {
    return strings.reduce((result, str, i) => {
        const value = i < values.length ? `**${values[i]}**` : '';
        return result + str + value;
    }, '');
}

const item = 'JavaScript';
const count = 42;
console.log(highlight`Learning ${item} in ${count} days`);
// Learning **JavaScript** in **42** days

// تكبير جميع القيم المستوفاة
function shout(strings, ...values) {
    return strings.reduce((result, str, i) => {
        const value = i < values.length
            ? String(values[i]).toUpperCase()
            : '';
        return result + str + value;
    }, '');
}

console.log(shout`Hello ${name}, welcome to ${item}!`);
// Hello WORLD, welcome to JAVASCRIPT!

توليد HTML الامن

احد اكثر الاستخدامات العملية للقوالب الموسومة هو منع هجمات Cross-Site Scripting (XSS) من خلال هروب كيانات HTML تلقائيا في القيم المستوفاة. هذا نفس النمط المستخدم في مكتبات مثل lit-html لعرض DOM الامن.

مثال: دالة وسم لهروب HTML

function safeHTML(strings, ...values) {
    const escapeHTML = (str) => {
        return String(str)
            .replace(/&/g, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&#039;');
    };

    return strings.reduce((result, str, i) => {
        const value = i < values.length ? escapeHTML(values[i]) : '';
        return result + str + value;
    }, '');
}

// آمن: ادخال المستخدم يتم هروبه تلقائيا
const userInput = '<script>alert("XSS!")</script>';
const userName = 'Alice <b>Bold</b>';

const html = safeHTML`
    <div class="user-profile">
        <h2>${userName}</h2>
        <p>Bio: ${userInput}</p>
    </div>
`;
console.log(html);
// <div class="user-profile">
//     <h2>Alice &lt;b&gt;Bold&lt;/b&gt;</h2>
//     <p>Bio: &lt;script&gt;alert(&quot;XSS!&quot;)&lt;/script&gt;</p>
// </div>

// البرنامج الضار يعرض كنص غير ضار
// قارن مع الضم غير الامن:
const unsafeHTML = '<div>' + userInput + '</div>';
// هذا سينفذ البرنامج اذا حقن في DOM!
مهم: دائما اهرب البيانات المقدمة من المستخدم قبل ادراجها في HTML. القوالب الموسومة تجعل هذا تلقائيا وصعب النسيان. اذا كنت تبني نصوص HTML يدويا بالضم فمن السهل تفويت خطوة هروب. استخدام دالة وسم يضمن ان كل قيمة مستوفاة تمر عبر مرشح الامان الخاص بك بشكل متسق.

النصوص الخام مع String.raw

يوفر JavaScript دالة وسم مدمجة تسمى String.raw تعيد النص الخام دون معالجة تسلسلات الهروب. عادة يتم تفسير تسلسلات الهروب بالشرطة المائلة العكسية مثل \n و\t و\\ بواسطة JavaScript. مع String.raw تعامل الشرطات المائلة العكسية كاحرف حرفية. هذا مفيد للتعبيرات المنتظمة ومسارات ملفات Windows واي موقف تريد فيه ان تبقى الشرطات المائلة العكسية كما هي.

مثال: String.raw

// القالب النصي العادي يعالج تسلسلات الهروب
const normal = `Line 1\nLine 2\tTabbed`;
console.log(normal);
// Line 1
// Line 2	Tabbed

// String.raw يبقي تسلسلات الهروب كنص حرفي
const raw = String.raw`Line 1\nLine 2\tTabbed`;
console.log(raw);
// Line 1\nLine 2\tTabbed

// مفيد لانماط التعبيرات المنتظمة
const regexPattern = String.raw`\d+\.\d+\.\d+\.\d+`;
const ipRegex = new RegExp(regexPattern);
console.log(ipRegex.test('192.168.1.1')); // true

// مسارات ملفات Windows
const filePath = String.raw`C:\Users\Alice\Documents\project`;
console.log(filePath);
// C:\Users\Alice\Documents\project

// تعبيرات شبيهة بـ LaTeX
const latex = String.raw`\frac{1}{2} + \frac{1}{3} = \frac{5}{6}`;
console.log(latex);
// \frac{1}{2} + \frac{1}{3} = \frac{5}{6}

// الاستيفاء لا يزال يعمل مع String.raw
const version = '1.0.0';
console.log(String.raw`Version: ${version}, Path: C:\app\v${version}`);
// Version: 1.0.0, Path: C:\app\v1.0.0

الوصول للنصوص الخام في الوسوم المخصصة

كل دالة وسم تستقبل مصفوفة strings التي لها خاصية raw خاصة. مصفوفة strings.raw تحتوي على النسخ الخام لكل جزء نصي بدون معالجة تسلسلات الهروب. هكذا يتم تنفيذ String.raw داخليا ويمكنك استخدامه في دوال الوسم الخاصة بك.

مثال: استخدام strings.raw في الوسوم المخصصة

function showRaw(strings, ...values) {
    console.log('Cooked strings:', strings);
    console.log('Raw strings:', strings.raw);
}

showRaw`Hello\nWorld\t${'test'}!`;
// Cooked strings: ['Hello\nWorld\t', '!']
// Raw strings: ['Hello\\nWorld\\t', '!']

// بناء نسختك الخاصة من String.raw
function myRaw(strings, ...values) {
    return strings.raw.reduce((result, str, i) => {
        const value = i < values.length ? values[i] : '';
        return result + str + value;
    }, '');
}

console.log(myRaw`Path: C:\Users\file.txt`);
// Path: C:\Users\file.txt

بناء HTML باستخدام القوالب النصية

القوالب النصية قوية للغاية لتوليد نصوص HTML. الجمع بين دعم الاسطر المتعددة والاستيفاء والقدرة على تضمين التعبيرات يجعلها مثالية لانشاء محتوى HTML ديناميكي. يستخدم هذا النمط على نطاق واسع في مكونات الويب والعرض من جانب الخادم واطر عمل الواجهة الامامية.

مثال: توليد HTML الديناميكي

// دالة شبيهة بالمكونات تعيد HTML
function UserCard({ name, avatar, role, skills, isOnline }) {
    return `
        <div class="user-card ${isOnline ? 'online' : 'offline'}">
            <img src="${avatar}" alt="${name}'s avatar" class="avatar">
            <div class="info">
                <h3>${name}</h3>
                <span class="role">${role}</span>
                <span class="status">${isOnline ? 'Online' : 'Offline'}</span>
            </div>
            ${skills.length > 0 ? `
                <ul class="skills">
                    ${skills.map(skill => `<li>${skill}</li>`).join('\n                    ')}
                </ul>
            ` : '<p>No skills listed</p>'}
        </div>
    `;
}

const cardHTML = UserCard({
    name: 'Alice',
    avatar: '/img/alice.jpg',
    role: 'Senior Developer',
    skills: ['JavaScript', 'React', 'Node.js'],
    isOnline: true
});
console.log(cardHTML);

// توليد تخطيط صفحة كامل
function PageLayout({ title, content, sidebar, footer }) {
    return `
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>${title}</title>
</head>
<body>
    <main>${content}</main>
    ${sidebar ? `<aside>${sidebar}</aside>` : ''}
    <footer>${footer}</footer>
</body>
</html>`;
}

// توليد جدول من البيانات
function DataTable(headers, rows) {
    return `
<table>
    <thead>
        <tr>${headers.map(h => `<th>${h}</th>`).join('')}</tr>
    </thead>
    <tbody>
        ${rows.map(row => `
        <tr>${row.map(cell => `<td>${cell}</td>`).join('')}</tr>`).join('')}
    </tbody>
</table>`;
}

const table = DataTable(
    ['Name', 'Score', 'Grade'],
    [
        ['Alice', 95, 'A'],
        ['Bob', 87, 'B+'],
        ['Charlie', 92, 'A-']
    ]
);
console.log(table);

حالات الاستخدام العملية

القوالب الموسومة والقوالب النصية تتيح بعض الانماط القوية جدا في تطوير JavaScript في الواقع. دعنا نستكشف عدة حالات استخدام عملية قد تنفذها او تواجهها في قواعد اكواد الانتاج.

منشئ استعلامات شبيه بـ SQL

يمكن استخدام القوالب الموسومة لبناء استعلامات محددة المعاملات بامان. دالة الوسم تفصل SQL الثابت عن القيم الديناميكية مما يمنع هجمات حقن SQL بالتصميم. هذا هو النمط الدقيق المستخدم في مكتبات مثل slonik لـ PostgreSQL.

مثال: منشئ استعلامات SQL الامن

function sql(strings, ...values) {
    // بناء استعلام محدد المعاملات
    let query = '';
    const params = [];

    strings.forEach((str, i) => {
        query += str;
        if (i < values.length) {
            params.push(values[i]);
            query += `$${params.length}`;  // عنصر نائب بنمط PostgreSQL
        }
    });

    return { query: query.trim(), params };
}

const userId = 42;
const status = 'active';
const minAge = 18;

const result = sql`
    SELECT name, email
    FROM users
    WHERE id = ${userId}
    AND status = ${status}
    AND age >= ${minAge}
    ORDER BY name ASC
`;

console.log(result.query);
// SELECT name, email
// FROM users
// WHERE id = $1
// AND status = $2
// AND age >= $3
// ORDER BY name ASC

console.log(result.params);
// [42, 'active', 18]

// القيم لا تضمن ابدا مباشرة في نص الاستعلام
// هذا يمنع حقن SQL بالتصميم

// ادخال المستخدم الضار محدد المعاملات بامان
const maliciousInput = "'; DROP TABLE users; --";
const safeQuery = sql`SELECT * FROM users WHERE name = ${maliciousInput}`;
console.log(safeQuery.query);  // SELECT * FROM users WHERE name = $1
console.log(safeQuery.params); // ["'; DROP TABLE users; --"]
// الادخال الضار يعامل كمعامل وليس ككود SQL

نظام التدويل (i18n)

يمكن للقوالب الموسومة تشغيل نظام تدويل حيث تبحث دالة الوسم عن ترجمات لاجزاء النص الثابتة وتنسق القيم المستوفاة وفقا لاتفاقيات اللغة المحلية.

مثال: التدويل مع القوالب الموسومة

const translations = {
    en: {
        'Hello, %1! You have %2 notifications.': 'Hello, %1! You have %2 notifications.',
        'Welcome back, %1.': 'Welcome back, %1.'
    },
    ar: {
        'Hello, %1! You have %2 notifications.': 'مرحبا، %1! لديك %2 إشعارات.',
        'Welcome back, %1.': 'مرحبا بعودتك، %1.'
    },
    es: {
        'Hello, %1! You have %2 notifications.': 'Hola, %1! Tienes %2 notificaciones.',
        'Welcome back, %1.': 'Bienvenido de vuelta, %1.'
    }
};

function createI18n(locale) {
    const dict = translations[locale] || translations.en;

    return function t(strings, ...values) {
        // بناء مفتاح من الاجزاء الثابتة
        let key = '';
        strings.forEach((str, i) => {
            key += str;
            if (i < values.length) {
                key += `%${i + 1}`;
            }
        });

        // البحث عن الترجمة
        let translated = dict[key] || key;

        // استبدال العناصر النائبة بالقيم
        values.forEach((value, i) => {
            translated = translated.replace(`%${i + 1}`, value);
        });

        return translated;
    };
}

const t_en = createI18n('en');
const t_ar = createI18n('ar');
const t_es = createI18n('es');

const name = 'Alice';
const count = 5;

console.log(t_en`Hello, ${name}! You have ${count} notifications.`);
// Hello, Alice! You have 5 notifications.

console.log(t_ar`Hello, ${name}! You have ${count} notifications.`);
// مرحبا، Alice! لديك 5 إشعارات.

console.log(t_es`Hello, ${name}! You have ${count} notifications.`);
// Hola, Alice! Tienes 5 notificaciones.

نمط المكونات المنسقة

مكتبة CSS-في-JS styled-components تستخدم القوالب الموسومة كواجهتها الاساسية. تكتب CSS داخل قالب موسوم ودالة الوسم تنشئ مكون React بتلك الانماط. فهم القوالب الموسومة يساعدك على فهم كيف تعمل هذه المكتبة الشائعة من الداخل.

مثال: نمط CSS-في-JS (مبسط)

// تنفيذ مبسط شبيه بـ styled-components
function createStyled(tag) {
    return function (strings, ...values) {
        return function StyledComponent(props) {
            // تقييم اي قيم ديناميكية (الدوال تستقبل props)
            const css = strings.reduce((result, str, i) => {
                let value = '';
                if (i < values.length) {
                    value = typeof values[i] === 'function'
                        ? values[i](props)
                        : values[i];
                }
                return result + str + value;
            }, '');

            // توليد اسم فئة فريد
            const className = 'sc-' + hashCode(css);

            console.log(`[${tag}.${className}] CSS:`, css.trim());
            return { tag, className, css: css.trim(), props };
        };
    };
}

function hashCode(str) {
    let hash = 0;
    for (let i = 0; i < str.length; i++) {
        hash = ((hash << 5) - hash) + str.charCodeAt(i);
        hash |= 0;
    }
    return Math.abs(hash).toString(36);
}

const styled = { div: createStyled('div'), button: createStyled('button') };

const Button = styled.button`
    background-color: ${props => props.primary ? '#007bff' : '#6c757d'};
    color: white;
    padding: 8px 16px;
    border: none;
    border-radius: 4px;
    font-size: ${props => props.large ? '18px' : '14px'};
    cursor: pointer;
`;

const primaryBtn = Button({ primary: true, large: false });
const secondaryBtn = Button({ primary: false, large: true });

مسجل التصحيح

دالة وسم تضيف معلومات النوع والتنسيق لمخرجات التصحيح. هذا مفيد اثناء التطوير لرؤية انواع القيم التي تعمل معها بسرعة.

مثال: دالة وسم التصحيح

function debug(strings, ...values) {
    const formatValue = (val) => {
        if (val === null) return 'null (null)';
        if (val === undefined) return 'undefined (undefined)';
        if (Array.isArray(val)) return `[${val.join(', ')}] (array, length: ${val.length})`;
        if (typeof val === 'object') return `${JSON.stringify(val)} (object)`;
        return `${val} (${typeof val})`;
    };

    let result = '';
    strings.forEach((str, i) => {
        result += str;
        if (i < values.length) {
            result += formatValue(values[i]);
        }
    });

    console.log('[DEBUG]', result);
    return result;
}

const user = { name: 'Alex', age: 30 };
const scores = [95, 87, 92];
const active = true;

debug`User: ${user}, Scores: ${scores}, Active: ${active}`;
// [DEBUG] User: {"name":"Alex","age":30} (object), Scores: [95, 87, 92] (array, length: 3), Active: true (boolean)

debug`Count: ${null}, Missing: ${undefined}, Zero: ${0}`;
// [DEBUG] Count: null (null), Missing: undefined (undefined), Zero: 0 (number)

وسم التحقق من النصوص

مثال: دالة وسم التحقق

// وسم يتحقق من القيم المستوفاة
function validate(strings, ...values) {
    const errors = [];

    values.forEach((value, i) => {
        if (value === null || value === undefined) {
            errors.push(`Value at position ${i + 1} is ${value}`);
        }
        if (typeof value === 'string' && value.trim() === '') {
            errors.push(`Value at position ${i + 1} is an empty string`);
        }
    });

    if (errors.length > 0) {
        throw new Error(`Template validation failed:\n${errors.join('\n')}`);
    }

    return strings.reduce((result, str, i) => {
        return result + str + (i < values.length ? values[i] : '');
    }, '');
}

// هذا يعمل بشكل جيد
const name = 'Alice';
const email = 'alice@example.com';
console.log(validate`User: ${name}, Email: ${email}`);
// User: Alice, Email: alice@example.com

// هذا يطلق خطا
try {
    const missing = null;
    validate`User: ${missing}, Email: ${email}`;
} catch (e) {
    console.error(e.message);
    // Template validation failed:
    // Value at position 1 is null
}

انواع القوالب النصية في TypeScript

على الرغم من ان هذا درس JavaScript فمن الجدير بالذكر ان TypeScript يوسع صيغة القوالب النصية لنظام الانواع. انواع القوالب النصية تتيح لك انشاء انواع نصية بناء على انماط مما يوفر امان وقت الترجمة لمعالجة النصوص. هذا يوضح مدى اساسية مفهوم القالب النصي في نظام JavaScript البيئي.

مثال: انواع القوالب النصية (TypeScript)

// انواع القوالب النصية في TypeScript (للمرجع)
// تعمل في وقت الترجمة وليس وقت التشغيل

// type Greeting = "Hello, World!"
// type Greeting = `Hello, ${string}!`;

// type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";
// type Endpoint = "/users" | "/posts" | "/comments";
// type Route = `${HTTPMethod} ${Endpoint}`;

// في JavaScript العادي يمكنك تحقيق انماط مماثلة مع القوالب الموسومة
function createRoute(strings, ...values) {
    const route = strings.reduce((result, str, i) => {
        return result + str + (i < values.length ? values[i] : '');
    }, '');

    const [method, path] = route.trim().split(' ');
    const validMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'];

    if (!validMethods.includes(method)) {
        throw new Error(`Invalid HTTP method: ${method}`);
    }

    if (!path.startsWith('/')) {
        throw new Error(`Path must start with /: ${path}`);
    }

    return { method, path };
}

// نهج JavaScript اكثر عملية
function route(method, path) {
    return `${method.toUpperCase()} ${path}`;
}
console.log(route('get', '/users')); // GET /users

الانماط المتقدمة والحالات الحدية

دعنا نغطي بعض الانماط والحالات الحدية الاضافية التي تظهر عند العمل مع القوالب النصية في كود الانتاج.

تسلسل القوالب الموسومة

مثال: تركيب دوال الوسم

// دالة وسم تعيد دالة (نمط الكاري)
function currency(currencyCode) {
    return function (strings, ...values) {
        const formatters = {
            USD: new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }),
            EUR: new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }),
            GBP: new Intl.NumberFormat('en-GB', { style: 'currency', currency: 'GBP' })
        };

        const formatter = formatters[currencyCode] || formatters.USD;

        return strings.reduce((result, str, i) => {
            let value = '';
            if (i < values.length) {
                value = typeof values[i] === 'number'
                    ? formatter.format(values[i])
                    : values[i];
            }
            return result + str + value;
        }, '');
    };
}

const usd = currency('USD');
const eur = currency('EUR');
const gbp = currency('GBP');

const price = 1299.99;
const item = 'Laptop';

console.log(usd`${item}: ${price}`);
// Laptop: $1,299.99

console.log(eur`${item}: ${price}`);
// Laptop: 1.299,99 EUR

console.log(gbp`${item}: ${price}`);
// Laptop: 1,299.99 GBP

منشئ المحتوى الشرطي

مثال: منشئ القوالب الشرطية

// بناء نصوص مع اقسام اختيارية
function when(condition) {
    return condition
        ? (strings, ...values) => strings.reduce((r, s, i) =>
            r + s + (i < values.length ? values[i] : ''), '')
        : () => '';
}

const user = { name: 'Alex', isAdmin: true, unreadCount: 5 };

const notification = `
Welcome, ${user.name}!
${when(user.isAdmin)`You have admin privileges.`}
${when(user.unreadCount > 0)`You have ${user.unreadCount} unread messages.`}
${when(user.unreadCount === 0)`No new messages.`}
`.trim();

console.log(notification);
// Welcome, Alex!
// You have admin privileges.
// You have 5 unread messages.


// نمط منشئ اكثر متانة
function buildString(strings, ...values) {
    return strings.reduce((result, str, i) => {
        let value = i < values.length ? values[i] : '';
        // تصفية القيم الزائفة (باستثناء 0)
        if (value === false || value === null || value === undefined) {
            value = '';
        }
        return result + str + value;
    }, '').replace(/\n\s*\n/g, '\n').trim();
}

const showBadge = false;
const content = buildString`
    <div>
        <h1>${user.name}</h1>
        ${showBadge && `<span class="badge">PRO</span>`}
        <p>Welcome!</p>
    </div>
`;
console.log(content);

اعتبارات الاداء

مثال: استخدام القوالب مع مراعاة الاداء

// القوالب النصية تقيم في كل مرة تعمل
// للنصوص الثابتة لا يوجد فرق في الاداء

// لدوال الوسم الثقيلة خزن النتائج مؤقتا عند الامكان
function expensiveTag(strings, ...values) {
    // مصفوفة strings هي نفس المرجع عبر الاستدعاءات
    // مع نفس القالب. هذا يتيح التخزين المؤقت.
    if (!expensiveTag.cache) expensiveTag.cache = new WeakMap();

    if (expensiveTag.cache.has(strings)) {
        const cached = expensiveTag.cache.get(strings);
        // التحقق من تطابق القيم
        if (cached.values.every((v, i) => v === values[i])) {
            console.log('Returning cached result');
            return cached.result;
        }
    }

    // معالجة مكلفة
    const result = strings.reduce((r, s, i) => {
        return r + s + (i < values.length ? values[i] : '');
    }, '');

    expensiveTag.cache.set(strings, { values: [...values], result });
    return result;
}

// الاستدعاء الاول: يحسب
console.log(expensiveTag`Hello ${"World"}`);
// الاستدعاء الثاني بنفس القيم: يعيد المخزن مؤقتا
console.log(expensiveTag`Hello ${"World"}`);

// هوية مصفوفة strings مضمونة لكل موقع استدعاء
// قالبان متطابقان في مواقع استدعاء مختلفة
// يحصلان على مصفوفات strings مختلفة
function demo() {
    const a = expensiveTag`test ${1}`;  // موقع استدعاء 1
    const b = expensiveTag`test ${1}`;  // موقع استدعاء 2
    // a و b يحسبان بشكل منفصل لانهما يأتيان من
    // مواقع مختلفة في الكود المصدري
}
نصيحة احترافية: مصفوفة strings الممررة لدالة الوسم مجمدة (غير قابلة للتغيير) وهي نفس مرجع الكائن في كل مرة يتم فيها الوصول لنفس موقع استدعاء القالب الموسوم. هذا يعني انك تستطيع استخدام مصفوفة strings كمفتاح WeakMap للتخزين المؤقت. مكتبات مثل lit-html وstyled-components تعتمد على هذا السلوك لتحسين الاداء واعادة معالجة القوالب فقط عندما تتغير القيم المستوفاة.

تمرين عملي

ابن محرك قوالب مصغر باستخدام القوالب الموسومة يدعم الميزات التالية. اولا انشئ دالة وسم safeHTML تهرب جميع القيم المستوفاة لمنع هجمات XSS -- اختبرها مع نصوص تحتوي على اقواس زاوية وعلامات اقتباس وعلامات عطف. ثانيا انشئ دالة وسم sql تبني استعلامات محددة المعاملات حيث تصبح القيم المستوفاة عناصر نائبة مرقمة مثل $1 و$2 والقيم الفعلية تجمع في مصفوفة params منفصلة. ثالثا انشئ مصنع دالة وسم currency يأخذ لغة محلية ورمز عملة وينسق اي ارقام مستوفاة كعملة بينما يترك النصوص كما هي. رابعا انشئ دالة وسم indent تأخذ معامل رقم وتضيف مسافة بادئة لكل سطر في النص الناتج بذلك العدد من المسافات. اختبر دوال الوسم الاربعة بشكل شامل: تحقق من ان safeHTML تبطل حقن البرامج وتحقق من ان sql لا تضمن ابدا القيم مباشرة في نص الاستعلام وتحقق من ان currency تنسق الارقام حسب اللغة المحلية وتحقق من ان indent تضيف المسافة البادئة بشكل صحيح للمخرجات متعددة الاسطر. اخيرا اجمعها معا -- مثلا ولد جدول HTML منسق بمسافة بادئة من مصفوفة كائنات حيث القيم المقدمة من المستخدم مهروبة والاسعار منسقة كعملة.