With Android’s expansive popularity among people from all over the world, getting Android localization done right has become a crucial step in app development.
You may be a novice developer building your very first Android app, or you may be an expert programmer getting yet another Android app under your belt to join a dozen others. But, let me ask you a question: Who are you creating this app for? In other words, what’s your target audience?
Hopefully, you want your app to grow and be used beyond your local community; to reach the across continents, and ultimately go global. Here’s the sticking point: most of the world does not speak English. Sooner or later, you will need to support multiple languages.
This warrants the need to localize your Android application.
In this article, we will explain how to get started with Android localization using step-by-step examples.
We will cover the following topics in this tutorial:
- Android i18n/l10n.
- Adding resources with the help of Android Studio l10n features.
- Programmatically switching locale using proxied
Context
. - Noun pluralization with Android quantity strings.
- Date-time localization.
- Setup and use of Lokalise OTA (over-the-air).
Check out how our platform can help you localize your Android apps faster.
Huge thanks to my colleague Illia Derevianko for technical review and assistance.
The source code is available on GitHub.
How to localize your Android App
First and foremost, let’s create a new Android project with an “Empty Activity” template provided by Android Studio. For demonstration purposes, let’s make a project with the following configuration:
Name: LokaliseI18n Package name: com.example.lokalisei18n Language: Kotlin Minimum SDK: API 31
Add some elements
Now it’s time to put some content into this empty MainActivity
. Open up the activity_main.xml
file located the in the app/src/main/res/layout
directory. Let’s put a simple TextView
with the string “Hello world!” in it:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:padding="10dp" android:gravity="center_horizontal" tools:context=".MainActivity"> <TextView android:id="@+id/test_text_view" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello world!" android:textSize="26sp" /> </LinearLayout>
But there’s an issue here: the “Hello world!” string value is embedded directly within the layout description of the TextView
itself. As you may already know, it is elementary level bad practice to hardcode string values within the code of an application. On top of that, you would need this text to dynamically change in case your Android localization application’s user decides to change the app’s language.
Language resources
How about stocking up our Android app with some language resources holding localization-related values? These will act as static assets holding language content for multiple languages.
The Android SDK provides a specific structure to hold Android app resources. Following this pattern, look inside the res/values
directory and you’ll be able to find a strings.xml
resource file holding the text strings used in our android-i18n
application.
Let’s add a new string resource in strings.xml
to hold the "Hello world!"
string:
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="app_name">LokaliseI18n</string> <string name="hello_world">Hello world!</string> </resources>
Note that we’ve provided the hello_world
key to refer to our sample text string value "Hello World!"
. It is acceptable to do so in simple contexts such as this, but please make it a habit to come up with self-explanatory names for your keys. You can check our blog post that summarizes some recommended practices when working with translation keys.
Add more resources
We can call upon Android Studio l10n support to conveniently add another resource file. Let’s create a new resource file for the Latvian language using Android Studio’s resource file wizard.
Firstly, right-click on the res
folder and choose New > Android resource file. The wizard will let you choose from a list of available resource qualifiers for your new folder. Qualifiers are used by the system to decide which resources to load based on the user’s device configuration, such as selected language, screen size, country code, etc. Keep in mind that these qualifiers can be used for other types of resources such as images or layouts as well. You can refer to this Android documentation to learn more about app resources.
Secondly, select Locale from the list of available qualifiers and add it to the chosen qualifiers. Now to add a Latvian language resource file, choose “lv: Latvian” from the language list. We can keep the Specific region only setting at its default value of “Any region” since we will not be narrowing down our localization to particular regions for this example.
Thirdly, set the File name to “strings.xml”. You will notice that Android Studio has automatically set the Directory name to values-lv:
Then lastly, click the OK button and a strings.xml
file inside the res/values-lv/
directory should be opened.
Now, let’s add Latvian language resources to this XML file. Be sure to add the same keys as when creating the strings.xml
for English US:
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="app_name">LokaliseI18n</string> <string name="hello_world">Sveika pasaule!</string> </resources>
Great job!
Refer the resources
Once we have our resources properly set up for two languages, let’s reference them in our TextView
. Open up the TextView
in the res/values/activity_main.xml
layout. Use the resource key hello_world
inside the android:text
attribute of the TextView
:
<TextView android:id="@+id/test_text_view" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/hello_world" android:textSize="26sp" />
Refer resources dynamically
If you need to dynamically change the strings used in a TextView
, you can do it programmatically within the MainActivity.kt
file, as follows:
package com.example.lokalisei18n import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import android.widget.TextView import androidx.appcompat.app.ActionBar class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val actionBar: ActionBar? = supportActionBar actionBar?.setTitle(R.string.app_name) // 1 val textView = findViewById<TextView>(R.id.test_text_view) textView.setText(R.string.hello_world); // 2 } }
Main things to note here:
- Set the title of this activity’s
ActionBar
to the string resourceapp_name
. - Set the text of
test_text_view
to the string resourcehello_world
.
Now our application is successfully localized for two languages.
You might be thinking…
It looks the same as before, duh.
But as a matter of fact, we’re no longer showing a hardcoded "Hello world!"
value on the TextView. Instead, in its place, we are now representing the test_text_view
resource localized according to our Android device’s system language. We can test this out by running our android-i18n
application on an Android device with its system language set to Latvian.
Following the same pattern, we can now present our application not just in US English, but also in countless other languages.
However, how do we switch our app to another language without touching the device settings? We’ll explore this in the next section.
Change the application locale programmatically
By default, Android will try to load resources based on the system language set on the user’s device. Therefore, if a Latvian user, Justine, with her Android set to the Latvian language, opened our application on her phone, she’d see an app localized to her own language. But what if another user wants to use Latvian for his app on a smartphone that has its default language set to English?
To deal with this, we will have to programmatically override the default locale set in the user’s system. Create a new utils/ContextUtils.kt
file with an updateLocale
method:
package com.example.lokalisei18n.utils import android.content.Context import android.content.ContextWrapper import android.os.LocaleList import java.util.Locale class ContextUtils(base: Context) : ContextWrapper(base) { companion object { fun updateLocale(context: Context, localeToSwitchTo: Locale): ContextUtils { val resources = context.resources val configuration = resources.configuration // 1 val localeList = LocaleList(localeToSwitchTo) // 2 LocaleList.setDefault(localeList) // 3 configuration.setLocales(localeList) // 4 val updatedContext = context.createConfigurationContext(configuration) // 5 return ContextUtils(updatedContext) } } }
- Create a new
configuration
object using the resource configuration in the givenContext
. - Create a locale list containing the locale to which we plan to switch.
- Assign the default locale for this instance of the JVM to the locale at the first index on this list. Since there’s only one locale present in our example, this will inadvertently set the locale on the
localeToSwitchTo
variable as the default locale. - Set the locale list we created in step 2 to the configuration object we created in step 1 using a setter.
- Set the current
context
variable to a newContext
object whose resources are adjusted to match the given configuration.
With our ContextUtils
sorted out, let’s see how to use it on our app to change locale. Let’s open up the MainActivity.kt
file and put in an overriding method like the below:
package com.example.lokalisei18n import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import android.widget.TextView import androidx.appcompat.app.ActionBar import java.util.Locale import android.content.Context import com.example.lokalisei18n.utils.ContextUtils class MainActivity : AppCompatActivity() { // ... override fun attachBaseContext(newBase: Context) { val localeToSwitch = Locale("lv") val localeUpdatedContext = ContextUtils.updateLocale(newBase, localeToSwitch) super.attachBaseContext(localeUpdatedContext) } }
This code firstly creates a new ContextWrapper
with its locale updated to the Latvian language. Secondly, it passes this newly created ContextWrapper
to be attached as the context used in MainActivity
. Afterward, when this activity loads on startup, it will work on a context that has its locale set to Latvian.
That’s it! Simply run the application and voilà, the text has successfully been localized without any manual changes to the Android’s system language on our end.
Some Android localization extras
Let’s take a look at a few other things you’ll surely run into on your Android app localization journey.
How to pluralize nouns
When dealing with nouns, we need them to be properly pluralized according to a particular language’s standards. For instance, we don’t want our Android application to say, “I have 3 cat” instead of “3 cats“, right? This is where Android’s built-in support for quantity strings (plurals) comes in handy. Let’s try it out!
First off, add some TextViews to your activity_main.xml
:
<TextView android:id="@+id/plural_view_one" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="26sp" /> <TextView android:id="@+id/plural_view_few" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="26sp" /> <TextView android:id="@+id/plural_view_many" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="26sp" />
These will consecutively hold text for one, few, and many quantities of objects.
As you may know, pluralization rules usually differ from language to language. English only has two main forms, i.e.:
- I have 0 cats
- I have 1 cat
- I have 3 cats
While Latvian has three variations when counting whole numbers, i.e.:
- Man ir 0 kaķu
- Man ir 1 kaķis
- Man ir 3 kaķi
Therefore, we will have to create separate plurals
elements for each of these languages.
Let’s add these pluralizations in our project’s respective language resource files. First, edit the res/values/strings.xml
file:
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="app_name">LokaliseI18n</string> <string name="hello_world">Hello world!</string> <plurals name="my_cats"> <item quantity="one">I have %s cat</item> <item quantity="other">I have %s cats</item> </plurals> </resources>
Next, take care of the res/values-lv/strings.xml
file:
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="app_name">LokaliseI18n</string> <string name="hello_world">Sveika pasaule!</string> <plurals name="my_cats"> <item quantity="zero">Man ir %s kaķu</item> <item quantity="one">Man ir %s kaķis</item> <item quantity="other">Man ir %s kaķi</item> </plurals> </resources>
In case you’re interested, the syntax for plurals
elements is documented in the syntax
section of the Android documentation for quantity strings.
Now let’s set some plural text values in the TextViews we created earlier. Add these lines to the onCreate
method of the MainActivity
class:
override fun onCreate(savedInstanceState: Bundle?) { // ... with (resources) { val pluralViewOne = findViewById<TextView>(R.id.plural_view_one) val quantityStringFor1 = getQuantityString(R.plurals.my_cats, 0, 0) pluralViewOne.text = quantityStringFor1 val pluralViewFew = findViewById<TextView>(R.id.plural_view_few) val quantityStringFor2 = getQuantityString(R.plurals.my_cats, 1, 1) pluralViewFew.text = quantityStringFor2 val pluralViewMany = findViewById<TextView>(R.id.plural_view_many) val quantityStringFor5 = getQuantityString(R.plurals.my_cats, 3, 3) pluralViewMany.text = quantityStringFor5 } }
Refer to the documentation on the Resources.getQuantityString method to get an idea of its parameter usage.
Now run the app and make sure the text is pluralized properly. Nice!
What about date and time?
We can call upon the getDateTimeInstance
method provided in the DateFormat class to handle the localization of date and time in Android projects.
Firstly, let’s add a simple TextView
within our application’s activity_main.xml
:
<TextView android:id="@+id/localized_date_time_view" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="26sp" />
Then, let’s fill this TextView
with text showing a localized date and time upon launch of our app. Open up the MainActivity
class and put this code in its onCreate
method:
// other imports... import java.text.DateFormat import java.util.Date class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { // ... val currentDateTime = Date() // 1 val localizedDateTimeView = findViewById<TextView>(R.id.localized_date_time_view) localizedDateTimeView.text = DateFormat.getDateTimeInstance().format(currentDateTime) // 2 } } // ... }
- Create a new Date object with a no-arguments constructor and allocate it to
currentDateTime
. Using this constructor makes theDate
object represent the exact time (and date) at which it was allocated. - The
DateFormat.getDateTimeInstance
static factory method is used to automatically formatcurrentDateTime
for the current locale.
You can now run the app to make sure the datetime is localized properly.
Lokalise saves the day
So my friend, if you’ve come this far in the article, you must be dead set on somehow getting your Android app internationalized. But let’s face it, do you — or your team — really have the time to learn all of these new classes and methods and deal with deprecated variables, just to localize your app?
Yeah, I heard you. You don’t.
As you add more features and expand to more markets, your app will grow to thousands of strings in dozens of languages. Surely you could use the help of a proper translation management system to carry out your Android app’s i18n while keeping yourself sane, am I right?
That’s where Lokalise comes in. Let’s see how it works.
First off, create an account or log in to Lokalise—either option will bring you to the Lokalise dashboard.
Secondly, click the New project button and create a localization project:
Thirdly, click the Upload files button to upload the language resource files of the project you plan to translate.
In the case of the Android application we created during the tutorial, you can choose the strings.xml
files we created for the two languages:
Note: If the detected language is incorrect, feel free to select the correct language from the dropdown.
Click the Import files button to complete the resource uploading process. Head back to the Editor
tab of the project’s Lokalise dashboard. You will see the resources neatly organized:
Now, you might be thinking…
Okay, Lokalise has my project’s language resources. How exactly is this useful for me or my organization?
Time to see how!
Generate resource bundle
No need to change countless text files if you need to change some resource values. With Lokalise, you can make resource value changes in a nice UI, and download automatically generated translation files for all your project’s locales for the specified platform.
For instance, suppose you are a dog person and wanted to change the resources to display dog values instead of cat. Simply click on the corresponding translation and edit it as needed:
The proceed to the Download page to download your new translations. Choose the Android Resources (.xml) file format from the list.
Press the Build and download button to obtain a ZIP archive with your new translations. Then simply extract the archive and replace the old files in your project with the new ones.
That was pretty fun and easy, wasn’t it? The Lokalise editor is a feature-rich and user-friendly interface that includes collaborative translation, translation quality assurance, automatic translation suggestions, professional translation services, and loads more to be used for your Android app’s localization.
“On the disk” to “Over the air”
With Lokalise, you also have access to a feature called “over-the-air” (OTA) that enables you to deliver translations the end users without the need to rebuild and republish the app. This is a very convenient tool if you want ot perform minor adjustements to the translations, fix some typos, and so on. Let’s see how to get started with OTA.
Return to the Download page and choose Android SDK file format (it can be found under the Mobile SDK section):
Scroll to the bottom of the page and click Build only. This will take you to the OTA bundles page where you can manage the generated bundles as you please (if it doesn’t, proceed to More > Settings > OTA bundles). Make sure to switch to the Android SDK tab:
Here you can see your generated bundle. Click on the Untitled text to give it a name if you’d like. You can learn more about OTA and its main concepts in our Developer Hub.
How to update localization values
Next, open the settings.gradle
file and add config for Maven:
dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() maven { url 'https://maven.lokalise.com' } } }
Add the Lokalise Android SDK dependency to the build.gradle
file:
dependencies { // other dependencies... implementation('com.lokalise.android:sdk:2.1.1') { transitive = true } }
Finally, it’s time to initialize the Lokalise Android SDK into your app.
A few prerequisites
Head over to your Lokalise project’s dashboard and click More > Settings. Here, take note of the Project ID, click Generate new token, and note down the resulting token value as well:
In order to let Lokalise handle its localization throughout the app, we’d need to initialize the SDK before other processes start. Let’s achieve this by adding a custom Application class inheriting from android.app.Application
. Create a new MyApplication.kt
file:
package com.example.lokalisei18n import android.app.Application import com.lokalise.sdk.Lokalise class MyApplication : Application() { override fun onCreate() { super.onCreate() Lokalise.init(this, "YOUR_TOKEN", "YOUR_PROJECT_ID") // 1 Lokalise.updateTranslations() // 2 } }
Important note: Make sure to always place these lines right after the call to super.onCreate()
.
- Initialize the Lokalise SDK by calling the
Lokalise.init
method. Remember to pass the SDK token and project ID noted down in the previous step as the method parameters. - Bring the latest localization values from Lokalise over to the
android-i18n
project.
Let’s not forget to introduce this MyApplication
to project’s AndroidManifest.xml
file:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <application android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/Theme.LokaliseI18n" tools:targetApi="31" android:name=".MyApplication"> <activity android:name=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
I’ve added the android:name=".MyApplication"
line to this file.
Now it’s time to ask your application to use an Activity context that has the Lokalise SDK injected into it. Open the MainActivity.kt
file and tweak the attachBaseContext
method in the following way:
override fun attachBaseContext(newBase: Context) { super.attachBaseContext(LokaliseContextWrapper.wrap(newBase)) }
You’re good to go! Next time, all updated localizations on the latest bundle will be sent over the air straight to your Android app!
Test it out
To test everything out, you can simply run your application and make sure the English text with plurals says “dog” instead of “cat” (if you’ve already created a bundle as explained in the previous section).
Of course, you can perform additional changes in the Lokalise editor and generate a new Android SDK bundle. However, in this case make sure to set this new bundle as production and click Save changes:
You can learn more about Android SDK and OTA bundles in our Developer Hub. I would also recommend checking the best practices that will help you optimize OTA bundle sizes.
Conclusion
In this tutorial, we checked out how to localize an Android application to multiple locales. We explored various API deprecations that caused Android l10n to become less of a walk in the park. Next, saw how we can switch to different languages; either by changing the device’s language or forcefully switching the application to a modified Context
. Then, we discovered how Lokalise helps us swiftly retrieve our localizations to our Android app OTA (over the air).
In addition to this, we looked into how we can pluralize nouns using Android quantity strings, and how to perform date-time localization.
Until next time, cheers to you!
Further reading
- How we created Lokalise-loader, an Android library for faster localization (blog post by OrangeSoft, covering their custom-made solution)
-
How to choose the best translation management system for your team and company