Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Models/AgentKeyAssignment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ struct CodexThreadDescriptor: Identifiable, Codable, Hashable, Sendable {
if !title.isEmpty { return title }
if !preview.isEmpty { return preview }
let shortID = String(id.prefix(8))
return isSubagent ? "Subagent · \(shortID)" : "Sitzung · \(shortID)"
return isSubagent ? "Subagent · \(shortID)" : AppLanguage.text("Sitzung · \(shortID)", "Session · \(shortID)")
}

var projectName: String? {
Expand Down
21 changes: 21 additions & 0 deletions Models/AppLanguage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,25 @@ enum AppLanguage: String, CaseIterable, Identifiable {
static func text(_ german: String, _ english: String) -> String {
current.text(german, english)
}

/// Looks a German source string up in the selected language's
/// `Localizable.strings`, the same table SwiftUI's `Text("…")` reads.
///
/// Needed wherever a translation has to be produced as a plain `String` at
/// runtime — catalog data decoded from JSON, `NSMenu` titles, values fed
/// into `Text(verbatim:)` — because those never pass through
/// `LocalizedStringKey`. Falls back to the German source when no entry
/// exists, so an untranslated string still renders.
static func localized(_ german: String) -> String {
current.localized(german)
}

func localized(_ german: String) -> String {
guard self != .german else { return german }
guard
let path = Bundle.module.path(forResource: rawValue, ofType: "lproj"),
let bundle = Bundle(path: path)
else { return german }
return bundle.localizedString(forKey: german, value: german, table: nil)
}
}
28 changes: 26 additions & 2 deletions Models/CodexActionCatalog.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,35 @@ struct CodexActionCatalog {
self.document = document
}

var actions: [CodexActionDefinition] { document.actions }
/// The catalog JSON ships its user-facing text in German (see
/// `Resources/CodexActions.json` and `Resources/ClaudeActions.json`). This
/// is the single place that text is translated: every read path —
/// `actions`, `categories`, `action(id:)` and the `KeyboardAction`s derived
/// from them — goes through here, so anything that groups, sorts or
/// filters by `category` keeps comparing like with like.
///
/// Only the four display fields are touched; `id`, `icon`, `shortcut`,
/// `deviceMacro`, `deepLink`, `modifiers`, `execution` and `compatibleWith`
/// stay exactly as decoded. Translating on read rather than once at decode
/// time means switching the app language takes effect immediately, without
/// rebuilding the catalog.
private static func localized(_ action: CodexActionDefinition) -> CodexActionDefinition {
var localized = action
localized.title = AppLanguage.localized(action.title)
localized.description = AppLanguage.localized(action.description)
localized.category = AppLanguage.localized(action.category)
localized.availabilityNote = action.availabilityNote.map(AppLanguage.localized)
// Most shortcuts are pure key symbols, but a few spell out a gesture
// ("⌘F17 halten"), so they need the same treatment.
localized.shortcut = action.shortcut.map(AppLanguage.localized)
return localized
}

var actions: [CodexActionDefinition] { document.actions.map(Self.localized) }
var categories: [String] { Array(Set(actions.map(\.category))).sorted() }

func action(id: String) -> CodexActionDefinition? {
actions.first(where: { $0.id == id })
document.actions.first(where: { $0.id == id }).map(Self.localized)
}

func keyboardAction(id: String) -> KeyboardAction? {
Expand Down
5 changes: 4 additions & 1 deletion Models/DeviceModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,10 @@ struct USBInterface: Codable, Hashable, Identifiable {
var endpoints: Int

var summary: String {
"Interface \(number) · Klasse \(interfaceClass)/\(subclass)/\(protocolCode) · \(endpoints) Endpoint\(endpoints == 1 ? "" : "s")"
AppLanguage.text(
"Interface \(number) · Klasse \(interfaceClass)/\(subclass)/\(protocolCode) · \(endpoints) Endpoint\(endpoints == 1 ? "" : "s")",
"Interface \(number) · class \(interfaceClass)/\(subclass)/\(protocolCode) · \(endpoints) endpoint\(endpoints == 1 ? "" : "s")"
)
}
}

Expand Down
9 changes: 6 additions & 3 deletions Models/KeyboardAction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,11 @@ struct KeyboardAction: Codable, Hashable, Identifiable {

/// Labels for built-in actions are stored in profiles for backwards
/// compatibility, but must follow the current app language at display
/// time. Custom action labels deliberately remain exactly as the user
/// entered them.
/// time. The stored `label` is therefore never rewritten — every kind that
/// can be reconstructed from other fields is rebuilt here, and anything
/// else (catalog titles, the factory `macOS` profile's German labels, the
/// `Pet anzeigen` migration) is looked up in the strings table. A label the
/// user typed themselves has no entry there and passes through untouched.
var displayLabel: String {
return switch kind {
case .disabled:
Expand All @@ -173,7 +176,7 @@ struct KeyboardAction: Codable, Hashable, Identifiable {
case .textSubmission:
submittedText.map { AppLanguage.text("„\($0)“ absenden", "Submit “\($0)”") } ?? label
default:
label
AppLanguage.localized(label)
}
}

Expand Down
8 changes: 7 additions & 1 deletion Models/MacropadProfile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -835,7 +835,13 @@ enum ProfileFactory {
action: KeyboardAction(kind: .singleKey, label: key.uppercased(), icon: "keyboard", deviceMacro: key)
)
}
return MacropadProfile(name: "Sichere F13–F21-Belegung", controls: controls)
// Generated fresh on each call and never matched against (only
// "Codex", "Claude" and "Codex · Reasoning triggers" are looked up by
// name), so this one is safe to localize at creation time.
return MacropadProfile(
name: AppLanguage.text("Sichere F13–F21-Belegung", "Safe F13–F21 mapping"),
controls: controls
)
}

static func macOS() -> MacropadProfile {
Expand Down
Loading