Merge pull request 'metrics' (#375) from stats into develop
[akkoma] / lib / pleroma / web / gettext.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2021 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