Idiom (idiom v0.1.0)

A new take on internationalisation in Elixir.

Basic usage

Interaction with Idiom happens through t/3.

# Set the locale
Idiom.put_locale("en-US")

t("landing.welcome")

# With natural language key
t("Hello Idiom!")

# With interpolation
t("Good morning, {{name}}. We hope you are having a great day.", %{name: "Tim"})

# With plural and interpolation
t("You need to buy {{count}} carrots", count: 1)

# With namespace
t("signup:Create your account")
t("Create your account", namespace: "signup")
Idiom.put_namespace("signup")
t("Create your account")

# With explicit locale
t("Create your account", to: "fr")

# With fallback locale
t("Create your account", to: "fr", fallback: "en")

Installation

To start off, add idiom to the list of your dependencies:

def deps do
  {:idiom, "~> 0.1"},
end

Additionally, in order to be able refresh translations in the background, add Idiom's Supervisor to your application:

def start(_type, _args) do
  children = [
    Idiom,
  ]

  # ...
end

Configuration

There are a few things around Idiom that you can configure on an application level. The following fence shows all of Idiom's settings and their defaults.

config :idiom,
  default_locale: "en",
  default_fallback: "en",
  default_namespace: "default",
  ota_provider: nil

In order to configure your OTA provider, please have a look at its module documentation.

Locales

When calling t/3, Idiom looks at the following settings to determine which locale to translate the key to, in order of priority:

  1. The explicit to option. When you call t("key", to: "fr"), Idiom will always use fr as a locale.
  2. The locale set in the current process. You can call Idiom.put_locale/1 to set it. Since this is just a wrapper around the process dictionary, it needs to be set for each process you are using Idiom in.
  3. The default_locale setting. See the Configuration section for more details on how to set it.

Resolution hierarchy

A note on examples

For ease of presentation, whenever an example in this module documentation includes a translation file for context, it will be merged from the multiple files that Idiom.Source.Local actually expects. Instead of giving you the contents of all en/default.json, en-US/default.json, en-GB/default.json and others, it will be represented here as one merged file, such as:

{ 
  "en": {"default": { [Contents of what would usually be `en/default.json` ] }},
  "en-US": {"default": { [Contents of what would usually be `en-US/default.json` ] }},
  ...
}

Locale codes can consist of multiple parts. Taking zh-Hant-HK as an example, we have the language (zh - Chinese), the script (Hant, Tradtional) and the region (HK - Hong Kong). For different regions, there might only be differences for some specific keys, whereas all other keys share a translation. In order to prevent needless repetition in your translation workflow, Idiom will always try to resolve translations in all of language, language and script, and language, script and region variants, in order of specifity.

Taking the following file as an example (see also File format):

{
  "en": {
    "default": {
      "Create your account": "Create your account"
    }
  },
  "en-US": {
    "default": {
      "Take the elevator": "Take the elevator"
    }
  },
  "en-GB": {
    "default": {
      "Take the elevator": "Take the lift"
    }
  }
}

The Create your account message is the same for both American and British English, whereas the key Take the elevator has different wording for each. With Idiom's resolution hierarchy, you can use both en-US and en-GB to refer to the Create your account key as well.

t("Take the elevator", to: "en-US")
# -> Take the elevator
t("Take the elevator", to: "en-GB")
# -> Take the lift
# Will first try to resolve the key in the `en-US` locale, then, since it does not exist, try `en`.
t("Create your account", to: "en-US")
# -> Create your account
t("Create your account", to: "en-GB")
# -> Create your account

Fallback locales

For when a key might not be available in the set locale, you can set a fallback.
A fallback can be either a string or a list of strings. If you set the fallback as a list, Idiom will return the translation of the first locale for which the key is available.

When you don't explicitly set a fallback for t/3, Idiom will try the default_fallback (see Configuration). When a key is available in neither the target or any of the fallback language, the key will be returned as-is.

# will return the translation for `en`
t("Key that is only available in `en` and `fr`", to: "es", fallback: "en")
# will return the translation for `fr`
t("Key that is only available in `en` and `fr`", to: "es", fallback: ["fr", "en"])
# will return the translation for `en`, which is set as `default_fallback`
t("Key that is only available in `en` and `fr`", to: "es")
# will return "Key that is not available in any locale"
t("Key that is not available in any locale", to: "es")

Namespaces

Idiom allows grouping your keys into namespaces.

When calling t/3, Idiom looks at the following settings to determine which namespace to resolve the key in, in order of priority:

  1. The namespace option, like t("Create your account", namespace: "signup")
  2. As prefix in the key itself.
    You can prefix your key with {namespace}: in order to select a namespace, like t("signup:Create your account")
  3. The namespace set in the current process. You can call Idiom.put_namespace/1 to set it.
    Since this is just a wrapper around the process dictionary, it needs to be set for each process you are using Idiom in.
  4. The default_namespace setting. See the Configuration section for more details on how to set it.

Namespace prefixes and natural language keys

By default, you can use a prefix separated by a colon (:) to namespace a key. When using natural language keys, this can cause issues, such as when the key itself contains a colon.

Consider the following key:

t("Get started on GitHub: create your account")

Using the colon as a separator, Idiom would try to resolve this as key create your account in the Get started on GitHub namespace.

There are multiple ways to work around this:

  1. Explicitly specify the namespace - when a namespace is set this way, the key is left as-is without trying to extract the namespace.

    t("Get started on GitHub: create your account", namespace: "default")
  2. Set a different namespace separator for the key.

    t("Get started on GitHub: create your account", namespace_separator: "|")

Interpolation

Idiom supports interpolation in messages.
Interpolation can be added by adding an interpolation key to the message, enclosing it in {{}}. Then, you can bind the key to any string by passing it as key inside the second parameter of t/3.

Taking the following file as an example (see also File format):

{
  "en": {
    "default": {
      "Welcome, {{name}}": "Welcome, {{name}}",
      "It is currently {{temperature}} degrees in {{city}}": "It is currently {{temperature}} degrees in {{city}}"
    }
  }
}

These messages can then be interpolated as such:

t("Welcome, {{name}}", %{name: "Tim"})
# -> Welcome, Tim
t("It is currently {{temperature}} degrees in {{city}}", %{temperature: "31", city: "Hong Kong"})
# -> It is currently 31 degrees in Hong Kong

Pluralisation

Idiom supports the following key suffixes for pluralisation:

  • zero
  • one
  • two
  • few
  • many
  • other

Your keys, for English, might then look like this:

{
  "carrot_one": "{{count}} carrot"
  "carrot_other": "{{count}} carrots"
}

You can then pluralise your messages by passing count to t/3, such as:

t("carrot", count: 1)
# -> 1 carrot
t("carrot", count: 2)
# -> 2 carrot

{{count}} and pluralisation

As you can see in the above example, we are not passing an extra %{count: x} binding. This is because the count option acts as a magic binding that is automatically available for interpolation.

Sources

Local

Idiom loads translations from the file system at startup.
Please see Idiom.Source.Local for details on the file structure, file format and things to keep in mind.

Over the air

Idiom is designed to be extensible with multiple over the air providers. Please see the modules in Idiom.Source for the ones built-in, and always feel free to extend the ecosystem by creating new ones.

Summary

Functions

Returns the locale that will be used by t/3.

Returns the namespace that will be used by t/3.

Sets the locale for the current process.

Sets the namespace for the current process.

Alias of t/3 for when you don't need any bindings.

Translates a key into a target language.

Types

Link to this type

translate_opts()

@type translate_opts() :: [
  namespace: String.t(),
  to: String.t(),
  fallback: String.t() | [String.t()],
  count: integer() | float() | Decimal.t() | String.t()
]

Functions

@spec get_locale() :: String.t() | nil

Returns the locale that will be used by t/3.

Examples

iex> Idiom.get_locale()
"en-US"
Link to this function

get_namespace()

@spec get_namespace() :: String.t() | nil

Returns the namespace that will be used by t/3.

Examples

iex> Idiom.get_namespace()
"signup"
Link to this function

put_locale(locale)

@spec put_locale(String.t()) :: String.t()

Sets the locale for the current process.

Examples

iex> Idiom.put_locale("fr-FR")
:ok
Link to this function

put_namespace(namespace)

@spec put_namespace(String.t()) :: String.t()

Sets the namespace for the current process.

Examples

iex> Idiom.put_namespace("signup")
:ok

Alias of t/3 for when you don't need any bindings.

Link to this function

t(key, bindings \\ %{}, opts \\ [])

@spec t(String.t(), map(), translate_opts()) :: String.t()

Translates a key into a target language.

The translate/2 function takes two arguments:

  • key: The specific key for which the translation is required.
  • opts: An optional list of options.

Target and fallback languages

For both target and fallback languages, the selected options are based on the following order of priority:

  1. The :to and :fallback keys in opts.
  2. The :locale and :fallback keys in the current process dictionary.
  3. The application configuration's :default_locale and :default_fallback keys.

The language needs to be a single string, whereas the fallback can both be a single string or a list of strings.

Namespaces

Keys can be namespaced. ... write stuff here

Configuration

Application-wide configuration can be set in config.exs like so:

config :idiom,
  default_locale: "en",
  default_fallback: "fr"
  # default_fallback: ["fr", "es"]

Examples

iex> translate("hello", to: "es")
"hola"

# If no `:to` option is provided, it will check the process dictionary:
iex> Process.put(:lang, "fr")
iex> translate("hello")
"bonjour"

# If neither `:to` option is provided nor `:lang` is set in the process, it will check the application configuration:
# Given `config :idiom, default_lang: "en"` is set in the `config.exs` file:
iex> translate("hello")
"hello"

# If a key does not exist in the target language, it will use the `:fallback` option:
iex> translate("hello", to: "de", fallback: "fr")
"bonjour"

# If a key does not exist in the target language or the first fallback language:
iex> translate("hello", to: "de", fallback: ["pl", "fr"])
"bonjour"