Fix #1921: Support figure substitutions by locale

This commit is contained in:
Takeshi KOMIYA 2016-01-27 01:36:43 +09:00
parent 46138ca605
commit 1f5aa28db0
13 changed files with 325 additions and 27 deletions

View File

@ -106,6 +106,7 @@ Features added
* #2320: classifier of glossary terms can be used for index entries grouping key. * #2320: classifier of glossary terms can be used for index entries grouping key.
The classifier also be used for translation. See also :ref:`glossary-directive`. The classifier also be used for translation. See also :ref:`glossary-directive`.
* Select an image by similarity if multiple images are globbed by ``.. image:: filename.*`` * Select an image by similarity if multiple images are globbed by ``.. image:: filename.*``
* #1921: Support figure substitutions by :confval:`language`
Bugs fixed Bugs fixed
---------- ----------

View File

@ -417,12 +417,18 @@ documentation on :ref:`intl` for details.
The code for the language the docs are written in. Any text automatically The code for the language the docs are written in. Any text automatically
generated by Sphinx will be in that language. Also, Sphinx will try to generated by Sphinx will be in that language. Also, Sphinx will try to
substitute individual paragraphs from your documents with the translation substitute individual paragraphs from your documents with the translation
sets obtained from :confval:`locale_dirs`. In the LaTeX builder, a suitable sets obtained from :confval:`locale_dirs`. Sphinx will search
language will be selected as an option for the *Babel* package. Default is language-specific figures named by `figure_language_filename` and substitute
``None``, which means that no translation will be done. them for original figures. In the LaTeX builder, a suitable language will
be selected as an option for the *Babel* package. Default is ``None``,
which means that no translation will be done.
.. versionadded:: 0.5 .. versionadded:: 0.5
.. versionchanged:: 1.4
Support figure substitution
Currently supported languages by Sphinx are: Currently supported languages by Sphinx are:
* ``bn`` -- Bengali * ``bn`` -- Bengali

View File

@ -39,8 +39,9 @@ from sphinx.util import url_re, get_matching_docs, docname_join, split_into, \
FilenameUniqDict, split_index_msg FilenameUniqDict, split_index_msg
from sphinx.util.nodes import clean_astext, make_refnode, WarningStream, is_translatable from sphinx.util.nodes import clean_astext, make_refnode, WarningStream, is_translatable
from sphinx.util.osutil import SEP, getcwd, fs_encoding, ensuredir from sphinx.util.osutil import SEP, getcwd, fs_encoding, ensuredir
from sphinx.util.i18n import find_catalog_files
from sphinx.util.images import guess_mimetype from sphinx.util.images import guess_mimetype
from sphinx.util.i18n import find_catalog_files, get_image_filename_for_language, \
search_image_for_language
from sphinx.util.console import bold, purple from sphinx.util.console import bold, purple
from sphinx.util.matching import compile_matchers from sphinx.util.matching import compile_matchers
from sphinx.util.parallel import ParallelTasks, parallel_available, make_chunks from sphinx.util.parallel import ParallelTasks, parallel_available, make_chunks
@ -884,6 +885,21 @@ class BuildEnvironment:
def process_images(self, docname, doctree): def process_images(self, docname, doctree):
"""Process and rewrite image URIs.""" """Process and rewrite image URIs."""
def collect_candidates(imgpath, candidates):
globbed = {}
for filename in glob(imgpath):
new_imgpath = relative_path(path.join(self.srcdir, 'dummy'),
filename)
try:
mimetype = guess_mimetype(filename)
if mimetype not in candidates:
globbed.setdefault(mimetype, []).append(new_imgpath)
except (OSError, IOError) as err:
self.warn_node('image file %s not readable: %s' %
(filename, err), node)
for key, files in iteritems(globbed):
candidates[key] = sorted(files, key=len)[0] # select by similarity
for node in doctree.traverse(nodes.image): for node in doctree.traverse(nodes.image):
# Map the mimetype to the corresponding image. The writer may # Map the mimetype to the corresponding image. The writer may
# choose the best image from these candidates. The special key * is # choose the best image from these candidates. The special key * is
@ -896,21 +912,23 @@ class BuildEnvironment:
candidates['?'] = imguri candidates['?'] = imguri
continue continue
rel_imgpath, full_imgpath = self.relfn2path(imguri, docname) rel_imgpath, full_imgpath = self.relfn2path(imguri, docname)
if self.config.language:
# substitute figures (ex. foo.png -> foo.en.png)
i18n_full_imgpath = search_image_for_language(full_imgpath, self)
if i18n_full_imgpath != full_imgpath:
full_imgpath = i18n_full_imgpath
rel_imgpath = relative_path(path.join(self.srcdir, 'dummy'),
i18n_full_imgpath)
# set imgpath as default URI # set imgpath as default URI
node['uri'] = rel_imgpath node['uri'] = rel_imgpath
if rel_imgpath.endswith(os.extsep + '*'): if rel_imgpath.endswith(os.extsep + '*'):
globbed = {} if self.config.language:
for filename in glob(full_imgpath): # Search language-specific figures at first
new_imgpath = relative_path(path.join(self.srcdir, 'dummy'), i18n_imguri = get_image_filename_for_language(imguri, self)
filename) _, full_i18n_imgpath = self.relfn2path(i18n_imguri, docname)
try: collect_candidates(full_i18n_imgpath, candidates)
mimetype = guess_mimetype(filename)
globbed.setdefault(mimetype, []).append(new_imgpath) collect_candidates(full_imgpath, candidates)
except (OSError, IOError) as err:
self.warn_node('image file %s not readable: %s' %
(filename, err), node)
for key, files in iteritems(globbed):
candidates[key] = sorted(files, key=len)[0] # select by similarity
else: else:
candidates['*'] = rel_imgpath candidates['*'] = rel_imgpath

View File

@ -22,6 +22,7 @@ import babel.dates
from babel.messages.pofile import read_po from babel.messages.pofile import read_po
from babel.messages.mofile import write_mo from babel.messages.mofile import write_mo
from sphinx.errors import SphinxError
from sphinx.util.osutil import walk from sphinx.util.osutil import walk
from sphinx.util import SEP from sphinx.util import SEP
@ -190,3 +191,27 @@ def format_date(format, date=None, language=None):
result.append(token) result.append(token)
return "".join(result) return "".join(result)
def get_image_filename_for_language(filename, env):
if not env.config.language:
return filename
root, ext = path.splitext(filename)
try:
return "{root}.{language}{ext}".format(root=root, ext=ext,
language=env.config.language)
except KeyError as exc:
raise SphinxError('Invalid figure_language_filename: %r' % exc)
def search_image_for_language(filename, env):
if not env.config.language:
return filename
translated = get_image_filename_for_language(filename, env)
dirname = path.dirname(env.docname)
if path.exists(path.join(env.srcdir, dirname, translated)):
return translated
else:
return filename

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 B

View File

@ -1,6 +1,8 @@
test-image-glob/subdir test-image-glob/subdir
====================== ======================
.. image:: rimg.png
.. image:: svgimg.* .. image:: svgimg.*
.. figure:: svgimg.* .. figure:: svgimg.*

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 B

View File

@ -0,0 +1,158 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://web.resource.org/cc/"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://inkscape.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
height="60"
width="60"
_SVGFile__filename="oldscale/apps/warning.svg"
version="1.0"
y="0"
x="0"
id="svg1"
sodipodi:version="0.32"
inkscape:version="0.41"
sodipodi:docname="exclamation.svg"
sodipodi:docbase="/home/danny/work/icons/primary/scalable/actions">
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0000000"
inkscape:pageshadow="2"
inkscape:zoom="7.5136000"
inkscape:cx="42.825186"
inkscape:cy="24.316071"
inkscape:window-width="1020"
inkscape:window-height="691"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:current-layer="svg1" />
<defs
id="defs3">
<linearGradient
id="linearGradient1160">
<stop
style="stop-color: #000000;stop-opacity: 1.0;"
id="stop1161"
offset="0" />
<stop
style="stop-color:#ffffff;stop-opacity:1;"
id="stop1162"
offset="1" />
</linearGradient>
<linearGradient
xlink:href="#linearGradient1160"
id="linearGradient1163" />
</defs>
<metadata
id="metadata12">
<RDF
id="RDF13">
<Work
about=""
id="Work14">
<title
id="title15">Part of the Flat Icon Collection (Thu Aug 26 14:31:40 2004)</title>
<description
id="description17" />
<subject
id="subject18">
<Bag
id="Bag19">
<li
id="li20" />
</Bag>
</subject>
<publisher
id="publisher21">
<Agent
about=""
id="Agent22">
<title
id="title23" />
</Agent>
</publisher>
<creator
id="creator24">
<Agent
about=""
id="Agent25">
<title
id="title26">Danny Allen</title>
</Agent>
</creator>
<rights
id="rights28">
<Agent
about=""
id="Agent29">
<title
id="title30">Danny Allen</title>
</Agent>
</rights>
<date
id="date32" />
<format
id="format33">image/svg+xml</format>
<type
id="type35"
resource="http://purl.org/dc/dcmitype/StillImage" />
<license
id="license36"
resource="http://creativecommons.org/licenses/LGPL/2.1/">
<date
id="date37" />
</license>
<language
id="language38">en</language>
</Work>
</RDF>
<rdf:RDF
id="RDF40">
<cc:Work
rdf:about=""
id="Work41">
<dc:format
id="format42">image/svg+xml</dc:format>
<dc:type
id="type44"
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
id="g2099">
<path
style="color:#000000;fill:none;fill-opacity:1.0000000;fill-rule:evenodd;stroke:#ffffff;stroke-width:8.1250000;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.0000000;stroke-dashoffset:0.0000000;stroke-opacity:1.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none"
d="M 55.311891,51.920745 L 4.6880989,51.920744 L 29.999995,8.0792542 L 55.311891,51.920745 z "
id="path1724" />
<path
style="color:#000000;fill:#ffe940;fill-opacity:1.0000000;fill-rule:evenodd;stroke:#000000;stroke-width:3.1250010;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.0000000;stroke-dashoffset:0.0000000;stroke-opacity:1.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none"
d="M 55.311891,51.920745 L 4.6880989,51.920744 L 29.999995,8.0792542 L 55.311891,51.920745 z "
id="path1722" />
<path
style="font-size:12.000000;font-weight:900;fill:none;fill-opacity:1.0000000;stroke:#ffffff;stroke-width:8.1250000;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.0000000;stroke-opacity:1.0000000"
d="M 34.944960,10.779626 L 34.944960,33.186510 C 34.944960,34.752415 34.501979,36.081368 33.616007,37.173380 C 32.750636,38.265402 31.545298,38.811408 29.999995,38.811408 C 28.475302,38.811408 27.269965,38.265402 26.383993,37.173380 C 25.498020,36.060767 25.055030,34.731804 25.055030,33.186510 L 25.055030,10.779626 C 25.055030,9.1931155 25.498020,7.8641562 26.383993,6.7927462 C 27.269965,5.7007332 28.475302,5.1547262 29.999995,5.1547262 C 31.009593,5.1547262 31.885265,5.4019740 32.627010,5.8964706 C 33.389356,6.3909681 33.966274,7.0709005 34.357752,7.9362696 C 34.749221,8.7810349 34.944960,9.7288200 34.944960,10.779626 z "
id="path1099" />
<path
style="font-size:12.000000;font-weight:900;fill:#e71c02;fill-opacity:1.0000000;stroke:none;stroke-width:3.1249981;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1.0000000"
d="M 29.999995,3.5986440 C 28.102272,3.5986440 26.318514,4.3848272 25.156245,5.8173940 C 24.028906,7.1806889 23.499995,8.9087770 23.499995,10.786144 L 23.499995,33.192394 C 23.499995,35.036302 24.050685,36.772771 25.156245,38.161144 C 26.318514,39.593721 28.102273,40.379893 29.999995,40.379894 C 31.913354,40.379894 33.697195,39.576736 34.843745,38.129894 C 35.959941,36.754118 36.499995,35.052976 36.499995,33.192394 L 36.499995,10.786144 C 36.499995,9.5413010 36.276626,8.3551469 35.781245,7.2861440 C 35.278844,6.1755772 34.477762,5.2531440 33.468745,4.5986440 C 32.454761,3.9226545 31.264694,3.5986439 29.999995,3.5986440 z "
id="path835"
sodipodi:nodetypes="cccccccccccc" />
<path
style="color:#000000;fill:none;fill-opacity:1.0000000;fill-rule:evenodd;stroke:#ffffff;stroke-width:5.0000000;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.0000000;stroke-dashoffset:0.0000000;stroke-opacity:1.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none"
d="M 36.506243,49.901522 C 36.506243,53.492972 33.591442,56.407773 29.999991,56.407773 C 26.408541,56.407773 23.493739,53.492972 23.493739,49.901522 C 23.493739,46.310071 26.408541,43.395270 29.999991,43.395270 C 33.591442,43.395270 36.506243,46.310071 36.506243,49.901522 z "
id="path1727" />
<path
style="color:#000000;fill:#e71c02;fill-opacity:1.0000000;fill-rule:evenodd;stroke:none;stroke-width:3.1250000;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.0000000;stroke-dashoffset:0.0000000;stroke-opacity:1.0000000;marker:none;marker-start:none;marker-mid:none;marker-end:none"
d="M 36.506243,49.901522 C 36.506243,53.492972 33.591442,56.407773 29.999991,56.407773 C 26.408541,56.407773 23.493739,53.492972 23.493739,49.901522 C 23.493739,46.310071 26.408541,43.395270 29.999991,43.395270 C 33.591442,43.395270 36.506243,46.310071 36.506243,49.901522 z "
id="path1725" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

@ -112,7 +112,7 @@ def test_numbered_circular_toctree(app, status, warning):
'contents <- sub <- contents') in warnings 'contents <- sub <- contents') in warnings
@with_app(buildername='html', testroot='image-glob') @with_app(buildername='dummy', testroot='image-glob')
def test_image_glob(app, status, warning): def test_image_glob(app, status, warning):
app.builder.build_all() app.builder.build_all()
@ -145,12 +145,16 @@ def test_image_glob(app, status, warning):
doctree = pickle.loads((app.doctreedir / 'subdir/index.doctree').bytes()) doctree = pickle.loads((app.doctreedir / 'subdir/index.doctree').bytes())
assert isinstance(doctree[0][1], nodes.image) assert isinstance(doctree[0][1], nodes.image)
assert doctree[0][1]['candidates'] == {'application/pdf': 'subdir/svgimg.pdf', assert doctree[0][1]['candidates'] == {'*': 'subdir/rimg.png'}
'image/svg+xml': 'subdir/svgimg.svg'} assert doctree[0][1]['uri'] == 'subdir/rimg.png'
assert doctree[0][1]['uri'] == 'subdir/svgimg.*'
assert isinstance(doctree[0][2], nodes.figure) assert isinstance(doctree[0][2], nodes.image)
assert isinstance(doctree[0][2][0], nodes.image) assert doctree[0][2]['candidates'] == {'application/pdf': 'subdir/svgimg.pdf',
assert doctree[0][2][0]['candidates'] == {'application/pdf': 'subdir/svgimg.pdf', 'image/svg+xml': 'subdir/svgimg.svg'}
assert doctree[0][2]['uri'] == 'subdir/svgimg.*'
assert isinstance(doctree[0][3], nodes.figure)
assert isinstance(doctree[0][3][0], nodes.image)
assert doctree[0][3][0]['candidates'] == {'application/pdf': 'subdir/svgimg.pdf',
'image/svg+xml': 'subdir/svgimg.svg'} 'image/svg+xml': 'subdir/svgimg.svg'}
assert doctree[0][2][0]['uri'] == 'subdir/svgimg.*' assert doctree[0][3][0]['uri'] == 'subdir/svgimg.*'

View File

@ -13,6 +13,8 @@ from __future__ import print_function
import os import os
import re import re
import pickle
from docutils import nodes
from subprocess import Popen, PIPE from subprocess import Popen, PIPE
from xml.etree import ElementTree from xml.etree import ElementTree
@ -20,9 +22,9 @@ from babel.messages import pofile
from nose.tools import assert_equal from nose.tools import assert_equal
from six import string_types from six import string_types
from util import tempdir, rootdir, path, gen_with_app, SkipTest, \ from util import tempdir, rootdir, path, gen_with_app, with_app, SkipTest, \
assert_re_search, assert_not_re_search, assert_in, assert_not_in, \ assert_re_search, assert_not_re_search, assert_in, assert_not_in, \
assert_startswith assert_startswith, assert_node
root = tempdir / 'test-intl' root = tempdir / 'test-intl'
@ -794,3 +796,44 @@ def test_references(app, status, warning):
warnings = warning.getvalue().replace(os.sep, '/') warnings = warning.getvalue().replace(os.sep, '/')
warning_expr = u'refs.txt:\\d+: ERROR: Unknown target name:' warning_expr = u'refs.txt:\\d+: ERROR: Unknown target name:'
yield assert_count(warning_expr, warnings, 0) yield assert_count(warning_expr, warnings, 0)
@with_app(buildername='dummy', testroot='image-glob', confoverrides={'language': 'xx'})
def test_image_glob_intl(app, status, warning):
app.builder.build_all()
# index.rst
doctree = pickle.loads((app.doctreedir / 'index.doctree').bytes())
assert_node(doctree[0][1], nodes.image, uri='rimg.xx.png',
candidates={'*': 'rimg.xx.png'})
assert isinstance(doctree[0][2], nodes.figure)
assert_node(doctree[0][2][0], nodes.image, uri='rimg.xx.png',
candidates={'*': 'rimg.xx.png'})
assert_node(doctree[0][3], nodes.image, uri='img.*',
candidates={'application/pdf': 'img.pdf',
'image/gif': 'img.gif',
'image/png': 'img.png'})
assert isinstance(doctree[0][4], nodes.figure)
assert_node(doctree[0][4][0], nodes.image, uri='img.*',
candidates={'application/pdf': 'img.pdf',
'image/gif': 'img.gif',
'image/png': 'img.png'})
# subdir/index.rst
doctree = pickle.loads((app.doctreedir / 'subdir/index.doctree').bytes())
assert_node(doctree[0][1], nodes.image, uri='subdir/rimg.xx.png',
candidates={'*': 'subdir/rimg.xx.png'})
assert_node(doctree[0][2], nodes.image, uri='subdir/svgimg.*',
candidates={'application/pdf': 'subdir/svgimg.pdf',
'image/svg+xml': 'subdir/svgimg.xx.svg'})
assert isinstance(doctree[0][3], nodes.figure)
assert_node(doctree[0][3][0], nodes.image, uri='subdir/svgimg.*',
candidates={'application/pdf': 'subdir/svgimg.pdf',
'image/svg+xml': 'subdir/svgimg.xx.svg'})

View File

@ -16,8 +16,9 @@ from os import path
from babel.messages.mofile import read_mo from babel.messages.mofile import read_mo
from sphinx.util import i18n from sphinx.util import i18n
from sphinx.errors import SphinxError
from util import with_tempdir from util import TestApp, with_tempdir, raises
def test_catalog_info_for_file_and_path(): def test_catalog_info_for_file_and_path():
@ -183,3 +184,33 @@ def test_format_date():
assert i18n.format_date(format, date=date, language='en') == 'February 07, 2016' assert i18n.format_date(format, date=date, language='en') == 'February 07, 2016'
assert i18n.format_date(format, date=date, language='ja') == u'2月 07, 2016' assert i18n.format_date(format, date=date, language='ja') == u'2月 07, 2016'
assert i18n.format_date(format, date=date, language='de') == 'Februar 07, 2016' assert i18n.format_date(format, date=date, language='de') == 'Februar 07, 2016'
def test_get_filename_for_language():
app = TestApp()
# language is None
app.env.config.language = None
assert app.env.config.language is None
assert i18n.get_image_filename_for_language('foo.png', app.env) == 'foo.png'
assert i18n.get_image_filename_for_language('foo.bar.png', app.env) == 'foo.bar.png'
assert i18n.get_image_filename_for_language('subdir/foo.png', app.env) == 'subdir/foo.png'
assert i18n.get_image_filename_for_language('../foo.png', app.env) == '../foo.png'
assert i18n.get_image_filename_for_language('foo', app.env) == 'foo'
# language is en
app.env.config.language = 'en'
assert i18n.get_image_filename_for_language('foo.png', app.env) == 'foo.en.png'
assert i18n.get_image_filename_for_language('foo.bar.png', app.env) == 'foo.bar.en.png'
assert i18n.get_image_filename_for_language('dir/foo.png', app.env) == 'dir/foo.en.png'
assert i18n.get_image_filename_for_language('../foo.png', app.env) == '../foo.en.png'
assert i18n.get_image_filename_for_language('foo', app.env) == 'foo.en'
# modify figure_language_filename and language is None
app.env.config.language = None
app.env.config.figure_language_filename = 'images/{language}/{root}{ext}'
assert i18n.get_image_filename_for_language('foo.png', app.env) == 'foo.png'
assert i18n.get_image_filename_for_language('foo.bar.png', app.env) == 'foo.bar.png'
assert i18n.get_image_filename_for_language('subdir/foo.png', app.env) == 'subdir/foo.png'
assert i18n.get_image_filename_for_language('../foo.png', app.env) == '../foo.png'
assert i18n.get_image_filename_for_language('foo', app.env) == 'foo'

View File

@ -94,6 +94,16 @@ def assert_startswith(thing, prefix):
assert False, '%r does not start with %r' % (thing, prefix) assert False, '%r does not start with %r' % (thing, prefix)
def assert_node(node, cls=None, **kwargs):
if cls:
assert isinstance(node, cls), '%r is not subclass of %r' % (node, cls)
for key, value in kwargs.items():
assert key in node, '%r does not have %r attribute' % (node, key)
assert node[key] == value, \
'%r[%s]: %r does not equals %r' % (node, key, node[key], value)
try: try:
from nose.tools import assert_in, assert_not_in from nose.tools import assert_in, assert_not_in
except ImportError: except ImportError: