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 2bc9343aa3..b662e9392d 100644 Binary files a/tests/regressiontests/i18n/other/locale/de/LC_MESSAGES/django.mo and b/tests/regressiontests/i18n/other/locale/de/LC_MESSAGES/django.mo differ 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 356147cf11..537135ec95 100644 Binary files a/tests/regressiontests/views/locale/fr/LC_MESSAGES/djangojs.mo and b/tests/regressiontests/views/locale/fr/LC_MESSAGES/djangojs.mo differ 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):