makemessages.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. import codecs
  2. from hashlib import md5
  3. import os
  4. import re
  5. from path import Path
  6. from django.core.management.commands.makemessages import Command as BaseCommand
  7. from django.utils.text import smart_split
  8. I18N_HELPERS = {
  9. # helper: min valid expression len
  10. 'gettext': 2,
  11. 'ngettext': 4,
  12. 'gettext_noop': 2,
  13. 'pgettext': 3,
  14. 'npgettext': 5
  15. }
  16. HBS_HELPERS = ('unbound', 'if')
  17. HBS_EXPRESSION = re.compile(r'({{{(.*?)}}})|({{(.*?)}})')
  18. class HandlebarsExpression(object):
  19. def __init__(self, unparsed_expression):
  20. cleaned_expression = self.clean_expression(unparsed_expression)
  21. all_helpers = self.parse_expression(
  22. unparsed_expression, cleaned_expression)
  23. self.i18n_helpers = self.clean_helpers(all_helpers)
  24. def get_i18n_helpers(self):
  25. return self.i18n_helpers
  26. def clean_expression(self, unparsed):
  27. cleaned = u''
  28. for piece in smart_split(unparsed):
  29. if not cleaned and piece in HBS_HELPERS:
  30. continue
  31. if not piece.startswith('=') and not cleaned.endswith('='):
  32. cleaned += ' '
  33. cleaned += piece
  34. return cleaned.strip()
  35. def parse_expression(self, unparsed, cleaned):
  36. helper = []
  37. helpers = [helper]
  38. stack = [helper]
  39. for piece in smart_split(cleaned):
  40. if piece.endswith(')'):
  41. stack[-1].append(piece.rstrip(')').strip())
  42. while piece.endswith(')'):
  43. piece = piece[:-1].strip()
  44. stack.pop()
  45. continue
  46. if not piece.startswith(('\'', '"')):
  47. if piece.startswith('('):
  48. piece = piece[1:].strip()
  49. if piece.startswith('('):
  50. continue
  51. else:
  52. helper = [piece]
  53. helpers.append(helper)
  54. stack.append(helper)
  55. else:
  56. is_kwarg = re.match(r'^[_a-zA-Z]+([_a-zA-Z0-9]+?)=', piece)
  57. if is_kwarg and not piece.endswith('='):
  58. piece = piece[len(is_kwarg.group(0)):]
  59. if piece.startswith('('):
  60. helper = [piece[1:].strip()]
  61. helpers.append(helper)
  62. stack.append(helper)
  63. else:
  64. stack[-1].append(piece)
  65. else:
  66. stack[-1].append(piece)
  67. return helpers
  68. def clean_helpers(self, all_helpers):
  69. i18n_helpers = []
  70. for helper in all_helpers:
  71. i18n_helper_len = I18N_HELPERS.get(helper[0])
  72. if i18n_helper_len and len(helper) >= i18n_helper_len:
  73. i18n_helpers.append(helper[:i18n_helper_len])
  74. return i18n_helpers
  75. class HandlebarsTemplate(object):
  76. def __init__(self, content):
  77. self.expressions = {}
  78. self.content = content
  79. def get_converted_content(self):
  80. stripped_content = self.strip_expressions(self.content)
  81. stripped_content = self.strip_non_expressions(stripped_content)
  82. replaced_content = self.replace_expressions(stripped_content)
  83. return replaced_content
  84. def strip_expressions(self, content):
  85. def replace_expression(matchobj):
  86. trimmed_expression = matchobj.group(0).lstrip('{').rstrip('}')
  87. parsed_expression = HandlebarsExpression(trimmed_expression)
  88. expression_i18n_helpers = parsed_expression.get_i18n_helpers()
  89. if expression_i18n_helpers:
  90. self.expressions[matchobj.group(0)] = expression_i18n_helpers
  91. return matchobj.group(0)
  92. else:
  93. return ''
  94. return HBS_EXPRESSION.sub(replace_expression, self.content)
  95. def strip_non_expressions(self, content):
  96. stripped = u''
  97. cursor = 0
  98. for expression in HBS_EXPRESSION.finditer(content):
  99. position = content.find(expression.group(0), cursor)
  100. content_slice = content[cursor:position]
  101. if content_slice:
  102. slice_lines = len(content_slice.splitlines())
  103. if slice_lines:
  104. stripped += '\n' * (slice_lines - 1)
  105. stripped += expression.group(0)
  106. cursor = position + len(expression.group(0))
  107. return stripped
  108. def replace_expressions(self, content):
  109. def replace_expression(matchobj):
  110. js_functions = []
  111. for helper in self.expressions.get(matchobj.group(0)):
  112. function, args = helper[0], helper[1:]
  113. js_functions.append('%s(%s);' % (function, ', '.join(args)))
  114. return ' '.join(js_functions)
  115. return HBS_EXPRESSION.sub(replace_expression, content)
  116. class HandlebarsFile(object):
  117. def __init__(self, hbs_path, make_js_file=True):
  118. self.hbs_path = hbs_path
  119. self.path_suffix = self.make_js_path_suffix(hbs_path)
  120. self.js_path = self.make_js_path(hbs_path, self.path_suffix)
  121. if make_js_file:
  122. self.make_js_file(self.hbs_path, self.js_path)
  123. def make_js_path_suffix(self, hbs_path):
  124. return '%s.makemessages.js' % md5(hbs_path).hexdigest()[:8]
  125. def make_js_path(self, hbs_path, path_suffix):
  126. return Path('%s.%s' % (unicode(hbs_path), path_suffix))
  127. def make_js_file(self, hbs_path, js_path):
  128. file_content = u''
  129. with codecs.open(hbs_path, encoding='utf-8', mode="r") as hbs_file:
  130. file_content = hbs_file.read()
  131. js_file = codecs.open(js_path, encoding='utf-8', mode='w')
  132. js_file.write(HandlebarsTemplate(file_content).get_converted_content())
  133. js_file.close()
  134. def delete(self):
  135. if self.js_path.exists() and self.js_path.isfile():
  136. self.js_path.unlink()
  137. class Command(BaseCommand):
  138. help = ("Runs over the entire source tree of the current directory and "
  139. "pulls out all strings marked for translation. It creates (or updates) a message "
  140. "file in the conf/locale (in the django tree) or locale (for projects and "
  141. "applications) directory.\n\nIf command is executed for JavaScript files, it "
  142. "also pulls strings from Misago Handlebars.js files.\n\nYou must run this "
  143. "command with one of either the --locale, --exclude or --all options.")
  144. JS_TEMPLATES = ('.hbs', '.handlebars')
  145. def handle(self, *args, **options):
  146. locales = options.get('locale')
  147. self.domain = options.get('domain')
  148. subdirs = [unicode(d.basename()) for d in Path(os.getcwd()).dirs()]
  149. use_subroutines = 'locale' in subdirs and self.domain == 'djangojs'
  150. tmp_js_files = []
  151. if use_subroutines:
  152. # fake js files from templates
  153. tmp_js_files = self.prepare_tmp_js_files();
  154. super(Command, self).handle(*args, **options)
  155. if use_subroutines:
  156. # cleanup everything
  157. self.cleanup_tmp_js_templates(tmp_js_files);
  158. self.cleanup_po_files(locales, tmp_js_files);
  159. def prepare_tmp_js_files(self):
  160. files = []
  161. for hbs_file in Path(os.getcwd()).walkfiles('*.hbs'):
  162. files.append(HandlebarsFile(hbs_file))
  163. return files
  164. def cleanup_po_files(self, locales, tmp_js_files):
  165. strip_tokens = [js_file.path_suffix for js_file in tmp_js_files]
  166. for po_file in Path(os.getcwd()).walkfiles('djangojs.po'):
  167. if not locales or po_file.splitall()[-3] in locales:
  168. self.cleanup_po_file(po_file, strip_tokens)
  169. def cleanup_po_file(self, po_path, strip_tokens):
  170. file_content = u''
  171. with codecs.open(po_path, encoding='utf-8', mode="r") as po_file:
  172. file_content = po_file.read()
  173. for token in strip_tokens:
  174. file_content = file_content.replace(token, '')
  175. po_file = codecs.open(po_path, encoding='utf-8', mode='w')
  176. po_file.write(file_content)
  177. po_file.close()
  178. def cleanup_tmp_js_templates(self, tmp_js_files):
  179. for js_file in tmp_js_files:
  180. js_file.delete()