Override locale in a SwiftUI view at runtime

Published on March 23, 2023

Sometimes a user wants to use an app in a different locale than their system locale. I faced this when developing Score Wonders. In the board game 7 Wonders (which the app is a companion for) there are different kinds of buildings/cards the player can build/play. All buildings/cards has a category like "Military", "Civilian", "Commerce" and "Science". The app is currently localized to English and Danish, so I naturally translated these categories to Danish, but it didn't feel right. As a long time player of the game, I have become accustomed to the English names, so I decided to make a setting for using the English names for the categories.

Localizing Xcode Previews

When it came to the implementation I had some ideas on how to do it, but suddenly I realized that SwiftUI has support for it all along, and that I have been using it in my previews in Xcode the whole time. It is done by using the .environment(\.locale, Locale(identifier: "da")).

Here is an example of the previews for my icon picker view, which uses my helper view LocalizedPreviews. This helper lets me easily see previews of my views in the supported locales:

import SwiftUI

struct AppIconPicker_Previews: PreviewProvider {
    static var previews: some View {
        LocalizedPreviews {
            NavigationStack {
                AppIconPicker()
                    .environmentObject(PremiumController())
                    .environmentObject(SettingsController())
            }
        }
    }
}

struct LocalizedPreviews<Content>: View where Content: View {
    var displayName: String?
    let content: () -> Content

    var body: some View {
        ForEach(previewLocales) {
            content()
                .environment(\.locale, $0.locale)
                .previewDisplayName([displayName, $0.name].compactMap { $0 }.joined(separator: " - "))
        }
    }
}

private let previewLocales: [PreviewLocale] = [
    .init(locale: Locale(identifier: "en"), name: "English"),
    .init(locale: Locale(identifier: "da"), name: "Danish")
]

private struct PreviewLocale: Identifiable {
    var id: String { locale.identifier }
    let locale: Locale
    let name: String
}

Localizing the categories in the Score calculator

So I took this functionality and added it to my score calculator, where I was showing the category names.

If the setting was enabled I assigned a newly created English locale to the environment for the PointRow and otherwise just used the current locale:

struct PlayView: View {
    ...
    @Environment(\.locale) private var locale
    @EnvironmentObject private var settingsController: SettingsController
    
    var body: some View {
        Form {
            ...
            PointRow(title: "Commerce", asset: .systemImage("person.line.dotted.person.fill", .orange), value: $play.commercePoints)
                .environment(\.locale, settingsController.useEnglishCategoryNames ? .init(identifier: "en") : locale)
            ...
        }
    }
}

Conclusion

SwiftUI has some simple features which makes it a lot easier to do powerful stuff. The .environment(...) is surely one of these features!

With this approach it was very easy to just override the locale for a small part of the view hierarchy and reach my goal of showing the categories in English.

The .environment(...) in SwiftUI has a lot more to offer than just the \.locale. Apple has some documentation of the key paths available in the SwiftUI environment.

Tagged with: