View Source Localized Routes with Routex
A core feature of Routex is to enable Localized Routes in Phoenix. Optionally with translated URLs, enhancing user engagement and content relevance.
In this tutorial we will explain how multiple extensions are combined to...
- have a product page with regional URL's
- (optional) use translated routes
- using automatically localized verified routes
- display links to other locales
All without changing a single route in your templates!
⇒ /products/:id/edit @loc.locale = "en_US"
/products/:id/edit ⇒ /eu/nederland/producten/:id/bewerken @loc.locale = "nl_NL"
⇒ /eu/espana/producto/:id/editar @loc.locale = "es_ES"
⇒ /gb/products/:id/edit @loc.locale = "en_GB"This tutorial assumes you have followed the usage guide to setup Routex.
If you encounter any issues with Routex or this tutorial, feel free to open a topic at Elixir Forums or create an issue at GitHub.
What we start with
This tutorial uses an example Router with multiple routes to the product page. The route.ex file contains something like the example below.
preprocess_using ExampleWeb.RoutexBackend do
scope "/", ExampleWeb do
pipe_through :browser
live "/products", ProductLive.Index, :index
live "/products/new", ProductLive.Index, :new
live "/products/:id/edit", ProductLive.Index, :edit
live "/products/:id", ProductLive.Show, :show
live "/products/:id/show/edit", ProductLive.Show, :edit
end
endWhen you run mix phx.routes you will see those routes as:
product_show_path GET /products/:id/show/edit ExampleWeb.ProductLive.Show :edit
product_show_path GET /products/:id ExampleWeb.ProductLive.Show :show
product_index_path GET /products/:id/edit ExampleWeb.ProductLive.Index :edit
product_index_path GET /products/new ExampleWeb.ProductLive.Index :new
product_index_path GET /products ExampleWeb.ProductLive.Index :indexYou want these pages to be accessible from multiple (translated) URLs.
Step 1: Generate alternative URLs
The Routex.Extension.Alternatives generates alternative routes. Add it to the list of extensions
and provide a minimal configuration.
use Routex.Backend,
extensions: [
Routex.Extension.AttrGetters, # required
+ Routex.Extension.Alternatives,
],
+ alternatives: %{
+ "/" => %{
+ branches: %{
+ "/europe" => %{
+ branches: %{
+ "/nl" => %{},
+ "/be" => %{}
+ }
+ },
+ "/gb" => %{}
+ }
+ }
+ }You can confirm it works by running mix phx.routes. It now shows a lot more
routes as alternatives are generated for each route within the
preprocess_using block. For example the route to /products/:id/show/edit has
multiple alternatives.
product_show_path GET /products/:id/show/edit ExampleWeb.ProductLive.Show :edit
product_show_europe_path GET /europe/products/:id/show/edit ExampleWeb.ProductLive.Show :edit
product_show_europe_be_path GET /europe/be/products/:id/show/edit ExampleWeb.ProductLive.Show :edit
product_show_europe_nl_path GET /europe/nl/products/:id/show/edit ExampleWeb.ProductLive.Show :edit
product_show_gb_path GET /gb/products/:id/show/edit ExampleWeb.ProductLive.Show :editAs you can see the routes are still in the English language; we need another extension to translate them
(optional) Step 2: Translate the alternative routes
The Routex.Extension.Translation makes routes translatable by splitting the route
into segments (e.g. ["products", "show", "edit"]) and extracting these
segments to a routes.po file for translation. You might recognize the .po
extension from your Phoenix project; it's the extension used by Gettext. Gettext
is a standard for i18n in different communities, meaning there is a great set of
tooling for developers and translators. This also means your routes segments can be
translated with the same tooling as used for all other translations in Phoenix!
Add the extension and it's minimal configuration.
use Routex.Backend,
extensions: [
Routex.Extension.AttrGetters, # required
Routex.Extension.Alternatives,
+ Routex.Extension.Translations,
],
alternatives: %{...},
+ translations_backend: ExampleWeb.Gettext,As Routex need to know which translation to use for what route, we need to set
an attribute :locale or :language per alternative.
Luckily this is covered by Extension.Alternatives as it supports setting the
:attrs key per branch. Let's extend the alternatives configuration with by
setting the :locale attribute. While we are add it, we also give the branches
a :display_name attribute.
alternatives: %{
"/" => %{
+ attrs: %{locale: "en-150", display_name: "Global"},
branches: %{
"/europe" => %{
+ attrs: %{locale: "en-150", display_name: "Europe"},
branches: %{
+ "/nl" => %{attrs: %{locale: "nl_NL", display_name: "The Netherlands"}},
+ "/be" => %{attrs: %{locale: "nl_BE", display_name: "Belgium"}}
}
},
+ "/gb" => %{attrs: %{locale: "en-150", display_name: "Great Britain"}}
}
}
}If this is the first time you add translations in your project, you need to generate the folder structure which Gettext can use to detect languages to translate to. We need two languages in this tutorial: 'en' and 'nl'. As 'en' is the default for routes we only need to create translations for 'nl'.
mix gettext.extract
mix gettext.merge priv/gettext --locale nlYou should see a message that Gettext has generated new translation files which
can be found in the priv/gettext/nl folder
priv/
gettext/
nl/
LC_MESSAGES/
default.po # phoenix translations
routes.po # routex translationsNow you can translate the segments by opening the routes.po file with your
favorite .po editor. Here are a few suggestions:
GNU Emacs (with po-mode): Linux, MacOSX, and Windows.Lokalize: runs on KDE desktop for Linux (replacement for KBabel; formerly known as KAider)Poedit: Linux, MacOSX, and WindowsOmegaT: Linux, MacOSX, and WindowsVim: Linux, MacOSX, and Windows with PO ftplugin for easier editing of GNU gettext PO files.gted plugin for Eclipse: (if you are already using Eclipse)gtranslator: Linux/GnomeVirtaal: Windows, Mac (Beta version)
Once you have translated the route segments, list all routes using mix phx.routes. You will see some routes have been translated. We are getting
there!
product_show_path GET /products/:id/show/edit
product_show_europe_path GET /europe/products/:id/show/edit
product_show_europe_be_path GET /europe/be/producten/:id/toon/bewerken
product_show_europe_nl_path GET /europe/nl/producten/:id/toon/bewerkenNow we have the routes it would be nice if users stay within their locale
while browsing pages.
Step 3: Dynamic links in your application
When you start your app with mix phx.server and you visit a 'localized' page
such as /europe/nl/producten, you will notice that every link on the page will
bring you back to the non-locale route. In the code the path of the link is written
like ~p"/products". It would be nice if instead of always rendering a link to
/products, Phoenix would instead render a localized link. This is done by
Routex.Extension.VerifiedRoutes.
Note In older Phoenix applications you might find something like
ExampleAppWeb.Router.Helpers.product_path(conn_or_endpoint, :show, "hello"). These are Phoenix Router Helpers and those are deprecated in favor of the Verified Routes using~p"/my_path". When you can't migrate, you can useRoutex.Extension.RouteHelpersinstead ofRoutex.Extension.VerifiedRoutes.
You might already have guessed it: we are gonna add the extension and some configuration to the backend.
use Routex.Backend,
extensions: [
Routex.Extension.AttrGetters, # required
Routex.Extension.Alternatives,
Routex.Extension.Translations,
+ Routex.Extension.VerifiedRoutes,
],
alternatives: %{...},
translations_backend: ExampleWeb.Gettext,
+ verified_sigil_routex: "~p",
+ verified_sigil_phoenix: "~o",
+ verified_url_routex: :url,
+ verified_path_routex: :pathBy default the extension uses non-standard macro names. As we want to have dynamic routes throughout our application, we choose to override the names used by Phoenix in your application and rename the originals. This way you do not need to modify all your templates. Convenient.
To not have duplicated imports, add this to your routex_helpers in example_web.ex
def routex_helpers do
quote do
+ import Phoenix.VerifiedRoutes,
+ except: [sigil_p: 2, url: 1, url: 2, url: 3, path: 2, path: 3]
import unquote(__MODULE__).Router.RoutexHelpers, only: :macros
alias unquote(__MODULE__).Router.RoutexHelpers, as: Routes
end
endNow when you start your app with mix phx.server you will notice an explanation
is printed about the usage of Routex Verified Routes. This informs other
developers of the overrides.
Due to the configuration in module `ExampleWeb.RoutexBackend` one or multiple
Routex variants use the default name of their native Phoenix equivalents. The native
macro's, sigils or functions have been renamed.
Native | Routex
-----------------------------------------
~o | ~p
url_phx | url
path_phx | path
Documentation: https://hexdocs.pm/routex/extensions/verified_routes.htmlWhen you visit a 'localized' page such as /europe/nl/producten you will notice
that every link on the page will keep you within the localized environment
/europe/nl/. Keeping users in a localized environment is great, but giving
them an option to switch to another locale would be even better.
Let's empower our visitors!
Step 4: Show alternative pages to the user
The Routex.Extension.AlternativeGetters generates function at compile time to
dynamically fetch alternative routes for the current url without overhead. Let's
once again add the extension.
use Routex.Backend,
extensions: [
Routex.Extension.AttrGetters, # required
Routex.Extension.Alternatives,
Routex.Extension.Translations,
Routex.Extension.VerifiedRoutes,
+ Routex.Extension.AlternativeGetters,
],
alternatives: %{...},
translations_backend: ExampleWeb.Gettext,
verified_sigil_routex: "~p",
verified_sigil_phoenix: "~o",
verified_url_routex: :url,
verified_path_routex: :pathAll created functions pattern match on a given URL and return the alternatives
with a slug, the match? attribute which is true if the route pattern matches
the provided path, and attributes of the route.
iex> ExampleWeb.Router.RoutexHelpers.alternatives("https://example.com/products?search=bar#top")
[
%Routex.Extension.AlternativeGetters{
slug: "/products?search=bar#top",
attrs: %{
name: "Worldwide",
locale: "en-US",
[...]
},
match?: true
},
%Routex.Extension.AlternativeGetters{
slug: "/europe/products?search=bar#top",
attrs: %{
name: "Europe",
locale: "en-150",
[...]
},
match?: false
},
%Routex.Extension.AlternativeGetters{
slug: "/europe/be/producten?search=bar#top",
attrs: %{
name: "Belgium",
locale: "nl-BE",
[...]
},
match?: false
},
[...]As Routex automatically assigns the current url to @url we have all
ingredients instantly available in our templates! It becomes a matter of looping
over the results to generate links.
<!-- alternatives/1 is located in ExampleWeb.Router.RoutexHelpers which is aliased as Routes -->
<.link
:for={alternative <- Routes.alternatives(@url)}
class="button"
rel="alternate"
hreflang={alternative.attrs.locale}
patch={alternative.slug}
>
<.button class={(alternative.match? && "bg-[#FD4F00]") || ""}>
<%= alternative.attrs.display_name %>
</.button>
</.link>Conclusion
In this tutorial you have learned how to create localized routes for your
Phoenix application using multiple extensions and how to add custom attributes
(such as :locale and :display_name) to these routes. There are a few more
extension you can add to the mix for extra flexibility and convenience, such as:
- Routex.Extension.Interpolation - Use any attribute to customize routes (e.g. "/#{locale}/products/#{display_name}/:id/edit")
- Routex.Extension.Assigns - Use any attribute in your templates using
@notation. - Routex.Extension.AttrGetters - Lazy load attributes
If you encounter any issues with Routex or this tutorial, feel free to open a topic at Elixir Forums or create an issue at GitHub.
Have a nice day!