|
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
|