Paraphrase: Type-Safe String Resource Formatting for Android

There are a couple basic ways to format <string> and <plurals> resources on Android.

The most common way is with String.format()-style arguments. It looks like this:

<plurals name="sent_payment">
  <item quantity="one">You sent a dollar to %1$s</item>
  <item quantity="other">You sent %2$d dollars to %1$s</item>
</plurals>
resources.getQuantityString(
  R.plurals.sent_payment,
  50,
  "Slime Guy",
  50,
)

This gets the job done, but there are some drawbacks:

  1. The format uses numbers to distinguish between arguments. If we could use descriptive names instead, that would provide helpful context for translators and future developers.
  2. The API doesn’t force us to pass the correct types for arguments. If we pass a string when an integer is expected we’ll get a runtime error.
  3. The API doesn’t force us to pass the correct number of arguments. If we pass too few arguments or too many we might get a runtime error, but in some cases we won’t get an error at all! In those cases, we would have to rely on Paparazzi or manual testing to catch the mistake.

Another option is to use the ICU message format. It looks like this:

<string name="sent_payment">
  {amount, plural,
    =1 {You sent a dollar to {recipient}}
    other {You sent # dollars to {recipient}}
  }
</string>
MessageFormat.format(
  resources.getString(R.string.sent_payment),
  mapOf(
    "amount" to 50,
    "recipient" to "Slime Guy",
  ),
)

This format allows us to use descriptive names for the arguments and also comes with some nice localization features, but the API is clunky and there’s still no validation of argument types or numbers.

Ideally we’d have something that guaranteed we were passing the correct number and correct type of arguments at compile time.

Enter Paraphrase

Paraphrase is a Gradle plugin that generates type safe formatters for Android string resources in the ICU message format. It integrates easily with Android Views and Compose UI.

Here’s what the earlier example looks like with an Android View:

resources.getString(
  FormattedResources.sent_payment(
    amount = 50,
    recipient = "Slime Guy",
  ),
)

And with Compose UI:

formattedResource(
  FormattedResources.sent_payment(
    amount = 50,
    recipient = "Slime Guy",
  ),
)

Notice that the API forces us to pass the correct arguments. Now if we add a new argument (or delete an existing one, or change its type) without updating the call sites, Paraphrase will generate a new formatter and the build will fail. Paraphrase takes an entire class of runtime errors and turns them into compile time errors. Pretty neat!

It’s been a long journey for Paraphrase - it started life as an experimental plugin 10 years ago - so we’re excited to finally make it official! Check out the README to get started and let us know what you think!

This post is part of Cash App’s Summer of Kotlin Multiplatform series.1

  1. “Hey, there’s no multiplatform in this post.” You’re right! We are watching Compose UI multiplatform closely and hope to support it in the future.