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
7 changes: 7 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,12 @@ build/test/release commandはpackage manifest、Makefile、workflowから読む
bundle置換監視を維持する。
- release appcastは専用`appcast` branchが配信正本。mainの`appcast.xml`は空seedであり履歴を追記しない。
- native appなのでDockerを使わない。release signing/notarizationとlocal ad-hoc buildを混同しない。
- ユーザー向け文字列は必ず`L(_:)`(Sources/CmdIMESwift/Localization.swift)経由。`swift build`/`swift test`は
`Localizable.xcstrings`をコンパイルしない(Xcode専用機能)ため、`Sources/CmdIMESwift/Resources/<locale>.lproj/
Localizable.strings`を`xcstringstool compile`で事前コンパイルして正本と一緒にcommitする。文字列を追加・変更したら
`xcrun xcstringstool compile Sources/CmdIMESwift/Resources/Localizable.xcstrings --output-directory
Sources/CmdIMESwift/Resources --serialization-format text`で再生成し、`LocalizationCatalogTests`のdriftチェックを
green にしてからcommitする。`scripts/package.sh`はSPMの`<Package>_<Target>.bundle`を`Contents/Resources/`へ
手動でdittoする一文が要る — 標準の`swift build -c release`だけでは.appにリソースが同梱されない。

変更後はSwift test、build、必要ならevent tap/Accessibilityの実機確認を行う。コメントは周囲の密度と言語へ合わせる。
3 changes: 2 additions & 1 deletion apps/cmd-ime-swift/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ let package = Package(
dependencies: [
.product(name: "Sparkle", package: "Sparkle")
],
path: "Sources/CmdIMESwift"
path: "Sources/CmdIMESwift",
resources: [.process("Resources")]
),
.testTarget(
name: "CmdIMESwiftTests",
Expand Down
30 changes: 15 additions & 15 deletions apps/cmd-ime-swift/Sources/CmdIMESwift/CmdIMEApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,21 +41,21 @@ class AppDelegate: NSObject, NSApplicationDelegate, SPUUpdaterDelegate,
let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0"

menu.addItem(
withTitle: "⌘IME \(version) — Preferences...",
withTitle: String(format: L("menu.preferencesTitle"), version),
action: #selector(AppDelegate.openPreferencesSelector(_:)),
keyEquivalent: ","
)
menu.addItem(NSMenuItem.separator())
let updateItem = NSMenuItem(
title: "Check for Updates...",
title: L("menu.checkForUpdates"),
action: #selector(SPUStandardUpdaterController.checkForUpdates(_:)),
keyEquivalent: ""
)
updateItem.target = updaterController
menu.addItem(updateItem)
menu.addItem(NSMenuItem.separator())
menu.addItem(withTitle: "Restart", action: #selector(AppDelegate.restart(_:)), keyEquivalent: "")
menu.addItem(withTitle: "Quit", action: #selector(AppDelegate.quit(_:)), keyEquivalent: "q")
menu.addItem(withTitle: L("menu.restart"), action: #selector(AppDelegate.restart(_:)), keyEquivalent: "")
menu.addItem(withTitle: L("menu.quit"), action: #selector(AppDelegate.quit(_:)), keyEquivalent: "q")

// A main menu lets the Preferences window honor ⌘W / ⌘Q and text-editing keys.
NSApp.mainMenu = makeMainMenu()
Expand Down Expand Up @@ -140,7 +140,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, SPUUpdaterDelegate,
let appMenu = NSMenu()
appItem.submenu = appMenu
let quitItem = appMenu.addItem(
withTitle: "Quit ⌘IME",
withTitle: L("menu.quitApp"),
action: #selector(handleCommandQ(_:)),
keyEquivalent: "q"
)
Expand All @@ -150,26 +150,26 @@ class AppDelegate: NSObject, NSApplicationDelegate, SPUUpdaterDelegate,
// text fields (key recorder, app pickers) get the usual shortcuts.
let editItem = NSMenuItem()
mainMenu.addItem(editItem)
let editMenu = NSMenu(title: "Edit")
let editMenu = NSMenu(title: L("menu.edit"))
editItem.submenu = editMenu
editMenu.addItem(withTitle: "Cut", action: #selector(NSText.cut(_:)), keyEquivalent: "x")
editMenu.addItem(withTitle: "Copy", action: #selector(NSText.copy(_:)), keyEquivalent: "c")
editMenu.addItem(withTitle: "Paste", action: #selector(NSText.paste(_:)), keyEquivalent: "v")
editMenu.addItem(withTitle: "Select All", action: #selector(NSText.selectAll(_:)), keyEquivalent: "a")
editMenu.addItem(withTitle: L("menu.cut"), action: #selector(NSText.cut(_:)), keyEquivalent: "x")
editMenu.addItem(withTitle: L("menu.copy"), action: #selector(NSText.copy(_:)), keyEquivalent: "c")
editMenu.addItem(withTitle: L("menu.paste"), action: #selector(NSText.paste(_:)), keyEquivalent: "v")
editMenu.addItem(withTitle: L("menu.selectAll"), action: #selector(NSText.selectAll(_:)), keyEquivalent: "a")

// Window menu — ⌘W closes the focused window (the agent stays in the menu
// bar); ⌘M minimizes it.
let windowItem = NSMenuItem()
mainMenu.addItem(windowItem)
let windowMenu = NSMenu(title: "Window")
let windowMenu = NSMenu(title: L("menu.window"))
windowItem.submenu = windowMenu
windowMenu.addItem(
withTitle: "Close",
withTitle: L("menu.close"),
action: #selector(NSWindow.performClose(_:)),
keyEquivalent: "w"
)
windowMenu.addItem(
withTitle: "Minimize",
withTitle: L("menu.minimize"),
action: #selector(NSWindow.performMiniaturize(_:)),
keyEquivalent: "m"
)
Expand Down Expand Up @@ -215,8 +215,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, SPUUpdaterDelegate,

NSApp.dockTile.badgeLabel = "1"
let content = UNMutableNotificationContent()
content.title = "⌘IME の新しいバージョンがあります"
content.body = "v\(update.displayVersionString) をインストールできます"
content.title = L("notification.updateAvailableTitle")
content.body = String(format: L("notification.updateAvailableBody"), update.displayVersionString)
let request = UNNotificationRequest(
identifier: Self.updateNotificationIdentifier,
content: content,
Expand Down
10 changes: 4 additions & 6 deletions apps/cmd-ime-swift/Sources/CmdIMESwift/KeyEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -274,13 +274,11 @@ class KeyEvent: NSObject {
private func presentTapFailureAlert() {
DispatchQueue.main.async {
let alert = NSAlert()
alert.messageText = "⌘IME could not start its keyboard listener"
alert.informativeText =
"Open System Settings → Privacy & Security → Accessibility, " +
"remove ⌘IME if listed, re-add it, then restart the app."
alert.messageText = L("alert.tapFailureTitle")
alert.informativeText = L("alert.tapFailureBody")
alert.alertStyle = .warning
alert.addButton(withTitle: "Open System Settings")
alert.addButton(withTitle: "Quit")
alert.addButton(withTitle: L("alert.openSystemSettings"))
alert.addButton(withTitle: L("menu.quit"))
if alert.runModal() == .alertFirstButtonReturn,
let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility") {
NSWorkspace.shared.open(url)
Expand Down
27 changes: 27 additions & 0 deletions apps/cmd-ime-swift/Sources/CmdIMESwift/Localization.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// Localization.swift
// ⌘IME
//
// Single entry point for every user-facing string. SPM never merges a
// target's resources into `Bundle.main` (unlike an Xcode app target), so a
// bundle-less `Text("...")`/`NSLocalizedString` lookup would silently
// return the raw key in a real .app build. This resolves explicitly against
// `Bundle.module`, and returns a plain `String` rather than a
// `LocalizedStringKey` so SwiftUI displays it verbatim instead of
// re-resolving it against `Bundle.main` a second time — the same reason
// callers must use the `StringProtocol` overloads of `Text`/`Toggle`/
// `Button`/`Picker`/`Label` (not the `LocalizedStringKey` ones) when passing
// the result. Works identically under `swift build`/`swift test` and Xcode,
// since it's a plain runtime `String(localized:)` call, not dependent on
// Xcode's String Catalog symbol-generation build phase.
//

import Foundation

/// Looks up `key` in `Localizable.xcstrings` for the current locale. `key` is
/// a stable catalog identifier (e.g. "general.launchAtLogin"), not the
/// English display text, so English wording can change without touching
/// every other locale's key.
func L(_ key: String) -> String {
String(localized: String.LocalizationValue(key), bundle: .module)
}
Loading
Loading