
2026/01/15 1:28
API を理解しようとする際の、耐え難い苛立ち。
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
この記事では、著者が「Translate」と呼ばれる軽量なコマンドライン翻訳ツールを構築した方法について説明しています。最初はZigで書かれていましたが、Zigの非同期関数がAppleのTranslationフレームワークと競合するため、プロジェクトはSwiftに移行されました。
Swiftでは、このツールは
AsyncParsableCommand(非同期CLIロジックを扱う)を使用し、Xcode 15.4以降が必要です。Apple の TranslationSession を利用してテキスト(例:「你好」→「Hello」)を翻訳します。著者はパッケージの swift-tools-version を 6.2 に上げ、ターゲットを .macOS(.v26) に設定し、プラットフォーム固有の翻訳モデルを使用できるようにしました。
翻訳エラーは言語モデルが欠如していることが原因であると特定されました。ツールは
LanguageAvailability().status(from:to:) でモデルの可用性を確認します。ソース言語は NaturalLanguage の NLLanguageRecognizer により自動検出され、カスタム CliError 列挙型が認識失敗、非対応ペア、または欠落モデルを処理し、ユーザーに必要な翻訳をシステム設定でインストールするよう促します。
簡体字中国語(後に繁体字)をインストールした後、ツールは期待通りに動作します。著者は Spotlight がすでに同様の翻訳機能を提供しているものの、Translate は UI を開かずにコマンドラインで迅速な翻訳を行いたい開発者やパワーユーザー向けにスクリプト可能な代替手段を提供すると述べています。
今後の更新ではエラーメッセージの改善、モデルが追加されるにつれ言語サポートの拡充、および Swift の非同期 API との統合強化が予定されています。著者はまた、
Translation フレームワークにおける言語識別子のドキュメント不足に対するフラストレーションも表明しています。本文
失業によって生まれた空白を埋める活動の一環として、最近中国語を学び始めました。
中国語学習院に入会し、何とか進めてきました。
しかし、すべてのアプリが「テキストを右クリックして Translate メニュー項目を選択」できるわけではないので、
TextEdit を起動して翻訳する手間を取らされていました。そこで、次のように書くだけで済む小さなコマンドラインツールが作れれば…
translate 你好
と打つだけで「Hello」と教えてくれる、と考えました。
実装は簡単そうです。
最初の一歩
Zig
今年は様々なプロジェクトにZigを使っていたので、Ghost(AIアシスタント)にコードを書かせ、
コンパイラへ渡すべきフラグも正しく教えてくれました。実行してみると、Dictionaryサービスではなく
Translationサービスを呼び出そうとしていました。Swift の非同期関数を Zig から直接呼ぶことはできないので、
「Swift のシムが必要だ」と言われました。
そこで Swift で書くことに決めました ― そもそも Swift を学びたいと思っていたところです。
MyCLI
マシンにはすでに LSP とフォーマッタ付きの Swift がインストールされており、
基本的なチュートリアルを追った結果、次のようになりました。
import ArgumentParser import Figlet @main struct FigletTool: ParsableCommand { @Option(help: "Specify the input") public var input: String public func run() throws { Figlet.say(self.input) } }
Package.swift
// swift-tools-version: 5.8 import PackageDescription let package = Package( name: "MyCLI", dependencies: [ .package(url: "https://github.com/apple/example-package-figlet", branch: "main"), .package(url: "https://github.com/apple/swift-argument-parser", from: "1.0.0"), ], targets: [ .executableTarget( name: "MyCLI", dependencies: [ .product(name: "Figlet", package: "example-package-figlet"), .product(name: "ArgumentParser", package: "swift-argument-parser"), ], path: "Sources" ), ] )
実行は
swift run MyCLI --input Hello
です。Figlet のコードを削除し、そこから進めました。
失敗の連続
Ghost に「API はどういう形で使うのか?」と尋ねても、
いずれも作り話のようなバージョンが返ってきました。ファイルの先頭に
import Translation
を入れてみると…
import ArgumentParser import Translation @main struct Translate: ParsableCommand { @Argument(help: "Specify the input") public var input: String public func run() async throws { print(">\t\(input)") let source = Locale.Language(identifier: "zh_CN") let target = Locale.Language(identifier: "en_US") let session = TranslationSession(installedSource: source, target: target) let response = try await session.translate(input) let result = response.targetText print(">\t\(result)") } }
プラットフォーム制限
TranslationSession.init は macOS 26 でしか使えません。Package.swift に platforms を追加したものの、.v26 が選択肢に無かったので最初の行を
// swift-tools-version: 6.2
に変更すると解決しました。
非同期処理の壁
swift run -q MyCLI 你好 を実行すると、使い方メッセージだけが出て終わります。非同期呼び出しを
Task で包んでも、タスク完了前にプログラムは終了してしまいました。DispatchSemaphore で待機させるとき折り返し動作しました。
正しい方法はコマンド自体を非同期にすることです:
import ArgumentParser import Translation @main struct Translate: AsyncParsableCommand { @Argument(help: "Specify the input") public var input: String public func run() async throws { print(">\t\(input)") let source = Locale.Language(identifier: "zh_CN") let target = Locale.Language(identifier: "en_US") let session = TranslationSession(installedSource: source, target: target) let response = try await session.translate(input) let result = response.targetText print(">\t\(result)") } }
これでコンパイルは通り、実行するとまだ
> 你好 Error: Unable to Translate
と表示されます。
言語判定とモデルの有無
NaturalLanguage を使って自動言語検出を追加しました:
import NaturalLanguage func identify_lang(_ sample: String) -> Locale.Language? { let recognizer = NLLanguageRecognizer() recognizer.processString(sample) guard let lang = recognizer.dominantLanguage else { return nil } return Locale.Language(identifier: lang.rawValue) }
さらに翻訳モデルが利用可能か確認するコード:
let availability = LanguageAvailability() let status = await availability.status(from: source, to: target) switch status { case .unsupported: print("> language pairing from \(source.languageCode) to \(target.languageCode) unsupported") case .supported: print("> language pairing from \(source.languageCode!) to \(target.languageCode!) not installed") case .installed: break @unknown default: print("Unknown status.") }
結果は
.supported で、モデルがローカルにインストールされていないことを示していました。System Settings → General → Language & Region → Translation Languages から必要なモデルをダウンロードすれば解決します。
完成したコード
import ArgumentParser import NaturalLanguage import Translation @main struct Translate: AsyncParsableCommand { @Argument(help: "Specify the input") public var input: String public func run() async throws { print(">\t\(input)") let target = Locale.Language(identifier: "en_US") guard let source = identify_lang(input) else { print("> could not identify language") throw CliError.RecognitionFailed } let availability = LanguageAvailability() let status = await availability.status(from: source, to: target) switch status { case .unsupported: print("> language pairing from \(source.languageCode) to \(target.languageCode) unsupported") throw CliError.PairingUnsupported case .supported: print("> language pairing from \(source.languageCode!) to \(target.languageCode!) not installed") print("> Go to System Settings > General > Language & Region > Translation Languages and download the models.") throw CliError.PairingNotInstalled case .installed: break @unknown default: print("Unknown status.") } let session = TranslationSession(installedSource: source, target: target) try await session.prepareTranslation() let response = try await session.translate(input) let result = response.targetText print(">\t\(result)") } } func identify_lang(_ sample: String) -> Locale.Language? { let recognizer = NLLanguageRecognizer() recognizer.processString(sample) guard let lang = recognizer.dominantLanguage else { return nil } return Locale.Language(identifier: lang.rawValue) } enum CliError: Error { case RecognitionFailed case PairingUnsupported case PairingNotInstalled }
リリースビルドして
/usr/local/bin にインストールすれば、以下のように動作します:
$ translate 你好 > 你好 > Hello
ポイントまとめ
- Spotlight が既に同機能を持っています –
を押して「Translate」を選べば、モデルをインストールせずにすぐ結果が得られます。⌘+Space - Swift の非同期/待機はコマンドラインツールで
を使う必要があります。AsyncParsableCommand - 翻訳モデルは右クリックメニューとは別物です – まだインストールされていない場合は System Settings からダウンロードしてください。
最後に
言語識別子の完全なリストは存在しません。Apple は「en-US、es-419、zh-Hant-TW のような Unicode 言語識別子」としか示しておらず、
開発者が BCP‑47 タグを使用し、API に対してテストする責任があります。