Android String Formatting with Phrase

Avoid translation mistakes with this simple Android text formatting library.

Written by Eric Burke.

Phrase is a micro-library for text token replacement on Android. Phrase solves a few simple problems we noticed after translating Square Register to French and Japanese.

Suppose strings.xml contains this string definition.

<string name="greeting">
Hello %1$s, today\'s cook yielded %2$d %3$s.
</string>

Formatting the greeting is straightforward. Android’s Context even provides an overloaded getString(…) method that retrieves the string and formats in a single step.

String name = "Walter";
int yield = 50;
String unit = "pounds";
String greeting = context.getString(R.string.greeting, name, yield, unit);

Error Prone

Format specifiers like %1$s are not obvious to linguists, who are rarely programmers. With each translation, we’d find a few typos and mistakes in these cryptic specifiers. The problem was worse when specifiers were adjacent to punctuation and other characters.

Another problem is that specifiers like %2$d are not descriptive. We must study surrounding text to discern the meaning and their order must match the Java code to avoid bugs.

// Bug! Parameters are in the wrong order.
String greeting = context.getString(R.string.greeting, name, unit, yield);

Finally, Context.getString(…) cannot handle styled text such as bold and italic. If your strings.xml contains simple HTML tags alongside format specifiers, the HTML tags are silently ignored.

Phrase

With Phrase, the greeting changes to the following. As you can see, cryptic specifiers are replaced with readable keys like {name} and {yield}.

<string name="greeting">
Hello {name}, today\'s cook yielded {yield} {unit}.
</string>

Reducing translation errors is our primary goal and we achieve this by keeping the rules simple.

  • Surround keys with curly braces; use two {{ to escape.
  • Keys start with lowercase letters followed by lowercase letters and underscores.

More flexibility adds complexity so we don’t allow uppercase letters, numbers, or any symbols other than underscores.

Formatting follows a fluent style. Using named keys makes life easier for programmers because key/value substitution is not order-dependent.

// Call put(...) in any order
CharSequence greeting = Phrase.from(context, R.string.greeting)
.put("unit", unit)
.put("name", name)
.put("yield", yield)
.format();

Notice that Phrase returns a CharSequence rather than String. This is because Phrase takes care to preserve spans, enabling a single string to include both HTML tags and keys.

<string name="did_you_learn">
<!-- class_type is something like Chemistry. -->
Did you learn <b>nothing</b> from my {class_type} class?
</string>

Validation

Phrase adheres to a fail-fast philosopy. Phrase throws an exception if you make any of the following mistakes.

  • Parse a pattern with mismatched curly braces or illegal characters in any key.
  • Pass null as either a key or value to the put(…) method.
  • Call put(…) with a key that doesn’t exist in the pattern.
  • Call format() without providing values for every key.

Throwing exceptions and crashing makes it easy to spot mistakes during development and minimizes odds of shipping embarrassing translation mistakes or displaying unformatted string templates.

Because keys are straightforward, one could easily write a post-translation validation script that ensures strings.xml files in each language contain the exact same Phrase keys.

Summary

That’s it! Phrase consists of a single Java class and a suite of unit tests. It reduces translation mistakes by simplifying the patterns a linguist must know, relying on named keys rather than positional arguments, and failing fast with helpful error messages.