Library for localized/multilingual routes in Phoenix

This lib is now published and has a new topic.


Hi @BartOtten , here’s some feedback since you are looking for one:

  1. Is this library is a drop-in replacement to Phoenix Routes? Routing — Phoenix v1.6.6 in which case aspect such as security (like token decoding, cross browser validation, etc.)
  2. What is the intended usage of the library? Why would people consider using AltRoutes rather than the standard Phoenix Routes? it’s not clear to me, right now it seems AltRoutes looks like a smaller codegen of standard Phoenix Route.

1 - I see PhxMultiLangRoutes as more descriptive of what the library does. PhxLocalizedRoutes could also be an idea.

2 - Would :short differ from :slug in many cases? If not, having one key less could be desirable. Even in europe/eu case, if the slug is europe, I would find it much more intuitively that the helper uses it as well (europe_product_index_path).

4 - A far alley, maybe even out of the scope of the library, could be the support of different domains/subdomains:

  "en" => "",
  "nl" => "",
  "be" => "",
  1. Great suggestion, was already done (just not pushed yet)
  2. I chose to call it rename it to prefix, but not include it in the example config as it’s just an option for then needed.
  3. As the assigns are freeform (you define the struct yourself), one can just add key :domain. Or did you have something else in mind?
1 Like

My idea was regarding sites that run each localized version on different subdomains entirely. So, the french version on, the english/default on etc, thus the path helpers/router macros will need to generate the paths differently and I suppose it would also involve different endpoint/router for each locale. Again, a very var away alley :grin:

1 Like

Major refactoring in progress; will soon push a revised edition.

Changelog (pushed so far, refactoring in progress)

- renamed lib
- restructured config
- less dynamic, more compiled; the config is extended with generated values during compilation time.
- use native :assigns for the scopes
- simplified Plug
- fixed compilation issues
- removed (dev) helper function clutter
- added assigned_values(key or list of keys)

I hope the linked README will now anwer those questions. If not, please let me know.

Afaik this can be done by pointing all subdomains to the same location (with all routes) and use a SetLocale plug (see README) to steer people to the correct localized route.

I have a conversation with @kip from ex_cldr to streamline/align libs and efforts.


  • replace live_session with on_mount/1
  • add option to get flattened config with composite keys
  • use dedicated loc_scope_helper
  • add loc_scope_helper to assigns during config compilation

BROKE A FEW THINGS, so use commit 8d2c608208af06e2edd75a0c45f4022f967193e9 to a checkout a working example

Next stop: Add example how to list all alternative routes on a page.
I had this working but the syntax was evil. Will probably add a loc_link macro or patch the native Phoenix link, live_redirect, etc


  • The public API is becoming stable
  • Localized Routes on Phoenix 0.16 was easier than on Phoenix 0.17. Ah well, got lot’s of goodies in return.
  • Lots of refactoring still to do



# the original route is wrapped in `PhxLocalizedRoutes.Helpers.loc_route` which receives a scope config (opts).

<%= for {slug, opts} <- ExampleWeb.LocalizedRoutes.local_scopes!(flat: true) do %>
       <%= live_redirect " [#{slug}] ", to: PhxLocalizedRoutes.Helpers.loc_route(Routes.product_index_path(@socket, :index), opts) %>
<% end %>

Using live_redirect and a (macro set) query parameter seemed to work well; but broke as soon as a user navigated to another LiveView.

  • A new LiveView route leads to a new proces, so the assigned loc_scope_helper was gone
  • Private info is only merged into the Socket on initial connected mount. So navigating between routes does not update it.
  • The fallback session value on mount was not updated as there was no Conn to do so.

So the example above has become:

<%= for {slug, opts} <- ExampleWeb.LocalizedRoutes.local_scopes do %>
       <%= link " [#{slug}] ", to: PhxLocalizedRoutes.Helpers.loc_route(Routes.product_index_path(@socket, :index), opts) %>
<% end %>


  • removed bang of global_scope/0 and local_scopes/0
  • removed setting scope with query param
  • fixed example

Tests are coming soon! (this project once was a dirty Proof of Concept)


  • Use namespce :loc for assigns to avoid collisions with other libs (so in template use, @loc.language etc)
  • Use String.to_existing_atom/1 for external input
  • Started refactoring Helpers
1 Like

Bump in the road: Covid-19. Will continue once recovered.


Get well soon!


Thanks! Rest assured it was not only Covid which caused the delay. A newborn is much more positive but even more intense (for me).

Changelog of this evening

  • Moved the example app out of the test folder for easier discovery and able to test the lib without running the web apps tests.
  • Improved Macro hygience
  • Added compile time validation of the configuration
  • More docstrings and comments
  • Wrote test for the ‘config builder’
  • Currently working on test for the route generator; seems much easier than expected

Current todo

Reminder: Tomorrow Saša Jurić and José Valim will talk at

1 Like


  • Raised test coverage to 100 percent
  • Checked that box!
100.0% lib/assigns/on_mount.ex                       
100.0% lib/assigns/plug.ex                           
100.0% lib/errors.ex                                
100.0% lib/helpers.ex                               
100.0% lib/localized_routes.ex                      
100.0% lib/router.ex                                 
100.0% lib/router_helpers.ex                               


  • Refactored configuration as :global and :locals kept bothering me. Now the scopes are simply nested maps, starting with slug ‘/’ to keep the native route.
  • Added some extra inline explanation in the example app.
  • Changed lib name: PhxLocalizedRoutes → Phoenix Localized Routes.
  • Added some #TODO comments
use PhxLocalizedRoutes,
    scopes: %{
      "/" => %{
        assigns: %Assigns{contact: ""},
        scopes: %{
          "/europe" => %{
            assigns: %Assigns{contact: ""},
            scopes: %{
              "/nl" => %{assigns: %Assigns{locale: "nl-NL", language: "nl", contact: ""}},
              "/be" => %{assigns: %Assigns{locale: "nl_BE", language: "nl", contact: ""}}
        "/uk" => %{assigns: %Assigns{contact: ""}
    gettext_module: ExampleWeb.Gettext

CLDR had it’s release of CLDR Route which was inspired by this lib-in-progress. There is overlap in purpose, yet enough difference in features to continue this lib. Will make a comparison table some day to help users making the choice what fits best for their purpose.

Due to the better documentation and function naming skills of @kip, I have merged back some improvements. So thanks Kip for part of this update!

  • alias original helper module as OriginalRoutes
  • improved docs
  • improved function naming
  • :assigns => :assign (to keep :assign_new as option for the future)
  • development: more debug logging
  • development: better stacktrace
  • much more


  • assign inheritance
  • getting rid of the Assigns struct
  • e2e tests in addition to current low level tests


  • loc_link which also translates the links(??)
  • option to not render a parent route


  • Add CI, with Credo which at the moment makes it fail :slight_smile:
  • Add e2e tests
  • Remove dependency on example app; use slimmed-down Phoenix just for test purposes

So now tests are in place, let’s go do those last few things we need to go to Hex.