Language functions

Description

Accessing and changing the language state of Plone programmatically.

Introduction

Each page view has a language associated with it.

The active language is negotiated by the plone.i18n.negotiator module. Several factors may be involved in determining what the language should be:

  • Cookies (setting from the language selector)
  • The top-level domain name (e.g. .fi for Finnish, .se for Swedish)
  • Context (current content) language
  • Browser language headers

Language is negotiated at the beginning of the page view.

Languages are managed by portal_languagetool.

Getting the current language

Example view/viewlet method of getting the current language.

from Products.Five.browser import BrowserView
from zope.component import getMultiAdapter

class MyView(BrowserView):

    ...

    def language(self):
        """
        @return: Two-letter string, the active language code
        """
        context = self.context.aq_inner
        portal_state = getMultiAdapter((context, self.request), name=u'plone_portal_state')
        current_language = portal_state.language()
        return current_language

Getting language of content item

All content objects don't necessarily support the Language() look-up defined by the IDublinCore interface. Below is the safe way to extract the served language on the content.

Example BrowserView method:

from Acquisition import aq_inner

def language(self):
    """ Get the language of the context.

    Useful in producing <html> tag.
    You need to output language for every HTML page, see http://www.w3.org/TR/xhtml1/#strict

    @return: The two letter language code of the current content.
    """
    portal_state = self.context.unrestrictedTraverse("@@plone_portal_state")

    return aq_inner(self.context).Language() or portal_state.default_language()

Getting available site languages

Example below:

# Python 2.6 compatible ordered dict
# NOTE: API is not 1:1, but for normal dict access of
# set member, iterate keys and values this is enough
try:
    from collections import OrderedDict
except ImportError:
    from odict import odict as OrderedDict

def getLanguages(self):
    """
    Return list of active langauges as ordered dictionary, the preferred first language as the first.

    Example output::

         {
            u'fi': {u'id' : u'fi', u'flag': u'/++resource++country-flags/fi.gif', u'name': u'Finnish', u'native': u'Suomi'},
            u'de': {u'id' : u'de', u'flag': u'/++resource++country-flags/de.gif', u'name': u'German', u'native': u'Deutsch'},
            u'en': {u'id' : u'en', u'flag': u'/++resource++country-flags/gb.gif', u'name': u'English', u'native': u'English'},
            u'ru': {u'id' : u'ru', u'flag': u'/++resource++country-flags/ru.gif', u'name': u'Russian', u'native': u'\u0420\u0443\u0441\u0441\u043a\u0438\u0439'}
          }
    """
    result = OrderedDict()

    portal_languages = self.context.portal_languages

    # Get barebone language listing from portal_languages tool
    langs = portal_languages.getAvailableLanguages()

    preferred = portal_languages.getPreferredLanguage()

    # Preferred first
    for lang, data in langs.items():
        if lang == preferred:
            result[lang] = data

    # Then other languages
    for lang, data in langs.items():
        if lang != preferred:
            result[lang] = data

    # For convenience, include the language ISO code in the export,
    # so it is easier to iterate data in the templates
    for lang, data in result.items():
        data["id"] = lang

    return result

Simple language conditions in page templates

You can do this if full translation strings are not worth the trouble:

<div class="main-text">
  <a tal:condition="python:context.restrictedTraverse('@@plone_portal_state').language() == 'fi'" href="http://www.saariselka.fi/sisalto?force-web">Siirry täydelle web-sivustolle</a>
  <a tal:condition="python:context.restrictedTraverse('@@plone_portal_state').language() != 'fi'" href="http://www.saariselka.fi/sisalto?force-web">Go to full website</a>
</div>

Set site language settings

Manually:

# Setup site language settings
portal = context.getSite()
ltool = portal.portal_languages
defaultLanguage = 'en'
supportedLanguages = ['en','es']
ltool.manage_setLanguageSettings(defaultLanguage, supportedLanguages,
                                      setUseCombinedLanguageCodes=False)

For unit testing, you need to run this in afterSetUp() after setting up the languages:

# THIS IS FOR UNIT TESTING ONLY
# Normally called by pretraverse hook,
# but must be called manually for the unit tests
# Goes only for the current request
ltool.setLanguageBindings()

Using GenericSetup and propertiestool.xml

<object name="portal_properties" meta_type="Plone Properties Tool">
   <object name="site_properties" meta_type="Plone Property Sheet">
      <property name="default_language" type="string">en</property>
   </object>
</object>

On LinguaPlone-enabled sites, using GenericSetup XML portal_languages.xml

<?xml version="1.0"?>
<object>
 <default_language value="fi"/>
 <use_path_negotiation value="False"/>
 <use_cookie_negotiation value="True"/>
 <use_request_negotiation value="False"/>
 <use_cctld_negotiation value="False"/>
 <use_combined_language_codes value="False"/>
 <display_flags value="True"/>
 <start_neutral value="False"/>
 <supported_langs>
  <element value="en"/>
  <element value="fi"/>
 </supported_langs>
</object>

Customizing language selector

Multilingual Plone has two kinds of language selector viewlets:

  • Plone vanilla
  • LinguaPlone - LinguaPlone has its own language selector which replaces the default Plone selector if the add on product is installed.

More information

Making language flags point to different top level domains

If you use multiple domain names for different languages it is often desirable to make the language selector point to a different domain. Search engines do not really like the dynamic language switchers and will index switching links, messing up your site search results.

Example

<tal:language
    tal:define="available view/available;
                languages view/languages;
                showFlags view/showFlags;">


    <ul id="portal-languageselector"
        tal:condition="python:available and len(languages)>=2">
        <tal:language repeat="lang languages">
        <li tal:define="code lang/code;
                        selected lang/selected"
            tal:attributes="class python: selected and 'currentLanguage' or '';">

                <a href=""
                   tal:condition="python:code =='fi'"
                   tal:define="flag lang/flag|nothing;
                               name lang/name"
                   tal:attributes="href string:http://www.twinapex.fi;
                                   title name">
                    <tal:flag condition="python:showFlags and flag">
                        <img
                             width="14"
                             height="11"
                             alt=""
                             tal:attributes="src string:${view/portal_url}${flag};
                                             title python: name;
                                             class python: selected and 'currentItem' or '';" />
                    </tal:flag>
                    <tal:nonflag condition="python:not showFlags or not flag"
                                 replace="name">language name</tal:nonflag>
                </a>

                <a href=""
                   tal:condition="python:code =='en'"
                   tal:define="flag lang/flag|nothing;
                               name lang/name"
                   tal:attributes="href string:http://www.twinapex.com;
                                   title name">
                    <tal:flag condition="python:showFlags and flag">
                        <img
                             width="14"
                             height="11"
                             alt=""
                             tal:attributes="src string:${view/portal_url}${flag};
                                             title python: name;
                                             class python: selected and 'currentItem' or '';" />
                    </tal:flag>
                    <tal:nonflag condition="python:not showFlags or not flag"
                                 replace="name">language name</tal:nonflag>
                </a>&nbsp;

        </li>
        </tal:language>
    </ul>
</tal:language>

Custom language negotiator

Below some example code.

languages.py:

""" Custom language negotiator based on hostname.
"""

from Products.PloneLanguageTool import LanguageTool

# These are default languages available when hostname cannot be solved
all_languages = [ "fi", "en" ]

def get_host_name(request):
    """ Extract host name in virtual host safe manner

    @param request: HTTPRequest object, assumed contains environ dictionary

    @return: Host DNS name, as requested by client. Lowercased, no port part.
    """

    if "HTTP_X_FORWARDED_HOST" in request.environ:
        # Virtual host
        host = request.environ["HTTP_X_FORWARDED_HOST"]
    elif "HTTP_HOST" in request.environ:
        # Direct client request
        host = request.environ["HTTP_HOST"]
    else:
        host = None
        return host

    # separate to domain name and port sections
    host=host.split(":")[0].lower()

    return host


def get_language(domain_name):
    """
    @param domain_name: Full qualified domain name of HTTP request
    """

    if domain_name.endswith(".mobi") or domain_name.endswith(".com"):
        return "en"
    elif domain_name.endswith(".fi"):
        return "fi"
    else:
        return "en"

def getCcTLDLanguages(self):
    """
    Monkey-patched top level domain language negotiator.

    This will be installed by collective.monkeypatcher.
    """

    if not hasattr(self, 'REQUEST'):
        return None

    request = self.REQUEST

    # Could not extract hostname
    hostname = get_host_name(request)

    if not hostname:
        return all_languages

    # Limit available languages based on hostname
    langs = [ get_language(hostname) ]

    return langs

# Also we need to fix a bug present in Plone 3.3.5
#
#    @memoize
#    def language(self):
#        # TODO Looking for lower-case language is wrong, the negotiator
#        # machinery uses uppercase LANGUAGE. We cannot change this as long
#        # as we don't ship with a newer PloneLanguageTool which respects
#        # the content language, though.
#        return self.request.get('language', None) or \
#                aq_inner(self.context).Language() or self.default_language()

from plone.memoize.view import memoize, memoize_contextless

def working_portal_state_language(self):
        return self.request.get('LANGUAGE', None) or \
                self.request.get('language', None) or \
                aq_inner(self.context).Language() or \
                self.default_language()

working_portal_state_language = memoize(working_portal_state_language)

configure.zcml

<!-- Use collective.monkeypatcher to introduce our custom language negotiation phase -->
<monkey:patch
      description="Add custom TLD language resolution"
      class="Products.PloneLanguageTool.LanguageTool"
      original="getCcTLDLanguages"
      replacement=".languages.getCcTLDLanguages"
      />

<monkey:patch
      description="Fix Plone 3.3.5 bug"
      class="plone.app.layout.globals.portal.PortalState"
      original="language"
      replacement=".languages.working_portal_state_language"
      />

Login-aware language negotiation

By default, language negotiation happens before authentication. Therefore, if you wish to use authenticated credentials in the negotiation, you can do the following.

Hook the after-traversal event.

Example event registration

<configure
    xmlns="http://namespaces.zope.org/zope"
    xmlns:browser="http://namespaces.zope.org/browser"
    xmlns:zcml="http://namespaces.zope.org/zcml"
    >
    <subscriber handler=".language_negotiation.Negotiator"/>
</configure>

Corresponding event handler:

from zope.interface import Interface
from zope.component import adapter
from ZPublisher.interfaces import IPubEvent,IPubAfterTraversal
from Products.CMFCore.utils import getToolByName
from AccessControl import getSecurityManager
from zope.app.component.hooks import getSite

@adapter(IPubAfterTraversal)
def Negotiator(event):

    # Keep the current request language (negotiated on portal_languages)
    # untouched

    site = getSite()
    ms = getToolByName(site, 'portal_membership')
    member = ms.getAuthenticatedMember()
    if member.getUserName() == 'Anonymous User':
        return

    language = member.language
    if language:
        # Fake new language for all authenticated users
        event.request['LANGUAGE'] = language
        event.request.LANGUAGE_TOOL.LANGUAGE = language
    else:
        lt = getToolByName(site, 'portal_languages')
        event.request['LANGUAGE'] = lt.getDefaultLanguage()
        event.request.LANGUAGE_TOOL.LANGUAGE = lt.getDefaultLanguage()