feat(i18n): close silently un-localizable sites from the audit

Burns down the follow-ups tracked in scarf/docs/I18N.md so that future
translation passes (Phase 2+) don't see English leak through ternary UI
copy, enum rawValue displays, or fixed-format strings.

- Ternary status copy: Text(cond ? "A" : "B") → cond ? Text("A") : Text("B")
  (each branch routes through LocalizedStringKey). Covers Health, Chat
  (voice/TTS/recording/ACP status), Profiles, MCPServer test result,
  SignalSetup, QuickCommands header.
- Enum .rawValue displays: LogFile, LogComponent, DashboardTab, Skills.Tab,
  InsightsPeriod, ToolKind, AuthType each expose a
  displayName: LocalizedStringResource. LogEntry.LogLevel stays verbatim
  (technical jargon — DEBUG/INFO/ERROR/… are industry-standard).
- displayName passthroughs: HermesToolPlatform, ServerRegistry.Entry,
  MCPServerPreset wrapped with Text(verbatim:) at call sites (brand names
  and user data, not UI chrome). MCPTransport.displayName promoted to
  LocalizedStringResource.
- Composite format strings: ModelPickerSheet "ctx" suffix, InsightsView
  "tokens" suffix and MCPServerTestResultView "%.1fs · %d tools" rewritten
  as Text("\(arg) suffix") LocalizedStringKey. Percent display uses
  .formatted(.percent) after /100.
- Day-of-week chart now sources from Calendar.current.shortWeekdaySymbols,
  re-indexed for the existing Mon=0 data model.
- ConnectionStatusPill's label + tooltip return Text (not String) so the
  .help(Text) / direct-render paths localize correctly.
- Catalog re-synced: 545 → 575 keys (+30 from new ternary branches and
  enum displayName values).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alan Wizemann
2026-04-20 17:40:56 -07:00
parent 6817c95681
commit b40182f2da
28 changed files with 227 additions and 101 deletions
+9 -47
View File
@@ -47,56 +47,18 @@ For the three supported non-English locales we use Xcode's built-in AI translati
Strings that are **user data** (session titles, memory file contents, log lines, shell commands shown in UI, file paths) should pass through without localization — this happens naturally when the value is a `String` variable, since those overloads skip the catalog.
## Outstanding audit follow-ups
## Audit status
The [initial i18n PR](#) enabled the catalog infrastructure and migrated clear locale-bug formatter sites. The following patterns remain in the codebase and should be refactored **before** translations ship in Phase 2 — otherwise their English copy will leak through regardless of locale:
Phase 1b (the `multi-language` PR) closed every tracked site from the original audit:
### Category A — UI copy held in a `String` variable (needs `LocalizedStringResource`)
- **Category A high-priority (ternary UI copy)** — converted to `Text`-ternary form so each branch routes through `LocalizedStringKey`.
- **Category A medium-priority (enum `.rawValue` displays)** — each enum now exposes `displayName: LocalizedStringResource` and call sites use it. `LogEntry.LogLevel` (technical jargon) stays verbatim.
- **Category A lower-priority (displayName passthroughs)** — wrapped with `Text(verbatim:)` for proper nouns / user data (`HermesToolPlatform`, `ServerRegistry.Entry`, `MCPServerPreset`). `MCPTransport.displayName` promoted to `LocalizedStringResource`.
- **Category B (composite format strings)** — migrated to `Text("\(arg) suffix")` with `LocalizedStringKey` or to `.percent` / `.currency` FormatStyle.
- **Category C (hard-coded day names)** — replaced with `Calendar.current.shortWeekdaySymbols`, re-indexed to match the existing Mon=0 data model.
- **Category D (`.help(stringVar)` sites)** — `ConnectionStatusPill` now returns `Text` from its `labelText` / `tooltipText` properties.
High-priority (ternary UI copy, visible status chrome):
- `Features/Health/Views/HealthView.swift:135``"Hermes Running"` / `"Hermes Stopped"` ternary
- `Features/Chat/Views/ChatView.swift:125``"Active"` fallback
- `Features/Chat/Views/ChatView.swift:241``"Voice On" / "Voice Off"`
- `Features/Chat/Views/ChatView.swift:256``"TTS On" / "TTS Off"`
- `Features/Chat/Views/ChatView.swift:271``"Recording..." / "Push to Talk"`
- `Features/MCPServers/Views/MCPServerTestResultView.swift:13``"Test passed" / "Test failed"`
- `Features/Profiles/Views/ProfilesView.swift:145``"Active profile" / "Inactive"`
- `Features/Platforms/Views/PlatformSetup/SignalSetupView.swift:54``"signal-cli is available on PATH"` / not-found message
- `Features/QuickCommands/Views/QuickCommandsView.swift:148``"Add Quick Command"` / `"Edit /\(name)"` ternary
- `Features/MCPServers/Views/MCPServersView.swift:131``.help("\(count) tools")` vs `"Test failed"` ternary
- `Features/MCPServers/Views/MCPServerDetailView.swift:157, 185` — masked-value placeholder
Medium-priority (enum `.rawValue` → display):
- `Features/Logs/Views/LogsView.swift:30,38,48,69` — file / component / level display
- `Features/Projects/Views/ProjectsView.swift:153` — tab display
- `Features/Skills/Views/SkillsView.swift:37` — tab display
- `Features/Insights/Views/InsightsView.swift:40, 201` — period picker, day-of-week names (see also Category C)
- `Features/CredentialPools/Views/CredentialPoolsView.swift:265` — type display
- `Features/Activity/Views/ActivityView.swift:117` — kind display
Lower-priority (displayName passthroughs from services that could be wrapped once at the source):
- `Features/Platforms/Views/PlatformsView.swift:43, 91`, `Features/Tools/Views/ToolsView.swift:46``platform.displayName`
- `Features/Gateway/Views/GatewayView.swift:105``platform.name.capitalized`
- `Features/MCPServers/Views/*``transport.displayName`, preset names / descriptions
- `Features/Servers/Views/ServerSwitcherToolbar.swift:40`, `ManageServersView.swift:96``server.displayName`
### Category B — Composite format strings with translatable suffixes
- `Features/Settings/Views/Components/ModelPickerSheet.swift:105``ctx + " ctx"` — literal " ctx" needs keying
- `Features/Insights/Views/InsightsView.swift:93``formatTokens(...) + " tokens"` — literal " tokens" needs keying
- `Features/MCPServers/Views/MCPServerTestResultView.swift:15``"%.1fs · %d tools"` — needs splitting into a `String(localized: "\(elapsed)s · \(count) tools")`
- `Features/Insights/Views/InsightsView.swift:167``"%.1f%%"` — needs `.formatted(.percent)` after verifying the input range (0…1 vs 0…100)
### Category C — Hard-coded day / month name arrays
- `Features/Insights/Views/InsightsView.swift:196``["Mon", "Tue", …]` literal — use `Calendar.current.shortWeekdaySymbols` (locale-aware).
### Category D — `.help(stringVar)` sites
- `Features/Servers/Views/ConnectionStatusPill.swift:42``.help(tooltip)`; refactor `tooltip` to return `LocalizedStringKey` / `LocalizedStringResource`.
If you spot a new silently-un-localizable site during translation review, prefer the patterns in the table above over one-off workarounds.
### Non-blocking (intentional verbatim)