Локализация в Яндекс Играх — редкий случай, когда требование платформы не зависит от того, сколько языков вы поддерживаете. Пункт 2.14 обязателен даже для игры, в которой один язык и вообще нет текстов: автоопределение языка через SDK должно быть подключено, и модератор проверяет его отдельным индикатором на debug-панели. «Автоопределение языка через SDK не реализовано» стоит в официальной таблице частых причин отказа — рядом с отсутствующим Game Ready.
Хорошая новость: код здесь на десять строк, никакие библиотеки не нужны, а вся сложность — в вёрстке и в дисциплине.
Что именно требует пункт 2.14
Формулировка требования короткая: в игру встроено автоопределение языка через SDK. Пояснения к нему добавляют две вещи, которые и ломают большинство реализаций:
- определение должно происходить во время запуска, а не в процессе игры;
- оно должно быть реализовано у всех игр, даже если заявлен только один язык или текстов в игре нет.
Первое означает, что нельзя читать язык при открытии меню настроек или после нажатия «Играть». Модерация смотрит на индикатор 文 на debug-панели: он должен позеленеть на загрузке. Допустима небольшая задержка — пока подгружается файл перевода, на экране может успеть мелькнуть загрузочный текст на другом языке.
Ручное переключение языка внутри игры разрешено: требование запрещает не переключатель, а подмену автоопределения ручным выбором. Если игрок один раз выбрал язык и вы сохранили выбор, при следующем запуске автоопределение может не сработать — документация прямо оговаривает это как допустимый случай.
Частая ошибка
Определять язык через navigator.language — типовая ошибка перенесённой с других платформ игры. Язык браузера и язык портала Яндекс Игр — разные вещи: игрок с английской системой может открыть каталог на yandex.ru и ждать русского интерфейса. Модерация проверяет именно SDK, а ysdk.environment.browser.lang в актуальной документации отсутствует.
Откуда берётся язык
Одно поле объекта окружения:
const ysdk = await YaGames.init();
const lang = ysdk.environment.i18n.lang; // 'ru', 'en', 'tr', ...Код языка приходит в формате ISO 639-1 — две буквы. Это язык интерфейса Яндекс Игр, на котором пользователь открыл каталог, а не язык его операционной системы.
Платформа поддерживает 36 языков. Полный список кодов:
| ar — арабский | az — азербайджанский | be — белорусский | bg — болгарский |
| ca — каталанский | cs — чешский | de — немецкий | en — английский |
| es — испанский | fa — персидский | fr — французский | he — иврит |
| hi — хинди | hu — венгерский | hy — армянский | id — индонезийский |
| it — итальянский | ja — японский | ka — грузинский | kk — казахский |
| nl — голландский | pl — польский | pt — португальский | ro — румынский |
| ru — русский | sk — словацкий | sr — сербский | th — тайский |
| tk — туркменский | tr — турецкий | uk — украинский | uz — узбекский |
| vi — вьетнамский | zh — китайский |
Ваша игра почти наверняка не будет переведена на все 36. Поэтому нужен запасной язык.
Резервный набор языков
Документация задаёт его явно:
| Язык пользователя | Запасной язык |
|---|---|
be, kk, uk, uz | ru |
| Все остальные | en |
Логика понятная: для языков постсоветского пространства русский ближе, чем английский, для всех прочих — наоборот. Эта таблица — единственная строчка «бизнес-логики» в вашем модуле локализации:
// i18n.js
const RU_FALLBACK = ['be', 'kk', 'uk', 'uz'];
function resolveLang(sdkLang, supported) {
if (supported.includes(sdkLang)) return sdkLang;
if (RU_FALLBACK.includes(sdkLang) && supported.includes('ru')) return 'ru';
if (supported.includes('en')) return 'en';
return supported[0]; // на случай игры только на русском
}Последняя строка важна: если игра переведена только на русский, англоязычный игрок должен получить русский, а не пустые строки.
Словари без библиотек
Для игры на несколько экранов полноценный i18n-фреймворк — лишний вес. Достаточно объекта со строками и функции t().
// i18n.js (продолжение)
const DICTS = {
ru: {
'menu.play': 'Играть',
'menu.records': 'Рекорды',
'hud.score': 'Очки: {score}',
'result.win': 'Уровень пройден!',
'ad.reward': 'Получить {coins} монет за просмотр рекламы',
},
en: {
'menu.play': 'Play',
'menu.records': 'Leaderboard',
'hud.score': 'Score: {score}',
'result.win': 'Level complete!',
'ad.reward': 'Watch an ad for {coins} coins',
},
};
const SUPPORTED = Object.keys(DICTS);
let current = 'ru';
export function initI18n(ysdk) {
const sdkLang = ysdk?.environment?.i18n?.lang || 'ru';
current = resolveLang(sdkLang, SUPPORTED);
document.documentElement.lang = current;
return current;
}
export function t(key, params) {
const dict = DICTS[current] || {};
let str = dict[key];
if (str === undefined) {
// Ключ забыли перевести — берём английский, потом сам ключ.
str = DICTS.en?.[key] ?? key;
console.warn('[i18n] нет перевода:', current, key);
}
if (params) {
for (const name of Object.keys(params)) {
str = str.split('{' + name + '}').join(String(params[name]));
}
}
return str;
}
export function getLang() {
return current;
}Три решения здесь неслучайны. Ключи — плоские строки вида menu.play, а не вложенные объекты: их проще грепать и проще отдавать Claude Code («переведи все ключи из ru в tr, верни готовый объект»). Подстановка через {score} избавляет от конкатенации, которая в другом языке разваливается из-за порядка слов. И падение на английский вместо пустой строки: непереведённый ключ — это косметическая проблема, пустая кнопка — отказ по пункту 1.15.
Разметку удобно размечать атрибутом и обходить один раз после инициализации:
<button id="play" data-i18n="menu.play">Играть</button>
<h2 data-i18n="result.win">Уровень пройден!</h2>export function applyTranslations(root = document) {
for (const el of root.querySelectorAll('[data-i18n]')) {
el.textContent = t(el.dataset.i18n);
}
}Порядок в жизненном цикле — тот же, что у остальных стартовых операций: YaGames.init() → initI18n(ysdk) → загрузка ресурсов и применение переводов → ysdk.features.LoadingAPI?.ready(). Язык должен быть определён до Game Ready, иначе индикатор на debug-панели позеленеет позже, чем нужно.
Вёрстка под длинные строки
Русский текст в среднем длиннее английского примерно на четверть, немецкий и турецкий — ещё длиннее, а «Играть» и «Play» отличаются вдвое. Всё, что сломается, сломается именно на этом.
Практические правила:
- Кнопки и панели — по содержимому, с
min-width, а не с фиксированной шириной.padding: 0 16pxвместоwidth: 120px. - Для длинных надписей — перенос по словам и уменьшение шрифта, а не обрезка многоточием. Обрезанный текст в интерфейсе модерация трактует как нечитаемую надпись.
- В
canvasизмеряйте текст сами:ctx.measureText(str).widthи уменьшайте кегль в цикле, пока строка не влезет в отведённую ширину. - Не собирайте фразы из кусков («Уровень » + n + « пройден») — в другом языке порядок слов другой. Один ключ на одну фразу.
function fitText(ctx, str, maxWidth, maxSize) {
let size = maxSize;
do {
ctx.font = size + 'px sans-serif';
if (ctx.measureText(str).width <= maxWidth) break;
size -= 1;
} while (size > 10);
return size;
}Отдельно про шрифты: если вы подключаете кастомный шрифт, проверьте, что в нём есть кириллица и нужные диакритики. Битая кодировка и квадраты вместо букв — прямой пример нарушения из пояснений к пункту 1.15.
Осторожно
Текст, зашитый в картинку («ИГРАТЬ» на спрайте кнопки), локализовать невозможно без перерисовки. Если вы планируете больше одного языка, выносите весь интерфейсный текст в DOM или рисуйте его на canvas программно — это решение принимается один раз и в начале, а переделывается неделю.
Что ещё локализуется, кроме интерфейса
Часть текстов живёт не в вашем коде, а в Консоли разработчика, и про них забывают.
| Что | Где задаётся | Нюанс |
|---|---|---|
| Названия и описания товаров | Консоль → покупки, поля на русском и английском (title_ru, title_en, description_ru, description_en) | Описание — до 200 символов; в диалоге оплаты показывается именно оно |
| Отображаемое название лидерборда | Консоль → Лидерборды, локализованное название по языкам | В коде вы используете техническое название, игрок видит локализованное |
| Название игры, «Об игре», «Как играть» | Вкладка «Черновик», отдельный набор полей на каждый язык | Заполняется для каждого выбранного языка отдельно |
Витрину карточки можно не переводить руками: у платформы есть функция AI-описаний, которая делает переводы карточки на другие языки на основе английского. Но она переводит только карточку — все тексты внутри игры остаются на вас.
Поле «Игра переведена на» и как это проверяет модерация
В черновике есть обязательное поле Игра переведена на. Указывайте в нём ровно те языки, которые действительно поддержаны: модерация проверяет каждый заявленный язык и отклоняет игру, если хотя бы один не переключился полностью.
Методика проверки простая, и её стоит прогнать самому:
- Очистить кеш браузера.
- Открыть игру с debug-панелью — через Консоль (Открыть с debug-панелью) или добавив
debug-mode=16в адрес. - Посмотреть на индикатор
文: I18N is used — автоопределение подключено, I18N is not used — нет. - Нажать кнопку SDK mocks ⚒️, выбрать язык из выпадающего списка — игра откроется на нём в новой вкладке.
- Повторить для каждого языка из поля Игра переведена на.
Совет
Заявлять языки «на вырост» невыгодно вдвойне. Непереведённый язык — это отказ, а лишний язык в списке не даёт трафика сам по себе: видимость в других странах даёт заполненная карточка на этом языке. Начните с честного минимума и добавляйте языки обновлениями.
Какие языки подключать первыми
Документация называет минимальный рекомендуемый набор для максимального трафика: русский, турецкий, английский. Турецкий здесь не случайность — у платформы есть отдельный домен yandex.com.tr, и турецкая аудитория для Яндекс Игр большая.
Разумная последовательность для небольшой игры:
- Русский — основной рынок.
- Английский — покрывает всё, что не попадает в запасной набор
ru. - Турецкий — третий по объёму, требует минимум работы (латиница, тот же шрифт).
- Дальше — по метрикам: смотрите, из каких стран приходят игроки, и переводите под них.
Гарантий прироста аудитории от каждого нового языка никто не даёт — но пункт 2.14 обязателен независимо от того, сколько их у вас.
Что дальше
Локализация закрывает вопрос «понятно ли», а следующий большой блок — «комфортно ли». Игра может быть переведена идеально и всё равно получить отказ, если она подтормаживает на среднем телефоне: заметные фризы и просадки производительности стоят отдельной строкой в списке частых причин отказа. В следующей главе разберём бюджет кадра, профилирование в DevTools и типичные причины просадок в 2D-играх — с измерениями вместо догадок.