Techniques for Automatic Merging of String Catalogs in Multi-Package Monorepos

Published on

Get weekly handpicked updates on Swift and SwiftUI!

Announced at WWDC23, Swift introduces an amazing new way for internationalization (i18n) for Swift apps and packages with String Catalogs. This guide covers the basics of getting started with String Catalogs in a new project: WWDC23 String Catalogs. However, it doesn’t touch on a use case where there is a single monorepo with multiple Swift packages (and there are no sample projects).

In this article, we’ll explore how to set up your monorepo so each Swift Package can have its own String Catalog, which gets auto-merged into one when building your host app, without breaking a sweat!

This article is written by my friend and fellow iOS developer, Luca Ban. He will be sharing his unique insights and experiences on Swift Catalogs. Thanks to his contribution, here is this fantastic guest post!

Initial Setup

In this blog post, we will create a simple monorepo containing two localized projects: an Xcode project and a Swift Package. Our goal is to have the following view localized separately (with one string in the host app and one string in the Swift Package) while combining the translations when building our host app.

Swift
VStack {
  // Button label localized in **host app** String Catalog:
  MyButton(action: {}, label: "Let's tap here!")

  // Default button label fallback localized in **Swift Package** String Catalog:
  MyButton(action: {})
}

We will complete the initial setup in steps 1 through 7 below. If you already have a monorepo with Swift Packages and a host app, you can skip ahead to Making the Swift Package Translations Visible in the HostApp.

  1. Create HostApp Project and MyComponents Package

    • Using Xcode, create one Xcode project (/HostApp) and one Swift Package (/MyComponents).
    • Referencing the WWDC23 video:
      • Add a new ‘String Catalog’ to your host app.
      • Add Text(LocalizedStringResource("Text from host app", comment: "shown on the main screen")).
      • Build, and you will see your String Catalog populated with "Text from host app" and the comment.
      • Add a new language by hitting + in the bottom left of the String Catalog and translate this string.
  2. Verify Initial Setup

    • Hold option and click the RUN button in Xcode on your HostApp.
    • Set the language to the new language to test, and run the app.
    • You should see the translated string.

Adding Localized Text in MyComponents Package

  1. Create Localized Button in MyComponents

    • In MyComponents, let’s create a button that can receive a label as LocalizedStringResource and has a fallback label already translated in the extra language.
    Swift
    public struct MyButton: View {
      private let label: LocalizedStringResource?
      private let action: () -> Void
    
      public init(
        action: @escaping () -> Void,
        label: LocalizedStringResource? = nil
      ) {
        self.action = action
        self.label = label
      }
    
      public var body: some View {
        Button(action: action) {
          Text(label ?? LocalizedStringResource("Tap here", comment: "Button label fallback text"))
        }
      }
    }
  2. Set Up String Catalog for MyComponents

    • Create a new folder for the String Catalog at MyComponents/Sources/MyComponents/Resources.
    • Save a String Catalog file named Localizable.xcstring in this folder.
    • In Package.swift you need to add one line to note that this Swift Package is localized:
    Swift
    let package = Package(
        name: "MyComponents",
        defaultLocalization: "en",
        // ...
    • Bundling the String Catalog in the Resources folder needs no extra configuration, as this is the default location for any resources bundled with a Swift Package.
  3. Import MyComponents into HostApp

    • In Xcode, go to HostApp target > General > Frameworks, Libraries, and Embedded Content > Add Other… > Add Package Dependency… > Add Local… > Select the /MyComponents folder.

Integrate MyButton & Build to Add New Translations

  1. Update ContentView in HostApp

    Swift
    VStack(spacing: 16) {
      Text(LocalizedStringResource("Text from host app", comment: "shown on the main screen"))
      
      MyButton(action: {}) // should use fallback label from MyButton
      
      MyButton(action: {}, label: "Let's tap here!")
    }
  2. Build and Add New Translations

    • If you build HostApp again, it will scan both HostApp and the imported MyComponents package for new Swift strings to add to the String Catalog.
    • It will now find new strings and add them to the String Catalogs:
      • in the HostApp String Catalog we see the newly added string “Let’s tap here!”
      • in the MyComponents String Catalog we see the newly added string “Tap here”
    • Translate both strings and run the app again in the new language.

Making the Swift Package Translations Visible in the HostApp

Despite localizing the fallback text for “Tap here” in MyComponents, it won’t be shown in HostApp without extra setup. The String Catalog bundled with MyComponents seems to be ignored completely by our HostApp.

Previously, with String Dictionaries, we would need an elaborate script to merge multiple files during a build step. However, now there is a solution that doesn’t require any extra scripts to run.

You have to make sure to scope each instance of LocalizedStringResource in your Swift Package to that module itself. Adding the bundle argument, however, doesn’t provide an easy way to do so out of the box:

screenshot_LocalizedStringResource_bundle

Let’s solve this in a clean way.

  1. Scoping a Swift Package’s LocalizedStringResource to its Own Module

    • Let’s define a new option for the bundle argument of a LocalizedStringResource.
    • Add an extension to define .module:
    Swift
    extension LocalizedStringResource.BundleDescription {
      /// Convenience property to compute the `BundleDescription` for _this_ Swift Package
      static let module: LocalizedStringResource.BundleDescription = .atURL(Bundle.module.bundleURL)
    }
  2. Update MyButton with Bundle

    • Now let’s use this new bundle: .module in our code for MyButton:
    Swift
    public struct MyButton: View {
      // ...
      public var body: some View {
        Button(action: action) {
          Text(label ?? LocalizedStringResource("Tap here", bundle: .module, comment: "Button label fallback text"))
        }
      }
    }
  3. Final Verification

    • Build and run HostApp again.
    • The translations bundled with the Swift Package String Catalog in /MyComponents are now displayed correctly!
    • You can repeat this process for as many Swift Packages as you have in your monorepo.
    • You can also use this method to provide a Swift Package via SPM that comes bundled with their own translations.

This solution provides a clean and neat way to merge multiple String Catalogs in a monorepo setup, greatly improving the process over older methods requiring custom scripts. No additional configuration in Xcode is needed.

Here is my proof of concept monorepo with the same setup as this article: github.com/mesqueeb/SwiftStringCatalogsPOC. Clone and run it locally to test it out.

I’m Luca Ban (mesqueeb on GitHub/Twitter), and I hope you enjoyed this article and it helped you out.

Profile

Luca Ban

Weekly Swift & SwiftUI insights, delivered every Monday night. Join developers worldwide.
Easy unsubscribe, zero spam guaranteed