Fix and improve language handling

- Refactor plugs to detect and set language
- Translate ecto validation errors
- Use Gettext directly, not Mobilizon.Web.Gettext
- Set the language in the <html> attribute according to the one loaded
  on front-end

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel 2021-07-27 19:47:54 +02:00
parent 7c943dc09a
commit a670a7d7a7
No known key found for this signature in database
GPG key ID: A061B9DDE0CA0773
37 changed files with 286 additions and 218 deletions

View file

@ -7,4 +7,5 @@
155A1FB53DE39EC8EFCFD7FB94EA823D 155A1FB53DE39EC8EFCFD7FB94EA823D
73B351E4CB3AF715AD450A085F5E6304 73B351E4CB3AF715AD450A085F5E6304
BBACD7F0BACD4A6D3010C26604671692 BBACD7F0BACD4A6D3010C26604671692
6D4D4A4821B93BCFAC9CDBB367B34C4B 6D4D4A4821B93BCFAC9CDBB367B34C4B
5674F0D127852889ED0132DC2F442AAB

View file

@ -39,9 +39,19 @@ const loadedLanguages = [DEFAULT_LOCALE];
function setI18nLanguage(lang: string): string { function setI18nLanguage(lang: string): string {
i18n.locale = lang; i18n.locale = lang;
setLanguageInDOM(lang);
return lang; return lang;
} }
function setLanguageInDOM(lang: string): void {
const fixedLang = lang.replaceAll("_", "-");
const html = document.documentElement;
const documentLang = html.getAttribute("lang");
if (documentLang !== fixedLang) {
html.setAttribute("lang", fixedLang);
}
}
function fileForLanguage(matches: Record<string, string>, lang: string) { function fileForLanguage(matches: Record<string, string>, lang: string) {
if (Object.prototype.hasOwnProperty.call(matches, lang)) { if (Object.prototype.hasOwnProperty.call(matches, lang)) {
return matches[lang]; return matches[lang];

View file

@ -5,7 +5,8 @@ defmodule Mobilizon.GraphQL.Error do
require Logger require Logger
alias __MODULE__ alias __MODULE__
import Mobilizon.Web.Gettext alias Mobilizon.Web.Gettext, as: GettextBackend
import Mobilizon.Web.Gettext, only: [dgettext: 2]
defstruct [:code, :message, :status_code, :field] defstruct [:code, :message, :status_code, :field]
@ -44,7 +45,7 @@ defmodule Mobilizon.GraphQL.Error do
defp handle(%Ecto.Changeset{} = changeset) do defp handle(%Ecto.Changeset{} = changeset) do
changeset changeset
|> Ecto.Changeset.traverse_errors(fn {err, _opts} -> err end) |> Ecto.Changeset.traverse_errors(&translate_error/1)
|> Enum.map(fn {k, v} -> |> Enum.map(fn {k, v} ->
%Error{ %Error{
code: :validation, code: :validation,
@ -96,4 +97,27 @@ defmodule Mobilizon.GraphQL.Error do
Logger.warn("Unhandled error code: #{inspect(code)}") Logger.warn("Unhandled error code: #{inspect(code)}")
{422, to_string(code)} {422, to_string(code)}
end end
# Translates an error message using gettext.
defp translate_error({msg, opts}) do
# Because error messages were defined within Ecto, we must
# call the Gettext module passing our Gettext backend. We
# also use the "errors" domain as translations are placed
# in the errors.po file.
# Ecto will pass the :count keyword if the error message is
# meant to be pluralized.
# On your own code and templates, depending on whether you
# need the message to be pluralized or not, this could be
# written simply as:
#
# dngettext "errors", "1 file", "%{count} files", count
# dgettext "errors", "is invalid"
#
if count = opts[:count] do
Gettext.dngettext(GettextBackend, "errors", msg, msg, count, opts)
else
Gettext.dgettext(GettextBackend, "errors", msg, opts)
end
end
end end

View file

@ -9,7 +9,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Actor do
alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Service.Workers.Background alias Mobilizon.Service.Workers.Background
alias Mobilizon.Users.User alias Mobilizon.Users.User
import Mobilizon.Web.Gettext import Mobilizon.Web.Gettext, only: [dgettext: 2]
require Logger require Logger

View file

@ -348,8 +348,7 @@ defmodule Mobilizon.Config do
end end
def generate_terms(locale) do def generate_terms(locale) do
import Mobilizon.Web.Gettext Gettext.put_locale(locale)
put_locale(locale)
Phoenix.View.render_to_string( Phoenix.View.render_to_string(
Mobilizon.Web.APIView, Mobilizon.Web.APIView,
@ -363,8 +362,7 @@ defmodule Mobilizon.Config do
end end
def generate_privacy(locale) do def generate_privacy(locale) do
import Mobilizon.Web.Gettext Gettext.put_locale(locale)
put_locale(locale)
Phoenix.View.render_to_string( Phoenix.View.render_to_string(
Mobilizon.Web.APIView, Mobilizon.Web.APIView,

View file

@ -5,7 +5,7 @@ defmodule Mobilizon.Service.Activity.Renderer.Comment do
alias Mobilizon.Activities.Activity alias Mobilizon.Activities.Activity
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Service.Activity.Renderer alias Mobilizon.Service.Activity.Renderer
alias Mobilizon.Web.{Endpoint, Gettext} alias Mobilizon.Web.Endpoint
alias Mobilizon.Web.Router.Helpers, as: Routes alias Mobilizon.Web.Router.Helpers, as: Routes
import Mobilizon.Web.Gettext, only: [dgettext: 3] import Mobilizon.Web.Gettext, only: [dgettext: 3]

View file

@ -5,7 +5,7 @@ defmodule Mobilizon.Service.Activity.Renderer.Discussion do
alias Mobilizon.Activities.Activity alias Mobilizon.Activities.Activity
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Service.Activity.Renderer alias Mobilizon.Service.Activity.Renderer
alias Mobilizon.Web.{Endpoint, Gettext} alias Mobilizon.Web.Endpoint
alias Mobilizon.Web.Router.Helpers, as: Routes alias Mobilizon.Web.Router.Helpers, as: Routes
import Mobilizon.Web.Gettext, only: [dgettext: 3] import Mobilizon.Web.Gettext, only: [dgettext: 3]

View file

@ -5,7 +5,7 @@ defmodule Mobilizon.Service.Activity.Renderer.Event do
alias Mobilizon.Activities.Activity alias Mobilizon.Activities.Activity
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Service.Activity.Renderer alias Mobilizon.Service.Activity.Renderer
alias Mobilizon.Web.{Endpoint, Gettext} alias Mobilizon.Web.Endpoint
alias Mobilizon.Web.Router.Helpers, as: Routes alias Mobilizon.Web.Router.Helpers, as: Routes
import Mobilizon.Web.Gettext, only: [dgettext: 3] import Mobilizon.Web.Gettext, only: [dgettext: 3]

View file

@ -5,7 +5,7 @@ defmodule Mobilizon.Service.Activity.Renderer.Group do
alias Mobilizon.Activities.Activity alias Mobilizon.Activities.Activity
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Service.Activity.Renderer alias Mobilizon.Service.Activity.Renderer
alias Mobilizon.Web.{Endpoint, Gettext} alias Mobilizon.Web.Endpoint
alias Mobilizon.Web.Router.Helpers, as: Routes alias Mobilizon.Web.Router.Helpers, as: Routes
import Mobilizon.Web.Gettext, only: [dgettext: 3] import Mobilizon.Web.Gettext, only: [dgettext: 3]

View file

@ -5,7 +5,7 @@ defmodule Mobilizon.Service.Activity.Renderer.Member do
alias Mobilizon.Activities.Activity alias Mobilizon.Activities.Activity
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Service.Activity.Renderer alias Mobilizon.Service.Activity.Renderer
alias Mobilizon.Web.{Endpoint, Gettext} alias Mobilizon.Web.Endpoint
alias Mobilizon.Web.Router.Helpers, as: Routes alias Mobilizon.Web.Router.Helpers, as: Routes
import Mobilizon.Web.Gettext, only: [dgettext: 3] import Mobilizon.Web.Gettext, only: [dgettext: 3]

View file

@ -5,7 +5,7 @@ defmodule Mobilizon.Service.Activity.Renderer.Post do
alias Mobilizon.Activities.Activity alias Mobilizon.Activities.Activity
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Service.Activity.Renderer alias Mobilizon.Service.Activity.Renderer
alias Mobilizon.Web.{Endpoint, Gettext} alias Mobilizon.Web.Endpoint
alias Mobilizon.Web.Router.Helpers, as: Routes alias Mobilizon.Web.Router.Helpers, as: Routes
import Mobilizon.Web.Gettext, only: [dgettext: 3] import Mobilizon.Web.Gettext, only: [dgettext: 3]

View file

@ -5,7 +5,7 @@ defmodule Mobilizon.Service.Activity.Renderer.Resource do
alias Mobilizon.Activities.Activity alias Mobilizon.Activities.Activity
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Service.Activity.Renderer alias Mobilizon.Service.Activity.Renderer
alias Mobilizon.Web.{Endpoint, Gettext} alias Mobilizon.Web.Endpoint
alias Mobilizon.Web.Router.Helpers, as: Routes alias Mobilizon.Web.Router.Helpers, as: Routes
import Mobilizon.Web.Gettext, only: [dgettext: 3] import Mobilizon.Web.Gettext, only: [dgettext: 3]

View file

@ -6,7 +6,7 @@ defmodule Mobilizon.Service.Metadata.Utils do
alias Mobilizon.Service.{Address, DateTime} alias Mobilizon.Service.{Address, DateTime}
alias Mobilizon.Service.Formatter.HTML, as: HTMLFormatter alias Mobilizon.Service.Formatter.HTML, as: HTMLFormatter
alias Phoenix.HTML alias Phoenix.HTML
import Mobilizon.Web.Gettext import Mobilizon.Web.Gettext, only: [gettext: 1]
@slice_limit 200 @slice_limit 200

View file

@ -22,17 +22,19 @@ defmodule Mobilizon.Web.Auth.Context do
def set_user_information_in_context(conn) do def set_user_information_in_context(conn) do
context = %{ip: conn.remote_ip |> :inet.ntoa() |> to_string()} context = %{ip: conn.remote_ip |> :inet.ntoa() |> to_string()}
context = {conn, context} =
case Guardian.Plug.current_resource(conn) do case Guardian.Plug.current_resource(conn) do
%User{id: user_id, email: user_email} = user -> %User{id: user_id, email: user_email} = user ->
if SentryAdapter.enabled?() do if SentryAdapter.enabled?() do
Sentry.Context.set_user_context(%{id: user_id, name: user_email}) Sentry.Context.set_user_context(%{id: user_id, name: user_email})
end end
Map.put(context, :current_user, user) context = Map.put(context, :current_user, user)
conn = assign(conn, :user_locale, user.locale)
{conn, context}
nil -> nil ->
context {conn, context}
end end
context = context =

View file

@ -10,7 +10,7 @@ defmodule Mobilizon.Web.Email.Activity do
alias Mobilizon.Activities.Activity alias Mobilizon.Activities.Activity
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Config alias Mobilizon.Config
alias Mobilizon.Web.{Email, Gettext} alias Mobilizon.Web.Email
@spec direct_activity(String.t(), list(), String.t()) :: @spec direct_activity(String.t(), list(), String.t()) ::
Bamboo.Email.t() Bamboo.Email.t()

View file

@ -13,7 +13,7 @@ defmodule Mobilizon.Web.Email.Admin do
alias Mobilizon.Reports.Report alias Mobilizon.Reports.Report
alias Mobilizon.Users.User alias Mobilizon.Users.User
alias Mobilizon.Web.{Email, Gettext} alias Mobilizon.Web.Email
@spec report(User.t(), Report.t(), String.t()) :: Bamboo.Email.t() @spec report(User.t(), Report.t(), String.t()) :: Bamboo.Email.t()
def report(%User{email: email} = user, %Report{} = report, default_locale \\ "en") do def report(%User{email: email} = user, %Report{} = report, default_locale \\ "en") do

View file

@ -16,7 +16,6 @@ defmodule Mobilizon.Web.Email.Event do
alias Mobilizon.Users.{Setting, User} alias Mobilizon.Users.{Setting, User}
alias Mobilizon.Web.Email alias Mobilizon.Web.Email
alias Mobilizon.Web.Gettext, as: GettextBackend
@important_changes [:title, :begins_on, :ends_on, :status, :physical_address] @important_changes [:title, :begins_on, :ends_on, :status, :physical_address]
@ -31,7 +30,7 @@ defmodule Mobilizon.Web.Email.Event do
timezone \\ "Etc/UTC", timezone \\ "Etc/UTC",
locale \\ "en" locale \\ "en"
) do ) do
GettextBackend.put_locale(locale) Gettext.put_locale(locale)
subject = subject =
gettext( gettext(

View file

@ -11,7 +11,7 @@ defmodule Mobilizon.Web.Email.Follow do
alias Mobilizon.Actors.{Actor, Follower} alias Mobilizon.Actors.{Actor, Follower}
alias Mobilizon.Federation.ActivityPub.Relay alias Mobilizon.Federation.ActivityPub.Relay
alias Mobilizon.Users.User alias Mobilizon.Users.User
alias Mobilizon.Web.{Email, Gettext} alias Mobilizon.Web.Email
@doc """ @doc """
Send follow notification to admins if the followed actor Send follow notification to admins if the followed actor

View file

@ -10,7 +10,7 @@ defmodule Mobilizon.Web.Email.Group do
alias Mobilizon.{Actors, Config, Users} alias Mobilizon.{Actors, Config, Users}
alias Mobilizon.Actors.{Actor, Member} alias Mobilizon.Actors.{Actor, Member}
alias Mobilizon.Users.User alias Mobilizon.Users.User
alias Mobilizon.Web.{Email, Gettext} alias Mobilizon.Web.Email
@doc """ @doc """
Send emails to local user Send emails to local user

View file

@ -9,7 +9,7 @@ defmodule Mobilizon.Web.Email.Notification do
alias Mobilizon.Events.{Event, Participant} alias Mobilizon.Events.{Event, Participant}
alias Mobilizon.Users.{Setting, User} alias Mobilizon.Users.{Setting, User}
alias Mobilizon.Web.{Email, Gettext} alias Mobilizon.Web.Email
@spec before_event_notification(String.t(), Participant.t(), String.t()) :: @spec before_event_notification(String.t(), Participant.t(), String.t()) ::
Bamboo.Email.t() Bamboo.Email.t()

View file

@ -12,7 +12,7 @@ defmodule Mobilizon.Web.Email.Participation do
alias Mobilizon.Events.{Event, Participant} alias Mobilizon.Events.{Event, Participant}
alias Mobilizon.Users alias Mobilizon.Users
alias Mobilizon.Users.User alias Mobilizon.Users.User
alias Mobilizon.Web.{Email, Gettext} alias Mobilizon.Web.Email
@doc """ @doc """
Send participation emails to local user Send participation emails to local user

View file

@ -7,13 +7,13 @@ defmodule Mobilizon.Web.Email.User do
import Bamboo.Phoenix import Bamboo.Phoenix
import Mobilizon.Web.Gettext import Mobilizon.Web.Gettext, only: [gettext: 2]
alias Mobilizon.{Config, Crypto, Users} alias Mobilizon.{Config, Crypto, Users}
alias Mobilizon.Storage.Repo alias Mobilizon.Storage.Repo
alias Mobilizon.Users.User alias Mobilizon.Users.User
alias Mobilizon.Web.{Email, Gettext} alias Mobilizon.Web.Email
require Logger require Logger

View file

@ -12,7 +12,7 @@ defmodule Mobilizon.Web.Endpoint do
use Phoenix.Endpoint, otp_app: :mobilizon use Phoenix.Endpoint, otp_app: :mobilizon
use Absinthe.Phoenix.Endpoint use Absinthe.Phoenix.Endpoint
plug(Mobilizon.Web.Plugs.SetLocalePlug) plug(Mobilizon.Web.Plugs.DetectLocalePlug)
if Application.fetch_env!(:mobilizon, :env) !== :dev do if Application.fetch_env!(:mobilizon, :env) !== :dev do
plug(Mobilizon.Web.Plugs.HTTPSecurityPlug) plug(Mobilizon.Web.Plugs.HTTPSecurityPlug)

View file

@ -21,30 +21,4 @@ defmodule Mobilizon.Web.Gettext do
See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage. See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
""" """
use Gettext, otp_app: :mobilizon use Gettext, otp_app: :mobilizon
def put_locale(locale) do
locale = determine_best_locale(locale)
Gettext.put_locale(__MODULE__, locale)
end
@spec determine_best_locale(String.t()) :: String.t()
def determine_best_locale(locale) do
locale = String.trim(locale)
locales = Gettext.known_locales(__MODULE__)
default = Keyword.get(Mobilizon.Config.instance_config(), :default_language, "en") || "en"
cond do
# Default if nothing provided
locale == "" -> default
# Either it matches directly, eg: "en" => "en", "fr" => "fr"
locale in locales -> locale
# Either the first part matches, "fr_CA" => "fr"
split_locale(locale) in locales -> split_locale(locale)
# Otherwise set to default
true -> default
end
end
# Keep only the first part of the locale
defp split_locale(locale), do: locale |> String.split("_", trim: true, parts: 2) |> hd
end end

View file

@ -40,7 +40,6 @@ defmodule Mobilizon.Web do
use Phoenix.HTML use Phoenix.HTML
import Mobilizon.Web.Router.Helpers import Mobilizon.Web.Router.Helpers
import Mobilizon.Web.ErrorHelpers
import Mobilizon.Web.Gettext import Mobilizon.Web.Gettext
end end
end end

View file

@ -0,0 +1,67 @@
# Portions of this file are derived from Pleroma:
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
# NOTE: this module is based on https://github.com/smeevil/set_locale
defmodule Mobilizon.Web.Plugs.DetectLocalePlug do
@moduledoc """
Plug to set locale for Gettext
"""
import Plug.Conn, only: [get_req_header: 2, assign: 3]
alias Mobilizon.Web.Gettext, as: GettextBackend
def init(_), do: nil
def call(conn, _) do
locale = get_locale_from_header(conn)
assign(conn, :detected_locale, locale)
end
defp get_locale_from_header(conn) do
conn
|> extract_accept_language()
|> Enum.find(&supported_locale?/1)
end
defp extract_accept_language(conn) do
case get_req_header(conn, "accept-language") do
[value | _] ->
value
|> String.split(",")
|> Enum.map(&parse_language_option/1)
|> Enum.sort(&(&1.quality > &2.quality))
|> Enum.map(& &1.tag)
|> Enum.reject(&is_nil/1)
|> ensure_language_fallbacks()
_ ->
[]
end
end
defp supported_locale?(locale) do
GettextBackend
|> Gettext.known_locales()
|> Enum.member?(locale)
end
defp parse_language_option(string) do
captures = Regex.named_captures(~r/^\s?(?<tag>[\w\-]+)(?:;q=(?<quality>[\d\.]+))?$/i, string)
quality =
case Float.parse(captures["quality"] || "1.0") do
{val, _} -> val
:error -> 1.0
end
%{tag: captures["tag"], quality: quality}
end
defp ensure_language_fallbacks(tags) do
Enum.flat_map(tags, fn tag ->
[language | _] = String.split(tag, "-")
if Enum.member?(tags, language), do: [tag], else: [tag, language]
end)
end
end

View file

@ -8,61 +8,55 @@ defmodule Mobilizon.Web.Plugs.SetLocalePlug do
@moduledoc """ @moduledoc """
Plug to set locale for Gettext Plug to set locale for Gettext
""" """
import Plug.Conn, only: [get_req_header: 2, assign: 3] import Plug.Conn, only: [assign: 3]
alias Mobilizon.Web.Gettext, as: GettextBackend alias Mobilizon.Web.Gettext, as: GettextBackend
def init(_), do: nil def init(_), do: nil
def call(conn, _) do def call(conn, _) do
locale = get_locale_from_header(conn) locale =
GettextBackend.put_locale(locale) [
conn.assigns[:user_locale],
conn.assigns[:detected_locale],
default_locale(),
"en"
]
|> Enum.map(&determine_best_locale/1)
|> Enum.filter(&supported_locale?/1)
|> hd()
Gettext.put_locale(locale)
assign(conn, :locale, locale) assign(conn, :locale, locale)
end end
defp get_locale_from_header(conn) do
conn
|> extract_accept_language()
|> Enum.find("", &supported_locale?/1)
end
defp extract_accept_language(conn) do
case get_req_header(conn, "accept-language") do
[value | _] ->
value
|> String.split(",")
|> Enum.map(&parse_language_option/1)
|> Enum.sort(&(&1.quality > &2.quality))
|> Enum.map(& &1.tag)
|> Enum.reject(&is_nil/1)
|> ensure_language_fallbacks()
_ ->
[]
end
end
defp supported_locale?(locale) do defp supported_locale?(locale) do
GettextBackend GettextBackend
|> Gettext.known_locales() |> Gettext.known_locales()
|> Enum.member?(locale) |> Enum.member?(locale)
end end
defp parse_language_option(string) do defp default_locale do
captures = Regex.named_captures(~r/^\s?(?<tag>[\w\-]+)(?:;q=(?<quality>[\d\.]+))?$/i, string) Keyword.get(Mobilizon.Config.instance_config(), :default_language, "en")
quality =
case Float.parse(captures["quality"] || "1.0") do
{val, _} -> val
:error -> 1.0
end
%{tag: captures["tag"], quality: quality}
end end
defp ensure_language_fallbacks(tags) do @spec determine_best_locale(String.t()) :: String.t()
Enum.flat_map(tags, fn tag -> def determine_best_locale(locale) when is_binary(locale) do
[language | _] = String.split(tag, "-") locale = String.trim(locale)
if Enum.member?(tags, language), do: [tag], else: [tag, language] locales = Gettext.known_locales(GettextBackend)
end)
cond do
locale == "" -> nil
# Either it matches directly, eg: "en" => "en", "fr" => "fr"
locale in locales -> locale
# Either the first part matches, "fr_CA" => "fr"
split_locale(locale) in locales -> split_locale(locale)
# Otherwise set to default
true -> nil
end
end end
def determine_best_locale(_), do: nil
# Keep only the first part of the locale
defp split_locale(locale), do: locale |> String.split("_", trim: true, parts: 2) |> hd
end end

View file

@ -7,10 +7,12 @@ defmodule Mobilizon.Web.Router do
pipeline :graphql do pipeline :graphql do
# plug(:accepts, ["json"]) # plug(:accepts, ["json"])
plug(Mobilizon.Web.Auth.Pipeline) plug(Mobilizon.Web.Auth.Pipeline)
plug(Mobilizon.Web.Plugs.SetLocalePlug)
end end
pipeline :graphiql do pipeline :graphiql do
plug(Mobilizon.Web.Auth.Pipeline) plug(Mobilizon.Web.Auth.Pipeline)
plug(Mobilizon.Web.Plugs.SetLocalePlug)
plug(Mobilizon.Web.Plugs.HTTPSecurityPlug, plug(Mobilizon.Web.Plugs.HTTPSecurityPlug,
script_src: ["cdn.jsdelivr.net"], script_src: ["cdn.jsdelivr.net"],
@ -46,6 +48,8 @@ defmodule Mobilizon.Web.Router do
plug(:accepts, ["html", "activity-json"]) plug(:accepts, ["html", "activity-json"])
plug(:put_secure_browser_headers) plug(:put_secure_browser_headers)
plug(Mobilizon.Web.Plugs.SetLocalePlug)
plug(Cldr.Plug.AcceptLanguage, plug(Cldr.Plug.AcceptLanguage,
cldr_backend: Mobilizon.Cldr, cldr_backend: Mobilizon.Cldr,
no_match_log_level: :debug no_match_log_level: :debug
@ -60,6 +64,8 @@ defmodule Mobilizon.Web.Router do
pipeline :browser do pipeline :browser do
plug(Plug.Static, at: "/", from: "priv/static") plug(Plug.Static, at: "/", from: "priv/static")
plug(Mobilizon.Web.Plugs.SetLocalePlug)
plug(Cldr.Plug.AcceptLanguage, plug(Cldr.Plug.AcceptLanguage,
cldr_backend: Mobilizon.Cldr, cldr_backend: Mobilizon.Cldr,
no_match_log_level: :debug no_match_log_level: :debug

View file

@ -1,22 +0,0 @@
defmodule Mobilizon.Web.ChangesetView do
@moduledoc """
View for changesets in case of errors
"""
use Mobilizon.Web, :view
@doc """
Traverses and translates changeset errors.
See `Ecto.Changeset.traverse_errors/2` and
`Mobilizon.Web.ErrorHelpers.translate_error/1` for more details.
"""
def translate_errors(changeset) do
Ecto.Changeset.traverse_errors(changeset, &translate_error/1)
end
def render("error.json", %{changeset: changeset}) do
# When encoded, the changeset returns its errors
# as a JSON object. So we just pass it forward.
%{errors: translate_errors(changeset)}
end
end

View file

@ -5,15 +5,6 @@ defmodule Mobilizon.Web.ErrorHelpers do
use Phoenix.HTML use Phoenix.HTML
@doc """
Generates tag for inlined form input errors.
"""
def error_tag(form, field) do
Enum.map(Keyword.get_values(form.errors, field), fn error ->
content_tag(:span, translate_error(error), class: "help-block")
end)
end
@doc """ @doc """
Translates an error message using gettext. Translates an error message using gettext.
""" """
@ -31,6 +22,7 @@ defmodule Mobilizon.Web.ErrorHelpers do
# dngettext "errors", "1 file", "%{count} files", count # dngettext "errors", "1 file", "%{count} files", count
# dgettext "errors", "is invalid" # dgettext "errors", "is invalid"
# #
if count = opts[:count] do if count = opts[:count] do
Gettext.dngettext(Mobilizon.Web.Gettext, "errors", msg, msg, count, opts) Gettext.dngettext(Mobilizon.Web.Gettext, "errors", msg, msg, count, opts)
else else

View file

@ -53,27 +53,6 @@ defmodule Mobilizon.Web.Views.Utils do
@spec get_locale(Plug.Conn.t()) :: String.t() @spec get_locale(Plug.Conn.t()) :: String.t()
def get_locale(%Plug.Conn{assigns: assigns}) do def get_locale(%Plug.Conn{assigns: assigns}) do
assigns Map.get(assigns, :locale)
|> Map.get(:locale)
|> check_locale()
end
def get_locale(_), do: default_locale()
defp check_locale(nil) do
default_locale()
|> check_locale()
end
defp check_locale("") do
check_locale(nil)
end
defp check_locale(locale) when is_binary(locale), do: locale
defp default_locale do
Mobilizon.Config.instance_config()
|> Keyword.get(:default_language, "en")
|> Kernel.||("en")
end end
end end

View file

@ -300,7 +300,6 @@ defmodule Mobilizon.Mixfile do
Mobilizon.Web.ChangesetView, Mobilizon.Web.ChangesetView,
Mobilizon.Web.JsonLD.ObjectView, Mobilizon.Web.JsonLD.ObjectView,
Mobilizon.Web.EmailView, Mobilizon.Web.EmailView,
Mobilizon.Web.ErrorHelpers,
Mobilizon.Web.ErrorView, Mobilizon.Web.ErrorView,
Mobilizon.Web.LayoutView, Mobilizon.Web.LayoutView,
Mobilizon.Web.PageView, Mobilizon.Web.PageView,

View file

@ -374,7 +374,7 @@ defmodule Mobilizon.Web.Resolvers.EventTest do
) )
assert hd(res["errors"])["message"] == %{ assert hd(res["errors"])["message"] == %{
"maximum_attendee_capacity" => ["must be greater than or equal to %{number}"] "maximum_attendee_capacity" => ["must be greater than or equal to 0"]
} }
end end

View file

@ -308,6 +308,7 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do
test "create_user/3 doesn't allow two users with the same email", %{conn: conn} do test "create_user/3 doesn't allow two users with the same email", %{conn: conn} do
res = res =
conn conn
|> put_req_header("accept-language", "fr")
|> AbsintheHelpers.graphql_query( |> AbsintheHelpers.graphql_query(
query: @create_user_mutation, query: @create_user_mutation,
variables: @user_creation variables: @user_creation
@ -397,6 +398,7 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do
test "register_person/3 doesn't register a profile from an unknown email", %{conn: conn} do test "register_person/3 doesn't register a profile from an unknown email", %{conn: conn} do
conn conn
|> put_req_header("accept-language", "fr")
|> AbsintheHelpers.graphql_query( |> AbsintheHelpers.graphql_query(
query: @create_user_mutation, query: @create_user_mutation,
variables: @user_creation variables: @user_creation
@ -416,6 +418,7 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do
test "register_person/3 can't be called with an existing profile", %{conn: conn} do test "register_person/3 can't be called with an existing profile", %{conn: conn} do
conn conn
|> put_req_header("accept-language", "fr")
|> AbsintheHelpers.graphql_query( |> AbsintheHelpers.graphql_query(
query: @create_user_mutation, query: @create_user_mutation,
variables: @user_creation variables: @user_creation
@ -423,6 +426,7 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do
res = res =
conn conn
|> put_req_header("accept-language", "fr")
|> AbsintheHelpers.graphql_query( |> AbsintheHelpers.graphql_query(
query: @register_person_mutation, query: @register_person_mutation,
variables: @user_creation variables: @user_creation
@ -447,6 +451,7 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do
insert(:actor, preferred_username: "myactor") insert(:actor, preferred_username: "myactor")
conn conn
|> put_req_header("accept-language", "fr")
|> AbsintheHelpers.graphql_query( |> AbsintheHelpers.graphql_query(
query: @create_user_mutation, query: @create_user_mutation,
variables: @user_creation variables: @user_creation

View file

@ -1,39 +0,0 @@
defmodule Mobilizon.Web.GettextTest do
use ExUnit.Case, async: true
alias Mobilizon.Config
alias Mobilizon.Web.Gettext, as: GettextBackend
describe "test determine_best_locale/1" do
setup do
Config.put([:instance, :default_language], "en")
:ok
end
test "with empty string returns the default locale" do
assert GettextBackend.determine_best_locale("") == "en"
end
test "with empty string returns the default configured locale" do
Config.put([:instance, :default_language], "es")
assert GettextBackend.determine_best_locale("") == "es"
end
test "with empty string returns english as a proper fallback if the default configured locale is nil" do
Config.put([:instance, :default_language], nil)
assert GettextBackend.determine_best_locale("") == "en"
end
test "returns fallback with an unexisting locale" do
assert GettextBackend.determine_best_locale("yolo") == "en"
end
test "maps the correct part if the locale has multiple ones" do
assert GettextBackend.determine_best_locale("fr_CA") == "fr"
end
test "returns the locale if valid" do
assert GettextBackend.determine_best_locale("es") == "es"
end
end
end

View file

@ -0,0 +1,35 @@
# Portions of this file are derived from Pleroma:
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mobilizon.Web.Plugs.DetectLocalePlugTest do
use ExUnit.Case, async: true
use Plug.Test
alias Mobilizon.Web.Plugs.DetectLocalePlug
alias Plug.Conn
test "use supported locale from `accept-language`" do
conn =
:get
|> conn("/cofe")
|> Conn.put_req_header(
"accept-language",
"ru, fr-CH, fr;q=0.9, en;q=0.8, *;q=0.5"
)
|> DetectLocalePlug.call([])
assert %{detected_locale: "ru"} == conn.assigns
end
test "returns empty string if `accept-language` header is empty" do
conn =
:get
|> conn("/cofe")
|> Conn.put_req_header("accept-language", "tlh")
|> DetectLocalePlug.call([])
assert %{detected_locale: nil} == conn.assigns
end
end

View file

@ -7,42 +7,87 @@ defmodule Mobilizon.Web.Plugs.SetLocalePlugTest do
use ExUnit.Case, async: true use ExUnit.Case, async: true
use Plug.Test use Plug.Test
alias Mobilizon.Web.Gettext, as: GettextBackend alias Mobilizon.Config
alias Mobilizon.Web.Plugs.SetLocalePlug alias Mobilizon.Web.Plugs.SetLocalePlug
alias Plug.Conn alias Plug.Conn
test "default locale is `en`" do describe "test assigning locale to conn" do
conn = test "use supported locale from `accept-language`" do
:get conn =
|> conn("/cofe") :get
|> SetLocalePlug.call([]) |> conn("/cofe")
|> assign(:detected_locale, "ru")
|> SetLocalePlug.call([])
assert "en" == Gettext.get_locale() assert "ru" == Gettext.get_locale()
assert %{locale: ""} == conn.assigns assert %{locale: "ru", detected_locale: "ru"} == conn.assigns
end
test "use default locale if locale from `accept-language` is not supported" do
conn =
:get
|> conn("/cofe")
|> Conn.put_req_header("accept-language", "tlh")
|> SetLocalePlug.call([])
assert "en" == Gettext.get_locale()
assert %{locale: "en"} == conn.assigns
end
end end
test "use supported locale from `accept-language`" do describe "test getting default locale from instance" do
conn = test "default locale is `en`" do
:get conn =
|> conn("/cofe") :get
|> Conn.put_req_header( |> conn("/cofe")
"accept-language", |> SetLocalePlug.call([])
"ru, fr-CH, fr;q=0.9, en;q=0.8, *;q=0.5"
)
|> SetLocalePlug.call([])
assert "ru" == Gettext.get_locale(GettextBackend) assert "en" == Gettext.get_locale()
assert %{locale: "ru"} == conn.assigns assert %{locale: "en"} == conn.assigns
end
test "with empty string returns the default configured locale" do
Config.put([:instance, :default_language], "es")
conn =
:get
|> conn("/cofe")
|> SetLocalePlug.call([])
assert %{locale: "es"} == conn.assigns
Config.put([:instance, :default_language], "en")
end
test "with empty string returns english as a proper fallback if the default configured locale is nil" do
Config.put([:instance, :default_language], nil)
conn =
:get
|> conn("/cofe")
|> SetLocalePlug.call([])
assert %{locale: "en"} == conn.assigns
Config.put([:instance, :default_language], "en")
end
end end
test "use default locale if locale from `accept-language` is not supported" do describe "test determine_best_locale/1" do
conn = test "with empty string returns the default locale" do
:get assert SetLocalePlug.determine_best_locale("") == nil
|> conn("/cofe") end
|> Conn.put_req_header("accept-language", "tlh")
|> SetLocalePlug.call([])
assert "en" == Gettext.get_locale(GettextBackend) test "returns fallback with an unexisting locale" do
assert %{locale: ""} == conn.assigns assert SetLocalePlug.determine_best_locale("yolo") == nil
end
test "maps the correct part if the locale has multiple ones" do
assert SetLocalePlug.determine_best_locale("fr_CA") == "fr"
end
test "returns the locale if valid" do
assert SetLocalePlug.determine_best_locale("es") == "es"
end
end end
end end