Translating text strings¶
Description
Translating Python and TAL template source code text strings using the term:gettext framework and other Plone/Zope term:i18n facilities.
Introduction¶
Internationalization is a process to make your code locale- and language-aware. Usually this means supplying translation files for text strings used in the code.
Plone internally uses the UNIX standard term:gettext tool to perform term:i18n.
There are two separate gettext systems. Both use the .po file format to describe translations.
Note that this chapter concerns only code-level translations. Content translations are managed by the Products.LinguaPlone add-on product.
zope.i18n¶
- Follows term:gettext best practices
-
Translations are stored in the
locales
folder of your application. Example:locales/fi/LC_MESSAGES/your.app.po
- Has zope.i18nmessageid package, which provides a string-like class which allows storing the translation domain with translatable text strings easily.
-
.po
files must usually be manually converted to.mo
binary files every time the translations are updated. See i18ndude. (It is also possible to set an environment variable to trigger recompilation of.mo
files; see below.)
Plone (at least 3.3) uses only filename and path to search
for the translation files. Information in the
.po
file headers is ignored.
Generating a
.pot
template file for your package(s)¶
infrae.i18nextract can be used in your buildout to create a script which searches particular packages for translation strings. This can be particularly useful for creating a single translations package which contains the translations for the set of packages which make up your application.
Add the following to your
buildout.cfg
:
[translation]
recipe = infrae.i18nextract
packages =
myapplication.policy
myapplication .theme
output = ${buildout:directory}/src/myapplication.translation/myapplication/translation/locales
output-package = myapplication.translations
domain = mypackage
Running the
./bin/translation-extract
script will produce a
.pot
file in the specified output directory which can then be
used to create the
.po
files for each translation:
msginit --locale=fr --input=locales/mypackage.pot --output=locales/fr/LC_MESSAGES/mypackage.po
The
locales
directory should contain a directory for each language,
and a directory called
LC_MESSAGES
within each of these, followed by the corresponding
.po
files containing the translation strings:
./locales/en/LC_MESSAGES/mypackage.po
./locales/fi/LC_MESSAGES/mypackage.po
./locales/ga/LC_MESSAGES/mypackage.po
Marking translatable strings in Python¶
Each module declares its own
MessageFactory
which is a callable and marks strings with translation
domain.
MessageFactory
is declared in the main
__init__.py
file of your package.
from zope.i18nmessageid import MessageFactory
# your.app.package must match domain declaration in .po files
MessageFactory = MessageFactory('youpackage.name')
You also need to have the following ZCML entry:
<configure xmlns:i18n="http://namespaces.zope.org/i18n">
<i18n:registerTranslations directory="locales" />
</configure>
After the setup above you can use message factory to
mark strings with translation domains.
i18ndude
translation utilities use underscore
_
to mark translatable strings (term:gettext
message ids). Message ids must be unicode strings.
from your.app.package import yourAppMessageFactory as _
my_translatable_text = _(u"My text")
The object will still look like a string:
>>> my_translatable_text
u'My text'
But in reality it is a
zope.i18nmessageid.message.Message
object:
>>> my_translatable_text.__class__
<type 'zope.i18nmessageid.message.Message'>
>>> my_translatable_text.domain
'your.app.package'
To see the translation:
>>> from zope.i18n import translate
>>> translate(my_translatable_text)
u"The text of the translation." # This is the corresponding msgstr from the .po file
Marking translatable strings in TAL page templates¶
Declare XML namespace
i18n
and translation domain at the beginning of your
template, at the first element
<div id="mobile-header" xmlns:i18n="http://xml.zope.org/namespaces/i18n" i18n:domain="plomobile">
Translate element content text using
i18n:translate=""
. It will use the text content of the element as msgid.
<li class="heading" i18n:translate="">
Sections
</li>
- Use attributes i18n:translate, i18n:attributes and so on
For examples look any core Plone .pt files
Automatically translated message ids¶
Plone will automatically perform translation for message ids which are output in page templates.
The following code would translate
my_translateable_text
to the native language activated for the current page.
<span tal:content="view/my_translateable_text">
Note
Since
my_translateable_text
is a
zope.i18nmessageid.message.Message
instance containing its own gettext domain
information, the
i18n:domain
attribute in page templates does not affect message
ids declared through message factories.
Manually translated message ids¶
If you need to manipulate translated text outside page templates, you need to perform the final translation manually.
Translation always needs context (i.e. under which site the translation happens), as the active language and other preferences are read from the HTTP request object and site object settings.
Translation can be performed using the
context.translate()
method:
# Translate some text
msgid = _(u"My text") # my_text is zope.
# Use inherited translate() function to get the final text string
translated = self.context.translate(msgid)
# translated is now u"Käännetty teksti" (in Finnish)
context.translate()
uses the
translate.py
Python script from
LanguageTool
.
It has the signature:
def translate(self, domain, msgid, mapping=None, context=None,
target_language=None, default=None):
and does the trick:
from Products.CMFCore.utils import getToolByName
# get tool
tool = getToolByName(context, 'translation_service')
# this returns type unicode
value = tool.translate(msgid,
domain,
mapping,
context=context,
target_language=target_language,
default=default)
Note
Translation needs HTTP request object and thus may not work correctly from command-line scripts.
Non-python message ids¶
There are also other message id markers in code outside the Python domain, that have their own mechanisms:
- ZCML entries
- GenericSetup XML
- TAL page templates
Translating browser view names¶
Often you might want to translate browser view names, so that the "Display" contentmenu shows something more human readable than, for example, "my_awesome_view".
These are the steps needed to get it translated:
- Use the "plone" domain for your browser view name translations. Wether put the whole ZCML in the plone domain of just the view definitions with i18n:domain="plone".
- The msgids for the views are their names. Translate them in a plone.po override file in your locales folder.
Please note, i18ndude does not parse the zcml files for translation strings (see below "Translating other ZCML").
Testing translations¶
Here is a simple way to check if your gettext domains are correctly loaded.
Plone 4¶
You can start the Plone debug shell and manually check if translations can be performed.
First start Plone in debug shell:
bin/instance debug
and then call translation service, in your site, manually:
>>> site = app.yoursiteid
>>> translation_service = site.translation_service
>>> translation_service.translate("Add Events Portlet", domain="plone", target_language="fi")
u'Lis\xe4\xe4 Tapahtumasovelma'
Translation string substitution¶
Translation string substitutions must be used when the final translated message contains variable strings.
Plone content classes inherit the
translate()
function which can be used to get the final translated
string. It will use the currently activate language.
Translation domain will be taken from the msgid object
itself, which is a string-like
zope.i18nmessageid
instance.
Message ids are immutable (read-only) objects so you need to always create a new message id if you use different variable substitution mappings.
Python code:
from saariselka.app import appMessageFactory as _
class SomeView(BrowserView):
def do_stuff(self):
msgid = _(u"search_results_found_msg", default=u"Found ${results} results", mapping={ u"results" : len(self.contents)})
# Use inherited translate() function to get the final text string
translated = self.context.translate(msgid)
# Show the final result count to the user as a portal status message
messages = IStatusMessage(self.request)
messages.addStatusMessage(translated, type="info")
Corresponding
.po
file entry:
#. Default: "Found ${results} results"
#: ./browser/accommondationsummaryview.py:429
msgid "search_results_found_msg"
msgstr "Löytyi ${results} majoituskohdetta"
For more information, see
PlacelessTranslationService¶
- Historic, being phased out.
-
Stores
.po
files ini18n
folder of your add-on product. - Used for main "plone" translation catalog (until Plone 3.3.x)
-
Translation files are processed when Plone is restarted.
Example:
i18n/yourapp-fi.po
.
i18ndude¶
i18ndude
is a developer-oriented command-line utility to manage
.po
and
.mo
files.
Usually you build our own shell script wrapper around
i18ndude
to automate generation of
.mo
files of your product
.po
files.
Note
Plone 3.3 and onwards do not need manual
.po
->
.mo
compilation. It is done on start up. Plone 4 has a
special switch for this: in your
buildout.cfg
in the part using
plone.recipe.zope2instance
you can set an environment variable for this:
environment-vars =
zope_i18n_compile_mo_files true
Note that the value does not matter: the code in
zope.i18n
simply looks for the existence of the variable and does
not care what its value is.
Note
If you use i18ndude make sure to use
_
as an alias for your
MessageFactory
else i18ndude won't find your message strings in python
code and report that "no entries for domain"
were found.
See:
Examples:
Installing i18ndude¶
The recommended method is to have term:i18ndude installed via your buildout.
Add the following to your buildout.cfg:
parts =
...
i18ndude
[i18ndude]
unzip = true
recipe = zc.recipe.egg
eggs = i18ndude
After this
i18ndude
is available in your
buildout/bin
folder
For Plone 3 you might need to add:
[versions]
# i18ndude pindowns for Plone 3.3
zope.i18nmessageid = 3.6.1
zope.interface = 3.8.0
bin/i18ndude -h
Usage: i18ndude command [options] [path | file1 file2 ...]]
You can also call it relative to your current package source folder
server:home moo$ cd src/mfabrik.plonezohointegration/
server:mfabrik.plonezohointegration moo$ ../../bin/i18ndude
Warning
Do not
easy_install
i18ndude
.
i18ndude
depends on various Zope packages and pulling them to
your system-wide Python configuration could be
dangerous, due to potential conflicts with
corresponding, but different versions, of the same
packages used with Plone.
More information
Setting up folder structure for Finnish and English¶
Example:
mkdir locales
mkdir locales/fi
mkdir locales/en
mkdir locales/fi/LC_MESSAGES
mkdir locales/en/LC_MESSAGES
Creating
.pot
base file¶
Example:
i18ndude rebuild-pot --pot locales/mydomain.pot --create your.app.package .
Manual
.po
entries¶
i18ndude
scans source
.py
and
.pt
files for translatable text strings. On some occasions
this is not enough - for example if you dynamically
generate message ids in your code. Entries which cannot
be detected by automatic code scan are called
manual po entries. They are managed in
locales/manual.pot
which is merged to generated
locales/yournamespace.app.pot
file.
Here is a sample
manual.pot
file:
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0\n"
"Preferred-Encodings: utf-8 latin1\n"
"Domain: mfabrik.app\n"
# This entry is used in gomobiletheme.mfabrik templates for the campaign page header
# It is not automatically picked, since it is referred from external package
#. Default: "Watch video"
msgid "watch_video"
msgstr ""
Managing
.po
files¶
Example shell script to manage i18n files. Change
CATALOGNAME
to reflect the actual package of your product:
The script will:
- pick up all changes to i18n strings in code and reflect them back to the translation catalog of each language;
-
pick up changes in
manual.pot
file and reflect them back to the translation catalog of each language.
#!/bin/sh
#
# Shell script to manage .po files.
#
# Run this file in the folder main __init__.py of product
#
# E.g. if your product is yourproduct.name
# you run this file in yourproduct.name/yourproduct/name
#
#
# Copyright 2010 mFabrik http://mfabrik.com
#
# https://plone.org/documentation/manual/plone-community-developer-documentation/i18n/localization
#
# Assume the product name is the current folder name
CURRENT_PATH=`pwd`
CATALOGNAME="yourproduct.app"
# List of languages
LANGUAGES="en fi de"
# Create locales folder structure for languages
install -d locales
for lang in $LANGUAGES; do
install -d locales/$lang/LC_MESSAGES
done
# Assume i18ndude is installed with buildout
# and this script is run under src/ folder with two nested namespaces in the package name (like mfabrik.plonezohointegration)
I18NDUDE=../../../../bin/i18ndude
if test ! -e $I18NDUDE; then
echo "You must install i18ndude with buildout"
echo "See https://github.com/collective/collective.developermanual/blob/master/source/i18n/localization.txt"
exit
fi
#
# Do we need to merge manual PO entries from a file called manual.pot.
# this option is later passed to i18ndude
#
if test -e locales/manual.pot; then
echo "Manual PO entries detected"
MERGE="--merge locales/manual.pot"
else
echo "No manual PO entries detected"
MERGE=""
fi
# Rebuild .pot
$I18NDUDE rebuild-pot --pot locales/$CATALOGNAME.pot $MERGE --create $CATALOGNAME .
# Compile po files
for lang in $(find locales -mindepth 1 -maxdepth 1 -type d); do
if test -d $lang/LC_MESSAGES; then
PO=$lang/LC_MESSAGES/${CATALOGNAME}.po
# Create po file if not exists
touch $PO
# Sync po file
echo "Syncing $PO"
$I18NDUDE sync --pot locales/$CATALOGNAME.pot $PO
# Plone 3.3 and onwards do not need manual .po -> .mo compilation,
# but it will happen on start up if you have
# registered the locales directory in ZCML
# For more info see http://vincentfretin.ecreall.com/articles/my-translation-doesnt-show-up-in-plone-4
# Compile .po to .mo
# MO=$lang/LC_MESSAGES/${CATALOGNAME}.mo
# echo "Compiling $MO"
# msgfmt -o $MO $lang/LC_MESSAGES/${CATALOGNAME}.po
fi
done
Note
Remember to register the
locales
directory in
configure.zcml
for automatic
.mo
compilation as instructed above.
More information
Distributing compiled translations¶
The rule for compiled .mo files is that
- Source code repositories (SVN, Git) must not contain compiled .mo files
- Released eggs on PyPi, however, must contain compiled .mo files
The easiest way to manage this is to use zest.releaser tool together with zest.pocompile package to release your eggs.
Dynamic content¶
If your HTML template contains dynamic content such as
<h1 i18n:translate="search_form_heading">Search from <span tal:content="context/@@plone_portal_state/portal_title" /></h1>
it will produce
.po
entry:
msgstr "Hae sivustolta <span>${DYNAMIC_CONTENT}</span>"
You need to give the name to the dynamic part
<h1 i18n:translate="search_form_heading">
Search from
<span i18n:name="site_title"
tal:content="context/@@plone_portal_state/portal_title" /></h1>
... and then you can refer the dynamic part by a name:
#. Default: "Search from <span>${site_title}</span>"
#: ./skins/gomobiletheme_basic/search.pt:46
#: ./skins/gomobiletheme_plone3/search.pt:46
msgid "search_form_heading"
msgstr "Hae sivustolta ${site_title}
More info
Overriding translations¶
If you need to change a translation from a
.po
file, you could create a new python package and register
your own
.po
files.
To do this, create the package and add a
locales
directory in there, along the lines of what
plone.app.locales
does. Then you can add your own translations in the
language that you need; for example
locales/fr/LC_MESSAGES/plone.po
to override French messages in the
plone
domain.
Reference the translation in
configure.zcml
of your package:
<configure xmlns:i18n="http://namespaces.zope.org/i18n"
i18n_domain="my.package">
<i18n:registerTranslations directory="locales" />
</configure>
Your ZCML needs to be included before the one from plone.app.locales: the first translation of a msgid wins. To manage this, you can include the ZCML in the buildout:
[instance]
recipe = plone.recipe.zope2instance
user = admin:admin
http-address = 8280
eggs =
Plone
my.package
${buildout:eggs}
environment-vars =
zope_i18n_compile_mo_files true
# my.package is needed here so its configure.zcml
# is loaded before plone.app.locales
zcml = my.package
See the Overriding Translations section of Maurits van Rees's blog entry on Plone i18n, and Vincent Fretin's posting on the Plone-Users mailing list.
Other¶
- http://grok.zope.org/documentation/how-to/how-to-internationalize-your-application
- http://reinout.vanrees.org/weblog/2007/12/14/translating-schemata-names.html
- https://plone.org/products/archgenxml/documentation/how-to/handling-i18n-translation-files-with-archgenxml-and-i18ndude/view?searchterm=
- http://vincentfretin.ecreall.com/articles/my-translation-doesnt-show-up-in-plone-4
- http://dev.plone.org/plone/ticket/9089