Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
A custom `gettext_with_link` macro for easily putting inline links into gettext strings
# Has one external dependency except for Gettext: https://github.com/rrrene/html_sanitize_ex
defmodule MyApp.Gettext do
@doc """
A helper for translations with links.
Pass in the translation string which must include
`%{link_start}`/`%{link_end}`. For multiple URLs, use
`%{link_start_<0,1,2...>}`.
Pass in either a single `url_string` or tuple `{url_string,
html_attributes_string}`, or a list thereof, under the key `:link` or
`:links`, in the same order as the link start markers (i.e. the first item in
the list will be injected as `link_start_0` etc.) in the translation string.
## Usage examples:
### Single links:
iex> import MyApp.Gettext
iex> gettext_with_link("This will be %{link_start}linked to Google%{link_end}", link: {"https://google.com", ~s(target="_blank")})
{:safe, ~s|This will be <a href="https://google.com" target="_blank">linked to Google</a>|}
### Multiple links:
iex> gettext_with_link("This will be %{link_start_0}linked to Google (new tab)%{link_end_0} and %{link_start_1}this to Yahoo%{link_end_1}",
...> links: [{"https://google.com", ~s(target="_blank" class="foobar")}, "https://yahoo.com"]
...> )
{:safe, ~s|This will be <a href="https://google.com" target="_blank" class="foobar">linked to Google (new tab)</a> and <a href="https://yahoo.com">this to Yahoo</a>|}
### Links mixed with other variables:
iex> import MyApp.Gettext
iex> gettext_with_link("Read more of %{link_start}%{post_title}%{link_end}", post_title: "Hello World!", links: [{"https://example.com/myblog/post1", ~s(target="_blank")}])
{:safe, ~s|Read more of <a href="https://example.com/myblog/post1" target="_blank">Hello World!</a>|}
"""
defmacro gettext_with_link(string, opts) do
quote do
dgettext_with_link("default", unquote(string), unquote(opts))
end
end
@doc """
Same as gettext_with_link/2 but allows for choosing a domain.
"""
defmacro dgettext_with_link(domain, string, opts) do
links =
Keyword.get(opts, :links) || Keyword.get(opts, :link) ||
raise "You must pass one or more links as `:link` or `:links`"
links = List.wrap(links)
opts = Keyword.drop(opts, [:links, :link])
if length(links) > 1 do
{_, links} =
Enum.reduce(links, {0, []}, fn link_definition, {i, links} ->
{url, link_attrs} =
case link_definition do
{url, link_attrs} -> {url, link_attrs}
url -> {url, nil}
end
links =
quote do
unquote(links)
|> Keyword.put(
String.to_atom("link_start_#{unquote(i)}"),
"<a href=\"#{unquote(url)}\" #{unquote(link_attrs)}>"
)
|> Keyword.put(String.to_atom("link_end_#{unquote(i)}"), "</a>")
end
{i + 1, links}
end)
quote do
string =
dgettext(unquote(domain), unquote(string), Keyword.merge(unquote(links), unquote(opts)))
MyApp.RawHelpers.sanitized_raw(:inline, string)
end
else
link = List.first(links)
{url, link_attrs} =
case link do
{url, link_attrs} -> {url, link_attrs}
url -> {url, nil}
end
links =
quote do
[
link_start: "<a href=\"#{unquote(url)}\" #{unquote(link_attrs)}>",
link_end: "</a>"
]
end
quote do
string =
dgettext(unquote(domain), unquote(string), Keyword.merge(unquote(links), unquote(opts)))
MyApp.RawHelpers.sanitized_raw(:inline, string)
end
end
end
end
defmodule MyApp.RawHelpers do
@doc """
This function is meant as a compromise for when you want to use Phoenix.HTML.raw.
It will print the string raw, but first it will sanitize it with the chosen scrubber module.
It is NOT a replacement for sanitizing user input, but an additional safeguard.
- `:inline` - leaves safe elements that are allowed where inline elements are allowed (e.g. links, buttons, em).
Meant to be used with gettext strings that need to use <em>, <strong> etc.
- `:block` - similar to `:inline`, but also allows some block elements (e.g. h1-6, p, blockquote)
Meant to be used in emails where `raw` is necessary but `:inline` is not enough
"""
def sanitized_raw(:inline, string), do: do_sanitized_raw(string, MyApp.HTMLScrubber.Inline)
def sanitized_raw(:block, string), do: do_sanitized_raw(string, MyApp.HTMLScrubber.Block)
# sobelow_skip ["XSS.Raw"]
defp do_sanitized_raw(string, scrubber_module) do
string
|> HtmlSanitizeEx.Scrubber.scrub(scrubber_module)
|> Phoenix.HTML.raw()
end
end
defmodule MyApp.HTMLScrubber.Inline do
@moduledoc """
HTMLScrubber for the content of inline elements, for example gettext strings that include em/strong/links.
"""
require HtmlSanitizeEx.Scrubber.Meta
alias HtmlSanitizeEx.Scrubber.Meta
@valid_schemes ["http", "https", "mailto"]
Meta.remove_cdata_sections_before_scrub()
Meta.strip_comments()
for tag <- ~w(span strong em b i br) do
Meta.allow_tag_with_these_attributes(unquote(tag), [])
end
Meta.allow_tag_with_these_attributes("button", [
"id",
"class",
"style",
"aria-label",
"aria-hidden"
])
Meta.allow_tag_with_these_attributes(
"a",
[
"id",
"title",
"target",
"class",
"style",
"aria-label",
"aria-hidden"
]
)
Meta.allow_tag_with_uri_attributes("a", ["href"], @valid_schemes)
Meta.strip_everything_not_covered()
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment