Назад в блог
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

Почему ваш контент в 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).
// Сопоставьте их с ID локалей Contentful, если они отличаются (например, fr-FR, de-DE).
const localeMap = { fr: 'fr', de: 'de' } // настройте под ID локалей вашего пространства

// Извлеките только строковые поля на английском — пропустите ссылки, булевы значения и числа
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),
})

// Запишите переведённые значения обратно в запись, используя ID локалей 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 возвращает и французский, и немецкий. Запись обновляется и публикуется в одном скрипте.

Примечание по ID локалей: PolyLingo использует коды BCP-47 без регионального суффикса (fr, de). Пространства Contentful часто используют регионально-квалифицированные ID, например fr-FR или de-DE. Объект localeMap выше — место, где вы их согласуете — обновите его, чтобы соответствовать ID локалей вашего пространства 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 с множеством записей разных типов контента, batch endpoint обрабатывает до 100 элементов за запрос:

// Получить несколько записей и перевести их одним batch-запросом
const entries = await environment.getEntries({
  content_type: 'blogPost',
  limit: 50,
})
 
// Сопоставить коды локалей PolyLingo с ID локалей 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()
}

Пятьдесят записей, три языка, один batch-запрос.


Поддержание актуальности переводов

Скрипты выше можно запускать по требованию или интегрировать в ваш рабочий процесс публикации. Распространённая практика — запускать перевод как часть 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

Получите ваш API ключ