13 июля 2023

ВРЕМЯ ПРОЧТЕНИЯ — 4 МИН

.xcstrings в Xcode 15

How to upload an app to the App Store and not get rejected
Один из самых неприятных аспектов 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, переводить строки стало намного удобнее, проще и нагляднее без дублирования на несколько файлов.
Другие статьи по теме: