From 83aeb3c768173f48dc295c3194fecd705d1c05ac Mon Sep 17 00:00:00 2001
From: Jannis Leidel <jannis@leidel.info>
Date: Thu, 4 Nov 2010 10:48:27 +0000
Subject: [PATCH] Fixed #9988 -- Added support for translation contexts.
 Thanks, Claude Paroz.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@14450 bcc190cf-cafb-0310-a4f2-bffc1f526a37
---
 .../core/management/commands/makemessages.py  |   4 +--
 django/utils/translation/__init__.py          |  11 +++++-
 django/utils/translation/trans_null.py        |   6 ++++
 django/utils/translation/trans_real.py        |  20 +++++++++++
 django/views/i18n.py                          |  17 +++++++++
 docs/releases/1.3.txt                         |   8 +++++
 docs/topics/i18n/internationalization.txt     |  33 ++++++++++++++++++
 .../other/locale/de/LC_MESSAGES/django.mo     | Bin 469 -> 611 bytes
 .../other/locale/de/LC_MESSAGES/django.po     |  17 +++++++++
 tests/regressiontests/i18n/tests.py           |  18 +++++++++-
 .../views/locale/fr/LC_MESSAGES/djangojs.mo   | Bin 484 -> 519 bytes
 .../views/locale/fr/LC_MESSAGES/djangojs.po   |   4 +++
 tests/regressiontests/views/tests/i18n.py     |   3 ++
 13 files changed, 137 insertions(+), 4 deletions(-)

diff --git a/django/core/management/commands/makemessages.py b/django/core/management/commands/makemessages.py
index 955a822e0c..34c9b8ce91 100644
--- a/django/core/management/commands/makemessages.py
+++ b/django/core/management/commands/makemessages.py
@@ -190,7 +190,7 @@ def make_messages(locale=None, domain='django', verbosity='1', all=False,
                     f.write(src)
                 finally:
                     f.close()
-                cmd = 'xgettext -d %s -L Perl --keyword=gettext_noop --keyword=gettext_lazy --keyword=ngettext_lazy:1,2 --from-code UTF-8 -o - "%s"' % (domain, os.path.join(dirpath, thefile))
+                cmd = 'xgettext -d %s -L Perl --keyword=gettext_noop --keyword=gettext_lazy --keyword=ngettext_lazy:1,2 --keyword=pgettext:1c,2 --keyword=npgettext:1c,2,3 --from-code UTF-8 -o - "%s"' % (domain, os.path.join(dirpath, thefile))
                 msgs, errors = _popen(cmd)
                 if errors:
                     raise CommandError("errors happened while running xgettext on %s\n%s" % (file, errors))
@@ -225,7 +225,7 @@ def make_messages(locale=None, domain='django', verbosity='1', all=False,
                         raise SyntaxError(msg)
                 if verbosity > 1:
                     sys.stdout.write('processing file %s in %s\n' % (file, dirpath))
-                cmd = 'xgettext -d %s -L Python --keyword=gettext_noop --keyword=gettext_lazy --keyword=ngettext_lazy:1,2 --keyword=ugettext_noop --keyword=ugettext_lazy --keyword=ungettext_lazy:1,2 --from-code UTF-8 -o - "%s"' % (
+                cmd = 'xgettext -d %s -L Python --keyword=gettext_noop --keyword=gettext_lazy --keyword=ngettext_lazy:1,2 --keyword=ugettext_noop --keyword=ugettext_lazy --keyword=ungettext_lazy:1,2 --keyword=pgettext:1c,2 --keyword=npgettext:1c,2,3 --keyword=pgettext_lazy:1c,2 --keyword=npgettext_lazy:1c,2,3 --from-code UTF-8 -o - "%s"' % (
                     domain, os.path.join(dirpath, thefile))
                 msgs, errors = _popen(cmd)
                 if errors:
diff --git a/django/utils/translation/__init__.py b/django/utils/translation/__init__.py
index 4477c291f4..0e1b4f8d67 100644
--- a/django/utils/translation/__init__.py
+++ b/django/utils/translation/__init__.py
@@ -10,7 +10,8 @@ __all__ = ['gettext', 'gettext_noop', 'gettext_lazy', 'ngettext',
         'get_language', 'get_language_bidi', 'get_date_formats',
         'get_partial_date_formats', 'check_for_language', 'to_locale',
         'get_language_from_request', 'templatize', 'ugettext', 'ugettext_lazy',
-        'ungettext', 'deactivate_all']
+        'ungettext', 'ungettext_lazy', 'pgettext', 'pgettext_lazy',
+        'npgettext', 'npgettext_lazy', 'deactivate_all']
 
 # Here be dragons, so a short explanation of the logic won't hurt:
 # We are trying to solve two problems: (1) access settings, in particular
@@ -63,10 +64,18 @@ def ugettext(message):
 def ungettext(singular, plural, number):
     return _trans.ungettext(singular, plural, number)
 
+def pgettext(context, message):
+    return _trans.pgettext(context, message)
+
+def npgettext(context, singular, plural, number):
+    return _trans.npgettext(context, singular, plural, number)
+
 ngettext_lazy = lazy(ngettext, str)
 gettext_lazy = lazy(gettext, str)
 ungettext_lazy = lazy(ungettext, unicode)
 ugettext_lazy = lazy(ugettext, unicode)
+pgettext_lazy = lazy(pgettext, unicode)
+npgettext_lazy = lazy(npgettext, unicode)
 
 def activate(language):
     return _trans.activate(language)
diff --git a/django/utils/translation/trans_null.py b/django/utils/translation/trans_null.py
index 8a075806ec..a8bebad942 100644
--- a/django/utils/translation/trans_null.py
+++ b/django/utils/translation/trans_null.py
@@ -15,6 +15,12 @@ ngettext_lazy = ngettext
 def ungettext(singular, plural, number):
     return force_unicode(ngettext(singular, plural, number))
 
+def pgettext(context, message):
+    return ugettext(message)
+
+def npgettext(context, singular, plural, number):
+    return ungettext(singular, plural, number)
+
 activate = lambda x: None
 deactivate = deactivate_all = lambda: None
 get_language = lambda: settings.LANGUAGE_CODE
diff --git a/django/utils/translation/trans_real.py b/django/utils/translation/trans_real.py
index fe34b6ab0f..8486fdf8f4 100644
--- a/django/utils/translation/trans_real.py
+++ b/django/utils/translation/trans_real.py
@@ -24,6 +24,9 @@ _default = None
 # file lookups when checking the same locale on repeated requests.
 _accepted = {}
 
+# magic gettext number to separate context from message
+CONTEXT_SEPARATOR = u"\x04"
+
 # Format of Accept-Language header values. From RFC 2616, section 14.4 and 3.9.
 accept_language_re = re.compile(r'''
         ([A-Za-z]{1,8}(?:-[A-Za-z]{1,8})*|\*)   # "en", "en-au", "x-y-z", "*"
@@ -279,6 +282,14 @@ def gettext(message):
 def ugettext(message):
     return do_translate(message, 'ugettext')
 
+def pgettext(context, message):
+    result = do_translate(
+        u"%s%s%s" % (context, CONTEXT_SEPARATOR, message), 'ugettext')
+    if CONTEXT_SEPARATOR in result:
+        # Translation not found
+        result = message
+    return result
+
 def gettext_noop(message):
     """
     Marks strings for translation but doesn't translate them now. This can be
@@ -313,6 +324,15 @@ def ungettext(singular, plural, number):
     """
     return do_ntranslate(singular, plural, number, 'ungettext')
 
+def npgettext(context, singular, plural, number):
+    result = do_ntranslate(u"%s%s%s" % (context, CONTEXT_SEPARATOR, singular),
+                           u"%s%s%s" % (context, CONTEXT_SEPARATOR, plural),
+                           number, 'ungettext')
+    if CONTEXT_SEPARATOR in result:
+        # Translation not found
+        result = do_ntranslate(singular, plural, number, 'ungettext')
+    return result
+
 def check_for_language(lang_code):
     """
     Checks whether there is a global language file for the given language
diff --git a/django/views/i18n.py b/django/views/i18n.py
index 2078649e3d..133c42f05b 100644
--- a/django/views/i18n.py
+++ b/django/views/i18n.py
@@ -68,6 +68,8 @@ NullSource = """
 function gettext(msgid) { return msgid; }
 function ngettext(singular, plural, count) { return (count == 1) ? singular : plural; }
 function gettext_noop(msgid) { return msgid; }
+function pgettext(context, msgid) { return msgid; }
+function npgettext(context, singular, plural, count) { return (count == 1) ? singular : plural; }
 """
 
 LibHead = """
@@ -98,6 +100,21 @@ function ngettext(singular, plural, count) {
 
 function gettext_noop(msgid) { return msgid; }
 
+function pgettext(context, msgid) {
+  var value = gettext(context + '\x04' + msgid);
+  if (value.indexOf('\x04') != -1) {
+    value = msgid;
+  }
+  return value;
+}
+
+function npgettext(context, singular, plural, count) {
+  var value = ngettext(context + '\x04' + singular, context + '\x04' + plural, count);
+  if (value.indexOf('\x04') != -1) {
+    value = ngettext(singular, plural, count);
+  }
+  return value;
+}
 """
 
 LibFormatHead = """
diff --git a/docs/releases/1.3.txt b/docs/releases/1.3.txt
index b371f6dc64..7e843d90fd 100644
--- a/docs/releases/1.3.txt
+++ b/docs/releases/1.3.txt
@@ -86,6 +86,14 @@ Users of Python 2.5 and above may now use :ref:`transaction management functions
 
 For more information, see :ref:`transaction-management-functions`.
 
+Contextual markers in translatable strings
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+For translation strings with ambiguous meaning, you can now
+use the ``pgettext`` function to specify the context of the string.
+
+For more information, see :ref:`contextual-markers`
+
 Everything else
 ~~~~~~~~~~~~~~~
 
diff --git a/docs/topics/i18n/internationalization.txt b/docs/topics/i18n/internationalization.txt
index 5fc347c89d..e4ed85ce3f 100644
--- a/docs/topics/i18n/internationalization.txt
+++ b/docs/topics/i18n/internationalization.txt
@@ -193,6 +193,39 @@ cardinality of the elements at play.
     ``django-admin.py compilemessages`` or a ``KeyError`` Python exception at
     runtime.
 
+.. _contextual-markers:
+
+Contextual markers
+------------------
+
+.. versionadded:: 1.3
+
+Sometimes words have several meanings, such as ``"May"`` in English, which
+refers to a month name and to a verb. To enable translators to translate
+these words correctly in different contexts, you can use the
+``django.utils.translation.pgettext()`` function, or the
+``django.utils.translation.npgettext()`` function if the string needs
+pluralization. Both take a context string as the first variable.
+
+In the resulting .po file, the string will then appear as often as there are
+different contextual markers for the same string (the context will appear on
+the ``msgctxt`` line), allowing the translator to give a different translation
+for each of them.
+
+For example::
+
+    from django.utils.translation import pgettext
+
+    month = pgettext("month name", "May")
+
+will appear in the .po file as:
+
+.. code-block:: po
+
+    msgctxt "month name"
+    msgid "May"
+    msgstr ""
+
 .. _lazy-translations:
 
 Lazy translation
diff --git a/tests/regressiontests/i18n/other/locale/de/LC_MESSAGES/django.mo b/tests/regressiontests/i18n/other/locale/de/LC_MESSAGES/django.mo
index 2bc9343aa31850042669f03865cb890cc73e35d3..b662e9392da234d2b1a618dcc20ea5d84b0da19c 100644
GIT binary patch
delta 243
zcmcc0{FtTwo)F7a1|VPsVi_QI0b+I_&H-W&=m26)AnpWWJ|Lb9#L_^#2#7g=cpnhQ
zGcqt72hx&2tj@&1zzn1vfwTmWb_LQbKspXc1C=oVDKG$u0oA#KIUvg!ToOxC^-D5y
zQyFse^GY%l@)C1XS$q>K8H!UAi;^=~R8tg+Qj1G-N*Lg*VurHRq9l-_jZxK%O1_Di
VAh{s0szk6l7(120J25Yh0RZ(vED`_!

delta 99
zcmaFNa+Nvio)F7a1|VPpVi_RT0b*7lwgF-g2moSEAPxlLct!?>Xdo>K#JhoPAOh<H
UQV8IZSdyw=l9`*j@mMt@01^lbx&QzG

diff --git a/tests/regressiontests/i18n/other/locale/de/LC_MESSAGES/django.po b/tests/regressiontests/i18n/other/locale/de/LC_MESSAGES/django.po
index 2fdcee5015..fa297e5683 100644
--- a/tests/regressiontests/i18n/other/locale/de/LC_MESSAGES/django.po
+++ b/tests/regressiontests/i18n/other/locale/de/LC_MESSAGES/django.po
@@ -20,3 +20,20 @@ msgstr ""
 #: models.py:3
 msgid "Date/time"
 msgstr "Datum/Zeit (LOCALE_PATHS)"
+
+#: models.py:5
+msgctxt "month name"
+msgid "May"
+msgstr "Mai"
+
+#: models.py:7
+msgctxt "verb"
+msgid "May"
+msgstr "Kann"
+
+#: models.py:9
+msgctxt "search"
+msgid "%d result"
+msgid_plural "%d results"
+msgstr[0] "%d Resultat"
+msgstr[1] "%d Resultate"
diff --git a/tests/regressiontests/i18n/tests.py b/tests/regressiontests/i18n/tests.py
index bac77b0dbc..615ffe822c 100644
--- a/tests/regressiontests/i18n/tests.py
+++ b/tests/regressiontests/i18n/tests.py
@@ -11,7 +11,7 @@ from django.test import TestCase
 from django.utils.formats import get_format, date_format, time_format, localize, localize_input, iter_format_modules
 from django.utils.numberformat import format as nformat
 from django.utils.safestring import mark_safe, SafeString, SafeUnicode
-from django.utils.translation import ugettext, ugettext_lazy, activate, deactivate, gettext_lazy, to_locale
+from django.utils.translation import ugettext, ugettext_lazy, activate, deactivate, gettext_lazy, pgettext, npgettext, to_locale
 from django.utils.importlib import import_module
 
 
@@ -54,6 +54,22 @@ class TranslationTests(TestCase):
         s2 = pickle.loads(pickle.dumps(s1))
         self.assertEqual(unicode(s2), "test")
 
+    def test_pgettext(self):
+        # Reset translation catalog to include other/locale/de
+        self.old_locale_paths = settings.LOCALE_PATHS
+        settings.LOCALE_PATHS += (os.path.join(os.path.dirname(os.path.abspath(__file__)), 'other', 'locale'),)
+        from django.utils.translation import trans_real
+        trans_real._active = {}
+        trans_real._translations = {}
+        activate('de')
+
+        self.assertEqual(pgettext("unexisting", "May"), u"May")
+        self.assertEqual(pgettext("month name", "May"), u"Mai")
+        self.assertEqual(pgettext("verb", "May"), u"Kann")
+        self.assertEqual(npgettext("search", "%d result", "%d results", 4) % 4, u"4 Resultate")
+
+        settings.LOCALE_PATHS = self.old_locale_paths
+
     def test_string_concat(self):
         """
         unicode(string_concat(...)) should not raise a TypeError - #4796
diff --git a/tests/regressiontests/views/locale/fr/LC_MESSAGES/djangojs.mo b/tests/regressiontests/views/locale/fr/LC_MESSAGES/djangojs.mo
index 356147cf11c3f3a9765a75abba326f449f4c5bad..537135ec95ab782afb80ce0be85ec6f357834718 100644
GIT binary patch
delta 132
zcmaFD+|H7CPl#nI0}!wPu?!H~05K~N#{e-16aX<V5ElY59}w3Au>=tJ0kJ6~1H*D4
zEeOPSfox_V{T@gQ1L@B|8i*KJCI-s!<>u#=WGLh%=BBdvCRT3DOJZcsP0XBpo>3G4
D4`&ow

delta 114
zcmZo?dBPlcPl#nI0}wC+u?!HK05K~N`v5TrBmgll5GMk$1Q6!~u_+@1LkEx+1mb-_
qwlI*s0HlG4fnj2z+{V5{Mv=@Mg|x)d5`~;pg_5Ggl+w(iR0aS5$`O<R

diff --git a/tests/regressiontests/views/locale/fr/LC_MESSAGES/djangojs.po b/tests/regressiontests/views/locale/fr/LC_MESSAGES/djangojs.po
index 0d03f95845..9259aab91b 100644
--- a/tests/regressiontests/views/locale/fr/LC_MESSAGES/djangojs.po
+++ b/tests/regressiontests/views/locale/fr/LC_MESSAGES/djangojs.po
@@ -22,3 +22,7 @@ msgstr "il faut le traduire"
 
 msgid "Choose a time"
 msgstr "Choisir une heure"
+
+msgctxt "month name"
+msgid "May"
+msgstr "mai"
diff --git a/tests/regressiontests/views/tests/i18n.py b/tests/regressiontests/views/tests/i18n.py
index 9a34738411..de023be444 100644
--- a/tests/regressiontests/views/tests/i18n.py
+++ b/tests/regressiontests/views/tests/i18n.py
@@ -30,6 +30,9 @@ class I18NTests(TestCase):
             # catalog['this is to be translated'] = 'same_that_trans_txt'
             # javascript_quote is used to be able to check unicode strings
             self.assertContains(response, javascript_quote(trans_txt), 1)
+            if lang_code == 'fr':
+                # Message with context (msgctxt)
+                self.assertContains(response, "['month name\x04May'] = 'mai';", 1)
 
 
 class JsI18NTests(TestCase):