Commit graph

3 commits

Author SHA1 Message Date
ozymandiashh
a208c25f86 fix(menubar): pin multi-argument catalog keys to positional specifiers
Some checks are pending
CI / semgrep (push) Waiting to run
macOS Menubar CI / test (push) Waiting to run
Tests / test (22) (push) Waiting to run
Tests / test (22.13.0) (push) Waiting to run
Tests / test (24) (push) Waiting to run
Tests / test (26) (push) Waiting to run
Tests / test-platforms (macos-latest) (push) Waiting to run
Tests / test-platforms (windows-latest) (push) Waiting to run
Tests / desktop (push) Waiting to run
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
2026-09-15 01:33:28 +03:00
iamtoruk
d9b6d1ed77 test(menubar): let the scanner see tooltips and notification titles
.toolTip = and post(title:) are both user-facing and both were invisible to
the scan. Nothing on main is newly flagged.
2026-09-12 16:12:11 -07:00
ozymandiashh
480ab73b95 test(menubar): fail the build when a string never reaches the catalog
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
2026-09-12 15:22:47 -07:00