Permits sorting by localized language name. Also hides the language code, which the user doesn't really care about. Removes get_language_string from libtranslate.py, no longer needed. svn: r21236
594 lines
22 KiB
Python
594 lines
22 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
|
|
#
|
|
#------------------------------------------------------------------------
|
|
import gettext
|
|
import sys
|
|
import os
|
|
import locale
|
|
import logging
|
|
LOG = logging.getLogger("gramps.gen.util.grampslocale")
|
|
#-------------------------------------------------------------------------
|
|
#
|
|
# gramps modules
|
|
#
|
|
#-------------------------------------------------------------------------
|
|
from ..const import LOCALE_DIR
|
|
from ..constfunc import mac, win, UNITYPE
|
|
|
|
#------------------------------------------------------------------------
|
|
#
|
|
# 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".
|
|
"""
|
|
__first_instance = 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 __init_first_instance(self, localedir=None, lang=None,
|
|
domain=None, language=None):
|
|
|
|
if localedir and os.path.exists(localedir):
|
|
self.localedir = localedir
|
|
else:
|
|
if ("GRAMPSI18N" in os.environ
|
|
and os.path.exists(os.environ["GRAMPSI18N"])):
|
|
self.localedir = os.environ["GRAMPSI18N"]
|
|
elif os.path.exists(LOCALE_DIR):
|
|
self.localedir = LOCALE_DIR
|
|
elif os.path.exists(os.path.join(sys.prefix, "share", "locale")):
|
|
self.localedir = os.path.join(sys.prefix, "share", "locale")
|
|
else:
|
|
if not lang:
|
|
lang = os.environ.get('LANG', 'en')
|
|
if lang and lang[:2] == 'en':
|
|
pass # No need to display warning, we're in English
|
|
else:
|
|
LOG.warning('Locale dir does not exist at %s', LOCALE_DIR)
|
|
LOG.warning('Running python setup.py install --prefix=YourPrefixDir might fix the problem')
|
|
|
|
if not self.localedir:
|
|
#No localization files, no point in continuing
|
|
return
|
|
if domain:
|
|
self.localedomain = domain
|
|
else:
|
|
self.localedomain = 'gramps'
|
|
|
|
if not language or not isinstance(language, list):
|
|
language = []
|
|
else:
|
|
language = [l for l in languages
|
|
if l in self.get_available_translations()]
|
|
|
|
if mac():
|
|
from . import maclocale
|
|
(self.lang, self.language) = maclocale.mac_setup_localization(self, lang, language)
|
|
else:
|
|
if not lang:
|
|
lang = ' '
|
|
try:
|
|
lang = os.environ["LANG"]
|
|
except KeyError:
|
|
lang = locale.getlocale()[0]
|
|
if not lang:
|
|
try:
|
|
lang = locale.getdefaultlocale()[0] + '.UTF-8'
|
|
except TypeError:
|
|
LOG.warning('Unable to determine your Locale, using English')
|
|
lang = 'C.UTF-8'
|
|
self.lang = lang
|
|
|
|
if not language or len(language) == 0:
|
|
if "LANGUAGE" in os.environ:
|
|
language = [l for l in os.environ["LANGUAGE"].split(":")
|
|
if l in self.get_available_translations()]
|
|
self.language = language
|
|
elif not lang == "C.UTF-8":
|
|
self.language = [lang[0:2]]
|
|
else:
|
|
self.language = ["en"]
|
|
|
|
#GtkBuilder depends on reading Glade files as UTF-8 and crashes if it
|
|
#doesn't, so set $LANG to have a UTF-8 locale. NB: This does *not*
|
|
#affect locale.getpreferredencoding() or sys.getfilesystemencoding()
|
|
#which are set by python long before we get here.
|
|
check_lang = self.lang.split('.')
|
|
if len(check_lang) < 2 or check_lang[1] not in ["utf-8", "UTF-8"]:
|
|
self.lang = '.'.join((check_lang[0], 'UTF-8'))
|
|
os.environ["LANG"] = self.lang
|
|
# Set Gramps's translations
|
|
try:
|
|
# First try the environment to preserve individual variables
|
|
locale.setlocale(locale.LC_ALL, '')
|
|
try:
|
|
#Then set LC_MESSAGES to lang
|
|
locale.setlocale(locale.LC_MESSAGES, lang)
|
|
except locale.Error:
|
|
LOG.warning("Unable to set translations to %s, locale not found.", lang)
|
|
except locale.Error:
|
|
# That's not a valid locale -- on Linux, probably not installed.
|
|
try:
|
|
# First fallback is lang
|
|
locale.setlocale(locale.LC_ALL, self.lang)
|
|
LOG.warning("Setting locale to individual LC_ variables failed, falling back to %s.", lang)
|
|
|
|
except locale.Error:
|
|
# No good, set the default encoding to C.UTF-8. Don't
|
|
# mess with anything else.
|
|
locale.setlocale(locale.LC_ALL, 'C.UTF-8')
|
|
LOG.error("Failed to set locale %s, falling back to English", lang)
|
|
# $LANGUAGE is what sets the Gtk+ translations
|
|
os.environ["LANGUAGE"] = ':'.join(self.language)
|
|
# GtkBuilder uses GLib's g_dgettext wrapper, which oddly is bound
|
|
# with locale instead of gettext.
|
|
locale.bindtextdomain(self.localedomain, self.localedir)
|
|
|
|
self.initialized = True
|
|
|
|
|
|
def __init__(self, lang=None, localedir=None, domain=None, languages=None):
|
|
"""
|
|
Init a GrampsLocale. Run __init_first_instance() to set up the
|
|
environement if this is the first run. Return __first_instance
|
|
otherwise if called without arguments.
|
|
"""
|
|
if self == self._GrampsLocale__first_instance:
|
|
if not self.initialized:
|
|
self._GrampsLocale__init_first_instance(lang, localedir,
|
|
domain, languages)
|
|
else:
|
|
return
|
|
|
|
else:
|
|
if domain:
|
|
self.localedomain = domain
|
|
else:
|
|
self.localedomain = self._GrampsLocale__first_instance.localedomain
|
|
if localedir:
|
|
self.localedir = localedir
|
|
else:
|
|
self.localedir = self._GrampsLocale__first_instance.localedir
|
|
|
|
self.language = []
|
|
if languages and len(languages) > 0:
|
|
self.language = [l for l in languages
|
|
if l in self.get_available_translations()]
|
|
if len(self.language) == 0:
|
|
self.language = self._GrampsLocale__first_instance.language
|
|
|
|
|
|
self.translation = self._get_translation(self.localedomain,
|
|
self.localedir, self.language)
|
|
self._set_dictionaries()
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
if gettext.find(domain, localedir, languages):
|
|
return gettext.translation(domain, localedir,
|
|
languages,
|
|
class_ = GrampsTranslations)
|
|
else:
|
|
if not languages == ["en"]:
|
|
LOG.debug("Unable to find translations for %s and %s in %s",
|
|
domain, languages, localedir)
|
|
return GrampsNullTranslations()
|
|
|
|
def _set_dictionaries(self):
|
|
"""
|
|
Create a dictionary of language names localized to the
|
|
GrampsLocale's primary language, keyed by language and
|
|
country code.
|
|
"""
|
|
_ = self.translation.gettext
|
|
self.lang_map = {
|
|
"bg" : _("Bulgarian"),
|
|
"ca" : _("Catalan"),
|
|
"cs" : _("Czech"),
|
|
"da" : _("Danish"),
|
|
"de" : _("German"),
|
|
"el" : _("Greek"),
|
|
"en" : _("English"),
|
|
"eo" : _("Esperanto"),
|
|
"es" : _("Spanish"),
|
|
"fi" : _("Finnish"),
|
|
"fr" : _("French"),
|
|
"he" : _("Hebrew"),
|
|
"hr" : _("Croatian"),
|
|
"hu" : _("Hungarian"),
|
|
"it" : _("Italian"),
|
|
"ja" : _("Japanese"),
|
|
"lt" : _("Lithuanian"),
|
|
"mk" : _("Macedonian"),
|
|
"nb" : _("Norwegian Bokmal"),
|
|
"nl" : _("Dutch"),
|
|
"nn" : _("Norwegian Nynorsk"),
|
|
"pl" : _("Polish"),
|
|
"pt" : _("Portuguese"),
|
|
"ro" : _("Romanian"),
|
|
"ru" : _("Russian"),
|
|
"sk" : _("Slovak"),
|
|
"sl" : _("Slovenian"),
|
|
"sq" : _("Albanian"),
|
|
"sv" : _("Swedish"),
|
|
"tr" : _("Turkish"),
|
|
"uk" : _("Ukrainian"),
|
|
"vi" : _("Vietnamese"),
|
|
"zh" : _("Chinese")
|
|
}
|
|
|
|
self.country_map = {
|
|
"BR" : _("Brazil"),
|
|
"CN" : _("China"),
|
|
"PT" : _("Portugal")
|
|
}
|
|
|
|
def _get_language_string(self, lang_code):
|
|
"""
|
|
Given a language code of the form "lang_region", return a text string
|
|
representing that language.
|
|
"""
|
|
code_parts = lang_code.rsplit("_")
|
|
|
|
lang = code_parts[0]
|
|
if lang in self.lang_map:
|
|
lang = self.lang_map[lang]
|
|
|
|
country = None
|
|
if len(code_parts) > 1:
|
|
country = code_parts[1]
|
|
if country in self.country_map:
|
|
country = self.country_map[country]
|
|
lang = "%(language)s (%(country)s)" % \
|
|
{ 'language' : lang, 'country' : country }
|
|
|
|
return lang
|
|
|
|
#-------------------------------------------------------------------------
|
|
#
|
|
# 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_translation(self, domain = None, languages = None):
|
|
"""
|
|
Get a translation object for a particular language.
|
|
See the gettext documentation for the available functions
|
|
>>> glocale = GrampsLocale()
|
|
>>> _ = glocale.get_translation('foo', 'French')
|
|
>>> _ = tr.gettext
|
|
"""
|
|
|
|
if ((domain and not domain == self.localedomain)
|
|
or (languages and not languages == self.language)):
|
|
if not domain:
|
|
domain = self.localedomain
|
|
if not languages:
|
|
languages = self.language
|
|
fallback = False
|
|
if "en" in languages:
|
|
fallback = True
|
|
try:
|
|
# Don't use _get_translation because we want to fall
|
|
# back on the singleton rather than a NullTranslation
|
|
return gettext.translation(domain, self.localedir,
|
|
languages,
|
|
class_ = GrampsTranslations,
|
|
fallback = fallback)
|
|
except IOError:
|
|
LOG.warning("None of the requested languages (%s) were available, using %s instead", ', '.join(languages), self.lang)
|
|
return self.translation
|
|
else:
|
|
return self.translation
|
|
|
|
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 running code
|
|
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.
|
|
"""
|
|
path = self.localedir
|
|
# If get the path of the calling module's uncompiled file. This seems a remarkably bad idea.
|
|
# if filename is None:
|
|
# filename = sys._getframe(1).f_code.co_filename
|
|
|
|
gramps_translator = self._get_translation()
|
|
|
|
path = os.path.dirname(os.path.abspath(filename))
|
|
# Check if path is of type str. Do import and conversion if so.
|
|
# The import cannot be done at the top as that will conflict with the translation system.
|
|
|
|
if not isinstance(path, UNITYPE) == str:
|
|
from .file import get_unicode_path_from_env_var
|
|
path = get_unicode_path_from_env_var(path)
|
|
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):
|
|
"""
|
|
Get a list of available translations.
|
|
|
|
:returns: A list of translation languages.
|
|
:rtype: unicode[]
|
|
|
|
"""
|
|
languages = ["en"]
|
|
|
|
if self.localedir is None:
|
|
return languages
|
|
|
|
for langdir in os.listdir(self.localedir):
|
|
mofilename = os.path.join(self.localedir, langdir,
|
|
"LC_MESSAGES",
|
|
"%s.mo" % self.localedomain )
|
|
if os.path.exists(mofilename):
|
|
languages.append(langdir)
|
|
|
|
languages.sort()
|
|
|
|
return languages
|
|
|
|
def get_language_dict(self):
|
|
'''
|
|
return a dictionary of language names : codes for use by language
|
|
pickers.
|
|
'''
|
|
langs = {}
|
|
for code in self.get_available_translations():
|
|
langs[self._get_language_string(code)] = code
|
|
|
|
return langs
|
|
|
|
|
|
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"
|
|
|
|
#-------------------------------------------------------------------------
|
|
#
|
|
# 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.info()["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"
|