Slide 1

Slide 1 text

Hi! Oi! Aluu! Jambo! Salut ! Demystifying Locale on Android Julien Salvi

Slide 2

Slide 2 text

Lead Android Engineer @ Aircall Julien Salvi Android GDE PAUG, Punk and IPAs! @JulienSalvi Bonjour !

Slide 3

Slide 3 text

TABLE OF CONTENTS 01 WHAT’S A LOCALE? Locale explained and what’s behind it 02 LOCALES IN ACTION! Use Locale in your Android app and handle localization 03 COMMON PITFALLS Beware the Locale! It can be very tricky

Slide 4

Slide 4 text

WHAT’S A LOCALE? Locale explained 01 ¡Hola!

Slide 5

Slide 5 text

— Android documentation “An object that represents a specific geographical, political, or cultural region. An operation that requires a Locale to perform its task is called locale-sensitive and uses the Locale to tailor information for the user.”

Slide 6

Slide 6 text

Locale on Android // Default Locale of your application val defaultLocale = Locale.getDefault() // Locale from a given IETF BCP 47 language tag val localeByTag = Locale.forLanguageTag("fr-CA") val localeFrCA = Locale("fr", "CA") // Create a Locale with Locale.Builder() val bLocale = Locale.Builder() .setLanguage("sr") .setScript("Latn") .setRegion("RS") .build()

Slide 7

Slide 7 text

Locale on Android // Used as the language/country neutral locale // for the locale sensitive operations val root = Locale.ROOT // French language Locale without country restriction val fr = Locale.FRENCH // French Locale for France val frFR = Locale.FRANCE // Similar to Locale(“fr”, “FR”)

Slide 8

Slide 8 text

Locale on Android // First Locale of your device since API 24 val fLocale = Resources.getSystem().configuration.locales[0] // Compat method val fLocale = ConfigurationCompat.getLocales(Resources.getSystem().configuration)[0] // The application Locale + all device Locales since API 24 val list = LocaleList.getDefault()

Slide 9

Slide 9 text

So Locale are based on languages and countries? Well… yes but it is more complex

Slide 10

Slide 10 text

IETF Language tag zh-Latn-TW-pinyin (Chinese spoken in Taiwan in pinyin) language-extlang-script-region-variant-extension-privateuse th-TH-u-nu-thai (Thai language with thai numbers)

Slide 11

Slide 11 text

Constructing language tags language: ● Primary subtag that identifies the language. fr (French), en (English) or hi (Hindi) extlang: ● Secondary language subtag. Associated to the primary tag. zh-yue (Cantonese Chinese) or ar-afb (Gulf Arabic) language-extlang-script-region-variant-extension-privateuse

Slide 12

Slide 12 text

Constructing language tags script: ● ISO 15924 alpha-4 script code that describes how the text is written. Latn (Latin alphabet), Cyrl (Cyrillic) or Hans (Simplified Chinese) region: ● ISO 3166 alpha-2 country code or UN M.49 numeric-3 area code CA (Canada), GE (Georgia) or 029 (Caribbean) language-extlang-script-region-variant-extension-privateuse

Slide 13

Slide 13 text

Constructing language tags variant: ● Subtags that represents dialects or script variation not covered by combinations of language, script and region subtags. sl-nedis (Nadiza dialect), de-CH-1901 (German variant from 1901 reform ) language-extlang-script-region-variant-extension-privateuse

Slide 14

Slide 14 text

Constructing language tags language-extlang-script-region-variant-extension-privateuse extension & privateuse: ● Used for additional information about the language. It often starts with “u-” for extension and “x-” for privateuse de-DE-u-co-phonebk or en-US-x-twain

Slide 15

Slide 15 text

LOCALE IN ACTION! 02 Jambo! Localization and Locale in your Android apps

Slide 16

Slide 16 text

Where to use Locale on Android?

Slide 17

Slide 17 text

Where Locale are used? გამარჯობა! Dates 28 Февраль 2021 г 󰐮 Currencies ¥67,889,786.50 󰎩 Numbers 76 876,50 󰏃 Text TEŞEKKÜR EDERİM 󰑍

Slide 18

Slide 18 text

Locale on Android // Dates SimpleDateFormat("dd MMMM yyyy", Locale.FRENCH) // Texts "teşekkür ederim".toUpperCase(Locale("tr", "TR")) // Numbers NumberFormat.getInstance(Locale.ENGLISH) NumberFormat.getInstance(Locale.ROOT) // Currencies NumberFormat.getCurrencyInstance(Locale.GERMANY) NumberFormat.getCurrencyInstance(Locale.CHINA) NumberFormat.getCurrencyInstance(Locale.ROOT) 28 Février 2021 TEŞEKKÜR EDERİM 10,000.50 67.889.786,50 € ¥67,889,786.50 ¤ 67,889,786.50

Slide 19

Slide 19 text

Locale resolution on Android

Slide 20

Slide 20 text

Locale resolution res ├── values ├── values-fr-rFR // French from France ├── values-de // German ├── values-es // Spanish ├── values-es-rAR // Spanish from Argentina └── values-iro // Iroquoian languages Only from API 21

Slide 21

Slide 21 text

Locale resolution Prior to Android 7 🛑 Failure because French Candian does not match any language in the app Since Android 7 🟢 Success! The resolution changed: it looks at the parent tag first, then the children of the language 1 Français 󰎟 2 Italiano 󰏢 3 Español 󰎆 4 English 󰑔

Slide 22

Slide 22 text

Multiple string resources

Slide 23

Slide 23 text

Multiple string resources Awesome String : %1$s Unique String

Slide 24

Slide 24 text

Multiple string resources First row Second row String Strings String No strings

Slide 25

Slide 25 text

No content

Slide 26

Slide 26 text

What’s Lokalise? ● Manage translations and handle language switch in your app ● Dynamic update, cross-platform tool (Android, iOS…) ● Can be fully automated with your CI/CD ● Other alternatives: PhraseApp, Transifex, Smartling...

Slide 27

Slide 27 text

Setup Localise implementation("com.lokalise.android:sdk:2.0.0-beta10") // In the onCreate() of your Application.kt Lokalise.init( appContext, BuildConfig.LOKALIZE_TOKEN, BuildConfig.LOKALIZE_PROJECT_ID )

Slide 28

Slide 28 text

Handle language switch // Wrap your base context with Lokalize class BaseActivity : AppCompatActivity() { override fun attachBaseContext(newBase: Context) { super.attachBaseContext(LokaliseContextWrapper.wrap(newBase)) } } // Switch language at runtime val language = "fr" val region = "CA" Lokalise.setLocale(language, region)

Slide 29

Slide 29 text

Lokalise + CI/CD Automate your translation process with Lokalise and your CI “https://medium.com/inside-aircall/4547855fce55”

Slide 30

Slide 30 text

COMMON PITFALLS Beware the Locale! 03 Aluu!

Slide 31

Slide 31 text

Beware 3rd party libraries ⚠ Third party libraries bring new string resources to your app What happens if unsupported string resources are added?

Slide 32

Slide 32 text

Beware 3rd party libraries ?? Let’s say we have the string “developer” R.string.abc_string translated in many languages. Our app supports English (default), French and German. What’s going to be the value of the string if we switch the device to Italian?

Slide 33

Slide 33 text

Beware 3rd party libraries sviluppatore Bringing 3rd party libraries to your app without control can lead to multiple language texts. Make sure to use English as string default. How can we add some restriction?

Slide 34

Slide 34 text

Beware 3rd party libraries android { defaultConfig { resConfigs "en", "fr", "de" // All supported languages } }

Slide 35

Slide 35 text

App Locale vs device Locale

Slide 36

Slide 36 text

App Locale vs device Locale ⚠ Locale.getDefault() == app language ⚠ Locale.getDefault() ≠ device language Locale.getDefault() is the current value of the default locale for the instance of the JVM

Slide 37

Slide 37 text

App Locale vs device Locale // Default Locale of your application val defaultLocale = Locale.getDefault() // Current Locale of your device // Since API 24 val fLocale = Resources.getSystem().configuration.locales[0] // Compat method val fLocale = ConfigurationCompat .getLocales(Resources.getSystem().configuration)[0]

Slide 38

Slide 38 text

Fear the WebView!

Slide 39

Slide 39 text

Fear the WebView! Starting with Android 7, Google decided to change the way WebView is launched and use Google Chrome instead. ⚠ Opening a WebView will revert the app Locale to the device default language 😃 This issue has already been reported to Google but...

Slide 40

Slide 40 text

Fear the WebView! // init and load webview setupWebview() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { val res = context.resources val config = res.configuration // App locale from pref, storage... val appLanguage = languageGateWay.getAppLocale() config.setLocale(appLanguage) res.updateConfiguration(config, res.displayMetrics) }

Slide 41

Slide 41 text

Fear the WebView! override fun attachBaseContext(newBase: Context) { var base: Context = newBase if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { val config = resources.configuration val locales = languageGateWay.getSupportedLocales() config.setLocales(locales) base = createConfigurationContext(config) } super.attachBaseContext(base) }

Slide 42

Slide 42 text

Want to know more? Demystifying Locale on Android “https://medium.com/inside-aircall/95450adf5aec” Automate your translation process with Lokalise and your CI “https://medium.com/inside-aircall/4547855fce55” IETF's BCP 47 “https://www.rfc-editor.org/rfc/bcp/bcp47.txt”

Slide 43

Slide 43 text

CREDITS: This presentation template was created by Slidesgo, including icons by Flaticon, and infographics & images by Freepik Thanks! Do you have any questions? @JulienSalvi Aluu! გამარჯობა! Jambo! Julien Salvi