I just had the same use-case. I'm currently migrating https://www.dipf.de with multiple subsites (each hat theit own domain) and language-folders for the main site from Plone 4 (AT, LinguaPlone) to Plone 5 (DX, plone.app.multilingual).
The structure is like this:
Plone (SiteRoot)
├── de (RLF from plone.app.multilingual)
├── en (RLF from plone.app.multilingual)
├── subsite1 (subsite folder)
│ ├── de (RLF from plone.app.multilingual)
│ └── en (RLF from plone.app.multilingual)
├── subsite2 (subsite folder)
│ ├── de (RLF from plone.app.multilingual)
│ └── en (RLF from plone.app.multilingual)
└── subsite3 (subsite folder)
├── document (non multilingual content)
└── folder (non multilingual content)
And yes, you can mix Lineage and plone.app.multilingual but there is no User-Interface (yet) to create new subsites with their own language-root folders.
The code I use to create them below is mostly taken from plone.app.multilingual.browser.setup.SetupMultilingualSite
with mainly two changes:
-
Do not create and LIF (Assets) folders in subsites. Asset folders do not work out of the box with lineage because items in LIFs are indexed once for each LIF with a suffixed language. Since there would now be multiple LIFs for the same language a image would be indexed multiple times with the same UID. You can actually create images in such a LIF and it would be accessible from all subsites and is visible in all LIFs and there may be a way around the indexing-issue (e.g. by adding the id of the subsite as another suffix) but I decided to ignore this because the client does not use the asset folders.
-
Use aq_base(self.context)
when checking for the LRF in setUpLanguage
. Otherwise the code would assume the acquired LRF /Plone/en
is what you are trying to create whereas is it /Plone/subsite_1/en
.
You need to allow using the language-switcher on subsites:
<browser:view
for="plone.dexterity.interfaces.IDexterityContainer"
class="plone.app.multilingual.browser.switcher.LanguageSwitcher"
name="language-switcher"
permission="zope.Public"
layer="plone.app.multilingual.interfaces.IPloneAppMultilingualInstalled
"/>
Here is the code that creates a site with the above structure in a setuphandler. The generic setup profile is configured to use two languages and has dependencies on profile-collective.lineage:default
and profile-plone.app.multilingual:default
.
# -*- coding: utf-8 -*-
from Acquisition import aq_base
from collective.lineage.utils import enable_childsite
from logging import getLogger
from plone import api
from plone.app.dexterity.behaviors.exclfromnav import IExcludeFromNavigation
from plone.app.layout.navigation.interfaces import INavigationRoot
from plone.app.multilingual import _
from plone.app.multilingual.browser.setup import SetupMultilingualSite
from plone.app.multilingual.dx.interfaces import IDexterityTranslatable
from plone.app.multilingual.interfaces import ITranslatable
from plone.app.multilingual.interfaces import ITranslationManager
from plone.app.multilingual.interfaces import LANGUAGE_INDEPENDENT
from plone.dexterity.interfaces import IDexterityFTI
from Products.CMFCore.utils import getToolByName
from Products.CMFPlone.interfaces import ILanguage
from Products.CMFPlone.utils import _createObjectByType
from zope.event import notify
from zope.i18n import translate
from zope.interface import alsoProvides
from zope.lifecycleevent import modified
logger = getLogger(__name__)
def post_install(context):
"""Post install script"""
# Do something at the end of the installation of this package.
portal = api.portal.get()
_delete_example_content(portal)
setupTool = SetupMultilingualSite()
setupTool.setupSite(portal)
subsite_setup = SetupMultilingualSubsite()
subsite_1 = api.content.create(
container=portal,
type='Folder',
id='subsite_1',
title='Subsite 1',
)
enable_childsite(subsite_1)
subsite_setup.setupSubsite(subsite_1)
subsite_2 = api.content.create(
container=portal,
type='Folder',
id='subsite_2',
title='Subsite 2',
)
enable_childsite(subsite_2)
subsite_setup.setupSubsite(subsite_2)
subsite_3 = api.content.create(
container=portal,
type='Folder',
id='subsite_3',
title='Subsite 3 ',
)
enable_childsite(subsite_3)
subsite_setup.setupSubsite(subsite_3)
def _delete_example_content(portal):
"""Remove default content."""
to_delete = ['front-page', 'news', 'events', 'Members']
for item in to_delete:
if item in portal:
portal.manage_delObjects([item])
logger.info('Removed default content.')
class SetupMultilingualSubsite(object):
# portal_type that is added as root language folder
folder_type = 'LRF'
def __init__(self, context=None):
self.context = context
self.folders = {}
self.languages = []
self.defaultLanguage = None
def setupSubsite(self, context, forceOneLanguage=False):
self.context = context
self.folders = {}
language_tool = getToolByName(self.context, 'portal_languages')
self.languages = languages = language_tool.getSupportedLanguages()
self.defaultLanguage = language_tool.getDefaultLanguage()
if len(languages) == 1 and not forceOneLanguage:
return u'Only one supported language configured.'
doneSomething = False
available = language_tool.getAvailableLanguages()
for language in languages:
info = available[language]
name = info.get('native', info.get('name'))
doneSomething += self.setUpLanguage(language, name)
doneSomething += self.linkTranslations()
doneSomething += self.setupLanguageSwitcher()
if not doneSomething:
return u'Nothing done.'
else:
return u"Setup of language root folders on Plone site '%s'" % (
self.context.getId())
def linkTranslations(self):
"""Links the translations of the default language Folders
"""
doneSomething = False
try:
canonical = ITranslationManager(self.folders[self.defaultLanguage])
except TypeError as e:
raise TypeError(str(e) + u' Are your folders ITranslatable?')
for language in self.languages:
if language == self.defaultLanguage:
continue
if not canonical.has_translation(language):
language_folder = self.folders[language]
canonical.register_translation(language, language_folder)
doneSomething = True
if doneSomething:
logger.info(u'Translations linked.')
return doneSomething
def setUpLanguage(self, code, name):
"""Create the language folders in the subsite
"""
doneSomething = False
if code == 'id':
folderId = 'id-id'
else:
folderId = str(code)
folder = getattr(aq_base(self.context), folderId, None)
wftool = getToolByName(self.context, 'portal_workflow')
assets_folder_title = translate(_('assets_folder_title',
default=u'Assets'),
domain='plone',
target_language=folderId)
if folder is None:
_createObjectByType(self.folder_type, self.context, folderId)
folder = self.context[folderId]
ILanguage(folder).set_language(code)
folder.setTitle(name)
# This assumes a direct 'publish' transition from the initial state
# We are going to check if its private and has publish action for
# the out of the box case otherwise don't do anything
state = wftool.getInfoFor(folder, 'review_state', None)
available_transitions = [t['id'] for t in
wftool.getTransitionsFor(folder)]
if state != 'published' and 'publish' in available_transitions:
wftool.doActionFor(folder, 'publish')
# Exclude folder from navigation (if applicable)
adapter = IExcludeFromNavigation(folder, None)
if adapter is not None:
adapter.exclude_from_nav = True
# We've modified the object; reindex.
notify(modified(folder))
doneSomething = True
logger.info(u"Added '%s' folder: %s" % (code, folderId))
self.folders[code] = folder
if not INavigationRoot.providedBy(folder):
alsoProvides(folder, INavigationRoot)
doneSomething = True
logger.info(u"INavigationRoot setup on folder '%s'" % code)
return doneSomething
def setupLanguageSwitcher(self):
doneSomething = False
if self.context.getLayout() != '@@language-switcher':
self.context.setLayout('@@language-switcher')
self.context.reindexObject()
logger.info(u'Language switcher set up for subsite.')
doneSomething = True
return doneSomething
I will probably create a custom dexterity type that always is a subsite and run the above code from a event-handler or a action.