
ทำไมเนื้อหา Contentful ของคุณยังไม่ใช่หลายภาษาอย่างแท้จริง
By Robert
ทำไมเนื้อหา Contentful ของคุณยังไม่ใช่หลายภาษาอย่างแท้จริง
Contentful จัดการ locales ได้ดี คุณสามารถกำหนดได้ตามที่ต้องการ ตั้งค่าลำดับ fallback สลับระหว่าง locales เหล่านั้นในตัวแก้ไข และสร้าง frontend ที่ให้บริการภาษาที่ถูกต้องตามเส้นทาง โครงสร้างพื้นฐานสำหรับเนื้อหาหลายภาษานั้นดีจริงๆ
สิ่งที่ Contentful ไม่ได้ทำคือการแปลเนื้อหาของคุณ
สิ่งนี้ฟังดูชัดเจนเมื่อพูดตรงๆ แต่ก็ง่ายมากที่จะสับสนระหว่าง "เรามีการตั้งค่า fields ของ locale" กับ "เราหลายภาษา" สองสิ่งนี้ไม่เหมือนกัน fields ของ locale เป็นเพียงภาชนะ หลายภาษาหมายถึงภาชนะนั้นมีเนื้อหาอยู่
ถ้าพื้นที่ Contentful ของคุณมี locales ภาษาฝรั่งเศสและเยอรมันตั้งค่าไว้ แต่ fields ใน locales เหล่านั้นว่างเปล่า หรือเติมด้วยข้อความภาษาอังกฤษเป็นตัวแทน หรือเติมด้วยร่างแรกที่ยังไม่มีใครตรวจสอบตั้งแต่เว็บไซต์เปิดตัว — คุณยังไม่ใช่หลายภาษา คุณมีแค่โครงสร้างสำหรับหลายภาษา
โพสต์นี้เกี่ยวกับการปิดช่องว่างนั้น
สิ่งที่ 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('Entry translated and published.')
การเรียก 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('Entry translated and published.')
การแปลในระดับใหญ่
ตัวอย่างข้างต้นจัดการกับรายการเดียว สำหรับ Contentful space ที่มีหลายรายการในหลายประเภทเนื้อหา จุดสิ้นสุดแบบแบตช์รองรับได้สูงสุด 100 รายการต่อคำขอ:
// ดึงหลายรายการและแปลในคำขอแบบแบตช์ครั้งเดียว
const entries = await environment.getEntries({
content_type: 'blogPost',
limit: 50,
})
// แมปโค้ดภาษาของ PolyLingo กับ ID ภาษาของ Contentful
const localeMap = { fr: 'fr', de: 'de', es: 'es' } // ปรับให้ตรงกับ 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()
}
ห้าสิบรายการ สามภาษา คำขอแบบแบตช์หนึ่งครั้ง
การรักษาการแปลให้ทันสมัย
สคริปต์ข้างต้นสามารถรันตามความต้องการหรือเชื่อมต่อเข้ากับเวิร์กโฟลว์การเผยแพร่ของคุณ รูปแบบที่พบบ่อยคือการรันการแปลเป็นส่วนหนึ่งของงาน CI ที่ถูกทริกเกอร์เมื่อเนื้อหาแหล่งที่มามีการเปลี่ยนแปลง หรือในตารางเวลาที่จับการอัปเดตใด ๆ ใน 24 ชั่วโมงที่ผ่านมา
Contentful ยังรองรับเว็บฮุก — คุณสามารถกำหนดค่าเว็บฮุกที่จะทำงานเมื่อมีการเผยแพร่รายการในภาษาหลัก ซึ่งสามารถทริกเกอร์งานแปลโดยอัตโนมัติ การเชื่อมต่อ PolyLingo เข้ากับเว็บฮุกนั้นหมายความว่า ทุกครั้งที่บรรณาธิการเผยแพร่เนื้อหาอังกฤษที่อัปเดต ภาษาที่แปลจะได้รับการอัปเดตโดยไม่ต้องมีขั้นตอนด้วยตนเอง นี่เป็นแผนงานสำหรับการรวม PolyLingo โดยเฉพาะ; ในระหว่างนี้ API ให้ทุกอย่างที่คุณต้องการเพื่อสร้างกระบวนการนั้นด้วยตัวเอง
หมายเหตุเกี่ยวกับประเภทฟิลด์
วิธีการข้างต้นทำงานได้ดีสำหรับข้อความสั้น ข้อความยาว และข้อความที่มีรูปแบบซึ่งเก็บเป็นสตริง ประเภทฟิลด์บางประเภทใน Contentful ต้องจัดการแยกต่างหาก:
ฟิลด์ข้อความที่มีรูปแบบ ที่เก็บในรูปแบบเอกสารของ Contentful (ไม่ใช่สตริงธรรมดา) จำเป็นต้องถูกแปลงเป็นข้อความธรรมดาหรือ HTML ก่อนส่งไปยัง PolyLingo จากนั้นจึงแปลงกลับ ฟังก์ชัน @contentful/rich-text-html-renderer จะจัดการขั้นตอนการแปลง และ format: "html" ในฝั่ง PolyLingo จะรักษารูปแบบเครื่องหมายได้อย่างถูกต้อง
ฟิลด์อ้างอิง (ลิงก์ไปยังรายการหรือแอสเซ็ตอื่น) ไม่ควรแปล — เพราะเป็น ID ไม่ใช่ข้อความ ให้ยกเว้นฟิลด์เหล่านี้จากข้อมูลที่คุณส่งไปยัง API
ฟิลด์ตัวเลข, บูลีน, และวันที่ โดยธรรมชาติแล้วไม่สามารถแปลได้ ให้กรองออกก่อนสร้างอ็อบเจ็กต์ต้นทางของคุณ
การเริ่มต้น
ระดับฟรีของ PolyLingo รวมโทเค็น 50,000 โทเค็นต่อเดือน สำหรับพื้นที่ Contentful ที่มีเนื้อหาปานกลาง นั่นครอบคลุมการแปลครั้งแรกในหลายรายการในหลายภาษาพร้อมพื้นที่เหลือสำหรับการอัปเดตต่อเนื่อง
เอกสาร API ฉบับเต็มอยู่ที่ usepolylingo.com/docs แพ็กเกจ SDK มีให้สำหรับ Node.js, Python, Ruby, PHP, Java และ Go
npm install polylingo