Blame dnf/i18n.py

Packit Service 21c75c
# i18n.py
Packit Service 21c75c
#
Packit Service 21c75c
# Copyright (C) 2012-2016 Red Hat, Inc.
Packit Service 21c75c
#
Packit Service 21c75c
# This copyrighted material is made available to anyone wishing to use,
Packit Service 21c75c
# modify, copy, or redistribute it subject to the terms and conditions of
Packit Service 21c75c
# the GNU General Public License v.2, or (at your option) any later version.
Packit Service 21c75c
# This program is distributed in the hope that it will be useful, but WITHOUT
Packit Service 21c75c
# ANY WARRANTY expressed or implied, including the implied warranties of
Packit Service 21c75c
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
Packit Service 21c75c
# Public License for more details.  You should have received a copy of the
Packit Service 21c75c
# GNU General Public License along with this program; if not, write to the
Packit Service 21c75c
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
Packit Service 21c75c
# 02110-1301, USA.  Any Red Hat trademarks that are incorporated in the
Packit Service 21c75c
# source code or documentation are not subject to the GNU General Public
Packit Service 21c75c
# License and may only be used or replicated with the express permission of
Packit Service 21c75c
# Red Hat, Inc.
Packit Service 21c75c
#
Packit Service 21c75c
Packit Service 21c75c
from __future__ import print_function
Packit Service 21c75c
from __future__ import unicode_literals
Packit Service 21c75c
from dnf.pycomp import unicode
Packit Service 21c75c
Packit Service 21c75c
import dnf
Packit Service 21c75c
import locale
Packit Service 21c75c
import os
Packit Service 21c75c
import signal
Packit Service 21c75c
import sys
Packit Service 21c75c
import unicodedata
Packit Service 21c75c
Packit Service 21c75c
"""
Packit Service 21c75c
Centralize i18n stuff here. Must be unittested.
Packit Service 21c75c
"""
Packit Service 21c75c
Packit Service 21c75c
class UnicodeStream(object):
Packit Service 21c75c
    def __init__(self, stream, encoding):
Packit Service 21c75c
        self.stream = stream
Packit Service 21c75c
        self.encoding = encoding
Packit Service 21c75c
Packit Service 21c75c
    def write(self, s):
Packit Service 21c75c
        if not isinstance(s, str):
Packit Service 21c75c
            s = (s.decode(self.encoding, 'replace') if dnf.pycomp.PY3 else
Packit Service 21c75c
                 s.encode(self.encoding, 'replace'))
Packit Service 21c75c
        try:
Packit Service 21c75c
            self.stream.write(s)
Packit Service 21c75c
        except UnicodeEncodeError:
Packit Service 21c75c
            s_bytes = s.encode(self.stream.encoding, 'backslashreplace')
Packit Service 21c75c
            if hasattr(self.stream, 'buffer'):
Packit Service 21c75c
                self.stream.buffer.write(s_bytes)
Packit Service 21c75c
            else:
Packit Service 21c75c
                s = s_bytes.decode(self.stream.encoding, 'ignore')
Packit Service 21c75c
                self.stream.write(s)
Packit Service 21c75c
Packit Service 21c75c
Packit Service 21c75c
    def __getattr__(self, name):
Packit Service 21c75c
        return getattr(self.stream, name)
Packit Service 21c75c
Packit Service 21c75c
def _full_ucd_support(encoding):
Packit Service 21c75c
    """Return true if encoding can express any Unicode character.
Packit Service 21c75c
Packit Service 21c75c
    Even if an encoding can express all accented letters in the given language,
Packit Service 21c75c
    we can't generally settle for it in DNF since sometimes we output special
Packit Service 21c75c
    characters like the registered trademark symbol (U+00AE) and surprisingly
Packit Service 21c75c
    many national non-unicode encodings, including e.g. ASCII and ISO-8859-2,
Packit Service 21c75c
    don't contain it.
Packit Service 21c75c
Packit Service 21c75c
    """
Packit Service 21c75c
    if encoding is None:
Packit Service 21c75c
        return False
Packit Service 21c75c
    lower = encoding.lower()
Packit Service 21c75c
    if lower.startswith('utf-') or lower.startswith('utf_'):
Packit Service 21c75c
        return True
Packit Service 21c75c
    return False
Packit Service 21c75c
Packit Service 21c75c
def _guess_encoding():
Packit Service 21c75c
    """ Take the best shot at the current system's string encoding. """
Packit Service 21c75c
    encoding = locale.getpreferredencoding(False)
Packit Service 21c75c
    return 'utf-8' if encoding.startswith("ANSI") else encoding
Packit Service 21c75c
Packit Service 21c75c
def setup_locale():
Packit Service 21c75c
    try:
Packit Service 21c75c
        dnf.pycomp.setlocale(locale.LC_ALL, '')
Packit Service 21c75c
    except locale.Error:
Packit Service 21c75c
        # default to C.UTF-8 or C locale if we got a failure.
Packit Service 21c75c
        try:
Packit Service 21c75c
            dnf.pycomp.setlocale(locale.LC_ALL, 'C.UTF-8')
Packit Service 21c75c
            os.environ['LC_ALL'] = 'C.UTF-8'
Packit Service 21c75c
        except locale.Error:
Packit Service 21c75c
            dnf.pycomp.setlocale(locale.LC_ALL, 'C')
Packit Service 21c75c
            os.environ['LC_ALL'] = 'C'
Packit Service 21c75c
        print('Failed to set locale, defaulting to {}'.format(os.environ['LC_ALL']),
Packit Service 21c75c
              file=sys.stderr)
Packit Service 21c75c
Packit Service 21c75c
def setup_stdout():
Packit Service 21c75c
    """ Check that stdout is of suitable encoding and handle the situation if
Packit Service 21c75c
        not.
Packit Service 21c75c
Packit Service 21c75c
        Returns True if stdout was of suitable encoding already and no changes
Packit Service 21c75c
        were needed.
Packit Service 21c75c
    """
Packit Service 21c75c
    stdout = sys.stdout
Packit Service 21c75c
    if not stdout.isatty():
Packit Service 21c75c
        signal.signal(signal.SIGPIPE, signal.SIG_DFL)
Packit Service 21c75c
    try:
Packit Service 21c75c
        encoding = stdout.encoding
Packit Service 21c75c
    except AttributeError:
Packit Service 21c75c
        encoding = None
Packit Service 21c75c
    if not _full_ucd_support(encoding):
Packit Service 21c75c
        sys.stdout = UnicodeStream(stdout, _guess_encoding())
Packit Service 21c75c
        return False
Packit Service 21c75c
    return True
Packit Service 21c75c
Packit Service 21c75c
Packit Service 21c75c
def ucd_input(ucstring):
Packit Service 21c75c
    # :api, deprecated in 2.0.0, will be erased when python2 is abandoned
Packit Service 21c75c
    """ It uses print instead of passing the prompt to raw_input.
Packit Service 21c75c
Packit Service 21c75c
        raw_input doesn't encode the passed string and the output
Packit Service 21c75c
        goes into stderr
Packit Service 21c75c
    """
Packit Service 21c75c
    print(ucstring, end='')
Packit Service 21c75c
    return dnf.pycomp.raw_input()
Packit Service 21c75c
Packit Service 21c75c
Packit Service 21c75c
def ucd(obj):
Packit Service 21c75c
    # :api, deprecated in 2.0.0, will be erased when python2 is abandoned
Packit Service 21c75c
    """ Like the builtin unicode() but tries to use a reasonable encoding. """
Packit Service 21c75c
    if dnf.pycomp.PY3:
Packit Service 21c75c
        if dnf.pycomp.is_py3bytes(obj):
Packit Service 21c75c
            return str(obj, _guess_encoding(), errors='ignore')
Packit Service 21c75c
        elif isinstance(obj, str):
Packit Service 21c75c
            return obj
Packit Service 21c75c
        return str(obj)
Packit Service 21c75c
    else:
Packit Service 21c75c
        if isinstance(obj, dnf.pycomp.unicode):
Packit Service 21c75c
            return obj
Packit Service 21c75c
        if hasattr(obj, '__unicode__'):
Packit Service 21c75c
            # see the doc for the unicode() built-in. The logic here is: if obj
Packit Service 21c75c
            # implements __unicode__, let it take a crack at it, but handle the
Packit Service 21c75c
            # situation if it fails:
Packit Service 21c75c
            try:
Packit Service 21c75c
                return dnf.pycomp.unicode(obj)
Packit Service 21c75c
            except UnicodeError:
Packit Service 21c75c
                pass
Packit Service 21c75c
        return dnf.pycomp.unicode(str(obj), _guess_encoding(), errors='ignore')
Packit Service 21c75c
Packit Service 21c75c
Packit Service 21c75c
# functions for formatting output according to terminal width,
Packit Service 21c75c
# They should be used instead of build-in functions to count on different
Packit Service 21c75c
# widths of Unicode characters
Packit Service 21c75c
Packit Service 21c75c
def _exact_width_char(uchar):
Packit Service 21c75c
    return 2 if unicodedata.east_asian_width(uchar) in ('W', 'F') else 1
Packit Service 21c75c
Packit Service 21c75c
Packit Service 21c75c
def chop_str(msg, chop=None):
Packit Service 21c75c
    """ Return the textual width of a Unicode string, chopping it to
Packit Service 21c75c
        a specified value. This is what you want to use instead of %.*s, as it
Packit Service 21c75c
        does the "right" thing with regard to different Unicode character width
Packit Service 21c75c
        Eg. "%.*s" % (10, msg)   <= becomes => "%s" % (chop_str(msg, 10)) """
Packit Service 21c75c
Packit Service 21c75c
    if chop is None:
Packit Service 21c75c
        return exact_width(msg), msg
Packit Service 21c75c
Packit Service 21c75c
    width = 0
Packit Service 21c75c
    chopped_msg = ""
Packit Service 21c75c
    for char in msg:
Packit Service 21c75c
        char_width = _exact_width_char(char)
Packit Service 21c75c
        if width + char_width > chop:
Packit Service 21c75c
            break
Packit Service 21c75c
        chopped_msg += char
Packit Service 21c75c
        width += char_width
Packit Service 21c75c
    return width, chopped_msg
Packit Service 21c75c
Packit Service 21c75c
Packit Service 21c75c
def exact_width(msg):
Packit Service 21c75c
    """ Calculates width of char at terminal screen
Packit Service 21c75c
        (Asian char counts for two) """
Packit Service 21c75c
    return sum(_exact_width_char(c) for c in msg)
Packit Service 21c75c
Packit Service 21c75c
Packit Service 21c75c
def fill_exact_width(msg, fill, chop=None, left=True, prefix='', suffix=''):
Packit Service 21c75c
    """ Expand a msg to a specified "width" or chop to same.
Packit Service 21c75c
        Expansion can be left or right. This is what you want to use instead of
Packit Service 21c75c
        %*.*s, as it does the "right" thing with regard to different Unicode
Packit Service 21c75c
        character width.
Packit Service 21c75c
        prefix and suffix should be used for "invisible" bytes, like
Packit Service 21c75c
        highlighting.
Packit Service 21c75c
Packit Service 21c75c
        Examples:
Packit Service 21c75c
Packit Service 21c75c
        ``"%-*.*s" % (10, 20, msg)`` becomes
Packit Service 21c75c
            ``"%s" % (fill_exact_width(msg, 10, 20))``.
Packit Service 21c75c
Packit Service 21c75c
        ``"%20.10s" % (msg)`` becomes
Packit Service 21c75c
            ``"%s" % (fill_exact_width(msg, 20, 10, left=False))``.
Packit Service 21c75c
Packit Service 21c75c
        ``"%s%.10s%s" % (pre, msg, suf)`` becomes
Packit Service 21c75c
            ``"%s" % (fill_exact_width(msg, 0, 10, prefix=pre, suffix=suf))``.
Packit Service 21c75c
        """
Packit Service 21c75c
    width, msg = chop_str(msg, chop)
Packit Service 21c75c
Packit Service 21c75c
    if width >= fill:
Packit Service 21c75c
        if prefix or suffix:
Packit Service 21c75c
            msg = ''.join([prefix, msg, suffix])
Packit Service 21c75c
    else:
Packit Service 21c75c
        extra = " " * (fill - width)
Packit Service 21c75c
        if left:
Packit Service 21c75c
            msg = ''.join([prefix, msg, suffix, extra])
Packit Service 21c75c
        else:
Packit Service 21c75c
            msg = ''.join([extra, prefix, msg, suffix])
Packit Service 21c75c
Packit Service 21c75c
    return msg
Packit Service 21c75c
Packit Service 21c75c
Packit Service 21c75c
def textwrap_fill(text, width=70, initial_indent='', subsequent_indent=''):
Packit Service 21c75c
    """ Works like we want textwrap.wrap() to work, uses Unicode strings
Packit Service 21c75c
        and doesn't screw up lists/blocks/etc. """
Packit Service 21c75c
Packit Service 21c75c
    def _indent_at_beg(line):
Packit Service 21c75c
        count = 0
Packit Service 21c75c
        byte = 'X'
Packit Service 21c75c
        for byte in line:
Packit Service 21c75c
            if byte != ' ':
Packit Service 21c75c
                break
Packit Service 21c75c
            count += 1
Packit Service 21c75c
        if byte not in ("-", "*", ".", "o", '\xe2'):
Packit Service 21c75c
            return count, 0
Packit Service 21c75c
        list_chr = chop_str(line[count:], 1)[1]
Packit Service 21c75c
        if list_chr in ("-", "*", ".", "o",
Packit Service 21c75c
                        "\u2022", "\u2023", "\u2218"):
Packit Service 21c75c
            nxt = _indent_at_beg(line[count+len(list_chr):])
Packit Service 21c75c
            nxt = nxt[1] or nxt[0]
Packit Service 21c75c
            if nxt:
Packit Service 21c75c
                return count, count + 1 + nxt
Packit Service 21c75c
        return count, 0
Packit Service 21c75c
Packit Service 21c75c
    text = text.rstrip('\n')
Packit Service 21c75c
    lines = text.replace('\t', ' ' * 8).split('\n')
Packit Service 21c75c
Packit Service 21c75c
    ret = []
Packit Service 21c75c
    indent = initial_indent
Packit Service 21c75c
    wrap_last = False
Packit Service 21c75c
    csab = 0
Packit Service 21c75c
    cspc_indent = 0
Packit Service 21c75c
    for line in lines:
Packit Service 21c75c
        line = line.rstrip(' ')
Packit Service 21c75c
        (lsab, lspc_indent) = (csab, cspc_indent)
Packit Service 21c75c
        (csab, cspc_indent) = _indent_at_beg(line)
Packit Service 21c75c
        force_nl = False # We want to stop wrapping under "certain" conditions:
Packit Service 21c75c
        if wrap_last and cspc_indent:        # if line starts a list or
Packit Service 21c75c
            force_nl = True
Packit Service 21c75c
        if wrap_last and csab == len(line):  # is empty line
Packit Service 21c75c
            force_nl = True
Packit Service 21c75c
        # if line doesn't continue a list and is "block indented"
Packit Service 21c75c
        if wrap_last and not lspc_indent:
Packit Service 21c75c
            if csab >= 4 and csab != lsab:
Packit Service 21c75c
                force_nl = True
Packit Service 21c75c
        if force_nl:
Packit Service 21c75c
            ret.append(indent.rstrip(' '))
Packit Service 21c75c
            indent = subsequent_indent
Packit Service 21c75c
            wrap_last = False
Packit Service 21c75c
        if csab == len(line):  # empty line, remove spaces to make it easier.
Packit Service 21c75c
            line = ''
Packit Service 21c75c
        if wrap_last:
Packit Service 21c75c
            line = line.lstrip(' ')
Packit Service 21c75c
            cspc_indent = lspc_indent
Packit Service 21c75c
Packit Service 21c75c
        if exact_width(indent + line) <= width:
Packit Service 21c75c
            wrap_last = False
Packit Service 21c75c
            ret.append(indent + line)
Packit Service 21c75c
            indent = subsequent_indent
Packit Service 21c75c
            continue
Packit Service 21c75c
Packit Service 21c75c
        wrap_last = True
Packit Service 21c75c
        words = line.split(' ')
Packit Service 21c75c
        line = indent
Packit Service 21c75c
        spcs = cspc_indent
Packit Service 21c75c
        if not spcs and csab >= 4:
Packit Service 21c75c
            spcs = csab
Packit Service 21c75c
        for word in words:
Packit Service 21c75c
            if (width < exact_width(line + word)) and \
Packit Service 21c75c
               (exact_width(line) > exact_width(subsequent_indent)):
Packit Service 21c75c
                ret.append(line.rstrip(' '))
Packit Service 21c75c
                line = subsequent_indent + ' ' * spcs
Packit Service 21c75c
            line += word
Packit Service 21c75c
            line += ' '
Packit Service 21c75c
        indent = line.rstrip(' ') + ' '
Packit Service 21c75c
    if wrap_last:
Packit Service 21c75c
        ret.append(indent.rstrip(' '))
Packit Service 21c75c
Packit Service 21c75c
    return '\n'.join(ret)
Packit Service 21c75c
Packit Service 21c75c
Packit Service 21c75c
def select_short_long(width, msg_short, msg_long):
Packit Service 21c75c
    """ Automatically selects the short (abbreviated) or long (full) message
Packit Service 21c75c
        depending on whether we have enough screen space to display the full
Packit Service 21c75c
        message or not. If a caller by mistake passes a long string as
Packit Service 21c75c
        msg_short and a short string as a msg_long this function recognizes
Packit Service 21c75c
        the mistake and swaps the arguments. This function is especially useful
Packit Service 21c75c
        in the i18n context when you cannot predict how long are the translated
Packit Service 21c75c
        messages.
Packit Service 21c75c
Packit Service 21c75c
        Limitations:
Packit Service 21c75c
Packit Service 21c75c
        1. If msg_short is longer than width you will still get an overflow.
Packit Service 21c75c
           This function does not abbreviate the string.
Packit Service 21c75c
        2. You are not obliged to provide an actually abbreviated string, it is
Packit Service 21c75c
           perfectly correct to pass the same string twice if you don't want
Packit Service 21c75c
           any abbreviation. However, if you provide two different strings but
Packit Service 21c75c
           having the same width this function is unable to recognize which one
Packit Service 21c75c
           is correct and you should assume that it is unpredictable which one
Packit Service 21c75c
           is returned.
Packit Service 21c75c
Packit Service 21c75c
       Example:
Packit Service 21c75c
Packit Service 21c75c
       ``select_short_long (10, _("Repo"), _("Repository"))``
Packit Service 21c75c
Packit Service 21c75c
       will return "Repository" in English but the results in other languages
Packit Service 21c75c
       may be different. """
Packit Service 21c75c
    width_short = exact_width(msg_short)
Packit Service 21c75c
    width_long = exact_width(msg_long)
Packit Service 21c75c
    # If we have two strings of the same width:
Packit Service 21c75c
    if width_short == width_long:
Packit Service 21c75c
        return msg_long
Packit Service 21c75c
    # If the short string is wider than the long string:
Packit Service 21c75c
    elif width_short > width_long:
Packit Service 21c75c
        return msg_short if width_short <= width else msg_long
Packit Service 21c75c
    # The regular case:
Packit Service 21c75c
    else:
Packit Service 21c75c
        return msg_long if width_long <= width else msg_short
Packit Service 21c75c
Packit Service 21c75c
Packit Service 21c75c
def translation(name):
Packit Service 21c75c
    # :api, deprecated in 2.0.0, will be erased when python2 is abandoned
Packit Service 21c75c
    """ Easy gettext translations setup based on given domain name """
Packit Service 21c75c
Packit Service 21c75c
    setup_locale()
Packit Service 21c75c
    def ucd_wrapper(fnc):
Packit Service 21c75c
        return lambda *w: ucd(fnc(*w))
Packit Service 21c75c
    t = dnf.pycomp.gettext.translation(name, fallback=True)
Packit Service 21c75c
    return map(ucd_wrapper, dnf.pycomp.gettext_setup(t))
Packit Service 21c75c
Packit Service 21c75c
Packit Service 21c75c
def pgettext(context, message):
Packit Service 21c75c
    result = _(context + chr(4) + message)
Packit Service 21c75c
    if "\004" in result:
Packit Service 21c75c
        return message
Packit Service 21c75c
    else:
Packit Service 21c75c
        return result
Packit Service 21c75c
Packit Service 21c75c
# setup translations
Packit Service 21c75c
_, P_ = translation("dnf")
Packit Service 21c75c
C_ = pgettext