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.
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.
-
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.
- Using Xcode, create one Xcode project (
-
Verify Initial Setup
- Hold
option
and click the RUN button in Xcode on yourHostApp
. - Set the language to the new language to test, and run the app.
- You should see the translated string.
- Hold
Adding Localized Text in MyComponents Package
-
Create Localized Button in MyComponents
- In
MyComponents
, let’s create a button that can receive alabel
asLocalizedStringResource
and has a fallback label already translated in the extra language.
Swiftpublic 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")) } } }
- In
-
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:
Swiftlet 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.
- Create a new folder for the String Catalog at
-
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.
- In Xcode, go to
Integrate MyButton & Build to Add New Translations
-
Update ContentView in HostApp
SwiftVStack(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!") }
-
Build and Add New Translations
- If you build
HostApp
again, it will scan bothHostApp
and the importedMyComponents
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”
- in the
- Translate both strings and run the app again in the new language.
- If you build
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:
Let’s solve this in a clean way.
-
Scoping a Swift Package’s
LocalizedStringResource
to its Own Module- Let’s define a new option for the
bundle
argument of aLocalizedStringResource
. - Add an extension to define
.module
:
Swiftextension LocalizedStringResource.BundleDescription { /// Convenience property to compute the `BundleDescription` for _this_ Swift Package static let module: LocalizedStringResource.BundleDescription = .atURL(Bundle.module.bundleURL) }
- Let’s define a new option for the
-
Update MyButton with Bundle
- Now let’s use this new
bundle: .module
in our code forMyButton
:
Swiftpublic struct MyButton: View { // ... public var body: some View { Button(action: action) { Text(label ?? LocalizedStringResource("Tap here", bundle: .module, comment: "Button label fallback text")) } } }
- Now let’s use this new
-
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.
- Build and run
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
- x: mesqueeb
- github: mesqueeb
- I make board games for visionOS! 🎲 Check out craftingtable.studio