The catalog comparison matched specifier lists, so a key whose
placeholders are all %@ passed even when the Chinese value bound the
arguments into the wrong slots — the drift that shipped in three
early-reset keys and was only caught by rendering real data bilingually
(#1331).
All 42 multi-argument keys in both tables now use positional specifiers
(%1$@, %2$lld, ...); the parity check compares slot to verb so Chinese
word order stays free while each slot keeps its type; a new test fails
on any multi-argument key that is not positional.
The source scanner closes its three expression gaps: ternary branches,
concatenated literals, and let-bound literals passed by name.
Text(verbatim:) stays the explicit opt-out; the four provider-name
fallbacks the new rules surfaced in HeatmapSection route through the
catalog as verbatim entries.
Closes#1331
LocalizationCatalogTests diffs en against zh-Hans. That catches a key
translated in one locale and not the other, but it is blind to the failure
that actually happens: a feature ships a bare `Text("Second row")`, the
literal never becomes a key, both tables stay in perfect agreement, and a
zh-Hans build renders English. #1252 and #1243 landed exactly that way, which
is why the previous commit exists at all.
So this suite reads mac/Sources instead of the tables, in three passes that
cover each other:
1. Call sites. The SwiftUI and AppKit surfaces that put a string on screen —
Text/Button/Toggle/Picker/…, the accessibility modifiers, NSMenuItem,
NSAlert and window titles — must be handed `L(…)`, not a literal.
2. Display-label properties. `displayName`/`displayLabel`/`settingsLabel` are
how this codebase names an enum for a picker, and they are not call sites,
so a new metric case with a bare literal would slip past pass 1.
3. Catalog round-trip. Every key a view asks for has an entry in both
locales, and every entry has a call site — so a key added to the code but
not the table, or left behind after a rewording, fails too.
Scoped to avoid false positives rather than by suppressing findings:
- A literal is exempt when, with `\(…)` segments removed, nothing is left but
figures, symbols, or words in a three-entry vocabulary (`CodeBurn`, `tok`,
`USD`). That covers `Text("$25")`, `Text("\(count)")` and `Text("— ")`
without naming a single file, so the exemption cannot go stale.
- Pass 2 only looks at bracket-depth zero, which is what separates the
property's result (`case .pro: "Pro"`) from machinery it calls
(`Locale(identifier: "en_US")`).
- The two denylists are documented with reasons and keyed to the declaring
type, so `PlanType.displayName` being exempt does not exempt every
`displayName` in the app.
Comments are stripped first, or Localization.swift's own documentation of
`Text("literal")` would read as a violation; strings win over comment markers
so a URL in a literal does not swallow the rest of the file.
The scanner lives in its own file with no `import Testing` so the standalone
swiftc harness can exercise the shipped code rather than a copy of it —
`swift test` cannot run on this host, and a reimplementation would drift.
Mutation-checked: un-routing `Toggle(L("Second row"))` fails pass 1,
un-routing `MenubarSecondRowMetric.settingsLabel` fails pass 2, and dropping
`L("Show %@ usage")` fails pass 3. Writing it also turned up two real bugs in
the scan (`Label(` matching inside `.accessibilityLabel(`, and a nested
`enum PlanType` reporting its outer type), both now covered by a test.
Closes#1289