There is an easy way to implement language switching in a Single-Activity application. The stack of screens in this approach is not reset, the user remains where he switched the language. When the user goes to previous screens, they are immediately displayed translated. And the result of the localization of numbers, sums of money and interest may surprise designers.
Further there will be nothing about:
And we will talk about:
Let there be a screen with settings in our application, and we want to add a couple of new items to it, one of which would allow you to switch the language of the application, and the other to change the currency in which the money amounts are displayed. Here are some examples of how this might look.
In addition to translating the text and displaying the layout from right to left, these settings should affect the format for displaying numerical values. It is necessary that everything is displayed according to the selected locale.
Imagine that our application is written in accordance with the Single-Activity approach . Then the language switching mechanism can be implemented as follows.
SettingsInteractor
is the source of the current language value. It allows you to subscribe to this value, receive it synchronously and only subscribe to updates. If necessary, you can introduce an additional abstraction over SettingsInteractor
according to the principle of interface separation . In the diagram, irrelevant details are omitted.
AppActivity
at creation replaces the context with a new one so that the application uses resources for the selected language.
override fun attachBaseContext(base: Context) { super.attachBaseContext(applySelectedAppLanguage(base)) } private fun applySelectedAppLanguage(context: Context): Context { val locale = settingsInteractor.getUserSelectedLanguageBlocking() val newConfig = Configuration(context.resources.configuration) Locale.setDefault(locale) newConfig.setLocale(locale) return context.createConfigurationContext(newConfig) }
AppPresenter
in turn, subscribes to language updates and notifies View of the changes.
override fun onFirstViewAttach() { super.onFirstViewAttach() subscribeToLanguageUpdates() } private fun subscribeToLanguageUpdates() { settingsInteractor .getUserSelectedLanguageUpdates() .subscribe( { newLang -> viewState.applyNewAppLanguage(newLang) }, { error -> errorHandler.handle(error) } ) .disposeOnDestroy() }
AppActivity
re-created when a notification of a language change is received.
override fun applyNewAppLanguage(lang: Locale) = recreate()
AppActivity
is the only one in the application. All other screens are implemented in fragments. Therefore, when recreating the activity, the stack of screens is saved by the system. If you return to previous screens, they will be reinitialized and displayed translated. The user will remain on the language selection list and see the result of his choice instantly.
In addition to replacing the context, it is necessary to format the data - numbers, money, interest. Let each View delegate this task to a separate component, let's call it UiLocalizer
.
UiLocalizer
uses the appropriate NumberFormat
instances to convert a number to a string.
private var numberFormat = NumberFormat.getNumberInstance(lang) private var percentFormat = NumberFormat.getPercentInstance(lang) private fun getNumberFormatForCurrency(currency: Currency) = NumberFormat .getCurrencyInstance(lang) .also { it.currency = currency }
Please note that the currency must be set separately.
If you save CPU cycles and bits of memory, and switching currency and language is the main and often used function of your application, then, of course, you need a cache.
Instances of the Locale
class are created by a language tag , which consists of a two-letter language code and a two-letter region code. And instances of the Currency
class are based on a three-letter ISO code . In this form, the language and currency must be serialized to save to disk or transfer over the network, and then it will be good. Here are some examples.
// IETF BCP 47 language tag string. private val langs = arrayOf( Locale.forLanguageTag("ru-RU"), Locale.forLanguageTag("en-US"), Locale.forLanguageTag("en-GB"), Locale.forLanguageTag("he-IL"), Locale.forLanguageTag("ar-SA"), Locale.forLanguageTag("ar-AE"), Locale.forLanguageTag("fr-FR"), Locale.forLanguageTag("fr-CH"), Locale.forLanguageTag("de-DE"), Locale.forLanguageTag("de-CH"), Locale.forLanguageTag("da-DK") ) // ISO 4217 code of the currency. private val currencies = arrayListOf( Currency.getInstance("RUB"), Currency.getInstance("USD"), Currency.getInstance("GBP"), Currency.getInstance("ILS"), Currency.getInstance("SAR"), Currency.getInstance("AED"), Currency.getInstance("EUR"), Currency.getInstance("CHF"), Currency.getInstance("DKK") )
The result of formatting numbers in accordance with regional standards may differ from what was expected. The currency symbol or its three-letter code in different languages will be displayed in different ways. Minus signs of negative monetary values will appear in unexpected places, and in some places brackets will be displayed instead. The percent sign may not be exactly the sign we are used to.
The fact is that, from the point of view of regional patterns, the final line consists of a prefix and suffix for positive and negative numbers, a thousand separator and a decimal separator, and they are different for different locales.
Language | Negative Prefix | Negative Suffix | Positive Prefix | Positive suffix | Grouping sepaarator | Decimal separator |
---|---|---|---|---|---|---|
ru-RU | "-" | "" | "," | |||
en-US | "-" | "," | "." | |||
iw-IL | "-" | "," | "." | |||
ar-AE | "-" | "٬" | "٫" | |||
fr-FR | "-" | "" | "," | |||
de-de | "-" | "." | "," | |||
de-CH | "-" | "'" | "." | |||
da-dk | "-" | "." | "," |
Language | Negative Prefix | Negative Suffix | Positive Prefix | Positive suffix | Grouping sepaarator | Decimal separator |
---|---|---|---|---|---|---|
ru-RU | "-" | "₽" | "₽" | "" | "," | |
en-US | "- $" | "$" | "," | "." | ||
iw-IL | "-" | "₪" | "₪" | "," | "." | |
ar-AE | "-" | "د.إ." | "د.إ." | "٬" | "٫" | |
fr-FR | "-" | "€" | "€" | "" | "," | |
de-de | "-" | "€" | "€" | "." | "," | |
de-CH | "CHF-" | CHF | "'" | "." | ||
da-dk | "-" | "kr." | "kr." | "." | "," |
Language | Negative Prefix | Negative Suffix | Positive Prefix | Positive suffix | Grouping sepaarator | Decimal separator |
---|---|---|---|---|---|---|
ru-RU | "-" | "%" | "%" | "" | "," | |
en-US | "-" | "%" | "%" | "," | "." | |
iw-IL | "-" | "%" | "%" | "," | "." | |
ar-AE | "-" | "٪" | "٪" | "٬" | "٫" | |
fr-FR | "-" | "%" | "%" | "" | "," | |
de-de | "-" | "%" | "%" | "." | "," | |
de-CH | "-" | "%" | "%" | "'" | "." | |
da-dk | "-" | "%" | "%" | "." | "," |
Moreover, the formatting results for Android SDK and JDK may be different. Moreover, all options are correct, each of them is used in certain contexts.
When we create NumberFormat
to format certain values, we get objects of the DecimalFormat
class that are simply configured by different templates. By DecimalFormat
object to the DecimalFormat
type and using its interface, you can change parts of the template to break everything. But it is better to worship givenness.
You can also write a test to enjoy the variety. Not for all locales the same currency is displayed with a symbol.
The general scheme of the solution is as follows.
AppActivity
life cycle is the life cycle of the entire application. Therefore, it is enough to recreate it to restart the entire application and apply the selected language. And since there is only one activity, it’s enough to keep the subscription for changing the language in one place - in AppPresenter
.
As we saw, regional formats for outputting numbers are not trivial. You should not rigidly set a single template for all occasions. It is better to entrust the formatting of the SDK and agree that the numbers will be displayed according to the standard, and not as drawn on the layouts.
To save time, you can use the following flag.
android { ... buildTypes { debug { pseudoLocalesEnabled true } } ... }
Select the desired pseudo-locale in the phone settings.
And observe how the layout goes because of the long text, and some elements of the UI stubbornly do not want to be displayed from right to left.
More information can be found in the documentation .
It is worth noting that pseudo-locales will not work if you change the context, as in the solution above. You are changing the context. Therefore, you must add en-XA
and ar-XB
to the language selection list inside the application.
That's all. Have a good localization and good mood!
Source: https://habr.com/ru/post/461085/
All Articles