| 1 | diff --git a/django/template/base.py b/django/template/base.py | 
|---|
| 2 | index 8001e93..3129886 100644 | 
|---|
| 3 | --- a/django/template/base.py | 
|---|
| 4 | +++ b/django/template/base.py | 
|---|
| 5 | @@ -10,7 +10,7 @@ from django.utils.itercompat import is_iterable | 
|---|
| 6 | from django.utils.text import (smart_split, unescape_string_literal, | 
|---|
| 7 | get_text_list) | 
|---|
| 8 | from django.utils.encoding import smart_unicode, force_unicode, smart_str | 
|---|
| 9 | -from django.utils.translation import ugettext_lazy | 
|---|
| 10 | +from django.utils.translation import ugettext_lazy, pgettext_lazy | 
|---|
| 11 | from django.utils.safestring import (SafeData, EscapeData, mark_safe, | 
|---|
| 12 | mark_for_escaping) | 
|---|
| 13 | from django.utils.formats import localize | 
|---|
| 14 | @@ -673,6 +673,7 @@ class Variable(object): | 
|---|
| 15 | self.literal = None | 
|---|
| 16 | self.lookups = None | 
|---|
| 17 | self.translate = False | 
|---|
| 18 | +        self.trans_context = None | 
|---|
| 19 |  | 
|---|
| 20 | try: | 
|---|
| 21 | # First try to treat this variable as a number. | 
|---|
| 22 | @@ -720,7 +721,10 @@ class Variable(object): | 
|---|
| 23 | # We're dealing with a literal, so it's already been "resolved" | 
|---|
| 24 | value = self.literal | 
|---|
| 25 | if self.translate: | 
|---|
| 26 | -            return ugettext_lazy(value) | 
|---|
| 27 | +            if self.trans_context: | 
|---|
| 28 | +                return pgettext_lazy(self.trans_context, value) | 
|---|
| 29 | +            else: | 
|---|
| 30 | +                return ugettext_lazy(value) | 
|---|
| 31 | return value | 
|---|
| 32 |  | 
|---|
| 33 | def __repr__(self): | 
|---|
| 34 | @@ -814,11 +818,10 @@ class NodeList(list): | 
|---|
| 35 | bits = [] | 
|---|
| 36 | for node in self: | 
|---|
| 37 | if isinstance(node, Node): | 
|---|
| 38 | -                bit = self.render_node(node, context) | 
|---|
| 39 | +                bits.append(self.render_node(node, context)) | 
|---|
| 40 | else: | 
|---|
| 41 | -                bit = node | 
|---|
| 42 | -            bits.append(force_unicode(bit)) | 
|---|
| 43 | -        return mark_safe(u''.join(bits)) | 
|---|
| 44 | +                bits.append(node) | 
|---|
| 45 | +        return mark_safe(''.join([force_unicode(b) for b in bits])) | 
|---|
| 46 |  | 
|---|
| 47 | def get_nodes_by_type(self, nodetype): | 
|---|
| 48 | "Return a list of all nodes of the given type" | 
|---|
| 49 | diff --git a/django/templatetags/i18n.py b/django/templatetags/i18n.py | 
|---|
| 50 | index 669cdc9..4f64316 100644 | 
|---|
| 51 | --- a/django/templatetags/i18n.py | 
|---|
| 52 | +++ b/django/templatetags/i18n.py | 
|---|
| 53 | @@ -70,15 +70,21 @@ class GetCurrentLanguageBidiNode(Node): | 
|---|
| 54 |  | 
|---|
| 55 |  | 
|---|
| 56 | class TranslateNode(Node): | 
|---|
| 57 | -    def __init__(self, filter_expression, noop, asvar=None): | 
|---|
| 58 | +    def __init__(self, filter_expression, noop, asvar=None, | 
|---|
| 59 | +                 trans_context=None): | 
|---|
| 60 | self.noop = noop | 
|---|
| 61 | self.asvar = asvar | 
|---|
| 62 | +        self.trans_context = trans_context | 
|---|
| 63 | self.filter_expression = filter_expression | 
|---|
| 64 | if isinstance(self.filter_expression.var, basestring): | 
|---|
| 65 | -            self.filter_expression.var = Variable(u"'%s'" % self.filter_expression.var) | 
|---|
| 66 | +            self.filter_expression.var = Variable(u"'%s'" % | 
|---|
| 67 | +                                                  self.filter_expression.var) | 
|---|
| 68 |  | 
|---|
| 69 | def render(self, context): | 
|---|
| 70 | self.filter_expression.var.translate = not self.noop | 
|---|
| 71 | +        if self.trans_context: | 
|---|
| 72 | +            self.filter_expression.var.trans_context = ( | 
|---|
| 73 | +                self.trans_context.resolve(context)) | 
|---|
| 74 | output = self.filter_expression.resolve(context) | 
|---|
| 75 | value = _render_value_in_context(output, context) | 
|---|
| 76 | if self.asvar: | 
|---|
| 77 | @@ -90,12 +96,13 @@ class TranslateNode(Node): | 
|---|
| 78 |  | 
|---|
| 79 | class BlockTranslateNode(Node): | 
|---|
| 80 | def __init__(self, extra_context, singular, plural=None, countervar=None, | 
|---|
| 81 | -            counter=None): | 
|---|
| 82 | +            counter=None, trans_context=None): | 
|---|
| 83 | self.extra_context = extra_context | 
|---|
| 84 | self.singular = singular | 
|---|
| 85 | self.plural = plural | 
|---|
| 86 | self.countervar = countervar | 
|---|
| 87 | self.counter = counter | 
|---|
| 88 | +        self.trans_context = trans_context | 
|---|
| 89 |  | 
|---|
| 90 | def render_token_list(self, tokens): | 
|---|
| 91 | result = [] | 
|---|
| 92 | @@ -109,6 +116,10 @@ class BlockTranslateNode(Node): | 
|---|
| 93 | return ''.join(result), vars | 
|---|
| 94 |  | 
|---|
| 95 | def render(self, context): | 
|---|
| 96 | +        if self.trans_context: | 
|---|
| 97 | +            trans_context = self.trans_context.resolve(context) | 
|---|
| 98 | +        else: | 
|---|
| 99 | +            trans_context = None | 
|---|
| 100 | tmp_context = {} | 
|---|
| 101 | for var, val in self.extra_context.items(): | 
|---|
| 102 | tmp_context[var] = val.resolve(context) | 
|---|
| 103 | @@ -123,10 +134,17 @@ class BlockTranslateNode(Node): | 
|---|
| 104 | context[self.countervar] = count | 
|---|
| 105 | plural, plural_vars = self.render_token_list(self.plural) | 
|---|
| 106 | plural = re.sub(u'%(?!\()', u'%%', plural) | 
|---|
| 107 | -            result = translation.ungettext(singular, plural, count) | 
|---|
| 108 | +            if trans_context: | 
|---|
| 109 | +                result = translation.npgettext(trans_context, singular, plural, | 
|---|
| 110 | +                                               count) | 
|---|
| 111 | +            else: | 
|---|
| 112 | +                result = translation.ungettext(singular, plural, count) | 
|---|
| 113 | vars.extend(plural_vars) | 
|---|
| 114 | else: | 
|---|
| 115 | -            result = translation.ugettext(singular) | 
|---|
| 116 | +            if trans_context: | 
|---|
| 117 | +                result = translation.pgettext(trans_context, singular) | 
|---|
| 118 | +            else: | 
|---|
| 119 | +                result = translation.ugettext(singular) | 
|---|
| 120 | data = dict([(v, _render_value_in_context(context.get(v, ''), context)) for v in vars]) | 
|---|
| 121 | context.pop() | 
|---|
| 122 | try: | 
|---|
| 123 | @@ -301,7 +319,6 @@ def do_translate(parser, token): | 
|---|
| 124 | # backwards compatibility with existing uses of ``trans`` | 
|---|
| 125 | # where single quote use is supported. | 
|---|
| 126 | if value[0] == "'": | 
|---|
| 127 | -                pos = None | 
|---|
| 128 | m = re.match("^'([^']+)'(\|.*$)", value) | 
|---|
| 129 | if m: | 
|---|
| 130 | value = '"%s"%s' % (m.group(1).replace('"','\\"'), m.group(2)) | 
|---|
| 131 | @@ -310,19 +327,23 @@ def do_translate(parser, token): | 
|---|
| 132 |  | 
|---|
| 133 | noop = False | 
|---|
| 134 | asvar = None | 
|---|
| 135 | +            trans_context = None | 
|---|
| 136 |  | 
|---|
| 137 | while self.more(): | 
|---|
| 138 | tag = self.tag() | 
|---|
| 139 | if tag == 'noop': | 
|---|
| 140 | noop = True | 
|---|
| 141 | +                elif tag == 'context': | 
|---|
| 142 | +                    trans_context = parser.compile_filter(self.tag()) | 
|---|
| 143 | elif tag == 'as': | 
|---|
| 144 | asvar = self.tag() | 
|---|
| 145 | else: | 
|---|
| 146 | raise TemplateSyntaxError( | 
|---|
| 147 | "only options for 'trans' are 'noop' and 'as VAR.") | 
|---|
| 148 | -            return (value, noop, asvar) | 
|---|
| 149 | -    value, noop, asvar = TranslateParser(token.contents).top() | 
|---|
| 150 | -    return TranslateNode(parser.compile_filter(value), noop, asvar) | 
|---|
| 151 | +            return value, noop, asvar, trans_context | 
|---|
| 152 | +    value, noop, asvar, trans_context = TranslateParser(token.contents).top() | 
|---|
| 153 | +    return TranslateNode(parser.compile_filter(value), noop, asvar, | 
|---|
| 154 | +                         trans_context) | 
|---|
| 155 |  | 
|---|
| 156 | @register.tag("blocktrans") | 
|---|
| 157 | def do_block_translate(parser, token): | 
|---|
| 158 | @@ -369,6 +390,13 @@ def do_block_translate(parser, token): | 
|---|
| 159 | if len(value) != 1: | 
|---|
| 160 | raise TemplateSyntaxError('"count" in %r tag expected exactly ' | 
|---|
| 161 | 'one keyword argument.' % bits[0]) | 
|---|
| 162 | +        elif option == "context": | 
|---|
| 163 | +            try: | 
|---|
| 164 | +                value = remaining_bits.pop(0) | 
|---|
| 165 | +                value = parser.compile_filter(value) | 
|---|
| 166 | +            except Exception: | 
|---|
| 167 | +                raise TemplateSyntaxError('"context" in %r tag expected ' | 
|---|
| 168 | +                                          'exactly one argument.' % bits[0]) | 
|---|
| 169 | else: | 
|---|
| 170 | raise TemplateSyntaxError('Unknown argument for %r tag: %r.' % | 
|---|
| 171 | (bits[0], option)) | 
|---|
| 172 | @@ -378,7 +406,11 @@ def do_block_translate(parser, token): | 
|---|
| 173 | countervar, counter = options['count'].items()[0] | 
|---|
| 174 | else: | 
|---|
| 175 | countervar, counter = None, None | 
|---|
| 176 | -    extra_context = options.get('with', {}) | 
|---|
| 177 | +    if 'context' in options: | 
|---|
| 178 | +        trans_context = options['context'] | 
|---|
| 179 | +    else: | 
|---|
| 180 | +        trans_context = None | 
|---|
| 181 | +    extra_context = options.get('with', {}) | 
|---|
| 182 |  | 
|---|
| 183 | singular = [] | 
|---|
| 184 | plural = [] | 
|---|
| 185 | @@ -401,7 +433,7 @@ def do_block_translate(parser, token): | 
|---|
| 186 | raise TemplateSyntaxError("'blocktrans' doesn't allow other block tags (seen %r) inside it" % token.contents) | 
|---|
| 187 |  | 
|---|
| 188 | return BlockTranslateNode(extra_context, singular, plural, countervar, | 
|---|
| 189 | -            counter) | 
|---|
| 190 | +            counter, trans_context) | 
|---|
| 191 |  | 
|---|
| 192 | @register.tag | 
|---|
| 193 | def language(parser, token): | 
|---|
| 194 | diff --git a/tests/regressiontests/i18n/other/locale/de/LC_MESSAGES/django.mo b/tests/regressiontests/i18n/other/locale/de/LC_MESSAGES/django.mo | 
|---|
| 195 | index a1a93c83296f0fb97d500466a8df30338fdb137e..9ab2f4672535fe38d864ec37cbd7815cac8db453 100644 | 
|---|
| 196 | GIT binary patch | 
|---|
| 197 | delta 554 | 
|---|
| 198 | zcmaKnu}T9$5QZnoX^dbBMbSj$5KofA(n@TMR$2%?fLxNr3&vG;bCt+7d4V7{zJ{F$ | 
|---|
| 199 | z;zL;KD;Qru{5J70N_5$Ozq#4jf97uUkA>uQ*1HkbG+IV&G=m<HTYYqh-cbSVWQKJI | 
|---|
| 200 | z(Br!Wi%`QFyn<!;1h?S}oPy=7$T9I6bbj5O$dX7R&jc$Z-ryW8;z}s1a2Kw_MR*D~ | 
|---|
| 201 | zVHdLHo&!IbKyFPAF*xZlXzB*eBlmO8)4h|Ra-mE^=}y%KLGPo~>#O9v6Di}{m>b`! | 
|---|
| 202 | z&@>`1sOb2tu1wW_<&Cm&yTkLbc`NC=rk_@(5qJMPFO0Xoi!$@s+IPQDH<eM^KXPsx | 
|---|
| 203 | jd*(#db<2)UG_3elVAb_yuCz5W_}>OUZ>*C2P;2=F0$+a) | 
|---|
| 204 |  | 
|---|
| 205 | delta 225 | 
|---|
| 206 | zcmdnbIg7pio)F7a1|Z-7Vi_Qg0b*_-o&&@nZ~}+}fcPX3a{{pxBSf7FkY)k$9f33- | 
|---|
| 207 | zkah#o(m*;CNQ2BO1k&+9Tm{4+{kNDH7$kwTHZuc*9FTSe(!xM(3@Jbc5HVB$2?-!w | 
|---|
| 208 | z1EfI)PX*FIy>I{$1DXK_Y(NsEhaHGPma$HJJ7MxcMw!jhOgv1Jb66x;y%Y2D7y!I` | 
|---|
| 209 | B6te&T | 
|---|
| 210 |  | 
|---|
| 211 | diff --git a/tests/regressiontests/i18n/other/locale/de/LC_MESSAGES/django.po b/tests/regressiontests/i18n/other/locale/de/LC_MESSAGES/django.po | 
|---|
| 212 | index 7226ad1..9bf79ce 100644 | 
|---|
| 213 | --- a/tests/regressiontests/i18n/other/locale/de/LC_MESSAGES/django.po | 
|---|
| 214 | +++ b/tests/regressiontests/i18n/other/locale/de/LC_MESSAGES/django.po | 
|---|
| 215 | @@ -52,3 +52,18 @@ msgid "%(percent)s%% represents %(num)s object" | 
|---|
| 216 | msgid_plural "%(percent)s%% represents %(num)s objects" | 
|---|
| 217 | msgstr[0] "%(percent)s%% stellt %(num)s Objekt dar" | 
|---|
| 218 | msgstr[1] "%(percent)s%% stellt %(num)s Objekte dar" | 
|---|
| 219 | + | 
|---|
| 220 | +#: models.py:17 | 
|---|
| 221 | +msgctxt "super search" | 
|---|
| 222 | +msgid "%(number)s super result" | 
|---|
| 223 | +msgid_plural "%(number)s super results" | 
|---|
| 224 | +msgstr[0] "%(number)s Super-Ergebnis" | 
|---|
| 225 | +msgstr[1] "%(number)s Super-Ergebnisse" | 
|---|
| 226 | + | 
|---|
| 227 | +#: models.py:19 | 
|---|
| 228 | +msgctxt "other super search" | 
|---|
| 229 | +msgid "%(number)s super result" | 
|---|
| 230 | +msgid_plural "%(number)s super results" | 
|---|
| 231 | +msgstr[0] "%(number)s anderen Super-Ergebnis" | 
|---|
| 232 | +msgstr[1] "%(number)s andere Super-Ergebnisse" | 
|---|
| 233 | + | 
|---|
| 234 | diff --git a/tests/regressiontests/i18n/tests.py b/tests/regressiontests/i18n/tests.py | 
|---|
| 235 | index fc69d9c..7b0e285 100644 | 
|---|
| 236 | --- a/tests/regressiontests/i18n/tests.py | 
|---|
| 237 | +++ b/tests/regressiontests/i18n/tests.py | 
|---|
| 238 | @@ -9,6 +9,7 @@ from threading import local | 
|---|
| 239 |  | 
|---|
| 240 | from django.conf import settings | 
|---|
| 241 | from django.template import Template, Context | 
|---|
| 242 | +from django.template.base import TemplateSyntaxError | 
|---|
| 243 | from django.test import TestCase, RequestFactory | 
|---|
| 244 | from django.test.utils import override_settings | 
|---|
| 245 | from django.utils import translation | 
|---|
| 246 | @@ -87,6 +88,69 @@ class TranslationTests(TestCase): | 
|---|
| 247 | self.assertEqual(pgettext("verb", "May"), u"Kann") | 
|---|
| 248 | self.assertEqual(npgettext("search", "%d result", "%d results", 4) % 4, u"4 Resultate") | 
|---|
| 249 |  | 
|---|
| 250 | +    def test_template_tags_pgettext(self): | 
|---|
| 251 | +        # Reset translation catalog to include other/locale/de | 
|---|
| 252 | +        extended_locale_paths = settings.LOCALE_PATHS + ( | 
|---|
| 253 | +            os.path.join(here, 'other', 'locale'), | 
|---|
| 254 | +        ) | 
|---|
| 255 | +        with self.settings(LOCALE_PATHS=extended_locale_paths): | 
|---|
| 256 | +            from django.utils.translation import trans_real | 
|---|
| 257 | +            trans_real._active = local() | 
|---|
| 258 | +            trans_real._translations = {} | 
|---|
| 259 | +            with translation.override('de'): | 
|---|
| 260 | + | 
|---|
| 261 | +                # Blocktrans --------- | 
|---|
| 262 | + | 
|---|
| 263 | +                # Inexisting context... | 
|---|
| 264 | +                t = Template('{% load i18n %}{% blocktrans context "unexisting" %}May{% endblocktrans %}') | 
|---|
| 265 | +                rendered = t.render(Context()) | 
|---|
| 266 | +                self.assertEqual(rendered, 'May') | 
|---|
| 267 | + | 
|---|
| 268 | +                # Existing context... | 
|---|
| 269 | +                # Using a literal | 
|---|
| 270 | +                t = Template('{% load i18n %}{% blocktrans context "month name" %}May{% endblocktrans %}') | 
|---|
| 271 | +                rendered = t.render(Context()) | 
|---|
| 272 | +                self.assertEqual(rendered, 'Mai') | 
|---|
| 273 | +                t = Template('{% load i18n %}{% blocktrans context "verb" %}May{% endblocktrans %}') | 
|---|
| 274 | +                rendered = t.render(Context()) | 
|---|
| 275 | +                self.assertEqual(rendered, 'Kann') | 
|---|
| 276 | + | 
|---|
| 277 | +                # Using a variable | 
|---|
| 278 | +                t = Template('{% load i18n %}{% blocktrans context trans_context %}May{% endblocktrans %}') | 
|---|
| 279 | +                rendered = t.render(Context({'trans_context': 'month name'})) | 
|---|
| 280 | +                self.assertEqual(rendered, 'Mai') | 
|---|
| 281 | +                t = Template('{% load i18n %}{% blocktrans context trans_context %}May{% endblocktrans %}') | 
|---|
| 282 | +                rendered = t.render(Context({'trans_context': 'verb'})) | 
|---|
| 283 | +                self.assertEqual(rendered, 'Kann') | 
|---|
| 284 | + | 
|---|
| 285 | +                # Using a filter | 
|---|
| 286 | +                t = Template('{% load i18n %}{% blocktrans context trans_context|lower %}May{% endblocktrans %}') | 
|---|
| 287 | +                rendered = t.render(Context({'trans_context': 'MONTH NAME'})) | 
|---|
| 288 | +                self.assertEqual(rendered, 'Mai') | 
|---|
| 289 | +                t = Template('{% load i18n %}{% blocktrans context trans_context|lower %}May{% endblocktrans %}') | 
|---|
| 290 | +                rendered = t.render(Context({'trans_context': 'VERB'})) | 
|---|
| 291 | +                self.assertEqual(rendered, 'Kann') | 
|---|
| 292 | + | 
|---|
| 293 | +                # Using 'count' | 
|---|
| 294 | +                t = Template('{% load i18n %}{% blocktrans count number=1 context "super search" %}{{ number }} super result{% plural %}{{ number }} super results{% endblocktrans %}') | 
|---|
| 295 | +                rendered = t.render(Context()) | 
|---|
| 296 | +                self.assertEqual(rendered, '1 Super-Ergebnis') | 
|---|
| 297 | +                t = Template('{% load i18n %}{% blocktrans count number=2 context "super search" %}{{ number }} super result{% plural %}{{ number }} super results{% endblocktrans %}') | 
|---|
| 298 | +                rendered = t.render(Context()) | 
|---|
| 299 | +                self.assertEqual(rendered, '2 Super-Ergebnisse') | 
|---|
| 300 | +                t = Template('{% load i18n %}{% blocktrans count number=1 context "other super search" %}{{ number }} super result{% plural %}{{ number }} super results{% endblocktrans %}') | 
|---|
| 301 | +                rendered = t.render(Context()) | 
|---|
| 302 | +                self.assertEqual(rendered, '1 anderen Super-Ergebnis') | 
|---|
| 303 | +                t = Template('{% load i18n %}{% blocktrans count number=2 context "other super search" %}{{ number }} super result{% plural %}{{ number }} super results{% endblocktrans %}') | 
|---|
| 304 | +                rendered = t.render(Context()) | 
|---|
| 305 | +                self.assertEqual(rendered, '2 andere Super-Ergebnisse') | 
|---|
| 306 | + | 
|---|
| 307 | +                # Mis-uses | 
|---|
| 308 | +                self.assertRaises(TemplateSyntaxError, Template, '{% load i18n %}{% blocktrans context with month="May" %}{{ month }}{% endblocktrans %}') | 
|---|
| 309 | +                self.assertRaises(TemplateSyntaxError, Template, '{% load i18n %}{% blocktrans context %}{% endblocktrans %}') | 
|---|
| 310 | +                self.assertRaises(TemplateSyntaxError, Template, '{% load i18n %}{% blocktrans count number=2 context %}{{ number }} super result{% plural %}{{ number }} super results{% endblocktrans %}') | 
|---|
| 311 | + | 
|---|
| 312 | + | 
|---|
| 313 | def test_string_concat(self): | 
|---|
| 314 | """ | 
|---|
| 315 | unicode(string_concat(...)) should not raise a TypeError - #4796 | 
|---|