Attention
TYPO3 v9 has reached its end-of-life September 30th, 2021 and is not maintained by the community anymore. Looking for a stable version? Use the version switch on the top left.
You can order Extended Long Term Support (ELTS) here: TYPO3 ELTS.
Localizing and Internationalizing an Extension¶
Particularly in business relationships there is often the need to build a website in more than one language. Therefore not only does the translation of the websites content need to be completed, but also the extensions which are used must also be available in multiple languages.
The configuration options for localization inside TYPO3 are versatile. You will find a comprehensive description of all concepts and options in the Frontend Localization Guide (https://docs.typo3.org/typo3cms/FrontendLocalizationGuide/). For the following sections we assume a correct configuration of the localization, which is normally done in the site configuration.
The selection of the frontend language is carried out with a parameter
in the URL (linkVars = L
). Important is the definition of the
UID of the language (sys_language_uid = 0
) and the language key
of the language (language = default
). When the URL of the
website contains the parameter L=1
the output occurs in german,
if the parameter is not set the output of the website occurs in the default
language (in our example in english).
In the next section, we start with the translation of static text like captions of links which appear in templates. After this we go to translate the content of extensions, thus the domain objects. Finally we explain how you can adjust the date formats in accordance with the date conventions in the particular country.
Multi Language Templates¶
When you style the output of your extension using Fluid, you often have to localize particular terms or maybe short text in the templates. In the following sample template of the blog example which displays a single blog post with its comments there are some constant terms:
<h3>{post.title}</h3>
<p>By: {post.author.fullName}</p>
<p>{post.content -> f:format.nl2br()}</p>
<h3>Comments</h3>
<f:for each="{post.comments}" as="comment">
{comment.content -> f:format.nl2br()}
<hr>
</f:for>
Tip
The template is a little bit simplified and reduced to the basic.
First of all the text "By:" in front of the author of the post is hard coded in the template, as well as the caption "Comments". For the use of the extension on an English website this is no problem but if you want to use it on a German website, the texts "By" and "Comments" would be displayed instead of "Von" and "Kommentare". To make such text exchangeable it has to be removed from the template and inserted in a language file. Every text which is to be translated is given an identifier that can be inserted in the template later. Table 9-1 shows the identifier used in the sample and their translations into german and english.
Table 9-1: The texts how we want to translate them
Identifier |
English |
German |
---|---|---|
author_prefix |
By: |
Von: |
comment_header |
Comments |
Kommentare |
In TYPO3 (and also in Extbase) the language file, in which the
translated terms are stored, is named locallang.xlf
.
It should contain all terms that have to be translated, in our example
"By:" and "Comments", and their translations. Using Extbase the the file
locallang.xlf
must reside in the folder
Resources/Private/Language/
. To localize the above
terms we create the locallang.xlf
file the following
way:
<?xml version="1.0" encoding="UTF-8"?>
<xliff version="1.0" xmlns="urn:oasis:names:tc:xliff:document:1.1">
<file source-language="en" datatype="plaintext" original="messages" date="2011-10-18T18:20:51Z" product-name="my-ext">
<header/>
<body>
<trans-unit id="author_prefix">
<source>By:</source>
</trans-unit>
<trans-unit id="comment_header">
<source>Comments</source>
</trans-unit>
</body>
</file>
</xliff>
Tip
The TYPO3 Core API describes in detail the construction of the
locallang.xlf
file
(https://docs.typo3.org/typo3cms/CoreApiReference/ApiOverview/Internationalization/XliffFormat.html).
Now the placeholder for the translated terms must be inserted into
the template. To do this, Fluid offers the ViewHelper
f:translate
. In this ViewHelper you give the identifier of
the term to be inserted as argument key
and the ViewHelper
inserts either the german or the english translation according to the
current language selection
<f:translate key="comment_header" />
<!-- or -->
{f:translate(key: 'comment_header')}
Tip
The used language is defined in the TypoScript template of the
website. By default the english texts are used; but when with setting of
the TypoScript setting config.language = de
you can set the
used language to german for example.
To implement a language selection normally TypoScript conditions
are used. These are comparable with an if/else
block
[globalVar = GP:L = 1]
config.language = de
[else]
config.language = default
[end]
When the URL of the website contains a parameter L=1, then the output is in German; if the parameter is not set the output is in the default language English.
With the use of complex TypoScript conditions the language selection could be set to depend on the forwarded language of the browser.
By replacing all terms of the template with the
translate
ViewHelper we could fit the output of the extension
to the currently selected language. Here we have a look at the Fluid
template for the output of the blog posts, now without the hardcoded
english terms:
<h3>{post.title}</h3>
<p><f:translate key="author_prefix"> {post.author.fullName}</p>
<p>{post.content -> f:format.nl2br()}</p>
<h3><f:translate key="comment_header"></h3>
<f:for each="{post.comments}" as="comment">
{comment.content -> f:format.nl2br()}
<hr>
</f:for>
Tip
Sometimes you have to localize a string in the PHP code, for
example inside of a controller or a ViewHelper. In that case you
can use the static method
\TYPO3\CMS\Extbase\Utility\LocalizationUtility::translate($key, $extensionName)
.
This method requires the localization key as the first and the name of the extension as the second
parameter. Then the corresponding text in the current language will be loaded from this extension's
locallang.xlf
file .
Output Localized Strings Using sprintf
¶
In the above example we have outputted the name of the blog post
author simply by using {blog.author.fullName}
. Many
languages have special rules on how names are to be used - especially in
Thailand it is common to only show the first name and place the word
"Khan" in front of it (which is a polite form). We want to enhance our
template now as far as it can to output the name of the blog author
according to the current language. In German and English this is the
form "first name last name" and in Thai "Khan first name".
Also for this use cases the translate
ViewHelper can
be used. With the aid of the array arguments,
values can be
embedded into the translated string. To do this, the syntax of the PHP
function sprintf
is used.
If we want to implement the above example, we must assign the
first name and the last name of the blog author separate to the
translate
ViewHelper:
<f:translate key="name" arguments="{1:post.author.firstName, 2: post.author.lastName}" />
How should the corresponding string in the
locallang.xml
file looks like? It describes on
which position the placeholder are to be inserted. For English and
German it looks like this:
<label index="name">%1$s %2$s</label>
Important are the placeholder strings %1$s
and
%2$s
. These will be replaced with the assigned parameters.
Every placeholder starts with the % sign, followed by the position
number inside the arguments array, starting with 1, followed by the $
sign. After that the usual formatting specifications follows - in the
example it is the data type string (s)
. Now we can define
for Thai, that "Khan" followed by the first name should be
output:
<label index="name">Khan %1$s</label>
Tip
The keys in the arguments array of the ViewHelper have no relevance. We recommend to give them numbers like the positions (starting with 1), because it is easy understandable.
Tip
For a full reference of the formatting options for
sprintf
you should have a look at the PHP documentation:
http://php.net/manual/de/function.sprintf.php.
Changing Localized Terms Using TypoScript¶
If you use an existing extension for a customer project, you sometimes find out that the extension is insufficient translated or that the translations have to be adjusted. TYPO3 offers the possibility to overwrite the localization of a term by TypoScript. Fluid also support this.
If, for example, you want use the text "Remarks" instead of the
text "Comments", you have to overwrite the identifier
comment_header
for the English language. For this you can
add following line to your TypoScript template:
plugin.tx_blogexample._LOCAL_LANG.default.comment_header = Remarks
With this you will overwrite the localization of the term
comment_header
for the default language in the blog
example. So you can adjust the translation of the texts like you wish,
without changing the locallang.xml
file.
Until now we have shown how to translate static text of templates. Of course it is important that also the data of an extension is translated according to the national language. We will show this in the next section.
Multi Language Domain Objects¶
With TYPO3 you can localize the data sets in the backend. This also
applies to domain data, because they are treated like "normal" data sets
in the TYPO3 backend. To make your domain objects translatable you have
to create additional fields in the database and tell TYPO3 about them. The
class definitions must not be changed. Lets have a look at the required
steps based on the blog
class of the blog example. TYPO3
needs three additional database fields which you should insert in the
ext_tables.sql
file:
CREATE TABLE tx_blogexample_domain_model_blog {
// ...
sys_language_uid int(11) DEFAULT '0' NOT NULL,
l10n_parent int(11) DEFAULT '0' NOT NULL,
l10n_diffsource mediumblob NOT NULL,
// ...
};
You are free to choose the names of the database fields, but the
names we use here are common in the world of TYPO3. In any case you have
to tell TYPO3 which name you have chosen. This is done in the ctrl
section of the TCA configuration file
Configuration/TCA/tx_blogexample_domain_model_blog.php
<?php
return [
'ctrl' => [
// ...
'languageField' => 'sys_language_uid',
'transOrigPointerField' => 'l10n_parent',
'transOrigDiffSourceField' => 'l10n_diffsource',
// ...
]
];
The field sys_language_uid
is used for storing
the UID of the language in which the blog is written. Based on this UID
Extbase choose the right translation depending on the current
TypoScript setting in config.sys_language_uid
. In the field
l10n_parent
the UID of the original blog created in the
default language, which the current blog is a translation of. The third
field l10n_diffsource
contains a snapshot of the source of
the translation. This snapshot is used in the backend for creation of a
differential view and is not used by Extbase.
In the section columns
of the TCA
you have
to configure the fields accordingly. The following configuration adds two
fields to the backend form of the blog: one field for the editor to define
the language of a data record and one field to select the data record the
translation relates to.
<?php
return [
// ...
'types' => [
'1' => ['showitem' => 'l18n_parent , sys_language_uid, hidden, title,
description, logo, posts, administrator'],
],
'columns' => [
'sys_language_uid' => [
'exclude' => 1,
'label' => 'LLL:EXT:lang/locallang_general.php:LGL.language',
'config' => [
'type' => 'select',
'foreign_table' => 'sys_language',
'foreign_table_where' => 'ORDER BY sys_language.title',
'items' => [
['LLL:EXT:lang/locallang_general.php:LGL.allLanguages',-1],
['LLL:EXT:lang/locallang_general.php:LGL.default_value',0]
],
],
],
'l18n_parent' => [
'displayCond' => 'FIELD:sys_language_uid:>:0',
'exclude' => 1,
'label' => 'LLL:EXT:lang/locallang_general.php:LGL.l18n_parent',
'config' => [
'type' => 'select',
'items' => [
['', 0],
],
'foreign_table' => 'tx_blogexample_domain_model_blog',
'foreign_table_where' => 'AND tx_blogexample_domain_model_blog.uid=###REC_FIELD_
l18n_parent### AND tx_blogexample_domain_model_blog.
sys_language_uid IN (-1,0)',
],
],
'l18n_diffsource' => [
'config' => [
'type' =>'passthrough'
],
],
// ...
],
];
With it, the localization of the domain object is already
configured. By adding &L=1
to the URL, the language of
the frontend will be changed to german. If there is an existing
translation of a blog, it will be shown. Otherwise the blog is output in
the default language.
Tip
You can control this behavior. If you set the option
config.sys_language_mode
to strict
in the
TypoScript configuration, then only these objects are shown which really
have content in the frontend language. More information for this you
will find in the Frontend Localization Guide of the
Core Documentation.
How TYPO3 handles the localization of content offers two important specific features: The first is that all translations of a data record respectively a data record that is valid for all languages (UID of the language is 0 or -1) will be "added" to the data record in the default language. The second special feature is that always the UID of the record in the default language is bound for identification although the translated data record in the database table has another UID. This conception has a serious disadvantage: If you want to create a data record for a language that has no data record in the default language, you have to create the latter before. But with what content?
Lets have an example for illustration: You create a blog in the default language English (=default). It is stored in the database like this:
uid: 7 (given by the database)
title: "My first Blog"
sys_language_uid: 0 (selected in backend)
l10n_parent: 0 (no translation original exists)
After a while you create a German translation in the backend. In the database the following record is stored:
uid: 42 (given by the database)
title: "Mein erster Blog"
sys_language_uid: 1 (selected in backend)
l10n_parent: 7 (selected in backend respectively given automatically)
A link that references the single view of a blog looks like this:
https://example.org/index.php?id=99&tx_blogexample_pi1[controller]=Blog&tx_blogexample_pi1[action]=show&tx_blogexample_pi1[blog]=7
By adding &L=1
we referencing now the German
version:
https://example.org/index.php?id=99&tx_blogexample_pi1[controller]=Blog&tx_blogexample_pi1[action]=show&tx_blogexample_pi1[blog]=7&L=1
Notice that the given UID in tx_blogexample_pi1[blog]=7 is not
changed. There is not UID of the data record of the german translation
(42). This has the advantage that only the parameter for the language
selection is enough. Concurrently it has the disadvantage of a higher
administration effort during persistence. Extbase will do this for you by
carrying the UID of the language of the domain model and the UID of the
data record in which the domain data is effectively stored as "hidden"
properties of the AbstractDomainObject
internally.
In Table 9-2 you find for different actions in the frontend the behavior
of Extbase for localized domain objects.
Table 9-2: Behavior of Extbase for localized domain objects in the frontend.
No parameter L given, or L=0 |
L=x (x>0) |
|
Display (index, list, show) |
Objects in the default language
( |
The objects are shown in the
selected language x. If an object
doesn't exist in the selected
language the object of the default
language is shown (except by
|
Editing (edit, update) |
Like displaying an object. The domain data is stored in the "translated" data record, in the above example in the record with the UID 42. |
|
Creation (new, create) |
Independent of the selected frontend language the data is stored in a
new record in whose field |
Extbase also supports all default functions of the localization of domain objects. It has its limits when a domain object should be created exclusively in a target language. Especially when no data record exists in the default language.
Localization of Date Output¶
It often occurs that a date or time must be displayed in a template.
Every language area has its own convention on how the date is to be
displayed: While in Germany the date is displayed in the form
Day.Month.Year
, in the USA the form
Month/Day/Year
is used. Depending on the language the date
must be formatted different.
Generally the date or time is formatted by the
format.date
ViewHelper:
<f:format.date date="{dateObject}" format="d.m.Y" />
<!-- or -->
{dateObject -> f:format.date(format: 'd.m.Y')}
The date object {dateObject}
is displayed with the date
format given in the parameter format
. This format string must
be in a format which is readable by the PHP function date()
and declares the format of the output. Table 9-3 shows the some important
placeholders.
Table 9-3: Some place holder of date.
Format character |
Description |
Example |
---|---|---|
d |
Day of the month as number, double-digit, with leading zero |
01 ... 31 |
m |
Month as number, with leading zero |
01 ... 12 |
Y |
Year as number, with 4 digits |
2011 |
y |
Year as number, with 2 digits |
11 |
H |
Hour in 24 hour format |
00 ... 23 |
i |
Minutes, with leading zero |
00 ... 59 |
But the ViewHelper has to be configured different. Depending on the
language area, which is controlled by the language of the user, an other
format string should be used. Here we combine the format.date
ViewHelper with the translate
ViewHelper which you got to
know in the section "Multilanguage templates"
<f:format.date date="{dateObject}" format="{f:translate(key: 'date_format')}" />
Than you can store an other format string for every language in the
locallang.xml
file and you can change the format
string via TypoScript if needed. This method to translate content you got
to know in the section "Multilanguage templates".
Tip
There are other formatting ViewHelpers for adjusting the output of
currencies or big numbers. These ViewHelpers all starts with
format
. You can find an overview of these ViewHelpers in
Appendix C. These ViewHelpers can be used like the
f:format.date
ViewHelper you have just learned.
In this section you have learned how you can translate and localize an extension. First we have worked on the localization of single terms in the template, after this we had a look at the content of the extension. Finally the customization of date information for country-specific formats where explained. In the next section you will see how constraints of the domain objects can be preserved.
TYPO3 v9 and Higher¶
Starting with version 9 extbase renders the translated records in the same way TypoScript rendering does.
The new behaviour is controlled by the Extbase feature switch consistentTranslationOverlayHandling
.
config.tx_extbase.features.consistentTranslationOverlayHandling = 1
The new behaviour is enabled by default in TYPO3 v9. The feature switch will be removed in TYPO3 v10, so there will be just one way of fetching records. You can override the setting using normal TypoScript.
Users relying on the old behaviour can disable the feature switch.
The change modifies how Extbase interprets the TypoScript settings
config.sys_language_mode
and config.sys_language_overlay
and the
Typo3QuerySettings
properties languageOverlayMode
and languageMode
.
Changes in the rendering:
Setting
Typo3QuerySettings->languageMode
does not influence how Extbase queries records anymore. The corresponding TypoScript settingconfig.sys_language_mode
is used by the core to decide what to do when a page is not translated to the given language (display 404, or try page with different language). Users who used to setTypo3QuerySettings->languageMode
tostrict
should useTypo3QuerySettings->setLanguageOverlayMode('hideNonTranslated')
to get translated records only.The old behavior was confusing, because
languageMode
had a different meaning and accepted different values in TS context and in Extbase context.Setting
Typo3QuerySettings->languageOverlayMode
totrue
makes Extbase fetch records from default language and overlay them with translated values. So e.g. when a record is hidden in the default language, it will not be shown. Also records without translation parents will not be shown. For relations, Extbase reads relations from a translated record (so it’s not possible to inherit a field value from translation source) and then passes the related records through$pageRepository->getRecordOverlay()
. So e.g. when you have a translatedtt_content
with FAL relation, Extbase will show only thosesys_file_reference
records which are connected to the translated record (not caring whether some of these files havel10n_parent
set).Previously
Typo3QuerySettings->languageOverlayMode
had no effect. Extbase always performed an overlay process on the result set.Setting
Typo3QuerySettings->languageOverlayMode
tofalse
makes Extbase fetch aggregate root records from a given language only. Extbase will follow relations (child records) as they are, without checking theirsys_language_uid
fields, and then it will pass these records through$pageRepository->getRecordOverlay()
. This way the aggregate root record's sorting and visibility doesn't depend on default language records. Moreover, the relations of a record, which are often stored using default language uids, are translated in the final result set (so overlay happens).For example: Given a translated
tt_content
having relation to 2 categories (in the mm table translated tt_content record is connected to category uid in default language), and one of the categories is translated. Extbase will return att_content
model with both categories. If you want to have just translated category shown, remove the relation in the translatedtt_content
record in the TYPO3 Backend.
Note that by default Typo3QuerySettings
uses the global TypoScript configuration like
config.sys_language_overlay
and $GLOBALS['TSFE']->sys_language_content
(calculated based on config.sys_language_uid
and config.sys_language_mode
).
So you need to change Typo3QuerySettings
manually only if your Extbase code should
behave different than other tt_content
rendering.
Setting setLanguageOverlayMode()
on a query influences only fetching of the aggregate root. Relations are always
fetched with setLanguageOverlayMode(true)
.
When querying data in translated language, and having setLanguageOverlayMode(true)
, the relations
(child objects) are overlaid even if the aggregate root is not translated.
See QueryLocalizedDataTest->queryFirst5Posts()
.
The following examples show how to query data in Extbase in different scenarios, independent of the global TS settings:
Fetch records from the language uid=1 only, with no overlays. Previously (
consistentTranslationOverlayHandling = 0
):This was not possible.
Now (
consistentTranslationOverlayHandling = 1
):
$querySettings = $query->getQuerySettings();
$querySettings->setLanguageUid(1);
$querySettings->setLanguageOverlayMode(false);
Fetch records from the language uid=1, with overlay, but hide non-translated records Previously (
consistentTranslationOverlayHandling = 0
):
$querySettings = $query->getQuerySettings();
$querySettings->setLanguageUid(1);
$querySettings->setLanguageMode('strict');
Now (:typoscript:`consistentTranslationOverlayHandling = 1`):
$querySettings = $query->getQuerySettings();
$querySettings->setLanguageUid(1);
$querySettings->setLanguageOverlayMode('hideNonTranslated');
QuerySettings property |
old behaviour |
new behaviour |
default value (TSFE|Extbase) |
---|---|---|---|
languageUid |
same |
0 |
|
respectSysLanguage |
same |
|
|
languageOverlayMode |
not used |
values: |
0 | |
languageMode |
documented values: |
not used |
|
Identifiers¶
Domain models have a main identifier uid
and two additional properties _localizedUid
and _versionedUid
.
Depending on whether the languageOverlayMode
mode is enabled (true
or 'hideNonTranslated'
) or disabled (false
),
the identifier contains different values.
When languageOverlayMode
is enabled then uid
property contains uid
value of the default language record,
the uid
of the translated record is kept in the _localizedUid
.
Context |
Record in language 0 |
Translated record |
---|---|---|
Database |
uid:2 |
uid:11, l10n_parent:2 |
Domain Object values with |
uid:2, _localizedUid:2 |
uid:2, _localizedUid:11 |
Domain Object values with |
uid:2, _localizedUid:2 |
uid:11, _localizedUid:11 |
See tests in extbase/Tests/Functional/Persistence/QueryLocalizedDataTest.php
.
The $repository->findByUid()
(or $persistenceManager->getObjectByIdentifier()
) method takes current
rendering language into account (e.g. L=1). It does not take defaultQuerySetting
set on the repository into account.
This method always performs an overlay.
Values in braces show previous behaviour (disabled flag) if different than current.
The bottom line is that with the feature flag on, you can now use findByUid()
using translated record uid to get
translated content independently from language set in global context.
L=0 |
L=1 |
||||
---|---|---|---|---|---|
repository method |
property |
Overlay |
No overlay |
Overlay |
No overlay |
findByUid(2) |
title |
Post 2 |
Post 2 |
Post 2 - DK |
Post 2 - DK |
uid |
2 |
2 |
2 |
2 |
|
_localizedUid |
2 |
2 |
11 |
11 |
|
findByUid(11) |
title |
Post 2 - DK (Post 2) |
Post 2 - DK (Post 2) |
Post 2 - DK |
Post 2 - DK |
uid |
2 |
2 |
2 |
2 |
|
_localizedUid |
11 (2) |
11 (2) |
11 |
11 |
Note
Note that $repository->findByUid()
internally sets respectSysLanguage(false)
so it behaves differently
than a regular query by an uid
like $query->matching($query->equals('uid', 11));
The regular query will return null
if passed uid
doesn't match
the language set in the $querySettings->setLanguageUid()
method.
Filtering & sorting¶
When filtering by an aggregate root property like Post->title
,
both filtering and sorting take translated values into account and you will get correct results, same with pagination.
When filtering or ordering by a child object property, Extbase does a left join between the aggregate root
table and the child record table.
Then the filter is applied as where clause. This means filtering or ordering by a child record property
only takes values from child records whose uids are stored in the database (in most cases its default language record).
See TranslationTest::fetchingTranslatedPostByBlogTitle()
This limitation also applies to Extbase with the feature flag being disabled.
Summary of the important code changes compared to previous versions¶
DataMapper
gets aQuery
as a constructor parameter. This allows to use the aggregate root'sQuerySettings
(language) when fetching child records/relations. SeeDataMapper->getPreparedQuery
method.DataMapper
is passed toLazyLoadingProxy
andLazyObjectStorage
, so the settings don't get lost when fetching data lazily.Query
object gets a new propertyparentQuery
which is useful to detect whether we're fetching the aggregate root or a child object.Extbase model for
FileReference
uses_localizedUid
for fetchingOriginalResource
DataMapper
forces child records to be fetched usingsetLanguageOverlayMode(true)
.When getRespectSysLanguage is set,
DataMapper
uses aggregate root's language to overlay child records to the correct language.The
where
clause used for finding translated records in overlay mode (true
,hideNonTranslated
) has been fixed in version 9. It filters out the non translated records on the database side in casehideNonTranslated
is set. It allows for filtering and sorting by translated values. SeeTypo3DbQueryParser->getLanguageStatement()
Most important known issues¶
The persistence session uses the same key for the default language record and the translation - https://forge.typo3.org/issues/59992
Extbase allows to fetch deleted/hidden records - https://forge.typo3.org/issues/86307
For more information about rendering please refer to the TypoScript reference.