diff --git a/scarf/docs/I18N.md b/scarf/docs/I18N.md index efc8cf5..fab79a4 100644 --- a/scarf/docs/I18N.md +++ b/scarf/docs/I18N.md @@ -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) diff --git a/scarf/scarf/Core/Models/HermesMCPServer.swift b/scarf/scarf/Core/Models/HermesMCPServer.swift index 4f43ac9..d35cb14 100644 --- a/scarf/scarf/Core/Models/HermesMCPServer.swift +++ b/scarf/scarf/Core/Models/HermesMCPServer.swift @@ -6,7 +6,7 @@ enum MCPTransport: String, Sendable, Equatable, CaseIterable, Identifiable { var id: String { rawValue } - var displayName: String { + var displayName: LocalizedStringResource { switch self { case .stdio: return "Local (stdio)" case .http: return "Remote (HTTP)" diff --git a/scarf/scarf/Core/Models/HermesMessage.swift b/scarf/scarf/Core/Models/HermesMessage.swift index 2b696f4..1cd0394 100644 --- a/scarf/scarf/Core/Models/HermesMessage.swift +++ b/scarf/scarf/Core/Models/HermesMessage.swift @@ -99,6 +99,17 @@ enum ToolKind: String, Sendable, CaseIterable { case browser case other + var displayName: LocalizedStringResource { + switch self { + case .read: return "Read" + case .edit: return "Edit" + case .execute: return "Execute" + case .fetch: return "Fetch" + case .browser: return "Browser" + case .other: return "Other" + } + } + var icon: String { switch self { case .read: return "doc.text.magnifyingglass" diff --git a/scarf/scarf/Features/Activity/Views/ActivityView.swift b/scarf/scarf/Features/Activity/Views/ActivityView.swift index 811f563..9ebf53f 100644 --- a/scarf/scarf/Features/Activity/Views/ActivityView.swift +++ b/scarf/scarf/Features/Activity/Views/ActivityView.swift @@ -114,7 +114,7 @@ struct ActivityView: View { VStack(alignment: .leading, spacing: 2) { Text(entry.toolName) .font(.title3.bold().monospaced()) - Text(entry.kind.rawValue.capitalized) + Text(entry.kind.displayName) .font(.caption) .foregroundStyle(.secondary) } diff --git a/scarf/scarf/Features/Chat/Views/ChatView.swift b/scarf/scarf/Features/Chat/Views/ChatView.swift index 289c7c0..e9e7318 100644 --- a/scarf/scarf/Features/Chat/Views/ChatView.swift +++ b/scarf/scarf/Features/Chat/Views/ChatView.swift @@ -122,7 +122,7 @@ struct ChatView: View { Circle() .fill(.green) .frame(width: 6, height: 6) - Text(viewModel.acpStatus.isEmpty ? "Active" : viewModel.acpStatus) + (viewModel.acpStatus.isEmpty ? Text("Active") : Text(viewModel.acpStatus)) .font(.caption) .foregroundStyle(.secondary) .lineLimit(1) @@ -238,7 +238,7 @@ struct ChatView: View { HStack(spacing: 4) { Image(systemName: viewModel.voiceEnabled ? "mic.fill" : "mic.slash") .foregroundStyle(viewModel.voiceEnabled ? .green : .secondary) - Text(viewModel.voiceEnabled ? "Voice On" : "Voice Off") + (viewModel.voiceEnabled ? Text("Voice On") : Text("Voice Off")) .font(.caption) .foregroundStyle(viewModel.voiceEnabled ? .primary : .secondary) } @@ -253,7 +253,7 @@ struct ChatView: View { HStack(spacing: 4) { Image(systemName: viewModel.ttsEnabled ? "speaker.wave.2.fill" : "speaker.slash") .foregroundStyle(viewModel.ttsEnabled ? .green : .secondary) - Text(viewModel.ttsEnabled ? "TTS On" : "TTS Off") + (viewModel.ttsEnabled ? Text("TTS On") : Text("TTS Off")) .font(.caption) .foregroundStyle(viewModel.ttsEnabled ? .primary : .secondary) } @@ -268,7 +268,7 @@ struct ChatView: View { Image(systemName: viewModel.isRecording ? "waveform.circle.fill" : "waveform.circle") .foregroundStyle(viewModel.isRecording ? .red : Color.accentColor) .symbolEffect(.pulse, isActive: viewModel.isRecording) - Text(viewModel.isRecording ? "Recording..." : "Push to Talk") + (viewModel.isRecording ? Text("Recording…") : Text("Push to Talk")) .font(.caption) } } diff --git a/scarf/scarf/Features/CredentialPools/Views/CredentialPoolsView.swift b/scarf/scarf/Features/CredentialPools/Views/CredentialPoolsView.swift index 7508b5c..68a1568 100644 --- a/scarf/scarf/Features/CredentialPools/Views/CredentialPoolsView.swift +++ b/scarf/scarf/Features/CredentialPools/Views/CredentialPoolsView.swift @@ -194,6 +194,13 @@ private struct AddCredentialSheet: View { case apiKey = "API Key" case oauth = "OAuth" var id: String { rawValue } + + var displayName: LocalizedStringResource { + switch self { + case .apiKey: return "API Key" + case .oauth: return "OAuth" + } + } } @State private var providerID: String = "" @@ -262,7 +269,7 @@ private struct AddCredentialSheet: View { Text("Credential Type").font(.caption).foregroundStyle(.secondary) Picker("", selection: $authType) { ForEach(AuthType.allCases) { type in - Text(type.rawValue).tag(type) + Text(type.displayName).tag(type) } } .pickerStyle(.segmented) diff --git a/scarf/scarf/Features/Gateway/Views/GatewayView.swift b/scarf/scarf/Features/Gateway/Views/GatewayView.swift index 8611b94..1e908ba 100644 --- a/scarf/scarf/Features/Gateway/Views/GatewayView.swift +++ b/scarf/scarf/Features/Gateway/Views/GatewayView.swift @@ -102,7 +102,7 @@ struct GatewayView: View { Image(systemName: platform.icon) .font(.title2) .foregroundStyle(platform.isConnected ? Color.accentColor : .secondary) - Text(platform.name.capitalized) + Text(verbatim: platform.name.capitalized) .font(.caption.bold()) StatusBadge( label: platform.state, diff --git a/scarf/scarf/Features/Health/Views/HealthView.swift b/scarf/scarf/Features/Health/Views/HealthView.swift index 10e1b8a..a2482fd 100644 --- a/scarf/scarf/Features/Health/Views/HealthView.swift +++ b/scarf/scarf/Features/Health/Views/HealthView.swift @@ -132,7 +132,7 @@ struct HealthView: View { Circle() .fill(viewModel.hermesRunning ? .green : .red) .frame(width: 8, height: 8) - Text(viewModel.hermesRunning ? "Hermes Running" : "Hermes Stopped") + (viewModel.hermesRunning ? Text("Hermes Running") : Text("Hermes Stopped")) .font(.caption.bold()) if let pid = viewModel.hermesPID { Text("PID \(pid)") diff --git a/scarf/scarf/Features/Insights/ViewModels/InsightsViewModel.swift b/scarf/scarf/Features/Insights/ViewModels/InsightsViewModel.swift index 725ee2f..bf1ce76 100644 --- a/scarf/scarf/Features/Insights/ViewModels/InsightsViewModel.swift +++ b/scarf/scarf/Features/Insights/ViewModels/InsightsViewModel.swift @@ -8,6 +8,15 @@ enum InsightsPeriod: String, CaseIterable, Identifiable { var id: String { rawValue } + var displayName: LocalizedStringResource { + switch self { + case .week: return "7 Days" + case .month: return "30 Days" + case .quarter: return "90 Days" + case .all: return "All Time" + } + } + var sinceDate: Date { let calendar = Calendar.current switch self { diff --git a/scarf/scarf/Features/Insights/Views/InsightsView.swift b/scarf/scarf/Features/Insights/Views/InsightsView.swift index 436b110..496458b 100644 --- a/scarf/scarf/Features/Insights/Views/InsightsView.swift +++ b/scarf/scarf/Features/Insights/Views/InsightsView.swift @@ -37,7 +37,7 @@ struct InsightsView: View { private var periodPicker: some View { Picker("Period", selection: $viewModel.period) { ForEach(InsightsPeriod.allCases) { period in - Text(period.rawValue).tag(period) + Text(period.displayName).tag(period) } } .pickerStyle(.segmented) @@ -90,7 +90,7 @@ struct InsightsView: View { VStack(alignment: .trailing, spacing: 2) { Text("\(model.sessions) sessions") .font(.caption) - Text(formatTokens(model.totalTokens) + " tokens") + Text("\(formatTokens(model.totalTokens)) tokens") .font(.caption) .foregroundStyle(.secondary) } @@ -164,7 +164,7 @@ struct InsightsView: View { .font(.caption.monospaced()) .foregroundStyle(.secondary) .frame(width: 40, alignment: .trailing) - Text(String(format: "%.1f%%", tool.percentage)) + Text((tool.percentage / 100).formatted(.percent.precision(.fractionLength(1)))) .font(.caption) .foregroundStyle(.tertiary) .frame(width: 50, alignment: .trailing) @@ -193,12 +193,12 @@ struct InsightsView: View { Text("By Day") .font(.caption.bold()) .foregroundStyle(.secondary) - let dayNames = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] + let dayNames = Calendar.current.shortWeekdaySymbols let maxVal = max(1, viewModel.dailyActivity.values.max() ?? 1) ForEach(0..<7, id: \.self) { day in let count = viewModel.dailyActivity[day] ?? 0 HStack(spacing: 6) { - Text(dayNames[day]) + Text(verbatim: dayNames[(day + 1) % 7]) .font(.caption.monospaced()) .frame(width: 30, alignment: .trailing) RoundedRectangle(cornerRadius: 2) diff --git a/scarf/scarf/Features/Logs/ViewModels/LogsViewModel.swift b/scarf/scarf/Features/Logs/ViewModels/LogsViewModel.swift index 5ed799e..9af34d2 100644 --- a/scarf/scarf/Features/Logs/ViewModels/LogsViewModel.swift +++ b/scarf/scarf/Features/Logs/ViewModels/LogsViewModel.swift @@ -23,6 +23,14 @@ final class LogsViewModel { case gateway = "gateway.log" var id: String { rawValue } + + var displayName: LocalizedStringResource { + switch self { + case .agent: return "Agent" + case .errors: return "Errors" + case .gateway: return "Gateway" + } + } } private func path(for file: LogFile) -> String { @@ -43,6 +51,17 @@ final class LogsViewModel { var id: String { rawValue } + var displayName: LocalizedStringResource { + switch self { + case .all: return "All" + case .gateway: return "Gateway" + case .agent: return "Agent" + case .tools: return "Tools" + case .cli: return "CLI" + case .cron: return "Cron" + } + } + var loggerPrefix: String? { switch self { case .all: return nil diff --git a/scarf/scarf/Features/Logs/Views/LogsView.swift b/scarf/scarf/Features/Logs/Views/LogsView.swift index d21aa89..afd4dae 100644 --- a/scarf/scarf/Features/Logs/Views/LogsView.swift +++ b/scarf/scarf/Features/Logs/Views/LogsView.swift @@ -27,7 +27,7 @@ struct LogsView: View { set: { file in Task { await viewModel.switchLogFile(file) } } )) { ForEach(LogsViewModel.LogFile.allCases) { file in - Text(file.rawValue).tag(file) + Text(file.displayName).tag(file) } } .pickerStyle(.segmented) @@ -35,7 +35,7 @@ struct LogsView: View { Picker("Component", selection: $viewModel.selectedComponent) { ForEach(LogsViewModel.LogComponent.allCases) { component in - Text(component.rawValue).tag(component) + Text(component.displayName).tag(component) } } .frame(maxWidth: 140) @@ -45,7 +45,7 @@ struct LogsView: View { Picker("Level", selection: $viewModel.filterLevel) { Text("All Levels").tag(LogEntry.LogLevel?.none) ForEach(LogEntry.LogLevel.allCases, id: \.rawValue) { level in - Text(level.rawValue).tag(LogEntry.LogLevel?.some(level)) + Text(verbatim: level.rawValue).tag(LogEntry.LogLevel?.some(level)) } } .frame(maxWidth: 150) @@ -66,7 +66,7 @@ struct LogsView: View { .font(.caption.monospaced()) .foregroundStyle(.secondary) .frame(width: 140, alignment: .leading) - Text(entry.level.rawValue) + Text(verbatim: entry.level.rawValue) .font(.caption.monospaced().bold()) .foregroundStyle(colorForLevel(entry.level)) .frame(width: 60, alignment: .leading) diff --git a/scarf/scarf/Features/MCPServers/Views/MCPServerDetailView.swift b/scarf/scarf/Features/MCPServers/Views/MCPServerDetailView.swift index d678c14..f076800 100644 --- a/scarf/scarf/Features/MCPServers/Views/MCPServerDetailView.swift +++ b/scarf/scarf/Features/MCPServers/Views/MCPServerDetailView.swift @@ -154,7 +154,7 @@ struct MCPServerDetailView: View { Text(key) .font(.system(.caption, design: .monospaced)) Spacer() - Text(String(repeating: "•", count: 10)) + Text("••••••••••") .font(.caption.monospaced()) .foregroundStyle(.secondary) } @@ -182,7 +182,7 @@ struct MCPServerDetailView: View { Text(key) .font(.system(.caption, design: .monospaced)) Spacer() - Text(String(repeating: "•", count: 10)) + Text("••••••••••") .font(.caption.monospaced()) .foregroundStyle(.secondary) } diff --git a/scarf/scarf/Features/MCPServers/Views/MCPServerPresetPickerView.swift b/scarf/scarf/Features/MCPServers/Views/MCPServerPresetPickerView.swift index b51a5bd..58491c6 100644 --- a/scarf/scarf/Features/MCPServers/Views/MCPServerPresetPickerView.swift +++ b/scarf/scarf/Features/MCPServers/Views/MCPServerPresetPickerView.swift @@ -33,9 +33,9 @@ struct MCPServerPresetPickerView: View { } } VStack(alignment: .leading, spacing: 2) { - Text(selectedPreset?.displayName ?? "Add from Preset") + (selectedPreset.map { Text(verbatim: $0.displayName) } ?? Text("Add from Preset")) .font(.headline) - Text(selectedPreset?.description ?? "Pick an MCP server to add.") + (selectedPreset.map { Text(verbatim: $0.description) } ?? Text("Pick an MCP server to add.")) .font(.caption) .foregroundStyle(.secondary) .lineLimit(1) @@ -83,14 +83,14 @@ struct MCPServerPresetPickerView: View { Image(systemName: preset.iconSystemName) .font(.title3) .foregroundStyle(Color.accentColor) - Text(preset.displayName) + Text(verbatim: preset.displayName) .font(.body.bold()) Spacer() Image(systemName: preset.transport == .http ? "network" : "terminal") .font(.caption) .foregroundStyle(.secondary) } - Text(preset.description) + Text(verbatim: preset.description) .font(.caption) .foregroundStyle(.secondary) .lineLimit(3) diff --git a/scarf/scarf/Features/MCPServers/Views/MCPServerTestResultView.swift b/scarf/scarf/Features/MCPServers/Views/MCPServerTestResultView.swift index 55dee3f..b15e947 100644 --- a/scarf/scarf/Features/MCPServers/Views/MCPServerTestResultView.swift +++ b/scarf/scarf/Features/MCPServers/Views/MCPServerTestResultView.swift @@ -10,9 +10,9 @@ struct MCPServerTestResultView: View { Image(systemName: result.succeeded ? "checkmark.seal.fill" : "xmark.seal.fill") .foregroundStyle(result.succeeded ? .green : .red) VStack(alignment: .leading, spacing: 2) { - Text(result.succeeded ? "Test passed" : "Test failed") + (result.succeeded ? Text("Test passed") : Text("Test failed")) .font(.subheadline.bold()) - Text(String(format: "%.1fs · %d tools", result.elapsed, result.tools.count)) + Text("\(result.elapsed.formatted(.number.precision(.fractionLength(1))))s · \(result.tools.count) tools") .font(.caption) .foregroundStyle(.secondary) } @@ -20,8 +20,12 @@ struct MCPServerTestResultView: View { Button { showOutput.toggle() } label: { - Label(showOutput ? "Hide Output" : "Show Output", systemImage: showOutput ? "chevron.up" : "chevron.down") - .font(.caption) + Label { + showOutput ? Text("Hide Output") : Text("Show Output") + } icon: { + Image(systemName: showOutput ? "chevron.up" : "chevron.down") + } + .font(.caption) } .buttonStyle(.borderless) } diff --git a/scarf/scarf/Features/MCPServers/Views/MCPServersView.swift b/scarf/scarf/Features/MCPServers/Views/MCPServersView.swift index bc54d79..4aef8e4 100644 --- a/scarf/scarf/Features/MCPServers/Views/MCPServersView.swift +++ b/scarf/scarf/Features/MCPServers/Views/MCPServersView.swift @@ -128,7 +128,7 @@ struct MCPServersView: View { } else if let result = viewModel.testResults[server.name] { Image(systemName: result.succeeded ? "checkmark.circle.fill" : "xmark.circle.fill") .foregroundStyle(result.succeeded ? .green : .red) - .help(result.succeeded ? "\(result.tools.count) tools" : "Test failed") + .help(result.succeeded ? Text("\(result.tools.count) tools") : Text("Test failed")) } } } diff --git a/scarf/scarf/Features/Platforms/Views/PlatformSetup/SignalSetupView.swift b/scarf/scarf/Features/Platforms/Views/PlatformSetup/SignalSetupView.swift index ec49419..54ba83e 100644 --- a/scarf/scarf/Features/Platforms/Views/PlatformSetup/SignalSetupView.swift +++ b/scarf/scarf/Features/Platforms/Views/PlatformSetup/SignalSetupView.swift @@ -51,7 +51,9 @@ struct SignalSetupView: View { HStack(spacing: 8) { Image(systemName: viewModel.signalCLIInstalled ? "checkmark.circle.fill" : "exclamationmark.triangle.fill") .foregroundStyle(viewModel.signalCLIInstalled ? .green : .orange) - Text(viewModel.signalCLIInstalled ? "signal-cli is available on PATH" : "signal-cli not found on PATH — install it first") + (viewModel.signalCLIInstalled + ? Text("signal-cli is available on PATH") + : Text("signal-cli not found on PATH — install it first")) .font(.caption) .foregroundStyle(viewModel.signalCLIInstalled ? Color.primary : Color.orange) Spacer() diff --git a/scarf/scarf/Features/Platforms/Views/PlatformsView.swift b/scarf/scarf/Features/Platforms/Views/PlatformsView.swift index fe7a798..ac28df8 100644 --- a/scarf/scarf/Features/Platforms/Views/PlatformsView.swift +++ b/scarf/scarf/Features/Platforms/Views/PlatformsView.swift @@ -40,7 +40,7 @@ struct PlatformsView: View { HStack(spacing: 8) { Image(systemName: KnownPlatforms.icon(for: platform.name)) .frame(width: 20) - Text(platform.displayName) + Text(verbatim: platform.displayName) Spacer() Circle() .fill(statusColor(viewModel.connectivity(for: platform))) @@ -88,7 +88,7 @@ struct PlatformsView: View { Image(systemName: KnownPlatforms.icon(for: viewModel.selected.name)) .font(.title) VStack(alignment: .leading) { - Text(viewModel.selected.displayName) + Text(verbatim: viewModel.selected.displayName) .font(.title2.bold()) Text(statusDescription(viewModel.connectivity(for: viewModel.selected))) .font(.caption) diff --git a/scarf/scarf/Features/Profiles/Views/ProfilesView.swift b/scarf/scarf/Features/Profiles/Views/ProfilesView.swift index 042a711..8ad1ce9 100644 --- a/scarf/scarf/Features/Profiles/Views/ProfilesView.swift +++ b/scarf/scarf/Features/Profiles/Views/ProfilesView.swift @@ -142,7 +142,7 @@ struct ProfilesView: View { .font(.title) VStack(alignment: .leading) { Text(profile.name).font(.title2.bold()) - Text(profile.isActive ? "Active profile" : "Inactive") + (profile.isActive ? Text("Active profile") : Text("Inactive")) .font(.caption) .foregroundStyle(.secondary) } diff --git a/scarf/scarf/Features/Projects/Views/ProjectsView.swift b/scarf/scarf/Features/Projects/Views/ProjectsView.swift index 0eadb21..c8c1154 100644 --- a/scarf/scarf/Features/Projects/Views/ProjectsView.swift +++ b/scarf/scarf/Features/Projects/Views/ProjectsView.swift @@ -3,6 +3,13 @@ import SwiftUI private enum DashboardTab: String, CaseIterable { case dashboard = "Dashboard" case site = "Site" + + var displayName: LocalizedStringResource { + switch self { + case .dashboard: return "Dashboard" + case .site: return "Site" + } + } } struct ProjectsView: View { @@ -150,7 +157,7 @@ struct ProjectsView: View { HStack(spacing: 4) { Image(systemName: tab == .dashboard ? "square.grid.2x2" : "globe") .font(.caption) - Text(tab.rawValue) + Text(tab.displayName) .font(.subheadline) } .padding(.horizontal, 12) diff --git a/scarf/scarf/Features/QuickCommands/Views/QuickCommandsView.swift b/scarf/scarf/Features/QuickCommands/Views/QuickCommandsView.swift index 3131106..1a14303 100644 --- a/scarf/scarf/Features/QuickCommands/Views/QuickCommandsView.swift +++ b/scarf/scarf/Features/QuickCommands/Views/QuickCommandsView.swift @@ -145,7 +145,7 @@ private struct QuickCommandEditor: View { var body: some View { VStack(alignment: .leading, spacing: 12) { - Text(initial == nil ? "Add Quick Command" : "Edit /\(initial!.name)") + (initial == nil ? Text("Add Quick Command") : Text("Edit /\(initial!.name)")) .font(.headline) VStack(alignment: .leading, spacing: 4) { Text("Name (no leading slash)") diff --git a/scarf/scarf/Features/Servers/Views/ConnectionStatusPill.swift b/scarf/scarf/Features/Servers/Views/ConnectionStatusPill.swift index 6961de3..5707934 100644 --- a/scarf/scarf/Features/Servers/Views/ConnectionStatusPill.swift +++ b/scarf/scarf/Features/Servers/Views/ConnectionStatusPill.swift @@ -31,7 +31,7 @@ struct ConnectionStatusPill: View { Image(systemName: iconName) .foregroundStyle(color) .symbolRenderingMode(.hierarchical) - Text(label) + labelText .font(.caption) .foregroundStyle(.secondary) .lineLimit(1) @@ -39,7 +39,7 @@ struct ConnectionStatusPill: View { .padding(.horizontal, 4) } .buttonStyle(.plain) - .help(tooltip) + .help(tooltipText) .popover(isPresented: $showDetails, arrowEdge: .bottom) { errorDetails.frame(width: 400) } @@ -70,27 +70,27 @@ struct ConnectionStatusPill: View { } } - private var label: String { + private var labelText: Text { switch status.status { - case .connected: return "Connected" - case .degraded: return "Connected — can't read Hermes state" - case .idle: return "Checking…" - case .error(let message, _): return message + case .connected: return Text("Connected") + case .degraded: return Text("Connected — can't read Hermes state") + case .idle: return Text("Checking…") + case .error(let message, _): return Text(verbatim: message) } } - private var tooltip: String { + private var tooltipText: Text { switch status.status { case .connected: if let ts = status.lastSuccess { let fmt = RelativeDateTimeFormatter() - return "Last probe: \(fmt.localizedString(for: ts, relativeTo: Date()))" + return Text("Last probe: \(fmt.localizedString(for: ts, relativeTo: Date()))") } - return "Connected" + return Text("Connected") case .degraded(let reason): - return "SSH works but \(reason). Click for diagnostics." - case .idle: return "Waiting for first probe" - case .error(_, _): return "Click for details" + return Text("SSH works but \(reason). Click for diagnostics.") + case .idle: return Text("Waiting for first probe") + case .error: return Text("Click for details") } } diff --git a/scarf/scarf/Features/Servers/Views/ManageServersView.swift b/scarf/scarf/Features/Servers/Views/ManageServersView.swift index ffaa929..015e24b 100644 --- a/scarf/scarf/Features/Servers/Views/ManageServersView.swift +++ b/scarf/scarf/Features/Servers/Views/ManageServersView.swift @@ -93,7 +93,7 @@ struct ManageServersView: View { Image(systemName: "server.rack") .foregroundStyle(.blue) VStack(alignment: .leading, spacing: 2) { - Text(entry.displayName).font(.body) + Text(verbatim: entry.displayName).font(.body) if case .ssh(let config) = entry.kind { Text(summary(for: config)) .font(.caption) diff --git a/scarf/scarf/Features/Servers/Views/ServerSwitcherToolbar.swift b/scarf/scarf/Features/Servers/Views/ServerSwitcherToolbar.swift index c7c5dc0..64ed473 100644 --- a/scarf/scarf/Features/Servers/Views/ServerSwitcherToolbar.swift +++ b/scarf/scarf/Features/Servers/Views/ServerSwitcherToolbar.swift @@ -37,7 +37,7 @@ struct ServerSwitcherToolbar: View { Circle() .fill(current.isRemote ? Color.blue : Color.green) .frame(width: 8, height: 8) - Text(current.displayName) + Text(verbatim: current.displayName) .font(.callout) .lineLimit(1) Image(systemName: "chevron.down") diff --git a/scarf/scarf/Features/Settings/Views/Components/ModelPickerSheet.swift b/scarf/scarf/Features/Settings/Views/Components/ModelPickerSheet.swift index 674d60f..999af77 100644 --- a/scarf/scarf/Features/Settings/Views/Components/ModelPickerSheet.swift +++ b/scarf/scarf/Features/Settings/Views/Components/ModelPickerSheet.swift @@ -102,7 +102,7 @@ struct ModelPickerSheet: View { .font(.system(.body, design: .default, weight: .medium)) Spacer() if let ctx = model.contextDisplay { - Text(ctx + " ctx") + Text("\(ctx) ctx") .font(.caption2.monospaced()) .foregroundStyle(.secondary) } diff --git a/scarf/scarf/Features/Skills/Views/SkillsView.swift b/scarf/scarf/Features/Skills/Views/SkillsView.swift index 4c9edd0..0ec9834 100644 --- a/scarf/scarf/Features/Skills/Views/SkillsView.swift +++ b/scarf/scarf/Features/Skills/Views/SkillsView.swift @@ -14,6 +14,14 @@ struct SkillsView: View { case hub = "Browse Hub" case updates = "Updates" var id: String { rawValue } + + var displayName: LocalizedStringResource { + switch self { + case .installed: return "Installed" + case .hub: return "Browse Hub" + case .updates: return "Updates" + } + } } var body: some View { @@ -34,7 +42,7 @@ struct SkillsView: View { HStack { Picker("", selection: $currentTab) { ForEach(Tab.allCases) { tab in - Text(tab.rawValue).tag(tab) + Text(tab.displayName).tag(tab) } } .pickerStyle(.segmented) diff --git a/scarf/scarf/Features/Tools/Views/ToolsView.swift b/scarf/scarf/Features/Tools/Views/ToolsView.swift index 9c202b2..ec813fa 100644 --- a/scarf/scarf/Features/Tools/Views/ToolsView.swift +++ b/scarf/scarf/Features/Tools/Views/ToolsView.swift @@ -43,7 +43,7 @@ struct ToolsView: View { } label: { HStack(spacing: 8) { Image(systemName: KnownPlatforms.icon(for: viewModel.selectedPlatform.name)) - Text(viewModel.selectedPlatform.displayName) + Text(verbatim: viewModel.selectedPlatform.displayName) .fontWeight(.medium) statusDot(for: viewModel.connectivity[viewModel.selectedPlatform.name] ?? .notConfigured) Image(systemName: "chevron.down") diff --git a/scarf/scarf/Localizable.xcstrings b/scarf/scarf/Localizable.xcstrings index 8158e7b..05e3e35 100644 --- a/scarf/scarf/Localizable.xcstrings +++ b/scarf/scarf/Localizable.xcstrings @@ -6,6 +6,9 @@ }, "#%lld" : { + }, + "%@ ctx" : { + }, "%@ in / %@ out" : { "localizations" : { @@ -19,6 +22,9 @@ }, "%@ reasoning" : { + }, + "%@ tokens" : { + }, "%@ · %@" : { "localizations" : { @@ -40,6 +46,16 @@ } } }, + "%@s · %lld tools" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@s · %2$lld tools" + } + } + } + }, "%lld" : { }, @@ -117,6 +133,15 @@ }, "22" : { + }, + "30 Days" : { + + }, + "7 Days" : { + + }, + "90 Days" : { + }, "<%@>" : { @@ -132,6 +157,9 @@ }, "Actions" : { + }, + "Active" : { + }, "Active profile" : { @@ -186,12 +214,21 @@ }, "After approving in your browser, the provider shows a code. Paste it below and submit." : { + }, + "Agent" : { + + }, + "All" : { + }, "All Levels" : { }, "All Sessions" : { + }, + "All Time" : { + }, "All installed hub skills are up to date." : { @@ -243,18 +280,27 @@ }, "Browse" : { + }, + "Browse Hub" : { + }, "Browse the Hub" : { }, "Browse..." : { + }, + "Browser" : { + }, "By Day" : { }, "By Hour" : { + }, + "CLI" : { + }, "Call timeout" : { @@ -285,6 +331,9 @@ }, "Check for Updates…" : { + }, + "Checking…" : { + }, "Choose a cron job from the list" : { @@ -315,6 +364,9 @@ }, "Click Add to connect to a remote Hermes installation over SSH." : { + }, + "Click for details" : { + }, "Clicking Start OAuth opens the provider's authorization page in your browser. After you approve, copy the code the provider displays and paste it back into the terminal that appears next." : { @@ -357,6 +409,9 @@ }, "Connected" : { + }, + "Connected — can't read Hermes state" : { + }, "Connection" : { @@ -420,6 +475,9 @@ }, "Credentials" : { + }, + "Cron" : { + }, "Cron Jobs" : { @@ -528,9 +586,15 @@ }, "Error" : { + }, + "Errors" : { + }, "Exclude" : { + }, + "Execute" : { + }, "Expected at %@" : { @@ -552,6 +616,9 @@ }, "Feishu Setup Docs" : { + }, + "Fetch" : { + }, "Files" : { @@ -663,6 +730,9 @@ }, "Install signal-cli" : { + }, + "Installed" : { + }, "Interact" : { @@ -681,6 +751,9 @@ }, "Last Output" : { + }, + "Last probe: %@" : { + }, "Last run: %@" : { @@ -939,6 +1012,9 @@ }, "Optionally focus the summary on a specific topic. Leave blank to compress evenly." : { + }, + "Other" : { + }, "Output" : { @@ -975,6 +1051,9 @@ }, "Personalities" : { + }, + "Pick an MCP server to add." : { + }, "Pick one from the list, or add a new server from the toolbar." : { @@ -1044,6 +1123,9 @@ }, "Re-run" : { + }, + "Read" : { + }, "Reasoning" : { @@ -1054,7 +1136,7 @@ "Reconnect" : { }, - "Recording..." : { + "Recording…" : { }, "Refresh" : { @@ -1185,6 +1267,9 @@ }, "SOUL.md describes the agent's voice, values, and personality at ~/.hermes/SOUL.md. It is injected into every session's context." : { + }, + "SSH works but %@. Click for diagnostics." : { + }, "Save" : { @@ -1314,6 +1399,9 @@ }, "Signal integration requires signal-cli (Java-based) installed locally. Link this Mac as a Signal device, then keep the daemon running so hermes can send/receive messages." : { + }, + "Site" : { + }, "Skills" : { @@ -1498,6 +1586,9 @@ }, "Updated: %@" : { + }, + "Updates" : { + }, "Upload" : { @@ -1537,6 +1628,9 @@ }, "Waiting for authorization URL…" : { + }, + "Waiting for first probe" : { + }, "Waiting for hermes to prompt for the code…" : { @@ -1684,6 +1778,9 @@ }, "•" : { + }, + "••••••••••" : { + } }, "version" : "1.0"