Packages
Localize your Phoenix website with multilingual URLs and custom template assigns; enhancing user engagement and content relevance.
Current section
Files
Jump to
Current section
Files
phoenix_localized_routes
USAGE.md
USAGE.md
# Usage
## Requirements
- Elixir >=1.11
- Phoenix >= 1.6.0
- Phoenix LiveView >= 0.16 (optional)
## Installation
You can install this library by adding it to your list of dependencies in `mix.exs`:
```diff
def deps do
[
...other deps
+ {:phoenix_localized_routes, "~> 0.1.0"}
]
end
```
We pressed on making the installation as non-intrusive as possible. The following
modules / files need changes.
## Helpers
`Phoenix Localized Routes` adds localization to the helpers created by Phoenix;
no code changes in controllers and (live)views necessary.
```diff
# file: lib/example_web.ex
# in router
+ use PhxLocalizedRoutes.Router
# in controller
unquote(verified_routes())
+ unquote(loc_helpers())
# in live_view
unquote(html_helpers())
+ on_mount(ExampleWeb.LocalizedRoutes.LiveHelpers)
# in view_helpers
unquote(verified_routes())
+ unquote(loc_helpers())
# insert new private function
+ defp loc_helpers do
+ quote do
+ import PhxLocalizedRoutes.Helpers
+ import ExampleWeb.Router.VerifiedRoutes
+
+ alias ExampleWeb.LocalizedRoutes, as: Loc
+
+ # Uncomment when using the Phoenix Route Helpers
+ # alias ExampleWeb.Router.Helpers, as: OriginalRoutes
+ # alias ExampleWeb.Router.Helpers.Localized, as: Routes
+
+ # Uncomment when overriding the default Verified Route sigil
+ import Phoenix.VerifiedRoutes, except: [sigil_p: 2]
+ end
+ end
```
```diff
# file: lib/example_web/router.ex
# Add to browser pipeline
pipeline :browser do
[...other plugs]
+ plug(PhxLocalizedRoutes.Plug)
```
## Configuration
Create the module `[MyAppWeb].LocalizedRoutes` in the directory of your web
application. The example shows a nested configuration using the default
`[MyAppWeb].Gettext` module for multilingual URLs.
It is possible to set:
* `:scopes` - scopes as map of maps, keys are used as URL segments (slugs).
* `:gettext_module` - `Gettext` module used to extract and translate URL segments.
* `:sigil_localized` - Sigil used for Localized Verified Routes. (default: "~l")
* `:sigil_original` - Sigil used for the original Verified Routes when `:sigil_localized` is set to "~p" (default: "~o")
For each local scope you can set.
* `:assign` - a `Map` or `Struct` of values to assign to the `Plug.Conn`
and/or `Phoenix.Socket`. When using a `Map` nested scopes inherit assigns from
their parent.
* `:scopes` - nested scopes
Assigns are namespaced with `:loc`. Templates can access them with
`@loc.{key_name}` (e.g. `@loc.contact`)
> #### Note {: .info}
>
> - using a `Struct` for `:assign`'s improves the developer experience.
> - when using a `Struct` for assigns it should not be nested in the
> configuration module; but it can be in the same file as shown in the example.
> - when a `Gettext` module is provided, the assigns must include a value for `:locale`.
>
> During compilation the configuration is validated.
```elixir
# file /lib/example_web/localized_routes.ex
# This example uses a `Struct` for assign, so there is no assign inheritance;
# only struct defaults. When using maps, nested scopes will inherit key/values
# from their parent.
defmodule ExampleWeb.LocalizedRoutes.Assigns do
@moduledoc false
defstruct [:contact, locale: "en"]
end
defmodule ExampleWeb.LocalizedRoutes do
alias Exampleeb.LocalizedRoutes.Assigns
use PhxLocalizedRoutes,
scopes: %{
"/" => %{
assign: %Assigns{contact: "root@example.com"},
scopes: %{
"/europe" => %{
assign: %Assigns{contact: "europe@example.com"},
scopes: %{
"/nl" => %{assigns: %Assigns{locale: "nl", contact: "verkoop@example.nl"}},
"/be" => %{assigns: %Assigns{locale: "nl", contact: "handel@example.be"}}
}
},
"/gb" => %{assign: %Assigns{contact: "sales@example.com"}
}
},
gettext_module: ExampleWeb.Gettext
end
```
> #### Note {: .info}
>
> Your visitors may prefer another locale than the one set for the route they
> landed on. Libraries like
> [Cldr.Plug.SetLocale](https://hexdocs.pm/ex_cldr/Cldr.Plug.SetLocale.html) can
> detect their preferences. You can combine the value set by the route and the
> value set by a third party library to detect mismatches and guide your visitors
> accordingly.
## Wrapping routes
Wrap the routes within the scope in an `localized` block, providing your created
`LocalizedRoutes` module as argument.
```diff
# file: router.ex
scope "/", ExampleWeb do
+ localize ExampleWeb.LocalizedRoutes do
[...routes]
+ end
end
```
## Extract translatable segments into `routes.po` files
- Run `mix gettext.merge priv/gettext --locale {locale}` to create a locales' folder
- Run `mix gettext.extract --merge` after you updated routes.
Now, we have created new routes PO file in our structure:
web_app/priv/gettext
└─ nl
| └─ LC_MESSAGES
| | └─ default.po
| | └─ errors.po
| | └─ routes.po <---- new!
└─ en
| └─ LC_MESSAGES
| | └─ default.po
| | └─ errors.po
| | └─ routes.po <---- new!
└─ default.pot
└─ errors.pot
└─ routes.pot <---- new!
You can translate the route segments in the `.po`-file and recompile the Router
module to generate the new multilingual routes.
Finally, Phoenix Localized Routes is able to recompile routes whenever PO files
change. To enable this feature **in Elixir < 1.14**, the :gettext compiler needs to be added to the
list of Mix compilers.
In mix.exs:
```elixir
def project do
[
compilers: [:gettext] ++ Mix.compilers,
]
end
```
## Using Phoenix Verified Routes
Phoenix 1.7 includes a new Phoenix.VerifiedRoutes feature which
provides ~p for route generation with compile-time verification.
Phoenix Localized Routes has support for *localized* Verified Routes using
sigil ~l. The sigil used can be customized by setting the `sigil_localized`
option in the configuration.
### Overriding sigil ~p
The default sigil ~p used by Phoenix.VerifiedRoutes can be
overridden by setting `sigil_localized: "~p"`. When doing so, the original
sigil is by default renamed to ~o. This can be customized by setting
the `sigil_original` option.
**Example**
```elixir
sigil_localized: "~p",
sigil_original: "~q"
```
To prevent import conflicts the original `sigil_p/2` function of `Phoenix.VerifiedRoutes`
should be excluded from import. This is done by adding an exception in `loc_helpers/0`.
```diff
# file: lib/example_web.ex
defp loc_helpers do
quote do
[...]
+ # Uncomment when overriding the default Verified Route sigil
+ import Phoenix.VerifiedRoutes, except: [sigil_p: 2]
end
```
As you normally don't want different static files per local scope, adjust
their references.
```diff
# file components/layouts/root.html.heex
- <link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
- <script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
+ <link phx-track-static rel="stylesheet" href={~o"/assets/app.css"} />
+ <script defer phx-track-static type="text/javascript" src={~o"/assets/app.js"}>
```