
# MessageSource

`MessageSource` は、メッセージコードとロケールを、引数が埋め込まれたその言語のテキストに変換します。

## 使う理由

ユーザーが読み、翻訳が必要なテキストはすべてこれを通します。ラベル、メール、エラーメッセージなどです。`MessageSource` を Bean に注入するか、それ自身もメッセージソースであるコンテキストの `getMessage` を呼びます。

実装:

- **`ReloadableResourceBundleMessageSource`**: 言語ごとに一つの `.properties` ファイルを読み、変更されたときに読み直すこともできます。通常はこれを使います。
- **`StaticMessageSource`**: コードで追加したメッセージを保持します。テストや小さなプログラムで使います。
- **`MessageSourceAccessor`**: デフォルトのロケールを覚えておくラッパーで、検索のたびにロケールを渡す必要がなくなります。
- **`MessageFormat`**: テキストの `{0}` 構文を処理するフォーマッターです。メッセージソースから来ないパターンを整形するときは直接呼びます。

Spring Boot を使わない場合は、`messageSource` という名前の Bean を宣言します。コンテキストは自身の `getMessage` をこれに委譲します。子コンテキストでは、親のメッセージソースにフォールバックします。

## 例

```properties
# lib/resources/i18n/messages.properties
greeting=Hello {0}!
cart.items=You have {0,choice,0#no items|1#one item|1<{0} items}.
# lib/resources/i18n/messages_de.properties
greeting=Hallo {0}!
```

```dart
import 'package:spring_core_dart/spring_core_dart.dart';

@Configuration()
class I18nConfig {
  // i18n/messages_<locale>.properties、次に i18n/messages.properties の順に探す。
  @Bean()
  MessageSource messageSource() => ReloadableResourceBundleMessageSource(
        basenames: ['resources:i18n/messages'],
        // 変更されたファイルを最大 10 秒ごとに読み直す。
        cacheSeconds: 10,
      )
        // 要求したロケールにメッセージがなければ英語を使う。
        ..defaultLocale = 'en'
        // マシンのシステムロケールに関係なく、どこでも同じ結果にする。
        ..fallbackToSystemLocale = false;
}

@Service()
class Greeter {
  Greeter(this.messages);
  final MessageSource messages;

  // de_AT 専用のファイルはないので messages_de.properties が使われる。
  String greet(String name) =>
      messages.getMessage('greeting', args: [name], locale: 'de_AT');

  // デフォルトのテキストがあれば、コードが見つからなくても例外にならない。
  String items(int count, String locale) => messages.getMessage(
        'cart.items',
        args: [count],
        locale: locale,
        defaultMessage: '{0} items',
      );
}
```

すべての検索に一つのロケールを使う例と、メッセージソースを使わない整形の例:

```dart
const log = SpringLog('neon.cli');

final messages = MessageSourceAccessor(context, defaultLocale: 'de');
log.info(messages.getMessage('greeting', args: ['Ada'])); // Hallo Ada!

MessageFormat.format("It''s {0,number,#.##}", [3.14159]); // It's 3.14
MessageFormat.format('{0,number}', [1234.5], locale: 'de'); // 1.234,5
```

デフォルトメッセージなしで見つからないコードは `NoSuchMessageException` を投げます。翻訳がそろっていない間は、`useCodeAsDefaultMessage = true` にすると代わりにコードが表示されます。

## メンバー

`MessageSource`:

| メンバー | 意味 |
|---|---|
| `getMessage(code, {args, defaultMessage, locale})` | `locale` での `code` のテキスト。`args` が `{0}`、`{1}` ... に埋め込まれる。ロケールがなければ、フォールバックのロケールと基本ファイルだけを参照する。 |
| `getMessageFromResolvable(resolvable, {locale})` | `MessageSourceResolvable` (コード、引数、デフォルトのテキスト。たとえばバリデーションエラー) の、最初に見つかったコードのテキスト。 |

`ReloadableResourceBundleMessageSource`:

| 名前 | 型 | デフォルト | 意味 |
|---|---|---|---|
| `basenames` | `List<String>` | `const []` | ロケールと拡張子を除いたファイルの場所 (`resources:`、`file:`、独自プロトコル)。先の basename が優先される。 |
| `resourceLoader` | `ResourceLoader?` | `DefaultResourceLoader()` | ファイルを探す。Flutter のアセットなど独自の場所には、`ProtocolResolver` を持つものを渡す。 |
| `cacheSeconds` | `int` | `-1` | `-1` はファイルを一度だけ読む。`0` は検索のたびに確認する。正の値は、最大でその秒数ごとに確認する。 |
| `fallbackToSystemLocale` | `bool` | `true` | `defaultLocale` がないとき、マシンのロケールにフォールバックするかどうか。 |
| `defaultLocale` | `String` | `''` | 要求したロケールの次に試すロケール。 |
| `defaultEncoding` | `Encoding` | `utf8` | ファイルの文字コード。 |
| `useCodeAsDefaultMessage` | `bool` | `false` | 未知のコードで例外を投げず、コードを返す。 |
| `alwaysUseMessageFormat` | `bool` | `false` | 引数のないメッセージも整形し、`''` を `'` にする。 |
| `commonMessages` | `Map<String, String>?` | `null` | どの言語でも同じメッセージ。すべてのロケールの後に試される。 |
| `parentMessageSource` | `MessageSource?` | `null` | このソースにないコードを問い合わせる先。 |
| `addBasenames(list)` | メソッド | | 既存の basename の後に追加する。 |
| `preload(locales)` | メソッド | | ファイルを非同期に読む。非同期でしか読めないファイル (Flutter のアセット) では、検索の前に必要。 |

ロケール `ru_RU` と basename `i18n/messages` の場合、検索順は `messages_ru_RU`、`messages_ru`、デフォルト (またはシステム) のロケール、そして `messages.properties` です。2 番目の basename の前に、最初の basename のすべてのファイルが参照されます。

`MessageFormat` が対応するのは、`{0}`、クォート (`''` は引用符一つ)、`integer`・`percent`・`DecimalFormat` パターンを指定できる `{0,number}`、`short`・`medium`・`long`・`full`・`SimpleDateFormat` パターンを指定できる `{0,date}` と `{0,time}`、そして `{0,choice,...}` です。

## Spring (Java) との対応

```java
@Bean
MessageSource messageSource() {
  var source = new ReloadableResourceBundleMessageSource();
  source.setBasenames("classpath:i18n/messages");
  source.setCacheSeconds(10);
  source.setDefaultLocale(Locale.ENGLISH);
  source.setFallbackToSystemLocale(false);
  return source;
}

String text = messageSource.getMessage("greeting", new Object[] {"Ada"},
    Locale.GERMAN);
```

重要な違い:

- ロケールは文字列 (`de`、`ru_RU`、`ru-RU`、`zh_Hant_TW`) で、`Locales.normalize` で正規化されます。テストでは `Locale.setDefault` と同じように `Locales.systemLocale` を設定できます。
- `locale` 引数は省略できます。`LocaleContextHolder` はありません。
- ファイルはデフォルトで UTF-8 として読まれます。Spring の `.properties` のデフォルトは ISO-8859-1 です。
- ソースはコンテキストのリソースローダーを自動では受け取りません。basename が独自プロトコルを使う場合は `resourceLoader` を渡してください。
- 再読み込みで更新時刻を比べるのは、ファイルシステム上のファイルだけです。その他のリソースは確認のたびに読み直され、プリロードしたものは再度の `preload()` でしか変わりません。
- `MessageFormat` は、ロケールに関係なく日付と時刻を US 英語の形式で書き、指数表記には対応しません。

## 関連項目

- [メッセージ](index.md)
- [Resource](../resources/resource.md)
- [バリデーション](../validation/index.md)
- [@Bean](../beans/bean.md)
- [ApplicationContext](../beans/application-context.md)
