diff --git a/django/bin/make-messages.py b/django/bin/make-messages.py index 92f6cbfd9a..e8c21db70f 100755 --- a/django/bin/make-messages.py +++ b/django/bin/make-messages.py @@ -7,6 +7,8 @@ import getopt from django.utils.translation import templateize +pythonize_re = re.compile(r'\n\s*//') + localedir = None if os.path.isdir(os.path.join('conf', 'locale')): @@ -39,6 +41,9 @@ for o, v in opts: elif o == '-a': all = True +if domain not in ('django', 'djangojs'): + print "currently make-messages.py only supports domains 'django' and 'djangojs'" + sys.exit(1) if (lang is None and not all) or domain is None: print "usage: make-messages.py -l " print " or: make-messages.py -a" @@ -66,7 +71,28 @@ for lang in languages: for (dirpath, dirnames, filenames) in os.walk("."): for file in filenames: - if file.endswith('.py') or file.endswith('.html'): + if domain == 'djangojs' and file.endswith('.js'): + if verbose: sys.stdout.write('processing file %s in %s\n' % (file, dirpath)) + src = open(os.path.join(dirpath, file), "rb").read() + src = pythonize_re.sub('\n#', src) + open(os.path.join(dirpath, '%s.py' % file), "wb").write(src) + thefile = '%s.py' % file + cmd = 'xgettext %s -d %s -L Perl --keyword=gettext_noop --keyword=gettext_lazy --keyword=ngettext_lazy -o - "%s"' % ( + os.path.exists(potfile) and '--omit-header' or '', domain, os.path.join(dirpath, thefile)) + (stdin, stdout, stderr) = os.popen3(cmd, 'b') + msgs = stdout.read() + errors = stderr.read() + if errors: + print "errors happened while running xgettext on %s" % file + print errors + sys.exit(8) + old = '#: '+os.path.join(dirpath, thefile)[2:] + new = '#: '+os.path.join(dirpath, file)[2:] + msgs = msgs.replace(old, new) + if msgs: + open(potfile, 'ab').write(msgs) + os.unlink(os.path.join(dirpath, thefile)) + elif domain == 'django' and (file.endswith('.py') or file.endswith('.html')): thefile = file if file.endswith('.html'): src = open(os.path.join(dirpath, file), "rb").read() @@ -91,22 +117,23 @@ for lang in languages: if thefile != file: os.unlink(os.path.join(dirpath, thefile)) - (stdin, stdout, stderr) = os.popen3('msguniq %s' % potfile, 'b') - msgs = stdout.read() - errors = stderr.read() - if errors: - print "errors happened while running msguniq" - print errors - sys.exit(8) - open(potfile, 'w').write(msgs) - if os.path.exists(pofile): - (stdin, stdout, stderr) = os.popen3('msgmerge -q %s %s' % (pofile, potfile), 'b') + if os.path.exists(potfile): + (stdin, stdout, stderr) = os.popen3('msguniq %s' % potfile, 'b') msgs = stdout.read() errors = stderr.read() if errors: - print "errors happened while running msgmerge" + print "errors happened while running msguniq" print errors sys.exit(8) - open(pofile, 'wb').write(msgs) - os.unlink(potfile) + open(potfile, 'w').write(msgs) + if os.path.exists(pofile): + (stdin, stdout, stderr) = os.popen3('msgmerge -q %s %s' % (pofile, potfile), 'b') + msgs = stdout.read() + errors = stderr.read() + if errors: + print "errors happened while running msgmerge" + print errors + sys.exit(8) + open(pofile, 'wb').write(msgs) + os.unlink(potfile) diff --git a/django/utils/text.py b/django/utils/text.py index 6ac8352ac6..8206095f42 100644 --- a/django/utils/text.py +++ b/django/utils/text.py @@ -1,5 +1,7 @@ import re +from django.conf.settings import DEFAULT_CHARSET + # Capitalizes the first letter of a string. capfirst = lambda x: x and x[0].upper() + x[1:] @@ -90,3 +92,20 @@ def compress_string(s): zfile.write(s) zfile.close() return zbuf.getvalue() + +ustring_re = re.compile(u"([\u0080-\uffff])") +def javascript_quote(s): + + def fix(match): + return r"\u%04x" % ord(match.group(1)) + + if type(s) == str: + s = s.decode(DEFAULT_ENCODING) + elif type(s) != unicode: + raise TypeError, s + s = s.replace('\\', '\\\\') + s = s.replace('\n', '\\n') + s = s.replace('\t', '\\t') + s = s.replace("'", "\\'") + return str(ustring_re.sub(fix, s)) + diff --git a/django/utils/translation.py b/django/utils/translation.py index a8a943e391..9c36850fb9 100644 --- a/django/utils/translation.py +++ b/django/utils/translation.py @@ -212,6 +212,21 @@ def get_language(): from django.conf.settings import LANGUAGE_CODE return LANGUAGE_CODE +def catalog(): + """ + This function returns the current active catalog for further processing. + This can be used if you need to modify the catalog or want to access the + whole message catalog instead of just translating one string. + """ + global _default, _active + t = _active.get(currentThread(), None) + if t is not None: + return t + if _default is None: + from django.conf import settings + _default = translation(settings.LANGUAGE_CODE) + return _default + def gettext(message): """ This function will be patched into the builtins module to provide the _ diff --git a/django/views/i18n.py b/django/views/i18n.py index 7bb2f00b2f..7b67cd2417 100644 --- a/django/views/i18n.py +++ b/django/views/i18n.py @@ -1,5 +1,12 @@ +import re +import os + +import gettext as gettext_module + from django.utils import httpwrappers -from django.utils.translation import check_for_language +from django.utils.translation import check_for_language, activate, to_locale, get_language +from django.utils.text import javascript_quote +from django.conf import settings def set_language(request): """ @@ -20,3 +27,163 @@ def set_language(request): else: response.set_cookie('django_language', lang_code) return response + +NullSource = """ +/* gettext identity library */ + +function gettext(msgid) { + return msgid; +} + +function ngettext(singular, plural, count) { + if (count == 1) { + return singular; + } else { + return plural; + } +} + +function gettext_noop(msgid) { + return msgid; +} +""" + +LibHead = """ +/* gettext library */ + +var catalog = new Array(); +""" + +LibFoot = """ + +function gettext(msgid) { + var value = catalog[msgid]; + if (typeof(value) == 'undefined') { + return msgid; + } else { + if (typeof(value) == 'string') { + return value; + } else { + return value[0]; + } + } +} + +function ngettext(singular, plural, count) { + value = catalog[singular]; + if (typeof(value) == 'undefined') { + if (count == 1) { + return singular; + } else { + return plural; + } + } else { + return value[pluralidx(count)]; + } +} + +function gettext_noop(msgid) { + return msgid; +} +""" + +SimplePlural = """ +function pluralidx(count) { + if (count == 1) { + return 0; + } else { + return 1; + } +} +""" + +InterPolate = r""" +function interpolate(fmt, obj, named) { + if (named) { + return fmt.replace(/%\(\w+\)s/, function(match){return String(obj[match.slice(2,-2)])}); + } else { + return fmt.replace(/%s/, function(match){return String(obj.shift())}); + } +} +""" + +def javascript_catalog(request, domain='djangojs', packages=None): + """ + Returns the selected language catalog as a javascript library. + + Receives the list of packages to check for translations in the + packages parameter either from an infodict or as a +-delimited + string from the request. Default is 'django.conf'. + + Additionally you can override the gettext domain for this view, + but usually you don't want to do that, as JavaScript messages + go to the djangojs domain. But this might be needed if you + deliver your JavaScript source from Django templates. + """ + if request.GET: + if request.GET.has_key('language'): + if check_for_language(request.GET['language']): + activate(request.GET['language']) + if packages is None: + packages = ['django.conf'] + if type(packages) in (str, unicode): + packages = packages.split('+') + default_locale = to_locale(settings.LANGUAGE_CODE) + locale = to_locale(get_language()) + t = {} + paths = [] + for package in packages: + p = __import__(package, {}, {}, ['']) + path = os.path.join(os.path.dirname(p.__file__), 'locale') + paths.append(path) + #!!! add loading of catalogs from settings.LANGUAGE_CODE and request.LANGUAGE_CODE! + try: + catalog = gettext_module.translation(domain, path, [default_locale]) + except IOError, e: + catalog = None + if catalog is not None: + t.update(catalog._catalog) + if locale != default_locale: + for path in paths: + try: + catalog = gettext_module.translation(domain, path, [locale]) + except IOError, e: + catalog = None + if catalog is not None: + t.update(catalog._catalog) + src = [LibHead] + plural = None + for l in t[''].split('\n'): + if l.startswith('Plural-Forms:'): + plural = l.split(':',1)[1].strip() + if plural is not None: + # this should actually be a compiled function of a typical plural-form: + # Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2; + plural = [el.strip() for el in plural.split(';') if el.strip().startswith('plural=')][0].split('=',1)[1] + src.append('function pluralidx(n) {\n return %s;\n}\n' % plural) + else: + src.append(SimplePlural) + csrc = [] + pdict = {} + for k, v in t.items(): + if k == '': + continue + if type(k) in (str, unicode): + csrc.append("catalog['%s'] = '%s';\n" % (javascript_quote(k), javascript_quote(v))) + elif type(k) == tuple: + if not pdict.has_key(k[0]): + pdict[k[0]] = k[1] + else: + pdict[k[0]] = max(k[1], pdict[k[0]]) + csrc.append("catalog['%s'][%d] = '%s';\n" % (javascript_quote(k[0]), k[1], javascript_quote(v))) + else: + raise TypeError, k + csrc.sort() + for k,v in pdict.items(): + src.append("catalog['%s'] = [%s];\n" % (javascript_quote(k), ','.join(["''"]*(v+1)))) + src.extend(csrc) + src.append(LibFoot) + src.append(InterPolate) + src = ''.join(src) + return httpwrappers.HttpResponse(src, 'text/javascript') +