# Idiom - a modern internationalisation library
Idiom is an internationalisation library for Elixir. Its goal is to be simple, yet flexible, with interchangeable sources. At a base level, it supports
reading translations from the local file system at application startup. It also comes with a few (well… not yet) over-the-air sources that are continuously
updated in the back, so you can update your application's translations without having to deploy anything.
> #### Stability notice
>
> This is currently nowhere near stable. I'm messing around with different APIs, the documentation is incomplete, and some modules are missing proper tests.
> I would appreciate feedback on the current API and notes on where I could improve existing documentation, but please don't use this right now.
## Features
- Reading localisation files from the file system (see [Local](#local))
- Fetching translations from third-party services (see [Over-the-air](#over-the-air))
- Pluralisation
- Interpolation
## Installation
Add `idiom` to your `mix.exs`:
```elixir
deps = [
…
{:idiom, "0.1.1"}
…
]
```
Then start it with your application (most likely in an `application.ex`, but it can also be added to a `Supervisor` manually):
```elixir
…
def start(_type, _args) do
children = [
Idiom,
…
]
…
```
Depending on which source you decide to add, you might also need to configure it specifically. Please see the source's module documentation.
## Configuration
The default locale, fallback and namespace can be set in `config.exs`:
```
config :idiom,
default_locale: "fr",
default_fallback: "en",
default_namespace: "translations"
```
These defaults will be used when the option isn't passed to either `t` itself or set in the process dictionary.
## Basic usage
The main way you are going to interact with Idiom is its `t` function.
```elixir
data = %{
"en" => %{
"translations" => %{
"foo" => "bar",
"hello" => "Hello {{name}}",
"carrot_one" => "1 carrot",
"carrot_other" => "{{count}} carrots"
},
"signup" => %{
"Create account" => "Create account"
}
},
"de" => %{
"signup" => "Account erstellen"
},
}
# `translations` is configured as the default namespace.
t("foo", to: "en") # bar
t("foo", to: "en-US") # bar
t("foo", to: "de", fallback: "en") # bar
t("carrot", to: "en", count: 1) # 1 carrot
t("carrot", %{count: 3}, to: "en", count: 3) # 3 carrots
t("Create account", to: "en", namespace: "signup") # Create account
t("signup:Create account", to: "en") # Create account
t("signup:Create account", to: "de") # Account erstellen
t("hello", %{name: "Phil"}, to: "en") # Hello Phil
```
For the `to` and `fallback` options, Idiom also supports setting them through the process dictionary.
```elixir
Process.put(:locale, "en-US")
t("key")
Process.put(:fallback, "fr")
t("key.that.does.not.have.an.english.translation")
```
### Languages, locales and scripts
Idiom automatically builds a hierarchy to resolve a given key. Assuming your user has set their locale to `en-US`, but you don't differentiate between regions
(or scripts) in your translation files and only offer an `en` locale, this will be handled automatically. For a translation that is requested with
`to: "en-Latn-US"`, Idiom will try to resolve the key for `en-Latn-US`, `en-Latn` and finally `en`, returning the first that exists.
### Plurals
For translations that have different versions based on a plural count, Idiom supports those using the
[Unicode CLDR Plural Rules](https://cldr.unicode.org/index/cldr-spec/plural-rules) specification. In detail, this means that keys in your translation files
should offer the following suffixes for translations that support pluralization:
- `zero`
- `one`
- `two`
- `few`
- `many`
- `other`
You can then pass a `count` to `t`. `count` can be an integer, string, float or `Decimal`.
### Interpolation
Idiom also supports interpolation in your translations. Variables can be marked inside `{{}}`, for example `Hello, {{name}}`.
You can then pass bindings to `t` as second parameter, such as `t("Hello, {{name}}!", %{name: "world"}, to: "de")`.
If the variable has no binding, it will be left as-is, without the braces: `t("Hello, {{name}}!", %{}, to: "en")` results in `Hello, name!`.
### Namespaces
Keys can be separated into different namespaces. These can be accessed in multiple ways (in order of priority):
- In the key itself, as a prefix separated by a colon: `t("signup:Create account")`
- As an option: `t("Create account", namespace: "signup")`
- As a key in the process dictionary: `Process.put(:namespace, "signup")`
- The default namespace set in `config.exs` (see [Configuration](#configuration))
## Sources
### Local
Idiom by default automatically loads files from the file system on startup. These are placed in your `priv/idiom/` directory, although you can change the
directory in your `config.exs`:
```elixir
config :idiom, Idiom.Source.Local,
data_dir: "priv/idiom/"
```
#### Directory structure
The `Local` source expects its data directory to follow this directory structure:
```
priv/idiom
└── en
├── default.json
└── login.json
```
where `en` is the locale and `default` and `login` are namespaces separating the keys.
#### File format
The `json` files roughly follow the [i18next format](https://www.i18next.com/misc/json-format), with not all of its features supported. The following example
shows all of its features that Idiom currently supports.
```json
{
"key": "value",
"keyDeep": {
"inner": "value"
},
"keyInterpolate": "replace this {{value}}",
"keyPluralSimple_one": "the singular",
"keyPluralSimple_other": "the plural",
"keyPluralMultipleEgArabic_zero": "the plural form 0",
"keyPluralMultipleEgArabic_one": "the plural form 1",
"keyPluralMultipleEgArabic_two": "the plural form 2",
"keyPluralMultipleEgArabic_few": "the plural form 3",
"keyPluralMultipleEgArabic_many": "the plural form 4",
"keyPluralMultipleEgArabic_other": "the plural form 5",
"keyWithObjectValue": {
"valueA": "return this with valueB",
"valueB": "more text"
}
}
```
## Over-the-air
### [Phrase Strings](https://phrase.com)
...