Repository: elixir-gettext/gettext
Pull request #228 (2019-11-14 15:51:57)

added context aware translation through pgettext, dpgettext, pngettext and dpngettext
As stated in the title, this pull request implements the context aware functionality. What follows are the biggest changes:

## Gettext.Compiler:
`lgettext/4` has been updated to `lgettext/5` to accept the `msgctxt` argument, the same goes for the `lngettext/7` function. I updated the tests to reflect these changes. 

`dpgettext` and `dpngettext` are now the final functions that are called by the other functions that have less functionality (dgettext, pgettext, gettext and ngettext, pngettext dngettext respectively). Defaults are applied as per the specification, a non-existent context is represented as `nil`. 

`dpgettext_noop` returns a tuple of {msgid, msgctxt}, `dpngettext_noop` now returns a tuple of {{msgid, msgid_plural}, msgctxt}. I tried a flat tuple for the `dpngettext_noop` function but was given a cryptic AST error... This is my pragmatic solution to that error.

## Gettext.Extractor:
`extract/5` has been changed to `extract/6` by the addition of the msgctxt argument. This is passed to the Translation struct properly. 
`merge_translations/2` has been modified to set the `msgctxt` of the new Translation struct.

I also updated the Context section of the documentation to reflect the changes. 
Please let me know what is missing/wrong so I can add/change it.

josevalim: Btw, please let me know when this is ready for one final review. :)
Zurga: @josevalim I will, thank you for the comments and the AST node solution. I learned something!
Zurga: I think this pull request is ready for a final review.
Zurga: @josevalim I think it is ready for review. Anything you find, I will fix later.

Patch:
diff --git a/lib/gettext.ex b/lib/gettext.ex
index 9ae3012..3a255cd 100644
--- a/lib/gettext.ex
+++ b/lib/gettext.ex
@@ -28,6 +28,17 @@ defmodule Gettext do
       # Domain-based translation
       dgettext "errors", "Here is the error message to translate"
 
+      # Context-based translation
+      pgettext "email", "Email text to translate"
+      
+      # All of the above
+      dpngettext "errors", "context", "Here is the string to translate",
+                                      "Here are the strings to translate",
+                                      3
+
+  The arguments for the gettext macros and their order can be derived from
+  their names. For `dpgettext/4` the arguments are: `domain`, `context`, `msgid`, `bindings` (default to `%{}`).
+
   Translations are looked up from `.po` files. In the following sections we will
   explore exactly what are those files before we explore the "Gettext API" in
   detail.
@@ -40,6 +51,7 @@ defmodule Gettext do
       # This is a comment
       msgid "Hello world!"
       msgstr "Ciao mondo!"
+      msgctxt "context" # Optional
 
   PO files containing translations for an application must be stored in a
   directory (by default it's `priv/gettext`) that has the following structure:
@@ -153,9 +165,13 @@ defmodule Gettext do
 
     * `gettext/2`
     * `dgettext/3`
+    * `pgettext/3`
+    * `dpgettext/4`
     * `ngettext/4`
     * `dngettext/5`
-    * `gettext_noop/1`, `dgettext_noop/2`, `ngettext_noop/3`, `dngettext_noop/4`
+    * `pngettext/5`
+    * `dpngettext/6`
+    * `gettext_noop/1`, `dgettext_noop/2`, `pgettext_noop/2`, `dpgettext_noop/3`, `ngettext_noop/3`, `pngettext_noop/4`, `dngettext_noop/4`, `dpngettext_noop/5`
 
   Supposing the caller module is `MyApp.Gettext`, the macros mentioned above
   behave as follows:
@@ -164,10 +180,18 @@ defmodule Gettext do
       like `Gettext.gettext(MyApp.Gettext, msgid, bindings)`
     * `dgettext(domain, msgid, bindings \\ %{})` -
       like `Gettext.dgettext(MyApp.Gettext, domain, msgid, bindings)`
+    * `pgettext(msgctxt, msgid, bindings \\ %{})` -
+      like `Gettext.pgettext(MyApp.Gettext, msgctxt, msgid, bindings)`
+    * `dpgettext(domain, msgctxt, msgid, bindings \\ %{})` -
+      like `Gettext.dpgettext(MyApp.Gettext, domain, msgctxt, msgid, bindings)`
     * `ngettext(msgid, msgid_plural, n, bindings \\ %{})` -
       like `Gettext.ngettext(MyApp.Gettext, msgid, msgid_plural, n, bindings)`
     * `dngettext(domain, msgid, msgid_plural, n, bindings \\ %{})` -
       like `Gettext.dngettext(MyApp.Gettext, domain, msgid, msgid_plural, n, bindings)`
+    * `pngettext(msgctxt, msgid, msgid_plural, n, bindings \\ %{})` -
+      like `Gettext.pngettext(MyApp.Gettext, msgctxt, msgid, msgid_plural, n, bindings)`
+    * `dpngettext(domain, msgctxt, msgid, msgid_plural, n, bindings \\ %{})` -
+      like `Gettext.dpngettext(MyApp.Gettext, domain, msgctxt, msgid, msgid_plural, n, bindings)`
     * `*_noop` family of functions - used to mark translations for extraction
       without translating them; see the documentation for these macros in
       `Gettext.Backend`
@@ -193,8 +217,8 @@ defmodule Gettext do
       MyApp.Gettext.gettext @msgid
       #=> "Ciao mondo"
 
-  The `gettext`/`dgettext`/`ngettext`/`dngettext` macros raise an
-  `ArgumentError` exception if they receive a `domain`, `msgid`, or
+  The `gettext`/`dgettext`/`pgettext`/`dpgettext`/`ngettext`/`pngettext`/`dngettext`/`dpngettext` macros raise an
+  `ArgumentError` exception if they receive a `domain`, `msgctxt`, `msgid`, or
   `msgid_plural` that doesn't expand to a string at compile time:
 
       msgid = "Hello world"
@@ -308,14 +332,26 @@ defmodule Gettext do
   To learn more about pluralization rules, plural forms and what they mean to
   Gettext check the documentation for `Gettext.Plural`.
 
+  ## Contexts
+
+  The GNU Gettext implementation supports
+  [*contexts*](https://www.gnu.org/software/gettext/manual/html_node/Contexts.html),
+  which are a way to "contextualize" translations. For example, in English, the
+  word "file" could be used both as a noun or as a verb. Contexts can be used to
+  solve similar problems: one could have a "imperative_verbs" context and a
+  "nouns" context as to avoid ambiguity. The functions that handle contexts are 
+  are `pgettext`, `dpgettext`, `pngettext` and `dpngettext`. 
+  `dpgettext` can be read as d(omain)p(articular)gettext. The same reading rules
+  apply for `dpngettext`.
+
   ## Missing translations
 
   When a translation is missing in the specified locale (both with functions as
   well as with macros), the argument is returned:
 
-    * in case of calls to `gettext`/`dgettext`, the `msgid` argument is returned
+    * in case of calls to `gettext`/`dgettext`/`pgettext`/`dpgettext`, the `msgid` argument is returned
       as is;
-    * in case of calls to `ngettext`/`dngettext`, the `msgid` argument is
+    * in case of calls to `ngettext`/`dngettext`/`pngettext`/`dpngettext`, the `msgid` argument is
       returned in case of a singular value and the `msgid_plural` is returned in
       case of a plural value (following the English pluralization rule).
 
@@ -334,21 +370,6 @@ defmodule Gettext do
   translation is considered to have an empty `msgstr` if at least one
   translation in the `msgstr` is empty.
 
-  ## Contexts
-
-  The GNU Gettext implementation supports
-  [*contexts*](https://www.gnu.org/software/gettext/manual/html_node/Contexts.html),
-  which are a way to "contextualize" translations. For example, in English, the
-  word "file" could be used both as a noun or as a verb. Contexts can be used to
-  solve similar problems: one could have a "imperative_verbs" context and a
-  "nouns" context as to avoid ambiguity. However, contexts increase the
-  complexity of Gettext and would increase the complexity of the implementation
-  of Gettext for Elixir, and for this reason we decided to not support them. The
-  problem they try to solve can still be solved just using domains: for example,
-  one could have the `default-imperative_verbs` domain and the `default-nouns`
-  domain and use the `d(n)gettext` family of macros/functions, and the final
-  result would be similar
-
   ## Compile-time features
 
   As mentioned above, using the gettext macros (as opposed to functions) allows
@@ -487,20 +508,23 @@ defmodule Gettext do
     An error message raised for missing bindings errors.
     """
 
-    @enforce_keys [:backend, :domain, :locale, :msgid, :missing]
-    defexception [:backend, :domain, :locale, :msgid, :missing]
+    @enforce_keys [:backend, :domain, :msgctxt, :locale, :msgid, :missing]
+    defexception [:backend, :domain, :msgctxt, :locale, :msgid, :missing]
 
     @type t() :: %__MODULE__{}
 
     def message(%{
           backend: backend,
           domain: domain,
+          msgctxt: msgctxt,
           locale: locale,
           msgid: msgid,
           missing: missing
         }) do
       "missing Gettext bindings: #{inspect(missing)} (backend #{inspect(backend)}, " <>
-        "locale #{inspect(locale)}, domain #{inspect(domain)}, msgid #{inspect(msgid)})"
+        "locale #{inspect(locale)}, domain #{inspect(domain)}, msgctxt #{inspect(msgctxt)}, msgid #{
+          inspect(msgid)
+        })"
     end
   end
 
@@ -634,6 +658,44 @@ defmodule Gettext do
   def put_locale(_backend, locale),
     do: raise(ArgumentError, "put_locale/2 only accepts binary locales, got: #{inspect(locale)}")
 
+  @doc """
+  Returns the translation of the given string with a given context in the given domain.
+
+  The string is translated by the `backend` module.
+
+  The translated string is interpolated based on the `bindings` argument. For
+  more information on how interpolation works, refer to the documentation of the
+  `Gettext` module.
+
+  If the translation for the given `msgid` is not found, the `msgid`
+  (interpolated if necessary) is returned.
+
+  ## Examples
+
+      defmodule MyApp.Gettext do
+        use Gettext, otp_app: :my_app
+      end
+
+      Gettext.put_locale(MyApp.Gettext, "it")
+
+      Gettext.dpgettext(MyApp.Gettext, "errors", "user error", "Invalid")
+      #=> "Non valido"
+      
+      Gettext.dgettext(MyApp.Gettext, "errors", "signup form", "%{name} is not a valid name", name: "Meg")
+      #=> "Meg non è un nome valido"
+  """
+  @spec dpgettext(module, binary, binary, binary, bindings) :: binary
+  def dpgettext(backend, domain, msgctxt, msgid, bindings) when is_list(bindings) do
+    dpgettext(backend, domain, msgctxt, msgid, Map.new(bindings))
+  end
+
+  def dpgettext(backend, domain, msgctxt, msgid, bindings)
+      when is_atom(backend) and is_binary(domain) and is_binary(msgid) and is_map(bindings) do
+    locale = get_locale(backend)
+    result = backend.lgettext(locale, domain, msgctxt, msgid, bindings)
+    handle_backend_result(result, backend, locale, domain, msgctxt, msgid)
+  end
+
   @doc """
   Returns the translation of the given string in the given domain.
 
@@ -665,17 +727,42 @@ defmodule Gettext do
 
   """
   @spec dgettext(module, binary, binary, bindings) :: binary
-  def dgettext(backend, domain, msgid, bindings \\ %{})
-
-  def dgettext(backend, domain, msgid, bindings) when is_list(bindings) do
-    dgettext(backend, domain, msgid, Map.new(bindings))
+  def dgettext(backend, domain, msgid, bindings \\ %{}) do
+    dpgettext(backend, domain, nil, msgid, bindings)
   end
 
-  def dgettext(backend, domain, msgid, bindings)
-      when is_atom(backend) and is_binary(domain) and is_binary(msgid) and is_map(bindings) do
-    locale = get_locale(backend)
-    result = backend.lgettext(locale, domain, msgid, bindings)
-    handle_backend_result(result, backend, locale, domain, msgid)
+  @doc """
+  Returns the translation of the given string with the given context
+
+  The string is translated by the `backend` module.
+
+  The translated string is interpolated based on the `bindings` argument. For
+  more information on how interpolation works, refer to the documentation of the
+  `Gettext` module.
+
+  If the translation for the given `msgid` is not found, the `msgid`
+  (interpolated if necessary) is returned.
+
+  ## Examples
+
+      defmodule MyApp.Gettext do
+        use Gettext, otp_app: :my_app
+      end
+
+      Gettext.put_locale(MyApp.Gettext, "it")
+
+      Gettext.pgettext(MyApp.Gettext, "user-interface", "Invalid")
+      #=> "Non valido"
+
+      Gettext.pgettext(MyApp.Gettext, "user-interface", "%{name} is not a valid name", name: "Meg")
+      #=> "Meg non è un nome valido"
+
+      Gettext.pgettext(MyApp.Gettext, "alerts-users", "nonexisting")
+      #=> "nonexisting"
+  """
+  @spec pgettext(module, binary, binary, bindings) :: binary
+  def pgettext(backend, msgctxt, msgid, bindings \\ %{}) do
+    dpgettext(backend, "default", msgctxt, msgid, bindings)
   end
 
   @doc """
@@ -692,7 +779,7 @@ defmodule Gettext do
   end
 
   @doc """
-  Returns the pluralized translation of the given string in the given domain.
+  Returns the pluralized translation of the given string with a given context in the given domain.
 
   The string is translated and pluralized by the `backend` module.
 
@@ -710,27 +797,70 @@ defmodule Gettext do
         use Gettext, otp_app: :my_app
       end
 
-      Gettext.dngettext(MyApp.Gettext, "errors", "Error", "%{count} errors", 3)
+      Gettext.dpngettext(MyApp.Gettext, "errors", "user error", "Error", "%{count} errors", 3)
       #=> "3 errori"
-      Gettext.dngettext(MyApp.Gettext, "errors", "Error", "%{count} errors", 1)
+      Gettext.dpngettext(MyApp.Gettext, "errors", "user error", "Error", "%{count} errors", 1)
       #=> "Errore"
 
   """
-  @spec dngettext(module, binary, binary, binary, non_neg_integer, bindings) :: binary
-  def dngettext(backend, domain, msgid, msgid_plural, n, bindings \\ %{})
+  @spec dpngettext(module, binary, binary, binary, binary, non_neg_integer, bindings) :: binary
+  def dpngettext(backend, domain, msgctxt, msgid, msgid_plural, n, bindings \\ %{})
 
-  def dngettext(backend, domain, msgid, msgid_plural, n, bindings) when is_list(bindings) do
-    dngettext(backend, domain, msgid, msgid_plural, n, Map.new(bindings))
+  def dpngettext(backend, domain, msgctxt, msgid, msgid_plural, n, bindings)
+      when is_list(bindings) do
+    dpngettext(backend, domain, msgctxt, msgid, msgid_plural, n, Map.new(bindings))
   end
 
-  def dngettext(backend, domain, msgid, msgid_plural, n, bindings)
+  def dpngettext(backend, domain, msgctxt, msgid, msgid_plural, n, bindings)
       when is_atom(backend) and is_binary(domain) and is_binary(msgid) and is_binary(msgid_plural) and
              is_integer(n) and n >= 0 and is_map(bindings) do
     locale = get_locale(backend)
-    result = backend.lngettext(locale, domain, msgid, msgid_plural, n, bindings)
-    handle_backend_result(result, backend, locale, domain, msgid)
+    result = backend.lngettext(locale, domain, msgctxt, msgid, msgid_plural, n, bindings)
+    handle_backend_result(result, backend, locale, domain, msgctxt, msgid)
   end
 
+  @doc """
+  Returns the pluralized translation of the given string in the given domain.
+
+  The string is translated and pluralized by the `backend` module.
+
+  The translated string is interpolated based on the `bindings` argument. For
+  more information on how interpolation works, refer to the documentation of the
+  `Gettext` module.
+
+  If the translation for the given `msgid` and `msgid_plural` is not found, the
+  `msgid` or `msgid_plural` (based on `n` being singular or plural) is returned
+  (interpolated if necessary).
+
+  ## Examples
+
+      defmodule MyApp.Gettext do
+        use Gettext, otp_app: :my_app
+      end
+
+      Gettext.dngettext(MyApp.Gettext, "errors", "Error", "%{count} errors", 3)
+      #=> "3 errori"
+      Gettext.dngettext(MyApp.Gettext, "errors", "Error", "%{count} errors", 1)
+      #=> "Errore"
+
+  """
+  @spec dngettext(module, binary, binary, binary, non_neg_integer, bindings) :: binary
+  def dngettext(backend, domain, msgid, msgid_plural, n, bindings),
+    do: dpngettext(backend, domain, nil, msgid, msgid_plural, n, bindings)
+
+  @doc """
+  Returns the pluralized translation of the given string with a given context
+  in the `"default"` domain.
+
+  Works exactly like:
+
+      Gettext.dpngettext(backend, "default", context, msgid, msgid_plural, n, bindings)
+
+  """
+  @spec pngettext(module, binary, binary, binary, non_neg_integer, bindings) :: binary
+  def pngettext(backend, msgctxt, msgid, msgid_plural, n, bindings),
+    do: dpngettext(backend, "default", msgctxt, msgid, msgid_plural, n, bindings)
+
   @doc """
   Returns the pluralized translation of the given string in the `"default"`
   domain.
@@ -863,11 +993,11 @@ defmodule Gettext do
     backend.__gettext__(:known_locales)
   end
 
-  defp handle_backend_result({:ok, string}, _backend, _locale, _domain, _msgid) do
+  defp handle_backend_result({:ok, string}, _backend, _locale, _domain, _msgctxt, _msgid) do
     string
   end
 
-  defp handle_backend_result({:default, string}, _backend, _locale, _domain, _msgid) do
+  defp handle_backend_result({:default, string}, _backend, _locale, _domain, _msgctxt, _msgid) do
     string
   end
 
@@ -876,12 +1006,14 @@ defmodule Gettext do
          backend,
          locale,
          domain,
+         msgctxt,
          msgid
        ) do
     exception = %MissingBindingsError{
       backend: backend,
       locale: locale,
       domain: domain,
+      msgctxt: msgctxt,
       msgid: msgid,
       missing: missing
     }
@@ -889,7 +1021,7 @@ defmodule Gettext do
     backend.handle_missing_bindings(exception, incomplete)
   end
 
-  defp handle_backend_result({:error, reason}, _backend, _locale, _domain, _msgid) do
+  defp handle_backend_result({:error, reason}, _backend, _locale, _domain, _msgctxt, _msgid) do
     raise Error, reason
   end
 end
diff --git a/lib/gettext/backend.ex b/lib/gettext/backend.ex
index c8d8bb2..18f1334 100644
--- a/lib/gettext/backend.ex
+++ b/lib/gettext/backend.ex
@@ -83,6 +83,29 @@ defmodule Gettext.Backend do
               bindings :: map()
             ) :: {:ok | :default, String.t()}
 
+  @doc """
+  Translates the given `msgid` with a given context (`msgctxt`) in the given `domain`.
+
+  `bindings` is a map of bindings to support interpolation.
+
+  See also `Gettext.dpgettext/5`.
+  """
+  @macrocallback dpgettext(
+                   domain :: Macro.t(),
+                   msgctxt :: String.t(),
+                   msgid ::
+                     String.t(),
+                   bindings :: Macro.t()
+                 ) :: Macro.t()
+
+  @doc """
+  Same as `dpgettext(domain, msgid, %{})`.
+
+  See also `Gettext.dpgettext/5`.
+  """
+  @macrocallback dpgettext(domain :: Macro.t(), msgctxt :: String.t(), msgid :: String.t()) ::
+                   Macro.t()
+
   @doc """
   Translates the given `msgid` in the given `domain`.
 
@@ -100,6 +123,23 @@ defmodule Gettext.Backend do
   """
   @macrocallback dgettext(domain :: Macro.t(), msgid :: String.t()) :: Macro.t()
 
+  @doc """
+  Translates the given `msgid` with the given context (`msgctxt`).
+
+  `bindings` is a map of bindings to support interpolation.
+
+  See also `Gettext.pgettext/4`.
+  """
+  @macrocallback pgettext(msgctxt :: String.t(), msgid :: String.t(), bindings :: Macro.t()) ::
+                   Macro.t()
+
+  @doc """
+  Same as `pgettext(msgctxt, msgid, %{})`.
+
+  See also `Gettext.pgettext/4`.
+  """
+  @macrocallback pgettext(msgctxt :: String.t(), msgid :: String.t()) :: Macro.t()
+
   @doc """
   Same as `dgettext("default", msgid, %{})`.
 
@@ -114,6 +154,37 @@ defmodule Gettext.Backend do
   """
   @macrocallback gettext(msgid :: String.t()) :: Macro.t()
 
+  @doc """
+  Translates the given plural translation (`msgid` + `msgid_plural`) with the given context (`msgctxt`)
+  in the given `domain`.
+
+  `n` is an integer used to determine how to pluralize the
+  translation. `bindings` is a map of bindings to support interpolation.
+
+  See also `Gettext.dpngettext/7`.
+  """
+  @macrocallback dpngettext(
+                   domain :: Macro.t(),
+                   msgctxt :: String.t(),
+                   msgid :: String.t(),
+                   msgid_plural :: String.t(),
+                   n :: Macro.t(),
+                   bindings :: Macro.t()
+                 ) :: Macro.t()
+
+  @doc """
+  Same as `dpngettext(domain, msgctxt, msgid, msgid_plural, n, %{})`.
+
+  See also `Gettext.dpngettext/7`.
+  """
+  @macrocallback dpngettext(
+                   domain :: Macro.t(),
+                   msgctxt :: String.t(),
+                   msgid :: String.t(),
+                   msgid_plural :: String.t(),
+                   n :: Macro.t()
+                 ) :: Macro.t()
+
   @doc """
   Translates the given plural translation (`msgid` + `msgid_plural`) in the
   given `domain`.
@@ -143,6 +214,34 @@ defmodule Gettext.Backend do
                    n :: Macro.t()
                  ) :: Macro.t()
 
+  @doc """
+  Translates the given plural translation (`msgid` + `msgid_plural`) with the given context (`msgctxt`).
+
+  `n` is an integer used to determine how to pluralize the
+  translation. `bindings` is a map of bindings to support interpolation.
+
+  See also `Gettext.pngettext/6`.
+  """
+  @macrocallback pngettext(
+                   msgctxt :: String.t(),
+                   msgid :: String.t(),
+                   msgid_plural :: String.t(),
+                   n :: Macro.t(),
+                   bindings :: Macro.t()
+                 ) :: Macro.t()
+
+  @doc """
+  Same as `pngettext(msgctxt, msgid, msgid_plural, n, %{})`.
+
+  See also `Gettext.pngettext/6`.
+  """
+  @macrocallback pngettext(
+                   msgctxt :: String.t(),
+                   msgid :: String.t(),
+                   msgid_plural :: String.t(),
+                   n :: Macro.t()
+                 ) :: Macro.t()
+
   @doc """
   Same as `dngettext("default", msgid, msgid_plural, n, bindings)`.
 
diff --git a/lib/gettext/compiler.ex b/lib/gettext/compiler.ex
index 2261a3c..5fbb239 100644
--- a/lib/gettext/compiler.ex
+++ b/lib/gettext/compiler.ex
@@ -60,48 +60,63 @@ defmodule Gettext.Compiler do
 
       # These are the two functions we generated inside the backend. Here we define the bodyless
       # clauses.
-      def lgettext(locale, domain, msgid, bindings)
-      def lngettext(locale, domain, msgid, msgid_plural, n, bindings)
+      def lgettext(locale, domain, msgctxt, msgid, bindings)
+      def lngettext(locale, domain, msgctxt, msgid, msgid_plural, n, bindings)
 
       unquote(compile_po_files(env, translations_dir, opts))
 
       # Catch-all clauses.
-      def lgettext(locale, domain, msgid, bindings),
+      def lgettext(locale, domain, msgctxt, msgid, bindings),
         do: handle_missing_translation(locale, domain, msgid, bindings)
 
-      def lngettext(locale, domain, msgid, msgid_plural, n, bindings),
+      def lngettext(locale, domain, msgctxt, msgid, msgid_plural, n, bindings),
         do: handle_missing_plural_translation(locale, domain, msgid, msgid_plural, n, bindings)
     end
   end
 
   defp macros() do
     quote unquote: false do
-      defmacro dgettext_noop(domain, msgid) do
+      defmacro dpgettext_noop(domain, msgctxt, msgid) do
         domain = Gettext.Compiler.expand_to_binary(domain, "domain", __MODULE__, __CALLER__)
         msgid = Gettext.Compiler.expand_to_binary(msgid, "msgid", __MODULE__, __CALLER__)
+        msgctxt = Gettext.Compiler.expand_to_binary(msgctxt, "msgctxt", __MODULE__, __CALLER__)
 
         if Gettext.Extractor.extracting?() do
           Gettext.Extractor.extract(
             __CALLER__,
             __MODULE__,
             domain,
+            msgctxt,
             msgid,
             Gettext.Compiler.get_and_flush_extracted_comments()
           )
         end
 
-        msgid
+        {msgid, msgctxt}
+      end
+
+      defmacro dgettext_noop(domain, msgid) do
+        quote do
+          unquote(__MODULE__).dpgettext_noop(unquote(domain), nil, unquote(msgid))
+        end
       end
 
       defmacro gettext_noop(msgid) do
         quote do
-          unquote(__MODULE__).dgettext_noop("default", unquote(msgid))
+          unquote(__MODULE__).dpgettext_noop("default", nil, unquote(msgid))
         end
       end
 
-      defmacro dngettext_noop(domain, msgid, msgid_plural) do
+      defmacro pgettext_noop(msgid, context) do
+        quote do
+          unquote(__MODULE__).dpgettext_noop("default", unquote(context), unquote(msgid))
+        end
+      end
+
+      defmacro dpngettext_noop(domain, msgctxt, msgid, msgid_plural) do
         domain = Gettext.Compiler.expand_to_binary(domain, "domain", __MODULE__, __CALLER__)
         msgid = Gettext.Compiler.expand_to_binary(msgid, "msgid", __MODULE__, __CALLER__)
+        msgctxt = Gettext.Compiler.expand_to_binary(msgctxt, "msgctxt", __MODULE__, __CALLER__)
 
         msgid_plural =
           Gettext.Compiler.expand_to_binary(msgid_plural, "msgid_plural", __MODULE__, __CALLER__)
@@ -111,45 +126,100 @@ defmodule Gettext.Compiler do
             __CALLER__,
             __MODULE__,
             domain,
+            msgctxt,
             {msgid, msgid_plural},
             Gettext.Compiler.get_and_flush_extracted_comments()
           )
         end
 
-        {msgid, msgid_plural}
+        {:{}, [], [msgid, msgid_plural, msgctxt]}
+      end
+
+      defmacro dngettext_noop(domain, msgid, msgid_plural) do
+        quote do
+          unquote(__MODULE__).dpngettext_noop(
+            unquote(domain),
+            nil,
+            unquote(msgid),
+            unquote(msgid_plural)
+          )
+        end
+      end
+
+      defmacro pngettext_noop(msgctxt, msgid, msgid_plural) do
+        quote do
+          unquote(__MODULE__).dpngettext_noop(
+            "default",
+            unquote(msgctxt),
+            unquote(msgid),
+            unquote(msgid_plural)
+          )
+        end
       end
 
       defmacro ngettext_noop(msgid, msgid_plural) do
         quote do
-          unquote(__MODULE__).dngettext_noop("default", unquote(msgid), unquote(msgid_plural))
+          unquote(__MODULE__).dpngettext_noop(
+            "default",
+            nil,
+            unquote(msgid),
+            unquote(msgid_plural)
+          )
+        end
+      end
+
+      defmacro dpgettext(domain, msgctxt, msgid, bindings \\ Macro.escape(%{})) do
+        quote do
+          {msgid, msgctxt} =
+            unquote(__MODULE__).dpgettext_noop(unquote(domain), unquote(msgctxt), unquote(msgid))
+
+          Gettext.dpgettext(
+            unquote(__MODULE__),
+            unquote(domain),
+            msgctxt,
+            msgid,
+            unquote(bindings)
+          )
         end
       end
 
       defmacro dgettext(domain, msgid, bindings \\ Macro.escape(%{})) do
         quote do
-          msgid = unquote(__MODULE__).dgettext_noop(unquote(domain), unquote(msgid))
-          Gettext.dgettext(unquote(__MODULE__), unquote(domain), msgid, unquote(bindings))
+          unquote(__MODULE__).dpgettext(unquote(domain), nil, unquote(msgid), unquote(bindings))
+        end
+      end
+
+      defmacro pgettext(msgctxt, msgid, bindings \\ Macro.escape(%{})) do
+        quote do
+          unquote(__MODULE__).dpgettext(
+            "default",
+            unquote(msgctxt),
+            unquote(msgid),
+            unquote(bindings)
+          )
         end
       end
 
       defmacro gettext(msgid, bindings \\ Macro.escape(%{})) do
         quote do
-          unquote(__MODULE__).dgettext("default", unquote(msgid), unquote(bindings))
+          unquote(__MODULE__).dpgettext("default", nil, unquote(msgid), unquote(bindings))
         end
       end
 
-      defmacro dngettext(domain, msgid, msgid_plural, n, bindings \\ Macro.escape(%{})) do
+      defmacro dpngettext(domain, msgctxt, msgid, msgid_plural, n, bindings \\ Macro.escape(%{})) do
         quote do
-          {msgid, msgid_plural} =
-            unquote(__MODULE__).dngettext_noop(
+          {msgid, msgid_plural, msgctxt} =
+            unquote(__MODULE__).dpngettext_noop(
               unquote(domain),
+              unquote(msgctxt),
               unquote(msgid),
               unquote(msgid_plural)
             )
 
-          Gettext.dngettext(
+          Gettext.dpngettext(
             unquote(__MODULE__),
             unquote(domain),
+            msgctxt,
             msgid,
             msgid_plural,
             unquote(n),
@@ -158,10 +228,40 @@ defmodule Gettext.Compiler do
         end
       end
 
+      defmacro dngettext(domain, msgid, msgid_plural, n, bindings \\ Macro.escape(%{})) do
+        quote do
+          unquote(__MODULE__).dpngettext(
+            unquote(domain),
+            # Context
+            nil,
+            unquote(msgid),
+            unquote(msgid_plural),
+            unquote(n),
+            unquote(bindings)
+          )
+        end
+      end
+
       defmacro ngettext(msgid, msgid_plural, n, bindings \\ Macro.escape(%{})) do
         quote do
-          unquote(__MODULE__).dngettext(
+          unquote(__MODULE__).dpngettext(
+            # Domain
             "default",
+            # Context
+            nil,
+            unquote(msgid),
+            unquote(msgid_plural),
+            unquote(n),
+            unquote(bindings)
+          )
+        end
+      end
+
+      defmacro pngettext(msgctxt, msgid, msgid_plural, n, bindings \\ Macro.escape(%{})) do
+        quote do
+          unquote(__MODULE__).dpngettext(
+            "default",
+            unquote(msgctxt),
             unquote(msgid),
             unquote(msgid_plural),
             unquote(n),
@@ -184,7 +284,7 @@ defmodule Gettext.Compiler do
   """
   @spec expand_to_binary(binary, binary, module, Macro.Env.t()) :: binary | no_return
   def expand_to_binary(term, what, gettext_module, env)
-      when what in ~w(domain msgid msgid_plural comment) do
+      when what in ~w(domain msgctxt msgid msgid_plural comment) do
     raiser = fn term ->
       raise ArgumentError, """
       Gettext macros expect translation keys (msgid and msgid_plural),
@@ -204,6 +304,9 @@ defmodule Gettext.Compiler do
     end
 
     case Macro.expand(term, env) do
+      term when is_nil(term) ->
+        nil
+
       term when is_binary(term) ->
         term
 
@@ -286,12 +389,12 @@ defmodule Gettext.Compiler do
     quote do
       unquote(quoted)
 
-      def lgettext(unquote(locale), unquote(domain), msgid, bindings) do
-        unquote(singular_fun)(msgid, bindings)
+      def lgettext(unquote(locale), unquote(domain), msgctxt, msgid, bindings) do
+        unquote(singular_fun)(msgctxt, msgid, bindings)
       end
 
-      def lngettext(unquote(locale), unquote(domain), msgid, msgid_plural, n, bindings) do
-        unquote(plural_fun)(msgid, msgid_plural, n, bindings)
+      def lngettext(unquote(locale), unquote(domain), msgctxt, msgid, msgid_plural, n, bindings) do
+        unquote(plural_fun)(msgctxt, msgid, msgid_plural, n, bindings)
       end
     end
   end
@@ -304,12 +407,12 @@ defmodule Gettext.Compiler do
 
     current_module_quoted =
       quote do
-        def lgettext(unquote(locale), unquote(domain), msgid, bindings) do
-          unquote(module).unquote(singular_fun)(msgid, bindings)
+        def lgettext(unquote(locale), unquote(domain), msgctxt, msgid, bindings) do
+          unquote(module).unquote(singular_fun)(msgctxt, msgid, bindings)
         end
 
-        def lngettext(unquote(locale), unquote(domain), msgid, msgid_plural, n, bindings) do
-          unquote(module).unquote(plural_fun)(msgid, msgid_plural, n, bindings)
+        def lngettext(unquote(locale), unquote(domain), msgctxt, msgid, msgid_plural, n, bindings) do
+          unquote(module).unquote(plural_fun)(msgctxt, msgid, msgid_plural, n, bindings)
         end
       end
 
@@ -317,8 +420,8 @@ defmodule Gettext.Compiler do
     {current_module_quoted, locales}
   end
 
-  # Compiles a .po file into a list of lgettext/4 (for translations) and
-  # lngettext/6 (for plural translations) clauses.
+  # Compiles a .po file into a list of lgettext/5 (for translations) and
+  # lngettext/7 (for plural translations) clauses.
   defp compile_po_file(kind, path, env, plural_mod) do
     {locale, domain} = locale_and_domain_from_path(path)
     %PO{translations: translations, file: file} = PO.parse_file!(path)
@@ -332,7 +435,8 @@ defmodule Gettext.Compiler do
       quote do
         unquote(translations)
 
-        Kernel.unquote(kind)(unquote(singular_fun)(msgid, bindings)) do
+        # We ignore the msgctxt in the handle_missing_translation.
+        Kernel.unquote(kind)(unquote(singular_fun)(msgctxt, msgid, bindings)) do
           unquote(env.module).handle_missing_translation(
             unquote(locale),
             unquote(domain),
@@ -341,7 +445,7 @@ defmodule Gettext.Compiler do
           )
         end
 
-        Kernel.unquote(kind)(unquote(plural_fun)(msgid, msgid_plural, n, bindings)) do
+        Kernel.unquote(kind)(unquote(plural_fun)(msgctxt, msgid, msgid_plural, n, bindings)) do
           unquote(env.module).handle_missing_plural_translation(
             unquote(locale),
             unquote(domain),
@@ -373,6 +477,7 @@ defmodule Gettext.Compiler do
        ) do
     msgid = IO.iodata_to_binary(t.msgid)
     msgstr = IO.iodata_to_binary(t.msgstr)
+    msgctxt = t.msgctxt && IO.iodata_to_binary(t.msgctxt)
 
     # Only actually generate this function clause if the msgstr is not empty. If
     # it's empty, not generating this clause (by returning `nil` from this `if`)
@@ -380,7 +485,9 @@ defmodule Gettext.Compiler do
     # msgid}` (with interpolation and so on).
     if msgstr != "" do
       quote do
-        Kernel.unquote(kind)(unquote(singular_fun)(unquote(msgid), var!(bindings))) do
+        Kernel.unquote(kind)(
+          unquote(singular_fun)(unquote(msgctxt), unquote(msgid), var!(bindings))
+        ) do
           unquote(compile_interpolation(msgstr, :translation))
         end
       end
@@ -401,6 +508,7 @@ defmodule Gettext.Compiler do
     msgid = IO.iodata_to_binary(t.msgid)
     msgid_plural = IO.iodata_to_binary(t.msgid_plural)
     msgstr = Enum.map(t.msgstr, fn {form, str} -> {form, IO.iodata_to_binary(str)} end)
+    msgctxt = t.msgctxt && IO.iodata_to_binary(t.msgctxt)
 
     # If any of the msgstrs is empty, then we skip the generation of this
     # function clause. The reason we do this is the same as for the
@@ -424,7 +532,13 @@ defmodule Gettext.Compiler do
 
       quote do
         Kernel.unquote(kind)(
-          unquote(plural_fun)(unquote(msgid), unquote(msgid_plural), n, bindings)
+          unquote(plural_fun)(
+            unquote(msgctxt),
+            unquote(msgid),
+            unquote(msgid_plural),
+            n,
+            bindings
+          )
         ) do
           plural_form = unquote(plural_mod).plural(unquote(locale), n)
           var!(bindings) = Map.put(bindings, :count, n)
diff --git a/lib/gettext/extractor.ex b/lib/gettext/extractor.ex
index 1330b52..e5b94f1 100644
--- a/lib/gettext/extractor.ex
+++ b/lib/gettext/extractor.ex
@@ -54,9 +54,11 @@ defmodule Gettext.Extractor do
 
   Note that this function doesn't perform any operation on the filesystem.
   """
-  @spec extract(Macro.Env.t(), module, binary, binary | {binary, binary}, [binary]) :: :ok
-  def extract(%Macro.Env{} = caller, backend, domain, id, extracted_comments) do
-    translation = create_translation_struct(id, caller.file, caller.line, extracted_comments)
+  @spec extract(Macro.Env.t(), module, binary, binary, binary | {binary, binary}, [binary]) :: :ok
+  def extract(%Macro.Env{} = caller, backend, domain, msgctxt, id, extracted_comments) do
+    translation =
+      create_translation_struct(id, msgctxt, caller.file, caller.line, extracted_comments)
+
     ExtractorAgent.add_translation(backend, domain, translation)
   end
 
@@ -144,9 +146,10 @@ defmodule Gettext.Extractor do
     update_in(translation.references, &Enum.sort/1)
   end
 
-  defp create_translation_struct({msgid, msgid_plural}, file, line, extracted_comments) do
+  defp create_translation_struct({msgid, msgid_plural}, msgctxt, file, line, extracted_comments) do
     %PluralTranslation{
       msgid: [msgid],
+      msgctxt: if(msgctxt != nil, do: [msgctxt], else: nil),
       msgid_plural: [msgid_plural],
       msgstr: %{0 => [""], 1 => [""]},
       flags: MapSet.new([@extracted_translations_flag]),
@@ -155,9 +158,10 @@ defmodule Gettext.Extractor do
     }
   end
 
-  defp create_translation_struct(msgid, file, line, extracted_comments) do
+  defp create_translation_struct(msgid, msgctxt, file, line, extracted_comments) do
     %Translation{
       msgid: [msgid],
+      msgctxt: if(msgctxt != nil, do: [msgctxt], else: nil),
       msgstr: [""],
       flags: MapSet.new([@extracted_translations_flag]),
       references: [{Path.relative_to_cwd(file), line}],
@@ -308,6 +312,7 @@ defmodule Gettext.Extractor do
     %Translation{
       msgid: old.msgid,
       msgstr: old.msgstr,
+      msgctxt: new.msgctxt,
       # The new in-memory translation has no new flags.
       flags: old.flags,
       # The new in-memory translation has no comments since it was extracted
@@ -327,6 +332,7 @@ defmodule Gettext.Extractor do
     # The logic here is the same as for %Translation{}s.
     %PluralTranslation{
       msgid: old.msgid,
+      msgctxt: new.msgctxt,
       msgid_plural: old.msgid_plural,
       msgstr: old.msgstr,
       flags: old.flags,


Test patch:
diff --git a/test/fixtures/test_application/priv/gettext/it/LC_MESSAGES/default.po b/test/fixtures/test_application/priv/gettext/it/LC_MESSAGES/default.po
index 123e459..8400c60 100644
--- a/test/fixtures/test_application/priv/gettext/it/LC_MESSAGES/default.po
+++ b/test/fixtures/test_application/priv/gettext/it/LC_MESSAGES/default.po
@@ -1,11 +1,25 @@
 msgid "Hello world"
 msgstr "Ciao mondo"
 
+msgctxt "test"
+msgid "Hello world"
+msgstr "Ciao mondo"
+
+msgctxt "test"
+msgid "Hello %{name}"
+msgstr "Ciao %{name}"
+
 msgid "One new email"
 msgid_plural "%{count} new emails"
 msgstr[0] "Una nuova email"
 msgstr[1] "%{count} nuove email"
 
+msgctxt "test"
+msgid "One new email"
+msgid_plural "%{count} new emails"
+msgstr[0] "Una nuova test email"
+msgstr[1] "%{count} nuove test email"
+
 msgid "Concatenated" " and long "
   "string"
 msgstr "Stringa" " lunga e "
diff --git a/test/fixtures/test_application/priv/gettext/it/LC_MESSAGES/interpolations.po b/test/fixtures/test_application/priv/gettext/it/LC_MESSAGES/interpolations.po
index 0c4589e..d16ca98 100644
--- a/test/fixtures/test_application/priv/gettext/it/LC_MESSAGES/interpolations.po
+++ b/test/fixtures/test_application/priv/gettext/it/LC_MESSAGES/interpolations.po
@@ -8,3 +8,9 @@ msgid "You have one message, %{name}"
 msgid_plural "You have %{count} messages, %{name}"
 msgstr[0] "Hai un messaggio, %{name}"
 msgstr[1] "Hai %{count} messaggi, %{name}"
+
+msgctxt "test"
+msgid "You have one message, %{name}"
+msgid_plural "You have %{count} messages, %{name}"
+msgstr[0] "Hai un messaggio, %{name}"
+msgstr[1] "Hai %{count} messaggi, %{name}"
diff --git a/test/fixtures/test_application/translations/it/LC_MESSAGES/default.po b/test/fixtures/test_application/translations/it/LC_MESSAGES/default.po
index 329eff7..2d8252f 100644
--- a/test/fixtures/test_application/translations/it/LC_MESSAGES/default.po
+++ b/test/fixtures/test_application/translations/it/LC_MESSAGES/default.po
@@ -1,2 +1,6 @@
 msgid "Hello world"
 msgstr "Ciao mondo"
+
+msgctxt "test"
+msgid "Hello world"
+msgstr "Ciao mondo"
diff --git a/test/gettext/extractor_test.exs b/test/gettext/extractor_test.exs
index bf39616..ccae1f7 100644
--- a/test/gettext/extractor_test.exs
+++ b/test/gettext/extractor_test.exs
@@ -232,6 +232,7 @@ defmodule Gettext.ExtractorTest do
         gettext_comment "one more comment"
         gettext "foo"
         Gettext.ExtractorTest.MyOtherGettext.dgettext "greetings", "hi"
+        pgettext "test", "context based translation"
       end
     end
     """
@@ -251,6 +252,12 @@ defmodule Gettext.ExtractorTest do
        #: foo.ex:16 foo.ex:19
        msgid "foo"
        msgstr ""
+
+       #, elixir-format
+       #: foo.ex:21
+       msgctxt "test"
+       msgid "context based translation"
+       msgstr ""
        """},
       {"priv/gettext/errors.pot",
        ~S"""
diff --git a/test/gettext_test.exs b/test/gettext_test.exs
index 5549a67..55677eb 100644
--- a/test/gettext_test.exs
+++ b/test/gettext_test.exs
@@ -102,61 +102,64 @@ defmodule GettextTest do
   end
 
   test "found translations return {:ok, translation}" do
-    assert Translator.lgettext("it", "default", "Hello world", %{}) == {:ok, "Ciao mondo"}
+    assert Translator.lgettext("it", "default", nil, "Hello world", %{}) == {:ok, "Ciao mondo"}
 
-    assert Translator.lgettext("it", "errors", "Invalid email address", %{}) ==
+    assert Translator.lgettext("it", "errors", nil, "Invalid email address", %{}) ==
              {:ok, "Indirizzo email non valido"}
   end
 
   test "non-found translations return the argument message" do
     # Unknown msgid.
-    assert Translator.lgettext("it", "default", "nonexistent", %{}) == {:default, "nonexistent"}
+    assert Translator.lgettext("it", "default", nil, "nonexistent", %{}) ==
+             {:default, "nonexistent"}
 
     # Unknown domain.
-    assert Translator.lgettext("it", "unknown", "Hello world", %{}) == {:default, "Hello world"}
+    assert Translator.lgettext("it", "unknown", nil, "Hello world", %{}) ==
+             {:default, "Hello world"}
 
     # Unknown locale.
-    assert Translator.lgettext("pt_BR", "nonexistent", "Hello world", %{}) ==
+    assert Translator.lgettext("pt_BR", "nonexistent", nil, "Hello world", %{}) ==
              {:default, "Hello world"}
   end
 
   test "translations with empty msgstrs fallback to {:default, _}" do
-    assert Translator.lgettext("it", "default", "Empty msgstr!", %{}) ==
+    assert Translator.lgettext("it", "default", nil, "Empty msgstr!", %{}) ==
              {:default, "Empty msgstr!"}
   end
 
   test "a custom 'priv' directory can be used to store translations" do
-    assert TranslatorWithCustomPriv.lgettext("it", "default", "Hello world", %{}) ==
+    assert TranslatorWithCustomPriv.lgettext("it", "default", nil, "Hello world", %{}) ==
              {:ok, "Ciao mondo"}
 
-    assert TranslatorWithCustomPriv.lgettext("it", "errors", "Invalid email address", %{}) ==
+    assert TranslatorWithCustomPriv.lgettext("it", "errors", nil, "Invalid email address", %{}) ==
              {:ok, "Indirizzo email non valido"}
   end
 
   test "using a custom Gettext.Plural module" do
     alias TranslatorWithCustomPluralForms, as: T
 
-    assert T.lngettext("it", "default", "One new email", "%{count} new emails", 1, %{}) ==
+    assert T.lngettext("it", "default", nil, "One new email", "%{count} new emails", 1, %{}) ==
              {:ok, "1 nuove email"}
 
-    assert T.lngettext("it", "default", "One new email", "%{count} new emails", 2, %{}) ==
+    assert T.lngettext("it", "default", nil, "One new email", "%{count} new emails", 2, %{}) ==
              {:ok, "Una nuova email"}
   end
 
   test "translations can be pluralized" do
-    import Translator, only: [lngettext: 6]
+    import Translator, only: [lngettext: 7]
 
-    t = lngettext("it", "errors", "There was an error", "There were %{count} errors", 1, %{})
+    t = lngettext("it", "errors", nil, "There was an error", "There were %{count} errors", 1, %{})
     assert t == {:ok, "C'è stato un errore"}
 
-    t = lngettext("it", "errors", "There was an error", "There were %{count} errors", 3, %{})
+    t = lngettext("it", "errors", nil, "There was an error", "There were %{count} errors", 3, %{})
     assert t == {:ok, "Ci sono stati 3 errori"}
   end
 
   test "by default, non-found pluralized translation behave like regular translation" do
-    assert Translator.lngettext("it", "not a domain", "foo", "foos", 1, %{}) == {:default, "foo"}
+    assert Translator.lngettext("it", "not a domain", nil, "foo", "foos", 1, %{}) ==
+             {:default, "foo"}
 
-    assert Translator.lngettext("it", "not a domain", "foo", "foos", 10, %{}) ==
+    assert Translator.lngettext("it", "not a domain", nil, "foo", "foos", 10, %{}) ==
              {:default, "foos"}
   end
 
@@ -164,10 +167,10 @@ defmodule GettextTest do
     msgid = "Not even one msgstr"
     msgid_plural = "Not even %{count} msgstrs"
 
-    assert Translator.lngettext("it", "default", msgid, msgid_plural, 1, %{}) ==
+    assert Translator.lngettext("it", "default", nil, msgid, msgid_plural, 1, %{}) ==
              {:default, "Not even one msgstr"}
 
-    assert Translator.lngettext("it", "default", msgid, msgid_plural, 2, %{}) ==
+    assert Translator.lngettext("it", "default", nil, msgid, msgid_plural, 2, %{}) ==
              {:default, "Not even 2 msgstrs"}
   end
 
@@ -191,22 +194,21 @@ defmodule GettextTest do
     assert_raise Gettext.Error,
                  ~r/plural form 2 is required for locale \"ru\" but is missing/,
                  fn ->
-                   BadTranslations.lngettext("ru", "errors", msgid, msgid_plural, 8, %{})
-                   |> IO.inspect()
+                   BadTranslations.lngettext("ru", "errors", nil, msgid, msgid_plural, 8, %{})
                  end
   end
 
   test "interpolation is supported by lgettext" do
-    assert Translator.lgettext("it", "interpolations", "Hello %{name}", %{name: "Jane"}) ==
+    assert Translator.lgettext("it", "interpolations", nil, "Hello %{name}", %{name: "Jane"}) ==
              {:ok, "Ciao Jane"}
 
     msgid = "My name is %{name} and I'm %{age}"
 
-    assert Translator.lgettext("it", "interpolations", msgid, %{name: "Meg", age: 33}) ==
+    assert Translator.lgettext("it", "interpolations", nil, msgid, %{name: "Meg", age: 33}) ==
              {:ok, "Mi chiamo Meg e ho 33 anni"}
 
     # A map of bindings is supported as well.
-    assert Translator.lgettext("it", "interpolations", "Hello %{name}", %{name: "Jane"}) ==
+    assert Translator.lgettext("it", "interpolations", nil, "Hello %{name}", %{name: "Jane"}) ==
              {:ok, "Ciao Jane"}
   end
 
@@ -214,52 +216,70 @@ defmodule GettextTest do
     msgid = "There was an error"
     msgid_plural = "There were %{count} errors"
 
-    assert Translator.lngettext("it", "errors", msgid, msgid_plural, 1, %{}) ==
+    assert Translator.lngettext("it", "errors", nil, msgid, msgid_plural, 1, %{}) ==
              {:ok, "C'è stato un errore"}
 
-    assert Translator.lngettext("it", "errors", msgid, msgid_plural, 4, %{}) ==
+    assert Translator.lngettext("it", "errors", nil, msgid, msgid_plural, 4, %{}) ==
              {:ok, "Ci sono stati 4 errori"}
 
     msgid = "You have one message, %{name}"
     msgid_plural = "You have %{count} messages, %{name}"
 
-    assert Translator.lngettext("it", "interpolations", msgid, msgid_plural, 1, %{name: "Jane"}) ==
+    assert Translator.lngettext("it", "interpolations", nil, msgid, msgid_plural, 1, %{
+             name: "Jane"
+           }) ==
              {:ok, "Hai un messaggio, Jane"}
 
-    assert Translator.lngettext("it", "interpolations", msgid, msgid_plural, 0, %{name: "Jane"}) ==
+    assert Translator.lngettext("it", "interpolations", nil, msgid, msgid_plural, 0, %{
+             name: "Jane"
+           }) ==
              {:ok, "Hai 0 messaggi, Jane"}
   end
 
   test "strings are concatenated before generating function clauses" do
     msgid = "Concatenated and long string"
 
-    assert Translator.lgettext("it", "default", msgid, %{}) ==
+    assert Translator.lgettext("it", "default", nil, msgid, %{}) ==
              {:ok, "Stringa lunga e concatenata"}
 
+    # assert Translator.lgettext("it", "default", msgid, %{}) ==
+    #         {:ok, "Stringa lunga e concatenata"}
+
     msgid = "A friend"
     msgid_plural = "%{count} friends"
-    assert Translator.lngettext("it", "default", msgid, msgid_plural, 1, %{}) == {:ok, "Un amico"}
+
+    assert Translator.lngettext("it", "default", nil, msgid, msgid_plural, 1, %{}) ==
+             {:ok, "Un amico"}
   end
 
   test "lgettext/4: default handle_missing_binding preserves key" do
     msgid = "My name is %{name} and I'm %{age}"
 
-    assert Translator.lgettext("it", "interpolations", msgid, %{name: "José"}) ==
+    assert Translator.lgettext("it", "interpolations", nil, msgid, %{name: "José"}) ==
              {:missing_bindings, "Mi chiamo José e ho %{age} anni", [:age]}
   end
 
+  test "MissingBindingsError log messages" do
+    assert capture_log(fn ->
+             Translator.pgettext("test", "Hello %{name}, missing translation!", %{})
+           end) =~
+             "missing Gettext bindings: [:name] (backend GettextTest.Translator," <>
+               " locale \"en\", domain \"default\", msgctxt \"test\", msgid \"Hello " <>
+               "%{name}, missing translation!\")"
+  end
+
   test "lgettext/4: interpolation works when a translation is missing" do
     msgid = "Hello %{name}, missing translation!"
 
-    assert Translator.lgettext("pl", "foo", msgid, %{name: "Samantha"}) ==
+    assert Translator.lgettext("pl", "foo", nil, msgid, %{name: "Samantha"}) ==
              {:default, "Hello Samantha, missing translation!"}
 
     msgid = "Hello world!"
-    assert Translator.lgettext("pl", "foo", msgid, %{}) == {:default, "Hello world!"}
+    assert Translator.lgettext("pl", "foo", nil, msgid, %{}) == {:default, "Hello world!"}
 
     msgid = "Hello %{name}"
 
-    assert Translator.lgettext("pl", "foo", msgid, %{}) ==
+    assert Translator.lgettext("pl", "foo", nil, msgid, %{}) ==
              {:missing_bindings, "Hello %{name}", [:name]}
   end
 
@@ -267,7 +287,7 @@ defmodule GettextTest do
     msgid = "Hello %{name}"
     bindings = %{name: "Jane"}
 
-    assert HandleMissingTranslation.lgettext("pl", "foo", msgid, bindings) ==
+    assert HandleMissingTranslation.lgettext("pl", "foo", nil, msgid, bindings) ==
              {:default, "Hello Jane"}
 
     assert_receive {"pl", "foo", ^msgid, ^bindings}
@@ -277,10 +297,10 @@ defmodule GettextTest do
     msgid = "You have one message, %{name}"
     msgid_plural = "You have %{count} messages, %{name}"
 
-    assert Translator.lngettext("it", "interpolations", msgid, msgid_plural, 1, %{}) ==
+    assert Translator.lngettext("it", "interpolations", nil, msgid, msgid_plural, 1, %{}) ==
              {:missing_bindings, "Hai un messaggio, %{name}", [:name]}
 
-    assert Translator.lngettext("it", "interpolations", msgid, msgid_plural, 6, %{}) ==
+    assert Translator.lngettext("it", "interpolations", nil, msgid, msgid_plural, 6, %{}) ==
              {:missing_bindings, "Hai 6 messaggi, %{name}", [:name]}
   end
 
@@ -288,10 +308,10 @@ defmodule GettextTest do
     msgid = "One error"
     msgid_plural = "%{count} errors"
 
-    assert Translator.lngettext("pl", "foo", msgid, msgid_plural, 1, %{}) ==
+    assert Translator.lngettext("pl", "foo", nil, msgid, msgid_plural, 1, %{}) ==
              {:default, "One error"}
 
-    assert Translator.lngettext("pl", "foo", msgid, msgid_plural, 9, %{}) ==
+    assert Translator.lngettext("pl", "foo", nil, msgid, msgid_plural, 9, %{}) ==
              {:default, "9 errors"}
   end
 
@@ -300,7 +320,7 @@ defmodule GettextTest do
     msgid_plural = "Hello %{name}"
     bindings = %{name: "Jane"}
 
-    assert HandleMissingTranslation.lngettext("pl", "foo", msgid, msgid_plural, 4, bindings) ==
+    assert HandleMissingTranslation.lngettext("pl", "foo", nil, msgid, msgid_plural, 4, bindings) ==
              {:default, "Hello Jane"}
 
     assert_receive {"pl", "foo", ^msgid, ^msgid_plural, 4, ^bindings}
@@ -332,6 +352,103 @@ defmodule GettextTest do
     assert Translator.gettext(~s(Hello world)) == "Ciao mondo"
   end
 
+  test "pgettext/3: test with context based translations" do
+    Gettext.put_locale(Translator, "it")
+    assert Translator.pgettext("test", @gettext_msgid) == "Ciao mondo"
+    assert Translator.pgettext("test", ~s(Hello world)) == "Ciao mondo"
+    assert Translator.pgettext("test", "Hello world", %{}) == "Ciao mondo"
+    assert Translator.pgettext("test", "Hello %{name}", %{name: "Marco"}) == "Ciao Marco"
+    # Missing translation
+    assert Translator.pgettext("test", "Hello missing", %{}) == "Hello missing"
+  end
+
+  test "pgettext/3, pngettext/4: dynamic context raises" do
+    code =
+      quote do
+        require Translator
+        context = "test"
+        Translator.pgettext(context, "Hello world")
+      end
+
+    error = assert_raise ArgumentError, fn -> Code.eval_quoted(code) end
+    message = ArgumentError.message(error)
+    assert message =~ "Gettext macros expect translation keys"
+    assert message =~ "{:context"
+    assert message =~ "Gettext.gettext(GettextTest.Translator, string)"
+
+    code =
+      quote do
+        require Translator
+        context = "test"
+        Translator.pngettext(context, "Hello world", "Hello world", 5)
+      end
+
+    error = assert_raise ArgumentError, fn -> Code.eval_quoted(code) end
+    message = ArgumentError.message(error)
+    assert message =~ "Gettext macros expect translation keys"
+    assert message =~ "{:context"
+    assert message =~ "Gettext.gettext(GettextTest.Translator, string)"
+  end
+
+  test "dpgettext/4, dpngettext/5: dynamic context or dynamic domain raises" do
+    code =
+      quote do
+        require Translator
+        context = "test"
+        Translator.dpgettext("default", context, "Hello world")
+      end
+
+    error = assert_raise ArgumentError, fn -> Code.eval_quoted(code) end
+    message = ArgumentError.message(error)
+    assert message =~ "Gettext macros expect translation keys"
+    assert message =~ "{:context"
+    assert message =~ "Gettext.gettext(GettextTest.Translator, string)"
+
+    code =
+      quote do
+        require Translator
+        domain = "test"
+        Translator.dpgettext(domain, "test", "Hello world")
+      end
+
+    error = assert_raise ArgumentError, fn -> Code.eval_quoted(code) end
+    message = ArgumentError.message(error)
+    assert message =~ "Gettext macros expect translation keys"
+    assert message =~ "{:domain"
+    assert message =~ "Gettext.gettext(GettextTest.Translator, string)"
+
+    code =
+      quote do
+        require Translator
+        context = "test"
+        Translator.dpngettext("default", context, "Hello world", "Hello world", n)
+      end
+
+    error = assert_raise ArgumentError, fn -> Code.eval_quoted(code) end
+    message = ArgumentError.message(error)
+    assert message =~ "Gettext macros expect translation keys"
+    assert message =~ "{:context"
+    assert message =~ "Gettext.gettext(GettextTest.Translator, string)"
+
+    code =
+      quote do
+        require Translator
+        domain = "test"
+        Translator.dpngettext(domain, "test", "Hello world", "Hello World", n)
+      end
+
+    error = assert_raise ArgumentError, fn -> Code.eval_quoted(code) end
+    message = ArgumentError.message(error)
+    assert message =~ "Gettext macros expect translation keys"
+    assert message =~ "{:domain"
+    assert message =~ "Gettext.gettext(GettextTest.Translator, string)"
+  end
+
+  test "dpgettext/4: context and domain based translations" do
+    Gettext.put_locale(Translator, "it")
+    assert Translator.dpgettext("default", "test", "Hello world", %{}) == "Ciao mondo"
+  end
+
   test "dgettext/3 and dngettext/2: non-binary things at compile-time" do
     code =
       quote do
@@ -404,22 +521,45 @@ defmodule GettextTest do
     assert Translator.ngettext(@ngettext_msgid, @ngettext_msgid_plural, 2) == "2 nuove email"
   end
 
+  test "pngettext/4" do
+    Gettext.put_locale(Translator, "it")
+
+    assert Translator.pngettext("test", "One new email", "%{count} new emails", 1) ==
+             "Una nuova test email"
+
+    assert Translator.pngettext("test", "One new email", "%{count} new emails", 2) ==
+             "2 nuove test email"
+
+    assert Translator.pngettext("test", @ngettext_msgid, @ngettext_msgid_plural, 1) ==
+             "Una nuova test email"
+
+    assert Translator.pngettext("test", @ngettext_msgid, @ngettext_msgid_plural, 2) ==
+             "2 nuove test email"
+  end
+
   test "the d?n?gettext macros support a kw list for interpolation" do
     Gettext.put_locale(Translator, "it")
     assert Translator.gettext("%{msg}", msg: "foo") == "foo"
   end
 
-  test "(d)gettext_noop" do
-    assert Translator.dgettext_noop("errors", "Oops") == "Oops"
-    assert Translator.gettext_noop("Hello %{name}!") == "Hello %{name}!"
+  test "(d)(p)gettext_noop" do
+    assert Translator.dpgettext_noop("errors", "test", "Oops") == {"Oops", "test"}
+    assert Translator.dgettext_noop("errors", "Oops") == {"Oops", nil}
+    assert Translator.gettext_noop("Hello %{name}!") == {"Hello %{name}!", nil}
   end
 
-  test "n(d)gettext_noop" do
+  test "(d)(p)ngettext_noop" do
+    assert Translator.dpngettext_noop("errors", "test", "One error", "%{count} errors") ==
+             {"One error", "%{count} errors", "test"}
+
     assert Translator.dngettext_noop("errors", "One error", "%{count} errors") ==
-             {"One error", "%{count} errors"}
+             {"One error", "%{count} errors", nil}
 
     assert Translator.ngettext_noop("One message", "%{count} messages") ==
-             {"One message", "%{count} messages"}
+             {"One message", "%{count} messages", nil}
+
+    assert Translator.pngettext_noop("test", "One message", "%{count} messages") ==
+             {"One message", "%{count} messages", "test"}
   end
 
   # Actual Gettext functions (not the ones generated in the modules that `use
@@ -448,6 +588,12 @@ defmodule GettextTest do
     assert Gettext.gettext(Translator, "Nonexistent") == "Nonexistent"
   end
 
+  test "pgettext/3" do
+    Gettext.put_locale(Translator, "it")
+    assert Gettext.pgettext(Translator, "test", "Hello world") == "Ciao mondo"
+    assert Gettext.pgettext(Translator, "test", "Nonexistent") == "Nonexistent"
+  end
+
   test "dngettext/6" do
     Gettext.put_locale(Translator, "it")
     msgid = "You have one message, %{name}"
@@ -460,6 +606,45 @@ defmodule GettextTest do
              "Hai 5 messaggi, Meg"
   end
 
+  test "dpngettext/6" do
+    Gettext.put_locale(Translator, "it")
+    msgid = "You have one message, %{name}"
+    msgid_plural = "You have %{count} messages, %{name}"
+
+    assert Gettext.dpngettext(Translator, "interpolations", "test", msgid, msgid_plural, 1, %{
+             name: "Meg"
+           }) ==
+             "Hai un messaggio, Meg"
+
+    assert Gettext.dpngettext(Translator, "interpolations", "test", msgid, msgid_plural, 5, %{
+             name: "Meg"
+           }) ==
+             "Hai 5 messaggi, Meg"
+
+    assert Gettext.dpngettext(
+             Translator,
+             "default",
+             "test",
+             "One new email",
+             "%{count} new emails",
+             5,
+             %{name: "Meg"}
+           ) == "5 nuove test email"
+  end
+
+  test "pngettext/6" do
+    Gettext.put_locale(Translator, "it")
+    msgctxt = "test"
+    msgid = "One cake, %{name}"
+    msgid_plural = "%{count} cakes, %{name}"
+
+    assert Gettext.pngettext(Translator, msgctxt, msgid, msgid_plural, 1, %{name: "Meg"}) ==
+             "One cake, Meg"
+
+    assert Gettext.pngettext(Translator, msgctxt, msgid, msgid_plural, 5, %{name: "Meg"}) ==
+             "5 cakes, Meg"
+  end
+
   test "ngettext/5" do
     Gettext.put_locale(Translator, "it")
     msgid = "One cake, %{name}"
@@ -531,28 +716,37 @@ defmodule GettextTest do
     end
 
     test "may define one module per locale" do
-      import TranslatorWithOneModulePerLocale, only: [lgettext: 4, lngettext: 6]
+      import TranslatorWithOneModulePerLocale, only: [lgettext: 5, lngettext: 7]
       assert Code.ensure_loaded?(TranslatorWithOneModulePerLocale.T_it)
 
       # Found on default domain.
-      assert lgettext("it", "default", "Hello world", %{}) == {:ok, "Ciao mondo"}
+      assert lgettext("it", "default", nil, "Hello world", %{}) == {:ok, "Ciao mondo"}
 
       # Found on errors domain.
-      assert lgettext("it", "errors", "Invalid email address", %{}) ==
+      assert lgettext("it", "errors", nil, "Invalid email address", %{}) ==
                {:ok, "Indirizzo email non valido"}
 
       # Found with plural form.
-      assert lngettext("it", "errors", "There was an error", "There were %{count} errors", 1, %{}) ==
+      assert lngettext(
+               "it",
+               "errors",
+               nil,
+               "There was an error",
+               "There were %{count} errors",
+               1,
+               %{}
+             ) ==
                {:ok, "C'è stato un errore"}
 
       # Unknown msgid.
-      assert lgettext("it", "default", "nonexistent", %{}) == {:default, "nonexistent"}
+      assert lgettext("it", "default", nil, "nonexistent", %{}) == {:default, "nonexistent"}
 
       # Unknown domain.
-      assert lgettext("it", "unknown", "Hello world", %{}) == {:default, "Hello world"}
+      assert lgettext("it", "unknown", nil, "Hello world", %{}) == {:default, "Hello world"}
 
       # Unknown locale.
-      assert lgettext("pt_BR", "nonexistent", "Hello world", %{}) == {:default, "Hello world"}
+      assert lgettext("pt_BR", "nonexistent", nil, "Hello world", %{}) ==
+               {:default, "Hello world"}
     end
   end
 end