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