Illustration by Virginia Poltrack

Styling internationalized text in Android

In a previous article we talked about styling characters or paragraphs in Android using spans. But all the examples used were based on hard coded text, where we knew exactly at which indexes to apply the span. Most of the time, the text we work with comes from resources and is internationalized. So the text we need to style and its positioning within a sentence may change.

For example, let’s say that we have the string “Best practices for text on Android” in English and Spanish, and we want to style the word “text”.

values/strings.xml

<resources>
<string name=”title”>Best practices for text on Android</string>
</resources>

values-es/strings.xml

<resources>
<string name=”title”>Texto en Android: mejores prácticas</string>
</resources>

We end up having to style different words, “text” and “Texto”, that have different positions in the strings they belong to, so looking for occurrences of a specific word is not a feasible task. Depending on what kind of styling we want to apply, we have multiple ways of styling internationalized text: by using HTML tags or using the Annotation span together with the annotation tag.

Basic text styling with HTML tags

If you only want to apply basic styling, use HTML tags in the string resources or Html.fromHtml. The following styling is supported:

  • Bold: <b>, <em>
  • Italic: <i>, <cite>, <dfn>
  • 25% increase in text size <big>
  • 20% decrease in text size <small>
  • Setting font properties: <font face=”font_family“ color=”hex_color”>. Examples of the possible font families include monospace, serif and sans_serif.
  • Setting a monospace font family: <tt>
  • Strikethrough: <s>, <strike>, <del>
  • Underline: <u>
  • Superscript: <sup>
  • Subscript: <sub>
  • Bullet points: <ul>, <li>
  • Line breaks: <br>
  • Division: <div>
  • CSS style: <span style=”color|background_color|text-decoration”>
  • Paragraphs: <p dir=”rtl | ltr” style=”…”>

For example, to have the word “text” bold, the text in the strings.xml would be this:

// values/strings.xml

<string name=”title”>Best practices for <b>text</b> on Android</string>

// values-es/strings.xml

<string name=”title”><b>Texto</b> en Android: mejores prácticas</string>

In the UI, the text can be set like this:

textView.setText(R.string.title)

Complex text styling with Annotation

If your styling needs exceed the capabilities supported by HTML tags, or if you want to use custom styles, like custom bullet point styling or even completely new styles, then we need another solution. Mark the words to be styled and work with the android.text.Annotation class and the corresponding <annotation> tag in the strings.xml resource files.

The annotation tag allows us to define custom <key, value> pairs in the xml. When getting string resources as SpannedString, these pairs are automatically converted by the Android framework into Annotation spans, with the corresponding key and value. We can then parse the list of annotations attached to the text and add the right span to our text.

Make sure you add the <annotation> tag to all the translations of your string, in every strings.xml file.

Apply a custom typeface to the word “text”, in all languages

Let’s say that we want to set a custom typeface, by applying a CustomTypefaceSpan to the word “text”. Here’s what we need to do:

  1. Add the <annotation> tag and define the <key, value> pair. In our case, the key is font, and the value is the type of font we want to use: title_emphasis.

// values/strings.xml

<string name=”title”>Best practices for 
<annotation font=”title_emphasis”>text</annotation> on Android</string>

// values-es/strings.xml

<string name=”title”>
<annotation font=”title_emphasis”>Texto</annotation> en Android: mejores prácticas</string>

2. Get the string from resources, iterate through the annotations and get the ones with the key font and the corresponding value. Then create the custom span and set it to text in the same positions as the annotation span.

// get the text as SpannedString so we can get the spans attached to the text
val titleText = getText(R.string.title) as SpannedString
// get all the annotation spans from the text
val annotations = titleText.getSpans(0, titleText.length, Annotation::class.java)
// create a copy of the title text as a SpannableString 
// so we can add and remove spans
val spannableString = SpannableString(titleText)
// iterate through all the annotation spans
for (annotation in annotations) {
    // look for the span with the key "font"
if (annotation.key == "font") {
val fontName = annotation.value
        // check the value associated with the annotation key
if (fontName == "title_emphasis") {
            // create the typeface
val typeface = getFontCompat(R.font.permanent_marker)
            // set the span to the same indices as the annotation
spannableString.setSpan(CustomTypefaceSpan(typeface),
titleText.getSpanStart(annotation),
titleText.getSpanEnd(annotation),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
}
// now the spannableString contains both the annotation spans and the CustomTypefaceSpan
styledText.text = spannableString

You can find the complete code for this example here.

For a more complex example, in Java, using Annotations to add an image to the text and to set the foreground color, check out this commit from Plaid.

If you’re using the same text several times, for example in a RecyclerView, and you reconstruct the SpannableString for every item, there are some performance and memory implications you need to keep in mind:

  • The string is iterated multiple times: once by the framework to add the Annotation spans and once by you, to manually add your spans
  • A new instance of the SpannableString is created for every item.

To solve this, build the text only once, cache and then reuse the SpannableString.

Bonus section: Annotation spans and text parceling

In a previous article we mentioned that only framework spans are parceled when styled text is passed either in the same process (via Intent bundles, for example) or in between processes (by copying text).

Because Annotation spans are also ParcelableSpans, then the <key, value> pairs are parceled and unparceled, making the Annotation span a way of applying custom styling to text that is parceled, as long as the receiving end knows how to interpret the annotations.

So, to keep your custom styling when you pass the text to an Intent Bundle, you first need to add Annotation spans to your text. You can do this in the XML resources via the <annotation> tag, like shown above, or in code by creating a new Annotation and setting it as a span:

val spannableString = SpannableString(“My spantastic text”)
val annotation = Annotation(“font”, “title_emphasis”)
spannableString.setSpan(annotation, 3, 7, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
// start Activity with text with spans
val intent = Intent(this, MainActivity::class.java)
intent.putExtra(TEXT_EXTRA, spannableString)
startActivity(intent)

Retrieve the text from the Bundle as a SpannableString and then parse the Annotations attached, like in the example above.

// read text with Spans
val intentCharSequence = intent.getCharSequenceExtra(TEXT_EXTRA) as SpannableString

For simple styling of internationalized text, update your string resources to use HTML tags. But when your styling needs exceed the limits of the supported HTML tags, use the Annotation span and the associated annotation tag. As an extra benefit, the Annotation spans are parcelable, so you can style your text easily, no matter how you work with it.

For more best practices with text, checkout the Google I/O 2018 talk: