Software localization

How to Localize Date and Time Formats in Android

Date and time can vary greatly across markets. Learn how to make your Android app support date and time formats for users around the world.
Software localization blog category featured image | Phrase

In today's ever-growing global market for mobile apps, making your app ready to support different languages and cultures from the start is key for organic growth. With Google Play being responsible for 111.3B downloads in 2021, it only makes sense to implement Android localization for your app to look and feel native to users in different corners of the world.

This goes for date and time formats, too. From the order of days and months in dates, and how hours and minutes in the time are separated, to displaying dates in various long or short formats—date and time information can vary greatly across markets.

To help you get it right from the get-go, this tutorial outlines best practices for displaying date and time formats to Android users based on their time zone and locale. We'll build a news feed application using localized timestamps and learn more about standardized times, handling time zones, absolute and relative dates, and formatting time based on locale.

What classes and libraries should you use?

There are many libraries and in-built classes to choose from to manipulate time formats on your Android application: joda-time, java.util.Date, java.util.Calendar, GregorianCalendar, and java.text.SimpleDateFormat. However, we won't use any of them as most of them are mutable, not thread-safe, and their APIs are inconsistent.

Instead, we'll use the java.time API built into Java 8. The Java 8 Date/Time APIs are immutable, thread-safe, and follow consistent date and time models; java.time also comes with a lot of utility methods that can handle time zone logic.

Still, when you try to use these newer APIs, Android Studio will "confront" you with a warning:

Android Studio warning about using an Android version lower than 26 | Phrase

The modern java.time APIs are only available for Android versions greater than or equal to 26. If you run it on a version lower than 26, your app will crash, throwing a NoClassDefFoundError exception, but we can work around this crash.

🗒 Note » If the minimum SDK version for your app is above 25 (~ 60% of devices), you can skip the next step..Making Java 8 APIs Backwards Compatible

With the help of desugaring, lower Android versions can still work with newer Java APIs. Desugaring enables you to include the latest APIs in apps that support older versions of Android. To enable desugaring for your app, follow these steps:

  • Update the Android Plugin version to 4.0.0 or higher.
  • In the build.gradle file of your app's module, make the following changes:
android {

  defaultConfig {

  // Required when setting minSdkVersion to 20 or lower

  multiDexEnabled = true

  }

  compileOptions {

  // Flag to enable support for the new language APIs

  coreLibraryDesugaringEnabled = true

  // Sets Java compatibility to Java 8

  sourceCompatibility = JavaVersion.VERSION_1_8

  targetCompatibility = JavaVersion.VERSION_1_8

  }

}

dependencies {

  coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5")

}

  • Rebuild the project.

You should now be able to work with newer Java APIs on an older Android version.

🗒 Note » Adding java.time desugaring adds about 400 KB to your Android Package (APK) size.

Standardized time

Imagine the following use case: A global news company, based out of London, is publishing news articles that users consume via an Android app. The company submits an article at 5 pm on 15th January (London time). Most app users are located in the USA and Japan. On both sides, users expect to see an article timestamp in their local date and time format instead of London time (12 pm, 15 January, i.e. 2 am, 16 January). The matter can get even more complex if users are spread all over the world in different time zones.

To solve the issue, the backend must send news articles with a standardized timestamp that clients (Android) can use and convert into the local time format depending on the user's time zone.

Coordinated Universal Time (UTC)

Coordinated Universal Time (UTC) is the global standard for regulating clocks and times, with time zones around the world expressed using either positive or negative offsets from the UTC. There are many standard ways of representing UTC, but we'll highlight the two most common formats here:

  • Unix-Epoch: the number of seconds that have elapsed since 00:00:00 UTC on January 1, 1970, excluding leap seconds; for example, the UTC of January 31, 2022, 17:55:50, is represented as "1643651750."
  • ISO 8601: a string used to represent the UTC; for example, the UTC of January 32, 2022, 17:55:50, is represented as "2022-01-31T17:55:50Z," where the Z represents a zero UTC offset.

Building a demo app

Currently, we have a news app that displays a list of news articles fetched from News API. Each article contains an image, a headline, as well as a timestamp corresponding to when the article is published. We'll use the java.time API to internationalize and localize the timestamp into something more intuitive and familiar to our app users across the globe.

🗒 Note » You can get the starting code for the app on GitHub.

A news app that uses java.time APIs | Phrase

The News API endpoint returns a json file consisting of a list of articles:

articles": [

 {

  "title": "Georgia prosecutor asks FBI for security assistance following Trump comments at Texas rally - The Washington Post",

  "description": "In a letter Sunday, Fulton County District Attorney Fani Willis pointed to Trump's characterization of prosecutors as “racist” and “mentally sick.”",

  "url": https://www.washingtonpost.com/politics/2022/01/31/willis-fbi-help-trump-comments/,

  "urlToImage":https://www.washingtonpost.com/wp-apps/imrs.php?src=https://arc-anglerfish-washpost-prod-washpost.s3.amazonaws.com/public/2INRWAECT4I6ZFI4DYGMG4R6KM.jpg&w=1440,

  "publishedAt": "2022-01-31T16:27:23Z",

  "content": "Security concerns were escalated this weekend by the rhetoric of former President Trump at a public event in Conroe, Texas that was broadcast and covered by national media outlets and shared widely o… [+2539 chars]"},

       ...

 ]

Note that each article item consists of a key called publishedAt, which represents the UTC in the ISO 8601 format. At the moment, we're displaying the publishedAt time directly as is it in the bind method of the NewsRecyclerAdapter.kt file.

fun bind(article: Article) {

  with(containerView) {

    // Sets the text for the TextView

    textview_date_time.text = article.publishedAt

    textview_description.text = article.description

      ...

	}

 }

The first step would be to convert the UTC into the user's time zone. Using the java.time API, make the following changes to the NewsRecyclerAdapter.kt class:

...

import java.time.Instant

import java.time.LocalDate

import java.time.ZoneId

import java.time.ZonedDateTime

class NewsRecyclerAdapter() {

...

    fun bind(article: Article) {

	with(containerView) {

	val timestampInstant = Instant.parse(article.publishedAt)

        val articlePublishedZonedTime = ZonedDateTime.ofInstant(timestampInstant, ZoneId.systemDefault())

        textview_date_time.text = articlePublishedZonedTime.toString()

...

    }

}

Instant.parse(article.publishedAt) takes in the UTC string as a parameter and returns a Java Time Instant object. The Java Instant class represents a time passed in seconds since the origin (epoch) of 1970-01-01T00:00:00Z. We pass this and the user's time zone using ZoneId.systemDefault()  as a parameter in the ZonedDateTime.ofInstant() method, which returns an object of the ZonedDateTime type. Finally, we convert articlePublishedZonedTime to a string and set it to TextView.

We have now successfully converted a UTC ISO 8601 string into the user's time zone in the same format.

This is what a user from New York, US, would see:

An app screen representing the user's time in the ISO 8601 format | Phrase

Users from Berlin, Germany, get the following screen displayed:
App screen representing the user's time in the ISO 8601 format | Phrase

Internationalizing dates

ISO 8601 isn't really the most human-readable format you can show to an app user. This representation is far from natural language and hardly provides a good user experience. To change that, we'll internationalize the date according to the user's set locale and display it.

🗒 Note » To know more about internationalizing Dates in Android, read our Deep Dive on Internationalizing Jetpack Compose Android Apps.

Make the following changes to the NewsRecyclerAdapter.kt file.

val timestampInstant = Instant.parse(article.publishedAt)

val articlePublishedZonedTime = ZonedDateTime.ofInstant(timestampInstant, ZoneId.systemDefault())

val dateFormatter: DateTimeFormatter = DateTimeFormatter.ofLocalizedDateTime(MEDIUM)

textview_date_time.text = articlePublishedZonedTime.format(dateFormatter)

We've used java.time's DateTimeFormatter to format articlePublishedZonedTime from ISO 8601 into a localized date and time in a string depending on the user's locale settings.

🗒 Note » You can also use FormatStyle.LONG, FormatStyle.FULL, or FormatStyle.MEDIUM for a more detailed representation of date and time information formatted based on the user's set locale.

Users located in New York—Eastern Time (ET) zone—get to see the screen below:

News feed displayed to a user from New York, US (Eastern Time) | Phrase

Here is what the news feed looks like for users in Berlin—Central European Time (CET):

News feed displayed to a user from Berlin (CET) | Phrase

Handling relative dates

While developing your app, you may also need to display relative dates, e.g., "today" or "yesterday" instead of absolute dates. Recent relative dates are easier to understand for most users.

To make the use case clearer in the context of our news app, we'll use relative dates for articles that were published either today or yesterday.

In all articles published more than two days ago, we'd like to display the absolute date.

🗒 Note » Relative dates are relative to the user's time zone and not the UTC (zero offset) time zone.

To do this, implement the following changes in the same NewsRecyclerAdapter.kt file:

import java.time.Instant

import java.time.LocalDate

import java.time.ZoneId

import java.time.ZonedDateTime

import java.time.format.DateTimeFormatter

import java.time.temporal.ChronoUnit.DAYS

import java.time.format.FormatStyle.MEDIUM

...

fun bind(article: Article) {

  val timestampInstant = Instant.parse(article.publishedAt)

  val articlePublishedZonedTime = ZonedDateTime.ofInstant(timestampInstant, ZoneId.systemDefault())

  val dateFormatter = DateTimeFormatter.ofLocalizedDateTime(MEDIUM)

  val currentTimestamp = Instant.now()

  // Get current Instant

  val currentZonedTime = ZonedDateTime.ofInstant(currentTimestamp, ZoneId.systemDefault())

  //Convert current Instant to local time zone

  val gapInDays = articlePublishedZonedTime.toLocalDate().until(currentZonedTime, DAYS)

  //Find difference in current and published date of article

  val finalDate = when(gapInDays){

                    0L -> context.getString(R.string.today)

                    1L -> context.getString(R.string.yesterday)

                    else -> articlePublishedZonedTime.format(dateFormatter)

                }

   textview_date_time.text = finalDate

   ...

We use Instant.now() to get the current Instant (timestamp) and then pass it along with the user's time zone Id in the ZonedDateTime.ofInstant(currentTimestamp, ZoneId.systemDefault()) method, which returns a ZonedDateTime.

Now we compare the currentZonedTime and articlePublishedZonedTime using articlePublishedZonedTime.toLocalDate().until(currentZonedTime, ChronoUnit.DAYS), which returns the difference in days in Long between the two ZonedDateTimes.

We use this gapInDays to find out the corresponding time in string.

🗒 Note » Use context.getString() instead of hardcoding "today" or "yesterday" directly as we want to dynamically load the relevant string respecting the user's locale settings.

Now add the above string references to the default strings.xml file:

<resources>

 <string name="today">Today</string>

 <string name="yesterday">Yesterday</string>

</resources>

We also want to add support in our news app for the de-DE locale. To do this, add a new strings.xml file for de_DE locale and add values for the same keys in German.

<resources>

 <string name="today">Heute</string>

 <string name="yesterday">Gestern</string>

</resources>

Now let's take a look at the app screens for users browsing from NewYork (ET) and Berlin (CET):

Android news app screens displaying relative dates | Phrase

Both users see the same news articles. However, there are quite some differences in the way data is displayed on both sides. With both absolute and relative dates formatted and translated depending on the user's locale and time zone, we provide a unique user experience for everyone regardless of location and language.

🗒 Note » The final code for the app can be found on Github.

Wrapping up our tutorial on localizing Android date and time formats

In this tutorial, we learned about standardized time formats, leveraging various java.time APIs to convert the UTC into the user's time zone and format it depending on the user's locale.  By building a news feed app, in which we displayed relative and absolute dates that take the user's locale and time zone into account, we've managed to deliver a distinctive app experience to all users across the globe.

As soon as you’ve got your app ready for localization, let your team of translators work with Phrase. The fastest, leanest, and most reliable localization solution on the market will help you streamline the Android localization process with everything you need to reach a global user base. Sign up for a free 14-day trial and let the team know any questions you might have.

If you want to drill down on Android internationalization even more, we suggest the following guides: