Takayuki Shimizukawa avatar Takayuki Shimizukawa committed 5b074b3

Closes #976: Fix gettext does not extract index entries.

Comments (0)

Files changed (12)

 * #869: sphinx-build now has the option :option:`-T` for printing the full
   traceback after an unhandled exception.
 
+* #976: Fix gettext does not extract index entries.
+
 * #940: Fix gettext does not extract figure caption.
 
 * #1067: Improve the ordering of the JavaScript search results: matches in titles

sphinx/builders/gettext.py

 from collections import defaultdict
 
 from sphinx.builders import Builder
-from sphinx.util.nodes import extract_messages
+from sphinx.util import split_index_msg
+from sphinx.util.nodes import extract_messages, traverse_translatable_index
 from sphinx.util.osutil import SEP, safe_relpath, ensuredir, find_catalog
 from sphinx.util.console import darkgreen
+from sphinx.locale import pairindextypes
 
 POHEADER = ur"""
 # SOME DESCRIPTIVE TITLE.
         for node, msg in extract_messages(doctree):
             catalog.add(msg, node)
 
+        # Extract translatable messages from index entries.
+        for node, entries in traverse_translatable_index(doctree):
+            for typ, msg, tid, main in entries:
+                for m in split_index_msg(typ, msg):
+                    if typ == 'pair' and m in pairindextypes.values():
+                        # avoid built-in translated message was incorporated
+                        # in 'sphinx.util.nodes.process_index_entry'
+                        continue
+                    catalog.add(m, node)
+
 
 class MessageCatalogBuilder(I18nBuilder):
     """

sphinx/directives/other.py

         indexnode = addnodes.index()
         indexnode['entries'] = ne = []
         indexnode['inline'] = False
+        set_source_info(self, indexnode)
         for entry in arguments:
             ne.extend(process_index_entry(entry, targetid))
         return [indexnode, targetnode]

sphinx/environment.py

 
 from sphinx import addnodes
 from sphinx.util import url_re, get_matching_docs, docname_join, split_into, \
-     FilenameUniqDict
+     split_index_msg, FilenameUniqDict
 from sphinx.util.nodes import clean_astext, make_refnode, extract_messages, \
-     WarningStream
+     traverse_translatable_index, WarningStream
 from sphinx.util.osutil import movefile, SEP, ustrftime, find_catalog, \
      fs_encoding
 from sphinx.util.matching import compile_matchers
                 child.parent = node
             node.children = patch.children
 
+        # Extract and translate messages for index entries.
+        for node, entries in traverse_translatable_index(self.document):
+            new_entries = []
+            for type, msg, tid, main in entries:
+                msg_parts = split_index_msg(type, msg)
+                msgstr_parts = []
+                for part in msg_parts:
+                    msgstr = catalog.gettext(part)
+                    if not msgstr:
+                        msgstr = part
+                    msgstr_parts.append(msgstr)
+
+                new_entries.append((type, ';'.join(msgstr_parts), tid, main))
+
+            node['raw_entries'] = entries
+            node['entries'] = new_entries
+
 
 class SphinxStandaloneReader(standalone.Reader):
     """
         entries = [('single', target, targetid, main)]
     indexnode = addnodes.index()
     indexnode['entries'] = entries
+    set_role_source_info(inliner, lineno, indexnode)
     textnode = nodes.Text(title, title)
     return [indexnode, targetnode, textnode], []
 

sphinx/util/__init__.py

     return parts
 
 
+def split_index_msg(type, value):
+    # new entry types must be listed in directives/other.py!
+    result = []
+    try:
+        if type == 'single':
+            try:
+                result = split_into(2, 'single', value)
+            except ValueError:
+                result = split_into(1, 'single', value)
+        elif type == 'pair':
+            result = split_into(2, 'pair', value)
+        elif type == 'triple':
+            result = split_into(3, 'triple', value)
+        elif type == 'see':
+            result = split_into(2, 'see', value)
+        elif type == 'seealso':
+            result = split_into(2, 'see', value)
+    except ValueError:
+        pass
+
+    return result
+
+
 def format_exception_cut_frames(x=1):
     """Format an exception with traceback, but only the last x frames."""
     typ, val, tb = sys.exc_info()

sphinx/util/nodes.py

             yield node, msg
 
 
+def traverse_translatable_index(doctree):
+    """Traverse translatable index node from a document tree."""
+    def is_block_index(node):
+        return isinstance(node, addnodes.index) and  \
+            node.get('inline') == False
+    for node in doctree.traverse(is_block_index):
+        if 'raw_entries' in node:
+            entries = node['raw_entries']
+        else:
+            entries = node['entries']
+        yield node, entries
+
+
 def nested_parse_with_titles(state, content, node):
     """Version of state.nested_parse() that allows titles and does not require
     titles to have the same decoration as the calling document.

tests/root/i18n/index.txt

    literalblock
    definition_terms
    figure_caption
+   index_entries

tests/root/i18n/index_entries.po

+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2013, foo
+# This file is distributed under the same license as the foo package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: foo foo\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2013-01-05 18:10\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+msgid "i18n with index entries"
+msgstr ""
+
+msgid "index target section"
+msgstr ""
+
+msgid "this is :index:`Newsletter` target paragraph."
+msgstr "THIS IS :index:`NEWSLETTER` TARGET PARAGRAPH."
+
+msgid "various index entries"
+msgstr ""
+
+msgid "That's all."
+msgstr ""
+
+msgid "Mailing List"
+msgstr "MAILING LIST"
+
+msgid "Newsletter"
+msgstr "NEWSLETTER"
+
+msgid "Recipients List"
+msgstr "RECIPIENTS LIST"
+
+msgid "First"
+msgstr "FIRST"
+
+msgid "Second"
+msgstr "SECOND"
+
+msgid "Third"
+msgstr "THIRD"
+
+msgid "Entry"
+msgstr "ENTRY"
+
+msgid "See"
+msgstr "SEE"
+
+msgid "Module"
+msgstr "MODULE"
+
+msgid "Keyword"
+msgstr "KEYWORD"
+
+msgid "Operator"
+msgstr "OPERATOR"
+
+msgid "Object"
+msgstr "OBJECT"
+
+msgid "Exception"
+msgstr "EXCEPTION"
+
+msgid "Statement"
+msgstr "STATEMENT"
+
+msgid "Builtin"
+msgstr "BUILTIN"

tests/root/i18n/index_entries.txt

+:tocdepth: 2
+
+i18n with index entries
+=======================
+
+.. index::
+   single: Mailing List
+   pair: Newsletter; Recipients List
+
+index target section
+--------------------
+
+this is :index:`Newsletter` target paragraph.
+
+
+various index entries
+---------------------
+
+.. index::
+   triple: First; Second; Third
+   see: Entry; Mailing List
+   seealso: See; Newsletter
+   module: Module
+   keyword: Keyword
+   operator: Operator
+   object: Object
+   exception: Exception
+   statement: Statement
+   builtin: Builtin
+
+That's all.

tests/test_build_gettext.py

 
 import gettext
 import os
+import re
 from subprocess import Popen, PIPE
 
 from util import *
 
     _ = gettext.translation('test_root', app.outdir, languages=['en']).gettext
     assert _("Testing various markup") == u"Testing various markup"
+
+
+@with_app(buildername='gettext',
+          confoverrides={'gettext_compact': False})
+def test_gettext_index_entries(app):
+    # regression test for #976
+    app.builder.build(['i18n/index_entries'])
+
+    _msgid_getter = re.compile(r'msgid "(.*)"').search
+    def msgid_getter(msgid):
+        m = _msgid_getter(msgid)
+        if m:
+            return m.groups()[0]
+        return None
+
+    pot = (app.outdir / 'i18n' / 'index_entries.pot').text(encoding='utf-8')
+    msgids = filter(None, map(msgid_getter, pot.splitlines()))
+
+    expected_msgids = [
+        "i18n with index entries",
+        "index target section",
+        "this is :index:`Newsletter` target paragraph.",
+        "various index entries",
+        "That's all.",
+        "Mailing List",
+        "Newsletter",
+        "Recipients List",
+        "First",
+        "Second",
+        "Third",
+        "Entry",
+        "See",
+        "Module",
+        "Keyword",
+        "Operator",
+        "Object",
+        "Exception",
+        "Statement",
+        "Builtin",
+    ]
+    for expect in expected_msgids:
+        assert expect in msgids
+        msgids.remove(expect)
+
+    # unexpected msgid existent
+    assert msgids == []

tests/test_intl.py

               u"\n   MY DESCRIPTION PARAGRAPH2 OF THE FIGURE.\n")
 
     assert result == expect
+
+
+@with_app(buildername='html',
+          confoverrides={'language': 'xx', 'locale_dirs': ['.'],
+                         'gettext_compact': False})
+def test_i18n_index_entries(app):
+    # regression test for #976
+    app.builder.build(['i18n/index_entries'])
+    result = (app.outdir / 'genindex.html').text(encoding='utf-8')
+
+    def wrap(tag, keyword):
+        start_tag = "<%s[^>]*>" % tag
+        end_tag = "</%s>" % tag
+        return r"%s\s*%s\s*%s" % (start_tag, keyword, end_tag)
+
+    expected_exprs = [
+        wrap('a', 'NEWSLETTER'),
+        wrap('a', 'MAILING LIST'),
+        wrap('a', 'RECIPIENTS LIST'),
+        wrap('a', 'FIRST SECOND'),
+        wrap('a', 'SECOND THIRD'),
+        wrap('a', 'THIRD, FIRST'),
+        wrap('dt', 'ENTRY'),
+        wrap('dt', 'SEE'),
+        wrap('a', 'MODULE'),
+        wrap('a', 'KEYWORD'),
+        wrap('a', 'OPERATOR'),
+        wrap('a', 'OBJECT'),
+        wrap('a', 'EXCEPTION'),
+        wrap('a', 'STATEMENT'),
+        wrap('a', 'BUILTIN'),
+    ]
+    for expr in expected_exprs:
+        assert re.search(expr, result, re.M)
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.