Apple Intelligenceにロシア語を話させる方法#
Foundation Modelsは、iOS 26からApple Intelligenceを支えるのと同じオンデバイスモデルに、開発者が直接アクセスできるようにするフレームワークだ。LanguageModelSessionというSwiftの型がひとつあれば、クラウドとの往復もAPIキーも、端末外に出ていくユーザーデータもなしにテキスト生成ができる。ほぼどんなテキスト機能への答えのように聞こえる。
ただしひとつだけ問題がある。このモデルはロシア語を話さない。「話すのが下手」なのではなく、supportedLanguagesのレベルで、公式には一切話さない。
これに直面したのは、現在開発中の日記アプリLanternlyを作っているときだった。ルナという名のAIコンパニオンが付いている。このアプリの前提は、AIが完全にオンデバイスで動くこと——ログインなし、サーバーなし、日記の内容がどこにも送信されない、というものだ。このアーキテクチャにおいて、ロシア語は「あれば嬉しい」言語ではない。ロシア語話者にとって製品がそもそも成立するかどうかを分ける言語だ。以下は、実際のコードとともに、それをアーキテクチャのレベルでどう解決したかという話。
問題: Foundation Modelsにはロシア語がない#
Apple Intelligenceを支えるモデルは多言語対応として訓練されている——Apple自身の研究発表からもそれは明らかだ。しかし、ある言語が学習データのどこかに存在することと、その言語が消費者向けフレームワークで公式にサポートされているとして提供されることは別の話だ。2026年半ば時点で、Apple Intelligenceが公式に発表している言語リストは、英語・フランス語・ドイツ語・イタリア語・ポルトガル語(ブラジル)・スペイン語・日本語・韓国語・簡体字中国語をカバーし、オランダ語・スウェーデン語・トルコ語といった追加ロケールを含めても、全部で20言語ほどだ。ロシア語はそこに含まれていない。
これは生成品質の問題ではない。SystemLanguageModelが公式にロシア語をサポート対象と宣言していない以上、ロシア語での直接生成を前提にした呼び出しは静かに品質が落ちるか、そもそも動作が保証されない、という話だ。開発者フォーラムでは繰り返し話題になっている——Apple Discussionsでロシア語対応の予定を尋ねるユーザーは定期的に現れ、そのたびにどのコミュニティフォーラムでも見られるのと同じ答えが返ってくる。公式情報はなく、推測だけがある(Appleがロシアの小売市場から撤退したことが、この言語の優先度を下げたのではないか、という説も含めて)。どちらの答えも、実務上の要件を変えるわけではない——アプリはそれでもユーザーにロシア語で応答しなければならない。
エンジニアリング上重要なのは、このリストが定数ではないという点だ。Appleは、サポート言語の集合はリリースごとに変わりうるため、コードに書き込むのではなく実行時に確認するようはっきりと求めている。これはLanternlyのアーキテクチャに直接影響した——いつかAppleがロシア語を追加した日に、コードを一行も変えずに直接生成のパスへ切り替わるように書かれている。
推測しない: supportedLanguagesを実行時に確認する#
最初のアーキテクチャ上の判断は、アプリ内に「サポート済み」言語のリストを決して固定書きしないことだ。代わりに、モデルへ直接問い合わせる。
@available(iOS 26, macOS 26, *)
private static func fmSupports(_ lang: Locale.Language) -> Bool {
guard let code = lang.languageCode else { return false }
return SystemLanguageModel.default.supportedLanguages
.contains { $0.languageCode == code }
}小さな関数だが、アーキテクチャ全体の分岐点がここにかかっている——返信が短い経路(直接生成)を取るか、長い経路(往復翻訳)を取るかを決めるのはこの関数だ。すぐ隣にもう一つ実務上の課題がある。短いテキストからユーザーの言語を検出することだ。NaturalLanguageのNLLanguageRecognizerは短いフレーズでは信頼性が低く、キリル文字をブルガリア語・ウクライナ語・マケドニア語と誤認することも珍しくない。解決策は、実際に処理できる言語まで候補を絞り込み、事前確率を与えることだ。
private static func languageOf(_ text: String) -> Locale.Language {
let recognizer = NLLanguageRecognizer()
recognizer.languageConstraints = candidateLanguages() // モデルがサポートする言語 + ru + 端末の言語
recognizer.languageHints = [.russian: 0.5, .english: 0.35]
recognizer.processString(text)
if let lang = recognizer.dominantLanguage {
return Locale.Language(identifier: lang.rawValue)
}
return Locale.current.language
}この制約がなければ、ロシア語の短い挨拶がブルガリア語と分類される可能性は十分にあり、以降のロジックがすべて誤った分岐に進んでしまう。
パスA: 言語が直接サポートされている場合#
fmSupportsがtrueを返せば、流れはシンプルだ。返信言語での指示を持つLanguageModelSessionをひとつ用意し、respond(to:)を一度呼ぶだけ。英語・スペイン語・日本語など、公式にサポートされているすべての言語について、Lanternlyはまさにこのパスを使う——翻訳なし、最小の遅延で。
ロシア語については特筆すべき点がある。公式にサポートされていないにもかかわらず、Lanternlyのコードベースには、ロシア語のシステムプロンプトが逐語的な正解として丸ごと保存されている。今日どこかで直接呼び出されているからではない。AppleがsupportedLanguagesにロシア語を追加した日に、コンパニオンの語調と人格のロジックを書き直す必要がないようにするためだ。すでに用意され、条件分岐の出番を待っている。
パスB: ピボット翻訳——オンデバイスで制限を回避する#
言語が直接サポートされていない場合、二つ目のパスが動く。生成そのものは引き続きオンデバイスで行われるが、ユーザーの言語ではなく英語で行われ、入力時と出力時に翻訳が挟まる。
@available(iOS 26, macOS 26, *)
private static func pivotReply(userText: String, userLang: Locale.Language,
history: [ChatMessage]) async -> String? {
let english = Locale.Language(identifier: "en")
// 1. 入力を英語へ翻訳する——ダウンロード済みの言語パックが必要。
guard let userEN = await Translator.translate(userText, from: userLang, to: english) else {
return nil // パック未ダウンロード——誤魔化さず正直にnilを返す
}
// 2. 英語で生成する——モデルが公式にサポートする範囲内で。
guard let replyEN = await runFM(instructions: systemPromptEN, prompt: userEN) else {
return nil
}
// 3. 返信をユーザーの言語へ翻訳し直す。
return await Translator.translate(replyEN, from: english, to: userLang)
}3段階、1回ではなく3回のオンデバイス呼び出し——直接生成よりも明らかに遅い。しかしサポート対象外の言語にとっては、すべてのステップが端末を出ることなく、定型文ではなく実際に生成された意味のある返信を返す唯一の方法だ。
Lanternlyの実際のコードは、上の簡略版より少し粘り強い。メッセージ本文の翻訳は成功したが、それに付随する文脈(会話履歴)の翻訳が失敗した場合、返信全体をフォールバックのバンクへ落とすのではなく、履歴なしでモデルによる返信を行う。翻訳の部分的な失敗が会話そのものを断ち切ることはない——これは文脈の完全性と応答の生きた質感との間の意図的なトレードオフだ。
Translation framework: オンデバイスだが無条件ではない#
見落としやすい点がある。Translationもまた完全にオンデバイスのフレームワークだ(WWDC 2024、iOS 18で登場)。だが、あらゆる言語ペアが最初から同梱されているわけではない。ある言語ペアの翻訳が機能するのは、対応する言語パックがすでに端末にダウンロードされている場合に限られる——これはシステム全体で共有される、すべてのアプリ共通のリソースだ。ユーザーがシステムの翻訳機能を一度も開いたことがなく、ru↔enのパックをダウンロードしていない場合、最初のTranslationSession呼び出しはダウンロードを開始させるか(SwiftUIでは.translationTaskがシステムにダウンロードを提案させる)、アプリがそれを要求できる立場になければ単純に失敗する。
ここから導かれる実務上のルールは、ピボット翻訳のパイプラインは決して成功を黙って前提にできない、ということだ。パックがダウンロードされていなければ翻訳は失敗する——それは稀なエッジケースではなく、通常の想定される結果だ。
沈黙のフォールバックではなく、正直な診断#
このようなアーキテクチャで最もコストの高い間違いは、フォールバックがあること自体ではなく、そのフォールバックが不透明であることだ。Lanternlyの以前のバージョンの類似コードは、モデル呼び出しをtry?で包んでいた——便利ではあるが、失敗時(モデルが拒否した、翻訳が使えない、言語パックがない)に理由が黙って消え、残るのは「返信はバンクから来た」という一つの信号だけになる。実際のユーザーの端末上でそれを診断する手段はない。
現在のバージョンは、各ステップで実際の理由を記録する。
do {
let response = try await session.respond(to: prompt)
return response.content
} catch {
// try? ではない——理由を飲み込まず、実際の原因を記録する。
Diagnostics.shared.lastGenerationError = String(describing: error)
return nil
}加えて、「なぜ今ルナは自分で生成せず定型文で返しているのか」という問いにいつでも答えられる専用の診断レイヤーがある。モデルが利用可能か、言語がサポートされているか、シミュレータか実機か、直近の生成エラーは何か。これは本番環境のエンドユーザー向けではない。開発・保守の段階で、「なぜ動かないのか分からない」状態と「何を直せばいいか分かっている」状態との違いを生むものだ。
ここで一つ、プライバシーに関わる重要な点がある。診断レイヤーはメッセージの内容を一切ログに残さない——記録するのは言語コード・エラー種別・返信の出所といった技術的なメタデータだけだ。そして危機検出プロトコルに引っかかったメッセージについては、この診断が明示的に無効化される——そうしたやり取りは、端末上のデバッグログにさえ、いかなる痕跡も残してはならない。
三つのパスを並べると、次のように整理できる。
| パスA(直接) | パスB(ピボット) | バンク(フォールバック) | |
|---|---|---|---|
| 発動条件 | 言語がsupportedLanguagesに含まれる | 言語は非サポートだが翻訳は利用可能 | モデル利用不可・言語パック未ダウンロード・いずれかのステップが失敗 |
| オンデバイス呼び出し数 | 1回(生成) | 最大3回(翻訳→生成→翻訳) | 0回 |
| 必要条件 | Apple Intelligence有効、iOS 26以降 | 加えてru↔en翻訳パックのダウンロード | なし |
| 遅延 | 最小 | 明らかに大きい(生成に加え翻訳2回分) | 即時 |
| プライバシー | 100%オンデバイス | 100%オンデバイス | オンデバイス(静的テキスト) |
| 失敗した場合 | バンクへ移行 | バンクへ移行 | 失敗しない——必ず返信を返す |
このパターンから持ち帰るべきこと#
このアプローチは、AIコンパニオン付き日記アプリに限った話ではない。モデルが公式にサポートする言語の外側にいるユーザー層を持つ、あらゆるオンデバイスLLM機能に通用する一般的なパターンだ。候補を絞り込み事前確率を与えて言語を検出すること、リストをコードに固定書きせず実行時にサポート状況を確認すること、システムのローカル言語パックを一時的な橋渡しとして使うこと、そして何より、フォールバックが実際の原因を覆い隠してしまう場所で、エラーを黙って飲み込まないこと。
ロシア語は遅かれ早かれsupportedLanguagesに加わる可能性が高い——Appleは対応言語リストを継続的に拡大しており、Apple自身の研究発表によれば、基盤となるモデルの学習対象言語にはすでにロシア語が含まれている。それまでの間、ピボット翻訳は場当たり的な回避策ではなく、アプリをユーザーに対して正直であり続けさせる、機能するアーキテクチャだ。オンデバイスで意味のある返信をするか、あるいは今日それができなかった理由を診断を通じてはっきり示すか、そのどちらかを常に選び取る。



