Because there is no en.po for Gramps, so the C gettext called from GtkBuilder skips to the next translation in the list. Fix makes sure that there is no next translation.
1045 lines
40 KiB
Python
1045 lines
40 KiB
Python
#
|
|
# Gramps - a GTK+/GNOME based genealogy program
|
|
#
|
|
# Copyright (C) 2000-2006 Donald N. Allingham
|
|
# Copyright (C) 2009 Brian G. Matherly
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software
|
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
#
|
|
|
|
# $Id$
|
|
|
|
#------------------------------------------------------------------------
|
|
#
|
|
# python modules
|
|
#
|
|
#------------------------------------------------------------------------
|
|
from __future__ import print_function
|
|
import gettext
|
|
import sys
|
|
import os
|
|
import codecs
|
|
import locale
|
|
import logging
|
|
LOG = logging.getLogger("." + __name__)
|
|
LOG.propagate = True
|
|
HAVE_ICU = False
|
|
_hdlr = None
|
|
# GrampsLocale initialization comes before command-line argument
|
|
# passing, so one must set the log level directly. The default is
|
|
# logging.WARN. Uncomment the following to change it to logging.DEBUG:
|
|
#LOG.setLevel(logging.DEBUG)
|
|
try:
|
|
from icu import Locale, Collator
|
|
HAVE_ICU = True
|
|
except ImportError:
|
|
try:
|
|
from PyICU import Locale, Collator
|
|
HAVE_ICU = True
|
|
except ImportError as err:
|
|
LOG.warning("ICU not loaded because %s. Localization will be impaired. "
|
|
"Use your package manager to install PyICU", str(err))
|
|
ICU_LOCALES = None
|
|
if HAVE_ICU:
|
|
ICU_LOCALES = Locale.getAvailableLocales()
|
|
|
|
# Map of languages for converting to Microsoft locales and naming
|
|
# locales for display to the user. It's important to add to this list
|
|
# when a new translation is added. Note the dummy _(): That's just to
|
|
# get xgettext to include the string in gramps.pot; actual translation
|
|
# is done in _get_language_string() below.
|
|
_ = lambda x: x
|
|
_LOCALE_NAMES = {
|
|
'ar': ('Arabic_Saudi Arabia', '1256', _("Arabic")),
|
|
'bg': ('Bulgrian_Bulgaria', '1251', _("Bulgarian")),
|
|
'br': (None, None, _("Breton")), #Windows has no translation for Breton
|
|
'ca': ('Catalan_Spain', '1252', _("Catalan")),
|
|
'cs': ('Czech_Czech Republic', '1250', _("Czech")),
|
|
'da': ('Danish_Denmark', '1252', _("Danish")),
|
|
'de': ('German_Germany', '1252', _("German")),
|
|
'el': ('Greek_Greece', '1253', _("Greek")),
|
|
'en': ('English_United States', '1252', _("English (USA)")),
|
|
'en_GB': ('English_United Kingdom', '1252', _("English")),
|
|
'eo': (None, None, _("Esperanto")), #Windows has no translation for Esperanto
|
|
'es': ('Spanish_Spain', '1252', _("Spanish")),
|
|
'fi': ('Finnish_Finland', '1252', _("Finnish")),
|
|
'fr': ('French_France', '1252', _("French")),
|
|
'ga': (None, None, _("Gaelic")), #Windows has no translation for Gaelic
|
|
'he': ('Hebrew_Israel', '1255', _("Hebrew")),
|
|
'hr': ('Croatian_Croatia', '1250', _("Croatian")),
|
|
'hu': ('Hungarian_Hungary', '1250', _("Hungarian")),
|
|
'it': ('Italian_Italy', '1252', _("Italian")),
|
|
'ja': ('Japanese_Japan', '932', _("Japanese")),
|
|
'lt': ('Lithuanian_Lithuania', '1252', _("Lithuanian")),
|
|
'mk': (None, None, _("Macedonian")), #Windows has no translation for Macedonian
|
|
'nb': ('Norwegian_Norway', '1252', _("Norwegian Bokmal")),
|
|
'nl': ('Dutch_Netherlands', '1252', _("Dutch")),
|
|
'nn': ('Norwegian-Nynorsk_Norway', '1252', _("Norwegian Nynorsk")),
|
|
'pl': ('Polish_Poland', '1250', _("Polish")),
|
|
'pt_BR': ('Portuguese_Brazil', '1252', _("Portuguese (Brazil)")),
|
|
'pt_PT': ('Portuguese_Portugal', '1252', _("Portuguese (Portugal)")),
|
|
'ro': ('Romanian_Romania', '1250', _("Romanian")),
|
|
'ru': ('Russian_Russia', '1251', _("Russian")),
|
|
'sk': ('Slovak_Slovakia', '1250', _("Slovak"),),
|
|
'sl': ('Slovenian_Slovenia', '1250', _("Slovenian")),
|
|
'sq': ('Albanian_Albania', '1250', _("Albanian")),
|
|
'sr': ('Serbian(Cyrillic)_Serbia and Montenegro', '1251', None), #Gramps's Serbian translation is not yet ready
|
|
'sv': ('Swedish_Sweden', '1252', _("Swedish")),
|
|
'tr': ('Turkish_Turkey', '1254', _("Turkish")),
|
|
'uk': ('Ukrainian_Ukraine', '1251', _("Ukrainian")),
|
|
'vi': ('Vietnamese_Viet Nam', '1258', _("Vietnamese")),
|
|
'zh_CN': ('Chinese_China', '936', _("Chinese Simplified")),
|
|
}
|
|
|
|
def _check_mswin_locale(locale):
|
|
msloc = None
|
|
try:
|
|
msloc = _LOCALE_NAMES[locale[:5]][:2]
|
|
locale = locale[:5]
|
|
except KeyError:
|
|
try:
|
|
msloc = _LOCALE_NAMES[locale[:2]][:2]
|
|
locale = locale[:2]
|
|
except KeyError:
|
|
return (None, None)
|
|
return (locale, msloc)
|
|
|
|
def _check_mswin_locale_reverse(locale):
|
|
for (loc, msloc) in _LOCALE_NAMES.items():
|
|
if msloc and locale == msloc[0]:
|
|
return (loc, msloc[1])
|
|
|
|
return (None, None)
|
|
|
|
#------------------------------------------------------------------------
|
|
#
|
|
# GrampsLocale Class
|
|
#
|
|
#------------------------------------------------------------------------
|
|
class GrampsLocale(object):
|
|
"""
|
|
Encapsulate a locale. This class is a sort-of-singleton: The
|
|
first instance created will query the environment and OSX defaults
|
|
for missing parameters (precedence is parameters passed to the
|
|
constructor, environment variables LANG, LC_COLLATE, LC_TIME,
|
|
etc., and LANGUAGE, OSX defaults settings when that's the
|
|
platform). Subsequent calls to the constructor with no or
|
|
identical parameters will return the same Grampslocale
|
|
object. Construction with different parameters will result in a
|
|
new GrampsLocale instance with the specified parameters, but any
|
|
parameters left out will be filled in from the first instance.
|
|
|
|
@localedir: The full path to the top level directory containing
|
|
the translation files. Defaults to sys.prefix/share/locale.
|
|
|
|
@lang: A single locale value which is used for unset locale.LC_FOO
|
|
settings.
|
|
|
|
@domain: The name of the applicable translation file. The default is
|
|
"gramps", indicating files in LC_MESSAGES named gramps.mo.
|
|
|
|
@languages: A list of two or five character codes corresponding to
|
|
subidrectries in the localedir, e.g. "fr" or "zh_CN".
|
|
"""
|
|
DEFAULT_TRANSLATION_STR = "default"
|
|
__first_instance = None
|
|
encoding = None
|
|
|
|
def __new__(cls, localedir=None, lang=None, domain=None, languages=None):
|
|
if not GrampsLocale.__first_instance:
|
|
cls.__first_instance = super(GrampsLocale, cls).__new__(cls)
|
|
cls.__first_instance.initialized = False
|
|
return cls.__first_instance
|
|
|
|
if not cls.__first_instance.initialized:
|
|
raise RuntimeError("Second GrampsLocale created before first one was initialized")
|
|
if ((lang is None or lang == cls.__first_instance.lang)
|
|
and (localedir is None or localedir == cls.__first_instance.localedir)
|
|
and (domain is None or domain == cls.__first_instance.localedomain)
|
|
and (languages is None or len(languages) == 0 or
|
|
languages == cls.__first_instance.languages)):
|
|
return cls.__first_instance
|
|
|
|
return super(GrampsLocale, cls).__new__(cls)
|
|
|
|
def _win_init_environment(self):
|
|
"""
|
|
The Windows implementation of Python ignores environment
|
|
variables when setting the locale; it only pays attention to
|
|
the control panel language settings -- which for practical
|
|
purposes limits one to the language for which one purchased
|
|
Windows. This function enables using alternative
|
|
localizations.
|
|
"""
|
|
|
|
if 'LANG' in os.environ:
|
|
(lang, loc) = _check_mswin_locale(os.environ['LANG'])
|
|
if loc:
|
|
locale.setlocale(locale.LC_ALL, '.'.join(loc))
|
|
self.lang = lang
|
|
self.encoding = loc[1]
|
|
else:
|
|
LOG.debug("%%LANG%% value %s not usable", os.environ['LANG'])
|
|
if not self.lang:
|
|
locale.setlocale(locale.LC_ALL, '')
|
|
(lang, encoding) = locale.getlocale()
|
|
loc = _check_mswin_locale_reverse(lang)
|
|
if loc[0]:
|
|
self.lang = loc[0]
|
|
self.encoding = loc[1]
|
|
else:
|
|
(lang, loc) = _check_mswin_locale(lang)
|
|
if lang:
|
|
self.lang = lang
|
|
self.encoding = loc[1]
|
|
else:
|
|
LOG.debug("No usable locale found in environment")
|
|
|
|
if not self.lang:
|
|
self.lang = 'C'
|
|
self.encoding = 'cp1252'
|
|
|
|
if 'LC_MESSAGES' in os.environ:
|
|
lang = self.check_available_translations(os.environ['LC_MESSAGES'])
|
|
if lang:
|
|
self.language = [lang]
|
|
else:
|
|
LOG.debug("No translation for %%LC_MESSAGES%% locale")
|
|
if 'LANGUAGE' in os.environ:
|
|
language = [x for x in [self.check_available_translations(l)
|
|
for l in os.environ["LANGUAGE"].split(":")]
|
|
if x]
|
|
if language:
|
|
self.language = language
|
|
else:
|
|
LOG.debug("No languages with translations found in %%LANGUAGES%%")
|
|
if not self.language:
|
|
self.language = [self.lang[:5]]
|
|
|
|
if 'LC_COLLATE' in os.environ:
|
|
coll = os.environ['LC_COLLATE']
|
|
if HAVE_ICU:
|
|
if coll[:5] in ICU_LOCALES:
|
|
self.collation = coll
|
|
else:
|
|
self.collation = self.lang
|
|
else:
|
|
(coll, loc) = _check_mswin_locale(coll)
|
|
if not loc:
|
|
(coll, loc) = _check_mswin_locale(self.lang)
|
|
self.collation = '.'.join(loc)
|
|
locale.setlocale(locale.LC_COLLATE, self.collation )
|
|
else:
|
|
if HAVE_ICU:
|
|
self.collation = self.lang
|
|
else:
|
|
(coll, loc) = _check_mswin_locale(self.lang)
|
|
if loc:
|
|
self.collation = '.'.join(loc)
|
|
else:
|
|
self.collation = 'C'
|
|
locale.setlocale(locale.LC_COLLATE, self.collation )
|
|
|
|
# We can't import datahandler stuff or we'll get a circular
|
|
# dependency, so we rely on the available translations list
|
|
if 'LC_TIME' in os.environ:
|
|
self.calendar = self.check_available_translations(os.environ['LC_TIME']) or self.lang
|
|
else:
|
|
self.calendar = self.lang
|
|
|
|
if 'LC_NUMERIC' in os.environ:
|
|
self.numeric = os.environ['LC_NUMERIC']
|
|
else:
|
|
self.numeric = self.lang
|
|
|
|
if 'LC_MONETARY' in os.environ:
|
|
self.currency = os.environ['LC_MONETARY']
|
|
else:
|
|
self.currency = self.lang
|
|
|
|
def _init_from_environment(self):
|
|
|
|
def _check_locale(locale):
|
|
if not locale[0]:
|
|
return False
|
|
lang = self.check_available_translations(locale[0])
|
|
if not lang:
|
|
return False
|
|
self.lang = locale[0]
|
|
self.encoding = locale[1]
|
|
self.language = [lang]
|
|
return True
|
|
|
|
_failure = False
|
|
try:
|
|
locale.setlocale(locale.LC_ALL, '')
|
|
if not _check_locale(locale.getlocale()):
|
|
if not _check_locale(locale.getdefaultlocale()):
|
|
LOG.debug("Usable locale not found, localization settings ignored.");
|
|
self.lang = 'C'
|
|
self.encoding = 'ascii'
|
|
self.language = ['en']
|
|
_failure = True
|
|
|
|
except locale.Error as err:
|
|
LOG.debug("Locale error %s, localization settings ignored.",
|
|
err);
|
|
self.lang = 'C'
|
|
self.encoding = 'ascii'
|
|
self.language = ['en']
|
|
_failure = True
|
|
|
|
#LC_MESSAGES
|
|
(loc, enc) = locale.getlocale(locale.LC_MESSAGES)
|
|
if loc:
|
|
language = self.check_available_translations(loc)
|
|
if language:
|
|
self.language = [language]
|
|
else:
|
|
LOG.debug("No translation for LC_MESSAGES locale %s", loc)
|
|
|
|
# $LANGUAGE overrides $LANG, $LC_MESSAGES
|
|
if "LANGUAGE" in os.environ:
|
|
language = [x for x in [self.check_available_translations(l)
|
|
for l in os.environ["LANGUAGE"].split(":")]
|
|
if x]
|
|
if language:
|
|
self.language = language
|
|
if not self.lang.startswith(self.language[0]):
|
|
LOG.debug("Overiding locale setting %s with LANGUAGE setting %s", self.lang, self.language[0])
|
|
elif _failure:
|
|
LOG.warning("No valid locale settings found, using US English")
|
|
|
|
if HAVE_ICU:
|
|
self.calendar = locale.getlocale(locale.LC_TIME)[0] or self.lang[:5]
|
|
self.collation = locale.getlocale(locale.LC_COLLATE)[0] or self.lang[:5]
|
|
else:
|
|
loc = locale.getlocale(locale.LC_TIME)
|
|
if loc and self.check_available_translations(loc[0]):
|
|
self.calendar = '.'.join(loc)
|
|
else:
|
|
self.calendar = self.lang
|
|
|
|
loc = locale.getlocale(locale.LC_COLLATE)
|
|
if loc and loc[0]:
|
|
self.collation = '.'.join(loc)
|
|
else:
|
|
self.collation = self.lang
|
|
|
|
loc = locale.getlocale(locale.LC_NUMERIC)
|
|
if loc and loc[0]:
|
|
self.numeric = '.'.join(loc)
|
|
else:
|
|
self.numeric = self.lang
|
|
|
|
loc = locale.getlocale(locale.LC_MONETARY)
|
|
if loc and loc[0]:
|
|
self.currency = '.'.join(loc)
|
|
else:
|
|
self.currency = self.lang
|
|
|
|
def _win_bindtextdomain(self, localedomain, localedir):
|
|
"""
|
|
Help routine for loading and setting up libintl attributes
|
|
Returns libintl
|
|
"""
|
|
from ctypes import cdll
|
|
try:
|
|
libintl = cdll.LoadLibrary('libintl-8')
|
|
libintl.bindtextdomain(localedomain,
|
|
localedir.encode(sys.getfilesystemencoding()))
|
|
libintl.textdomain(localedomain)
|
|
libintl.bind_textdomain_codeset(localedomain, "UTF-8")
|
|
|
|
except WindowsError:
|
|
LOG.warning("Localization library libintl not on %PATH%, localization will be incomplete")
|
|
|
|
def __init_first_instance(self):
|
|
"""
|
|
Initialize the primary locale from whatever might be
|
|
available. We only do this once, and the resulting
|
|
GrampsLocale is returned by default.
|
|
"""
|
|
global _hdlr
|
|
_hdlr = logging.StreamHandler()
|
|
_hdlr.setFormatter(logging.Formatter(fmt="%(name)s.%(levelname)s: %(message)s"))
|
|
LOG.addHandler(_hdlr)
|
|
|
|
# Even the first instance can be overridden by passing lang
|
|
# and languages to the constructor. If it isn't (which is the
|
|
# expected behavior), do platform-specific setup:
|
|
if not (self.lang and self.language):
|
|
if sys.platform == 'darwin':
|
|
from . import maclocale
|
|
maclocale.mac_setup_localization(self)
|
|
elif sys.platform == 'win32':
|
|
self._win_init_environment()
|
|
else:
|
|
self._init_from_environment()
|
|
else:
|
|
self.numeric = self.currency = self.calendar = self.collation = self.lang
|
|
|
|
if not self.lang:
|
|
self.lang = 'en_US.UTF-8'
|
|
if not self.language:
|
|
self.language.append('en')
|
|
if not self.localedir and not self.lang.startswith('en'):
|
|
LOG.warning("No translations for %s were found, setting localization to U.S. English", self.localedomain)
|
|
self.lang = 'en_US.UTF-8'
|
|
self.language = ['en']
|
|
|
|
#Next, we need to know what is the encoding from the native
|
|
#environment. This is used by python standard library funcions which
|
|
#localize their output, e.g. time.strftime(). NB: encoding is a class variable.
|
|
if not self.encoding:
|
|
self.encoding = (locale.getpreferredencoding()
|
|
or sys.getdefaultencoding())
|
|
LOG.debug("Setting encoding to %s", self.encoding)
|
|
#Ensure that output is encoded correctly to stdout and stderr. This is
|
|
#much less cumbersome and error-prone than encoding individual outputs
|
|
#and better handles the differences between Python 2 and Python 3:
|
|
_encoding = sys.stdout.encoding or sys.getdefaultencoding()
|
|
if sys.version_info[0] < 3:
|
|
sys.stdout = codecs.getwriter(_encoding)(sys.stdout,
|
|
'backslashreplace')
|
|
sys.stderr = codecs.getwriter(_encoding)(sys.stderr,
|
|
'backslashreplace')
|
|
else:
|
|
sys.stdout = codecs.getwriter(_encoding)(sys.stdout.detach(),
|
|
'backslashreplace')
|
|
sys.stderr = codecs.getwriter(_encoding)(sys.stderr.detach(),
|
|
'backslashreplace')
|
|
|
|
|
|
# Make sure that self.lang and self.language are reflected
|
|
# back into the environment for Gtk to use when its
|
|
# initialized. If self.lang isn't 'C', make sure that it has a
|
|
# 'UTF-8' suffix, because that's all that GtkBuilder can
|
|
# digest.
|
|
|
|
# Gtk+ has an 'en' po, but we don't. This is worked-around for
|
|
# our GrampsTranslation class but that isn't used to retrieve
|
|
# translations in GtkBuilder (glade), a direct call to libintl
|
|
# (gettext) is. If 'en' is in the translation list it gets
|
|
# skipped in favor of the next language, which can cause
|
|
# inappropriate translations of strings in glade/ui files. To
|
|
# prevent this, if 'en' is in self.language it's the last
|
|
# entry:
|
|
|
|
if 'en' in self.language:
|
|
self.language = self.language[:self.language.index('en') + 1]
|
|
|
|
# Linux note: You'll get unsupported locale errors from Gtk
|
|
# and untranslated strings if the requisite UTF-8 locale isn't
|
|
# installed. This is particularly a problem on Debian and
|
|
# Debian-derived distributions which by default don't install
|
|
# a lot of locales.
|
|
lang = locale.normalize(self.language[0] if self.language[0] else 'C')
|
|
check_lang = lang.split('.')
|
|
if not check_lang[0] in ('C', 'en'):
|
|
if len(check_lang) < 2 or check_lang[1] not in ("utf-8", "UTF-8"):
|
|
lang = '.'.join((check_lang[0], 'UTF-8'))
|
|
|
|
os.environ["LANG"] = lang
|
|
#We need to convert 'en' and 'en_US' to 'C' to avoid confusing
|
|
#GtkBuilder when it's retrieving strings from our Glade files
|
|
#since we have neither an en.po nor an en_US.po.
|
|
|
|
os.environ["LANGUAGE"] = ':'.join(self.language)
|
|
|
|
# GtkBuilder uses GLib's g_dgettext wrapper, which oddly is bound
|
|
# with locale instead of gettext. Win32 doesn't support bindtextdomain.
|
|
if self.localedir:
|
|
if not sys.platform == 'win32':
|
|
locale.bindtextdomain(self.localedomain, self.localedir)
|
|
else:
|
|
self._win_bindtextdomain(self.localedomain, self.localedir)
|
|
|
|
def _init_secondary_locale(self):
|
|
"""
|
|
Init a secondary locale. Secondary locales are used to provide
|
|
an alternate localization to the one used for the UI; for
|
|
example, some reports offer the option to use a different
|
|
language.
|
|
"""
|
|
if not self.localedir:
|
|
LOG.warning("No Localedir provided, unable to find translations")
|
|
|
|
if not self.localedomain:
|
|
if _firstlocaledomain:
|
|
self.localedomain = _first.localedomain
|
|
else:
|
|
self.localedomain = "gramps"
|
|
|
|
_first = self._GrampsLocale__first_instance
|
|
if not self.lang and _first.lang:
|
|
self.lang = _first.lang
|
|
|
|
if not self.language:
|
|
if self.lang:
|
|
trans = self.check_available_translations(self.lang)
|
|
if trans:
|
|
self.language = [trans]
|
|
|
|
if not self.language and _first.language:
|
|
self.language = _first.language
|
|
|
|
self.calendar = self.collation = self.lang
|
|
|
|
def __init__(self, localedir=None, lang=None, domain=None, languages=None):
|
|
"""
|
|
Init a GrampsLocale. Run __init_first_instance() to set up the
|
|
environment if this is the first run. Return __first_instance
|
|
otherwise if called without arguments.
|
|
"""
|
|
global _hdlr
|
|
#initialized is special, used only for the "first instance",
|
|
#and created by __new__(). It's used to prevent re-__init__ing
|
|
#__first_instance when __new__() returns its pointer.
|
|
if hasattr(self, 'initialized') and self.initialized:
|
|
return
|
|
_first = self._GrampsLocale__first_instance
|
|
|
|
# Everything breaks without localedir, so get that set up
|
|
# first. Warnings are logged in _init_first_instance or
|
|
# _init_secondary_locale if this comes up empty.
|
|
if localedir and os.path.exists(os.path.abspath(localedir)):
|
|
self.localedir = localedir
|
|
elif _first and _first.localedir:
|
|
self.localedir = _first.localedir
|
|
else:
|
|
self.localedir = None
|
|
|
|
self.lang = lang
|
|
self.localedomain = domain or 'gramps'
|
|
if languages:
|
|
self.language = [x for x in [self.check_available_translations(l)
|
|
for l in languages.split(":")]
|
|
if x]
|
|
else:
|
|
self.language = None
|
|
|
|
#For alternate collation sequences. Works only with ICU, and
|
|
#set only on Macs.
|
|
self.coll_qualifier = None
|
|
_first = self._GrampsLocale__first_instance
|
|
if self == _first:
|
|
self._GrampsLocale__init_first_instance()
|
|
else:
|
|
self._init_secondary_locale()
|
|
|
|
self.icu_locales = {}
|
|
self.collator = None
|
|
if HAVE_ICU:
|
|
self.icu_locales["default"] = Locale.createFromName(self.lang)
|
|
if self.collation and self.collation != self.lang:
|
|
self.icu_locales["collation"] = Locale.createFromName(self.collation)
|
|
else:
|
|
self.icu_locales["collation"] = self.icu_locales["default"]
|
|
try:
|
|
self.collator = Collator.createInstance(self.icu_locales["collation"])
|
|
except ICUError as err:
|
|
LOG.warning("Unable to create collator: %s", str(err))
|
|
self.collator = None
|
|
|
|
try:
|
|
self.translation = self._get_translation(self.localedomain,
|
|
self.localedir,
|
|
self.language)
|
|
except ValueError:
|
|
LOG.warning("Unable to find translation for languages in %s, using US English", ':'.join(self.language))
|
|
self.translation = GrampsNullTranslations()
|
|
self.translation._language = "en"
|
|
|
|
if _hdlr:
|
|
LOG.removeHandler(_hdlr)
|
|
_hdlr = None
|
|
self._dd = self._dp = None
|
|
#Guards against running twice on the first instance.
|
|
self.initialized = True
|
|
|
|
def _get_translation(self, domain = None,
|
|
localedir = None,
|
|
languages=None):
|
|
"""
|
|
Get a translation of one of our classes. Doesn't return the
|
|
singleton so that it can be used by get_addon_translation()
|
|
"""
|
|
if not domain:
|
|
domain = self.localedomain
|
|
if not languages:
|
|
languages = self.language
|
|
if not localedir:
|
|
localedir = self.localedir
|
|
|
|
for lang in languages:
|
|
if gettext.find(domain, localedir, [lang]):
|
|
translator = gettext.translation(domain, localedir,
|
|
[lang],
|
|
class_ = GrampsTranslations)
|
|
translator._language = lang
|
|
return translator
|
|
|
|
elif lang.startswith("en") or lang.startswith("C"):
|
|
translator = GrampsNullTranslations()
|
|
translator._language = "en"
|
|
return translator
|
|
|
|
if not languages or len(languages) == 0:
|
|
LOG.warning("No language provided, using US English")
|
|
else:
|
|
raise ValueError("No usable translations in %s" %
|
|
':'.join(languages))
|
|
translator = GrampsNullTranslations()
|
|
translator._language = "en"
|
|
return translator
|
|
|
|
def _get_language_string(self, lang_code):
|
|
"""
|
|
Given a language code of the form "lang_region", return a text string
|
|
representing that language.
|
|
"""
|
|
try:
|
|
lang = _LOCALE_NAMES[lang_code][2]
|
|
except KeyError:
|
|
try:
|
|
lang = _LOCALE_NAMES[lang_code[:2]][2]
|
|
except KeyError:
|
|
LOG.debug("Gramps has no translation for %s", lang_code)
|
|
lang = None
|
|
except IndexError as err:
|
|
LOG.debug("Bad Index for tuple %s\n" % _LOCALE_NAMES[lang_code][0])
|
|
lang = None
|
|
|
|
if lang:
|
|
return self.translation.gettext(lang)
|
|
return lang
|
|
|
|
#-------------------------------------------------------------------------
|
|
#
|
|
# Properties
|
|
#
|
|
#-------------------------------------------------------------------------
|
|
@property
|
|
def date_displayer(self):
|
|
"""
|
|
Return the locale's date displayer; if it hasn't already been
|
|
cached, set it from datehandler.LANG_TO_DISPLAY. If one isn't
|
|
available for the selected locale, attempt to fall back on the
|
|
first_instance's locale before settling on the 'C' displayer.
|
|
NB: This is the getter for the date_displayer property
|
|
"""
|
|
if self._dd:
|
|
return self._dd
|
|
|
|
from gramps.gen.config import config
|
|
try:
|
|
val = config.get('preferences.date-format')
|
|
except AttributeError:
|
|
val = 0;
|
|
|
|
from gramps.gen.datehandler import LANG_TO_DISPLAY as displayers
|
|
_first = self._GrampsLocale__first_instance
|
|
if self.calendar in displayers:
|
|
self._dd = displayers[self.calendar](val)
|
|
elif self.calendar[:2] in displayers:
|
|
self._dd = displayers[self.calendar[:2]](val)
|
|
elif self != _first and _first.calendar in displayers:
|
|
self._dd = displayers[_first.calendar](val)
|
|
elif self != _first and _first.calendar[:2] in displayers:
|
|
self._dd = displayers[_first.calendar[:2]](val)
|
|
else:
|
|
self._dd = displayers['C'](val)
|
|
|
|
return self._dd
|
|
|
|
@property
|
|
def date_parser(self):
|
|
"""
|
|
Return the locale's date parser; if it hasn't already been
|
|
cached, set it from datehandler.LANG_TO_PARSER. If one isn't
|
|
available for the selected locale, attempt to fall back on the
|
|
first_instance's locale before settling on the 'C' parser.
|
|
NB: This is the getter for the date_parser property
|
|
"""
|
|
if self._dp:
|
|
return self._dp
|
|
|
|
from gramps.gen.datehandler import LANG_TO_PARSER as parsers
|
|
_first = self._GrampsLocale__first_instance
|
|
if self.calendar in parsers:
|
|
self._dp = parsers[self.calendar]()
|
|
elif self.calendar[:2] in parsers:
|
|
self._dp = parsers[self.calendar]()
|
|
elif self != _first and _first.calendar in parsers:
|
|
self._dp = parsers[_first.calendar]()
|
|
elif self != _first and _first.calendar[:2] in parsers:
|
|
self._dp = parsers[_first.calendar[:2]]()
|
|
else:
|
|
self._dp = parsers['C']()
|
|
|
|
return self._dp
|
|
|
|
#-------------------------------------------------------------------------
|
|
#
|
|
# Public Functions
|
|
#
|
|
#-------------------------------------------------------------------------
|
|
|
|
def get_localedomain(self):
|
|
"""
|
|
Get the LOCALEDOMAIN used for the Gramps application.
|
|
Required by gui/glade.py to pass to Gtk.Builder
|
|
"""
|
|
return self.localedomain
|
|
|
|
def get_language_list(self):
|
|
"""
|
|
Return the list of configured languages. Used by
|
|
ViewManager.check_for_updates to select the language for the
|
|
addons descriptions.
|
|
"""
|
|
return self.language
|
|
|
|
|
|
def get_addon_translator(self, filename, domain="addon",
|
|
languages=None):
|
|
"""
|
|
Get a translator for an addon.
|
|
|
|
filename - filename of a file in directory with full path, or
|
|
None to get from self.
|
|
domain - the name of the .mo file under the LANG/LC_MESSAGES dir
|
|
languages - a list of languages to force
|
|
returns - a gettext.translation object
|
|
|
|
Example:
|
|
_ = glocale.get_addon_translator(languages=["fr_BE.utf8"]).gettext
|
|
|
|
See the python gettext documentation.
|
|
Assumes path/filename
|
|
path/locale/LANG/LC_MESSAGES/addon.mo.
|
|
"""
|
|
gramps_translator = self._get_translation()
|
|
|
|
path = self.localedir
|
|
if filename:
|
|
path = os.path.join(os.path.dirname(os.path.abspath(filename)), "locale")
|
|
if languages:
|
|
addon_translator = self._get_translation(domain,
|
|
path,
|
|
languages=languages)
|
|
else:
|
|
addon_translator = self._get_translation(domain, path)
|
|
gramps_translator.add_fallback(addon_translator)
|
|
return gramps_translator # with a language fallback
|
|
|
|
def get_available_translations(self, localedir = None, localedomain = None):
|
|
"""
|
|
Get a list of available translations.
|
|
|
|
:returns: A list of translation languages.
|
|
:rtype: unicode[]
|
|
|
|
"""
|
|
languages = ["en"]
|
|
|
|
if not localedir and self.localedir:
|
|
localedir = self.localedir
|
|
else:
|
|
return languages
|
|
|
|
if not localedomain and self.localedomain:
|
|
localedomain = self.localedomain
|
|
else:
|
|
localedomain = 'gramps'
|
|
|
|
for langdir in os.listdir(self.localedir):
|
|
mofilename = os.path.join(localedir, langdir,
|
|
"LC_MESSAGES",
|
|
"%s.mo" % localedomain )
|
|
if os.path.exists(mofilename):
|
|
languages.append(langdir)
|
|
|
|
languages.sort()
|
|
|
|
return languages
|
|
|
|
def check_available_translations(self, locale):
|
|
"""
|
|
Test a locale for having a translation available
|
|
locale -- string with standard language code, locale code, or name
|
|
"""
|
|
if not self.localedir:
|
|
return None
|
|
#Note that this isn't a typo for self.language; self.languages
|
|
#is cached so we don't have to query the file system every
|
|
#time this function is called.
|
|
if not hasattr(self, 'languages'):
|
|
self.languages = self.get_available_translations()
|
|
|
|
if not locale:
|
|
return None
|
|
|
|
if locale[:5] in self.languages:
|
|
return locale[:5]
|
|
if locale[:2] in self.languages:
|
|
return locale[:2]
|
|
|
|
return None
|
|
|
|
def get_language_dict(self):
|
|
'''
|
|
return a dictionary of language names : codes for use by language
|
|
pickers.
|
|
'''
|
|
return {self._get_language_string(code) : code
|
|
for code in self.get_available_translations()
|
|
if self._get_language_string(code)}
|
|
|
|
def trans_objclass(self, objclass_str):
|
|
"""
|
|
Translates objclass_str into "... %s", where objclass_str
|
|
is 'Person', 'person', 'Family', 'family', etc.
|
|
"""
|
|
_ = self.translation.gettext
|
|
objclass = objclass_str.lower()
|
|
if objclass == "person":
|
|
return _("the person")
|
|
elif objclass == "family":
|
|
return _("the family")
|
|
elif objclass == "place":
|
|
return _("the place")
|
|
elif objclass == "event":
|
|
return _("the event")
|
|
elif objclass == "repository":
|
|
return _("the repository")
|
|
elif objclass == "note":
|
|
return _("the note")
|
|
elif objclass in ["media", "mediaobject"]:
|
|
return _("the media")
|
|
elif objclass == "source":
|
|
return _("the source")
|
|
elif objclass == "filter":
|
|
return _("the filter")
|
|
else:
|
|
return _("See details")
|
|
|
|
def getfilesystemencoding(self):
|
|
"""
|
|
If the locale isn't configured correctly, this will return
|
|
'ascii' or 'ANSI_X3.4-1968' or some other unfortunate
|
|
result. Current unix systems all encode filenames in utf-8,
|
|
and Microsoft Windows uses utf-16 (which they call mbcs). Make
|
|
sure we return the right value.
|
|
"""
|
|
encoding = sys.getfilesystemencoding()
|
|
|
|
if encoding in ("utf-8", "UTF-8", "utf8", "UTF8", "mbcs", "MBCS"):
|
|
return encoding
|
|
|
|
return "utf-8"
|
|
|
|
def sort_key(self, string):
|
|
"""
|
|
Return a value suitable to pass to the "key" parameter of sorted()
|
|
"""
|
|
|
|
if HAVE_ICU and self.collator:
|
|
#ICU can digest strings and unicode
|
|
return self.collator.getCollationKey(string).getByteArray()
|
|
else:
|
|
try:
|
|
if sys.version_info[0] < 3 and isinstance(string, unicode):
|
|
key = locale.strxfrm(string.encode("utf-8", "replace"))
|
|
else:
|
|
key = locale.strxfrm(string)
|
|
|
|
except Exception as err:
|
|
LOG.warn("Failed to obtain key for %s because %s",
|
|
self.collation, str(err))
|
|
return string
|
|
return key
|
|
|
|
def strcoll(self, string1, string2):
|
|
"""
|
|
Given two localized strings, compare them and return -1 if
|
|
string1 would sort first, 1 if string2 would, and 0 if
|
|
they are the same.
|
|
"""
|
|
key1 = self.sort_key(string1)
|
|
key2 = self.sort_key(string2)
|
|
return (-1 if key1 < key2 else (1 if key1 > key2 else 0))
|
|
|
|
|
|
def get_date(self, date):
|
|
"""
|
|
Return a string representing the date appropriate for the language being
|
|
translated.
|
|
|
|
:param date: The date to be represented.
|
|
:type date: :class:`~gen.lib.date.Date`
|
|
:returns: The date as text in the proper language.
|
|
:rtype: unicode
|
|
"""
|
|
return self.date_displayer.display(date)
|
|
|
|
def get_type(self, name):
|
|
"""
|
|
Return a string representing the name appropriate for the language being
|
|
translated.
|
|
|
|
:param name: The name type to be represented.
|
|
:returns: The name as text in the proper language.
|
|
:rtype: unicode
|
|
"""
|
|
from gramps.gen.lib.grampstype import GrampsType
|
|
return GrampsType.xml_str(name)
|
|
|
|
def format(self, format, val, grouping=False, monetary=False):
|
|
"""
|
|
Format a number in the current numeric locale. See python's
|
|
locale.format for details. ICU's formatting codes are
|
|
incompatible with locale's, so just use locale.format for now.
|
|
"""
|
|
return locale.format(format, val, grouping, monetary)
|
|
|
|
def format_string(self, format, val, grouping=False):
|
|
"""
|
|
Format a string in the current numeric locale. See python's
|
|
locale.format_string for details. ICU's message formatting codes are
|
|
incompatible with locale's, so just use locale.format_string
|
|
for now.
|
|
"""
|
|
return locale.format_string(format, val, grouping)
|
|
|
|
def float(self, val):
|
|
"""
|
|
Parse a string to a floating point number. Uses locale.atof(),
|
|
in future with ICU present will use icu.NumberFormat.parse().
|
|
"""
|
|
try:
|
|
return locale.atof(val)
|
|
except ValueError:
|
|
point = locale.localeconv()['decimal_point']
|
|
sep = locale.localeconv()['thousands_sep']
|
|
try:
|
|
if point == ',':
|
|
return locale.atof(val.replace(' ', sep).replace('.', sep))
|
|
elif point == '.':
|
|
return locale.atof(val.replace(' ', sep).replace(',', sep))
|
|
else:
|
|
return None
|
|
except ValueError:
|
|
return None
|
|
|
|
#-------------------------------------------------------------------------
|
|
#
|
|
# Translations Classes
|
|
#
|
|
#-------------------------------------------------------------------------
|
|
class GrampsTranslations(gettext.GNUTranslations):
|
|
"""
|
|
Overrides and extends gettext.GNUTranslations. See the Python gettext
|
|
"Class API" documentation for how to use this.
|
|
"""
|
|
def language(self):
|
|
"""
|
|
Return the target languge of this translations object.
|
|
"""
|
|
return self._language
|
|
|
|
def gettext(self, msgid):
|
|
"""
|
|
Obtain translation of gettext, return a unicode object
|
|
:param msgid: The string to translated.
|
|
:type msgid: unicode
|
|
:returns: Translation or the original.
|
|
:rtype: unicode
|
|
"""
|
|
# If msgid =="" then gettext will return po file header
|
|
# and that's not what we want.
|
|
if len(msgid.strip()) == 0:
|
|
return msgid
|
|
if sys.version_info[0] < 3:
|
|
return gettext.GNUTranslations.ugettext(self, msgid)
|
|
else:
|
|
return gettext.GNUTranslations.gettext(self, msgid)
|
|
|
|
def ngettext(self, singular, plural, num):
|
|
"""
|
|
The translation of singular/plural is returned unless the translation is
|
|
not available and the singular contains the separator. In that case,
|
|
the returned value is the singular.
|
|
|
|
:param singular: The singular form of the string to be translated.
|
|
may contain a context seperator
|
|
:type singular: unicode
|
|
:param plural: The plural form of the string to be translated.
|
|
:type plural: unicode
|
|
:param num: the amount for which to decide the translation
|
|
:type num: int
|
|
:returns: Translation or the original.
|
|
:rtype: unicode
|
|
"""
|
|
if sys.version_info[0] < 3:
|
|
return gettext.GNUTranslations.ungettext(self, singular,
|
|
plural, num)
|
|
else:
|
|
return gettext.GNUTranslations.ngettext(self, singular,
|
|
plural, num)
|
|
|
|
def sgettext(self, msgid, sep='|'):
|
|
"""
|
|
Strip the context used for resolving translation ambiguities.
|
|
|
|
The translation of msgid is returned unless the translation is
|
|
not available and the msgid contains the separator. In that case,
|
|
the returned value is the portion of msgid following the last
|
|
separator. Default separator is '|'.
|
|
|
|
:param msgid: The string to translated.
|
|
:type msgid: unicode
|
|
:param sep: The separator marking the context.
|
|
:type sep: unicode
|
|
:returns: Translation or the original with context stripped.
|
|
:rtype: unicode
|
|
"""
|
|
msgval = self.gettext(msgid)
|
|
if msgval == msgid:
|
|
sep_idx = msgid.rfind(sep)
|
|
msgval = msgid[sep_idx+1:]
|
|
return msgval
|
|
|
|
class GrampsNullTranslations(gettext.NullTranslations):
|
|
"""
|
|
Extends gettext.NullTranslations to provide the sgettext method.
|
|
|
|
Note that it's necessary for msgid to be unicode. If it's not,
|
|
neither will be the returned string.
|
|
"""
|
|
def sgettext(self, msgid, sep='|'):
|
|
msgval = self.gettext(msgid)
|
|
if msgval == msgid:
|
|
sep_idx = msgid.rfind(sep)
|
|
msgval = msgid[sep_idx+1:]
|
|
return msgval
|
|
|
|
def language(self):
|
|
"""
|
|
The null translation returns the raw msgids, which are in English
|
|
"""
|
|
return "en"
|