Merge pull request #2322 from tk0miya/794_babel_format_date

Fix #794: Date formatting in latex output is not localized
This commit is contained in:
Takeshi KOMIYA 2016-02-14 22:01:05 +09:00
commit 3e6f608778
11 changed files with 141 additions and 24 deletions

View File

@ -29,6 +29,11 @@ Incompatible changes
* The default highlight language is now Python 3. This means that source code * The default highlight language is now Python 3. This means that source code
is highlighted as Python 3 (which is mostly a superset of Python 2), and no is highlighted as Python 3 (which is mostly a superset of Python 2), and no
parsing is attempted to distinguish valid code. parsing is attempted to distinguish valid code.
* `Locale Date Markup Language
<http://unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns>`_ like
``"MMMM dd, YYYY"`` is default format for `today_fmt` and `html_last_updated_fmt`.
However strftime format like ``"%B %d, %Y"`` is also supported for backward
compatibility until Sphinx-1.5. Later format will be disabled from Sphinx-1.5.
Features added Features added
-------------- --------------
@ -99,6 +104,7 @@ Bugs fixed
anatoly techtonik. anatoly techtonik.
* #2311: Fix sphinx.ext.inheritance_diagram raises AttributeError * #2311: Fix sphinx.ext.inheritance_diagram raises AttributeError
* #2251: Line breaks in .rst files are transferred to .pot files in a wrong way. * #2251: Line breaks in .rst files are transferred to .pot files in a wrong way.
* #794: Fix date formatting in latex output is not localized
Documentation Documentation

View File

@ -310,12 +310,19 @@ Project information
replacement for ``|today|``. replacement for ``|today|``.
* If you set :confval:`today` to a non-empty value, it is used. * If you set :confval:`today` to a non-empty value, it is used.
* Otherwise, the current time is formatted using :func:`time.strftime` and * Otherwise, the current time is formatted using `Locale Data Markup Language
the format given in :confval:`today_fmt`. <http://unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns>`_
and the format given in :confval:`today_fmt`.
The default is no :confval:`today` and a :confval:`today_fmt` of ``'%B %d, The default is no :confval:`today` and a :confval:`today_fmt` of ``'MMMM dd,
%Y'`` (or, if translation is enabled with :confval:`language`, an equivalent YYYY'`` (or, if translation is enabled with :confval:`language`, an
%format for the selected locale). equivalent %format for the selected locale).
.. versionchanged:: 1.4
Format specification was changed from strftime to Locale Data Markup
Language. strftime format is also supported for backward compatibility
until Sphinx-1.5.
.. confval:: highlight_language .. confval:: highlight_language
@ -653,10 +660,17 @@ that use Sphinx's HTMLWriter class.
.. confval:: html_last_updated_fmt .. confval:: html_last_updated_fmt
If this is not None, a 'Last updated on:' timestamp is inserted If this is not None, a 'Last updated on:' timestamp is inserted
at every page bottom, using the given :func:`strftime` format. at every page bottom, using the given `Locale Data Markup Language
The empty string is equivalent to ``'%b %d, %Y'`` (or a <http://unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns>`_
format. The empty string is equivalent to ``'MMM dd, YYYY'`` (or a
locale-dependent equivalent). locale-dependent equivalent).
.. versionchanged:: 1.4
Format specification was changed from strftime to Locale Data Markup
Language. strftime format is also supported for backward compatibility
until Sphinx-1.5.
.. confval:: html_use_smartypants .. confval:: html_use_smartypants
If true, `SmartyPants <http://daringfireball.net/projects/smartypants/>`_ If true, `SmartyPants <http://daringfireball.net/projects/smartypants/>`_

View File

@ -28,7 +28,8 @@ from docutils import nodes
from sphinx import addnodes from sphinx import addnodes
from sphinx.builders.html import StandaloneHTMLBuilder from sphinx.builders.html import StandaloneHTMLBuilder
from sphinx.util.osutil import ensuredir, copyfile, ustrftime, EEXIST from sphinx.util.i18n import format_date
from sphinx.util.osutil import ensuredir, copyfile, EEXIST
from sphinx.util.smartypants import sphinx_smarty_pants as ssp from sphinx.util.smartypants import sphinx_smarty_pants as ssp
from sphinx.util.console import brown from sphinx.util.console import brown
@ -529,7 +530,7 @@ class EpubBuilder(StandaloneHTMLBuilder):
metadata['copyright'] = self.esc(self.config.epub_copyright) metadata['copyright'] = self.esc(self.config.epub_copyright)
metadata['scheme'] = self.esc(self.config.epub_scheme) metadata['scheme'] = self.esc(self.config.epub_scheme)
metadata['id'] = self.esc(self.config.epub_identifier) metadata['id'] = self.esc(self.config.epub_identifier)
metadata['date'] = self.esc(ustrftime('%Y-%m-%d')) metadata['date'] = self.esc(format_date('YYYY-MM-dd', language=self.config.language))
metadata['files'] = files metadata['files'] = files
metadata['spine'] = spine metadata['spine'] = spine
metadata['guide'] = guide metadata['guide'] = guide

View File

@ -28,8 +28,9 @@ from docutils.readers.doctree import Reader as DoctreeReader
from sphinx import package_dir, __display_version__ from sphinx import package_dir, __display_version__
from sphinx.util import jsonimpl, copy_static_entry, copy_extra_entry from sphinx.util import jsonimpl, copy_static_entry, copy_extra_entry
from sphinx.util.i18n import format_date
from sphinx.util.osutil import SEP, os_path, relative_uri, ensuredir, \ from sphinx.util.osutil import SEP, os_path, relative_uri, ensuredir, \
movefile, ustrftime, copyfile movefile, copyfile
from sphinx.util.nodes import inline_all_toctrees from sphinx.util.nodes import inline_all_toctrees
from sphinx.util.matching import patmatch, compile_matchers from sphinx.util.matching import patmatch, compile_matchers
from sphinx.locale import _ from sphinx.locale import _
@ -291,7 +292,8 @@ class StandaloneHTMLBuilder(Builder):
# typically doesn't include the time of day # typically doesn't include the time of day
lufmt = self.config.html_last_updated_fmt lufmt = self.config.html_last_updated_fmt
if lufmt is not None: if lufmt is not None:
self.last_updated = ustrftime(lufmt or _('%b %d, %Y')) self.last_updated = format_date(lufmt or _('MMM dd, YYYY'),
language=self.config.language)
else: else:
self.last_updated = None self.last_updated = None

View File

@ -24,8 +24,7 @@ from sphinx.util.nodes import (
traverse_translatable_index, extract_messages, LITERAL_TYPE_NODES, IMAGE_TYPE_NODES, traverse_translatable_index, extract_messages, LITERAL_TYPE_NODES, IMAGE_TYPE_NODES,
apply_source_workaround, apply_source_workaround,
) )
from sphinx.util.osutil import ustrftime from sphinx.util.i18n import find_catalog, format_date
from sphinx.util.i18n import find_catalog
from sphinx.util.pycompat import indent from sphinx.util.pycompat import indent
from sphinx.domains.std import make_glossary_term, split_term_classifiers from sphinx.domains.std import make_glossary_term, split_term_classifiers
@ -54,7 +53,8 @@ class DefaultSubstitutions(Transform):
text = config[refname] text = config[refname]
if refname == 'today' and not text: if refname == 'today' and not text:
# special handling: can also specify a strftime format # special handling: can also specify a strftime format
text = ustrftime(config.today_fmt or _('%B %d, %Y')) text = format_date(config.today_fmt or _('MMMM dd, YYYY'),
language=config.language)
ref.replace_self(nodes.Text(text, text)) ref.replace_self(nodes.Text(text, text))

View File

@ -10,9 +10,15 @@
""" """
import gettext import gettext
import io import io
import os
import re
import warnings
from os import path from os import path
from time import time
from datetime import datetime
from collections import namedtuple from collections import namedtuple
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
@ -118,3 +124,69 @@ def find_catalog_source_files(locale_dirs, locale, domains=None, gettext_compact
catalogs.add(cat) catalogs.add(cat)
return catalogs return catalogs
# date_format mappings: ustrftime() to bable.dates.format_date()
date_format_mappings = {
'%a': 'EEE', # Weekday as locales abbreviated name.
'%A': 'EEEE', # Weekday as locales full name.
'%b': 'MMM', # Month as locales abbreviated name.
'%B': 'MMMM', # Month as locales full name.
'%d': 'dd', # Day of the month as a zero-padded decimal number.
'%j': 'DDD', # Day of the year as a zero-padded decimal number.
'%m': 'MM', # Month as a zero-padded decimal number.
'%U': 'WW', # Week number of the year (Sunday as the first day of the week)
# as a zero padded decimal number. All days in a new year preceding
# the first Sunday are considered to be in week 0.
'%w': 'e', # Weekday as a decimal number, where 0 is Sunday and 6 is Saturday.
'%W': 'WW', # Week number of the year (Monday as the first day of the week)
# as a decimal number. All days in a new year preceding the first
# Monday are considered to be in week 0.
'%x': 'medium', # Locales appropriate date representation.
'%y': 'YY', # Year without century as a zero-padded decimal number.
'%Y': 'YYYY', # Year with century as a decimal number.
'%%': '%',
}
def babel_format_date(date, format, locale):
if locale is None:
locale = 'en'
try:
return babel.dates.format_date(date, format, locale=locale)
except babel.core.UnknownLocaleError:
# fallback to English
return babel.dates.format_date(date, format, locale='en')
def format_date(format, date=None, language=None):
if format is None:
format = 'medium'
if date is None:
# If time is not specified, try to use $SOURCE_DATE_EPOCH variable
# See https://wiki.debian.org/ReproducibleBuilds/TimestampsProposal
source_date_epoch = os.getenv('SOURCE_DATE_EPOCH')
if source_date_epoch is not None:
date = time.gmtime(float(source_date_epoch))
else:
date = datetime.now()
if '%' not in format:
# consider the format as babel's
return babel_format_date(date, format, locale=language)
else:
warnings.warn('ustrftime format support will be dropped at Sphinx-1.5',
DeprecationWarning)
# consider the format as ustrftime's and try to convert it to babel's
result = []
tokens = re.split('(%.)', format)
for token in tokens:
if token in date_format_mappings:
babel_format = date_format_mappings.get(token, '')
result.append(babel_format_date(date, babel_format, locale=language))
else:
result.append(token)
return "".join(result)

View File

@ -158,7 +158,8 @@ def make_filename(string):
def ustrftime(format, *args): def ustrftime(format, *args):
# strftime for unicode strings # [DEPRECATED] strftime for unicode strings
# It will be removed at Sphinx-1.5
if not args: if not args:
# If time is not specified, try to use $SOURCE_DATE_EPOCH variable # If time is not specified, try to use $SOURCE_DATE_EPOCH variable
# See https://wiki.debian.org/ReproducibleBuilds/TimestampsProposal # See https://wiki.debian.org/ReproducibleBuilds/TimestampsProposal

View File

@ -26,8 +26,8 @@ from sphinx import highlighting
from sphinx.errors import SphinxError from sphinx.errors import SphinxError
from sphinx.locale import admonitionlabels, _ from sphinx.locale import admonitionlabels, _
from sphinx.util import split_into from sphinx.util import split_into
from sphinx.util.i18n import format_date
from sphinx.util.nodes import clean_astext, traverse_parent from sphinx.util.nodes import clean_astext, traverse_parent
from sphinx.util.osutil import ustrftime
from sphinx.util.texescape import tex_escape_map, tex_replace_map from sphinx.util.texescape import tex_escape_map, tex_replace_map
from sphinx.util.smartypants import educate_quotes_latex from sphinx.util.smartypants import educate_quotes_latex
@ -353,8 +353,9 @@ class LaTeXTranslator(nodes.NodeVisitor):
if builder.config.today: if builder.config.today:
self.elements['date'] = builder.config.today self.elements['date'] = builder.config.today
else: else:
self.elements['date'] = ustrftime(builder.config.today_fmt or self.elements['date'] = format_date(builder.config.today_fmt or
_('%B %d, %Y')) _('MMMM dd, YYYY'),
language=builder.config.language)
if builder.config.latex_logo: if builder.config.latex_logo:
self.elements['logo'] = '\\includegraphics{%s}\\par' % \ self.elements['logo'] = '\\includegraphics{%s}\\par' % \
path.basename(builder.config.latex_logo) path.basename(builder.config.latex_logo)

View File

@ -20,8 +20,8 @@ from docutils.writers.manpage import (
from sphinx import addnodes from sphinx import addnodes
from sphinx.locale import admonitionlabels, _ from sphinx.locale import admonitionlabels, _
from sphinx.util.osutil import ustrftime
from sphinx.util.compat import docutils_version from sphinx.util.compat import docutils_version
from sphinx.util.i18n import format_date
class ManualPageWriter(Writer): class ManualPageWriter(Writer):
@ -97,8 +97,9 @@ class ManualPageTranslator(BaseTranslator):
if builder.config.today: if builder.config.today:
self._docinfo['date'] = builder.config.today self._docinfo['date'] = builder.config.today
else: else:
self._docinfo['date'] = ustrftime(builder.config.today_fmt or self._docinfo['date'] = format_date(builder.config.today_fmt or
_('%B %d, %Y')) _('MMMM dd, YYYY'),
language=builder.config.language)
self._docinfo['copyright'] = builder.config.copyright self._docinfo['copyright'] = builder.config.copyright
self._docinfo['version'] = builder.config.version self._docinfo['version'] = builder.config.version
self._docinfo['manual_group'] = builder.config.project self._docinfo['manual_group'] = builder.config.project

View File

@ -20,7 +20,7 @@ from docutils import nodes, writers
from sphinx import addnodes, __display_version__ from sphinx import addnodes, __display_version__
from sphinx.locale import admonitionlabels, _ from sphinx.locale import admonitionlabels, _
from sphinx.util import ustrftime from sphinx.util.i18n import format_date
from sphinx.writers.latex import collected_footnote from sphinx.writers.latex import collected_footnote
@ -218,8 +218,9 @@ class TexinfoTranslator(nodes.NodeVisitor):
'project': self.escape(self.builder.config.project), 'project': self.escape(self.builder.config.project),
'copyright': self.escape(self.builder.config.copyright), 'copyright': self.escape(self.builder.config.copyright),
'date': self.escape(self.builder.config.today or 'date': self.escape(self.builder.config.today or
ustrftime(self.builder.config.today_fmt or format_date(self.builder.config.today_fmt or
_('%B %d, %Y'))) _('MMMM dd, YYYY'),
language=self.builder.config.language))
}) })
# title # title
title = elements['title'] title = elements['title']

View File

@ -11,6 +11,7 @@
from __future__ import print_function from __future__ import print_function
import os import os
import datetime
from os import path from os import path
from babel.messages.mofile import read_mo from babel.messages.mofile import read_mo
@ -162,3 +163,20 @@ def test_get_catalogs_with_compact(dir):
catalogs = i18n.find_catalog_source_files([dir / 'loc1'], 'xx', gettext_compact=True) catalogs = i18n.find_catalog_source_files([dir / 'loc1'], 'xx', gettext_compact=True)
domains = set(c.domain for c in catalogs) domains = set(c.domain for c in catalogs)
assert domains == set(['test1', 'test2', 'sub']) assert domains == set(['test1', 'test2', 'sub'])
def test_format_date():
date = datetime.date(2016, 2, 7)
format = None
assert i18n.format_date(format, date=date) == 'Feb 7, 2016'
assert i18n.format_date(format, date=date, language='en') == 'Feb 7, 2016'
assert i18n.format_date(format, date=date, language='ja') == '2016/02/07'
assert i18n.format_date(format, date=date, language='de') == '07.02.2016'
format = '%B %d, %Y'
print(i18n.format_date(format, date=date))
assert i18n.format_date(format, date=date) == '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='de') == 'Februar 07, 2016'