# Internationalization

`MVC::Keayl::I18n` is a translation and localization backend. It loads locale
files into one store, looks up translations by dotted key, interpolates and
pluralizes them, localizes dates, times, and numbers, and integrates with
controllers, views, and forms.

## The backend

```perl6
use MVC::Keayl::I18n;

my $i18n = MVC::Keayl::I18n.new(
  default-locale    => 'en',
  available-locales => <en fr ru>,
  use-fallbacks     => True,
  raise-on-missing  => False,
);

$i18n.load-locales('config/locales'.IO);
```

`load-locales` reads every `.yml`, `.yaml`, and `.json` file in a directory.
Each file's top-level keys are locales, and their values are merged into one
store, so `en.yml` and `en/forms.yml` combine:

```yaml
en:
  greeting: "Hello %{name}"
  apples:
    one: "one apple"
    other: "%{count} apples"
```

`load-file` loads a single file and `store-translations` merges a hash directly:

```perl6
$i18n.store-translations('en', { hello => 'Hello world' });
```

## Translation

`translate` (aliased `t`) looks up a dotted key in the current locale:

```perl6
$i18n.translate('greeting', name => 'Ada');   # "Hello Ada"
$i18n.t('messages.welcome', name => 'Greg');
```

Named placeholders written `%{name}` are replaced by the matching argument. A
missing interpolation argument raises `X::MVC::Keayl::I18n::MissingInterpolation`.

### Pluralization

When a `count` argument is given and the entry is a hash of plural categories,
the category is chosen by the locale's rule and `%{count}` is interpolated:

```perl6
$i18n.translate('apples', count => 1);   # "one apple"
$i18n.translate('apples', count => 5);   # "5 apples"
$i18n.translate('apples', count => 0);   # "no apples" when a zero key exists
```

The English rule is `one`/`other`. `MVC::Keayl::I18n::Pluralization` ships rules
for languages whose plurals differ, including French (`0` and `1` are `one`),
Russian (`one`/`few`/`many`), and Polish. Register another with
`register-plural-rule`:

```perl6
use MVC::Keayl::I18n::Pluralization;

register-plural-rule('cy', -> $count { ... });   # returns a category name
```

### Missing translations, defaults, and fallback

A missing key returns the placeholder `translation missing: <locale>.<key>`, or
raises `X::MVC::Keayl::I18n::MissingTranslation` when the backend is built with
`raise-on-missing => True`.

A `default` is consulted before reporting a key missing. A single default is a
literal string. A list is a chain: each entry is tried as a translation key, and
the last entry doubles as a literal fallback:

```perl6
$i18n.translate('absent', default => 'Fallback text');
$i18n.translate('absent', default => ['other.key', 'Plain text']);
```

When `use-fallbacks` is on, a region locale falls back to its base locale before
the default locale, so `en-CA` resolves through `en`.

## Localization

`localize` (aliased `l`) formats a `Date`, `DateTime`, or number against the
locale's format data:

```perl6
$i18n.localize(Date.new(2020, 2, 5));                  # "2020-02-05"
$i18n.localize(Date.new(2020, 2, 5), format => 'long'); # "February 05, 2020"
$i18n.localize($time, format => 'short');               # "02:30 pm"
```

Format strings live under `date.formats`, `time.formats`, and so on, and use
strftime directives. Month and day names come from `date.month_names`,
`date.day_names`, and their abbreviated variants, and `%p` reads `time.am`/`time.pm`.

Numbers are formatted from the `number` section of the store:

```perl6
$i18n.number-to-delimited(1234567.5);   # "1,234,567.5"
$i18n.number-to-currency(1234.5);        # "$1,234.50"
$i18n.number-to-percentage(66.666);      # "66.7%"
$i18n.number-to-human-size(1536);        # "1.5 KB"
```

## Per-request locale

`locale` returns the active locale. `set-locale` changes the backend default,
and `with-locale` applies a locale for the duration of a block and resets it
afterwards:

```perl6
$i18n.with-locale('fr', { $i18n.translate('greeting', name => 'Ada') });
```

`MVC::Keayl::I18n::Locale` resolves a request's locale from several sources.
`resolve-locale` tries strategies in order and returns the first acceptable
match, falling back to a default:

```perl6
use MVC::Keayl::I18n::Locale;

resolve-locale($request,
  strategies => <param header>,
  available  => <en fr>,
  default    => 'en',
);
```

The `param` strategy reads `?locale=fr`, `header` parses `Accept-Language` by
quality, `subdomain` reads the first host label, and `domain` reads the host
suffix. `parse-accept-language` and the per-source subs are available on their
own.

`locale-url-options` builds the options that carry a locale into generated URLs,
merged through `MVC::Keayl::Routing::UrlHelpers`' `default-url-options`:

```perl6
locale-url-options('fr');   # { locale => 'fr' }
```

## Controller and view integration

Configure I18n in `config/application.json` and the application wires a backend
into every controller:

```json
{
  "i18n": {
    "default-locale": "en",
    "available-locales": ["en", "fr"],
    "load-path": "config/locales",
    "strategies": ["param", "header"]
  }
}
```

Each request resolves the locale from the configured strategies, sets it for the
duration of the action, and resets it afterwards. Controllers gain `translate`
(`t`), `localize` (`l`), `current-locale`, and `default-url-options`. A leading
dot is a lazy key resolved against the controller and action, so in
`WidgetsController#show` `self.t('.title')` looks up `widgets.show.title`:

```perl6
method show {
  self.render(plain => self.t('.title'));
}
```

Views expose `t`, `translate`, `l`, and `localize` as helpers:

```haml
%h1= t('.title')
%p= l(post.published-at, format => 'long')
```

## Model and form translation

The backend provides the ActiveModel-style translation surface:

```perl6
$i18n.human-attribute-name(User, 'email_address');         # "Email address"
$i18n.human-model-name(User);                              # "Member"
$i18n.translate-error(User, 'name', 'too_short', count => 8);
```

`translate-error` walks the message chain from the most specific
`activerecord.errors.models.<model>.attributes.<attr>.<type>` to the generic
`errors.messages.<type>`.

`FormBuilder` consults the backend for labels, placeholders, and submit text
when one is present. The form helpers pass the controller's backend through:

```haml
= simple_form_for(user) -> $f {
  $f.input('email_address', { placeholder => True })
  $f.submit
}
```

Labels read `helpers.label.<model>.<attr>`, placeholders (when `placeholder` is
`True`) read `helpers.placeholder.<model>.<attr>`, and submit text reads
`helpers.submit.<model>.<create|update>` with the model name interpolated as
`%{model}`. Each falls back to the humanized attribute or `Save` without a
translation.
