Обновление строк на лету в мобильных приложениях: часть 1

wvryqxbyer9hz4qyifajpaacruc.png

Введение


Свою статью я начну с признания: я немного завидую людям, чей родной язык — английский. В современном мире он стал языком интернационального общения, негласным стандартом. Практически любое популярное приложение поддерживает английский язык. Англоговорящие люди вряд ли когда-нибудь скачивали долгожданную игру из App Store и разочарованно понимали, что она не поддерживает их родной язык.

rjixxfsijepgkltilipy8jrwdeu.png

Экран выбора языка в Badoo

Игры и приложения приятнее и проще использовать на родном языке, а для некоторых людей он вообще единственно доступный. Именно поэтому компании, работающие на международном рынке, переводят свои приложения на распространённые языки.

Эта статья — для тех разработчиков, которые уже задумались о локализации своего приложения, сделали первые шаги в этом направлении и наступили на свои первые грабли. В сегодняшней статье я коротко опишу подходы к локализации приложений, остановлюсь подробнее на локализации на стороне клиента, расскажу о недостатках этого подхода и предложу способ их обойти. А в следующей части мои коллеги подробнее расскажут о деталях имплементации нашего подхода в мобильных ОС.

О локализации веб-приложений мы уже писали здесь.

Способы локализации


Мобильные приложения с точки зрения подхода к хранению строк, подлежащих переводу, можно разделить на три группы:

  • все строки приходят с сервера;
  • все строки хранятся в самом приложении;
  • часть строк хранится в приложении, часть — приходит с сервера.


Приложения, которые мы разрабатываем в Badoo, относятся к третьей группе, но подходы, о которых пойдёт речь в статье, можно успешно применить и ко второй при условии, что у приложения всё же есть серверная часть. Поэтому статью я посвящу именно таким приложениям.

Популярные мобильные ОС имеют встроенные средства для локализации приложений. Например, в Android можно создать по ресурс-файлу со строками для каждого из поддерживаемых языков, и ОС автоматически будет выбирать значения для строк сначала из файла, соответствующего текущей локали, и только если там такого значения нет (или нет самого файла), будет откатываться к файлу по умолчанию:

res/values-en-rGB/strings.xml → res/values-en/strings.xml → res/values/strings.xml


️В дальнейшем в статье я буду иллюстрировать сказанное примерами для Android, хотя похожая схема используется и в iOS.

Этот способ используется во множестве приложений за счёт своей простоты и удобства. Но то, что удобно разработчикам, не всегда удобно копирайтерам и переводчикам. Сложно представить переводчика, который будет коммитить изменения переводов прямо в Git. Поэтому многие компании (включая, конечно, и нашу) изобретают свои велосипеды системы для упрощения жизни переводчикам.

Продвинутая локализация


md7tuulmyk7xeylujq0co7bp6pu.png
Скриншот из нашей системы управления переводами

Давайте подумаем, чего мы хотели бы от подобной системы.

  • Разработчики могут удобным для них способом добавлять новые строки в систему в процессе разработки фичи.
    Тут возможны два подхода: либо разработчик добавляет строки в ресурс-файл (то есть прямо в исходный код), а система автоматически подтягивает этот файл к себе и добавляет созданные строки в свою БД, либо наоборот: разработчик добавляет строки в БД системы через удобный интерфейс, а система затем генерирует ресурс-файлы для приложения и добавляет их в исходный код.aakp8mtbkh9qy3fg2r3aw482ah4.png
  • Переводчики в системе видят добавленные строки и могут их переводить на различные языки.
    Желательно, чтобы переводчики понимали контекст, в котором используется фраза. Оптимальный вариант — скриншот экрана приложения, на котором эта фраза встречается.
  • Система умеет работать с подстановками, а также со строками, зависящими от пола и чисел («У вас 1 друг» vs «У вас 15 друзей»).
  • Переведённые строки попадают в приложение в процессе сборки.
    Из готовых переводов автоматически генерируются ресурс-файлы со строками, по одному на каждый язык (res/values-en-rXX/strings.xml), а также ресурсный файл по умолчанию (res/values/strings.xml) — в нём находятся строки, не переведённые ни на какие языки, в том виде, в котором их добавил разработчик.
  • К моменту релиза фичи уже готовы переводы как минимум на основные языки.
    Это достигается за счёт распараллеливания работы переводчиков и разработчиков. Дополнительные переводы могут быть добавлены в следующих релизах.

Более подробно о нашей системе переводов можно прочитать здесь и здесь.

Жизнь после релиза


У строк, включённых в код приложения, есть существенный минус: чтобы их обновить, приходится релизить новую версию приложения. Учитывая, что иногда процесс одобрения новой версии может занимать неделю (привет, Apple!), это не всегда подходящий вариант.
Поэтому продуктовой команде нужен способ изменения текста в приложениях, не дожидаясь релиза. Например, перед Днём святого Валентина мы меняли текст в странах, празднующих этот день, с «Looking for: dates» на «Looking for: Valentine«s dates».

К тому же есть много других ситуаций, когда может понадобиться экстренное обновление переводов. Бывает, что переведённая фраза получается слишком длинной и не помещается в отведённое ей место, из-за чего интерфейс разъезжается или фраза обрезается. Иногда случается, что переводчики неправильно понимают контекст фразы, и переведённая фраза выглядит неуместно. Или, скажем, в какой-то стране выходит новый закон, регулирующий платежи, и приходится менять текст на странице оплаты услуг.

1wjkrztige8qybdpetxjpz_mzhi.pngНу и без курьёзов не обходится: в одном из наших партнёрских проектов в процессе редизайна сильно увеличили размер шрифта заголовка для окна уведомления о новом контакте, но не успели обновить перевод. Французское слово Connexion (Connection) над изображением нового контакта обрезалось до Con. Желающие могут поискать перевод и представить, какова была реакция французских пользователей.

Безусловно, такого рода накладки требуют немедленного вмешательства, и ждать неделю до выпуска релиза нельзя.

Приложения, получающие все строки с сервера вместо использования локальных ресурсов, такой проблемы не имеют, но далеко не всегда есть смысл при каждом использования приложения гонять через Сеть килобайты текста, особенно если этот текст редко меняется. К тому же есть приложения, которые должны уметь работать и без подключения к Сети (к таким приложениям относятся многие игры, например).

«Горячее» обновление переводов


Конечно, в голову тут же приходит идея объединить подходы: использовать локальные переводы, хранящиеся в самом приложении, но уметь обновлять их в фоновом режиме, когда есть подключение к Интернету.

Если у вас уже есть система переводов, похожая на ту, что я описал выше, то с небольшими доработками она может решить и эту задачу. Вот какие изменения потребуются:

  • Состояние переводов в любой момент времени должно описываться некой версией.
    Каждое изменение переводов должно повышать версию (добавили к версии 25 новую строку — считаем, что теперь у нас версия 26; перевели строку на португальский язык — повышаем версию до 27; и так далее). Это похоже на поведение системы контроля версий: изменение переводов — это коммит. На практике можно построить систему переводов поверх системы контроля версий и получить описанную функциональность «из коробки».
  • Возможность помечать версии как «готовые к выпуску в продакшн».
    Это не обязательное условие, но очень полезное. Например, представьте, что мы решили в игре переименовать героиню: из Анны сделать Елену. Очевидно, что требуется изменить все строки, где встречается имя, и только после этого отдавать новую версию перевода в релиз, иначе может получиться, что какое-то приложение получит неконсистентную версию перевода, где в половине сюжета встречается Анна, а в другой половине — Елена. Этого можно избежать, если вносить изменения переводов в общую базу не атомарно, а крупными кусками, но тогда возникает проблема конфликтов изменений, если переводчиков несколько (а так обычно и бывает). С другой стороны, система контроля версий может пригодиться и здесь.
  • Умение системы генерировать дифф между двумя версиями переводов.
    Диффы могут быть большими, и их генерация может занимать много времени, поэтому было бы неплохо сделать очередь генерации диффов и где-то хранить готовые диффы вместо того, чтобы каждый раз считать их на лету.


Мобильное приложение должно знать версию переводов, которой оно в данный момент обладает, и уметь передавать эту версию на сервер в тот момент, когда у него есть активное подключение.

AppStartup {
    translation_version: , // текущая версия лексем на клиенте
    ... // другие параметры, необходимые на старте приложения
}


Получив версию переводов от мобильного приложения, сервер проверяет, доступна ли новая версия. Если новая версия доступна, сервер проверяет, есть ли готовый дифф между этими двумя версиями. Если его нет, то создаётся задача в очереди генерации диффов (если такой задачи ещё в очереди нет, конечно).

В момент, когда дифф готов, сервер уведомляет приложение о том, что доступна новая версия переводов. Теперь приложение может запросить дифф и сохранить его локально.

y2pnhk2hh0podz-xi50k1dosajs.png
Ниже — пример того, как может выглядеть ответ сервера. Он включает в себя все строки (мы исторически называем их «лексемы», хоть это и не совсем верное использование термина), изменённые с той версии, которая в данный момент есть у приложения.

Lexemes {
    server_version: , // текущая версия лексем
    lexemes: [
        Lexeme { // простая лексама
            key: "badoocredits.profile.button.topup",
            mode: LEXEME_MODE_SIMPLE,
            value: LexemeValue {
                text: "Пополнить баланс",
            }
        },
        Lexeme { // лексема, зависящая от числа
            key: "cmd.deleteselected",
            mode: LEXEME_MODE_NUM_DEPENDANT,
            value: LexemeValue {
                plural_forms: [
                    PluralForm {
                        category: PLURAL_CATEGORY_ZERO,
                        text: "Выберите записи для удаления",
                    },
                    PluralForm {
                        category: PLURAL_CATEGORY_ONE,
                        text: "Удалить %d запись",
                    },
                    PluralForm {
                        category: PLURAL_CATEGORY_TWO,
                        text: "Удалить %d записи",
                    },
                    PluralForm {
                        category: PLURAL_CATEGORY_MANY,
                        text: "Удалить %d записей",
                    },
                ]
            }
        }
    ]
}


Как видно, фразы могут быть простыми, а могут зависеть от числа. Во фразах, зависящих от числа, могут использоваться подстановки. Для зависящих от числа строк мы используем шесть форм: zero, one, two, few, many, other.

Вот как, например, выглядит фраза «N собак, N кошек» на валлийском языке (Уэльс, Великобритания):
n0pktsnh6ityti8wdmj4nopa7p0.png
Как видите, используются все шесть форм. Подробнее об этом можно почитать здесь.

A/B-тестирование


Система обновления переводов на лету имеет ещё одно неоспоримое преимущество. Помимо исправления ошибок перевода и решения других проблем с локализацией, мы получаем возможность проводить A/B-тестирование различных вариантов переводов.

Lexemes {
    server_version: , // текущая версия лексем
    lexemes: [
        Lexeme { // лексема с А/Б вариантами
            key: "popularity.share.title",
            mode: LEXEME_MODE_SIMPLE,
            value: LexemeValue { // вариант по-умолчанию
                text: "Ты популярна!",
            },
            test_id: "popularity_test_android", // серверный id теста
            variations: [
                LexemeVariation {
                    variation_id: "control",
                    value: LexemeValue {
                        text: "Ты популярна!",
                    }
                },
                LexemeVariation {
                    variation_id: "rock",
                    value: LexemeValue {
                        text: "Ты классная!",
                    }
                },
                LexemeVariation {
                    variation_id: "cool",
                    value: LexemeValue {
                        text: "Ты крутая!",
                    }
                },
            ]
        }
    ]
}


В приведённом выше примере есть фраза «Ты популярна!», которая может использоваться, например, как текст в нотификации. Для того чтобы увеличить click rate этой нотификации, наша продуктовая команда решила провести A/B-тест и посмотреть, как изменившийся текст повлияет на процент кликов по нотификации. Без системы обновления на лету пришлось бы ставить задачу разработчикам приложения, затем релизить новую версию (ещё неделя ожидания) и только потом тестировать. С новой системой аналитики могут провести тест своими силами, проверить результат и во всех приложениях заменить текст на выигравший вариант.

Заключение


Ещё раз вкратце: если вы храните переводы в самом приложении, вы теряете возможность оперативно их изменять. Мы предлагаем идею обновления переводов в фоновом режиме, которая позволяет быстро исправлять ошибки переводов и тестировать различные варианты фраз.
Если наш подход вас заинтересовал и вы уже задумываетесь о том, как реализовать его в вашем приложении на iOS или Android, то вторая часть статьи, которую мы опубликуем в ближайшее время, — для вас. В ней мы расскажем, как изменить ваше мобильное приложение так, чтобы оно могло использовать полученный дифф, но при этом избежать трудозатратного и долгого переписывания всех мест в коде, где используются переводы.

© Habrahabr.ru