Code Monkey home page Code Monkey logo

languageswitcher's Introduction

LanguageSwitcher

Example how to switch iOS app's language on-the-fly, instantly

iOS – and Apple OSs in general – have excellent I18N support. You more or less don't have to worry about date and number formatting as long as you are following the rules. The essential rules are:

  • all translations are prepared beforehand and compiled into the app
  • language choice is done outside your app, in system' Settings

Fairly often, I have requests from clients to implement in-app language change, which should (at least) instantly translate the app. There is no API support in iOS frameworks for this.

It's not impossible though.

Force-load correct set of translations

First issue is that all the translations are kept inside LANGUAGE_CODE.lproj folders, like en.lproj, fr.lproj etc.

When your app starts, iOS looks into UserDefaults.standard.value(forKey: "AppleLanguages") value and loads the corresponding translations from the .lproj folder. It processes all the .xib and .storyboard files plus makes an internal dictionary (or something similar) that NSLocalizedString() is using to load appropriate string.

This is done at the app start and there is no way I know of to force-change the language without restarting the app itself.

So we need to cheat and since iOS runtime is dynamism heaven enabled by Objective-C, we can actually do that.

Maxim Bilan found neat solution which I have converted into Swift:

  • subclass Bundle and override its localizedString(forKey…) method to check for Bundle.main.localizedBundle
  • force-change the Class of the Bundle.main to be that subclass so our method is called instead of default one (thanks Objective-C!)
  • when app's language change is initiated somewhere in the app, load the Main bundle again but with appropriate .lproj
  • saved that loaded bundle as Bundle.main.localizedBundle

Things now work on their own. This will be enough to instantly translate the app if your app is using LocalizedString() only. If your strings are in IB files though, then you need to force-reload those files.

In the demo app, I am pushing the notification that informs everyone about Locale change and AppDelegate is responding to that by reloading the window.rootController:

let nc = NotificationCenter.default
nc.addObserver(forName: NSLocale.currentLocaleDidChangeNotification, 
               object: nil, 
               queue: OperationQueue.main) {
	[weak self] notification in
	guard let `self` = self else { return }

	let sb = UIStoryboard(name: "Main", bundle: nil)
	let vc = sb.instantiateInitialViewController()
	self.window?.rootViewController = vc
}

Overriding Locale.current

Reloading string translations is not enough, though. Large number of system-wide methods on various types is consulting Locale.current (or rather Locale.autoupdatingCurrent..?) to perform its function.

Example: say you have a UITextField for number input. You set its keyboard to DecimalPad which display decimal separator as lower left button. The caption and the actual character that's a result of the tap is the value of decimalSeparator property on the currently active Locale.

Further, if you try to convert textField.text to a Double or Decimal, conversion will accept only that one value as valid decimal separator. So if you've entered "5,15" and your localeIdentifier is "en_US", that will be converted to 0, not 5.15. Bummer. While this is solvable issue by re-setting cached Formatters, the keyboard issue mentioned above is not.

Thus you need to create your own custom keyboard (yuck!) or…you can swizzle NSLocale.current property. (Thanks again, Objective-C!)

extension NSLocale {
fileprivate static func swizzle(selector: Selector) {
	let originalSelector = selector
	let swizzledSelector = #selector(getter: NSLocale.app)
	let originalMethod = class_getClassMethod(self, originalSelector)
	let swizzledMethod = class_getClassMethod(self, swizzledSelector)
	method_exchangeImplementations(originalMethod, swizzledMethod)
}
}


NSLocale.swizzle(selector: #selector(getter: NSLocale.current))

NSLocale.app is my own custom Locale I build in any way I need. I start with system's original value for autoupdatingCurrent and then add and/or replace components using whatever app-level or user-level settings I have.

Swizzling needs to be done only once, as early as possible in app's lifecycle.

Demo

Look into the localize() method in demo's ViewController. This is actually my preferred method to handle instant translations as there's no loss of user context. It's more work but it leads to better customer experience.

If you cache your DateFormatter and NumberFormatter instances - as you certainly should - then on language change you need to make sure to re-set up Locale and dateFormat values.

  • LanguageSwitcher project uses Main.storyboard and shows how you can "restart" the app and thus render the changes.
  • LanguageSwitcher2 project builds the UI stack in AppDelegate and shows how you can keep the user context and use notifications to inform all the views to re-populate their content using NSLocalizedString()

Issues

There's a non-critical issue with the keyboard: when the keyboard is shown, iOS will cache that generated view. Thus if you do this sequence:

  • open the app
  • make sure English is shown
  • tap the text field so the keyboard appears (you can see the . shown as decimal separator)
  • dismiss it
  • switch to Serbian (which uses , as decimal separator
  • tap the text field again

You can see that keyboard view still shows the . but if you tap it, it will correctly enter ,. Hence it's annoyance at best.

I'm not aware of any way to force-clear the keyboard cache in iOS. If you do, please let me know.

languageswitcher's People

Contributors

radianttap avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

languageswitcher's Issues

Selected language isn't retained

if you select a language and then close and run the app again, the previously selected language is not set as the default language.

RTL layouts

Hello,
thanks for the example your provided.

I have this old problem, that I want to switch
the layout direction for RTL languages, e.g., Arabic.

simulator screen shot apr 25 2017 15 01 25

As you can see from the screenshot, the Arabic text is switched, but the direction remains the same.

locale swizzle doesn't work with decimal input

It appears that swizzle for NSLocale.current no longer fixes the decimal input problem as described in: http://aplus.rs/2017/in-app-language-change-in-ios-app/

You can see that keyboard view still shows the . but if you tap it, it will correctly enter ,. Hence it’s annoyance at best.

In iOS 13, simulator or device, tapping the "." always enters a ".", and the parsing logic fails.

Can verify this by checking out this repository and and following these steps:

  1. open the app
  2. make sure English is shown
  3. tap the text field so the keyboard appears (you can see the . shown as decimal separator)
  4. dismiss it
  5. switch to Serbian (which uses , as decimal separator
  6. tap the text field again

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.