Ex_cldr - Common Locale Data Repository (CLDR) functions for Elixir

I’m wondering if these two calls should return the same result?

iex> Cldr.Time.Interval.to_string! ~T[20:00:00], ~T[22:00:00]
"8:00 – 10:00 PM"

iex> Cldr.Time.Interval.to_string! ~T[08:00:00], ~T[22:00:00]
"8:00 – 10:00 PM"

In the 2nd case, I would have expected "8:00 AM – 10:00 PM"

1 Like

Yes, definitely looks like a bug. Would you mind opening an issue on ex_cldr_dates_times?

3 Likes

Some updates to the Elixir CLDR libraries:

  • Improved locale parsing for language tags including complete parsing of the -u- and -t- extensions in ex_cldr version 2.23.0. For a more accessible description see this presentation by Mark Davis from Google. It also adds the Cldr.Language.Sigil module defining sigil_l for defining language tags.

  • Added the ex_cldr_locale_display library to - you guessed it - render a language tag in a human readable format suitable for presentation in a UI.

  • Updated ex_cldr_html to support producing localised Phoenix.HTML.Form.select/4 select tags.

Locale display with ex_cldr_locale_display

For common locales the output is as you would expect - and perhaps you’re thinking why even have a library for these?

iex> Cldr.display_name ~l"en"u
"English"
iex`> Cldr.display_name ~l"fr"u
"French"
iex`> Cldr.display_name ~l"zh"u
"Chinese"

Things get more interesting with combinations of languages, scrips and territories:

iex> Cldr.display_name ~l"en-GB"u
"British English"
iex> Cldr.display_name ~l"fr-CA"u
"Canadian French"
iex> Cldr.display_name ~l"zh-Hant"u
"Traditional Chinese"
iex> Cldr.display_name ~l"zh-Hant" 
"Traditional Chinese (Taiwan)"
iex> Cldr.display_name ~l"zh-Hans"
"Simplified Chinese (China)"
iex> Cldr.display_name ~l"nl-BE"u
"Flemish"

And if you really want to get into the power of language tags then this example``` can be a bit mind-bending:

iex> Cldr.display_name ~l"fr-CA-u-ca-gregory-nu-arab-cu-usd-cf-account-ms-uksystem-t-hi-h0-hybrid"u
"Canadian French (Hybrid: Hindi, Gregorian Calendar, Accounting Currency Format, Currency: US$, Imperial Measurement System, Arabic-Indic Digits)"

iex> Cldr.display_name ~l"fr-CA-u-ca-gregory-nu-arab-cu-usd-cf-account-ms-uksystem"u, locale: "zh-Hant"     
"法文(加拿大)(公曆,會計貨幣格式,貨幣:US$,英制度量單位系統,阿拉伯-印度數字)"

iex> Cldr.display_name ~l"fr-CA-u-ca-gregory-nu-arab-cu-usd-cf-account-ms-uksystem"u, locale: "ar"     
"الفرنسية الكندية (التقويم الميلادي، تنسيق العملة للحسابات، العملة: US$، نظام القياس البريطاني، الأرقام العربية الهندية)"

iex> Cldr.display_name ~l"fr-CA-u-ca-gregory-nu-arab-cu-usd-cf-account-ms-uksystem"u, locale: "he"
"צרפתית (קנדה) (לוח השנה הגרגוריאני, תבנית מטבע למטרות חשבונאות, מטבע:‏ US$, מערכת מדידה אימפריאלית, ספרות הודיות-ערביות)"

iex> Cldr.display_name ~l"fr-CA-u-ca-gregory-nu-arab-cu-usd-cf-account-ms-uksystem"u, locale: "th"
"ฝรั่งเศส - แคนาดา (ปฏิทินเกรกอเรียน, รูปแบบสกุลเงินบัญชี, สกุลเงิน: US$, ระบบการวัดอิมพีเรียล, ตัวเลขอารบิก-อินดิก)"

ex_cldr supports most of the power of language tags and during localisation will respect regional overrides, subdivisions, currencies, currency formats, unit measurement systems and so on.

HTML select tags for locales with ex_cldr_html

Providing users with a means to select their preferred locale is important for good UX and DX. With Cldr.HTML.Locale.select/3 thats now easy:

# Select from the locales configured in `Cldr.default_backend/0` and localize them
# using the locale `Cldr.default_locale/0`
iex> Cldr.HTML.Locale.select(:my_form, :locales) |> Phoenix.HTML.safe_to_string()

Produces, when formatted:

<select id="my_form_locales" name="my_form[locales]">
	<option value="ar">Arabic</option>
	<option value="zh-Hans">Chinese (Simplified)</option>
	<option value="zh-Hant">Chinese (Traditional)</option>
	<option value="en">English</option>
	<option value="he">Hebrew</option>
	<option value="th">Thai</option>
</select>
# Select from the locales configured in `Cldr.default_backend/0` and localize them
# using their own locale`
iex> Cldr.HTML.Locale.select(:my_form, :locales, locale: :identity) |> Phoenix.HTML.safe_to_string()

Produces, when formatted:

<select id="my_form_locales" name="my_form[locales]">
	<option value="en">English</option>
	<option value="he">עברית</option>
	<option value="ar">العربية</option>
	<option value="th">ไทย</option>
	<option value="zh-Hans">简体中文</option>
	<option value="zh-Hant">繁體中文</option>
</select>
8 Likes

CLDR release 40 was release today by the Unicode Consortium.

CLDR 40 Changes

  • Adds locales sc, kgp, dsb, hsb, yrl
  • Add grammatical features (gender and case) to an additional 29 locales. In many languages, forming grammatical phrases requires dealing with grammatical gender and case. Without that, it can sound as bad as “on top of 3 hours” instead of “in 3 hours”. The overall goal for CLDR is to supply building blocks so that implementations of advanced message formatting can handle gender and case. ex_cldr_units allows specifying grammatical gender and case when formatting units of measure.
  • Added or modified over 140,000 data fields

Ex_CLDR library updates

4 Likes

Its release Sunday and today ex_cldr_units version 3.9.0 is released with the following enhancements.

Enhancements

  • Add support for currency-based units. This allows for calculations and formatting of units such as “$2 per gallon”. For this example, the unit would be created with Cldr.Unit.new(2, "curr-usd-per-gallon"). The inverse is also possible, for example:
iex> MyApp.Cldr.Unit.to_string(Cldr.Unit.new!(2, "curr-usd-per-gallon"))
{:ok, "$2.00 per gallon"}

iex> MyApp.Cldr.Unit.to_string(Cldr.Unit.new!(2, "gallon-per-curr-usd"))
{:ok, "2 gallons per US dollar"}
  • Add support for binary factor prefixed units. These units are factors of 1024 and include “kibi”, “mebi”, “gibi”, “tebi”, “pebi”, “exbi”, “zebi” and “yobi”. For example:
iex> MyApp.Cldr.Unit.to_string Cldr.Unit.new!(3, :gibibyte)
{:ok, "3 gibibytes"}
  • Add support for integer prefixes for units. This is useful for units like “liters per 100 kilometers” or “25 calories per 100 grams”. For example:
iex> MyApp.Cldr.Unit.to_string Cldr.Unit.new!(25, "calorie_per_100-gram")
{:ok, "25 calories per 100 grams"}
4 Likes

This is so crazy! I was playing with it and I noticed that not all units will work (probably “per” is being parsed). For example:

iex [8] > Gaming.Cldr.Unit.to_string(Cldr.Unit.new!(2, "curr-usd-per-mile-per-gallon"))
** (Cldr.UnknownUnitError) Unknown unit was detected at "per_gallon"
    (ex_cldr_units 3.9.0) lib/cldr/unit.ex:637: Cldr.Unit.new!/3

“per” occurs twice. Although it’s strange because this works fine:

iex [9] > Gaming.Cldr.Unit.to_string(Cldr.Unit.new!(2, "curr-usd-per-meter-per-second"))
{:ok, "$2.00 per meter per second"}
1 Like

Thanks for trying it out! The reason the first example works, and the second doesn’t parse, is that meter-per-second is a built-in unit and mile-per-gallon isn’t. The current implementation doesn’t support multiple instances of per in a compound unit definition. But … now I’ll look to making that work.

You can certainly do some crazy stuff, like:

iex> u = Cldr.Unit.new!(2, "curr-usd-foot-pound-per-ampere-light-year")
#Cldr.Unit<"USD_pound_foot_per_light_year_ampere", 2>

iex> MyApp.Cldr.Unit.to_string u
{:ok, "$2.00 pound-feet per light year-ampere"}

iex> MyApp.Cldr.Unit.to_string u, style: :short, fractional_digits: 0 
{:ok, "$2 lb⋅ft/ly⋅A"}
4 Likes

I’ve published ex_cldr_units version 3.9.1 that now supports multiple -per- phrases in a unit definition.

iex> u = Cldr.Unit.new!(2, "curr-usd-per-mile-per-gallon") 
#Cldr.Unit<"curr_usd_per_gallon_mile", 2>

But you’ll notice that this resolves to curr_usd_per_gallon_mile since per the specification:

  • Multiplication binds more tightly than division, so kilogram-meter-per-second-ampere is interpreted as (kg ⋅ m) / (s ⋅ a).
  • Thus if -per- occurs multiple times, each occurrence after the first is equivalent to a multiplication:
    • kilogram-meter-per-second-ampere ⩧ kilogram-meter-per-second-per-ampere.

And then the canonical unit sort order is applied. When output as a string:

iex> MyApp.Cldr.Unit.to_string u                           
{:ok, "$2.00 per gallon-mile"}

Which is logically consistent but maybe not what you expect. That why there are pre-defined units like meter-per-second. Unfortunately, mile-per-gallon is not currently predefined It is predefined but as a compound conversion, which results in the name being resolved as above. meter-per-second is predefined with a single conversion factor which is why its name remains unchanged.

Thanks for the gentle push @slouchpie to get this fixed, even if it doesn’t necessarily serve your purpose.

2 Likes

I didn’t even have a purpose! :grinning_face_with_smiling_eyes: I was just playing with it. For some reason, mile_per_gallon was returned by known_units in one of my projects so I tried it.

I think the solution is perfect though. Multiple “per” is, ultimately, linguistically ambiguous.
Example: x per y per z can be either x / (y /z) = xz / y or (x/y) / z = x /yz .
So even an implementation that rigorously adheres to the specification will be controversial for somebody’s use case.

1 Like

Today ex_cldr hit one million downloads on hex.pm.

I wrote the first message on this topic in 2018 so it’s been a while. And maybe it’s three enthusiasts with a non-caching CI/CD pipeline only :grinning: But it’s the most popular (by downloads) OSS library I’ve ever written. And because it’s Elixir it’s still fun!

Thanks to everyone who has used it, tried it, reported an issue or found a bug. The support and encouragement is much appreciated.

For early 2022 I’ll be launching:

And then in the background I’m still working on transformations and collations.

Here’s to an exciting 2022!

14 Likes

You can explore the latest version of Mezzofanti here: https://github.com/tmbb/mezzofanti

Currently it supports only a gettext backend (that it, it supports translations supplied as gettext files, even though the messages themselves use the ICU message format).

Currently I don’t have much use for this, but I might ad dit in the future.

My rationale is that it’s not possible to translate a string that isn’t a static string marked as translatable in the source, so all translatable strings should be wrapped ina ,macro and available at compile-time.

This is different from gettext, where one can feed it dynamic strings, which will be translated according to the .po files.

This is a requirement for packages like ecto, which can’t access the dependent application’s Gettext backend.

However, because my Mezzofanti library is able to translate strings in dependencies (this was actually the main design criterion for my library), being able to handle dynamic strings is not as useful.

That is what I’m doing now. I’m currently embedding these lists in the code, but I can add them to a persistent term with little work and probably no relevant performance impact.

I’m not doing this right now because I’m trying to reduce the amount of moving parts, but I have plans for adding it in the future.

I’m supplying the locale through the process dictionary.

This module illustrates a some interesting features (explained in the comments):

defmodule Mezzofanti.Fixtures.ExampleModule do
  use Mezzofanti
  # Not that I don't need to require or impor a Mezzofanti backend here.
  # I just use the Mezzofanti library, and once a backend is configured
  # it will automatically become aware of these messages
  # (even if the messages exist in a different application)

  def f() do
    # A simple static translation
    translate("Hello world!")
  end

  def g(guest) do
    # A translation with a variable.
    # This translation contains a context, possibly to disambiguate it
    # from a similar string which should be translated in a different way.
    # Mezzofanti will keep equal strings with different contexts separate.
    translate("Hello {guest}!", context: "a message", variables: [guest: guest])
  end

  def h(user, nr_photos) do
    # A more complex translation with two variables and plural forms.
    # It also defines a different domain.
    translate("""
    {nr_photos, plural,
      =0 {{user} didn't take any photos.}
      =1 {{user} took one photo.}
      other {{user} took # photos.}}\
    """,
      domain: "photos",
      variables: [
        user: user,
        nr_photos: nr_photos
      ])
  end
end

The following commands will create a priv/mezofanti/ dir for the locale data:

mix mezzofanti.extract
mix mezzofanti.new_locale pt-PT
mix mezzofanti.merge

This will result in:

priv/
  mezzofanti/
    pt-PT/
      LC_MESSAGES/
        default.po
        photos.po
    default.pot
    photos.pot

The Mezzofanti and Cldr backends should be created and added to the apps’ config.

Mezzofanti expects a single global Mezzofanti backend, so that you can centralize the translations of several applications/dependencies in the same place (this is a quastionable choice, but again, it was the main design goal of this library).

Like gettext, Mezzofanti reads the locale from the process dictionary.

There are Mezzofanti.set_locale() and Mezzofanti.get_locale() for that.

And also my favourite function for testing, the function Mezzofanti.with_locale(locale, func), which sets the locale, executes the func and then resets the old locale.

For example:

iex> alias Mezzofanti.Fixtures.ExampleModule
Mezzofanti.Fixtures.ExampleModule

iex> ExampleModule.f()
["Hello world!"]

iex> ExampleModule.f() |> to_string()
"Hello world!"

iex> ExampleModule.g("tmbb") |> to_string()
"Hello tmbb!"

iex> ExampleModule.h("kip", 2) |> to_string()
"kip took 2 photos."

iex> ExampleModule.h("kip", 1) |> to_string()
"kip took one photo."

iex> ExampleModule.h("kip", 0) |> to_string()
"kip didn't take any photos."

iex> Mezzofanti.with_locale("pt-PT", fn -> ExampleModule.h("kip", 0) |> to_string() end)
"kip não tirou fotografias nenhumas."

iex> Mezzofanti.with_locale("pt-PT", fn -> ExampleModule.h("kip", 1) |> to_string() end)
"kip tirou uma fotografia"

iex> Mezzofanti.with_locale("pt-PT", fn -> ExampleModule.h("kip", 2) |> to_string() end)
"kip tirou 2 fotografias"

Mezzofanti also contains two “fake” locales for pseudoloalization.

Pseudolocalization is a process of automatically replacing latin characters by similar foreign characters (like characters that are visually similar or contain diacritics not present in English) and extending words with extra characters so that they ar longer than the originals.

This produces still readable strings, helps detect places where strings in your application haven’t been translated and helps detect places where you don’t have space for longer strings.

The “pseudo” locale provides pseudolocalization for simple text:

iex> Mezzofanti.with_locale("pseudo", fn -> ExampleModule.h("kip", 2) |> to_string() end)
"ǩıƥ~ ťøøǩ~ 2 ƥȟøťøš~~."

iex> Mezzofanti.with_locale("pseudo", fn -> ExampleModule.h("kip", 1) |> to_string() end)
"ǩıƥ~ ťøøǩ~ øñê~ ƥȟøťø~."

The “pseudo_html” provides pseudolocalization for HTML text. It preserves HTML tags and HTML entities, while localizing the rest of the text.

In the first line below, you can see how “normal” pseudolocalization is not sufficient for HTML:

iex> Mezzofanti.with_locale("pseudo", fn -> ExampleModule.i() |> to_string() end)        
"Ťȟıš~ ɱêššàğê~~ ċøñťàıñš~~ <šťȓøñğ>ȟťɱĺ~~~~ ťàğš</šťȓøñğ>~~~~ &àɱƥ~; ñàšťÿ~ šťüƒƒ~..."

iex> Mezzofanti.with_locale("pseudo_html", fn -> ExampleModule.i() |> to_string() end)
"Ťȟıš~ ɱêššàğê~~ ċøñťàıñš~~ <strong>ȟťɱĺ~ ťàğš~</strong> &amp; ñàšťÿ~ šťüƒƒ~..."

Welcome to 2022! Today ex_cldr_messages gets a material update that fully integrates it with gettext. The heavy lifting in both packages was done by @maennchen to whom much thanks is due.

Now you can use the ICU Message format in a normal gettext workflow.

A good description of motivations and differences can be found in this presentation by Mark Davis from Google in 2012.

The ICU message format is a more flexible format which also addresses two specific issues:

Two specific shortcomings that the ICU message format addresses:

Grammatical Gender

Many languages inflect in gender specific way. One example in French might be:

# You are the only participant for a male and female
Vous êtes le seul participant
Vous êtes la seule participante

# Married for a male and a female
Marié
Mariée

In Gettext this requires individual messages and conditional code in the application in order to present the correct message to an audience. This is compounded by the fact that some languages have more than two grammatical genders (most have two and four but some are attested with up to 20).

The ICU message format provides a mechanism (the select format that helps translator and UX designers implement a single message to easily encapsulate messages conditional on grammatical gender (or any other selector).

For the example above, the message format could be:

"{grammatical_gender, select,
  male {Marié}
  female {Mariée}
  other {perhaps}
}"

Standardised plural rules

Although Gettext supports pluralization for messages through the Gettext.Plural module in Elixir and the Gettext functions like Gettext.ngettext/4, the plural rules for a language have to be implemented for each message. Given the wide differences in how plural forms are structured in different languages this can be a material challenge. For example:

  • English has two plural forms: singular and plural
  • French applies the singular rule to two values (0 and 1) and a plural form to larger groupings
  • Japanese does not differentiate
  • Russian has 4 categories
  • Arabic has 6 categories

Since CLDR has a strong set of pluralization rules defined for ~500 locales, each of which is supported by ex_cldr, the ICU message format can reuse these pluralization rules in a simple and consistent fashion using the plural format.

6 Likes

Today is Lunary New Year’s Eve where I live in Singapore (and the rest of Asia). The celebrate, today is launch day of ex_cldr_calendars_lunisolar that implements the Chinese, Japanese and Korean traditional calendars. As lunisolar calendars these calendars use the lunar cycle to define months and the solar cycle to define the year.

This is a beta release meaning it likely has bugs, needs documentation improvement and community feedback. I am releasing this early version for two reasons. It must be auspicious to launch on New Years Eve! But more importantly, I lack the linguistic skills and cultural knowledge to validate correctness. So I’m hoping there may be community members interesting in collaborating to deliver a quality library.

Examples

# New year in difference calendars and locales
iex> Date.convert!(~D[2022-02-01], Cldr.Calendar.Chinese) |> Cldr.Date.to_string!(locale: "zh")                 
"2022年正月初一"
iex> Date.convert!(~D[2022-02-01], Cldr.Calendar.Chinese) |> Cldr.Date.to_string!(locale: "zh-Hant")   
"2022年正月初一"
iex> Date.convert!(~D[2022-02-01], Cldr.Calendar.Chinese) |> Cldr.Date.to_string!(locale: "zh-Hant-HK")
"4659年正月1"
iex> Date.convert!(~D[2022-02-01], Cldr.Calendar.Japanese) |> Cldr.Date.to_string!(locale: "ja")       
"令和4年1月1日"
iex> Date.convert!(~D[2022-02-01], Cldr.Calendar.Korean) |> Cldr.Date.to_string!(locale: "ko")  
"4355. 1. 1."
iex> Date.convert!(~D[2022-02-01], Cldr.Calendar.Korean) |> Cldr.Date.to_string!(locale: "ko", format: :full)
"4355년 1월 1일 화요일"

Notes on Lunisolar epochs

Lunisolar calendars work on the sexagenary cycle that doesn’t consider an epoch as many other calendar systems do, including the Gregorian calendar. To fit within the Elixir date structure an epoch needs to be chosen.

  • The Chinese calendar uses ~D[-2636-02-15] as its epoch. There are however other epochs in use in the wild.
  • The Japanese calendar uses ~D[0001-01-01] as the epoch. In common use in Japan (as I understand it) the Gregorian year is used. The imperial era is used for traditional date formatting. This implementation of the Japanese calendar is intended to correctly render the traditional era for all dates as far back as the Taika (大化) era in 645 CE.
  • The Korean epoch is ~D[-2332-02-15] defined as the establishment of the Gojoseon Kingdom

Feedback and Comments requested

Please open an issue with any comments, issues, bugs or suggestions. They will be warmly welcomed.

Happy New Year!

  • Gong xi fa cai!
  • Kung hai fat choi!
  • Chúc mừng năm mới! (Vietnam also has a lunisolar calendar, but there is no localisation data for it in CLDR);
  • Akemashite omedetou!
  • Saehae bok mani badeuseyo
1 Like

새해 복 많이 받으세요!

Can you get the year’s animal and element (wood, fire, earth, …) from the date as well?

I’m not an expert at all, but I am wondering whether it’s really ok to use the modern months names like “1월” for a traditionnal looking calendar. Lunar months in Korean were called xxx달, with 달 meaning moon, so maybe that would be a better default when displaying such dates?

Definitely can. All the data is available including the cyclical names for the days, months and years. Related to your next question.

You may well be right. The formatting is driven by the CLDR data which has the following formats for the “ko” locale and “:dangi” calendar:

iex> MyApp.Cldr.DateTime.Format.date_formats "ko", :dangi               
{:ok,
 %Cldr.Date.Styles{
   full: "U년 MMM d일 EEEE",
   long: "U년 MMM d일",
   medium: "y. M. d.", 
   short: "y. M. d."
 }}

Here U is the cyclic year. But according to TR35, y, M and d are numeric year, month and day. There are no format codes for indicating the cyclic months and days. The spec isn’t very clear on when/how to use the numeric context or the cyclic context. I’ll dig into this some more.

1 Like

Introducing ex_cldr_trans, a CLDR-integrated version of the fabulous trans by @belaustegui (more about trans in the forum).

The main enhancements are:

  • Infers configured locales from the CLDR backend module
  • Uses the CLDR backend default locale to identify the locale of the base untranslated column
  • Uses the locale fallback chain to find a translation
  • Executes in-database translations via a database function
  • Returns NULL (not JSON ‘Null’) for all cases

If in the future trans accepts my PR then this library will just become an API on top of it. But its a large-ish PR so no guarantees.

6 Likes

This is an incredible contribution @kip and I am super thankful.

I plan to accept the PR and integrate your proposed changes in trans but unfortunately I didn’t have too much free time to do it yet. I will do it as soon as possible.

Thank you again. Happy to be part of this community.

3 Likes

A rainy Saturday is an opportunity to publish the first release of ex_cldr_routes that supports localised route generation and localised path helpers. Localised routes allow a user to enter URLs in their local language yet still route to the correct controller and action. Similarly, localised path helpers generate paths in the users locale. Here’s a simple example:

Route generators

# First, add the `Cldr.Routes` provider to a `Cldr` backend module configuration
# And ensure that a `Gettext` module is also configured (this is use for the
# translation process)
defmodule MyApp.Cldr do
  use Cldr,
    locales: ["en", "fr"],
    default_locale: "en",
    gettext: MyAppWeb.Gettext,
    providers: [Cldr.Routes]
end

# Then add `use MyApp.Cldr.Routes` to your router after
# `use Phoenix.Router`
defmodule MyApp.Router do
  use Phoenix.Router
  use MyApp.Cldr.Routes

  # The localize/1 macro wraps the normal route definitions
  # and it will generate localised versions of these routes at
  # compile time.
  localize do
    get "/pages/:page", PageController, :show, assigns: %{key: :value}
    resources "/users", UserController do
      resources "/faces", UserController
    end
  end

Now we can see what routes have been generated:

% mix phx.routes MyApp.Router
Compiling 3 files (.ex)
Generating MyApp.Cldr for 3 locales named [:en, :fr, :und] with a default locale named :en
     page_path  GET     /pages/:page                          PageController :show
     page_path  GET     /pages_fr/:page                       PageController :show
     user_path  GET     /users                                UserController :index
     user_path  GET     /users/:id/edit                       UserController :edit
     user_path  GET     /users/new                            UserController :new
     user_path  GET     /users/:id                            UserController :show
     user_path  POST    /users                                UserController :create
     user_path  PATCH   /users/:id                            UserController :update
                PUT     /users/:id                            UserController :update
     user_path  DELETE  /users/:id                            UserController :delete
user_user_path  GET     /users/:user_id/faces                 UserController :index
user_user_path  GET     /users/:user_id/faces/:id/edit        UserController :edit
user_user_path  GET     /users/:user_id/faces/new             UserController :new
user_user_path  GET     /users/:user_id/faces/:id             UserController :show
user_user_path  POST    /users/:user_id/faces                 UserController :create
user_user_path  PATCH   /users/:user_id/faces/:id             UserController :update
                PUT     /users/:user_id/faces/:id             UserController :update
user_user_path  DELETE  /users/:user_id/faces/:id             UserController :delete
     user_path  GET     /users_fr                             UserController :index
     user_path  GET     /users_fr/:id/edit                    UserController :edit
     user_path  GET     /users_fr/new                         UserController :new
     user_path  GET     /users_fr/:id                         UserController :show
     user_path  POST    /users_fr                             UserController :create
     user_path  PATCH   /users_fr/:id                         UserController :update
                PUT     /users_fr/:id                         UserController :update
     user_path  DELETE  /users_fr/:id                         UserController :delete
user_user_path  GET     /users_fr/:user_id/faces_fr           UserController :index
user_user_path  GET     /users_fr/:user_id/faces_fr/:id/edit  UserController :edit
user_user_path  GET     /users_fr/:user_id/faces_fr/new       UserController :new
user_user_path  GET     /users_fr/:user_id/faces_fr/:id       UserController :show
user_user_path  POST    /users_fr/:user_id/faces_fr           UserController :create
user_user_path  PATCH   /users_fr/:user_id/faces_fr/:id       UserController :update
                PUT     /users_fr/:user_id/faces_fr/:id       UserController :update
user_user_path  DELETE  /users_fr/:user_id/faces_fr/:id       UserController :delete

Path helpers

Localised path helpers are generated in a module MyApp.Router.LocalizedHelpers. The helpers have exactly the same API as the standard Phoenix MyApp.Router.Helpers module that is also generated. The only difference is that if a route is localised, the path helper is also localised. For example:

iex> Gettext.put_locale MyAppWeb.Gettext, "en"
iex> MyApp.Router.LocalizedHelpers.page_path(%Plug.Conn{}, :show, 1)
"/pages/1"

iex> Gettext.put_locale MyAppWeb.Gettext, "fr"
iex> MyApp.Router.LocalizedHelpers.page_path(%Plug.Conn{}, :show, 1)
"/pages_fr/1"

This is quite a simple library that builds upon existing Phoenix macros. There are a couple of capabilities to be completed before a 1.0 release:

  • Generate a list of locales and paths suitable to be used in generating a series of link rel="alternate" href="http://example.com/localized/path" hreflang="locale" />. The function will generate a list of locales and URLs, not the link tag itself.
  • Consider %-encoding. Given the specific focus on localisation and recognising that the URL specification allows US-ASCII only characters in the URL, there is a higher likelihood of non-ASCII translations of path segments leading to the generation of invalid URLs.
2 Likes

April 6th is the release of CLDR 41 and with it the release of related ex_cldr libraries for Elixir:

In addition, ex_money version 5.10.0 has been released with the addition of some helpers functions kindly contributed by @emaiax.

All the other members of the family do not require any update to work with this new CLDR release. As always, please open an issue on the appropriate repo if you spot any problems.

The additional libraries that have not been updated since they are compatible with the new release of ex_cldr:

6 Likes

Following @josevalim’s lead (again!) I have enabled discussions on the ex_cldr repository. I will use that space to announce upcoming roadmap items and for any for more detailed technical discussions/questions.

Of course questions, comments, suggestions all welcome here. And all announcements will come here as well.

2 Likes