diff --git a/buildout.cfg b/buildout.cfg index 3362bd6..895e5db 100644 --- a/buildout.cfg +++ b/buildout.cfg @@ -17,8 +17,11 @@ eggs = Sphinx unittest2 nose + Babel + lingua scripts= nosetests=tests +interpreter = python [test] recipe = zc.recipe.egg diff --git a/pyramid_formalchemy/__init__.py b/pyramid_formalchemy/__init__.py index dfb1eb6..d73af11 100644 --- a/pyramid_formalchemy/__init__.py +++ b/pyramid_formalchemy/__init__.py @@ -4,12 +4,15 @@ from pyramid_formalchemy.resources import Models def includeme(config): """include formalchemy's zcml""" + config.add_translation_dirs('formalchemy:i18n_resources/', 'pyramid_formalchemy:locale/') config.add_static_view('fa_admin', 'pyramid_formalchemy:static') config.add_directive('formalchemy_admin', 'pyramid_formalchemy.formalchemy_admin') config.add_directive('formalchemy_model', 'pyramid_formalchemy.formalchemy_model') config.add_directive('formalchemy_model_view', 'pyramid_formalchemy.formalchemy_model_view') config.registry.pyramid_formalchemy_views = {} + config.add_route('set_language', '/set_language', 'pyramid_formalchemy.views.set_language') + def formalchemy_model_view(config, route_name, model=None, name='', diff --git a/pyramid_formalchemy/actions.py b/pyramid_formalchemy/actions.py index 2f96fc8..010b8b9 100644 --- a/pyramid_formalchemy/actions.py +++ b/pyramid_formalchemy/actions.py @@ -1,6 +1,9 @@ # -*- coding: utf-8 -*- from chameleon.zpt.template import PageTemplate from pyramid.util import DottedNameResolver +from pyramid_formalchemy.i18n import TranslationString +from pyramid_formalchemy.i18n import get_localizer +from pyramid_formalchemy.i18n import _ import functools def action(name=None): @@ -29,16 +32,16 @@ class Action(object): >>> request = Request.blank('/') >>> class MyAction(Action): - ... body = u'' + ... body = u'${content}' - >>> action = MyAction('myaction', content=repr('Click here'), + >>> action = MyAction('myaction', content=_('Click here'), ... attrs={'href': repr('#'), 'onclick': repr('$.click()')}) >>> action.render(request) u'Click here' """ - def __init__(self, id, action='', content="", attrs=None, **rcontext): + def __init__(self, id, content="", alt="", attrs=None, **rcontext): self.id = id self.attrs = attrs or {} self.rcontext = rcontext @@ -46,8 +49,9 @@ class Action(object): self.attrs['id'] = repr(id) self.update() attributes = u';'.join([u'%s %s' % v for v in self.attrs.items()]) - rcontext.update(attrs=self.attrs, attributes=attributes, id=id, content=content, action=action) + rcontext.update(attrs=self.attrs, attributes=attributes, id=id) body = self.body % self.rcontext + rcontext.update(content=content, alt=alt) self.template = PageTemplate(body) def update(self): @@ -56,6 +60,17 @@ class Action(object): def render(self, request): rcontext = {'action': self, 'request': request} rcontext.update(self.rcontext) + localizer = get_localizer(request) + mapping = getattr(request, 'action_mapping', {}) + if not mapping: + for k in ('model_name', 'model_id'): + mapping[k] = getattr(request, k, '') + request.action_mapping = mapping + for k in ('content', 'alt'): + v = rcontext[k] + if isinstance(v, TranslationString): + v = TranslationString(v, domain=v.domain, mapping=request.action_mapping) + rcontext[k] = localizer.translate(v) return self.template.render(**rcontext) def __repr__(self): @@ -67,14 +82,30 @@ class Link(Action): >>> from webob import Request >>> request = Request.blank('/') - >>> action = Link('myaction', content='label', + >>> action = Link('myaction', ... attrs={'href': 'request.application_url'}, - ... label='Click here') + ... content=_('Click here')) >>> action.render(request) u'Click here' """ - body = u'' + body = u'${content}' + +class ListItem(Action): + """ + An action rendered as a link:: + + >>> from webob import Request + >>> request = Request.blank('/') + >>> action = ListItem('myaction', + ... attrs={'href': 'request.application_url'}, + ... content=_('Click here')) + >>> action.render(request) + u'
  • Click here
  • ' + + """ + body = u'
  • ${content}
  • ' + class Input(Action): """An action rendered as an input:: @@ -82,7 +113,7 @@ class Input(Action): >>> from webob import Request >>> request = Request.blank('/') >>> action = Input('myaction', - ... value=repr('Click here')) + ... value=_('Click here')) >>> action.render(request) u'' @@ -95,21 +126,21 @@ class Input(Action): if 'type' not in self.attrs: self.attrs['type'] = repr('submit') -class UILink(Action): +class UIButton(Action): """An action rendered as an jquery.ui aware link:: >>> from webob import Request >>> request = Request.blank('/') - >>> action = UILink('myaction', icon='ui-icon-trash', - ... label="string:Click here") + >>> action = UIButton('myaction', icon='ui-icon-trash', + ... content=_("Click here")) >>> print action.render(request) Click here - >>> action = UILink('myaction', icon='ui-icon-trash', - ... label="'Click here'", attrs={'onclick':'$(#link).click();'}) + >>> action = UIButton('myaction', icon='ui-icon-trash', + ... content=_("Click here"), attrs={'onclick':'$(#link).click();'}) >>> print action.render(request) @@ -121,7 +152,7 @@ class UILink(Action): - + ${content} ''' def update(self): if 'state' not in self.rcontext: @@ -135,17 +166,18 @@ class UILink(Action): class Actions(list): """ >>> actions = Actions('pyramid_formalchemy.actions.delete', - ... Link('link1', content=repr('A link'), attrs={'href':'request.application_url'})) + ... Link('link1', content=_('A link'), attrs={'href':'request.application_url'})) >>> actions - [, ] + [, ] >>> from webob import Request >>> request = Request.blank('/') - >>> print actions.render(request) #doctest: +ELLIPSIS + >>> print actions.render(request) #doctest: +ELLIPSIS +NORMALIZE_WHITESPACE Delete - A link + + A link """ @@ -154,44 +186,70 @@ class Actions(list): list.__init__(self, [res.maybe_resolve(a) for a in args]) def render(self, request, **kwargs): - return u''.join([a.render(request, **kwargs) for a in self]) + return u'\n'.join([a.render(request, **kwargs) for a in self]) +class Languages(Actions): + """ + >>> langs = Languages('fr', 'en') + >>> langs + [, ] + >>> from webob import Request + >>> request = Request.blank('/') + >>> request.route_url = lambda name, _query: 'http://localhost/set_language?_LOCALE_=%(_LOCALE_)s' % _query + >>> print langs.render(request) #doctest: +ELLIPSIS +NORMALIZE_WHITESPACE + fr + en + + """ -new = UILink( + def __init__(self, *args, **kwargs): + list.__init__(self) + klass=kwargs.get('class_', ListItem) + for l in args: + self.append(Link(id=l, content=_(l), attrs=dict(href="request.route_url('set_language', _query={'_LOCALE_': '%s'})" % l))) + +new = UIButton( id='new', - label='string:New ${request.model_name}', + content=_('New ${model_name}'), icon='ui-icon-circle-plus', attrs=dict(href="request.fa_url(request.model_name, 'new')"), ) -save = UILink( +save = UIButton( id='save', - label='string:Save', + content=_('Save'), icon='ui-icon-check', attrs=dict(onclick="jQuery(this).parents('form').submit();"), ) -save_and_add_another = UILink( +save_and_add_another = UIButton( id='save_and_add_another', - label='string:Save and add another', + content=_('Save and add another'), icon='ui-icon-check', attrs=dict(onclick=("var f = jQuery(this).parents('form');" "jQuery('#next', f).val(window.location.href);" "f.submit();")), ) -edit = UILink( +edit = UIButton( id='edit', - label='string:Edit', + content=_('Edit'), icon='ui-icon-check', attrs=dict(href="request.fa_url(request.model_name, request.model_id, 'edit')"), ) -delete = UILink( +back = UIButton( + id='back', + content=_('Back'), + icon='ui-icon-circle-arrow-w', + attrs=dict(href="request.fa_url(request.model_name)"), + ) + +delete = UIButton( id='delete', views='edit', - label='string:Delete', + content=_('Delete'), state='ui-state-error', icon='ui-icon-trash', attrs=dict(onclick=("string:var f = jQuery(this).parents('form');" @@ -199,10 +257,10 @@ delete = UILink( "f.submit();")), ) -cancel = UILink( +cancel = UIButton( id='cancel', views='edit', - label='string:Cancel', + content=_('Cancel'), icon='ui-icon-circle-arrow-w', attrs=dict(href="request.fa_url(request.model_name)"), ) @@ -210,6 +268,6 @@ cancel = UILink( defaults_actions = dict( listing_buttons=Actions(new), new_buttons=Actions(save, save_and_add_another, cancel), - show_buttons=Actions(edit, cancel), + show_buttons=Actions(edit, back), edit_buttons=Actions(save, delete, cancel), ) diff --git a/pyramid_formalchemy/i18n.py b/pyramid_formalchemy/i18n.py new file mode 100644 index 0000000..0a45e6a --- /dev/null +++ b/pyramid_formalchemy/i18n.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +from pyramid.i18n import TranslationStringFactory +from pyramid.i18n import TranslationString +from pyramid.i18n import get_localizer + +_ = TranslationStringFactory('pyramid_formalchemy') + diff --git a/pyramid_formalchemy/locale/fr/LC_MESSAGES/pyramid_formalchemy.mo b/pyramid_formalchemy/locale/fr/LC_MESSAGES/pyramid_formalchemy.mo new file mode 100644 index 0000000..e30251f Binary files /dev/null and b/pyramid_formalchemy/locale/fr/LC_MESSAGES/pyramid_formalchemy.mo differ diff --git a/pyramid_formalchemy/locale/fr/LC_MESSAGES/pyramid_formalchemy.po b/pyramid_formalchemy/locale/fr/LC_MESSAGES/pyramid_formalchemy.po new file mode 100644 index 0000000..987aa6d --- /dev/null +++ b/pyramid_formalchemy/locale/fr/LC_MESSAGES/pyramid_formalchemy.po @@ -0,0 +1,56 @@ +# French translations for pyramid_formalchemy. +# Copyright (C) 2011 ORGANIZATION +# This file is distributed under the same license as the pyramid_formalchemy +# project. +# FIRST AUTHOR , 2011. +# +msgid "" +msgstr "" +"Project-Id-Version: pyramid_formalchemy 0.3\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2011-06-18 12:02+0200\n" +"PO-Revision-Date: 2011-06-18 13:18+0200\n" +"Last-Translator: FULL NAME \n" +"Language-Team: fr \n" +"Plural-Forms: nplurals=2; plural=(n > 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 0.9.5\n" + +#: pyramid_formalchemy/actions.py:190 +msgid "New ${model_name}" +msgstr "Nouveau ${model_name}" + +#: pyramid_formalchemy/actions.py:198 +msgid "Save" +msgstr "Sauver" + +#: pyramid_formalchemy/actions.py:205 +msgid "Save and add another" +msgstr "Sauver et ajouter un autre" + +#: pyramid_formalchemy/actions.py:214 +msgid "Edit" +msgstr "Editer" + +#: pyramid_formalchemy/actions.py:221 +msgid "Back" +msgstr "Retour" + +#: pyramid_formalchemy/actions.py:229 +msgid "Delete" +msgstr "Supprimer" + +#: pyramid_formalchemy/actions.py:240 +msgid "Cancel" +msgstr "Annuler" + +#: pyramid_formalchemy/views.py:245 +msgid "edit" +msgstr "Editer" + +#: pyramid_formalchemy/views.py:252 +msgid "delete" +msgstr "Supprimer" + diff --git a/pyramid_formalchemy/locale/pyramid_formalchemy.pot b/pyramid_formalchemy/locale/pyramid_formalchemy.pot new file mode 100644 index 0000000..1278b5c --- /dev/null +++ b/pyramid_formalchemy/locale/pyramid_formalchemy.pot @@ -0,0 +1,56 @@ +# Translations template for pyramid_formalchemy. +# Copyright (C) 2011 ORGANIZATION +# This file is distributed under the same license as the pyramid_formalchemy +# project. +# FIRST AUTHOR , 2011. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: pyramid_formalchemy 0.3\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2011-06-18 13:16+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 0.9.5\n" + +#: pyramid_formalchemy/actions.py:190 +msgid "New ${model_name}" +msgstr "" + +#: pyramid_formalchemy/actions.py:198 +msgid "Save" +msgstr "" + +#: pyramid_formalchemy/actions.py:205 +msgid "Save and add another" +msgstr "" + +#: pyramid_formalchemy/actions.py:214 +msgid "Edit" +msgstr "" + +#: pyramid_formalchemy/actions.py:221 +msgid "Back" +msgstr "" + +#: pyramid_formalchemy/actions.py:229 +msgid "Delete" +msgstr "" + +#: pyramid_formalchemy/actions.py:240 +msgid "Cancel" +msgstr "" + +#: pyramid_formalchemy/views.py:245 +msgid "edit" +msgstr "" + +#: pyramid_formalchemy/views.py:252 +msgid "delete" +msgstr "" + diff --git a/pyramid_formalchemy/resources.py b/pyramid_formalchemy/resources.py index a82a8ce..203b9ac 100644 --- a/pyramid_formalchemy/resources.py +++ b/pyramid_formalchemy/resources.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- from pyramid.exceptions import NotFound +from pyramid_formalchemy import actions from sqlalchemy import exceptions as sqlalchemy_exceptions import logging @@ -27,6 +28,12 @@ class Base(object): if self.__model_class__: request.model_class = self.__model_class__ request.model_name = self.__model_class__.__name__ + langs = request.registry.settings.get('available_languages', '') + if langs: + if isinstance(langs, basestring): + langs = langs.split() + request.language_actions = actions.Languages(*langs) + def get_model(self): request = self.request diff --git a/pyramid_formalchemy/templates/admin/new.pt b/pyramid_formalchemy/templates/admin/new.pt index 9d21e99..6f1c30e 100644 --- a/pyramid_formalchemy/templates/admin/new.pt +++ b/pyramid_formalchemy/templates/admin/new.pt @@ -12,7 +12,7 @@ - ${F_('Cancel')} + ${'Cancel'}

    diff --git a/pyramid_formalchemy/views.py b/pyramid_formalchemy/views.py index 48e9e0f..fa09c89 100644 --- a/pyramid_formalchemy/views.py +++ b/pyramid_formalchemy/views.py @@ -38,6 +38,11 @@ class Session(object): def commit(self): """commit transaction""" +def set_language(request): + resp = exc.HTTPFound(location=request.referer or request.application_url) + resp.set_cookie('_LOCALE_', request.GET.get('_LOCALE_', 'en')) + return resp + class ModelView(object): """A RESTful view bound to a model""" @@ -132,7 +137,7 @@ class ModelView(object): main = get_renderer('pyramid_formalchemy:templates/admin/master.pt').implementation(), model_name=request.model_name, breadcrumb=self.breadcrumb(**kwargs), - F_=get_translator().gettext) + F_=get_translator()) return kwargs def render_grid(self, **kwargs): diff --git a/setup.py b/setup.py index 8e0c31f..aca65cf 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ here = os.path.abspath(os.path.dirname(__file__)) README = open(os.path.join(here, 'README.txt')).read() CHANGES = open(os.path.join(here, 'CHANGES.txt')).read() -requires = ['pyramid>=1.0', 'WebError', 'FormAlchemy>=1.3.8'] +requires = ['pyramid>=1.0', 'WebError', 'FormAlchemy>=1.3.8', 'Babel'] setup(name='pyramid_formalchemy', version='0.3', @@ -23,6 +23,10 @@ setup(name='pyramid_formalchemy', url='http://docs.formalchemy.org/pyramid_formalchemy/', keywords='web pyramid pylons formalchemy', packages=find_packages(exclude=['pyramidapp']), + message_extractors = { 'pyramid_formalchemy': [ + ('*.py', 'lingua_python', None ), + ('templates/**.pt', 'lingua_xml', None ), + ]}, include_package_data=True, zip_safe=False, install_requires=requires,