From 43aa3f73fcf059d6e3939ae05d65fcb42bdcc47f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 4 Mar 2007 20:06:58 +0100 Subject: [PATCH] [svn] added support for translations (unstable api) --HG-- branch : trunk --- jinja/datastructure.py | 7 +- jinja/environment.py | 6 +- jinja/loaders.py | 3 + jinja/nodes.py | 28 ++++++++ jinja/parser.py | 129 +++++++++++++++++++++++++++++++++++- jinja/translators/python.py | 48 +++++++++++--- www/static/style.css | 3 +- 7 files changed, 208 insertions(+), 16 deletions(-) diff --git a/jinja/datastructure.py b/jinja/datastructure.py index 22f5fbc..e936165 100644 --- a/jinja/datastructure.py +++ b/jinja/datastructure.py @@ -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): """ diff --git a/jinja/environment.py b/jinja/environment.py index 124ee47..3311c8d 100644 --- a/jinja/environment.py +++ b/jinja/environment.py @@ -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() \ diff --git a/jinja/loaders.py b/jinja/loaders.py index 455232d..734eedb 100644 --- a/jinja/loaders.py +++ b/jinja/loaders.py @@ -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): """ diff --git a/jinja/nodes.py b/jinja/nodes.py index 42326e1..72423db 100644 --- a/jinja/nodes.py +++ b/jinja/nodes.py @@ -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 + ) diff --git a/jinja/parser.py b/jinja/parser.py index af77022..a95dda8 100644 --- a/jinja/parser.py +++ b/jinja/parser.py @@ -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 diff --git a/jinja/translators/python.py b/jinja/translators/python.py index 86c2577..aa3dd24 100644 --- a/jinja/translators/python.py +++ b/jinja/translators/python.py @@ -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): diff --git a/www/static/style.css b/www/static/style.css index 1e89fa3..9070d40 100644 --- a/www/static/style.css +++ b/www/static/style.css @@ -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; -- 2.26.2