0%
balqalam Logo
بالقلم
المميزاتالمجتمعالتسعيرالوثائق
تسجيل الدخول
  • مقدمة
  • العرض
  • MVP
  • خارطة الطريق
  • متطلبات المنتج
  • ابدأ الآن
  • البيئة المحلية
  • البنية المعمارية
  • الهيكل
  • الأنماط
  • الحزمة التقنية
  • قاعدة البيانات
  • تعدّد المستأجرين
  • الفهرس (Catalog)
  • العمل دون اتصال
  • المصادقة
  • التهيئة (Onboarding)
  • تدفق التكامل (Integration Flow)
  • التهيئة (Provision)
  • القبول
  • الحضور
  • الامتثال التنظيمي
  • الملف الشخصي
  • الاجتماعات
  • إدارة الرسوم
  • الفاتورة
  • التدويل
  • الترجمة
  • دليل الترجمة
  • أمان قاعدة البيانات
  • المساهمة
  • هَوْغوارتس
  • عرض حي
  • الإلهام
المبيعات والانتشار
  • المبيعات
  • الذهاب إلى السوق
  • التسويق
  • القبول — تسليط الضوء
  • البرنامج التجريبي
  • العملاء المحتملون
  • العرض التجاري
  • قوالب التواصل
  • دراسة الحالة
  • المنافسون
  • نموذج العمل
  • الاقتصاد المشترك
  • الجذب والنمو
التمويل والشراكات
  • الحصول على الدعم
  • عرض المستثمر
  • غرفة البيانات
  • المستثمرون
  • المُسرِّعات
  • الحاضنات
  • المنح
  • الرعاة
  • الشركاء
  • المسابقات والهاكاثونات
  • الجامعات ومراكز التدريب

دليل الترجمة

السابقالتالي

كيف يُترجم محتوى قاعدة البيانات عند الطلب — تخزين بلغة واحدة، localize()، المخبأ المدعوم بـ Google، مساعدات الأسماء، الموازنات، أفضل الممارسات، والأدوات.

البنية

src/components/translation/— the whole feature — one self-contained module
display.ts— getText, getFields — read-time translation
actions.ts— translate, autoTranslate, translateText/Fields
google.ts— translateRaw, translateBatch — provider calls
person.ts— getName, getNames, getLabels
search.ts— search — bilingual, cache-only
transliterate.ts— transliterate, formatName — ar→Latin
util.ts— withLang, detectScript, detectLang, fullName
types.ts— Lang + request/result types
config.ts— Google API URL + constants
__tests__/
actions.test.ts
display.test.ts
google.test.ts
prisma/models/translation.prisma— Translation model (table translation_cache)
src/app/api/mobile/translate/route.ts— POST /api/mobile/translate
scripts/audit-untranslated.ts— finds DB content rendered without translation

كيف يعمل

التدفق هو: خزّن بلغة واحدة ← اختمها بحقل lang ← ترجم عند العرض ← خبّئ النتيجة لكل مدرسة.

الاستدعاء الوحيد الذي تحتاجه: localize()

لأي نموذج في السجل (src/components/translation/registry.ts)، ترجم صفحة كاملة في تمريرة مُجمَّعة واحدة — بلا استدعاءات لكل حقل، وبلا تمرير schoolId/lang:

// content.tsx — حُلّ على الخادم، وسلّم نصوصًا جاهزة لجدول العميل
import { localize } from "@/components/translation/localize"
 
const rows = await getAnnouncementsList(schoolId, filters)
const localized = await localize("Announcement", rows) // ← هذا كل شيء

تَطوي localize ما كان سابقًا N×M استدعاء getText في db.translation.findMany واحدة، وتُقدِّم المصطلحات الساخنة من ذاكرة LRU داخلية (صفر رحلات لقاعدة البيانات)، وتعود إلى النص المصدر إن فشل أي شيء (العرض لا يتعطل أبدًا). إضافة نموذج محتوى جديد هي إدخال سطر واحد في registry.ts — وبعدها تُترجم كل قائمة منه تلقائيًا. اقرنها بـ prewarm في مسار الكتابة (after(() => prewarm("Announcement", row, { schoolId }))) فلا ينتظر أول قارئ بأي لغة على Google ولا يرى لغة المصدر أبدًا — الترجمة غير مرئية.

تُحلّ lang تلقائيًا من السياق (getDisplayLang() تقرأ ترويسة x-locale)، فتتوقف عن تمريرها عبر الخصائص. المساعدات أدناه هي اللبنات الأدنى التي بُنيت عليها localize — لا تلجأ إليها إلا لحقل واحد عابر، أو اسم شخص، أو تسمية عرضية.

أي مساعد أستخدم؟

لديك…استخدممن
قائمة/كيان من نموذج محتوىlocalize / localizeOne (مُجمَّعة، الافتراضي)localize.ts
حقل مخزَّن واحد للعرضgetTextdisplay.ts
عدة حقول لكيان واحدgetFieldsdisplay.ts
اسم شخصgetNameperson.ts
قائمة صفوف بأسماءgetNames (تزيل التكرار)person.ts
تسميات عشوائية (قاعات، مواد، صفوف)getLabels (تزيل التكرار)person.ts
كتابة محتوى لقاعدة البياناتwithLang / detectScriptutil.ts
ترجمة مسبقة عند الكتابة (بلا كمون قراءة)prewarmprewarm.ts
البحث عبر اللغتينsearchsearch.ts

كل قراءة هنا خادم-فقط وتحتاج schoolId (يُحلّ تلقائيًا إن لم تمرره). حُلّ في content.tsx، ومرّر نصوصًا جاهزة إلى columns.tsx في العميل.

التخزين بلغة واحدة (نموذج البيانات)

كل نموذج محتوى يخزّن النص بلغة واحدة مع حقل lang. أسماء الحقول عامة — أبدًا titleAr/titleEn. لغة التخزين الافتراضية تتبع School.preferredLanguage.

model Announcement {
  title String?          // اسم عام (ليس titleEn/titleAr)
  body  String? @db.Text
  lang  String  @default("ar") // اللغة التي خُزِّن بها هذا الصف
}

نوع اللغة هو Lang = "en" | "ar" (في src/components/translation/types.ts).

كتابة المحتوى — اختم اللغة

withLang ببساطة تلحق حقل lang بما توشك على حفظه:

// src/components/translation/util.ts
await db.announcement.create({ data: withLang(input, lang) })
// => { ...input, lang }

حين لا تملك لغة موثوقة وقت الكتابة، اكتشفها من الحروف الفعلية بـ detectScript — الحرف العربي يفوز، ثم اللاتيني، وإلا "ar". هذا متين عمدًا أمام علم خاطئ (القبول يكتب أسماء لاتينية بـ lang="ar" الافتراضي) وأمام علم غائب:

// src/components/translation/util.ts
detectScript("محمد علي") // "ar"
detectScript("Mohammed Ali") // "en"
detectScript("B102") // "en"

تعتمد detectScript على الحروف ذاتها، ولهذا هي الكاشف الصحيح للأسماء ولأي علم lang غير موثوق.

قراءة المحتوى — ترجم عند العرض

getText هي بدائية القراءة الأساسية للقيمة المفردة. إن تطابقت لغتا المحتوى والعرض تعيد النص كما هو؛ وتتحقق من تعارض الحروف (فلا يتشوه صف موسوم خطأ)؛ وإن فشلت الترجمة تعود للنص المصدر — لا تعطّل العرض أبدًا.

// src/components/translation/display.ts
const title = await getText(a.title, a.lang, displayLang, schoolId)

لعدة حقول من كيان واحد، تترجمها getFields بالتوازي. لا تستدعِ getText داخل .map() أبدًا — هذا هو N+1 الذي وُجدت الواجهات المُجمَّعة لقتله.

التخبئة — ترجم مرة واحدة لكل مدرسة

الترجمة "المخبأ أولًا". تبحث translate في Translation، وترفع hitCount عند الإصابة، ولا تستدعي Google إلا عند الإخفاق — فكل نص فريد يُدفع ثمنه مرة واحدة لكل مدرسة:

// prisma/models/translation.prisma — على مستوى المدرسة، إزالة تكرار بمفتاح مركّب
model Translation {
  schoolId       String
  sourceText     String @db.Text
  sourceLanguage String
  targetLanguage String
  translatedText String @db.Text
  provider       String @default("google")
  hitCount       Int    @default(0)
  lastAccessedAt DateTime @default(now())
  @@unique([schoolId, sourceText, sourceLanguage, targetLanguage])
  @@index([schoolId, sourceLanguage, targetLanguage])
  @@index([lastAccessedAt])
  @@map("translation_cache")
}

طبقة Google

تغلّف src/components/translation/google.ts واجهة Cloud Translation v2:‏ translateRaw() وtranslateBatch() تقرآن GOOGLE_TRANSLATE_API_KEY (الطبقة المجانية ~500 ألف حرف/شهر). المفتاح الغائب أو المرفوض يرمي خطأً (فتعود getText/translate للمصدر) ويُسجَّل عبر reportTranslationDegraded() المخنوقة — console.error واحدة كحد أقصى كل 5 دقائق. الطلبات محدودة بمهلة 2.5 ثانية (مسار القراءة يفشل سريعًا للمصدر بدل تعليق العرض)، والدفعات تتقسم تلقائيًا (100 مقطع / 4 آلاف حرف)، وإعادة المحاولة للأعطال العابرة اختيارية ({ retry: true }) ويستخدمها prewarm فقط لأنه يعمل خارج مسار الاستجابة.

الأسماء والتسميات — استخدم المساعد القياسي

الأسماء أعلى الحالات ترددًا، فلها وحدة مخصصة — src/components/translation/person.ts. فضّلها على getText للأسماء. تركّب الأجزاء، تكشف الحروف (متجاهلةً علم lang المحتمل خطؤه)، تزيل التكرار لتجنب N+1، و— حصريًا — تنقحر عربي←لاتيني دون اتصال حين تتعطل الواجهة، فيعرض /en المتدهور "Mohammed Ali" لا "محمد علي".

// حُلّ مرة على الخادم، ثم سلّم نصوصًا جاهزة لأعمدة العميل
const names = await getNames(rows, (r) => r.student, lang, schoolId)
const display = names.get(fullName(row.student)) ?? fullName(row.student)
 
// getName(person, lang, schoolId)   — اسم واحد
// getLabels(values, lang, schoolId) — قاعات / مواد / تسميات صفوف

هذه المساعدات خادم-فقط (مخبأ قاعدة البيانات + واجهة Google). لجداول العميل، حُلّ الأسماء في content.tsx الخادمي ومرّر نصوصًا مُترجمة جاهزة إلى columns.tsx — لا تستدعها من مكوّن عميل أبدًا.

البحث ثنائي اللغة

تبني search (في src/components/translation/search.ts) شروط OR لـ Prisma تطابق الحقل الخام وترجمته المخبأة معًا — فالبحث عن "Ahmed" يجد "أحمد". تقرأ Translation فقط ولا تستدعي الواجهة أبدًا.

واجهة الموبايل

POST /api/mobile/translate تتيح لـ iOS/Android إعادة استخدام المخبأ نفسه بدل بناء خطٍّ ثانٍ. قائمة بيضاء فقط (announcement، assignment، event، exam) وموثَّقة. محتوى المدرسة يبقى مقيدًا بـ schoolId؛ assignment وexam قالبا كتالوج عالميان (بلا عمود schoolId) فيُجلبان بالمعرّف. يفحص المسار المخبأ ليُبلغ بصدق cached: true|false ثم يستدعي translate.

التخبئة والضبط الدقيق

مفتاح المخبأ هو (schoolId, sourceText, sourceLanguage, targetLanguage) — لكل مدرسة عمدًا، و"المخبأ أولًا"، ما يجعل جدول Translation طبقة إزالة التكرار وطبقة التجاوزات معًا.

الضبط الدقيق ممكن اليوم

لأن القراءات تضرب Translation قبل Google، تضبط الترجمة بكتابة الصف الذي تريد: ضع الصياغة المصححة في translatedText وعلّمها (provider: "manual") فلا يكتب فوقها أي استدعاء لاحق — لا prewarm ولا أداة التنظيف تمسّان تجاوزًا يدويًا أبدًا.

لماذا ليس مخبأً عالميًا واحدًا

إسقاط schoolId (مخبأ عالمي) يرفع نسبة الإصابة ويخفض إنفاق Google — لكنه (أ) يخلط محتوى المستأجرين في صف مشترك كاسرًا عزل schoolId الذي يقوم عليه التطبيق كله، و(ب) لا يحمل إلا ترجمة واحدة، فلا يمثل صياغتين مفضلتين لمدرستين مختلفتين. عمّم شريحة عامة إن لزم، لا الكل.

"خبّئ الكلمات المفردة" ← مسرد، لا تقطيع جمل

الترجمة الآلية ليست تركيبية — لا تُترجم جملة بترجمة كلماتها وضمّها (بنك الدم = "blood bank" لا "bank blood"). لكن الحدس يصح لمجموعة محدودة من المصطلحات المتكررة (المواد، تسميات الصفوف، الحالات، رموز القاعات): ترجم كل مصطلح مرة وأعد استخدامه — هذا مسرد. وأعلى الحالات تكرارًا، الأسماء, محلولة أصلًا دون ترجمة آلية: getNames/getLabels تزيلان التكرار وtransliterate تُرومن دون اتصال.

أداء واقعي

إصابة المخبأ بحثٌ مفهرس واحد في الحالين — التعميم يجعل طلبات أكثر تصيب (استدعاءات Google أبطأ أقل)، لا أسرع. مكسب الكمون الحقيقي هو مسرد بالذاكرة للمصطلحات الساخنة (memory-cache.ts — 5000 إدخال، ≤120 حرفًا، صفر رحلات DB عند الإصابة) — وهو مُفعَّل اليوم في translate() وكل ما فوقها.

إيجابيات وسلبيات

لماذا هذا النهج

الفائدةالتفصيل
لا تضخم حقول ثنائية اللغةحقل title واحد + lang، لا titleAr/titleEn لكل نموذج. صف واحد، مسار كتابة واحد.
ترجمة تُدفع مرةTranslation مقيد بالمدرسة مع hitCount؛ كل نص فريد يضرب Google مرة واحدة.
تدهور رشيقgetText تعود للمصدر؛ الأسماء تُنقحر عربي←لاتيني دون اتصال. لا واجهة فارغة أبدًا.
واعٍ بالحروفdetectScript تثق بالحروف الفعلية، فلا يكسر العرضَ علمُ lang خاطئ أو غائب.
تفادي N+1localize تطوي الصفحة في findMany واحدة؛ getNames/getLabels تزيلان التكرار للقوائم.
بحث عبر اللغتينالترجمات المخبأة تجعل البحث ثنائي اللغة بصفر تكلفة واجهة.
مخبأ واحد للويب والموبايلواجهة الموبايل تكشف جدول Translation ذاته — لا خط أنابيب مكرر.

ما الثمن

الموازنةالتفصيل
خادم-فقط + لاتزامنيالمساعدات تحتاج schoolId ولا تعمل على العميل؛ يجب الحل في content.tsx.
كمون أول عرضللمحتوى القديم غير المُدفأ: نص غير مخبأ يدفع رحلة Google (محدودة بـ2.5 ث) أول مرة يُعرض — ثم يُخبأ.
جودة الترجمة الآليةترجمة Google، أضعف ما تكون في أسماء الأعلام؛ النقحرة استدلالية.
مورّد واحد + سقفاعتماد واحد على Google؛ الطبقة المجانية (~500 ألف حرف/شهر) سقف حقيقي؛ مفتاح غائب ← عودة صامتة للمصدر.
حالات حدّية للكشفالنصوص مختلطة الحروف تُحسم عربية؛ الرموز القصيرة مثل B102 تُحسم إنجليزية.

أفضل الممارسات

  1. اختم lang في كل كتابة — withLang(data, lang)، أو detectScript(text) حين لا تكون اللغة موثوقة.
  2. القوائم عبر localize، الكيان عبر localizeOne — وسجّل النموذج في registry.ts (سطر واحد؛ يتحقق registry-schema.test.ts من مطابقة الأعمدة للمخطط).
  3. اقرن كل كتابة بـ after(() => prewarm(...)) — فلا ينتظر أول قارئ باللغة الأخرى أبدًا (حارس PREWARM_BASELINE يفشل الحزمة إن نسيت).
  4. لا تثق بعلم lang المخزن للأسماء — اكشف من الحروف (مساعدات الأسماء تفعل ذلك أصلًا).
  5. حُلّ على الخادم، اعرض على العميل — ترجم في content.tsx ومرّر نصوصًا جاهزة إلى columns.tsx.
  6. ابحث عبر search — ثنائي اللغة، مخبأ-فقط، بلا تكلفة واجهة.
  7. لا تُعد حقولًا ثنائية اللغة أبدًا (titleAr/titleEn، nameEn) — حقل عام واحد مع lang هو النمط الوحيد.
  8. الموبايل عبر /api/mobile/translate — أعد استخدام المخبأ، لا تبنِ خطًا ثانيًا.
  9. راقب حوارس التدقيق — pnpm i18n:audit-content يسرد الفجوات، وثلاثة اختبارات Vitest تمنع نموها (أسماء خام / ميزات بلا ترجمة / كتابات بلا prewarm).
  10. عند النشر — شغّل pnpm i18n:backfill (تقرير تكلفة بوضع المعاينة افتراضيًا، ثم --execute) ليقرأ المحتوى القديم بسلاسة من اليوم الأول.

مجالات التحسين

شُحن معظم القائمة الأصلية في تمريرة الجاهزية الإنتاجية 2026-06 (التبني الكامل، prewarm في كل مسار كتابة مدرسي، تقوية المحرك بالمهلة والتقسيم وإعادة المحاولة، سجل مطابق للمخطط، توسيع واجهة الموبايل). المتبقي:

  • قابلية رصد التدهور. اليوم فقط console.error مخنوقة — أضف إشارة عامة (حقل /api/health) ليرى لوح المراقبة متى يعود /en للمصدر بصمت.
  • جودة أسماء الأعلام. مسرد / قائمة "لا تُترجم" ومراجعة بشرية للأسماء عالية الظهور.
  • تجريد المزوّد. عمود provider لا يخزن إلا "google" — جرّد العميل ليكون مزوّد ثانٍ ممكنًا.
  • اتساق المخطط. اختم lang صحيحًا في كل كتابة، فيصبح كشف الحروف شبكة أمان لا الآلية الأساسية.
  • ترجمة قوالب تحفظ المتغيرات. أجسام AnnouncementTemplate/NotificationTemplate غير مسجلة عمدًا — الترجمة الآلية تفسد {{placeholders}}.

انظر أيضًا

  • التدويل — النظام الآخر: نصوص الواجهة الثابتة (القواميس)، التوجيه، RTL
  • الترجمة — متتبع التبني والتغطية لكل منطقة
  • تعدد المستأجرين — لماذا تحتاج كل قراءة هنا schoolId
  • قاعدة البيانات — حقل lang على نماذج المحتوى
الترجمةأمان قاعدة البيانات

On This Page

البنيةكيف يعملالاستدعاء الوحيد الذي تحتاجه: localize()أي مساعد أستخدم؟التخزين بلغة واحدة (نموذج البيانات)كتابة المحتوى — اختم اللغةقراءة المحتوى — ترجم عند العرضالتخبئة — ترجم مرة واحدة لكل مدرسةطبقة Googleالأسماء والتسميات — استخدم المساعد القياسيالبحث ثنائي اللغةواجهة الموبايلالتخبئة والضبط الدقيقالضبط الدقيق ممكن اليوملماذا ليس مخبأً عالميًا واحدًا"خبّئ الكلمات المفردة" ← مسرد، لا تقطيع جملأداء واقعيإيجابيات وسلبياتلماذا هذا النهجما الثمنأفضل الممارساتمجالات التحسينانظر أيضًا

من تطوير Databayt ·

مرحباً بك في بالقلم.

رحلة عظيمة على وشك أن تبدأ.