5
votes

I've started to teach myself Phoenix Framework and their documentation is pretty good. I have however hit a stumbling block when it comes to specifying optional routing parameters. The Phoenix Framework Routing Documentation makes no mention of this as a feature, so I'm assuming the onus falls on the dev to come up with a solution.

I'll lay out my use case:

  1. User visits site at /page/test, a custom Plug then implements some code to find or assign a locale to the connection.
  2. Since there's no :locale parameter in the path, the default is used as per the line in my pipeline, which is plug HelloPhoenix.Plugs.Locale, "en".
  3. User visits site at /fr/page/test, and the same code gets executed in the pipeline, except time as the :locale parameter is present in the route, the custom Plug (HelloPhoenix.Plugs.Locale).

Now from a routing perspective, if I can't specify that the :locale parameter is optional, I end up with double the number of routes, e.g.:

scope "/", HelloPhoenix do
  use_pipeline :browser
  plug HelloPhoenix.Plugs.Locale, "en"

  # Route without locale
  get "/page/:slug", PageController, :show
  # Route with locale
  get "/:locale/page/:slug", PageController, :show
end

as you can tell, this could quickly become very arduous and repetitive without the ability to specify an optional routing parameter.

No I do have a workaround, which I'll post in an answer, but I'm not sure if it's (a) right, and (b) the easiest solution, since I'm new to Erlang, Elixir and Phoenix (I'm coming from a background in Ruby & PHP OOP).

2

2 Answers

9
votes

You could have a simple plug like:

defmodule MyApp.SetLocale do

  @locales ~w{en fr}

  def init(opts), do: opts

  def call(conn, _opts) do
    case conn.path_info do
      [locale | rest] when locale in @locales ->
        %{conn | path_info: rest}
        |> Plug.Conn.assign(:locale, locale)
      _  -> Plug.Conn.assign(conn, :locale, "en")
    end
  end
end

Then place this plug before your router in endpoint.ex

  plug MyApp.SetLocale
  plug MyApp.Router
end

This way you can be confident the locale has been set before you even get to the router. You don't need to mention it in your router at all.

This technique will 404 if you enter a locale that is not in the @locales though.

2
votes

As mentioned in my question, I've come up with a solution which works in my case, but I'm not sure it's right or the easiest solution (especially if routing gets more complex)...

My solution uses Enum.each to loop over a list of prefixes, and then the routes only need to be specified once. This seems to work:

scope "/", HelloPhoenix do
  use_pipeline :browser
  plug HelloPhoenix.Plugs.Locale, "en"

  # Loop over list containing prefix :locale and no prefix.
  Enum.each ["/:locale", "/"], fn prefix ->
    # No need to duplicate routes
    get prefix <> "/page/:slug", PageController, :show
  end
end