appy.gen: every field can now define a dict of i18n mappings that will be used for translating their label and/or description and/or help message(s).

This commit is contained in:
Gaetan Delannay 2011-03-04 10:30:45 +01:00
parent fd9dd569db
commit 66a02c453e
4 changed files with 85 additions and 36 deletions

View file

@ -20,6 +20,7 @@ nullValues = (None, '', ' ')
validatorTypes = (types.FunctionType, types.UnboundMethodType, validatorTypes = (types.FunctionType, types.UnboundMethodType,
type(re.compile(''))) type(re.compile('')))
emptyTuple = () emptyTuple = ()
labelTypes = ('label', 'descr', 'help')
# Descriptor classes used for refining descriptions of elements in types # Descriptor classes used for refining descriptions of elements in types
# (pages, groups,...) ---------------------------------------------------------- # (pages, groups,...) ----------------------------------------------------------
@ -356,7 +357,7 @@ class Type:
editDefault, show, page, group, layouts, move, indexed, editDefault, show, page, group, layouts, move, indexed,
searchable, specificReadPermission, specificWritePermission, searchable, specificReadPermission, specificWritePermission,
width, height, colspan, master, masterValue, focus, width, height, colspan, master, masterValue, focus,
historized, sync): historized, sync, mapping):
# The validator restricts which values may be defined. It can be an # The validator restricts which values may be defined. It can be an
# interval (1,None), a list of string values ['choice1', 'choice2'], # interval (1,None), a list of string values ['choice1', 'choice2'],
# a regular expression, a custom function, a Selection instance, etc. # a regular expression, a custom function, a Selection instance, etc.
@ -443,6 +444,9 @@ class Type:
# self.sync below determines if the field representations will be # self.sync below determines if the field representations will be
# retrieved in a synchronous way by the browser or not (Ajax). # retrieved in a synchronous way by the browser or not (Ajax).
self.sync = self.formatSync(sync) self.sync = self.formatSync(sync)
# Mapping is a dict of contexts that, if specified, are given when
# translating the label, descr or help related to this field.
self.mapping = self.formatMapping(mapping)
self.id = id(self) self.id = id(self)
self.type = self.__class__.__name__ self.type = self.__class__.__name__
self.pythonType = None # The True corresponding Python type self.pythonType = None # The True corresponding Python type
@ -571,6 +575,26 @@ class Type:
sync[layoutType] = False sync[layoutType] = False
return sync return sync
def formatMapping(self, mapping):
'''Creates a dict of mappings, one entry by label type (label, descr,
help).'''
if isinstance(mapping, dict):
# Is it a dict like {'label':..., 'descr':...}, or is it directly a
# dict with a mapping?
for k, v in mapping.iteritems():
if (k not in labelTypes) or isinstance(v, basestring):
# It is already a mapping
return {'label':mapping, 'descr':mapping, 'help':mapping}
# If we are here, we have {'label':..., 'descr':...}. Complete
# it if necessary.
for labelType in labelTypes:
if labelType not in mapping:
mapping[labelType] = None # No mapping for this value.
return mapping
else:
# Mapping is a method that must be applied to any i18n message.
return {'label':mapping, 'descr':mapping, 'help':mapping}
def formatLayouts(self, layouts): def formatLayouts(self, layouts):
'''Standardizes the given p_layouts. .''' '''Standardizes the given p_layouts. .'''
# First, get the layouts as a dictionary, if p_layouts is None or # First, get the layouts as a dictionary, if p_layouts is None or
@ -696,10 +720,12 @@ class Type:
# Return self.default, of self.default() if it is a method # Return self.default, of self.default() if it is a method
if callable(self.default): if callable(self.default):
try: try:
return self.callMethod(obj, self.default, return self.callMethod(obj, self.default)
raiseOnError=True)
except Exception, e: except Exception, e:
# Already logged. # Already logged. Here I do not raise the exception,
# because it can be raised as the result of reindexing
# the object in situations that are not foreseen by
# method in self.default.
return None return None
else: else:
return self.default return self.default
@ -850,7 +876,7 @@ class Type:
res.validator = None res.validator = None
return res return res
def callMethod(self, obj, method, raiseOnError=False): def callMethod(self, obj, method, raiseOnError=True):
'''This method is used to call a p_method on p_obj. p_method is part of '''This method is used to call a p_method on p_obj. p_method is part of
this type definition (ie a default method, the method of a Computed this type definition (ie a default method, the method of a Computed
field, a method used for showing or not a field...). Normally, those field, a method used for showing or not a field...). Normally, those
@ -881,12 +907,12 @@ class Integer(Type):
searchable=False, specificReadPermission=False, searchable=False, specificReadPermission=False,
specificWritePermission=False, width=6, height=None, specificWritePermission=False, width=6, height=None,
colspan=1, master=None, masterValue=None, focus=False, colspan=1, master=None, masterValue=None, focus=False,
historized=False): historized=False, mapping=None):
Type.__init__(self, validator, multiplicity, index, default, optional, Type.__init__(self, validator, multiplicity, index, default, optional,
editDefault, show, page, group, layouts, move, indexed, editDefault, show, page, group, layouts, move, indexed,
searchable, specificReadPermission, searchable, specificReadPermission,
specificWritePermission, width, height, colspan, master, specificWritePermission, width, height, colspan, master,
masterValue, focus, historized, True) masterValue, focus, historized, True, mapping)
self.pythonType = long self.pythonType = long
def validateValue(self, obj, value): def validateValue(self, obj, value):
@ -910,7 +936,8 @@ class Float(Type):
searchable=False, specificReadPermission=False, searchable=False, specificReadPermission=False,
specificWritePermission=False, width=6, height=None, specificWritePermission=False, width=6, height=None,
colspan=1, master=None, masterValue=None, focus=False, colspan=1, master=None, masterValue=None, focus=False,
historized=False, precision=None, sep=(',', '.')): historized=False, mapping=None, precision=None,
sep=(',', '.')):
# The precision is the number of decimal digits. This number is used # The precision is the number of decimal digits. This number is used
# for rendering the float, but the internal float representation is not # for rendering the float, but the internal float representation is not
# rounded. # rounded.
@ -929,7 +956,7 @@ class Float(Type):
editDefault, show, page, group, layouts, move, indexed, editDefault, show, page, group, layouts, move, indexed,
False, specificReadPermission, specificWritePermission, False, specificReadPermission, specificWritePermission,
width, height, colspan, master, masterValue, focus, width, height, colspan, master, masterValue, focus,
historized, True) historized, True, mapping)
self.pythonType = float self.pythonType = float
def getFormattedValue(self, obj, value): def getFormattedValue(self, obj, value):
@ -1078,7 +1105,7 @@ class String(Type):
indexed=False, searchable=False, specificReadPermission=False, indexed=False, searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None, specificWritePermission=False, width=None, height=None,
colspan=1, master=None, masterValue=None, focus=False, colspan=1, master=None, masterValue=None, focus=False,
historized=False, transform='none'): historized=False, mapping=None, transform='none'):
self.format = format self.format = format
# The following field has a direct impact on the text entered by the # The following field has a direct impact on the text entered by the
# user. It applies a transformation on it, exactly as does the CSS # user. It applies a transformation on it, exactly as does the CSS
@ -1090,7 +1117,7 @@ class String(Type):
editDefault, show, page, group, layouts, move, indexed, editDefault, show, page, group, layouts, move, indexed,
searchable, specificReadPermission, searchable, specificReadPermission,
specificWritePermission, width, height, colspan, master, specificWritePermission, width, height, colspan, master,
masterValue, focus, historized, True) masterValue, focus, historized, True, mapping)
self.isSelect = self.isSelection() self.isSelect = self.isSelection()
# Default width and height vary according to String format # Default width and height vary according to String format
if width == None: if width == None:
@ -1312,12 +1339,12 @@ class Boolean(Type):
searchable=False, specificReadPermission=False, searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None, specificWritePermission=False, width=None, height=None,
colspan=1, master=None, masterValue=None, focus=False, colspan=1, master=None, masterValue=None, focus=False,
historized=False): historized=False, mapping=None):
Type.__init__(self, validator, multiplicity, index, default, optional, Type.__init__(self, validator, multiplicity, index, default, optional,
editDefault, show, page, group, layouts, move, indexed, editDefault, show, page, group, layouts, move, indexed,
searchable, specificReadPermission, searchable, specificReadPermission,
specificWritePermission, width, height, colspan, master, specificWritePermission, width, height, colspan, master,
masterValue, focus, historized, True) masterValue, focus, historized, True, mapping)
self.pythonType = bool self.pythonType = bool
def getDefaultLayouts(self): def getDefaultLayouts(self):
@ -1355,7 +1382,7 @@ class Date(Type):
searchable=False, specificReadPermission=False, searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None, specificWritePermission=False, width=None, height=None,
colspan=1, master=None, masterValue=None, focus=False, colspan=1, master=None, masterValue=None, focus=False,
historized=False): historized=False, mapping=None):
self.format = format self.format = format
self.calendar = calendar self.calendar = calendar
self.startYear = startYear self.startYear = startYear
@ -1364,7 +1391,7 @@ class Date(Type):
editDefault, show, page, group, layouts, move, indexed, editDefault, show, page, group, layouts, move, indexed,
searchable, specificReadPermission, searchable, specificReadPermission,
specificWritePermission, width, height, colspan, master, specificWritePermission, width, height, colspan, master,
masterValue, focus, historized, True) masterValue, focus, historized, True, mapping)
def getCss(self, layoutType): def getCss(self, layoutType):
if (layoutType == 'edit') and self.calendar: if (layoutType == 'edit') and self.calendar:
@ -1419,13 +1446,13 @@ class File(Type):
searchable=False, specificReadPermission=False, searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None, specificWritePermission=False, width=None, height=None,
colspan=1, master=None, masterValue=None, focus=False, colspan=1, master=None, masterValue=None, focus=False,
historized=False, isImage=False): historized=False, mapping=None, isImage=False):
self.isImage = isImage self.isImage = isImage
Type.__init__(self, validator, multiplicity, index, default, optional, Type.__init__(self, validator, multiplicity, index, default, optional,
editDefault, show, page, group, layouts, move, indexed, editDefault, show, page, group, layouts, move, indexed,
False, specificReadPermission, specificWritePermission, False, specificReadPermission, specificWritePermission,
width, height, colspan, master, masterValue, focus, width, height, colspan, master, masterValue, focus,
historized, True) historized, True, mapping)
@staticmethod @staticmethod
def getFileObject(filePath, fileName=None, zope=False): def getFileObject(filePath, fileName=None, zope=False):
@ -1569,8 +1596,8 @@ class Ref(Type):
searchable=False, specificReadPermission=False, searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=5, specificWritePermission=False, width=None, height=5,
colspan=1, master=None, masterValue=None, focus=False, colspan=1, master=None, masterValue=None, focus=False,
historized=False, queryable=False, queryFields=None, historized=False, mapping=None, queryable=False,
queryNbCols=1): queryFields=None, queryNbCols=1):
self.klass = klass self.klass = klass
self.attribute = attribute self.attribute = attribute
# May the user add new objects through this ref ? # May the user add new objects through this ref ?
@ -1620,7 +1647,7 @@ class Ref(Type):
editDefault, show, page, group, layouts, move, indexed, editDefault, show, page, group, layouts, move, indexed,
False, specificReadPermission, specificWritePermission, False, specificReadPermission, specificWritePermission,
width, height, colspan, master, masterValue, focus, width, height, colspan, master, masterValue, focus,
historized, sync) historized, sync, mapping)
self.validable = self.link self.validable = self.link
def getDefaultLayouts(self): return {'view': Table('l-f'), 'edit': 'lrv-f'} def getDefaultLayouts(self): return {'view': Table('l-f'), 'edit': 'lrv-f'}
@ -1787,7 +1814,7 @@ class Computed(Type):
specificWritePermission=False, width=None, height=None, specificWritePermission=False, width=None, height=None,
colspan=1, method=None, plainText=True, master=None, colspan=1, method=None, plainText=True, master=None,
masterValue=None, focus=False, historized=False, sync=True, masterValue=None, focus=False, historized=False, sync=True,
context={}): mapping=None, context={}):
# The Python method used for computing the field value # The Python method used for computing the field value
self.method = method self.method = method
# Does field computation produce plain text or XHTML? # Does field computation produce plain text or XHTML?
@ -1804,7 +1831,7 @@ class Computed(Type):
False, show, page, group, layouts, move, indexed, False, False, show, page, group, layouts, move, indexed, False,
specificReadPermission, specificWritePermission, width, specificReadPermission, specificWritePermission, width,
height, colspan, master, masterValue, focus, historized, height, colspan, master, masterValue, focus, historized,
sync) sync, mapping)
self.validable = False self.validable = False
def callMacro(self, obj, macroPath): def callMacro(self, obj, macroPath):
@ -1853,7 +1880,8 @@ class Action(Type):
searchable=False, specificReadPermission=False, searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None, specificWritePermission=False, width=None, height=None,
colspan=1, action=None, result='computation', confirm=False, colspan=1, action=None, result='computation', confirm=False,
master=None, masterValue=None, focus=False, historized=False): master=None, masterValue=None, focus=False, historized=False,
mapping=None):
# Can be a single method or a list/tuple of methods # Can be a single method or a list/tuple of methods
self.action = action self.action = action
# For the 'result' param: # For the 'result' param:
@ -1874,7 +1902,7 @@ class Action(Type):
False, show, page, group, layouts, move, indexed, False, False, show, page, group, layouts, move, indexed, False,
specificReadPermission, specificWritePermission, width, specificReadPermission, specificWritePermission, width,
height, colspan, master, masterValue, focus, historized, height, colspan, master, masterValue, focus, historized,
False) False, mapping)
self.validable = False self.validable = False
def getDefaultLayouts(self): return {'view': 'l-f', 'edit': 'lrv-f'} def getDefaultLayouts(self): return {'view': 'l-f', 'edit': 'lrv-f'}
@ -1922,12 +1950,12 @@ class Info(Type):
searchable=False, specificReadPermission=False, searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None, specificWritePermission=False, width=None, height=None,
colspan=1, master=None, masterValue=None, focus=False, colspan=1, master=None, masterValue=None, focus=False,
historized=False): historized=False, mapping=None):
Type.__init__(self, None, (0,1), index, default, optional, Type.__init__(self, None, (0,1), index, default, optional,
False, show, page, group, layouts, move, indexed, False, False, show, page, group, layouts, move, indexed, False,
specificReadPermission, specificWritePermission, width, specificReadPermission, specificWritePermission, width,
height, colspan, master, masterValue, focus, historized, height, colspan, master, masterValue, focus, historized,
False) False, mapping)
self.validable = False self.validable = False
class Pod(Type): class Pod(Type):
@ -1943,8 +1971,9 @@ class Pod(Type):
searchable=False, specificReadPermission=False, searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None, specificWritePermission=False, width=None, height=None,
colspan=1, master=None, masterValue=None, focus=False, colspan=1, master=None, masterValue=None, focus=False,
historized=False, template=None, context=None, action=None, historized=False, mapping=None, template=None, context=None,
askAction=False, stylesMapping={}, freezeFormat='pdf'): action=None, askAction=False, stylesMapping={},
freezeFormat='pdf'):
# The following param stores the path to a POD template # The following param stores the path to a POD template
self.template = template self.template = template
# The context is a dict containing a specific pod context, or a method # The context is a dict containing a specific pod context, or a method
@ -1964,7 +1993,7 @@ class Pod(Type):
False, show, page, group, layouts, move, indexed, False, show, page, group, layouts, move, indexed,
searchable, specificReadPermission, searchable, specificReadPermission,
specificWritePermission, width, height, colspan, master, specificWritePermission, width, height, colspan, master,
masterValue, focus, historized, False) masterValue, focus, historized, False, mapping)
self.validable = False self.validable = False
def isFrozen(self, obj): def isFrozen(self, obj):

View file

@ -1175,8 +1175,15 @@ class BaseMixin:
return res return res
def translate(self, label, mapping={}, domain=None, default=None, def translate(self, label, mapping={}, domain=None, default=None,
language=None, format='html'): language=None, format='html', field=None, className=None):
'''Translates a given p_label into p_domain with p_mapping.''' '''Translates a given p_label into p_domain with p_mapping.
If p_field is given, p_label does not correspond to a full label
name, but to a label type linked to p_field: "label", "descr"
or "help". Indeed, in this case, a specific i18n mapping may be
available on the field, so we must merge this mapping into
p_mapping. If p_className is not given, we consider p_self being an
instance of the class where p_field is defined.'''
cfg = self.getProductConfig() cfg = self.getProductConfig()
if not domain: domain = cfg.PROJECTNAME if not domain: domain = cfg.PROJECTNAME
if domain != cfg.PROJECTNAME: if domain != cfg.PROJECTNAME:
@ -1190,6 +1197,19 @@ class BaseMixin:
# TranslationService # TranslationService
res = label res = label
else: else:
# Get the label name, and the field-specific mapping if any.
if field:
# Maybe we do not have the field itself, but only its name
if isinstance(field, basestring):
field = self.getAppyType(field, className=className)
# Include field-specific mapping if any.
fieldMapping = field.mapping[label]
if fieldMapping:
if callable(fieldMapping):
fieldMapping = field.callMethod(self, fieldMapping)
mapping.update(fieldMapping)
# Get the label
label = getattr(field, label+'Id')
# We will get the translation from a Translation object. # We will get the translation from a Translation object.
# In what language must we get the translation? # In what language must we get the translation?
if not language: language = self.getUserLanguage() if not language: language = self.getUserLanguage()

View file

@ -120,9 +120,9 @@
maxReached python:(multiplicity[1] != None) and (len(objs) >= multiplicity[1]); maxReached python:(multiplicity[1] != None) and (len(objs) >= multiplicity[1]);
showPlusIcon python:not appyType['isBack'] and appyType['add'] and not maxReached and member.has_permission(addPermission, folder) and canWrite; showPlusIcon python:not appyType['isBack'] and appyType['add'] and not maxReached and member.has_permission(addPermission, folder) and canWrite;
atMostOneRef python: (multiplicity[1] == 1) and (len(objs)<=1); atMostOneRef python: (multiplicity[1] == 1) and (len(objs)<=1);
label python: tool.translate(appyType['labelId']); label python: contextObj.translate('label', field=appyType['name']);
addConfirmMsg python: tool.translate('%s_addConfirm' % appyType['labelId']); addConfirmMsg python: tool.translate('%s_addConfirm' % appyType['labelId']);
description python: tool.translate(appyType['descrId']); description python: contextObj.translate('descr', field=appyType['name']);
navBaseCall python: 'askRefField(\'%s\',\'%s\',\'%s\',\'%s\',**v**)' % (ajaxHookId, contextObj.absolute_url(), fieldName, innerRef)"> navBaseCall python: 'askRefField(\'%s\',\'%s\',\'%s\',\'%s\',**v**)' % (ajaxHookId, contextObj.absolute_url(), fieldName, innerRef)">
<tal:comment replace="nothing">This macro displays the Reference widget on a "consult" page. <tal:comment replace="nothing">This macro displays the Reference widget on a "consult" page.

View file

@ -186,17 +186,17 @@
<tal:label metal:define-macro="label" condition="widget/hasLabel"> <tal:label metal:define-macro="label" condition="widget/hasLabel">
<label tal:attributes="for widget/name" <label tal:attributes="for widget/name"
tal:condition="python: not ((widget['type'] == 'Action') or ((widget['type'] == 'Ref') and (widget['add'])))" tal:condition="python: not ((widget['type'] == 'Action') or ((widget['type'] == 'Ref') and (widget['add'])))"
tal:content="structure python: contextObj.translate(widget['labelId'])"></label> tal:content="structure python: contextObj.translate('label', field=widget['name'])"></label>
</tal:label> </tal:label>
<tal:comment replace="nothing">Displays a field description.</tal:comment> <tal:comment replace="nothing">Displays a field description.</tal:comment>
<tal:description metal:define-macro="description" condition="widget/hasDescr"> <tal:description metal:define-macro="description" condition="widget/hasDescr">
<span class="discreet" tal:content="structure python: contextObj.translate(widget['descrId'])"></span> <span class="discreet" tal:content="structure python: contextObj.translate('descr', field=widget['name'])"></span>
</tal:description> </tal:description>
<tal:comment replace="nothing">Displays a field help.</tal:comment> <tal:comment replace="nothing">Displays a field help.</tal:comment>
<tal:help metal:define-macro="help"> <tal:help metal:define-macro="help">
<acronym tal:attributes="title python: contextObj.translate(widget['helpId'])"> <acronym tal:attributes="title python: contextObj.translate('help', field=widget['name'])">
<img tal:attributes="src string: $portal_url/skyn/help.png"/> <img tal:attributes="src string: $portal_url/skyn/help.png"/>
</acronym> </acronym>
</tal:help> </tal:help>