This guide covers compile-time route localization using the localize/1 macro and verified localized routes using the ~q sigil.
Prerequisites
Localized routing requires a Gettext backend. Path segments are translated at compile time using Gettext.dgettext/3 with the "routes" domain. Only locales defined in the Gettext backend can have localized routes.
# mix.exs
def deps do
[
{:localize_web, "~> 0.1.0"},
{:gettext, "~> 1.0"}
]
end# lib/my_app/gettext.ex
defmodule MyApp.Gettext do
use Gettext.Backend, otp_app: :my_app
endRouter Configuration
Add use Localize.Routes to your router alongside use Phoenix.Router:
defmodule MyApp.Router do
use Phoenix.Router
use Localize.Routes, gettext: MyApp.Gettext
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug Localize.Plug.PutLocale,
from: [:route, :session, :accept_language],
gettext: MyApp.Gettext
plug Localize.Plug.PutSession
end
scope "/", MyApp do
pipe_through :browser
localize do
get "/pages/:page", PageController, :show
resources "/users", UserController
end
end
endThe localize/1 macro wraps standard Phoenix route macros (get, post, put, patch, delete, resources, live, etc.) and generates a localized version of each route for every locale in the Gettext backend.
Include :route in the Localize.Plug.PutLocale :from list so that the locale embedded in a matched route is automatically set for the request.
Setting Up Route Translations
Route translations use the Gettext domain "routes". Create a PO file for each locale under priv/gettext/{locale}/LC_MESSAGES/routes.po:
# priv/gettext/fr/LC_MESSAGES/routes.po
msgid ""
msgstr ""
"Language: fr\n"
msgid "pages"
msgstr "pages_fr"
msgid "users"
msgstr "utilisateurs"# priv/gettext/de/LC_MESSAGES/routes.po
msgid ""
msgstr ""
"Language: de\n"
msgid "pages"
msgstr "seiten"
msgid "users"
msgstr "benutzer"Each msgid is a single path segment (the text between / characters in the route path). After compilation, the router contains routes for each locale:
/pages/:page(English, the default)/pages_fr/:page(French)/seiten/:page(German)
If a translation is empty or missing for a given segment, the original English segment is used for that locale.
Interpolating Locale Data into Paths
Route paths can include locale interpolations using the #{} syntax. This is useful for URL schemes that embed the locale as a path prefix:
localize do
get "/#{locale}/pages/:page", PageController, :show
get "/#{language}/help", HelpController, :index
get "/#{territory}/store", StoreController, :index
endThe supported interpolations are:
locale— the CLDR locale name (e.g.,en,fr,de).language— the language code (e.g.,en,fr,de).territory— the territory code (e.g.,us,fr,de).
Interpolation is resolved at compile time. The first example above generates routes like /en/pages/:page, /fr/pages_fr/:page, and /de/seiten/:page.
Localizing a Subset of Locales
By default, localize/1 generates routes for all locales known to the Gettext backend. To restrict to specific locales, pass a list:
localize [:en, :fr] do
resources "/comments", CommentController
endA single locale also works:
localize "fr" do
get "/chapters/:page", PageController, :show, as: "chap"
endSupported Route Macros
The localize macro supports all standard Phoenix route macros:
get,post,put,patch,delete,options,head,connectresources(including nested resources)live
Nested Resources
Nested resources are fully supported. Each level is localized independently:
localize do
resources "/users", UserController do
resources "/faces", FaceController, except: [:delete] do
resources "/#{locale}/visages", VisageController
end
end
endLocalized Route Helpers
A LocalizedHelpers module is generated at compile time. If your router is MyApp.Router, the helpers are at MyApp.Router.LocalizedHelpers.
The helper functions automatically dispatch to the correct locale-specific route based on the current locale:
iex> import MyApp.Router.LocalizedHelpers
iex> Localize.put_locale("fr")
iex> page_path(conn, :show, "intro")
"/pages_fr/intro"
iex> Localize.put_locale("de")
iex> page_path(conn, :show, "intro")
"/seiten/intro"The same helper name works for all locales. The current process locale determines which translated path is returned.
To disable helper generation:
use Localize.Routes, gettext: MyApp.Gettext, helpers: falseStatic and URL Helpers
The LocalizedHelpers module also delegates to the standard Phoenix helpers:
path/2— generates the path including any necessary prefix.url/1— generates the base URL without path information.static_path/2,static_url/2,static_integrity/2— static asset helpers.
Generating hreflang Links
The generated helpers include *_links functions that produce a map of locale-to-URL pairs. These are used to build <link rel="alternate" hreflang="..."> tags for SEO:
iex> url_map = MyApp.Router.LocalizedHelpers.page_links(conn, :show, "intro")
%{"en" => "http://localhost/pages/intro", "fr" => "http://localhost/pages_fr/intro"}
iex> MyApp.Router.LocalizedHelpers.hreflang_links(url_map)
{:safe, ...} # Generates <link href="..." rel="alternate" hreflang="..."/> tagsPlace the output of hreflang_links/1 in your layout's <head> section to help search engines discover the localized versions of your pages.
Verified Localized Routes with ~q
For compile-time verified routes, use Localize.VerifiedRoutes instead of Phoenix.VerifiedRoutes:
# lib/my_app_web.ex
defp html_helpers do
quote do
use Localize.VerifiedRoutes,
router: MyApp.Router,
endpoint: MyApp.Endpoint,
gettext: MyApp.Gettext
end
endThen use the ~q sigil in templates and controllers:
<.link navigate={~q"/users"}>Users</.link>The ~q sigil generates a case expression at compile time that dispatches to the correct localized ~p path based on the current locale:
# ~q"/users" compiles to something like:
case Localize.get_locale().cldr_locale_id do
:de -> ~p"/benutzer"
:en -> ~p"/users"
:fr -> ~p"/utilisateurs"
endThe ~p sigil remains available for non-localized routes.
Using ~q with url/1
The url/1, url/2, and url/3 functions work with ~q to produce full URLs:
iex> Localize.put_locale("fr")
iex> url(~q"/users")
"http://localhost/utilisateurs"Locale Interpolation in ~q
The ~q sigil supports the same locale interpolations as the localize macro:
~q"/#{locale}/pages/intro"
# Produces "/fr/pages_fr/intro" when the locale is :frInspecting Localized Routes
Localized routes are stored in a LocalizedRoutes submodule. You can inspect them with the phx.routes mix task:
mix phx.routes MyApp.Router.LocalizedRoutes
This shows all generated localized routes with their paths, verbs, and controller actions.
Acknowledgements
Attribution
Localize.Routes is based on ex_cldr_routes which was inspired by the work originally done by Bart Otten on PhxAltRoutes — which has evolved to the much enhanced Routex. Users seeking a more comprehensive and extensible localized routing solution should consider Routex as an alternative to Localize.Routes.