new languages patch with pluralization, thanks Vladyslav
This commit is contained in:
+513
-150
@@ -5,46 +5,68 @@
|
||||
This file is part of the web2py Web Framework
|
||||
Copyrighted by Massimo Di Pierro <mdipierro@cs.depaul.edu>
|
||||
License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html)
|
||||
|
||||
Plural subsystem is created by Vladyslav Kozlovskyy (Ukraine)
|
||||
<dbdevelop@gmail.com>
|
||||
"""
|
||||
|
||||
import os
|
||||
from os import path as ospath, stat as ostat, sep as osep
|
||||
import re
|
||||
import cgi
|
||||
from utf8 import Utf8
|
||||
from cgi import escape
|
||||
import portalocker
|
||||
import logging
|
||||
import marshal
|
||||
import numbers
|
||||
import copy_reg
|
||||
from fileutils import listdir
|
||||
from fileutils import abspath, listdir
|
||||
import settings
|
||||
from cfs import getcfs
|
||||
from cfs import getcfs, cfs
|
||||
from thread import allocate_lock
|
||||
from html import XML, xmlescape
|
||||
|
||||
try:
|
||||
from gluon.html import MARKMIN
|
||||
except ImportError:
|
||||
MARKMIN = None
|
||||
from contrib.markmin.markmin2html import render, markmin_escape
|
||||
from string import maketrans
|
||||
|
||||
__all__ = ['translator', 'findT', 'update_all_languages']
|
||||
|
||||
is_gae = settings.global_settings.web2py_runtime_gae
|
||||
# used as a default filter i translator.M()
|
||||
markmin = lambda s: render( regex_param.sub(
|
||||
lambda m: '{' + markmin_escape(m.group('s')) + '}',
|
||||
s ), sep='br', auto=False )
|
||||
|
||||
# pattern to find T(blah blah blah) expressions
|
||||
|
||||
PY_STRING_LITERAL_RE = r'(?<=[^\w]T\()(?P<name>'\
|
||||
+ r"[uU]?[rR]?(?:'''(?:[^']|'{1,2}(?!'))*''')|"\
|
||||
+ r"(?:'(?:[^'\\]|\\.)*')|" + r'(?:"""(?:[^"]|"{1,2}(?!"))*""")|'\
|
||||
+ r'(?:"(?:[^"\\]|\\.)*"))'
|
||||
|
||||
regex_translate = re.compile(PY_STRING_LITERAL_RE, re.DOTALL)
|
||||
regex_param=re.compile(r'(?P<b>(?<!\\)(?:\\\\)*){(?P<s>.+?(?<!\\)(?:\\\\)*)}')
|
||||
regex_param=re.compile(r'{(?P<s>.+?)}')
|
||||
|
||||
# patter for a valid accept_language
|
||||
# pattern for a valid accept_language
|
||||
|
||||
regex_language = \
|
||||
re.compile('^([a-zA-Z]{2})(\-[a-zA-Z]{2})?(\-[a-zA-Z]+)?$')
|
||||
regex_langfile = re.compile('^[a-zA-Z]{2}(-[a-zA-Z]{2})?\.py$')
|
||||
regex_langinfo = re.compile("^[^'\"]*['\"]([^'\"]*)['\"]\s*:\s*['\"]([^'\"]*)['\"].*$")
|
||||
regex_backslash = re.compile(r"\\([\\{}%])")
|
||||
regex_plural = re.compile('%({.+?})')
|
||||
regex_plural_dict = re.compile('^{(?P<w>[^()[\]][^()[\]]*?)\((?P<n>[^()\[\]]+)\)}$') # %%{word(varname or number)}
|
||||
regex_plural_tuple = re.compile('^{(?P<w>[^[\]()]+)(?:\[(?P<i>\d+)\])?}$') # %%{word[index]} or %%{word}
|
||||
regex_plural_q = re.compile('^asdf$') # %%{?word?cnt}, %%{??cnt} or %%{?cnt}
|
||||
regex_plural_rules = re.compile('^plural_rules-[a-zA-Z]{2}(-[a-zA-Z]{2})?\.py$')
|
||||
|
||||
upper_fun = lambda s: unicode(s,'utf-8').upper().encode('utf-8')
|
||||
title_fun = lambda s: unicode(s,'utf-8').title().encode('utf-8')
|
||||
cap_fun = lambda s: unicode(s,'utf-8').capitalize().encode('utf-8')
|
||||
|
||||
# DEFAULT PLURAL-FORMS RULES:
|
||||
default_nplurals = 1 # language doesn't use plural forms
|
||||
default_get_plural_id = lambda n: 0 # only one singular/plural form is used
|
||||
default_construct_plural_form = lambda word, plural_id: word # word is unchangeable
|
||||
|
||||
ttab_in = maketrans("\\%{}", '\x1c\x1d\x1e\x1f')
|
||||
ttab_out = maketrans('\x1c\x1d\x1e\x1f', "\\%{}")
|
||||
|
||||
# cache of translated messages:
|
||||
# of structure:
|
||||
@@ -58,29 +80,37 @@ regex_langinfo = re.compile("^[^'\"]*['\"]([^'\"]*)['\"]\s*:\s*['\"]([^'\"]*)['\
|
||||
tcache={}
|
||||
|
||||
def get_from_cache(cache, val, fun):
|
||||
lock = cache[1]
|
||||
lock=cache[1]
|
||||
lock.acquire()
|
||||
try:
|
||||
result = cache[0].get(val);
|
||||
if not result:
|
||||
result = cache[0].setdefault(val, fun())
|
||||
result=cache[0].get(val);
|
||||
finally:
|
||||
lock.release()
|
||||
if result:
|
||||
return result
|
||||
lock.acquire()
|
||||
try:
|
||||
result=cache[0].setdefault(val, fun())
|
||||
finally:
|
||||
lock.release()
|
||||
return result
|
||||
|
||||
def clear_cache(cache):
|
||||
cache[1].acquire()
|
||||
lock=cache[1]
|
||||
lock.acquire()
|
||||
try:
|
||||
cache[0].clear();
|
||||
finally:
|
||||
cache[1].release()
|
||||
lock.release()
|
||||
|
||||
|
||||
def lang_sampling(lang_tuple, langlist):
|
||||
""" search lang_tuple in langlist
|
||||
""" search *lang_tuple* in *langlist*
|
||||
|
||||
Args:
|
||||
lang_tuple (tuple of strings): ('aa'[[,'-bb'],'-cc'])
|
||||
langlist (list of strings): [available languages]
|
||||
|
||||
Returns:
|
||||
language from langlist or None
|
||||
"""
|
||||
@@ -109,22 +139,23 @@ def read_dict_aux(filename):
|
||||
fp = portalocker.LockedFile(filename, 'r')
|
||||
lang_text = fp.read().replace('\r\n', '\n')
|
||||
fp.close()
|
||||
# clear cache of translated messages:
|
||||
# clear cache of processed messages:
|
||||
clear_cache(tcache.setdefault(filename, ({}, allocate_lock())))
|
||||
clear_cache(tcache.setdefault('@'+filename, ({}, allocate_lock())))
|
||||
if not lang_text.strip():
|
||||
return {}
|
||||
try:
|
||||
return eval(lang_text)
|
||||
except:
|
||||
logging.error('Syntax error in %s' % filename)
|
||||
return {'__corrupted__':True}
|
||||
except Exception as e:
|
||||
status='Syntax error in %s (%s)' % (filename, e)
|
||||
logging.error(status)
|
||||
return {'__corrupted__':status}
|
||||
|
||||
|
||||
def read_dict(filename):
|
||||
""" return dictionary with translation messages
|
||||
"""
|
||||
return getcfs('lang:'+filename, filename,
|
||||
lambda:read_dict_aux(filename))
|
||||
lambda: read_dict_aux(filename))
|
||||
|
||||
|
||||
def get_lang_info(lang, langdir):
|
||||
@@ -140,9 +171,8 @@ def get_lang_info(lang, langdir):
|
||||
tuple(langcode, langname, langfile_mtime)
|
||||
e.g.: ('en', 'English', 1338549043.0)
|
||||
"""
|
||||
filename = os.path.join(langdir, lang+'.py')
|
||||
langcode=''
|
||||
langname=''
|
||||
filename = ospath.join(langdir, lang+'.py')
|
||||
langcode=langname=''
|
||||
f = portalocker.LockedFile(filename, 'r')
|
||||
try:
|
||||
while not (langcode and langname):
|
||||
@@ -151,32 +181,28 @@ def get_lang_info(lang, langdir):
|
||||
break
|
||||
match=regex_langinfo.match(line)
|
||||
if match:
|
||||
if match.group(1) == '!langname!':
|
||||
langname=match.group(2)
|
||||
elif match.group(1) == '!langcode!':
|
||||
langcode=match.group(2)
|
||||
elif match.group(1)[0] > '!':
|
||||
k = match.group(1)
|
||||
if k == '!langname!':
|
||||
langname = match.group(2)
|
||||
elif k == '!langcode!':
|
||||
langcode = match.group(2)
|
||||
elif k[0:1] > '!':
|
||||
break
|
||||
finally:
|
||||
f.close()
|
||||
if not langcode:
|
||||
langcode = lang if lang != 'default' else 'en'
|
||||
return langcode, langname or langcode, os.stat(filename).st_mtime
|
||||
return langcode, langname or langcode, ostat(filename).st_mtime
|
||||
|
||||
def read_possible_languages_aux(langdir, item=None):
|
||||
if not item: item = {}
|
||||
def read_possible_languages_aux(langdir):
|
||||
langs = {}
|
||||
# scan languages directory for langfiles:
|
||||
for langfile in (listdir(langdir, regex_langfile) +
|
||||
listdir(langdir, '^default\.py$')):
|
||||
for langfile in [f for f in
|
||||
listdir(langdir, regex_langfile) +
|
||||
listdir(langdir, '^default\.py$')
|
||||
if osep not in f]:
|
||||
lang=langfile[:-3]
|
||||
if (lang in item and item[lang][2] ==
|
||||
os.stat(os.path.join(langdir, langfile)).st_mtime):
|
||||
# if langfile's mtime wasn't changed - use previous value:
|
||||
langs[lang]=item[lang]
|
||||
else:
|
||||
# otherwise, reread langfile:
|
||||
langs[lang]=get_lang_info(lang, langdir)
|
||||
langs[lang]=get_lang_info(lang, langdir)
|
||||
if 'default' not in langs:
|
||||
# if default.py is not found, add default value:
|
||||
langs['default'] = ('en', 'English', 0)
|
||||
@@ -187,83 +213,208 @@ def read_possible_languages_aux(langdir, item=None):
|
||||
return langs
|
||||
|
||||
def read_possible_languages(path):
|
||||
lang_path = os.path.join(path, 'languages')
|
||||
return getcfs('langs:'+path.rstrip(os.path.sep), lang_path,
|
||||
lang_path = ospath.join(path, 'languages')
|
||||
return getcfs('langs:'+lang_path, lang_path,
|
||||
lambda: read_possible_languages_aux(lang_path))
|
||||
|
||||
def utf8_repr(s):
|
||||
r"""
|
||||
|
||||
# note that we use raw strings to avoid having to use double back slashes below
|
||||
|
||||
utf8_repr() works same as repr() when processing ascii string
|
||||
>>> utf8_repr('abc') == utf8_repr("abc") == repr('abc') == repr("abc") == "'abc'"
|
||||
True
|
||||
>>> utf8_repr('a"b"c') == repr('a"b"c') == '\'a"b"c\''
|
||||
True
|
||||
>>> utf8_repr("a'b'c") == repr("a'b'c") == '"a\'b\'c"'
|
||||
True
|
||||
>>> utf8_repr('a\'b"c') == repr('a\'b"c') == utf8_repr("a'b\"c") == repr("a'b\"c") == '\'a\\\'b"c\''
|
||||
True
|
||||
>>> utf8_repr('a\r\nb') == repr('a\r\nb') == "'a\\r\\nb'" # Test for \r, \n
|
||||
True
|
||||
def read_plural_rules_aux(filename):
|
||||
"""retrieve plural rules from rules/*plural_rules-lang*.py file.
|
||||
|
||||
Unlike repr(), utf8_repr() remains utf8 content when processing utf8 string
|
||||
>>> utf8_repr('中文字') == utf8_repr("中文字") == "'中文字'" != repr('中文字')
|
||||
True
|
||||
>>> utf8_repr('中"文"字') == "'中\"文\"字'" != repr('中"文"字')
|
||||
True
|
||||
>>> utf8_repr("中'文'字") == '"中\'文\'字"' != repr("中'文'字")
|
||||
True
|
||||
>>> utf8_repr('中\'文"字') == utf8_repr("中'文\"字") == '\'中\\\'文"字\'' != repr('中\'文"字') == repr("中'文\"字")
|
||||
True
|
||||
>>> utf8_repr('中\r\n文') == "'中\\r\\n文'" != repr('中\r\n文') # Test for \r, \n
|
||||
True
|
||||
args:
|
||||
filename (str): plural_rules filename
|
||||
|
||||
returns:
|
||||
tuple(nplurals, get_plural_id, construct_plural_form)
|
||||
e.g.: (3, <function>, <function>)
|
||||
"""
|
||||
if (s.find("'") >= 0) and (s.find('"') < 0): # only single quote exists
|
||||
s = ''.join(['"', s, '"']) # s = ''.join(['"', s.replace('"','\\"'), '"'])
|
||||
else:
|
||||
s = ''.join(["'", s.replace("'","\\'"), "'"])
|
||||
return s.replace("\n","\\n").replace("\r","\\r")
|
||||
f = portalocker.LockedFile(filename, 'r')
|
||||
plural_py=f.read().replace('\r\n','\n')
|
||||
f.close()
|
||||
try:
|
||||
exec(plural_py)
|
||||
nplurals=locals().get('nplurals', default_nplurals)
|
||||
get_plural_id=locals().get('get_plural_id', default_get_plural_id)
|
||||
construct_plural_form=locals().get('construct_plural_form',
|
||||
default_construct_plural_form)
|
||||
status='ok'
|
||||
except Exception as e:
|
||||
nplurals=default_nplurals
|
||||
get_plural_id=default_get_plural_id
|
||||
construct_plural_form=default_construct_plural_form
|
||||
status='Syntax error in %s (%s)' % (filename, e)
|
||||
logging.error(status)
|
||||
return (nplurals, get_plural_id, construct_plural_form, status)
|
||||
|
||||
def read_plural_rules(lang):
|
||||
filename = abspath('gluon','contrib','rules', 'plural_rules-%s.py' % lang)
|
||||
return getcfs('plural_rules-'+lang, filename,
|
||||
lambda: read_plural_rules_aux(filename))
|
||||
|
||||
pcache={}
|
||||
def read_possible_plurals():
|
||||
""" create list of all possible plural rules files
|
||||
result is cached to increase speed
|
||||
"""
|
||||
global pcache
|
||||
pdir = abspath('gluon','contrib','rules')
|
||||
plurals = {}
|
||||
# scan rules directory for plural_rules-*.py files:
|
||||
for pname in [f for f in listdir(pdir, regex_plural_rules)
|
||||
if osep not in f]:
|
||||
|
||||
lang=pname[13:-3]
|
||||
fname=ospath.join(pdir, pname)
|
||||
mtime=ostat(fname).st_mtime
|
||||
if lang in pcache and pcache[lang][2] == mtime:
|
||||
# if plural_file's mtime wasn't changed - use previous value:
|
||||
plurals[lang]=pcache[lang]
|
||||
else:
|
||||
# otherwise, reread plural_rules-file:
|
||||
if 'plural_rules-'+lang in cfs:
|
||||
n,f1,f2,status=read_plural_rules(lang)
|
||||
else:
|
||||
n,f1,f2,status=read_plural_rules_aux(fname)
|
||||
plurals[lang]=(n, pname, mtime, status)
|
||||
pcache=plurals
|
||||
return pcache
|
||||
|
||||
def get_plural_rules(languages):
|
||||
"""get plural-forms rules for language *lang*
|
||||
if rules not found - default rules will be return and lang=='unknown'
|
||||
|
||||
args:
|
||||
lang (str): the languages, for one of which the plural-forms is return
|
||||
|
||||
returns:
|
||||
tuples(lang, plural_rules-filename, nplurals,
|
||||
get_plural_id(), construct_plural_form(), status)
|
||||
"""
|
||||
if isinstance(languages, str):
|
||||
languages = [languages]
|
||||
|
||||
all_plurals=read_possible_plurals()
|
||||
for lang in languages:
|
||||
match_language = regex_language.match(lang.strip().lower())
|
||||
if match_language:
|
||||
match_language = tuple(part
|
||||
for part in match_language.groups()
|
||||
if part)
|
||||
lang = lang_sampling(match_language, all_plurals.keys())
|
||||
if lang:
|
||||
( nplurals,
|
||||
get_plural_id,
|
||||
construct_plural_form,
|
||||
status
|
||||
) = read_plural_rules(lang)
|
||||
return (lang, all_plurals[lang][1], nplurals,
|
||||
get_plural_id,
|
||||
construct_plural_form,
|
||||
status)
|
||||
return ('unknown', None, default_nplurals,
|
||||
default_get_plural_id,
|
||||
default_construct_plural_form,
|
||||
'ok')
|
||||
|
||||
|
||||
def write_dict(filename, contents):
|
||||
if contents.get('__corrupted__',False):
|
||||
def read_plural_dict_aux(filename):
|
||||
fp = portalocker.LockedFile(filename, 'r')
|
||||
lang_text = fp.read().replace('\r\n', '\n')
|
||||
fp.close()
|
||||
if not lang_text.strip():
|
||||
return {}
|
||||
try:
|
||||
return eval(lang_text)
|
||||
except Exception as e:
|
||||
status='Syntax error in %s (%s)' % (filename, e)
|
||||
logging.error(status)
|
||||
return {'__corrupted__':status}
|
||||
|
||||
def read_plural_dict(filename):
|
||||
return getcfs('plurals:'+filename, filename,
|
||||
lambda: read_plural_dict_aux(filename))
|
||||
|
||||
|
||||
def write_plural_dict(filename, contents):
|
||||
if '__corrupted__' in contents:
|
||||
return
|
||||
try:
|
||||
fp = portalocker.LockedFile(filename, 'w')
|
||||
fp.write('#!/usr/bin/env python\n{\n# "singular form (0)": ["first plural form (1)", "second plural form (2)", ...],\n')
|
||||
# coding: utf8\n{\n')
|
||||
for key in sorted(contents,lambda x,y: cmp(unicode(x,'utf-8').lower(), unicode(y,'utf-8').lower())):
|
||||
forms = '['+','.join([repr(Utf8(form)) for form in contents[key]])+']'
|
||||
fp.write('%s: %s,\n' % (repr(Utf8(key)), forms))
|
||||
fp.write('}\n')
|
||||
except (IOError, OSError):
|
||||
if not is_gae:
|
||||
logging.warning('Unable to write to file %s' % filename)
|
||||
return
|
||||
finally:
|
||||
fp.close()
|
||||
|
||||
|
||||
def write_dict(filename, contents):
|
||||
if '__corrupted__' in contents:
|
||||
return
|
||||
try:
|
||||
fp = portalocker.LockedFile(filename, 'w')
|
||||
except (IOError, OSError):
|
||||
if not settings.global_settings.web2py_runtime_gae:
|
||||
logging.warning('Unable to write to file %s' % filename)
|
||||
return
|
||||
fp.write('# coding: utf8\n{\n')
|
||||
for key in sorted(contents):
|
||||
fp.write('%s: %s,\n' % (utf8_repr(key), utf8_repr(contents[key])))
|
||||
for key in sorted(contents,lambda x,y: cmp(unicode(x,'utf-8').lower(), unicode(y,'utf-8').lower())):
|
||||
fp.write('%s: %s,\n' % (repr(Utf8(key)), repr(Utf8(contents[key]))))
|
||||
fp.write('}\n')
|
||||
fp.close()
|
||||
|
||||
|
||||
|
||||
class lazyT(object):
|
||||
|
||||
"""
|
||||
never to be called explicitly, returned by translator.__call__
|
||||
never to be called explicitly, returned by
|
||||
translator.__call__() or translator.M()
|
||||
"""
|
||||
m = s = T = None
|
||||
m = s = T = f = t = M = None
|
||||
|
||||
def __init__(self, message, symbols = {}, T = None, filter=None):
|
||||
self.m, self.s, self.T, self.f = message, symbols, T, filter
|
||||
def __init__(
|
||||
self,
|
||||
message,
|
||||
symbols = {},
|
||||
T = None,
|
||||
filter = None,
|
||||
ftag = None,
|
||||
M = False
|
||||
):
|
||||
self.M = M
|
||||
if isinstance(message, lazyT):
|
||||
self.m = message.m
|
||||
self.s = symbols or message.s
|
||||
self.T = T or message.T
|
||||
self.f = filter or message.f
|
||||
self.t = ftag or message.t
|
||||
else:
|
||||
self.m = message
|
||||
self.s = symbols
|
||||
self.T = T
|
||||
self.f = filter
|
||||
self.t = ftag
|
||||
|
||||
def __repr__(self):
|
||||
return "<lazyT %s>" % (repr(str(self.m)), )
|
||||
|
||||
def __str__(self):
|
||||
return self.T.translate(self.m, self.s, self.f)
|
||||
return str(self.T.apply_filter(self.m, self.s, self.f, self.t) if self.M else
|
||||
self.T.translate(self.m, self.s))
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.T.translate(self.m, self.s) == other
|
||||
return (self.T.apply_filter(self.m, self.s, self.f, self.t) if self.M else
|
||||
self.T.translate(self.m, self.s)) == other
|
||||
|
||||
def __ne__(self, other):
|
||||
return self.T.translate(self.m, self.s) != other
|
||||
return (self.T.apply_filter(self.m, self.s, self.f, self.t) if self.M else
|
||||
self.T.translate(self.m, self.s)) != other
|
||||
|
||||
def __add__(self, other):
|
||||
return '%s%s' % (self, other)
|
||||
@@ -293,7 +444,7 @@ class lazyT(object):
|
||||
return len(str(self))
|
||||
|
||||
def xml(self):
|
||||
return cgi.escape(str(self))
|
||||
return str(self) if self.M else escape(str(self))
|
||||
|
||||
def encode(self, *a, **b):
|
||||
return str(self).encode(*a, **b)
|
||||
@@ -305,7 +456,8 @@ class lazyT(object):
|
||||
return str(self)
|
||||
|
||||
def __mod__(self, symbols):
|
||||
return self.T.translate(self.m, symbols, filter=self.f)
|
||||
return (self.T.apply_filter(self.m, self.s, self.f, self.t) if self.M else
|
||||
self.T.translate(self.m, self.s))
|
||||
|
||||
|
||||
class translator(object):
|
||||
@@ -314,6 +466,8 @@ class translator(object):
|
||||
this class is instantiated by gluon.compileapp.build_environment
|
||||
as the T object
|
||||
|
||||
::
|
||||
|
||||
T.force(None) # turns off translation
|
||||
T.force('fr, it') # forces web2py to translate using fr.py or it.py
|
||||
|
||||
@@ -332,25 +486,35 @@ class translator(object):
|
||||
global tcache
|
||||
self.request = request
|
||||
self.folder = request.folder
|
||||
default = os.path.join(self.folder,'languages','default.py')
|
||||
if os.path.exists(default):
|
||||
self.default_language_file = default
|
||||
self.default_t = read_dict(default)
|
||||
dfile = ospath.join(self.folder,'languages','default.py')
|
||||
if ospath.exists(dfile):
|
||||
self.default_language_file = dfile
|
||||
self.default_t = read_dict(dfile)
|
||||
else: # languages/default.py is not found
|
||||
self.default_language_file = os.path.join(self.folder,'languages','')
|
||||
self.default_language_file = ospath.join(self.folder, 'languages','')
|
||||
self.default_t = {}
|
||||
self.cache = tcache.setdefault(self.default_language_file, ({}, allocate_lock()))
|
||||
self.current_languages = [self.get_possible_languages_info('default')[0]]
|
||||
self.accepted_language = None # filed in self.force()
|
||||
self.language_file = None # filed in self.force()
|
||||
self.http_accept_language = request.env.http_accept_language
|
||||
# self.accepted_language = None # filled in self.force()
|
||||
# self.language_file = None # filled in self.force()
|
||||
# self.plural_language = None # filled in self.force()
|
||||
# self.nplurals = None # filled in self.force()
|
||||
# self.get_plural_id = None # filled in self.force()
|
||||
# self.construct_plural_form = None # filled in self.force()
|
||||
# self.plural_rules_file = None # filled in self.force()
|
||||
# self.plural_file = None # filled in self.force()
|
||||
# self.plural_dict = None # filled in self.force()
|
||||
# self.plural_status = None # filled in self.force()
|
||||
self.requested_languages = self.force(self.http_accept_language)
|
||||
self.lazy = True
|
||||
self.otherTs = {}
|
||||
self.filter = markmin
|
||||
self.ftag = 'markmin'
|
||||
|
||||
def get_possible_languages_info(self, lang=None):
|
||||
"""
|
||||
returns info for selected language or dictionary with all
|
||||
return info for selected language or dictionary with all
|
||||
possible languages info from APP/languages/*.py
|
||||
args:
|
||||
*lang* (str): language
|
||||
@@ -386,6 +550,65 @@ class translator(object):
|
||||
self.current_languages = languages
|
||||
self.force(self.http_accept_language)
|
||||
|
||||
def set_plural(self, languages):
|
||||
""" initialize plural forms subsystem
|
||||
invoked from self.force()
|
||||
"""
|
||||
( self.plural_language,
|
||||
self.plural_rules_file,
|
||||
self.nplurals,
|
||||
self.get_plural_id,
|
||||
self.construct_plural_form,
|
||||
self.plural_status
|
||||
) = get_plural_rules(languages)
|
||||
|
||||
if self.plural_language == 'unknown':
|
||||
self.plural_file = None
|
||||
self.plural_dict = {}
|
||||
else:
|
||||
self.plural_file = ospath.join(self.folder,
|
||||
'languages',
|
||||
'plural-%s.py' % self.plural_language)
|
||||
if ospath.exists(self.plural_file):
|
||||
self.plural_dict = read_plural_dict(self.plural_file)
|
||||
else:
|
||||
self.plural_dict = {}
|
||||
|
||||
def plural(self, word, n):
|
||||
""" get plural form of word for number *n*
|
||||
NOTE: *word" MUST be defined in current language
|
||||
(T.accepted_language)
|
||||
|
||||
invoked from T()/M() in %%{} tag
|
||||
args:
|
||||
word (str): word in singular
|
||||
n (numeric): number plural form created for
|
||||
|
||||
returns:
|
||||
(str): word in appropriate singular/plural form
|
||||
"""
|
||||
nplurals = self.nplurals
|
||||
if word:
|
||||
id = self.get_plural_id(abs(int(n)))
|
||||
if id > 0:
|
||||
forms = self.plural_dict.get(word, [])
|
||||
if forms:
|
||||
try:
|
||||
form = forms[id-1]
|
||||
except:
|
||||
form = None
|
||||
if form: return form
|
||||
form = self.construct_plural_form(word, id)
|
||||
if len(forms) < nplurals-1:
|
||||
forms.extend('' for i in xrange(nplurals-len(forms)-1))
|
||||
forms[id-1] = form
|
||||
self.plural_dict[word] = forms
|
||||
if (self.plural_file and
|
||||
not settings.global_settings.web2py_runtime_gae):
|
||||
write_plural_dict(self.plural_file, self.plural_dict)
|
||||
return form
|
||||
return word
|
||||
|
||||
def force(self, *languages):
|
||||
"""
|
||||
|
||||
@@ -395,11 +618,13 @@ class translator(object):
|
||||
first language from this list that matches the ones
|
||||
from the possible_languages dictionary will be
|
||||
selected
|
||||
|
||||
|
||||
default language will be selected if none
|
||||
of them matches possible_languages.
|
||||
"""
|
||||
global tcache
|
||||
language = ''
|
||||
|
||||
if not languages or languages[0] is None:
|
||||
languages = []
|
||||
if len(languages) == 1 and isinstance(languages[0], (str, unicode)):
|
||||
@@ -413,8 +638,8 @@ class translator(object):
|
||||
languages.extend(al.split(','))
|
||||
|
||||
possible_languages = self.get_possible_languages()
|
||||
for language in languages:
|
||||
match_language = regex_language.match(language.strip().lower())
|
||||
for lang in languages:
|
||||
match_language = regex_language.match(lang.strip().lower())
|
||||
if match_language:
|
||||
match_language = tuple(part
|
||||
for part in match_language.groups()
|
||||
@@ -422,43 +647,91 @@ class translator(object):
|
||||
language = lang_sampling(match_language,
|
||||
self.current_languages)
|
||||
if language:
|
||||
self.accepted_language = language
|
||||
break
|
||||
language = lang_sampling(match_language, possible_languages)
|
||||
if language:
|
||||
self.language_file = os.path.join(self.folder,
|
||||
'languages',
|
||||
language + '.py')
|
||||
if os.path.exists(self.language_file):
|
||||
self.language_file = ospath.join(self.folder,
|
||||
'languages',
|
||||
language + '.py')
|
||||
if ospath.exists(self.language_file):
|
||||
self.t = read_dict(self.language_file)
|
||||
self.cache = tcache.setdefault(self.language_file,
|
||||
({},allocate_lock()))
|
||||
self.set_plural(language)
|
||||
self.accepted_language = language
|
||||
self.cache = tcache.setdefault(
|
||||
self.language_file, ({},allocate_lock()))
|
||||
return languages
|
||||
self.accepted_language = language or self.current_languages[0]
|
||||
self.language_file = self.default_language_file
|
||||
self.cache = tcache[self.language_file]
|
||||
self.t = self.default_t
|
||||
self.set_plural(language or self.current_languages)
|
||||
return languages
|
||||
|
||||
def __call__(self, message, symbols={}, language=None, lazy=None, filter=None):
|
||||
def __call__(self, message, symbols={}, language=None, lazy=None):
|
||||
"""
|
||||
get cached translated plain text message with inserted parameters(symbols)
|
||||
if lazy==True lazyT object is returned
|
||||
"""
|
||||
lazy = lazy or self.lazy
|
||||
if not language and lazy:
|
||||
return lazyT(message, symbols, self, filter=filter)
|
||||
elif not language:
|
||||
return self.translate(message, symbols, filter=filter)
|
||||
elif language in self.otherTs:
|
||||
otherT = self.otherTs[language]
|
||||
if lazy is None:
|
||||
lazy = self.lazy
|
||||
if not language:
|
||||
if lazy :
|
||||
return lazyT(message, symbols, self)
|
||||
else:
|
||||
return self.translate(message, symbols)
|
||||
else:
|
||||
otherT = self.otherTs[language] = translator(self.request)
|
||||
otherT.force(language)
|
||||
return otherT(message, symbols, lazy=lazy, filter=filter)
|
||||
try:
|
||||
otherT = self.otherTs[language]
|
||||
except KeyError:
|
||||
otherT = self.otherTs[language] = translator(self.request)
|
||||
otherT.force(language)
|
||||
return otherT(message, symbols, lazy=lazy)
|
||||
|
||||
def apply_filter(self, message, symbols={}, filter=None, ftag=None):
|
||||
def get_tr(message, prefix, filter):
|
||||
s = self.get_t(message, prefix)
|
||||
return filter(s) if filter else self.filter(s)
|
||||
if filter:
|
||||
prefix = '@'+(ftag or 'userdef')+'\x01'
|
||||
else:
|
||||
prefix = '@'+self.ftag+'\x01'
|
||||
message = get_from_cache(self.cache, prefix+message,
|
||||
lambda: get_tr(message, prefix, filter))
|
||||
if symbols or symbols == 0 or symbols == "":
|
||||
if isinstance(symbols, dict):
|
||||
symbols.update( (key, xmlescape(value).translate(ttab_in))
|
||||
for key, value in symbols.iteritems()
|
||||
if not isinstance(value, numbers.Number) )
|
||||
else:
|
||||
if not isinstance(symbols, tuple):
|
||||
symbols = (symbols,)
|
||||
symbols = tuple(value if isinstance(value, numbers.Number)
|
||||
else xmlescape(value).translate(ttab_in)
|
||||
for value in symbols)
|
||||
message = self.params_substitution(message, symbols)
|
||||
return XML(message.translate(ttab_out))
|
||||
|
||||
def get_t(self, message, filter=None, startwith_templ='##'):
|
||||
def M(self, message, symbols={}, language=None, lazy=None, filter=None, ftag=None):
|
||||
""" get cached translated markmin-message with inserted parametes
|
||||
|
||||
if lazy==True lazyT object is returned
|
||||
"""
|
||||
if lazy is None:
|
||||
lazy = self.lazy
|
||||
if not language:
|
||||
if lazy:
|
||||
return lazyT(message, symbols, self, filter, ftag, True)
|
||||
else:
|
||||
return self.apply_filter(message, symbols, filter, ftag)
|
||||
else:
|
||||
try:
|
||||
otherT = self.otherTs[language]
|
||||
except KeyError:
|
||||
otherT = self.otherTs[language] = translator(self.request)
|
||||
otherT.force(language)
|
||||
return otherT.M(message, symbols, lazy=lazy)
|
||||
|
||||
def get_t(self, message, prefix=''):
|
||||
"""
|
||||
user ## to add a comment into a translation string
|
||||
the comment can be useful do discriminate different possible
|
||||
@@ -471,50 +744,140 @@ class translator(object):
|
||||
the ## notation is ignored in multiline strings and strings that
|
||||
start with ##. this is to allow markmin syntax to be translated
|
||||
"""
|
||||
mt = self.t.get(message, None)
|
||||
if mt is None:
|
||||
if not message.startswith(startwith_templ) and not '\n' in message:
|
||||
tokens = message.rsplit('##', 1)
|
||||
else:
|
||||
# this allows markmin syntax in translations
|
||||
tokens = [message]
|
||||
self.t[message] = mt = self.default_t.get(message, tokens[0])
|
||||
if (self.language_file != self.default_language_file and not is_gae):
|
||||
write_dict(self.language_file, self.t)
|
||||
if filter:
|
||||
mt = filter(mt)
|
||||
return mt
|
||||
key = prefix+message
|
||||
mt = self.t.get(key, None)
|
||||
if mt is not None:
|
||||
return mt
|
||||
if not message.startswith('##') and not '\n' in message:
|
||||
tokens = message.rsplit('##', 1)
|
||||
else:
|
||||
# this allows markmin syntax in translations
|
||||
tokens = [message]
|
||||
self.t[key] = mt = self.default_t.get(key, tokens[0])
|
||||
if (self.language_file != self.default_language_file and
|
||||
not settings.global_settings.web2py_runtime_gae):
|
||||
write_dict(self.language_file, self.t)
|
||||
return regex_backslash.sub(lambda m: m.group(1).translate(ttab_in), mt)
|
||||
|
||||
def params_substitution(self, message, symbols):
|
||||
"""
|
||||
substitute parameters from symbols into message using %.
|
||||
also parse %%{} placeholders for plural-forms processing.
|
||||
returns: string with parameters
|
||||
NOTE: *symbols* MUST BE OR tuple OR dict of parameters!
|
||||
"""
|
||||
return message % symbols
|
||||
def sub_plural(m):
|
||||
"""string in %{} is transformed by this rules:
|
||||
If string starts with \\, ! or ? such transformations
|
||||
take place:
|
||||
|
||||
def translate(self, message, symbols, filter=None):
|
||||
"!string of words" -> "String of word" (Capitalize)
|
||||
"!!string of words" -> "String Of Word" (Title)
|
||||
"!!!string of words" -> "STRING OF WORD" (Upper)
|
||||
"\\!string of words" -> "!string of word"
|
||||
(remove \\ and disable transformations)
|
||||
"?word?number" -> "word" (return word, if number == 1)
|
||||
"?number" or "??number" -> "" (remove number,
|
||||
if number == 1)
|
||||
"?word?number" -> "number" (if number != 1)
|
||||
"""
|
||||
def sub_tuple(m):
|
||||
""" word[number], !word[number], !!word[number], !!!word[number]
|
||||
word, !word, !!word, !!!word, ?word?number, ??number, ?number
|
||||
?word?word[number], ?word?[number], ??word[number]
|
||||
"""
|
||||
w,i = m.group('w','i')
|
||||
c = w[0]
|
||||
word = w[c=='\\':]
|
||||
if c not in '!?':
|
||||
return self.plural(word, symbols[int(i or 0)])
|
||||
elif c == '?':
|
||||
part2 = w[max(1,w.find('?',1)+1):]
|
||||
if i is None:
|
||||
# ?[word]?number or ?number
|
||||
num = part2
|
||||
else:
|
||||
# ?[word]?word2[number], ?word2[number] or ?word2[number]
|
||||
num = symbols[int(i or 0)]
|
||||
return w[1:abs(w.find('?',1))] if int(num) == 1 else part2
|
||||
elif w.startswith('!!!'):
|
||||
word = w[3:]
|
||||
fun = upper_fun
|
||||
elif w.startswith('!!'):
|
||||
word = w[2:]
|
||||
fun = title_fun
|
||||
else:
|
||||
word = w[1:]
|
||||
fun = cap_fun
|
||||
if i is not None:
|
||||
return fun(self.plural(word, symbols[int(i)]))
|
||||
return fun(word)
|
||||
|
||||
def sub_dict(m):
|
||||
""" word(var), !word(var), !!word(var), !!!word(var)
|
||||
word(num), !word(num), !!word(num), !!!word(num)
|
||||
"""
|
||||
w,n = m.group('w','n')
|
||||
c = w[0]
|
||||
word = w[c=='\\':]
|
||||
n = int(n) if n.isdigit() else symbols[n]
|
||||
if c not in '!?':
|
||||
return self.plural(word, n)
|
||||
elif c == '?':
|
||||
# ?[word]?word2(var or num), ?word2(var or num) or ?word2(var or num)
|
||||
return w[1:abs(w.find('?',1))] if int(n) == 1 else w[max(1,w.find('?',1)+1):]
|
||||
elif w.startswith('!!!'):
|
||||
word = w[3:]
|
||||
fun = upper_fun
|
||||
elif w.startswith('!!'):
|
||||
word = w[2:]
|
||||
fun = title_fun
|
||||
else:
|
||||
word = w[1:]
|
||||
fun = cap_fun
|
||||
return fun(self.plural(word, n))
|
||||
|
||||
s = m.group(1)
|
||||
part = regex_plural_tuple.sub(sub_tuple, s)
|
||||
if part == s:
|
||||
part = regex_plural_dict.sub(sub_dict, s)
|
||||
if part == s:
|
||||
return m.group(0)
|
||||
return part
|
||||
message = message % symbols
|
||||
message = regex_plural.sub(sub_plural, message )
|
||||
return message
|
||||
|
||||
def translate(self, message, symbols):
|
||||
"""
|
||||
get cached translated message with inserted parameters(symbols)
|
||||
"""
|
||||
if isinstance(message,lazyT):
|
||||
message = str(message)
|
||||
message = get_from_cache(self.cache, (message, filter),
|
||||
lambda: self.get_t(message,filter))
|
||||
message = get_from_cache(self.cache, message, lambda: self.get_t(message))
|
||||
if symbols or symbols == 0 or symbols == "":
|
||||
return self.params_substitution(message, symbols)
|
||||
return message
|
||||
if isinstance(symbols, dict):
|
||||
symbols.update( (key, str(value).translate(ttab_in))
|
||||
for key, value in symbols.iteritems()
|
||||
if not isinstance(value, numbers.Number) )
|
||||
else:
|
||||
if not isinstance(symbols, tuple):
|
||||
symbols = (symbols,)
|
||||
symbols = tuple(value if isinstance(value, numbers.Number)
|
||||
else str(value).translate(ttab_in)
|
||||
for value in symbols)
|
||||
|
||||
message = self.params_substitution(message, symbols)
|
||||
return message.translate(ttab_out)
|
||||
|
||||
def findT(path, language='en'):
|
||||
"""
|
||||
must be run by the admin app
|
||||
"""
|
||||
filename = os.path.join(path, 'languages', language +'.py')
|
||||
filename = ospath.join(path, 'languages', language + '.py')
|
||||
sentences = read_dict(filename)
|
||||
mp = os.path.join(path, 'models')
|
||||
cp = os.path.join(path, 'controllers')
|
||||
vp = os.path.join(path, 'views')
|
||||
mop = os.path.join(path, 'modules')
|
||||
mp = ospath.join(path, 'models')
|
||||
cp = ospath.join(path, 'controllers')
|
||||
vp = ospath.join(path, 'views')
|
||||
mop = ospath.join(path, 'modules')
|
||||
for file in listdir(mp, '^.+\.py$', 0) + listdir(cp, '^.+\.py$', 0)\
|
||||
+ listdir(vp, '^.+\.html$', 0) + listdir(mop, '^.+\.py$', 0):
|
||||
fp = portalocker.LockedFile(file, 'r')
|
||||
@@ -552,7 +915,7 @@ copy_reg.pickle(lazyT, lazyT_pickle, lazyT_unpickle)
|
||||
|
||||
|
||||
def update_all_languages(application_path):
|
||||
path = os.path.join(application_path, 'languages/')
|
||||
path = ospath.join(application_path, 'languages/')
|
||||
for language in listdir(path, regex_langfile):
|
||||
findT(application_path, language[:-3])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user