######################################################################## # $Header: /var/local/cvsroot/4Suite/Ft/Lib/Gettext.py,v 1.2 2006/08/13 22:44:33 jkloth Exp $ """ Internationalization and localization support. Copyright 2006 Fourthought, Inc. (USA). Detailed license and copyright information: http://4suite.org/COPYRIGHT Project home, documentation, distributions: http://4suite.org/ """ import os import sys import gettext from Ft.Lib import ImportUtil __all__ = ['GetTranslation'] if sys.version < '2.3': def test(condition, true, false): """ Implements the C expression: condition ? true : false Required to correctly interpret plural forms. """ if condition: return true else: return false def c2py(plural): """Gets a C expression as used in PO files for plural forms and returns a Python lambda function that implements an equivalent expression. """ # Security check, allow only the "n" identifier from cStringIO import StringIO import token, tokenize tokens = tokenize.generate_tokens(StringIO(plural).readline) try: danger = [x for x in tokens if x[0] == token.NAME and x[1] != 'n'] except tokenize.TokenError: raise ValueError( 'plural forms expression error, maybe unbalanced parenthesis') else: if danger: raise ValueError('plural forms expression could be dangerous') # Replace some C operators by their Python equivalents plural = plural.replace('&&', ' and ') plural = plural.replace('||', ' or ') expr = re.compile(r'\!([^=])') plural = expr.sub(' not \\1', plural) # Regular expression and replacement function used to transform # "a?b:c" to "test(a,b,c)". expr = re.compile(r'(.*?)\?(.*?):(.*)') def repl(x): return "test(%s, %s, %s)" % (x.group(1), x.group(2), expr.sub(repl, x.group(3))) # Code to transform the plural expression, taking care of parentheses stack = [''] for c in plural: if c == '(': stack.append('') elif c == ')': if len(stack) == 1: # Actually, we never reach this code, because unbalanced # parentheses get caught in the security check at the # beginning. raise ValueError, 'unbalanced parenthesis in plural form' s = expr.sub(repl, stack.pop()) stack[-1] += '(%s)' % s else: stack[-1] += c plural = expr.sub(repl, stack.pop()) return eval('lambda n: int(%s)' % plural) if sys.version >= '2.4': NullTranslations = gettext.NullTranslations GNUTranslations = gettext.GNUTranslations else: import locale try: getpreferredencoding = locale.getpreferredencoding except AttributeError: import sys if sys.platform in ('win32', 'darwin', 'mac'): def getpreferredencoding(do_setlocale=True): import _locale return _locale._getdefaultlocale()[1] elif hasattr(locale, 'CODESET'): # Return the charset that the user is likely using, according # to the system configuration. def getpreferredencoding(do_setlocale=True): if do_setlocale: oldloc = locale.setlocale(locale.LC_CTYPE) locale.setlocale(locale.LC_CTYPE, "") result = locale.nl_langinfo(locale.CODESET) locale.setlocale(locale.LC_CTYPE, oldloc) return result else: return locale.nl_langinfo(locale.CODESET) else: # Fall back to parsing environment variables def getpreferredencoding(do_setlocale=True): return locale.getdefaultlocale()[1] class NullTranslations(gettext.NullTranslations): _output_charset = None if sys.version < '2.3': _fallback = None def add_fallback(self, fallback): if self._fallback: self._fallback.add_fallback(fallback) else: self._fallback = fallback def gettext(self, message): if self._fallback: return self._fallback.gettext(message) return message def ngettext(self, msgid1, msgid2, n): if self._fallback: return self._fallback.ngettext(msgid1, msgid2, n) if n == 1: return msgid1 else: return msgid2 def ugettext(self, message): if self._fallback: return self._fallback.ugettext(message) return unicode(message) def ungettext(self, msgid1, msgid2, n): if self._fallback: return self._fallback.ungettext(msgid1, msgid2, n) if n == 1: return unicode(msgid1) else: return unicode(msgid2) def lgettext(self, message): if self._fallback: return self._fallback.lgettext(message) return message def lngettext(self, msgid1, msgid2, n): if self._fallback: return self._fallback.lngettext(msgid1, msgid2, n) if n == 1: return msgid1 else: return msgid2 def output_charset(self): return self._output_charset def set_output_charset(self, charset): self._output_charset = charset class GNUTranslations(gettext.GNUTranslations, NullTranslations): _output_charset = None if sys.version < '2.3': def _parse(self, fp): gettext.GNUTranslations._parse(self, fp) # Get the plural selector function if 'plural-forms' in self._info: v = self._info['plural-forms'].split(';') plural = v[1].split('plural=')[1] self.plural = c2py(plural) else: # germanic plural by default self.plural = lambda n: int(n != 1) # Unconditionally convert both msgids and msgstrs to # Unicode using the character encoding specified in the # charset parameter of the Content-Type header. messages = self._catalog.iteritems() self._catalog = catalog = {} for msg, tmsg in messages: if '\x00' in msg: # Plural forms msgid1, msgid2 = msg.split('\x00') tmsg = tmsg.split('\x00') if self._charset: msgid1 = unicode(msgid1, self._charset) tmsg = [unicode(x, self._charset) for x in tmsg] for i in range(len(tmsg)): catalog[(msgid1, i)] = tmsg[i] else: if self._charset: msg = unicode(msg, self._charset) tmsg = unicode(tmsg, self._charset) catalog[msg] = tmsg return def gettext(self, message): missing = object() tmsg = self._catalog.get(message, missing) if tmsg is missing: if self._fallback: return self._fallback.gettext(message) return message # Encode the Unicode tmsg back to an 8-bit string, if possible if self._output_charset: return tmsg.encode(self._output_charset) elif self._charset: return tmsg.encode(self._charset) return tmsg def lgettext(self, message): missing = object() tmsg = self._catalog.get(message, missing) if tmsg is missing: if self._fallback: return self._fallback.lgettext(message) return message if self._output_charset: return tmsg.encode(self._output_charset) return tmsg.encode(locale.getpreferredencoding()) def ngettext(self, msgid1, msgid2, n): try: tmsg = self._catalog[(msgid1, self.plural(n))] if self._output_charset: return tmsg.encode(self._output_charset) elif self._charset: return tmsg.encode(self._charset) return tmsg except KeyError: if self._fallback: return self._fallback.ngettext(msgid1, msgid2, n) if n == 1: return msgid1 else: return msgid2 def lngettext(self, msgid1, msgid2, n): try: tmsg = self._catalog[(msgid1, self.plural(n))] if self._output_charset: return tmsg.encode(self._output_charset) return tmsg.encode(locale.getpreferredencoding()) except KeyError: if self._fallback: return self._fallback.lngettext(msgid1, msgid2, n) if n == 1: return msgid1 else: return msgid2 def ugettext(self, message): missing = object() tmsg = self._catalog.get(message, missing) if tmsg is missing: if self._fallback: return self._fallback.ugettext(message) return unicode(message) return tmsg def ungettext(self, msgid1, msgid2, n): try: tmsg = self._catalog[(msgid1, self.plural(n))] except KeyError: if self._fallback: return self._fallback.ungettext(msgid1, msgid2, n) if n == 1: tmsg = unicode(msgid1) else: tmsg = unicode(msgid2) return tmsg # Locate a .mo file using the gettext strategy def FindCatalogs(domain, localedir=None, languages=None): # Get some reasonable defaults for arguments that were not supplied if localedir is None: localedir = gettext._default_localedir if languages is None: languages = [] for envar in ('LANGUAGE', 'LC_ALL', 'LC_MESSAGES', 'LANG'): val = os.environ.get(envar) if val: languages = val.split(':') break if 'C' not in languages: languages.append('C') # now normalize and expand the languages nelangs = [] for lang in languages: for nelang in gettext._expand_lang(lang): if nelang not in nelangs: nelangs.append(nelang) # select a language result = [] for lang in nelangs: if lang == 'C': break mofile = os.path.join(localedir, lang, 'LC_MESSAGES', '%s.mo' % domain) result.append(mofile) return result def GetTranslation(domain, localedir=None, languages=None, class_=None, fallback=False, codeset=None, bundle=None): if class_ is None: class_ = GNUTranslations result = None for mofile in FindCatalogs(domain, localedir, languages): try: if bundle: resource = ImportUtil.OsPathToResource(mofile) stream = ImportUtil.GetResourceStream(bundle, resource) else: stream = open(mofile, 'rb') except IOError: continue t = class_(stream) if codeset: t.set_output_charset(codeset) if result is None: result = t else: result.add_fallback(t) if result is None: if fallback: return NullTranslations() from errno import ENOENT raise IOError(ENOENT, 'No translation file found for domain', domain) return result