Packages
gettext
0.22.2
1.0.2
1.0.1
retired
1.0.0
0.26.2
0.26.1
0.26.0
retired
0.25.0
0.24.0
0.23.1
0.23.0
0.22.3
0.22.2
0.22.1
0.22.0
0.21.0
0.20.0
0.19.1
0.19.0
0.18.2
0.18.1
0.18.0
0.17.4
0.17.3
0.17.2
0.17.1
0.17.0
0.16.1
0.16.0
0.15.0
0.14.1
0.14.0
0.13.1
0.13.0
0.12.2
0.12.1
0.12.0
0.11.0
0.10.0
0.9.0
0.8.0
0.7.0
0.6.1
0.6.0
0.5.0
Internationalization and localization through gettext
Current section
Files
Jump to
Current section
Files
lib/gettext/interpolation/default.ex
defmodule Gettext.Interpolation.Default do
@moduledoc """
Default implementation for the `Gettext.Interpolation` behaviour.
Replaces `%{binding_name}` with the string value of the `binding_name` binding.
"""
@behaviour Gettext.Interpolation
@typedoc """
Something that can be interpolated.
It's either a string (a literal) or an atom (representing a binding name).
"""
@type interpolatable() :: [String.t() | atom()]
# Extracts interpolations from a given string.
# This function extracts all interpolations in the form `%{interpolation}`
# contained inside `str`, converts them to atoms and then returns a list of
# string and interpolation keys.
@doc false
@spec to_interpolatable(String.t()) :: interpolatable()
def to_interpolatable(string) when is_binary(string) do
start_pattern = :binary.compile_pattern("%{")
end_pattern = :binary.compile_pattern("}")
string
|> to_interpolatable(_current = "", _acc = [], start_pattern, end_pattern)
|> Enum.reverse()
end
defp to_interpolatable(string, current, acc, start_pattern, end_pattern) do
case :binary.split(string, start_pattern) do
# If we have one element, no %{ was found so this is the final part of the
# string.
[rest] ->
prepend_if_not_empty(current <> rest, acc)
# If we found a %{ but it's followed by an immediate }, then we just
# append %{} to the current string and keep going.
[before, "}" <> rest] ->
new_current = current <> before <> "%{}"
to_interpolatable(rest, new_current, acc, start_pattern, end_pattern)
# Otherwise, we found the start of a binding.
[before, binding_and_rest] ->
case :binary.split(binding_and_rest, end_pattern) do
# If we don't find the end of this binding, it means we're at a string
# like "foo %{ no end". In this case we consider no bindings to be
# there.
[_] ->
[current <> string | acc]
# This is the case where we found a binding, so we put it in the acc
# and keep going.
[binding, rest] ->
new_acc = [String.to_atom(binding) | prepend_if_not_empty(before, acc)]
to_interpolatable(rest, "", new_acc, start_pattern, end_pattern)
end
end
end
defp prepend_if_not_empty("", list), do: list
defp prepend_if_not_empty(string, list), do: [string | list]
@doc """
Interpolate a message or interpolatable with the given bindings.
Implementation of the `c:Gettext.Interpolation.runtime_interpolate/2` callback.
This function takes a message and some bindings and returns an `{:ok,
interpolated_string}` tuple if interpolation is successful. If it encounters
a binding in the message that is missing from `bindings`, it returns
`{:missing_bindings, incomplete_string, missing_bindings}` where
`incomplete_string` is the string with only the present bindings interpolated
and `missing_bindings` is a list of atoms representing bindings that are in
`interpolatable` but not in `bindings`.
## Examples
iex> msgid = "Hello %{name}, you have %{count} unread messages"
iex> good_bindings = %{name: "José", count: 3}
iex> Gettext.Interpolation.Default.runtime_interpolate(msgid, good_bindings)
{:ok, "Hello José, you have 3 unread messages"}
iex> Gettext.Interpolation.Default.runtime_interpolate(msgid, %{name: "José"})
{:missing_bindings, "Hello José, you have %{count} unread messages", [:count]}
iex> msgid = "Hello %{name}, you have %{count} unread messages"
iex> interpolatable = Gettext.Interpolation.Default.to_interpolatable(msgid)
iex> good_bindings = %{name: "José", count: 3}
iex> Gettext.Interpolation.Default.runtime_interpolate(interpolatable, good_bindings)
{:ok, "Hello José, you have 3 unread messages"}
iex> Gettext.Interpolation.Default.runtime_interpolate(interpolatable, %{name: "José"})
{:missing_bindings, "Hello José, you have %{count} unread messages", [:count]}
"""
@impl true
def runtime_interpolate(message, bindings)
def runtime_interpolate(message, %{} = bindings) when is_binary(message) do
message |> to_interpolatable() |> runtime_interpolate(bindings)
end
def runtime_interpolate(interpolatable, %{} = bindings) when is_list(interpolatable) do
interpolate(interpolatable, bindings, [], [])
end
defp interpolate([string | segments], bindings, strings, missing) when is_binary(string) do
interpolate(segments, bindings, [string | strings], missing)
end
defp interpolate([atom | segments], bindings, strings, missing) when is_atom(atom) do
case bindings do
%{^atom => value} ->
interpolate(segments, bindings, [to_string(value) | strings], missing)
%{} ->
strings = ["%{" <> Atom.to_string(atom) <> "}" | strings]
interpolate(segments, bindings, strings, [atom | missing])
end
end
defp interpolate([], _bindings, strings, []) do
{:ok, IO.iodata_to_binary(Enum.reverse(strings))}
end
defp interpolate([], _bindings, strings, missing) do
missing = missing |> Enum.reverse() |> Enum.uniq()
{:missing_bindings, IO.iodata_to_binary(Enum.reverse(strings)), missing}
end
# Returns all the interpolation keys contained in the given string or list of
# segments.
# This function returns a list of all the interpolation keys (patterns in the
# form `%{interpolation}`) contained in its argument.
# If the argument is a segment list, that is, a list of strings and atoms where
# atoms represent interpolation keys, then only the atoms in the list are
# returned.
@doc false
@spec keys(String.t() | interpolatable()) :: [atom()]
def keys(string_or_interpolatable)
def keys(string) when is_binary(string), do: string |> to_interpolatable() |> keys()
def keys(interpolatable) when is_list(interpolatable),
do: interpolatable |> Enum.filter(&is_atom/1) |> Enum.uniq()
@doc """
Compiles a static message to interpolate with dynamic bindings.
Implementation of the `c:Gettext.Interpolation.compile_interpolate/3` macro callback.
Takes a static message and some dynamic bindings. The generated
code will return an `{:ok, interpolated_string}` tuple if the interpolation
is successful. If it encounters a binding in the message that is missing from
`bindings`, it returns `{:missing_bindings, incomplete_string, missing_bindings}`,
where `incomplete_string` is the string with only the present bindings interpolated
and `missing_bindings` is a list of atoms representing bindings that are in
`interpolatable` but not in `bindings`.
"""
@impl true
defmacro compile_interpolate(message_type, message, bindings) do
unless is_binary(message) do
raise """
#{inspect(__MODULE__)}.compile_interpolate/2 can only be used at compile time with \
static messages. Alternatively, use #{inspect(__MODULE__)}.runtime_interpolate/2.
"""
end
interpolatable = to_interpolatable(message)
keys = keys(interpolatable)
match_clause = match_clause(keys)
compile_string = compile_string(interpolatable)
case {keys, message_type} do
# If no keys are in the message, the message can be returned without interpolation
{[], _message_type} ->
quote do: {:ok, unquote(message)}
# If the message only contains the key `count` and it is a plural message,
# gettext ensures that `count` is always set. Therefore the dynamic interpolation
# will never be needed.
{[:count], :plural_translation} ->
quote do
unquote(match_clause) = unquote(bindings)
{:ok, unquote(compile_string)}
end
{_keys, _message_type} ->
quote do
case unquote(bindings) do
unquote(match_clause) ->
{:ok, unquote(compile_string)}
%{} = other_bindings ->
unquote(__MODULE__).runtime_interpolate(unquote(interpolatable), other_bindings)
end
end
end
end
# Compiles a list of atoms into a "match" map. For example `[:foo, :bar]` gets
# compiled to `%{foo: foo, bar: bar}`. All generated variables are under the
# current `__MODULE__`.
defp match_clause(keys) do
{:%{}, [], Enum.map(keys, &{&1, Macro.var(&1, __MODULE__)})}
end
# Compiles a string into a binary with `%{var}` patterns turned into `var`
# variables, namespaced inside the current `__MODULE__`.
defp compile_string(interpolatable) do
parts =
Enum.map(interpolatable, fn
key when is_atom(key) ->
quote do: to_string(unquote(Macro.var(key, __MODULE__))) :: binary
str ->
str
end)
{:<<>>, [], parts}
end
@doc """
Implementation of `c:Gettext.Interpolation.message_format/0`.
## Examples
iex> Gettext.Interpolation.Default.message_format()
"elixir-format"
"""
@impl true
def message_format, do: "elixir-format"
end