言いたいことはそれだけか

KotlinとかAndroidとかが好きです。調べたことをメモします。٩( 'ω' )و

localeに繁體中文を設定してもvalues-zhが選択されない

今回もマニアックなトピックです。
タイトルの通り。ちょっと困ったので調べて見た。

何が起こったか

下記どちらかに当てはまる場合、

  • 端末で繁體中文を言語設定で選択している
  • アプリ独自のlocale設定でzh_HK(中国語-香港)を設定している

res 下の values-zh/strings.xml が読まれず res/strings.xml が選択された。

例えば、values/strings.xml と、

<resources>
    <string name="test">Test</string>
</resources>

values-zh/strings.xml がある場合、

<resources>
    <string name="test">測試</string>
</resources>

言語設定を繁體中文にしていても測試ではなくTestが表示される。同じ中国語(zh)ならvalues-zhが優先されると思いきや、そんなことない。
ここでvalues-zh/strings.xmlvalues-zh-rHK/strings.xml にすれば測試になってまあ当面困ることはないが釈然としない。

ちなみに確認環境はNexus 6P (OS 7.1.2) 実機。

N以降でおきてるっぽい

そういえばNからLocale周りで変更入ったな?🤔 というのを思い出した。

muumuutech.hatenablog.com

試しにEmulator (Nexus 5 / API 23) で動かして見たらvalues-zh/strings.xml が選択されて測試が表示された。 同じEmulator環境でもN系のものはだめ。(Nexus 6P / API 25とNexus 5X / API 23 で確認。)

一瞬Multi localeになった影響でEnglishがlocale listの二番目にきている影響か?と思ってEnglishを削除して見たけどだめ。 よく考えたらそもそもvalues-enを作ってないテスト環境でも再現したから影響しようがなかった。

Frameworkのコードを読んでみよう

N以降のFrameworkのコードを読んでみる。ResourcesImpl.java#320 あたりから見ていくと読みやすいかな。 この辺りのコードでbestLocaleを取ってきて設定しているっぽい。

    320     public void updateConfiguration(Configuration config, DisplayMetrics metrics,
    321                                     CompatibilityInfo compat) {


    351                 // If even after the update there are no Locales set, grab the default locales.
    352                 LocaleList locales = mConfiguration.getLocales();
    353                 if (locales.isEmpty()) {
    354                     locales = LocaleList.getDefault();
    355                     mConfiguration.setLocales(locales);
    356                 }
    357 
    358                 if ((configChanges & ActivityInfo.CONFIG_LOCALE) != 0) {
    359                     if (locales.size() > 1) {
    360                         // The LocaleList has changed. We must query the AssetManager's available
    361                         // Locales and figure out the best matching Locale in the new LocaleList.
    362                         String[] availableLocales = mAssets.getNonSystemLocales();
    363                         if (LocaleList.isPseudoLocalesOnly(availableLocales)) {
    364                             // No app defined locales, so grab the system locales.
    365                             availableLocales = mAssets.getLocales();
    366                             if (LocaleList.isPseudoLocalesOnly(availableLocales)) {
    367                                 availableLocales = null;
    368                             }
    369                         }
    370 
    371                         if (availableLocales != null) {
    372                             final Locale bestLocale = locales.getFirstMatchWithEnglishSupported(
    373                                     availableLocales);
    374                             if (bestLocale != null && bestLocale != locales.get(0)) {
    375                                 mConfiguration.setLocales(new LocaleList(bestLocale, locales));
    376                             }
    377                         }
    378                     }
    379                 }

では「何がbestなのか」を評価するためのロジックはどうなっているかというと、LocaleList.java#302 あたり。

    302     @IntRange(from=0, to=1)
    303     private static int matchScore(Locale supported, Locale desired) {
    304         if (supported.equals(desired)) {
    305             return 1;  // return early so we don't do unnecessary computation
    306         }
    307         if (!supported.getLanguage().equals(desired.getLanguage())) {
    308             return 0;
    309         }
    310         if (isPseudoLocale(supported) || isPseudoLocale(desired)) {
    311             // The locales are not the same, but the languages are the same, and one of the locales
    312             // is a pseudo-locale. So this is not a match.
    313             return 0;
    314         }
    315         final String supportedScr = getLikelyScript(supported);
    316         if (supportedScr.isEmpty()) {
    317             // If we can't guess a script, we don't know enough about the locales' language to find
    318             // if the locales match. So we fall back to old behavior of matching, which considered
    319             // locales with different regions different.
    320             final String supportedRegion = supported.getCountry();
    321             return (supportedRegion.isEmpty() ||
    322                     supportedRegion.equals(desired.getCountry()))
    323                     ? 1 : 0;
    324         }
    325         final String desiredScr = getLikelyScript(desired);
    326         // There is no match if the two locales use different scripts. This will most imporantly
    327         // take care of traditional vs simplified Chinese.
    328         return supportedScr.equals(desiredScr) ? 1 : 0;
    329     }

まず完全にLocaleが一致するか見ている。ここは一致しないはずなので次へ。
次にLonguageが一致しないか見ている。ここは一致するので次へ。
次にPseudo Localeか見ている。ここは当てはまらないので次へ。
最後ににscriptが一致するかを見ている。ISO15924で繁体字Hant と定められていて、これがいわゆる中国語の簡体字Hansとは別扱いだった。 わざわざコメントでも書いてあった。

There is no match if the two locales use different scripts. This will most imporantly take care of traditional vs simplified Chinese.

まとめ

というわけで、values-zh簡体字扱いなので、端末の設定を繁體中文(繁体字)にしても文字が違うということで選択されなかったという結論になった。スッキリ!

LocaleといえばLanguageとCountry (region) くらいしか気にしていなかったけれど、script(文字) も気にしないといけなかった。 Referenceにも当然だけどscriptのことが書いてあった。ただ、これだけだとscriptがmatchingのロジックにどう関わるかわからないんじゃないかなぁ… というわけでたまにはFrameworkのコード読むの大事!٩( ‘ω’ )و

Locale | Android Developers