Đa ngôn ngữ trong iOS

Giới thiệu

Trong nhiều ứng dụng iOS, chúng ta cần có nhiều phiên bản ngôn ngữ, việc chuyển đổi giữa các ngôn ngữ với nhau phải đảm bảo các đoạn text trong app được thay đổi chính xác. Trong bài viết lần này, chúng ta sẽ cùng nhau tìm hiểu về cách chuyển đổi đa ngôn ngữ trong ứng dụng.

Nội dung

Để dễ quản lý, chúng ta cần tạo một LocationManager adopt một singleton pattern để quản lý ngôn ngữ xuyên suốt app như sau:

//MARK: - LanguageManager

class LanguageManager {
    static let share = LanguageManager()
    
    private var bundle: Bundle?
    
    init() {
        bundle = Bundle.main
    }
    
    func changeLanguage(language: Language, complete: () -> ()) {
        guard let currentLanguageCode = getCurrentLanguage() else {
            UserDefaults.standard.set(language.languageCode(), forKey: LanguageKey)
            UserDefaults.standard.synchronize()
            return
        }
        if language.languageCode() != currentLanguageCode {
            UserDefaults.standard.set(language.languageCode(), forKey: LanguageKey)
            UserDefaults.standard.synchronize()
            Bundle.setLanguage(lang: language.languageCode())
            complete()
        }
    }
    
    func setCurrentLanguage() {
        guard let language = UserDefaults.standard.object(forKey: LanguageKey) as? String else {
            var languageCode: String!
            defer {
                UserDefaults.standard.setValue(languageCode, forKeyPath: LanguageKey)
                UserDefaults.standard.synchronize()
                Bundle.setLanguage(lang: languageCode)
            }
            guard let langStr = Locale.current.languageCode else {
                languageCode = Language.English.languageCode()
                return
            }
            if langStr == Language.English.languageCode() || langStr == Language.Vietnamese.languageCode() {
                languageCode = langStr
            } else {
                languageCode = Language.English.languageCode()
            }
            return
        }
        Bundle.setLanguage(lang: language)
        
    }
    
    private func getCurrentLanguage() -> String? {
        return UserDefaults.standard.string(forKey: LanguageKey)
    }
}

//MARK: - Bundle

private var LangBundleKey = 0
private var firstTime = false

extension Bundle {
    private class BundleEx: Bundle {
        
        override func localizedString(forKey key: String, value: String?, table tableName: String?) -> String {
            guard let bundle = objc_getAssociatedObject(self, &LangBundleKey) as? Bundle else {
                return super.localizedString(forKey: key, value: value, table: tableName)
            }
            return bundle.localizedString(forKey: key, value: value, table: tableName)
        }
    }
    
    class func setLanguage(lang: String) {
        if !firstTime {
            object_setClass(Bundle.main, BundleEx.self)
            firstTime = true
        }
        if let bundle = Bundle(path: Bundle.main.path(forResource: lang, ofType: Constant.String.LocalizedFileType)!) {
            objc_setAssociatedObject(Bundle.main, &LangBundleKey, bundle, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }
}

Đoạn code trên tạo một LanguageManager với singleton là share để xử lý việc thay đổi ngôn ngữ trong toàn ứng dụng. Class LanguageManager này có một public function là

func changeLanguage(language: Language, complete: () -> ()) {
        guard let currentLanguageCode = getCurrentLanguage() else {
            UserDefaults.standard.set(language.languageCode(), forKey: LanguageKey)
            UserDefaults.standard.synchronize()
            return
        }
        if language.languageCode() != currentLanguageCode {
            UserDefaults.standard.set(language.languageCode(), forKey: LanguageKey)
            UserDefaults.standard.synchronize()
            Bundle.setLanguage(lang: language.languageCode())
            complete()
        }
    }

Function này dùng để change language, đi kèm với một complete closure. Trong thân hàm, ý tưởng chính là khi thay đổi ngôn ngữ, thì sẽ save languageCode vào UserDefault và sau đó tiến hành gọi setLanguage từ Bundle

Việc setLanaguage từ Bunlde như sau:

    class func setLanguage(lang: String) {
        if !firstTime {
            object_setClass(Bundle.main, BundleEx.self)
            firstTime = true
        }
        if let bundle = Bundle(path: Bundle.main.path(forResource: lang, ofType: Constant.String.LocalizedFileType)!) {
            objc_setAssociatedObject(Bundle.main, &LangBundleKey, bundle, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }

Ý tưởng chính của phần này là các file ngôn ngữ sẽ được để trong các folder riêng với path riêng. Khi đó, việc set lại đường dẫn sẽ khiến các folder ngôn ngữ thay đổi, từ đó làm thay đổi ngôn ngữ trong ứng dụng.

Tuy nhiên, một số thay đổi như thay đổi file Localizable.strings, file xib, storyboard..., việc thay đổi là tự động. Ứng dụng sẽ tự động gọi Bundle.main và function

func localizedString(forKey key: String, value: String?, table tableName: String?) -> String

cũng sẽ được gọi mà chúng ta không thể can thiệp được. Để khắc phục, chúng ta tạo class BundleEx - là subClass của Bundle.

 private class BundleEx: Bundle {
        
        override func localizedString(forKey key: String, value: String?, table tableName: String?) -> String {
            guard let bundle = objc_getAssociatedObject(self, &LangBundleKey) as? Bundle else {
                return super.localizedString(forKey: key, value: value, table: tableName)
            }
            return bundle.localizedString(forKey: key, value: value, table: tableName)
        }
    }

và dùng một số hàm từ Objective-C để gán class của Bundle cho BunldeEX, khi đó việc thay đổi ngôn ngữ sẽ được tự động thực hiện trên class BundleEX.

Kết luận

Việc thay đổi ngôn ngữ trong ứng dụng như cách làm trên giúp ứng dụng thay đổi được ngôn ngữ mà không cần phải restart lại app hay thay đổi toàn bộ ngôn ngữ của thiết bị. Chúc các bạn thành công!