背景
モバイルアプリではよく考慮すべき事項として多言語対応というものがあります。
Flutterの場合、いくつか手段があるのですが以下のパッケージは人気があります。
しかし、こちらのパッケージを用いると以下のようにDateFormatに影響が出てしまいます
final today = DateTime('2024-2-28');
// easy_localization適用前
print(DateFormat('d').format(today));
// -> 28
// 適用後
print(DateFormat('d').format(today));
// -> 28日
日付関連がよく絡むパッケージで言うと、syncfusion_flutter_calendarのようなカレンダー系のパッケージではこういった影響を受けやすいかと思います。
この不具合の対処中にissueを起票してしまいましたが、このような影響が発生します
1. デバイスの言語:英語, Localeは日本語 | 2. 日本語を追加 | 3. システム言語を日本語に設定 | 4. 問題のUI |
---|---|---|---|
原因
根本原因として、DateFormatクラスはIntlで設定されているlocation設定に依存したフォーマットを選択します。どのようなフォーマットが選択されるかは言語別でこちらの箇所に定義されています。
また、eazy_localization
を用いる際は必ず以下のような初期化処理が必要なはずです。
await EasyLocalization.ensureInitialized();
上記メソッドの中身を見ていきましょう。ensureInitialized
の中で、以下のようなメソッドが呼ばれています。
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にもセットしているようですね。
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の言語設定が日本語になる以上、それを上書きして戻すしかありません。任意の箇所で以下を呼び出してあげれば今回の課題は解消します。
Intl.defaultLocale = 'en_US';
もうちょい深掘り
では、IntlはどうやってLocaleを選択しているか?についてみていきましょう。
こちらがDateFormatに関する処理です
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
は以下のようになっています。
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を指定しない(できない)場合、以下の処理を通ります。
if (newLocale == null) {
return verifiedLocale(
global_state.getCurrentLocale(), localeExists, onFailure);
}
そのため、getCurrentLocale
を見てみましょう。global_state.dart
と言うファイル内に存在していてファイルも小さいため全体記載します。
global_state.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を使うことができそうですね。
Intl.defaultLocale = 'en_US';
追記
ただ、根本的に日本語フォーマットで「日」をつけなくても良くない?ってことでissueを立ててみました。
よければコメントやスタンプをお願いします。
後日談
先ほど提示したjsonについても、以下のリポジトリから生成しているようでした。
そのため、これを変えるのは結構きつい。
現状は「日本語」というTierS言語だから仕方ないと片付けることとします。
ちなみに、私は直接的にdefaultLocale
を更新するようにしましたが、以下のようにformatのLocaleをセットできる関数もあるみたいです。
これをEazyLocalozationの初期化処理より後に呼ぶのも良さそうですね!
initializeDateFormatting('ja_JP');