5ef49d8419bcd3637e1d1fc6a368923665cc2cc2
[anni] / lib / pleroma / web / gettext.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.Gettext do
6   @moduledoc """
7   A module providing Internationalization with a gettext-based API.
8
9   By using [Gettext](https://hexdocs.pm/gettext),
10   your module gains a set of macros for translations, for example:
11
12       import Pleroma.Web.Gettext
13
14       # Simple translation
15       gettext "Here is the string to translate"
16
17       # Plural translation
18       ngettext "Here is the string to translate",
19                "Here are the strings to translate",
20                3
21
22       # Domain-based translation
23       dgettext "errors", "Here is the error message to translate"
24
25   See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
26   """
27   use Gettext, otp_app: :pleroma
28
29   def language_tag do
30     # Naive implementation: HTML lang attribute uses BCP 47, which
31     # uses - as a separator.
32     # https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang
33
34     Gettext.get_locale()
35     |> String.replace("_", "-", global: true)
36   end
37
38   def normalize_locale(locale) do
39     if is_binary(locale) do
40       String.replace(locale, "-", "_", global: true)
41     else
42       nil
43     end
44   end
45
46   def supports_locale?(locale) do
47     Pleroma.Web.Gettext
48     |> Gettext.known_locales()
49     |> Enum.member?(locale)
50   end
51
52   def variant?(locale), do: String.contains?(locale, "_")
53
54   def language_for_variant(locale) do
55     Enum.at(String.split(locale, "_"), 0)
56   end
57
58   def ensure_fallbacks(locales) do
59     locales
60     |> Enum.flat_map(fn locale ->
61       others =
62         other_supported_variants_of_locale(locale)
63         |> Enum.filter(fn l -> not Enum.member?(locales, l) end)
64
65       [locale] ++ others
66     end)
67   end
68
69   def other_supported_variants_of_locale(locale) do
70     cond do
71       supports_locale?(locale) ->
72         []
73
74       variant?(locale) ->
75         lang = language_for_variant(locale)
76         if supports_locale?(lang), do: [lang], else: []
77
78       true ->
79         Gettext.known_locales(Pleroma.Web.Gettext)
80         |> Enum.filter(fn l -> String.starts_with?(l, locale <> "_") end)
81     end
82   end
83
84   def get_locales do
85     Process.get({Pleroma.Web.Gettext, :locales}, [])
86   end
87
88   def is_locale_list(locales) do
89     Enum.all?(locales, &is_binary/1)
90   end
91
92   def put_locales(locales) do
93     if is_locale_list(locales) do
94       Process.put({Pleroma.Web.Gettext, :locales}, Enum.uniq(locales))
95       Gettext.put_locale(Enum.at(locales, 0, Gettext.get_locale()))
96       :ok
97     else
98       {:error, :not_locale_list}
99     end
100   end
101
102   def locale_or_default(locale) do
103     if supports_locale?(locale) do
104       locale
105     else
106       Gettext.get_locale()
107     end
108   end
109
110   def with_locales_func(locales, fun) do
111     prev_locales = Process.get({Pleroma.Web.Gettext, :locales})
112     put_locales(locales)
113
114     try do
115       fun.()
116     after
117       if prev_locales do
118         put_locales(prev_locales)
119       else
120         Process.delete({Pleroma.Web.Gettext, :locales})
121         Process.delete(Gettext)
122       end
123     end
124   end
125
126   defmacro with_locales(locales, do: fun) do
127     quote do
128       Pleroma.Web.Gettext.with_locales_func(unquote(locales), fn ->
129         unquote(fun)
130       end)
131     end
132   end
133
134   def to_locale_list(locale) when is_binary(locale) do
135     locale
136     |> String.split(",")
137     |> Enum.filter(&supports_locale?/1)
138   end
139
140   def to_locale_list(_), do: []
141
142   defmacro with_locale_or_default(locale, do: fun) do
143     quote do
144       Pleroma.Web.Gettext.with_locales_func(
145         Pleroma.Web.Gettext.to_locale_list(unquote(locale))
146         |> Enum.concat(Pleroma.Web.Gettext.get_locales()),
147         fn ->
148           unquote(fun)
149         end
150       )
151     end
152   end
153
154   defp next_locale(locale, list) do
155     index = Enum.find_index(list, fn item -> item == locale end)
156
157     if not is_nil(index) do
158       Enum.at(list, index + 1)
159     else
160       nil
161     end
162   end
163
164   # We do not yet have a proper English translation. The "English"
165   # version is currently but the fallback msgid. However, this
166   # will not work if the user puts English as the first language,
167   # and at the same time specifies other languages, as gettext will
168   # think the English translation is missing, and call
169   # handle_missing_translation functions. This may result in
170   # text in other languages being shown even if English is preferred
171   # by the user.
172   #
173   # To prevent this, we do not allow fallbacking when the current
174   # locale missing a translation is English.
175   defp should_fallback?(locale) do
176     locale != "en"
177   end
178
179   def handle_missing_translation(locale, domain, msgctxt, msgid, bindings) do
180     next = next_locale(locale, get_locales())
181
182     if is_nil(next) or not should_fallback?(locale) do
183       super(locale, domain, msgctxt, msgid, bindings)
184     else
185       {:ok,
186        Gettext.with_locale(next, fn ->
187          Gettext.dpgettext(Pleroma.Web.Gettext, domain, msgctxt, msgid, bindings)
188        end)}
189     end
190   end
191
192   def handle_missing_plural_translation(
193         locale,
194         domain,
195         msgctxt,
196         msgid,
197         msgid_plural,
198         n,
199         bindings
200       ) do
201     next = next_locale(locale, get_locales())
202
203     if is_nil(next) or not should_fallback?(locale) do
204       super(locale, domain, msgctxt, msgid, msgid_plural, n, bindings)
205     else
206       {:ok,
207        Gettext.with_locale(next, fn ->
208          Gettext.dpngettext(
209            Pleroma.Web.Gettext,
210            domain,
211            msgctxt,
212            msgid,
213            msgid_plural,
214            n,
215            bindings
216          )
217        end)}
218     end
219   end
220 end