From 59f55a5c120482fac3968071f2ff5824a07232c7 Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Sun, 5 Jul 2026 22:02:38 -0400 Subject: [PATCH 1/4] Add Foreman string catalogs and enable generated symbols Introduce a per-module Localizable.xcstrings for the Foreman app target and ForemanCore, wire the app target's STRING_CATALOG_GENERATE_SYMBOLS build setting and Resources glob in Project.swift, and process ForemanCore's Resources in Package.swift. Groundwork: catalogs are added but not yet referenced. Co-authored-by: Cursor --- .../Foreman/Resources/Localizable.xcstrings | 930 ++++++++++++++++++ .../Sources/Resources/Localizable.xcstrings | 126 +++ Package.swift | 3 + Project.swift | 11 +- 4 files changed, 1067 insertions(+), 3 deletions(-) create mode 100644 Foreman/Foreman/Resources/Localizable.xcstrings create mode 100644 Foreman/ForemanCore/Sources/Resources/Localizable.xcstrings diff --git a/Foreman/Foreman/Resources/Localizable.xcstrings b/Foreman/Foreman/Resources/Localizable.xcstrings new file mode 100644 index 00000000..74ddd251 --- /dev/null +++ b/Foreman/Foreman/Resources/Localizable.xcstrings @@ -0,0 +1,930 @@ +{ + "sourceLanguage" : "en", + "strings" : { + "common.cancel" : { + "comment" : "Button that dismisses an editor sheet without saving.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancel" + } + } + } + }, + "common.save" : { + "comment" : "Button that commits the current edits.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Save" + } + } + } + }, + "detail.action.restart" : { + "comment" : "Button in the worker detail status row that stops and restarts a running worker.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Restart" + } + } + } + }, + "detail.action.restart.help" : { + "comment" : "Tooltip for the Restart button in the worker detail.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Stop the worker and start it again with the saved options." + } + } + } + }, + "detail.action.retry" : { + "comment" : "Button in the worker detail status row that retries a failed worker.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Retry" + } + } + } + }, + "detail.action.retry.help" : { + "comment" : "Tooltip for the Retry button in the worker detail.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Try starting the worker again." + } + } + } + }, + "detail.command.header" : { + "comment" : "Section header above the cursor-agent command the next start will spawn.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Command" + } + } + } + }, + "detail.empty.description" : { + "comment" : "Description of the empty detail pane shown when no repo is selected.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pick a repo to inspect and control its worker." + } + } + } + }, + "detail.empty.title" : { + "comment" : "Title of the empty detail pane shown when no repo is selected.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Select a Repository" + } + } + } + }, + "detail.failure.label" : { + "comment" : "Label for the row showing why a worker failed.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Failure" + } + } + } + }, + "detail.log.header" : { + "comment" : "Section header above the live worker log tail.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Log" + } + } + } + }, + "detail.log.openFile" : { + "comment" : "Button that opens the worker's log file in the default app.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Open File" + } + } + } + }, + "detail.path.label" : { + "comment" : "Label for the row showing the repository's path.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Path" + } + } + } + }, + "detail.pid.label" : { + "comment" : "Label for the row showing the running worker's process id.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "PID" + } + } + } + }, + "detail.status.label" : { + "comment" : "Label for the row showing the worker's status.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Status" + } + } + } + }, + "detail.uptime.label" : { + "comment" : "Label for the row showing how long a worker has been running.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uptime" + } + } + } + }, + "favorite.add" : { + "comment" : "Menu/toolbar action that pins a repo as a favorite.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Add to Favorites" + } + } + } + }, + "favorite.add.help" : { + "comment" : "Tooltip for the favorite toolbar button when the repo is not a favorite.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pin this repo to the top of its section." + } + } + } + }, + "favorite.remove" : { + "comment" : "Menu/toolbar action that unpins a favorited repo.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Remove from Favorites" + } + } + } + }, + "favorite.remove.help" : { + "comment" : "Tooltip for the favorite toolbar button when the repo is already a favorite.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Remove this repo from favorites." + } + } + } + }, + "log.empty" : { + "comment" : "Shown in the log tail when the worker has no log file yet.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No log yet — this worker hasn't started." + } + } + } + }, + "log.readFailed" : { + "comment" : "Shown when the worker's log file couldn't be read. The placeholder is the underlying error message.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Couldn't read the log: %(message)@" + } + } + } + }, + "options.header" : { + "comment" : "Section header for the worker options editor.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Options" + } + } + } + }, + "options.idleRelease.label" : { + "comment" : "Field label for the worker's idle-release timeout in seconds.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Idle release (seconds)" + } + } + } + }, + "options.idleRelease.prompt" : { + "comment" : "Placeholder for the idle-release field; zero means the worker is never released.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "0 = never" + } + } + } + }, + "options.label.key" : { + "comment" : "Placeholder for the key part of a worker key=value label.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "key" + } + } + } + }, + "options.label.value" : { + "comment" : "Placeholder for the value part of a worker key=value label.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "value" + } + } + } + }, + "options.labels.add.help" : { + "comment" : "Tooltip for the button that adds a worker key=value label.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Add a key=value label for this worker." + } + } + } + }, + "options.labels.header" : { + "comment" : "Header for the worker labels sub-section.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Labels" + } + } + } + }, + "options.locked.footer" : { + "comment" : "Footer explaining that options can only be edited while the worker is stopped.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Stop the worker to edit — options apply when it starts. Restart respawns with the saved options." + } + } + } + }, + "options.pool.toggle" : { + "comment" : "Toggle that runs the worker in pool mode (one agent at a time).", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pool worker (one agent at a time)" + } + } + } + }, + "options.poolName.label" : { + "comment" : "Field label for the worker's pool name.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pool name" + } + } + } + }, + "options.poolName.prompt" : { + "comment" : "Placeholder for the pool-name field (the default pool).", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "default" + } + } + } + }, + "options.resetDefaults" : { + "comment" : "Button that resets the worker options to their defaults.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reset to Defaults" + } + } + } + }, + "options.revert" : { + "comment" : "Button that discards unsaved worker option edits.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Revert" + } + } + } + }, + "options.verbose.toggle" : { + "comment" : "Toggle for verbose worker startup logs.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verbose startup logs" + } + } + } + }, + "options.workerName.label" : { + "comment" : "Field label for the worker's display name.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Worker name" + } + } + } + }, + "options.workerName.prompt" : { + "comment" : "Placeholder for the worker-name field (defaults to the host name).", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Host name" + } + } + } + }, + "row.failed" : { + "comment" : "Caption under a repo row whose worker failed, pointing to the detail pane.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Failed — see detail" + } + } + } + }, + "row.favorite.help" : { + "comment" : "Tooltip on the star shown for a favorited repo row.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Favorite" + } + } + } + }, + "settings.agent.autoDetect" : { + "comment" : "Shown when no explicit cursor-agent path is set, and as the field placeholder.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Auto-detect" + } + } + } + }, + "settings.agent.caption" : { + "comment" : "Caption in the cursor-agent path editor sheet.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Leave empty to auto-detect from the known install locations." + } + } + } + }, + "settings.agent.editorTitle" : { + "comment" : "Title of the sheet for editing the cursor-agent executable path.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "cursor-agent executable" + } + } + } + }, + "settings.agent.searchFooter" : { + "comment" : "Footer listing the paths searched for cursor-agent. The placeholder is a comma-separated path list.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Leave empty to search: %(paths)@" + } + } + } + }, + "settings.agent.title" : { + "comment" : "Sidebar title for the Agent settings pane.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Agent" + } + } + } + }, + "settings.change" : { + "comment" : "Button that opens an editor sheet to change a path setting.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Change…" + } + } + } + }, + "settings.choose" : { + "comment" : "Button that opens a folder picker in a path editor sheet.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Choose…" + } + } + } + }, + "settings.general.approvalNeeded" : { + "comment" : "Shown when the login item is registered but awaiting approval in System Settings.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Approve Foreman in System Settings to finish enabling this." + } + } + } + }, + "settings.general.launchFooter" : { + "comment" : "Footer under the launch-at-login toggle.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Foreman lives in the menu bar. Turn this on to start it automatically when you log in — it will restore any enabled workers." + } + } + } + }, + "settings.general.launchToggle" : { + "comment" : "Toggle that registers Foreman to launch at login.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Launch Foreman at login" + } + } + } + }, + "settings.general.openLoginItems" : { + "comment" : "Button that opens System Settings' Login Items pane.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Open Login Items…" + } + } + } + }, + "settings.general.title" : { + "comment" : "Sidebar title for the General settings pane.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "General" + } + } + } + }, + "settings.repositories.caption" : { + "comment" : "Caption in the scan-directory editor sheet.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Leave empty to use the default (~/Development)." + } + } + } + }, + "settings.repositories.default" : { + "comment" : "Shown for the scan directory when it falls back to the default location.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "~/Development (default)" + } + } + } + }, + "settings.repositories.footer" : { + "comment" : "Footer explaining what the scan directory is used for.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Foreman lists the git repositories directly inside this directory." + } + } + } + }, + "settings.repositories.prompt" : { + "comment" : "Placeholder for the scan-directory field.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "~/Development" + } + } + } + }, + "settings.repositories.scanDirectory" : { + "comment" : "Label and editor-sheet title for the repository scan directory.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Scan directory" + } + } + } + }, + "settings.repositories.title" : { + "comment" : "Sidebar title for the Repositories settings pane.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Repositories" + } + } + } + }, + "sidebar.empty.title" : { + "comment" : "Shown when no git repositories were found in the scan directory.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No Git Repositories" + } + } + } + }, + "sidebar.section.disabled" : { + "comment" : "Sidebar section header for repos whose worker is disabled.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Disabled" + } + } + } + }, + "sidebar.section.enabled" : { + "comment" : "Sidebar section header for repos whose worker is enabled.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enabled" + } + } + } + }, + "status.failed" : { + "comment" : "Worker status: the worker failed.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Failed" + } + } + } + }, + "status.failed.reason" : { + "comment" : "Worker status tooltip with the failure reason. The placeholder is the reason text.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Failed: %(reason)@" + } + } + } + }, + "status.restarting" : { + "comment" : "Worker status: stopping in order to restart.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Restarting…" + } + } + } + }, + "status.running" : { + "comment" : "Worker status: the worker is running.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Running" + } + } + } + }, + "status.running.pid" : { + "comment" : "Worker status tooltip with the running process id. The placeholder is the pid.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Running (pid %(pid)lld)" + } + } + } + }, + "status.stopped" : { + "comment" : "Worker status: the worker is stopped.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Stopped" + } + } + } + }, + "status.stopping" : { + "comment" : "Worker status: the worker is stopping.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Stopping…" + } + } + } + }, + "toolbar.preventingSleep" : { + "comment" : "Toolbar badge shown while the sleep assertion is held.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Preventing sleep" + } + } + } + }, + "toolbar.preventingSleep.help" : { + "comment" : "Tooltip for the preventing-sleep toolbar badge.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "The Mac won't idle-sleep while workers are running." + } + } + } + }, + "toolbar.quit" : { + "comment" : "Toolbar button that quits Foreman.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Quit" + } + } + } + }, + "toolbar.quit.help" : { + "comment" : "Tooltip for the Quit toolbar button.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Quit Foreman and stop all workers." + } + } + } + }, + "toolbar.rescan" : { + "comment" : "Toolbar button that re-scans for repositories.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rescan" + } + } + } + }, + "toolbar.rescan.help" : { + "comment" : "Tooltip for the Rescan toolbar button.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Re-scan the development directory for repositories." + } + } + } + }, + "toolbar.settings" : { + "comment" : "Toolbar button that opens Foreman's settings window.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Settings" + } + } + } + }, + "toolbar.settings.help" : { + "comment" : "Tooltip for the Settings toolbar button.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Open Foreman's settings." + } + } + } + }, + "worker.enabled.toggle" : { + "comment" : "Toggle that enables or disables a repo's worker.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Worker enabled" + } + } + } + } + }, + "version" : "1.1" +} diff --git a/Foreman/ForemanCore/Sources/Resources/Localizable.xcstrings b/Foreman/ForemanCore/Sources/Resources/Localizable.xcstrings new file mode 100644 index 00000000..9a8fc12e --- /dev/null +++ b/Foreman/ForemanCore/Sources/Resources/Localizable.xcstrings @@ -0,0 +1,126 @@ +{ + "sourceLanguage" : "en", + "strings" : { + "config.loadFailure" : { + "comment" : "Surfaced when the saved configuration file existed but couldn't be read; Foreman falls back to defaults.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Couldn't read saved settings — using defaults." + } + } + } + }, + "cursorAgent.notFound" : { + "comment" : "Error when the cursor-agent executable couldn't be located. The placeholder is a comma-separated list of paths that were checked.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "cursor-agent was not found. Checked: %(paths)@" + } + } + } + }, + "loginItem.turnOffFailed" : { + "comment" : "Surfaced when unregistering the launch-at-login item fails. The placeholder is the underlying error message.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Couldn't turn off “Launch at login”: %(error)@" + } + } + } + }, + "loginItem.turnOnFailed" : { + "comment" : "Surfaced when registering the launch-at-login item fails. The placeholder is the underlying error message.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Couldn't turn on “Launch at login”: %(error)@" + } + } + } + }, + "save.failed" : { + "comment" : "Surfaced when saving the configuration fails. The placeholder is the underlying error message.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Couldn't save settings: %(error)@" + } + } + } + }, + "scan.failed" : { + "comment" : "Surfaced when scanning the development directory fails. First placeholder is the directory path, second is the underlying error message.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Couldn't scan %(path)@: %(error)@" + } + } + } + }, + "services.shuttingDown" : { + "comment" : "Error used when a repo's executable resolution runs after the owning services were released.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Foreman is shutting down." + } + } + } + }, + "worker.exitedWithCode" : { + "comment" : "Worker failure reason for a nonzero exit. The placeholder is the exit status code.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Exited with code %(code)lld" + } + } + } + }, + "worker.terminatedBySignal" : { + "comment" : "Worker failure reason when the process was killed by a signal. The placeholder is the signal number.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Terminated by signal %(signal)lld" + } + } + } + }, + "worker.terminatedUnknown" : { + "comment" : "Worker failure reason for an unrecognized termination. The placeholder is the raw status value.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Terminated (unknown reason, status %(status)lld)" + } + } + } + } + }, + "version" : "1.1" +} diff --git a/Package.swift b/Package.swift index 47916d16..16cfce53 100644 --- a/Package.swift +++ b/Package.swift @@ -33,6 +33,9 @@ let package = Package( .target(name: "LogKit"), ], path: "Foreman/ForemanCore/Sources", + resources: [ + .process("Resources"), + ], ), .target( name: "LifecycleKit", diff --git a/Project.swift b/Project.swift index 9537406f..3cb846bc 100644 --- a/Project.swift +++ b/Project.swift @@ -186,15 +186,20 @@ let project = Project( "NSPrincipalClass": .string("NSApplication"), ]), sources: ["Foreman/Foreman/Sources/**"], - // No Resources folder: the menu bar icon is an SF Symbol and the - // app ships no asset catalog, so clear the names the asset-catalog - // compiler would otherwise look for. + // The only resource is the string catalog (see below); the menu bar + // icon is an SF Symbol and the app ships no asset catalog, so the + // asset-catalog name settings below stay cleared. + resources: ["Foreman/Foreman/Resources/**"], dependencies: [ .package(product: "ForemanCore"), ], + // `STRING_CATALOG_GENERATE_SYMBOLS` turns `Resources/Localizable.xcstrings` + // into type-safe `LocalizedStringResource` symbols the views reference + // (e.g. `Text(.toolbarRescan)`); there is no hand-written strings enum. settings: .settings(base: [ "ASSETCATALOG_COMPILER_APPICON_NAME": "", "ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME": "", + "STRING_CATALOG_GENERATE_SYMBOLS": "YES", ]), ), .target( From 3e4fa2443d9dc47117d092c5670bcbe8cc9073a1 Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Sun, 5 Jul 2026 22:02:44 -0400 Subject: [PATCH 2/4] Reference Foreman UI copy through generated string symbols Replace every hardcoded literal in the Foreman app views with the type-safe LocalizedStringResource symbols generated from Localizable.xcstrings (Text(.x), Button(.x), .help(.x), Text(.statusFailedReason(reason:)), etc.). The app name and the cursor-agent CLI name stay literal, and dynamic data is passed as placeholder arguments. Co-authored-by: Cursor --- Foreman/Foreman/Sources/MainWindowView.swift | 29 ++++----- Foreman/Foreman/Sources/SettingsView.swift | 60 ++++++++++--------- .../Foreman/Sources/WorkerDetailView.swift | 44 +++++++------- Foreman/Foreman/Sources/WorkerLogView.swift | 4 +- .../Foreman/Sources/WorkerOptionsView.swift | 43 +++++++------ Foreman/Foreman/Sources/WorkerRowView.swift | 20 +++---- 6 files changed, 104 insertions(+), 96 deletions(-) diff --git a/Foreman/Foreman/Sources/MainWindowView.swift b/Foreman/Foreman/Sources/MainWindowView.swift index 9e69c946..031f773f 100644 --- a/Foreman/Foreman/Sources/MainWindowView.swift +++ b/Foreman/Foreman/Sources/MainWindowView.swift @@ -21,9 +21,9 @@ struct MainWindowView: View { .id(repo.id) } else { ContentUnavailableView( - "Select a Repository", + .detailEmptyTitle, systemImage: "hammer", - description: Text("Pick a repo to inspect and control its worker."), + description: Text(.detailEmptyDescription), ) } } @@ -66,12 +66,13 @@ struct MainWindowView: View { .overlay { if session.repos.isEmpty { ContentUnavailableView { - Label("No Git Repositories", systemImage: "folder.badge.questionmark") + Label(.sidebarEmptyTitle, systemImage: "folder.badge.questionmark") } description: { Text(session.settings.resolvedScanDirectory.path) } } } + // The app name is a proper noun, so it stays a literal. .navigationTitle("Foreman") } @@ -85,10 +86,10 @@ struct MainWindowView: View { } } - private func title(for kind: RepoSection.Kind) -> String { + private func title(for kind: RepoSection.Kind) -> LocalizedStringResource { switch kind { - case .enabled: "Enabled" - case .disabled: "Disabled" + case .enabled: .sidebarSectionEnabled + case .disabled: .sidebarSectionDisabled } } @@ -132,31 +133,31 @@ struct MainWindowView: View { private var toolbarContent: some ToolbarContent { if session.isInhibitingSleep { ToolbarItem { - Label("Preventing sleep", systemImage: "moon.zzz.fill") - .help("The Mac won't idle-sleep while workers are running.") + Label(.toolbarPreventingSleep, systemImage: "moon.zzz.fill") + .help(.toolbarPreventingSleepHelp) } } ToolbarItem { Button { session.rescan() } label: { - Label("Rescan", systemImage: "arrow.clockwise") + Label(.toolbarRescan, systemImage: "arrow.clockwise") } - .help("Re-scan the development directory for repositories.") + .help(.toolbarRescanHelp) } ToolbarItem { SettingsLink { - Label("Settings", systemImage: "gearshape") + Label(.toolbarSettings, systemImage: "gearshape") } - .help("Open Foreman's settings.") + .help(.toolbarSettingsHelp) } ToolbarItem { Button { NSApplication.shared.terminate(nil) } label: { - Label("Quit", systemImage: "power") + Label(.toolbarQuit, systemImage: "power") } - .help("Quit Foreman and stop all workers.") + .help(.toolbarQuitHelp) } } } diff --git a/Foreman/Foreman/Sources/SettingsView.swift b/Foreman/Foreman/Sources/SettingsView.swift index dc87d64b..8131bf47 100644 --- a/Foreman/Foreman/Sources/SettingsView.swift +++ b/Foreman/Foreman/Sources/SettingsView.swift @@ -79,7 +79,7 @@ struct SettingsView: View { /// General preferences: whether Foreman launches at login. private struct GeneralSettingsPane: SettingsPane { - static let title = "General" + static let title = String(localized: .settingsGeneralTitle) static let icon = "gearshape" @Bindable var session: ForemanSession @@ -92,22 +92,20 @@ private struct GeneralSettingsPane: SettingsPane { var body: some View { Form { - Toggle("Launch Foreman at login", isOn: $session.startsAtLogin) - Text( - "Foreman lives in the menu bar. Turn this on to start it automatically when you log in — it will restore any enabled workers.", - ) - .font(.caption) - .foregroundStyle(.secondary) + Toggle(.settingsGeneralLaunchToggle, isOn: $session.startsAtLogin) + Text(.settingsGeneralLaunchFooter) + .font(.caption) + .foregroundStyle(.secondary) // Registered but macOS is waiting for the user to approve it. if session.loginItemNeedsApproval { LabeledContent { - Button("Open Login Items…") { + Button(.settingsGeneralOpenLoginItems) { session.openSystemSettingsLoginItems() } } label: { Label( - "Approve Foreman in System Settings to finish enabling this.", + .settingsGeneralApprovalNeeded, systemImage: "exclamationmark.triangle.fill", ) .foregroundStyle(.orange) @@ -139,7 +137,7 @@ private struct GeneralSettingsPane: SettingsPane { /// read-only here; editing happens in an explicit-commit sheet so switching /// panes can't drop a half-typed path. private struct RepositoriesSettingsPane: SettingsPane { - static let title = "Repositories" + static let title = String(localized: .settingsRepositoriesTitle) static let icon = "folder" let session: ForemanSession @@ -152,13 +150,14 @@ private struct RepositoriesSettingsPane: SettingsPane { var body: some View { Form { - LabeledContent("Scan directory") { - Text(session.settings.scanDirectory?.path ?? "~/Development (default)") + LabeledContent(.settingsRepositoriesScanDirectory) { + Text(session.settings.scanDirectory? + .path ?? String(localized: .settingsRepositoriesDefault)) .foregroundStyle(.secondary) .textSelection(.enabled) } - Button("Change…") { isEditing = true } - Text("Foreman lists the git repositories directly inside this directory.") + Button(.settingsChange) { isEditing = true } + Text(.settingsRepositoriesFooter) .font(.caption) .foregroundStyle(.secondary) } @@ -166,9 +165,9 @@ private struct RepositoriesSettingsPane: SettingsPane { .navigationTitle(Self.title) .sheet(isPresented: $isEditing) { PathEditorSheet( - title: "Scan directory", - prompt: "~/Development", - caption: "Leave empty to use the default (~/Development).", + title: String(localized: .settingsRepositoriesScanDirectory), + prompt: String(localized: .settingsRepositoriesPrompt), + caption: String(localized: .settingsRepositoriesCaption), directoryPickerStart: session.settings.resolvedScanDirectory, initialValue: session.settings.scanDirectory?.path ?? "", onSave: { session.settings.scanDirectory = $0 }, @@ -180,7 +179,7 @@ private struct RepositoriesSettingsPane: SettingsPane { /// Agent settings: which `cursor-agent` executable to run. Read-only here; /// edited via an explicit-commit sheet (see `RepositoriesSettingsPane`). private struct AgentSettingsPane: SettingsPane { - static let title = "Agent" + static let title = String(localized: .settingsAgentTitle) static let icon = "terminal" let session: ForemanSession @@ -193,15 +192,18 @@ private struct AgentSettingsPane: SettingsPane { var body: some View { Form { + // "cursor-agent" is the CLI executable name (a proper noun), so it + // stays a literal rather than a catalog entry. LabeledContent("cursor-agent") { - Text(session.settings.agentExecutable?.path ?? "Auto-detect") + Text(session.settings.agentExecutable? + .path ?? String(localized: .settingsAgentAutoDetect)) .foregroundStyle(.secondary) .textSelection(.enabled) } - Button("Change…") { isEditing = true } - Text( - "Leave empty to search: \(CursorAgentLocator.defaultSearchPaths.joined(separator: ", "))", - ) + Button(.settingsChange) { isEditing = true } + Text(.settingsAgentSearchFooter( + paths: CursorAgentLocator.defaultSearchPaths.joined(separator: ", "), + )) .font(.caption2) .foregroundStyle(.tertiary) } @@ -209,9 +211,9 @@ private struct AgentSettingsPane: SettingsPane { .navigationTitle(Self.title) .sheet(isPresented: $isEditing) { PathEditorSheet( - title: "cursor-agent executable", - prompt: "Auto-detect", - caption: "Leave empty to auto-detect from the known install locations.", + title: String(localized: .settingsAgentEditorTitle), + prompt: String(localized: .settingsAgentAutoDetect), + caption: String(localized: .settingsAgentCaption), directoryPickerStart: nil, initialValue: session.settings.agentExecutable?.path ?? "", onSave: { session.settings.agentExecutable = $0 }, @@ -264,7 +266,7 @@ private struct PathEditorSheet: View { .focused($isFocused) .onSubmit { save() } if directoryPickerStart != nil { - Button("Choose…") { choose() } + Button(.settingsChoose) { choose() } } } Text(caption) @@ -272,9 +274,9 @@ private struct PathEditorSheet: View { .foregroundStyle(.secondary) HStack { Spacer() - Button("Cancel", role: .cancel) { dismiss() } + Button(.commonCancel, role: .cancel) { dismiss() } .keyboardShortcut(.cancelAction) - Button("Save") { save() } + Button(.commonSave) { save() } .keyboardShortcut(.defaultAction) } } diff --git a/Foreman/Foreman/Sources/WorkerDetailView.swift b/Foreman/Foreman/Sources/WorkerDetailView.swift index dbc2fd08..58beb5c0 100644 --- a/Foreman/Foreman/Sources/WorkerDetailView.swift +++ b/Foreman/Foreman/Sources/WorkerDetailView.swift @@ -12,7 +12,7 @@ struct WorkerDetailView: View { Form { Section { - LabeledContent("Status") { + LabeledContent(.detailStatusLabel) { HStack(spacing: 8) { StatusDot(state: state) Text(statusText(for: state)) @@ -20,25 +20,25 @@ struct WorkerDetailView: View { } } if case let .running(pid, since) = state { - LabeledContent("PID") { + LabeledContent(.detailPidLabel) { Text(String(pid)).textSelection(.enabled) } - LabeledContent("Uptime") { + LabeledContent(.detailUptimeLabel) { Text(since, style: .relative) } } if case let .failed(reason) = state { - LabeledContent("Failure") { + LabeledContent(.detailFailureLabel) { Text(reason).foregroundStyle(.red) } } - LabeledContent("Path") { + LabeledContent(.detailPathLabel) { Text(repo.rootURL.path).textSelection(.enabled) } - Toggle("Worker enabled", isOn: $repo.isEnabled) + Toggle(.workerEnabledToggle, isOn: $repo.isEnabled) } - Section("Command") { + Section(.detailCommandHeader) { // What the next start will spawn (options apply at spawn). Text(commandPreview) .font(.system(.caption, design: .monospaced)) @@ -51,9 +51,9 @@ struct WorkerDetailView: View { WorkerLogView(url: repo.worker.logFileURL) } header: { HStack { - Text("Log") + Text(.detailLogHeader) Spacer() - Button("Open File") { + Button(.detailLogOpenFile) { NSWorkspace.shared.open(repo.worker.logFileURL) } .controlSize(.small) @@ -69,13 +69,11 @@ struct WorkerDetailView: View { repo.isFavorite.toggle() } label: { Label( - repo.isFavorite ? "Remove from Favorites" : "Add to Favorites", + repo.isFavorite ? .favoriteRemove : .favoriteAdd, systemImage: repo.isFavorite ? "star.fill" : "star", ) } - .help(repo.isFavorite - ? "Remove this repo from favorites." - : "Pin this repo to the top of its section.") + .help(repo.isFavorite ? .favoriteRemoveHelp : .favoriteAddHelp) } } } @@ -88,30 +86,30 @@ struct WorkerDetailView: View { switch repo.worker.state { case .failed: if repo.isEnabled { - Button("Retry") { + Button(.detailActionRetry) { repo.retry() } .controlSize(.small) - .help("Try starting the worker again.") + .help(.detailActionRetryHelp) } case .running: - Button("Restart") { + Button(.detailActionRestart) { repo.restart() } .controlSize(.small) - .help("Stop the worker and start it again with the saved options.") + .help(.detailActionRestartHelp) case .stopped, .stopping: EmptyView() } } - private func statusText(for state: Worker.State) -> String { + private func statusText(for state: Worker.State) -> LocalizedStringResource { switch state { - case .stopped: "Stopped" - case .running: "Running" - case .stopping(restartPending: true): "Restarting…" - case .stopping(restartPending: false): "Stopping…" - case .failed: "Failed" + case .stopped: .statusStopped + case .running: .statusRunning + case .stopping(restartPending: true): .statusRestarting + case .stopping(restartPending: false): .statusStopping + case .failed: .statusFailed } } diff --git a/Foreman/Foreman/Sources/WorkerLogView.swift b/Foreman/Foreman/Sources/WorkerLogView.swift index 58eea91f..ecba4eb7 100644 --- a/Foreman/Foreman/Sources/WorkerLogView.swift +++ b/Foreman/Foreman/Sources/WorkerLogView.swift @@ -29,7 +29,7 @@ struct WorkerLogView: View { Group { switch tail { case .noFileYet: - Text("No log yet — this worker hasn't started.") + Text(.logEmpty) .font(.callout) .foregroundStyle(.secondary) case let .content(text): @@ -74,7 +74,7 @@ struct WorkerLogView: View { latest = .noFileYet } } catch { - latest = .failed("Couldn't read the log: \(error.localizedDescription)") + latest = .failed(String(localized: .logReadFailed(message: error.localizedDescription))) } // Only write when the tail actually changed: an idle worker polls the // same content once a second, and each redundant @State write re-runs diff --git a/Foreman/Foreman/Sources/WorkerOptionsView.swift b/Foreman/Foreman/Sources/WorkerOptionsView.swift index b85e1c73..e4e4fdc4 100644 --- a/Foreman/Foreman/Sources/WorkerOptionsView.swift +++ b/Foreman/Foreman/Sources/WorkerOptionsView.swift @@ -69,37 +69,45 @@ struct WorkerOptionsView: View { var body: some View { Section { Group { - TextField("Worker name", text: $draft.displayName, prompt: Text("Host name")) + TextField( + .optionsWorkerNameLabel, + text: $draft.displayName, + prompt: Text(.optionsWorkerNamePrompt), + ) - Toggle("Pool worker (one agent at a time)", isOn: $draft.isPool) + Toggle(.optionsPoolToggle, isOn: $draft.isPool) if draft.isPool { - TextField("Pool name", text: $draft.poolName, prompt: Text("default")) + TextField( + .optionsPoolNameLabel, + text: $draft.poolName, + prompt: Text(.optionsPoolNamePrompt), + ) } TextField( - "Idle release (seconds)", + .optionsIdleReleaseLabel, value: $draft.idleReleaseTimeoutSeconds, format: .number, - prompt: Text("0 = never"), + prompt: Text(.optionsIdleReleasePrompt), ) - Toggle("Verbose startup logs", isOn: $draft.verbose) + Toggle(.optionsVerboseToggle, isOn: $draft.verbose) labelsSection } .disabled(isLocked) HStack { - Button("Reset to Defaults") { + Button(.optionsResetDefaults) { draft = Draft(.standard) } .disabled(isLocked) Spacer() - Button("Revert") { + Button(.optionsRevert) { draft = Draft(repo.options) } .disabled(isLocked || !isDirty) - Button("Save") { + Button(.commonSave) { repo.options = draft.options } .keyboardShortcut(.defaultAction) @@ -107,12 +115,10 @@ struct WorkerOptionsView: View { } .controlSize(.small) } header: { - Text("Options") + Text(.optionsHeader) } footer: { if isLocked { - Text( - "Stop the worker to edit — options apply when it starts. Restart respawns with the saved options.", - ) + Text(.optionsLockedFooter) } } } @@ -120,7 +126,7 @@ struct WorkerOptionsView: View { private var labelsSection: some View { VStack(alignment: .leading, spacing: 4) { HStack { - Text("Labels") + Text(.optionsLabelsHeader) .font(.caption) .foregroundStyle(.secondary) Spacer() @@ -130,14 +136,15 @@ struct WorkerOptionsView: View { Image(systemName: "plus") } .buttonStyle(.borderless) - .help("Add a key=value label for this worker.") + .help(.optionsLabelsAddHelp) } ForEach($draft.labels) { $label in HStack(spacing: 4) { - TextField("key", text: $label.key) - Text("=") + TextField(.optionsLabelKey, text: $label.key) + // Decorative separator between a label's key and value. + Text(verbatim: "=") .foregroundStyle(.secondary) - TextField("value", text: $label.value) + TextField(.optionsLabelValue, text: $label.value) Button { draft.labels.removeAll { $0.id == label.id } } label: { diff --git a/Foreman/Foreman/Sources/WorkerRowView.swift b/Foreman/Foreman/Sources/WorkerRowView.swift index 2df24341..0d6c2ffa 100644 --- a/Foreman/Foreman/Sources/WorkerRowView.swift +++ b/Foreman/Foreman/Sources/WorkerRowView.swift @@ -13,7 +13,7 @@ struct WorkerRowView: View { VStack(alignment: .leading, spacing: 1) { Text(repo.name) if case .failed = repo.worker.state { - Text("Failed — see detail") + Text(.rowFailed) .font(.caption) .foregroundStyle(.red) } @@ -23,12 +23,12 @@ struct WorkerRowView: View { Image(systemName: "star.fill") .font(.caption2) .foregroundStyle(.yellow) - .help("Favorite") + .help(.rowFavoriteHelp) } Spacer() - Toggle("Worker enabled", isOn: $repo.isEnabled) + Toggle(.workerEnabledToggle, isOn: $repo.isEnabled) .labelsHidden() .toggleStyle(.switch) .controlSize(.small) @@ -36,7 +36,7 @@ struct WorkerRowView: View { .padding(.vertical, 2) .contextMenu { Button( - repo.isFavorite ? "Remove from Favorites" : "Add to Favorites", + repo.isFavorite ? .favoriteRemove : .favoriteAdd, systemImage: repo.isFavorite ? "star.slash" : "star", ) { repo.isFavorite.toggle() @@ -65,13 +65,13 @@ struct StatusDot: View { } } - private var help: String { + private var help: LocalizedStringResource { switch state { - case .stopped: "Stopped" - case let .running(pid, _): "Running (pid \(pid))" - case .stopping(restartPending: true): "Restarting…" - case .stopping(restartPending: false): "Stopping…" - case let .failed(reason): "Failed: \(reason)" + case .stopped: .statusStopped + case let .running(pid, _): .statusRunningPid(pid: Int(pid)) + case .stopping(restartPending: true): .statusRestarting + case .stopping(restartPending: false): .statusStopping + case let .failed(reason): .statusFailedReason(reason: reason) } } } From 951e006050c2362ed49b0513f4da43c97cb74807 Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Sun, 5 Jul 2026 22:02:50 -0400 Subject: [PATCH 3/4] Localize ForemanCore user-facing strings via generated symbols Route worker failure reasons, the scan/save/login errors and config-load fallback, and CursorAgentLocator.NotFoundError through String(localized: .x) against ForemanCore's catalog. The en values match the current literals verbatim, so tests asserting exact reason/message text still pass. Co-authored-by: Cursor --- .../ForemanCore/Sources/CursorAgentLocator.swift | 2 +- .../ForemanCore/Sources/ForemanServices.swift | 16 +++++++++------- Foreman/ForemanCore/Sources/Worker.swift | 15 ++++++++++++--- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/Foreman/ForemanCore/Sources/CursorAgentLocator.swift b/Foreman/ForemanCore/Sources/CursorAgentLocator.swift index 2332dd03..69a28ddd 100644 --- a/Foreman/ForemanCore/Sources/CursorAgentLocator.swift +++ b/Foreman/ForemanCore/Sources/CursorAgentLocator.swift @@ -23,7 +23,7 @@ public struct CursorAgentLocator: Sendable { } public var errorDescription: String? { - "cursor-agent was not found. Checked: \(searchedPaths.joined(separator: ", "))" + String(localized: .cursorAgentNotFound(paths: searchedPaths.joined(separator: ", "))) } } diff --git a/Foreman/ForemanCore/Sources/ForemanServices.swift b/Foreman/ForemanCore/Sources/ForemanServices.swift index a4163745..fd3140f8 100644 --- a/Foreman/ForemanCore/Sources/ForemanServices.swift +++ b/Foreman/ForemanCore/Sources/ForemanServices.swift @@ -76,8 +76,8 @@ public final class ForemanServices { } catch { Self.logger.error("Couldn't update the login item: \(error)") loginItemError = newValue - ? "Couldn't turn on “Launch at login”: \(error.localizedDescription)" - : "Couldn't turn off “Launch at login”: \(error.localizedDescription)" + ? String(localized: .loginItemTurnOnFailed(error: error.localizedDescription)) + : String(localized: .loginItemTurnOffFailed(error: error.localizedDescription)) } } } @@ -154,7 +154,7 @@ public final class ForemanServices { } catch { Self.logger.error("Couldn't load configuration: \(error)") configuration = .initial - configLoadFailure = "Couldn't read saved settings — using defaults." + configLoadFailure = String(localized: .configLoadFailure) } } @@ -199,8 +199,10 @@ public final class ForemanServices { } } catch { Self.logger.error("Repo scan failed: \(error)") - issueMessage = - "Couldn't scan \(directory.path): \(error.localizedDescription)" + issueMessage = String(localized: .scanFailed( + path: directory.path, + error: error.localizedDescription, + )) } } @@ -211,7 +213,7 @@ public final class ForemanServices { /// retains a `Repo` beyond the root's life and starts it. private struct ServicesReleasedError: Error, LocalizedError { var errorDescription: String? { - "Foreman is shutting down." + String(localized: .servicesShuttingDown) } } @@ -278,7 +280,7 @@ public final class ForemanServices { try configStore.save(configuration) } catch { Self.logger.error("Couldn't save configuration: \(error)") - issueMessage = "Couldn't save settings: \(error.localizedDescription)" + issueMessage = String(localized: .saveFailed(error: error.localizedDescription)) } } } diff --git a/Foreman/ForemanCore/Sources/Worker.swift b/Foreman/ForemanCore/Sources/Worker.swift index 1aeb966e..8b93afec 100644 --- a/Foreman/ForemanCore/Sources/Worker.swift +++ b/Foreman/ForemanCore/Sources/Worker.swift @@ -190,11 +190,20 @@ public final class Worker { case .exit: // A clean self-exit (e.g. the CLI released the worker) is // a stop, not a failure. - status == 0 ? .stopped : .failed(reason: "Exited with code \(status)") + status == 0 + ? .stopped + : + .failed( + reason: String(localized: .workerExitedWithCode(code: Int(status))), + ) case .uncaughtSignal: - .failed(reason: "Terminated by signal \(status)") + .failed( + reason: String(localized: .workerTerminatedBySignal(signal: Int(status))), + ) @unknown default: - .failed(reason: "Terminated (unknown reason, status \(status))") + .failed( + reason: String(localized: .workerTerminatedUnknown(status: Int(status))), + ) } } From 9f85aa2323e29128713d14f73b0aa29abcd64967 Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Sun, 5 Jul 2026 22:02:55 -0400 Subject: [PATCH 4/4] Document Foreman's generated-symbol localization workflow Add Localization sections to the Foreman and ForemanCore docs (catalog-first keys, named placeholders, no bundle: .module) and note the STRING_CATALOG_ GENERATE_SYMBOLS wiring in the root AGENTS.md. Call out that symbol generation runs only under the Tuist/xcodebuild flow, not plain swift build. Co-authored-by: Cursor --- AGENTS.md | 4 ++-- Foreman/Foreman/AGENTS.md | 20 ++++++++++++++++++++ Foreman/Foreman/README.md | 9 +++++++++ Foreman/ForemanCore/AGENTS.md | 20 ++++++++++++++++++++ Foreman/ForemanCore/README.md | 12 ++++++++++++ 5 files changed, 63 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 4a7137f5..f8c89530 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -53,8 +53,8 @@ by `./sync-agents`. ## Targets -- **Package products** ([`Package.swift`](Package.swift)) — **StuffCore** ([`Shared/StuffCore/Sources/`](Shared/StuffCore/Sources/)), **LifecycleKit** ([`Shared/LifecycleKit/Sources/`](Shared/LifecycleKit/Sources/)), **LogKit** ([`Shared/LogKit/Sources/`](Shared/LogKit/Sources/), the logging facade), **LogViewerUI** ([`Shared/LogViewerUI/Sources/`](Shared/LogViewerUI/Sources/), the generic SwiftUI log viewer), and **SwiftDataInspector** ([`Shared/SwiftDataInspector/Sources/`](Shared/SwiftDataInspector/Sources/), the generic SwiftData browser) under [`Shared/`](Shared/); **WhereCore** / **WhereUI** / **WhereTesting** under [`Where/`](Where/); **ForemanCore** ([`Foreman/ForemanCore/Sources/`](Foreman/ForemanCore/Sources/), the only **macOS-only** package library) under [`Foreman/`](Foreman/). -- **Tuist targets** ([`Project.swift`](Project.swift)) — **Where** app ([`Where/Where/`](Where/Where/)), **RegionViewer** ([`Where/RegionViewer/`](Where/RegionViewer/), a thin standalone **Mac Catalyst** host for the WhereUI region-map developer tool — the only target with a `.macCatalyst` destination), **Foreman** ([`Foreman/Foreman/`](Foreman/Foreman/), the **native-macOS** menu bar app that runs Cursor local agent workers per repo), **StuffTestHost** ([`Shared/StuffTestHost/`](Shared/StuffTestHost/)), **WhereTests** (app tests, no host), hosted **\*Tests** bundles (**StuffCoreTests**, **LifecycleKitTests**, **LogKitTests**, **LogViewerUITests**, **SwiftDataInspectorTests**, **WhereCoreTests**, **WhereUITests**) that depend on **StuffTestHost** + **WhereTesting** + the relevant package product, and the hostless macOS bundle **ForemanCoreTests**. +- **Package products** ([`Package.swift`](Package.swift)) — **StuffCore** ([`Shared/StuffCore/Sources/`](Shared/StuffCore/Sources/)), **LifecycleKit** ([`Shared/LifecycleKit/Sources/`](Shared/LifecycleKit/Sources/)), **LogKit** ([`Shared/LogKit/Sources/`](Shared/LogKit/Sources/), the logging facade), **LogViewerUI** ([`Shared/LogViewerUI/Sources/`](Shared/LogViewerUI/Sources/), the generic SwiftUI log viewer), and **SwiftDataInspector** ([`Shared/SwiftDataInspector/Sources/`](Shared/SwiftDataInspector/Sources/), the generic SwiftData browser) under [`Shared/`](Shared/); **WhereCore** / **WhereUI** / **WhereTesting** under [`Where/`](Where/); **ForemanCore** ([`Foreman/ForemanCore/Sources/`](Foreman/ForemanCore/Sources/), the only **macOS-only** package library, which processes a generated-symbol `Resources/Localizable.xcstrings`) under [`Foreman/`](Foreman/). +- **Tuist targets** ([`Project.swift`](Project.swift)) — **Where** app ([`Where/Where/`](Where/Where/)), **RegionViewer** ([`Where/RegionViewer/`](Where/RegionViewer/), a thin standalone **Mac Catalyst** host for the WhereUI region-map developer tool — the only target with a `.macCatalyst` destination), **Foreman** ([`Foreman/Foreman/`](Foreman/Foreman/), the **native-macOS** menu bar app that runs Cursor local agent workers per repo; its user-facing copy is a generated-symbol `Resources/Localizable.xcstrings` via `STRING_CATALOG_GENERATE_SYMBOLS`), **StuffTestHost** ([`Shared/StuffTestHost/`](Shared/StuffTestHost/)), **WhereTests** (app tests, no host), hosted **\*Tests** bundles (**StuffCoreTests**, **LifecycleKitTests**, **LogKitTests**, **LogViewerUITests**, **SwiftDataInspectorTests**, **WhereCoreTests**, **WhereUITests**) that depend on **StuffTestHost** + **WhereTesting** + the relevant package product, and the hostless macOS bundle **ForemanCoreTests**. - Add SPM library targets in `Package.swift` and wire apps/tests in `Project.swift` (see existing `unitTests` helper; macOS test bundles are declared directly, like `ForemanCoreTests`). A new module also ships a root `README.md` and `AGENTS.md` — see [Per-module docs](#per-module-docs). - **CI schemes**: the workspace mixes iOS and macOS targets, which no single xcodebuild destination can build, so CI runs two explicit shared schemes — **Stuff-iOS-Tests** (all iOS bundles) and **Foreman-macOS-Tests** (Foreman app + ForemanCoreTests). New iOS test bundles must be added to the `Stuff-iOS-Tests` scheme in `Project.swift` or CI won't run them. diff --git a/Foreman/Foreman/AGENTS.md b/Foreman/Foreman/AGENTS.md index cc895ff1..60c560a1 100644 --- a/Foreman/Foreman/AGENTS.md +++ b/Foreman/Foreman/AGENTS.md @@ -27,6 +27,26 @@ formatting, global conventions) and ForemanCore's - Every previewable view ships a `#Preview` using `PreviewSupport` fixtures (temp-directory backed; never the real config or `~/Development`). +## Localization + +All user-facing copy lives in +[`Resources/Localizable.xcstrings`](Resources/Localizable.xcstrings) and is +referenced through Xcode 26's **generated symbols** (the +`STRING_CATALOG_GENERATE_SYMBOLS` build setting, set on the target in +[`Project.swift`](../../Project.swift)) — e.g. `Text(.toolbarRescan)`, +`Button(.commonSave)`, `.help(.toolbarRescanHelp)`, +`Text(.statusFailedReason(reason: …))`. There is **no** hand-written `Strings` +enum (unlike WhereUI). Add the key to the catalog first (a manual entry with a +stable dotted key like `toolbar.rescan`), then reference the generated symbol; +a missing key is a compile error. Interpolated strings use named placeholders +(`%(pid)lld`, `%(message)@`) so the symbol is a function with typed arguments. + +Proper nouns stay literal — the app name **"Foreman"** (`navigationTitle`, +`NSWindow.title`, the icon's accessibility description) and the **"cursor-agent"** +CLI name. Dynamic data (repo names, paths, pids, upstream +`error.localizedDescription`, the command preview) is passed as arguments into +placeholder symbols, never added as catalog keys. + ## Hard-won platform lessons (don't undo) - **The status item is AppKit (`NSStatusItem` + a reused `NSWindow`), not diff --git a/Foreman/Foreman/README.md b/Foreman/Foreman/README.md index 8d396d34..5ab2901c 100644 --- a/Foreman/Foreman/README.md +++ b/Foreman/Foreman/README.md @@ -79,6 +79,15 @@ small editor sheet that commits only on **Save** (Cancel or Escape discards). from the current scan directory are pruned; settings for other scan directories are kept and re-apply when you switch back. +## Localization + +All of Foreman's on-screen copy lives in +[`Resources/Localizable.xcstrings`](Resources/Localizable.xcstrings) and is +referenced through Xcode 26's type-safe generated symbols (the +`STRING_CATALOG_GENERATE_SYMBOLS` build setting) — the views hold no English +literals, so retitling a button is a one-line catalog edit. The app name and +the `cursor-agent` command stay as-is. + ## Limitations - The sleep assertion blocks *idle* sleep only (like `caffeinate -i`); closing diff --git a/Foreman/ForemanCore/AGENTS.md b/Foreman/ForemanCore/AGENTS.md index 85de2789..c0ddb01f 100644 --- a/Foreman/ForemanCore/AGENTS.md +++ b/Foreman/ForemanCore/AGENTS.md @@ -67,6 +67,26 @@ build system, formatting, and global conventions. Read that first. is the only place that spells flags). **`RepoID` everywhere** — never key by display name or raw path strings. +## Localization + +User-facing strings (worker failure reasons in `Worker`, the scan/save/login +errors and config-load fallback in `ForemanServices`, +`CursorAgentLocator.NotFoundError`) resolve through Xcode 26 **generated +symbols** against [`Sources/Resources/Localizable.xcstrings`](Sources/Resources/Localizable.xcstrings) +(a processed resource in [`Package.swift`](../../Package.swift)) — used as +`String(localized: .workerExitedWithCode(code: …))` etc. The symbol already +carries the module bundle, so there is no `bundle: .module`. Add the key to the +catalog first; interpolated strings use named placeholders (`%(code)lld`, +`%(error)@`). + +- **The `en` catalog value is the source of truth** for tests that assert exact + reason/message text (e.g. `"Exited with code 3"`, `"Couldn't read saved + settings — using defaults."`) — keep them byte-for-byte when editing. +- Symbol generation runs under both the Tuist / `xcodebuild` flow **and** a plain + `swift build` (the Swift 6.2 toolchain builds SwiftPM targets with the Swift + Build engine, which processes the catalog into + `GeneratedStringSymbols_Localizable.swift`), so the module compiles either way. + ## Testing Swift Testing in [`Tests/`](Tests), hostless on macOS (`tuist test diff --git a/Foreman/ForemanCore/README.md b/Foreman/ForemanCore/README.md index ef4e2e74..7f2ce440 100644 --- a/Foreman/ForemanCore/README.md +++ b/Foreman/ForemanCore/README.md @@ -146,6 +146,18 @@ let argv = repo.options.arguments(workerDirectory: repo.rootURL) - **`ForemanLog`** — the logging facade over LogKit: `ForemanLog.channel(_:)` with a typed `Category`, subsystem `com.stuff.foreman`. +## Localization + +User-facing strings — worker failure reasons (`Worker`), the scan/save/login +errors and config-load fallback (`ForemanServices`), and +`CursorAgentLocator.NotFoundError` — come from `Sources/Resources/Localizable.xcstrings` +via Xcode 26's generated symbols (`String(localized: .workerExitedWithCode(code:))`, +etc.). The catalog is a `.process`-ed resource, so `defaultLocalization` and the +module bundle are wired automatically — no `bundle: .module`. Symbol generation +runs under both the Tuist / `xcodebuild` flow and a plain `swift build` (the +Swift 6.2 toolchain builds SwiftPM targets with the Swift Build engine, which +generates the symbols), so the module compiles either way. + ## Contracts & limitations - `WorkerOptions.arguments(workerDirectory:)` is the single place that knows