Shark
June 24, 2026 · View on GitHub
Shark is a Swift command line tool for type-safe resources and a complete localization workflow:
shark generate(the default) — type-safe enums for your images, colors, storyboards, fonts, data assets, and localizations, forUIKit,AppKit, andSwiftUI.shark lint— a CI gate that catches missing translations, dead keys, and format-specifier mismatches across all locales.shark translate— fills localization gaps with a local Claude Code or Codex install, or the Claude API for CI, machine-validated and routed through human review.
Because Shark reads your .xcodeproj to find these assets, the setup is extremely simple: one binary, one build phase line, no config file.
Why Shark?
Xcode has been nibbling at this space for years — asset symbols arrived with Xcode 15, type-safe string symbols with String Catalogs after that. SwiftGen and R.swift have generated resource accessors for even longer. So why Shark, in 2026?
Versus Xcode's generated symbols:
- Generated symbols cover images, colors, and strings — the latter only if you've migrated to String Catalogs. Shark additionally covers fonts, storyboards, and data assets, speaks both
.xcstringsand classic.strings(which is what long-lived production apps actually contain), and generates the same API across UIKit, AppKit, and SwiftUI. - Shark namespaces by your asset-catalog folder structure (Provides Namespace) and by localization key separators:
L.login.error.message("…")instead of a flat symbol soup. With--target,--exclude, and--nameit handles white-label/multi-target setups that symbol generation has no answer for. - Most importantly: codegen was never the whole problem — keeping localizations complete is. Xcode will not tell your CI that 21 keys are missing in Japanese.
shark lintwill, andshark translatewill fix it, with format-specifier preservation checked by machine and the result parked asneeds_reviewfor a human.
Versus SwiftGen / R.swift:
- Shark reads the
.xcodeprojdirectly and is target-aware — noswiftgen.yml, no templates, no resource-bundle plumbing. The generated file is plain Swift with zero runtime dependency. - Both are codegen-only tools. Neither lints localization completeness, neither translates, and neither validates that
%1$@survived a translator.
Versus project generators (XcodeGen, Tuist): different problem — they generate projects, not resource accessors or localization workflows. Shark happily works on projects they generate.
The three subcommands share one project model and one format-specifier parser: what generate turns into a function signature is exactly what lint checks and what translate refuses to let a model break.
A bit of history: WWDC 2023 sherlocked our colors and images, WWDC 2025 our localization accessors. We took it as a compliment — and built the workflow parts Apple didn't.
The generated code
Here's what a generated Shark.swift file for UIKit looks like and how it is used in a codebase:
// Shark.swift
// Generated by Shark https://github.com/kaandedeoglu/Shark
import UIKit
// swiftlint:disable all
public enum Shark {
private static let bundle: Bundle = {
class Custom {}
return Bundle(for: Custom.self)
}()
public enum I {
public enum Button {
public static var profile: UIImage { return UIImage(named:"profile", in: bundle, compatibleWith: nil)! }
public static var cancel: UIImage { return UIImage(named:"cancel", in: bundle, compatibleWith: nil)! }
public static var user_avatar: UIImage { return UIImage(named:"user_avatar", in: bundle, compatibleWith: nil)! }
}
}
public enum C {
public static var blue1: UIColor { return UIColor(named: "blue1", in: bundle, compatibleWith: nil)! }
public static var blue2: UIColor { return UIColor(named: "blue2", in: bundle, compatibleWith: nil)! }
public static var gray1: UIColor { return UIColor(named: "gray1", in: bundle, compatibleWith: nil)! }
public static var gray2: UIColor { return UIColor(named: "gray2", in: bundle, compatibleWith: nil)! }
public static var green1: UIColor { return UIColor(named: "green1", in: bundle, compatibleWith: nil)! }
public static var green2: UIColor { return UIColor(named: "green2", in: bundle, compatibleWith: nil)! }
}
public enum F {
public static func gothamBold(ofSize size: CGFloat) -> UIFont { return UIFont(name: "Gotham-Bold", size: size)! }
public static func gothamMedium(ofSize size: CGFloat) -> UIFont { return UIFont(name: "Gotham-Medium", size: size)! }
public static func gothamRegular(ofSize size: CGFloat) -> UIFont { return UIFont(name: "Gotham-Regular", size: size)! }
}
public enum L {
public enum button {
/// Login
public static var login: String { return NSLocalizedString("button.login", bundle: bundle, comment: "") }
/// Logout
public static var logout: String { return NSLocalizedString("button.logout", bundle: bundle, comment: "") }
}
public enum login {
/// Please log in to continue
public static var title: String { return NSLocalizedString("login.title", bundle: bundle, comment: "") }
/// Skip login and continue
public static var skip: String { return NSLocalizedString("login.skip", bundle: bundle, comment: "") }
public enum error {
/// Login failed
public static var title: String { return NSLocalizedString("login.error.title", bundle: bundle, comment: "") }
/// Operation failed with error: %@
public static func message(_ value1: String) -> String {
return String(format: NSLocalizedString("login.error.message", bundle: bundle, comment: ""), value1)
}
}
}
}
}
// At the call site
imageView.image = Shark.I.Button.profile
label.font = Shark.F.gothamBold(ofSize: 16.0)
label.text = Shark.L.login.title
view.backgroundColor = Shark.C.green1
// You can also make it prettier with typealiases
typealias I = Shark.I
typealias C = Shark.C
typealias F = Shark.F
typealias L = Shark.L
imageView.image = I.Button.profile
label.font = F.gothamBold(ofSize: 16.0)
label.text = L.login.error.message("I disobeyed my masters")
view.backgroundColor = C.green1
There are a few things to notice:
First, have a look at this Xcode screenshot from the inspector pane of an asset catalogue's folder entry:

If you place your image and color assets in folders, Shark will create namespaced enums – provided you have configured the respective Xcode setting Provides Namespace. If you have deeply nested folders, Shark will respect every one's individual namespace setting.
Localizations are always namespaced with separators. Currently Shark uses the dot symbol . as the separator.
As you can see localization keys are recursively namespaced until we get to the last component.
Installation
Homebrew
brew install kaandedeoglu/formulae/shark
Mint
mint install kaandedeoglu/formulae/shark
Manually
Clone the project, then do:
> swift build -c release
> cp ./build/release/Shark /usr/local/bin
You can then verify the installation by doing
> shark --help
Compatibility Notes
Shark 1.8.6 handles Xcode projects whose local Swift package dependencies emit manifest warnings during swift package dump-package. Older Shark builds may fail these projects with a generic "data couldn't be read because it isn't in the correct format" parse error.
If you use Xcode's discovered dependency file setting in a Run Script phase, keep the dependencyFile project value quoted when it contains path separators, for example:
dependencyFile = "/tmp/deps-MyApp.d";
Setup (Easy)
-
Add a new Run Script phase to your target's build phases. This build phase should ideally run before the
Compile Sourcesphase. The script body should look like the following:unset SDKROOT if [ -x "$(command -v shark)" ]; then shark $PROJECT_FILE_PATH $PROJECT_DIR/$PROJECT_NAME/ fithe
if/fiblock makes sure that Shark runs only if it's installed on the current machine. -
Build your project. You should now see a file named
Shark.swiftin your project folder. -
Add this file to your target. Voila!
Shark.swiftwill be updated every time you build the project. -
Alternatively you can do the following:
# Write to a specific file called MyAssets.swift shark $PROJECT_FILE_PATH $PROJECT_DIR/$PROJECT_NAME/MyAssets.swift# Write to a specific file in a different folder shark $PROJECT_FILE_PATH $PROJECT_DIR/$PROJECT_NAME/Utility/MyAssets.swiftWith this setup, Shark will generate the resource files on every code run. This can add several seconds to the build time which you may not like.
Setup (Advanced)
If you want to run Shark only when it really needs to, then make the following adjustments:
- Edit the command line to have Shark create a dependency file. This file will contain the paths of all the assets in your project.
shark $PROJECT_FILE_PATH $PROJECT_DIR/$PROJECT_NAME --deps /tmp/deps-$PROJECT_NAME.d
-
Add the output file (e.g.
${SRCROOT}/Shark.swift) as a dependency to the Run Script phase. -
Check the [x] Based on dependency analyses in the build phase.
-
Check the [x] Use discovered dependency file: and enter the path to the dependency file.
At the end of all that, your run Script phase should look similar to this:
Options & Flags
Shark also accepts the following command line options to configure behavior
--name
By default, the top level enum everything else lives under is called - you guessed it - Shark. You can change this by using the --name flag.
shark $PROJECT_FILE_PATH $PROJECT_DIR/$PROJECT_NAME --name Assets
--locale
By default, Shark will try to find English localizations to generate the localizations enum. If there are no English .strings file in your project, or you'd like Shark to take another localization as base, you can specify the language code with the --locale flag.
# Use Spanish localizations for generation
shark $PROJECT_FILE_PATH $PROJECT_DIR/$PROJECT_NAME --locale es
--target
In case your Xcode project has multiple application targets, you should specify which one Shark should look at by using the --target flag.
shark $PROJECT_FILE_PATH $PROJECT_DIR/$PROJECT_NAME --target MyAppTarget
--visibility
By default, Shark will create all properties with the visibilty of public. Submit this option to change this to, e.g., internal.
--framework
By default, Shark creates code for UIKit. Specify --framework appkit to create code for AppKit, and --framework swiftui for SwiftUI.
--separator
Shark will split localization keys using the separator character value, and create nested enums until we hit the last element. For example, the lines login.button.positive = "Log in!"; and login.button.negative = "Go back..."; will create the following structure inside the top level localizations enum L:
public enum login {
public enum button {
public static var positive: String { return NSLocalizedString("login.button.positive") }
public static var negative: String { return NSLocalizedString("login.button.negative") }
}
}
By default, the separator is ., only single character inputs are accepted for this option.
--top-level-scope
Declares the I, C, F, L enums in the top level scope instead of nesting it in a top level Shark enum.
--deps
Add the path to a Makefile-style dependency file. This gives Xcode a hint when to skip calling build phase because nothing has changed.
When Xcode persists this path into project.pbxproj, paths such as /tmp/deps-MyApp.d should remain quoted for parser compatibility.
--exclude
By default, Shark will process all resource files it knows about. If you don't want that, you can specify exceptions. Note that Shark uses infix matching to identify files to exclude.
--help
Prints the overview, example usage and available flags to the console.
Localization workflow: lint & translate
Beyond code generation (shark generate, the default subcommand), Shark 2.0 helps with the localization workflow itself.
shark lint
Checks all localization tables of a target across all locales:
shark lint MyApp.xcodeproj
| Rule | Severity |
|---|---|
missing-key — key exists in the source locale but is missing or empty in another | fails the run |
placeholder-mismatch — format specifiers differ between source and translation (catches String(format:) crashes; positional reordering like %2$@ … %1$@ is not a mismatch, and prose percent signs like "25% and" are filtered) | fails the run |
empty-source-value — key has an empty value in the source locale itself (a dead key, reported once at the root instead of per locale) | fails the run |
orphaned-key — key exists only outside the source locale | fails only with --strict |
Exit code 1 on findings makes it a CI gate. --format github emits workflow annotations, --format json is for tooling. Plural keys are reported but not checked yet.
shark translate
Finds keys that are missing in the target locale(s) and translates them with a local agent by default:
shark translate MyApp.xcodeproj --to de,fr --glossary Glossary.md --context AppContext.md
# CI / direct API
ANTHROPIC_API_KEY=sk-ant-... shark translate MyApp.xcodeproj --to de,fr --backend api --yes
# Codex CLI
shark translate MyApp.xcodeproj --to de,fr --backend codex --yes
How it stays safe:
- Only missing or empty entries are candidates — existing translations (including ones in review) are never overwritten.
- Every returned value is machine-validated: format specifiers must survive translation exactly (normalized by position). Rejected values get one retry with the rejection reason; persistent failures are reported, never written.
- Results land as
needs_reviewin.xcstrings, so Xcode's String Catalog editor surfaces them for human review. For.stringsfiles, new keys are appended under a review comment. - A confirmation prompt shows a token/cost estimate before anything is sent (
--yesskips it,--dry-runonly lists).
Backends (--backend claude-code|api|codex|auto):
- claude-code (default) — pipes through a locally installed Claude Code binary with structured-output validation and bills against your existing Claude subscription. No API key needed.
- api — direct Messages API via
ANTHROPIC_API_KEY: structured output, prompt caching across batches, parallel requests. The right choice for CI. - codex — pipes through a locally installed Codex CLI binary (
codex exec) and writes the final response through Codex's structured-output schema support. - auto —
apiif a key is set, otherwiseclaude-code, otherwisecodex.
--glossary takes a Markdown file with project terminology, --context a short app description — both are passed to the model (and cached across batches on the api backend). --model selects the model; Claude backends default to claude-opus-4-8, while codex uses your Codex CLI default unless set.
Smoke tests
Shark keeps committed synthetic .xcodeproj fixtures under Examples/ and runs them in CI:
Scripts/smoke-fixtures.sh
The fixture smoke covers:
Format90Example— objectVersion 90 and the Xcode 16.3 shell-script array form, plus a clean generate/lint path.XcodeGenSmoke— a temporary project generated by XcodeGen during the smoke run. This currently covers objectVersion 77 on XcodeGen 2.45.x and guards against parser regressions in generator-produced projects.LocalizationWorkflowExample— expected localization findings across.stringsand.xcstrings, skipped plural catalog entries, andtranslate --dry-rungap discovery without calling a model backend.
Real-world projects are useful before release tags, but they are intentionally opt-in because they may be private, target-dependent, or machine-specific:
SHARK_REAL_WORLD_ROOT="$HOME/Documents/late" Scripts/smoke-real-world.sh
When a real-world smoke exposes a bug, reduce it to the smallest reproducible synthetic fixture and commit that fixture with the regression test.
License
The MIT License (MIT)
Copyright (c) Kaan Dedeoglu, Dr. Michael 'Mickey' Lauer, and contributors.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.