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)
- Fetching translations from third-party services (see Over-the-air)
- Pluralisation
- Interpolation
Installation
Add idiom to your mix.exs:
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):
…
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.
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.
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 specification. In detail, this means that keys in your translation files should offer the following suffixes for translations that support pluralization:
zeroonetwofewmanyother
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)
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:
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, with not all of its features supported. The following example
shows all of its features that Idiom currently supports.
{
"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
...