<!--
  Internationalization (i18n)
  From the Iris book, chapter 15, covering Iris 14.0.0 on Go 1.27.
  Import path: github.com/kataras/iris/v14
  Canonical page: https://iris-go.com/docs/internationalization-i18n/
-->
# Chapter 15: Internationalization (i18n)

Iris ships internationalization as a regular middleware package,
`github.com/kataras/iris/v14/middleware/i18n`. It loads translation files in
INI, JSON, TOML or YAML format, matches the request language against your
registered locales using BCP 47 tags, and exposes the matched locale to your
handlers. This chapter covers only what the package actually provides: loading
translations, per-request language detection, message rendering with plurals
and variables, and the optional path/subdomain rewriting router.

## Background

Internationalization (abbreviated i18n, for the eighteen letters between
the "i" and the "n") is structuring an application so it can address each
user in their own language. The obstacle is rarely translation itself; it
is where the words live. When user-facing strings are hardcoded, written
literally inside handlers and templates, adding a second language means
hunting down every string in the codebase, and translators, who are
usually not programmers, would have to edit Go source to do their job.
Worse, a fixed string like `"You have 3 new messages"` glued together in
code embeds English grammar into program logic.

The standard cure is a *message catalog*: user-facing text moves out of
code and into plain data files, one set per language, where every message
has a *key*: a stable name like `welcome` or `FreeDay`. Code asks for the
key; the catalog answers in whichever language was matched for the current
request. That is the problem that disappears from your handlers: they no
longer contain any user-facing prose at all, and shipping a new language
becomes adding files rather than changing code.

Translation alone is not enough, which is why catalogs are more than
key-to-string maps:

- **Plurals.** English has two forms (1 day, 5 days), but languages
  disagree wildly: Russian uses different forms for 1, 2–4 and 5+; Arabic
  distinguishes six cases. A catalog therefore stores several variants of
  a message and picks one based on a count at run time.
- **Variables and word order.** A sentence with a value in the middle
  ("You are 10 minutes late") cannot be built by concatenating halves,
  because other languages put the value elsewhere. Messages carry
  placeholders instead, and the placeholder can move freely per language.
- **Locale codes.** Languages are identified by standardized *language
  tags* (BCP 47, defined in RFC 5646) such as `el-GR`: a language code
  (`el`, Greek) optionally followed by a region code (`GR`, Greece). The
  region part matters when regions diverge: `en-US` and `en-GB` spell,
  format dates and count currency differently. A *locale* is the matched
  combination the application serves.

The remaining question is how a server knows which language a given
request wants. Every browser volunteers an answer: the `Accept-Language`
request header carries the user's ranked preferences, taken from their
operating system or browser settings (for example
`el-GR, el;q=0.9, en;q=0.8`, where `q` weights the ranking). That is a
good default and a poor final word, since users borrow machines and
override preferences, so real applications also honor explicit choices: a
`?lang=` URL parameter, a saved cookie, a language subdomain
(`el.example.com`) or a path prefix (`/el-GR/account`). The i18n
middleware you meet below checks exactly these sources, in a documented
order, and falls back to your default language when nothing matches.

## Package Overview

The package has three cooperating pieces:

- `i18n.New()` returns an `*i18n.I18n` catalog. You load locale files into it
  once at startup with `Load`, `LoadFS`, `LoadKV` or `LoadAssets`.
- `i18n.NewHandler(locales, i18n.HandlerOptions{...})` returns a regular
  middleware handler. Register it with `Use`. On every request it detects the
  language and stores the matched locale on the context.
- `i18n.NewRouter(locales, i18n.RouterOptions{...})` is optional. Register it
  with `UseRouter` to strip language prefixes such as `/el-GR/...` (or
  language subdomains such as `el.example.com`) before routing happens.

Handlers then read translations through the package-level functions
`i18n.GetLocale(ctx)` and `i18n.GetMessage(ctx, key, args...)`.

## Loading Translations

`Load` accepts a glob pattern and an optional list of language codes. The
language of each file is deduced from its path: a directory named after the
language (`locales/el-GR/anything.yml`) or a file named after it
(`locales/el-GR.yml`) both work.

```go
package main

import "github.com/kataras/iris/v14/middleware/i18n"

func main() {
    locales := i18n.New()

    // First language is the default/fallback one.
    err := locales.Load("./locales/*", "en-US", "el-GR")
    if err != nil {
        panic(err)
    }
}
```

Rules worth knowing:

- The first language in the list is the default. `SetDefault("el-GR")` can
  reorder that after loading, but usually you just list the default first.
- If you omit the language list entirely, the languages are discovered
  dynamically from the file paths.
- The file extension selects the parser: `.ini`, `.json`, `.toml`/`.tml`,
  and `.yaml`/`.yml` (the default). Multiple files for the same language are
  merged into one message catalog.
- Nested keys flatten with dots: a YAML map `user: {name: ...}` is fetched as
  `"user.name"`. INI sections behave the same way.

### Embedded Files

Go can compile arbitrary files straight into the executable: the
`//go:embed` directive tells the compiler to bundle the named files, and
the resulting `embed.FS` value serves them through the standard `fs.FS`
read-only filesystem interface. For translations this means the deployed
binary is self-contained. There is no `locales/` directory to ship,
forget or mismatch.

`LoadFS` works with any `fs.FS`, including `embed.FS`, so translations can be
compiled into the binary:

```go
import "embed"

//go:embed locales/*
var localesFS embed.FS

func load(locales *i18n.I18n) error {
    return locales.LoadFS(localesFS, "locales/*", "en-US", "el-GR")
}
```

### Maps

`LoadKV` loads translations from an in-memory `i18n.LangMap`
(`map[string]map[string]any`), which is convenient in tests:

```go
langMap := i18n.LangMap{
    "en-US": map[string]any{
        "hello": "Hello",
    },
    "el-GR": map[string]any{
        "hello": "Γειά",
    },
}

err := locales.LoadKV(langMap, "en-US", "el-GR")
```

`LoadAssets(assetNames, asset, languages...)` exists as well, for go-bindata
style generated assets.

### Loader Options

The `Loader` field of `*i18n.I18n` is an `i18n.LoaderConfig`. Configure it
before calling any `Load` method:

```go
locales := i18n.New()
locales.Loader.Strict = true  // fail unless every language loads.
locales.Loader.Left = "{{"    // template delimiters (defaults).
locales.Loader.Right = "}}"

locales.SetDefaultMessageFunc(
    func(langMatched, key string, args ...any) string {
        return "[missing: " + key + "]"
    })
```

Without a default message function, a lookup for an unknown key returns an
empty string.

## The Request Handler

`i18n.NewHandler` builds the middleware that performs language detection. All
detection sources are opt-in through `i18n.HandlerOptions`:

```go
app.Use(i18n.NewHandler(locales, i18n.HandlerOptions{
    // ?lang=el-GR
    URLParameter: "lang",
    // read the language from a cookie named "lang".
    Cookie: "lang",
    // treat a subdomain as a language: el.example.com.
    Subdomain: true,
    // optional custom source, checked before the ones above.
    ExtractFunc: nil,
}))
```

The handler checks the sources in a fixed order and stops at the first
registered language that matches:

1. A language force-set by server code (`i18n.SetLanguage`, see below).
2. `ExtractFunc`, if set.
3. The URL query parameter (`URLParameter`).
4. The cookie (`Cookie`). The handler only reads this cookie; writing it is
   your job or the router's (see `RouterOptions.Cookie`).
5. The subdomain, if `Subdomain` is true.
6. The `Accept-Language` request header (always checked).
7. Fallback: the default locale, i.e. the first registered language.

\[Diagram: The i18n handler tries seven sources in order and the first match wins; only a language set on the context short-circuits, and it yields no locale at all when nothing registered matches it.]

Matching is lenient: for a registered `el-GR`, the inputs `el-GR`, `el-gr`
and plain `el` all resolve to the same locale.

One asymmetry in that list is worth knowing. Rungs 2 through 7 fall
through: a source that is unset, empty or unmatched simply hands over to
the next. Rung 1 does not. A language force-set with `i18n.SetLanguage`
that matches nothing registered leaves the request with *no* locale at
all, so `i18n.GetLocale(ctx)` returns nil and `i18n.GetMessage` returns
an empty string. It does not fall back to the default.

## Translating in Handlers

`i18n.GetLocale(ctx)` returns the matched `locale.Locale`, an interface with
`Language()`, `Index()`, `Tag()` and `GetMessage(key, args...)`.
`i18n.GetMessage(ctx, key, args...)` is a shortcut for
`GetLocale(ctx).GetMessage(key, args...)`.

```go
app.Get("/", func(ctx iris.Context) {
    locale := i18n.GetLocale(ctx)
    hi := locale.GetMessage("hi", "iris")
    // Equivalent: hi := i18n.GetMessage(ctx, "hi", "iris")

    ctx.Writef("[%s] %s", locale.Language(), hi)
})
```

Plain messages are rendered with printf-style verbs, so a locale entry
`hi = hello, %s` combined with `GetMessage("hi", "iris")` produces
`hello, iris`. Indexed verbs such as `%[1]d` are supported too.

To override the detected language:

- `i18n.SetLocaleByLanguage(ctx, "el-GR")` switches the locale for the rest
  of the current request. Use it inside route handlers, after the i18n
  handler has run.
- `i18n.SetLanguage(ctx, "el-GR")` stores a language code that the i18n
  handler honors above every other source. It only has an effect when called
  before the handler runs, e.g. in a middleware registered earlier.

## Pluralization and Variables

### Plural Forms

A message becomes plural-aware when its value is a map whose keys are plural
forms. The supported forms are `zero`, `one`, `two`, `other` and the
comparisons `=N`, `<N`, `>N`:

```yaml
FreeDay:
  one: "You have a day off"
  other: "You have %[1]d free days"
  "=3": "You have three days and %[2]d minutes off."
```

The plural count is the first argument to `GetMessage` and it also feeds the
printf verbs:

```go
i18n.GetMessage(ctx, "FreeDay", 1)      // You have a day off
i18n.GetMessage(ctx, "FreeDay", 5)      // You have 5 free days
i18n.GetMessage(ctx, "FreeDay", 3, 15)  // ... three days and 15 minutes off.
```

Calling a plural message without a count does not panic; the returned string
is an error description, which makes the mistake visible during development.

### Variables

A locale file can declare a `Vars` list. Each variable has its own plural
forms and is referenced inside messages as `${Name}`. The count that selects
the variable's form comes from the message arguments:

```yaml
Vars:
  - Minutes:
      one: "minute"
      other: "minutes"

YouLate: "You are %[1]d ${Minutes} late."
```

```go
i18n.GetMessage(ctx, "YouLate", 1)   // You are 1 minute late.
i18n.GetMessage(ctx, "YouLate", 10)  // You are 10 minutes late.
```

`Vars` declared at the top level of a file are shared by every message of
that locale. A message can also carry its own local variables by using a map
value with `Text` and `Vars` keys:

```yaml
HouseCount:
  Text: "She has %[1]d ${Houses}"
  Vars:
    - Houses:
        one: "house"
        other: "houses"
```

### Template Messages

Go's standard library includes `text/template`, a small templating
language: a string containing placeholders such as `{{.name}}` is parsed
once, then rendered against a data value that fills the placeholders in.
Compared to printf verbs, templates name their inputs, which reads better
when a message takes several values.

A message whose value contains the configured delimiters (default
`{{` and `}}`) is parsed as a Go `text/template`. The first `GetMessage`
argument becomes the template data, usually a map:

```yaml
welcome: "Welcome back, {{.name}}!"
```

```go
i18n.GetMessage(ctx, "welcome", iris.Map{"name": "Makis"})
// Welcome back, Makis!
```

For plural template messages the reserved data key `PluralCount` selects the
plural form. Custom per-locale template functions can be registered through
the `Funcs` field of the loader configuration; every locale template also has
a built-in `tr` function that resolves other keys of the same locale.

## Path and Subdomain Rewriting

`i18n.NewRouter` lets URLs carry the language, e.g. `/el-GR/account` or
`el.example.com/account`. It must run before routing, so register it with
`UseRouter`, and keep the regular handler registered with `Use` as well:

```go
app.UseRouter(i18n.NewRouter(locales, i18n.RouterOptions{
    // write the detected language to this response cookie.
    Cookie: "lang",
    // also detect and strip language subdomains.
    Subdomain: true,
    // optional hook, called with the detected language tag.
    InjectFunc: nil,
}))

app.Use(i18n.NewHandler(locales, i18n.HandlerOptions{
    URLParameter: "lang",
    Cookie:       "lang",
}))
```

When the first path segment (or the subdomain, with `Subdomain: true`)
matches a registered language, the router rewrites the request in place: it
removes the language part, sets the request's `Accept-Language` header to the
matched tag and, if `Cookie` is set, writes the language to a response cookie
shared across subdomains. Routing then continues with the clean path, so a
route defined as `/account` serves `/el-GR/account` too, and the i18n handler
picks the language up from the rewritten header. Lenient matching applies
here as well: `/el/account` and `/el-gr/account` both work.

## Template Integration

Server-side views (see [Chapter 6](https://iris-go.com/docs/response-handling/)) integrate
through plain template functions. The pattern used by the package itself
registers a static `tr` function on the engine and passes per-request
closures as view data:

```go
import (
    "github.com/kataras/blocks"

    "github.com/kataras/iris/v14"
    "github.com/kataras/iris/v14/middleware/i18n"
    "github.com/kataras/iris/v14/middleware/view"
)

func registerViews(app *iris.Application, locales *i18n.I18n) {
    templates := blocks.New("./views")
    // Static form, any language: {{tr "el-GR" "hi" "iris"}}.
    templates.AddFunc("tr",
        func(lang, key string, args ...any) string {
            if locale, ok := locales.GetLocale(lang); ok {
                return locale.GetMessage(key, args...)
            }
            return "[INVALID LOCALE] " + lang
        })
    templates.Load()
    app.UseRouter(view.Handler(templates))

    app.Get("/", func(ctx iris.Context) {
        view.Render(ctx, "index.html", "", iris.Map{
            // Request-aware form: {{call .tr "hi" "iris"}}.
            "tr": func(key string, args ...any) string {
                return i18n.GetMessage(ctx, key, args...)
            },
        })
    })
}
```

```html
<h1>{{call .tr "hi" "iris"}}</h1>
<p>{{tr "el-GR" "hi" "iris"}}</p>
```

## Complete Example

A runnable program with two locale files, plural messages, URL-parameter and
header detection plus path prefixes:

```
myapp/
├── locales/
│   ├── en-US.yml
│   └── el-GR.yml
└── main.go
```

**locales/en-US.yml**

```yaml
hi: "hello, %s"
FreeDay:
  one: "You have one day off."
  other: "You have %d days off."
```

**locales/el-GR.yml**

```yaml
hi: "γεια, %s"
FreeDay:
  one: "Έχεις μία ελεύθερη μέρα."
  other: "Έχεις %d ελεύθερες μέρες."
```

**main.go**

```go
package main

import (
    "github.com/kataras/iris/v14"
    "github.com/kataras/iris/v14/middleware/i18n"
)

func main() {
    app := iris.New()

    locales := i18n.New()
    if err := locales.Load("./locales/*", "en-US", "el-GR"); err != nil {
        panic(err)
    }

    app.UseRouter(i18n.NewRouter(locales, i18n.RouterOptions{
        Cookie: "lang",
    }))

    app.Use(i18n.NewHandler(locales, i18n.HandlerOptions{
        URLParameter: "lang",
        Cookie:       "lang",
    }))

    app.Get("/", func(ctx iris.Context) {
        locale := i18n.GetLocale(ctx)
        ctx.Writef("[%s] %s", locale.Language(),
            locale.GetMessage("hi", "iris"))
    })

    app.Get("/days/{count:int}", func(ctx iris.Context) {
        count := ctx.Params().GetInt("count")
        ctx.WriteString(i18n.GetMessage(ctx, "FreeDay", count))
    })

    app.Listen(":8080")
}
```

```sh
curl http://localhost:8080/
# [en-US] hello, iris

curl -H "Accept-Language: el" http://localhost:8080/
# [el-GR] γεια, iris

curl "http://localhost:8080/days/5?lang=el-GR"
# Έχεις 5 ελεύθερες μέρες.

curl http://localhost:8080/el-GR/days/1
# Έχεις μία ελεύθερη μέρα.
```

Test i18n behavior the same way as any other handler chain, with
`httptest.New(t, app)` and requests that vary the `lang` query parameter or
the `Accept-Language` header; [Chapter 16](https://iris-go.com/docs/testing/) covers the tooling.

## Summary

- `i18n.New()` creates the catalog; `Load`, `LoadFS`, `LoadKV` and
  `LoadAssets` fill it from glob patterns, embedded filesystems, maps or
  generated assets. INI, JSON, TOML and YAML files are supported, and the
  language is deduced from the file path.
- `i18n.NewHandler` (registered with `Use`) detects the language per request
  in a fixed order: server-set value, `ExtractFunc`, URL parameter, cookie,
  subdomain, `Accept-Language` header, then the default locale.
- Handlers translate with `i18n.GetMessage(ctx, key, args...)` or through
  `i18n.GetLocale(ctx)`. Messages use printf verbs, Go templates, plural
  forms (`zero`, `one`, `two`, `other`, `=N`, `<N`, `>N`) and `${Var}`
  variables.
- `i18n.NewRouter` (registered with `UseRouter`) rewrites language path
  prefixes and subdomains before routing and can persist the choice in a
  cookie.
- View engines integrate via ordinary template functions such as a `tr`
  helper backed by `GetLocale`/`GetMessage`.

## Further Reading

- [RFC 5646: Tags for Identifying Languages](https://www.rfc-editor.org/rfc/rfc5646):
  the BCP 47 language-tag grammar behind codes like `el-GR` and `en-US`.
- [MDN: Accept-Language](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language):
  how browsers express ranked language preferences, including `q`
  weights.
- [MDN: Content negotiation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation):
  the wider HTTP mechanism that language selection is one instance of.
- [The Go text/template package](https://pkg.go.dev/text/template): the
  full syntax available inside template-style messages.
- [The Go embed package](https://pkg.go.dev/embed): `//go:embed` and
  `embed.FS`, used to compile locale files into the binary.
