Альтернатива NSLocalizedString, смена языка без перезагрузки приложения

в 8:47, , рубрики: iOS разработка, NSLocalizedString, objective-c, swift 3, разработка под iOS, метки:

Всем доброго времени суток. Работая над рядом проектов с поддержкой нескольких языков, столкнулся с рядом неудобств стандартного инструмента NSLocalizedString.

Основной проблемой было то, что изменения языка вступают в силу только при перегрузке приложения, что с точки зрения юзабилити, не очень приятно и удобно для пользователя.

Если конечно же у вас нет четкой задачи смены языка в приложении, то можно использовать и стандартный NSLocalizedString. Если же у вас предусмотрена такая возможность NSLocalizedString становиться очень неудобным.

Возможно я пытался изобрести велосипед.

Какова была задача?

1. Первое и самое главное, ни какой перезагрузки приложения;
2. Удобство в использовании в коде по принципу NSLocalizedString(key: String);
3. Удобство для переводчиков(но в данном случае у меня сомнения, что в каком угодно виде оно будет удобно).

До выхода языка Swift, на Objective-c для реализации выше поставленной задачи использовал макрос.

Сам код выглядит примерно так:

//ключ для NSUserDefaults
#define kLocale @"kLocale"
//тип файла .plist
#define kTypeLocalizable @"plist"

//AppDelegate
//в этом месте мы проверяем сохраняли мы по ключу значение(название файла)
if (![[NSUserDefaults standardUserDefaults]objectForKey:kLocale]) {
//если нет, смотрим какой установлен язык на устройстве 
NSString *langValue =  kLangValue;
//Проверяем, поддерживает наше приложение данный язык, если нет то ставим базовый.
NSString *key =  (![langValue isEqualToString:@"ru"] && ![langValue isEqualToString:@"en"]) ? @"en" : langValue;
//Сохраняем название файла 
[[NSUserDefaults standardUserDefaults]setObject:[NSString stringWithFormat:@"%@_Localizable",key]  forKey:kLocale];
}

#define kLangValue  ([kLanguserDefaultValue length]>2)? [kLanguserDefaultValue substringToIndex:[kLanguserDefaultValue length]-([kLanguserDefaultValue length]-2)]:kLanguserDefaultValue;
 
//Возвращает имя файла
#define kNameFile [[NSUserDefaults standardUserDefaults]objectForKey:kLocale]

//В данном месте берем наш .plist и как из обычного NSDictionary возвращаем значение по ключу 
#define KOLocalizable(key) [[NSDictionary dictionaryWithContentsOfFile:[[NSBundle mainBundle] pathForResource:kNameFile ofType:kTypeLocalizable]] objectForKey:key]

Реализация выглядит так:

textLabel.text = KOLocalizable(@"kText")
//очень похоже на NSLocalizedString ("ключ","комментарий" ) только без комментария 

И собственно смена языка, это просто заменить название фала:

[[NSUserDefaults standardUserDefaults]setObject:@"ru_Localizable"  forKey:kLocale];

Не могу сказать, что данный код придел совершенства, он работает.

Но к великому сожалению в Swift меня лишили такой возможности и пришлось думать над альтернативой. Долгое ломание своего мозга, как же красивее и практичнее это реализовать.

Идея заключалась в том что бы это было также кратко как и стандартная реализация NSLocalizedString («ключ»,«комментарий») только без комментария.

textLabel.text = KOLocalized(key:"kText")

Swift порадовал тем, что можно функцию вынести за пределы класса и не привязывать к чему либо. Получается такая глобальная функция, общедоступная (по сути, тот же макрос).


import Foundation

func KOLocalized(key:String)->String{
    return KOLocalizedClass.instanc.valueWith(key: key)
}

В этой функции мы обращаемся к классу где и происходит вся магия.


class KOLocalizedClass: NSObject {
    static let instanc = KOLocalizedClass()

    private let localeArray:Array = ["ru","en"]
    private let keyLocale: String = "kLocale"
    private let endNameFile: String = "Localizable"

    private var localeDictionary : NSDictionary!
    private let typeLocalizable  : String = "plist"
    private var nameFile         : String!

    override init() {
        super.init()
        checkFirstInit()
    }
    //MARK: Public Methods
    public func changeLocalized(key:String){
        UserDefaults.standard.set("(key)_(endNameFile)", forKey: keyLocale)
        nameFile = "(key)_(endNameFile)"
        updateDictionary()
    }

    //MARK: Internal Methods
    internal func valueWith(key:String) -> String {
        var value:String
        value = localeDictionary.object(forKey: key) as? String ?? key
        return value
    }

    //MARK: Privat Methods
    private func checkFirstInit(){
        if UserDefaults.standard.object(forKey: keyLocale) == nil{
            var langValue:String {
                var systemLocale : String = NSLocale.preferredLanguages[0]

                if systemLocale.characters.count > 2 {
                    let index = systemLocale.range(of: "_")?.lowerBound
                    systemLocale = systemLocale.substring(to: index!)
                }

                for localeString in localeArray{
                    if localeString == systemLocale{
                        systemLocale = localeString
                    }
                }
                return systemLocale == "" ? systemLocale: "en"
            }
            UserDefaults.standard.set("(langValue)_(endNameFile)", forKey: keyLocale)
            nameFile = "(langValue)_(endNameFile)"
        }else{
            nameFile = UserDefaults.standard.object(forKey: keyLocale) as! String
        }
        updateDictionary()
    }
    //Update Dictionary
    private func updateDictionary(){
        if let path =  Bundle.main.path(forResource: nameFile, ofType: typeLocalizable) {
            localeDictionary = NSDictionary(contentsOfFile: path)!
        }
    }
}

Единственное отличие от реализации на Objective-c, в Swift создали класс как singleton в котором храним переменную типа Dictionary:

private var localeDictionary : NSDictionary!

И не дергаем каждый раз файл. И смена языка теперь происходит через функцию:

KOLocalizedClass.instanc.changeLocalized(key: "ru")

Вот пример как это работает.

Надеюсь данный материал будет полезен.

Автор: SethSky

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js