2018-12-23 13:04:54 -07:00
|
|
|
# Pleroma: A lightweight social networking server
|
2023-01-02 13:38:50 -07:00
|
|
|
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
2018-12-23 13:04:54 -07:00
|
|
|
# SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
2017-03-17 10:09:58 -06:00
|
|
|
defmodule Pleroma.Web.Gettext do
|
|
|
|
@moduledoc """
|
|
|
|
A module providing Internationalization with a gettext-based API.
|
|
|
|
|
|
|
|
By using [Gettext](https://hexdocs.pm/gettext),
|
|
|
|
your module gains a set of macros for translations, for example:
|
|
|
|
|
|
|
|
import Pleroma.Web.Gettext
|
|
|
|
|
|
|
|
# Simple translation
|
|
|
|
gettext "Here is the string to translate"
|
|
|
|
|
|
|
|
# Plural translation
|
|
|
|
ngettext "Here is the string to translate",
|
|
|
|
"Here are the strings to translate",
|
|
|
|
3
|
|
|
|
|
|
|
|
# Domain-based translation
|
|
|
|
dgettext "errors", "Here is the error message to translate"
|
|
|
|
|
|
|
|
See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
|
|
|
|
"""
|
|
|
|
use Gettext, otp_app: :pleroma
|
2022-02-27 23:28:23 -07:00
|
|
|
|
|
|
|
def language_tag do
|
|
|
|
# Naive implementation: HTML lang attribute uses BCP 47, which
|
|
|
|
# uses - as a separator.
|
|
|
|
# https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang
|
|
|
|
|
|
|
|
Gettext.get_locale()
|
|
|
|
|> String.replace("_", "-", global: true)
|
|
|
|
end
|
2022-03-01 19:24:17 -07:00
|
|
|
|
2022-03-01 23:41:13 -07:00
|
|
|
def normalize_locale(locale) do
|
|
|
|
if is_binary(locale) do
|
2022-03-03 00:31:36 -07:00
|
|
|
String.replace(locale, "-", "_", global: true)
|
2022-03-01 23:41:13 -07:00
|
|
|
else
|
|
|
|
nil
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-03-01 19:24:17 -07:00
|
|
|
def supports_locale?(locale) do
|
|
|
|
Pleroma.Web.Gettext
|
|
|
|
|> Gettext.known_locales()
|
|
|
|
|> Enum.member?(locale)
|
|
|
|
end
|
|
|
|
|
Fallback to a variant if the language in general is not supported
For an example, here, zh is not supported, but zh_Hans and zh_Hant
are. If the user asks for zh, we should choose a variant for them
instead of fallbacking to default.
Some browsers (e.g. Firefox) does not allow users to customize
their language codes. For example, there is no zh-Hans, but only
zh, zh-CN, zh-TW, zh-HK, etc. This provides a workaround for
those users suffering from bad design decisions.
2022-03-02 17:59:11 -07:00
|
|
|
def variant?(locale), do: String.contains?(locale, "_")
|
|
|
|
|
2022-03-03 00:31:36 -07:00
|
|
|
def language_for_variant(locale) do
|
|
|
|
Enum.at(String.split(locale, "_"), 0)
|
|
|
|
end
|
2022-03-02 20:56:19 -07:00
|
|
|
|
2022-03-03 00:31:36 -07:00
|
|
|
def ensure_fallbacks(locales) do
|
|
|
|
locales
|
|
|
|
|> Enum.flat_map(fn locale ->
|
2022-03-03 07:40:18 -07:00
|
|
|
others =
|
|
|
|
other_supported_variants_of_locale(locale)
|
2022-03-03 00:31:36 -07:00
|
|
|
|> Enum.filter(fn l -> not Enum.member?(locales, l) end)
|
|
|
|
|
|
|
|
[locale] ++ others
|
|
|
|
end)
|
|
|
|
end
|
|
|
|
|
|
|
|
def other_supported_variants_of_locale(locale) do
|
|
|
|
cond do
|
Fallback to a variant if the language in general is not supported
For an example, here, zh is not supported, but zh_Hans and zh_Hant
are. If the user asks for zh, we should choose a variant for them
instead of fallbacking to default.
Some browsers (e.g. Firefox) does not allow users to customize
their language codes. For example, there is no zh-Hans, but only
zh, zh-CN, zh-TW, zh-HK, etc. This provides a workaround for
those users suffering from bad design decisions.
2022-03-02 17:59:11 -07:00
|
|
|
supports_locale?(locale) ->
|
2022-03-03 00:31:36 -07:00
|
|
|
[]
|
|
|
|
|
|
|
|
variant?(locale) ->
|
|
|
|
lang = language_for_variant(locale)
|
|
|
|
if supports_locale?(lang), do: [lang], else: []
|
2022-03-02 20:56:19 -07:00
|
|
|
|
Fallback to a variant if the language in general is not supported
For an example, here, zh is not supported, but zh_Hans and zh_Hant
are. If the user asks for zh, we should choose a variant for them
instead of fallbacking to default.
Some browsers (e.g. Firefox) does not allow users to customize
their language codes. For example, there is no zh-Hans, but only
zh, zh-CN, zh-TW, zh-HK, etc. This provides a workaround for
those users suffering from bad design decisions.
2022-03-02 17:59:11 -07:00
|
|
|
true ->
|
|
|
|
Gettext.known_locales(Pleroma.Web.Gettext)
|
|
|
|
|> Enum.filter(fn l -> String.starts_with?(l, locale <> "_") end)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-03-03 07:40:18 -07:00
|
|
|
def get_locales do
|
2022-03-03 00:03:44 -07:00
|
|
|
Process.get({Pleroma.Web.Gettext, :locales}, [])
|
|
|
|
end
|
|
|
|
|
|
|
|
def is_locale_list(locales) do
|
|
|
|
Enum.all?(locales, &is_binary/1)
|
|
|
|
end
|
|
|
|
|
|
|
|
def put_locales(locales) do
|
|
|
|
if is_locale_list(locales) do
|
|
|
|
Process.put({Pleroma.Web.Gettext, :locales}, Enum.uniq(locales))
|
|
|
|
Gettext.put_locale(Enum.at(locales, 0, Gettext.get_locale()))
|
|
|
|
:ok
|
|
|
|
else
|
|
|
|
{:error, :not_locale_list}
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-03-01 19:24:17 -07:00
|
|
|
def locale_or_default(locale) do
|
|
|
|
if supports_locale?(locale) do
|
|
|
|
locale
|
|
|
|
else
|
|
|
|
Gettext.get_locale()
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-03-03 00:03:44 -07:00
|
|
|
def with_locales_func(locales, fun) do
|
|
|
|
prev_locales = Process.get({Pleroma.Web.Gettext, :locales})
|
|
|
|
put_locales(locales)
|
|
|
|
|
|
|
|
try do
|
|
|
|
fun.()
|
|
|
|
after
|
|
|
|
if prev_locales do
|
|
|
|
put_locales(prev_locales)
|
|
|
|
else
|
|
|
|
Process.delete({Pleroma.Web.Gettext, :locales})
|
2022-03-06 09:43:31 -07:00
|
|
|
Process.delete(Gettext)
|
2022-03-03 00:03:44 -07:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
defmacro with_locales(locales, do: fun) do
|
2022-03-01 19:24:17 -07:00
|
|
|
quote do
|
2022-03-03 00:03:44 -07:00
|
|
|
Pleroma.Web.Gettext.with_locales_func(unquote(locales), fn ->
|
2022-03-01 19:24:17 -07:00
|
|
|
unquote(fun)
|
|
|
|
end)
|
|
|
|
end
|
|
|
|
end
|
2022-03-03 00:03:44 -07:00
|
|
|
|
|
|
|
def to_locale_list(locale) when is_binary(locale) do
|
|
|
|
locale
|
|
|
|
|> String.split(",")
|
|
|
|
|> Enum.filter(&supports_locale?/1)
|
|
|
|
end
|
|
|
|
|
|
|
|
def to_locale_list(_), do: []
|
|
|
|
|
|
|
|
defmacro with_locale_or_default(locale, do: fun) do
|
|
|
|
quote do
|
|
|
|
Pleroma.Web.Gettext.with_locales_func(
|
|
|
|
Pleroma.Web.Gettext.to_locale_list(unquote(locale))
|
|
|
|
|> Enum.concat(Pleroma.Web.Gettext.get_locales()),
|
|
|
|
fn ->
|
|
|
|
unquote(fun)
|
|
|
|
end
|
|
|
|
)
|
|
|
|
end
|
|
|
|
end
|
2022-03-06 09:43:31 -07:00
|
|
|
|
|
|
|
defp next_locale(locale, list) do
|
|
|
|
index = Enum.find_index(list, fn item -> item == locale end)
|
|
|
|
|
|
|
|
if not is_nil(index) do
|
|
|
|
Enum.at(list, index + 1)
|
|
|
|
else
|
|
|
|
nil
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-04-17 20:39:52 -06:00
|
|
|
# We do not yet have a proper English translation. The "English"
|
|
|
|
# version is currently but the fallback msgid. However, this
|
|
|
|
# will not work if the user puts English as the first language,
|
|
|
|
# and at the same time specifies other languages, as gettext will
|
|
|
|
# think the English translation is missing, and call
|
|
|
|
# handle_missing_translation functions. This may result in
|
|
|
|
# text in other languages being shown even if English is preferred
|
|
|
|
# by the user.
|
|
|
|
#
|
|
|
|
# To prevent this, we do not allow fallbacking when the current
|
|
|
|
# locale missing a translation is English.
|
|
|
|
defp should_fallback?(locale) do
|
|
|
|
locale != "en"
|
|
|
|
end
|
|
|
|
|
2022-03-06 09:43:31 -07:00
|
|
|
def handle_missing_translation(locale, domain, msgctxt, msgid, bindings) do
|
|
|
|
next = next_locale(locale, get_locales())
|
|
|
|
|
2022-04-17 20:39:52 -06:00
|
|
|
if is_nil(next) or not should_fallback?(locale) do
|
2022-03-06 09:43:31 -07:00
|
|
|
super(locale, domain, msgctxt, msgid, bindings)
|
|
|
|
else
|
|
|
|
{:ok,
|
|
|
|
Gettext.with_locale(next, fn ->
|
|
|
|
Gettext.dpgettext(Pleroma.Web.Gettext, domain, msgctxt, msgid, bindings)
|
|
|
|
end)}
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def handle_missing_plural_translation(
|
|
|
|
locale,
|
|
|
|
domain,
|
|
|
|
msgctxt,
|
|
|
|
msgid,
|
|
|
|
msgid_plural,
|
|
|
|
n,
|
|
|
|
bindings
|
|
|
|
) do
|
|
|
|
next = next_locale(locale, get_locales())
|
|
|
|
|
2022-04-17 20:39:52 -06:00
|
|
|
if is_nil(next) or not should_fallback?(locale) do
|
2022-03-06 09:43:31 -07:00
|
|
|
super(locale, domain, msgctxt, msgid, msgid_plural, n, bindings)
|
|
|
|
else
|
|
|
|
{:ok,
|
|
|
|
Gettext.with_locale(next, fn ->
|
|
|
|
Gettext.dpngettext(
|
|
|
|
Pleroma.Web.Gettext,
|
|
|
|
domain,
|
|
|
|
msgctxt,
|
|
|
|
msgid,
|
|
|
|
msgid_plural,
|
|
|
|
n,
|
|
|
|
bindings
|
|
|
|
)
|
|
|
|
end)}
|
|
|
|
end
|
|
|
|
end
|
2017-03-17 10:09:58 -06:00
|
|
|
end
|