Не так давно я опубликовал подробную инструкцию по использованию LocoLaser — утилиты для локализации Android и iOS приложений в Google Sheets. Мне бы хотелось продолжить тему локализации и обратить больше внимания на iOS приложения. В отличии от Android, в iOS разработке есть ряд мелких но неприятных моментов, которые, в сумме, могут привести к совсем не мелким проблемам.
Сегодня я хочу уделить особое внимание Interface Builder-у. Все мы знаем, он не идеален. Но это единственное, что у нас есть и с этим приходится мириться. В этой статье я расскажу о главной проблеме, с которой вы можете столкнуться при локализации приложений в Interface Builder, а также расскажу как с ней можно справиться.
Суть проблемы
Когда вы переводите Storyboard или XIB файл, помимо основного файла с разметкой, создаются дополнительные файлы с ресурсными строками. Эти ресурсные файлы принято выгружать в специальные таблицы и отдавать переводчикам. Беда заключается в том, что ключи для строк, в этом файле, строятся на основе Object ID, которые генерируются автоматически и нет ни какой возможности на них повлиять. Если вас угораздило скопировать или вырезать а затем вставить какой либо View, Interface Builder сгенерирует новые идентификаторы и перевод будет потерян.
Получается, вы не можете закрепить перевод за какой-то конкретной View. Если вы ее переместите, перевод не переместится. Например, у вас на storyboard есть ViewController который вы решили хранить отдельно. Вы создаете новый XIB файл, перемещаете туда ViewController, но переводы не перемещаются. Мало того, для каждой View создастся новый идентификатор и вам придется подгонять идентификаторы строк вручную. Просто так скопировать переводы не получится.
Кроме того, отсутствие возможности повлиять на идентификаторы строк не позволяет иметь общую базу строк сразу на несколько платформ. Что, в свою очередь, приведет к невозможности использовать утилиты, генерирующие ресурсные файлы под разные платформы.
Каждый решает эту проблему как может. Помню, 2 года назад, на мобильной конференции я спрашивал одного ведущего iOS разработчика одной из ведущих фирм о том как они решают проблему локализации интерфейса. В то время я только начинал изучать iOS, но у меня уже был достаточно богатый опыт в области Android и мне было с чем сравнивать. Честно говоря, я был ошарашен ответом. Во ViewController через IBOutlet они получали ссылки на Label и прочие View и переводили их программно. В коде это выглядит приблизительно так:
class MainViewController: UIViewController {
@IBOutlet var labelToTranslate: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
self.labelToTranslate.text = NSLocalizedString("scr_main_txt_example", comment: "Some Example text")
...
}
...
}
В этом случае все строки находятся в одном файле Localizable.strings. Подобный метод в настоящий момент является наиболее распространенным и используется почти повсеместно. Признайтесь, получается не слишком элегантно. Мало того что вы засоряете ViewController лишним кодом, которому тут не место, так еще и не решаете проблему с копированием или перемещением View. Пора бы найти что-то получше.
Решение
И тут у меня есть кое-что, что я могу вам предложить. Дело в том, что в Interface Builder, в свойствах View, можно прописать так называемые «User Defined Runtime Attributes». Их то мы и будем использовать. Но для начала необходимо создать Extension для UILabel.
extension UILabel {
public var lzText : String? {
set {
if newValue != nil {
self.text = NSLocalizedString(newValue, comment: “”)
}
else {
self.text = nil
}
}
get {
return self.text
}
}
}
Теперь у всех UILabel появилось свойство lzText при изменении которого в свойство text записывается локализованная строка. Используем это свойство в Interface Builder.
- Выбираем UILabel и переходим на вкладку «Identity Inspector»;
- Жмем кнопку добавления атрибута в «User Defined Runtime Attributes»;
- Указываем ключ атрибута «lzText», тип: «String», значение: «scr_main_txt_example»
И это все. Больше не нужно засорять код ничем лишним. Вы можете не бояться, что перевод или ссылка на View потеряется при копировании или перемещении в другой контейнер. Атрибуты копируются вместе с View. Единственное, что останется неизмеренным — это хранение всех строк в одном файле Localized.strings.
UPD:
Но это не все. DjPhoeniX предложил сделать еще лучше. И нам не потребуется почти ничего менять. Нужно лишь добавить
@IBInspectable
перед объявлением свойства.extension UILabel {
@IBInspectable public var lzText : String? {
set {
if newValue != nil {
self.text = NSLocalizedString(newValue, comment: “”)
}
else {
self.text = nil
}
}
get {
return self.text
}
}
}
Теперь это свойство также доступно на вкладке «Attributes Inspector».
Для еще большего удобства я подготовил файл, в котором собрано достаточно большое количество расширений для часто используемых View, где у каждого текстового свойства есть двойник с приставкой «lz»(сокращение слова «localized»). Вы можете найти этот файл в примере по использованию LocoLaser: LocalizationExtensions.swift. Весь проект опубликован под лицензией Apache 2.0, можете смело копировать этот файл к себе и начинать использовать.
Помимо расширений для View, в LocalizationExtensions.swift добавлено расширение к классу String. Оно добавляет вычисляемое свойство localized, которое возвращает локализованную строку. Если перевод найти не получается, отправляется оповещение через NotificationCenter. Вы можете подписаться на эти оповещения и обрабатывать их как вам заблагорассудится. В Debug билде можно писать в лог или показывать уведомления, в Release билде отправлять репорт в систему аналитики.
В итоге, после применения вышеописанного метода, вся работа со строками остается в Interface Builder. Плюс ко всему вы получите дополнительный механизм по отлову «битых» строк.
На этом закончу. Спасибо за внимание. Пользуйтесь на здоровье!
Комментарии (17)
saniaxxx
07.04.2017 14:46Вместо «User Defined Runtime Attributes» можно использовать IBInspectable свойства
PapaBubaDiop
07.04.2017 15:42Все равно не обойтись без файлов
Localizable.strings
InfoPlist.strings
Поскольку UILabel зачастую содержит не статичный текст.KamiSempai
07.04.2017 16:25Да, именно так. Однако, если текст статичный, лучше не выносить его в код.
Естественно, для локализации, описанным в статье способом, строки должны храниться вLocalizable.strings
как и в случае с программной локализацией. Все это по тому, что локализация, фактически, осуществляется программно. Она лишь скрыта таким образом, что создается ощущение редактирования обычных свойств в Interface Builder.
svanichkin
07.04.2017 18:27а в чем проблема сделать нормальную локализацию storyboard? никакого гемора, никаких лишних файлов… или я что то не понял?
KamiSempai
07.04.2017 18:35Ответ на ваш вопрос находится в первом абзаце главы «Суть проблемы».
Если вкратце, то вы не можете закрепить перевод за какой-то конкретной View. Если вы ее переместите, перевод не переместится. Например, у вас на storyboard был ViewController и вы решили хранить его в отдельном XIB файле. Вы создаете отдельный XIB, перемещаете туда ViewController, но переводы не перемещаются. Мало того, для каждой View создастся новый идентификатор и вам придется подгонять идентификаторы строк вручную. Просто так скопировать переводы не получится.svanichkin
07.04.2017 18:39почмему не получится… боже мой… без проблем всё перенесется. ) Storyboard с переводом может быть в двух состояниях, первый как тексты, воторй как отображения. Для переноса достаточно перевести ВСЕ переводы в режим отображения. Дальше дело техники. И да, при таких широченных возможностях storyboard'a возвращаться назад к XIB, смысла вообще никакого нет. И тем более ради этого код усложнять…
KamiSempai
07.04.2017 18:55Перевод storyboard в режиме отображения несет в себе кучу проблем. Его нужно использовать только в крайних случаях, когда для какого либо языка нужно использовать немного иную разметку.
Мало того. В этом случае вам придется копировать каждый язык вручную. И быть внимательным, что бы не перепутать языки.svanichkin
07.04.2017 19:01«Перевод storyboard в режиме отображения несет в себе кучу проблем.» каких например? Всегда можно вернуть режим текста после того как выдран View и перенесен из storyboard в xib. Это не односторонний процесс а обратимый без каких либо последствий.
KamiSempai
07.04.2017 19:24«Перевод storyboard в режиме отображения несет в себе кучу проблем.» каких например?
Тем что у вас на каждый язык своя разметка и их всех нужно редактировать независимо друг от друга.svanichkin
07.04.2017 19:25Если у нас своя разметка, тогда нет смысла переводить в текст, вы это понимаете? Значит вообще никаких проблем с копированием и переносом в XIB не будет.
KamiSempai
07.04.2017 19:33Допустим у меня приложение переведено на 20 языков. Мой storyboard использует локализацию в разметке. Мне нужно подвинуть одну кнопочку на одном из ViewController. Мои действия?
svanichkin
07.04.2017 19:21Вообще надо так же сказать, что есть сторонние инструменты, для перевода всего Xcode проекта которые сами следят за айдишниками и генерят по основному файлу дополнительные с переводами. И да у Apple эта сторона проекта действительно далека от идеала. И таких мелких не идеальных вещей много и про них мало кто знает.
KamiSempai
07.04.2017 19:29Я понимаю к чему вы клоните. Однако, описанный вами способ требует использования специальных инструментов. Будет замечательно если вы приведете ссылки на эти инструменты. Думаю, все скажут вам спасибо.
svanichkin
07.04.2017 19:41Ссылки сочтут за рекламу тех или иных продуктов, они есть и легко гуглятся.
DjPhoeniX
На правах лайфхака — всё становится лучше с
@IBInspectable
KamiSempai
Хм. Как же я такое мог упустить! Спасибо за совет.