I’m attempting to create localized routes like so:
@supported_locales ["en-us", "en-gb"]
scope "/", AppWeb do
pipe_through :browser
for supported_locale <- @supported_locales do
get "/#{supported_locale}", PageController, :home
get "/#{supported_locale}/features", PageController, :features
get "/#{supported_locale}/pricing", PageController, :pricing
end
end
All is good! But there’s a problem! I got the following error when I tried to use ~p.
no route path for AppWeb.Router matches "/#{@page_locale}/pricing"
You‘d need to wrap it at compile time like you created the routes at compile time. Once your routes are in the router they don‘t have a variable path prefix. There just remains a bunch if hardcoded paths, which need to be supplied as hardcoded paths for validation as well.
in ex_cldr_routes I introduced a sigil_q to do localised verified routes. In the end, it generates a case expression that, as @LostKobrakai says, detects the locale at runtime and uses that to decide which verified route to use.
You can see the implementation here, perhaps it will help define your own solution.
The docs have a description of how that all works together.
I suggest start with the doc link I posted above, and then look at the implementation of sigil_q, Happy to answer any questions here, on slack, or DM me.
After some readings on macros (this is my first time writing macros) and the implementation of ex_cldr_routes ~q, here’s a working solution that I got:
defmodule VerifiedLocalizedRoutes do
@moduledoc false
defmacro __using__(opts) do
quote location: :keep do
unquote(__MODULE__).__using__(__MODULE__, unquote(opts))
import unquote(__MODULE__), only: :macros
end
end
def __using__(mod, opts) do
Module.put_attribute(mod, :locales, Keyword.fetch!(opts, :locales))
Module.put_attribute(mod, :get_locale, Keyword.fetch!(opts, :get_locale))
end
defmacro sigil_q({:<<>>, _meta, _segments} = route, flags) do
locales = attr!(__CALLER__, :locales)
get_locale = attr!(__CALLER__, :get_locale)
case_clauses =
sigil_q_case_clauses(route, flags, locales)
quote location: :keep do
case unquote(get_locale).() do
unquote(case_clauses)
end
end
end
defp interpolate_path(path, locale) do
Macro.prewalk(path, fn
segment when is_binary(segment) ->
String.replace(segment, ":locale", locale)
other ->
other
end)
end
defp attr!(env, name) do
Module.get_attribute(env.module, name) || raise "expected @#{name} module attribute to be set"
end
defp sigil_q_case_clauses(route, flags, locales) do
for locale <- locales do
route_path = interpolate_path(route, locale)
quote location: :keep do
unquote(locale) -> sigil_p(unquote(route_path), unquote(flags))
end
end
|> Enum.reject(&is_nil/1)
|> Enum.map(&hd/1)
end
end
Usage Example:
use VerifiedLocalizedRoute,
locales: ["en-us", "en-gb"],
get_locale: &get_locale/0
Just a small matter of hygiene - you are strongly encouraged to use your own top-level module names, not that of another lib. ie replace Phoenix with a top level name of your own.
Just in case somebody is looking for a full example on how to use above module (assuming app named “Hello”):
# lib/hello_web.ex
def verified_routes do
quote do
use Phoenix.VerifiedRoutes,
endpoint: HelloWeb.Endpoint,
router: HelloWeb.Router,
statics: HelloWeb.static_paths()
# Add this after Phoenix.VerifiedRoutes
use HelloWeb.VerifiedLocalizedRoutes,
locales: ["en", "ru"],
get_locale: &Gettext.get_locale/0
end
end
# lib/hello_web/router.ex
scope "/:locale" do
pipe_through :browser
get "/pages", PageController, :index
end
# *.heex
<.link navigate={~q"/:locale/pages"}>Pages</.link>
# resulting HTML:
<a href="/en/pages">Pages</a>