Trevor Elkins
Published on
/
7 min read

How to Localize Your App

Preface

So you have an app, or are in the middle of making one, and now — quite literally — you want the world to enjoy it. Welcome to internationalization and localization, commonly referred to as i18n and l10n.

The problem many people run into is making localization an afterthought. You want your app to succeed in one region first, and expand to more later. That's fine, but you should still engineer your app with localization in mind.

Let's get to it. To start you can read through the Android guide and checklist.

The Basics

The most obvious thing to do is extract your text to strings.xml. You should never have a hardcoded string in layouts or code. Android Studio has a useful Option/Alt + Enter shortcut to extract text to a string resource and then replace the text with it.

If you have all your strings in strings.xml, then you can hand that off to a translator and get a new file back. You would place that file in the appropriate localized resource folder, and Android will automatically use it depending on the user's region. For example, French strings would go in res/values-fr/strings.xml. To me it's amazing how simple and powerful this is, and should get your app 80% of the way there.

Stop Concatenating Strings

I can only speak one language (for now!), so this caught me a bit by surprise. English doesn't translate 1:1, that is, you translate sentences and not words. Take this for example:

String carModel = "You drive a " + car.getModel() + " car!"; //You drive a Prius car!

With my previous advice, you might try changing that to:

// R.string.carSentenceBeginning = "You drive a "
// R.string.carSentenceEnd = " car!"
String carModel = getString(R.string.carSentenceBeginning) + car.getModel() + getString(R.string.carSentenceEnd); //You drive a Prius car!

This is actually wrong! Remember, you're translating sentences, and the sentence structure may be different in another language. Instead you should use format args. You'll notice that there's a second [getString() method](https://developer.android.com/reference/android/content/Context.html#getString(int, java.lang.Object...)) to use. This is how it should look:

// R.string.carSentence = "You drive a $1%s car!"
String carModel = getString(R.string.carSentence, car.getModel()); //You drive a Prius car!

$1%s is a format arg, $1 meaning it's the first arg and %s that it's a string. This format arg will be replaced by the argument you pass in, which is car.getModel().

Check out Square's library Phrase which has a more explicit way to format strings.

Beware indexOf()

I made an embarrassing mistake one day where I did something like this:

int end = text.indexOf(",") - 1;
boldedText.setSpan(new StyleSpan(Typeface.BOLD), 0, end, Spannable.SPAN_INCLUSIVE_INCLUSIVE);

We had some promo text that had to be bolded, so I figured this was OK to do. Wrong. It turns out that some locales use a different comma than we do, so indexOf() returned -1 and caused a crash. The lesson here is it's dangerous to depend on characters for text manipulation, seeing as other languages might use different character sets.

On a related note, be careful anytime you are using String indexes, such as programmatically setting a Spannable. The position of the text you are creating a span for can change per translation, and possibly cause an IndexOutOfBoundsException.

For example, say you wanted to bold everything up to a date in your string. You naievely set a span from 0 to string.indexOf(date) - 1. In Japanese this would throw an exception, because most of the translations I've seen push the date to the front. Your code would try setting a span from 0 to -1. Be mindful of these situations.

Language Wordiness

Some languages are more succinct than others. I'd say just about all languages are more succinct than German. :)

Be careful trying to exactly position a layout that has text in it. For example, you have a mock where everything looks perfect when the flavor text is three lines long. This won't be the case for all languages. I find it worthwhile to test your app in German since it frequently pushes text to new lines.

Make Your Dates Flexible

If your app makes use of dates then beware of timezones and date formats.

Make sure all timestamps you store are in UTC format. When you read the timestamp out you can apply whatever timezone offset to get the correct display.

Date formats are a resource, make sure you put them in a resource file so they can be overridden; I use constants.xml myself. Different locales have different ways of displaying dates, whether that be month or day first, which day starts the week, etc. As always, try using formats provided by Android before making your own.

Plurals

It's very common to have text saying "I have X widgets." For simplicity, let's say we have 0 or 1 widgets. That means we need text for:

  • "I have 0 widgets."
  • "I have 1 widget."

This is a perfect time to use quantity strings. First, create a plurals resource:

<?xml version="1.0" encoding="utf-8"?>

<resources>
  <plurals name="numberOfWidgets">
    <item quantity="zero">@string/widget_count_other</item>
    <item quantity="one">@string/widget_count_one</item>
    <item quantity="other">@string/widget_count_other</item>
  </plurals>
</resources>

And our strings:

<resources>
  <string name="widget_count_other">I have $1%d widgets.</string>
  <string name="widget_count_one">I have $1%d widget.</string>
</resources>

In code, we get our string like so:

String widgetCount = getResources().getQuantityString(R.plurals.numberOfWidgets, widgetCount, widgetCount);

Notice how we pass widgetCount in twice. The first selects the correct quantity string and the second is our format arg. We also use other alongside zero, which seems counterintuitive. That's because the quantity enum doesn't work quite how you'd expect:

The selection of which string to use is made solely based on grammatical necessity. In English, a string for zero will be ignored even if the quantity is 0, because 0 isn't grammatically different from 2, or any other number except 1 ("zero books", "one book", "two books", and so on). Conversely, in Korean only the other string will ever be used.

In English there's no distinction between zero and plurals, making zero redundant. I still added a zero case to account for other locales, but it will be completely ignored for English.

Localizing Part of Your Layout

In the app I work on we have a form section where users can enter their information. The form looked like this:

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/firstName"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <TextView
        android:id="@+id/lastName"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <TextView
        android:id="@+id/email"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <TextView
        android:id="@+id/phone"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</LinearLayout>

In some locales, it's customary to enter the "Last name" field first. A good way to solve this is by using the <includes /> tag in our XML layout. Android overrides all resources it finds, including layouts, in locale-specific resource directories. This is how our layout should look:

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <include layout="@layout/name_form" />

    <TextView
        android:id="@+id/email"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <TextView
        android:id="@+id/phone"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</LinearLayout>

And then our additional layout:

<?xml version="1.0" encoding="utf-8"?>

<!--name_form.xml-->

<merge xmlns:android="http://schemas.android.com/apk/res/android">

    <TextView
        android:id="@+id/firstName"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <TextView
        android:id="@+id/lastName"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</merge>

Notice that I use the <merge> tag in the layout that is included. You can only have one root tag in your XML, and we don't want to introduce a new ViewGroup layer to house our included fields, so this solves both problems.

With our layout structured like this, we can override it in different locales:

<?xml version="1.0" encoding="utf-8"?>

<!--res/layout-jp/name_form.xml-->

<merge xmlns:android="http://schemas.android.com/apk/res/android">

    <TextView
        android:id="@+id/lastName"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <TextView
        android:id="@+id/firstName"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</merge>