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
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
72
73
74
75 __orig_tag__ = u'Translate this or i18n into <en_EN> will not work properly !'
76
77
78
79
80
81
82
83
84
85
86
87
107
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
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
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
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
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
236
238 """Get system locale from environment."""
239 global system_locale
240
241
242 __log_locale_settings('unmodified startup locale settings (should be [C])')
243
244
245 loc, enc = None, None
246 try:
247
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
261 __log_locale_settings('locale settings after activating user-default locale')
262
263
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
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
279 if domain is None:
280 _log.info('domain not specified, deriving from script name')
281
282 domain = os.path.splitext(os.path.basename(sys.argv[0]))[0]
283 _log.info('text domain is [%s]' % domain)
284
285
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
301 candidates = []
302
303
304 if prefer_local_catalog:
305 _log.debug('preferring local message catalog')
306
307
308
309
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
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
319 if os.name == 'posix':
320 _log.debug('system is POSIX, looking in standard locations (see Python Manual)')
321
322
323 candidates.append(gettext.bindtextdomain(domain))
324 else:
325 _log.debug('No use looking in standard POSIX locations - not a POSIX system.')
326
327
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
338 if not prefer_local_catalog:
339
340
341
342
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
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
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
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
375 _log.warning("falling back to NullTranslations() class")
376
377 dummy = gettext.NullTranslations()
378 dummy.install()
379 return True
380
381 _encoding_mismatch_already_logged = False
382 _current_encoding = None
383
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
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
454
455
456
457 tmp = _('Translate this or i18n into <en_EN> will not work properly !')
458
459
460
461
462