Swift Package Manager существует уже довольно давно, его популярность растёт, как и количество модулей, доступных для использования. Если в вашем модуле присутствует (или будет присутствовать, если вы пришли сюда из поисковика) UI, то, вероятнее всего, в нём есть различные ресурсы - текстовые или изображения. В таком случае будет здорово, если ваш модуль будет локализован, ведь это гарантирует полезность вашего модуля для как можно большего числа разработчиков.

Обозначьте локализацию по умолчанию

Чтобы локализовать ресурсы вашего модуля, передайте опциональный параметр defaultLocalization внутри вашего Package.swift. В качестве значения он принимает строку с двухбуквенными кодами языков ISO 639-1 или трёхбуквенными кодами языков ISO 639-2 с дополнительными обозначениями региона или алфавита. Ознакомиться со списком кодов можно здесь.

В этом примере в качестве локализации по умолчанию используется английский язык:

let package = Package(
    name: "AnyLocalizedKit",
    defaultLocalization: "en",
    ...

Создайте директорию с ресурсами

Внутри папки с названием модуля создайте папку с "Resources". Теперь внутри Package.swift для каждого таргета добавьте опциональное поле resources - это массив, содержащий пути для ресурсов.

В нашем случае, добавим путь для целой папки:

targets: [
    .target(
        name: "AnyLocalizedKit",
        dependencies: [],
        resources: [.process("Resources")]
    )

Обратите внимание, как в приведенном выше примере кода используется функция process(_:localization:). Когда вы явно объявляете список ресурсов, вы должны выбрать одно из этих правил, чтобы определить, как Xcode обрабатывает файл ресурсов:

  • Apple советует использовать process(_:localization:) в большинстве случаев, чтобы Xcode обрабатывал ресурсы в соответствии с платформой, для которой вы создаете модуль. Например, Xcode может оптимизировать файлы изображений для платформы, которая поддерживает такую оптимизацию.

  • Используйте copy(_:) для тех случаев, когда существуют требования, чтобы сохранить определенную структуру каталогов для ресурсов или просто чтобы файл ресурсов оставался нетронутым.

Добавьте ресурсы для различных языков

Добавление локализованных ресурсов осуществляется в директориях, имеющих название, в котором используется вышеупомянутый код языка ISO-639, за которым следует .lproj. Примеры: en-GB.lproj, eng.lproj, en.lproj. Xcode распознает локализованные ресурсы в каталогах .lproj и автоматически создает resource bundles.

Примечание: код языка должен быть одинаковым для всего модуля. Не получится указать defaultLocalization: "eng", а папку с ресурсами назвать en-GB.lproj. Вам выведется соответствующее предупреждение в Xcode.

После создания всех папок, вы можете начать добавлять ресурсы. Если с изображениями всё понятно, то с текстовой локализацией есть вопросы, ведь классического "Strings File" для создания нет. Вам необходимо создать "Empty" файл, который нужно переименовать в Localizable.strings. Выглядеть это будет примерно так:

В данном примере для немецкого языка уже создан файл с локализацией, а для английского - ещё нет

На следующем скриншоте показана структура модуля с локализованными ресурсами:

Пример структуры модуля. Может отличаться, в зависимости от вашего видения

В каждый из файлов Localizable.strings добавим для примера одну строчку: greetings = "Hello!";

Описание строк для локализации

Пример создания UIView с локализацией

Для примера создадим экземпляры классов UIImageView и UILabel. Создадим экземпляр класса UIImage, в который передадим нашу локализованную картинку:

let image = UIImage(named: "flag", in: .module, with: nil)

Для UILabel необходимо задать свойство text:

label.text = NSLocalizedString("greetings", bundle: .module, comment: "")

Пример создания локализованного UIView:

import UIKit

open class AnyLocalizedView: UIView {
    
    private let imageView: UIImageView = {
        let imageView = UIImageView(image: UIImage(named: "flag", in: .module, with: nil))
        imageView.contentMode = .scaleAspectFit
        return imageView
    }()
    
    private let label: UILabel = {
        let label = UILabel()
        label.text = NSLocalizedString("greetings", bundle: .module, comment: "")
        return label
    }()
    
    private lazy var stackView: UIStackView = {
        let stackView = UIStackView(arrangedSubviews: [imageView, label])
        stackView.spacing = 12
        stackView.alignment = .center
        stackView.axis = .vertical
        stackView.translatesAutoresizingMaskIntoConstraints = false
        return stackView
    }()
    
    public init() {
        super.init(frame: .zero)
        
        setupView()
    }
    
    required public init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private func setupView() {
        addSubview(stackView)
        
        NSLayoutConstraint.activate([
            stackView.centerYAnchor.constraint(equalTo: centerYAnchor),
            stackView.centerXAnchor.constraint(equalTo: centerXAnchor),
        ])
    }
}

Всё что нам осталось - это использовать наш локализованный модуль:

import UIKit
import AnyLocalizedKit

class ViewController: UIViewController {
    
    private let localizedView = AnyLocalizedView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.addSubview(localizedView)
        localizedView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            localizedView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            localizedView.centerXAnchor.constraint(equalTo: view.centerXAnchor)
        ])
    }
}

Полученный результат будет выглядеть как-то так

В каждом из этих симуляторов используется разный язык

SwiftUI

Если вы используете SwiftUI, то воспользуйтесь следующим кодом:

Text("greetings", bundle: .module)
Пример локализации SwiftUIView

Локализация Storyboard и файлов Interface Builder

Если ваш модуль содержит Storyboard или файлы Interface Builder в качестве ресурсов, используйте базовую интернационализацию, чтобы избавить локализаторы от необходимости изменять эти файлы напрямую. Чтобы Xcode автоматически распознавал базовую локализацию в вашем модуле, выполните следующие шаги:

  • Создайте каталог с именем, например, "Resources", для ваших локализованных ресурсов.

  • Создайте подкаталог с именем Base.lproj и поместите в него раскадровки пакета и файлы Interface Builder.

  • Поместите каталоги .lproj для всех поддерживаемых языков в каталог Ресурсов. Если вы предпочитаете явно объявлять ресурсы для базовой интернационализации, используйте process(_:) и передайте Resource.Localization.base для этого.

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

targets: [
    .target(
        ...
        resources: [
          .process("Resources"),
          .process(”путь/к/ViewController.xib”, localization: .base)
        ]
    )

Примечание

В процессе локализации вы можете столкнуться с такой ошибкой:

Error: Type: 'Bundle?' has no member 'module'

Для её решения вам необходимо просто собрать проект. Если не получилось, то воспользуйтесь набором стандартных действий:

  • Очистить Build Folder;

  • Перезапустить Xcode;

  • Очистить DerivedData.