חזרה לבלוג
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 של PolyLingo מתרגם תוכן מובנה תוך שמירה על המבנה שלו. עבור 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')

// 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 למעלה הוא המקום שבו אתה מתאים אותם — עדכן אותו כך שיתאים לקודי השפות שהמרחב שלך משתמש בהם.

אותו דפוס עובד בפייתון:

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('הרשומה תורגמה ופורסמה.')

Translating at scale

The examples above handle a single entry. For a Contentful space with many entries across multiple content types, the batch endpoint handles up to 100 items per request:

// Fetch multiple entries and translate them in one batch call
const entries = await environment.getEntries({
  content_type: 'blogPost',
  limit: 50,
})
 
// Map PolyLingo locale codes to Contentful locale IDs
const localeMap = { fr: 'fr', de: 'de', es: 'es' } // adjust to match your space
 
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()
}

Fifty entries, three languages, one batch request.


שמירת התרגומים מעודכנים

ניתן להריץ את הסקריפטים שלמעלה לפי דרישה או לשלב אותם בתהליך הפרסום שלך. דפוס נפוץ הוא להריץ את התרגום כחלק מעבודת CI שמופעלת כאשר התוכן המקורי משתנה, או על פי לוח זמנים שתופס כל ערך שעודכן ב-24 השעות האחרונות.

Contentful תומך גם ב-webhooks — ניתן להגדיר webhook שמתפצל כאשר ערך מתפרסם בשפת ברירת המחדל, מה שיכול להפעיל אוטומטית משימת תרגום. חיבור PolyLingo ל-webhook הזה אומר שבכל פעם שעורך מפרסם תוכן אנגלי מעודכן, השפות המתורגמות מתעדכנות ללא כל שלב ידני. זה נמצא במפת הדרכים לאינטגרציה ייעודית של PolyLingo; בינתיים ה-API נותן לך את כל מה שאתה צריך כדי לבנות את הזרימה הזו בעצמך.


הערה על סוגי שדות

הגישה שלמעלה עובדת טוב עבור טקסט קצר, טקסט ארוך וטקסט עשיר המאוחסן כמחרוזות. כמה סוגי שדות ב-Contentful דורשים טיפול נפרד:

שדות טקסט עשיר המאוחסנים בפורמט המסמך של Contentful (ולא כמחרוזות פשוטות) צריכים להיות מסודרים לטקסט פשוט או HTML לפני שליחה ל-PolyLingo, ואז מפוענחים חזרה. חבילת @contentful/rich-text-html-renderer מטפלת בשלב הסידור, ו-format: "html" בצד PolyLingo שומר על הסימון כראוי.

שדות הפניה (קישורים לרשומות או נכסים אחרים) לא צריכים להיות מתורגמים — הם מזהים, לא טקסט. יש להוציא אותם מהשדות שאתם שולחים ל-API.

שדות מספר, בוליאני ותאריך אינם ניתנים לתרגום מטבעם. סננו אותם לפני בניית אובייקט המקור שלכם.


Getting started

PolyLingo's free tier includes 50,000 tokens per month. For a Contentful space with a moderate amount of content, that covers an initial translation pass across several entries in multiple locales with room to spare for ongoing updates.

Full API documentation is at usepolylingo.com/docs. SDK packages are available for Node.js, Python, Ruby, PHP, Java, and Go.

npm install polylingo

Get your API key