Has anyone used plone.app.multilingual with collective.lineage at the same Plone Site?

collective.lineage expects a folder to be used as a "Subsite". plone.app.multilingual makes the site able to have content translations.

In theory, to use both plugins, you would need to have a structure like this:

Plone (SiteRoot)
├── subsite1 (normal folder, subsite activated by collective.lineage)
│   ├── en (RLF from plone.app.multilingual)
│   └── es (RLF from plone.app.multilingual)
└── subsite2 (normal folder, subsite activated by collective.lineage)
    ├── en (RLF from plone.app.multilingual)
    └── es (RLF from plone.app.multilingual)

But plone.app.multilingual creates all language folders at website root. In a plone.app.multilingual scenario, it doesn't make sense to make en as a subsite.

Has anyone used both plugins in combination?

I tried it and it does no work. I think with some effort it is possible to make it work. I would love to see this working, but currently I do not have a contract with this requirement in scope...

1 Like

Back in Bristol Plone Conference I asked this exact issue in a talk about lineage, but got no answer.

We have managed to do this sort of things marking the subsites just below the LRFs:

The con is that you need to create the root LRF for the language of the subsite nevertheless you do not need it on the top of the site.

You can have different language configurations for the subsites using lineage.registry but I accept that this is not the perfect solution.

I would say that it will require some work to make it work with p.a.multilingual because of the nature of the LRFs and also the LIF (the 'media' or 'assets' folder), but I would also say that it's feasible.

I remember that on passed times of Localizer we just added a new Localizer instance in the subsite folder and configure there the different language settings. The power (?) of Acquisition :wink:

Here we would need something like this, possibly using lineage.registry, but I thing that it would require also changes in p.a.multilingual.

Anyway we haven't had such a requirement yet (they have managed with the above-mentioned solution) and haven't evaluated the cost of developing it.

1 Like

Thanks for the inputs. Indeed is a challenge.

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:

  1. 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.

  2. 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.

4 Likes

I had a rough look some time ago what is needed to modify PAM/lineage and decided it may take up to 3 weeks, because there are so many side effect (collection, search, ...) . Since I had no customer paying this time this was not an option.

This is possible. But the other way around (multiple sites with then each its own URL) does not work easily.

Completely unrelated, but I mention it since someone might run into this issue:

When making theme fragments (and probably other templates), the following did not work in multilingual setups when I wanted a folder for images (to show as a slider):

<field name="linked_folder" type="zope.schema.Choice">
      <description>Choose Folder </description>
      <source>collective.themefragments.tiles.CatalogSource</source>
 </field>

I had to make my own vocabulary, something like:

folders = api.content.find(portal_type=['Folder', 'Collection'])

if folders:
    terms = [ SimpleTerm(value=folder.UID, token=folder.UID, title=format_title(folder)) for folder in folders ]
return SimpleVocabulary(terms)

I do not remember what the problem was, but I remember it took me some time to sort it out.

Are you talking about the assets-folder (LIF) or language-folder(LRF) within subsites? My code above seems to work but I only wrote it yesterday and will still have to find out if there are any downsides.

1 Like

It's not about making it just work with LIF and LRF. I think that's the easy part. I talk about the side effects coming in using full featured lineage sites, with lineage.registry, lineage.controlpanels. Then collective.rooter and search integrations in both worlds. Next fullfeatured pam-sites with integration like plone.app.multilingualindexes - so all the nice stuff we wrote and need.
Making this work together is some effort.