Phoenix Localized Routing

Copy Markdown View Source

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
end

Router 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
end

The 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
end

The 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
end

A single locale also works:

localize "fr" do
  get "/chapters/:page", PageController, :show, as: "chap"
end

Supported Route Macros

The localize macro supports all standard Phoenix route macros:

  • get, post, put, patch, delete, options, head, connect
  • resources (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
end

Localized 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: false

Static 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.

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="..."/> tags

Place 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
end

Then 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"
end

The ~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

Inspecting 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.