Package Gnumed :: Package pycommon :: Module gmI18N |
[frames] | no frames]

# Source Code for Module Gnumed.pycommon.gmI18N

      1  """GNUmed client internationalization/localization.
      2
      3  All i18n/l10n issues should be handled through this modules.
      4
      5  Theory of operation:
      6
      7  To activate proper locale settings and translation services you need to
      8
      9  - import this module
     10  - call activate_locale()
     11  - call install_domain()
     12
     13  The translating method gettext.gettext() will then be
     14  installed into the global (!) namespace as _(). Your own
     15  modules thus need not do _anything_ (not even import gmI18N)
     16  to have _() available to them for translating strings. You
     17  need to make sure, however, that gmI18N is imported in your
     18  main module before any of the modules using it. In order to
     19  resolve circular references involving modules that
     20  absolutely _have_ to be imported before this module you can
     21  explicitly import gmI18N into them at the very beginning.
     22
     23  The text domain (i.e. the name of the message catalog file)
     24  is derived from the name of the main executing script unless
     25  explicitly passed to install_domain(). The language you
     26  want to translate to is derived from environment variables
     27  by the locale system unless explicitly passed to
     28  install_domain().
     29
     30  This module searches for message catalog files in 3 main locations:
     31
     32   - standard POSIX places (/usr/share/locale/ ...)
     33   - below "${YOURAPPNAME_DIR}/po/"
     34   - below "<directory of binary of your app>/../po/"
     35
     36  For DOS/Windows I don't know of standard places so probably
     37  only the last option will work. I don't know a thing about
     38  classic Mac behaviour. New Macs are POSIX, of course.
     39
     40  It will then try to install candidates and *verify* whether
     41  the translation works by checking for the translation of a
     42  tag within itself (this is similar to the self-compiling
     43  compiler inserting a backdoor into its self-compiled
     44  copies).
     45
     46  If none of this works it will fall back to making _() a noop.
     47
     48  @copyright: authors
     49  """
     50  #===========================================================================
     51  __version__ = "$Revision: 1.50 $"
     52  __author__ = "H. Herb <hherb@gnumed.net>, I. Haywood <i.haywood@ugrad.unimelb.edu.au>, K. Hilbert <Karsten.Hilbert@gmx.net>"
     53  __license__ = "GPL v2 or later (details at http://www.gnu.org)"
     54
     55
     56  # stdlib
     57  import sys, os.path, os, re as regex, locale, gettext, logging, codecs
     58
     59
     60  _log = logging.getLogger('gm.i18n')
     61  _log.info(__version__)
     62
     63  system_locale = ''
     64  system_locale_level = {}
     65
     66
     67  _translate_original = lambda x:x
     68  _substitutes_regex = regex.compile(r'%\(.+?\)s')
     69
     70  # **********************************************************
     71  # == do not remove this line ===============================
     72  # it is needed to check for successful installation of
     73  # the desired message catalog
     74  # **********************************************************
     75  __orig_tag__ = u'Translate this or i18n into <en_EN> will not work properly !'
     76  # **********************************************************
     77  # **********************************************************
     78
     79  # Q: I can't use non-ascii characters in labels and menus.
     80  # A: This can happen if your Python's sytem encoding is ascii and
     81  #    wxPython is non-unicode. Edit/create the file sitecustomize.py
     82  #    (should be somewhere in your PYTHONPATH), and put these magic lines:
     83  #
     84  #       import sys
     85  #       sys.setdefaultencoding('iso8859-1') # replace with encoding you want to be the default one
     86
     87  #===========================================================================
     88 -def __split_locale_into_levels():
     89          """Split locale into language, country and variant parts.
     90
     91          - we have observed the following formats in the wild:
     92            - de_DE@euro
     93            - ec_CA.UTF-8
     94            - en_US:en
     95            - German_Germany.1252
     96          """
     97          _log.debug('splitting canonical locale [%s] into levels', system_locale)
     98
     99          global system_locale_level
    100          system_locale_level['full'] = system_locale
    101          # trim '@<variant>' part
    102          system_locale_level['country'] = regex.split('@|:|\.', system_locale, 1)[0]
    103          # trim '_<COUNTRY>@<variant>' part
    104          system_locale_level['language'] = system_locale.split('_', 1)[0]
    105
    106          _log.debug('system locale levels: %s', system_locale_level)
    107  #---------------------------------------------------------------------------
    108 -def __log_locale_settings(message=None):
    109          _setlocale_categories = {}
    110          for category in 'LC_ALL LC_CTYPE LC_COLLATE LC_TIME LC_MONETARY LC_MESSAGES LC_NUMERIC'.split():
    111                  try:
    112                          _setlocale_categories[category] = getattr(locale, category)
    113                  except:
    114                          _log.warning('this OS does not have locale.%s', category)
    115
    116          _getlocale_categories = {}
    117          for category in 'LC_CTYPE LC_COLLATE LC_TIME LC_MONETARY LC_MESSAGES LC_NUMERIC'.split():
    118                  try:
    119                          _getlocale_categories[category] = getattr(locale, category)
    120                  except:
    121                          pass
    122
    123          if message is not None:
    124                  _log.debug(message)
    125
    126          _log.debug('current locale settings:')
    127          _log.debug('locale.get_locale(): %s' % str(locale.getlocale()))
    128          for category in _getlocale_categories.keys():
    129                  _log.debug('locale.get_locale(%s): %s' % (category, locale.getlocale(_getlocale_categories[category])))
    130
    131          for category in _setlocale_categories.keys():
    132                  _log.debug('(locale.set_locale(%s): %s)' % (category, locale.setlocale(_setlocale_categories[category])))
    133
    134          try:
    135                  _log.debug('locale.getdefaultlocale() - default (user) locale: %s' % str(locale.getdefaultlocale()))
    136          except ValueError:
    137                  _log.exception('the OS locale setup seems faulty')
    138
    139          _log.debug('encoding sanity check (also check "locale.nl_langinfo(CODESET)" below):')
    140          pref_loc_enc = locale.getpreferredencoding(do_setlocale=False)
    141          loc_enc = locale.getlocale()[1]
    142          py_str_enc = sys.getdefaultencoding()
    143          sys_fs_enc = sys.getfilesystemencoding()
    144          _log.debug('sys.getdefaultencoding(): [%s]' % py_str_enc)
    145          _log.debug('locale.getpreferredencoding(): [%s]' % pref_loc_enc)
    146          _log.debug('locale.getlocale()[1]: [%s]' % loc_enc)
    147          _log.debug('sys.getfilesystemencoding(): [%s]' % sys_fs_enc)
    148          if loc_enc is not None:
    149                  loc_enc = loc_enc.upper()
    150                  loc_enc_compare = loc_enc.replace(u'-', u'')
    151          else:
    152                  loc_enc_compare = loc_enc
    153          if pref_loc_enc.upper().replace(u'-', u'') != loc_enc_compare:
    154                  _log.warning('encoding suggested by locale (%s) does not match encoding currently set in locale (%s)' % (pref_loc_enc, loc_enc))
    155                  _log.warning('this might lead to encoding errors')
    156          for enc in [pref_loc_enc, loc_enc, py_str_enc, sys_fs_enc]:
    157                  if enc is not None:
    158                          try:
    159                                  codecs.lookup(enc)
    160                                  _log.debug('<codecs> module CAN handle encoding [%s]' % enc)
    161                          except LookupError:
    162                                  _log.warning('<codecs> module can NOT handle encoding [%s]' % enc)
    163          _log.debug('on Linux you can determine a likely candidate for the encoding by running "locale charmap"')
    164
    165          _log.debug('locale related environment variables (${LANG} is typically used):')
    166          for var in 'LANGUAGE LC_ALL LC_CTYPE LANG'.split():
    167                  try:
    168                          _log.debug('${%s}=%s' % (var, os.environ[var]))
    169                  except KeyError:
    170                          _log.debug('${%s} not set' % (var))
    171
    172          _log.debug('database of locale conventions:')
    173          data = locale.localeconv()
    174          for key in data.keys():
    175                  if loc_enc is None:
    176                          _log.debug(u'locale.localeconv(%s): %s', key, data[key])
    177                  else:
    178                          try:
    179                                  _log.debug(u'locale.localeconv(%s): %s', key, unicode(data[key]))
    180                          except UnicodeDecodeError:
    181                                  _log.debug(u'locale.localeconv(%s): %s', key, unicode(data[key], loc_enc))
    182          _nl_langinfo_categories = {}
    183          for category in 'CODESET D_T_FMT D_FMT T_FMT T_FMT_AMPM RADIXCHAR THOUSEP YESEXPR NOEXPR CRNCYSTR ERA ERA_D_T_FMT ERA_D_FMT ALT_DIGITS'.split():
    184                  try:
    185                          _nl_langinfo_categories[category] = getattr(locale, category)
    186                  except:
    187                          _log.warning('this OS does not support nl_langinfo category locale.%s' % category)
    188          try:
    189                  for category in _nl_langinfo_categories.keys():
    190                          if loc_enc is None:
    191                                  _log.debug('locale.nl_langinfo(%s): %s' % (category, locale.nl_langinfo(_nl_langinfo_categories[category])))
    192                          else:
    193                                  try:
    194                                          _log.debug(u'locale.nl_langinfo(%s): %s', category, unicode(locale.nl_langinfo(_nl_langinfo_categories[category])))
    195                                  except UnicodeDecodeError:
    196                                          _log.debug(u'locale.nl_langinfo(%s): %s', category, unicode(locale.nl_langinfo(_nl_langinfo_categories[category]), loc_enc))
    197          except:
    198                  _log.exception('this OS does not support nl_langinfo')
    199
    200          _log.debug('gmI18N.get_encoding(): %s', get_encoding())
    201  #---------------------------------------------------------------------------
    202 -def _translate_protected(term):
    203          """This wraps _().
    204
    205          It protects against translation errors such as a different number of "%s".
    206          """
    207          translation = _translate_original(term)
    208
    209          # different number of %s substitutes ?
    210          if translation.count(u'%s') != term.count(u'%s'):
    211                  _log.error('count("%s") mismatch, returning untranslated string')
    212                  _log.error('original   : %s', term)
    213                  _log.error('translation: %s', translation)
    214                  return term
    215
    216          term_substitutes = _substitutes_regex.findall(term)
    217          trans_substitutes = _substitutes_regex.findall(translation)
    218
    219          # different number of %(...)s substitutes ?
    220          if len(term_substitutes) != len(trans_substitutes):
    221                  _log.error('count("%(...)s") mismatch, returning untranslated string')
    222                  _log.error('original   : %s', term)
    223                  _log.error('translation: %s', translation)
    224                  return term
    225
    226          # different %(...)s substitutes ?
    227          if set(term_substitutes) != set(trans_substitutes):
    228                  _log.error('"%(...)s" name mismatch, returning untranslated string')
    229                  _log.error('original   : %s', term)
    230                  _log.error('translation: %s', translation)
    231                  return term
    232
    233          return translation
    234  #---------------------------------------------------------------------------
    235  # external API
    236  #---------------------------------------------------------------------------
    237 -def activate_locale():
    238          """Get system locale from environment."""
    239          global system_locale
    240
    241          # logging state of affairs
    242          __log_locale_settings('unmodified startup locale settings (should be [C])')
    243
    244          # activate user-preferred locale
    245          loc, enc = None, None
    246          try:
    247                  # check whether already set
    248                  loc, loc_enc = locale.getlocale()
    249                  if loc is None:
    250                          loc = locale.setlocale(locale.LC_ALL, '')
    251                          _log.debug("activating user-default locale with <locale.setlocale(locale.LC_ALL, '')> returns: [%s]" % loc)
    252                  else:
    253                          _log.info('user-default locale already activated')
    254                  loc, loc_enc = locale.getlocale()
    255          except AttributeError:
    256                  _log.exception('Windows does not support locale.LC_ALL')
    257          except:
    258                  _log.exception('error activating user-default locale')
    259
    260          # logging state of affairs
    261          __log_locale_settings('locale settings after activating user-default locale')
    262
    263          # did we find any locale setting ? assume en_EN if not
    264          if loc in [None, 'C']:
    265                  _log.error('the current system locale is still [None] or [C], assuming [en_EN]')
    266                  system_locale = "en_EN"
    267          else:
    268                  system_locale = loc
    269
    270          # generate system locale levels
    271          __split_locale_into_levels()
    272
    273          return True
    274  #---------------------------------------------------------------------------
    275 -def install_domain(domain=None, language=None, prefer_local_catalog=False):
    276          """Install a text domain suitable for the main script."""
    277
    278          # text domain directly specified ?
    279          if domain is None:
    280                  _log.info('domain not specified, deriving from script name')
    281                  # get text domain from name of script
    282                  domain = os.path.splitext(os.path.basename(sys.argv[0]))[0]
    283          _log.info('text domain is [%s]' % domain)
    284
    285          # http://www.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap08.html
    286          _log.debug('searching message catalog file for system locale [%s]' % system_locale)
    287
    288          for env_var in ['LANGUAGE', 'LC_ALL', 'LC_MESSAGES', 'LANG']:
    289                  tmp = os.getenv(env_var)
    290                  if env_var is None:
    291                          _log.debug('${%s} not set' % env_var)
    292                  else:
    293                          _log.debug('${%s} = [%s]' % (env_var, tmp))
    294
    295          if language is not None:
    296                  _log.info('explicit setting of ${LANG} requested: [%s]' % language)
    297                  _log.info('this will override the system locale language setting')
    298                  os.environ['LANG'] = language
    299
    300          # search for message catalog
    301          candidates = []
    302
    303          # - locally
    304          if prefer_local_catalog:
    305                  _log.debug('preferring local message catalog')
    306                  # - one level above path to binary
    307                  #    last resort for inferior operating systems such as DOS/Windows
    308                  #    strip one directory level
    309                  #    this is a rather neat trick :-)
    310                  loc_dir = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), '..', 'po'))
    311                  _log.debug('looking above binary install directory [%s]' % loc_dir)
    312                  candidates.append(loc_dir)
    313                  # - in path to binary
    314                  loc_dir = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), 'po'))
    315                  _log.debug('looking in binary install directory [%s]' % loc_dir)
    316                  candidates.append(loc_dir)
    317
    318          # - standard places
    319          if os.name == 'posix':
    320                  _log.debug('system is POSIX, looking in standard locations (see Python Manual)')
    321                  # if this is reported to segfault/fail/except on some
    322                  # systems we may have to assume "sys.prefix/share/locale/"
    323                  candidates.append(gettext.bindtextdomain(domain))
    324          else:
    325                  _log.debug('No use looking in standard POSIX locations - not a POSIX system.')
    326
    327          # - $(<script-name>_DIR)/
    328          env_key = "%s_DIR" % os.path.splitext(os.path.basename(sys.argv[0]))[0].upper()
    329          _log.debug('looking at ${%s}' % env_key)
    330          if os.environ.has_key(env_key):
    331                  loc_dir = os.path.abspath(os.path.join(os.environ[env_key], 'po'))
    332                  _log.debug('${%s} = "%s" -> [%s]' % (env_key, os.environ[env_key], loc_dir))
    333                  candidates.append(loc_dir)
    334          else:
    335                  _log.info("${%s} not set" % env_key)
    336
    337          # - locally
    338          if not prefer_local_catalog:
    339                  # - one level above path to binary
    340                  #    last resort for inferior operating systems such as DOS/Windows
    341                  #    strip one directory level
    342                  #    this is a rather neat trick :-)
    343                  loc_dir = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), '..', 'po'))
    344                  _log.debug('looking above binary install directory [%s]' % loc_dir)
    345                  candidates.append(loc_dir)
    346                  # - in path to binary
    347                  loc_dir = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), 'po' ))
    348                  _log.debug('looking in binary install directory [%s]' % loc_dir)
    349                  candidates.append(loc_dir)
    350
    351          # now try to actually install it
    352          for candidate in candidates:
    353                  _log.debug('trying [%s](/%s/LC_MESSAGES/%s.mo)', candidate, system_locale, domain)
    354                  if not os.path.exists(candidate):
    355                          continue
    356                  try:
    357                          gettext.install(domain, candidate, unicode=1)
    358                  except:
    359                          _log.exception('installing text domain [%s] failed from [%s]', domain, candidate)
    360                          continue
    361                  global _
    362                  # does it translate ?
    363                  if _(__orig_tag__) == __orig_tag__:
    364                          _log.debug('does not translate: [%s] => [%s]', __orig_tag__, _(__orig_tag__))
    365                          continue
    366                  else:
    367                          _log.debug('found msg catalog: [%s] => [%s]', __orig_tag__, _(__orig_tag__))
    368                          import __builtin__
    369                          global _translate_original
    370                          _translate_original = __builtin__._
    371                          __builtin__._ = _translate_protected
    372                          return True
    373
    374          # 5) install a dummy translation class
    375          _log.warning("falling back to NullTranslations() class")
    376          # this shouldn't fail
    377          dummy = gettext.NullTranslations()
    378          dummy.install()
    379          return True
    380  #===========================================================================
    381  _encoding_mismatch_already_logged = False
    382  _current_encoding = None
    383
    384 -def get_encoding():
    385          """Try to get a sane encoding.
    386
    387          On MaxOSX locale.setlocale(locale.LC_ALL, '') does not
    388          have the desired effect, so that locale.getlocale()[1]
    389          still returns None. So in that case try to fallback to
    390          locale.getpreferredencoding().
    391
    392          <sys.getdefaultencoding()>
    393                  - what Python itself uses to convert string <-> unicode
    394                    when no other encoding was specified
    395                  - ascii by default
    396                  - can be set in site.py and sitecustomize.py
    397          <locale.getlocale()[1]>
    398                  - what the current locale is *actually* using
    399                    as the encoding for text conversion
    400          <locale.getpreferredencoding()>
    401                  - what the current locale would *recommend* using
    402                    as the encoding for text conversion
    403          """
    404          global _current_encoding
    405          if _current_encoding is not None:
    406                  return _current_encoding
    407
    408          enc = sys.getdefaultencoding()
    409          if enc != 'ascii':
    410                  _current_encoding = enc
    411                  return _current_encoding
    412
    413          enc = locale.getlocale()[1]
    414          if enc is not None:
    415                  _current_encoding = enc
    416                  return _current_encoding
    417
    418          global _encoding_mismatch_already_logged
    419          if not _encoding_mismatch_already_logged:
    420                  _log.debug('*actual* encoding of locale is None, using encoding *recommended* by locale')
    421                  _encoding_mismatch_already_logged = True
    422
    423          return locale.getpreferredencoding(do_setlocale=False)
    424  #===========================================================================
    425  # Main
    426  #---------------------------------------------------------------------------
    427  if __name__ == "__main__":
    428
    429          if len(sys.argv) == 1:
    430                  sys.exit()
    431
    432          if sys.argv[1] != u'test':
    433                  sys.exit()
    434
    435          logging.basicConfig(level = logging.DEBUG)
    436
    437          print "======================================================================"
    438          print "GNUmed i18n"
    439          print ""
    440          print "authors:", __author__
    441          print "license:", __license__, "; version:", __version__
    442          print "======================================================================"
    443
    444          activate_locale()
    445          print "system locale: ", system_locale, "; levels:", system_locale_level
    446          print "likely encoding:", get_encoding()
    447
    448          if len(sys.argv) > 1:
    449                  install_domain(domain = sys.argv[2])
    450          else:
    451                  install_domain()
    452          # ********************************************************
    453          # == do not remove this line =============================
    454          # it is needed to check for successful installation of
    455          # the desired message catalog
    456          # ********************************************************
    457          tmp = _('Translate this or i18n into <en_EN> will not work properly !')
    458          # ********************************************************
    459          # ********************************************************
    460
    461  #=====================================================================
    462