Проблема с локализацией

Стандартный подход к локализации в iOS — строки вида NSLocalizedString("welcome_title", comment: ""). Это работает, но у него есть минусы: нет автодополнения, легко опечататься в ключе, рефакторинг болезненный.

Популярное решение — SwiftGen, который генерирует типобезопасный код из Localizable.strings. Но это внешний инструмент, его нужно встраивать в CI, обновлять, объяснять новым разработчикам.

Swift Macros дают возможность сделать то же самое нативно, без внешних зависимостей.

Что хотим получить

// Хотим писать вот так: @Localizable enum Strings { static let welcomeTitle = "Добро пожаловать" static let loginButton = "Войти" static let errorMessage = "Что-то пошло не так" } // А использовать так (с автодополнением): label.text = Strings.welcomeTitle button.setTitle(Strings.loginButton, for: .normal)

Создаём макрос

Создайте новый Swift Package: File → New → Package → Swift Macro. Xcode создаст структуру с двумя таргетами — сам макрос и его реализация.

// Package.swift .target( name: "LocalizableMacros", dependencies: [ .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), ] )

Реализация

Макрос типа @attached(member) позволяет добавлять членов к типу. Нам нужно для каждого static let заменить строку на вызов NSLocalizedString:

public struct LocalizableMacro: MemberAttributeMacro { public static func expansion( of node: AttributeSyntax, attachedTo declaration: some DeclGroupSyntax, providingAttributesFor member: some DeclSyntax, in context: some MacroExpansionContext ) throws -> [AttributeSyntax] { guard let varDecl = member.as(VariableDeclSyntax.self), varDecl.bindingSpecifier.tokenKind == .keyword(.let) else { return [] } return ["@LocalizedString"] } }
Важно: Реализация макроса компилируется как отдельный плагин и запускается на хосте (вашем Mac), не на целевой платформе. Это значит что в ней нельзя использовать Foundation или UIKit.

Тестирование макроса

Swift предоставляет специальный фреймворк для тестирования макросов:

import MacroTesting import XCTest final class LocalizableTests: XCTestCase { func testExpansion() { assertMacroExpansion( """ @Localizable enum Strings { static let title = "Hello" } """, expandedSource: """ enum Strings { static let title = NSLocalizedString("title", comment: "") } """, macros: ["Localizable": LocalizableMacro.self] ) } }

Итог

Полная реализация с обработкой edge cases занимает около 150 строк. Код доступен по запросу. Макрос уже используем в двух проектах на проде, проблем не было.

Swift Macros — это то направление куда движется язык. Рекомендую начать изучать сейчас, пока экосистема только формируется.