improved language handling, thanks Vladyslav

This commit is contained in:
mdipierro
2012-07-12 14:58:29 -05:00
parent 4b42023171
commit 76b0339148
7 changed files with 336 additions and 161 deletions
+1 -1
View File
@@ -1 +1 @@
Version 2.00.0 (2012-07-12 14:05:01) dev
Version 2.00.0 (2012-07-12 14:58:26) dev
+4 -1
View File
@@ -909,7 +909,10 @@ def plugin():
statics.sort()
# Get all languages
languages = listdir(apath('%s/languages/' % app, r=request), '[\w-]*\.py')
languages = sorted([lang+'.py' for lang, info in
T.get_possible_languages_info().iteritems()
if info[2]!=0]) # info[2] is langfile_mtime:
# get only existed files
#Get crontab
crontab = apath('%s/cron/crontab' % app, r=request)
+1
View File
@@ -144,6 +144,7 @@
'Use an url:': 'Use an url:',
'Version': 'Version',
'Views': 'Views',
'Web Framework': 'Web Framework',
'Welcome to web2py': 'Добре дошъл в web2py',
'YES': 'YES',
'additional code for your application': 'additional code for your application',
+25 -22
View File
@@ -26,6 +26,7 @@
'Administrator Password:': 'Пароль администратора:',
'Application name:': 'Название приложения:',
'Are you sure you want to delete file "%s"?': 'Вы действительно хотите удалить файл "%s"?',
'Are you sure you want to delete this object?': 'Are you sure you want to delete this object?',
'Are you sure you want to uninstall application "%s"': 'Вы действительно хотите удалить приложение "%s"',
'Are you sure you want to uninstall application "%s"?': 'Вы действительно хотите удалить приложение "%s"?',
'Are you sure you want to upgrade web2py now?': 'Вы действительно хотите обновить web2py сейчас?',
@@ -34,12 +35,17 @@
'Cannot be empty': 'Не может быть пустым',
'Cannot compile: there are errors in your app. Debug it, correct errors and try again.': 'Невозможно компилировать: в приложении присутствуют ошибки. Отладьте его, исправьте ошибки и попробуйте заново.',
'Change Password': 'Изменить пароль',
'Change admin password': 'Изменить пароль администратора',
'Check for upgrades': 'проверить обновления',
'Check to delete': 'Поставьте для удаления',
'Checking for upgrades...': 'Проверка обновлений...',
'Clean': 'Очистить',
'Client IP': 'IP клиента',
'Compile': 'Компилировать',
'Controller': 'Контроллер',
'Controllers': 'Контроллеры',
'Copyright': 'Copyright',
'Create': 'Создать',
'Create new simple application': 'Создать новое простое приложение',
'Current request': 'Текущий запрос',
'Current response': 'Текущий ответ',
@@ -48,11 +54,15 @@
'DESIGN': 'ДИЗАЙН',
'Database': 'База данных',
'Date and Time': 'Дата и время',
'Debug': 'Debug',
'Delete': 'Удалить',
'Delete:': 'Удалить:',
'Deploy': 'Развернуть',
'Deploy on Google App Engine': 'Развернуть на Google App Engine',
'Deploy to OpenShift': 'Deploy to OpenShift',
'Description': 'Описание',
'Design for': 'Дизайн для',
'Disable': 'Disable',
'E-mail': 'E-mail',
'EDIT': 'ПРАВКА',
'Edit': 'Правка',
@@ -65,18 +75,22 @@
'Editing file "%s"': 'Правка файла "%s"',
'Enterprise Web Framework': 'Enterprise Web Framework',
'Error logs for "%(app)s"': 'Журнал ошибок для "%(app)"',
'Errors': 'Ошибка',
'Exception instance attributes': 'Атрибуты объекта исключения',
'Expand Abbreviation': 'Раскрыть аббревиатуру',
'First name': 'Имя',
'Functions with no doctests will result in [passed] tests.': 'Функции без doctest будут давать [прошел] в тестах.',
'Get from URL:': 'Get from URL:',
'Go to Matching Pair': 'К подходящей паре',
'Group ID': 'ID группы',
'Hello World': 'Привет, Мир',
'Help': 'Помощь',
'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'Если отчет выше содержит номер ошибки, это указывает на ошибку при работе контроллера, до попытки выполнить doctest. Причиной чаще является неверные отступы или ошибки в коде вне функции. \nЗеленый заголовок указывает на успешное выполнение всех тестов. В этом случае результаты тестов не показываются.',
'If you answer "yes", be patient, it may take a while to download': 'Если вы ответили "Да", потерпите, загрузка может потребовать времени',
'If you answer yes, be patient, it may take a while to download': 'Если вы ответили "Да", потерпите, загрузка может потребовать времени',
'Import/Export': 'Импорт/Экспорт',
'Index': 'Индекс',
'Install': 'Установить',
'Installed applications': 'Установленные приложения',
'Internal State': 'Внутренний статус',
'Invalid Query': 'Неверный запрос',
@@ -92,7 +106,7 @@
'License for': 'Лицензия для',
'Login': 'Логин',
'Login to the Administrative Interface': 'Вход в интерфейс администратора',
'Logout': 'Выход',
'Logout': 'выход',
'Lost Password': 'Забыли пароль',
'Main Menu': 'Главное меню',
'Match Pair': 'Найти пару',
@@ -109,6 +123,9 @@
'No databases in this application': 'В приложении нет базы данных',
'Origin': 'Оригинал',
'Original/Translation': 'Оригинал/Перевод',
'Overwrite installed app': 'Переписать на установленное приложение',
'Pack all': 'упаковать все',
'Pack compiled': 'Архив скомпилирован',
'Password': 'Пароль',
'Peeking at file': 'Просмотр',
'Plugin "%s" in application': 'Плагин "%s" в приложении',
@@ -119,14 +136,19 @@
'Record ID': 'ID записи',
'Register': 'Зарегистрироваться',
'Registration key': 'Ключ регистрации',
'Reload routes': 'Reload routes',
'Remove compiled': 'Удалить скомпилированное',
'Reset Password key': 'Сброс пароля',
'Resolve Conflict file': 'Решить конфликт в файле',
'Role': 'Роль',
'Rows in table': 'Строк в таблице',
'Rows selected': 'Выбрано строк',
'Running on %s': 'Running on %s',
'Save via Ajax': 'Сохранить через Ajax',
'Saved file hash:': 'Хэш сохраненного файла:',
'Searching:': 'Поиск:',
'Site': 'сайт',
'Start wizard': 'запустить мастер',
'Static files': 'Статические файлы',
'Stylesheet': 'Таблицы стилей',
'Sure you want to delete this object?': 'Действительно хотите удалить данный объект?',
@@ -156,9 +178,11 @@
'Unable to check for upgrades': 'Невозможно проверить обновления',
'Unable to download': 'Невозможно загрузить',
'Unable to download app': 'Невозможно загрузить',
'Uninstall': 'Удалить',
'Update:': 'Обновить:',
'Upload & install packed application': 'Загрузить и установить приложение в архиве',
'Upload a package:': 'Загрузить пакет:',
'Upload and install packed application': 'Upload and install packed application',
'Upload existing application': 'Загрузить существующее приложение',
'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Используйте (...)&(...) для AND, (...)|(...) для OR, и ~(...) для NOT при создании сложных запросов.',
'Use an url:': 'Используйте url:',
@@ -174,7 +198,6 @@
'You are successfully running web2py': 'Вы успешно запустили web2by',
'You can modify this application and adapt it to your needs': 'Вы можете изменить это приложение и подогнать под свои нужды',
'You visited the url': 'Вы посетили URL',
'About': 'О',
'additional code for your application': 'добавочный код для вашего приложения',
'admin disabled because no admin password': 'админка отключена, потому что отсутствует пароль администратора',
'admin disabled because not supported on google apps engine': 'админка отключена, т.к. не поддерживается на google app engine',
@@ -195,20 +218,15 @@
'call': 'вызов',
'cannot create file': 'Невозможно создать файл',
'cannot upload file "%(filename)s"': 'Невозможно загрузить файл "%(filename)s"',
'Change admin password': 'Изменить пароль администратора',
'change password': 'изменить пароль',
'check all': 'проверить все',
'Check for upgrades': 'проверить обновления',
'Clean': 'Очистить',
'click here for online examples': 'нажмите здесь для онлайн примеров',
'click here for the administrative interface': 'нажмите здесь для интерфейса администратора',
'click to check for upgrades': 'нажмите для проверки обновления',
'code': 'код',
'collapse/expand all': 'свернуть/развернуть все',
'Compile': 'Компилировать',
'compiled application removed': 'скомпилированное приложение удалено',
'controllers': 'контроллеры',
'Create': 'Создать',
'create file with filename:': 'Создать файл с названием:',
'create new application:': 'создать новое приложение:',
'created by': 'создано',
@@ -225,18 +243,15 @@
'delete': 'удалить',
'delete all checked': 'удалить выбранные',
'delete plugin': 'удалить плагин',
'Deploy': 'Развернуть',
'design': 'дизайн',
'direction: ltr': 'направление: ltr',
'documentation': 'документация',
'done!': 'выполнено!',
'download layouts': 'загрузить шаблоны',
'download plugins': 'загрузить плагины',
'Edit': 'Правка',
'edit controller': 'правка контроллера',
'edit profile': 'правка профиля',
'edit views:': 'правка видов:',
'Errors': 'Ошибка',
'escape': 'escape',
'export as csv file': 'Экспорт в CSV',
'exposes': 'открывает',
@@ -253,13 +268,11 @@
'file saved on %s': 'файл сохранен %s',
'files': 'файлы',
'filter': 'фильтр',
'Help': 'Помощь',
'htmledit': 'htmledit',
'includes': 'включает',
'index': 'index',
'insert new': 'вставить новый',
'insert new %s': 'вставить новый %s',
'Install': 'Установить',
'internal error': 'внутренняя ошибка',
'invalid password': 'неверный пароль',
'invalid request': 'неверный запрос',
@@ -270,7 +283,6 @@
'loading...': 'загрузка...',
'located in the file': 'расположенный в файле',
'login': 'логин',
'Logout': 'выход',
'lost password?': 'Пароль утерен?',
'merge': 'объединить',
'models': 'модели',
@@ -281,9 +293,6 @@
'or import from csv file': 'или испорт из cvs файла',
'or provide app url:': 'или URL приложения:',
'or provide application url:': 'или URL приложения:',
'Overwrite installed app': 'Переписать на установленное приложение',
'Pack all': 'упаковать все',
'Pack compiled': 'Архив скомпилирован',
'pack plugin': 'Упаковать плагин',
'please wait!': 'подождите, пожалуйста!',
'plugins': 'плагины',
@@ -292,16 +301,13 @@
'record does not exist': 'запись не существует',
'record id': 'id записи',
'register': 'зарегистрироваться',
'Remove compiled': 'Удалить скомпилированное',
'restore': 'восстановить',
'revert': 'откатиться',
'save': 'сохранить',
'selected': 'выбрано',
'session expired': 'сессия истекла',
'shell': 'shell',
'Site': 'сайт',
'some files could not be removed': 'некоторые файлы нельзя удалить',
'Start wizard': 'запустить мастер',
'state': 'статус',
'static': 'статичные файлы',
'submit': 'Отправить',
@@ -324,7 +330,6 @@
'unable to parse csv file': 'невозможно разобрать файл csv',
'unable to uninstall "%s"': 'невозможно удалить "%s"',
'uncheck all': 'снять выбор всего',
'Uninstall': 'Удалить',
'update': 'обновить',
'update all languages': 'обновить все языки',
'upgrade web2py now': 'обновить web2py сейчас',
@@ -341,5 +346,3 @@
'web2py is up to date': 'web2py обновлен',
'xml': 'xml',
}
+25 -21
View File
@@ -26,6 +26,7 @@
'Application name:': 'Application name:',
'Are you sure you want to delete file "%s"?': '你真想删除文件"%s"?',
'Are you sure you want to delete plugin "%s"?': 'Are you sure you want to delete plugin "%s"?',
'Are you sure you want to delete this object?': 'Are you sure you want to delete this object?',
'Are you sure you want to uninstall application "%s"': '你真确定要卸载应用 "%s"',
'Are you sure you want to uninstall application "%s"?': '你真确定要卸载应用 "%s" ?',
'Are you sure you want to upgrade web2py now?': 'Are you sure you want to upgrade web2py now?',
@@ -34,23 +35,33 @@
'Cannot compile: there are errors in your app.        Debug it, correct errors and try again.': '无法编译: 应用中有错误,请修订后再试.',
'Cannot compile: there are errors in your app:': 'Cannot compile: there are errors in your app:',
'Change Password': '修改密码',
'Change admin password': 'change admin password',
'Check for upgrades': 'check for upgrades',
'Check to delete': '检验删除',
'Checking for upgrades...': '是否有升级版本检查ing...',
'Clean': '清除',
'Client IP': '客户端IP',
'Compile': '编译',
'Controllers': '控制器s',
'Create': 'create',
'Create new simple application': '创建一个新应用',
'Current request': '当前请求',
'Current response': '当前返回',
'Current session': '当前会话',
'DESIGN': '设计',
'Date and Time': '时间日期',
'Debug': 'Debug',
'Delete': '删除',
'Delete:': '删除:',
'Deploy': 'deploy',
'Deploy on Google App Engine': '部署到GAE',
'Deploy to OpenShift': 'Deploy to OpenShift',
'Description': '描述',
'Design for': '设计:',
'Disable': 'Disable',
'E-mail': '邮箱:',
'EDIT': '编辑',
'Edit': '修改',
'Edit Profile': '修订配置',
'Edit application': '修订应用',
'Edit current record': '修订当前记录',
@@ -59,13 +70,17 @@
'Editing file "%s"': '修订文件 %s',
'Enterprise Web Framework': '强悍的网络开发框架',
'Error logs for "%(app)s"': '"%(app)s" 的错误日志',
'Errors': '错误',
'Exception instance attributes': 'Exception instance attributes',
'First name': '',
'Functions with no doctests will result in [passed] tests.': '',
'Get from URL:': 'Get from URL:',
'Group ID': '组ID',
'Hello World': '道可道,非常道;名可名,非常名',
'Help': '帮助',
'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.': 'If the report above contains a ticket number it indicates a failure in executing the controller, before any attempt to execute the doctests. This is usually due to an indentation error or an error outside function code.\nA green title indicates that all tests (if defined) passed. In this case test results are not shown.',
'Import/Export': '导入/导出',
'Install': 'install',
'Installed applications': '已安装的应用',
'Internal State': '内部状态',
'Invalid Query': '无效查询',
@@ -90,7 +105,10 @@
'No databases in this application': '这应用没有数据库',
'Origin': '起源',
'Original/Translation': '原始文件/翻译文件',
'Overwrite installed app': 'overwrite installed app',
'PAM authenticated user, cannot change password here': 'PAM authenticated user, cannot change password here',
'Pack all': '全部打包',
'Pack compiled': '包编译完',
'Password': '密码',
'Peeking at file': '选个文件',
'Plugin "%s" in application': 'Plugin "%s" in application',
@@ -100,11 +118,16 @@
'Record ID': '记录ID',
'Register': '注册',
'Registration key': '注册密匙',
'Reload routes': 'Reload routes',
'Remove compiled': '已移除',
'Resolve Conflict file': '解决冲突文件',
'Role': '角色',
'Rows in table': '表行',
'Rows selected': '行选择',
'Running on %s': 'Running on %s',
'Saved file hash:': '已存文件Hash:',
'Site': '总站',
'Start wizard': 'start wizard',
'Static files': '静态文件',
'Sure you want to delete this object?': '真的要删除这个对象?',
'TM': '',
@@ -126,9 +149,11 @@
'Unable to download app': '无法下载应用',
'Unable to download app because:': 'Unable to download app because:',
'Unable to download because': 'Unable to download because',
'Uninstall': '卸载',
'Update:': '更新:',
'Upload & install packed application': 'Upload & install packed application',
'Upload a package:': 'Upload a package:',
'Upload and install packed application': 'Upload and install packed application',
'Upload existing application': '上传已有应用',
'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.': 'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...) for NOT to build more complex queries.',
'Use (...)&(...) for AND, (...)|(...) for OR, and ~(...)  for NOT to build more complex queries.': '',
@@ -138,7 +163,6 @@
'Views': '视图',
'Welcome to web2py': '欢迎使用web2py',
'YES': '',
'About': '关于',
'additional code for your application': '给你的应用附加代码',
'admin disabled because no admin password': '管理员需要设定密码,否则无法管理',
'admin disabled because not supported on google app engine': '未支持GAE,无法管理',
@@ -157,18 +181,13 @@
'cache, errors and sessions cleaned': '缓存、错误、sesiones已被清空',
'cannot create file': '无法创建文件',
'cannot upload file "%(filename)s"': '无法上传文件 "%(filename)s"',
'Change admin password': 'change admin password',
'check all': '检查所有',
'Check for upgrades': 'check for upgrades',
'Clean': '清除',
'click here for online examples': '猛击此处,查看在线实例',
'click here for the administrative interface': '猛击此处,进入管理界面',
'click to check for upgrades': '猛击查看是否有升级版本',
'code': 'code',
'Compile': '编译',
'compiled application removed': '已编译应用已移走',
'controllers': '控制器',
'Create': 'create',
'create file with filename:': '创建文件用这名:',
'create new application:': '创建新应用:',
'created by': '创建自',
@@ -185,14 +204,11 @@
'delete': '删除',
'delete all checked': '删除所有选择的',
'delete plugin': 'delete plugin',
'Deploy': 'deploy',
'design': '设计',
'direction: ltr': 'direction: ltr',
'done!': '搞定!',
'Edit': '修改',
'edit controller': '修订控制器',
'edit views:': 'edit views:',
'Errors': '错误',
'export as csv file': '导出为CSV文件',
'exposes': '暴露',
'extends': '扩展',
@@ -207,12 +223,10 @@
'file does not exist': '文件并不存在',
'file saved on %(time)s': '文件保存于 %(time)s',
'file saved on %s': '文件保存在 %s',
'Help': '帮助',
'htmledit': 'html编辑',
'includes': '包含',
'insert new': '新插入',
'insert new %s': '新插入 %s',
'Install': 'install',
'internal error': '内部错误',
'invalid password': '无效密码',
'invalid request': '无效请求',
@@ -222,7 +236,6 @@
'languages updated': '语言已被刷新',
'loading...': '载入中...',
'login': '登录',
'Logout': '注销',
'merge': '合并',
'models': '模型s',
'modules': '模块s',
@@ -234,9 +247,6 @@
'or import from csv file': '或者,从csv文件导入',
'or provide app url:': 'or provide app url:',
'or provide application url:': '或者,提供应用所在地址链接:',
'Overwrite installed app': 'overwrite installed app',
'Pack all': '全部打包',
'Pack compiled': '包编译完',
'pack plugin': 'pack plugin',
'password changed': 'password changed',
'plugin "%(plugin)s" deleted': 'plugin "%(plugin)s" deleted',
@@ -244,16 +254,13 @@
'record': 'record',
'record does not exist': '记录并不存在',
'record id': '记录ID',
'Remove compiled': '已移除',
'restore': '重存',
'revert': '恢复',
'save': '保存',
'selected': '已选',
'session expired': '会话过期',
'shell': '',
'Site': '总站',
'some files could not be removed': '有些文件无法被移除',
'Start wizard': 'start wizard',
'state': '状态',
'static': '静态文件',
'submit': '提交',
@@ -275,7 +282,6 @@
'unable to uninstall "%s"': '无法卸载 "%s"',
'unable to upgrade because "%s"': 'unable to upgrade because "%s"',
'uncheck all': '反选全部',
'Uninstall': '卸载',
'update': '更新',
'update all languages': '更新所有语言',
'upgrade web2py now': 'upgrade web2py now',
@@ -290,5 +296,3 @@
'web2py is up to date': 'web2py现在已经是最新的版本了',
'web2py upgraded; please restart it': 'web2py upgraded; please restart it',
}
+11 -11
View File
@@ -28,17 +28,17 @@
</div>
</div>
<div id="footer" class="fixed">
{{=T('Powered by')}} {{=A('web2py', _href='http://www.web2py.com')}}&trade; {{=T('created by')}} Massimo Di Pierro &copy;2007-2011 -
{{if hasattr(T,'get_possible_languages'):}}
<span>
{{=T('Admin language')}}
<select name="adminlanguage" onchange="var date = new Date();cookieDate=date.setTime(date.getTime()+(100*24*60*60*1000));document.cookie='adminLanguage='+this.options[this.selectedIndex].value+'; expires='+cookieDate+'; path=/';window.location.reload()">
{{for language in T.get_possible_languages():}}
<option {{=T.accepted_language==language and 'selected' or ''}} >{{=language}}</option>
{{pass}}
</select>
</span>
{{pass}}
{{=T('Powered by')}} {{=A('web2py', _href='http://www.web2py.com')}}&trade; {{=T('created by')}} Massimo Di Pierro &copy;2007-2012 -
{{if hasattr(T,'get_possible_languages_info'):}}
<span>
{{=T('Admin language')}}
<select name="adminlanguage" onchange="var date = new Date();cookieDate=date.setTime(date.getTime()+(100*24*60*60*1000));document.cookie='adminLanguage='+this.options[this.selectedIndex].id+'; expires='+cookieDate+'; path=/';window.location.reload()">
{{for langinfo in sorted([(code,info[1]) for code,info in T.get_possible_languages_info().iteritems() if code != 'default']):}}
<option {{=T.accepted_language==langinfo[0] and 'selected' or ''}} {{='id='+langinfo[0]}} >{{=langinfo[1]}}</option>
{{pass}}
</select>
</span>
{{pass}}
</div>
</body>
</html>
+269 -105
View File
@@ -17,6 +17,8 @@ import copy_reg
from fileutils import listdir
import settings
from cfs import getcfs
from thread import allocate_lock
from html import XML, xmlescape
__all__ = ['translator', 'findT', 'update_all_languages']
@@ -30,17 +32,86 @@ PY_STRING_LITERAL_RE = r'(?<=[^\w]T\()(?P<name>'\
+ r'(?:"(?:[^"\\]|\\.)*"))'
regex_translate = re.compile(PY_STRING_LITERAL_RE, re.DOTALL)
regex_param=re.compile(r'(?P<b>(?<!\\)(?:\\\\)*){(?P<s>.+?(?<!\\)(?:\\\\)*)}')
# patter 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*['\"]([^'\"]*)['\"].*$")
# cache of translated messages:
# of structure:
# { 'languages/xx.py':
# ( {"def-message": "xx-message",
# ...
# "def-message": "xx-message"}, lock_object )
# 'languages/yy.py': ( {dict}, lock_object )
# ...
# }
tcache={}
def get_from_cache(cache, val, fun):
lock=cache[1]
lock.acquire()
try:
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()
try:
cache[0].clear();
finally:
cache[1].release()
def lang_sampling(lang_tuple, 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
"""
# step 1:
# compare "aa-bb-cc" | "aa-bb" | "aa" from lang_tuple
# with strings from langlist. Return appropriate string
# from langlist:
tries = range(len(lang_tuple),0,-1)
for i in tries:
language="".join(lang_tuple[:i])
if language in langlist:
return language
# step 2 (if not found in step 1):
# compare "aa-bb-cc" | "aa-bb" | "aa" from lang_tuple
# with left part of a string from langlist. Return
# appropriate string from langlist
for i in tries:
lang="".join(lang_tuple[:i])
for language in langlist:
if language.startswith(lang):
return language
return None
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(tcache.setdefault(filename, ({}, allocate_lock())))
clear_cache(tcache.setdefault('@'+filename, ({}, allocate_lock())))
if not lang_text.strip():
return {}
try:
@@ -50,8 +121,75 @@ def read_dict_aux(filename):
return {'__corrupted__':True}
def read_dict(filename):
return getcfs('language:%s'%filename,filename,
lambda filename=filename:read_dict_aux(filename))
""" return dictionary with translation messages
"""
return getcfs('lang:'+filename, filename,
lambda:read_dict_aux(filename))
def get_lang_info(lang, langdir):
"""retrieve lang information from *langdir*/*lang*.py file.
Read few strings from lang.py file until keys !langname!,
!langcode! or keys greater then '!*' were found
args:
lang (str): lang-code or 'default'
langdir (str): path to 'languages' directory in web2py app dir
returns:
tuple(langcode, langname, langfile_mtime)
e.g.: ('en', 'English', 1338549043.0)
"""
filename = os.path.join(langdir, lang+'.py')
langcode=''
langname=''
f = portalocker.LockedFile(filename, 'r')
try:
while not (langcode and langname):
line = f.readline()
if not line:
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] > '!':
break
finally:
f.close()
if not langcode:
langcode = lang if lang != 'default' else 'en'
return langcode, langname or langcode, os.stat(filename).st_mtime
def read_possible_languages_aux(langdir, item=None):
if not item: item = {}
langs = {}
# scan languages directory for langfiles:
for langfile in (listdir(langdir, regex_langfile) +
listdir(langdir, '^default\.py$')):
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)
if 'default' not in langs:
# if default.py is not found, add default value:
langs['default'] = ('en', 'English', 0)
deflang=langs['default']
if deflang[0] not in langs:
# create language from default.py:
langs[deflang[0]] = (deflang[0], deflang[1], 0)
return langs
def read_possible_languages(path):
lang_path = os.path.join(path, 'languages')
return getcfs('langs:'+path.rstrip(os.path.sep), 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
@@ -108,26 +246,16 @@ class lazyT(object):
"""
never to be called explicitly, returned by translator.__call__
"""
m = s = T = None
m = None
s = None
T = None
def __init__(
self,
message,
symbols = {},
T = None,
):
self.m = message
self.s = symbols
self.T = T
def __init__(self, message, symbols = {}, T = None, filter=None):
self.m, self.s, self.T, self.f = message, symbols, T, filter
def __repr__(self):
return "<lazyT %s>" % (repr(str(self.m)), )
def __str__(self):
return self.T.translate(self.m, self.s)
return self.T.translate(self.m, self.s, self.f)
def __eq__(self, other):
return self.T.translate(self.m, self.s) == other
@@ -175,7 +303,7 @@ class lazyT(object):
return str(self)
def __mod__(self, symbols):
return self.T.translate(self.m, symbols)
return self.T.translate(self.m, symbols, filter=self.f)
class translator(object):
@@ -192,65 +320,83 @@ class translator(object):
T(\"Hello World\") # translates \"Hello World\" using the selected file
notice 1: there is no need to force since, by default, T uses
accept_language to determine a translation file.
http_accept_language to determine a translation file.
notice 2: en and en-en are considered different languages!
notice 3: if language xx-yy is not found force() probes other similar
languages using such algorithm: xx-yy.py -> xx.py -> xx-yy*.py -> xx*.py
"""
def __init__(self, request):
global tcache
self.request = request
self.folder = request.folder
self.current_languages = ['en']
self.accepted_language = None
self.language_file = None
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)
else: # languages/default.py is not found
self.default_language_file = os.path.join(self.folder,'languages','')
self.default_t = {}
self.cache = tcache.setdefault(self.default_language_file, ({}, allocate_lock()))
self.mcache = 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.requested_languages = self.force(self.http_accept_language)
self.lazy = True
self.otherTs = {}
def get_possible_languages_info(self, lang=None):
""" return info for selected language or dictionary with all
possible languages info from APP/languages/*.py
args:
*lang* (str): language
returns:
if *lang* is defined:
return tuple(langcode, langname, langfile_mtime) or None
if *lang* is NOT defined:
returns dictionary with all possible languages:
{ langcode(from filename): ( langcode(from !langcode! key),
langname(from !langname! key),
langfile_mtime ) }
"""
if lang:
return read_possible_languages(self.folder).get(lang)
return read_possible_languages(self.folder)
def get_possible_languages(self):
possible_languages = [lang for lang in self.current_languages]
file_ending = re.compile("\.py$")
for langfile in os.listdir(os.path.join(self.folder,'languages')):
if file_ending.search(langfile):
possible_languages.append(file_ending.sub('',langfile))
return possible_languages
""" get list of all possible languages for current applications """
return sorted( set(lang for lang in
read_possible_languages(self.folder).iterkeys()
if lang != 'default')
| set(self.current_languages))
def set_current_languages(self, *languages):
""" set current AKA "default" languages
setting one of this languages makes force() function
turn translation off to use default language
"""
if len(languages) == 1 and isinstance(languages[0], (tuple, list)):
languages = languages[0]
self.current_languages = languages
self.force(self.http_accept_language)
def force(self, *languages):
def lang_sampling(lang_tuple, 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
"""
# step 1:
# compare "aa-bb-cc" | "aa-bb" | "aa" from lang_tuple
# with strings from langlist. Return appropriate string
# from langlist:
tries = range(len(lang_tuple),0,-1)
for i in tries:
language="".join(lang_tuple[:i])
if language in langlist:
return language
# step 2 (if not found in step 1):
# compare "aa-bb-cc" | "aa-bb" | "aa" from lang_tuple
# with left part of a string from langlist. Return
# appropriate string from langlist
for i in tries:
lang="".join(lang_tuple[:i])
for language in langlist:
if language.startswith(lang):
return language
return None
""" select language(s) for translation
if a list of languages is passed as a parameter,
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
if not languages or languages[0] is None:
languages = []
if len(languages) == 1 and isinstance(languages[0], (str, unicode)):
@@ -260,7 +406,8 @@ class translator(object):
if isinstance(languages, (str, unicode)):
parts = languages.split(';')
languages = []
[languages.extend(al.split(',')) for al in parts]
for al in parts:
languages.extend(al.split(','))
possible_languages = self.get_possible_languages()
for language in languages:
@@ -276,79 +423,94 @@ class translator(object):
break
language = lang_sampling(match_language, possible_languages)
if language:
filename = os.path.join(
self.folder,'languages/',language + '.py')
if os.path.exists(filename):
self.language_file = os.path.join(self.folder,
'languages',
language + '.py')
if os.path.exists(self.language_file):
self.t = read_dict(self.language_file)
self.accepted_language = language
self.language_file = filename
self.t = read_dict(filename)
self.cache = tcache.setdefault(self.language_file, ({},allocate_lock()))
self.mcache = tcache.setdefault('@'+self.language_file, ({},allocate_lock()))
return languages
self.language_file = None
self.t = {} # ## no language by default
self.language_file = self.default_language_file
self.cache = tcache[self.language_file]
self.mcache = tcache['@'+self.language_file]
self.t = self.default_t
return languages
def __call__(self, message, symbols={}, language=None, lazy=None):
if lazy is None:
lazy = self.lazy
if not language:
if lazy:
return lazyT(message, symbols, self)
else:
return self.translate(message, symbols)
else:
try:
otherT = self.otherTs[language]
except KeyError:
otherT = self.otherTs[language] = translator(self.request)
otherT.force(language)
return otherT(message, symbols, lazy=lazy)
def __call__(self, message, symbols={}, language=None, lazy=None, filter=None):
""" get cached translated plain text message with inserted
parameters(symbols)
def translate(self, message, 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]
else:
otherT = self.otherTs[language] = translator(self.request)
otherT.force(language)
return otherT(message, symbols, lazy=lazy, filter=filter)
def get_t(self, message, filter=None, startwith_templ='##'):
"""
user ## to add a comment into a translation string
the comment can be useful do discriminate different possible
translations for the same string (for example different locations)
T(' hello world ') -> ' hello world '
T(' hello world ## token') -> 'hello world'
T('hello ## world ## token') -> 'hello ## world'
T(' hello world ## token') -> ' hello world '
T('hello ## world## token') -> 'hello ## world'
the ## notation is ignored in multiline strings and strings that
start with ##. this is to allow markmin syntax to be translated
"""
#for some reason languages.py gets executed before gaehandler.py
# is able to set web2py_runtime_gae, so re-check here
is_gae = settings.global_settings.web2py_runtime_gae
if not message.startswith('#') and not '\n' in message:
tokens = message.rsplit('##', 1)
else:
# this allows markmin syntax in translations
tokens = [message]
if len(tokens) == 2:
tokens[0] = tokens[0].strip()
message = tokens[0] + '##' + tokens[1].strip()
mt = self.t.get(message, None)
if mt is None:
self.t[message] = mt = tokens[0]
if self.language_file and not is_gae:
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 symbols or symbols == 0 or symbols == "":
return mt % symbols
if filter:
mt = filter(mt)
return 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
"""
return message % symbols
def findT(path, language='en-us'):
def translate(self, message, symbols, filter=None):
""" get cached translated message with inserted parameters(symbols) """
message = get_from_cache(self.cache, message,
lambda: self.get_t(message,filter))
if symbols or symbols == 0 or symbols == "":
return self.params_substitution(message, symbols)
return message
def findT(path, language='en'):
"""
must be run by the admin app
"""
filename = os.path.join(path, 'languages', '%s.py' % language)
filename = os.path.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')
for file in listdir(mp, '.+\.py', 0) + listdir(cp, '.+\.py', 0)\
+ listdir(vp, '.+\.html', 0) + listdir(mop, '.+\.py', 0):
for file in listdir(mp, '^.+\.py$', 0) + listdir(cp, '^.+\.py$', 0)\
+ listdir(vp, '^.+\.html$', 0) + listdir(mop, '^.+\.py$', 0):
fp = portalocker.LockedFile(file, 'r')
data = fp.read()
fp.close()
@@ -367,6 +529,12 @@ def findT(path, language='en-us'):
sentences[message] = message
except:
pass
if not '!langcode!' in sentences:
sentences['!langcode!'] = ('en' if language in ('default', 'en')
else language)
if not '!langname!' in sentences:
sentences['!langname!'] = ('English' if language in ('default', 'en')
else sentences['!langcode!'])
write_dict(filename, sentences)
### important to allow safe session.flash=T(....)
@@ -376,9 +544,10 @@ def lazyT_pickle(data):
return lazyT_unpickle, (marshal.dumps(str(data)),)
copy_reg.pickle(lazyT, lazyT_pickle, lazyT_unpickle)
def update_all_languages(application_path):
path = os.path.join(application_path, 'languages/')
for language in listdir(path, '^\w+(\-\w+)?\.py$'):
for language in listdir(path, regex_langfile):
findT(application_path, language[:-3])
@@ -386,8 +555,3 @@ if __name__ == '__main__':
import doctest
doctest.testmod()