diff --git a/r2/r2/config/routing.py b/r2/r2/config/routing.py index 5fa0694ad..fefed3676 100644 --- a/r2/r2/config/routing.py +++ b/r2/r2/config/routing.py @@ -83,10 +83,6 @@ def make_map(global_conf={}, app_conf={}): mc('/feedback', controller='feedback', action='feedback') mc('/ad_inq', controller='feedback', action='ad_inq') - mc('/admin/i18n', controller='i18n', action='list') - mc('/admin/i18n/:action', controller='i18n') - mc('/admin/i18n/:action/:lang', controller='i18n') - mc('/admin/usage', controller='usage') # Used for editing ads diff --git a/r2/r2/controllers/api.py b/r2/r2/controllers/api.py index 1cff924d6..0d4df29c6 100644 --- a/r2/r2/controllers/api.py +++ b/r2/r2/controllers/api.py @@ -1698,14 +1698,6 @@ class ApiController(RedditController): c.user._commit() - @noresponse(VAdmin(), - tr = VTranslation("lang"), - user = nop('user')) - def POST_deltranslator(self, tr, user): - if tr: - tr.author.remove(user) - tr.save() - @noresponse(VUser(), VModhash(), action = VOneOf('action', ('sub', 'unsub')), @@ -1734,18 +1726,6 @@ class ApiController(RedditController): # some other proc has already handled this subscribe request. return - @noresponse(VAdmin(), - tr = VTranslation("id")) - def POST_disable_lang(self, tr): - if tr: - tr._is_enabled = False - - @noresponse(VAdmin(), - tr = VTranslation("id")) - def POST_enable_lang(self, tr): - if tr: - tr._is_enabled = True - @validatedForm(VAdmin(), hexkey=VLength("hexkey", max_length=32), diff --git a/r2/r2/controllers/i18n.py b/r2/r2/controllers/i18n.py deleted file mode 100644 index 6131cbc54..000000000 --- a/r2/r2/controllers/i18n.py +++ /dev/null @@ -1,187 +0,0 @@ -# The contents of this file are subject to the Common Public Attribution -# License Version 1.0. (the "License"); you may not use this file except in -# compliance with the License. You may obtain a copy of the License at -# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public -# License Version 1.1, but Sections 14 and 15 have been added to cover use of -# software over a computer network and provide for limited attribution for the -# Original Developer. In addition, Exhibit A has been modified to be consistent -# with Exhibit B. -# -# Software distributed under the License is distributed on an "AS IS" basis, -# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for -# the specific language governing rights and limitations under the License. -# -# The Original Code is Reddit. -# -# The Original Developer is the Initial Developer. The Initial Developer of the -# Original Code is CondeNet, Inc. -# -# All portions of the code written by CondeNet are Copyright (c) 2006-2010 -# CondeNet, Inc. All Rights Reserved. -################################################################################ -from pylons import request, g -from reddit_base import RedditController -from r2.lib.pages import UnfoundPage, AdminTranslations, \ - AdminPage, Translator_Message - -from r2.lib.translation import Translator, TranslatorTemplate, get_translator -from validator import * -from gettext import _translations - -from r2.lib.wrapped import Wrapped - -class VTranslator(Validator): - def run(self, lang): - if (not c.user_is_admin and - (not c.user_is_loggedin or not lang or - c.user.name not in Translator.get_author(lang))): - abort(404, 'page not found') - -class VTranslationEnabled(Validator): - def run(self): - if not g.translator: - abort(403, 'forbidden') - - -class I18nController(RedditController): - - @validate(VTranslationEnabled(), - VAdmin(), - lang = nop('lang'), - a = VExistingUname('name')) - def POST_adduser(self, lang, a): - from r2.lib.db import queries - if a and Translator.exists(lang): - tr = get_translator(locale = lang) - tr.author.add(a.name) - tr.save() - - # send the user a message - body = Translator_Message(lang, a).render("html") - subject = "Thanks for offering to help translate!" - m, inbox_rel = Message._new(c.user, a, subject, body, request.ip) - queries.new_message(m, inbox_rel) - - return self.redirect("/admin/i18n") - - - @validate(VTranslationEnabled(), - VAdmin()) - def GET_list(self): - res = AdminPage(content = AdminTranslations(), - title = 'translate reddit', - show_sidebar = False).render() - return res - - - @validate(VTranslationEnabled(), - VAdmin(), - lang = nop('name')) - def POST_new(self, lang): - if lang and not Translator.exists(lang): - tr = get_translator(locale = lang) - tr.save() - return self.redirect("/admin/i18n") - - - @validate(VTranslationEnabled(), - VTranslator('lang'), - lang = nop('lang')) - def GET_edit(self, lang): - if not lang and c.user_is_admin: - content = Wrapped(TranslatorTemplate()) - elif Translator.exists(lang): - content = Wrapped(get_translator(locale = lang)) - else: - content = UnfoundPage() - res = AdminPage(content = content, - title = 'translate reddit', - show_sidebar = False).render() - return res - - @validate(VTranslationEnabled(), - VTranslator('lang'), - lang = nop('lang'), - numbers = VBoolean("numbers")) - def GET_try(self, lang, numbers): - if lang: - tr = get_translator(locale = lang) - tr.save(compile=True, include_index = numbers) - - tran_keys = _translations.keys() - for key in tran_keys: - if key.endswith(tr._out_file('mo')): - del _translations[key] - - return self.redirect("http://%s.%s/" % - (lang, g.domain)) - return abort(404, 'page not found') - - @validate(VTranslationEnabled(), - VTranslator('lang'), - post = nop('post'), - try_trans = nop('try'), - lang = nop('lang')) - def POST_edit(self, lang, post, try_trans): - if (lang and not Translator.exists(lang)): - return self.redirect('/admin/i18n') - - if lang: - tr = get_translator(locale = lang) - else: - tr = TranslatorTemplate() - - enabled = set() - for k, val in request.post.iteritems(): - if k.startswith('trans_'): - k = k.split('_') - val = val.replace('\n', ' ').replace('\r', ' ') - # check if this is a translation string - if k[1:] and tr.get(k[1]): - tr.set(k[1], val, indx = int(k[2] if k[2:] else -1)) - # check if this is an admin editing the source/comment lines - elif c.user_is_admin and tr.sources.get(k[1]): - source = tr.sources.get(k[1]) - tr.source_trans[source] = val - elif c.user_is_admin and k.startswith('enabled_'): - k = k.split('_') - enabled.add(k[1]) - - # update the enabled state of the buttons - if c.user_is_admin and enabled: - strings = set(tr.string_dict.keys()) - disabled = strings - enabled - for s in strings: - tr.set_enabled(s, True) - for s in disabled: - tr.set_enabled(s, False) - - if request.post.get('nplurals'): - try: - tr.plural_names = [request.post.get('pluralform_%d' % i) \ - for i in xrange(tr.nplurals)] - tr.nplurals = int(request.post.get('nplurals')) - except ValueError: - pass - if request.post.get('langname'): - tr.name = request.post['langname'] - if request.post.get('enlangname'): - tr.en_name = request.post['enlangname'] - - tr.save(compile=bool(try_trans)) - - if try_trans: - tran_keys = _translations.keys() - for key in tran_keys: - if key.endswith(tr._out_file('mo')): - del _translations[key] - - return self.redirect("http://%s/?lang=%s" % - (g.domain, lang)) - - whereto = request.post.get('bttn_num', '') - if whereto: - whereto = 'bttn_num_%s' % whereto - return self.redirect("/admin/i18n/edit/%s#%s" % (lang or '', whereto)) - return res - diff --git a/r2/r2/controllers/validator/validator.py b/r2/r2/controllers/validator/validator.py index 8dd441cd8..94a0db379 100644 --- a/r2/r2/controllers/validator/validator.py +++ b/r2/r2/controllers/validator/validator.py @@ -1345,11 +1345,6 @@ class VCnameDomain(Validator): except UnicodeEncodeError: self.set_error(errors.BAD_CNAME) -class VTranslation(Validator): - def run(self, param): - from r2.lib.translation import Translator - if Translator.exists(param): - return Translator(locale = param) # NOTE: make sure *never* to have res check these are present # otherwise, the response could contain reference to these errors...! diff --git a/r2/r2/lib/logger.py b/r2/r2/lib/logger.py deleted file mode 100644 index 66e2471b1..000000000 --- a/r2/r2/lib/logger.py +++ /dev/null @@ -1,153 +0,0 @@ -# The contents of this file are subject to the Common Public Attribution -# License Version 1.0. (the "License"); you may not use this file except in -# compliance with the License. You may obtain a copy of the License at -# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public -# License Version 1.1, but Sections 14 and 15 have been added to cover use of -# software over a computer network and provide for limited attribution for the -# Original Developer. In addition, Exhibit A has been modified to be consistent -# with Exhibit B. -# -# Software distributed under the License is distributed on an "AS IS" basis, -# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for -# the specific language governing rights and limitations under the License. -# -# The Original Code is Reddit. -# -# The Original Developer is the Initial Developer. The Initial Developer of the -# Original Code is CondeNet, Inc. -# -# All portions of the code written by CondeNet are Copyright (c) 2006-2010 -# CondeNet, Inc. All Rights Reserved. -################################################################################ -from __future__ import with_statement -import cPickle as pickle -import os, shutil, time -from utils import Storage -from datetime import datetime, timedelta - - -class LoggedSlots(object): - - def __init__(self, logfile, **kw): - for k, v in kw.iteritems(): - super(LoggedSlots, self).__setattr__(k, v) - self.__logfile = logfile - self.load_slots() - - def __setattr__(self, k, v): - super(LoggedSlots, self).__setattr__(k, v) - if k in self.__slots__: - self.dump_slots() - - def load_slots(self): - d = self._get_slots(self.__logfile) - for k, v in d.iteritems(): - super(LoggedSlots, self).__setattr__(k, v) - - def dump_slots(self): - if self.__logfile: - with WithWriteLock(self.__logfile) as handle: - d = {} - for s in self.__slots__: - try: - d[s] = getattr(self, s) - except AttributeError: - continue - pickle.dump(d, handle) - - @classmethod - def _get_slots(self, file): - if os.path.exists(file): - with open(file) as handle: - return Storage(pickle.load(handle)) - return Storage() - - - -class WriteLockExistsException(): pass - -class WithWriteLock(): - def __init__(self, file_name, mode = 'w', force = False, age = 60): - self.fname = file_name - self.lock_file = file_name + ".write_lock" - self.time = datetime.now() - self.handle = None - self.created = True - - if self.exists(): - if force: - self.destroy() - elif not self.try_expire(age): - raise WriteLockExistsException - - # back up the file to be written to - if os.path.exists(self.fname): - shutil.copyfile(self.fname, self.backup_file) - # write out a lock file - with open(self.lock_file, 'w') as handle: - pickle.dump(self.time, handle) - # lastly, open the file! - self.handle = open(file_name, mode) - - def write(self, *a, **kw): - self.handle.write(*a, **kw) - - - @property - def backup_file(self): - return "%s-%s.bak" % (self.fname, - time.mktime(self.time.timetuple())) - - def exists(self): - return os.path.exists(self.lock_file) - - def try_expire(self, age): - '''destroys an existing lock file if it is more than age seconds old''' - with open(self.lock_file, 'r') as handle: - time = pickle.load(handle) - if self.time - time > timedelta(0, age): - self.destroy() - return True - return False - - def destroy(self): - # close any open handles - if self.handle: - self.handle.close() - self.handle = None - - # wipe the lock file and the back-up - if self.created: - self.created = False - if self.exists(): - os.unlink(self.lock_file) - if os.path.exists(self.backup_file): - os.unlink(self.backup_file) - - def close(self): - self.destroy() - - def rollback(self): - # close any open handles - if self.handle: - self.handle.close() - self.handle = None - - if self.created: - # clobber any changes to the file with our archive - if os.path.exists(self.backup_file): - shutil.copyfile(self.backup_file, self.fname) - - #destroy as usual - self.destroy() - - def __enter__(self): - return self - - - def __exit__(self, type, value, tb): - if tb is None: - self.close() - else: - self.rollback() - diff --git a/r2/r2/lib/pages/pages.py b/r2/r2/lib/pages/pages.py index 78e0c49bd..b78456b68 100644 --- a/r2/r2/lib/pages/pages.py +++ b/r2/r2/lib/pages/pages.py @@ -1918,24 +1918,6 @@ class Bookmarklets(Templated): Templated.__init__(self, buttons = buttons) -class Translator_Message(Templated): - def __init__(self, locale, user): - from r2.lib.translation import Translator - self.user = user - self.locale = locale - self.lang_name = Translator.get_name(self.locale) - self.en_name = Translator.get_en_name(self.locale) - Templated.__init__(self) - -class AdminTranslations(Templated): - """The translator control interface, used for determining which - user is allowed to edit which translation file and for providing a - summary of what translation files are done and/or in use.""" - def __init__(self): - from r2.lib.translation import list_translations - Templated.__init__(self) - self.translations = list_translations() - class UserAwards(Templated): """For drawing the regular-user awards page.""" def __init__(self): diff --git a/r2/r2/lib/translation.py b/r2/r2/lib/translation.py index ee1d817f7..d36a3aaf5 100644 --- a/r2/r2/lib/translation.py +++ b/r2/r2/lib/translation.py @@ -19,28 +19,27 @@ # All portions of the code written by CondeNet are Copyright (c) 2006-2010 # CondeNet, Inc. All Rights Reserved. ################################################################################ -from __future__ import with_statement -from distutils.cmd import Command -from distutils.errors import DistutilsOptionError - -from pylons.i18n import _ from babel import Locale -import os, re -import cPickle as pickle -from wrapped import Templated -from utils import Storage -from hashlib import md5 - -from logger import WithWriteLock, LoggedSlots -from r2.lib.i18n import I18N_PATH as _i18n_path - from datetime import datetime, timedelta +import gettext +from hashlib import md5 +import json +import os +import pylons +from pylons.i18n import _ +from pylons.i18n.translation import translation, LanguageError, NullTranslations +import random +import re import time -import pylons -from pylons.i18n.translation import translation, LanguageError, NullTranslations +from r2.lib.i18n import I18N_PATH as _i18n_path +from r2.lib.utils import Storage -def _get_translator(lang, graceful_fail = False, **kwargs): + +_domain = 'r2' + + +def _get_translator(lang, graceful_fail=False, **kwargs): from pylons import config as conf """Utility method to get a valid translator object from a language name""" if not isinstance(lang, list): @@ -67,540 +66,26 @@ def set_lang(lang, graceful_fail = False, **kwargs): registry.replace(pylons.translator, translator) -comment = re.compile(r'\A\s*#') -msgid = re.compile(r'\A\s*msgid\s+"') -msgid_pl = re.compile(r'\A\s*msgid_plural\s+"') -msgstr = re.compile(r'\A\s*msgstr(\[\d\])?\s+"') -str_only = re.compile(r'\A\s*"') - -substr = re.compile("(%(\([^\)]+\))?([\d\.]+)?[a-zA-Z])") - -source_line = re.compile(": (\S+\.[^\.]+):(\d+)") - -from r2.config.templates import tpm - -tpm.add('translator', 'html', file = "translation.html") - - - -_domain = 'r2' - -def hax(string): - """when site translation strings change, this will allow the subsequent - language translations to be updated without giving the translator more - work""" - hax_dict = { } - return hax_dict.get(string, string) - - -class TranslatedString(Templated): - class _SubstString: - def __init__(self, string, enabled = True): - self.str = hax(string) - subst = substr.findall(string) - self.subst = set(x[0] for x in subst) - self.name = set(x[1].strip(')').strip('(') for x in subst if x[1]) - - def __str__(self): - return self.str - - def unicode(self): - if isinstance(self.str, unicode): - return self.str - return unicode(self.str, "utf-8") - - def __repr__(self): - return self.str - - def _po_str(self, header, cut = 60, include_index = False): - s = self.str - if include_index: - s = ("/*%s:*/" % include_index) + s - if len(s) > cut: - if '\\n' in s: - txt = [i + "\\n" for i in s.split('\\n') if i] - else: - txt = [s] - res = '%s ""\n' % header - for t in txt: - res += '"%s"\n' % t.replace('"', '\\"') - else: - res = '%s "%s"\n' % (header, s.replace('"', '\\"')) - if isinstance(res, unicode): - return res.encode('utf8') - return res - - def highlight(self, func): - res = self.str - for x in self.subst: - res = res.replace(x, func(x)) - return res - - def valid(self): - try: - # enforce validatation for named replacement rules only - if self.name: - x = self.str % dict((k, 0) for k in self.name if k) - return True - except: - return False - - def compatible(self, other): - # compatibility implies every substitution rule in other - # must be in self. - return other.valid() and (not self.name or other.subst.issubset(self.subst)) - - def __init__(self, translator, sing, plural = '', message = '', - enabled = True, locale = '', tip = '', index = 0): - Templated.__init__(self) - - self.translator = translator - self.message = message - self.enabled = enabled - self.locale = locale - self.tip = '' - self.index = 0 - - # figure out where this item appears in the source - source = source_line.findall(message) - if source: - self.source, self.line = source[0] - else: - self.source = self.line = '' - - self.msgid = self._SubstString(sing) - self.msgid_plural = self._SubstString(plural) - if str(self.msgid_plural): - self.msgstr = [] - else: - self.msgstr = self._SubstString('') - - @property - def singular(self): - return self.msgid.unicode() or '' - - def _singular(self, func): - return self.msgid.highlight(func) - - @property - def plural(self): - return self.msgid_plural.unicode() or '' - - def _plural(self, func): - return self.msgid_plural.highlight(func) - - def is_translated(self): - if str(self.msgid_plural): - return bool(self.msgstr) and any([x.str for x in self.msgstr]) - else: - return bool(self.msgstr.str) - - - def translation(self, indx = 0): - if self.plural: - if indx < len(self.msgstr): - return self.msgstr[indx].unicode() or '' - return '' - else: - return self.msgstr.unicode() or '' - - def add(self, translation, index = -1): - new = self._SubstString(translation) - if unicode(self.msgid_plural): - if index >= 0: - while index >= len(self.msgstr): - self.msgstr.append('') - self.msgstr[index] = new - else: - self.msgstr.append(new) - else: - self.msgstr = new - - def __getitem__(self, indx): - return self.translation(indx) - - def __setitem__(self, indx, value): - return self.add(value, index = indx) - - @property - def md5(self): - return md5(unicode(self.singular) + unicode(self.plural)).hexdigest() - - def __repr__(self): - return "" - - def to_string(self, include_index = False): - res = '' - if self.message: - res = '#' + self.message.replace('\n', '\n#') - if res[-1] == '#': res = res[:-1] - res += self.msgid._po_str('msgid', include_index = False) - if unicode(self.msgid_plural): - res += self.msgid_plural._po_str('msgid_plural', - include_index = False) - for i in range(0, min(len(self.msgstr), self.translator.nplurals)): - res += self.msgstr[i]._po_str('msgstr[%d]'%i, - include_index = include_index) - else: - res += self.msgstr._po_str('msgstr', include_index = include_index) - res += "\n" - try: - return str(res) - except UnicodeEncodeError: - return unicode(res + "\n").encode('utf-8') - - def __str__(self): - return self.to_string(False) - - - def is_valid(self, indx = -1): - if self.plural: - if indx < 0: - return all(self.is_valid(i) for i in range(0,len(self.msgstr))) - elif indx < len(self.msgstr): - return self.msgid.compatible(self.msgstr[indx]) #or \ - #self.msgstr.compatible(self.msgstr[indx]) - return True - else: - return self.msgid.compatible(self.msgstr) - - -class GettextHeader(TranslatedString): - def __init__(self, translator, sing, plural = '', message = '', - enabled = True, locale = ''): - TranslatedString.__init__(self, translator, '', '', message=message, - enabled = False, locale = locale) - self.headers = [] - - def add(self, translation, index = -1): - if index < 0: - header_keys = set() - self.msgstr = self._SubstString(translation) - for line in translation.split('\\n'): - line = line.split(':') - header_keys.add(line[0]) - self.headers.append([line[0], ':'.join(line[1:])]) - # http://www.gnu.org/software/gettext/manual/gettext.html#Plural-forms - if "Plural-Forms" not in header_keys: - self.headers.append(["Plural-Forms", - "nplurals=2; plural=(n != 1);"]) - - elif self.headers and len(self.headers) > index: - self.headers[index][1] = translation - t = '\\n'.join('%s:%s' % tuple(h) for h in self.headers if h[0]) - self.msgstr = self._SubstString(t) - - def __repr__(self): - return "" - -class Translator(LoggedSlots): - - __slots__ = ['enabled', 'num_completed', 'num_total', 'author', - 'nplurals', 'plural_names', 'source_trans', 'name', - 'en_name', '_is_enabled'] - - def __init__(self, domain = _domain, path = _i18n_path, - locale = ''): - self.strings = [] - self.string_dict = {} - self.sources = {} - - self.locale = locale - self.input_file = TranslatorTemplate.outfile(locale) - - def _out_file(extension = None): - d = dict(extension=extension) if extension is not None else {} - return self.outfile(locale, path=path, domain=domain, **d) - self._out_file = _out_file - - # create directory for translation - if not os.path.exists(os.path.dirname(self._out_file('po'))): - os.makedirs(os.path.dirname(self._out_file('po'))) - - LoggedSlots.__init__(self, self._out_file('data'), - plural_names = ['singular', 'plural'], - nplurals = 2, - source_trans = {}, - author = set([]), - enabled = {}, - num_completed = 0, - num_total = 0, - en_name = locale, - name = locale, - _is_enabled = False - ) - - # no translation, use the infile to make one - if not os.path.exists(self._out_file('po')): - self.from_file(self.input_file) - # translation exists: make sure infile is not newer - else: - i_stat = os.stat(self.input_file) - o_stat = os.stat(self._out_file('po')) - if i_stat.st_mtime > o_stat.st_mtime: - self.from_file(self.input_file) - self.load_specialty_strings() - self.from_file(self._out_file('po'), merge=True) - self.save() - else: - self.from_file(self._out_file('po')) - self.load_specialty_strings() - - - def is_valid(self): - for x in self.get_invalid(): - return False - return True - - def get_invalid(self): - for k, indx in self.string_dict.iteritems(): - if not self.strings[indx].is_valid(): - yield (k, self.strings[indx].singular) - - def from_file(self, file, merge = False): - indx = 0 - with open(file, 'r') as handle: - line = True - while line: - line = handle.readline() - msg = '' - while comment.match(line): - msg += '#'.join(line.split('#')[1:]) - line = handle.readline() - if msgid.match(line): - txt_pl = '' - r, txt_sing, line = get_next_str_block(line, handle) - # grab plural if it exists - if msgid_pl.match(line): - r, txt_pl, line = get_next_str_block(line, handle) - if txt_sing or txt_pl: - ts = TranslatedString(self, txt_sing, txt_pl, - message = msg, - locale = self.locale) - else: - ts = GettextHeader(self, txt_sing, txt_pl, - message = msg, - locale = self.locale) - key = ts.md5 - if not self.enabled.has_key(key) or self.enabled[key]: - indx += 1 - ts.index = indx - - while msgstr.match(line): - r, translation, line = get_next_str_block(line, handle) - ts.add(translation) - - if not merge and not self.string_dict.has_key(key): - self.string_dict[key] = len(self.strings) - self.strings.append(ts) - self.sources[md5(ts.source).hexdigest()] = ts.source - elif merge and self.string_dict.has_key(key): - i = self.string_dict[key] - self.strings[i].msgstr = ts.msgstr - self.sources[md5(ts.source).hexdigest()] = ts.source - - def load_specialty_strings(self): - indx = len(self.strings) - from r2.lib.strings import rand_strings - for name, rs in rand_strings: - for desc in rs: - message = ": randomstring:%s\n" % name - ts = TranslatedString(self, desc, "", message = message, - locale = self.locale) - key = ts.md5 - if not self.string_dict.has_key(key): - self.string_dict[key] = len(self.strings) - self.strings.append(ts) - else: - ts = self.strings[self.string_dict[key]] - self.sources[md5(ts.source).hexdigest()] = ts.source - ts.enabled = True - indx += 1 - ts.index = indx - - - def __getitem__(self, key): - return self.strings[self.string_dict[key]] - - def get(self, key, alt = None): - indx = self.string_dict.get(key) - if indx is not None: - return self.strings[self.string_dict[key]] - else: - return alt - - def set(self, key, val, indx = -1): - s = self.get(key) - if s: s[indx] = val - - - def to_file(self, file, compile=False, mo_file=None, - include_index = False): - with WithWriteLock(file) as handle: - for string in self.strings: - indx = include_index and string.index - handle.write(string.to_string(include_index = indx)) - if compile and self.is_valid(): - if mo_file: - out_file = mo_file - elif file.endswith(".po"): - out_file = file[:-3] + ".mo" - else: - out_file = file + ".mo" - - cmd = 'msgfmt -o "%s" "%s"' % (out_file, file) - print cmd - with os.popen(cmd) as handle: - x = handle.read() - if include_index: - with WithWriteLock(file) as handle: - for string in self.strings: - handle.write(string.to_string(include_index = False)) - - - def __iter__(self): - for x in self.strings: - yield x - - def __repr__(self): - return "" - - @classmethod - def outfile(cls, locale, domain = _domain, path = _i18n_path, - extension = 'po'): - return os.path.join(path, locale, 'LC_MESSAGES', +def load_data(lang, path=_i18n_path, domain=_domain, extension='data'): + filename = os.path.join(path, locale, 'LC_MESSAGES', domain + '.' + extension) - - @classmethod - def in_use(cls, locale, domain = _domain, path = _i18n_path): - return os.path.exists(cls.outfile(locale, domain=domain, path=path, - extension='mo')) + with open(filename) as datafile: + data = json.load(datafile) - @classmethod - def exists(cls, locale, domain = _domain, path = _i18n_path): - return os.path.exists(cls.outfile(locale, domain=domain, path=path)) - - - def save(self, compile = False, include_index = False): - self.to_file(self._out_file('po'), compile = compile, - mo_file = self._out_file('mo'), - include_index = include_index) - self.gen_stats() - self.dump_slots() - - def __repr__(self): - return "" - - - @classmethod - def get_slots(cls, locale = None): - locale = locale or pylons.g.lang - f = cls.outfile(locale, extension='data') - return LoggedSlots._get_slots(f) - - def load_slots(self): - LoggedSlots.load_slots(self) - # clobber enabled and translation using primary template - if self.input_file != self._out_file(): - parent_slots = TranslatorTemplate.get_slots() - self.enabled = parent_slots.get('enabled',{}) - self.source_trans = parent_slots.get('source_trans', {}) - - #if self.enabled: - # for key, state in self.enabled.iteritems(): - # self.set_enabled(key, state) - - def gen_stats(self): - enabled = {} - num_completed = 0 - num_strings = 0 - for h, indx in self.string_dict.iteritems(): - s = self.strings[indx] - enabled[h] = s.enabled - if s.enabled: - num_strings +=1 - if s.is_translated(): - num_completed += 1 - self.enabled = enabled - self.num_completed = num_completed - self.num_total = num_strings - - - @classmethod - def get_author(cls, locale): - slots = cls.get_slots(locale) - return slots.get("author", set([])) - - @classmethod - def get_name(cls, locale): - slots = cls.get_slots(locale) - return slots.get("name", locale) - - @classmethod - def get_en_name(cls, locale): - slots = cls.get_slots(locale) - return slots.get("en_name", locale) - - @classmethod - def is_enabled(cls, locale): - slots = cls.get_slots(locale) - return slots.get("_is_enabled", False) - - @classmethod - def get_complete_frac(cls, locale): - infos = cls.get_slots(locale) - comp = infos.get('num_completed', 0) - tot = infos.get('num_total', 1) - return float(comp)/float(tot) - - def set_enabled(self, key, state = True): - indx = self.string_dict.get(key, None) - if indx is not None: - self.strings[indx].enabled = state - -def list_translations(path = _i18n_path): - trans = [] - for lang in os.listdir(path): - x = os.path.join(path, lang, 'LC_MESSAGES') - if os.path.exists(x) and os.path.isdir(x): - if Translator.exists(lang): - trans.append(lang) - return trans - -class rebuild_translations(Command): - user_options = [] - def initialize_options(self): - pass - - def finalize_options(self): - pass - - def run(self, path = _i18n_path): - _rebuild_trans(path) - -def _rebuild_trans(path = _i18n_path): - for lang in os.listdir(path): - x = os.path.join(path, lang, 'LC_MESSAGES') - if os.path.exists(x) and os.path.isdir(x): - if Translator.exists(lang): - print "recompiling '%s'" % (lang) - t = get_translator(lang) - t.save(compile = True) - - -def get_active_langs(path = _i18n_path, default_lang = 'en'): +def get_active_langs(path=_i18n_path, default_lang='en'): trans = [] trans_name = {} for lang in os.listdir(path): x = os.path.join(path, lang, 'LC_MESSAGES') if os.path.isdir(x): - name = [Translator.get_name(lang), ''] - if Translator.is_enabled(lang) and Translator.in_use(lang): - if lang != default_lang: - trans.append(lang) - if Translator.get_complete_frac(lang) < .5: - name[1] = ' (*)' + data = load_data(lang) + name = [data['name'], ''] + if data['_is_enabled'] and lang != default_lang: + trans.append(lang) + completion = float(data['num_completed']) / float(data['num_total']) + if completion < .5: + name[1] = ' (*)' trans_name[lang] = name trans.sort() # insert the default language at the top of the list @@ -608,278 +93,3 @@ def get_active_langs(path = _i18n_path, default_lang = 'en'): if default_lang not in trans_name: trans_name[default_lang] = default_lang return trans, trans_name - - -class TranslatorTemplate(Translator): - @classmethod - def outfile(cls, locale, domain = _domain, path = _i18n_path, - extension = 'pot'): - return os.path.join(path, domain + '.' + extension) - - # defunct to_file since pot file is uneditable - def to_file(*a, **kw): - pass - - -class AutoTranslator(Translator): - def __init__(self, **kw): - Translator.__init__(self, **kw) - for string in self.strings: - if not string.is_translated(): - string.add(self.translate(string.singular), index = 0) - if string.plural: - string.add(self.translate(string.plural), index = 1) - - def translate(self, string): - s = string.split("%") - s, d = s[0], s[1:] - substr = re.compile("((\([^\)]+\))?([\d\.]+)?[a-zA-Z])(.*)") - def _sub(m): - g = m.groups() - return "%s%s" % (g[0], self.trans_rules(g[-1])) - - d = [self.trans_rules(s)] + [substr.sub(_sub, x) for x in d] - return '%'.join(d) - - def trans_rules(self, text): - return text - -class Transliterator(AutoTranslator): - def __init__(self, **kw): - Translator.__init__(self, **kw) - for string in self.strings: - if not string.is_translated() \ - and not isinstance(string, GettextHeader): - if string.plural: - string.add(self.translate(string.msgstr[0].unicode()), - index = 0) - string.add(self.translate(string.msgstr[1].unicode()), - index = 1) - else: - string.add(self.translate(string.msgstr.unicode()), - index = 0) - - -class USEnglishTranslator(AutoTranslator): - def trans_rules(self, string): - return string - - -class TamilTranslator(Transliterator): - transliterator = dict([(u'a', u'\u0b85'), - (u'A', u'\u0b86'), - (u'i', u'\u0b87'), - (u'I', u'\u0b88'), - (u'u', u'\u0b89'), - (u'U', u'\u0b8a'), - (u'e', u'\u0b8e'), - (u'E', u'\u0b8f'), - (u'o', u'\u0b92'), - (u'O', u'\u0b93'), - - (u'g', u'\u0b95\u0bcd'), - (u'c', u'\u0b95\u0bcd'), - (u'k', u'\u0b95\u0bcd'), - (u'q', u'\u0b95\u0bcd'), - (u'G', u'\u0b95\u0bcd'), - (u'K', u'\u0b95\u0bcd'), - - (u's', u'\u0b9a\u0bcd'), - (u'C', u'\u0b9a\u0bcd'), - - (u't', u'\u0b9f\u0bcd'), - (u'D', u'\u0b9f\u0bcd'), - (u'T', u'\u0b9f\u0bcd'), - (u'N', u'\u0ba3\u0bcd'), - (u'd', u'\u0ba4\u0bcd'), - (u'$', u'\u0ba8\u0bcd'), - (u'n', u'\u0ba9\u0bcd'), - (u'B', u'\u0baa\u0bcd'), - (u'b', u'\u0baa\u0bcd'), - (u'f', u'\u0baa\u0bcd'), - (u'p', u'\u0baa\u0bcd'), - (u'F', u'\u0baa\u0bcd'), - (u'P', u'\u0baa\u0bcd'), - (u'm', u'\u0bae\u0bcd'), - (u'M', u'\u0bae\u0bcd'), - (u'y', u'\u0baf\u0bcd'), - (u'r', u'\u0bb0\u0bcd'), - (u'R', u'\u0bb1\u0bcd'), - (u'l', u'\u0bb2\u0bcd'), - (u'L', u'\u0bb3\u0bcd'), - (u'Z', u'\u0bb4\u0bcd'), - (u'z', u'\u0bb4\u0bcd'), - (u'v', u'\u0bb5\u0bcd'), - (u'w', u'\u0bb5\u0bcd'), - (u'V', u'\u0bb5\u0bcd'), - (u'W', u'\u0bb5\u0bcd'), - - (u'Q', u'\u0b83'), - - (u'h', u'\u0bb9\u0bcd'), - (u'j', u'\u0b9c\u0bcd'), - (u'J', u'\u0b9c\u0bcd'), - (u'S', u'\u0bb8\u0bcd'), - (u'H', u'\u0bb9\u0bcd'), - - (u'Y', u'\u0b20\u0bcd'), - (u'^', u'\u0b20')]) - - ligatures = ((u'\u0ba9\u0bcd\u0b95\u0bcd', u'\u0B99\u0BCD'), # ng - (u'\u0ba9\u0bcd\u0b9c\u0bcd', u'\u0B9e\u0BCD'), # nj - (u'\u0b95\u0bcd\u0bb9\u0bcd', u'\u0b9a\u0bcd'), # ch -> C - (u'\u0b9f\u0bcd\u0bb9\u0bcd', u'\u0ba4\u0bcd'), # th -> d - (u'\u0ba4\u0bcd\u0bb9\u0bcd', u'\u0ba4\u0bcd'), # dh -> d - - (u'\u0b85\u0b85', u'\u0b86'), # aa -> A - (u'\u0b85\u0b87', u'\u0b90'), # ai - (u'\u0b85\u0b89', u'\u0b94'), # au - (u'\u0b87\u0b87', u'\u0b88'), # ii -> I - (u'\u0b89\u0b89', u'\u0b8a'), # uu -> U - (u'\u0b8e\u0b8e', u'\u0b8f'), # ee -> E - (u'\u0b92\u0b92', u'\u0b93'), # oo -> O - # remove accent from consonants and convert to ligature - # based on the subsequent vowell - (u'\u0bcd\u0b85', u''), - (u'\u0bcd\u0b86', u'\u0bbe'), - (u'\u0bcd\u0b87', u'\u0bbf'), - (u'\u0bcd\u0b88', u'\u0bc0'), - (u'\u0bcd\u0b89', u'\u0bc1'), - (u'\u0bcd\u0b8a', u'\u0bc2'), - (u'\u0bcd\u0b8e', u'\u0bc6'), - (u'\u0bcd\u0b8f', u'\u0bc7'), - (u'\u0bcd\u0b90', u'\u0bc8'), - (u'\u0bcd\u0b92', u'\u0bca'), - (u'\u0bcd\u0b93', u'\u0bcb'), - (u'\u0bcd\u0b94', u'\u0bcc'), - ) - - def trans_rules(self, string): - t = u''.join(self.transliterator.get(x, x) for x in string) - for k, v in self.ligatures: - t = t.replace(k, v) - return t - -class SerbianCyrillicTranslator(Transliterator): - letters = \ - (( "A" , u'\u0410'), - ( "B" , u'\u0411'), - ( "V" , u'\u0412'), - ( "G" , u'\u0413'), - ( "D" , u'\u0414'), - ( u'\u0110' , u'\u0402'), - ( "E" , u'\u0415'), - ( u"\u017d" , u'\u0416'), - ( "Z" , u'\u0417'), - ( "I" , u'\u0418'), - ( "J" , u'\u0408'), - ( "K" , u'\u041a'), - ( "L" , u'\u041b'), - ( "Lj" , u'\u0409'), - ( "M" , u'\u041c'), - ( "N" , u'\u041d'), - ( "Nj" , u'\u040a'), - ( "O" , u'\u041e'), - ( "P" , u'\u041f'), - ( "R" , u'\u0420'), - ( "S" , u'\u0421'), - ( "T" , u'\u0422'), - ( u"\u0106" , u'\u040b'), - ( "U" , u'\u0423'), - ( "F" , u'\u0424'), - ( 'H' , u'\u0425'), - ( "C" , u'\u0426'), - ( u"\u010c", u'\u0427'), - ( u"D\u017e", u'\u040f'), - ( u"\u0160", u'\u0428'), - - ( "a" , u'\u0430'), - ( "b" , u'\u0431'), - ( "v" , u'\u0432'), - ( "g" , u'\u0433'), - ( "d" , u'\u0434'), - ( u'\u0111' , u'\u0452'), - ( "e" , u'\u0435'), - ( u"\u017e" , u'\u0436'), - ( "z" , u'\u0437'), - ( "i" , u'\u0438'), - ( "j" , u'\u0458'), - ( "k" , u'\u043a'), - ( "l" , u'\u043b'), - ( "lj" , u'\u0459'), - ( "m" , u'\u043c'), - ( "n" , u'\u043d'), - ( "nj" , u'\u045a'), - ( "o" , u'\u043e'), - ( "p" , u'\u043f'), - ( "r" , u'\u0440'), - ( "s" , u'\u0441'), - ( "t" , u'\u0442'), - ( u"\u0107" , u'\u045b'), - ( "u" , u'\u0443'), - ( "f" , u'\u0444'), - ( 'h' , u'\u0445'), - ( "c" , u'\u0446'), - ( u"\u010d", u'\u0447'), - ( u"d\u017e", u'\u045f'), - ( u"\u0161", u'\u0448')) - ligatures = [(x,y) for x, y in letters if len(x) == 2] - letters = dict((x, y) for x, y in letters if len(x) == 1) - def trans_rules(self, string): - for x, y in self.ligatures: - string = string.replace(x, y) - return "".join(self.letters.get(s, s) for s in string) - -import random -class LeetTranslator(AutoTranslator): - def trans_rules(self, string): - print string - key = dict(a=["4","@"], - b=["8"], c=["("], - d=[")", "|)"], e=["3"], - f=["ph"], g=["6"], - i=["1", "!"], j=["_/"], - k=["X"], l=["1"], o=["0"], - q=["0_"], s=["5", "$"], t=["7"], - z=["2"]) - s = string.lower() - s = (random.choice(key.get(x, [x])) for x in s) - return ''.join(s) - -def get_translator(locale): - #if locale == 'sr': - # return SerbianCyrillicTranslator(locale = locale) - if locale == 'leet': - return LeetTranslator(locale = locale) - elif locale.startswith('en'): - return USEnglishTranslator(locale = locale) - elif locale == 'ta': - return TamilTranslator(locale = locale) - return Translator(locale = locale) - -def get_next_str_block(line, handle): - res = '' - before, middle, after = qnd_parse(line) - txt = [middle] - res += line - line = handle.readline() - # grab multi-line strings for this element - while True: - if not str_only.match(line): break - res += line - b, m, a = qnd_parse(line) - txt.append(m) - line = handle.readline() - return res, (''.join(txt)).replace('\\"', '"'), line - - -def qnd_parse(line): - p = line.split('#') - after = '#'.join(p[1:]) - if after: after = "#" + after - s = p[0].split('"') - after = s[-1] + after - before = s[0] - middle = '"'.join(s[1:-1]) - return before, middle, after - diff --git a/r2/r2/templates/admintranslations.html b/r2/r2/templates/admintranslations.html deleted file mode 100644 index 7b5f933a8..000000000 --- a/r2/r2/templates/admintranslations.html +++ /dev/null @@ -1,113 +0,0 @@ -## The contents of this file are subject to the Common Public Attribution -## License Version 1.0. (the "License"); you may not use this file except in -## compliance with the License. You may obtain a copy of the License at -## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public -## License Version 1.1, but Sections 14 and 15 have been added to cover use of -## software over a computer network and provide for limited attribution for the -## Original Developer. In addition, Exhibit A has been modified to be consistent -## with Exhibit B. -## -## Software distributed under the License is distributed on an "AS IS" basis, -## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for -## the specific language governing rights and limitations under the License. -## -## The Original Code is Reddit. -## -## The Original Developer is the Initial Developer. The Initial Developer of -## the Original Code is CondeNet, Inc. -## -## All portions of the code written by CondeNet are Copyright (c) 2006-2010 -## CondeNet, Inc. All Rights Reserved. -################################################################################ - -<%namespace file="translation.html" import="statusbar"/> -<%namespace file="printablebuttons.html" import="ynbutton, state_button" /> -<% - from r2.lib.translation import Translator -%> -
-

- create new set: - - -

-
- -

- - Edit base translation template - -

- - - %for t in sorted(thing.translations): - <% - name = Translator.get_name(t) - en_name = Translator.get_en_name(t) - authors = Translator.get_author(t) - frac = Translator.get_complete_frac(t) - %> - - - - - - - - - - %endfor -
- ${en_name}/${name} - - [${t}] - - - %if Translator.in_use(t): - %if Translator.is_enabled(t): - ${state_button("disable_lang", _("disable"), \ - "return change_state(this, 'disable_lang');", _("disabled"), - hidden_data = dict(id=t))} - %else: - ${state_button("enable_lang", _("enable"), \ - "return change_state(this, 'enable_lang');", _("enabled"), - hidden_data = dict(id=t))} - %endif - %if t in g.languages: - [in use] - %endif - %endif - - - %if Translator.in_use(t): - [try] - %endif - - ${statusbar(frac)} - - ${"%d%%" % int(100*frac)} -
diff --git a/r2/r2/templates/translatedstring.html b/r2/r2/templates/translatedstring.html deleted file mode 100644 index 83a3363d4..000000000 --- a/r2/r2/templates/translatedstring.html +++ /dev/null @@ -1,93 +0,0 @@ -## The contents of this file are subject to the Common Public Attribution -## License Version 1.0. (the "License"); you may not use this file except in -## compliance with the License. You may obtain a copy of the License at -## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public -## License Version 1.1, but Sections 14 and 15 have been added to cover use of -## software over a computer network and provide for limited attribution for the -## Original Developer. In addition, Exhibit A has been modified to be consistent -## with Exhibit B. -## -## Software distributed under the License is distributed on an "AS IS" basis, -## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for -## the specific language governing rights and limitations under the License. -## -## The Original Code is Reddit. -## -## The Original Developer is the Initial Developer. The Initial Developer of -## the Original Code is CondeNet, Inc. -## -## All portions of the code written by CondeNet are Copyright (c) 2006-2010 -## CondeNet, Inc. All Rights Reserved. -################################################################################ - -<% - highlight = lambda x: ("" + x + "") - singular = thing._singular(highlight).replace("\\n", "
") - plural = thing._plural(highlight).replace("\\n", "
") - # downplay explanation strings - singular = singular.replace(" {", " (").replace("}", ")") - plural = plural.replace(" {", " (").replace("}", ")") - singular = unsafe(singular) - plural = unsafe(plural) - - %> - -%if (singular and thing.enabled) or c.user_is_admin: - - - (${thing.index}) - - %if plural: - - Singular: ${singular},  - Plural: ${plural} - - %else: - - ${singular} - - %endif - - - %if c.user_is_admin and not thing.locale: - - %endif - - %if plural: - - %for i in xrange(thing.translator.nplurals): - ${text_input(thing.translation(i), thing.is_valid(i), - index=i)} - - (${thing.translator.plural_names[i] if len(thing.translator.plural_names) > i else "case #%d" % (i+1)}) -
- %endfor - %else: - - ${text_input(thing.translation(), thing.is_valid(), - len=len(singular))} - %endif - %if thing.tip: - ${thing.tip} - %endif - - -%endif - - -<%def name="text_input(text, valid, index = 0, len = 0)"> -

- - ${text} - - edit - %if not valid: - - Invalid Translation - - %endif -
- diff --git a/r2/r2/templates/translation.html b/r2/r2/templates/translation.html deleted file mode 100644 index 0773217b0..000000000 --- a/r2/r2/templates/translation.html +++ /dev/null @@ -1,181 +0,0 @@ -## The contents of this file are subject to the Common Public Attribution -## License Version 1.0. (the "License"); you may not use this file except in -## compliance with the License. You may obtain a copy of the License at -## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public -## License Version 1.1, but Sections 14 and 15 have been added to cover use of -## software over a computer network and provide for limited attribution for the -## Original Developer. In addition, Exhibit A has been modified to be consistent -## with Exhibit B. -## -## Software distributed under the License is distributed on an "AS IS" basis, -## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for -## the specific language governing rights and limitations under the License. -## -## The Original Code is Reddit. -## -## The Original Developer is the Initial Developer. The Initial Developer of -## the Original Code is CondeNet, Inc. -## -## All portions of the code written by CondeNet are Copyright (c) 2006-2010 -## CondeNet, Inc. All Rights Reserved. -################################################################################ - -<% - from md5 import md5 -%> - - - -

-%if thing.locale: - Translation: en to ${thing.locale} - %if thing.is_valid(): - - %endif -%else: - Base Translation Template -%endif -

- -

- - - - - - -
Number complete: ${thing.num_completed}/${thing.num_total}${statusbar(float(thing.num_completed)/max(thing.num_total,1))}${"%d%%" % int(100 * thing.num_completed/max(thing.num_total,1))}
-

- - -%if not thing.is_valid(): -
    -%for hash, string in thing.get_invalid(): -<% - if len(string) > 50: - s = string[:50] + '...' - else: - s = string -%> -
  • Error: "${s}"
  • -%endfor -
-%endif - -%if c.user_is_admin: -%if thing.locale: -

- - « Edit base translation template - -

-%endif -

- - « Translation summary - -

-%endif - -

- %if c.user_is_admin: -

- Name of this language: -

- English Name of this language: -

- number of plural forms: - - %for i in xrange(thing.nplurals): - - - - - %endfor -
- Plural #${i+1} is called: - - -
-

- %endif - <% source = None %> - - %for i, s in enumerate(thing.strings): - %if s.source != source: - - %if source is not None: - - %endif - <% - source = s.source - source_string = thing.source_trans.get(source, source) - digest = md5(source).hexdigest() - %> - %if source is not None and (source_string or c.user_is_admin): - -

- ${source_string} - %if c.user_is_admin and not thing.locale: - - edit - - %endif - -

- %endif - - %endif - ${s} - %endfor -
- - - -<%def name="statusbar(fraction, width = 100)"> -<% - color = 'red' - if fraction > .3: color = 'orangered' - if fraction > .6: color = 'yellow' - if fraction > .9: color = 'green' - if fraction == 1.: color = 'blue' - %> -
-
-
- diff --git a/r2/r2/templates/translator_message.html b/r2/r2/templates/translator_message.html deleted file mode 100644 index 2610bacd9..000000000 --- a/r2/r2/templates/translator_message.html +++ /dev/null @@ -1,47 +0,0 @@ -## The contents of this file are subject to the Common Public Attribution -## License Version 1.0. (the "License"); you may not use this file except in -## compliance with the License. You may obtain a copy of the License at -## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public -## License Version 1.1, but Sections 14 and 15 have been added to cover use of -## software over a computer network and provide for limited attribution for the -## Original Developer. In addition, Exhibit A has been modified to be consistent -## with Exhibit B. -## -## Software distributed under the License is distributed on an "AS IS" basis, -## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for -## the specific language governing rights and limitations under the License. -## -## The Original Code is Reddit. -## -## The Original Developer is the Initial Developer. The Initial Developer of -## the Original Code is CondeNet, Inc. -## -## All portions of the code written by CondeNet are Copyright (c) 2006-2009 -## CondeNet, Inc. All Rights Reserved. -################################################################################ -I've added you to the access list for editing ${thing.en_name} (${thing.lang_name}) on our staging machine: - -http://${g.domain}/ - -%if g.translator_username and g.translator_password: -The user name to get past the web server is "${g.translator_username}" and the pw is "${g.translator_password}" (without quotes in both cases, and with spaces for the latter). -%endif -If that works as expected, you should be confronted by a site which looks exactly like reddit.com. -Log into reddit with your existing username (${thing.user.name}) and password, and you should be able to go to this link without a 404: - -http://${g.domain}/admin/i18n/edit/${thing.locale} - -Specific things to be aware of: - - * Save often. The operation should be pretty cheap for the webserver if a little taxing to your browser. - * If you get a blank page at any point after submitting, please let me know -- that means something has gone wrong! - * Strings in orange have to carry over untranslated into the translated string. These are python formatting placeholders for text that will probably be untranslated or translated separately. - * Hitting "try translation" will compile the translation file and should pop open a translated version of the site which should reflect your latest pass of translations. If you hit instead "try translation with numbers", it will do the same thing except it will also add a numeric placeholder to each translation that matches the list - -Let me know if the context of the phrases is unclear. Also let me know if there is no way to (unawkwardly) translate a string. I've tried to remove as many grammar restrictions as possible in longer strings, but I'm sure there could still be some incompatibilities. - -No worries about grammar either: the point of adding the preview option is to check to see if it looks/reads correctly to a native speaker. There's a lot to translate, so feel free to pick at it as you have the time. Also, I'd recommend doing the longer (multiline) strings last. There is some flakiness with those that I've had a heck of a time tracking down. - -Thanks for doing this, and let me know if any problems/suggestions come up. - -