Skip to content

Commit

Permalink
Allow to pass custom Cldr module
Browse files Browse the repository at this point in the history
  • Loading branch information
dolfinus committed Feb 28, 2021
1 parent 7e45fa5 commit 831ad18
Show file tree
Hide file tree
Showing 8 changed files with 319 additions and 34 deletions.
54 changes: 52 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,59 @@ iex> I18n.t!("en", "users.title")

## Configuration

### Pluralization key

The key to use for pluralization is configurable, and should likely be an atom:

```
```elixir
# default
config :linguist, pluralization_key: :count
```
will cause the system to pluralize based on the `count` parameter passed to the `t` function.

Will cause the system to pluralize based on the `count` parameter passed to the `t` function.

But also you should add proper locale to Cldr backend.

There are multiple ways to perform that:

1. By default, linguist creates its own Cldr backend module `Linguist.Cldr`. It handles only `en` locale plurals but you can add more locales:

```elixir
config :linguist, Linguist.Cldr, locales: ["fr", "es"]
```

But this way is not recommended because it is not flexible.


2. You can pass your own Cldr backend module within application config:

```elixir
config :linguist, cldr: MyApp.Cldr
```

3. If you've using `Linguist.Vocabulary`, you can pass own Cldr module name explicitly in your I18n module:

```elixir
defmodule I18n do
use Linguist.Vocabulary, cldr: MyApp.Cldr

...
end
```

or like that:

```elixir
defmodule I18n do
use Linguist.Vocabulary

@cldr MyApp.Cldr

end
```

4. If you've using `Linguist.MemoizedVocabulary`, you can set up own Cldr module name:

```elixir
Linguist.MemorizedVocabulary.cldr(MyApp.Cldr)
```
4 changes: 3 additions & 1 deletion config/config.exs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use Mix.Config

config :linguist, pluralization_key: :count
config :linguist,
pluralization_key: :count,
cldr: Linguist.Cldr

config :ex_cldr, json_library: Jason

Expand Down
46 changes: 28 additions & 18 deletions lib/linguist/compiler.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
defmodule Linguist.Compiler do
alias Linguist.Cldr.Number.Cardinal
alias Linguist.NoTranslationError

@doc ~S"""
Expand Down Expand Up @@ -41,8 +40,11 @@ defmodule Linguist.Compiler do
@escaped_interpol_rgx ~r/%%{/
@simple_interpol "%{"

def compile(translations) do
def compile(translations, cldr \\ nil) do
langs = Keyword.keys(translations)
cldr = cldr || Application.fetch_env!(:linguist, :cldr)

cardinal = "#{cldr}.Number.Cardinal" |> String.to_atom()

translations =
for {locale, source} <- translations do
Expand All @@ -51,7 +53,28 @@ defmodule Linguist.Compiler do

quote do
def t(locale, path, binding \\ [])
def t(locale, path, bindings) do
pluralization_key = Application.fetch_env!(:linguist, :pluralization_key)

if Keyword.has_key?(bindings, pluralization_key) do
plural_atom =
bindings
|> Keyword.get(pluralization_key)
|> unquote(cardinal).plural_rule(locale)

case plural_atom do
value when is_atom(value) ->
do_t(locale, "#{path}.#{plural_atom}", bindings)
other ->
other
end
else
do_t(locale, path, bindings)
end
end

unquote(translations)

def do_t(_locale, _path, _bindings), do: {:error, :no_translation}

def t!(locale, path, bindings \\ []) do
Expand All @@ -61,6 +84,9 @@ defmodule Linguist.Compiler do

{:error, :no_translation} ->
raise %NoTranslationError{message: "#{locale}: #{path}"}

{:error, {err, msg}} ->
raise struct(err) |> Map.put(:message, msg)
end
end

Expand All @@ -78,22 +104,6 @@ defmodule Linguist.Compiler do
deftranslations(locale, path, val)
else
quote do
def t(locale, path, bindings) do
pluralization_key = Application.fetch_env!(:linguist, :pluralization_key)

if Keyword.has_key?(bindings, pluralization_key) do
plural_atom =
bindings
|> Keyword.get(pluralization_key)
|> Cardinal.plural_rule(locale)

new_path = "#{path}.#{plural_atom}"
do_t(locale, new_path, bindings)
else
do_t(locale, path, bindings)
end
end

def do_t(unquote(locale), unquote(path), bindings) do
{:ok, unquote(interpolate(val, :bindings))}
end
Expand Down
40 changes: 37 additions & 3 deletions lib/linguist/memorized_vocabulary.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
defmodule Linguist.MemorizedVocabulary do
alias Linguist.Cldr.Number.Cardinal
alias Linguist.Compiler
alias Linguist.{LocaleError, NoTranslationError}

Expand Down Expand Up @@ -38,16 +37,31 @@ defmodule Linguist.MemorizedVocabulary do
def t(nil, _, _), do: raise(LocaleError, nil)

def t(locale, path, bindings) do
cldr = case :ets.lookup(:translations_registry, "memorized_vocabulary.cldr") do
[] ->
Application.get_env(:linguist, :cldr)

[{_, value}] ->
value
end

cardinal = "#{cldr}.Number.Cardinal" |> String.to_atom()

pluralization_key = Application.fetch_env!(:linguist, :pluralization_key)
norm_locale = normalize_locale(locale)

if Keyword.has_key?(bindings, pluralization_key) do
plural_atom =
bindings
|> Keyword.get(pluralization_key)
|> Cardinal.plural_rule(norm_locale)
|> cardinal.plural_rule(norm_locale)

do_t(norm_locale, "#{path}.#{plural_atom}", bindings)
case plural_atom do
value when is_atom(value) ->
do_t(norm_locale, "#{path}.#{plural_atom}", bindings)
other ->
other
end
else
do_t(norm_locale, path, bindings)
end
Expand All @@ -60,6 +74,9 @@ defmodule Linguist.MemorizedVocabulary do

{:error, :no_translation} ->
raise %NoTranslationError{message: "#{locale}: #{path}"}

{:error, {err, msg}} ->
raise struct(err) |> Map.put(:message, msg)
end
end

Expand Down Expand Up @@ -113,6 +130,23 @@ defmodule Linguist.MemorizedVocabulary do
end)
end

@doc """
Sets Cldr backend for handling locales
* value - Module initialized with `use Cldr` macros
Examples
cldr MyProject.Cldr
"""
def cldr(value) do

:ets.insert(
:translations_registry,
{"memorized_vocabulary.cldr", value}
)
end

@doc """
Embeds locales from provided source
Expand Down
9 changes: 7 additions & 2 deletions lib/linguist/vocabulary.ex
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,21 @@ defmodule Linguist.Vocabulary do
@doc """
Compiles all the translations and inject the functions created in the current module.
"""
defmacro __using__(_options) do
defmacro __using__(options \\ []) do
cldr = Keyword.get(options, :cldr, Application.get_env(:linguist, :cldr))
quote do
Module.register_attribute(__MODULE__, :locales, accumulate: true, persist: false)

Module.register_attribute(__MODULE__, :cldr, accumulate: false, persist: false)
Module.put_attribute(__MODULE__, :cldr, unquote(cldr))

import unquote(__MODULE__)
@before_compile unquote(__MODULE__)
end
end

defmacro __before_compile__(env) do
Compiler.compile(Module.get_attribute(env.module, :locales))
Compiler.compile(Module.get_attribute(env.module, :locales), Module.get_attribute(env.module, :cldr))
end

@doc """
Expand Down
64 changes: 63 additions & 1 deletion test/memorized_vocabulary_test.exs
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
defmodule MemorizedVocabularyTest do
use ExUnit.Case

defmodule Ru.Cldr do
use Cldr,
providers: [],
default_locale: "en",
locales: ["ru", "en"]
end

setup do
Linguist.MemorizedVocabulary.locale("es", Path.join([__DIR__, "es.yml"]))
Linguist.MemorizedVocabulary.locale("fr-FR", Path.join([__DIR__, "fr-FR.yml"]))
Linguist.MemorizedVocabulary.locale("ru", Path.join([__DIR__, "ru.yml"]))
:ok
end

test "locales() returns locales" do
assert ["fr-FR", "es"] == Linguist.MemorizedVocabulary.locales()
assert ["ru", "fr-FR", "es"] == Linguist.MemorizedVocabulary.locales()
end

test "t returns a translation" do
Expand All @@ -25,6 +33,14 @@ defmodule MemorizedVocabularyTest do
)
end

test "it handles unknown locales" do
assert Linguist.MemorizedVocabulary.t!("ru", "simple") == "простое"
assert Linguist.MemorizedVocabulary.t("ru", "simple") == {:ok, "простое"}

assert Linguist.MemorizedVocabulary.t!("ru", "interpolate", value: "значения") == "интерполяция значения"
assert Linguist.MemorizedVocabulary.t("ru", "interpolate", value: "значения") == {:ok, "интерполяция значения"}
end

test "t returns {:error, :no_translation} when translation is missing" do
assert Linguist.MemorizedVocabulary.t("es", "flash.not_exists") == {:error, :no_translation}
end
Expand Down Expand Up @@ -52,4 +68,50 @@ defmodule MemorizedVocabularyTest do
Linguist.MemorizedVocabulary.t(nil, "flash.notice.alert")
end
end

test "t pluralized returns an error for unknown locale" do
assert Linguist.MemorizedVocabulary.t("ru", "countable", count: 1) |> elem(0) == :error
end

test "t! pluralized throws an error for unknown locale" do
assert_raise Cldr.UnknownLocaleError, fn ->
assert Linguist.MemorizedVocabulary.t!("ru", "countable", count: 1)
end
end

test "t! pluralizes unknown locale with custom Cldr backend" do
:ets.delete(:translations_registry, "memorized_vocabulary.cldr")

Linguist.MemorizedVocabulary.cldr(Ru.Cldr)

assert Linguist.MemorizedVocabulary.t!("ru", "countable", count: 1) == "1 элемент"
assert Linguist.MemorizedVocabulary.t!("ru", "countable", count: 2) == "2 элемента"
assert Linguist.MemorizedVocabulary.t!("ru", "countable", count: 3) == "3 элемента"
assert Linguist.MemorizedVocabulary.t!("ru", "countable", count: 4) == "4 элемента"
assert Linguist.MemorizedVocabulary.t!("ru", "countable", count: 5) == "5 элементов"
assert Linguist.MemorizedVocabulary.t!("ru", "countable", count: 6) == "6 элементов"
assert Linguist.MemorizedVocabulary.t!("ru", "countable", count: 7) == "7 элементов"
assert Linguist.MemorizedVocabulary.t!("ru", "countable", count: 8) == "8 элементов"
assert Linguist.MemorizedVocabulary.t!("ru", "countable", count: 9) == "9 элементов"
assert Linguist.MemorizedVocabulary.t!("ru", "countable", count: 10) == "10 элементов"
:ets.delete(:translations_registry, "memorized_vocabulary.cldr")
end

test "t pluralizes unknown locale with custom Cldr backend" do
:ets.delete(:translations_registry, "memorized_vocabulary.cldr")

Linguist.MemorizedVocabulary.cldr(Ru.Cldr)

assert Linguist.MemorizedVocabulary.t("ru", "countable", count: 1) == {:ok, "1 элемент"}
assert Linguist.MemorizedVocabulary.t("ru", "countable", count: 2) == {:ok, "2 элемента"}
assert Linguist.MemorizedVocabulary.t("ru", "countable", count: 3) == {:ok, "3 элемента"}
assert Linguist.MemorizedVocabulary.t("ru", "countable", count: 4) == {:ok, "4 элемента"}
assert Linguist.MemorizedVocabulary.t("ru", "countable", count: 5) == {:ok, "5 элементов"}
assert Linguist.MemorizedVocabulary.t("ru", "countable", count: 6) == {:ok, "6 элементов"}
assert Linguist.MemorizedVocabulary.t("ru", "countable", count: 7) == {:ok, "7 элементов"}
assert Linguist.MemorizedVocabulary.t("ru", "countable", count: 8) == {:ok, "8 элементов"}
assert Linguist.MemorizedVocabulary.t("ru", "countable", count: 9) == {:ok, "9 элементов"}
assert Linguist.MemorizedVocabulary.t("ru", "countable", count: 10) == {:ok, "10 элементов"}
:ets.delete(:translations_registry, "memorized_vocabulary.cldr")
end
end
8 changes: 8 additions & 0 deletions test/ru.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
simple: "простое"
interpolate: "интерполяция %{value}"
countable:
one: "%{count} элемент"
few: "%{count} элемента"
many: "%{count} элементов"
other: "%{count} элементов"
Loading

0 comments on commit 831ad18

Please sign in to comment.