[svn] added support for translations (unstable api)
authorArmin Ronacher <armin.ronacher@active-4.com>
Sun, 4 Mar 2007 19:06:58 +0000 (20:06 +0100)
committerArmin Ronacher <armin.ronacher@active-4.com>
Sun, 4 Mar 2007 19:06:58 +0000 (20:06 +0100)
--HG--
branch : trunk

jinja/datastructure.py
jinja/environment.py
jinja/loaders.py
jinja/nodes.py
jinja/parser.py
jinja/translators/python.py
www/static/style.css

index 22f5fbcacd68c33d87e4567988d0b20dd0c9fcd8..e936165b5a77077b2803d48e4f0cf62ca96a5e58 100644 (file)
@@ -112,6 +112,10 @@ class Context(object):
         # cache object used for filters and tests
         self.cache = {}
 
+    def get_translator(self):
+        """Return the translator for i18n."""
+        return lambda a, b, c: a
+
     def pop(self):
         if len(self._stack) <= 2:
             raise ValueError('cannot pop initial layer')
@@ -217,9 +221,6 @@ class LoopContext(object):
             return self.loop_function(seq)
         return Undefined
 
-    def __repr__(self):
-        return str(self._stack)
-
 
 class CycleContext(object):
     """
index 124ee47ab2e502fbd03d88da81073d2f5262658e..3311c8d47209d0f66a81becf2815d591f3a3994a 100644 (file)
@@ -12,7 +12,7 @@ import re
 from jinja.lexer import Lexer
 from jinja.parser import Parser
 from jinja.loaders import LoaderWrapper
-from jinja.datastructure import Undefined
+from jinja.datastructure import Undefined, Context
 from jinja.utils import escape
 from jinja.exceptions import FilterNotFound, TestNotFound, SecurityException
 from jinja.defaults import DEFAULT_FILTERS, DEFAULT_TESTS, DEFAULT_NAMESPACE
@@ -37,7 +37,8 @@ class Environment(object):
                  namespace=None,
                  loader=None,
                  filters=None,
-                 tests=None):
+                 tests=None,
+                 context_class=Context):
 
         # lexer / parser information
         self.block_start_string = block_start_string
@@ -55,6 +56,7 @@ class Environment(object):
         self.filters = filters is None and DEFAULT_FILTERS.copy() or filters
         self.tests = tests is None and DEFAULT_TESTS.copy() or tests
         self.auto_escape = auto_escape
+        self.context_class = context_class
 
         # global namespace
         self.globals = namespace is None and DEFAULT_NAMESPACE.copy() \
index 455232d9341b0b1f487573fced27a2fc192c1d85..734eedb067f092d9e4b2a15ee1fde000d94acf68 100644 (file)
@@ -68,6 +68,9 @@ class LoaderWrapper(object):
         loader is defined."""
         raise RuntimeError('no loader defined')
 
+    def __nonzero__(self):
+        return self.loader is not None
+
 
 class FileSystemLoader(object):
     """
index 42326e128c96556031785c86c626bea4e2cfa6f5..72423db390ad894048996abf7efe5f9dd3955139 100644 (file)
@@ -300,3 +300,31 @@ class Include(Node):
 
     def __repr__(self):
         return 'Include(%r)' % self.template
+
+
+class Trans(Node):
+    """
+    A node for translatable sections.
+    """
+
+    def __init__(self, lineno, singular, plural, indicator, replacements):
+        self.lineno = lineno
+        self.singular = singular
+        self.plural = plural
+        self.indicator = indicator
+        self.replacements = replacements
+
+    def get_items(self):
+        rv = [self.singular, self.plural, self.indicator]
+        if self.replacements:
+            rv.extend(self.replacements.values())
+            rv.extend(self.replacements.keys())
+        return rv
+
+    def __repr__(self):
+        return 'Trans(%r, %r, %r, %r)' % (
+            self.singular,
+            self.plural,
+            self.indicator,
+            self.replacements
+        )
index af7702261e40fd62f6631d826b1467c7a151417a..a95dda89464eb0fedfa0d96b7a4636d461b1079c 100644 (file)
@@ -26,7 +26,25 @@ end_of_if = lambda p, t, d: t == 'name' and d == 'endif'
 end_of_filter = lambda p, t, d: t == 'name' and d == 'endfilter'
 end_of_macro = lambda p, t, d: t == 'name' and d == 'endmacro'
 end_of_block_tag = lambda p, t, d: t == 'name' and d == 'endblock'
-end_of_raw = lambda p, t, d: t == 'name' and d == 'endraw'
+end_of_trans = lambda p, t, d: t == 'name' and d == 'endtrans'
+
+
+string_inc_re = re.compile(r'(?:[^\d]*(\d+)[^\d]*)+')
+
+
+def inc_string(s):
+    """
+    Increment a string
+    """
+    m = string_inc_re.search(s)
+    if m:
+        next = str(int(m.group(1)) + 1)
+        start, end = m.span(1)
+        s = s[:max(end - len(next), start)] + next + s[end:]
+    else:
+        name, ext = s.rsplit('.', 1)
+        return '%s2.%s' % (name, ext)
+    return s
 
 
 class Parser(object):
@@ -57,7 +75,8 @@ class Parser(object):
             'macro':        self.handle_macro_directive,
             'block':        self.handle_block_directive,
             'extends':      self.handle_extends_directive,
-            'include':      self.handle_include_directive
+            'include':      self.handle_include_directive,
+            'trans':        self.handle_trans_directive
         }
 
     def handle_for_directive(self, lineno, gen):
@@ -231,6 +250,112 @@ class Parser(object):
             raise TemplateSyntaxError('include requires a string', lineno)
         return nodes.Include(lineno, tokens[0][2][1:-1])
 
+    def handle_trans_directive(self, lineno, gen):
+        """
+        Handle translatable sections.
+        """
+        # save the initial line number for the resulting node
+        flineno = lineno
+        try:
+            # check for string translations
+            lineno, token, data = gen.next()
+            if token == 'string':
+                # check that there are not any more elements
+                try:
+                    gen.next()
+                except StopIteration:
+                    #XXX: what about escapes?
+                    return nodes.Trans(lineno, data[1:-1], None, None, None)
+                raise TemplateSyntaxError('string based translations '
+                                          'require at most one argument.',
+                                          lineno)
+
+            # create a new generator with the popped item as first one
+            def wrapgen(oldgen):
+                yield lineno, token, data
+                for item in oldgen:
+                    yield item
+            gen = wrapgen(gen)
+
+            # block based translations
+            first_var = None
+            replacements = {}
+            for arg in self.parse_python(lineno, gen, '_trans(%s)').expr.args:
+                if arg.__class__ is not ast.Keyword:
+                    raise TemplateSyntaxError('translation tags need explicit '
+                                              'names for values.', lineno)
+                if first_var is None:
+                    first_var = arg.name
+                replacements[arg.name] = arg.expr
+
+            # look for endtrans/pluralize
+            buf = singular = []
+            plural = indicator = None
+
+            while True:
+                lineno, token, data = self.tokenstream.next()
+                # nested variables
+                if token == 'variable_begin':
+                    _, variable_token, variable_name = self.tokenstream.next()
+                    if variable_token != 'name' or variable_name not in replacements:
+                        raise TemplateSyntaxError('unregistered translation '
+                                                  'variable %r.' % variable_name,
+                                                  lineno)
+                    if self.tokenstream.next()[1] != 'variable_end':
+                        raise TemplateSyntaxError('invalid syntax for variable '
+                                                  'expression.', lineno)
+                    buf.append('%%(%s)s' % variable_name)
+                # nested blocks are not supported, just look for end blocks
+                elif token == 'block_begin':
+                    _, block_token, block_name = self.tokenstream.next()
+                    if block_token != 'name' or \
+                       block_name not in ('pluralize', 'endtrans'):
+                        raise TemplateSyntaxError('blocks in translatable sections '
+                                                  'are not supported', lineno)
+                    # pluralize
+                    if block_name == 'pluralize':
+                        if plural is not None:
+                            raise TemplateSyntaxError('translation blocks support '
+                                                      'at most one plural block',
+                                                      lineno)
+                        _, plural_token, plural_name = self.tokenstream.next()
+                        if plural_token == 'block_end':
+                            indicator = first_var
+                        elif plural_token == 'name':
+                            if plural_name not in replacements:
+                                raise TemplateSyntaxError('unknown tranlsation '
+                                                          'variable %r' %
+                                                          plural_name, lineno)
+                            elif self.tokenstream.next()[1] != 'block_end':
+                                raise TemplateSyntaxError('pluralize takes '
+                                                          'at most one '
+                                                          'argument', lineno)
+                            indicator = plural_name
+                        else:
+                            raise TemplateSyntaxError('pluralize requires no '
+                                                      'argument or a variable '
+                                                      'name.')
+                        plural = buf = []
+                    # end translation
+                    elif block_name == 'endtrans':
+                        self.close_remaining_block()
+                        break
+                # normal data
+                else:
+                    if replacements:
+                        data = data.replace('%', '%%')
+                    buf.append(data)
+
+        except StopIteration:
+            raise TemplateSyntaxError('unexpected end of translation section',
+                                      self.tokenstream.last[0])
+
+        singular = u''.join(singular)
+        if plural is not None:
+            plural = u''.join(plural)
+        return nodes.Trans(flineno, singular, plural, indicator, replacements or None)
+
+
     def parse_python(self, lineno, gen, template):
         """
         Convert the passed generator into a flat string representing
index 86c25778f4e74c3c5501d28542df6c96ad2c8dcc..aa3dd2400ef529780be921565a025ef0b98d9d5e 100644 (file)
@@ -11,7 +11,6 @@
 from compiler import ast
 from jinja import nodes
 from jinja.parser import Parser
-from jinja.datastructure import Context
 from jinja.exceptions import TemplateSyntaxError
 from jinja.translators import Translator
 
@@ -55,7 +54,7 @@ class Template(object):
             exec self.code in ns
             self.generate_func = ns['generate']
         result = []
-        ctx = Context(self.environment, *args, **kwargs)
+        ctx = self.environment.context_class(self.environment, *args, **kwargs)
         self.generate_func(ctx, result.append)
         return u''.join(result)
 
@@ -90,6 +89,7 @@ class PythonTranslator(Translator):
             nodes.Filter:           self.handle_filter,
             nodes.Block:            self.handle_block,
             nodes.Include:          self.handle_include,
+            nodes.Trans:            self.handle_trans,
             # used python nodes
             ast.Name:               self.handle_name,
             ast.AssName:            self.handle_name,
@@ -128,6 +128,8 @@ class PythonTranslator(Translator):
                 ast.GenExpr:        'generator expressions'
             })
 
+        self.require_translations = False
+
     # -- public methods
 
     def process(environment, node):
@@ -219,19 +221,20 @@ class PythonTranslator(Translator):
             'from __future__ import division\n'
             'from jinja.datastructure import Undefined, LoopContext, CycleContext\n\n'
             'def generate(context, write):\n'
-            '    # BOOTSTRAPPING CODE\n'
             '    environment = context.environment\n'
             '    get_attribute = environment.get_attribute\n'
             '    perform_test = environment.perform_test\n'
             '    apply_filters = environment.apply_filters\n'
             '    call_function = environment.call_function\n'
             '    call_function_simple = environment.call_function_simple\n'
-            '    finish_var = environment.finish_var\n\n'
-            '    # TEMPLATE CODE'
+            '    finish_var = environment.finish_var\n'
         ]
-        self.indention += 1
-        lines.append(self.handle_node_list(node))
-        self.indention -= 1
+        self.indention = 1
+        rv = self.handle_node_list(node)
+
+        if self.require_translations:
+            lines.append('    translate = context.get_translator()')
+        lines.append(rv)
 
         return '\n'.join(lines)
 
@@ -442,6 +445,32 @@ class PythonTranslator(Translator):
         buf.append(self.indent('# END OF INCLUSION'))
         return '\n'.join(buf)
 
+    def handle_trans(self, node):
+        """
+        Handle translations.
+        """
+        self.require_translations = True
+        if node.replacements:
+            replacements = []
+            for name, n in node.replacements.iteritems():
+                replacements.append('%r: %s' % (
+                    name,
+                    self.handle_node(n)
+                ))
+            replacements = '{%s}' % ', '.join(replacements)
+        else:
+            replacements = 'None'
+        if node.indicator is not None:
+            indicator = 'context[\'%s\']' % node.indicator
+        else:
+            indicator = 'None'
+        return self.indent('write(translate(%r, %r, %s) %% %s)' % (
+            node.singular,
+            node.plural,
+            indicator,
+            replacements
+        ))
+
     # -- python nodes
 
     def handle_name(self, node):
@@ -450,6 +479,9 @@ class PythonTranslator(Translator):
         """
         if node.name in self.constants:
             return self.constants[node.name]
+        elif node.name == '_':
+            self.require_translations = True
+            return 'translate'
         return 'context[%r]' % node.name
 
     def handle_compare(self, node):
index 1e89fa35ce96f0a83f7ca251a1b0fe5aba0db2ca..9070d40738a65f939021390cb5a184b4f38c41ae 100644 (file)
@@ -59,7 +59,8 @@ pre {
 
 #footer {
     margin: 0 25px 25px 25px;
-    border-bottom: 4px solid #ddd;
+    border: 4px solid #ddd;
+    border-top: none;
     background-color: #fff;
     text-align: right;
     padding: 0 10px 5px 0;