海外展開を視野に入れるなら文字列の扱いを最初から考えようという話

# 前置き

なろう系の小説タイトルみたいに長いタイトルですが、そういう話です

# 想定読者

主に今から海外展開も視野に入れたアプリを作るぞって人
（Android開発をベースに話しますが、根本的な考え方はどのアプリでも通ずると思います）

# 海外展開を視野に入れるときに注意すること

単純で「文字列を外部化しよう」と「文字列が翻訳される事前提で扱おう」という話です
これを想定してない文字列処理があちこちになると、急に海外展開するってなった時に地獄を見ます

「いや、俺のプロジェクトは日本にしか展開しないって経営者や社長が言ってた」といっても
あいつら、急に海外展開とか欲深い事を言い出すんで
商業アプリとかは海外展開することを前提で最初から設計したほうがいいです
ちょっとめんどくさいですが、最初からやっておけば後で地獄をあまり見ずに済みます

# お前の説明なんて見たくない

https://developer.android.com/guide/topics/resources/string-resource?hl=ja

じゃあドキュメントでも眺めるんだな

# とりあえずプロジェクトを作ろう

Android開発をベースに話します
AndroidStudioから、新規プロジェクト作成で「Empty Activity」で新規プロジェクトを作成
パッケージ名とかプロジェクト名はなんでもいいんですが、自分は

パッケージ名：com.stringtest.stringtest
プロジェクト名：StringTest

で作りました

とりあえず作ってビルドすればHelloWorldが表示されるActivityができるはず

# activity_main.xmlをとりあえず書き換える

~~~ activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/text_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="こんにちは世界！"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
~~~

MainActivity.kt側でidを元にTextViewを引っ張れるようにして、こいつに今後の出力結果を載せます
Hello Worldを日本人らしく「こんにちは世界」とかに変えときます

たしかに、海外展開も視野にいれず、プログラムの状態によって文字列の内容が変化しなければ
XMLに直書きでも問題ないのですが…

# 「こんにちは世界」ではなく、「こんにちはプリンセス！」とか表示する必要になった

たぶん、プリンセス以外にも、いろんな名前が飛んでくると思うので
XML直書きでは対応できませんね

手っ取り早い修正は、MainAcivity.ktの内容を下記に修正することです

~~~ MainActivity.kt
package com.stringtest.stringtest

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.TextView

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val userName = "プリンセス"
        val textview : TextView = findViewById(R.id.text_view) as TextView
        textview.text = "こんにちは" + userName
    }
}
~~~

こうする事で、万が一、プリンセスじゃなくて別の人の名前になっても
userNameの値を元に表示を切り替える事ができます

まぁ、日本で展開するだけなら、これで終わるんですが

# アメリカで展開することになったよ！

となると
日本版：こんにちはプリンセス
アメリカ版：Hello プリンセス

って表示しないといけなくなります
手っ取り早く考えられる手法としては
そのアプリがどの国でリリースしたかを保持するIDを持っておいて
それを元に文字列構築処理を考える

~~~ MainActivity.kt
class MainActivity : AppCompatActivity() {

    val LANGAGE_CODE_JP = 1
    val LANGAGE_CODE_EN = 2
    val langageCode : Int = LANGAGE_CODE_EN

    @SuppressLint("SetTextI18n")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val userName = "プリンセス"
        val textview : TextView = findViewById(R.id.text_view) as TextView

        when( langageCode )
        {
            LANGAGE_CODE_JP -> { textview.text = "こんにちは" + userName }
            LANGAGE_CODE_EN -> { textview.text = "Hello " + userName }
        }
    }
}
~~~

冷静に考えてみてください
今は国の数も少ないし、文字列処理も１か所だけなのでいいんですけど、
これがアメリカだけじゃなくて、中国、韓国、台湾、ロシアとかに数増えると
その分だけ、langageCodeに対応した処理を書かないといけないし、
文字列を使う場所が増えると、それでまた、こんな処理が増えます
プログラムの見通しが悪くなりますね
考えるだけで吐き気がして鬱病が悪化しそうになります

しかも、一番最悪なのは、ソースコードに直に文字列書いてるんで、
翻訳するときに、翻訳担当にソースコードと、処理が正常に動くようにソースの修正まで
要求することになるんですよね
普通に考えて翻訳を担当する人はプログラムに精通してないので無理があります

# せめて文字列外部化しましょう

Androidでは、そういう問題に一応対応できるように文字列を外部に吐き出す機能があります

プロジェクトの「app/res/value」フォルダにある「strings.xml」というファイルがあり
ここに文字列を定義することで、プログラムからstrings.xmlに定義した文字列を引っ張れます
試しに書いてみましょう

~~~ strings.xml
<resources>
    <string name="app_name">StringTest</string>
    <string name="hello_jp">こんにちは</string>
    <string name="hello_en">Hello </string>
</resources>
~~~

MainActivity.kt　を以下に修正

~~~ MainActivity.kt
class MainActivity : AppCompatActivity() {

    val LANGAGE_CODE_JP = 0
    val LANGAGE_CODE_EN = 1
    val langageCode : Int = LANGAGE_CODE_EN

    @SuppressLint("SetTextI18n")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val userName = "プリンセス"
        val textview : TextView = findViewById(R.id.text_view) as TextView

        when( langageCode )
        {
            LANGAGE_CODE_JP -> { textview.text = getString(R.string.hello_jp) + userName }
            LANGAGE_CODE_EN -> { textview.text = getString(R.string.hello_en)  + userName }
        }

    }
}
~~~

こうする事で、翻訳担当に渡すファイルはstrings.xmlだけになるので
翻訳担当にソースコードに触れさせずに翻訳を依頼することができます

しかし、国が増えるたびに冗長なlangageCode判定が要求されるという問題点は解決していません

# 冗長な判定をしたくない

XMLの記法でカバーしてみましょう

ドキュメントを見ると、こういう事ができるらしいです

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2740385/0cf5cde2-5b52-06dd-15fd-c993e434bb0a.png)

これを利用してみましょう

~~~ strings.xml
<resources>
    <string name="app_name">StringTest</string>
    <string-array name="hello">
        <item>こんにちは</item>
        <item>Hello</item>
    </string-array>
</resources>
~~~

~~~ MainActivity.kt
class MainActivity : AppCompatActivity() {

    val LANGAGE_CODE_JP = 0
    val LANGAGE_CODE_EN = 1
    val langageCode : Int = LANGAGE_CODE_EN

    @SuppressLint("SetTextI18n")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val userName = "プリンセス"
        val textview : TextView = findViewById(R.id.text_view) as TextView
        val array = resources.getStringArray(R.array.hello)
        textview.text = array.get(langageCode) + userName

    }
}
~~~

こうすることで、たとえ国が増えたり、文字列の使用箇所が増えても必要最小の実装で対応できます
問題点は、XML側のitemの順番がずれたりすると、意図しない文字列が表示されたり
index処理を誤ると配列外参照などを起こしてエラーになるので、そこに気を付けないといけない

# 複雑な文字結合処理で生じる問題

例えば
「プリンセスは薬草を１つ使った」という文字列を表示する

使用者はプリンセス以外になるかもしれないし、
使うアイテムは薬草以外になるかもしれないし、
使う個数は１個とは限らないかもしれないし、
それを表示しようとすると、以下のような処理になる

~~~ MainActivity.kt
        override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val userName = "プリンセス"
        val itemName = "薬草"
        val itemNum = 1

        val textview : TextView = findViewById(R.id.text_view) as TextView
        textview.text = userName + "は" + itemName + "を" + Integer.toString(itemNum) +"個使った"
    }
~~~

ここで使われてる日本語部分を、愚直に外部化してみよう

~~~ strings.xml
<resources>
    <string name="app_name">StringTest</string>
    <string name="ha">は</string>
    <string name="wo">を</string>
    <string name="use">個使った</string>
</resources>
~~~

さあ、あなたが仮に翻訳担当になったとして
こんなstring.xmlの翻訳を依頼されたと考えてほしい

お前理解できるか？

# ここでFormat処理の出番ではある

上記問題を解決するために、修正を試みる

~~~ strings.xml
<resources>
    <string name="app_name">StringTest</string>
    <string name="player_use_item">%sは%sを%d個使った</string>
</resources>
~~~

~~~ MainActivity.kt
        override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val userName = "プリンセス"
        val itemName = "薬草"
        val itemNum = 1

        val textview : TextView = findViewById(R.id.text_view) as TextView

        textview.text = String.format(getString(R.string.player_use_item)
        ,userName , itemName , itemNum )

    }
~~~

こうする事により、翻訳は%sや%dの意味するものは分からないとしても
日本語１文字とかの翻訳をするより、だいぶマシな状態で翻訳が可能になる

これでだいたい解決できると思われるが、嫌な落とし穴があるんですよね

# 単語の位置が入れ替わる！

次は
「プリンセスは姫子に薬草を使った」
という文章で考えてみる

日本語だと、こうすればいいだろう

~~~ strings.xml
<resources>
    <string name="app_name">StringTest</string>
    <string-array name="player_use_item_on_other">
        <item>%sは%sに%sを使った</item>
    </string-array>
</resources>
~~~

~~~ MainActivity.kt
class MainActivity : AppCompatActivity() {

    val LANGAGE_CODE_JP = 0
    val LANGAGE_CODE_EN = 1
    val langageCode : Int = LANGAGE_CODE_JP

    @SuppressLint("SetTextI18n")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val userName = "プリンセス"
        val targetName = "姫子"
        val itemName = "薬草"

        val textview : TextView = findViewById(R.id.text_view) as TextView

        val array = resources.getStringArray(R.array.player_use_item_on_other)
        textview.text = String.format(array.get(langageCode)
        ,userName , targetName , itemName )

    }
}
~~~

※）langageCode の値を「LANGAGE_CODE_JP」にすることをお忘れなく

こうする事で、「プリンセスは姫子に薬草を使った」という表示が問題なくされるはず

さあ、これを英語にしてみましょう

日本語：「プリンセスは姫子に薬草を使った」
英語：「The Princess used herbs on Himeko.」

そう、英語だと、「薬草」と「姫子」の単語の位置が入れ替わっているのだ

これを何も考えずに英語対応してみる

~~~ strings.xml
<resources>
    <string name="app_name">StringTest</string>
    <string-array name="player_use_item_on_other">
        <item>%sは%sに%sを使った</item>
        <item>The %s used %s on %s.</item>
    </string-array>
</resources>
~~~

~~~ MainActivity.kt
    val LANGAGE_CODE_JP = 0
    val LANGAGE_CODE_EN = 1
    val langageCode : Int = LANGAGE_CODE_EN
~~~

結果：The プリンセス used 姫子 on 薬草.

なんか姫子ってアイテムを薬草って名前のプレイヤーに使ったという事になってしまう

あーめんどくせぇな海外対応。鬱病が悪化しそうだわ

# formatを使わずにreplace関数を使いましょう

そもそも、%sとか%dとかいうのが、翻訳担当には意味不明だと思うので、別の文字に変えましょう

~~~ strings.xml
<resources>
    <string name="app_name">StringTest</string>
    <string-array name="player_use_item_on_other">
        <item>{user}は{target}に{itemName}を使った</item>
        <item>The {user} used {itemName} on {target}.</item>
    </string-array>
</resources>
~~~

これなら、%sや%dよりは理解しやすく、また指定した文字列を置換するという処理で対応できるはずです

~~~ MainActivity.kt
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val userName = "プリンセス"
        val targetName = "姫子"
        val itemName = "薬草"

        val textview : TextView = findViewById(R.id.text_view) as TextView

        val array = resources.getStringArray(R.array.player_use_item_on_other)
        var message = array.get(langageCode)
        message = message.replace("{user}" , userName )
        message = message.replace("{target}",targetName)
        message = message.replace("{itemName}" , itemName )
        textview.text = message
    }
~~~

これで単語が入れ替わっても対応できます
たぶん、これが最適解だと思います

# 複数形による単語変化への対応

まだ厄介なのが残ってます
英語だと、対象が１つか複数かで、単語に「ｓ」がついたりするので、その対応も必要です

Androidのドキュメントを見ると、「数量文字列（複数形）」で対応するらしいので、実装してみます


https://developer.android.com/guide/topics/resources/string-resource?hl=ja

~~~ strings.xml
<resources>
    <string name="app_name">StringTest</string>
    <plurals name="numberOfHarbsAvailable">
        <item quantity="one">The {user} used {count} herb.</item>
        <item quantity="other">The {user} used {count} herbs.</item>
    </plurals>
</resources>
~~~

~~~ MainActivity.kt
class MainActivity : AppCompatActivity() {

    @SuppressLint("SetTextI18n")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val userName = "プリンセス"
        val useCount = 1

        val textview : TextView = findViewById(R.id.text_view) as TextView

        var message = resources.getQuantityString(R.plurals.numberOfHarbsAvailable, useCount )
        message = message.replace("{user}",userName)
        message = message.replace("{count}", Integer.toString(useCount))
        textview.text = message

    }
}
~~~

「なんでや！useCountを１にしても単数形にならないぞ！」って思ったら
実機の端末の言語設定が「日本語」だと、常時otherを返してくるらしい

https://qiita.com/mstssk/items/6697db6947918e58593b

こんなのわかるわけねーだろｗｗｗｗｗｗｗｗｗｗｗｗｗｗｗｗｗｗｗｗｗｗｗ
よく気づいたなｗｗｗｗｗｗｗｗｗｗｗｗｗｗｗ
おハーブ生えますわ～🌿

# 実際の運用にあたって

翻訳担当にXML直接渡すと、Indexのズレとか起きると思うので、
文字列はエクセル等で管理したほうがいいと思います

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2740385/9100142f-b1a5-1248-982e-4cffd5c9cf47.png)


こんな感じのエクセルで各言語の文字列を管理し、翻訳担当にはこれを渡す

このエクセルを元にプログラマがC#かPythonか何使ってもいいけど
Androidなら、strings.xmlを自動生成する仕組みを作るのが事故率が少なくなると思います

一応、PowerShellで作ってみました

https://qiita.com/princess114514/items/c73682b01c8c70fe8c41

# おまけ

## こんな記事書いた理由

後悔の念から

## 特に個別に文字列管理したほうがいいもの（ゲーム編）

・NPCの名前
・UIに使う文字列
・アイテム名
・アイテムの説明文
・自分やNPCや敵が使う技の名前
・敵の名前
・そのゲームに度々登場する固有名詞