Avatar ·

Как добавить локализованное описание для ошибок в Swift (Error протокол)

Всем привет! Я пишу своё приложение на Swift 3 и столкнулся с проблемой локализации сообщений об ошибках. У меня есть свой тип ошибок, реализующий протокол Error:
enum MyError: Error {
    case networkError
    case invalidInput(String)
    case unknown
}
Я хочу, чтобы при возникновении ошибки пользователь видел понятное сообщение на его языке. Я знаю про NSLocalizedString, но не понимаю, как правильно интегрировать её с Error. Пробовал сделать так:
extension MyError: LocalizedError {
    var errorDescription: String? {
        switch self {
        case .networkError:
            return NSLocalizedString("Ошибка сети", comment: "Network error")
        case .invalidInput(let field):
            return NSLocalizedString("Неверный ввод в поле \(field)", comment: "Invalid input")
        case .unknown:
            return NSLocalizedString("Неизвестная ошибка", comment: "Unknown error")
        }
    }
}
Но когда я ловлю ошибку в do-catch и печатаю error.localizedDescription, получаю просто "The operation couldn’t be completed." без моего текста. Что я делаю не так? Как правильно заставить Swift использовать моё локализованное описание? Нужно ли что-то дополнительно настраивать в проекте для работы с NSLocalizedString? Заранее спасибо за помощь! <<>>

Правильная реализация локализованных ошибок через протокол LocalizedError

Вы почти всё сделали правильно, но есть одна важная деталь. Протокол LocalizedError требует, чтобы ваша ошибка была классом или структурой, а не просто перечислением. Однако в Swift перечисления могут соответствовать протоколам, и ваша реализация errorDescription верна. Проблема в том, что вы используете error.localizedDescription в неправильном контексте.

Давайте разберёмся по шагам. Когда вы ловите ошибку в catch, Swift автоматически вызывает localizedDescription из протокола Error. Но базовая реализация Error не знает о вашем errorDescription. Чтобы это работало, нужно явно привести ошибку к типу LocalizedError или использовать localizedDescription из протокола LocalizedError.

Вот правильный подход:

enum MyError: LocalizedError {
    case networkError
    case invalidInput(String)
    case unknown

    var errorDescription: String? {
        switch self {
        case .networkError:
            return NSLocalizedString("Ошибка сети. Проверьте подключение к интернету.", comment: "Network error description")
        case .invalidInput(let field):
            return NSLocalizedString("Неверный ввод в поле: \(field). Пожалуйста, проверьте данные.", comment: "Invalid input error")
        case .unknown:
            return NSLocalizedString("Произошла неизвестная ошибка. Попробуйте позже.", comment: "Unknown error")
        }
    }

    var failureReason: String? {
        switch self {
        case .networkError:
            return NSLocalizedString("Сервер недоступен", comment: "Network failure reason")
        case .invalidInput:
            return NSLocalizedString("Некорректные данные", comment: "Invalid input reason")
        case .unknown:
            return NSLocalizedString("Неизвестная причина", comment: "Unknown failure reason")
        }
    }

    var recoverySuggestion: String? {
        switch self {
        case .networkError:
            return NSLocalizedString("Проверьте Wi-Fi или мобильные данные.", comment: "Network recovery suggestion")
        case .invalidInput:
            return NSLocalizedString("Введите корректные значения.", comment: "Invalid input recovery")
        case .unknown:
            return NSLocalizedString("Перезапустите приложение.", comment: "Unknown recovery")
        }
    }
}

Теперь, когда вы ловите ошибку, используйте:

do {
    try someFunction()
} catch let error as LocalizedError {
    // Используем error.errorDescription, failureReason, recoverySuggestion
    print(error.errorDescription ?? "Неизвестная ошибка")
    print(error.failureReason ?? "")
    print(error.recoverySuggestion ?? "")
} catch {
    // Для ошибок, не реализующих LocalizedError
    print(error.localizedDescription)
}

Или ещё проще — используйте localizedDescription напрямую, если вы реализовали errorDescription:

do {
    try someFunction()
} catch {
    // Swift автоматически вызовет errorDescription, если ошибка соответствует LocalizedError
    print(error.localizedDescription)
}

Важные моменты:

  • Не забудьте добавить файлы Localizable.strings для каждого поддерживаемого языка. NSLocalizedString будет искать ключи именно там.
  • Используйте комментарии в NSLocalizedString — они помогают переводчикам понять контекст.
  • Проверьте настройки проекта: в Build Settings > Localization должны быть добавлены нужные языки.
  • Для сложных ошибок можно реализовать все три свойства (errorDescription, failureReason, recoverySuggestion) — это даст пользователю максимально полную информацию.

Если после всех исправлений у вас всё равно выводится стандартное сообщение "The operation couldn’t be completed", убедитесь, что ваша ошибка действительно соответствует протоколу LocalizedError. Проверьте через if let localizedError = error as? LocalizedError.

<<>>

Альтернативный подход: использование структуры вместо enum

Если вам не нравится работать с перечислениями, можно создать структуру, которая будет хранить всю информацию об ошибке и сразу предоставлять локализованное описание.

struct AppError: LocalizedError {
    let message: String
    let reason: String?
    let suggestion: String?

    var errorDescription: String? {
        return NSLocalizedString(message, comment: "Error description")
    }

    var failureReason: String? {
        return reason.map { NSLocalizedString($0, comment: "Failure reason") }
    }

    var recoverySuggestion: String? {
        return suggestion.map { NSLocalizedString($0, comment: "Recovery suggestion") }
    }

    // Удобные инициализаторы для частых случаев
    static func networkError() -> AppError {
        return AppError(
            message: "Ошибка сети",
            reason: "Сервер недоступен",
            suggestion: "Проверьте подключение к интернету"
        )
    }

    static func invalidInput(field: String) -> AppError {
        return AppError(
            message: "Неверный ввод в поле \(field)",
            reason: "Некорректные данные",
            suggestion: "Введите правильное значение"
        )
    }
}

// Использование:
func fetchData() throws {
    throw AppError.networkError()
}

do {
    try fetchData()
} catch let error as AppError {
    print(error.errorDescription ?? "")
}

Этот подход более гибкий: вы можете легко добавлять новые типы ошибок без расширения enum, а также передавать дополнительные данные (например, HTTP-код или имя поля). Минус — нужно помнить о создании правильных экземпляров структуры.