Локализация ошибок в Swift: как сделать Error понятным пользователю

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Как заставить ошибки говорить на языке пользователя?

Разработчики Swift часто сталкиваются с проблемой: ошибка выброшена, но пользователь видит нечто вроде Error Domain=MyApp Code=42. Это технически верно, но абсолютно бесполезно для человека. На Stack Overflow вопрос "How to provide a localized description with an Error type in Swift?" собрал тысячи просмотров — и неспроста.

В этой статье мы разберём, как превратить сухие коды ошибок в понятные, локализованные сообщения. Вы узнаете, как использовать протокол LocalizedError, работать с NSError и правильно подготавливать строки для разных языков. Готовы сделать ваше приложение дружелюбнее? Поехали!

Почему просто Error не работает?

В Swift протокол Error — это минимальный контракт. Он не требует ни описания, ни локализации. Когда вы создаёте свою ошибку:

enum NetworkError: Error {
    case timeout
    case serverError(code: Int)
}

...вы получаете лишь имя кейса. Попробуйте вызвать localizedDescription:

let error = NetworkError.timeout
print(error.localizedDescription) // "The operation couldn’t be completed. (NetworkError error 0.)"

Система подставляет стандартную фразу — неинформативно и некрасиво. Чтобы дать пользователю осмысленное сообщение, нужно реализовать протокол LocalizedError.

Знакомьтесь: протокол LocalizedError

LocalizedError расширяет Error и добавляет четыре опциональных свойства:

  • errorDescription — основное сообщение для пользователя
  • failureReason — причина ошибки (техническая)
  • recoverySuggestion — что делать пользователю
  • helpAnchor — ссылка на справку (редко используется)

Самое важное — errorDescription. Именно его отображают системные компоненты (алерты, SwiftUI).

Реализуем локализованную ошибку

Добавим LocalizedError к нашему NetworkError:

enum NetworkError: LocalizedError {
    case timeout
    case serverError(code: Int)
    case noInternet
    
    var errorDescription: String? {
        switch self {
        case .timeout:
            return NSLocalizedString(
                "Сервер не отвечает. Проверьте соединение и попробуйте снова.",
                comment: "Timeout error"
            )
        case .serverError(let code):
            let format = NSLocalizedString(
                "Ошибка сервера: %d. Попробуйте позже.",
                comment: "Server error with code"
            )
            return String(format: format, code)
        case .noInternet:
            return NSLocalizedString(
                "Нет подключения к интернету.",
                comment: "No internet error"
            )
        }
    }
    
    var failureReason: String? {
        switch self {
        case .timeout:
            return NSLocalizedString(
                "Превышено время ожидания ответа от сервера.",
                comment: "Timeout reason"
            )
        case .serverError(let code):
            return NSLocalizedString(
                "Сервер вернул код \(code).",
                comment: "Server error reason"
            )
        case .noInternet:
            return NSLocalizedString(
                "Отсутствует активное интернет-соединение.",
                comment: "No internet reason"
            )
        }
    }
    
    var recoverySuggestion: String? {
        switch self {
        case .timeout:
            return NSLocalizedString(
                "Проверьте Wi-Fi или мобильные данные и повторите запрос.",
                comment: "Timeout recovery"
            )
        case .serverError:
            return NSLocalizedString(
                "Подождите несколько минут и попробуйте снова.",
                comment: "Server error recovery"
            )
        case .noInternet:
            return NSLocalizedString(
                "Включите интернет и повторите попытку.",
                comment: "No internet recovery"
            )
        }
    }
}

Теперь error.localizedDescription вернёт осмысленную строку на языке пользователя:

let error = NetworkError.timeout
print(error.localizedDescription) 
// "Сервер не отвечает. Проверьте соединение и попробуйте снова."

Работа с NSError: мост между мирами

Иногда вам нужно взаимодействовать с Objective-C кодом или системными API, которые используют NSError. Swift автоматически мостит Error в NSError, но локализованное описание теряется, если не реализовать LocalizedError.

Хорошая новость: если вы реализовали errorDescription, то NSError автоматически подхватит его через userInfo[NSLocalizedDescriptionKey].

Пример работы с системным API:

func loadData() throws {
    // Представим, что возникла сетевая ошибка
    throw NetworkError.timeout
}

do {
    try loadData()
} catch {
    let nsError = error as NSError
    print(nsError.localizedDescription) // Ваше описание!
    print(nsError.userInfo[NSLocalizedFailureReasonErrorKey] ?? "")
    print(nsError.userInfo[NSLocalizedRecoverySuggestionErrorKey] ?? "")
}

Локализация строк: NSLocalizedString и String Catalog

Для настоящей локализации используйте NSLocalizedString. Храните строки в Localizable.strings (Xcode 15+ рекомендует String Catalog).

Пример Localizable.strings для английского:

"Сервер не отвечает. Проверьте соединение и попробуйте снова." = "Server is not responding. Check your connection and try again.";
"Ошибка сервера: %d. Попробуйте позже." = "Server error: %d. Please try later.";
"Нет подключения к интернету." = "No internet connection.";

Для других языков создайте соответствующие файлы (Localizable.strings (Russian) — для русского, Localizable.strings (English) — для английского).

Продвинутый пример: структура с параметрами

Иногда ошибка содержит динамические данные — например, имя файла или ID пользователя. Создадим структуру:

struct FileError: LocalizedError {
    let fileName: String
    let operation: String
    
    var errorDescription: String? {
        let format = NSLocalizedString(
            "Не удалось %@ файл \"%@\".",
            comment: "File error description"
        )
        return String(format: format, operation, fileName)
    }
    
    var failureReason: String? {
        let format = NSLocalizedString(
            "Файл \"%@\" недоступен для %@.",
            comment: "File error reason"
        )
        return String(format: format, fileName, operation)
    }
    
    var recoverySuggestion: String? {
        NSLocalizedString(
            "Проверьте права доступа и имя файла.",
            comment: "File error recovery"
        )
    }
}

Использование:

let error = FileError(fileName: "data.txt", operation: "открыть")
print(error.localizedDescription) 
// "Не удалось открыть файл "data.txt"."

Лучшие практики для production

  1. Всегда используйте NSLocalizedString — даже если приложение только на русском. Это подготовит код к будущей локализации.
  2. <

Блоги

Рекомендаций по книгам