Powrót do bloga
A Contentful-style content entry card showing a filled English locale tab and an empty French locale tab with a warning icon.

Dlaczego Twoje treści w Contentful nie są jeszcze naprawdę wielojęzyczne

By Robert

Dlaczego twoje treści w Contentful nie są jeszcze naprawdę wielojęzyczne

Contentful dobrze radzi sobie z lokalizacjami. Możesz zdefiniować ich tyle, ile potrzebujesz, ustawić łańcuch zapasowy, przełączać się między nimi w edytorze i budować frontend, który serwuje odpowiedni język dla każdej ścieżki. Infrastruktura dla treści wielojęzycznych jest naprawdę dobra.

Contentful nie tłumaczy jednak twoich treści.

Brzmi to oczywiście, gdy jest powiedziane wprost, ale łatwo jest pomylić „mamy skonfigurowane pola lokalizacji” z „jesteśmy wielojęzyczni”. To nie to samo. Pola lokalizacji to pojemnik. Wielojęzyczność oznacza, że pojemnik zawiera treść.

Jeśli twoja przestrzeń Contentful ma skonfigurowane lokalizacje francuską i niemiecką, ale pola w tych lokalizacjach są puste, lub wypełnione angielskim tekstem jako zastępczym, albo wypełnione wstępnym szkicem, który nikt nie sprawdził od czasu uruchomienia strony — nie jesteś wielojęzyczny. Masz szkielet dla wielojęzyczności.

Ten wpis dotyczy zamknięcia tej luki.


Co tak naprawdę daje Ci Contentful

System lokalizacji Contentful jest dobrze zaprojektowany. Każdy wpis treści może mieć wartości pól dla poszczególnych lokalizacji. Ustawiasz domyślną lokalizację, konfigurujesz fallbacki, a Twoje API dostarczające zwraca odpowiednią lokalizację na żądanie. Modelowanie treści jest na tyle elastyczne, że radzi sobie z złożonymi wymaganiami wielojęzycznymi w różnych typach treści.

Jednak system jest całkowicie neutralny wobec tego, jak przetłumaczona treść trafia do środka. Contentful nie wie, czy Twoje pola lokalizacji francuskiej zawierają profesjonalne tłumaczenia, tekst przetłumaczony maszynowo, angielski tekst wklejony przez przypadek, czy też nic. Po prostu przechowuje i dostarcza to, co włożysz.

Problem tłumaczenia jest całkowicie po Twojej stronie do rozwiązania.


Jak zespoły zwykle to obsługują (i gdzie to zawodzi)

Większość zespołów zajmujących się tłumaczeniem Contentful wpisuje się w jeden z kilku wzorców.

Wzorzec ręcznego eksportu. Programista eksportuje treść z Contentful, wysyła ją do agencji tłumaczeniowej lub freelancera, czeka na jej zwrot, formatuje ponownie i importuje. To działa przy jednorazowym uruchomieniu, ale staje się nie do utrzymania, gdy treść się zmienia. Każda aktualizacja lokalizacji źródłowej oznacza przejście przez cały cykl od nowa. W praktyce przetłumaczona treść szybko pozostaje w tyle za źródłem i nikt nie ma czasu, by ją nadrobić.

Wzorzec tłumaczenia w edytorze. Redaktor otwiera każdy wpis, przełącza się na docelową lokalizację i tłumaczy pole po polu, ręcznie lub wklejając treść do narzędzia tłumaczeniowego. To jest dokładne, ale powolne. Nie skalowalność — jeśli masz setki wpisów w kilkunastu typach treści, ilość pracy ręcznej jest znaczna.

Wzorzec „jakoś to będzie”. Przetłumaczona treść istnieje, ale nie była przeglądana od momentu jej pierwszego stworzenia. Lokalizacja źródłowa była od tego czasu wielokrotnie aktualizowana. Francuska wersja twojej strony z cennikiem nadal odnosi się do planu, który wycofałeś osiem miesięcy temu. Niemiecka strona główna nadal ma stare hasło. Nikt tego nie zgłosił, ponieważ nikt tego nie sprawdza.

Wszystkie trzy wzorce mają ten sam podstawowy problem: tłumaczenie jest traktowane jako jednorazowe zadanie, a nie jako ciągła część przepływu pracy z treścią.


Czego tak naprawdę wymaga wielojęzyczność

Prawdziwa wielojęzyczna zawartość w Contentful wymaga współdziałania trzech elementów.

Dokładne początkowe tłumaczenie. Każde pole w każdej docelowej lokalizacji wymaga przetłumaczonej zawartości, która jest dokładna, odpowiednio zlokalizowana i faktycznie czyta się tak, jakby została napisana dla tego rynku, a nie tylko przetworzona przez podstawowe narzędzie tłumaczeniowe i pozostawiona bez przeglądu.

Proces utrzymywania tłumaczeń na bieżąco. Gdy zmienia się zawartość źródłowa, tłumaczenia również muszą się zmieniać. To jest część, którą większość zespołów niedoszacowuje. Zespół zajmujący się treścią, który publikuje kilka aktualizacji tygodniowo w przestrzeni Contentful z czterema lokalizacjami i pięćdziesięcioma typami treści, stoi przed znacznym, ciągłym obciążeniem pracą tłumaczeniową, jeśli jest to obsługiwane ręcznie.

Sposób na wiedzę, kiedy tłumaczenia są nieaktualne. System lokalizacji Contentful nie pokazuje przestarzałości. Jeśli zaktualizujesz angielską wersję wpisu i zapomnisz zaktualizować wersję francuską, wersja francuska będzie cicho dalej wyświetlać starą zawartość. Potrzebujesz albo procesu, albo narzędzi, aby to wykryć.


Gdzie pasuje PolyLingo

API PolyLingo tłumaczy treści strukturalne, zachowując ich strukturę. Dla Contentful odpowiednim formatem jest JSON — wartości pól wpisu Contentful, wyodrębnione i wysłane do API, wracają przetłumaczone z zachowaniem tej samej struktury.

Podstawowy przepływ w 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')

// Pobierz wpis, który chcesz przetłumaczyć
const entry = await environment.getEntry('your-entry-id')

// PolyLingo używa kodów BCP-47 bez regionu (fr, de).
// Mapuj je na swoje ID lokalizacji Contentful, jeśli się różnią (np. fr-FR, de-DE).
const localeMap = { fr: 'fr', de: 'de' } // dostosuj, aby pasowało do ID lokalizacji twojej przestrzeni

// Wyodrębnij tylko pola tekstowe w języku angielskim — pomiń referencje, linki, wartości logiczne i liczby
const sourceFields = Object.fromEntries(
  Object.entries(entry.fields)
    .filter(([, value]) => typeof value['en-US'] === 'string')
    .map(([key, value]) => [key, value['en-US']])
)

// Przetłumacz na francuski i niemiecki w jednym żądaniu
const result = await poly.translate({
  content: JSON.stringify(sourceFields),
  format: 'json',
  targets: Object.keys(localeMap),
})

// Zapisz przetłumaczone wartości z powrotem do wpisu, używając ID lokalizacji 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('Wpis przetłumaczony i opublikowany.')

Jedno wywołanie API zwraca zarówno francuski, jak i niemiecki. Wpis jest aktualizowany i publikowany w tym samym skrypcie.

Uwaga dotycząca ID lokalizacji: PolyLingo używa kodów BCP-47 bez sufiksu regionu (fr, de). Przestrzenie Contentful często używają ID kwalifikowanych regionem, takich jak fr-FR lub de-DE. Obiekt localeMap powyżej to miejsce, gdzie je dopasowujesz — zaktualizuj go, aby pasował do ID lokalizacji używanych w twojej przestrzeni Contentful.

Ten sam wzorzec działa w Pythonie:

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')

# Wyodrębnij wartości pól w języku angielskim
source_fields = {
    key: value.get('en-US')
    for key, value in entry.fields().items()
    if value.get('en-US')
}

# Przetłumacz na francuski i niemiecki
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']

# Zapisz z powrotem do wpisu
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('Wpis przetłumaczony i opublikowany.')

Tłumaczenie na dużą skalę

Powyższe przykłady obsługują pojedynczy wpis. Dla przestrzeni Contentful z wieloma wpisami w różnych typach treści, punkt końcowy batch obsługuje do 100 elementów na żądanie:

// Pobierz wiele wpisów i przetłumacz je w jednym wywołaniu batch
const entries = await environment.getEntries({
  content_type: 'blogPost',
  limit: 50,
})
 
// Mapuj kody lokalizacji PolyLingo na identyfikatory lokalizacji Contentful
const localeMap = { fr: 'fr', de: 'de', es: 'es' } // dostosuj do swojej przestrzeni
 
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()
}

Pięćdziesiąt wpisów, trzy języki, jedno żądanie batch.


Utrzymywanie tłumaczeń na bieżąco

Powyższe skrypty można uruchamiać na żądanie lub zintegrować z przepływem pracy publikacji. Powszechnym wzorcem jest uruchamianie tłumaczenia jako części zadania CI wyzwalanego, gdy zmienia się zawartość źródłowa, lub według harmonogramu, który obejmuje wszystkie wpisy zaktualizowane w ciągu ostatnich 24 godzin.

Contentful obsługuje również webhooki — możesz skonfigurować webhook, który uruchamia się, gdy wpis zostanie opublikowany w domyślnej lokalizacji, co może automatycznie wyzwolić zadanie tłumaczenia. Podłączenie PolyLingo do tego webhooka oznacza, że za każdym razem, gdy redaktor publikuje zaktualizowaną zawartość w języku angielskim, tłumaczone lokalizacje są aktualizowane bez żadnego ręcznego kroku. Jest to na mapie drogowej dedykowanej integracji PolyLingo; tymczasem API daje wszystko, czego potrzebujesz, aby samodzielnie zbudować ten przepływ.


Uwagi dotyczące typów pól

Powyższe podejście dobrze sprawdza się w przypadku krótkiego tekstu, długiego tekstu oraz tekstu sformatowanego przechowywanego jako ciągi znaków. Niektóre typy pól Contentful wymagają osobnego traktowania:

Pola tekstu sformatowanego przechowywane w formacie dokumentu Contentful (nie jako zwykłe ciągi znaków) muszą być serializowane do zwykłego tekstu lub HTML przed wysłaniem do PolyLingo, a następnie deserializowane z powrotem. Pakiet @contentful/rich-text-html-renderer obsługuje etap serializacji, a format: "html" po stronie PolyLingo poprawnie zachowuje znaczniki.

Pola referencyjne (odnośniki do innych wpisów lub zasobów) nie powinny być tłumaczone — są to identyfikatory, nie tekst. Wyklucz je z pól, które wysyłasz do API.

Pola liczbowe, logiczne i daty z natury nie są tłumaczalne. Przefiltruj je przed zbudowaniem obiektu źródłowego.


Rozpoczęcie

Darmowy plan PolyLingo obejmuje 50 000 tokenów miesięcznie. Dla przestrzeni Contentful z umiarkowaną ilością treści, pokrywa to początkową rundę tłumaczeń dla kilku wpisów w wielu lokalizacjach, z zapasem na bieżące aktualizacje.

Pełna dokumentacja API znajduje się na usepolylingo.com/docs. Pakiety SDK są dostępne dla Node.js, Python, Ruby, PHP, Java i Go.

npm install polylingo

Uzyskaj swój klucz API

Dlaczego Twoje treści w Contentful nie są jeszcze naprawdę wielojęzyczne | PolyLingo