Двенадцатого октября я закончил заливать карточку дневника Lanternly на 39 языков. Если посчитать поля, получится 819: название, подзаголовок и ссылка на политику в «Информации о приложении», шесть полей версии iOS, пять полей версии macOS и локализации подписки — группа и три плана. Команды переводчиков у меня нет, ASO-агентства тоже.
Это рассказ о том, как я это собирал и где спотыкался. Цифры лимитов сверены с документацией App Store Connect 13 октября 2026 года.
Сначала решил, о чём нельзя врать#
Интерфейс Lanternly есть только на английском и русском. Карточка на шведском или турецком создаёт ожидание, что приложение тоже будет на шведском или турецком. Человек скачает, увидит английский и поставит одну звезду или попросит вернуть деньги.
Поэтому во всех языках, кроме английского и русского, вторая строка описания одна и та же по смыслу:
Die App-Oberfläche ist auf Englisch und Russisch.
Appens gränssnitt är på engelska och ryska.Она стоит сразу после первого абзаца, до списка возможностей. Ниже в тексте её никто бы не нашёл.
Второе решение касалось объёма. 14 языков получили полное описание на 3–4 тысячи символов, ещё 25 короткое, около 1300 символов: суть приложения, честная строка про интерфейс, блок о подписке и юридические ссылки. 11 индийских языков я не стал заливать вовсе. Проверить качество перевода на хинди или тамильский мне не на чем, а плохой перевод в карточке хуже английского оригинала.
Один файл вместо 819 полей формы#
Править 39 языков прямо в форме App Store Connect нельзя: поправил формулировку подписки в английском, и надо вспомнить, в каких ещё 38 местах она стоит.
Источник истины переехал в один JSON: локаль → поля. Описания для iOS и macOS отличаются одним последним абзацем (у Mac свой список «где ещё работает»), поэтому хранится общий текст и подмена для Mac. Любая правка сначала идёт в файл, проходит проверку и только потом уходит в магазин. Рядом лежит TSV с названиями, подзаголовками и ключами: его удобно смотреть глазами и проверять скриптом.
Лимиты, в которые всё упирается#
| Поле | Лимит | Где задаётся |
|---|---|---|
| Name | 30 символов | Информация о приложении, общее для iOS и macOS |
| Subtitle | 30 символов | Там же |
| Keywords | 100 байт по документации | Версия, у каждой платформы своё |
| Promotional Text | 170 символов | Версия, меняется без проверки |
| Description | 4000 символов | Версия |
| What's New | 4000 символов | Версия |
Про ключевые слова документация пишет так: «You can provide up to 100 bytes of content». Байты, не символы. В UTF-8 кириллица занимает два байта на букву, японский — три. Мои русские ключи — 93 символа, а это 176 байт, и App Store Connect принял их без вопросов. На практике считаются символы, но рассчитывать на то, что это не поменяется, я бы не стал.
Самые тесные места оказались не там, где я ждал. Название и подзаголовок по 30 символов уложились везде: немецкие сложные слова длинные, но их мало. Упёрся я в другое:
- Немецкий промотекст вышел ровно 170 из 170. Добавить хоть одну запятую уже нельзя.
- Нидерландский промотекст пришлось сокращать: в первой версии не влез.
- Французское описание не влезало в 4000. Спасло то, что ссылки на политику и условия встречались дважды: в тексте и в обязательном блоке внизу. Убрал повтор — получилось 3917.
Скрипт, который проверяет то, чего не видит форма#
App Store Connect проверяет только длину. Остальное он молча принимает, и ошибку вы увидите разве что по отсутствию показов. Поэтому перед каждой заливкой я гоняю небольшой скрипт по TSV. Вот его упрощённая рабочая версия:
#!/usr/bin/env python3
"""Проверка name/subtitle/keywords по локалям: python3 check_fields.py fields.tsv"""
import csv, re, sys
LIMITS = {"name": 30, "subtitle": 30, "keywords": 100}
BANNED = {"app", "free", "apple", "iphone"}
def words(s):
return re.findall(r"[\w’'-]+", s.lower())
bad = 0
for r in csv.DictReader(open(sys.argv[1], encoding="utf-8"), delimiter="\t"):
issues = [f"{f}: {len(r[f])}/{n}" for f, n in LIMITS.items() if len(r[f]) > n]
kw = r["keywords"]
if re.search(r",\s", kw):
issues.append("пробел после запятой")
kw_words = [w for t in kw.split(",") for w in words(t)]
dup = {w for w in kw_words if kw_words.count(w) > 1}
if dup:
issues.append(f"повтор в keywords: {sorted(dup)}")
title = set(words(r["name"] + " " + r["subtitle"]))
if set(kw_words) & title:
issues.append(f"повтор с name/subtitle: {sorted(set(kw_words) & title)}")
if (set(kw_words) | title) & BANNED:
issues.append(f"лишние слова: {sorted((set(kw_words) | title) & BANNED)}")
bad += bool(issues)
print(("FAIL " if issues else "OK ") + r["locale"], *issues, sep="\n - ")
sys.exit(1 if bad else 0)На вход идёт TSV с колонками locale, name, subtitle, keywords. Что он ловит и почему:
- Пробел после запятой. Это просто потерянный символ из ста. Apple разделяет ключи запятыми, пробел ей не нужен.
- Повтор слова внутри ключей и с названием или подзаголовком. Слова из названия и подзаголовка уже участвуют в поиске, повторять их в ключах значит тратить место дважды.
- Слова, которые не нужны.
appиfreeничего не добавляют, а имена чужих приложений и компаний документация запрещает прямо. Полный список у меня длиннее: туда же идут слова, которые я не использую в продукте вообще, например медицинские ярлыки.
Длины описаний и промотекстов проверяет второй скрипт, а результат я сохраняю в файл рядом с JSON. Через месяц, при следующей правке, сразу видно, у какого языка нет запаса.
Как переводил#
Черновики переводов я делал с языковой моделью. Носители их не вычитывали. Скрывать это бессмысленно: это ровно то, что может себе позволить разработчик-одиночка. Поэтому я держал себя в нескольких правилах.
Переводить смысл, а не фразы. Английский промотекст «Free to write as much as you like» в немецком стал «Schreib, so viel du willst»: слово «бесплатно» там не уместилось бы в лимит, а смысл остался.
Ключи писать так, как их вводят. По-русски ищут «дневник эмоций» и «дневник с паролем», поэтому в ключах стоят эмоций и паролем в родительном и творительном падежах. Немцы ищут сложными словами, и dankbarkeitstagebuch идёт одним словом целиком. В польском слово dziennik чаще значит школьный электронный журнал, поэтому основное слово в названии — Pamiętnik.
Короткие описания для языков, где проверить нечем. Чем короче текст, тем меньше в нём мест для странной фразы, которую я не замечу.
Ссылки, которые тоже надо локализовать#
Локализовать приходится и ссылки. У каждой локали свой Privacy Policy URL, Support URL и Marketing URL, а в конце каждого описания стоит блок с юридическими ссылками. Документы на сайте есть на пяти языках, поэтому испанские локали ведут на /es/, японская на /ja/, китайские на /zh-CN/, остальные на /en/. Поддержка есть на английском и русском. Про сам блок EULA и почему он обязателен для подписки я написал отдельно.
Загрузка: только скриптом#
Вбивать 819 полей руками значит потратить день и наверняка где-то опечататься. Я заливал скриптом из своей сессии App Store Connect. Если вы делаете это регулярно, правильный путь — официальный App Store Connect API: у него есть ресурсы appInfoLocalizations, appStoreVersionLocalizations, subscriptionGroupLocalizations и subscriptionLocalizations, то есть всё, что я перечислил выше.
Две вещи, на которые я наткнулся по дороге:
- У первой Mac-версии нет поля What's New. Версия macOS 1.1 у Lanternly первая, поля для неё просто нет, и скрипт должен его пропускать.
- Название и подзаголовок общие для iOS и macOS, а ключи и описание у каждой платформы свои. Я копирую их один в один, меняется только последний абзац описания.
Что осталось несделанным#
Скриншоты во всех новых языках пока английские: интерфейс всё равно английский или русский, так что это честно, но не идеально. Тексты на 37 языках никто из носителей не читал. Если кто-то из читателей говорит по-шведски, по-турецки или по-польски и найдёт в карточке Lanternly ошибку, я буду рад письму.
Если бы я начинал заново, я бы с первого дня завёл один файл-источник и скрипт проверки, ещё до первого перевода. Остальное в этой работе повторяется из раза в раз, и его можно доверить скрипту.



