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, "~> 1.0"},
{:gettext, "~> 1.0"}
]
end# lib/my_app_web/gettext.ex
defmodule MyAppWeb.Gettext do
use Gettext.Backend, otp_app: :my_app
endRouter Configuration
Add use Localize.Routes to your router alongside use Phoenix.Router:
defmodule MyAppWeb.Router do
use Phoenix.Router
use Localize.Routes, gettext: MyAppWeb.Gettext
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug Localize.Plug.PutLocale,
from: [:route, :session, :accept_language],
gettext: MyAppWeb.Gettext
plug Localize.Plug.PutSession
end
scope "/", MyAppWeb 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 MyAppWeb.Router, the helpers are at MyAppWeb.Router.LocalizedHelpers.
The helper functions automatically dispatch to the correct locale-specific route based on the current locale:
iex> import MyAppWeb.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: MyAppWeb.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 = MyAppWeb.Router.LocalizedHelpers.page_links(conn, :show, "intro")
%{"en" => "http://localhost/pages/intro", "fr" => "http://localhost/pages_fr/intro"}
iex> MyAppWeb.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: MyAppWeb.Router,
endpoint: MyAppWeb.Endpoint,
gettext: MyAppWeb.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 :fr#{language} and #{territory} are recognised in the same way, and the token may appear in any segment, not only the first:
~q"/users/#{id}/faces/#{face_id}/#{locale}/visages"Despite the syntax, locale is not a variable — it is a token the sigil recognises, resolved at compile time once per locale branch, and no binding of that name is consulted. A path may mix it freely with genuine runtime interpolation such as #{id} above.
~q also accepts a colon form — ~q"/:locale/pages/intro" — which does the same thing. Prefer the interpolation form: it is the only one the localize macro accepts when defining routes, because in a router :locale is an ordinary Phoenix path parameter and is left untouched. Using the interpolation form in both places keeps route definitions and the paths that reference them written the same way.
Rendering a Path or URL in a Specific Locale
~q dispatches on the current process locale (Localize.get_locale/0). When you need to render a link in a different locale at the call site — without changing the process locale — use path_for/2 and url_for/2. Typical use cases are language switchers and emitting hreflang links per locale in one template pass.
<.link navigate={path_for(:fr, "/users")}>Français</.link>
<.link navigate={path_for(:de, "/users")}>Deutsch</.link>The locale argument can also be a runtime expression, which is the more common form when the locale list is iterated:
<%= for locale <- [:en, :fr, :de] do %>
<.link navigate={path_for(locale, "/users")}><%= locale %></.link>
<% end %>url_for/2 returns a full URL via Phoenix.VerifiedRoutes.url/1:
url_for(:fr, "/users")
#=> "http://localhost:4000/users_fr"The route argument accepts the same form as ~q — a string literal with optional #{...} interpolations and :locale / :language / :territory substitutions.
For ad-hoc blocks where you need a whole region of code to run under a specific locale (perhaps because you also want locale-sensitive number or date formatting), Localize.with_locale/2 temporarily switches the locale and restores it afterwards:
Localize.with_locale(:fr, fn -> ~q"/users" end)
#=> "/users_fr"Use path_for/2 for single-call locale forcing; use Localize.with_locale/2 for whole-block temporary locale changes.
Inspecting Localized Routes
Localized routes are stored in a LocalizedRoutes submodule. You can inspect them with the phx.routes mix task:
mix phx.routes MyAppWeb.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.