Added I18n fields, properties and widgets
authorThierry Florac <thierry.florac@onf.fr>
Fri, 20 Mar 2015 17:28:43 +0100
changeset 2 a44a73ee12f9
parent 1 c62b53e70d9d
child 3 c43dd3c755d7
Added I18n fields, properties and widgets
.installed.cfg
buildout.cfg
setup.py
src/pyams_i18n.egg-info/SOURCES.txt
src/pyams_i18n.egg-info/requires.txt
src/pyams_i18n/attr.py
src/pyams_i18n/interfaces/__init__.py
src/pyams_i18n/interfaces/schema.py
src/pyams_i18n/interfaces/widget.py
src/pyams_i18n/locales/fr/LC_MESSAGES/pyams_i18n.mo
src/pyams_i18n/locales/fr/LC_MESSAGES/pyams_i18n.po
src/pyams_i18n/locales/pyams_i18n.pot
src/pyams_i18n/property.py
src/pyams_i18n/schema.py
src/pyams_i18n/vocabulary.py
src/pyams_i18n/widget/__init__.py
src/pyams_i18n/widget/templates/i18n-input.pt
--- a/.installed.cfg	Wed Mar 11 11:58:56 2015 +0100
+++ b/.installed.cfg	Fri Mar 20 17:28:43 2015 +0100
@@ -1,26 +1,35 @@
 [buildout]
-installed_develop_eggs = /home/tflorac/Dropbox/src/PyAMS/pyams_i18n/develop-eggs/pyams-i18n.egg-link
-parts = package i18n pyflakes pyflakesrun test
+installed_develop_eggs = /home/tflorac/Dropbox/src/PyAMS/pyams_i18n/develop-eggs/pyams-file.egg-link
+	/home/tflorac/Dropbox/src/PyAMS/pyams_i18n/develop-eggs/pyams-i18n.egg-link
+	/home/tflorac/Dropbox/src/PyAMS/pyams_i18n/develop-eggs/pyams-utils.egg-link
+parts = package i18n pyflakes test
 
 [package]
-__buildout_installed__ = /home/tflorac/Dropbox/src/PyAMS/pyams_i18n/bin/pserve
+__buildout_installed__ = /home/tflorac/Dropbox/src/PyAMS/pyams_i18n/bin/pyams_upgrade
+	/home/tflorac/Dropbox/src/PyAMS/pyams_i18n/bin/proutes
+	/home/tflorac/Dropbox/src/PyAMS/pyams_i18n/bin/pshell
+	/home/tflorac/Dropbox/src/PyAMS/pyams_i18n/bin/pviews
+	/home/tflorac/Dropbox/src/PyAMS/pyams_i18n/bin/pdistreport
 	/home/tflorac/Dropbox/src/PyAMS/pyams_i18n/bin/pcreate
-	/home/tflorac/Dropbox/src/PyAMS/pyams_i18n/bin/proutes
-	/home/tflorac/Dropbox/src/PyAMS/pyams_i18n/bin/pviews
 	/home/tflorac/Dropbox/src/PyAMS/pyams_i18n/bin/prequest
-	/home/tflorac/Dropbox/src/PyAMS/pyams_i18n/bin/pdistreport
-	/home/tflorac/Dropbox/src/PyAMS/pyams_i18n/bin/pshell
+	/home/tflorac/Dropbox/src/PyAMS/pyams_i18n/bin/pserve
 	/home/tflorac/Dropbox/src/PyAMS/pyams_i18n/bin/ptweens
-__buildout_signature__ = zc.recipe.egg-2.0.1-py3.4.egg setuptools-12.0.4-py3.4.egg zc.buildout-2.3.1-py3.4.egg
+__buildout_signature__ = zc.recipe.egg-2.0.1-py3.4.egg setuptools-14.3-py3.4.egg zc.buildout-2.3.1-py3.4.egg
 _b = /home/tflorac/Dropbox/src/PyAMS/pyams_i18n/bin
 _d = /home/tflorac/Dropbox/src/PyAMS/pyams_i18n/develop-eggs
 _e = /var/local/env/pyams/eggs
 bin-directory = /home/tflorac/Dropbox/src/PyAMS/pyams_i18n/bin
 develop-eggs-directory = /home/tflorac/Dropbox/src/PyAMS/pyams_i18n/develop-eggs
 eggs = pyams_i18n
+	persistent
+	pyams_file
+	pyams_utils
 	pyramid
 	zope.component
+	zope.container
 	zope.interface
+	zope.schema
+	zope.site
 eggs-directory = /var/local/env/pyams/eggs
 recipe = zc.recipe.egg
 
@@ -28,7 +37,7 @@
 __buildout_installed__ = /home/tflorac/Dropbox/src/PyAMS/pyams_i18n/bin/pybabel
 	/home/tflorac/Dropbox/src/PyAMS/pyams_i18n/bin/pot-create
 	/home/tflorac/Dropbox/src/PyAMS/pyams_i18n/bin/polint
-__buildout_signature__ = zc.recipe.egg-2.0.1-py3.4.egg setuptools-12.0.4-py3.4.egg zc.buildout-2.3.1-py3.4.egg
+__buildout_signature__ = zc.recipe.egg-2.0.1-py3.4.egg setuptools-14.3-py3.4.egg zc.buildout-2.3.1-py3.4.egg
 _b = /home/tflorac/Dropbox/src/PyAMS/pyams_i18n/bin
 _d = /home/tflorac/Dropbox/src/PyAMS/pyams_i18n/develop-eggs
 _e = /var/local/env/pyams/eggs
@@ -42,7 +51,7 @@
 [pyflakes]
 __buildout_installed__ = /home/tflorac/Dropbox/src/PyAMS/pyams_i18n/bin/pyflakes
 	/home/tflorac/Dropbox/src/PyAMS/pyams_i18n/bin/pyflakes
-__buildout_signature__ = zc.recipe.egg-2.0.1-py3.4.egg setuptools-12.0.4-py3.4.egg zc.buildout-2.3.1-py3.4.egg
+__buildout_signature__ = zc.recipe.egg-2.0.1-py3.4.egg setuptools-14.3-py3.4.egg zc.buildout-2.3.1-py3.4.egg
 _b = /home/tflorac/Dropbox/src/PyAMS/pyams_i18n/bin
 _d = /home/tflorac/Dropbox/src/PyAMS/pyams_i18n/develop-eggs
 _e = /var/local/env/pyams/eggs
@@ -55,17 +64,10 @@
 recipe = zc.recipe.egg
 scripts = pyflakes
 
-[pyflakesrun]
-__buildout_installed__ = 
-__buildout_signature__ = collective.recipe.cmd-0.9-py3.4.egg zc.buildout-2.3.1-py3.4.egg setuptools-12.0.4-py3.4.egg
-cmds = ./bin/pyflakes
-on_install = true
-recipe = collective.recipe.cmd
-
 [test]
 __buildout_installed__ = /home/tflorac/Dropbox/src/PyAMS/pyams_i18n/parts/test
 	/home/tflorac/Dropbox/src/PyAMS/pyams_i18n/bin/test
-__buildout_signature__ = zc.recipe.testrunner-2.0.0-py3.4.egg zc.recipe.egg-2.0.1-py3.4.egg setuptools-12.0.4-py3.4.egg zope.testrunner-4.4.6-py3.4.egg zc.buildout-2.3.1-py3.4.egg zope.interface-4.1.2-py3.4-linux-x86_64.egg zope.exceptions-4.0.7-py3.4.egg six-45a2be65d681713a598787ec39be3290
+__buildout_signature__ = zc.recipe.testrunner-2.0.0-py3.4.egg zc.recipe.egg-2.0.1-py3.4.egg setuptools-14.3-py3.4.egg zope.testrunner-4.4.6-py3.4.egg zc.buildout-2.3.1-py3.4.egg zope.interface-4.1.2-py3.4-linux-x86_64.egg zope.exceptions-4.0.7-py3.4.egg six-1482e89f68d85eea27f4ed7749df2819
 _b = /home/tflorac/Dropbox/src/PyAMS/pyams_i18n/bin
 _d = /home/tflorac/Dropbox/src/PyAMS/pyams_i18n/develop-eggs
 _e = /var/local/env/pyams/eggs
@@ -76,3 +78,21 @@
 location = /home/tflorac/Dropbox/src/PyAMS/pyams_i18n/parts/test
 recipe = zc.recipe.testrunner
 script = /home/tflorac/Dropbox/src/PyAMS/pyams_i18n/bin/test
+
+[buildout]
+installed_develop_eggs = /home/tflorac/Dropbox/src/PyAMS/pyams_i18n/develop-eggs/pyams-file.egg-link
+	/home/tflorac/Dropbox/src/PyAMS/pyams_i18n/develop-eggs/lingua.egg-link
+	/home/tflorac/Dropbox/src/PyAMS/pyams_i18n/develop-eggs/pyams-i18n.egg-link
+	/home/tflorac/Dropbox/src/PyAMS/pyams_i18n/develop-eggs/pyams-utils.egg-link
+
+[buildout]
+parts = i18n pyflakes test package
+
+[buildout]
+parts = pyflakes test package i18n
+
+[buildout]
+parts = test package i18n pyflakes
+
+[buildout]
+parts = package i18n pyflakes test
--- a/buildout.cfg	Wed Mar 11 11:58:56 2015 +0100
+++ b/buildout.cfg	Fri Mar 20 17:28:43 2015 +0100
@@ -18,12 +18,13 @@
 
 src = src
 develop = .
-
+          /var/local/src/pyams/ext/lingua
+          ../pyams_file
+          ../pyams_utils
 parts =
     package
     i18n
     pyflakes
-    pyflakesrun
     test
 
 [package]
@@ -31,6 +32,7 @@
 eggs =
     pyams_i18n
     persistent
+    pyams_file
     pyams_utils
     pyramid
     zope.component
--- a/setup.py	Wed Mar 11 11:58:56 2015 +0100
+++ b/setup.py	Fri Mar 20 17:28:43 2015 +0100
@@ -60,6 +60,7 @@
           'setuptools',
           # -*- Extra requirements: -*-
           'persistent',
+          'pyams_file',
           'pyams_utils',
           'pyramid',
           'zope.component',
--- a/src/pyams_i18n.egg-info/SOURCES.txt	Wed Mar 11 11:58:56 2015 +0100
+++ b/src/pyams_i18n.egg-info/SOURCES.txt	Fri Mar 20 17:28:43 2015 +0100
@@ -3,10 +3,14 @@
 docs/HISTORY.txt
 docs/README.txt
 src/pyams_i18n/__init__.py
+src/pyams_i18n/attr.py
 src/pyams_i18n/configure.zcml
 src/pyams_i18n/language.py
 src/pyams_i18n/negotiator.py
+src/pyams_i18n/property.py
+src/pyams_i18n/schema.py
 src/pyams_i18n/site.py
+src/pyams_i18n/vocabulary.py
 src/pyams_i18n.egg-info/PKG-INFO
 src/pyams_i18n.egg-info/SOURCES.txt
 src/pyams_i18n.egg-info/dependency_links.txt
@@ -16,6 +20,8 @@
 src/pyams_i18n.egg-info/requires.txt
 src/pyams_i18n.egg-info/top_level.txt
 src/pyams_i18n/interfaces/__init__.py
+src/pyams_i18n/interfaces/schema.py
+src/pyams_i18n/interfaces/widget.py
 src/pyams_i18n/locales/pyams_i18n.pot
 src/pyams_i18n/locales/fr/LC_MESSAGES/pyams_i18n.mo
 src/pyams_i18n/locales/fr/LC_MESSAGES/pyams_i18n.po
@@ -275,6 +281,8 @@
 src/pyams_i18n/resources/img/flags/zh_SG.png
 src/pyams_i18n/resources/img/flags/zh_TW.png
 src/pyams_i18n/resources/img/flags/zu_ZA.png
+src/pyams_i18n/widget/__init__.py
+src/pyams_i18n/widget/templates/i18n-input.pt
 src/pyams_i18n/zmi/__init__.py
 src/pyams_i18n/zmi/configure.zcml
 src/pyams_i18n/zmi/negotiator.py
\ No newline at end of file
--- a/src/pyams_i18n.egg-info/requires.txt	Wed Mar 11 11:58:56 2015 +0100
+++ b/src/pyams_i18n.egg-info/requires.txt	Fri Mar 20 17:28:43 2015 +0100
@@ -1,5 +1,6 @@
 setuptools
 persistent
+pyams_file
 pyams_utils
 pyramid
 zope.component
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_i18n/attr.py	Fri Mar 20 17:28:43 2015 +0100
@@ -0,0 +1,36 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from zope.traversing.interfaces import ITraversable
+
+# import packages
+from pyams_utils.adapter import ContextAdapter, adapter_config
+from pyramid.exceptions import NotFound
+from zope.interface import Interface
+
+
+@adapter_config(name='i18n', context=Interface, provides=ITraversable)
+class I18nAttributeTraverser(ContextAdapter):
+    """++i18n++attr:lang namespace traverser"""
+
+    def traverse(self, name, furtherpath=None):
+        try:
+            attr, lang = name.split(':')
+            return getattr(self.context, attr, {}).get(lang)
+        except AttributeError:
+            raise NotFound
--- a/src/pyams_i18n/interfaces/__init__.py	Wed Mar 11 11:58:56 2015 +0100
+++ b/src/pyams_i18n/interfaces/__init__.py	Fri Mar 20 17:28:43 2015 +0100
@@ -19,7 +19,7 @@
 from zope.interface import Interface, invariant, Invalid
 
 # import packages
-from zope.schema import Choice, Set, Bool
+from zope.schema import Choice, Set, Bool, List
 
 from pyams_i18n import _
 
@@ -55,8 +55,8 @@
                                           "user select languages which are offered in "
                                           "a skin."""),
                             value_type=Choice(vocabulary='PyAMS base languages'),
-                            default={'en', },
-                            required=False)
+                            default={'en'},
+                            required=True)
 
     cache_enabled = Bool(title=_("Language caching enabled"),
                          description=_("Language caching enabled (per request)"),
@@ -77,6 +77,18 @@
         """Clear cached language value"""
 
 
+class II18nManager(Interface):
+    """Context languages manager
+
+    This interface is used to handle contents providing several languages
+    """
+
+    languages = List(title=_("Content languages"),
+                     description=_("List of languages available for this content"),
+                     required=True,
+                     value_type=Choice(vocabulary='PyAMS offered languages'))
+
+
 class IUserPreferredLanguage(Interface):
     """This interface provides language negotiation based on user preferences"""
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_i18n/interfaces/schema.py	Fri Mar 20 17:28:43 2015 +0100
@@ -0,0 +1,45 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from zope.schema.interfaces import IDict
+
+# import packages
+
+
+class II18nField(IDict):
+    """I18n field marker interface"""
+
+
+class II18nTextLineField(II18nField):
+    """I18n text line field marker interface"""
+
+
+class II18nTextField(II18nField):
+    """I18n text field marker interface"""
+
+
+class II18nFileField(II18nField):
+    """I18n file field marker interface"""
+
+
+class II18nImageField(II18nFileField):
+    """I18n image field marker interface"""
+
+
+class II18nThumbnailImageField(II18nImageField):
+    """I18n image field with thumbnail marker interface"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_i18n/interfaces/widget.py	Fri Mar 20 17:28:43 2015 +0100
@@ -0,0 +1,37 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from z3c.form.interfaces import IWidget
+
+# import packages
+
+
+class II18nWidget(IWidget):
+    """I18n base widget interface"""
+
+
+class II18nTextLineWidget(II18nWidget):
+    """I18n text line widget interface"""
+
+
+class II18nTextWidget(II18nWidget):
+    """I18n text widget interface"""
+
+
+class II18nFileWidget(II18nWidget):
+    """I18n file widget interface"""
Binary file src/pyams_i18n/locales/fr/LC_MESSAGES/pyams_i18n.mo has changed
--- a/src/pyams_i18n/locales/fr/LC_MESSAGES/pyams_i18n.po	Wed Mar 11 11:58:56 2015 +0100
+++ b/src/pyams_i18n/locales/fr/LC_MESSAGES/pyams_i18n.po	Fri Mar 20 17:28:43 2015 +0100
@@ -5,7 +5,7 @@
 msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE 1.0\n"
-"POT-Creation-Date: 2015-02-03 21:41+0100\n"
+"POT-Creation-Date: 2015-03-20 17:26+0100\n"
 "PO-Revision-Date: 2015-02-03 21:43+0100\n"
 "Last-Translator: Thierry Florac <tflorac@ulthar.net>\n"
 "Language-Team: French\n"
@@ -1540,7 +1540,11 @@
 msgid "Zulu (South Africa)"
 msgstr "Zoulou (Afrique du Sud)"
 
-#: src/pyams_i18n/zmi/negotiator.py:37
+#: src/pyams_i18n/vocabulary.py:43 src/pyams_i18n/vocabulary.py:59
+msgid "<unknown>"
+msgstr ""
+
+#: src/pyams_i18n/zmi/negotiator.py:40
 msgid "Update languages negotiator properties"
 msgstr "Mise à jour des propriétés du gestionnaire de langues"
 
@@ -1580,6 +1584,14 @@
 msgid "Language caching enabled (per request)"
 msgstr "Un cache de langue peut être activé pour chaque requête..."
 
+#: src/pyams_i18n/interfaces/__init__.py:86
+msgid "Content languages"
+msgstr "Langues proposées"
+
+#: src/pyams_i18n/interfaces/__init__.py:87
+msgid "List of languages available for this content"
+msgstr "Liste des langues disponibles pour la traduction des contenus"
+
 #: src/pyams_i18n/interfaces/__init__.py:68
 msgid "Unsupported language policy"
 msgstr "Cette politique de sélection de langue n'est pas supportée..."
--- a/src/pyams_i18n/locales/pyams_i18n.pot	Wed Mar 11 11:58:56 2015 +0100
+++ b/src/pyams_i18n/locales/pyams_i18n.pot	Fri Mar 20 17:28:43 2015 +0100
@@ -6,7 +6,7 @@
 msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE 1.0\n"
-"POT-Creation-Date: 2015-02-03 21:41+0100\n"
+"POT-Creation-Date: 2015-03-20 17:26+0100\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -14,7 +14,7 @@
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Lingua 3.8\n"
+"Generated-By: Lingua 3.10.dev0\n"
 
 #: ./src/pyams_i18n/language.py:28
 msgid "Afar"
@@ -1540,7 +1540,11 @@
 msgid "Zulu (South Africa)"
 msgstr ""
 
-#: ./src/pyams_i18n/zmi/negotiator.py:37
+#: ./src/pyams_i18n/vocabulary.py:43 ./src/pyams_i18n/vocabulary.py:59
+msgid "<unknown>"
+msgstr ""
+
+#: ./src/pyams_i18n/zmi/negotiator.py:40
 msgid "Update languages negotiator properties"
 msgstr ""
 
@@ -1578,6 +1582,14 @@
 msgid "Language caching enabled (per request)"
 msgstr ""
 
+#: ./src/pyams_i18n/interfaces/__init__.py:86
+msgid "Content languages"
+msgstr ""
+
+#: ./src/pyams_i18n/interfaces/__init__.py:87
+msgid "List of languages available for this content"
+msgstr ""
+
 #: ./src/pyams_i18n/interfaces/__init__.py:68
 msgid "Unsupported language policy"
 msgstr ""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_i18n/property.py	Fri Mar 20 17:28:43 2015 +0100
@@ -0,0 +1,120 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_file.interfaces import DELETED_FILE, IFile, IFileInfo, IFileFieldContainer
+from zope.annotation.interfaces import IAnnotations
+from zope.schema.interfaces import IField
+
+# import packages
+from pyams_file.file import FileFactory
+from pyams_file.property import FILE_CONTAINER_ATTRIBUTES
+from pyramid.threadlocal import get_current_registry
+from z3c.form.interfaces import NOT_CHANGED
+from zope.interface import alsoProvides
+from zope.lifecycleevent import ObjectCreatedEvent, ObjectRemovedEvent, ObjectAddedEvent
+from zope.location import locate
+
+
+_marker = object()
+
+
+class I18nFileProperty(object):
+    """I18n property class used to handle files"""
+
+    def __init__(self, field, name=None, klass=None, **args):
+        if not IField.providedBy(field):
+            raise ValueError("Provided field must implement IField interface...")
+        if name is None:
+            name = field.__name__
+        self.__field = field
+        self.__name = name
+        self.__klass = klass
+        self.__args = args
+
+    def __get__(self, instance, klass):
+        if instance is None:
+            return self
+        value = instance.__dict__.get(self.__name, _marker)
+        if value is _marker:
+            field = self.__field.bind(instance)
+            value = getattr(field, 'default', _marker)
+            if value is _marker:
+                raise AttributeError(self.__name)
+        return value
+
+    def __set__(self, instance, value):
+        registry = get_current_registry()
+        for lang in value:
+            lang_value = value[lang]
+            if (lang_value is DELETED_FILE) or (lang_value is NOT_CHANGED):
+                continue
+            elif lang_value is not None:
+                filename = None
+                # file upload data converter returns a tuple containing
+                # filename and buffered IO stream extracted from FieldStorage...
+                if isinstance(lang_value, tuple):
+                    filename, lang_value = lang_value
+                # initialize file through factory
+                if not IFile.providedBy(lang_value):
+                    factory = self.__klass or FileFactory
+                    file = factory(lang_value, **self.__args)
+                    registry.notify(ObjectCreatedEvent(file))
+                    if not file.get_size():
+                        lang_value.seek(0)  # because factory may read until end of file...
+                        file.data = lang_value
+                    lang_value = file
+                if filename is not None:
+                    info = IFileInfo(lang_value)
+                    if info is not None:
+                        info.filename = filename
+            value[lang] = lang_value
+        field = self.__field.bind(instance)
+        field.validate(value)
+        if field.readonly and instance.__dict__.has_key(self.__name):
+            raise ValueError(self.__name, "Field is readonly")
+        old_value = instance.__dict__.get(self.__name, _marker)
+        if old_value != value:
+            # check for previous value
+            if old_value is _marker:
+                old_value = {}
+            for lang in value:
+                new_lang_value = value.get(lang)
+                if new_lang_value is NOT_CHANGED:
+                    continue
+                old_lang_value = old_value.get(lang, _marker)
+                if (old_lang_value is not _marker) and (old_lang_value is not None):
+                    registry.notify(ObjectRemovedEvent(old_lang_value))
+                if new_lang_value is DELETED_FILE:
+                    if self.__name in instance.__dict__:
+                        del old_value[lang]
+                else:
+                    # set name of new value
+                    name = '++i18n++{0}:{1}'.format(self.__name, lang)
+                    if new_lang_value is not None:
+                        locate(new_lang_value, instance, name)
+                    old_value[lang] = new_lang_value
+                    # store file attributes of instance
+                    if not IFileFieldContainer.providedBy(instance):
+                        alsoProvides(instance, IFileFieldContainer)
+                    annotations = IAnnotations(instance)
+                    attributes = annotations.get(FILE_CONTAINER_ATTRIBUTES)
+                    if attributes is None:
+                        attributes = annotations[FILE_CONTAINER_ATTRIBUTES] = set()
+                    attributes.add('{0}::{1}'.format(self.__name, lang))
+                    registry.notify(ObjectAddedEvent(new_lang_value, instance, name))
+                instance.__dict__[self.__name] = old_value
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_i18n/schema.py	Fri Mar 20 17:28:43 2015 +0100
@@ -0,0 +1,136 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_i18n.interfaces.schema import II18nField, II18nTextLineField, II18nTextField, II18nFileField, II18nImageField, \
+    II18nThumbnailImageField
+from zope.schema.interfaces import RequiredMissing
+
+# import packages
+from persistent.mapping import PersistentMapping
+from pyams_file.schema import FileField, ImageField, ThumbnailImageField
+from zope.interface import implementer
+from zope.schema import Dict, TextLine, Text
+
+
+_marker = object()
+
+
+class DefaultValueDict(PersistentMapping):
+    """Persistent mapping with default value"""
+
+    def __init__(self, default=None, *args, **kwargs):
+        super(DefaultValueDict, self).__init__(*args, **kwargs)
+        self._default = default
+
+    def __missing__(self, key):
+        return self._default
+
+    def get(self, key, default=None):
+        result = super(DefaultValueDict, self).get(key, _marker)
+        if result is _marker:
+            if default is not None:
+                return default
+            else:
+                return self._default
+        else:
+            return result
+
+    def copy(self):
+        return DefaultValueDict(default=self._default, **self)
+
+
+@implementer(II18nField)
+class I18nField(Dict):
+    """I18n base field class"""
+
+    def __init__(self, key_type=None, value_type=None, **kwargs):
+        # DefaultValueDict.__init__(self, default)
+        Dict.__init__(self, key_type=TextLine(), value_type=value_type, **kwargs)
+
+    def _validate(self, value):
+        super(I18nField, self)._validate(value)
+        if self.required:
+            if self.default:
+                return
+            if not value:
+                raise RequiredMissing
+            for lang in value.values():
+                if lang:
+                    return
+            raise RequiredMissing
+
+
+@implementer(II18nTextLineField)
+class I18nTextLineField(I18nField):
+    """I18n text line field"""
+
+    def __init__(self, key_type=None, value_type=None, default=None,
+                 value_constraint=None, value_min_length=0, value_max_length=None, **kwargs):
+        super(I18nTextLineField, self).__init__(value_type=TextLine(constraint=value_constraint,
+                                                                    min_length=value_min_length,
+                                                                    max_length=value_max_length,
+                                                                    default=default,
+                                                                    required=False),
+                                                **kwargs)
+
+
+@implementer(II18nTextField)
+class I18nTextField(I18nField):
+    """I18n text field"""
+
+    def __init__(self, key_type=None, value_type=None, default=None,
+                 value_constraint=None, value_min_length=0, value_max_length=None, **kwargs):
+        super(I18nTextField, self).__init__(value_type=Text(constraint=value_constraint,
+                                                            min_length=value_min_length,
+                                                            max_length=value_max_length,
+                                                            default=default,
+                                                            required=False),
+                                            **kwargs)
+
+
+@implementer(II18nFileField)
+class I18nFileField(I18nField):
+    """I18n file field"""
+
+    def __init__(self, key_type=None, value_type=None, value_min_length=None, value_max_length=None, **kwargs):
+        super(I18nFileField, self).__init__(value_type=FileField(min_length=value_min_length,
+                                                                 max_length=value_max_length,
+                                                                 required=False),
+                                            **kwargs)
+
+
+@implementer(II18nImageField)
+class I18nImageField(I18nField):
+    """I18n image field"""
+
+    def __init__(self, key_type=None, value_type=None, value_min_length=None, value_max_length=None, **kwargs):
+        super(I18nImageField, self).__init__(value_type=ImageField(min_length=value_min_length,
+                                                                   max_length=value_max_length,
+                                                                   required=False),
+                                             **kwargs)
+
+
+@implementer(II18nThumbnailImageField)
+class I18nThumbnailImageField(I18nField):
+    """I18n thumbnail image field"""
+
+    def __init__(self, key_type=None, value_type=None, value_min_length=None, value_max_length=None, **kwargs):
+        super(I18nThumbnailImageField, self).__init__(value_type=ThumbnailImageField(min_length=value_min_length,
+                                                                                     max_length=value_max_length,
+                                                                                     required=False),
+                                                      **kwargs)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_i18n/vocabulary.py	Fri Mar 20 17:28:43 2015 +0100
@@ -0,0 +1,62 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+from pyams_utils.traversing import get_parent
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_i18n.interfaces import INegotiator, II18nManager
+from zope.schema.interfaces import IVocabularyFactory
+
+# import packages
+from pyams_i18n.language import BASE_LANGUAGES
+from pyams_utils.registry import query_utility
+from pyams_utils.request import check_request
+from zope.interface import provider
+from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm, getVocabularyRegistry
+
+from pyams_i18n import _
+
+
+@provider(IVocabularyFactory)
+class I18nOfferedLanguages(SimpleVocabulary):
+    """I18n offered languages vocabulary"""
+
+    def __init__(self, context):
+        terms = []
+        negotiator = query_utility(INegotiator)
+        if negotiator is not None:
+            translate = check_request().localizer.translate
+            for lang in negotiator.offered_languages:
+                terms.append(SimpleTerm(lang, title=translate(BASE_LANGUAGES.get(lang) or _("<unknown>"))))
+        super(I18nOfferedLanguages, self).__init__(terms)
+
+getVocabularyRegistry().register('PyAMS offered languages', I18nOfferedLanguages)
+
+
+@provider(IVocabularyFactory)
+class I18nContentLanguages(SimpleVocabulary):
+    """I18n content languages vocabulary"""
+
+    def __init__(self, context):
+        terms = []
+        manager = get_parent(context, II18nManager)
+        if manager is not None:
+            translate = check_request().localizer.translate
+            for lang in manager.languages:
+                terms.append(SimpleTerm(lang, title=translate(BASE_LANGUAGES.get(lang) or _("<unknown>"))))
+        super(I18nContentLanguages, self).__init__(terms)
+
+getVocabularyRegistry().register('PyAMS content languages', I18nContentLanguages)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_i18n/widget/__init__.py	Fri Mar 20 17:28:43 2015 +0100
@@ -0,0 +1,152 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+from pyramid.decorator import reify
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_form.interfaces.form import IFormLayer
+from pyams_i18n.interfaces import II18nManager, INegotiator
+from pyams_i18n.interfaces.schema import II18nField, II18nTextLineField, II18nTextField, II18nFileField
+from pyams_i18n.interfaces.widget import II18nWidget, II18nTextLineWidget, II18nTextWidget, II18nFileWidget
+from z3c.form.interfaces import IDataConverter, IFieldWidget, NO_VALUE
+
+# import packages
+from pyams_form.widget import widgettemplate_config
+from pyams_utils.adapter import adapter_config
+from pyams_utils.registry import query_utility
+from pyams_utils.traversing import get_parent
+from z3c.form.browser.widget import HTMLInputWidget
+from z3c.form.converter import BaseDataConverter
+from z3c.form.util import expandPrefix
+from z3c.form.widget import Widget, FieldWidget
+from zope.interface import implementer_only
+
+
+@adapter_config(context=(II18nField, II18nWidget), provides=IDataConverter)
+class I18nDataConverter(BaseDataConverter):
+    """I18n base data converter"""
+
+    def __init__(self, field, widget):
+        super(I18nDataConverter, self).__init__(field, widget)
+
+    def toWidgetValue(self, value):
+        # I18n widget is using a dict where each key is a lang
+        # and each value is matching widget value
+        result = {}
+        registry = self.widget.request.registry
+        for lang, val in value.items():
+            converter = registry.queryMultiAdapter((self.field.value_type, self.widget.get_widget(lang)),
+                                                   IDataConverter)
+            if converter is not None:
+                result[lang] = converter.toWidgetValue(val)
+        return result
+
+    def toFieldValue(self, value):
+        # I18n widget is using a dict where each key is a lang
+        # and each value is matching widget value
+        result = {}
+        registry = self.widget.request.registry
+        for lang in self.widget.langs:
+            converter = registry.queryMultiAdapter((self.field.value_type, self.widget.get_widget(lang)),
+                                                   IDataConverter)
+            if converter is not None:
+                result[lang] = converter.toFieldValue(self.widget.get_value(lang))
+        return result
+
+
+@widgettemplate_config(mode='input', template='templates/i18n-input.pt', widget=II18nWidget, layer=IFormLayer)
+@widgettemplate_config(mode='display', template='templates/i18n-input.pt', widget=II18nWidget, layer=IFormLayer)
+@implementer_only(II18nWidget)
+class I18nWidget(HTMLInputWidget, Widget):
+    """I18n base widget"""
+
+    @reify
+    def langs(self):
+        langs = []
+        negotiator = query_utility(INegotiator)
+        if negotiator is not None:
+            langs.append(negotiator.server_language)
+        manager = get_parent(self.context, II18nManager)
+        if manager is not None:
+            langs.extend(sorted(filter(lambda x: x not in langs, manager.languages)))
+        elif negotiator is not None:
+            langs.extend(sorted(filter(lambda x: x not in langs, negotiator.offered_languages)))
+        else:
+            langs.append('en')
+        return langs
+
+    def update(self):
+        self.widgets = {}
+        for lang in self.langs:
+            widget = self.request.registry.queryMultiAdapter((self.field.value_type, self.request), IFieldWidget)
+            if widget is not None:
+                prefix = expandPrefix(self.form.prefix) + expandPrefix(self.form.widgets.prefix) + expandPrefix(lang)
+                name = self.field.value_type.__name__ = self.field.__name__
+                widget.name = prefix + name
+                widget.id = widget.name.replace('.', '-')
+                widget.form = self.form
+                widget.field = self.field.value_type
+                widget.context = self.context
+                widget.ignoreContext = self.ignoreContext
+                widget.ignoreRequest = self.ignoreRequest
+                widget.lang = lang
+                widget.update()
+            self.widgets[lang] = widget
+        super(I18nWidget, self).update()
+
+    def extract(self, default=NO_VALUE):
+        result = {}
+        [result.setdefault(lang, self.widgets[lang].extract(default)) for lang in self.widgets.keys()]
+        return result
+    
+    def get_widget(self, lang):
+        return self.widgets.get(lang)
+
+    def get_value(self, lang):
+        return self.get_widget(lang).value
+
+
+@implementer_only(II18nTextLineWidget)
+class I18nTextLineWidget(I18nWidget):
+    """I18n text line widget"""
+
+
+@adapter_config(context=(II18nTextLineField, IFormLayer), provides=IFieldWidget)
+def I18nTextLineFieldWidget(field, request):
+    """I18n text line field widget factory"""
+    return FieldWidget(field, I18nTextLineWidget(request))
+
+
+@implementer_only(II18nTextWidget)
+class I18nTextWidget(I18nWidget):
+    """I18n text widget"""
+
+
+@adapter_config(context=(II18nTextField, IFormLayer), provides=IFieldWidget)
+def I18nTextFieldWidget(field, request):
+    """I18n text field widget factory"""
+    return FieldWidget(field, I18nTextWidget(request))
+
+
+@implementer_only(II18nFileWidget)
+class I18nFileWidget(I18nWidget):
+    """I18n file widget"""
+
+
+@adapter_config(context=(II18nFileField, IFormLayer), provides=IFieldWidget)
+def I18nFileFieldWidget(field, request):
+    """I18n file field widget factory"""
+    return FieldWidget(field, I18nFileWidget(request))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_i18n/widget/templates/i18n-input.pt	Fri Mar 20 17:28:43 2015 +0100
@@ -0,0 +1,32 @@
+<tal:var define="langs view/langs">
+	<tal:if condition="python:len(langs) == 1">
+		<div class="clearfix">
+			<tal:var replace="structure python:view.get_widget(tuple(langs)[0]).render()" />
+		</div>
+	</tal:if>
+	<tal:if condition="python:len(langs) > 1">
+		<ul class="nav nav-tabs">
+			<tal:loop repeat="lang langs">
+				<li tal:define="active python:'active' if repeat['lang'].start() else ''"
+					tal:attributes="class string:small ${active}">
+					<a data-toggle="tab"
+					   tal:attributes="href string:#${view/id}-${lang}">
+						<img tal:attributes="src string:/--static--/pyams_i18n/img/flags/${lang}.png" />
+						<i class="fa fa-fw fa-star"
+						   tal:define="widget python:view.get_widget(lang)"
+						   tal:condition="widget/current_value | widget/value"></i>
+					</a>
+				</li>
+			</tal:loop>
+		</ul>
+		<div class="tab-content bordered nohover">
+			<tal:loop repeat="lang langs">
+				<div tal:define="active python:'active' if repeat['lang'].start() else ''"
+					 tal:attributes="class string:clearfix tab-pane ${active} fade in padding-5;
+									 id string:${view/id}-${lang};">
+					<tal:var replace="structure python:view.get_widget(lang).render()" />
+				</div>
+			</tal:loop>
+		</div>
+	</tal:if>
+</tal:var>