diff options
author | Takeshi KOMIYA <i.tkomiya@gmail.com> | 2018-03-01 23:51:23 +0900 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-03-01 23:51:23 +0900 |
commit | 8dbc1b53d1c40fc3b0d90a9b7c0ca55ea28305c8 (patch) | |
tree | 26274fb52ebd6a7ba55a9e3a2628e9239801b253 | |
parent | d48e1ef9e5d2fda22b48e25d5f2289b19b919b31 (diff) | |
parent | 278577c2ef6832dfae96d970ba5703ec98fc60d7 (diff) | |
download | sphinx-git-8dbc1b53d1c40fc3b0d90a9b7c0ca55ea28305c8.tar.gz |
Merge pull request #4674 from tk0miya/improve_i18n
Improve i18n
31 files changed, 360 insertions, 172 deletions
@@ -31,6 +31,7 @@ Deprecated * ``sphinx.cmdline`` is deprecated. Please use ``sphinx.cmd.build`` instead. * All ``env.update()``, ``env._read_serial()`` and ``env._read_parallel()`` are deprecated. Please use ``builder.read()`` instead. +* ``sphinx.locale.l_()`` is deprecated. Please use ``_()`` instead Features added -------------- @@ -41,10 +42,15 @@ Features added * :confval:`source_suffix` allows a mapping fileext to file types * Add :confval:`author` as a configuration value * #2852: imgconverter: Support to convert GIF to PNG +* ``sphinx-build`` command supports i18n console output +* Add ``app.add_message_catalog()`` and ``sphinx.locale.get_translations()`` to + support translation for 3rd party extensions Bugs fixed ---------- +* i18n: message catalogs were reset on each initialization + Testing -------- diff --git a/doc/extdev/appapi.rst b/doc/extdev/appapi.rst index 3143bf4f2..584cf375b 100644 --- a/doc/extdev/appapi.rst +++ b/doc/extdev/appapi.rst @@ -93,6 +93,8 @@ package. .. automethod:: Sphinx.add_html_theme(name, theme_path) +.. automethod:: Sphinx.add_message_catalog(catalog, locale_dir) + .. automethod:: Sphinx.is_parallel_allowed(typ) .. exception:: ExtensionError diff --git a/doc/extdev/i18n.rst b/doc/extdev/i18n.rst new file mode 100644 index 000000000..c8c54da36 --- /dev/null +++ b/doc/extdev/i18n.rst @@ -0,0 +1,17 @@ +.. _i18n-api: + +i18n API +======== + +.. currentmodule:: sphinx.locale + +.. autofunction:: init + +.. autofunction:: init_console + +.. autofunction:: get_translation + +.. autofunction:: _ + +.. autofunction:: __ + diff --git a/doc/extdev/index.rst b/doc/extdev/index.rst index 3380c31ea..e04aa15c0 100644 --- a/doc/extdev/index.rst +++ b/doc/extdev/index.rst @@ -93,3 +93,4 @@ APIs used for writing extensions parserapi nodes logging + i18n diff --git a/sphinx/application.py b/sphinx/application.py index 7ac6b810c..b39996106 100644 --- a/sphinx/application.py +++ b/sphinx/application.py @@ -273,7 +273,7 @@ class Sphinx(object): locale_dirs = [None, path.join(package_dir, 'locale')] + user_locale_dirs # type: ignore # NOQA else: locale_dirs = [] - self.translator, has_translation = locale.init(locale_dirs, self.config.language) + self.translator, has_translation = locale.init(locale_dirs, self.config.language) # type: ignore # NOQA if self.config.language is not None: if has_translation or self.config.language == 'en': # "en" never needs to be translated @@ -1113,6 +1113,19 @@ class Sphinx(object): logger.debug('[app] adding HTML theme: %r, %r', name, theme_path) self.html_themes[name] = theme_path + def add_message_catalog(self, catalog, locale_dir): + # type: (unicode, unicode) -> None + """Register a message catalog. + + The *catalog* is a name of catalog, and *locale_dir* is a base path + of message catalog. For more details, see + :func:`sphinx.locale.get_translation()`. + + .. versionadded:: 1.8 + """ + locale.init([locale_dir], self.config.language, catalog) + locale.init_console(locale_dir, catalog) + # ---- other methods ------------------------------------------------- def is_parallel_allowed(self, typ): # type: (unicode) -> bool diff --git a/sphinx/builders/html.py b/sphinx/builders/html.py index 86dc23540..b8c43d08c 100644 --- a/sphinx/builders/html.py +++ b/sphinx/builders/html.py @@ -37,7 +37,7 @@ from sphinx.environment.adapters.asset import ImageAdapter from sphinx.environment.adapters.indexentries import IndexEntries from sphinx.environment.adapters.toctree import TocTree from sphinx.highlighting import PygmentsBridge -from sphinx.locale import _, l_ +from sphinx.locale import _ from sphinx.search import js_index from sphinx.theming import HTMLThemeFactory from sphinx.util import jsonimpl, logging, status_iterator @@ -446,7 +446,7 @@ class StandaloneHTMLBuilder(Builder): # typically doesn't include the time of day lufmt = self.config.html_last_updated_fmt if lufmt is not None: - self.last_updated = format_date(lufmt or _('%b %d, %Y'), # type: ignore + self.last_updated = format_date(lufmt or _('%b %d, %Y'), language=self.config.language) else: self.last_updated = None @@ -1427,7 +1427,7 @@ def setup(app): app.add_config_value('html_theme_path', [], 'html') app.add_config_value('html_theme_options', {}, 'html') app.add_config_value('html_title', - lambda self: l_('%s %s documentation') % (self.project, self.release), + lambda self: _('%s %s documentation') % (self.project, self.release), 'html', string_classes) app.add_config_value('html_short_title', lambda self: self.html_title, 'html') app.add_config_value('html_style', None, 'html', string_classes) diff --git a/sphinx/cmd/build.py b/sphinx/cmd/build.py index 5851b0958..496adab67 100644 --- a/sphinx/cmd/build.py +++ b/sphinx/cmd/build.py @@ -11,6 +11,7 @@ from __future__ import print_function import argparse +import locale import multiprocessing import os import sys @@ -20,7 +21,8 @@ from typing import TYPE_CHECKING from docutils.utils import SystemMessage from six import text_type, binary_type -from sphinx import __display_version__ +import sphinx.locale +from sphinx import __display_version__, package_dir from sphinx.application import Sphinx from sphinx.errors import SphinxError from sphinx.util import Tee, format_exception_cut_frames, save_traceback @@ -304,6 +306,9 @@ def build_main(argv=sys.argv[1:]): # type: ignore def main(argv=sys.argv[1:]): # type: ignore # type: (List[unicode]) -> int + locale.setlocale(locale.LC_ALL, '') + sphinx.locale.init_console(os.path.join(package_dir, 'locale'), 'sphinx') + if sys.argv[1:2] == ['-M']: return make_main(argv) else: diff --git a/sphinx/cmd/quickstart.py b/sphinx/cmd/quickstart.py index 6a53b40f2..91f7ef350 100644 --- a/sphinx/cmd/quickstart.py +++ b/sphinx/cmd/quickstart.py @@ -12,6 +12,7 @@ from __future__ import absolute_import from __future__ import print_function import argparse +import locale import os import re import sys @@ -36,6 +37,7 @@ from six import PY2, PY3, text_type, binary_type from six.moves import input from six.moves.urllib.parse import quote as urlquote +import sphinx.locale from sphinx import __display_version__, package_dir from sphinx.util import texescape from sphinx.util.console import ( # type: ignore @@ -598,6 +600,9 @@ Makefile to be used with sphinx-build. def main(argv=sys.argv[1:]): # type: (List[str]) -> int + locale.setlocale(locale.LC_ALL, '') + sphinx.locale.init_console(os.path.join(package_dir, 'locale'), 'sphinx') + if not color_terminal(): nocolor() diff --git a/sphinx/cmdline.py b/sphinx/cmdline.py index e05bbec63..3ad16d041 100644 --- a/sphinx/cmdline.py +++ b/sphinx/cmdline.py @@ -8,6 +8,7 @@ :copyright: Copyright 2007-2018 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ +from __future__ import absolute_import from __future__ import print_function import sys diff --git a/sphinx/config.py b/sphinx/config.py index 46e57fd6b..5c7e2bb73 100644 --- a/sphinx/config.py +++ b/sphinx/config.py @@ -18,7 +18,7 @@ from typing import TYPE_CHECKING, Any, NamedTuple, Union from six import PY2, PY3, iteritems, string_types, binary_type, text_type, integer_types from sphinx.errors import ConfigError -from sphinx.locale import l_, __ +from sphinx.locale import _, __ from sphinx.util import logging from sphinx.util.i18n import format_date from sphinx.util.osutil import cd @@ -143,10 +143,10 @@ class Config(object): nitpick_ignore = ([], None), numfig = (False, 'env'), numfig_secnum_depth = (1, 'env'), - numfig_format = ({'section': l_('Section %s'), - 'figure': l_('Fig. %s'), - 'table': l_('Table %s'), - 'code-block': l_('Listing %s')}, + numfig_format = ({'section': _('Section %s'), + 'figure': _('Fig. %s'), + 'table': _('Table %s'), + 'code-block': _('Listing %s')}, 'env'), tls_verify = (True, 'env'), @@ -204,7 +204,7 @@ class Config(object): # type: () -> None # check all values for deviation from the default value's type, since # that can result in TypeErrors all over the place - # NB. since config values might use l_() we have to wait with calling + # NB. since config values might use _() we have to wait with calling # this method until i18n is initialized for name in self._raw_config: if name not in self.values: @@ -214,7 +214,7 @@ class Config(object): permitted = settings[2] if len(settings) == 3 else () if hasattr(default, '__call__'): - default = default(self) # could invoke l_() + default = default(self) # could invoke _() if default is None and not permitted: continue # neither inferrable nor expliclitly permitted types current = self[name] diff --git a/sphinx/domains/c.py b/sphinx/domains/c.py index c06651eb4..709395710 100644 --- a/sphinx/domains/c.py +++ b/sphinx/domains/c.py @@ -18,7 +18,7 @@ from docutils import nodes from sphinx import addnodes from sphinx.directives import ObjectDescription from sphinx.domains import Domain, ObjType -from sphinx.locale import l_, _ +from sphinx.locale import _ from sphinx.roles import XRefRole from sphinx.util.docfields import Field, TypedField from sphinx.util.nodes import make_refnode @@ -62,12 +62,12 @@ class CObject(ObjectDescription): """ doc_field_types = [ - TypedField('parameter', label=l_('Parameters'), + TypedField('parameter', label=_('Parameters'), names=('param', 'parameter', 'arg', 'argument'), typerolename='type', typenames=('type',)), - Field('returnvalue', label=l_('Returns'), has_arg=False, + Field('returnvalue', label=_('Returns'), has_arg=False, names=('returns', 'return')), - Field('returntype', label=l_('Return type'), has_arg=False, + Field('returntype', label=_('Return type'), has_arg=False, names=('rtype',)), ] @@ -254,11 +254,11 @@ class CDomain(Domain): name = 'c' label = 'C' object_types = { - 'function': ObjType(l_('function'), 'func'), - 'member': ObjType(l_('member'), 'member'), - 'macro': ObjType(l_('macro'), 'macro'), - 'type': ObjType(l_('type'), 'type'), - 'var': ObjType(l_('variable'), 'data'), + 'function': ObjType(_('function'), 'func'), + 'member': ObjType(_('member'), 'member'), + 'macro': ObjType(_('macro'), 'macro'), + 'type': ObjType(_('type'), 'type'), + 'var': ObjType(_('variable'), 'data'), } directives = { diff --git a/sphinx/domains/cpp.py b/sphinx/domains/cpp.py index 0bd883a30..dfad97a6c 100644 --- a/sphinx/domains/cpp.py +++ b/sphinx/domains/cpp.py @@ -21,7 +21,7 @@ from sphinx import addnodes from sphinx.directives import ObjectDescription from sphinx.domains import Domain, ObjType from sphinx.environment import NoUri -from sphinx.locale import l_, _ +from sphinx.locale import _ from sphinx.roles import XRefRole from sphinx.util import logging from sphinx.util.docfields import Field, GroupedField @@ -5563,16 +5563,16 @@ class CPPObject(ObjectDescription): """Description of a C++ language object.""" doc_field_types = [ - GroupedField('parameter', label=l_('Parameters'), + GroupedField('parameter', label=_('Parameters'), names=('param', 'parameter', 'arg', 'argument'), can_collapse=True), - GroupedField('template parameter', label=l_('Template Parameters'), + GroupedField('template parameter', label=_('Template Parameters'), names=('tparam', 'template parameter'), can_collapse=True), - GroupedField('exceptions', label=l_('Throws'), rolename='cpp:class', + GroupedField('exceptions', label=_('Throws'), rolename='cpp:class', names=('throws', 'throw', 'exception'), can_collapse=True), - Field('returnvalue', label=l_('Returns'), has_arg=False, + Field('returnvalue', label=_('Returns'), has_arg=False, names=('returns', 'return')), ] @@ -5985,13 +5985,13 @@ class CPPDomain(Domain): name = 'cpp' label = 'C++' object_types = { - 'class': ObjType(l_('class'), 'class', 'type', 'identifier'), - 'function': ObjType(l_('function'), 'function', 'func', 'type', 'identifier'), - 'member': ObjType(l_('member'), 'member', 'var'), - 'type': ObjType(l_('type'), 'type', 'identifier'), - 'concept': ObjType(l_('concept'), 'concept', 'identifier'), - 'enum': ObjType(l_('enum'), 'enum', 'type', 'identifier'), - 'enumerator': ObjType(l_('enumerator'), 'enumerator') + 'class': ObjType(_('class'), 'class', 'type', 'identifier'), + 'function': ObjType(_('function'), 'function', 'func', 'type', 'identifier'), + 'member': ObjType(_('member'), 'member', 'var'), + 'type': ObjType(_('type'), 'type', 'identifier'), + 'concept': ObjType(_('concept'), 'concept', 'identifier'), + 'enum': ObjType(_('enum'), 'enum', 'type', 'identifier'), + 'enumerator': ObjType(_('enumerator'), 'enumerator') } directives = { diff --git a/sphinx/domains/javascript.py b/sphinx/domains/javascript.py index ac68a295b..7f21adf65 100644 --- a/sphinx/domains/javascript.py +++ b/sphinx/domains/javascript.py @@ -18,7 +18,7 @@ from sphinx import addnodes from sphinx.directives import ObjectDescription from sphinx.domains import Domain, ObjType from sphinx.domains.python import _pseudo_parse_arglist -from sphinx.locale import l_, _ +from sphinx.locale import _ from sphinx.roles import XRefRole from sphinx.util.docfields import Field, GroupedField, TypedField from sphinx.util.nodes import make_refnode @@ -202,15 +202,15 @@ class JSCallable(JSObject): has_arguments = True doc_field_types = [ - TypedField('arguments', label=l_('Arguments'), + TypedField('arguments', label=_('Arguments'), names=('argument', 'arg', 'parameter', 'param'), typerolename='func', typenames=('paramtype', 'type')), - GroupedField('errors', label=l_('Throws'), rolename='err', + GroupedField('errors', label=_('Throws'), rolename='err', names=('throws', ), can_collapse=True), - Field('returnvalue', label=l_('Returns'), has_arg=False, + Field('returnvalue', label=_('Returns'), has_arg=False, names=('returns', 'return')), - Field('returntype', label=l_('Return type'), has_arg=False, + Field('returntype', label=_('Return type'), has_arg=False, names=('rtype',)), ] @@ -297,12 +297,12 @@ class JavaScriptDomain(Domain): label = 'JavaScript' # if you add a new object type make sure to edit JSObject.get_index_string object_types = { - 'function': ObjType(l_('function'), 'func'), - 'method': ObjType(l_('method'), 'meth'), - 'class': ObjType(l_('class'), 'class'), - 'data': ObjType(l_('data'), 'data'), - 'attribute': ObjType(l_('attribute'), 'attr'), - 'module': ObjType(l_('module'), 'mod'), + 'function': ObjType(_('function'), 'func'), + 'method': ObjType(_('method'), 'meth'), + 'class': ObjType(_('class'), 'class'), + 'data': ObjType(_('data'), 'data'), + 'attribute': ObjType(_('attribute'), 'attr'), + 'module': ObjType(_('module'), 'mod'), } directives = { 'function': JSCallable, diff --git a/sphinx/domains/python.py b/sphinx/domains/python.py index fb864a7a0..6f3c27c89 100644 --- a/sphinx/domains/python.py +++ b/sphinx/domains/python.py @@ -19,7 +19,7 @@ from six import iteritems from sphinx import addnodes from sphinx.directives import ObjectDescription from sphinx.domains import Domain, ObjType, Index -from sphinx.locale import l_, _ +from sphinx.locale import _ from sphinx.roles import XRefRole from sphinx.util import logging from sphinx.util.docfields import Field, GroupedField, TypedField @@ -173,21 +173,21 @@ class PyObject(ObjectDescription): } doc_field_types = [ - PyTypedField('parameter', label=l_('Parameters'), + PyTypedField('parameter', label=_('Parameters'), names=('param', 'parameter', 'arg', 'argument', 'keyword', 'kwarg', 'kwparam'), typerolename='class', typenames=('paramtype', 'type'), can_collapse=True), - PyTypedField('variable', label=l_('Variables'), rolename='obj', + PyTypedField('variable', label=_('Variables'), rolename='obj', names=('var', 'ivar', 'cvar'), typerolename='class', typenames=('vartype',), can_collapse=True), - PyGroupedField('exceptions', label=l_('Raises'), rolename='exc', + PyGroupedField('exceptions', label=_('Raises'), rolename='exc', names=('raises', 'raise', 'exception', 'except'), can_collapse=True), - Field('returnvalue', label=l_('Returns'), has_arg=False, + Field('returnvalue', label=_('Returns'), has_arg=False, names=('returns', 'return')), - PyField('returntype', label=l_('Return type'), has_arg=False, + PyField('returntype', label=_('Return type'), has_arg=False, names=('rtype',), bodyrolename='class'), ] @@ -631,8 +631,8 @@ class PythonModuleIndex(Index): """ name = 'modindex' - localname = l_('Python Module Index') - shortname = l_('modules') + localname = _('Python Module Index') + shortname = _('modules') def generate(self, docnames=None): # type: (Iterable[unicode]) -> Tuple[List[Tuple[unicode, List[List[Union[unicode, int]]]]], bool] # NOQA @@ -702,15 +702,15 @@ class PythonDomain(Domain): name = 'py' label = 'Python' object_types = { - 'function': ObjType(l_('function'), 'func', 'obj'), - 'data': ObjType(l_('data'), 'data', 'obj'), - 'class': ObjType(l_('class'), 'class', 'exc', 'obj'), - 'exception': ObjType(l_('exception'), 'exc', 'class', 'obj'), - 'method': ObjType(l_('method'), 'meth', 'obj'), - 'classmethod': ObjType(l_('class method'), 'meth', 'obj'), - 'staticmethod': ObjType(l_('static method'), 'meth', 'obj'), - 'attribute': ObjType(l_('attribute'), 'attr', 'obj'), - 'module': ObjType(l_('module'), 'mod', 'obj'), + 'function': ObjType(_('function'), 'func', 'obj'), + 'data': ObjType(_('data'), 'data', 'obj'), + 'class': ObjType(_('class'), 'class', 'exc', 'obj'), + 'exception': ObjType(_('exception'), 'exc', 'class', 'obj'), + 'method': ObjType(_('method'), 'meth', 'obj'), + 'classmethod': ObjType(_('class method'), 'meth', 'obj'), + 'staticmethod': ObjType(_('static method'), 'meth', 'obj'), + 'attribute': ObjType(_('attribute'), 'attr', 'obj'), + 'module': ObjType(_('module'), 'mod', 'obj'), } # type: Dict[unicode, ObjType] directives = { diff --git a/sphinx/domains/rst.py b/sphinx/domains/rst.py index 3c053a9d1..ef3a94d6a 100644 --- a/sphinx/domains/rst.py +++ b/sphinx/domains/rst.py @@ -17,7 +17,7 @@ from six import iteritems from sphinx import addnodes from sphinx.directives import ObjectDescription from sphinx.domains import Domain, ObjType -from sphinx.locale import l_, _ +from sphinx.locale import _ from sphinx.roles import XRefRole from sphinx.util.nodes import make_refnode @@ -116,8 +116,8 @@ class ReSTDomain(Domain): label = 'reStructuredText' object_types = { - 'directive': ObjType(l_('directive'), 'dir'), - 'role': ObjType(l_('role'), 'role'), + 'directive': ObjType(_('directive'), 'dir'), + 'role': ObjType(_('role'), 'role'), } directives = { 'directive': ReSTDirective, diff --git a/sphinx/domains/std.py b/sphinx/domains/std.py index 801f54b14..54d0e5de5 100644 --- a/sphinx/domains/std.py +++ b/sphinx/domains/std.py @@ -22,7 +22,7 @@ from six import iteritems from sphinx import addnodes from sphinx.directives import ObjectDescription from sphinx.domains import Domain, ObjType -from sphinx.locale import l_, _ +from sphinx.locale import _ from sphinx.roles import XRefRole from sphinx.util import ws_re, logging, docname_join from sphinx.util.nodes import clean_astext, make_refnode @@ -81,7 +81,7 @@ class GenericObject(ObjectDescription): class EnvVar(GenericObject): - indextemplate = l_('environment variable; %s') + indextemplate = _('environment variable; %s') class EnvVarXRefRole(XRefRole): @@ -452,13 +452,13 @@ class StandardDomain(Domain): label = 'Default' object_types = { - 'term': ObjType(l_('glossary term'), 'term', searchprio=-1), - 'token': ObjType(l_('grammar token'), 'token', searchprio=-1), - 'label': ObjType(l_('reference label'), 'ref', 'keyword', + 'term': ObjType(_('glossary term'), 'term', searchprio=-1), + 'token': ObjType(_('grammar token'), 'token', searchprio=-1), + 'label': ObjType(_('reference label'), 'ref', 'keyword', searchprio=-1), - 'envvar': ObjType(l_('environment variable'), 'envvar'), - 'cmdoption': ObjType(l_('program option'), 'option'), - 'doc': ObjType(l_('document'), 'doc', searchprio=-1) + 'envvar': ObjType(_('environment variable'), 'envvar'), + 'cmdoption': ObjType(_('program option'), 'option'), + 'doc': ObjType(_('document'), 'doc', searchprio=-1) } # type: Dict[unicode, ObjType] directives = { @@ -495,9 +495,9 @@ class StandardDomain(Domain): 'citations': {}, # citation_name -> docname, labelid, lineno 'citation_refs': {}, # citation_name -> list of docnames 'labels': { # labelname -> docname, labelid, sectionname - 'genindex': ('genindex', '', l_('Index')), - 'modindex': ('py-modindex', '', l_('Module Index')), - 'search': ('search', '', l_('Search Page')), + 'genindex': ('genindex', '', _('Index')), + 'modindex': ('py-modindex', '', _('Module Index')), + 'search': ('search', '', _('Search Page')), }, 'anonlabels': { # labelname -> docname, labelid 'genindex': ('genindex', ''), diff --git a/sphinx/ext/apidoc.py b/sphinx/ext/apidoc.py index 8a93271c1..87f55b7cc 100644 --- a/sphinx/ext/apidoc.py +++ b/sphinx/ext/apidoc.py @@ -19,6 +19,7 @@ from __future__ import print_function import argparse import glob +import locale import os import sys from fnmatch import fnmatch @@ -27,7 +28,8 @@ from typing import TYPE_CHECKING from six import binary_type -from sphinx import __display_version__ +import sphinx.locale +from sphinx import __display_version__, package_dir from sphinx.cmd.quickstart import EXTENSIONS from sphinx.util import rst from sphinx.util.osutil import FileAvoidWrite, ensuredir, walk @@ -382,6 +384,9 @@ Note: By default this script will not overwrite already created files.""") def main(argv=sys.argv[1:]): # type: (List[str]) -> int """Parse and check the command line arguments.""" + locale.setlocale(locale.LC_ALL, '') + sphinx.locale.init_console(os.path.join(package_dir, 'locale'), 'sphinx') + parser = get_parser() args = parser.parse_args(argv) diff --git a/sphinx/ext/autosummary/generate.py b/sphinx/ext/autosummary/generate.py index 9946ab037..d266e675c 100644 --- a/sphinx/ext/autosummary/generate.py +++ b/sphinx/ext/autosummary/generate.py @@ -21,6 +21,7 @@ from __future__ import print_function import argparse import codecs +import locale import os import pydoc import re @@ -30,6 +31,7 @@ from typing import TYPE_CHECKING from jinja2 import FileSystemLoader, TemplateNotFound from jinja2.sandbox import SandboxedEnvironment +import sphinx.locale from sphinx import __display_version__ from sphinx import package_dir from sphinx.ext.autosummary import import_by_name, get_documenter @@ -402,6 +404,9 @@ The format of the autosummary directive is documented in the def main(argv=sys.argv[1:]): # type: (List[str]) -> None + locale.setlocale(locale.LC_ALL, '') + sphinx.locale.init_console(os.path.join(package_dir, 'locale'), 'sphinx') + app = DummyApplication() setup_documenters(app) args = get_parser().parse_args(argv) diff --git a/sphinx/ext/napoleon/__init__.py b/sphinx/ext/napoleon/__init__.py index 28e48d13a..cd7831946 100644 --- a/sphinx/ext/napoleon/__init__.py +++ b/sphinx/ext/napoleon/__init__.py @@ -325,14 +325,13 @@ def _patch_python_domain(): pass else: import sphinx.domains.python - import sphinx.locale - l_ = sphinx.locale.lazy_gettext + from sphinx.locale import _ for doc_field in sphinx.domains.python.PyObject.doc_field_types: if doc_field.name == 'parameter': doc_field.names = ('param', 'parameter', 'arg', 'argument') break sphinx.domains.python.PyObject.doc_field_types.append( - PyTypedField('keyword', label=l_('Keyword Arguments'), + PyTypedField('keyword', label=_('Keyword Arguments'), names=('keyword', 'kwarg', 'kwparam'), typerolename='obj', typenames=('paramtype', 'kwtype'), can_collapse=True)) diff --git a/sphinx/locale/__init__.py b/sphinx/locale/__init__.py index c97624f72..e79db3ec2 100644 --- a/sphinx/locale/__init__.py +++ b/sphinx/locale/__init__.py @@ -10,11 +10,17 @@ """ import gettext +import locale +import warnings +from collections import defaultdict +from gettext import NullTranslations from typing import TYPE_CHECKING -from six import PY3, text_type +from six import text_type from six.moves import UserString +from sphinx.deprecation import RemovedInSphinx30Warning + if TYPE_CHECKING: from typing import Any, Callable, Dict, Iterator, List, Tuple # NOQA @@ -180,6 +186,8 @@ def mygettext(string): """Used instead of _ when creating TranslationProxies, because _ is not bound yet at that time. """ + warnings.warn('sphinx.locale.mygettext() is deprecated. Please use `_()` instead.', + RemovedInSphinx30Warning) return _(string) @@ -188,119 +196,167 @@ def lazy_gettext(string): """A lazy version of `gettext`.""" # if isinstance(string, _TranslationProxy): # return string + warnings.warn('sphinx.locale.laxy_gettext() is deprecated. Please use `_()` instead.', + RemovedInSphinx30Warning) return _TranslationProxy(mygettext, string) # type: ignore -l_ = lazy_gettext - - -admonitionlabels = { - 'attention': l_('Attention'), - 'caution': l_('Caution'), - 'danger': l_('Danger'), - 'error': l_('Error'), - 'hint': l_('Hint'), - 'important': l_('Important'), - 'note': l_('Note'), - 'seealso': l_('See also'), - 'tip': l_('Tip'), - 'warning': l_('Warning'), -} # type: Dict[unicode, unicode] - -versionlabels = { - 'versionadded': l_('New in version %s'), - 'versionchanged': l_('Changed in version %s'), - 'deprecated': l_('Deprecated since version %s'), -} # type: Dict[unicode, unicode] - -# XXX Python specific -pairindextypes = { - 'module': l_('module'), - 'keyword': l_('keyword'), - 'operator': l_('operator'), - 'object': l_('object'), - 'exception': l_('exception'), - 'statement': l_('statement'), - 'builtin': l_('built-in function'), -} # Dict[unicode, _TranslationProxy] +translators = defaultdict(NullTranslations) # type: Dict[Tuple[unicode, unicode], NullTranslations] # NOQA -translators = {} # type: Dict[unicode, Any] -if PY3: - def _(message, *args): - # type: (unicode, *Any) -> unicode - try: - if len(args) <= 1: - return translators['sphinx'].gettext(message) - else: # support pluralization - return translators['sphinx'].ngettext(message, args[0], args[1]) - except KeyError: - return message -else: - def _(message, *args): - # type: (unicode, *Any) -> unicode - try: - if len(args) <= 1: - return translators['sphinx'].ugettext(message) - else: # support pluralization - return translators['sphinx'].ungettext(message, args[0], args[1]) - except KeyError: - return message - - -def __(message, *args): - # type: (unicode, *Any) -> unicode - """A dummy wrapper to i18n'ize exceptions and command line messages. - - In future, the messages are translated using LC_MESSAGES or any other - locale settings. - """ - return message if len(args) <= 1 else args[0] - - -def init(locale_dirs, language, catalog='sphinx'): - # type: (List, unicode, unicode) -> Tuple[Any, bool] +def init(locale_dirs, language, catalog='sphinx', namespace='general'): + # type: (List[unicode], unicode, unicode, unicode) -> Tuple[NullTranslations, bool] """Look for message catalogs in `locale_dirs` and *ensure* that there is at least a NullTranslations catalog set in `translators`. If called multiple times or if several ``.mo`` files are found, their contents are merged together (thus making ``init`` reentrable). """ global translators - translator = translators.get(catalog) + translator = translators.get((namespace, catalog)) # ignore previously failed attempts to find message catalogs - if isinstance(translator, gettext.NullTranslations): + if translator.__class__ is NullTranslations: translator = None # the None entry is the system's default locale path has_translation = True + if language and '_' in language: + # for language having country code (like "de_AT") + languages = [language, language.split('_')[0]] + else: + languages = [language] + # loading for dir_ in locale_dirs: try: trans = gettext.translation(catalog, localedir=dir_, # type: ignore - languages=[language]) # type: ignore + languages=languages) if translator is None: translator = trans else: - translator._catalog.update(trans._catalog) # type: ignore + translator.add_fallback(trans) except Exception: # Language couldn't be found in the specified path pass - # guarantee translators[catalog] exists + # guarantee translators[(namespace, catalog)] exists if translator is None: - translator = gettext.NullTranslations() + translator = NullTranslations() has_translation = False - translators[catalog] = translator + translators[(namespace, catalog)] = translator if hasattr(translator, 'ugettext'): - translator.gettext = translator.ugettext + translator.gettext = translator.ugettext # type: ignore return translator, has_translation -def get_translator(catalog='sphinx'): - # type: (unicode) -> gettext.NullTranslations - global translators - translator = translators.get(catalog) - if translator is None: - translator = gettext.NullTranslations() - if hasattr(translator, 'ugettext'): - translator.gettext = translator.ugettext - return translator +def init_console(locale_dir, catalog): + # type: (unicode, unicode) -> Tuple[NullTranslations, bool] + """Initialize locale for console. + + .. versionadded:: 1.8 + """ + language, _ = locale.getlocale(locale.LC_MESSAGES) # encoding is ignored + return init([locale_dir], language, catalog, 'console') + + +def get_translator(catalog='sphinx', namespace='general'): + # type: (unicode, unicode) -> NullTranslations + return translators[(namespace, catalog)] + + +def is_translator_registered(catalog='sphinx', namespace='general'): + # type: (unicode, unicode) -> bool + return (namespace, catalog) in translators + + +def _lazy_translate(catalog, namespace, message): + # type: (unicode, unicode, unicode) -> unicode + """Used instead of _ when creating TranslationProxy, because _ is + not bound yet at that time. + """ + translator = get_translator(catalog, namespace) + return translator.gettext(message) # type: ignore + + +def get_translation(catalog, namespace='general'): + """Get a translation function based on the *catalog* and *namespace*. + + The extension can use this API to translate the messages on the + extension:: + + import os + from sphinx.locale import get_translation + + _ = get_translation(__name__) + text = _('Hello Sphinx!') + + + def setup(app): + package_dir = path.abspath(path.dirname(__file__)) + locale_dir = os.path.join(package_dir, 'locales') + app.add_message_catalog(__name__, locale_dir) + + With this code, sphinx searches a message catalog from + ``${package_dir}/locales/${language}/LC_MESSAGES/${__name__}.mo`` + The :confval:`language` is used for the searching. + + .. versionadded:: 1.8 + """ + def gettext(message, *args): + # type: (unicode, *Any) -> unicode + if not is_translator_registered(catalog, namespace): + # not initialized yet + return _TranslationProxy(_lazy_translate, catalog, namespace, message) # type: ignore # NOQA + else: + translator = get_translator(catalog, namespace) + if len(args) <= 1: + return translator.gettext(message) # type: ignore + else: # support pluralization + return translator.ngettext(message, args[0], args[1]) # type: ignore + + return gettext + + +# A shortcut for sphinx-core +#: Translation function for messages on documentation (menu, labels, themes and so on). +#: This function follows :confval:`language` setting. +_ = get_translation('sphinx') +#: Translation function for console messages +#: This function follows locale setting (`LC_ALL`, `LC_MESSAGES` and so on). +__ = get_translation('sphinx', 'console') + + +def l_(*args): + warnings.warn('sphinx.locale.l_() is deprecated. Please use `_()` instead.', + RemovedInSphinx30Warning) + return _(*args) + + +# labels +admonitionlabels = { + 'attention': _('Attention'), + 'caution': _('Caution'), + 'danger': _('Danger'), + 'error': _('Error'), + 'hint': _('Hint'), + 'important': _('Important'), + 'note': _('Note'), + 'seealso': _('See also'), + 'tip': _('Tip'), + 'warning': _('Warning'), +} # type: Dict[unicode, unicode] + +versionlabels = { + 'versionadded': _('New in version %s'), + 'versionchanged': _('Changed in version %s'), + 'deprecated': _('Deprecated since version %s'), +} # type: Dict[unicode, unicode] + +# XXX Python specific +pairindextypes = { + 'module': _('module'), + 'keyword': _('keyword'), + 'operator': _('operator'), + 'object': _('object'), + 'exception': _('exception'), + 'statement': _('statement'), + 'builtin': _('built-in function'), +} # Dict[unicode, _TranslationProxy] diff --git a/sphinx/testing/util.py b/sphinx/testing/util.py index 6fbcd26d9..1a9ea3bc9 100644 --- a/sphinx/testing/util.py +++ b/sphinx/testing/util.py @@ -19,7 +19,7 @@ from docutils import nodes from docutils.parsers.rst import directives, roles from six import string_types -from sphinx import application +from sphinx import application, locale from sphinx.builders.latex import LaTeXBuilder from sphinx.ext.autodoc import AutoDirective from sphinx.pycode import ModuleAnalyzer @@ -147,6 +147,7 @@ class SphinxTestApp(application.Sphinx): AutoDirective._registry.clear() ModuleAnalyzer.cache.clear() LaTeXBuilder.usepackages = [] + locale.translators.clear() sys.path[:] = self._saved_path sys.modules.pop('autodoc_fodder', None) directives._directives = self._saved_directives diff --git a/sphinx/transforms/__init__.py b/sphinx/transforms/__init__.py index 24fe8660e..8367dcde5 100644 --- a/sphinx/transforms/__init__.py +++ b/sphinx/transforms/__init__.py @@ -125,7 +125,7 @@ class DefaultSubstitutions(SphinxTransform): text = self.config[refname] if refname == 'today' and not text: # special handling: can also specify a strftime format - text = format_date(self.config.today_fmt or _('%b %d, %Y'), # type: ignore + text = format_date(self.config.today_fmt or _('%b %d, %Y'), language=self.config.language) ref.replace_self(nodes.Text(text, text)) diff --git a/sphinx/transforms/i18n.py b/sphinx/transforms/i18n.py index 63a362ac4..9dddce343 100644 --- a/sphinx/transforms/i18n.py +++ b/sphinx/transforms/i18n.py @@ -86,6 +86,8 @@ class Locale(SphinxTransform): def apply(self): # type: () -> None settings, source = self.document.settings, self.document['source'] + msgstr = u'' + # XXX check if this is reliable assert source.startswith(self.env.srcdir) docname = path.splitext(relative_path(path.join(self.env.srcdir, 'dummy'), @@ -102,7 +104,7 @@ class Locale(SphinxTransform): # phase1: replace reference ids with translated names for node, msg in extract_messages(self.document): - msgstr = catalog.gettext(msg) + msgstr = catalog.gettext(msg) # type: ignore # XXX add marker to untranslated parts if not msgstr or msgstr == msg or not msgstr.strip(): # as-of-yet untranslated @@ -219,7 +221,7 @@ class Locale(SphinxTransform): if node.get('translated', False): # to avoid double translation continue # skip if the node is already translated by phase1 - msgstr = catalog.gettext(msg) + msgstr = catalog.gettext(msg) # type: ignore # XXX add marker to untranslated parts if not msgstr or msgstr == msg: # as-of-yet untranslated continue @@ -438,7 +440,7 @@ class Locale(SphinxTransform): msg_parts = split_index_msg(type, msg) msgstr_parts = [] for part in msg_parts: - msgstr = catalog.gettext(part) + msgstr = catalog.gettext(part) # type: ignore if not msgstr: msgstr = part msgstr_parts.append(msgstr) diff --git a/sphinx/writers/latex.py b/sphinx/writers/latex.py index daa6e1be7..8112a16b1 100644 --- a/sphinx/writers/latex.py +++ b/sphinx/writers/latex.py @@ -580,7 +580,7 @@ class LaTeXTranslator(nodes.NodeVisitor): if builder.config.today: self.elements['date'] = builder.config.today else: - self.elements['date'] = format_date(builder.config.today_fmt or _('%b %d, %Y'), # type: ignore # NOQA + self.elements['date'] = format_date(builder.config.today_fmt or _('%b %d, %Y'), language=builder.config.language) if builder.config.numfig: diff --git a/sphinx/writers/manpage.py b/sphinx/writers/manpage.py index f647a7cf9..df2f74c02 100644 --- a/sphinx/writers/manpage.py +++ b/sphinx/writers/manpage.py @@ -108,7 +108,7 @@ class ManualPageTranslator(BaseTranslator): if builder.config.today: self._docinfo['date'] = builder.config.today else: - self._docinfo['date'] = format_date(builder.config.today_fmt or _('%b %d, %Y'), # type: ignore # NOQA + self._docinfo['date'] = format_date(builder.config.today_fmt or _('%b %d, %Y'), language=builder.config.language) self._docinfo['copyright'] = builder.config.copyright self._docinfo['version'] = builder.config.version diff --git a/sphinx/writers/texinfo.py b/sphinx/writers/texinfo.py index 3767ba3fc..d17566429 100644 --- a/sphinx/writers/texinfo.py +++ b/sphinx/writers/texinfo.py @@ -237,7 +237,7 @@ class TexinfoTranslator(nodes.NodeVisitor): 'project': self.escape(self.builder.config.project), 'copyright': self.escape(self.builder.config.copyright), 'date': self.escape(self.builder.config.today or - format_date(self.builder.config.today_fmt or _('%b %d, %Y'), # type: ignore # NOQA + format_date(self.builder.config.today_fmt or _('%b %d, %Y'), language=self.builder.config.language)) }) # title diff --git a/tests/roots/test-locale/locale1/en/LC_MESSAGES/myext.mo b/tests/roots/test-locale/locale1/en/LC_MESSAGES/myext.mo Binary files differnew file mode 100644 index 000000000..6aa00f77a --- /dev/null +++ b/tests/roots/test-locale/locale1/en/LC_MESSAGES/myext.mo diff --git a/tests/roots/test-locale/locale1/en/LC_MESSAGES/myext.po b/tests/roots/test-locale/locale1/en/LC_MESSAGES/myext.po new file mode 100644 index 000000000..ee1f6c23f --- /dev/null +++ b/tests/roots/test-locale/locale1/en/LC_MESSAGES/myext.po @@ -0,0 +1,2 @@ +msgid "Hello world" +msgstr "HELLO WORLD" diff --git a/tests/roots/test-locale/locale2/en/LC_MESSAGES/myext.mo b/tests/roots/test-locale/locale2/en/LC_MESSAGES/myext.mo Binary files differnew file mode 100644 index 000000000..14c34d0b7 --- /dev/null +++ b/tests/roots/test-locale/locale2/en/LC_MESSAGES/myext.mo diff --git a/tests/roots/test-locale/locale2/en/LC_MESSAGES/myext.po b/tests/roots/test-locale/locale2/en/LC_MESSAGES/myext.po new file mode 100644 index 000000000..d376cf9bd --- /dev/null +++ b/tests/roots/test-locale/locale2/en/LC_MESSAGES/myext.po @@ -0,0 +1,2 @@ +msgid "Hello sphinx" +msgstr "HELLO SPHINX" diff --git a/tests/test_locale.py b/tests/test_locale.py new file mode 100644 index 000000000..9b1921bd6 --- /dev/null +++ b/tests/test_locale.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +""" + test_locale + ~~~~~~~~~~ + + Test locale. + + :copyright: Copyright 2007-2018 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +import pytest + +from sphinx import locale + + +@pytest.fixture(autouse=True) +def cleanup_translations(): + yield + locale.translators.clear() + + +def test_init(rootdir): + # not initialized yet + _ = locale.get_translation('myext') + assert _('Hello world') == 'Hello world' + assert _('Hello sphinx') == 'Hello sphinx' + assert _('Hello reST') == 'Hello reST' + + # load locale1 + locale.init([rootdir / 'test-locale' / 'locale1'], 'en', 'myext') + _ = locale.get_translation('myext') + assert _('Hello world') == 'HELLO WORLD' + assert _('Hello sphinx') == 'Hello sphinx' + assert _('Hello reST') == 'Hello reST' + + # load a catalog to unrelated namespace + locale.init([rootdir / 'test-locale' / 'locale2'], 'en', 'myext', 'mynamespace') + _ = locale.get_translation('myext') + assert _('Hello world') == 'HELLO WORLD' + assert _('Hello sphinx') == 'Hello sphinx' # nothing changed here + assert _('Hello reST') == 'Hello reST' + + # load locale2 in addition + locale.init([rootdir / 'test-locale' / 'locale2'], 'en', 'myext') + _ = locale.get_translation('myext') + assert _('Hello world') == 'HELLO WORLD' + assert _('Hello sphinx') == 'HELLO SPHINX' + assert _('Hello reST') == 'Hello reST' + + +def test_init_with_unknown_language(rootdir): + locale.init([rootdir / 'test-locale' / 'locale1'], 'unknown', 'myext') + _ = locale.get_translation('myext') + assert _('Hello world') == 'Hello world' + assert _('Hello sphinx') == 'Hello sphinx' + assert _('Hello reST') == 'Hello reST' + + +def test_add_message_catalog(app, rootdir): + app.config.language = 'en' + app.add_message_catalog('myext', rootdir / 'test-locale' / 'locale1') + _ = locale.get_translation('myext') + assert _('Hello world') == 'HELLO WORLD' + assert _('Hello sphinx') == 'Hello sphinx' + assert _('Hello reST') == 'Hello reST' |