بازگشت به وبلاگ
A Contentful-style content entry card showing a filled English locale tab and an empty French locale tab with a warning icon.

چرا محتوای Contentful شما هنوز واقعاً چندزبانه نیست

By Robert

Why your Contentful content isn't actually multilingual yet

Contentful handles locales well. You can define as many as you need, set a fallback chain, switch between them in the editor, and build a frontend that serves the right language per route. The infrastructure for multilingual content is genuinely good.

What Contentful does not do is translate your content.

This sounds obvious when stated directly, but it is surprisingly easy to conflate "we have locale fields set up" with "we are multilingual." The two are not the same thing. Locale fields are a container. Multilingual means the container has content in it.

If your Contentful space has French and German locales configured but the fields in those locales are empty, or filled with the English copy as a placeholder, or populated with a rough first pass that nobody has reviewed since the site launched — you are not multilingual. You have the scaffolding for multilingual.

This post is about closing that gap.


آنچه Contentful واقعاً به شما می‌دهد

سیستم محلی‌سازی Contentful به خوبی طراحی شده است. هر ورودی محتوا می‌تواند مقادیر فیلد به ازای هر محلی داشته باشد. شما یک محلی پیش‌فرض تنظیم می‌کنید، پشتیبان‌گیری‌ها را پیکربندی می‌کنید، و API تحویل شما محلی مناسب را هنگام درخواست بازمی‌گرداند. مدل‌سازی محتوا به اندازه کافی انعطاف‌پذیر است تا نیازهای چندزبانه پیچیده را در انواع مختلف محتوا مدیریت کند.

اما سیستم کاملاً بی‌طرف است که چگونه محتوای ترجمه شده وارد می‌شود. Contentful نمی‌داند که آیا فیلدهای محلی فرانسوی شما ترجمه‌های حرفه‌ای، متن ترجمه شده ماشینی، متن انگلیسی که به اشتباه چسبانده شده یا هیچ چیز ندارد. فقط آنچه را که وارد می‌کنید ذخیره و تحویل می‌دهد.

مسئله ترجمه کاملاً بر عهده شماست.


چگونه تیم‌ها معمولاً آن را مدیریت می‌کنند (و کجا شکست می‌خورند)

اکثر تیم‌هایی که ترجمه‌های Contentful را مدیریت می‌کنند، در یکی از چند الگو قرار می‌گیرند.

الگوی صادرات دستی. یک توسعه‌دهنده محتوا را از Contentful صادر می‌کند، آن را به یک آژانس ترجمه یا فریلنسر می‌فرستد، منتظر بازگشت آن می‌ماند، دوباره قالب‌بندی می‌کند و وارد می‌کند. این برای یک راه‌اندازی یک‌باره کار می‌کند اما با تغییر محتوا غیرقابل تحمل می‌شود. هر به‌روزرسانی در زبان منبع به معنای گذراندن کل چرخه دوباره است. در عمل، محتوای ترجمه شده به سرعت از منبع عقب می‌ماند و هیچ‌کس وقت ندارد آن را به‌روز کند.

الگوی ترجمه در ویرایشگر. یک ویراستار هر ورودی را باز می‌کند، به زبان هدف تغییر می‌دهد و فیلد به فیلد ترجمه می‌کند، یا به صورت دستی یا با چسباندن محتوا در یک ابزار ترجمه. این دقیق است اما کند. همچنین مقیاس‌پذیر نیست — اگر صدها ورودی در دوازده نوع محتوا داشته باشید، حجم کار دستی قابل توجه است.

الگوی «همین خوب است». محتوای ترجمه شده وجود دارد اما از زمان تولید اولیه بررسی نشده است. زبان منبع چندین بار از آن زمان به‌روزرسانی شده است. نسخه فرانسوی صفحه قیمت‌گذاری شما هنوز به طرحی اشاره می‌کند که هشت ماه پیش بازنشسته کرده‌اید. صفحه اصلی آلمانی هنوز شعار قدیمی را دارد. هیچ‌کس آن را گزارش نکرده چون هیچ‌کس بررسی نمی‌کند.

هر سه الگو یک مشکل ریشه‌ای مشترک دارند: ترجمه به عنوان یک کار یک‌باره در نظر گرفته می‌شود نه بخشی مداوم از جریان کاری محتوا.


آنچه چندزبانه بودن واقعاً نیاز دارد

محتوای چندزبانه واقعی در Contentful به سه چیز نیاز دارد که با هم کار کنند.

ترجمه اولیه دقیق. هر فیلد در هر زبان هدف نیاز به محتوای ترجمه شده‌ای دارد که دقیق، به‌طور مناسب بومی‌سازی شده و واقعاً طوری خوانده شود که انگار برای آن بازار نوشته شده است، نه اینکه فقط از یک ابزار ترجمه ساده عبور کرده و بدون بازبینی باقی مانده باشد.

فرآیندی برای به‌روز نگه داشتن ترجمه‌ها. وقتی محتوای منبع تغییر می‌کند، محتوای ترجمه شده نیز باید تغییر کند. این بخشی است که بیشتر تیم‌ها دست کم می‌گیرند. تیم محتوایی که چندین به‌روزرسانی در هفته در یک فضای Contentful با چهار زبان و پنجاه نوع محتوا منتشر می‌کند، اگر این کار به صورت دستی انجام شود، با حجم قابل توجهی از کار ترجمه مداوم روبرو خواهد بود.

راهی برای دانستن اینکه ترجمه‌ها قدیمی شده‌اند. سیستم زبان Contentful کهنه بودن را نشان نمی‌دهد. اگر نسخه انگلیسی یک ورودی را به‌روزرسانی کنید و فراموش کنید نسخه فرانسوی را به‌روزرسانی کنید، نسخه فرانسوی به‌طور خاموش به ارائه محتوای قدیمی ادامه خواهد داد. شما به یک فرآیند یا ابزار نیاز دارید تا این موضوع را تشخیص دهد.


جایگاه PolyLingo

API پلی‌لینگو محتوای ساختاریافته را ترجمه می‌کند و ساختار آن را حفظ می‌کند. برای Contentful، فرمت مرتبط JSON است — مقادیر فیلدهای یک ورودی Contentful استخراج شده و به API ارسال می‌شوند و ترجمه شده با همان ساختار بازمی‌گردند.

روند پایه در 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')

// پلی‌لینگو از کدهای BCP-47 بدون منطقه استفاده می‌کند (fr, de).
// اگر متفاوت است، این‌ها را به شناسه‌های locale در Contentful خود نگاشت کنید (مثلاً fr-FR, de-DE).
const localeMap = { fr: 'fr', de: 'de' } // برای تطبیق با شناسه‌های locale فضای خود تنظیم کنید

// فقط فیلدهای رشته‌ای انگلیسی را استخراج کنید — مراجع، لینک‌ها، بولین‌ها و اعداد را رد کنید
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),
})

// مقادیر ترجمه شده را با استفاده از شناسه‌های locale 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 هم فرانسوی و هم آلمانی را برمی‌گرداند. ورودی در همان اسکریپت به‌روزرسانی و منتشر می‌شود.

نکته‌ای درباره شناسه‌های locale: پلی‌لینگو از کدهای BCP-47 بدون پسوند منطقه‌ای (fr, de) استفاده می‌کند. فضای Contentful اغلب از شناسه‌های منطقه‌ای مانند fr-FR یا de-DE استفاده می‌کند. شیء localeMap بالا جایی است که آن‌ها را هماهنگ می‌کنید — آن را به‌روزرسانی کنید تا با شناسه‌های locale فضای 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 با ورودی‌های متعدد در چندین نوع محتوا، نقطه پایانی دسته‌ای تا ۱۰۰ مورد را در هر درخواست مدیریت می‌کند:

// دریافت چندین ورودی و ترجمه آنها در یک فراخوانی دسته‌ای
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 اجرا کنید که هنگام تغییر محتوای منبع فعال می‌شود، یا طبق برنامه‌ای که هر ورودی به‌روزرسانی شده در ۲۴ ساعت گذشته را پوشش می‌دهد.

Contentful همچنین از وب‌هوک‌ها پشتیبانی می‌کند — می‌توانید یک وب‌هوک تنظیم کنید که هنگام انتشار یک ورودی در زبان پیش‌فرض فعال شود، که می‌تواند به‌طور خودکار یک کار ترجمه را فعال کند. اتصال PolyLingo به آن وب‌هوک به این معنی است که هر بار که یک ویراستار محتوای انگلیسی به‌روزشده را منتشر می‌کند، زبان‌های ترجمه‌شده بدون هیچ مرحله دستی به‌روزرسانی می‌شوند. این مورد در نقشه راه برای یک ادغام اختصاصی PolyLingo قرار دارد؛ در این میان، API همه چیزهایی را که برای ساخت این جریان به صورت خودتان نیاز دارید، در اختیار شما قرار می‌دهد.


یادداشتی درباره نوع فیلد

روش بالا برای متن کوتاه، متن بلند و متن غنی که به صورت رشته ذخیره شده است، خوب عمل می‌کند. چند نوع فیلد در Contentful نیاز به مدیریت جداگانه دارند:

فیلدهای متن غنی که در فرمت سند Contentful ذخیره شده‌اند (نه رشته‌های ساده) باید قبل از ارسال به PolyLingo به متن ساده یا HTML سریال شوند و سپس دوباره از سریال خارج شوند. بسته @contentful/rich-text-html-renderer مرحله سریال‌سازی را انجام می‌دهد و format: "html" در سمت PolyLingo نشانه‌گذاری را به درستی حفظ می‌کند.

فیلدهای مرجع (لینک به ورودی‌ها یا دارایی‌های دیگر) نباید ترجمه شوند — آن‌ها شناسه هستند، نه متن. آن‌ها را از فیلدهایی که به API ارسال می‌کنید حذف کنید.

فیلدهای عددی، بولی و تاریخ ذاتاً قابل ترجمه نیستند. قبل از ساخت شیء منبع خود آن‌ها را فیلتر کنید.


شروع به کار

سطح رایگان PolyLingo شامل ۵۰٬۰۰۰ توکن در ماه است. برای یک فضای Contentful با مقدار متوسطی از محتوا، این مقدار یک گذر اولیه ترجمه را در چندین ورودی در چندین زبان محلی پوشش می‌دهد و فضای کافی برای به‌روزرسانی‌های مداوم باقی می‌گذارد.

مستندات کامل API در usepolylingo.com/docs موجود است. بسته‌های SDK برای Node.js، Python، Ruby، PHP، Java و Go در دسترس هستند.

npm install polylingo

کلید API خود را دریافت کنید