Edgewall Software

Changeset 376

Show
Ignore:
Timestamp:
06/12/08 18:26:52 (13 months ago)
Author:
aronacher
Message:

Added JavaScript extractor

Location:
trunk
Files:
1 added
4 modified

Legend:

Unmodified
Added
Removed
  • trunk/babel/messages/extract.py

    r375 r376  
    429429        elif tok == NAME and value in keywords: 
    430430            funcname = value 
     431 
     432def extract_javascript(fileobj, keywords, comment_tags, options): 
     433    """Extract messages from JavaScript source code. 
     434 
     435    :param fileobj: the seekable, file-like object the messages should be 
     436                    extracted from 
     437    :param keywords: a list of keywords (i.e. function names) that should be 
     438                     recognized as translation functions 
     439    :param comment_tags: a list of translator tags to search for and include 
     440                         in the results 
     441    :param options: a dictionary of additional options (optional) 
     442    :return: an iterator over ``(lineno, funcname, message, comments)`` tuples 
     443    :rtype: ``iterator`` 
     444    """ 
     445    from babel.messages.jslexer import tokenize, unquote_string 
     446    funcname = message_lineno = None 
     447    messages = [] 
     448    last_argument = None 
     449    translator_comments = [] 
     450    encoding = options.get('encoding', 'utf-8') 
     451    last_token = None 
     452    call_stack = -1 
     453 
     454    for token in tokenize(fileobj.read().decode(encoding)): 
     455        if token.type == 'operator' and token.value == '(': 
     456            if funcname: 
     457                message_lineno = token.lineno 
     458                call_stack += 1 
     459 
     460        elif call_stack == -1 and token.type == 'linecomment': 
     461            value = token.value[2:].strip() 
     462            if translator_comments and \ 
     463               translator_comments[-1][0] == token.lineno - 1: 
     464                translator_comments.append((token.lineno, value)) 
     465                continue 
     466 
     467            for comment_tag in comment_tags: 
     468                if value.startswith(comment_tag): 
     469                    translator_comments.append((token.lineno, value.strip())) 
     470                    break 
     471 
     472        elif token.type == 'multilinecomment': 
     473            # only one multi-line comment may preceed a translation 
     474            translator_comments = [] 
     475            value = token.value[2:-2].strip() 
     476            for comment_tag in comment_tags: 
     477                if value.startswith(comment_tag): 
     478                    lines = value.splitlines() 
     479                    if lines: 
     480                        lines[0] = lines[0].strip() 
     481                        lines[1:] = dedent('\n'.join(lines[1:])).splitlines() 
     482                        for offset, line in enumerate(lines): 
     483                            translator_comments.append((token.lineno + offset, 
     484                                                        line)) 
     485                    break 
     486 
     487        elif funcname and call_stack == 0: 
     488            if token.type == 'operator' and token.value == ')': 
     489                if last_argument is not None: 
     490                    messages.append(last_argument) 
     491                if len(messages) > 1: 
     492                    messages = tuple(messages) 
     493                elif messages: 
     494                    messages = messages[0] 
     495                else: 
     496                    messages = None 
     497 
     498                # Comments don't apply unless they immediately preceed the 
     499                # message 
     500                if translator_comments and \ 
     501                   translator_comments[-1][0] < message_lineno - 1: 
     502                    translator_comments = [] 
     503 
     504                if messages is not None: 
     505                    yield (message_lineno, funcname, messages, 
     506                           [comment[1] for comment in translator_comments]) 
     507 
     508                funcname = message_lineno = last_argument = None 
     509                translator_comments = [] 
     510                messages = [] 
     511                call_stack = -1 
     512 
     513            elif token.type == 'string': 
     514                last_argument = unquote_string(token.value) 
     515 
     516            elif token.type == 'operator' and token.value == ',': 
     517                if last_argument is not None: 
     518                    messages.append(last_argument) 
     519                    last_argument = None 
     520                else: 
     521                    messages.append(None) 
     522 
     523        elif call_stack > 0 and token.type == 'operator' \ 
     524             and token.value == ')': 
     525            call_stack -= 1 
     526 
     527        elif funcname and call_stack == -1: 
     528            funcname = None 
     529 
     530        elif call_stack == -1 and token.type == 'name' and \ 
     531             token.value in keywords and \ 
     532             (last_token is None or last_token.type != 'name' or 
     533              last_token.value != 'function'): 
     534            funcname = token.value 
     535 
     536        last_token = token 
  • trunk/babel/messages/tests/extract.py

    r375 r376  
    322322                          u'a prefix too'], messages[1][2]) 
    323323 
     324class ExtractJavaScriptTestCase(unittest.TestCase): 
     325 
     326    def test_simple_extract(self): 
     327        buf = StringIO("""\ 
     328msg1 = _('simple') 
     329msg2 = gettext('simple') 
     330msg3 = ngettext('s', 'p', 42) 
     331        """) 
     332        messages = \ 
     333            list(extract.extract('javascript', buf, extract.DEFAULT_KEYWORDS, 
     334                                 [], {})) 
     335 
     336        self.assertEqual([(1, 'simple', []), 
     337                          (2, 'simple', []), 
     338                          (3, ('s', 'p'), [])], messages) 
     339 
     340    def test_various_calls(self): 
     341        buf = StringIO("""\ 
     342msg1 = _(i18n_arg.replace(/"/, '"')) 
     343msg2 = ungettext(i18n_arg.replace(/"/, '"'), multi_arg.replace(/"/, '"'), 2) 
     344msg3 = ungettext("Babel", multi_arg.replace(/"/, '"'), 2) 
     345msg4 = ungettext(i18n_arg.replace(/"/, '"'), "Babels", 2) 
     346msg5 = ungettext('bunny', 'bunnies', parseInt(Math.random() * 2 + 1)) 
     347msg6 = ungettext(arg0, 'bunnies', rparseInt(Math.random() * 2 + 1)) 
     348msg7 = _(hello.there) 
     349msg8 = gettext('Rabbit') 
     350msg9 = dgettext('wiki', model.addPage()) 
     351msg10 = dngettext(domain, 'Page', 'Pages', 3) 
     352""") 
     353        messages = \ 
     354            list(extract.extract('javascript', buf, extract.DEFAULT_KEYWORDS, [], 
     355                                 {})) 
     356        self.assertEqual([(5, (u'bunny', u'bunnies'), []), 
     357                          (8, u'Rabbit', []), 
     358                          (10, (u'Page', u'Pages'), [])], messages) 
     359 
     360    def test_message_with_line_comment(self): 
     361        buf = StringIO("""\ 
     362// NOTE: hello 
     363msg = _('Bonjour à tous') 
     364""") 
     365        messages = list(extract.extract_javascript(buf, ('_',), ['NOTE:'], {})) 
     366        self.assertEqual(u'Bonjour à tous', messages[0][2]) 
     367        self.assertEqual([u'NOTE: hello'], messages[0][3]) 
     368 
     369    def test_message_with_multiline_comment(self): 
     370        buf = StringIO("""\ 
     371/* NOTE: hello 
     372   and bonjour 
     373     and servus */ 
     374msg = _('Bonjour à tous') 
     375""") 
     376        messages = list(extract.extract_javascript(buf, ('_',), ['NOTE:'], {})) 
     377        self.assertEqual(u'Bonjour à tous', messages[0][2]) 
     378        self.assertEqual([u'NOTE: hello', 'and bonjour', '  and servus'], messages[0][3]) 
     379 
     380    def test_ignore_function_definitions(self): 
     381        buf = StringIO("""\ 
     382function gettext(value) { 
     383    return translations[language][value] || value; 
     384}""") 
     385 
     386        messages = list(extract.extract_javascript(buf, ('gettext',), [], {})) 
     387        self.assertEqual(messages, []) 
     388 
     389    def test_misplaced_comments(self): 
     390        buf = StringIO("""\ 
     391/* NOTE: this won't show up */ 
     392foo() 
     393 
     394/* NOTE: this will */ 
     395msg = _('Something') 
     396 
     397// NOTE: this will show up 
     398// too. 
     399msg = _('Something else') 
     400 
     401// NOTE: but this won't 
     402bar() 
     403 
     404_('no comment here') 
     405""") 
     406        messages = list(extract.extract_javascript(buf, ('_',), ['NOTE:'], {})) 
     407        self.assertEqual(u'Something', messages[0][2]) 
     408        self.assertEqual([u'NOTE: this will'], messages[0][3]) 
     409        self.assertEqual(u'Something else', messages[1][2]) 
     410        self.assertEqual([u'NOTE: this will show up', 'too.'], messages[1][3]) 
     411        self.assertEqual(u'no comment here', messages[2][2]) 
     412        self.assertEqual([], messages[2][3]) 
     413 
    324414class ExtractTestCase(unittest.TestCase): 
    325415 
     
    383473    suite.addTest(doctest.DocTestSuite(extract)) 
    384474    suite.addTest(unittest.makeSuite(ExtractPythonTestCase)) 
     475    suite.addTest(unittest.makeSuite(ExtractJavaScriptTestCase)) 
    385476    suite.addTest(unittest.makeSuite(ExtractTestCase)) 
    386477    return suite 
  • trunk/ChangeLog

    r375 r376  
    66 * The stripping of the comment tags in comments is optional now and 
    77   is done for each line in a comment. 
    8          
     8 * a JavaScript extractor was added. 
     9 
    910 
    1011Version 0.9.2 
  • trunk/setup.py

    r291 r376  
    7676    ignore = babel.messages.extract:extract_nothing 
    7777    python = babel.messages.extract:extract_python 
     78    javascript = babel.messages.extract:extract_javascript 
    7879    """, 
    7980