Сюжет этой истории развивался, когда мне довелось реализовывать одну из продуктовых задач нашей компании Jivo для платформы iOS. Но начну, пожалуй, с небольшого вступления.
Локализация – одна из часто обсуждаемых тем в мобильной разработке.
В основном по этой теме в отношении платформы iOS затрагиваются следующие аспекты:
сервисы для упрощения организации и синхронизации переводов;
best-practice по переводам xib файлов;
вспомогательные compile-time надстройки для верификации переводов.
Однако, наша история не об этом. Для синхронизации переводов у нас в компании успешно интегрирован сторонний сервис, вместо xib файлов мы отдаём предпочтение коду, а compile-time надстройки пока не используем (впрочем, есть мысли по внедрению).
Наша история – о том, как однажды нам довелось столкнуться с задачей, в рамках которой понадобилось практически жонглировать переводами одной и той же по смыслу фразы, которая слегка видоизменялась в зависимости от контекста.
С чего всё началось
В нашем продукте появился функционал по установке напоминаний. Напоминания могут быть полезны в случае, если оператор пожелает вернуться к клиенту несколько позже. Например, уточнить, не возникло ли дополнительных вопросов спустя некоторое время после оказания консультации, что помогает повысить лояльность. Напоминание на определённое время можно поставить для себя самого или для другого оператора, а также опционально указать текстовый комментарий (описание), если таковой нужен.
Разумеется, факт установки такого напоминания дублируется в ленте диалога системным сообщением. И как раз здесь-то и обнаружилось затруднение: в зависимости от конфигурации напоминания, информационная надпись может выглядеть совершенно по-разному. Например (из английской версии интерфейса):
?? You created the reminder for agent Alex on 08/29/20 at 4:30 PM
?? Agent Nick created the reminder "Ask about any issue happened since our call" on 10/03/20 at 11:30 AM
?? Agent Alex completed the reminder on 12/05/20 at 5:00 PM
Как можно заметить, конечный вид надписи зависит одновременно от нескольких факторов:
?? Кто произвёл действие – мы сами или наш коллега
?? Был ли указан комментарий
?? Автор произвёл действие для себя самого или для другого оператора
?? Которое было произведено действие – создание, изменение, отмена или завершение
По не особо сложным подсчётам получается, что из этого обилия может образоваться до 32 вариантов конечной надписи. Разумеется, если учитывать, что наше приложение переведено на шесть языков, то три десятка вариантов легко превращаются примерно в две сотни переводов. И потому несложно догадаться, что возникло стойкое желание сократить количество трудозатрат для жонглирования всеми этими переводами.
Какие рассматривались варианты
Итак, по уже озвученным причинам подход «в лоб» пришлось отсечь практически сразу, ибо довольно непросто манипулировать тремя десятками ключей и двумя сотнями переводов:
// either...
let caption = format(
"REMINDER_CREATE_SELF_FOR_SELF", // !!!
reminder.time)
// or...
let caption = format(
"REMINDER_CREATE_ANOTHER_WITH_COMMENT_FOR_SELF", // !!!
reminder.author.name,
reminder.comment,
reminder.time)
// etc...
Еще один из распространённых вариантов заключается в разбиении фразы на составные части. С одной стороны, в этом случае количество необходимых переводов сокращается раза в три, но с другой стороны это всё ещё весьма приличное количество, и к тому же добавляются дополнительные сложности. Например, необходимость помнить или вести учёт, какие вообще существуют составные части и в каком порядке они друг с другом взаимодействуют. Плюс, неизбежная привязка к коду:
if reminder.author.isMe {
slices += [format("REMINDER_AUTHOR_SELF")]
}
else {
slices += [format("REMINDER_AUTHOR_ANOTHER", reminder.author.name)]
}
if let comment = reminder.comment {
slices += [format("REMINDER_COMMENT", comment)]
}
if reminder.target.isMe {
slices += [format("REMINDER_FOR_SELF")]
}
else {
slices += [format("REMINDER_FOR_ANOTHER", reminder.target.name)]
}
slices += [format("REMINDER_TIME", reminder.time)]
let caption = slices.joined()
После некоторых размышлений стало понятно, что основная трудность нашего случая заключается лишь в обеспечении отображения тех или иных участков надписи в зависимости от какого-то условия. И чаще всего таким условием является непосредственно наличие либо отсутствие нужной информации для отображения.
Выход найден
В результате возникла идея оформить достаточно тривиальный язык разметки. Как bb-коды применяются для базового форматирования в интернете, так и предполагаемый язык должен был помочь оформлять разметку для переводов. И в итоге удалось составить сравнительно простую разметку, которая теперь с одной стороны помогает использовать тривиальную логику в обработке переводов, а с другой стороны не особо сложна для понимания людьми, не свянными с программированием. Между собой мы называем эту разметку «формулой переводов».
Основное описание и принцип действия можно найти в репозитории проекта (ссылка в конце статьи). Но если вкратце, то в формулах основными контролирующими элементами являются переменные, блоки и алиасы.
Переменная – символьное наименование той или иной надписи, например, $creatorName
, которая в итоге заменяется на соответствующую надпись, если таковая была назначена для переменной.
Блок – список отдельных фраз через специальный разделитель внутри управляющих скобок, например, $[Agent $creatorName ## You]
; в данном случае внутри блока есть две отдельные фразы - Agent $creatorName
и You
, разделённые символами ##
; при сканировании блока алгоритм будет проходить по очереди через эти две фразы, и в итоговый результат попадет лишь первая из фраз; та которая окажется корректной. Для того, чтобы фраза считалась корректной, все используемые в ней переменные (если таковые использованы) должны быть назначены; в ином случае алгоритм пропускает фразу и переходит к следующей.
Алиас – виртуальное имя фразы, обрамлённое в двоеточия, например, $[Agent $creatorName ## :another: Another agent ## You]
; в данном случае внутри блока теперь есть три фразы - Agent $creatorName
, Another agent
и You
; но для фразы Another agent
мы назначили алиас another
, что означает, что алгоритм будет учитывать эту фразу только в том случае, если предварительно мы активируем соответствующий ей алиас (позволим фразе принимать участие в парсинге).
Посмотрим на примере уже обозначенной ранее ситуации. Для события о создании напоминания у нас может быть несколько вариаций, вот некоторые из них:
?? Agent Nick created the reminder on 10/03/20 at 11:30 AM
?? You created the reminder "Ask about any issue happened since our call" on 10/03/20 at 11:30 AM
?? Agent Nick created the reminder "Ask about any issue happened since our call" on 10/03/20 at 11:30 AM
?? You created the reminder for Alex on 10/03/20 at 11:30 AM
Наша действующая формула для данного случая выглядит так:
$[Agent $creatorName ## You] created the reminder $["$comment"] $[for $targetName] on $date at $time
И теперь давайте рассмотрим, каких результатов можно достичь с её помощью. Абсолютно все возможные варианты перечислять не станем, но остановимся на тех четырёх, что представлены выше. Формула во всех этих примерах одна и та же, различается только установка переменных (сами примеры на языке Swift, но решение реализовано на C++, поэтому доступно и для многих других языков).
Другой оператор создал напоминание без комментария:
let parser = PureParser()
let formula = "$[Agent $creatorName ## You] created the reminder $[\"$comment\"] $[for $targetName] on $date at $time"
parser.assign(variable: "creatorName", value: "Nick")
parser.assign(variable: "date", value: "10/03/20")
parser.assign(variable: "time", value: "11:30 AM")
let result = parser.execute(formula, collapseSpaces: true, resetOnFinish: true)
print(result)
// Agent Nick created the reminder on 10/03/20 at 11:30 AM
Текущий оператор создал напоминание с комментарием:
let parser = PureParser()
let formula = "$[Agent $creatorName ## You] created the reminder $[\"$comment\"] $[for $targetName] on $date at $time"
parser.assign(variable: "comment", value: "Ask about any issue happened since our call")
parser.assign(variable: "date", value: "10/03/20")
parser.assign(variable: "time", value: "11:30 AM")
let result = parser.execute(formula, collapseSpaces: true, resetOnFinish: true)
print(result)
// You created the reminder "Ask about any issue happened since our call" on 10/03/20 at 11:30 AM
Другой оператор создал напоминание с комментарием:
let parser = PureParser()
let formula = "$[Agent $creatorName ## You] created the reminder $[\"$comment\"] $[for $targetName] on $date at $time"
parser.assign(variable: "creatorName", value: "Nick")
parser.assign(variable: "comment", value: "Ask about any issue happened since our call")
parser.assign(variable: "date", value: "10/03/20")parser.assign(variable: "time", value: "11:30 AM")
let result = parser.execute(formula, collapseSpaces: true, resetOnFinish: true)
print(result)
// Agent Nick created the reminder "Ask about any issue happened since our call" on 10/03/20 at 11:30 AM
Текущий оператор создал напоминание без комментария для другого оператора:
let parser = PureParser()
let formula = "$[Agent $creatorName ## You] created the reminder $[\"$comment\"] $[for $targetName] on $date at $time"
parser.assign(variable: "targetName", value: "Alex")
parser.assign(variable: "date", value: "10/03/20")
parser.assign(variable: "time", value: "11:30 AM")
let result = parser.execute(formula, collapseSpaces: true, resetOnFinish: true)
print(result)
// You created the reminder for Alex on 10/03/20 at 11:30 AM
Бонус: добавим алиас в формулу?
let parser = PureParser()
let formula = "$[Agent $creatorName ## :another: Another agent ## You] created the reminder $[\"$comment\"] $[for $targetName] on $date at $time"
parser.activate(alias: "another", true)
parser.assign(variable: "date", value: "10/03/20")
parser.assign(variable: "time", value: "11:30 AM")
let result = parser.execute(formula, collapseSpaces: true, resetOnFinish: true)
print(result)
// Another agent created the reminder on 10/03/20 at 11:30 AM
Профит
В итоге для каждого отдельного события (напоминание создано, завершено, отменено) формула подобного рода обеспечивает в нашем случае покрытие восьми различных случаев, и при этом остаётся сравнительно читабельной даже для менеджера. Такая же схема используется и ещё в нескольких местах помимо напоминаний.
Где пощупать
Библиотека написана на C++, а также есть обёртка на C и Swift.
Для Swift предусмотрено подключение через CocoaPods и Swift Package Manager.
Gargo
не совсем понятно. Вам не хватило возможностей форматирования строк в Swift, поэтому для парсинга вы выбрали формат из PHP?
bronenos Автор
Тут вопрос не только в форматировании…
Вернее, про форматирование речи даже и не было.
Упор был на одновременный учёт следующих аспектов:
— если нет искомой информации для той или иной части фразы, то заменять её дефолтной надписью (или надписями, в зависимости от контекста);
— в разных языках разный порядок слов, то есть нужна адаптивность;
— итоговый способ разметки или записи держать по возможности в одном месте для простоты редактирования.
Или какие возможности форматирования в Swift вы подразумеваете?