Localization

May 20, 2026 · View on GitHub

Osaurus uses String Catalogs (.xcstrings) for translations. There are no legacy .strings files or .lproj folders.

Where strings live

CatalogPathContents
UI (all app screens)Packages/OsaurusCore/Resources/Localizable.xcstringsMenus, settings, chat, agents, plugins, etc.
Info.plistApp/osaurus/InfoPlist.xcstringsPrivacy usage descriptions, bundle display name

All SwiftUI and String UI text in OsaurusCore must resolve against the package bundle, not the main app bundle.

Supported locales

LocaleCodeStatus
EnglishenSource language
GermandeRequired
Simplified Chinesezh-HansRequired

zh-Hans is listed in the Xcode project's knownRegions. Add new locales there when expanding support.

Swift API

Helpers live in Packages/OsaurusCore/Utils/:

APIUse for
L("…")String — menus, alerts, String(format:), labels passed to AppKit
Text(localized: "…")SwiftUI labels (uses package bundle)
.localizedHelp("…")Tooltips
ToastManager.shared.*Localized("…")Toasts with static title/message copy

Dynamic keys (stored in a String variable):

Text(LocalizedStringKey(title), bundle: .module)

After adding a key in code, add de and zh-Hans in Localizable.xcstrings (Xcode String Catalog editor).

Avoid raw Text("…"), .help("…"), Button("…"), panel.title = "…", and UNMutableNotificationContent.title = "…" in Packages/OsaurusCore. CI flags these because they usually resolve against the wrong bundle.

Adding a new language

  1. Add the locale to knownRegions in App/osaurus.xcodeproj/project.pbxproj.
  2. Add translations in Packages/OsaurusCore/Resources/Localizable.xcstrings.
  3. Translate Info.plist strings in App/osaurus/InfoPlist.xcstrings when needed.
  4. Run bash scripts/i18n/check.sh.
  5. Smoke-test with the system language set to the new locale.

Import from another catalog:

python3 scripts/i18n/merge-locale.py \
  --target Packages/OsaurusCore/Resources/Localizable.xcstrings \
  --source path/to/other/Localizable.xcstrings \
  --locale <locale-code>

Validation

bash scripts/i18n/check.sh

CI runs this on every pull request. It validates catalog coverage, checks that Swift localization literals exist in the catalog, and runs a Swift literal lint. Keys with no de/zh-Hans yet (including Xcode en-only auto-extractions and empty stubs the catalog editor injects for raw Text("…") literals) are ignored — the catalog is allowed to grow without breaking CI. Run the pruner manually when you want to clean it up (see scripts/i18n/prune-catalog.py below).

Export for external translators

In Xcode: Product → Export Localizations… / Import Localizations… (XLIFF).

Out of scope

  • OsaurusCLI is English-only.
  • User-generated content (chat, model output) is not localized.

Maintainer scripts

ScriptPurpose
scripts/i18n/check.shValidate core + InfoPlist catalogs, lint risky Swift literals, and dry-run pruning
scripts/i18n/check-swift-catalog-keys.pyEnsure Swift localization references exist in the core catalog
scripts/i18n/lint-swift-literals.pyFlag Swift literals that bypass package-bundle localization
scripts/i18n/merge-locale.pyCopy one locale from another catalog (existing keys only)
scripts/i18n/fill-zh-hans.pyOptional machine-translation backfill (pip install deep-translator)
scripts/i18n/prune-catalog.pyRemove en-only / empty Xcode auto-extraction stubs and stale keys

Shared logic: scripts/i18n/xcstrings_util.py.

Xcode's indexer will occasionally inject empty stubs into the catalog for raw Text("…") literals (emoji, single-char UI elements, interpolations the indexer canonicalizes). This is harmless — empty stubs resolve to English fallback at runtime exactly as if they weren't in the catalog — and CI no longer fails on them. Run the pruner manually when you want to drop them:

python3 scripts/i18n/prune-catalog.py \
  Packages/OsaurusCore/Resources/Localizable.xcstrings \
  --remove-stale