0.はじめに
この記事はNTTコノキュー アドベントカレンダー2025の記事です。
こんにちは、NTTコノキューの山本です。
今回はこれまで何度かに分けてご紹介させていただいていたAndroid XRのデモアプリのローカライゼーション対応についてご紹介させていただきます。
実装にあたり今回はGeminiを使ってみました。
1.言語設定用のUIの実装

画面上にこんな感じのアイコンを設定しようかなと思います。

押したら言語を選択が出てくるダイアログが出てくると言った構成です。
LanguageToggleButton(
onClick = { showDialog = true },
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(16.dp)
)
ボタンの方はとても簡単で、適当なアイコン用のComposableのonClickにダイアログを表示切り替えのロジックを入れてあげるだけです。
TextButton(onClick = { onLanguageSelected("ja") }) {
Text("日本語")
}
TextButton(onClick = { onLanguageSelected("en") }) {
Text("English")
}
LanguageSelectionDialog(
onLanguageSelected = { languageCode ->
viewModel.changeLanguage(languageCode)
showDialog = false
},
onDismiss = {
showDialog = false
}
)
ダイアログの方も、適当なリストを作っていただきViewModelに渡してあげる形となります。
private val prefs = context.getSharedPreferences("app_settings", Context.MODE_PRIVATE)
override fun setLanguageCode(languageCode: String) {
prefs.edit { putString(KEY_LANGUAGE, languageCode) }
}
言語設定の情報はSharedPreferenceに入れておきます。
2.翻訳する
実際に翻訳するのは翻訳したい画面で実施します。
今回はこの画面を英語にしてみようと思います。
2.1 言語情報を取得する
override fun getLanguageCode(): String {
return prefs.getString(KEY_LANGUAGE, null) ?: getDeviceLanguage()
}
private fun getDeviceLanguage(): String {
return Locale.getDefault().language
}
まず、先ほど保存した言語設定を取得します。
設定されていない場合は端末の言語設定を引っ張ってくる様にしています。
2.2 実際に翻訳する
private fun loadBaseTexts(): Map {
val baseLocale = Locale.forLanguageTag("ja")
val config = Configuration(context.resources.configuration).apply {
setLocale(baseLocale)
}
val japaneseContext = context.createConfigurationContext(config)
return mapOf(
"index_text_summary" to japaneseContext.getString(R.string.index_text_summary),
"title_summary" to japaneseContext.getString(R.string.title_summary),
"desc_summary" to japaneseContext.resources.getStringArray(R.array.desc_summary).toList(),
"index_text_history" to japaneseContext.getString(R.string.index_text_history),
"title_history" to japaneseContext.getString(R.string.title_history),
"desc_history" to japaneseContext.resources.getStringArray(R.array.desc_history).toList()
)
}
(ちょっと見辛いですが)
まずベースのテキストを用意します。
アプリ内のstringファイルに日本語の本文を用意しているので
それをプロンプト用にmapへ整形してベーステキストとしています。
2.2.1 Geminiのインスタンス生成
private fun generativeModel(): GenerativeModel {
// 返却してほしいJSONの構造を定義
val trainingContentSchema = Schema.obj(
mapOf(
"index_text_summary" to Schema.string(),
"title_summary" to Schema.string(),
"desc_summary" to Schema.array(Schema.string()),
"index_text_history" to Schema.string(),
"title_history" to Schema.string(),
"desc_history" to Schema.array(Schema.string())
)
)
// 生成設定(GenerationConfig)を作成
val config = generationConfig {
responseMimeType = "application/json"
responseSchema = trainingContentSchema
}
return Firebase.ai.generativeModel(
modelName = "gemini-2.5-flash",
generationConfig = config
)
}
今回はGeminiに翻訳をしてもらいます。
素晴らしいことにGeminiのチャット機能には「JSONで返してくれい」というオーダーができてしまいます。
2.2.2 Geminiに翻訳してもらう
val prompt = "Translate the following texts into the language with ISO 639-1 code '$targetLangCode'. Please provide the translated values in the predefined JSON schema.\n\nTexts:\n$textToTranslate"
今回使用したプロンプトはこちらになります。
翻訳してもらうテキスト(textToTranslate)としては、先ほどmapへ成形したものを使用します。
flattenToString()でmapを1つの文字列にします。
val baseTexts = loadBaseTexts()
val textToTranslate = baseTexts.flattenToString()
力技でいろんなことができてしまうのでAI様様ですね。
侵略される前に仲良くなっておいた方が良いかもしれません。
// 翻訳を実行
val translatedJsonString = sendTextToGemini(prompt)
// 結果をパースしてコンテンツを生成
val translatedTexts = parseJsonToMap(translatedJsonString)
return createContentsFromTexts(translatedTexts)
あとはsendTextToGemini("送りたいメッセージ")でGeminiへ送って貰って
JSONで結果が返ってくるのでmapへ変換してもらえれば完了です。

こんな感じでばっちり英語になりました。
余談ですが、Geminiから応答返ってくるまでにそれなりに時間がかかってしまいます。
(今回だと10秒くらい)

今回作ったアプリでは応答待ちの間ローディングアニメーションを出す様にしているのですが
結構頻繁にやり取りをするアプリを作る場合は、Geminiとのやり取りの仕方や応答待ちの間のUIなど工夫が必要だと思います。
3.おわりに
今回はGeminiを使ってアプリのローカライゼーション対応をやってみたという内容でした。
Android XR向けのアプリとして実装していますが、ご覧の通り通常のAndroidアプリと実装方法については相違無いです。
汎用性が高そうに見えていて、色々なアイデアを実現できる形まで持って行く想像ができて個人的にかなりおもしろそうだと感じています。
今回の翻訳機能の場合は、AWSの様なサーバを用意する必要が無く実装が容易であったり
リアルタイムで翻訳を行うにしても、自前で容易したサーバ内で翻訳を行うよりも通信回数が少なく、リアルタイムでのやり取りが求められるチャットなどで使えそうだなぁと思いました。
今回は実用可能かの検証まで出来ていないのですが、今後この機能を活用してプロダクトを作って行くとなるとこの辺りの検証が大変そうだなぁと思いました。