You can explore the latest version of Mezzofanti here: GitHub - tmbb/ex_i18n: Experimental translation library for Elixir
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> & ñàšťÿ~ šťüƒƒ~..."