elixir-cldr / cldr

Elixir implementation of CLDR/ICU
Other
440 stars 33 forks source link
cldr elixir elixir-lang i18n l10n list-format number-format unit-format

Getting Started with Cldr

Build status Hex.pm Hex.pm Hex.pm Hex.pm

Change to :json_library configuration {: .warning}

As of ex_cldr version 2.37.2 the configuration parameter :json_library does not attempt to read the configuration of either Phoenix of Ecto.

Specifing the :json_library parameter under the :ex_cldr configuration key in config.exs is recommended however when executing on OTP versions below OTP 27 but the availability of Jason or Poison will still be automatically detected and configured if the :json_library key is not set.

Introduction

ex_cldr is an Elixir library for the Unicode Consortium's Common Locale Data Repository (CLDR). The intentions of CLDR, and this library, is to simplify the locale specific formatting and parsing of numbers, lists, currencies, calendars, units of measure and dates/times. As of April 28th 2023 and ex_cldr Version 2.37.0, ex_cldr is based upon CLDR version 43.0.

The first step is to define a module that will host the desired ex_cldr configuration and the functions that serve as the public API. This module is referred to in this documentation as a backend module. For example:

defmodule MyApp.Cldr do
  @moduledoc """
  Define a backend module that will host our
  Cldr configuration and public API.

  Most function calls in Cldr will be calls
  to functions on this module.
  """
  use Cldr,
    locales: ["en", "fr", "zh", "th"],
    default_locale: "en"

end

This strategy means that different configurations can be defined and it also means that one ex_cldr implementation won't interfere with implementations in other, potentially dependent, applications.

The functions you are mostly likely to use are:

Use Case

Use this library if you need to:

It is highly likely that you will also want to install one or more of the dependent packages that provide localization and formatting for a particular data domain. See Additional Cldr Packages below.

Elixir Version Requirements

Installation

Add ex_cldr and the JSON library of your choice as a dependencies to your mix project. If running on OTP 27 or later, this step is not required since ex_cldr will detect and use the built-in :json module.

defp deps do
  [
    {:ex_cldr, "~> 2.37"},
    # Poison or any other compatible json library
    # that implements `encode!/1` and `decode!/1`
    # :jason is recommended
    {:jason, "~> 1.0"}
    # {:poison, "~> 2.1 or ~> 3.0"}
  ]
end

then retrieve ex_cldr and the JSON library from hex:

mix deps.get
mix deps.compile

Additional Cldr Packages

ex_cldr includes only basic functions to maintain the CLDR data repository in an accessible manner and to manage locale definitions. Additional functionality is available by adding additional packages:

Each of these packages includes ex_cldr as a dependency so configuring any of these additional packages will automatically install ex_cldr.

Configuration

ex_cldr attempts to maximise runtime performance at the expense of additional compile time. Where possible ex_cldr will create functions to encapsulate data at compile time. To perform these optimizations for all 541 locales known to Cldr wouldn't be an effective use of your time or your computer's. Therefore ex_cldr requires that you configure the locales you want to use.

ex_cldr is configured in your backend module. This removes any dependency on your mix.exs and therefore simplifies deployment as a release.

Backend Module Configuration

use Cldr {: .info}

When you use Cldr, a number of functions are generated that encapsulate CLDR data. A module that invokes use Cldr is referred to as a Cldr backend module.

The functions in a Cldr backend module form the primary recommended API for ex_cldr.

In additional, a number of additional modules may be generated with names that are prefixed by the name of the module in which use Cldr is invoked. The number and names of these additional modules is determined by the modules configured under the :providers option to use Cldr.

It is not recommended that a module that invokes use Cldr define any other functions.

defmodule MyApp.Cldr do
  use Cldr,
    default_locale: "en",
    locales: ["fr", "en", "bs", "si", "ak", "th"],
    add_fallback_locales: false,
    gettext: MyApp.Gettext,
    data_dir: "./priv/cldr",
    otp_app: :my_app,
    precompile_number_formats: ["¤¤#,##0.##"],
    precompile_transliterations: [{:latn, :arab}, {:thai, :latn}],
    providers: [Cldr.Number],
    generate_docs: true,
    force_locale_download: false
end

Otp App Configuration

In the backend configuration example above the :otp_app key has been defined. This means that ex_cldr will look for additional configuration, defined under the key :my_app with the sub-key MyApp.Cldr. For example:

# cldr.ex
defmodule MyApp.Cldr do
  use Cldr,
    otp_app: :my_app,
    default_locale: "en",
    gettext: MyApp.Gettext,
    json_library: Jason,
    data_dir: "./priv/cldr",
    precompile_number_formats: ["¤¤#,##0.##"],
    providers: [Cldr.Number]
end
# config/config.exs
config :my_app, MyApp.Cldr,
  # a single locale, for fast compilation in dev / test
  locales: ["en"]
# config/production.exs
config :my_app, MyApp.Cldr,
  # these will take a while to compile
  locales: ["fr", "en", "bs", "si", "ak", "th"],
  precompile_transliterations: [{:latn, :arab}, {:thai, :latn}]

Multiple backends can be configured under a single :otp_app if required.

Global configuration.

In config.exs a global configuration can be defined under the :ex_cldr key. Although any valid configuration keys can be used here, only the keys :json_library, :default_locale, :default_backend, :cacertfile, :data_dir, :force_locale_download are considered valid. Other configuration keys may be used to aid migration from ex_cldr version 1.x but a deprecation message will be printed during compilation. Here's an example of global configuration:

config :ex_cldr,
  default_locale: "en",
  default_backend: MyApp.Cldr,
  json_library: Jason,
  cacertfile: "path/to/cacertfile"

Note that the :json_library key can only be defined at the global level since it is required during compilation before any backend module is compiled.

On most platforms other than Windows the :cacertfile will be automatically detected. Any configured :cacertfile will take precedence on all platforms.

If configuration beyond the keys :default_locale, :cacertfile or :json_library are defined a deprecation warning is printed at compile time noting that configuration should be moved to a backend module.

Configuration Priority

When building the consolidated configuration the following priority applies:

Backend Configuration Keys

The configuration keys available for ex_cldr are:

use Cldr,
  default_locale: "en",
  locales: ["en-*", "fr"]
defmodule MyApp.Cldr do
  use Cldr,
    locales: ["en", "fr"],
    default_locale: "en",
    force_locale_download: Mix.env() == :prod
end

Providers

The data maintained by CLDR is quite large and not all capabilities are required by all applications. Hence ex_cldr has additional optional functionality that can be provided through additional hex packages. In order to support compile-time additions to a configured backend, any package can define a provider that will be called at compile time.

The currently known providers and their hex package names are:

Hex Package Provider Module Comment
ex_cldr_numbers Cldr.Number Formatting of numbers, currencies
ex_cldr_lists Cldr.List Formatting of lists
ex_cldr_units Cldr.Unit Formatting of SI and Imperial units
ex_cldr_person_names Cldr.PersonName Formatting person names
ex_cldr_currencies Cldr.Currency Currency definitions and localizations
ex_cldr_territories Cldr.Territory Formatting of territory (country) data
ex_cldr_languages Cldr.Language Formatting of language information
ex_cldr_dates_times Cldr.DateTime Formatting of dates, times & datetimes
ex_cldr_locale_display Cldr.LocaleDisplay Localising locale names
ex_cldr_routes Cldr.Route Localized routes and route helpers
ex_cldr_messages Cldr.Message Formatting of ICU-formatted messages
ex_money Money Operations on and formatting of a money type

Any library author can create a provider module by exposing a function called cldr_backend_provider/1 that takes a Cldr.Config struct as a single parameter. The function should return an AST that is inserted into the backend module being compiled.

Providers are configured on each backend module under the :providers key. It must be a list of provider modules. For example:

defmodule MyApp.Cldr do
  use Cldr,
    locales: ["en", "zh"],
    default_locale: "en",
    providers: [Cldr.Number, Cldr.List]
end

If :providers is nil (the default), ex_cldr will attempt to configure all of the providers described above if they have been installed as deps. If you don't wish to invoke any providers, use the empty list [].

Migrating from Cldr 1.x

  1. Create a backend module by following the configuration instructions
  2. Delete any duplicated global configuration in any config.exs files. Only the keys :default_locale and :json_library are supported in the global configuration
  3. Update any plugs to configure the desired backend
  4. Adjust any API calls from Cldr.some_function to MyApp.Cldr.some_function. Or better still, alias your backend module where required. ie. alias MyApp.Cldr, as: Cldr

Downloading Locales

ex_cldr can be installed from either github or from hex.

Localizing Numbers

The Cldr.Number module implemented in the ex_cldr_numbers package provides number formatting. The public API for number formatting is MyApp.Cldr.Number.to_string/2. Some examples:

iex> MyApp.Cldr.Number.to_string 12345
"12,345"

iex> MyApp.Cldr.Number.to_string 12345, locale: "fr"
"12 345"

iex> MyApp.Cldr.Number.to_string 12345, locale: "fr", currency: "USD"
"12 345,00 $US"

iex> MyApp.Cldr.Number.to_string 12345, format: "#E0"
"1.2345E4"

iex(> MyApp.Cldr.Number.to_string 1234, format: :roman
"MCCXXXIV"

iex> MyApp.Cldr.Number.to_string 1234, format: :ordinal
"1,234th"

iex> MyApp.Cldr.Number.to_string 1234, format: :spellout
"one thousand two hundred thirty-four"

See h MyApp.Cldr.Number and h MyApp.Cldr.Number.to_string in iex for further information.

Localizing Lists

The Cldr.List module provides list formatting and is implemented in the ex_cldr_lists package. The public API for list formatting is Cldr.List.to_string/2. Some examples:

iex> MyApp.Cldr.List.to_string(["a", "b", "c"], locale: "en")
"a, b, and c"

iex> MyApp.Cldr.List.to_string(["a", "b", "c"], locale: "en", format: :unit_narrow)
"a b c"

iex> MyApp.Cldr.List.to_string(["a", "b", "c"], locale: "fr")
"a, b et c"

See h MyApp.Cldr.List and h MyApp.Cldr.List.to_string in iex for further information.

Localizing Units

The Cldr.Unit module provides unit localization and is implemented in the ex_cldr_units package. The public API for unit localization is Cldr.Unit.to_string/3. Some examples:

iex> MyApp.Cldr.Unit.to_string 123, :gallon
"123 gallons"

iex> MyApp.Cldr.Unit.to_string 1234, :gallon, format: :long
"1 thousand gallons"

iex> MyApp.Cldr.Unit.to_string 1234, :gallon, format: :short
"1K gallons"

iex> MyApp.Cldr.Unit.to_string 1234, :megahertz
"1,234 megahertz"

iex> MyApp.Cldr.Unit.available_units
[:acre, :acre_foot, :ampere, :arc_minute, :arc_second, :astronomical_unit, :bit,
 :bushel, :byte, :calorie, :carat, :celsius, :centiliter, :centimeter, :century,
 :cubic_centimeter, :cubic_foot, :cubic_inch, :cubic_kilometer, :cubic_meter,
 :cubic_mile, :cubic_yard, :cup, :cup_metric, :day, :deciliter, :decimeter,
 :degree, :fahrenheit, :fathom, :fluid_ounce, :foodcalorie, :foot, :furlong,
 :g_force, :gallon, :gallon_imperial, :generic, :gigabit, :gigabyte, :gigahertz,
 :gigawatt, :gram, :hectare, :hectoliter, :hectopascal, :hertz, :horsepower,
 :hour, :inch, ...]

See h MyApp.Cldr.Unit and h MyApp.Cldr.Unit.to_string in iex for further information.

Localizing Dates

Formatting of relative dates and date times is supported in the Cldr.DateTime.Relative module implemented in the ex_cldr_dates_times package. The public API is MyApp.Cldr.DateTime.to_string/2 and MyApp.Cldr.DateTime.Relative.to_string/2. Some examples:

iex> MyApp.Cldr.Date.to_string Date.utc_today()
{:ok, "Aug 18, 2017"}

iex> MyApp.Cldr.Time.to_string Time.utc_now
{:ok, "11:38:55 AM"}

iex> MyApp.Cldr.DateTime.to_string DateTime.utc_now
{:ok, "Aug 18, 2017, 11:39:08 AM"}

iex> MyApp.Cldr.DateTime.Relative.to_string 1, unit: :day, format: :narrow
{:ok, "tomorrow"}

iex> MyApp.Cldr.DateTime.Relative.to_string(1, unit: :day, locale: "fr")
"demain"

iex> MyApp.Cldr.DateTime.Relative.to_string(1, unit: :day, format: :narrow)
"tomorrow"

iex> MyApp.Cldr.DateTime.Relative.to_string(1234, unit: :year)
"in 1,234 years"

iex> MyApp.Cldr.DateTime.Relative.to_string(1234, unit: :year, locale: "fr")
"dans 1 234 ans"

Gettext Pluralization

gettext allows for user-defined plural forms modules to be configured for a gettext backend.

To define a plural forms module that uses CLDR plural rules create a new module and then use Cldr.Gettext.Plural. For example:

defmodule MyApp.Gettext.Plural do
  use Cldr.Gettext.Plural, cldr_backend: MyApp.Cldr
end

This module can then be used in the configuration of a gettext backend. For example:

defmodule MyApp.Gettext do
  use Gettext, plural_forms: MyApp.Gettext.Plural
end

Note that MyApp.Gettext.Plural does not guarantee to return the same plural index as Gettext's own pluralization engine which can introduce some compatibility issues if you plan to mix plural engines. See Cldr.Gettext.Plural for more information.

About Language Tags

Note that ex_cldr defines locale strings according to the IETF standard as defined in RFC5646. ex_cldr also implements the u extension as defined in RFC6067 and the t extension defined in RFC6497. This is also the standard used by W3C.

The IETF standard is slightly different to the ISO/IEC 15897 standard used by Posix-based systems; primarily in that ISO 15897 uses a "_" separator whereas IETF and W3C use "-".

Locale string are case insensitive but there are common conventions:

Sigil_l

As of ex_cldr version 2.23.0, a sigil is available to simplify creating t:Cldr.LanguageTag structs. Usage is:

iex> import Cldr.LanguageTag.Sigil
Cldr.LanguageTag.Sigil

# Returns a locale that is valid and known to
# the default backend module
iex> ~l(en-US)
#Cldr.LanguageTag<en-US [validated]>

# Same, but specifying the backend module
# MyApp.Cldr specifically
iex> ~l(en-US|MyApp.Cldr)
#Cldr.LanguageTag<en-US [validated]>

# The `u` flag will parse and validate
# the language tag but it may not be known
# as a configured locale
iex> ~l(zh)u
#Cldr.LanguageTag<zh [canonical]>

# Language tags can convey a lot more information
# than might be initially expected!
iex> ~l(en-u-ca-ethiopic-cu-aud-sd-gbsct-t-d0-lower-k0-extended-m0-ungegn-x-ux)
#Cldr.LanguageTag<en-t-d0-lower-k0-extended-m0-ungegn-u-ca-ethiopic-cu-aud-sd-gbsct-x-ux [validated]>

Locale extensions

Unicode defines the U extension which support defining the requested treatment of CLDR data formats. For example, a locale name can configure the requested:

For example, the following locale name will request the use of the timezone Australia/Sydney, and request the use of accounting format when formatting currencies:

iex> MyApp.Cldr.validate_locale "en-AU-u-tz-ausyd-cf-account"
{:ok,
 %Cldr.LanguageTag{
   canonical_locale_name: "en-Latn-AU",
   cldr_locale_name: "en-AU",
   extensions: %{},
   gettext_locale_name: "en",
   language: "en",
   language_subtags: [],
   language_variants: nil,
   locale: %Cldr.LanguageTag.U{cf: :account, timezone: "Australia/Sydney"},
   private_use: [],
   rbnf_locale_name: "en",
   requested_locale_name: "en-AU",
   script: :Latn,
   territory: :AU,
   transform: %{}
 }}

The implementation of these extensions is governed by each library in the ex_cldr family. As of January 2020, ex_cldr_numbers version 2.10 implements the following U extension keys:

Other libraries in the family will progressively implement other extension keys.

Notes

Developing ex_cldr

See the file DEVELOPMENT.md in the github repository.

Testing

Tests cover the full ~700 locales defined in CLDR. Since ex_cldr attempts to maximize the work done at compile time in order to minimize runtime execution, the compilation phase for tests is several minutes.

Tests are run on Elixir 1.11 and later. ex_cldr is not supported on Elixir versions before 1.11.