【Flutter】eazy_localizationを使うとDateFormatが変わってしまう

# 背景

モバイルアプリではよく考慮すべき事項として多言語対応というものがあります。
Flutterの場合、いくつか手段があるのですが以下のパッケージは人気があります。

https://pub.dev/packages/easy_localization

しかし、こちらのパッケージを用いると以下のようにDateFormatに影響が出てしまいます

```dart
final today = DateTime('2024-2-28');
// easy_localization適用前
print(DateFormat('d').format(today));
// -> 28

// 適用後
print(DateFormat('d').format(today));
// -> 28日
```

日付関連がよく絡むパッケージで言うと、syncfusion_flutter_calendarのようなカレンダー系のパッケージではこういった影響を受けやすいかと思います。

https://pub.dev/packages/syncfusion_flutter_calendar

この不具合の対処中にissueを起票してしまいましたが、このような影響が発生します

| 1. デバイスの言語：英語, Localeは日本語 | 2. 日本語を追加 | 3. システム言語を日本語に設定 | 4. 問題のUI  |
|--------|--------|--------|--------|
|<img src="https://github.com/syncfusion/flutter-widgets/assets/67848399/1af05e03-3dab-4006-b1bb-6a153d79a0c8" > | <img src="https://github.com/syncfusion/flutter-widgets/assets/67848399/522a62a3-0577-44fd-93ea-c92ab80dec84"> | <img src="https://github.com/syncfusion/flutter-widgets/assets/67848399/0781e472-3ab7-4f9e-84c5-fa23b02442c6"> | <img src="https://github.com/syncfusion/flutter-widgets/assets/67848399/e3203521-a1f9-4048-a716-98b326131a87"> | 


# 原因

根本原因として、DateFormatクラスはIntlで設定されているlocation設定に依存したフォーマットを選択します。どのようなフォーマットが選択されるかは言語別でこちらの箇所に定義されています。

https://github.com/dart-lang/i18n/blob/main/pkgs/intl/lib/src/data/dates/patterns/ja.json


また、`eazy_localization`を用いる際は必ず以下のような初期化処理が必要なはずです。
```dart
await EasyLocalization.ensureInitialized();
```

上記メソッドの中身を見ていきましょう。`ensureInitialized`の中で、以下のようなメソッドが呼ばれています。

```dart
  static Future<void> initEasyLocation() async {
    final preferences = await SharedPreferences.getInstance();
    final strLocale = preferences.getString('locale');
    _savedLocale = strLocale?.toLocale();
    final foundPlatformLocale = await findSystemLocale();
    _deviceLocale = foundPlatformLocale.toLocale();
    EasyLocalization.logger.debug('Localization initialized');
  }
```

具体的には上記で登場している`findSystemLocale()`でIntlのLocale(systemLocale)が上書きされていて、それをeazy_localizationの_deviceLocaleにもセットしているようですね。

```dart
Future<String> findSystemLocale() {
  try {
    Intl.systemLocale = Intl.canonicalizedLocale(Platform.localeName);
  } catch (e) {
    return Future.value(Intl.systemLocale);
  }
  return Future.value(Intl.systemLocale);
}
```

Intlパッケージはデフォルトの言語が`en_US`なので、Intlの`defaultLocale`もしくは`systemLocale`を更新しない限り今回のタイトルのような影響は受けません。日本語設定特有の問題でしたね。

# どう対処するか
`eazy_locatization`で上書きされてしまい、Intlの言語設定が日本語になる以上、それを上書きして戻すしかありません。任意の箇所で以下を呼び出してあげれば今回の課題は解消します。

```dart
Intl.defaultLocale = 'en_US';
```

# もうちょい深掘り

では、IntlはどうやってLocaleを選択しているか？についてみていきましょう。
こちらがDateFormatに関する処理です
```dart
DateFormat([String? newPattern, String? locale])
      : _locale = helpers.verifiedLocale(locale, localeExists, null)! {
    // TODO(alanknight): It should be possible to specify multiple skeletons eg
    // date, time, timezone all separately. Adding many or named parameters to
    // the constructor seems awkward, especially with the possibility of
    // confusion with the locale. A 'fluent' interface with cascading on an
    // instance might work better? A list of patterns is also possible.
    addPattern(newPattern);
  }
```

ここの`verifiedLocale`は以下のようになっています。

```dart
String? verifiedLocale(String? newLocale, bool Function(String) localeExists,
    String? Function(String)? onFailure) {
// TODO(alanknight): Previously we kept a single verified locale on the Intl
// object, but with different verification for different uses, that's more
// difficult. As a result, we call this more often. Consider keeping
// verified locales for each purpose if it turns out to be a performance
// issue.
  if (newLocale == null) {
    return verifiedLocale(
        global_state.getCurrentLocale(), localeExists, onFailure);
  }
  if (localeExists(newLocale)) {
    return newLocale;
  }
  for (var each in [
    helpers.canonicalizedLocale(newLocale),
    helpers.shortLocale(newLocale),
    'fallback'
  ]) {
    if (localeExists(each)) {
      return each;
    }
  }
  return (onFailure ?? _throwLocaleError)(newLocale);
}
```

DateFormatのときにlocaleを指定しない（できない）場合、以下の処理を通ります。
```dart
  if (newLocale == null) {
    return verifiedLocale(
        global_state.getCurrentLocale(), localeExists, onFailure);
  }
```

そのため、`getCurrentLocale`を見てみましょう。`global_state.dart`と言うファイル内に存在していてファイルも小さいため全体記載します。

### global_state.dart
```dart
import 'dart:async';

String systemLocale = 'en_US';

String? _defaultLocale;

set defaultLocale(String? newLocale) {
  _defaultLocale = newLocale;
}

String? get defaultLocale {
  var zoneLocale = Zone.current[#Intl.locale] as String?;
  return zoneLocale ?? _defaultLocale;
}

String getCurrentLocale() {
  defaultLocale ??= systemLocale;
  return defaultLocale!;
}
```

答えに辿り着きました。
`defaultLocale ??= systemLocale;`となっていましたね。
`eazy_localization`パッケージでは`systemLocale`を更新していたため、特段`defaultLocale`を更新していなければ自ずと`systemLocale`が採用される状態だったのですね。

なので、あまり意識していない場合は基本的にlocalizationしたとてDateFormatは`en_US`の状態を使っていたアプリケーションも多かったのではないでしょうか？

おさらいですが、これを回避するためにはdefaultLocaleを更新してあげればそちらが優先されるようなので
以下のように更新してあげれば好きなLocaleでDateFormatを使うことができそうですね。

```Dart
Intl.defaultLocale = 'en_US';
```

## 追記

ただ、根本的に日本語フォーマットで「日」をつけなくても良くない？ってことでissueを立ててみました。
よければコメントやスタンプをお願いします。

https://github.com/dart-lang/i18n/issues/804


## 後日談

先ほど提示したjsonについても、以下のリポジトリから生成しているようでした。
そのため、これを変えるのは結構きつい。
現状は「日本語」というTierS言語だから仕方ないと片付けることとします。

https://st.unicode.org/cldr-apps/v#/ja/Gregorian/124541076119fb83

https://github.com/unicode-org/cldr


ちなみに、私は直接的に`defaultLocale`を更新するようにしましたが、以下のようにformatのLocaleをセットできる関数もあるみたいです。
これをEazyLocalozationの初期化処理より後に呼ぶのも良さそうですね！

```dart
initializeDateFormatting('ja_JP');
```