
Machine translation
Software localization
Android has been the leading mobile OS worldwide for quite a while. In the summer of 2020, it controlled 74.6 percent of the entire mobile OS market. If you're looking for best practices for building a multilingual Android app, the following Android app development tutorial will walk you through all the dos and don'ts.
Before we get it started, let's note that the Android application framework is powered by several i18n and l10n libraries/utilities. This means you don't need to import any external i18n or l10n libraries to enable multi-language support for your Android app. We can now start digging into the details!
The internationalization framework is part of the wider Android application framework, in which some – if not all – components rely on internationalization. Put another way, Android already provides a set of libraries within its SDK, such as date-time formatting, number formatting, message formatting, currency, measurement units, timezone, collation rules, plural rules, gender rules, transliteration, Bidi layout support, Emoji support, etc.
Let's say you're developing an app focused on European financial markets and need to handle all date-time differences, as well as identify both the currency and numbering system of each region. Ideally, you can refer to SDK classes, which provide full, standardized support. Additionally, Android has the localization framework, responsible for localizing the OS itself, as well as all applications. It also provides mechanisms for locale awareness, resource loading, locale matching, fallback, etc.
In general terms, Android leverages the ICU library and CLDR project to provide Unicode and other internationalization support.
🔗 Resource » Get a brief overview of ICU and CLDR in The Missing Guide to the ICU Message Format.
The relation between Unicode CLDR, ICU, and Android
Now that we're familiar with the basics of the Android i18n & l10n framework, we'll focus on four topics crucial to the development of multilingual Android apps:
To make sure an Android app is global-ready, we need to take care of a few key configurations both in the Manifest
and Gradle
file.
In the AndroidManifest.xml
file, we need to set the android:supportsRtl
property to true
. This property declares whether your application is willing to support right-to-left (RTL) layouts. Its default value is false
.
<manifest ... > ... <application ... android:supportsRtl="true"> </application> </manifest>
🗒️ Note » In the section on bidirectionality below, you can find the complete information on RTL layout support.
In the build.gradle
file, we need to set the following configurations:
targetSdkVersion
→ 17 (or higher)
An integer designating the API level that the application targets, where 17 is the minimal value accepted because prior to API 17, Android APIs didn't support right-to-left layouts.
resConfigs
→ list of supported locales
A list of the locales that the app supports.
android { ... defaultConfig { targetSdkVersion 17 // Or higher ... resConfigs "en", "da_DK", "de_DE", "it_IT" … } }
Why is the resConfigs
property necessary at all? Well, you need to make sure your application will work properly for the second-preferred locale setting. In other words, in case a user sets additional languages on their mobile device, your app will try to match all configurations of the device, including secondary, tertiary languages, etc.
See the scenario below:
User Settings
App Resources
Which language content should be picked and displayed for the user considering these settings?
Since Dutch (Belgium) is the primary option, and there are no resources in this language, as expected, the secondary option, which has content for the German language, is picked.
You may wonder why de_DE is picked despite de_BE being the user locale. Let's explain the resource-resolution strategy used by Android to define the best matching:
Use de_DE
With that, the user still has a language they understand, even though the app doesn’t support Dutch. However, it only happens due to the resConfigs
property.
In case your app imports any Android library, and you don't add the resConfigs
property, it will not work properly, and probably the default content (English) will be displayed, which wouldn't be that good from a UX perspective.
Localizability is the practice of enabling software to be localized into different languages without changing its source code. At this point, you should pay attention to the following steps:
The steps mentioned above are also applicable to other Android resources, such as layouts, sounds, graphics, and other data your app may need.
For more details on Android resources, feel free to check out the App resources overview.
Let's move on to some best-practice examples of localizability!
✋🏽 Heads up! » In the following overview, we're using the approach of do’s and don’ts. Please note that some of the don'ts below may work or even produce the same result for certain languages. Thereby, our goal isn't to point out errors but provide better solutions to localizability regardless of the locale selected.
Do
strings.xml <string name="home">HOME</string> Java: String strHome = res.getString(R.string.home); Log.d("phrase_i18n", strHome); Output (en-US): "HOME"
Don't
strings.xml <string name="home">Home</string> Java: String strHome = res.getString(R.string.home); String curHOME = strHome.toUppercase(strHome); Log.d("phrase_i18n", curHOME); Output (en-US): "HOME"
Using those methods without specifying a concrete locale can be a hot source of bugs. It happens because those methods will use the current locale on the user's device, and even though the code appears to work correctly when you're developing the app, it will fail in some locales.
Some case mappings depend on language or locale. A good example is Turkish, where the uppercase letter 'I' maps to the dotless lowercase 'ı', and the uppercase replacement for 'i' is not 'I'.
Example: (en) QUIT ⇒ quit / (tr) QUIT => quıt
If you want the methods to just perform an ASCII replacement for converting an enum name, call toUpperCase(Locale.ENGLISH)/toLowerCase(Locale.ENGLISH)
instead. If you really want to use the current locale, call toUpperCase(Locale.getDefault())/toLowerCase(Locale.getDefault())
instead.
For further details, click here.
Do
strings.xml <string name="hello_phrase_friend">Say hello to your Phrase's friend</string> <string name="hello_phrase_colleague">Say hello to a Phrase's colleague</string> Java: String helloFriend = res.getString(R.string.hello_phrase_friend); String helloColleague = res.getString(R.string.hello_phrase_colleague); Log.d("phrase_i18n", helloFriend); Log.d("phrase_i18n", helloColleague); Output (en-US): "Say hello to your Phrase's friend" "Say hello to a Phrase's colleague"
Don't
strings.xml <string name="say_hello">Say hello to</string> <string name="phrase_friend"> your Phrase's friend</string> <string name="phrase_colleague"> a Phrase's colleague</string> Java: String sayHello = res.getString(R.string.say_hello); String friend = res.getString(R.string.phrase_friend); String colleague = res.getString(R.string.phrase_colleague); Log.d("phrase_i18n", sayHello + friend); Log.d("phrase_i18n", sayHello + colleague); Output (en-US): "Say hello to your Phrase's friend" "Say hello to a Phrase's colleague"
Different languages have different rules for word order, capitalization, gender, singular, or plural, which may result in terrible grammar mistakes.
Do
- Place non-translatable resources in a separated file (donottranslate.xml) or Use translatable="false" property <string name="phrase_act_name" translatable="false">PhraseActivity</string>
Don't
strings.xml <string name="phrase_act_name">PhraseActivity</string> values-fr-rFR: <string name="phrase_act_name">PhraseActivity</string>
Do
strings.xml <!-- The action for submitting a form. This text is on a button that can fit 30 chars --> <string name="login_submit_button">Sign in</string>
Don't
strings.xml <string name="login_submit_button">Sign in</string>
In general, a translation tool only shows the comment for the string currently being edited/translated. Consider providing context information that may include:
Do
values/strings.xml <!-- Example placeholder for a name --> <string name="prod_name">Learn more at <xliff:g id="company">Phrase</xliff:g> blog</string> <!-- Example placeholder for a literal --> <string name="promo_message">Use \"<xliff:g id="promotion_code">PHRASEDISCOUNT</xliff:g>\" code to get a discount</string> <string name="countdown"><xliff:g id="time" example="5">%1$d</xliff:g> days until promotion</string> values-pt/strings.xml <string name="prod_name">Saiba mais no blog da <xliff:g id="company">Phrase</xliff:g></string> <string name="promo_message">Utilize o código \"<xliff:g id="promotion_code">PHRASEDISCOUNT</xliff:g>\" para obter um desconto</string> <string name="countdown"><xliff:g id="time" example="5">%1$d</xliff:g> dias até a promoção</string> Java: String title = res.getString(R.string.prod_name); String promoMessage = res.getString(R.string.promo_message); int days = getSharedPreferences("prefs", MODE_PRIVATE).getInt("days", 0); // 5 String countdownPromo = res.getString(R.string.countdown, days); Log.d("phrase_i18n", title); Log.d("phrase_i18n", promoMessage); Log.d("phrase_i18n", countdownPromo); Output (en-US): "Learn more at Phrase blog" "Use "PHRASEDISCOUNT" code to get a discount" "5 days until promotion" Output (pt): "Saiba mais no blog da Phrase" "Utilize o código "PHRASEDISCOUNT" para obter um desconto." "5 dias até a promoç˜ão"
Don't
values/strings.xml <!-- Example placeholder for a name --> <string name="prod_name">Learn more at Phrase blog</string> <!-- Example placeholder for a literal --> <string name="promo_message">Use \"PHRASEDISCOUNT\" code to get a discount</string> <string name="countdown">%s until holiday</string> values-pt/strings.xml <string name="prod_name">Saiba mais no blog da Frase</string> <string name="promo_message">Use o código DESCONTOFRASE para obter um desconto</string> <string name="countdown">%s dias até a promoção</string> Java: String title = res.getString(R.string.prod_name); String promoMessage = res.getString(R.string.promo_message); int days = getSharedPreferences("prefs", MODE_PRIVATE).getInt("days", 0); String countdownPromo = res.getString(R.string.countdown, days); Log.d("phrase_i18n", title); Log.d("phrase_i18n", promoMessage); Log.d("phrase_i18n", countdownPromo); Output (en-US): "Learn more at Phrase blog" "Use "PHRASEDISCOUNT" code to get a discount" "5 days until promotion" Output (pt): "Saiba mais no blog da Frase" "Use o código "DESCONTOFRASE" para obter um desconto" "5 dias até a promoção"
To mark the non-translatable content, use the <xliff:g>
tag. Common examples might be a piece of code, a placeholder for a value, a special symbol, or a name. As you prepare your strings for translation, look for, and mark text that should remain as it is, so that the translator doesn't change it.
When you declare a placeholder tag, always add an id attribute that explains what the placeholder is for. If your app replaces the placeholder value later on, be sure to provide an example attribute to clarify the expected use.
In the example above, the lack of XLIFF tags can lead to unexpected translations: Phrase > Frase, PHRASEDISCOUNT > DESCONTOFRASE, etc.
Do
Leave enough space for expansion (15% - 30%) or allow text wrapping and scrolling
Don't
Otherwise: “Одговори на сите” will truncate
As you can see in the example above, the English “Reply all” has a quite long counterpart in Macedonian: “Одговори на сите”.
Do
Use Android Layout to organize the UI elements
Don't
/res/values/strings.xml <string name="header">Watch our Free Webinars to\nlearn all\nabout the newest Phrase features.</string> /res/values-es/strings.xml: <string name="header">Vea nuestros seminarios web gratuitos\npara aprender todo\nsobre las nuevas funciones de frase</string> Text may wraps incorrectly (error prone)
Do
strings.xml <string name =”personsFound”> {count, plural, = 0 {No persons in the list.} = 1 {There is one person in the list.} = other {There are %d persons in the list.} } </string> Java: int personsCount = 18; String msg = getResources().getString(R.plurals.personsFound); String personsFound = MessageFormat.formatNamedArgs(msg, "count", personsCount); Log.d("phrase_i18n", personsFound); Output (en-US): "There are 42 persons in the list."
Don't
strings.xml <plurals name=”personsFound”> <item quantity=”zero”>No persons in the list.</item> <item quantity=”one”>There is one person in the list.</item> <item quantity=”other”>There are %d persons in the list.</item> <plurals> Java: int personsCount = 42; String msg = getResources().getQuantityString(R.plurals.personsFound, personsCount, personsCount); Output (en-US): "There are 42 persons in the list."
The recommended way is using the ICU MessageFormat class, a small library extracted from ICU.
When content is presented horizontally, most scripts display text/characters from left to right, but there are several scripts (such as Arabic, Persian, Hebrew, Urdu) in which the natural ordering of horizontal alignment in the display is from right to left.
The main difference between left-to-right (LTR) and right-to-left (RTL) language scripts is the direction in which content is displayed:
When a UI is changed from LTR to RTL (or vice versa), it’s often called "mirroring". An RTL layout is the mirror image of an LTR layout, and it affects layout, text, and graphics.
When a UI is mirrored, the following changes occur:
These items are not mirrored:
Now, let’s clarify the instructions to enable Bidi support for your app:
As mentioned in the section on configuration above, the android:supportsRtl
property needs to be set, as RTL mirroring is only supported on devices running on Android 4.2 (API level 17) or higher.
Instead of left and right, use start and end, respectively, to set the positions of UI elements in the layout resource files. It allows the FW to align app’s UI elements based on the user’s language settings.
Do
<TextView android:id="@+id/text" android:gravity="start" android:layout_marginStart android:layout_paddingEnd ...
Don't
<TextView android:id="@+id/text" android:gravity="left" android:layout_marginLeft android:layout_paddingRight ...
If you're wondering what would it be the case when all layout files of your app were implemented using left/right properties. Don't worry, Android Studio can do this for you!
Here are some further tips for other UI elements:
The well-known and widely used ViewPager class does not give native support for RTL layouts. If your app has paging behavior using this class, and you want to enable native right-to-left orientation, Google released the 2nd version of ViewPager class last year, bringing several improvements, including RTL layout support. You can find more details here. Google also released a document clarifying the migration process from ViewPager to ViewPager2.
For instance, in case your app has drawables that need to be mirrored for an RTL layout, complete one of these steps based on the target Android version:
-ldrt
resource files.android:autoMirrored="true"
when defining drawable, which allows the system to handle RTL layout mirroring automaticallyTo provide specialized assets for RTL languages, you can use direction- and language-specific resources. Resources inside such directories will only be used for RTL languages or for one specific locale. See below:
res/ layout/ main.xml layout-ldrtl/ main.xml res/ layout/ main.xml layout-ar/ main.xml layout-ldrtl/ main.xml
🗒️ Note I » Language-specific resources take precedence over layout-direction-specific resources, which take precedence over the default resources.
🗒️ Note II » Although you can define specific layouts to a specific language, this is recommended only when strictly necessary. Since Android supports auto mirroring, in majority of the cases, you may be able to define a flexible layout that will work perfectly for both LTR and RTL layouts, so you will not need to maintain several layout files.
To achieve world-readiness, it’s strongly recommended to use Android locale-sensitive APIs. The Android ecosystem leverages the ICU library and CLDR project to provide Unicode and other internationalization support.
Check out the following set of powerful libraries recommended to use during the development of your app:
Let’s have a look at how to best use some of the libraries mentioned above!
Do
Java: String datePattern = DateFormat.getBestDateTimePattern(locale, "MMMMd"); String curDate = DateFormat.format(datePattern , new Date()).toString(); Output: en-US "December 23" zh-CN "12月23日" es-US "23 de diciembre"
Don't
Java: SimpleDateFormat df = new SimpleDateFormat("MMMM d"); String curDate = df.format(new Date()); Log.d("phrase_i18n", curDate); Output: en-US "December 23" zh-CN "12月 23" es-US "diciembre 23"
On Don't output, via SimpleDateFormat
, note that the order of the content remained the same as the skeleton (MMMM d→ month day), regardless of the locale, which means the locale rule was not respected. The month and day data was localized, but the style wasn't.
While using DateFormat.getBestDateTimePattern
, we can observe the whole date, i.e., both style and content are properly localized.
With date, it's very important to pay attention to the pattern date style, which will indicate the output data. You can see the Date/Time format syntax here.
Do
Java: String skeleton = DateFormat.is24HourFormat(context) ? "Hm" : "hm"; String timePattern = DateFormat.getBestDateTimePattern(locale, skeleton); String curtime = DateFormat.format(skeleton, new Time()); Output: en-US "10:44 PM" zh-CN "下午10:44" es-US "10:44 p. m."
Don't
Java: String curTime = DateFormat.format("hh: mm a", new Date()).toString(); Log.d("phrase_i18n", curTime); Output: en-US "10:44 PM" zh-CN "10:44 PM" es-US "10:44 PM"
Similarly to date, it's very important to keep an eye on the time style pattern, which will indicate the output data. You can see the Date/Time format syntax here.
🗒️ Note » ICU on Android does not observe the user's 24h/12h time format setting (obtained from DateFormat.is24HourFormat()). To observe the setting, either use the DateFormat or DateUtils time formatting method, or use the ICU time formatting patterns with appropriate hour pattern symbols ('h' for 12h, 'H' for 24h) for different is24HourFormat() return values.
🔗 Resource » Our dedicated tutorial takes you through localizing date and time formats in Android step-by-step.
Do
Java: NumberFormat nf = NumberFormat.getPercentInstance(); nf.setMaximumFractionDigits(1); String curNumber = nf.format(0.149); Log.d("phrase_i18n", curNumber); Output: en-US "14.9%" fr-FR "14,9 %" tr-TR "%14,9"
Don't
Java: String curNumber = String.format(“%.1f”, 0.149*100) + “%”; Log.d("phrase_i18n", curNumber); Output: en-US "14.9%" fr-FR "14.9%" tr-TR "14.9%"
Do
Java: MeasureFormat.FormatWidth fWidth = MeasureFormat.FormatWidth.SHORT; MeasureFormat mf = MeasureFormat.getInstance(Locale.getDefault(), fWidth); String size = mf.formatMeasures(new Measure(45.7, MeasureUnit.GIGABYTE)); String speedKm = mf.formatMeasures(new Measure(50, MeasureUnit.KILOMETER_PER_HOUR)); String speedMph = mf.formatMeasures(new Measure(50, MeasureUnit.MILE_PER_HOUR)); String tempF = mf.formatMeasures(new Measure(62, MeasureUnit.FAHRENHEIT)); Log.d("phrase_i18n", size); Log.d("phrase_i18n", speedKm); Log.d("phrase_i18n", speedMph); Log.d("phrase_i18n", tempF); Output: en-US "45.7 GB, 50 kph, 50 mph, 62°F"" ru-RU "45,7 ГБ, 50 км/ч, 50 миль/час, 62°F" pt-BR "45,7 GB, 50 km/h, 50 mph, 62 °F" ar-EG "٤٥٫٧ غيغابايت |٥٠ كم/س |٥٠ ميل/س |٦٢°ف"
Don't
strings.xml <string name=”gb”>GB</string> Java: String size = String.format(“%f %s”,45.7, getString(R.string.gb)); int spValue = 50; String speedKm = String.format (“%d kph”, spValue); String speedMph = String.format (“%d mph”, spValue); String tempF = String.format (“%d ºF”, 62); Log.d("phrase_i18n", size); Log.d("phrase_i18n", speedKm); Log.d("phrase_i18n", speedMph); Log.d("phrase_i18n", tempF); Output: en-US "45.7 GB, 50 kph, 50 mph, 62°F" ru-RU "45.7 GB, 50 kph, 50 mph, 62°F" pt-BR "45.7 GB, 50 kph, 50 mph, 62°F" ar-EG "45.7 GB, 50 kph, 50 mph, 62°F"
Note that the MeasureFormat
class is able to format not only the measure units according to the locale but also numbers, spaces, the occurrence of white spaces, etc.
For more information on available measure units, click here. You also can use different widths for the measure units (NARROW, SHORT, WIDE, NUMERIC).
Do
res/values/strings.xml <string name=”did_you_mean”>Did you mean %s?</string> res/values-iw/strings.xml <string name=”did_you_mean”>האם התכוונת ל %s?</string> String mySuggestion = "15 Bay Street, Laurel, CA"; BidiFormatter bidiFormatter = BidiFormatter.getInstance(); String content = String.format(R.string.did_you_mean, bidiFormatter.unicodeWrap(mySuggestion)); Log.d("phrase_i18n", content); Output: iw-IL "?15 Bay Street, Laurel, CA האם התכוונת ל"
Don't
res/values/strings.xml <string name=”did_you_mean”>Did you mean %s?</string> res/values-iw/strings.xml <string name=”did_you_mean”>האם התכוונת ל %s?</string> String mySuggestion = "15 Bay Street, Laurel, CA"; String.format(R.string.did_you_mean, mySuggestion); Output: iw-IL "?Bay Street, Laurel, CA 15 האם התכוונת ל"
If you take a closer look, the house number (15) appears to the right of the address, and not to the left as expected, making the house number look more like a strange postal code. The same problem may occur if you include RTL text within a message that uses the LTR text direction.
To deal with that, the unicodeWrap() method detects the direction of a string and wraps it in Unicode formatting characters that declare that direction, so the content will be displayed accordingly.
We sincerely hope this Android localization tutorial helped clarify the most pressing questions and, most importantly, made you fit for taking your Android app global. Here's a short recap:
If you feel ready to start using best practices for developing multilingual Android apps, let Phrase Localization Suite do the heavy lifting and stay focused on the code you love. The world’s most powerful, connective, and customizable localization platform will save you tons of developers’ time, reduce manual errors, and increase translation quality to let you unlock the full potential of your Android app in the long run.
Last updated on September 22, 2023.