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.

Meshing: Explore Mesh Gradients for SwiftUI!
Meshing is the tool you need for working with stunning MeshGradients in SwiftUI with ease!Key Features:- Intuitive visual editor for 2x2, 3x3, and 4x4 MeshGradients- Real-time preview of your gradient creations- Precise control over color positions with draggable points- Customizable background color- Toggle for smooth color transitions- One-click code generation for integration into your SwiftUI projectsPerfect for:- Apple Platforms Developers- UI/UX Designers- SwiftUI Enthusiasts- Anyone looking to add vibrant, complex gradients to their appsBoost Your Workflow!- Experiment with complex gradients in seconds- Save hours of manual coding and tweaking- Achieve professional-looking results quicklyI was tired of working with Previews in Xcode for mesh gradients, so I created an app for it. MeshGradients is a fun visual experience. No more guesswork or manual coding – design the perfect gradient visually and let Meshing generate the SwiftUI code for you!!System Requirements:- macOS 15.0, iOS 18.0, iPadOS 18.0, visionOS 2.0 or later- Xcode 16.0 or later (for use in SwiftUI projects)Download now and start creating beautiful, complex gradients in minutes!

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:

  1. 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.
  2. Put your Localizable.xcstrings file in the right place. For me, it was inside the Sources folder of my package.
  3. Update your Package.swift file to include the localization resource.
  4. 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!