diff --git a/browser/common.py b/browser/common.py index 1af60ff..a56cb7a 100644 --- a/browser/common.py +++ b/browser/common.py @@ -51,6 +51,7 @@ from cybertools.relation.interfaces import IRelationRegistry from cybertools.text import mimetypes from cybertools.typology.interfaces import IType, ITypeManager from loops.common import adapted +from loops.i18n.browser import I18NView from loops.interfaces import IView from loops.resource import Resource from loops.type import ITypeConcept @@ -96,7 +97,7 @@ class EditForm(form.EditForm): return parentUrl + '/contents.html' -class BaseView(GenericView): +class BaseView(GenericView, I18NView): actions = {} # default only, don't update diff --git a/browser/concept.py b/browser/concept.py index 96360dd..62c3049 100644 --- a/browser/concept.py +++ b/browser/concept.py @@ -34,25 +34,27 @@ from zope.app.security.interfaces import IUnauthenticatedPrincipal from zope.cachedescriptors.property import Lazy from zope.dottedname.resolve import resolve from zope.event import notify -from zope.formlib.form import EditForm, FormFields +from zope.formlib.form import EditForm, FormFields, setUpEditWidgets from zope.formlib.namedtemplate import NamedTemplate from zope.interface import implements from zope.publisher.interfaces import BadRequest from zope.publisher.interfaces.browser import IBrowserRequest from zope.schema.interfaces import IIterableSource from zope.security.proxy import removeSecurityProxy +from zope.traversing.api import getName from cybertools.typology.interfaces import IType, ITypeManager -from loops.interfaces import IConcept -from loops.interfaces import ITypeConcept -from loops.concept import Concept, ConceptTypeSourceList, PredicateSourceList from loops.browser.common import EditForm, BaseView, LoopsTerms, conceptMacrosTemplate +from loops.common import adapted +from loops.concept import Concept, ConceptTypeSourceList, PredicateSourceList +from loops.i18n.browser import I18NView +from loops.interfaces import IConcept, IConceptSchema, ITypeConcept from loops import util from loops.util import _ from loops.versioning.util import getVersion -class ConceptEditForm(EditForm): +class ConceptEditForm(EditForm, I18NView): @Lazy def typeInterface(self): @@ -71,12 +73,16 @@ class ConceptEditForm(EditForm): return fields def setUpWidgets(self, ignore_request=False): - super(ConceptEditForm, self).setUpWidgets(ignore_request) + adapter = adapted(self.context, self.languageInfo) + self.adapters = {self.typeInterface: adapter, + IConceptSchema: adapter} + self.widgets = setUpEditWidgets( + self.form_fields, self.prefix, self.context, self.request, + adapters=self.adapters, ignore_request=ignore_request) desc = self.widgets.get('description') if desc: desc.height = 2 - class ConceptView(BaseView): template = ViewPageTemplateFile('concept_macros.pt') @@ -98,11 +104,24 @@ class ConceptView(BaseView): subMacro=self.template.macros['parents'], position=0, info=self) + @Lazy + def adapted(self): + return adapted(self.context, self.languageInfo) + + @Lazy + def title(self): + return self.adapted.title or getName(self.context) + + @Lazy + def description(self): + return self.adapted.description + def fieldData(self): + # TODO: use cybertools.composer.schema.instance, see loops.browser.form ti = IType(self.context).typeInterface if not ti: return - adapter = ti(self.context) + adapter = self.adapted for n, f in schema.getFieldsInOrder(ti): if n in ('title', 'description',): # already shown in header continue @@ -119,17 +138,21 @@ class ConceptView(BaseView): cm = self.loopsRoot.getConceptManager() hasType = cm.getTypePredicate() standard = cm.getDefaultPredicate() - rels = self.context.getChildRelations() + #rels = self.context.getChildRelations() + rels = (ConceptRelationView(r, self.request, contextIsSecond=True) + for r in self.context.getChildRelations(sort=None)) + rels = sorted(rels, key=lambda r: (r.order, r.title.lower())) for r in rels: if r.predicate == hasType: # only show top-level entries for type instances: skip = False - for parent in r.second.getParents((standard,)): + for parent in r.context.getParents((standard,)): if parent.conceptType == self.context: skip = True break if skip: continue - yield ConceptRelationView(r, self.request, contextIsSecond=True) + yield r + #yield ConceptRelationView(r, self.request, contextIsSecond=True) def parents(self): rels = sorted(self.context.getParentRelations(), @@ -310,6 +333,18 @@ class ConceptRelationView(BaseView): self.relation = relation self.request = request + @Lazy + def adapted(self): + return adapted(self.context, self.languageInfo) + + @Lazy + def title(self): + return self.adapted.title or getName(self.context) + + @Lazy + def description(self): + return self.adapted.description + @Lazy def token(self): return ':'.join((self.loopsRoot.getLoopsUri(self.context), diff --git a/browser/configure.zcml b/browser/configure.zcml index 4acabda..b84c18c 100644 --- a/browser/configure.zcml +++ b/browser/configure.zcml @@ -586,6 +586,13 @@ permission="zope.ManageContent" /> + + - + - + + + Edit Information Object - - + i18n:translate="">Edit Information Object + + + + en + + + diff --git a/browser/loops.js b/browser/loops.js index 96aee1f..b7bd4ff 100644 --- a/browser/loops.js +++ b/browser/loops.js @@ -19,6 +19,12 @@ function replaceFieldsNode(targetId, typeId, url) { dojo.io.updateNode(targetId, uri); } +function replaceFieldsNodeForLanguage(targetId, langId, url) { + lang = dojo.byId(langId).value; + uri = url + '?loops.language=' + lang; + dojo.io.updateNode(targetId, uri); +} + function submitReplacing(targetId, formId, actionUrl) { dojo.io.updateNode(targetId, { url: actionUrl, diff --git a/browser/skin/__init__.py b/browser/skin/__init__.py index 4bc90fb..7ca0fef 100644 --- a/browser/skin/__init__.py +++ b/browser/skin/__init__.py @@ -2,3 +2,9 @@ $Id$ """ +from cybertools.browser.liquid import Liquid + + +class Loopz(Liquid): + """ The Loopz (neutral enduser) skin """ + diff --git a/browser/skin/configure.zcml b/browser/skin/configure.zcml index f067dcf..ea98c06 100644 --- a/browser/skin/configure.zcml +++ b/browser/skin/configure.zcml @@ -6,21 +6,23 @@ i18n_domain="zope" > - - - + + layer="loops.browser.skin.Loopz" /> - - - + + + diff --git a/common.py b/common.py index fc8a564..b577e38 100644 --- a/common.py +++ b/common.py @@ -22,13 +22,15 @@ Common stuff. $Id$ """ +from zope import component from zope.app.container.contained import NameChooser as BaseNameChooser +from zope.cachedescriptors.property import Lazy +from zope.component import adapts from zope.dublincore.interfaces import IZopeDublinCore from zope.dublincore.annotatableadapter import ZDCAnnotatableAdapter from zope.dublincore.zopedublincore import ScalarProperty -from zope.component import adapts from zope.interface import implements -from zope.cachedescriptors.property import Lazy +from zope.security.proxy import isinstance from cybertools.storage.interfaces import IStorageInfo from cybertools.typology.interfaces import IType @@ -36,12 +38,15 @@ from loops.interfaces import ILoopsObject, ILoopsContained, IConcept, IResource from loops.interfaces import IResourceAdapter -def adapted(obj): +def adapted(obj, langInfo=None): t = IType(obj, None) if t is not None: ti = t.typeInterface if ti is not None: - adapted = ti(obj, None) + adapted = component.queryAdapter(obj, ti) + from loops.i18n.common import I18NAdapterBase + if isinstance(adapted, I18NAdapterBase): + adapted.languageInfo = langInfo if adapted is not None: return adapted return obj diff --git a/configure.zcml b/configure.zcml index 948ebfb..07defea 100644 --- a/configure.zcml +++ b/configure.zcml @@ -55,7 +55,7 @@ + interface="zope.annotation.interfaces.IAttributeAnnotatable" /> @@ -122,7 +122,7 @@ + interface="zope.annotation.interfaces.IAttributeAnnotatable" /> + interface="zope.annotation.interfaces.IAttributeAnnotatable" /> + zope.size.interfaces.ISized" /> + interface="zope.annotation.interfaces.IAttributeAnnotatable" /> + zope.size.interfaces.ISized" /> + interface="zope.annotation.interfaces.IAttributeAnnotatable" /> + zope.size.interfaces.ISized" /> --> @@ -269,7 +269,7 @@ interface="loops.interfaces.ILoopsObject" />--> + interface="zope.annotation.interfaces.IAttributeAnnotatable" /> - + @@ -457,7 +457,22 @@ factory="cybertools.storage.filesystem.fullPathStorage" name="fullpath" /> - + + + + + + + diff --git a/i18n/README.txt b/i18n/README.txt new file mode 100644 index 0000000..f1d324f --- /dev/null +++ b/i18n/README.txt @@ -0,0 +1,125 @@ +=============================================================== +loops - Linked Objects for Organization and Processing Services +=============================================================== + + ($Id$) + +Let's do some basic set up + + >>> from zope.app.testing.setup import placefulSetUp, placefulTearDown + >>> site = placefulSetUp(True) + + >>> from zope import component, interface + +and setup a simple loops site with a concept manager and some concepts +(with all the type machinery, what in real life is done via standard +ZCML setup): + + >>> from loops.interfaces import ILoops, IConcept + >>> from loops.concept import Concept + >>> from loops.setup import ISetupManager + >>> from loops.knowledge.setup import SetupManager + >>> component.provideAdapter(SetupManager, (ILoops,), ISetupManager, + ... name='knowledge') + + >>> from loops.tests.setup import TestSite + >>> t = TestSite(site) + >>> concepts, resources, views = t.setup() + >>> loopsRoot = site['loops'] + + >>> from loops.knowledge.knowledge import Topic + >>> component.provideAdapter(Topic) + +For testing and demonstration purposes let's create a topic. + + >>> topic = concepts['topic'] + >>> topic01 = concepts['topic01'] = Concept(u'loops for Zope 3') + >>> topic01.conceptType = topic + + +Content Internationalization +============================ + +Let's look at a certain concept that should contain i18n-alized data. + + >>> topic01.title + u'loops for Zope 3' + +We can query the available languages, the current language setting and +the default language using a LanguageInfo object that is similar to a view. + + >>> from zope.publisher.browser import TestRequest + >>> from loops.i18n.browser import LanguageInfo + >>> langInfo = LanguageInfo(topic01, TestRequest()) + >>> langInfo.availableLanguages + [] + >>> langInfo.language is None + True + >>> langInfo.defaultLanguage is None + True + +In order to use content i18n we have to define the available languages +as an option on the loops root object. + + >>> loopsRoot.options = ['languages:en,de,it'] + >>> langInfo = LanguageInfo(topic01, TestRequest()) + >>> langInfo.availableLanguages + ['en', 'de', 'it'] + >>> langInfo.defaultLanguage + 'en' + >>> langInfo.language + 'en' + +By setting an appropriate value in the URI we can select a certaing +language for processing of the current request. + + >>> input = {'loops.language': 'it'} + >>> langInfo = LanguageInfo(topic01, TestRequest(form=input)) + >>> langInfo.availableLanguages + ['en', 'de', 'it'] + >>> langInfo.defaultLanguage + 'en' + >>> langInfo.language + 'it' + +Let's now use a form to edit an i18n-sensible attribute. For this we have +to set up some components needed by the zope.formlib machinery. + + >>> from zope.publisher.interfaces.browser import IBrowserRequest + >>> from zope.app.form.browser import TextWidget, ChoiceInputWidget, DropdownWidget + >>> from zope.schema.interfaces import ITextLine, IText, IChoice + >>> from zope.app.form.interfaces import IInputWidget + >>> component.provideAdapter(TextWidget, (IText, IBrowserRequest), + ... IInputWidget) + >>> from loops.concept import ConceptTypeSourceList + >>> from zope.schema.vocabulary import getVocabularyRegistry + >>> getVocabularyRegistry().register('loops.conceptTypeSource', ConceptTypeSourceList) + >>> component.provideAdapter(ChoiceInputWidget, + ... (IChoice, IBrowserRequest), IInputWidget) + >>> component.provideAdapter(DropdownWidget, + ... (IChoice, ConceptTypeSourceList, IBrowserRequest), IInputWidget) + +We also have to mark the attributes that should be stored in multiple +languages on the type object. + + >>> from loops.common import adapted + >>> tTopic = adapted(topic) + >>> tTopic.options = ['i18nattributes:title,description'] + +Now we are ready to enter a language-specific title. + + >>> from loops.browser.concept import ConceptEditForm + >>> input = {'form.title': 'loops per Zope 3', 'loops.language': 'it', + ... 'form.actions.apply': 'Change'} + >>> form = ConceptEditForm(topic01, TestRequest(form=input)) + >>> form.update() + + >>> topic01.title + {'it': u'loops per Zope 3'} + + +Fin de partie +============= + + >>> placefulTearDown() + diff --git a/i18n/__init__.py b/i18n/__init__.py new file mode 100644 index 0000000..4bc90fb --- /dev/null +++ b/i18n/__init__.py @@ -0,0 +1,4 @@ +""" +$Id$ +""" + diff --git a/i18n/browser.py b/i18n/browser.py new file mode 100644 index 0000000..7f5e820 --- /dev/null +++ b/i18n/browser.py @@ -0,0 +1,98 @@ +# +# Copyright (c) 2007 Helmut Merz helmutm@cy55.de +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +""" +View extension for support of i18n content. + +$Id$ +""" + +from zope import interface, component +from zope.app.pagetemplate import ViewPageTemplateFile +from zope.cachedescriptors.property import Lazy +from zope.i18n.interfaces import IUserPreferredLanguages +from zope.i18n.negotiator import negotiator + +from loops.common import adapted + + +class LanguageInfo(object): + + def __init__(self, context, request): + self.context = context + self.request = request + + @Lazy + def loopsRoot(self): + return self.context.getLoopsRoot() + + @Lazy + def availableLanguages(self): + for opt in self.loopsRoot.options: + if opt.startswith('languages:'): + return opt[len('languages:'):].split(',') + return [] + + @Lazy + def defaultLanguage(self): + langs = self.availableLanguages + return langs and langs[0] or None + + @Lazy + def language(self): + lang = self.request.get('loops.language') + if lang is not None and lang in self.availableLanguages: + return lang + return (negotiator.getLanguage(self.availableLanguages, self.request) + or self.defaultLanguage) + + +class I18NView(object): + """ View mix-in class. + """ + + @Lazy + def languageInfo(self): + return LanguageInfo(self.context, self.request) + + @Lazy + def useI18N(self): + return (self.languageInfo.availableLanguages + and getattr(self.adapted, 'i18nAttributes', None)) + + @Lazy + def adapted(self): + return adapted(self.context, self.languageInfo) + + def checkLanguage(self): + # get language from session + self.setPreferredLanguage() + + def setLanguage(self, lang=None): + lang = lang or self.request.form.get('lang') + if lang: + upl = IUserPreferredLanguages(self.request) + upl.setPreferredLanguages([lang]) + + def switchLanguage(self, lang=None, keep=False): + keep = self.request.form.get('keep') + if keep: + pass # set in session + self.setPreferredLanguage(lang) + return self() + diff --git a/i18n/common.py b/i18n/common.py new file mode 100644 index 0000000..0e336f0 --- /dev/null +++ b/i18n/common.py @@ -0,0 +1,99 @@ +# +# Copyright (c) 2007 Helmut Merz helmutm@cy55.de +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +""" +Common stuff. + +$Id$ +""" + +from zope import component +from zope.component import adapts +from zope.interface import implements +from zope.cachedescriptors.property import Lazy +from zope.security.proxy import removeSecurityProxy +from persistent.mapping import PersistentMapping + +from cybertools.typology.interfaces import IType +from loops.common import adapted, AdapterBase + + +# support for i18n content + +class I18NValue(PersistentMapping): + """ A dictionary to be used for storing values for different languages. + """ + + def lower(self): + return str(self).lower() + + def __str__(self): + return self.values()[0] + + +def getI18nValue(obj, attr, langInfo=None): + obj = removeSecurityProxy(obj) + value = getattr(obj, attr, None) + lang = None + if isinstance(value, I18NValue): + lang = langInfo and langInfo.language or value.keys()[0] + value = value.get(lang) + #print '*** getI18nValue', attr, langInfo, lang, getattr(obj, attr, None), value + return value + +def setI18nValue(obj, attr, value, langInfo=None): + obj = removeSecurityProxy(obj) + old = getattr(obj, attr, None) + if langInfo is None: + setattr(obj, attr, value) + return + lang = langInfo.language + if isinstance(old, I18NValue): + old[lang] = value + else: + setattr(obj, attr, I18NValue(((lang, value),))) + #print '*** setI18nValue', attr, langInfo, lang, value, getattr(obj, attr, None) + + +class I18NAdapterBase(AdapterBase): + """ Base (or mix-in) class for concept adapters for internationalization of + context attributes. + """ + + _adapterAttributes = AdapterBase._adapterAttributes + ('languageInfo',) + languageInfo = None + + @Lazy + def i18nAttributes(self): + tp = IType(self.context) + attrs = tp.optionsDict.get('i18nattributes', '') + return [attr.strip() for attr in attrs.split(',')] + + def __getattr__(self, attr): + self.checkAttr(attr) + langInfo = attr in self.i18nAttributes and self.languageInfo or None + return getI18nValue(self.context, '_' + attr, langInfo) + + def __setattr__(self, attr, value): + if attr in self._adapterAttributes: + object.__setattr__(self, attr, value) + else: + langInfo = attr in self.i18nAttributes and self.languageInfo or None + self.checkAttr(attr) + setI18nValue(self.context, '_' + attr, value, langInfo) + diff --git a/i18n/configure.zcml b/i18n/configure.zcml new file mode 100644 index 0000000..7a78860 --- /dev/null +++ b/i18n/configure.zcml @@ -0,0 +1,14 @@ + + + + + + + diff --git a/i18n/tests.py b/i18n/tests.py new file mode 100755 index 0000000..6004b85 --- /dev/null +++ b/i18n/tests.py @@ -0,0 +1,24 @@ +# $Id$ + +import unittest, doctest +from zope.testing.doctestunit import DocFileSuite +from zope.app.testing import ztapi +from zope.interface.verify import verifyClass + + +class Test(unittest.TestCase): + "Basic tests for the i18n sub-package." + + def testSomething(self): + pass + + +def test_suite(): + flags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS + return unittest.TestSuite(( + unittest.makeSuite(Test), + DocFileSuite('README.txt', optionflags=flags), + )) + +if __name__ == '__main__': + unittest.main(defaultTest='test_suite') diff --git a/knowledge/configure.zcml b/knowledge/configure.zcml index 888ec39..860b00d 100644 --- a/knowledge/configure.zcml +++ b/knowledge/configure.zcml @@ -32,7 +32,10 @@ + interface="loops.knowledge.interfaces.ITopic" + set_attributes="languageInfo" /> +