2 # -*- coding: utf-8 -*-
5 # Copyright 2010,2011 Stephen Kelly <steveire@gmail.com>
7 # Redistribution and use in source and binary forms, with or without
8 # modification, are permitted provided that the following conditions
11 # 1. Redistributions of source code must retain the above copyright
12 # notice, this list of conditions and the following disclaimer.
13 # 2. Redistributions in binary form must reproduce the above copyright
14 # notice, this list of conditions and the following disclaimer in the
15 # documentation and/or other materials provided with the distribution.
17 # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
18 # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
19 # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
20 # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
21 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
22 # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26 # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 ## Parts of this file are reproduced from the Django framework. The Django licence appears below.
32 # Copyright (c) Django Software Foundation and individual contributors.
33 # All rights reserved.
35 # Redistribution and use in source and binary forms, with or without modification,
36 # are permitted provided that the following conditions are met:
38 # 1. Redistributions of source code must retain the above copyright notice,
39 # this list of conditions and the following disclaimer.
41 # 2. Redistributions in binary form must reproduce the above copyright
42 # notice, this list of conditions and the following disclaimer in the
43 # documentation and/or other materials provided with the distribution.
45 # 3. Neither the name of Django nor the names of its contributors may be used
46 # to endorse or promote products derived from this software without
47 # specific prior written permission.
49 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
50 # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
51 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
52 # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
53 # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
54 # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
55 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
56 # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
57 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
58 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
64 # == Introduction to the template syntax ==
66 # The template syntax looks like this:
67 # (For more see here: http://grantlee.org/apidox/for_themers.html )
70 # This is text with a {{ value }} substitution
71 # This is {% if condition_is_met %}a conditional{% endif %}
72 # {# This is a comment #}
73 # This is a {% comment %} multi-line
77 # That is, we have plain text.
78 # We have value substitution with {{ }}
79 # We have comments with {# #}
80 # We have control tags with {% %}
82 # The first token inside {% %} syntax is called a tag name. Above, we have
83 # an if tag and a comment tag.
85 # The 'value' in {{ value }} is called a filter expression. In the above case
86 # the filter expression is a simple value which was inserted into the context.
87 # In other cases it can be {{ value|upper }}, that is the value can be passed
88 # through a filter called 'upper' with the '|', or filter expression can
89 # be {{ value|join:"-" }}, that is it can be passed through the join filter
90 # which takes an argument. In this case, the 'value' would actually be a list,
91 # and the join filter would concatenate them with a dash. A filter can have
92 # either no arguments, like upper, or it can take one argument, delimited by
93 # a colon (';'). A filter expression can consist of a value followed by a
94 # chain of filters, such as {{ value|join:"-"|upper }}. A filter expression
95 # can appear one time inside {{ }} but may appear multiple times inside {% %}
96 # For example {% cycle foo|upper bar|join:"-" bat %} contains 3 filter
97 # expressions, 'foo|upper', 'bar|join:"-"' and 'bat'.
99 # Comments are ignored in the templates.
101 # == i18n in templates ==
103 # The purpose of this script is to extract translatable strings from templates
104 # The aim is to allow template authors to write templates like this:
106 # This is a {{ _("translatable string") }} in the template.
107 # This is a {% i18n "translatable string about %1" something %}
108 # This is a {% i18nc "Some context information" "string about %1" something %}
109 # This is a {% i18np "%1 string about %2" numthings something %}
110 # This is a {% i18ncp "some context" "%1 string about %2" numthings something %}
112 # That is, simple translation with _(), and i18n* tags to allow for variable
113 # substitution, context messages and plurals. Translatable strings may appear
114 # in a filter expression, either as the value begin filtered, or as the argument
117 # {{ _("hello")|upper }}
118 # {{ list|join:_("and") }}
120 # == How the strings are extracted ==
122 # The strings are extracted by parsing the template with regular expressions.
123 # The tag_re regular expression breaks the template into a stream of tokens
124 # containing plain text, {{ values }} and {% tags %}.
125 # That work is done by the tokenize method with the create_token method.
126 # Each token is then processed to extract the translatable strings from
127 # the filter expressions.
130 # The original context of much of this script is in the django template system:
131 # http://code.djangoproject.com/browser/django/trunk/django/template/base.py
139 # template syntax constants
140 FILTER_SEPARATOR
= '|'
141 FILTER_ARGUMENT_SEPARATOR
= ':'
142 BLOCK_TAG_START
= '{%'
144 VARIABLE_TAG_START
= '{{'
145 VARIABLE_TAG_END
= '}}'
146 COMMENT_TAG_START
= '{#'
147 COMMENT_TAG_END
= '#}'
149 # match a variable or block tag and capture the entire tag, including start/end delimiters
150 tag_re
= re
.compile('(%s.*?%s|%s.*?%s)' % (re
.escape(BLOCK_TAG_START
), re
.escape(BLOCK_TAG_END
),
151 re
.escape(VARIABLE_TAG_START
), re
.escape(VARIABLE_TAG_END
)))
154 # Expression to match some_token and some_token="with spaces" (and similarly
155 # for single-quoted strings).
156 smart_split_re
= re
.compile(r
"""
160 (?:"(?:[^"\\]|\\.)*" | '(?:[^'\\]|\\.)*')
166 def smart_split(text
):
168 Generator that splits a string by spaces, leaving quoted phrases together.
169 Supports both single and double quotes, and supports escaping quotes with
170 backslashes. In the output, strings will keep their initial and trailing
171 quote marks and escaped quotes will remain escaped (the results can then
172 be further processed with unescape_string_literal()).
174 >>> list(smart_split(r'This is "a person\'s" test.'))
175 [u'This', u'is', u'"a person\\\'s"', u'test.']
176 >>> list(smart_split(r"Another 'person\'s' test."))
177 [u'Another', u"'person\\'s'", u'test.']
178 >>> list(smart_split(r'A "\"funky
\" style
" test.'))
179 [u'A', u'"\\"funky\\" style
"', u'test.']
181 for bit in smart_split_re.finditer(text):
185 # This only matches constant *strings* (things in quotes or marked for
188 constant_string = r"(?
:%(strdq)s|
%(strsq)s)" % {
189 'strdq': r'"[^
"\\]*(?:\\.[^"\\]*)*"', # double-quoted string
190 'strsq': r"'[^'\\]*(?
:\\.[^
'\\]*)*'", # single-quoted string
193 filter_raw_string = r"""^%(i18n_open)s(?P<l10nable>%(constant_string)s)%(i18n_close)s""" % {
194 'constant_string': constant_string,
195 'i18n_open' : re.escape("_("),
196 'i18n_close' : re.escape(")"),
199 filter_re = re.compile(filter_raw_string, re.UNICODE|re.VERBOSE)
201 class TemplateSyntaxError(Exception):
204 class TranslatableString:
210 return "String('%s', '%s', '%s')" % (self._string, self.context, self.plural)
213 def __init__(self, token_type, contents):
214 # token_type must be TOKEN_TEXT, TOKEN_VAR, TOKEN_BLOCK or TOKEN_COMMENT.
215 self.token_type, self.contents = token_type, contents
218 return '<%s token: "%s...">' % \
219 ({TOKEN_TEXT: 'Text', TOKEN_VAR: 'Var', TOKEN_BLOCK: 'Block', TOKEN_COMMENT: 'Comment'}[self.token_type],
220 self.contents[:20].replace('\n', ''))
222 def create_token(token_string, in_tag):
224 Convert the given token string into a new Token object and return it.
225 If in_tag is True, we are processing something that matched a tag,
226 otherwise it should be treated as a literal string.
229 if token_string.startswith(VARIABLE_TAG_START):
230 token = Token(TOKEN_VAR, token_string[len(VARIABLE_TAG_START):-len(VARIABLE_TAG_END)].strip())
231 elif token_string.startswith(BLOCK_TAG_START):
232 token = Token(TOKEN_BLOCK, token_string[len(BLOCK_TAG_START):-len(BLOCK_TAG_END)].strip())
233 elif token_string.startswith(COMMENT_TAG_START):
234 token = Token(TOKEN_COMMENT, '')
236 token = Token(TOKEN_TEXT, token_string)
239 def tokenize(template_string):
243 for bit in tag_re.split(template_string):
245 result.append(create_token(bit, in_tag))
249 class TranslationOutputter:
250 translatable_strings = []
252 def get_translatable_filter_args(self, token):
254 Find the filter expressions in token and extract the strings in it.
256 matches = filter_re.finditer(token)
259 for match in matches:
260 l10nable = match.group("l10nable
")
263 # Make sure it's a quoted string
264 if l10nable.startswith('"') and l10nable.endswith('"') \
265 or l10nable.startswith("'") and l10nable.endswith("'"):
266 ts = TranslatableString()
267 ts._string = l10nable[1:-1]
268 self.translatable_strings.append(ts)
270 def get_contextual_strings(self, token):
272 _bits = smart_split(token.contents)
274 if _bit =="i18n
" or _bit == "i18n_var
":
275 # {% i18n "A one
%1, a two
%2, a three
%3" var1 var2 var3 %}
276 # {% i18n_var "A one
%1, a two
%2, a three
%3" var1 var2 var3 as result %}
278 if not _bit.startswith("'") and not _bit.startswith('"'):
282 if not _bit.endswith(sentinal):
285 translatable_string = TranslatableString()
286 translatable_string._string = _bit[1:-1]
287 self.translatable_strings.append(translatable_string)
288 elif _bit =="i18nc
" or _bit == "i18nc_var
":
289 # {% i18nc "An email send operation failed
." "%1 Failed
!" var1 %}
290 # {% i18nc_var "An email send operation failed
." "%1 Failed
!" var1 as result %}
292 if not _bit.startswith("'") and not _bit.startswith('"'):
296 if not _bit.endswith(sentinal):
299 translatable_string = TranslatableString()
300 translatable_string.context = _bit[1:-1]
302 translatable_string._string = _bit[1:-1]
303 self.translatable_strings.append(translatable_string)
304 elif _bit =="i18np
" or _bit =="i18np_var
":
305 # {% i18np "An email send operation failed
." "%1 email send operations failed
. Error
: % 2." count count errorMsg %}
306 # {% i18np_var "An email send operation failed
." "%1 email send operations failed
. Error
: % 2." count count errorMsg as result %}
308 if not _bit.startswith("'") and not _bit.startswith('"'):
312 if not _bit.endswith(sentinal):
315 translatable_string = TranslatableString()
316 translatable_string._string = _bit[1:-1]
318 translatable_string.plural = _bit[1:-1]
319 self.translatable_strings.append(translatable_string)
320 elif _bit =="i18ncp
" or _bit =="i18ncp_var
":
321 # {% i18np "The user tried to send an email
, but that failed
." "An email send operation failed
." "%1 email send operation failed
." count count %}
322 # {% i18np_var "The user tried to send an email
, but that failed
." "An email send operation failed
." "%1 email send operation failed
." count count as result %}
325 if not _bit.startswith("'") and not _bit.startswith('"'):
329 if not _bit.endswith(sentinal):
332 translatable_string = TranslatableString()
333 translatable_string.context = _bit[1:-1]
335 translatable_string._string = _bit[1:-1]
337 translatable_string.plural = _bit[1:-1]
338 self.translatable_strings.append(translatable_string)
346 self.get_translatable_filter_args(_bit)
348 def get_plain_strings(self, token):
350 bits = iter(smart_split(token.contents))
352 self.get_translatable_filter_args(bit)
354 def translate(self, template_file, outputfile):
355 template_string = template_file.read()
356 self.translatable_strings = []
357 for token in tokenize(template_string):
358 if token.token_type == TOKEN_VAR or token.token_type == TOKEN_BLOCK:
359 self.get_plain_strings(token)
360 if token.token_type == TOKEN_BLOCK:
361 self.get_contextual_strings(token)
362 self.createOutput(os.path.relpath(template_file.name), self.translatable_strings, outputfile)
364 def createOutput(self, template_filename, translatable_strings, outputfile):