edgewall/trac

View on GitHub
contrib/merge_catalog.py

Summary

Maintainability
A
3 hrs
Test Coverage
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Copyright (C) 2018-2023 Edgewall Software
# All rights reserved.
#
# This software is licensed as described in the file COPYING, which
# you should have received as part of this distribution. The terms
# are also available at https://trac.edgewall.org/wiki/TracLicense.
#
# This software consists of voluntary contributions made by many
# individuals. For the exact contribution history, see the revision
# history and logs, available at https://trac.edgewall.org/log/.

from datetime import datetime
import argparse
import io
import os.path
import sys

from babel.core import negotiate_locale
from babel.messages.pofile import read_po, write_po


class ScriptError(Exception):
    pass


def _is_fuzzy(message):
    return 'fuzzy' in message.flags


def _has_msgstr(message):
    msgstrs = message.string
    if not isinstance(msgstrs, (list, tuple)):
        msgstrs = [msgstrs]
    return any(v for v in msgstrs)


def _open_pofile(filename):
    try:
        with open(filename, 'rb') as f:
            return read_po(f)
    except IOError as e:
        raise ScriptError(e)


def _get_domains():
    return sorted(name[:-4]
                  for name in os.listdir(os.path.join('trac', 'locale'))
                  if name.endswith('.pot'))


def _get_locales(domain):
    def has_pofile(name):
        return os.path.isfile(os.path.join('trac', 'locale', name,
                                           'LC_MESSAGES', domain + '.po'))
    return sorted(name for name in os.listdir(os.path.join('trac', 'locale'))
                       if has_pofile(name))


def main():
    """Merge translated strings from another PO file.

    $ src=../trac-1.2-stable/trac/locale/de/LC_MESSAGES
    $ PYTHONPATH=. contrib/%(prog)s messages $src/messages.po [locale]
    """
    domains = _get_domains()
    if not domains:
        raise ScriptError('No trac/locale/*.pot files.')
    parser = argparse.ArgumentParser(usage=main.__doc__)
    parser.add_argument('domain', choices=domains,
                        help="Name of catalog to merge")
    parser.add_argument('pofile', help="Path of the catalog to merge from")
    parser.add_argument('locale', help="Locale of the catalog to merge from",
                        nargs='?', default=None)
    args = parser.parse_args()

    domain, source_file, locale = args.domain, args.pofile, args.locale
    locales = _get_locales(domain)
    if not locales:
        raise ScriptError('No trac/locale/*/LC_MESSAGES/*.po files.')
    pot = _open_pofile(os.path.join('trac', 'locale', domain + '.pot'))
    source = _open_pofile(source_file)
    if locale:
        preferred_locales = [locale]
    else:
        preferred_locales = [value.split(None, 1)[0]
                             for value in (source.locale and str(source.locale),
                                           source.language_team)
                             if value]
    locale = negotiate_locale(preferred_locales, locales)
    if not locale or locale == 'en_US':
        raise ScriptError('No available *.po file for %s.\n' %
                          ', '.join(preferred_locales))
    target_file = os.path.join('trac', 'locale', locale, 'LC_MESSAGES',
                               domain + '.po')
    target = _open_pofile(target_file)
    pot = _open_pofile(os.path.join('trac', 'locale', domain + '.pot'))
    n = 0
    for source_msg in source:
        msgid = source_msg.id
        if msgid == '':
            continue
        if not _has_msgstr(source_msg):
            continue
        if msgid in target:
            target_msg = target[msgid]
        elif msgid in pot:
            target_msg = pot[msgid]
        else:
            continue
        if target_msg.string == source_msg.string:
            continue
        if not _has_msgstr(source_msg):
            continue
        if _has_msgstr(target_msg) and not _is_fuzzy(target_msg):
            continue
        if msgid not in target:
            target_msg = target_msg.clone()
            target[msgid] = target_msg
        target_msg.string = source_msg.string
        target_msg.flags = source_msg.flags
        n += 1
    if n > 0:
        target.msgid_bugs_address = pot.msgid_bugs_address
        target.revision_date = datetime.utcnow()
        target.locale = locale
        target.language_team = '%s <%s>' % (locale, pot.msgid_bugs_address)
        target.fuzzy = False  # clear fuzzy flag of the header
        with io.BytesIO() as buf:
            write_po(buf, target)
            content = str(buf.getvalue(), 'utf-8')
        # write catalog with native newline
        with open(target_file, 'w', encoding='utf-8') as f:
            f.write(content)
            del f
        print('Merged %d messages from %s and updated %s' % (n, source_file,
                                                             target_file))
    else:
        print('Merged no messages from %s' % source_file)


if __name__ == '__main__':
    rc = 0
    try:
        main()
    except ScriptError as e:
        rc = 1
        sys.stderr.write('%s\n' % e)
    sys.exit(rc)