
Чому ваш контент у Contentful ще не є справді багатомовним
By Robert
Чому ваш контент у Contentful насправді ще не багатомовний
Contentful добре працює з локалями. Ви можете визначити стільки, скільки потрібно, встановити ланцюжок резервних варіантів, перемикатися між ними в редакторі та створити фронтенд, який подає правильну мову для кожного маршруту. Інфраструктура для багатомовного контенту справді хороша.
Що Contentful не робить — це не перекладає ваш контент.
Це звучить очевидно, коли сказано прямо, але дуже легко сплутати "ми налаштували поля локалі" з "ми багатомовні". Це не одне й те саме. Поля локалі — це контейнер. Багатомовність означає, що в контейнері є контент.
Якщо у вашому просторі Contentful налаштовані локалі французькою та німецькою, але поля в цих локалях порожні, або заповнені англійським текстом як заповнювачем, або наповнені грубим першим варіантом, який ніхто не переглядав з моменту запуску сайту — ви не багатомовні. У вас є лише каркас для багатомовності.
Цей допис про те, як закрити цю прогалину.
Що 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 вище — це місце, де ви їх узгоджуєте — оновіть його відповідно до локалей вашого простору Contentful.
Такий самий підхід працює в Python:
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 також підтримує вебхуки — ви можете налаштувати вебхук, який спрацьовує при публікації запису у стандартній локалі, що може автоматично запускати завдання перекладу. Підключення PolyLingo до цього вебхука означає, що кожного разу, коли редактор публікує оновлений англійський контент, перекладені локалі оновлюються без жодних ручних дій. Це у планах для спеціальної інтеграції PolyLingo; тим часом API надає все необхідне, щоб ви могли побудувати цей процес самостійно.
Примітка щодо типів полів
Вищенаведений підхід добре працює для короткого тексту, довгого тексту та форматованого тексту, що зберігається у вигляді рядків. Деякі типи полів Contentful потребують окремої обробки:
Поля форматованого тексту, що зберігаються у форматі документів Contentful (не прості рядки), потрібно серіалізувати у простий текст або HTML перед відправкою до PolyLingo, а потім десеріалізувати назад. Пакет @contentful/rich-text-html-renderer обробляє крок серіалізації, а параметр format: "html" на стороні PolyLingo правильно зберігає розмітку.
Поля посилань (посилання на інші записи або ресурси) не слід перекладати — це ідентифікатори, а не текст. Виключіть їх із полів, які ви надсилаєте до API.
Поля чисел, булеві та дати за своєю природою не підлягають перекладу. Відфільтруйте їх перед створенням об’єкта джерела.
Початок роботи
Безкоштовний рівень PolyLingo включає 50 000 токенів на місяць. Для простору Contentful із помірною кількістю контенту це покриває початковий прохід перекладу кількох записів у кількох локалях з запасом для подальших оновлень.
Повна документація API доступна за адресою usepolylingo.com/docs. Пакети SDK доступні для Node.js, Python, Ruby, PHP, Java та Go.
npm install polylingo