Question

In Android it is possible to use placeholders in strings, such as:

<string name="number">My number is %1$d</string>

and then in Java code (inside a subclass of Activity):

String res = getString(R.string.number);
String formatted = String.format(res, 5);

or even simpler:

String formatted = getString(R.string.number, 5);

It is also possible to use some HTML tags in Android string resources:

<string name="underline"><u>Underline</u> example</string>

Since the String itself cannot hold any information about formatting, one should use getText(int) instead of getString(int) method:

CharSequence formatted = getText(R.string.underline);

The returned CharSequence can be then passed to Android widgets, such as TextView, and the marked phrase will be underlined.

However, I could not find how to combine these two methodes, using formatted string together with placeholders:

<string name="underlined_number">My number is <u>%1$d</u></string>

How to process above resource in the Java code to display it in a TextView, substituting %1$d with an integer?

Was it helpful?

Solution

Finally I managed to find a working solution and wrote my own method for replacing placeholders, preserving formatting:

public static CharSequence getText(Context context, int id, Object... args) {
    for(int i = 0; i < args.length; ++i)
        args[i] = args[i] instanceof String? TextUtils.htmlEncode((String)args[i]) : args[i];
    return Html.fromHtml(String.format(Html.toHtml(new SpannedString(context.getText(id))), args));
}

This approach does not require to escape HTML tags manually neither in a string being formatted nor in strings that replace placeholders.

OTHER TIPS

Kotlin extension function that

  • works with all API versions
  • handles multiple arguments

Example usage

textView.text = context.getText(R.string.html_formatted, "Hello in bold")

HTML string resource wrapped in a CDATA section

<string name="html_formatted"><![CDATA[ bold text: <B>%1$s</B>]]></string>

Result

bold text: Hello in bold

Code

/**
* Create a formatted CharSequence from a string resource containing arguments and HTML formatting
*
* The string resource must be wrapped in a CDATA section so that the HTML formatting is conserved.
*
* Example of an HTML formatted string resource:
* <string name="html_formatted"><![CDATA[ bold text: <B>%1$s</B> ]]></string>
*/
fun Context.getText(@StringRes id: Int, vararg args: Any?): CharSequence =
    HtmlCompat.fromHtml(String.format(getString(id), *args), HtmlCompat.FROM_HTML_MODE_COMPACT)
<resources>
  <string name="welcome_messages">Hello, %1$s! You have &lt;b>%2$d new messages&lt;/b>.</string>
</resources>


Resources res = getResources();
String text = String.format(res.getString(R.string.welcome_messages), username, mailCount);
CharSequence styledText = Html.fromHtml(text);

More Infos here: http://developer.android.com/guide/topics/resources/string-resource.html

For the simple case where you want to replace a placeholder without number formatting (i.e. leading zeros, numbers after comma) you can use Square Phrase library.

The usage is very simple: first you have to change the placeholders in your string resource to this simpler format:

<string name="underlined_number">My number is <u> {number} </u></string>

then you can make the replacement like this:

CharSequence formatted = Phrase.from(getResources(), R.string.underlined_number)
   .put("number", 5)
   .format()

The formatted CharSequence is also styled. If you need to format your numbers, you can always pre-format them using String.format("%03d", 5) and then use the resulting string in the .put() function.

This is the code that finally worked for me

strings.xml

<string name="launch_awaiting_instructions">Contact <b>our</b> team on %1$s to activate.</string>
<string name="support_contact_phone_number"><b>555 555 555</b> Opt <b>3</b></string>

Kotlin code

fun Spanned.toHtmlWithoutParagraphs(): String {
    return HtmlCompat.toHtml(this, HtmlCompat.TO_HTML_PARAGRAPH_LINES_CONSECUTIVE)
        .substringAfter("<p dir=\"ltr\">").substringBeforeLast("</p>")
}

fun Resources.getText(@StringRes id: Int, vararg args: Any): CharSequence {
    val escapedArgs = args.map {
        if (it is Spanned) it.toHtmlWithoutParagraphs() else it
    }.toTypedArray()
    val resource = SpannedString(getText(id))
    val htmlResource = resource.toHtmlWithoutParagraphs()
    val formattedHtml = String.format(htmlResource, *escapedArgs)
    return HtmlCompat.fromHtml(formattedHtml, HtmlCompat.FROM_HTML_MODE_LEGACY)
}

Using this I was able to render styled text on Android with styled placeholders too

Output

Contact our team on 555 555 555 Opt 3 to activate.

I was then able to expand on this solution to create the following Compose methods.

Jetpack Compose UI

@Composable
fun annotatedStringResource(@StringRes id: Int, vararg formatArgs: Any): AnnotatedString {
    val resources = LocalContext.current.resources
    return remember(id) {
        val text = resources.getText(id, *formatArgs)
        spannableStringToAnnotatedString(text)
    }
}

@Composable
fun annotatedStringResource(@StringRes id: Int): AnnotatedString {
    val resources = LocalContext.current.resources
    return remember(id) {
        val text = resources.getText(id)
        spannableStringToAnnotatedString(text)
    }
}

private fun spannableStringToAnnotatedString(text: CharSequence): AnnotatedString {
    return if (text is Spanned) {
        val spanStyles = mutableListOf<AnnotatedString.Range<SpanStyle>>()
        spanStyles.addAll(text.getSpans(0, text.length, UnderlineSpan::class.java).map {
            AnnotatedString.Range(
                SpanStyle(textDecoration = TextDecoration.Underline),
                text.getSpanStart(it),
                text.getSpanEnd(it)
            )
        })
        spanStyles.addAll(text.getSpans(0, text.length, StyleSpan::class.java).map {
            AnnotatedString.Range(
                SpanStyle(fontWeight = FontWeight.Bold),
                text.getSpanStart(it),
                text.getSpanEnd(it)
            )
        })
        AnnotatedString(text.toString(), spanStyles = spanStyles)
    } else {
        AnnotatedString(text.toString())
    }
}

Similar to the accepted answer, I attempted to write a Kotlin extension method for this.

Here's the accepted answer in Kotlin

@Suppress("DEPRECATION")
fun Context.getText(id: Int, vararg args: Any): CharSequence {
    val escapedArgs = args.map {
        if (it is String) TextUtils.htmlEncode(it) else it
    }.toTypedArray()
    return Html.fromHtml(String.format(Html.toHtml(SpannedString(getText(id))), *escapedArgs))
}

The problem with the accepted answer, is that it doesn't seem to work when the format arguments themselves are styled (i.e. Spanned, not String). By experiment, it seems to do weird things, possibly to do with the fact that we're not escaping non-String CharSequences. I'm seeing that if I call

context.getText(R.id.my_format_string, myHelloSpanned)

where R.id.my_format_string is:

<string name="my_format_string">===%1$s===</string>

and myHelloSpanned is a Spanned that looks like <b>hello</b> (i.e. it would have HTML <i>&lt;b&gt;hello&lt;/b&gt;</i>) then I get ===hello=== (i.e. HTML ===<b>hello</b>===).

That is wrong, I should get ===<b>hello</b>===.

I tried to fix this by converting all CharSequences to HTML before applying String.format, and here is my resulting code.

@Suppress("DEPRECATION")
fun Context.getText(@StringRes resId: Int, vararg formatArgs: Any): CharSequence {
    // First, convert any styled Spanned back to HTML strings before applying String.format. This
    // converts the styling to HTML and also does HTML escaping.
    // For other CharSequences, just do HTML escaping.
    // (Leave any other args alone.)
    val htmlFormatArgs = formatArgs.map {
        if (it is Spanned) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                Html.toHtml(it, Html.TO_HTML_PARAGRAPH_LINES_CONSECUTIVE)
            } else {
                Html.toHtml(it)
            }
        } else if (it is CharSequence) {
            Html.escapeHtml(it)
        } else {
            it
        }
    }.toTypedArray()

    // Next, get the format string, and do the same to that.
    val formatString = getText(resId);
    val htmlFormatString = if (formatString is Spanned) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            Html.toHtml(formatString, Html.TO_HTML_PARAGRAPH_LINES_CONSECUTIVE)
        } else {
            Html.toHtml(formatString)
        }
    } else {
        Html.escapeHtml(formatString)
    }

    // Now apply the String.format
    val htmlResultString = String.format(htmlFormatString, *htmlFormatArgs)

    // Convert back to a CharSequence, recovering any of the HTML styling.
    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        Html.fromHtml(htmlResultString, Html.FROM_HTML_MODE_LEGACY)
    } else {
        Html.fromHtml(htmlResultString)
    }
}

However, this didn't quite work because when you call Html.toHtml it puts <p> tags around everything even when that extra padding wasn't in the input. Said another way, Html.fromHtml(Html.toHtml(myHelloSpanned)) is not equal to myHelloSpanned - it's got extra padding. I didn't know how to resolve this nicely.

Update: this answer https://stackoverflow.com/a/56944152/6007104 has been updated, and is now the preferred answer

Here's a more readable Kotlin extension which doesn't use deprecated APIs, works on all Android versions, and doesn't require strings to be wrapped in CDATA sections:

fun Context.getText(id: Int, vararg args: Any): CharSequence {

    val escapedArgs = args.map {
        if (it is String) TextUtils.htmlEncode(it) else it
    }.toTypedArray()

    val resource = SpannedString(getText(id))
    val htmlResource = HtmlCompat.toHtml(resource, HtmlCompat.TO_HTML_PARAGRAPH_LINES_CONSECUTIVE)
    val formattedHtml = String.format(htmlResource, *escapedArgs)
    return HtmlCompat.fromHtml(formattedHtml, HtmlCompat.FROM_HTML_MODE_LEGACY)
}

You can add an alias as as extension of Fragment - just remember to spread the args in between:

fun Fragment.getText(id: Int, vararg args: Any) = requireContext().getText(id, *args)

You can use java.lang.String for string formatting in Kotlin

fun main(args : Array<String>) {
  var value1 = 1
  var value2 = "2"
  var value3 = 3.0
  println(java.lang.String.format("%d, %s, %6f", value1, value2, value3))
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top