1defmodule GettextTest.Translator do
2  use Gettext, otp_app: :test_application
3end
4
5defmodule GettextTest.TranslatorWithCustomPriv do
6  use Gettext, otp_app: :test_application, priv: "translations"
7end
8
9defmodule GettextTest.TranslatorWithCustomPluralForms do
10  defmodule Plural do
11    @behaviour Gettext.Plural
12    def nplurals("elv"), do: 2
13    def nplurals(other), do: Gettext.Plural.nplurals(other)
14    # Opposite of Italian (where 1 is singular, everything else is plural)
15    def plural("it", 1), do: 1
16    def plural("it", _), do: 0
17  end
18
19  use Gettext, otp_app: :test_application, plural_forms: Plural
20end
21
22defmodule GettextTest do
23  use ExUnit.Case
24
25  import ExUnit.CaptureLog
26
27  alias GettextTest.Translator
28  alias GettextTest.TranslatorWithCustomPriv
29  alias GettextTest.TranslatorWithCustomPluralForms
30  require Translator
31  require TranslatorWithCustomPriv
32
33  test "the default locale is \"en\"" do
34    assert Gettext.get_locale() == "en"
35    assert Gettext.get_locale(Translator) == "en"
36  end
37
38  test "get_locale/0,1 and put_locale/1,2: setting/getting the locale" do
39    # First, we set the local for just one backend:
40    Gettext.put_locale(Translator, "pt_BR")
41
42    # Now, let's check that only that backend was affected.
43    assert Gettext.get_locale(Translator) == "pt_BR"
44    assert Gettext.get_locale(TranslatorWithCustomPriv) == "en"
45    assert Gettext.get_locale() == "en"
46
47    # Now, let's change the global locale:
48    Gettext.put_locale("it")
49
50    # Let's check that the global locale was affected and that get_locale/1
51    # returns the global locale, but only for backends that have no
52    # backend-specific locale set.
53    assert Gettext.get_locale() == "it"
54    assert Gettext.get_locale(TranslatorWithCustomPriv) == "it"
55    assert Gettext.get_locale(Translator) == "pt_BR"
56  end
57
58  test "get_locale/0,1: using the default locales" do
59    global_default = Application.get_env(:gettext, :default_locale)
60    backend_config = Application.get_env(:test_application, Translator)
61
62    try do
63      Application.put_env(:gettext, :default_locale, "fr")
64
65      assert Gettext.get_locale() == "fr"
66      assert Gettext.get_locale(Translator) == "fr"
67
68      Application.put_env(:test_application, Translator, default_locale: "es")
69
70      assert Gettext.get_locale() == "fr"
71      assert Gettext.get_locale(Translator) == "es"
72    after
73      Application.put_env(:gettext, :default_locale, global_default)
74
75      if backend_config do
76        Application.put_env(:test_application, Translator, backend_config)
77      else
78        Application.delete_env(:test_application, Translator)
79      end
80    end
81  end
82
83  test "put_locale/2: only accepts binaries" do
84    msg = "put_locale/2 only accepts binary locales, got: :en"
85
86    assert_raise ArgumentError, msg, fn ->
87      Gettext.put_locale(Translator, :en)
88    end
89  end
90
91  test "__gettext__(:priv): returns the directory where the translations are stored" do
92    assert Translator.__gettext__(:priv) == "priv/gettext"
93    assert TranslatorWithCustomPriv.__gettext__(:priv) == "translations"
94  end
95
96  test "__gettext__(:otp_app): returns the otp app for the given backend" do
97    assert Translator.__gettext__(:otp_app) == :test_application
98    assert TranslatorWithCustomPriv.__gettext__(:otp_app) == :test_application
99  end
100
101  test "found translations return {:ok, translation}" do
102    assert Translator.lgettext("it", "default", "Hello world", %{}) == {:ok, "Ciao mondo"}
103
104    assert Translator.lgettext("it", "errors", "Invalid email address", %{}) ==
105             {:ok, "Indirizzo email non valido"}
106  end
107
108  test "non-found translations return the argument message" do
109    # Unknown msgid.
110    assert Translator.lgettext("it", "default", "nonexistent", %{}) == {:default, "nonexistent"}
111
112    # Unknown domain.
113    assert Translator.lgettext("it", "unknown", "Hello world", %{}) == {:default, "Hello world"}
114
115    # Unknown locale.
116    assert Translator.lgettext("pt_BR", "nonexistent", "Hello world", %{}) ==
117             {:default, "Hello world"}
118  end
119
120  test "translations with empty msgstrs fallback to {:default, _}" do
121    assert Translator.lgettext("it", "default", "Empty msgstr!", %{}) ==
122             {:default, "Empty msgstr!"}
123  end
124
125  test "a custom 'priv' directory can be used to store translations" do
126    assert TranslatorWithCustomPriv.lgettext("it", "default", "Hello world", %{}) ==
127             {:ok, "Ciao mondo"}
128
129    assert TranslatorWithCustomPriv.lgettext("it", "errors", "Invalid email address", %{}) ==
130             {:ok, "Indirizzo email non valido"}
131  end
132
133  test "using a custom Gettext.Plural module" do
134    alias TranslatorWithCustomPluralForms, as: T
135
136    assert T.lngettext("it", "default", "One new email", "%{count} new emails", 1, %{}) ==
137             {:ok, "1 nuove email"}
138
139    assert T.lngettext("it", "default", "One new email", "%{count} new emails", 2, %{}) ==
140             {:ok, "Una nuova email"}
141  end
142
143  test "translations can be pluralized" do
144    import Translator, only: [lngettext: 6]
145
146    t = lngettext("it", "errors", "There was an error", "There were %{count} errors", 1, %{})
147    assert t == {:ok, "C'è stato un errore"}
148
149    t = lngettext("it", "errors", "There was an error", "There were %{count} errors", 3, %{})
150    assert t == {:ok, "Ci sono stati 3 errori"}
151  end
152
153  test "by default, non-found pluralized translation behave like regular translation" do
154    assert Translator.lngettext("it", "not a domain", "foo", "foos", 1, %{}) == {:default, "foo"}
155
156    assert Translator.lngettext("it", "not a domain", "foo", "foos", 10, %{}) ==
157             {:default, "foos"}
158  end
159
160  test "plural translations with empty msgstrs fallback to {:default, _}" do
161    msgid = "Not even one msgstr"
162    msgid_plural = "Not even %{count} msgstrs"
163
164    assert Translator.lngettext("it", "default", msgid, msgid_plural, 1, %{}) ==
165             {:default, "Not even one msgstr"}
166
167    assert Translator.lngettext("it", "default", msgid, msgid_plural, 2, %{}) ==
168             {:default, "Not even 2 msgstrs"}
169  end
170
171  test "an error is raised if a plural translation has no plural form for the given locale" do
172    log =
173      capture_log(fn ->
174        Code.eval_quoted(
175          quote do
176            defmodule BadTranslations do
177              use Gettext, otp_app: :test_application, priv: "bad_translations"
178            end
179          end
180        )
181      end)
182
183    assert log =~ "translation is missing plural form 2 which is required by the locale \"ru\""
184
185    msgid = "should be at least %{count} character(s)"
186    msgid_plural = "should be at least %{count} character(s)"
187
188    assert_raise Gettext.Error,
189                 ~r/plural form 2 is required for locale \"ru\" but is missing/,
190                 fn ->
191                   BadTranslations.lngettext("ru", "errors", msgid, msgid_plural, 8, %{})
192                   |> IO.inspect()
193                 end
194  end
195
196  test "interpolation is supported by lgettext" do
197    assert Translator.lgettext("it", "interpolations", "Hello %{name}", %{name: "Jane"}) ==
198             {:ok, "Ciao Jane"}
199
200    msgid = "My name is %{name} and I'm %{age}"
201
202    assert Translator.lgettext("it", "interpolations", msgid, %{name: "Meg", age: 33}) ==
203             {:ok, "Mi chiamo Meg e ho 33 anni"}
204
205    # A map of bindings is supported as well.
206    assert Translator.lgettext("it", "interpolations", "Hello %{name}", %{name: "Jane"}) ==
207             {:ok, "Ciao Jane"}
208  end
209
210  test "interpolation is supported by lngettext" do
211    msgid = "There was an error"
212    msgid_plural = "There were %{count} errors"
213
214    assert Translator.lngettext("it", "errors", msgid, msgid_plural, 1, %{}) ==
215             {:ok, "C'è stato un errore"}
216
217    assert Translator.lngettext("it", "errors", msgid, msgid_plural, 4, %{}) ==
218             {:ok, "Ci sono stati 4 errori"}
219
220    msgid = "You have one message, %{name}"
221    msgid_plural = "You have %{count} messages, %{name}"
222
223    assert Translator.lngettext("it", "interpolations", msgid, msgid_plural, 1, %{name: "Jane"}) ==
224             {:ok, "Hai un messaggio, Jane"}
225
226    assert Translator.lngettext("it", "interpolations", msgid, msgid_plural, 0, %{name: "Jane"}) ==
227             {:ok, "Hai 0 messaggi, Jane"}
228  end
229
230  test "strings are concatenated before generating function clauses" do
231    msgid = "Concatenated and long string"
232
233    assert Translator.lgettext("it", "default", msgid, %{}) ==
234             {:ok, "Stringa lunga e concatenata"}
235
236    msgid = "A friend"
237    msgid_plural = "%{count} friends"
238    assert Translator.lngettext("it", "default", msgid, msgid_plural, 1, %{}) == {:ok, "Un amico"}
239  end
240
241  test "lgettext/4: default handle_missing_binding preserves key" do
242    msgid = "My name is %{name} and I'm %{age}"
243
244    assert Translator.lgettext("it", "interpolations", msgid, %{name: "José"}) ==
245             {:missing_bindings, "Mi chiamo José e ho %{age} anni", [:age]}
246  end
247
248  test "lgettext/4: interpolation works when a translation is missing" do
249    msgid = "Hello %{name}, missing translation!"
250
251    assert Translator.lgettext("pl", "foo", msgid, %{name: "Samantha"}) ==
252             {:default, "Hello Samantha, missing translation!"}
253
254    msgid = "Hello world!"
255    assert Translator.lgettext("pl", "foo", msgid, %{}) == {:default, "Hello world!"}
256
257    msgid = "Hello %{name}"
258
259    assert Translator.lgettext("pl", "foo", msgid, %{}) ==
260             {:missing_bindings, "Hello %{name}", [:name]}
261  end
262
263  test "lngettext/6: default handle_missing_binding preserves key" do
264    msgid = "You have one message, %{name}"
265    msgid_plural = "You have %{count} messages, %{name}"
266
267    assert Translator.lngettext("it", "interpolations", msgid, msgid_plural, 1, %{}) ==
268             {:missing_bindings, "Hai un messaggio, %{name}", [:name]}
269
270    assert Translator.lngettext("it", "interpolations", msgid, msgid_plural, 6, %{}) ==
271             {:missing_bindings, "Hai 6 messaggi, %{name}", [:name]}
272  end
273
274  test "lngettext/6: interpolation works when a translation is missing" do
275    msgid = "One error"
276    msgid_plural = "%{count} errors"
277
278    assert Translator.lngettext("pl", "foo", msgid, msgid_plural, 1, %{}) ==
279             {:default, "One error"}
280
281    assert Translator.lngettext("pl", "foo", msgid, msgid_plural, 9, %{}) ==
282             {:default, "9 errors"}
283  end
284
285  test "dgettext/3: binary msgid at compile-time" do
286    Gettext.put_locale(Translator, "it")
287
288    assert Translator.dgettext("errors", "Invalid email address") == "Indirizzo email non valido"
289    keys = %{name: "Jim"}
290    assert Translator.dgettext("interpolations", "Hello %{name}", keys) == "Ciao Jim"
291
292    log =
293      capture_log(fn ->
294        assert Translator.dgettext("interpolations", "Hello %{name}") == "Ciao %{name}"
295      end)
296
297    assert log =~ ~s/[error] missing Gettext bindings: [:name]/
298  end
299
300  # Macros.
301
302  @gettext_msgid "Hello world"
303
304  test "gettext/2: binary-ish msgid at compile-time" do
305    Gettext.put_locale(Translator, "it")
306    assert Translator.gettext("Hello world") == "Ciao mondo"
307    assert Translator.gettext(@gettext_msgid) == "Ciao mondo"
308    assert Translator.gettext(~s(Hello world)) == "Ciao mondo"
309  end
310
311  test "dgettext/3 and dngettext/2: non-binary things at compile-time" do
312    code =
313      quote do
314        require Translator
315        msgid = "Invalid email address"
316        Translator.dgettext("errors", msgid)
317      end
318
319    error = assert_raise ArgumentError, fn -> Code.eval_quoted(code) end
320    assert ArgumentError.message(error) =~ "*gettext macros expect translation keys"
321    assert ArgumentError.message(error) =~ "Gettext.gettext(GettextTest.Translator, string)"
322
323    code =
324      quote do
325        require Translator
326        msgid_plural = ~s(foo #{1 + 1} bar)
327        Translator.dngettext("default", "foo", msgid_plural, 1)
328      end
329
330    error = assert_raise ArgumentError, fn -> Code.eval_quoted(code) end
331    assert ArgumentError.message(error) =~ "*gettext macros expect translation keys"
332    assert ArgumentError.message(error) =~ "Gettext.gettext(GettextTest.Translator, string)"
333
334    code =
335      quote do
336        require Translator
337        domain = "dynamic_domain"
338        Translator.dgettext(domain, "hello")
339      end
340
341    error = assert_raise ArgumentError, fn -> Code.eval_quoted(code) end
342    assert ArgumentError.message(error) =~ "*gettext macros expect translation keys"
343  end
344
345  test "dngettext/5" do
346    Gettext.put_locale(Translator, "it")
347
348    assert Translator.dngettext(
349             "interpolations",
350             "You have one message, %{name}",
351             "You have %{count} messages, %{name}",
352             1,
353             %{name: "James"}
354           ) == "Hai un messaggio, James"
355
356    assert Translator.dngettext(
357             "interpolations",
358             "You have one message, %{name}",
359             "You have %{count} messages, %{name}",
360             2,
361             %{name: "James"}
362           ) == "Hai 2 messaggi, James"
363  end
364
365  @ngettext_msgid "One new email"
366  @ngettext_msgid_plural "%{count} new emails"
367
368  test "ngettext/4" do
369    Gettext.put_locale(Translator, "it")
370    assert Translator.ngettext("One new email", "%{count} new emails", 1) == "Una nuova email"
371    assert Translator.ngettext("One new email", "%{count} new emails", 2) == "2 nuove email"
372
373    assert Translator.ngettext(@ngettext_msgid, @ngettext_msgid_plural, 1) == "Una nuova email"
374    assert Translator.ngettext(@ngettext_msgid, @ngettext_msgid_plural, 2) == "2 nuove email"
375  end
376
377  test "the d?n?gettext macros support a kw list for interpolation" do
378    Gettext.put_locale(Translator, "it")
379    assert Translator.gettext("%{msg}", msg: "foo") == "foo"
380  end
381
382  test "(d)gettext_noop" do
383    assert Translator.dgettext_noop("errors", "Oops") == "Oops"
384    assert Translator.gettext_noop("Hello %{name}!") == "Hello %{name}!"
385  end
386
387  test "n(d)gettext_noop" do
388    assert Translator.dngettext_noop("errors", "One error", "%{count} errors") ==
389             {"One error", "%{count} errors"}
390
391    assert Translator.ngettext_noop("One message", "%{count} messages") ==
392             {"One message", "%{count} messages"}
393  end
394
395  # Actual Gettext functions (not the ones generated in the modules that `use
396  # Gettext`).
397
398  test "dgettext/4" do
399    Gettext.put_locale(Translator, "it")
400
401    msgid = "Invalid email address"
402    assert Gettext.dgettext(Translator, "errors", msgid) == "Indirizzo email non valido"
403
404    assert Gettext.dgettext(Translator, "foo", "Foo") == "Foo"
405
406    log =
407      capture_log(fn ->
408        assert Gettext.dgettext(Translator, "interpolations", "Hello %{name}", %{}) ==
409                 "Ciao %{name}"
410      end)
411
412    assert log =~ "[error] missing Gettext bindings: [:name]"
413  end
414
415  test "gettext/3" do
416    Gettext.put_locale(Translator, "it")
417    assert Gettext.gettext(Translator, "Hello world") == "Ciao mondo"
418    assert Gettext.gettext(Translator, "Nonexistent") == "Nonexistent"
419  end
420
421  test "dngettext/6" do
422    Gettext.put_locale(Translator, "it")
423    msgid = "You have one message, %{name}"
424    msgid_plural = "You have %{count} messages, %{name}"
425
426    assert Gettext.dngettext(Translator, "interpolations", msgid, msgid_plural, 1, %{name: "Meg"}) ==
427             "Hai un messaggio, Meg"
428
429    assert Gettext.dngettext(Translator, "interpolations", msgid, msgid_plural, 5, %{name: "Meg"}) ==
430             "Hai 5 messaggi, Meg"
431  end
432
433  test "ngettext/5" do
434    Gettext.put_locale(Translator, "it")
435    msgid = "One cake, %{name}"
436    msgid_plural = "%{count} cakes, %{name}"
437    assert Gettext.ngettext(Translator, msgid, msgid_plural, 1, %{name: "Meg"}) == "One cake, Meg"
438    assert Gettext.ngettext(Translator, msgid, msgid_plural, 5, %{name: "Meg"}) == "5 cakes, Meg"
439  end
440
441  test "the d?n?gettext functions support kw list for interpolations" do
442    Gettext.put_locale(Translator, "it")
443    assert Gettext.gettext(Translator, "Hello %{name}", name: "José") == "Hello José"
444  end
445
446  test "with_locale/3 runs a function with a given locale and returns the returned value" do
447    Gettext.put_locale(Translator, "fr")
448    # no 'fr' translation
449    assert Gettext.gettext(Translator, "Hello world") == "Hello world"
450
451    res =
452      Gettext.with_locale(Translator, "it", fn ->
453        assert Gettext.gettext(Translator, "Hello world") == "Ciao mondo"
454        :foo
455      end)
456
457    assert Gettext.get_locale(Translator) == "fr"
458    assert res == :foo
459  end
460
461  test "with_locale/3 resets the locale even if the given function raises" do
462    Gettext.put_locale(Translator, "fr")
463
464    assert_raise RuntimeError, fn ->
465      Gettext.with_locale(Translator, "it", fn -> raise "foo" end)
466    end
467
468    assert Gettext.get_locale(Translator) == "fr"
469
470    catch_throw(Gettext.with_locale(Translator, "it", fn -> throw(:foo) end))
471    assert Gettext.get_locale(Translator) == "fr"
472  end
473
474  test "with_locale/3: doesn't raise if no locale was set (defaulting to 'en')" do
475    Process.delete(Translator)
476
477    Gettext.with_locale(Translator, "it", fn ->
478      assert Gettext.gettext(Translator, "Hello world") == "Ciao mondo"
479    end)
480
481    assert Gettext.get_locale(Translator) == "en"
482  end
483
484  test "known_locales/1: returns all the locales for which a backend has PO files" do
485    assert Gettext.known_locales(Translator) == ["it"]
486    assert Gettext.known_locales(TranslatorWithCustomPriv) == ["it"]
487  end
488
489  test "a warning is issued in l(n)gettext when the domain contains slashes" do
490    log =
491      capture_log(fn ->
492        assert Translator.dgettext("sub/dir/domain", "hello") == "hello"
493      end)
494
495    assert log =~ ~s(Slashes in domains are not supported: "sub/dir/domain")
496  end
497
498  if function_exported?(Kernel.ParallelCompiler, :async, 1) do
499    defmodule TranslatorWithOneModulePerLocale do
500      use Gettext, otp_app: :test_application, one_module_per_locale: true
501    end
502
503    test "may define one module per locale" do
504      import TranslatorWithOneModulePerLocale, only: [lgettext: 4, lngettext: 6]
505      assert Code.ensure_loaded?(TranslatorWithOneModulePerLocale.T_it)
506
507      # Found on default domain.
508      assert lgettext("it", "default", "Hello world", %{}) == {:ok, "Ciao mondo"}
509
510      # Found on errors domain.
511      assert lgettext("it", "errors", "Invalid email address", %{}) ==
512               {:ok, "Indirizzo email non valido"}
513
514      # Found with plural form.
515      assert lngettext("it", "errors", "There was an error", "There were %{count} errors", 1, %{}) ==
516               {:ok, "C'è stato un errore"}
517
518      # Unknown msgid.
519      assert lgettext("it", "default", "nonexistent", %{}) == {:default, "nonexistent"}
520
521      # Unknown domain.
522      assert lgettext("it", "unknown", "Hello world", %{}) == {:default, "Hello world"}
523
524      # Unknown locale.
525      assert lgettext("pt_BR", "nonexistent", "Hello world", %{}) == {:default, "Hello world"}
526    end
527  end
528end
529