-
Notifications
You must be signed in to change notification settings - Fork 23
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #4 from change/wbarrett/plan-dies-due-to-contact-w…
…ith-reality Add Linguist.MemorizedVocabulary module
- Loading branch information
Showing
8 changed files
with
228 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,5 @@ | ||
use Mix.Config | ||
|
||
config :linguist, pluralization_key: :count | ||
config :ex_cldr, | ||
locales: ["fr", "en", "es"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,173 @@ | ||
defmodule Linguist.MemorizedVocabulary do | ||
alias Linguist.Compiler | ||
alias Linguist.NoTranslationError | ||
alias Cldr.Number.Cardinal | ||
|
||
defmodule TranslationDecodeError do | ||
defexception [:message] | ||
end | ||
|
||
@moduledoc """ | ||
Defines lookup functions for given translation locales, binding interopolation | ||
Locales are defined with the `locale/2` function, accepting a locale name and | ||
a String path to evaluate for the translations list. | ||
For example, given the following translations : | ||
locale "en", [ | ||
flash: [ | ||
notice: [ | ||
hello: "hello %{first} %{last}", | ||
] | ||
], | ||
users: [ | ||
title: "Users", | ||
] | ||
] | ||
locale "fr", Path.join([__DIR__, "fr.exs"]) | ||
this module will respond to these functions : | ||
t("en", "flash.notice.hello", bindings \\ []), do: # ... | ||
t("en", "users.title", bindings \\ []), do: # ... | ||
t("fr", "flash.notice.hello", bindings \\ []), 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 = | ||
Cardinal.plural_rule( | ||
Keyword.get(bindings, pluralization_key), | ||
locale | ||
) | ||
|
||
do_t(locale, "#{path}.#{plural_atom}", bindings) | ||
else | ||
do_t(locale, path, bindings) | ||
end | ||
end | ||
|
||
def t!(locale, path, bindings \\ []) do | ||
case t(locale, path, bindings) do | ||
{:ok, translation} -> translation | ||
{:error, :no_translation} -> | ||
raise %NoTranslationError{message: "#{locale}: #{path}"} | ||
end | ||
end | ||
|
||
defp do_t(locale, translation_key, bindings) do | ||
case :ets.lookup(:translations_registry, "#{locale}.#{translation_key}") do | ||
[] -> {:error, :no_translation} | ||
[{_, string}] -> | ||
translation = | ||
Compiler.interpol_rgx() | ||
|> Regex.split(string, on: [:head, :tail]) | ||
|> Enum.reduce("", fn | ||
<<"%{" <> rest>>, acc -> | ||
key = String.to_atom(String.trim_trailing(rest, "}")) | ||
|
||
acc <> to_string(Keyword.fetch!(bindings, key)) | ||
segment, acc -> | ||
acc <> segment | ||
end) | ||
{:ok, translation} | ||
end | ||
end | ||
|
||
def locales do | ||
tuple = :ets.lookup(:translations_registry, "memorized_vocabulary.locales") | ||
|> List.first() | ||
if tuple do | ||
elem(tuple, 1) | ||
end | ||
end | ||
|
||
def add_locale(name) do | ||
current_locales = locales() || [] | ||
:ets.insert(:translations_registry, {"memorized_vocabulary.locales", [name | current_locales]}) | ||
end | ||
|
||
def update_translations(locale_name, loaded_source) do | ||
loaded_source | ||
|> Enum.map(fn({key, translation_string}) -> | ||
:ets.insert(:translations_registry, {"#{locale_name}.#{key}", translation_string}) | ||
end) | ||
end | ||
|
||
@doc """ | ||
Embeds locales from provided source | ||
* name - The String name of the locale, ie "en", "fr" | ||
* source - The String file path to load YAML from that returns a structured list of translations | ||
Examples | ||
locale "es", Path.join([__DIR__, "es.yml"]) | ||
""" | ||
def locale(name, source) do | ||
loaded_source = Linguist.MemorizedVocabulary._load_yaml_file(source) | ||
update_translations(name, loaded_source) | ||
add_locale(name) | ||
end | ||
|
||
@doc """ | ||
Function used internally to load a yaml file. Please use | ||
the `locale` macro with a path to a yaml file - this function | ||
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]} = Yomel.decode_file(source) | ||
if decode_status != :ok do | ||
raise %TranslationDecodeError{message: "Decode failed for file #{source}"} | ||
end | ||
|
||
%{paths: paths} = file_data | ||
|> Enum.reduce(%{paths: %{}, current_prefix: ""}, &Linguist.MemorizedVocabulary._yaml_reducer/2) | ||
paths | ||
end | ||
|
||
@doc """ | ||
Recursive function used internally for loading yaml files. | ||
Not intended for external use | ||
""" | ||
def _yaml_reducer({key, value}, acc) when is_binary(value) do | ||
key_name = if acc.current_prefix == "" do | ||
key | ||
else | ||
"#{acc.current_prefix}.#{key}" | ||
end | ||
|
||
%{ | ||
paths: Map.put(acc.paths, key_name, value), | ||
current_prefix: acc.current_prefix | ||
} | ||
end | ||
def _yaml_reducer({key, value}, acc) do | ||
next_prefix = if acc.current_prefix == "" do | ||
key | ||
else | ||
"#{acc.current_prefix}.#{key}" | ||
end | ||
|
||
reduced = Enum.reduce( | ||
value, | ||
%{ | ||
paths: acc.paths, | ||
current_prefix: next_prefix | ||
}, | ||
&Linguist.MemorizedVocabulary._yaml_reducer/2 | ||
) | ||
|
||
%{ | ||
paths: Map.merge(acc.paths, reduced.paths), | ||
current_prefix: acc.current_prefix | ||
} | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,12 @@ | ||
%{ | ||
"abnf2": {:hex, :abnf2, "0.1.2", "6f8792b8ac3288dba5fc889c2bceae9fe78f74e1a7b36bea9726ffaa9d7bef95", [:mix], [], "hexpm"}, | ||
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, | ||
"credo": {:hex, :credo, "0.9.2", "841d316612f568beb22ba310d816353dddf31c2d94aa488ae5a27bb53760d0bf", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:poison, ">= 0.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, | ||
"decimal": {:hex, :decimal, "1.5.0", "b0433a36d0e2430e3d50291b1c65f53c37d56f83665b43d79963684865beab68", [:mix], [], "hexpm"}, | ||
"ex_cldr": {:hex, :ex_cldr, "1.5.2", "5c8fe295fef680a821b9e0c19242ea34037af11eb59e6d98f194e6c9c3b4252e", [:mix], [{:abnf2, "~> 0.1", [hex: :abnf2, repo: "hexpm", optional: false]}, {:decimal, "~> 1.4", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.13", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.3", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, "~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}, {:poison, "~> 2.1 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm"}, | ||
"jason": {:hex, :jason, "1.0.0", "0f7cfa9bdb23fed721ec05419bcee2b2c21a77e926bce0deda029b5adc716fe2", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, | ||
"poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, | ||
"yamerl": {:hex, :yamerl, "0.7.0", "e51dba652dce74c20a88294130b48051ebbbb0be7d76f22de064f0f3ccf0aaf5", [:rebar3], [], "hexpm"}, | ||
"yaml_elixir": {:hex, :yaml_elixir, "2.0.0", "5d7c40e039b076c0da1921b2754d4a91bc435ac4434bef633f5506dbafd6b8f2", [:mix], [{:yamerl, "~> 0.5", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm"}, | ||
"yomel": {:hex, :yomel, "0.5.0", "c5a42d1818deda3f85ae14b1f01f6ece22b9ed8e8087012359fc04b59d85f621", [:make, :mix], [], "hexpm"}, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
defmodule MemorizedVocabularyTest do | ||
use ExUnit.Case | ||
|
||
setup do | ||
Linguist.MemorizedVocabulary.locale("es", Path.join([__DIR__, "es.yml"])) | ||
:ok | ||
end | ||
|
||
test "locales() returns locales" do | ||
assert ["es"] == Linguist.MemorizedVocabulary.locales() | ||
end | ||
|
||
test "t returns a translation" do | ||
assert {:ok, "bar"} == Linguist.MemorizedVocabulary.t("es", "foo") | ||
end | ||
|
||
test "t interpolates values" do | ||
assert {:ok, "hola Michael Westin"} == Linguist.MemorizedVocabulary.t("es", "flash.notice.hello", first: "Michael", last: "Westin") | ||
end | ||
|
||
test "t returns {:error, :no_translation} when translation is missing" do | ||
assert Linguist.MemorizedVocabulary.t("es", "flash.not_exists") == {:error, :no_translation} | ||
end | ||
|
||
test "t! raises NoTranslationError when translation is missing" do | ||
assert_raise Linguist.NoTranslationError, fn -> | ||
Linguist.MemorizedVocabulary.t!("es", "flash.not_exists") | ||
end | ||
end | ||
|
||
test "t pluralizes" do | ||
assert {:ok, "2 manzanas"} == Linguist.MemorizedVocabulary.t("es", "apple", count: 2) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters