diff --git a/CHANGES b/CHANGES index 76cb10292..0ec005213 100644 --- a/CHANGES +++ b/CHANGES @@ -29,6 +29,11 @@ Incompatible changes * 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 parsing is attempted to distinguish valid code. +* `Locale Date Markup Language + `_ 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 -------------- @@ -99,6 +104,7 @@ Bugs fixed anatoly techtonik. * #2311: Fix sphinx.ext.inheritance_diagram raises AttributeError * #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 diff --git a/doc/config.rst b/doc/config.rst index f7b426a65..0c6e06e61 100644 --- a/doc/config.rst +++ b/doc/config.rst @@ -310,12 +310,19 @@ Project information replacement for ``|today|``. * If you set :confval:`today` to a non-empty value, it is used. - * Otherwise, the current time is formatted using :func:`time.strftime` and - the format given in :confval:`today_fmt`. + * Otherwise, the current time is formatted using `Locale Data Markup Language + `_ + and the format given in :confval:`today_fmt`. - The default is no :confval:`today` and a :confval:`today_fmt` of ``'%B %d, - %Y'`` (or, if translation is enabled with :confval:`language`, an equivalent - %format for the selected locale). + The default is no :confval:`today` and a :confval:`today_fmt` of ``'MMMM dd, + YYYY'`` (or, if translation is enabled with :confval:`language`, an + 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 @@ -653,10 +660,17 @@ that use Sphinx's HTMLWriter class. .. confval:: html_last_updated_fmt If this is not None, a 'Last updated on:' timestamp is inserted - at every page bottom, using the given :func:`strftime` format. - The empty string is equivalent to ``'%b %d, %Y'`` (or a + at every page bottom, using the given `Locale Data Markup Language + `_ + format. The empty string is equivalent to ``'MMM dd, YYYY'`` (or a 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 If true, `SmartyPants `_ diff --git a/sphinx/builders/epub.py b/sphinx/builders/epub.py index 8c0109264..d1610bda9 100644 --- a/sphinx/builders/epub.py +++ b/sphinx/builders/epub.py @@ -28,7 +28,8 @@ from docutils import nodes from sphinx import addnodes 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.console import brown @@ -529,7 +530,7 @@ class EpubBuilder(StandaloneHTMLBuilder): metadata['copyright'] = self.esc(self.config.epub_copyright) metadata['scheme'] = self.esc(self.config.epub_scheme) 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['spine'] = spine metadata['guide'] = guide diff --git a/sphinx/builders/html.py b/sphinx/builders/html.py index 899c17d98..b06e9fd6e 100644 --- a/sphinx/builders/html.py +++ b/sphinx/builders/html.py @@ -28,8 +28,9 @@ from docutils.readers.doctree import Reader as DoctreeReader from sphinx import package_dir, __display_version__ 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, \ - movefile, ustrftime, copyfile + movefile, copyfile from sphinx.util.nodes import inline_all_toctrees from sphinx.util.matching import patmatch, compile_matchers from sphinx.locale import _ @@ -291,7 +292,8 @@ 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 = ustrftime(lufmt or _('%b %d, %Y')) + self.last_updated = format_date(lufmt or _('MMM dd, YYYY'), + language=self.config.language) else: self.last_updated = None diff --git a/sphinx/transforms.py b/sphinx/transforms.py index a5d44d73f..583271116 100644 --- a/sphinx/transforms.py +++ b/sphinx/transforms.py @@ -24,8 +24,7 @@ from sphinx.util.nodes import ( traverse_translatable_index, extract_messages, LITERAL_TYPE_NODES, IMAGE_TYPE_NODES, apply_source_workaround, ) -from sphinx.util.osutil import ustrftime -from sphinx.util.i18n import find_catalog +from sphinx.util.i18n import find_catalog, format_date from sphinx.util.pycompat import indent from sphinx.domains.std import make_glossary_term, split_term_classifiers @@ -54,7 +53,8 @@ class DefaultSubstitutions(Transform): text = config[refname] if refname == 'today' and not text: # 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)) diff --git a/sphinx/util/i18n.py b/sphinx/util/i18n.py index 47d981773..7fa8e4e6f 100644 --- a/sphinx/util/i18n.py +++ b/sphinx/util/i18n.py @@ -10,9 +10,15 @@ """ import gettext import io +import os +import re +import warnings from os import path +from time import time +from datetime import datetime from collections import namedtuple +import babel.dates from babel.messages.pofile import read_po 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) return catalogs + +# date_format mappings: ustrftime() to bable.dates.format_date() +date_format_mappings = { + '%a': 'EEE', # Weekday as locale’s abbreviated name. + '%A': 'EEEE', # Weekday as locale’s full name. + '%b': 'MMM', # Month as locale’s abbreviated name. + '%B': 'MMMM', # Month as locale’s 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', # Locale’s 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) diff --git a/sphinx/util/osutil.py b/sphinx/util/osutil.py index 653530644..585dc6104 100644 --- a/sphinx/util/osutil.py +++ b/sphinx/util/osutil.py @@ -158,7 +158,8 @@ def make_filename(string): 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 time is not specified, try to use $SOURCE_DATE_EPOCH variable # See https://wiki.debian.org/ReproducibleBuilds/TimestampsProposal diff --git a/sphinx/writers/latex.py b/sphinx/writers/latex.py index 151db3854..22f97d256 100644 --- a/sphinx/writers/latex.py +++ b/sphinx/writers/latex.py @@ -26,8 +26,8 @@ from sphinx import highlighting from sphinx.errors import SphinxError from sphinx.locale import admonitionlabels, _ 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.osutil import ustrftime from sphinx.util.texescape import tex_escape_map, tex_replace_map from sphinx.util.smartypants import educate_quotes_latex @@ -353,8 +353,9 @@ class LaTeXTranslator(nodes.NodeVisitor): if builder.config.today: self.elements['date'] = builder.config.today else: - self.elements['date'] = ustrftime(builder.config.today_fmt or - _('%B %d, %Y')) + self.elements['date'] = format_date(builder.config.today_fmt or + _('MMMM dd, YYYY'), + language=builder.config.language) if builder.config.latex_logo: self.elements['logo'] = '\\includegraphics{%s}\\par' % \ path.basename(builder.config.latex_logo) diff --git a/sphinx/writers/manpage.py b/sphinx/writers/manpage.py index f44dc4cbd..7454a0f20 100644 --- a/sphinx/writers/manpage.py +++ b/sphinx/writers/manpage.py @@ -20,8 +20,8 @@ from docutils.writers.manpage import ( from sphinx import addnodes from sphinx.locale import admonitionlabels, _ -from sphinx.util.osutil import ustrftime from sphinx.util.compat import docutils_version +from sphinx.util.i18n import format_date class ManualPageWriter(Writer): @@ -97,8 +97,9 @@ class ManualPageTranslator(BaseTranslator): if builder.config.today: self._docinfo['date'] = builder.config.today else: - self._docinfo['date'] = ustrftime(builder.config.today_fmt or - _('%B %d, %Y')) + self._docinfo['date'] = format_date(builder.config.today_fmt or + _('MMMM dd, YYYY'), + language=builder.config.language) self._docinfo['copyright'] = builder.config.copyright self._docinfo['version'] = builder.config.version self._docinfo['manual_group'] = builder.config.project diff --git a/sphinx/writers/texinfo.py b/sphinx/writers/texinfo.py index 360b7d56d..fc7d31534 100644 --- a/sphinx/writers/texinfo.py +++ b/sphinx/writers/texinfo.py @@ -20,7 +20,7 @@ from docutils import nodes, writers from sphinx import addnodes, __display_version__ from sphinx.locale import admonitionlabels, _ -from sphinx.util import ustrftime +from sphinx.util.i18n import format_date from sphinx.writers.latex import collected_footnote @@ -218,8 +218,9 @@ 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 - ustrftime(self.builder.config.today_fmt or - _('%B %d, %Y'))) + format_date(self.builder.config.today_fmt or + _('MMMM dd, YYYY'), + language=self.builder.config.language)) }) # title title = elements['title'] diff --git a/tests/test_util_i18n.py b/tests/test_util_i18n.py index df59653b3..7497eddef 100644 --- a/tests/test_util_i18n.py +++ b/tests/test_util_i18n.py @@ -11,6 +11,7 @@ from __future__ import print_function import os +import datetime from os import path 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) domains = set(c.domain for c in catalogs) 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'