Android Studioのローカライズ
Android Studio + Jetpack Composeでは、iOSの Localizable.xcstrings に相当するものとして、基本的には res/values/strings.xml を使ってローカライズします。
SwiftUIとの対応関係で見ると、かなり分かりやすいです。
| iOS / SwiftUI | Android / Jetpack Compose |
|---|---|
Localizable.xcstrings | res/values/strings.xml |
String(localized: "Hello") | stringResource(R.string.hello) |
Localizable.xcstrings の日本語 | res/values-ja/strings.xml |
Localizable.xcstrings の英語 | res/values-en/strings.xml |
1. 基本的な構成
例えば、
app/
└── src/
└── main/
└── res/
├── values/
│ └── strings.xml
├── values-ja/
│ └── strings.xml
└── values-en/
└── strings.xml
という構成にします。
デフォルトを英語にするなら values/strings.xml を英語にして、日本語を values-ja/strings.xml にします。
2. strings.xml
英語:
<resources>
<string name="app_name">Table Memo</string>
<string name="settings">Settings</string>
<string name="save">Save</string>
</resources>
日本語:
<resources>
<string name="app_name">テーブルメモ</string>
<string name="settings">設定</string>
<string name="save">保存</string>
</resources>
3. Jetpack Composeから使用
Composeでは stringResource() を使います。
import androidx.compose.ui.res.stringResource
Text(
text = stringResource(R.string.settings)
)
例えばボタンなら、
Button(
onClick = {
// 保存処理
}
) {
Text(stringResource(R.string.save))
}
これで端末の言語設定が日本語なら
保存
英語なら
Save
と自動的に切り替わります。
4. SwiftUIの Localizable.xcstrings と違うところ
ここは結構重要です。
iOSの Localizable.xcstrings は、最近のXcodeでは1つのファイルの中に各言語の翻訳をまとめて管理できます。
一方Androidは、基本的に言語ごとにXMLファイルを分けます。
例えば、
res/
├── values/
│ └── strings.xml
├── values-ja/
│ └── strings.xml
├── values-en/
│ └── strings.xml
├── values-ko/
│ └── strings.xml
└── values-zh-rCN/
└── strings.xml
となります。
中国語(簡体字)なら values-zh-rCN、韓国語なら values-ko という感じです。
5. 変数を含む文章
例えば、
Hello, Gacharis!
のように変数が入る文章は、%1$s などを使えます。
<string name="hello_user">Hello, %1$s!</string>
日本語:
<string name="hello_user">%1$sさん、こんにちは!</string>
Compose:
Text(
text = stringResource(
R.string.hello_user,
userName
)
)
例えば userName = "Gacharis" なら、
Gacharisさん、こんにちは!
となります。
6. Composeで特に注意したいこと
コードに直接、
Text("設定")
のように日本語を書かないことです。
ローカライズするなら、
Text(stringResource(R.string.settings))
にしておきます。
ただし、ログ出力や内部的な処理など、ユーザーに表示しない文字列まで全部 strings.xml に入れる必要はありません。
例えば、
Log.d("MyApp", "Purchase completed")
のような内部ログはそのままで大丈夫です。
7. Android Studioにも翻訳支援があります
Android Studioでは strings.xml に対して言語ごとのリソースを追加できます。
例えば、
res
→ New
→ Android Resource File
から
File name: strings.xml
Resource type: values
を作成し、Localeを指定して values-ja などを作れます。
つまり、iOSで
Localizable.xcstrings
を中心に管理するのに対し、Androidでは
strings.xml
values-ja/strings.xml
values-en/strings.xml
...
という構成で管理する、と考えると分かりやすいです。
8. 今作っているアプリなら
今回のような Jetpack Composeで作ったテーブル型メモアプリをiOS版・Android版の両方でローカライズする場合、
iOS
Localizable.xcstrings
↓
"settings" = "設定"
Android
res/values-ja/strings.xml
↓
<string name="settings">設定</string>
という対応にしておくと、SwiftUI版とCompose版で翻訳キーを揃えられるので管理しやすいです。
たとえば両方で
app_name
settings
save
cancel
delete
add_row
add_column
premium
restore_purchase
のようにキーを統一しておくと、かなり扱いやすくなります。
なお、Android には strings.xml 以外にも plurals.xml、翻訳者向けの属性、Composeの pluralStringResource() などがあります。特に「1行」「2行」のように数によって文章が変わる場合は plurals を使うのがAndroidらしい実装です。
ViewModel等でstringResource()が使用できないとき
stringResource() は Composable関数専用なので、ViewModelや通常のKotlinクラスからは直接使えません。
基本的には、ViewModelでは文字列そのものではなく「文字列ID」や「エラー種別」を返して、Composable側で stringResource() を呼ぶのがおすすめです。
1. ViewModelから R.string.xxx を返す
例えばエラーを表示したい場合です。
class MainViewModel : ViewModel() {
var errorMessageResId: Int? by mutableStateOf(null)
private set
fun doSomething() {
errorMessageResId = R.string.network_error
}
}
Composable側:
@Composable
fun MainScreen(
viewModel: MainViewModel
) {
viewModel.errorMessageResId?.let { resId ->
Text(
text = stringResource(resId)
)
}
}
これが一番シンプルです。
2. SnackbarやDialogでも同じ
例えば、
viewModel.messageResId?.let { resId ->
Snackbar(
modifier = Modifier.padding(16.dp)
) {
Text(stringResource(resId))
}
}
のようにできます。
3. 引数が必要な文字列の場合
今回の strings.xml には、
<string name="hello_user">Hello, %1$s!</string>
のような文字列もあります。
ViewModelで、
data class UiMessage(
val resId: Int,
val args: List<Any> = emptyList()
)
として、
class MainViewModel : ViewModel() {
var message: UiMessage? by mutableStateOf(null)
private set
fun showUserMessage(name: String) {
message = UiMessage(
resId = R.string.hello_user,
args = listOf(name)
)
}
}
Composable側で、
message?.let { message ->
Text(
text = stringResource(
id = message.resId,
*message.args.toTypedArray()
)
)
}
とできます。
今回作った変換スクリプトでは %@ を %1$s に変換していますので、この方式との相性も良いです。
4. もう一つの方法:Contextを使う
ViewModelに Context を持たせて、
context.getString(R.string.network_error)
とする方法もあります。
ただし、通常のAndroidアプリでは ViewModelにActivity/FragmentのContextを直接持たせるのは避けたほうがよいです。
FragmentActivityを持っているならgetString() が適切な言語の文字列を取得してくれるのでcontext.getString(R.string.network_error)が利用できてその場で完結するので超楽。
どうしてもContextが必要な処理なら AndroidViewModel で Application Contextを使う方法があります。
class MainViewModel(
application: Application
) : AndroidViewModel(application) {
fun getMessage(): String {
return getApplication<Application>()
.getString(R.string.network_error)
}
}
ただ、UIに表示する文章なら「文字列IDをViewModel → Composableで解決」する設計のほうが綺麗です。
使い分けの目安
ComposableのUI
→ stringResource()
通常のAndroid/Kotlin APIにStringを渡す
→ context.getString()
ViewModelからUIへ文字列を伝える
→ R.string.xxx / 状態を渡してComposable側で解決
5. 私なら今回のアプリではこの方法にします
今回のテーブルメモアプリのように、ViewModelでファイル操作や暗号化、購入処理などを行っていて、そこでエラーを発生させる場合は、
sealed class UiMessage {
data class StringRes(
val resId: Int
) : UiMessage()
data class StringResWithArgs(
val resId: Int,
val args: List<Any>
) : UiMessage()
}
のようにして、
_uiMessage.value =
UiMessage.StringRes(R.string.file_not_found)
View側で、
when (val message = uiMessage) {
is UiMessage.StringRes ->
stringResource(message.resId)
is UiMessage.StringResWithArgs ->
stringResource(
message.resId,
*message.args.toTypedArray()
)
}
という形にすると、ViewModelが日本語・英語などの実際の文章を一切知らなくて済みます。
つまり、
ViewModel
↓
「ファイルがない」
↓
R.string.file_not_found
↓
Composable
↓
stringResource(...)
↓
現在の言語の文章
という流れです。
これは今回の Localizable.xcstrings → strings.xml の移植方法とも非常に相性が良いです。
なお、ViewModelから R.string.xxx を返す設計自体は問題ありません。R.string.xxx はただのリソースIDなので、ViewModelがローカライズ処理そのものを行っているわけではありません。