Launching any service in a new country or locale requires adapting the platform to the local customs and conventions. While many of these local tweaks can be done manually, building a streamlined process for international localization saves time when expanding an app or platform into multiple new countries. 

While DoorDash, a US company, had an easier time expanding its platform to support services in Canada and Australia, English-speaking countries with similar street address formatting that call their currency the dollar, than launching in Japan, which proved much more difficult.

Our move into Japan required many internationalization and localization adjustments, commonly abbreviated as i18n and l10n, primarily around ensuring currency, addresses, personal names, and time formats worked locally. We took this opportunity not only to tailor our platform to Japan, but also to build processes and tools to make it easier to expand DoorDash to any new international market in the future.

Defining the four main challenges 

Preparing DoorDash’s platform for Japan, we found four overarching themes that we needed to localize to facilitate a smooth launch:

  • Currency: Displaying the Japanese Yen and calculating its value in our backend systems 
  • Addresses: Configuring Japanese address fields, which are fundamentally different from those in the US  
  • Honorifics: Adding the ability to show honorifics, a culturally important aspect of Japanese society  
  • Dates: Showing the year first in the date, similar to much of the world, but different than the standard date format in the US

All of these areas were necessary to ensure a successful Japan launch, but as any good engineer would, we focused on generalizing these solutions so they could work for any new market language or region.  

Before we get into the particular details of these four areas, we first need to define how we approach locales in our apps and web experience. The key components of a locale are language and region, which are critical for performing localization. For example, the locale en-US means the language is English (en) and the region is the United States (US). Choosing language, region, or both depends on the market or country. 

Calculating a foreign currency

The first step in any international launch is ensuring that customers can see prices and pay in local currency. The challenges we had to confront here mostly came down to ensuring that the currency was displayed properly and that we could actually represent the currency in our systems, keeping it distinct from dollars. 

Modeling currency

Modeling currency is a two part system, the first half being the type of currency and the second half being the amount of that currency. For example, when showing $3.03, the type of currency is actually ambiguous as many countries use $ to represent monetary value. It is important to know the exact type of currency being displayed and its relative value.

Modeling the type of currency is relatively easy. The International Organization for Standardization (ISO) has created currency codes, as outlined in ISO 4217. This standard allows DoorDash’s systems to easily communicate the type of currency with each other.

For representing the amount of a specific currency, one may look at $3.03, for example, and simply store it as a decimal or floating point number. This strategy seems reasonable, but when this information moves across language types, such as Swift, Java, and Python, it may run into conversion bugs.

With these different programming languages using currency, DoorDash went with the solution of representing currency in terms of a Unit Amount and Currency Code. The Unit Amount represents the amount in the smallest unit of that currency. In our $3.03 example, the unit_amount = 303 and currency = USD. This format allows us to treat currencies as Integers, which have no precision issues. The monetary amount can be translated into a decimal number through the following equation:

Decimal number = unit amount / (10 ^ decimal precision)

The United States Dollar (USD) is represented with two decimal places of precision. This means the lowest value the USD can have is $0.01. However, the precision level is dependent on the currency. The Japanese Yen (¥) contains zero decimal places of precision, while the Jordanian Dinar contains three decimal places of precision. That means the smallest amount of Yen one can have is ¥1, and the smallest amount of the Jordanian Dinar is 0.001.  

Accurately displaying currency

How to display currency depends on the locale where currency is to be displayed. Locale is the term we use to describe a place based on a combination of its language (e.g. English) and region (e.g. Japan). Different locales have different formats for writing out currency. Some have the currency symbol after the number, and some use a comma instead of a period to break apart the number. For example, in Spain, what Americans would write as €2.22 (two euro and twenty-two cents), the Spanish would write as 2,22 €. What Americans and Japanese would write as ¥120 (one hundred and twenty Japanese Yen), the British would write as JP¥120.  

There are different libraries for each programming language that can be used to help display currencies accurately, such as NumberFormatter for Swift or ICU for Java. Passing the locale to these libraries delivers the correct format. However, these formatters can be programmatically expensive to create and use. We mitigate this resource usage at DoorDash by calculating most monetary display strings on our backend and pass it back to the client. This method makes our clients run more quickly and dissuades teams from building client-side calculations, which may be more difficult to troubleshoot than backend monetary calculations.

The following code example shows how to create a string representation of monetary value in iOS:

/// Helper method to correctly calculate the value using the number of decimal places 
private func stringUsing(formatter: NumberFormatter, money: Money) -> String {        let factor = pow(10.0, Double(money.decimalPlaces))        let value = Double(money.unitAmount) / factor        return formatter.string(from: NSNumber(value: value))!    }

DoorDash primarily uses Kotlin for our backend. This code example show how to create a string representation of monetary value in Kotlin:

fun formatMoney(
    unitAmount: Int,
    currencyIso: String,
    locale: Locale,
): String {
    val currency = Currency.getInstance(currencyIso)
    val formatter = getCurrencyInstance(ULocale.forLocale(locale))
        .apply {
            this.currency = currency
        }
    val moneyAmountWithDecimalPlaces = unitAmount.toDouble() / 10.0.pow(currency.defaultFractionDigits.toDouble())
    return formatter.format(moneyAmountWithDecimalPlaces)
}

Translating addresses to all users 

Prior to launching in Japan, all countries supported on DoorDash used the Roman alphabet and had similar address formats. This meant that we were able to display the same strings to all users: the consumer ordering the food, the Dasher (our term for a delivery driver) delivering the food, and our support agents assisting with any issues. Adding Japan to our platform introduced a number of issues, the most obvious being that, instead of the Roman alphabet, the Japanese use the Kanji writing system. (Hiragana and Katakana are also used for addresses, but we focused on Kanji for DoorDash services.) Although many Japanese users might be able to read addresses in the Roman alphabet, Kanji is the preferred local form.

In order to address this problem, we needed to re-engineer how we presented addresses. Previously, if a consumer entered “〒027-0052 岩手県宮古市宮町1丁目1−38” as their address, then that is what would be displayed to all parties. Now, we translate all Kanji addresses into Romaji (the Romanization of Japanese). If a client’s preferred locale is set to Japanese, we translate the address back to Kanji before returning it. This allows us to display addresses to different customers in whichever language they have associated with their account.

Displaying names in different languages

We use three different formats to display user names in the DoorDash apps across our mobile and web platforms: formalName, informalName, formalNameAbbreviated. We choose which format to display depending on the context or the screen.

Side-by-side DoorDash app screens
Figure 1: Our localization lets us show different forms of a name depending on the context or screen.

For givenName = “Sid” and familyName = “Kakarla”, we might store:

informalName“Sid”
formalName“Sid Kakarla”
formalNameAbbreviated“Sid K”

Note that western countries tend to use first and last name terminology, whereas given name (first name) and family name or surname (last name) terminology is applicable across the world. 

These formats work well for the markets DoorDash currently operates in and the logic for generating these three formats using the given and family names is sprinkled across our backend services, clients, and BFFs, among other systems. 

In Japan, names start with the family name followed by the given name, the reverse order of most western countries. It’s culturally inappropriate to address someone using their given name even in an app. In addition, honorifics like sama (様) and san (さん) are commonly added as suffixes to names. Another major difference is the lack of a space between the family and given names. Adapting our platform to these differences required changing our logic.

Applying Japanese naming customs 

An honorific is a title that conveys esteem, courtesy, or respect for position or rank when used in addressing or referring to a person. In Japan, san (さん) is the most commonplace honorific and is a title of respect typically used between equals of any age. Although the closest analog in English are the honorifics Mr., Miss, Ms., and Mrs., san is almost universally added to a person’s name.

Sama (様) is a more respectful version for individuals of a higher rank than oneself. Appropriate usages include divine entities, guests, or customers, and sometimes towards people one greatly admires.

We decided to use language as the determining factor for which honorific to use. Think of a scenario where a Japanese user travels to the US. If their phone’s language is set to Japanese, they should expect to see the Japanese naming conventions. Similarly, if an American user travels to Japan and has their device’s language set to English, they should expect to see the English names version. To support use cases like these, the language in the user’s locale is used, and not the region.

                                 For givenName = “Sid” and familyName = “Kakarla”, we now store:

FormatEnglish lang (en-JP, en-US)Japanese lang (ja-US, ja-JP)
informalName“Sid”“Kakarlaさん”
formalName“Sid Kakarla”“KakarlaSid様”
formalNameAbbreviated“Sid K”“Kakarla様”

We also display individual name fields in the account page for adding/updating the respective given and family names.

DoorDash account page in English

For the Japanese use case these fields are switched to show the family name before the given name.

DoorDash account page in Japanese

Implementing naming conventions 

As mentioned above, our naming implementation is sprinkled across different systems, making it difficult to update for potential future markets. To systematize our approach, we built libraries for all the platforms we currently use, including Kotlin, Python, and JavaScript. The goal is to have the logic implemented into a single source of truth, the library, instead of having the same code implemented over and over again as service-specific utilities.

The implementation was pretty simple as we only had to support two naming conventions for our Japan launch, while making it extensible into the future. The code snippet below shows the logic where, if the user’s locale language is Japanese, we generate the names accordingly.  

object NameLocalizer {
    fun getLocalizedNames(userInfo: UserInfo, locale: Locale): LocalizedNames {
        return when (locale.language) {
            Locale.JAPAN.language -> JapaneseNameLocalizer.getLocalizedNames(userInfo)
            else -> DefaultNameLocalizer.getLocalizedNames(userInfo)
        }
    }
}

Standardizing date and time formats  

In addition to addressing localization for currency, names, and addresses, as covered above, the final area we focused on was date and time. We will use the term datetimes going forward to represent this area. Datetimes are important information that indicate a point of time for an event or action. As an example, the consumer app might show the phrase “Your DoorDash delivery is delivered at 4/14/2021 4:32 PM”. Because datetimes are represented differently in different markets, we need to localize this display based on the user’s locale. Here are some examples:

Locale: en-USLocale: ja-JP
2/22/212021/02/23
3:30 PM午後3:30
February 23, 2021 at 3:30:00 PM PST2021年2月23日 15:30:00 GMT-8

Before our work on internationalization, we just used platform-specific datetime libraries for formatting, such as Python’s datetime library, which worked well for the markets we catered to. However, as we began preparing for our Japan launch, we realized that the current solution doesn’t scale. As a better solution, we created Python, Kotlin, and JavaScript libraries and integrated them into our platform so the corresponding services can adopt them and localize datetimes. These libraries reduce the burden on services, which don’t need to employ custom logic, and provide a consistent user experience for DoorDash users. 

The libraries we built for datetimes are based on the standard and open source libraries ICU4J, PyICU, and INTL (the same libraries we used for our currency localization) and also provide support for generating short, medium, long, and full datetime formats. The library supports three actions for formatting dates, times, and datetimes. Here’s an example implementation for formatting datetime in the Kotlin library:

 /**
      *
      * Formats a date time object as a date time string.
      *
      * Params:
      * dt – The date time object (with a time zone) to be formatted.
      * locale - The locale to display the time string in.
      * fmt - The format to display the time string in
      *
      * Returns: A string with the time formatted as specified.
      *
      * SHORT = '2/23/21, 3:30 PM'
      * MEDIUM = 'Feb 23, 2021, 3:30:00 PM'
      * LONG = 'February 23, 2021 at 3:30:00 PM PST'
      * FULL = 'Tuesday, February 23, 2021 at 3:30:00 PM Pacific Standard Time
      *
      */
     fun formatDateTime(
         dt: ZonedDateTime,
         locale: Locale,
         fmt: DateTimeFormat
     ): String {
         var df = when (fmt) {
             DateTimeFormat.SHORT -> DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, locale)
             DateTimeFormat.MEDIUM -> DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM, locale)
             DateTimeFormat.LONG -> DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, locale)
             DateTimeFormat.FULL -> DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL, locale)
         }
         df.timeZone = TimeZone.getFrozenTimeZone(dt.zone.id)
         return df.format(dt.toEpochSecond()*1000)
     } 

Coding internationalization and localization into our platform

First and foremost for any international launch, the most important piece is to make sure all user-facing interfaces, mobile and web, feel natural by localizing the product with accurate translations. For the Japan launch, we were extremely lucky as we were able to leverage the many translation tools we previously used to launch in Puerto Rico, Canada, and Australia. 

Translation service

We developed a translation service which integrates with our third-party translation provider to allow our developers to rapidly build and localize new features. 

All of our applications are integrated with this translation service, and when developing a new feature, an engineer simply needs to create their Git branch with an i18n_ prefix and create a pull request. When the translation service receives a pull request with the i18n_ prefix, it will make a commit and apply the translations. 

With the new strings effortlessly being added into the DoorDash platform, we were able to launch Japan and build out new features in our marketplace, Dasher, and logistics systems.

Identifying the user’s locale and getting the localized strings

Each of our different user-facing clients, iOS, Android, and web, require different approaches to determine the user’s locale so that we can serve the correct strings on initial load. 

On the web, we have a localization library that uses i18next, an open source internationalization framework, to set a user’s locale based on several factors in descending priority order, starting with: 

  1. Browser URL (on DoorDash.com we use a prefix in the URL, such as DoorDash.com/ja-JP)
  2. Query param (query param set to a supported locale, such as ?intl=ja-JP)
  3. Cookies 
  4. Browser property localStorage
  5. The navigator (we fall back to the user’s browser language to determine their locale if all other factors fail to help us identify the locale; this typically happens on initial load for a guest navigating directly to doordash.com)

On the web, we store our strings client-side as a JSON file and initialize our localization library on the initial app load, which uses the above factors to determine the user’s locale. This code example shows our client-side strings:

 const i18n = createI18nInstance({
     // english us
     englishStrings: en_us,
     // french canada
     frenchStrings: fr_ca,
     // spanish mexico
     spanishMexicoStrings: es_mx,
     // English Canada
     englishCanadaStrings: en_ca,
     // English Australia
     englishAustraliaStrings: en_au,
     // Japanese Japan
     japaneseJapanStrings: ja_jp,
 }) 

Once our localization library is initialized, we can use the instance of the localization library across our application and get the correct string for each user’s locale, as shown in this code example:

i18n.t(‘exampleString_test’)

Calling the above function returns the localized string based on the user’s locale.

In this example, exampleString_test refers to the string which is a key in our JSON; we have a localized version of exampleString_test for every locale we support. 

Localization for iOS

For our iOS apps, we define all our strings using Localizable.string files and use SwiftGen, an open source Swift code generator, to convert these string files into enums that we reference throughout our iOS codebases, as shown in Figure 2, below:

Localizable string files in an IDE
Figure 2: Localizable.string files are a standard means of localizing iOS apps.

Localization for Android

For our Android apps, we leverage the built-in Locale.getDefault() class to get the user’s locale, which is based on the device’s language and region settings. 

We store all our strings as XML files following the best practices laid out in the Android Localization Documentation. This allows us to use getString(R.id.EXAMPLE_STRING) to automatically grab the correct string by the user’s locale.

Conclusion

We’ve come a long way as a company since we launched in Canada and Australia, and as noted, Japan posed significant challenges not found in the aforementioned countries. Our Japan launch inspired us to build tools and automation to make this process easier in the future.

Through this effort, large sections of our codebase have been localized to allow us to adapt to any language and location without having to make any code changes. We can support any currency, and if we want to support an additional language it can be as simple as adding the translations to our system. 

The methods described above can be employed by any company ready to offer its apps or services in new countries. An important lesson we learned, something that other engineers should take note of, was the necessity of thorough scoping early on. Our team was resource-constrained when we took on this task, limiting our ability to understand the problem in depth. As a result, we came across some large issues that forced us to scramble at the last minute. This lesson applies to all projects: understand the problem and all the tasks required to solve it before any coding.

Photo by Erik Eastman on Unsplash.