設定画面に言語切り替えのスイッチを置いてほしい、という要望は、海外向けに公開しているアプリほど早く届きます。個人開発で運用している壁紙・ユーティリティ系の Android アプリでも、英語圏の方から「端末は英語のままで、このアプリだけ日本語で見たい」という声をいただいたことがありました。端末の言語設定を丸ごと切り替えてもらう、という案内では解決しない要望です。
Android にはこの「アプリ内だけ言語を切り替える」仕組みが、API 33(Android 13)で正式に入りました。Per-App Language Preferences と呼ばれる機能です。ただ、実際に組んでみると、API 33 未満の端末をどう扱うか、言語を変えた瞬間に画面がちらつく問題、設定が再起動で消える問題と、地味なつまずきが連続します。この記事は、Antigravity のエージェントに下調べと実装の叩き台を任せつつ、私が手元で詰めた実装メモです。
端末言語の上書きと、アプリ言語の切り替えは別物です
古くからある手法は、Configuration の locale を上書きして Context を作り直すやり方でした。attachBaseContext でロケールを差し替える、というコードを見たことがある方も多いと思います。これは動くのですが、二つの弱点があります。
一つは、OS がアプリの言語設定を知らないこと。システムの設定アプリにある「アプリの言語」一覧に、自分のアプリが出てきません。もう一つは、再生成のたびに自前でロケールを当て直す必要があり、ライブラリやシステムダイアログが端末言語のまま残りやすいことです。
API 33 以降の setApplicationLocales は、この設定を OS に預けます。OS がアプリ単位の言語を覚えてくれるので、設定アプリにも反映され、起動のたびに OS が正しいロケールで Context を用意してくれます。両者の違いを整理すると、次のようになります。
観点 Configuration 上書き(旧来) setApplicationLocales(API 33+ / AppCompat 後方互換)
OS への登録 されない される(設定アプリのアプリ言語に表示)
設定の永続化 自前で SharedPreferences 等に保存 OS(または AppCompat の保存先)が保持
再起動後の復元 自前で再適用 自動で復元
システムダイアログの言語 端末言語のまま残りやすい アプリ言語に追従しやすい
実装量 多い(Context 差し替え) 少ない(1 行の呼び出し+設定)
新規・既存を問わず、これから組むなら AppCompatDelegate.setApplicationLocales を中心に据えるのが私の結論です。AppCompat 1.7 系がこの API を API 33 未満にも後方移植してくれるため、最小バージョンが古くても同じコードで書けます。
対応言語を OS に宣言する locales_config
まず、アプリが対応している言語を OS に教えます。res/xml/locales_config.xml を作り、android:localeConfig でマニフェストから参照します。これを置くと、システム設定の「アプリの言語」画面に言語一覧が出るようになります。
<!-- res/xml/locales_config.xml -->
<? xml version = "1.0" encoding = "utf-8" ?>
< locale-config xmlns:android = "http://schemas.android.com/apk/res/android" >
< locale android:name = "en" />
< locale android:name = "ja" />
< locale android:name = "ko" />
< locale android:name = "zh-Hans" />
</ locale-config >
<!-- AndroidManifest.xml -->
< application
android:localeConfig = "@xml/locales_config"
... >
</ application >
ここで一つ目の落とし穴があります。locales_config.xml に並べる言語タグは、実際に values-xx フォルダが存在する言語と一致させてください。zh-Hans のような地域・文字種つきのタグは、リソースフォルダ側を values-zh-rCN ではなく values-b+zh+Hans で用意すると食い違いません。並べた言語とリソースがずれると、設定画面には出るのに選んでも切り替わらない、という分かりにくい状態になります。
後方互換のための autoStoreLocales 設定
API 33 未満でも setApplicationLocales を効かせるには、AppCompat が選んだ言語をどこに保存するかを指定します。一番手軽なのは自動保存を有効にする方法です。マニフェストに次のメタデータとサービスを追加します。
<!-- AndroidManifest.xml の <application> 内 -->
< service
android:name = "androidx.appcompat.app.AppLocalesMetadataHolderService"
android:enabled = "false"
android:exported = "false" >
< meta-data
android:name = "autoStoreLocales"
android:value = "true" />
</ service >
autoStoreLocales を true にすると、API 33 未満では AppCompat が内部の保存先に言語を覚え、起動時に自動で復元します。API 33 以降では OS が保持するので、この設定は無視されます。つまり同じコードのまま、新旧どちらの端末でも動きます。
この自動保存にはわずかなコストがあります。AppCompat は最初の Activity が立ち上がる前に保存済みロケールを読み込むため、コールドスタートにごく短い同期 I/O が乗ります。私の手元の計測では、Pixel 6a で数ミリ秒程度で、体感には影響しませんでした。自前で保存先を管理したい場合は autoStoreLocales を使わず、起動時に明示的に setApplicationLocales を呼ぶ実装に切り替えられます。
言語を切り替える 1 行と、設定画面への導線
実際の切り替えは 1 行です。ユーザーが言語を選んだら、その言語タグを LocaleListCompat に包んで渡します。
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.LocaleListCompat
fun applyAppLanguage (languageTag: String ) {
// 例: "ja" / "en" / "zh-Hans"。空文字でシステム既定に戻す
val locales = if (languageTag. isEmpty ()) {
LocaleListCompat. getEmptyLocaleList ()
} else {
LocaleListCompat. forLanguageTags (languageTag)
}
AppCompatDelegate. setApplicationLocales (locales)
}
「端末の言語に従う」へ戻す選択肢を必ず用意してください。getEmptyLocaleList() を渡すとシステム既定に戻ります。これがないと、一度日本語を選んだユーザーが端末を英語に変えてもアプリだけ日本語のまま、という取り残しが起きます。
API 33 以降では、自前の設定画面を持たずに OS の言語設定へ飛ばす導線も用意できます。Settings.ACTION_APP_LOCALE_SETTINGS のインテントで、システム側のアプリ言語画面を直接開けます。アプリ内 UI と OS 設定のどちらを主にするかは方針次第ですが、私はアプリ内に切り替えを置きつつ、OS 設定への導線も補助的に添えるようにしています。
切り替えた瞬間のちらつきと、二重再生成
setApplicationLocales を呼ぶと、Activity が再生成されて新しい言語で描き直されます。ここで二つの現象に出会いました。
一つ目は、呼び出し方によって Activity が二回再生成されることです。設定画面の中で言語を選んでそのまま recreate() も呼ぶと、API がトリガーする再生成と自前の再生成が重なります。setApplicationLocales を呼んだら、自前の recreate() は呼ばないでください。再描画は API に任せます。
二つ目は、再生成の瞬間に一瞬だけ前の言語の画面が見えることがある点です。これは設定ダイアログを閉じてから言語を当てると目立ちにくくなります。私はダイアログの dismiss を先に行い、わずかに遅延させてから setApplicationLocales を呼ぶようにして、視覚的なちらつきを抑えました。
// 言語選択ダイアログ内
fun onLanguageSelected (tag: String ) {
dismiss () // 先にダイアログを閉じる
view?. post {
applyAppLanguage (tag) // 次フレームで適用すると切り替えが滑らかになる
}
}
ライブラリやリソースがアプリ言語に追従しないとき
切り替え自体は効いているのに、一部だけ端末言語のまま、という相談が一番多いです。原因はだいたい次のどれかでした。
まず、Context を取り違えているケースです。文字列を読むときに applicationContext から取得していると、Activity のロケールではなくアプリ全体のベースを参照してしまうことがあります。表示に使う文字列は、その画面の Activity/Fragment の Context から取得してください。
次に、WebView です。WebView 内のコンテンツは Android のリソース言語とは別系統で、Accept-Language ヘッダや読み込む URL のロケールに依存します。アプリ言語に合わせたいなら、現在の AppCompatDelegate.getApplicationLocales() から言語タグを取り出し、読み込み時に明示的に渡す必要があります。
そして AdMob などの広告 SDK です。広告のクリエイティブ言語は、原則として端末の言語やユーザーの推定地域に基づくため、アプリ内言語の切り替えには追従しません。これは仕様で、無理に合わせようとするより、UI 文言(同意バナーの自前部分など)だけアプリ言語に合わせ、広告本体は SDK に委ねる、と割り切るのが現実的でした。下の表は、私が切り分けに使っているチェック順です。
追従しない箇所 主な原因 対処
一部の画面だけ旧言語 applicationContext から文字列取得 Activity/Fragment の Context から取得する
WebView の中身 リソース言語と別系統 言語タグを Accept-Language や URL に明示的に渡す
広告クリエイティブ SDK が端末言語・地域で判定 追従させず、自前 UI 文言のみ合わせる
通知の文言 通知生成時のロケール 通知作成時に現在のアプリ言語で文字列を解決する
エージェントに任せた範囲と、人が握った範囲
この実装で Antigravity のエージェントに任せたのは、locales_config.xml の雛形生成、対応言語タグとリソースフォルダの対応表づくり、そして「API 33 未満で setApplicationLocales を動かす最小構成」の下調べでした。雛形が一通り出てくるので、最初の方向決めがとても速くなります。
一方で、二重再生成のような実機でしか分からない挙動、AdMob の追従を諦めるという製品判断、そして「端末既定に戻す」選択肢を必ず置くという仕様は、人が握るべき部分でした。エージェントは正しく動くコードを書いてくれますが、ユーザーが取り残される導線かどうかは、自分のアプリの読者を思い浮かべながら決める必要があります。実機を一台、言語を切り替えながら触ってみる時間が、結局いちばん確実だと感じています。
公開しているアプリに言語切り替えを足すと、レビューやメールの言語が少しずつ変わっていくのが分かります。小さな機能ですが、届く相手が広がる手応えのある実装でした。同じように多言語のアプリを育てている方の参考になれば幸いです。