ローカライズ方法

Android Studioのローカライズ

Android Studio + Jetpack Composeでは、iOSの Localizable.xcstrings に相当するものとして、基本的には res/values/strings.xml を使ってローカライズします。

SwiftUIとの対応関係で見ると、かなり分かりやすいです。

iOS / SwiftUIAndroid / Jetpack Compose
Localizable.xcstringsres/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がローカライズ処理そのものを行っているわけではありません。

タイトルとURLをコピーしました