mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Currently, when Babel fails for a locale, the entire PR fails. Instead, just skip compiling the catalogue for that locale, and report the failures in the body of the PR to be more visible.
284 lines
9.2 KiB
Python
284 lines
9.2 KiB
Python
"""Run babel for translations.
|
|
|
|
Usage:
|
|
|
|
babel_runner.py extract
|
|
Extract messages from the source code and update the ".pot" template file.
|
|
|
|
babel_runner.py update
|
|
Update all language catalogues in "sphinx/locale/<language>/LC_MESSAGES"
|
|
with the current messages in the template file.
|
|
|
|
babel_runner.py compile
|
|
Compile the ".po" catalogue files to ".mo" and ".js" files.
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
|
|
from babel.messages.catalog import Catalog
|
|
from babel.messages.extract import (
|
|
DEFAULT_KEYWORDS,
|
|
extract,
|
|
extract_javascript,
|
|
extract_python,
|
|
)
|
|
from babel.messages.mofile import write_mo
|
|
from babel.messages.pofile import read_po, write_po
|
|
from babel.util import pathmatch
|
|
from jinja2.ext import babel_extract as extract_jinja2
|
|
|
|
IS_CI = 'CI' in os.environ
|
|
ROOT = os.path.realpath(os.path.join(os.path.abspath(__file__), '..', '..'))
|
|
TEX_DELIMITERS = {
|
|
'variable_start_string': '<%=',
|
|
'variable_end_string': '%>',
|
|
'block_start_string': '<%',
|
|
'block_end_string': '%>',
|
|
}
|
|
METHOD_MAP = [
|
|
# Extraction from Python source files
|
|
('**.py', extract_python),
|
|
# Extraction from Jinja2 template files
|
|
('**/templates/latex/**.tex.jinja', extract_jinja2),
|
|
('**/templates/latex/**.tex_t', extract_jinja2),
|
|
('**/templates/latex/**.sty.jinja', extract_jinja2),
|
|
('**/templates/latex/**.sty_t', extract_jinja2),
|
|
# Extraction from Jinja2 HTML templates
|
|
('**/themes/**.html', extract_jinja2),
|
|
# Extraction from Jinja2 XML templates
|
|
('**/themes/**.xml', extract_jinja2),
|
|
# Extraction from JavaScript files
|
|
('**.js', extract_javascript),
|
|
('**.js.jinja', extract_javascript),
|
|
('**.js_t', extract_javascript),
|
|
]
|
|
OPTIONS_MAP = {
|
|
# Extraction from Python source files
|
|
'**.py': {
|
|
'encoding': 'utf-8',
|
|
},
|
|
# Extraction from Jinja2 template files
|
|
'**/templates/latex/**.tex.jinja': TEX_DELIMITERS.copy(),
|
|
'**/templates/latex/**.tex_t': TEX_DELIMITERS.copy(),
|
|
'**/templates/latex/**.sty.jinja': TEX_DELIMITERS.copy(),
|
|
'**/templates/latex/**.sty_t': TEX_DELIMITERS.copy(),
|
|
# Extraction from Jinja2 HTML templates
|
|
'**/themes/**.html': {
|
|
'encoding': 'utf-8',
|
|
'ignore_tags': 'script,style',
|
|
'include_attrs': 'alt title summary',
|
|
},
|
|
}
|
|
KEYWORDS = {**DEFAULT_KEYWORDS, '_': None, '__': None}
|
|
|
|
|
|
def run_extract() -> None:
|
|
"""Message extraction function."""
|
|
log = _get_logger()
|
|
|
|
with open('sphinx/__init__.py', encoding='utf-8') as f:
|
|
for line in f.read().splitlines():
|
|
if line.startswith('__version__ = '):
|
|
# remove prefix; strip whitespace; remove quotation marks
|
|
sphinx_version = line[14:].strip()[1:-1]
|
|
break
|
|
|
|
input_path = 'sphinx'
|
|
catalogue = Catalog(project='Sphinx', version=sphinx_version, charset='utf-8')
|
|
|
|
base = os.path.abspath(input_path)
|
|
for root, dirnames, filenames in os.walk(base):
|
|
relative_root = os.path.relpath(root, base) if root != base else ''
|
|
dirnames.sort()
|
|
for filename in sorted(filenames):
|
|
relative_name = os.path.join(relative_root, filename)
|
|
for pattern, method in METHOD_MAP:
|
|
if not pathmatch(pattern, relative_name):
|
|
continue
|
|
|
|
options = {}
|
|
for opt_pattern, opt_dict in OPTIONS_MAP.items():
|
|
if pathmatch(opt_pattern, relative_name):
|
|
options = opt_dict
|
|
with open(os.path.join(root, filename), 'rb') as fileobj:
|
|
for lineno, message, comments, context in extract(
|
|
method, fileobj, KEYWORDS, options=options
|
|
):
|
|
filepath = os.path.join(input_path, relative_name)
|
|
catalogue.add(
|
|
message,
|
|
None,
|
|
[(filepath, lineno)],
|
|
auto_comments=comments,
|
|
context=context,
|
|
)
|
|
break
|
|
|
|
output_file = os.path.join('sphinx', 'locale', 'sphinx.pot')
|
|
log.info('writing PO template file to %s', output_file)
|
|
with open(output_file, 'wb') as outfile:
|
|
write_po(outfile, catalogue)
|
|
|
|
|
|
def run_update() -> None:
|
|
"""Catalog merging command."""
|
|
log = _get_logger()
|
|
|
|
domain = 'sphinx'
|
|
locale_dir = os.path.join('sphinx', 'locale')
|
|
template_file = os.path.join(locale_dir, 'sphinx.pot')
|
|
|
|
with open(template_file, encoding='utf-8') as infile:
|
|
template = read_po(infile)
|
|
|
|
for locale in os.listdir(locale_dir):
|
|
filename = os.path.join(locale_dir, locale, 'LC_MESSAGES', f'{domain}.po')
|
|
if not os.path.exists(filename):
|
|
continue
|
|
|
|
log.info('updating catalog %s based on %s', filename, template_file)
|
|
with open(filename, encoding='utf-8') as infile:
|
|
catalog = read_po(infile, locale=locale, domain=domain)
|
|
|
|
catalog.update(template)
|
|
tmp_name = os.path.join(
|
|
os.path.dirname(filename),
|
|
tempfile.gettempprefix() + os.path.basename(filename),
|
|
)
|
|
try:
|
|
with open(tmp_name, 'wb') as tmpfile:
|
|
write_po(tmpfile, catalog)
|
|
except Exception:
|
|
os.remove(tmp_name)
|
|
raise
|
|
|
|
os.replace(tmp_name, filename)
|
|
|
|
|
|
def run_compile() -> None:
|
|
"""
|
|
Catalog compilation command.
|
|
|
|
An extended command that writes all message strings that occur in
|
|
JavaScript files to a JavaScript file along with the .mo file.
|
|
|
|
Unfortunately, babel's setup command isn't built very extensible, so
|
|
most of the run() code is duplicated here.
|
|
"""
|
|
log = _get_logger()
|
|
|
|
directory = os.path.join('sphinx', 'locale')
|
|
total_errors = {}
|
|
|
|
for locale in os.listdir(directory):
|
|
po_file = os.path.join(directory, locale, 'LC_MESSAGES', 'sphinx.po')
|
|
if not os.path.exists(po_file):
|
|
continue
|
|
|
|
with open(po_file, encoding='utf-8') as infile:
|
|
catalog = read_po(infile, locale)
|
|
|
|
if catalog.fuzzy:
|
|
log.info('catalog %s is marked as fuzzy, skipping', po_file)
|
|
continue
|
|
|
|
locale_errors = 0
|
|
for message, errors in catalog.check():
|
|
for error in errors:
|
|
locale_errors += 1
|
|
log.error(
|
|
'error: %s:%d: %s\nerror: in message string: %r',
|
|
po_file,
|
|
message.lineno,
|
|
error,
|
|
message.string,
|
|
)
|
|
|
|
if locale_errors:
|
|
total_errors[locale] = locale_errors
|
|
log.info('%d errors encountered in %r locale, skipping', locale_errors, locale)
|
|
continue
|
|
|
|
mo_file = os.path.join(directory, locale, 'LC_MESSAGES', 'sphinx.mo')
|
|
log.info('compiling catalog %s to %s', po_file, mo_file)
|
|
with open(mo_file, 'wb') as outfile:
|
|
write_mo(outfile, catalog, use_fuzzy=False)
|
|
|
|
js_file = os.path.join(directory, locale, 'LC_MESSAGES', 'sphinx.js')
|
|
log.info('writing JavaScript strings in catalog %s to %s', po_file, js_file)
|
|
js_catalogue = {}
|
|
for message in catalog:
|
|
if any(
|
|
x[0].endswith(('.js', '.js.jinja', '.js_t', '.html'))
|
|
for x in message.locations
|
|
):
|
|
msgid = message.id
|
|
if isinstance(msgid, (list, tuple)):
|
|
msgid = msgid[0]
|
|
js_catalogue[msgid] = message.string
|
|
|
|
obj = json.dumps(
|
|
{
|
|
'messages': js_catalogue,
|
|
'plural_expr': catalog.plural_expr,
|
|
'locale': str(catalog.locale),
|
|
},
|
|
sort_keys=True,
|
|
indent=4,
|
|
)
|
|
with open(js_file, 'wb') as outfile:
|
|
# to ensure lines end with ``\n`` rather than ``\r\n``:
|
|
outfile.write(f'Documentation.addTranslations({obj});'.encode())
|
|
|
|
if total_errors:
|
|
_write_pr_body_line('## Babel catalogue errors')
|
|
_write_pr_body_line('')
|
|
for locale, err_count in total_errors.items():
|
|
log.error('error: %d errors encountered in %r locale.', err_count, locale)
|
|
s = 's' if err_count != 1 else ''
|
|
_write_pr_body_line(f'* {locale}: {err_count} error{s}')
|
|
|
|
|
|
def _get_logger():
|
|
log = logging.getLogger('babel')
|
|
log.setLevel(logging.INFO)
|
|
handler = logging.StreamHandler()
|
|
handler.setFormatter(logging.Formatter('%(message)s'))
|
|
log.addHandler(handler)
|
|
return log
|
|
|
|
|
|
def _write_pr_body_line(message: str) -> None:
|
|
if not IS_CI:
|
|
return
|
|
with open('babel_compile.txt', 'a', encoding='utf-8') as f:
|
|
f.write(f'{message}\n')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
try:
|
|
action = sys.argv[1].lower()
|
|
except IndexError:
|
|
print(__doc__, file=sys.stderr)
|
|
raise SystemExit(2) from None
|
|
|
|
os.chdir(ROOT)
|
|
if action == 'extract':
|
|
run_extract()
|
|
elif action == 'update':
|
|
run_update()
|
|
elif action == 'compile':
|
|
run_compile()
|
|
elif action == 'all':
|
|
run_extract()
|
|
run_update()
|
|
run_compile()
|
|
else:
|
|
msg = f"invalid action: '{action}'"
|
|
raise ValueError(msg)
|
|
raise SystemExit
|