How to set user's locale View Source
General setup
The first step would be to write a migration to add a field to your table in order to store users's locale and, if you want to, also its timezone:
# priv/repo/migrations/`date '+%Y%m%d%H%M%S'`_user_locale_field.ex
defmodule YourApp.UserLocaleField do
use Ecto.Migration
def change do
alter table("users") do
add :locale, :string
add :timezone, :string # remove or comment if you don't need it
end
end
end
Then apply the very same migration by the mix ecto.migrate
command.
Next, you also need to define these field in your user schema by adding the following Ecto.Schema.field/3
:
# lib/your_app/user.ex
defmodule YourApp.User do
# ...
schema "users" do
# ...
field :locale, :string
field :timezone, :string
end
# ...
end
Now let's write a custom plug to set the appropriate locale for translations:
# lib/your_app_web/plugs/set_user_locale.ex
defmodule YourAppWeb.SetUserLocalePlug do
import Plug.Conn
@supported_locales Gettext.known_locales(YourAppWeb.Gettext)
@behaviour Plug
@impl Plug
def init(_opts), do: nil
@impl Plug
def call(conn = %Plug.Conn{assigns: %{current_user: %YourApp.User{locale: locale}}}, _options)
when locale in @supported_locales
do
Gettext.put_locale(locale)
conn
end
def call(conn, _options) do
conn
end
end
Add it to your :browser
pipeline in your router but after calling your Haytni stack (the plug YourApp.Haytni
line):
# lib/your_app_web/router.ex
# ...
pipeline :browser do
# ...
plug YourApp.Haytni
plug YourAppWeb.SetUserLocalePlug # <= line to add
end
# ...
Profile edition
It could be more useful if user can actually change its locale but we haven't taking care of this for now so let's remedy this.
We will begin by completing the template for editing registration:
# lib/your_app_web/templates/haytni/registration/edit.html.heex (global) or lib/your_app_web/templates/haytni/user/registration/edit.html.heex (scoped)
<div>
<%= label f, :locale, YourAppWeb.Gettext.dgettext("your_domain", "Locale") %>
<%= select f, :locale, Gettext.known_locales(YourAppWeb.Gettext) %>
<%= error_tag f, :locale %>
</div>
<div>
<%= label f, :timezone, YourAppWeb.Gettext.dgettext("your_domain", "Timezone") %>
<%= select f, :timezone, Tzdata.zone_lists_grouped() %>
<%= error_tag f, :timezone %>
</div>
Note: to support timezones, you'll need tzdata. To do so:
add tzdata by the tuple {:tzdata, "~> 1.1"}
in the deps/0
function of your mix.exs file
add config :elixir, :time_zone_database, Tzdata.TimeZoneDatabase
in config/config.exs
Finally, accept and validate those fields from the update_registration_changeset/2
function in the user schema:
# lib/your_app/user.ex
# function to add (only to support user's timezone)
defp validate_timezone(%Ecto.Changeset{} = changeset, field)
when is_atom(field)
do
validate_change changeset, field, {:inclusion, nil}, fn _, value ->
if Tzdata.zone_exists?(value) do
[]
else
[{field, {"is invalid", [validation: :inclusion]}}]
end
end
end
def update_registration_changeset(%__MODULE__{} = struct, params) do
struct
|> cast(params, ~W[locale timezone]a) # <= line to change
|> validate_inclusion(:locale, Gettext.known_locales(YourAppWeb.Gettext)) # <= line to add
|> validate_timezone(:timezone) # <= line to add (only needed for user to have a timezone)
|> YourApp.Haytni.validate_update_registration()
end
## Bonus : translating dates
Start by adding :ex_cldr_dates_times
as a dependency to your project (function deps/0
in your mix.exs file):
# mix.exs
{:ex_cldr_dates_times, "~> 2.0"},
Configure, through config/config.exs the default locale and timezone as you want (french here):
# config/config.exs
config :your_app,
default_locale: "fr",
default_timezone: "Europe/Paris"
Create a module for Cldr:
# lib/your_app/cldr.ex
defmodule YourApp.Cldr do
use Cldr,
locales: Gettext.known_locales(YourAppWeb.Gettext),
providers: [
Cldr.Number,
Cldr.Calendar,
Cldr.DateTime,
]
end
Now, you can run mix deps.get
and restart your application.
In a global (= imported everywhere via the function view/0
or view_helpers/0
of lib/your_app_web.ex) view add the following l/2
function:
defp do_l(dt = %DateTime{}, timezone, locale) do
dt
|> DateTime.shift_zone!(timezone)
|> YourApp.Cldr.DateTime.to_string!(format: :long, locale: locale)
end
def l(dt = %DateTime{}, user = %User{}) do
do_l(dt, user.timezone, user.locale)
end
def l(dt = %DateTime{}, nil) do
do_l(dt, Application.fetch_env!(:your_app, :default_timezone), Application.fetch_env!(:your_app, :default_locale))
end
Example of uses (in a template):
<%= l(~U[2018-07-16 10:00:00Z], @current_user) %>
<%= l(@post.created_at, @current_user) %>