Exploring Xcode: Localizing Text in Swift Packages and SwiftUI
I woke up this morning to a surprise: a message about my app, Meshing, in Simplified Chinese.
The user shared some feature requests and kind words about the app. Instead of replying in English, I took the time to translate my response to Simplified Chinese. A friend helped me confirm the translation before I sent it. 谢谢!
The Localization Challenge
This interaction gave me an idea. Why not translate my entire app into Simplified Chinese?
I started by adding the new Xcode Strings Catalog to the main scheme shared by iOS and macOS. But, it only showed one translation. Then I realised that most of my views are in a shared Swift Package called Meshing Shared. That is where I needed to add the translations.
So, I added the Localizable.xcstrings file to Meshing Shared.
I built the project. Nothing happened.
No translation keys appeared. I tried moving the file to the resources folder. Still nothing. After some trial and error, I found the right spot. The file needed to be in a folder inside Meshing Shared, which was inside the Sources directory.
Updating Package.swift
I had to update the Package.swift
file. I set the default translation and specified the path to the resources:
// swift-tools-version: 6.0
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "MeshingShared",
defaultLocalization: "en",
platforms: [
.macOS(.v15),
.iOS(.v18),
.tvOS(.v18),
.watchOS(.v10),
.macCatalyst(.v18),
.visionOS(.v2)
],
products: [
.library(
name: "MeshingShared",
targets: ["MeshingShared"]
)
],
targets: [
.target(
name: "MeshingShared",
resources: [.process("Localizable.xcstrings")]
)
]
)
I could finally see the keys in Localizable.xcstrings
file!
Although, when I ran the project, it still showed English words instead of Simplified Chinese.
Custom Initializers in SwiftUI
Then I realized I needed to specify the bundle as module for each key. I created initializers for Text
, Toggle
, and Button
to handle this:
import SwiftUI
extension Text {
init(_ key: LocalizedStringKey, comment: String = "") {
self.init(key, bundle: .module)
}
}
extension Toggle where Label == Text {
init(_ titleKey: LocalizedStringKey, isOn: Binding<Bool>) {
self.init(titleKey, isOn: isOn, bundle: .module)
}
}
extension Button where Label == Text {
init(_ titleKey: LocalizedStringKey, action: @escaping () -> Void) {
self.init(action: action) {
Text(titleKey, bundle: .module)
}
}
}
With these initializers in place, I could finally use localized strings in my SwiftUI views within the Swift package. Here is an example:
ColorPicker(selection: $viewModel.background, label: { Text("Background") })
Toggle("Smooth Colors", isOn: $viewModel.smoothsColors.animation())
Toggle("Show Control Points", isOn: $viewModel.showDots.animation())
Toggle("Show Control Labels", isOn: $viewModel.showLabels.animation())
Toggle("Show Mockup", isOn: $viewModel.showMockup.animation())
Using the same code with localized key:
HStack {
Button("Reset") {
viewModel.resetToDefault()
}
.buttonStyle(.borderedProminent)
.tint(.red)
Button("Copy") {
viewModel.copyGradientToClipboard(from: gradientView())
}
.buttonStyle(.borderedProminent)
}
These handy custom initializers in the package helped me create consistent localization and made managing translations easier.
Moving Forward
Even thought it was frustrating for a bit that I had to go through some workarounds, here is what I have learned so far:
- Xcode's new Strings Catalog (.xcstrings) is powerful, but needs extra setup in Swift packages. I directly give the
.json
file to Claude to provide me with the initial translations while I provide it with context about the app. Then I give it to a friend to do last-minute cross check to verify if everything makes sense. - Put your
Localizable.xcstrings
file in the right place. For me, it was inside the Sources folder of my package. - Update your Package.swift file to include the localization resource.
- Create custom initializers for SwiftUI components to simplify using localized strings and use
Bundle.module
to access resources (including localized strings) from within a Swift package.
Have you tried localizing a Swift package? What challenges did you face? I would love to hear about your experiences!