تخطَّ إلى المحتوى

مقالات

بناء منصة Next.js باثنتين وعشرين لغة: دروس من مشروع GAGA

توجيه اللغات، والصربية بالأحرف اللاتينية والكيريلية، وتنسيق Intl، والخطوط، وفحص الترجمات في CI، وأسعار الصرف المحدّثة: دروس من بناء موقع Next.js بـ22 لغة.

فريق هندسة sigmacode.io10 دقائق قراءة

في هذه الصفحة (11)
  1. امنح كل لغة عنوان URL خاصاً بها
  2. أبجديتان، لغة واحدة
  3. نسّق الأرقام والعملات والتواريخ بـIntl
  4. الخطوط وتغطية الحروف
  5. سير عمل ترجمة يصمد أمام 22 لغة
  6. بيانات آنية دون قيم قديمة
  7. مصدر حقيقة واحد لـPDF وExcel وCSV وXML
  8. SEO عبر لغات متعددة
  9. إمكانية الوصول
  10. الاختبار: مسارات × لغات
  11. ما يجب التخطيط له

GAGA Menjačnica شركة لتبادل العملات وتجارة المعادن الثمينة لديها عدة فروع في نوفي ساد بصربيا. بنى فريقنا منصتهم، menjacnicegaga.rs، بـNext.js وReact على Vercel. تعمل المنصة باثنتين وعشرين لغة، بما فيها الصربية بالأحرف اللاتينية والكيريلية، والألمانية، والصينية، والروسية، والتركية، والأوكرانية، واليونانية، والبلغارية. وتعرض أسعار الشراء والبيع المباشرة بجوار الأسعار المرجعية للبنك الوطني الصربي، وتتضمن محوّل عملات، وقوائم أسعار يمكن تنزيلها بخمس صيغ، ومحدد مواقع الفروع، وأدلة للاستثمار في الذهب والفضة.

عند اثنتين وعشرين لغة، تتجاوز الترجمة مرحلة كونها مجرد ميزة؛ فعند هذا الحجم تبدأ بتشكيل المعمارية. تتناول هذه المقالة ما نراه مهماً عند بناء منصة كهذه. وهي مكتوبة للمدراء التقنيين ومالكي المنتجات ومهندسي الواجهات الأمامية الذين يخططون لمشروع مماثل. وللمشروع نفسه، انظر دراسة حالة GAGA.

امنح كل لغة عنوان URL خاصاً بها#

القرار الأهم يأتي أولاً: كل نسخة لغوية من كل صفحة تحتاج عنوان URL ثابتاً خاصاً بها وقابلاً للزحف. لا تبدّل اللغات بملف تعريف ارتباط (cookie) أو بقراءة ترويسة Accept-Language. فمحركات البحث لا تستطيع فهرسة ذلك، والمستخدمون لا يستطيعون مشاركته، وشبكات CDN لا تستطيع تخزينه مؤقتاً بشكل نظيف.

نوصي ببادئة لغة في المسار، مثل /sr/... و/de/... و/zh/.... ومع App Router في Next.js، يعني ذلك مقطع [locale] في جذر التطبيق. ويمكن لوكيل الطلبات (proxy.ts، سابقاً middleware) إعادة توجيه الزيارة الأولى إلى خيار افتراضي مناسب بناءً على لغة المتصفح. وبعدها يصبح عنوان URL هو المصدر الوحيد للحقيقة، وعلى مبدّل اللغة أن يرتبط بالصفحة نفسها في اللغة الأخرى، لا بالصفحة الرئيسية لتلك اللغة.

خطّط لمعرّفات اللغات مبكراً واستخدم وسوم BCP 47 في كل مكان. الصربية وحدها تحتاج اثنين: sr-Latn وsr-Cyrl. وتعيينها إلى مقاطع URL وقيم hreflang ولغات Intl في مكان واحد يوفر الكثير من المتاعب لاحقاً.

hreflang وx-default وخرائط المواقع#

يجب أن تسرد كل صفحة جميع بدائلها، بما فيها هي نفسها، وأن تضيف x-default للمستخدمين الذين لا تدعم لغتهم. وفي App Router، تتولى واجهة metadata ذلك عنك:

ts
// app/[locale]/rates/page.tsx
import type { Metadata } from "next";

const BASE = "https://example.com";
const LOCALES = ["sr-Latn", "sr-Cyrl", "en", "de", "zh", "ru"] as const;
const segment = (l: string) => l.toLowerCase(); // "sr-Latn" -> "sr-latn"

export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }): Promise<Metadata> {
  const { locale } = await params;
  const path = "/rates";
  const languages: Record<string, string> = Object.fromEntries(
    LOCALES.map((l) => [l, `${BASE}/${segment(l)}${path}`])
  );
  languages["x-default"] = `${BASE}/en${path}`;

  return {
    alternates: { canonical: `${BASE}/${locale}${path}`, languages },
  };
}

ابنِ خريطة الموقع من قائمة اللغات نفسها وسجل المسارات نفسه، مع بدائل لكل إدخال. فبمجرد أن تأتي وسوم hreflang وإدخالات خريطة الموقع من مسارَي كود منفصلين، سيتعارضان عاجلاً أم آجلاً، وستتوقف محركات البحث عن الوثوق بهما معاً بصمت.

أبجديتان، لغة واحدة#

تُكتب الصربية بالأحرف اللاتينية والكيريلية معاً، ولكثير من القراء تفضيل قوي. عاملها كلغتين كاملتين، لا كلغة واحدة مع مبدّل عرض. لكل منهما عنوان URL خاص، وقيمة hreflang خاصة، وسمة lang خاصة.

من المغري كتابة كل شيء بأبجدية واحدة وتحويل الأخرى آلياً. وبالنسبة للنصوص الجارية، يمكن لخطوة نسخ صوتي (transliteration) مجرَّبة جيداً أن تكون نقطة بداية معقولة. لكن لا تطبّقها عمياءً على كل شيء:

  • الأسماء العلم والعلامات التجارية. أسماء الشركات والمنتجات والكلمات الأجنبية تحتفظ غالباً بشكلها اللاتيني حتى في النص الكيريلي. والمحوّل الآلي «سيساعد» بتحويلها إلى شيء لم يكتبه أحد.
  • التباس الحروف المركبة. اللاتينية nj وlj وdž تقابل عادةً أحرفاً كيريلية مفردة، لكن ليس دائماً. فالكلمات المركبة والدخيلة تكسر القاعدة. التحويل من الكيريلية إلى اللاتينية حتمي؛ أما العكس فليس كذلك.
  • عناوين URL والرموز والمعرّفات. رموز العملات مثل EUR، وعناوين البريد الإلكتروني، وslugs، وأي شيء داخل عناصر نائبة للاستيفاء، يجب ألا تُحوَّل أبجدياً أبداً.
  • البحث والفرز. قد يكتب المستخدمون بالأحرف اللاتينية في مربع بحث على صفحة كيريلية. طبّع الطرفين قبل المقارنة.

ما نوصي به: خزّن المصدر الكيريلي (الاتجاه الحتمي)، أو احتفظ بفهرسَي ترجمة منفصلين، وأبقِ قائمة استثناءات صغيرة يملكها متحدث أصلي. وأي شيء أنتجته آلة يجب أن يُراجَع قبل الإطلاق.

نسّق الأرقام والعملات والتواريخ بـIntl#

في موقع أسعار صرف، الأرقام هي المنتج. تستخدم الصربية الفاصلة فاصلةً عشرية، وتجمع الألمانية الآلاف بنقطة، ويتوقع المستخدمون الصينيون اصطلاحات مختلفة بدورهم. لا تنسّق يدوياً. استخدم واجهات Intl المدمجة ومرّر وسم اللغة الكامل:

ts
const rateFormatter = (locale: string) =>
  new Intl.NumberFormat(locale, {
    minimumFractionDigits: 4,
    maximumFractionDigits: 4,
  });

const moneyFormatter = (locale: string, currency: string) =>
  new Intl.NumberFormat(locale, { style: "currency", currency });

const asOf = (locale: string, date: Date) =>
  new Intl.DateTimeFormat(locale, {
    dateStyle: "long",
    timeStyle: "short",
    timeZone: "Europe/Belgrade",
  }).format(date);

rateFormatter("sr-Latn").format(117.1234);   // "117,1234"
rateFormatter("de").format(117.1234);        // "117,1234"
moneyFormatter("en", "EUR").format(1250);    // "€1,250.00"
asOf("sr-Cyrl", new Date());                 // e.g. "18. септембар 2026. 10:30" (exact output depends on the ICU version)

أمور قليلة تستحق التخطيط لها. حدّد المنطقة الزمنية صراحةً؛ وإلا فإن الصفحات المعروضة على الخادم تستخدم منطقة الخادم، وهي عادةً UTC. أنشئ المنسّقات مرة واحدة وأعد استخدامها، لأن إنشاءها بشكل متكرر في جدول كبير يتراكم أثره. واحتفظ بعدد الخانات العشرية في طبقة البيانات وكذلك في الواجهة، حتى تتفق الصادرات مع الشاشة.

الخطوط وتغطية الحروف#

اثنتان وعشرون لغة تعني الأحرف اللاتينية، واللاتينية الموسّعة (الصربية والكرواتية والتركية)، والكيريلية (الصربية والروسية والأوكرانية والبلغارية)، واليونانية، وأحرف CJK. قلّة من الخطوط المؤسسية تغطي كل ذلك، والتي تغطيه تكون كبيرة الحجم.

ما يهم:

  • افحص التغطية لكل أبجدية قبل اختيار الخط. اختبر نصوصاً حقيقية، لا «Lorem ipsum». فالكيريلية الصربية لها أحرفها الخاصة وأشكالها المائلة المحلية، والأوكرانية فيها أحرف تفتقر إليها الروسية، والبلغارية تفضّل أشكال حروف خاصة بها.
  • قسّم الخط حسب الأبجدية وحمّله حسب اللغة. يدعم next/font مجموعات فرعية مثل latin وlatin-ext وcyrillic وgreek. صفحة ألمانية لا يجب أن تنزّل حروفاً كيريلية.
  • لا تستضيف خط CJK كاملاً على كل صفحة. يمكن أن تبلغ الخطوط الصينية عدة ميغابايت. وغالباً ما يكون مكدس خطوط النظام لـCJK، مثل "PingFang SC", "Microsoft YaHei", "Noto Sans SC", sans-serif، هو الحل الأنسب.
  • صمّم مكدساً احتياطياً صريحاً واضبط بدائل متوافقة مقياسياً، حتى لا ينزاح النص عند تحميل خط الويب.

سير عمل ترجمة يصمد أمام 22 لغة#

مع لغتين أو ثلاث، يكفي جدول بيانات وبعض الانضباط. ومع 22 لغة، تحتاج خط أنابيب.

فهارس رسائل بمفاتيح ثابتة. استخدم ملف JSON واحداً لكل لغة، بمفاتيح مسماة بالمعنى، مثل rates.table.buy، ولا تسمّها بالنص الإنجليزي أبداً. استخدم ICU MessageFormat للجموع والاستيفاء. قواعد الجمع تختلف كثيراً: للروسية والأوكرانية والصربية عدة صيغ، والصينية ليس لها أي صيغة.

تكافؤ المفاتيح في CI. المفتاح المفقود في لغة ما هو أكثر الأخطاء شيوعاً في المواقع متعددة اللغات، وأسهلها اكتشافاً آلياً:

ts
// scripts/check-i18n.ts — run in CI, fail the build on drift
import { readdirSync, readFileSync } from "node:fs";

const dir = "messages";
const flatten = (o: Record<string, unknown>, p = ""): string[] =>
  Object.entries(o).flatMap(([k, v]) =>
    v && typeof v === "object" ? flatten(v as Record<string, unknown>, `${p}${k}.`) : [`${p}${k}`]
  );

const load = (f: string) => new Set(flatten(JSON.parse(readFileSync(`${dir}/${f}`, "utf8"))));
const source = load("en.json");
let failed = false;

for (const file of readdirSync(dir).filter((f) => f.endsWith(".json") && f !== "en.json")) {
  const keys = load(file);
  const missing = [...source].filter((k) => !keys.has(k));
  const extra = [...keys].filter((k) => !source.has(k));
  if (missing.length || extra.length) {
    failed = true;
    console.error(`${file}: missing ${missing.length}, extra ${extra.length}`, { missing, extra });
  }
}
process.exit(failed ? 1 : 0);

وسّع السكربت نفسه للتحقق من تطابق العناصر النائبة للاستيفاء عبر اللغات. فاسم عنصر نائب مُترجَم ينكسر عند التشغيل، لا عند البناء.

ترجمة بمساعدة الذكاء الاصطناعي مع مراجعة بشرية. تنتج الترجمة الآلية وترجمة النماذج اللغوية مسودات أولى جيدة الآن، وعند 22 لغة يغيّر ذلك المعادلة الاقتصادية. لكن مكتب صرافة يتعامل بالمال والثقة. يجب أن يراجع إنسان متمكّن تسميات الأسعار والإشعارات القانونية وأدلة الاستثمار قبل نشرها. زوّد المترجمين بالسياق: لقطات شاشة، وحدود أحرف، ومسرد مصطلحات ثابتة مثل «سعر الشراء» و«سعر البيع» و«السعر المرجعي».

بيانات آنية دون قيم قديمة#

أسعار الصرف تتغير خلال اليوم. والفخ هو تخزينها مؤقتاً بنفس شراسة بقية موقع Next.js ثابت-أولاً.

استراتيجيات عامة نوصي بها:

  • افصل الهيكل عن البيانات. يمكن أن يكون تخطيط الصفحة والترجمات والأدلة ثابتاً أو يُعاد التحقق منه نادراً. أما جدول الأسعار فيجب أن تكون له نافذة إعادة تحقق قصيرة خاصة به، أو يُجلب على العميل أو يُبَثّ.
  • اجعل إعادة التحقق قصيرة ومدروسة. اختر نافذة تطابق مدى تكرار تغيّر الأسعار فعلاً، ووثّقها. واستخدم حيث أمكن إعادة التحقق عند الطلب عند نشر أسعار جديدة، بدلاً من الاعتماد على مؤقّت فقط.
  • خزّن مؤقتاً على الحافة بحذر. s-maxage قصير مع stale-while-revalidate يُبقي الصفحات سريعة، لكن تأكد أن نافذة التقادم التي يقبلها العمل التجاري.
  • اعرض دائماً طابع «حتى تاريخه» الزمني، منسقاً لكل لغة بمنطقة الفرع الزمنية. فهو الإجابة الصادقة عن السؤال الذي يثيره كل نظام تخزين مؤقت: ما مدى حداثة هذا؟

مصدر حقيقة واحد لـPDF وExcel وCSV وXML#

تنشر GAGA قائمة أسعارها للتنزيل بصيغ PDF وJPG وExcel وCSV وXML. خمس صيغ تعني خمس فرص لاختلاف الأرقام.

القاعدة: ابنِ كل تصدير من بنية البيانات المطبَّعة نفسها التي تعرض الجدول على الشاشة. ضع التقريب والترتيب وبيانات العملة الوصفية في تلك البنية، لا في كل مُصدِّر. عندها تصبح كل صيغة مجرد طبقة تسلسل رقيقة. أمور تستحق التخطيط لها:

  • CSV يحتاج فاصلاً مصرَّحاً به وترميزاً محدداً. يتوقع Excel في كثير من اللغات الأوروبية فاصلة منقوطة ويتعامل مع UTF-8 بشكل أفضل مع BOM.
  • Excel يجب أن يحصل على خلايا رقمية حقيقية بتنسيقات أرقام، لا سلاسل منسقة مسبقاً، حتى يستطيع المستخدمون الحساب بها.
  • XML يحتاج مخططاً ثابتاً موثقاً، لأن أحدهم سيتكامل معه.
  • PDF والصور يجب أن تضمّن خطوطاً تغطي الأبجدية المطلوبة، وهو ما يعيدك إلى تغطية الحروف.

ضع طابع «حتى تاريخه» في كل تصدير أيضاً.

SEO عبر لغات متعددة#

إلى جانب hreflang وخرائط المواقع:

  • ترجم العناوين والأوصاف وبيانات Open Graph الوصفية لكل لغة. لا تترك بيانات إنجليزية على صفحة يونانية.
  • لا تُعَرِّب الـslugs إلا إذا استطعت إبقاءها ثابتة. فـslug متغيّر في لغة واحدة يعني إعادة توجيه وعنقود hreflang مكسوراً.
  • تجنّب النسخ المكررة الفقيرة. إذا كانت لغة ما تحتوي محتوى جزئياً فقط، ففكّر هل يجب فهرستها أصلاً.
  • يجب أن تشير عناوين canonical إلى الصفحة نفسها، لا إلى نسخة لغوية أخرى أبداً.

إمكانية الوصول#

اضبط lang على عنصر html لكل لغة، باستخدام الوسم الكامل (sr-Latn، sr-Cyrl)، حتى تختار قارئات الشاشة الصوت والنطق الصحيحين. وعندما تظهر عبارة بلغة أخرى داخل صفحة، علّمها بسمة lang الخاصة بها.

حتى لو لم تكن تدعم لغة من اليمين إلى اليسار اليوم، خطّط لها. استخدم خصائص CSS المنطقية مثل margin-inline-start بدلاً من margin-left، واشتق dir من اللغة. إضافة العربية لاحقاً أرخص بكثير عندما لا يفترض التخطيط اتجاه اليسار إلى اليمين.

الاختبار: مسارات × لغات#

مع 22 لغة، من السهل تفويت خطأ لا يظهر إلا في واحدة منها. نوصي باختبار دخان آلي (smoke test) يمر على كل مسار عام وكل لغة ويفحص الأساسيات:

  • تعيد الصفحة 200 وتُعرض دون أخطاء تشغيل؛
  • عنصر html يحمل lang الصحيح؛
  • بدائل hreflang كاملة وتشير إلى عناوين موجودة؛
  • لا مفاتيح رسائل خام (مثل rates.table.buy) ولا سلاسل فارغة ظاهرة؛
  • يعرض المحوّل وجدول الأسعار الأرقام بالتنسيق المتوقع.

أضف لقطات مرئية لأطول اللغات. فالعناوين الألمانية واليونانية كثيراً ما تكسر تخطيطات بدت جيدة بالإنجليزية.

ما يجب التخطيط له#

إذا كنت تبدأ منصة Next.js متعددة اللغات، فهذه هي القرارات التي تُتخذ في اليوم الأول: استراتيجية عنوان URL ببادئة اللغة، وسجل لغات واحد يقود التوجيه والبيانات الوصفية وخرائط المواقع، وسياسة واضحة للأبجديات والتحويل الأبجدي، وIntl لكل التنسيق، وخطة خطوط لكل أبجدية، وفحوصات CI لتكافؤ الترجمات، وسياسة حداثة صريحة للبيانات الآنية، ومصدر حقيقة واحد لكل تصدير.

لا شيء من هذا غريب، لكن تصميمه من البداية أرخص بكثير من إعادة تجهيزه لاحقاً. يقود عملنا قائد تقني بخبرة تتجاوز 20 عاماً، وهذا هو نوع الأساس الذي نركز عليه في الويب والمنصات. إذا كنت تخطط لشيء مشابه، تواصل معنا.

لديك مشروع في ذهنك؟

أخبرنا بما تبنيه. يرد مهندس senior خلال ساعة في أيام العمل — ثم تحصل على تقييم صادق ونطاق واضح وعرض بسعر ثابت أو مرحلي، عادة خلال أيام عمل قليلة.

تفضّل الكتابة أولًا؟ راسلنا

ستتحدث مباشرة مع Ing. Ismet Mesic، Tech lead.