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 4bb04e9 commit 94b1ea0
Show file tree
Hide file tree
Showing 8 changed files with 306 additions and 23 deletions.
56 changes: 53 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
```elixir
defmodule I18n do
use Linguist.Vocabulary

locale "en", [
flash: [
notice: [
Expand All @@ -22,7 +22,7 @@ defmodule I18n do
]
]
]

locale "fr", Path.join([__DIR__, "fr.exs"])

end
Expand All @@ -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
20 changes: 14 additions & 6 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,13 +40,16 @@ defmodule Linguist.Compiler do
@escaped_interpol_rgx ~r/%%{/
@simple_interpol "%{"

def compile(translations) do
def compile(translations, cldr \\ nil) do
langs = translations |> Enum.reduce([], fn item, acc ->
case item do
{name, _paths} -> acc ++ [to_string(name)]
_ -> acc
end
end)
cldr = cldr || Application.fetch_env!(:linguist, :cldr)

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

translations =
for {locale, source} <- translations do
Expand All @@ -68,15 +70,18 @@ defmodule Linguist.Compiler do
plural_atom =
bindings
|> Keyword.get(pluralization_key)
|> Cardinal.plural_rule(locale)
|> unquote(cardinal).plural_rule(locale)

new_path = "#{path}.#{plural_atom}"
do_t(locale, new_path, bindings)
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}
Expand All @@ -88,6 +93,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 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 @@ -42,16 +41,31 @@ defmodule Linguist.MemorizedVocabulary do
end

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 @@ -64,6 +78,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 @@ -117,6 +134,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 94b1ea0

Please sign in to comment.