Всем доброго времени суток. Работая над рядом проектов с поддержкой нескольких языков, столкнулся с рядом неудобств стандартного инструмента 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")

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

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

Комментарии (2)


  1. dimakey
    27.09.2016 13:45

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

    KOLocalized(key:"kChangeLanguage")
    

    а прямо
    KOLocalized(key:"Change Language")
    

    Во-первых, когда возникнет необходимость использовать составные строки (например, «Last seen: %@ minutes ago», где значение отдельно от строки локализации), будет гораздо нагляднее.

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

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


    1. mish
      27.09.2016 16:30

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

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