-
Notifications
You must be signed in to change notification settings - Fork 84
/
default.ex
238 lines (191 loc) · 8.8 KB
/
default.ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
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