To consolidate repeated messages in a Mac app, give each shared meaning one localization key and one Swift definition, then use it wherever that message appears. Keep its arguments, plural rules, and translator context together; matching English text alone is insufficient.

This walkthrough is for a Mac developer maintaining a selection summary in both SwiftUI and an Interface Builder window. The concrete task is to display messages such as 2 items selected in Batch A. consistently, without maintaining separate sentence templates in a XIB and Swift code. All counts and labels below are synthetic.

Decide which repeated messages belong together

Before editing catalogs, define the message contract: what the sentence means, which values it accepts, and where it appears. For this example, the contract is:

Contract element Definition
Meaning Number of currently selected items within a named collection
Key selection.summary
Table Localizable
Bundle Main app bundle
Argument 1 An Int64 selection count
Argument 2 A literal collection label, such as Batch A
Variation Plural form determined by the selection count
Context A complete status sentence displayed in SwiftUI and an AppKit label

Use this contract as a consolidation test. Two occurrences can share the message when their meaning, arguments, and grammatical role agree. A selected-item count and a total-item count should remain separate even if both currently display 2 items. A button named Select also needs its own message because it expresses an action.

Apple supports explicit keys with separate default values specifically to disambiguate text. This lets you name a message by purpose while retaining readable source wording. A comment supplies context; the key identifies the entry. String’s explicit-key initializer.

Treat the table and bundle as part of the lookup contract too. A key in another table is a separate lookup target. Apple documents table selection through the table or tableName parameter and recommends passing localizable text through LocalizedStringResource. For this example, keep the helper and catalog in the app target and use the main bundle. Preparing your app’s text for translation.

Understand the boundary between extraction and runtime formatting

Extraction happens while Xcode builds: it discovers supported localizable expressions and synchronizes their source text with catalogs. Runtime localization happens when the app resolves a message and substitutes actual values. Finding an entry in a catalog proves that extraction found it; it does not prove that every control displays it correctly.

Interface Builder has its own resource workflow. Apple documents catalogs paired with a storyboard or XIB, while Swift source normally contributes to the Localizable table. The documented workflow does not describe a cross-resource alias that merges messages simply because their visible wording matches. A practical supported path is to assign the shared localized message from Swift after the interface loads. This is an implementation derived from Apple’s documented APIs. Discover String Catalogs.

SwiftUI also distinguishes a literal localization key from an ordinary string value. For example, Text("Summary") requests localization, while passing a String variable displays its value without that lookup. Wrapping a runtime string in LocalizedStringKey requests lookup, but should not be confused with proving that extraction discovered its possible values. LocalizedStringKey.

The design below therefore keeps the extractable literal inside one helper. It carries the count and label in a LocalizedStringResource, then resolves that resource where a finished string is needed. Apple describes this resource type as a way to defer localization lookup. It is available on macOS 13 and later, which is the minimum target for this example. LocalizedStringResource.

Define the shared message once in Swift

Create a macOS app project using SwiftUI, set its deployment target to macOS 13 or later, and add Localizable.xcstrings to the app target. In current Xcode documentation, the creation path is File > New > File from Template; search for string and select String Catalog. Localizing and varying text with a string catalog.

Replace the generated app entry point with the complete program below. Remove the original app entry point so that the target contains only one @main declaration. The SwiftUI window runs independently of a XIB; the included AppKit subclass is used when adapting an existing Interface Builder label in the next section.

import AppKit
import Foundation
import SwiftUI

enum SelectionMessages {
    static func summary(
        count: Int64,
        collectionLabel: String
    ) -> LocalizedStringResource {
        LocalizedStringResource(
            "selection.summary",
            defaultValue: "\(count) items selected in \(collectionLabel).",
            table: "Localizable",
            comment: "Complete selection-status sentence used in SwiftUI and an AppKit label. Argument 1 is the selected-item count; argument 2 is a literal collection label. Pluralize by argument 1."
        )
    }
}

struct SelectionDemoView: View {
    private let counts: [Int64] = [0, 1, 2]
    private let collectionLabel = "Batch A"

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            ForEach(counts, id: \.self) { count in
                Text(String(localized: SelectionMessages.summary(
                    count: count,
                    collectionLabel: collectionLabel
                )))
            }
        }
        .padding(24)
        .frame(minWidth: 360, minHeight: 160)
    }
}

final class SelectionSummaryField: NSTextField {
    override func awakeFromNib() {
        super.awakeFromNib()
        // Synthetic initial state for the Interface Builder example.
        updateSelection(count: 2, collectionLabel: "Batch A")
    }

    func updateSelection(count: Int64, collectionLabel: String) {
        stringValue = String(localized: SelectionMessages.summary(
            count: count,
            collectionLabel: collectionLabel
        ))
    }
}

@main
struct SelectionDemoApp: App {
    var body: some Scene {
        WindowGroup {
            SelectionDemoView()
        }
    }
}

The static key identifies the message; the interpolated default value supplies its source sentence and runtime arguments. The initializer also accepts the table, bundle, and translator comment. Its default bundle is the main bundle. LocalizedStringResource’s explicit-key initializer.

The label Batch A is sample content inserted into the sentence. It is not another message key. This distinction keeps collection labels separate from the surrounding interface wording. If your real application uses predefined, translated collection names, localize those names through their own definitions before supplying them here.

Do not assemble this sentence from independently localized pieces such as a count, items selected in, and a label. Keeping the complete sentence in the contract gives localizers one unit whose wording and argument order can be adjusted together.

Route the Interface Builder label through the same definition

For an existing XIB or storyboard, use a static NSTextField label for the summary. Set its custom class to SelectionSummaryField and its module to the app target. Clear the old sentence from its design-time string value, because this particular label now receives its text from the shared Swift definition.

When the interface loads, the subclass calls super.awakeFromNib() and assigns the synthetic initial summary. Apple documents awakeFromNib() as the post-loading hook, with outlet and action connections established, and requires calling the superclass implementation. awakeFromNib().

In the real window, call updateSelection(count:collectionLabel:) on the connected label whenever the selection changes, using the same model values supplied to SwiftUI. Replace the synthetic initial assignment with the window’s actual initialization path. A one-time assignment during loading cannot track later selection changes.

The AppKit adapter sets stringValue, a documented way to assign an NSTextField’s text. This leaves layout in Interface Builder while moving ownership of the message to Swift. Allow sufficient width or wrapping for the complete sentence. NSTextField.

Keep unrelated static interface text in its existing resource workflow. This migration concerns one shared message; moving every label into Swift is unnecessary. Also check that a binding or another controller assignment does not subsequently replace the summary with the old wording.

Configure plural variants without changing the arguments

Choose Product > Build, open Localizable.xcstrings, and locate selection.summary. Control-click the entry, choose Vary by Plural, and select the numeric count argument if Xcode asks which argument to vary. Apple documents this workflow for interpolated messages. Localizing and varying text with a string catalog.

For the English source localization, make the singular variant produce 1 item selected in Batch A. and the other variant produce sentences such as 2 items selected in Batch A. Preserve both argument placeholders supplied by Xcode. Edit the surrounding wording rather than replacing a placeholder with a sample count or label.

Synthetic input Expected English result after plural configuration
Count 0, label Batch A 0 items selected in Batch A.
Count 1, label Batch A 1 item selected in Batch A.
Count 2, label Batch A 2 items selected in Batch A.
Count 2, label Batch B 2 items selected in Batch B.

These are illustrative acceptance cases, not measured results. Before plural configuration, the default source sentence still contains items for every count. Extraction preserves the interpolated message; it does not independently correct its grammar.

The changing label in the final row is useful diagnostically: if the display remains Batch A, a sample value may have been hardcoded in a variant or a caller may be supplying stale state. If the label disappears, inspect the variant’s placeholders before changing the helper’s signature.

Verify every display path before removing old entries

Test the SwiftUI window and the actual Interface Builder window separately. Use the same counts and labels in both. Check initial loading, a later selection change, and a longer synthetic label such as Batch A Extended. Confirm that the wording agrees and the sentence remains readable.

For each supported localization, choose Product > Scheme > Edit Scheme, select Run, open Options, and set App Language. Run the app again and inspect both display paths. Apple documents these scheme settings for testing localizations without changing the entire system’s language. Testing localizations when running your app.

Use the following sequence when a result is wrong:

  1. Entry missing: Confirm the catalog belongs to the app target and build the relevant scheme. Check that Use Compiler to Extract Swift Strings is enabled.
  2. Source wording appears unexpectedly: Check the key, table, bundle, and translation before treating readable fallback text as success.
  3. Only the Interface Builder label differs: Check its custom class, module, loading path, and subsequent assignments.
  4. Only later values differ: Check that both interfaces receive updated model values.
  5. Plural wording is wrong: Check the numeric argument selected for variation and every applicable variant.

Apple documents the compiler extraction setting and explains that translated entries no longer found in source may remain marked Stale. That state is a cleanup signal, not proof that a resource is unused. Review the old interface assignment and Swift callers before deleting an entry and its translations. Discover String Catalogs.

The consolidation is complete when both interfaces obtain the same contextual message from one definition, its arguments remain intact, and the old sentence templates have no remaining callers. Keep the contract and acceptance cases beside the helper so that future wording changes preserve the meaning shared by both windows.

Sources