mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-10 10:36:35 +00:00
feat(i18n): enable String Catalog + locale-aware numeric formatters
Lays the groundwork for zh-Hans / de / fr translations on an English base. No user-visible English-locale behavior changes. See scarf/docs/I18N.md for the full plan and remaining audit follow-ups. - Localizable.xcstrings seeded with 538 keys auto-extracted via `xcstringstool sync` from the Swift sources - InfoPlist.xcstrings carrying NSMicrophoneUsageDescription - knownRegions += zh-Hans, de, fr - Currency / byte-count / compact-number String(format:) sites migrated to Locale.current-aware .formatted() style (currency, byteCount(.file), compactName notation) — previously rendered POSIX separators + English unit names regardless of user locale Refs #13. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
# Internationalization (i18n)
|
||||
|
||||
Scarf uses Apple's modern **String Catalog** workflow. Source strings are auto-extracted from `Text("…")` and `String(localized: …)` literals into [`scarf/scarf/Localizable.xcstrings`](../scarf/Localizable.xcstrings) at build time (when built in Xcode.app; `xcodebuild` alone emits per-source `.stringsdata` but does not merge back into the catalog). Info.plist keys are localized via [`scarf/scarf/InfoPlist.xcstrings`](../scarf/InfoPlist.xcstrings).
|
||||
|
||||
## Languages
|
||||
|
||||
| Locale | Status |
|
||||
|---|---|
|
||||
| `en` (English) | Base / source |
|
||||
| `zh-Hans` (Simplified Chinese) | Region enabled, translations pending |
|
||||
| `de` (German) | Region enabled, translations pending |
|
||||
| `fr` (French) | Region enabled, translations pending |
|
||||
|
||||
Canadian French users are served by base `fr`. `fr-CA` will be added only if a concrete Québec-specific bug is reported.
|
||||
|
||||
## Adding a new language
|
||||
|
||||
1. Xcode → Project → Info → Localizations → `+` (add locale).
|
||||
2. Ensure the locale code is also listed in `knownRegions` of `scarf.xcodeproj/project.pbxproj`.
|
||||
3. Open `Localizable.xcstrings` in Xcode; the new locale appears as an empty column — translate or use Xcode's AI suggestions.
|
||||
4. Repeat for `InfoPlist.xcstrings` (microphone usage, etc.).
|
||||
5. Smoke-test via scheme language override (Edit Scheme → Run → App Language).
|
||||
|
||||
## Adding translations (AI-first workflow)
|
||||
|
||||
For the three supported non-English locales we use Xcode's built-in AI translation:
|
||||
|
||||
1. Open `Localizable.xcstrings` in Xcode.
|
||||
2. Select untranslated rows for a locale → right-click → **Translate** (Xcode 26+ provides GPT-backed suggestions with context from the surrounding code comment).
|
||||
3. Review each suggestion before marking **Translated**.
|
||||
4. For terms that should NOT translate (proper nouns like *Scarf*, *Hermes*, *Anthropic*; env var names; file paths), wrap the source site in `Text(verbatim: "…")` so the key never hits the catalog.
|
||||
|
||||
## Guardrails when writing new UI code
|
||||
|
||||
`Text("literal")` auto-localizes. These patterns **silently leak English** and need explicit handling:
|
||||
|
||||
| Pattern | Fix |
|
||||
|---|---|
|
||||
| `Text(someStringVar)` | `Text(LocalizedStringResource("key"))` or pass a `LocalizedStringKey` down the view tree |
|
||||
| `"Hello " + name` | `String(localized: "Hello \(name)")` |
|
||||
| `String(format: "$%.2f", cost)` | `cost.formatted(.currency(code: "USD").precision(.fractionLength(2)))` |
|
||||
| `String(format: "%.1f MB", size)` | `Int64(size).formatted(.byteCount(style: .file))` |
|
||||
| `String(format: "%.1fM", n)` | `n.formatted(.number.notation(.compactName))` |
|
||||
| Custom `DateFormatter` with fixed `dateFormat` | `date.formatted(.dateTime.month().day().year())` |
|
||||
| `.help(stringVar)` | Compute a `LocalizedStringKey` or use `.help(Text(…))` |
|
||||
| `Button(stringVar)` | `Button(LocalizedStringResource("key")) { … }` |
|
||||
|
||||
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
|
||||
|
||||
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:
|
||||
|
||||
### Category A — UI copy held in a `String` variable (needs `LocalizedStringResource`)
|
||||
|
||||
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`.
|
||||
|
||||
### Non-blocking (intentional verbatim)
|
||||
|
||||
The following are correct as-is because they pass user data or machine-readable content through to the UI:
|
||||
|
||||
- Session titles, message content, memory / skill / YAML file contents, log lines, shell commands, file paths, session IDs, model IDs, credential sources, URL strings.
|
||||
|
||||
If we later need to badge these (e.g. "(empty)" placeholder), the badge itself becomes a localizable key while the data passthrough stays verbatim.
|
||||
Reference in New Issue
Block a user