![](https://habrastorage.org/getpro/habr/upload_files/aae/655/2f3/aae6552f3b28dfe8c1b6417bba087dff.png)
Меня зовут Даниил Храповицкий, я iOS‑разработчик студии СleverPumpkin, и сегодня поговорим про xcstrings
в Xcode 15.
Один из самых неприятных аспектов iOS‑разработки — это локализация и плюрализация строк. Мало того, что они разбиты на разные файлы: strings
и stringsdict
, так ещё и работа с этими файлами для начинающего разработчика может оказаться не сильно очевидной. «Что такое %#@VARIABLE@
?», «Как добавлять несколько плюралок в одну строку?», «Как использовать плюралки в локализованных строках?», «Как добавлять разные переводы для разных девайсов?» — Все эти вопросы рано или поздно возникают у разработчика. После получения ответов на них каждый задаётся вопросом: «А почему всё так плохо?»
У зелёных наших братьев дела обстоят получше — у них один файл для локализации. У нас же всё так непросто, потому что формат данных достался нам из Objective‑C, где какая‑то строгость и защита от дураков были не совсем обязательны (вы вообще видели Objective‑C?). Поэтому раньше в файлах локализации можно было:
спокойно пропустить точку с запятой, о месте в коде которой Xcode стыдливо умолчит;
упустить тот момент, что название
NSStringLocalizedFormatKey
должно совпадать с ключом строчки, для которой мы предоставляем плюрализацию (когда об этом знаешь, то забыть сложно, но на первых порах все делали такие ошибки);вообще забыть добавить перевод для одного из ключей, после чего в приложении будет отображаться ключ вместо нормальной строки.
Однако всё изменилось с приходом Xcode 15, где локализация и плюрализация строк были значительно улучшены. Теперь там один файл xcstrings
. Это каталог, который хранит в себе все ключи и строки как для переводов на другие языки, так и для переводов для множественного числа. Важно отметить, что всё это умеет бэкпортиться на старые версии iOS путём разбиения xcstrings
на .strings и .stringsdict. То есть всё равно под капотом используется старый формат, но мы, как разработчики, работаем уже с удобным для нас интерфейсом.
При создании нового xcstrings
‑каталога интерфейс будет выглядеть следующим образом:
![](https://habrastorage.org/getpro/habr/upload_files/db4/392/da2/db4392da2090e0b076fd044fd691a836.png)
Если мы выделим язык, то нам отобразится, что каталог пустой:
![](https://habrastorage.org/getpro/habr/upload_files/f8a/15a/d0b/f8a15ad0b7058cb32c0d9e88a9abceb0.png)
Попробуем добавить строку и посмотрим, что произойдёт. Создалась строка, для которой мы можем задать ключ, локализованную строку и коммент. Состояние будет автоматически меняться для отображения информации о состоянии перевода и актуальности данной записи:
![](https://habrastorage.org/getpro/habr/upload_files/eb7/ac2/2cd/eb7ac22cdaacfca59a797cfaecc02d17.png)
Изменим строку:
![](https://habrastorage.org/getpro/habr/upload_files/0f2/25f/4f3/0f225f4f3250998578088eee8041f83c.png)
Теперь добавим перевод на русский язык:
![](https://habrastorage.org/getpro/habr/upload_files/324/a86/b57/324a86b576395cfcc1abbef85d281497.png)
Будет отображено, что имеется одна новая строка:
![](https://habrastorage.org/getpro/habr/upload_files/0d0/2b1/96b/0d02b196baaddd0c445a00d8c65ffc9e.png)
Переведём её. После добавления перевода состояние изменится на ОК.
![](https://habrastorage.org/getpro/habr/upload_files/955/ea8/390/955ea8390be8ab90b1b28873b5dfe505.png)
Добавим возможность передавать туда числа. Делается это следующим образом:
![](https://habrastorage.org/getpro/habr/upload_files/171/1d2/adb/1711d2adb561158d9a413f65ecea8760.gif)
Однако теперь нам необходимо добавить плюрализацию для строки, чтобы она выглядела естественно. Делается это следующим образом:
![](https://habrastorage.org/getpro/habr/upload_files/ce8/7f1/439/ce87f14390e3dde785e6d3610df8dcae.gif)
Состояние поменялось на Needs review. Это говорит о том, что строка требует нашего внимания.
После локализации двух переменных получается следующее:
![](https://habrastorage.org/getpro/habr/upload_files/c7a/b33/ea8/c7ab33ea8782044a9640902bbc6f4a5b.png)
Вернувшись к русскому языку, мы увидим, что строки необходимо обновить.
![](https://habrastorage.org/getpro/habr/upload_files/978/e0d/ca5/978e0dca551c7cc5c61ffb8c0b453a56.png)
Обновив, мы получим следующее:
![](https://habrastorage.org/getpro/habr/upload_files/436/264/b4a/436264b4a9c4ea707b846a425b1704c2.png)
Так происходит локализация и плюрализация. В коде это можно вызвать либо через старый‑добрый NSLocalizedString
:
String(format: NSLocalizedString("File instead of Files", comment: ""), 1, 2)
Либо через новомодную структуру LocalizedStringResource
, но для этого необходимо немного изменить ключ — добавить в него спецификаторы форматов аргументов (%d
, %lld
, %@
и так далее), которые используются в строке.
![](https://habrastorage.org/getpro/habr/upload_files/c50/70f/3eb/c5070f3eb702de77a1297ed22b7e911a.png)
Однако если мы вызовем строку этим способом, то всё будет работать не так, как мы ожидали:
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
файлом, создавая новые ключи и прописывая в них аргументы (хотя писать локализацию строк всё равно придётся):
![](https://habrastorage.org/getpro/habr/upload_files/acf/328/625/acf3286254f130614d522c73a4d3b54a.png)
Однако в случае с %d
, если же мы явно укажем тип, тогда всё отработает как ожидается:
String(localized: "\(Int16(2)) File instead of \(Int16(1)) Files")
// 2 Files instead of 1 File
Чтобы этого избежать, нужно указывать тип %lld
, который будет нормально парситься в Int
.
В дополнение есть возможность создания строк перевода для конкретного типа девайса. Например, можно задать различные переводы для iPhone и остальных устройств:
![](https://habrastorage.org/getpro/habr/upload_files/ed8/e59/3d0/ed8e593d0af31cec49a9924b2b7175e7.png)
![](https://habrastorage.org/getpro/habr/upload_files/4a9/bfe/6eb/4a9bfe6eb699664a5c009bf6ba080bc8.png)
![](https://habrastorage.org/getpro/habr/upload_files/625/7d0/4d0/6257d04d08cd1d81549cd631db4d4392.png)
Также из интересного стоит отметить, что теперь файлы вместо привычного xml
имеют json
формат, что очень неожиданно для Apple. А так как файл теперь один, то такие сервисы, как POEditor, смогут нормально генерировать строки для iOS‑проектов.
Вся эта прелесть поддерживается на всех версиях iOS, но, само собой, собранных с использованием Xcode 15. Магия в том, что при компиляции всё парсится в уже привычные strings
и stringsdict
файлы, и в бандле будут храниться именно они.
В заключение хочется сказать, что Apple делает правильные шаги в плане локализации. Начиная с Xcode 15, переводить строки стало намного удобнее, проще и нагляднее без дублирования на несколько файлов.
Chris_moler
Спасибо за статью!
Жаль конечно что не завезли кодогенерацию для этого из коробки тк String(localized:) совсем печально. Условный R.Swift с xcstrings будет хорош.
danyaffff Автор
Да, действительно жаль, было бы ещё удобнее этим пользоваться. Надеемся, что в будущих версиях это добавят.