
Меня зовут Даниил Храповицкий, я iOS‑разработчик студии СleverPumpkin, и сегодня поговорим про xcstrings
в Xcode 15.
Один из самых неприятных аспектов iOS‑разработки — это локализация и плюрализация строк. Мало того, что они разбиты на разные файлы: strings
и stringsdict
, так ещё и работа с этими файлами для начинающего разработчика может оказаться не сильно очевидной. «Что такое %#@VARIABLE@
?», «Как добавлять несколько плюралок в одну строку?», «Как использовать плюралки в локализованных строках?», «Как добавлять разные переводы для разных девайсов?» — Все эти вопросы рано или поздно возникают у разработчика. После получения ответов на них каждый задаётся вопросом: «А почему всё так плохо?»
У зелёных наших братьев дела обстоят получше — у них один файл для локализации. У нас же всё так непросто, потому что формат данных достался нам из Objective‑C, где какая‑то строгость и защита от дураков были не совсем обязательны (вы вообще видели Objective‑C?). Поэтому раньше в файлах локализации можно было:
спокойно пропустить точку с запятой, о месте в коде которой Xcode стыдливо умолчит;
упустить тот момент, что название
NSStringLocalizedFormatKey
должно совпадать с ключом строчки, для которой мы предоставляем плюрализацию (когда об этом знаешь, то забыть сложно, но на первых порах все делали такие ошибки);вообще забыть добавить перевод для одного из ключей, после чего в приложении будет отображаться ключ вместо нормальной строки.
Однако всё изменилось с приходом Xcode 15, где локализация и плюрализация строк были значительно улучшены. Теперь там один файл xcstrings
. Это каталог, который хранит в себе все ключи и строки как для переводов на другие языки, так и для переводов для множественного числа. Важно отметить, что всё это умеет бэкпортиться на старые версии iOS путём разбиения xcstrings
на .strings и .stringsdict. То есть всё равно под капотом используется старый формат, но мы, как разработчики, работаем уже с удобным для нас интерфейсом.
При создании нового xcstrings
‑каталога интерфейс будет выглядеть следующим образом:

Если мы выделим язык, то нам отобразится, что каталог пустой:

Попробуем добавить строку и посмотрим, что произойдёт. Создалась строка, для которой мы можем задать ключ, локализованную строку и коммент. Состояние будет автоматически меняться для отображения информации о состоянии перевода и актуальности данной записи:

Изменим строку:

Теперь добавим перевод на русский язык:

Будет отображено, что имеется одна новая строка:

Переведём её. После добавления перевода состояние изменится на ОК.

Добавим возможность передавать туда числа. Делается это следующим образом:

Однако теперь нам необходимо добавить плюрализацию для строки, чтобы она выглядела естественно. Делается это следующим образом:

Состояние поменялось на Needs review. Это говорит о том, что строка требует нашего внимания.
После локализации двух переменных получается следующее:

Вернувшись к русскому языку, мы увидим, что строки необходимо обновить.

Обновив, мы получим следующее:

Так происходит локализация и плюрализация. В коде это можно вызвать либо через старый‑добрый NSLocalizedString
:
String(format: NSLocalizedString("File instead of Files", comment: ""), 1, 2)
Либо через новомодную структуру LocalizedStringResource
, но для этого необходимо немного изменить ключ — добавить в него спецификаторы форматов аргументов (%d
, %lld
, %@
и так далее), которые используются в строке.

Однако если мы вызовем строку этим способом, то всё будет работать не так, как мы ожидали:
String(localized: "\(2) File instead of \(1) Files")
// 2 File instead of 1 Files
А всё потому, что %d
в локализованной строке — это Int16
. И как бы Swift ни умел в инициализацию через литерал, он не может знать, что, указывая 2
или 1
, мы имеем в виду Int16
, а не Int
. Поэтому он не может замэтчить переданную строку с ключом.
После этого в дело вступает ещё один нюанс работы с каталогом строк. По умолчанию для каждой строки, которая не была сопоставлена с существующим ключом, Xcode создаст новый ключ. Работает это как для новой String(localized:)
, так и для уже повидавшей жизнь NSLocalizedString
. Это может быть удобно, если не хочется самому напрямую работать с xcstrings
файлом, создавая новые ключи и прописывая в них аргументы (хотя писать локализацию строк всё равно придётся):

Однако в случае с %d
, если же мы явно укажем тип, тогда всё отработает как ожидается:
String(localized: "\(Int16(2)) File instead of \(Int16(1)) Files")
// 2 Files instead of 1 File
Чтобы этого избежать, нужно указывать тип %lld
, который будет нормально парситься в Int
.
В дополнение есть возможность создания строк перевода для конкретного типа девайса. Например, можно задать различные переводы для iPhone и остальных устройств:



Также из интересного стоит отметить, что теперь файлы вместо привычного xml
имеют json
формат, что очень неожиданно для Apple. А так как файл теперь один, то такие сервисы, как POEditor, смогут нормально генерировать строки для iOS‑проектов.
Вся эта прелесть поддерживается на всех версиях iOS, но, само собой, собранных с использованием Xcode 15. Магия в том, что при компиляции всё парсится в уже привычные strings
и stringsdict
файлы, и в бандле будут храниться именно они.
В заключение хочется сказать, что Apple делает правильные шаги в плане локализации. Начиная с Xcode 15, переводить строки стало намного удобнее, проще и нагляднее без дублирования на несколько файлов.
Chris_moler
Спасибо за статью!
Жаль конечно что не завезли кодогенерацию для этого из коробки тк String(localized:) совсем печально. Условный R.Swift с xcstrings будет хорош.
danyaffff Автор
Да, действительно жаль, было бы ещё удобнее этим пользоваться. Надеемся, что в будущих версиях это добавят.