Використання msgctxt: Додавання контексту до перекладів Gettext

Ви перекладаєте англійське слово "Book" на німецьку, і ваш перекладач впевнено повертає "Buch". Три тижні по тому з'являється звіт про помилку: кнопка "Book a table" у вашій формі бронювання тепер читається як "Buch a table" – іменник, а не дієслово. Слово було правильним. Контекст був відсутній. Це одна з найпоширеніших прихованих помилок у локалізації програмного забезпечення, і рішення існує в Gettext десятиліттями: msgctxt.
Якщо ви коли-небудь випускали переклад, де один англійський рядок був правильним в одному місці та безглуздим в іншому, ви стикалися з проблемою неоднозначності. Одне й те саме вихідне слово відображається на різні переклади залежно від місця його появи. Без msgctxt перекладач (людина чи ШІ) не має можливості розрізнити ці випадки, тому що все, що вони бачать, це голий рядок msgid "Book". Цей посібник пояснює, як msgctxt працює в Gettext, як розмітити ваш вихідний код за допомогою _x() та _ex(), як він відображається у ваших файлах .po, і чому контестно-орієнтований конвеєр перекладу є єдиним надійним способом правильно перекладати ці рядки в масштабі.
Проблема неоднозначності: Одне слово, багато значень
Природна мова сповнена слів, які згортаються до одного англійського токена, але розбиваються на кілька слів іншими мовами. Перекладач бачить лише вихідний рядок, тому, коли два непов'язані елементи інтерфейсу користувача мають одне й те саме англійське слово, вони мають той самий msgid – і Gettext об'єднує їх в один запис.
Слова, що розрізняються в різних мовах
Розглянемо три класичні приклади:
- "Book" – іменник (об'єкт на полиці) проти дієслова ("Book a flight"). Німецька: Buch проти buchen.
- "Post" – публікувати контент проти надсилати пошту. Французька: publier проти courrier.
- "Order" – послідовність/сортування проти покупки. Іспанська: orden проти pedido.
В англійській мові ваш код використовує той самий буквальний рядок в обох місцях:
// Somewhere in a library catalog screen
echo __( 'Book', 'mytextdomain' );
// Somewhere in a reservation form button
echo __( 'Book', 'mytextdomain' );
Як ідентичні рядки згортаються в один запис
Коли ви запускаєте wp i18n make-pot або xgettext, обидва виклики згортаються в один запис .po:
#: catalog.php:42 reservation-form.php:88
msgid "Book"
msgstr ""
Існує лише один msgstr для заповнення. Що б не обрав перекладач, один із двох екранів буде неправильним. Перекладач не може виправити це, навіть якщо розуміє проблему, оскільки сам формат не дозволяє надати два переклади для одного msgid. Неоднозначність закладена в дані.
Що таке msgctxt і як він усуває неоднозначність?
msgctxt – це контекстний рядок, прикріплений до запису перекладу. Коротка відповідь: він додає другий ключ поряд з msgid, тому Gettext розглядає (context, msgid) як унікальну пару. Два записи з однаковим msgid, але різними msgctxt стають двома окремими рядками, що перекладаються незалежно.
У моделі пошуку Gettext переклад зазвичай індексується лише за вихідним рядком. З msgctxt ключ стає комбінацією контексту та вихідного тексту. Це весь механізм, і саме тому він працює так чисто: ви не змінюєте відображуваний текст, лише внутрішній ключ пошуку.
Рядок msgctxt у файлі .po
У файлі .po контекст з'являється на окремому рядку безпосередньо над msgid:
#: catalog.php:42
msgctxt "noun"
msgid "Book"
msgstr "Buch"
#: reservation-form.php:88
msgctxt "verb"
msgid "Book"
msgstr "Reservieren"
Тепер є два записи. Вони мають ідентичні значення msgid, але різні значення msgctxt, тому кожен отримує свій msgstr. Каталог відображає Buch; кнопка бронювання відображає Reservieren. Середовище виконання вибирає правильний, тому що ваш код вказав, який контекст використовувати.
Порівняйте це з пошкодженою версією з одним записом вище. Відмінність не в якості перекладу – а в тому, чи дозволяє модель даних взагалі існувати правильній відповіді.
Розмітка вашого коду: _x() та _ex()
Щоб видати msgctxt у ваш шаблон .po, ви викликаєте функцію перекладу, що враховує контекст, замість звичайної. У WordPress це _x() та його відлунюючий побратим _ex(); у чистому Gettext вони відповідають pgettext().
Вибір між _x() та _ex()
Звичайна функція __() приймає рядок і текстовий домен. Варіант з контекстом вставляє контекст як другий аргумент:
// Without context - ambiguous
__( 'Book', 'mytextdomain' );
// With context - the second argument is the msgctxt
_x( 'Book', 'noun', 'mytextdomain' ); // returns the translated string
_ex( 'Book', 'verb', 'mytextdomain' ); // echoes it directly
// Real-world disambiguation
_x( 'Post', 'verb: publish content', 'mytextdomain' );
_x( 'Post', 'noun: mail item', 'mytextdomain' );
_x( 'Order', 'sequence or sorting', 'mytextdomain' );
_x( 'Order', 'customer purchase', 'mytextdomain' );
Другий аргумент – це мітка контексту. Вона ніколи не відображається вашим користувачам – вона існує виключно для усунення неоднозначності, як для пошуку Gettext, так і для того, хто (або що) виконує переклад. Пишіть контексти як короткі, описові підказки: 'noun', 'verb', 'button label', 'admin menu'. Нечіткий контекст, як '1', технічно допустимий, але марний для перекладача.
Коли ви регенеруєте шаблон, екстрактор розпізнає ці виклики та видає рядок msgctxt. Інструменти, такі як Poedit та інші редактори PO, потім візуально групують записи за контекстом, щоб людина-перекладач одразу бачила, що "Book (noun)" та "Book (verb)" – це дві різні задачі. Якщо ви все ще налаштовуєте свій інструментарій для вилучення або боретеся з тим, як змінні взаємодіють з цими рядками, наш посібник з перекладу файлів PO без пошкодження змінних коду детально охоплює навколишній робочий процес.
Чому наївні перекладачі – і багато інструментів ШІ – роблять це неправильно
Ось незручна частина. Додавання msgctxt до вашого вихідного коду є необхідним, але недостатнім. Контекст допомагає лише в тому випадку, якщо те, що виконує переклад, справді його читає.
Режим збою "відкинутий контекст"
Наївний скрипт пакетного перекладу робить так: він перебирає записи, бере кожен msgid, надсилає його до API перекладу та записує результат у msgstr. Він ніколи не дивиться на рядок msgctxt. Отже, незважаючи на те, що ви ретельно написали _x( 'Book', 'noun' ) та _x( 'Book', 'verb' ), скрипт надсилає два ідентичні запити – "translate Book" – і вставляє одну й ту саму відповідь в обидва. Всі ваші зусилля з розмітки відкидаються на останньому кроці.
Багато універсальних інструментів перекладу на основі ШІ мають таку ж сліпу пляму. Вони створені для перекладу тексту, а msgctxt – це метадані, а не текст. Якщо інструмент перетворює ваш файл .po на список рядків перед надсиланням до моделі, контекст ніколи не досягає запиту моделі. Модель, не маючи жодного сигналу, за замовчуванням вибирає найбільш статистично поширене значення слова – зазвичай іменник для "Book", значення "публікувати" для "Post" – і мовчки неправильно перекладає менш поширений випадок. Ви не побачите помилки. Ви побачите звіт про помилку від німецького користувача через кілька тижнів.
Контекст і множина мають одну й ту ж першопричину
Тут також перетинаються обробка множини та контекст, оскільки обидва залежать від того, чи інструмент розуміє структуру Gettext, а не розглядає файл як плоский текст. Якщо ваші рядки також містять форми на основі кількості, та сама структурна обізнаність має значення – ми розбираємо це в розумінні множин Gettext.
Як контестно-орієнтований конвеєр використовує msgctxt
Конвеєр перекладу, що поважає msgctxt, робить протилежне наївному скрипту. Він аналізує файл .po як структуровані дані, зберігає контекст кожного запису прив'язаним до його вихідного рядка та передає цей контекст до ШІ як частину запиту – так модель знає, що вона перекладає "Book" саме як дієслово, а не в абстрактному сенсі.
Контекст як першокласний сигнал
Саме так SimplePoTranslate підходить до цієї проблеми. Його ШІ, що враховує контекст, читає рядок msgctxt як першокласний сигнал: коли два записи мають спільний msgid, але відрізняються контекстом, вони перекладаються як окремі рядки, якими вони є насправді, а підказка контексту інформує модель про вибір слова. Результат полягає в тому, що "Book (noun)" повертається як Buch, тоді як "Book (verb)" повертається як buchen або reservieren – автоматично, без ручного виправлення кожного неоднозначного терміну.
Не менш важливо, що конвеєр зберігає рядок msgctxt у виводі. Слабший інструмент може видалити контекст під час циклу перетворення, тихо згортаючи ваші два записи назад в один і знову вводячи неоднозначність при наступному злитті. Повна підтримка Gettext означає, що контекст переживає переклад, компіляцію .mo та будь-які подальші повторні імпорти – тому ваше усунення неоднозначності є довговічним, а не одноразовою ручною виправленням. У поєднанні з Syntax Locking для заповнювачів у цих рядках ви отримуєте переклади, які є як контекстуально правильними, так і структурно цілісними.
Ви все ще приймаєте рішення щодо розмітки – тільки ви знаєте, що дане "Book" є дієсловом. Але як тільки ви анотували свій вихідний код за допомогою _x() та _ex(), контестно-орієнтований конвеєр перетворює цю анотацію на правильні переклади для кожної цільової мови без контролю кожного рядка.
Висновок
Проблема неоднозначності – одне англійське слово, багато значень – це не дивацтво, яке можна ігнорувати; це структурна особливість того, як Gettext зберігає рядки. Рішенням є msgctxt: анотуйте неоднозначні рядки у вашому вихідному коді за допомогою _x() та _ex(), надайте кожному вживанню чітку мітку контексту і дозвольте Gettext індексувати переклад за парою (context, msgid). Ця частина залежить від вас, і це займає хвилини.
Складніша частина полягає в тому, щоб переконатися, що ваш етап перекладу дійсно враховує цей контекст, а не відкидає його. Наївні скрипти та інструменти ШІ, що працюють лише з текстом, відкидають msgctxt і знову вводять саме ту помилку, яку ви намагалися запобігти. Конвеєр, що враховує контекст, читає контекст, правильно перекладає кожен роз'яснений запис і зберігає його під час компіляції та повторного імпорту.
Готові припинити випускати переклади, що ігнорують контекст? Спробуйте SimplePoTranslate безкоштовно – кредитна картка не потрібна. Безкоштовний тариф підтримує реальні файли
.poта.potз повною підтримкою контексту msgctxt, тому ваші роз'яснені рядки перекладаються правильно з першого разу.