| name | audit-swiftui-localization |
| description | Audit macOS SwiftUI localization for correctness, current APIs, and production conventions. Use for that domain or as part of a full audit. |
Bundled resource root
Let <swiftui-plugin-root> be the absolute plugin directory two levels above this SKILL.md. Resolve that path before running commands or opening shared references. When these instructions say swiftui-ctx, invoke <swiftui-plugin-root>/scripts/swiftui-ctx; do not assume the command is on PATH.
Audit SwiftUI Localization
AUDIT-ONLY · macOS-only · SwiftUI-only. Run this on a finished or in-progress macOS SwiftUI
project to detect — and flag — every way localization & internationalization goes wrong: hardcoded
strings that never reach a translator, Text(verbatim:) used both too much and too little, missing
String Catalog (.xcstrings), no translator comments, sentences built by interpolation (no
pluralization / grammar agreement), locale-unaware number/date formatting, and right-to-left-unsafe
layout. Findings are written to disk in the toolkit's unified schema. This is never a from-scratch
localization generator.
The core SwiftUI fact: Text("Save") with a string literal is a LocalizedStringKey and
auto-localizes — the literal becomes the catalog key. The defects are the ways code escapes that
auto-localization (a String variable, verbatim:, String(format:)) or feeds it un-translatable
input (a sentence assembled by interpolation).
✅ Correct (grounded, not a placeholder). The consensus shape from swiftui-ctx lookup Text --json
is the (_ key: LocalizedStringKey) overload at 99% (the bare-String/(verbatim) form is the 1%
outlier) — so a string literal, ideally with a translator comment:, is what shipping Mac apps
overwhelmingly write:
Text("label.text.unread", comment: "Unread")
Real permalink (verified, macOS 26 corpus): https://github.com/Ranchero-Software/NetNewsWire/blob/60295842054529c3450b91af15911cecb1a1cc4f/Widget/WidgetBundle.swift#L27
· Apple spec via Sosumi doc: https://sosumi.ai/documentation/swiftui/text. Refresh either with
swiftui-ctx lookup Text --json (VERIFY) and swiftui-ctx file <recommended.id> --smart (FIX).
Boundary / seam note (stay in lane)
AttributedString styling & markdown rendering belong to audit-swiftui-typography-text. When
Text(verbatim:) or markdown is about rendering (styling, + concatenation), typography-text is
primary; this skill owns only the catalog / translatability angle and emits a
cross_ref: typography-text on a shared site (per <swiftui-plugin-root>/references/_shared/cross-ref-graph.md).
- The blanket deprecated-API sweep belongs to
audit-swiftui-api-currency. A legacy
NSLocalizedString is flagged here for its translatability implication; currency owns generic
deprecation. Emit cross_ref: api-currency if both fire.
- Date/number
FormatStyle for async-loaded data seams to audit-swiftui-async-data; RTL
mirroring of layout containers seams to audit-swiftui-layout-and-tables; \.locale /
\.layoutDirection preview coverage seams to audit-swiftui-previews. Note in one line, route, do
not double-own.
Domain rules (the four that catch most bugs)
- A string literal in
Text/Label/.navigationTitle IS localized; a String variable is NOT.
The Text(_ key: LocalizedStringKey) overload localizes; the Text(_ content: some StringProtocol)
overload does not. Passing a variable silently bypasses the catalog (loc-02).
verbatim: is a deliberate opt-OUT of localization. Right for a brand name / version / number;
wrong for any human-readable UI copy (loc-01) — and its absence on a genuinely non-translatable
literal pollutes the catalog (loc-09).
- Never assemble a user-facing sentence by interpolation or
+. Plurals and grammar agreement
need the String Catalog's variations (%lld, automatic grammar agreement / inflect), not Swift
string-building (loc-06).
- Never format numbers/dates for display with
String(format:) or a hand-built formatter. Those
are locale-unaware; use a FormatStyle / .formatted() (loc-07).
Defect index (loc-01 … loc-10)
id · tell · severity · fix · open reference. Severities: hard-fail (build break / never-correct),
warning (compiles but non-native / wrong in another language), advisory (judgment / hygiene).
All fixes are flag — localization corrections are judgment-heavy (key naming, comment wording,
translatable-or-not), so this skill shows the ✅ and the dev applies it; none are mechanical auto-fixes.
| id | One-line tell | Sev | Fix | Reference |
|---|
| loc-01 | Text(verbatim:) wrapping human-readable UI copy → drop verbatim: so it auto-localizes | warning | flag | strings-and-catalog.md |
| loc-02 | a non-literal String passed to Text(_) / Label / .navigationTitle (StringProtocol overload — not localized) | warning | flag | strings-and-catalog.md |
| loc-03 | NSLocalizedString(...) legacy macro in SwiftUI code → String(localized:) / inline key | advisory | flag | strings-and-catalog.md |
| loc-04 | no .xcstrings String Catalog in the project (loose .strings or none) | advisory | flag | strings-and-catalog.md |
| loc-05 | a UI literal with no comment: for translators | advisory | flag | strings-and-catalog.md |
| loc-06 | a UI sentence built by interpolation / + → no pluralization / grammar agreement | warning | flag | plurals-grammar-and-format.md |
| loc-07 | String(format:) / DateFormatter / NumberFormatter for display → locale-unaware → FormatStyle | warning | flag | plurals-grammar-and-format.md |
| loc-08 | a directional SF Symbol Image(systemName: "…left/right…") doesn't mirror in RTL → .backward/.forward | warning | flag | rtl-and-layout-direction.md |
| loc-09 | a brand / version / non-translatable literal in Text("…") NOT wrapped verbatim: → pollutes the catalog | advisory | flag | strings-and-catalog.md |
| loc-10 | a hard-coded horizontal .offset(x:) / .position(x:) assumes LTR → mirror-unsafe | advisory | flag | rtl-and-layout-direction.md |
loc-04 is project-level (file-presence, not a .swift tell — checked in ORIENT). loc-05 and
loc-09 are READ-judgment (no clean regex distinguishes "human-readable copy" from "a brand name") —
they are detected in READ/DETECT, not by the lint runner. No rule is dropped; each is routed below.
The real API, at a glance
Real (use these): Text(_ key: LocalizedStringKey) (literal → auto-localizes),
Text(_:comment:), Text(verbatim:) (opt-out), LocalizedStringKey, LocalizedStringResource
(macOS 13+), String(localized:_:) / String.LocalizationValue (macOS 12+ — doc lives under
/documentation/swift/, not /foundation/), FormatStyle / .formatted(…),
@Environment(\.layoutDirection), \.locale, flipsForRightToLeftLayoutDirection(_:),
InflectionRule (macOS 12+, automatic grammar agreement). Catalog format: the String Catalog
(.xcstrings), not legacy .strings/.stringsdict.
Avoid for display: NSLocalizedString (legacy), String(format:), hand-built DateFormatter /
NumberFormatter without a \.locale, raw \(interpolation) for sentences. Floor values are the
reconciled truth in <swiftui-plugin-root>/references/_shared/floors-master.md — read, never restate.
There is no localization hallucination blacklist; if a symbol's existence is in doubt, VERIFY (step 5).
The 8-step audit workflow (execute verbatim)
- ORIENT.
tree / find the SwiftUI sources. Read the deployment target
(project.pbxproj MACOSX_DEPLOYMENT_TARGET, or Package.swift platforms:). Then check
loc-04: find . -name '*.xcstrings' — if none (and the app has user-facing copy), the project
ships no String Catalog. Note loose .strings/.stringsdict as the legacy form. Record both.
- LOCATE. Run the shared hybrid lint runner:
bash <swiftui-plugin-root>/scripts/swiftui-lint.sh --skill audit-swiftui-localization --dir <sources> --json /tmp/loc.json --sarif /tmp/loc.sarif.
It runs this skill's tier-1 grep tells (lint/grep-tells.tsv, loc-01/03/06/07/08/10) + the tier-2
structural ast-grep rule (lint/ast-grep/loc-02-nonliteral-text.yml — the String-variable-into-
Text case grep can't express), plus a per-file parse probe, and emits unified JSON + SARIF.
Read its parse_warnings — a flagged file did not fully parse, so READ those by hand. The runner
only LOCATES — never treat a hit as a finding. Engine + rule-file format + degradation:
<swiftui-plugin-root>/references/_shared/lint-architecture.md.
- READ. Open every located file in full — never pattern-match-and-patch blind. For each
Text/
Label/title decide: literal-or-variable, human-readable-or-not, has a comment:. This READ is where
loc-05 (missing comment) and loc-09 (a non-translatable literal that should be verbatim:) are
judged — the lint can't see meaning.
- DETECT. Apply the index. Assign each candidate a confidence; report a finding only at 100%
certainty. "Is this literal human-readable UI copy or a brand/version token?" is the load-bearing
call for loc-01/loc-09.
- VERIFY. For anything ≤ ~70% confidence (a symbol you're unsure exists, a floor you can't place,
a "what do shipping apps actually write?" question) run both evidence sources. (a) Practice —
bash <swiftui-plugin-root>/scripts/swiftui-ctx lookup <api> --json (and
swiftui-ctx deprecated <api> for a currency/deprecation rule): read its consensus (the canonical
shape), recommended permalink, introduced_macos, co_occurs_with; a lookup exit 3
(not-found, with a did-you-mean suggestion) corroborates a non-existent symbol. (b) Spec —
confirm via Sosumi: curl -sSL https://sosumi.ai/<apple-path> using references/source-directory.md
for the path and <swiftui-plugin-root>/references/_shared/sosumi-reference.md for the protocol
(never WebFetch developer.apple.com). Cross-check introduced_macos against floors-master.md.
The CLI contract is <swiftui-plugin-root>/references/_shared/swiftui-ctx-reference.md. Promote with
the citation or discard.
- REPORT. Write each confirmed finding (output contract below). One finding per file, zero-padded,
ordered. Write the run's
_index.md.
- FIX. Apply corrections under the fix-safety protocol
(
<swiftui-plugin-root>/references/_shared/fix-safety-protocol.md): clean-tree gate, findings-first.
Every loc rule is fix_mode: flag-only — leave findings open with the ✅ in ## Correct. The
✅ "Correct" is not a hand-written snippet — it is the swiftui-ctx consensus shape, backed by
a real macOS-26 example fetched with bash <swiftui-plugin-root>/scripts/swiftui-ctx file <recommended.id> --smart
whose GitHub permalink (plus the Sosumi doc:) goes in ## Source.
- DOUBLE-CHECK. Re-confirm every citation still resolves and still says the floor it claims. If a
reader applied a suggested ✅ and it would introduce a new tell, loop that file back to DETECT.
Confidence gating (load-bearing)
Report a finding only at 100% certainty. Anything ≤ ~70% goes to VERIFY (step 5) before it can
become a finding — never emit a speculative finding. loc-01 vs loc-09 turns entirely on the
human-readable-vs-token judgment; when unsure, carry as advisory and say so, don't assert.
Output contract
Inherits the toolkit's unified contract (full schema + body sections + frontmatter keys:
<swiftui-plugin-root>/references/_shared/finding-schema.md — do not restate it). Specialized for this
domain:
- Findings:
swiftui-audits/localization/<context>/NN-slug.md (one finding per file, zero-padded,
ordered). Per-run index: swiftui-audits/localization/_index.md.
domain: localization. Frontmatter is the canonical schema; fix_mode is flag-only for every loc
rule. availability reads from floors-master.md. source is an Apple URL + access date (fetched
via Sosumi) or verify against Xcode 26 SDK. Emit cross_ref per the seam note on shared sites.
Starter <context> folders (file here when…):
<context> | File a finding here when… |
|---|
hardcoded-strings/ | a String var escapes localization, or verbatim: is misused either way (loc-01, loc-02, loc-09) |
string-catalog/ | the project ships no .xcstrings, or a UI literal lacks a translator comment: (loc-04, loc-05) |
legacy-api/ | a legacy NSLocalizedString macro is used where a literal key / String(localized:) belongs (loc-03) |
plurals-and-grammar/ | a sentence is built by interpolation/+ so plurals & grammar agreement break (loc-06) |
locale-formatting/ | numbers or dates are formatted locale-unaware via String(format:) / a hand formatter (loc-07) |
rtl-layout/ | a directional SF Symbol or a hard-coded horizontal offset/position breaks in RTL (loc-08, loc-10) |
New-folder rule: if a finding does not fit any existing context folder, create a new one under
swiftui-audits/localization/ with a lowercase-hyphen slug naming the sub-category, and note it in the
run's _index.md. Prefer an existing folder when the fit is reasonable; consistency across runs is a
hard requirement. Two runs over the same code produce structurally identical trees.
Reference routing
| File | Open when |
|---|
references/strings-and-catalog.md | literal-vs-variable, verbatim: both ways, String Catalog (.xcstrings), translator comments, String(localized:) (loc-01/02/03/04/05/09) |
references/plurals-grammar-and-format.md | sentence assembly, pluralization, automatic grammar agreement / inflect, locale-aware number/date FormatStyle (loc-06/07) |
references/rtl-and-layout-direction.md | RTL: directional SF Symbols, layoutDirection, image mirroring, hard-coded horizontal offsets (loc-08/10) |
references/source-directory.md | step VERIFY — the Apple/WWDC/practitioner source map fetched via Sosumi |
lint/grep-tells.tsv + lint/ast-grep/*.yml | step LOCATE — this skill's declarative lint rule set fed to the shared runner (tier-1 grep + tier-2 structural ast-grep); edit here to tune detection |
Shared toolkit references (point in, never restate):
| Shared file | For |
|---|
<swiftui-plugin-root>/references/_shared/floors-master.md | every floor/availability value (the reconciled truth) |
<swiftui-plugin-root>/references/_shared/hallucination-blacklist.md | the canonical invented-name list |
<swiftui-plugin-root>/references/_shared/finding-schema.md | the unified finding schema + frontmatter keys |
<swiftui-plugin-root>/references/_shared/fix-safety-protocol.md | the 8-point fix-safety protocol (step 7) |
<swiftui-plugin-root>/references/_shared/sosumi-reference.md | the Apple-doc spec fetch protocol (step 5 VERIFY) |
<swiftui-plugin-root>/references/_shared/swiftui-ctx-reference.md | the practice-corpus CLI contract — lookup/deprecated/file --smart for the consensus shape + permalinked example (steps 5 VERIFY · 7 FIX) |
<swiftui-plugin-root>/references/_shared/cross-ref-graph.md | seam ownership + cross_ref targets (typography-text, api-currency, async-data, previews, layout-and-tables) |
Detection accelerator
bash <swiftui-plugin-root>/scripts/swiftui-lint.sh --skill audit-swiftui-localization --dir <files-or-dir> [--json out.json] [--sarif out.sarif] — the toolkit's one shared hybrid lint engine, fed this
skill's declarative rules: tier-1 grep tells (lint/grep-tells.tsv, loc-01/03/06/07/08/10) +
tier-2 ast-grep (lint/ast-grep/loc-02-nonliteral-text.yml — a String variable passed to
Text(_), which grep cannot distinguish from a literal). It runs a per-file parse probe (surfaces
"did not fully parse" so a structural miss can't look clean), emits unified JSON + SARIF, and
degrades to grep-only with a notice if ast-grep is unreachable (npx --package @ast-grep/cli ast-grep; faster: brew install ast-grep). loc-04 (no .xcstrings) is a project-file check done in
ORIENT, not by the runner; loc-05/loc-09 are READ-judgment. It only LOCATES — always READ each hit in
full before reporting (step 3). The thin scripts/loc-lint.sh is a pointer to this runner. Engine +
rule-file format + JSON/SARIF shape + safety rails:
<swiftui-plugin-root>/references/_shared/lint-architecture.md.