diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f4a2691..9085d57 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,15 +9,18 @@ on: jobs: test: name: Elixir ${{matrix.elixir}} / OTP ${{matrix.otp}} - runs-on: ubuntu-latest + runs-on: ubuntu-18.04 strategy: matrix: - include: - - elixir: '1.11' - otp: '23' - - elixir: '1.10' - otp: '22' + elixir: + - "1.9" + - "1.10" + - "1.11" + otp: + - "21.0" + - "22.0.2" + - "23.0" steps: - name: Checkout @@ -37,8 +40,6 @@ jobs: restore-keys: | ${{ runner.os }}-mix-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('**/mix.lock') }} ${{ runner.os }}-mix-${{ matrix.otp }}-${{ matrix.elixir }} - ${{ runner.os }}-mix-${{ matrix.otp }} - ${{ runner.os }}-mix - name: Restore _build cache uses: actions/cache@v2 @@ -48,8 +49,6 @@ jobs: restore-keys: | ${{ runner.os }}-build-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('**/mix.lock') }} ${{ runner.os }}-build-${{ matrix.otp }}-${{ matrix.elixir }} - ${{ runner.os }}-build-${{ matrix.otp }} - ${{ runner.os }}-build - name: Install Dependencies run: | @@ -60,4 +59,10 @@ jobs: - name: Run unit tests run: | mix clean - mix test \ No newline at end of file + mix test + + - name: Run unit tests with persistent_term backend + run: mix test + if: matrix.otp != '21.0' + env: + SCHEMA_PROVIDER: persistent_term \ No newline at end of file diff --git a/README.md b/README.md index a77a237..79bcd63 100644 --- a/README.md +++ b/README.md @@ -48,9 +48,22 @@ iex> I18n.t!("en", "users.title") ## Configuration +### Pluralization key + The key to use for pluralization is configurable, and should likely be an atom: ```elixir config :linguist, pluralization_key: :count ``` + will cause the system to pluralize based on the `count` parameter passed to the `t` function. + +### `:persistent_term` support + +Also you can use [`:persistent_term`](https://erlang.org/doc/man/persistent_term.html) backend instead of `:ets` in `Linguist.MemoizedVocabulary` by setting up: + +```elixir +config :linguist, vocabulary_backend: :persistent_term +``` + +**This is only available on OTP >= 21.2** diff --git a/config/config.exs b/config/config.exs index eca6aa4..10cb9c4 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,9 +1,9 @@ use Mix.Config -config :linguist, pluralization_key: :count - config :ex_cldr, json_library: Jason if Mix.env() == :test do config :linguist, Linguist.Cldr, locales: ["fr", "en", "es"] + + config :linguist, vocabulary_backend: (System.get_env("SCHEMA_PROVIDER") || "ets") |> String.to_existing_atom() end diff --git a/lib/linguist/compiler.ex b/lib/linguist/compiler.ex index 3811825..adf95a9 100644 --- a/lib/linguist/compiler.ex +++ b/lib/linguist/compiler.ex @@ -62,12 +62,10 @@ defmodule Linguist.Compiler do end def t(locale, path, bindings) do - pluralization_key = Application.fetch_env!(:linguist, :pluralization_key) - - if Keyword.has_key?(bindings, pluralization_key) do + if Keyword.has_key?(bindings, @pluralization_key) do plural_atom = bindings - |> Keyword.get(pluralization_key) + |> Keyword.get(@pluralization_key) |> Cardinal.plural_rule(locale) new_path = "#{path}.#{plural_atom}" @@ -76,7 +74,7 @@ defmodule Linguist.Compiler do do_t(locale, path, bindings) end end - + unquote(translations) def do_t(_locale, _path, _bindings), do: {:error, :no_translation} diff --git a/lib/linguist/memorized_vocabulary.ex b/lib/linguist/memorized_vocabulary.ex index c9662f8..171cb5f 100644 --- a/lib/linguist/memorized_vocabulary.ex +++ b/lib/linguist/memorized_vocabulary.ex @@ -7,6 +7,50 @@ defmodule Linguist.MemorizedVocabulary do defexception [:message] end + @pluralization_key Application.get_env(:linguist, :pluralization_key, :count) + + if Application.get_env(:linguist, :vocabulary_backend, :ets) == :persistent_term do + if not Code.ensure_loaded?(:persistent_term) do + raise("You've set up linguist to use :persistent_term backend, but it is available only if OTP >= 21.2") + end + + def create_backend() do + end + + def add_to_backend(key, value) do + :persistent_term.put({__MODULE__, key}, value) + end + + def remove_from_backend(key) do + :persistent_term.erase({__MODULE__, key}) + end + + def get_from_backend(key) do + :persistent_term.get({__MODULE__, key}, nil) + end + else + def create_backend() do + if :ets.info(__MODULE__) == :undefined do + :ets.new(__MODULE__, [:named_table, :set, :protected]) + end + end + + def add_to_backend(key, value) do + :ets.insert(__MODULE__, {key, value}) + end + + def remove_from_backend(key) do + :ets.delete(__MODULE__, key) + end + + def get_from_backend(key) do + case :ets.lookup(__MODULE__, key) |> List.first() do + {_, value} -> value + nil -> nil + end + end + end + @moduledoc """ Defines lookup functions for given translation locales, binding interopolation @@ -42,13 +86,12 @@ defmodule Linguist.MemorizedVocabulary do end def t(locale, path, bindings) do - pluralization_key = Application.fetch_env!(:linguist, :pluralization_key) norm_locale = normalize_locale(locale) - if Keyword.has_key?(bindings, pluralization_key) do + if Keyword.has_key?(bindings, @pluralization_key) do plural_atom = bindings - |> Keyword.get(pluralization_key) + |> Keyword.get(@pluralization_key) |> Cardinal.plural_rule(norm_locale) do_t(norm_locale, "#{path}.#{plural_atom}", bindings) @@ -69,11 +112,13 @@ defmodule Linguist.MemorizedVocabulary do # sobelow_skip ["DOS.StringToAtom"] defp do_t(locale, translation_key, bindings) do - case :ets.lookup(:translations_registry, "#{locale}.#{translation_key}") do - [] -> + result = get_from_backend("#{locale}.#{translation_key}") + + case result do + nil -> {:error, :no_translation} - [{_, string}] -> + string -> translation = Compiler.interpol_rgx() |> Regex.split(string, on: [:head, :tail]) @@ -92,28 +137,22 @@ defmodule Linguist.MemorizedVocabulary do end def locales do - tuple = - :ets.lookup(:translations_registry, "memorized_vocabulary.locales") - |> List.first() - - if tuple do - elem(tuple, 1) - end + get_from_backend("memorized_vocabulary.locales") || [] end def add_locale(name) do - current_locales = locales() || [] + current_locales = locales() + new_locales = [name | current_locales] |> Enum.uniq() - :ets.insert( - :translations_registry, - {"memorized_vocabulary.locales", [name | current_locales]} - ) + add_to_backend("memorized_vocabulary.locales", new_locales) end def update_translations(locale_name, loaded_source) do + create_backend() + loaded_source |> Enum.map(fn {key, translation_string} -> - :ets.insert(:translations_registry, {"#{locale_name}.#{key}", translation_string}) + add_to_backend("#{locale_name}.#{key}", translation_string) end) end @@ -139,10 +178,6 @@ defmodule Linguist.MemorizedVocabulary do will not work as expected if called directly. """ def _load_yaml_file(source) do - if :ets.info(:translations_registry) == :undefined do - :ets.new(:translations_registry, [:named_table, :set, :protected]) - end - {decode_status, [file_data]} = YamlElixir.read_all_from_file(source) if decode_status != :ok do diff --git a/lib/linguist/vocabulary.ex b/lib/linguist/vocabulary.ex index 3a90ca2..573469f 100644 --- a/lib/linguist/vocabulary.ex +++ b/lib/linguist/vocabulary.ex @@ -36,6 +36,7 @@ defmodule Linguist.Vocabulary do defmacro __using__(_options) do quote do Module.register_attribute(__MODULE__, :locales, accumulate: true, persist: false) + @pluralization_key Application.get_env(:linguist, :pluralization_key, :count) import unquote(__MODULE__) @before_compile unquote(__MODULE__) end