Blame dnf/i18n.py

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