Articles, podcasts and news about Swift development, by John Sundell.

Styling localized strings in Swift

Published on 13 Apr 2021
Basics article available: Strings

Localizing an app into multiple languages can often significantly boost its chances for success on the App Store, as many users tend to prefer using apps that support their own, native language.

However, while Apple does provide many APIs and other kinds of infrastructure for handling resources like localized strings, things can often get quite tricky if we want to incorporate some form of mixed styling into the strings that we render within our apps.

For example, let’s say that we’re working on an app that shows lists of new movies, and that we’d like to emphasize the word “New” within the title of one of our UIs. If our app wasn’t localized, then doing so would be quite straightforward — we could simply search the title string for that particular word and then treat it differently when rendering its label — but what if our app does indeed support multiple languages?

One approach on how to handle that type of situation would be to mark what part of each string that we wish to emphasize within our localized string files — like this:

// English
"NewMovies" = "**New** movies";

// Swedish
"NewMovies" = "**Nya** filmer";

// Polish
"NewMovies" = "**Nowe** filmy";

Looking at the above example, it might seem like another option could be to simply emphasize the first word within each string. However, that would be a quite fragile solution, since not all languages use the same word order, and what if we’d add some form of prefix to our strings in the future?

Next, we’re going to have to parse the above string format in order to turn each piece of text into either an NSAttributedString (for UIKIt-based UIs), or a SwiftUI Text instance.

To get started, let’s define a dedicated LocalizedString type, in which we’ll be able to implement all of the required logic. Initially, we could implement APIs for initializing an instance with a localized string key, as well as for resolving a raw String using the built-in NSLocalizedString function:

struct LocalizedString {
    var key: String

    init(_ key: String) {
        self.key = key
    }

    func resolve() -> String {
        NSLocalizedString(key, comment: "")
    }
}

extension LocalizedString: ExpressibleByStringLiteral {
    init(stringLiteral value: StringLiteralType) {
        key = value
    }
}

We also make it possible to express a LocalizedString value using a string literal, which will come very much in handy once we start to integrate our new type with both UIKit and SwiftUI.

With the above type in place, let’s now move on to our actual parsing and rendering, starting with NSAttributedString.

Instabug

Instabug: Whether it’s crashes, slow screen transitions, delayed network calls, or unresponsive UIs — Instabug automatically gives you all of the logs you need to fix bugs and issues, and to ship high-quality apps. Get started now.

Attributed strings

Like the name of the type implies, an NSAttributedString enables us to add rendering attributes to a plain String, which in this case make it possible for us to encode that certain parts of a given localized string should be emphasized.

To make that happen, let’s extend our LocalizedString type with a method that splits a given raw localized string into components using our chosen marker (**, Markdown-style), and then picks either a default or bold font depending on whether the index of a given component is even or odd:

extension LocalizedString {
    typealias Fonts = (default: UIFont, bold: UIFont)

    static func defaultFonts() -> Fonts {
        let font = UIFont.preferredFont(forTextStyle: .body)
        return (font, .boldSystemFont(ofSize: font.pointSize))
    }

    func attributedString(
        withFonts fonts: Fonts = defaultFonts()
    ) -> NSAttributedString {
        let components = resolve().components(separatedBy: "**")
        let sequence = components.enumerated()
        let attributedString = NSMutableAttributedString()

        return sequence.reduce(into: attributedString) { string, pair in
            let isBold = !pair.offset.isMultiple(of: 2)
            let font = isBold ? fonts.bold : fonts.default

            string.append(NSAttributedString(
                string: pair.element,
                attributes: [.font: font]
            ))
        }
    }
}

To learn more about the above use of tuples to define lightweight types, check out this article.

Using the above new API, we’ll now be able to render localized strings with mixed styling using UIKit classes like UILabel and UITextView, which both support attributed strings.

Now, before we continue by implementing a SwiftUI equivalent of the above functionality, let’s take a quick moment to refactor our actual string parsing and rendering logic into a reusable utility that we’ll be able to call from both implementations, as to avoid code duplication.

One way to do that would be to implement a generic, reduce-style rendering function that takes an initial result, as well as a handler that performs the actual string concatenation — for example like this:

private extension LocalizedString {
    func render<T>(
        into initialResult: T,
        handler: (inout T, String, _ isBold: Bool) -> Void
    ) -> T {
        let components = resolve().components(separatedBy: "**")
        let sequence = components.enumerated()

        return sequence.reduce(into: initialResult) { result, pair in
            let isBold = !pair.offset.isMultiple(of: 2)
            handler(&result, pair.element, isBold)
        }
    }
}

With the above in place, we can heavily simplify our NSAttributedString-based method from before, since it can now be focused on just annotating and combining the strings that were passed into its handler:

extension LocalizedString {
    ...

    func attributedString(
        withFonts fonts: Fonts = defaultFonts()
    ) -> NSAttributedString {
        render(
            into: NSMutableAttributedString(),
            handler: { fullString, string, isBold in
                let font = isBold ? fonts.bold : fonts.default

                fullString.append(NSAttributedString(
                    string: string,
                    attributes: [.font: font]
                ))
            }
        )
    }
}

With that little refactoring task finished, let’s now start implementing our SwiftUI-based string rendering.

SwiftUI texts

One somewhat “hidden” feature of SwiftUI’s Text type is that multiple text values can be directly concatenated using the add operator, just as if they were raw String values — which still preserves the styling of each individual instance.

So all that we have to do to add a SwiftUI-based rendering API to our LocalizedString type is to call our new render method, and to then combine each of strings that it gives us — like this:

extension LocalizedString {
    func styledText() -> Text {
        render(into: Text("")) { fullText, string, isBold in
            var text = Text(string)

            if isBold {
                text = text.bold()
            }

            fullText = fullText + text
        }
    }
}

Time to integrate

Next, to make both our UIKit and SwiftUI-based methods a bit easier to use, let’s also extend both UILabel and Text with convenience APIs that let us directly initialize a label using a LocalizedString value:

extension UILabel {
    convenience init(styledLocalizedString string: LocalizedString) {
        self.init(frame: .zero)
        attributedText = string.attributedString()
    }
}

extension Text {
    init(styledLocalizedString string: LocalizedString) {
        self = string.styledText()
    }
}

With the above in place, we can now use the fact that LocalizedString values can be expressed using string literals to create styled, localized labels using either SwiftUI or UIKit, simply by doing this:

// UIKit
UILabel(styledLocalizedString: "NewMovies")

// SwiftUI
Text(styledLocalizedString: "NewMovies")

Very nice! However, currently we’re always re-parsing each string every time that it’s requested, which might not be an issue if we’re not updating our UI too often, but let’s also explore how we could add caching to our implementation as well.

Caching

Since all of the strings that we’re parsing are loaded from a static resource (our localized strings file for the user’s current language), we should be able to cache them quite aggressively. One way to do that would be to use the Cache type that we built in “Caching in Swift”, and to then modify our render function so that it supports reading and writing from such a cache — like this:

private extension LocalizedString {
    static let attributedStringCache = Cache<String, NSMutableAttributedString>()
static let swiftUITextCache = Cache<String, Text>()

    func render<T>(
        into initialResult: @autoclosure () -> T,
        cache: Cache<String, T>,
        handler: (inout T, String, _ isBold: Bool) -> Void
    ) -> T {
        if let cached = cache.value(forKey: key) {
    return cached
}

        let components = resolve().components(separatedBy: "**")
        let sequence = components.enumerated()

        let result = sequence.reduce(into: initialResult()) { result, pair in
            let isBold = !pair.offset.isMultiple(of: 2)
            handler(&result, pair.element, isBold)
        }

        cache.insert(result, forKey: key)
        return result
    }
}

The reason we now mark our initialResult parameter with the @autoclosure attribute is to prevent it from being evaluated in case a cached value was found. To learn more, check out this article.

Note that our attributedStringCache stores NSMutableAttributedString instances, which is because that’s the type that we’re working with when calling render from our attributedString method. While there’s no real harm in using such mutable instances internally within our LocalizedString type, we should now definitely copy all attributed strings before returning them, as to prevent any accidental sharing of mutable state.

So let’s do that, while also updating both of our string rendering methods to support our new caching functionality:

extension LocalizedString {
    ...

    func attributedString(
        withFonts fonts: Fonts = defaultFonts()
    ) -> NSAttributedString {
        let string = render(
            into: NSMutableAttributedString(),
            cache: Self.attributedStringCache,
            handler: { fullString, string, isBold in
                ...
            }
        )

        return NSAttributedString(attributedString: string)
    }

    func styledText() -> Text {
        render(
            into: Text(""),
            cache: Self.swiftUITextCache,
            handler: { fullText, string, isBold in
                ...
            }
        )
    }
}

And with that final piece in place, our new LocalizedString API is finished, and we can now render fully localized, styled strings in a performant and predictable manner, using either SwiftUI or UIKit.

Supporting multiple styles, and HTML as an alternative

Of course, the system that we built in this article currently only supports turning parts of a string bold, but we could always continue iterating on it in case we wanted to add support for multiple kinds of styles, although that might require somewhat more sophisticated string parsing techniques.

For example, we could either use the open source Sweep library to identify ranges that should be styled with a given set of attributes, or use techniques like the ones that were covered in “String parsing in Swift” to make that happen.

Another option, which has its own set of tradeoffs, would be to render certain strings as HTML, which NSAttributedString actually has complete support for. That way, we could place any sort of HTML styles (such as <b> or <em>) within our localized strings and then turn them into fully renderable attributed strings like this:

extension LocalizedString {
    func attributedString() throws -> NSAttributedString {
        let data = Data(resolve().utf8)

        return try NSAttributedString(
            data: data,
            options: [
                .documentType: NSAttributedString.DocumentType.html,
                .characterEncoding: String.Encoding.utf8.rawValue
            ],
            documentAttributes: nil
        )
    }
}

However, one big downside of the above technique is that it requires our localized string files to contain HTML code (which could easily get malformed as multiple people, including external translators, might edit those files over time). Also, since we would be rendering those strings as if they were web clips, we’d then also have to style each such label using web technologies as well, which could quickly make our setup quite complex and hard to maintain.

Support Swift by Sundell by checking out this sponsor:

Instabug

Instabug: Whether it’s crashes, slow screen transitions, delayed network calls, or unresponsive UIs — Instabug automatically gives you all of the logs you need to fix bugs and issues, and to ship high-quality apps. Get started now.

Conclusion

Combining localization with dynamically rendered content or styles can at times be quite difficult — even something relatively simple as emphasizing parts of a given string can require a fair bit of code to implement. Hopefully this article has shown you a few tips and tricks on how that can be done, and perhaps the techniques covered can provide a starting point for building your own system for rendering styled, localized strings.

If you have any questions, comments, or feedback, then feel free to reach out via either Twitter or email.

Thanks for reading!