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