
لماذا محتوى Contentful الخاص بك ليس متعدد اللغات فعليًا بعد
By Robert
لماذا محتوى Contentful الخاص بك ليس متعدد اللغات فعليًا بعد
يتعامل Contentful مع اللغات المحلية بشكل جيد. يمكنك تعريف العدد الذي تحتاجه، وتعيين سلسلة بديلة، والتبديل بينها في المحرر، وبناء واجهة أمامية تقدم اللغة الصحيحة لكل مسار. البنية التحتية للمحتوى متعدد اللغات جيدة حقًا.
ما لا يفعله Contentful هو ترجمة محتواك.
يبدو هذا واضحًا عند ذكره مباشرة، لكنه من السهل بشكل مدهش الخلط بين "لدينا حقول لغة محلية مُعدة" و"نحن متعددو اللغات." الاثنان ليسا نفس الشيء. حقول اللغة المحلية هي حاوية. متعدد اللغات يعني أن الحاوية تحتوي على محتوى.
إذا كان لديك مساحة Contentful بها لغات فرنسية وألمانية مُعدة ولكن الحقول في تلك اللغات فارغة، أو مملوءة بنسخة إنجليزية كعنصر نائب، أو معبأة بترجمة أولية تقريبية لم يراجعها أحد منذ إطلاق الموقع — فأنت لست متعدد اللغات. لديك الهيكل الأساسي لتعدد اللغات.
هذه التدوينة تتعلق بسد تلك الفجوة.
ما الذي يقدمه لك Contentful فعليًا
نظام المواقع في Contentful مصمم بشكل جيد. يمكن لكل إدخال محتوى أن يحتوي على قيم حقول لكل موقع. تقوم بتعيين موقع افتراضي، وتكوين البدائل، وتعيد واجهة برمجة التطبيقات الخاصة بالتسليم الموقع الصحيح عند الطلب. نمذجة المحتوى مرنة بما يكفي للتعامل مع متطلبات متعددة اللغات معقدة عبر أنواع محتوى مختلفة.
لكن النظام محايد تمامًا بشأن كيفية إدخال المحتوى المترجم هناك. لا يعرف Contentful ما إذا كانت حقول موقعك الفرنسي تحتوي على ترجمات احترافية، أو نص مترجم آليًا، أو نص إنجليزي تم لصقه عن طريق الخطأ، أو لا شيء على الإطلاق. إنه فقط يخزن ويوصل ما تضعه.
مشكلة الترجمة هي مسؤوليتك بالكامل لحلها.
كيف تتعامل الفرق عادة مع الأمر (وأين يحدث الانهيار)
معظم الفرق التي تتعامل مع ترجمة Contentful تقع في أحد الأنماط القليلة.
نمط التصدير اليدوي. يقوم المطور بتصدير المحتوى من Contentful، ويرسله إلى وكالة ترجمة أو مستقل، وينتظر عودته، ثم يعيد تنسيقه ويستورده. هذا يعمل لإطلاق لمرة واحدة لكنه يصبح غير قابل للتحمل مع تغير المحتوى. كل تحديث للغة المصدر يعني المرور بالدورة بأكملها مرة أخرى. في الواقع، يتأخر المحتوى المترجم بسرعة عن المصدر ولا يملك أحد الوقت لتحديثه.
نمط الترجمة داخل المحرر. يفتح المحرر كل إدخال، ويحول إلى اللغة المستهدفة، ويترجم الحقل تلو الآخر إما يدويًا أو بلصق المحتوى في أداة ترجمة. هذا دقيق لكنه بطيء. كما أنه لا يتوسع — إذا كان لديك مئات الإدخالات عبر عدة أنواع محتوى، فإن حجم العمل اليدوي كبير.
نمط "سيكون جيدًا". المحتوى المترجم موجود لكنه لم يُراجع منذ إنتاجه الأولي. تم تحديث لغة المصدر عدة مرات منذ ذلك الحين. النسخة الفرنسية من صفحة التسعير الخاصة بك لا تزال تشير إلى خطة أوقفتها قبل ثمانية أشهر. الصفحة الرئيسية الألمانية لا تزال تحمل الشعار القديم. لم يشير أحد إلى ذلك لأن لا أحد يتحقق.
جميع الأنماط الثلاثة تشترك في نفس المشكلة الجذرية: تُعامل الترجمة كمهمة لمرة واحدة بدلاً من كونها جزءًا مستمرًا من سير عمل المحتوى.
ما يتطلبه التعدد اللغوي فعليًا
يحتاج المحتوى متعدد اللغات الحقيقي في Contentful إلى ثلاثة أشياء تعمل معًا.
ترجمة أولية دقيقة. يحتاج كل حقل في كل لغة مستهدفة إلى محتوى مترجم بدقة، ومُحَلَّى بشكل مناسب، ويقرأ فعليًا كما لو كُتب لتلك السوق بدلاً من أن يُمرر عبر أداة ترجمة أساسية ويُترك دون مراجعة.
عملية للحفاظ على تحديث الترجمات. عندما يتغير المحتوى المصدر، يجب أن يتغير المحتوى المترجم أيضًا. هذا هو الجزء الذي يستهين به معظم الفرق. فريق المحتوى الذي ينشر عدة تحديثات أسبوعيًا عبر مساحة Contentful بأربع لغات وخمسين نوع محتوى يواجه عبئ عمل ترجمة مستمر كبير إذا تم التعامل معه يدويًا.
طريقة لمعرفة متى تصبح الترجمات قديمة. نظام اللغات في Contentful لا يظهر قدم المحتوى. إذا قمت بتحديث النسخة الإنجليزية من إدخال ونسيت تحديث النسخة الفرنسية، ستستمر النسخة الفرنسية في تقديم المحتوى القديم بصمت. تحتاج إما إلى عملية أو أدوات لاكتشاف ذلك.
أين يناسب PolyLingo
تقوم واجهة برمجة التطبيقات الخاصة بـ PolyLingo بترجمة المحتوى المهيكل مع الحفاظ على هيكله. بالنسبة لـ Contentful، التنسيق المناسب هو JSON — حيث يتم استخراج قيم حقول إدخال Contentful وإرسالها إلى واجهة برمجة التطبيقات، وتعود مترجمة مع الحفاظ على نفس الهيكل.
التدفق الأساسي في Node.js:
import PolyLingo from 'polylingo'
import { createClient } from 'contentful-management'
const poly = new PolyLingo({ apiKey: process.env.POLYLINGO_API_KEY })
const contentful = createClient({
accessToken: process.env.CONTENTFUL_MANAGEMENT_TOKEN,
})
const space = await contentful.getSpace(process.env.CONTENTFUL_SPACE_ID)
const environment = await space.getEnvironment('master')
// جلب الإدخال الذي تريد ترجمته
const entry = await environment.getEntry('your-entry-id')
// يستخدم PolyLingo رموز BCP-47 بدون المنطقة (fr, de).
// قم بتعيين هذه إلى معرفات المواقع الخاصة بـ Contentful إذا اختلفت (مثل fr-FR, de-DE).
const localeMap = { fr: 'fr', de: 'de' } // عدل لتطابق معرفات المواقع في مساحتك
// استخراج حقول السلاسل الإنجليزية فقط — تخطى المراجع، الروابط، القيم المنطقية، والأرقام
const sourceFields = Object.fromEntries(
Object.entries(entry.fields)
.filter(([, value]) => typeof value['en-US'] === 'string')
.map(([key, value]) => [key, value['en-US']])
)
// الترجمة إلى الفرنسية والألمانية في طلب واحد
const result = await poly.translate({
content: JSON.stringify(sourceFields),
format: 'json',
targets: Object.keys(localeMap),
})
// كتابة القيم المترجمة مرة أخرى إلى الإدخال باستخدام معرفات المواقع الخاصة بـ Contentful
for (const [polyLocale, translated] of Object.entries(result.translations)) {
const contentfulLocale = localeMap[polyLocale]
const parsed = JSON.parse(translated)
for (const [field, value] of Object.entries(parsed)) {
if (!entry.fields[field]) entry.fields[field] = {}
entry.fields[field][contentfulLocale] = value
}
}
await entry.update()
await entry.publish()
console.log('تمت ترجمة الإدخال ونشره.')
تُعيد مكالمة API واحدة كل من الفرنسية والألمانية. يتم تحديث الإدخال ونشره في نفس السكريبت.
ملاحظة حول معرفات المواقع: يستخدم PolyLingo رموز BCP-47 بدون لاحقة المنطقة (fr, de). غالبًا ما تستخدم مساحات Contentful معرفات مؤهلة بالمنطقة مثل fr-FR أو de-DE. كائن localeMap أعلاه هو المكان الذي تقوم فيه بمحاذاتها — قم بتحديثه ليتطابق مع معرفات المواقع التي يستخدمها مساحة Contentful الخاصة بك.
نفس النمط يعمل في بايثون:
import os, json, requests
from contentful_management import Client
poly_key = os.environ['POLYLINGO_API_KEY']
contentful = Client(os.environ['CONTENTFUL_MANAGEMENT_TOKEN'])
space = contentful.spaces().find(os.environ['CONTENTFUL_SPACE_ID'])
environment = space.environments().find('master')
entry = environment.entries().find('your-entry-id')
# استخراج قيم الحقول الإنجليزية
source_fields = {
key: value.get('en-US')
for key, value in entry.fields().items()
if value.get('en-US')
}
# الترجمة إلى الفرنسية والألمانية
r = requests.post(
'https://api.usepolylingo.com/v1/translate',
headers={'Authorization': f"Bearer {poly_key}"},
json={
'content': json.dumps(source_fields),
'format': 'json',
'targets': ['fr', 'de'],
},
timeout=120,
)
r.raise_for_status()
translations = r.json()['translations']
# الكتابة مرة أخرى إلى الإدخال
for locale, translated in translations.items():
parsed = json.loads(translated)
for field, value in parsed.items():
if field in entry.fields():
entry.fields()[field][locale] = value
entry.save()
entry.publish()
print('تمت ترجمة الإدخال ونشره.')
الترجمة على نطاق واسع
الأمثلة أعلاه تتعامل مع إدخال واحد. بالنسبة لمساحة Contentful تحتوي على العديد من الإدخالات عبر أنواع محتوى متعددة، فإن نقطة النهاية الدُفعية تتعامل مع ما يصل إلى 100 عنصر لكل طلب:
// جلب عدة إدخالات وترجمتها في مكالمة دُفعية واحدة
const entries = await environment.getEntries({
content_type: 'blogPost',
limit: 50,
})
// ربط رموز اللغة في PolyLingo بمعرفات اللغة في Contentful
const localeMap = { fr: 'fr', de: 'de', es: 'es' } // قم بالتعديل لتتناسب مع مساحتك
const items = entries.items.map(entry => ({
id: entry.sys.id,
content: JSON.stringify(
Object.fromEntries(
Object.entries(entry.fields)
.filter(([, value]) => typeof value['en-US'] === 'string')
.map(([k, v]) => [k, v['en-US']])
)
),
format: 'json',
}))
const batch = await poly.batch({
items,
targets: Object.keys(localeMap),
})
for (const result of batch.results) {
const entry = entries.items.find(e => e.sys.id === result.id)
for (const [polyLocale, translated] of Object.entries(result.translations)) {
const contentfulLocale = localeMap[polyLocale]
const parsed = JSON.parse(translated)
for (const [field, value] of Object.entries(parsed)) {
if (!entry.fields[field]) entry.fields[field] = {}
entry.fields[field][contentfulLocale] = value
}
}
await entry.update()
await entry.publish()
}
خمسون إدخالًا، ثلاث لغات، طلب دُفعية واحد.
الحفاظ على تحديث الترجمات
يمكن تشغيل السكريبتات أعلاه عند الطلب أو ربطها بسير عمل النشر الخاص بك. نمط شائع هو تشغيل الترجمة كجزء من وظيفة CI يتم تشغيلها عند تغيير المحتوى المصدر، أو وفق جدول زمني يلتقط أي إدخالات تم تحديثها في آخر 24 ساعة.
يدعم Contentful أيضًا webhooks — يمكنك تكوين webhook يتم تفعيله عند نشر إدخال في اللغة الافتراضية، مما يمكن أن يشغل وظيفة الترجمة تلقائيًا. ربط PolyLingo بهذا الـ webhook يعني أنه في كل مرة ينشر فيها محرر محتوى إنجليزي محدث، يتم تحديث اللغات المترجمة دون أي خطوة يدوية. هذا ضمن خارطة الطريق لتكامل مخصص لـ PolyLingo؛ وفي الوقت الحالي توفر لك API كل ما تحتاجه لبناء هذا التدفق بنفسك.
ملاحظة حول أنواع الحقول
النهج أعلاه يعمل جيدًا مع النصوص القصيرة والطويلة والنصوص الغنية المخزنة كسلاسل نصية. بعض أنواع الحقول في Contentful تحتاج إلى معالجة منفصلة:
حقول النص الغني المخزنة بتنسيق مستند Contentful (وليس كسلاسل نصية عادية) تحتاج إلى تحويلها إلى نص عادي أو HTML قبل الإرسال إلى PolyLingo، ثم إعادة تحويلها مرة أخرى. حزمة @contentful/rich-text-html-renderer تتولى خطوة التحويل، وformat: "html" على جانب PolyLingo يحافظ على العلامات بشكل صحيح.
حقول المرجع (روابط إلى إدخالات أو أصول أخرى) لا يجب ترجمتها — فهي معرفات، وليست نصًا. استبعدها من الحقول التي ترسلها إلى API.
حقول الأرقام، والقيم المنطقية، والتواريخ ليست قابلة للترجمة بطبيعتها. قم بتصفية هذه قبل بناء كائن المصدر الخاص بك.
البدء
تشمل الطبقة المجانية من PolyLingo 50,000 رمز شهريًا. لمساحة Contentful تحتوي على كمية معتدلة من المحتوى، يغطي ذلك تمريرة ترجمة أولية عبر عدة إدخالات في عدة لغات مع وجود مساحة للتحديثات المستمرة.
التوثيق الكامل لواجهة برمجة التطبيقات موجود على usepolylingo.com/docs. تتوفر حزم SDK لـ Node.js و Python و Ruby و PHP و Java و Go.
npm install polylingo