Quay lại blog
A Contentful-style content entry card showing a filled English locale tab and an empty French locale tab with a warning icon.

Tại sao nội dung Contentful của bạn chưa thực sự đa ngôn ngữ

By Robert

Tại sao nội dung Contentful của bạn chưa thực sự đa ngôn ngữ

Contentful xử lý các locale rất tốt. Bạn có thể định nghĩa bao nhiêu tùy thích, thiết lập chuỗi fallback, chuyển đổi giữa chúng trong trình chỉnh sửa, và xây dựng frontend phục vụ đúng ngôn ngữ theo từng tuyến đường. Cơ sở hạ tầng cho nội dung đa ngôn ngữ thực sự tốt.

Điều Contentful không làm là dịch nội dung của bạn.

Điều này nghe có vẻ rõ ràng khi nói thẳng, nhưng thật dễ nhầm lẫn giữa "chúng tôi đã thiết lập các trường locale" với "chúng tôi đa ngôn ngữ." Hai điều này không giống nhau. Trường locale là một vùng chứa. Đa ngôn ngữ nghĩa là vùng chứa đó có nội dung bên trong.

Nếu không gian Contentful của bạn đã cấu hình các locale tiếng Pháp và tiếng Đức nhưng các trường trong các locale đó trống, hoặc được điền bằng bản sao tiếng Anh làm chỗ giữ chỗ, hoặc được điền bằng bản thảo đầu tiên thô mà chưa ai xem lại kể từ khi trang web ra mắt — bạn chưa đa ngôn ngữ. Bạn chỉ có khung cho đa ngôn ngữ.

Bài viết này nói về việc thu hẹp khoảng cách đó.


Contentful thực sự mang lại cho bạn điều gì

Hệ thống ngôn ngữ của Contentful được thiết kế tốt. Mỗi mục nội dung có thể có giá trị trường theo từng ngôn ngữ. Bạn đặt ngôn ngữ mặc định, cấu hình các phương án dự phòng, và API phân phối của bạn sẽ trả về ngôn ngữ đúng khi được yêu cầu. Mô hình hóa nội dung đủ linh hoạt để xử lý các yêu cầu đa ngôn ngữ phức tạp trên các loại nội dung khác nhau.

Nhưng hệ thống hoàn toàn trung lập về cách nội dung đã dịch được đưa vào đó. Contentful không biết các trường ngôn ngữ tiếng Pháp của bạn có chứa bản dịch chuyên nghiệp, bản dịch máy, văn bản tiếng Anh dán nhầm, hay hoàn toàn không có gì. Nó chỉ lưu trữ và phân phối những gì bạn đưa vào.

Vấn đề dịch thuật hoàn toàn là của bạn để giải quyết.


Các nhóm thường xử lý như thế nào (và điểm thất bại)

Hầu hết các nhóm xử lý việc dịch Contentful đều rơi vào một trong vài mô hình.

Mô hình xuất thủ công. Một lập trình viên xuất nội dung từ Contentful, gửi cho một công ty dịch thuật hoặc freelancer, chờ nhận lại, định dạng lại và nhập vào. Cách này phù hợp cho lần ra mắt một lần nhưng không thể duy trì khi nội dung thay đổi. Mỗi lần cập nhật ngôn ngữ nguồn đều phải lặp lại toàn bộ quy trình. Trên thực tế, nội dung đã dịch nhanh chóng bị tụt lại phía sau nguồn và không ai có thời gian để cập nhật.

Mô hình dịch trong trình chỉnh sửa. Một biên tập viên mở từng mục, chuyển sang ngôn ngữ đích và dịch từng trường một bằng tay hoặc dán nội dung vào công cụ dịch. Cách này chính xác nhưng chậm. Nó cũng không mở rộng được — nếu bạn có hàng trăm mục trên hàng chục loại nội dung, khối lượng công việc thủ công là rất lớn.

Mô hình "tạm được". Nội dung đã dịch tồn tại nhưng chưa được xem xét kể từ khi tạo ra lần đầu. Ngôn ngữ nguồn đã được cập nhật nhiều lần kể từ đó. Phiên bản tiếng Pháp của trang giá của bạn vẫn tham chiếu đến một gói bạn đã ngừng cung cấp tám tháng trước. Trang chủ tiếng Đức vẫn còn khẩu hiệu cũ. Không ai phát hiện vì không ai kiểm tra.

Cả ba mô hình đều chia sẻ cùng một vấn đề gốc: dịch thuật được xem như một nhiệm vụ làm một lần thay vì là một phần liên tục trong quy trình nội dung.


Điều mà đa ngôn ngữ thực sự yêu cầu

Nội dung đa ngôn ngữ thực sự trong Contentful cần ba yếu tố hoạt động cùng nhau.

Bản dịch ban đầu chính xác. Mỗi trường trong mỗi ngôn ngữ đích cần có nội dung được dịch chính xác, phù hợp với địa phương và thực sự đọc như thể nó được viết cho thị trường đó thay vì chỉ chạy qua công cụ dịch cơ bản và không được xem xét lại.

Quy trình để giữ cho bản dịch luôn cập nhật. Khi nội dung gốc thay đổi, nội dung dịch cũng cần thay đổi theo. Đây là phần mà hầu hết các nhóm đánh giá thấp. Một nhóm nội dung xuất bản vài bản cập nhật mỗi tuần trên một không gian Contentful với bốn ngôn ngữ và năm mươi loại nội dung sẽ phải đối mặt với khối lượng công việc dịch thuật đáng kể nếu xử lý thủ công.

Cách để biết khi nào bản dịch đã lỗi thời. Hệ thống ngôn ngữ của Contentful không hiển thị sự lỗi thời. Nếu bạn cập nhật bản sao tiếng Anh của một mục và quên cập nhật bản sao tiếng Pháp, phiên bản tiếng Pháp sẽ tiếp tục phục vụ nội dung cũ một cách âm thầm. Bạn cần một quy trình hoặc công cụ để phát hiện điều này.


Vị trí của PolyLingo

API của PolyLingo dịch nội dung có cấu trúc đồng thời giữ nguyên cấu trúc đó. Đối với Contentful, định dạng liên quan là JSON — các giá trị trường của một mục Contentful được trích xuất và gửi đến API, sẽ được trả về đã dịch với cùng cấu trúc nguyên vẹn.

Luồng cơ bản trong 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')

// Lấy mục bạn muốn dịch
const entry = await environment.getEntry('your-entry-id')

// PolyLingo sử dụng mã BCP-47 không có vùng (fr, de).
// Ánh xạ chúng với ID ngôn ngữ Contentful của bạn nếu khác (ví dụ fr-FR, de-DE).
const localeMap = { fr: 'fr', de: 'de' } // điều chỉnh cho phù hợp với ID ngôn ngữ của không gian bạn

// Chỉ trích xuất các trường chuỗi tiếng Anh — bỏ qua tham chiếu, liên kết, boolean và số
const sourceFields = Object.fromEntries(
  Object.entries(entry.fields)
    .filter(([, value]) => typeof value['en-US'] === 'string')
    .map(([key, value]) => [key, value['en-US']])
)

// Dịch sang tiếng Pháp và tiếng Đức trong một yêu cầu
const result = await poly.translate({
  content: JSON.stringify(sourceFields),
  format: 'json',
  targets: Object.keys(localeMap),
})

// Ghi các giá trị đã dịch trở lại mục sử dụng ID ngôn ngữ 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('Mục đã được dịch và xuất bản.')

Một lần gọi API trả về cả tiếng Pháp và tiếng Đức. Mục được cập nhật và xuất bản trong cùng một tập lệnh.

Lưu ý về ID ngôn ngữ: PolyLingo sử dụng mã BCP-47 không có hậu tố vùng (fr, de). Các không gian Contentful thường sử dụng ID có vùng như fr-FR hoặc de-DE. Đối tượng localeMap ở trên là nơi bạn căn chỉnh chúng — cập nhật nó để phù hợp với ID ngôn ngữ mà không gian Contentful của bạn sử dụng.

Cùng một mẫu hoạt động trong 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')

# Trích xuất giá trị trường tiếng Anh
source_fields = {
    key: value.get('en-US')
    for key, value in entry.fields().items()
    if value.get('en-US')
}

# Dịch sang tiếng Pháp và tiếng Đức
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']

# Ghi lại vào mục
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('Mục đã được dịch và xuất bản.')

Dịch quy mô lớn

Các ví dụ trên xử lý một mục duy nhất. Đối với một không gian Contentful có nhiều mục trên nhiều loại nội dung, điểm cuối theo lô xử lý tối đa 100 mục mỗi yêu cầu:

// Lấy nhiều mục và dịch chúng trong một lần gọi theo lô
const entries = await environment.getEntries({
  content_type: 'blogPost',
  limit: 50,
})
 
// Ánh xạ mã ngôn ngữ PolyLingo sang ID ngôn ngữ Contentful
const localeMap = { fr: 'fr', de: 'de', es: 'es' } // điều chỉnh cho phù hợp với không gian của bạn
 
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()
}

Năm mươi mục, ba ngôn ngữ, một yêu cầu theo lô.


Giữ cho bản dịch luôn cập nhật

Các script ở trên có thể được chạy theo yêu cầu hoặc tích hợp vào quy trình xuất bản của bạn. Một mô hình phổ biến là chạy bản dịch như một phần của công việc CI được kích hoạt khi nội dung nguồn thay đổi, hoặc theo lịch trình bắt kịp bất kỳ mục nào được cập nhật trong 24 giờ qua.

Contentful cũng hỗ trợ webhook — bạn có thể cấu hình một webhook kích hoạt khi một mục được xuất bản ở ngôn ngữ mặc định, điều này có thể tự động kích hoạt một công việc dịch thuật. Kết nối PolyLingo vào webhook đó có nghĩa là mỗi khi biên tập viên xuất bản nội dung tiếng Anh đã cập nhật, các ngôn ngữ được dịch sẽ được cập nhật mà không cần bước thủ công nào. Đây là kế hoạch trong lộ trình cho một tích hợp PolyLingo chuyên dụng; trong khi đó API cung cấp cho bạn mọi thứ bạn cần để xây dựng quy trình đó tự mình.


Ghi chú về loại trường

Cách tiếp cận trên hoạt động tốt cho văn bản ngắn, văn bản dài và văn bản phong phú được lưu dưới dạng chuỗi. Một số loại trường trong Contentful cần được xử lý riêng:

Trường văn bản phong phú được lưu trong định dạng tài liệu của Contentful (không phải chuỗi thuần) cần được tuần tự hóa thành văn bản thuần hoặc HTML trước khi gửi đến PolyLingo, sau đó giải tuần tự trở lại. Gói @contentful/rich-text-html-renderer xử lý bước tuần tự hóa, và format: "html" ở phía PolyLingo giữ nguyên đúng định dạng đánh dấu.

Trường tham chiếu (liên kết đến các mục hoặc tài sản khác) không nên dịch — chúng là ID, không phải văn bản. Loại trừ chúng khỏi các trường bạn gửi đến API.

Trường số, boolean và ngày tháng về bản chất không thể dịch được. Lọc chúng ra trước khi xây dựng đối tượng nguồn của bạn.


Bắt đầu

Gói miễn phí của PolyLingo bao gồm 50.000 token mỗi tháng. Đối với một không gian Contentful với lượng nội dung vừa phải, điều đó bao gồm một lượt dịch ban đầu trên nhiều mục trong nhiều ngôn ngữ với chỗ trống cho các cập nhật tiếp theo.

Tài liệu API đầy đủ có tại usepolylingo.com/docs. Các gói SDK có sẵn cho Node.js, Python, Ruby, PHP, Java và Go.

npm install polylingo

Lấy khóa API của bạn