Перевод i18n файлов нейросетью без сломанных плейсхолдеров
Скрипт, который переводит i18n-файлы через API, сохраняет {name}, %s и ICU-формы и откладывает сомнительные строки на ручную проверку.
6 мин · · разработка · автоматизация
Перевод i18n файлов нейросетью экономит часы, пока модель не «переведёт» {name} в {имя} или не сломает ICU-форму множественного числа — и экран приложения упадёт с ошибкой форматирования. Ниже рабочий скрипт: батчи, глоссарий, автоматическая проверка плейсхолдеров и расчёт стоимости по ценам Zukko.
Чем API лучше копипаста в переводчик
Обычный машинный переводчик видит строку без контекста и не знает правил вашего проекта. Через API вы управляете процессом сами:
- в каждый запрос уходит глоссарий и правила (не трогать плейсхолдеры, держать стиль интерфейса);
- ответ приходит в JSON с теми же ключами, его можно разобрать программой;
- каждую строку можно проверить кодом и отклонить, если структура изменилась;
- при обновлении исходника переводятся только изменённые ключи.
Человеческая вычитка всё равно нужна — особенно для маркетинговых текстов и юридических формулировок. Скрипт снимает рутину, а не ответственность.
Схема: разложить, перевести, проверить, собрать
- Прочитать исходный JSON и разложить вложенную структуру в плоский словарь
ключ → строка. - Отправлять строки батчами по 20 штук в виде JSON-объекта.
- Сравнить плейсхолдеры в оригинале и переводе; при расхождении оставить оригинал и вывести ключ в список на ручную проверку.
- Собрать вложенную структуру обратно и сохранить.
Скрипт на Python
Нужны Python 3.10+ и pip install openai. Ключ fetch_... лежит в переменной OPENAI_API_KEY.
import json
import os
import re
from openai import OpenAI
client = OpenAI(
base_url="https://api.zukko.pro/v1",
api_key=os.environ["OPENAI_API_KEY"],
)
MODEL = "glm-5.3-flash"
PLACEHOLDER_PATTERNS = [
r"\{\{\s*[A-Za-z_]\w*\s*\}\}", # {{name}}
r"\{\s*[A-Za-z_]\w*\s*[,}]", # {name} и начало ICU: {count,Что здесь важно:
- Ответ как JSON-объект с ключами, а не нумерованный список. Если модель пропустит строку, это видно по ключу, а не по сдвигу всего батча.
- Проверка нарочно строгая. Если в ветке ICU стоит одно латинское слово (
one {item}), проверка сработает ложно и вернёт оригинал. Такие строки попадут в список «Проверить вручную» — это дешевле, чем пропустить сломанную. - Ответ в обёртке
```jsonразбирается функциейparse_json. - YAML. Для файлов
.ymlзаменитеjson.load/json.dumpнаyaml.safe_loadиyaml.safe_dump(data, allow_unicode=True)из PyYAML — остальной код не меняется.
Подробнее о клиенте — в руководстве по Python SDK и справке по Chat Completions.
ICU plural: самое хрупкое место
В английском у множественного числа две категории (one, other), в русском — четыре: one, few, many, other. Если модель механически перенесёт английскую структуру, «21 элементов» и «2 элементов» гарантированы.
Исходник:
{
"items": "You have {count, plural, =0 {no items} one {# item} other {# items}}"
}Правильный русский перевод:
{
"items": "У вас {count, plural, =0 {нет элементов} one {# элемент} few {# элемента} many {# элементов} other {# элемента}}"
}one в русском покрывает не только 1, но и 21, 31, 101; other нужен для дробных чисел («1,5 элемента»). Поэтому в правилах промпта явно перечислены категории целевого языка. После перевода стоит прогнать строки через вашу ICU-библиотеку (например, отрендерить каждую для 0, 1, 2, 5, 21) — это ловит ошибки, которые регулярное выражение не видит.
Сколько стоит перевод 10 тысяч строк
Допущения: средняя строка — 15 токенов, батч — 20 строк, значит 10 000 / 20 = 500 запросов.
- Вход на запрос: 300 токенов строк + около 100 на ключи JSON + около 200 на правила и глоссарий ≈ 600 токенов. Всего 500 × 600 = 300 тыс.
- Выход на запрос: строки и ключи ≈ 500 токенов (русский текст обычно занимает больше токенов, чем английский). Всего 500 × 500 = 250 тыс.
| Модель | Цена вход/выход, $ за 1M | Расчёт | Итого на один язык |
|---|---|---|---|
glm-5.3-flash | 0.04 / 0.04 | (300 000 × 0.04 + 250 000 × 0.04) / 1 000 000 | $0.022 |
claude-sonnet-5 | 0.3 / 0.3 | (300 000 × 0.3 + 250 000 × 0.3) / 1 000 000 | $0.165 |
gpt-5.6-sol | 0.3 / 0.3 | то же | $0.165 |
Десять языков — умножьте на 10. Модель выбирайте не по цене, а по результату: переведите одни и те же 50–100 строк на glm-5.3-flash и на claude-sonnet-5, сравните, сколько строк отбраковала проверка и сколько правок внёс носитель языка. Актуальный список моделей — GET https://api.zukko.pro/v1/models или страница «Модели» в кабинете.
Про кэш: у вендоров чтение повторяющегося префикса из кэша стоит около 10% входной цены, а в Zukko кэшированные токены оплачиваются по обычной входной цене. Здесь повторяется только блок правил и глоссария — примерно 200 × 500 = 100 тыс. токенов, на glm-5.3-flash это $0.004, так что на итог это почти не влияет.
Чтобы запустить скрипт, создайте ключ в кабинете и положите его в OPENAI_API_KEY; общий порядок подключения описан в быстром старте.
Частые вопросы
Как добиться одинакового перевода терминов?
Передавайте глоссарий в каждом запросе — словарь из 20–30 пар добавит около 200 токенов на батч. Для 500 батчей это 100 тыс. токенов, на glm-5.3-flash — $0.004. Термины, которые нельзя переводить вовсе (название продукта), тоже внесите в глоссарий: "Zukko" -> "Zukko".
Модель всё равно ломает плейсхолдеры. Что делать?
Скрипт уже не пропустит такие строки в файл: они останутся на исходном языке и попадут в список «Проверить вручную». Если таких много — уменьшите батч до 5–10 строк, добавьте в правила пример «Hello {name} → Привет, {name}» и сравните результат на другой модели.
Можно ли переводить строки с Markdown и HTML?
Можно. Добавьте в правила «Keep Markdown and HTML tags unchanged, translate only visible text» и дополните PLACEHOLDER_PATTERNS шаблоном для тегов, например r"</?[a-z]+[^>]*>", чтобы проверка сравнивала и их.
Как переводить только изменённые строки?
Сравните старую и новую версии исходника в плоском виде и переведите только разницу, затем вмерджите её в существующий перевод:
def update_translation(old_src: dict, new_src: dict, old_tr: dict, language: str) -> dict:
old_flat, new_flat = flatten(old_src), flatten(new_src)
changed = {k: v for k, v in new_flat.items() if old_flat.get(k) != v}
merged = {k: v for k, v in flatten(old_tr).items() if k in new_flat}
merged.update(flatten(translate_dict(unflatten(changed), language)))
return unflatten(merged)Удалённые из исходника ключи при этом уходят и из перевода.
zukko
Платите в 25×
меньше
за Claude Code & ChatGPT
Один ключ fetch_* и base URL api.zukko.pro — в Cursor, Claude Code, Codex и любом SDK.
Похожие статьи
- Перевод субтитров нейросетью: SRT без сбитых таймкодовГлавный риск при переводе SRT нейросетью — сломанная структура файла. Показываем схему, где таймкоды не попадают к модели, и скрипт на Python с проверкой.
- Редактирование текстов нейросетью: корректура, стиль и сокращениеРазные виды редактуры требуют разных промптов. Показываем формулировки для корректуры, стиля и сокращения, как получать правки с объяснением и не терять авторский голос.
- Нейросеть для написания скриптов: Bash, PowerShell и PythonМодель быстро пишет скрипты для рутины, но запускать их вслепую нельзя. Показываем, как ставить задачу, требовать безопасный режим dry-run и проверять результат до запуска.
- Dify в Docker: установка, первичная настройка и подключение моделейСтавим Dify через Docker Compose, создаём администратора и подключаем модели Claude, GPT и DeepSeek по одному ключу. Отдельно — что делать, если контейнеры не стартуют.