SilverStripe Things
Bits » Translatable
Last updated: 31/01/2012 1:11pm
Check out the new website for a new or updated article on this topic:
www.balbuss.com/setting-up-a-multilingual-site/
24-05-2011
What is Translatable
Just to be confusing: translatable in SilverStripe lingo means multilingual as in 'a site in more then one language'.
Note: whether Translatable or not, it is always a good idea for modules and objects to be properly internationalized.
Translatable is meant to be a separate module as of version 3.0 and the sapphiredoc module, that generates the official documentation, doesn't pull modules quite yet, so the documentation is (temporarily) absent from the docs. You can still find it at github though:
https://github.com/silverstripe/silverstripe-translatable/blob/master/docs/en/index.md
'The Book' dedicates an entire section to Translatable. See also the ssbits tutorial. Finally, a lot of information can be found in the header of the Translatable class file itself (sapphire/core/model/Translatable.php).
Before enabling the Translatable module, I need to tell it what the default locale will be. Then define which objects I want to be able to translate. First of all this will be the SiteTree (all pages) and the SiteConfg, where I would store things like footer text and other messages. Put the following in /mysite/_config.php, then do a /dev/build/?flush=1
// Set the default locale for a multi-lingual site
Translatable::set_default_locale('en_US');
// Define which objects you wish to include in your translations (can be any DataObject)
Object::add_extension('SiteTree', 'Translatable');
Object::add_extension('SiteConfig', 'Translatable');
This will create an extra database table for every object involved, where the relationship between different versions of a translated object will be stored. In this case: SiteTree_translationgroups and SiteConfig_translationgroups.
An extra tab 'Translations' is now present for each page in the CMS, displaying what translations already exist for the page - and a 'select a new translation from a language' dropdown (note that existing languages no longer appear in the dropdown).
Tip: the number of available languages in the LanguageDroptdownField is hughe, but you can limit them by setting the i18n::$common_locales in your mysite/_config.php:
// Set the number of language you wish to have available
i18n::$common_locales = array(
'nl_NL' => array('Dutch', 'Nederlands'),
'en_US' => array('English (US)', 'English (US)'),
'fr_FR' => array('French', 'français'),
'de_DE' => array('German', 'Deutsch'),
'it_IT' => array('Italian', 'italiano'),
'es_ES' => array('Spanish', 'español')
);
Now select a page in the root, that has a couple of children (not the homepage) and add a new translation for it - say Dutch. This automatically leads to a totally new SiteTree, that has only the one page available. A language dropdown at the top of the SiteTree tells me that I'm now editing pages on my Dutch site. I can now also access the Dutch SiteConfig, where I can define all global settings for the Dutch websit - including a different template.
When I go back to the default language and create a translation for a child page, SilverStripe knows where it belongs, and adds it as a child to the previously translated page. In fact - I could have started by translating the child page first, because Silverstripe would have added the parent(s) as well, not to lose the site structure. So can I create a translation for an entire tree all at once? Unfortunately: no. SilverStripe doesn't care about sibblings, it's just the path down to the root that gets created.
This works just as well: add a (childpage) to a page in the Dutch site, and then add atranslation for the default English language - SilverStripe still 'knows' where it should be. There's lots of configurations I could test now - what if I move pages around in the new language, leaving them as they were in the old one?
This should make no difference - translated pages live in a common group called a 'TranslationGroup' (more later), identified by their ID's. A page that is part of such a group is considered a translation, never mind a different location or parent. But it would make keeping up the site extremely complex, so you probably shouldn't...
Every page on the website - be it English or Dutch or..., is just an ordinary page and all pages live on one and the same website. Still there is a fixed relation between pages that are 'each others translations'. This makes it possible to simply go from one page to one of its translations by just adding the required locale to the querystring. So suppose the following two pages are translations of one and the same page:
- http://mydomain/englishpage
- http://mydomain/dutchpage
Then http://mydomain/dutchpage?locale=en_US will redirect to http://mydomein/englishpage (watch the url) - and the other way around will work as well.
Homepage
The same goes for the homepage. There can only be one page that has 'home' for its urlsegment, and that is the page that's displayed when the webroot is requested in the url. But here too, adding the locale in the querystring will display the translated homepage - as long as it was added as a translation of the original homepage, and not created separately! In this case it's not a redirect as such - the querystring stays visible.
Default homepage:
http://mydomain.xx/
Homepage other language:
http://mydomain.xx/?locale=xx_XX
Why is all this useful? I's an easy way to direct a user to a translation of the page he's currently on, without having to know the actual url segment of that page. This will only work for pages that are 'connected' as translations.
Remember to set the correct language in the lang attribute of the HTML header. This cannot be done by using the Translatable currentlocale directly, you need to set the i18n locale in the Page_Controllers init() function separately:
public function init() {
parent::init();
if($this->dataRecord->hasExtension('Translatable')) {
i18n::set_locale($this->dataRecord->Locale);
}
...
Now you can use $ContentLocale in the page template to create the locale: it will convert en_US to en-US for you. Example:
<html lang="$ContentLocale" xmlns="http://www.w3.org/1999/xhtml">
Suppose you have a custom method on your page controller like the following:
function getSomePages() {
return DataObject::get('SiteTree_Live');
}
This would previously get you all (live)pages on the website. But since the SiteTree object is now translatable, SilverStripe automatically adds the so-called 'locale filter' AND (SiteTree_Live.Locale = 'en_US') to the sql query, limiting the results to only those pages belonging to the current locale.
You can enable or disable this behaviour, that is enabled by default, for a certain object, using the following setters:
MyTranslatableObject::enable_locale_filter(); MyTranslatableObject::disable_locale_filter();
When you translate a Page, most fields in the translated page are now prefixed by some extra text, showing the original fieldvalue on the 'master' page. These 'hints' do wonders to keep translations consistent.
Unfortunately they are only available for fields that belong to the original Translatable object - in this case the SiteTree class. So fields that are defined within subclasses, like Page, don't get the handy hints. But this can be remedied in your getCMSField() method, using the Translatable_Transformation class. It's a somewhat complex procedure where you first add the field as usual, and then replace it with something else. Suppose you defined this in your Page class:
public static $db = array(
'ImportantText' => 'Varchar'
}
Now in your Page's getCMSFields(method you do (this is more ore less copied from the SilverStripe documentation):
// first create the textfield as usual
$fields->addFieldToTab('Root.Content.Main',
$myField = new TextField('ImportantText', 'Enter important text')
);
// retrieve the 'master' Page from the default locale (if there is one)
$translation = $this->getTranslation(Translatable::default_locale());
// if the page exists check we're not already on the default locale
// (question: should we not do this first?)
if($translation && $this->Locale != Translatable::default_locale()) {
// create a 'Translatable_Transformation' object
$transformation = new Translatable_Transformation($translation);
// then replace the field by its transformFormField
$fields->replaceField(
'importantText',
$transformation->transformFormField($myField)
);
}
This works flawlessly, although I'd like to change the way the original field gets the label 'Original Enter important text', where I'd just as soon would have them all labeled something like 'Original entry'. This can be patched (hacked :-( ) in line 1576 of Translatable.php where it says: $nonEditableField->setTitle('Original '.$originalField->Title()); Replace by the following, and don't forget to update the language files:
$nonEditableField->setTitle(_t('Translatable.ORIGINALENTRY', 'Original entry'));
Extending the Translatable_Transformation class isn't really an option since it wouldn't affect what happens within the SiteTree class...
I've no idea what would happen if I used this with the ComplexTableField - crash probably - will try later...
When a Page is translated, related objects are not translated with it - not even if these objects are translatable. I suppose often this is just fine, and you wouldn't want this to happen anyway. Think about an event callendar: the events might be different in every language, there might be more or less - better to create new events after translation.But sometimes...
Maybe I could add some button that says 'copy/translate original objects'. That would make it somewaht more flexible. It would just copy the original objects, and add the correct Locale if it is a Translatable object. Still thinking about this... Maybe also have a look at the (probably still very 'trunky') TranslatableModelAdmin module.
Adding DataObjects to a translated) Page using the ComplexTableField (CTF) poses no problem - as long as the objects themselves are not set to Translatable! Once they are made Translatable and a Locale field is added, things go wrong. For some reason the Locale is always set to the Default. That can leave you with a DataObject pointing to the ID of a dutch Page, but having the en_US locale. You'll find it displays fine after adding it and closing the popup, but then disappears once you turn back to that Page later.
Possible solution: this patch.
There are two glitches though:
$fields->push(new HiddenField('Locale', 'Locale'));
Tip: only use Translatable DataObjects if you need the locale in some context - maybe for displaying all objects at once for a given locale... They will already have their link to the page, so that won't be the problem ...
The XXX_translationgroups table, that that is created for Translatable object XXX, looks something like this:
| ID | OriginalID | TranslationGroupID |
| 1 | 1 | 1 |
| 2 | 18 | 1 |
| 3 | 4 | 4 |
| 2 | 24 | 4 |
This is the table that links translated objects by placing them in the same translationgroup. In this example the objects with ID 1 and 18 belong to the same translationgroup (1) where object 1 is the master, lending its ID to the 'group'. Objects 4 and 24 again are translations of the same object, where object 4 is the master. These are some functions that can be called on a Translatable object:
getTranslationGroup()
Will return the ID (number) of the translationgroup the current object belongs to, use to find the master page...
addTranslationGroup($originalID, $overwrite = false)
will add a new record for the current object to a translationgroup. can be used to setting it up as a translation of some master object
removeTranslationGroup()
will remove the record for the current object from the translationgroup - ie the record where the OriginalID equals $this->ID. If this is the group's Master, this will probably leave the other translations in the group, so they will be linked but maserless. Handle with care I suppose...
getTranslatedLocales()
will get you an array of locales (sorted alfabeticale) for which a translation exists of the current object.
getTranslations($locale = null, $stage = null)
will get you a DataObjectSet of all translations for the current object, except the current object itself. If a locale is provided, it will only query for that special locale. If versioned is enabled for the object, you can provide a stage as well (Live, Stage); Unfortunately you cannot influence the order in which the translations are retreived. Try this in your Page template:
<% if Translations %> <ul> <% control Translations %> <li><a class="$Locale" hreflang="$Locale.RFC1766" href="$Link">$Locale</a></li> <% end_control %> </ul> <% end_if %>
This is one way to create a language switcher. For some reason, if the if-control is omitted, SilverStripe will still loop once if no translations are available. Never knew why that should be so...
This is still work in progress...
10 Most recently updated pages
Great site... A real useful site about silverstripe!!!
Posted by , 16/07/2011 3:28pm (10 months ago)
Hello,
fantastic! Thanks a lot for sharing your knowledge.
I searched for hours but didn't find a solution. Even the SilverLight book lacks your suggestions in 'Translatable fields - hints'.
Best regards,
Andreas
Posted by Andreas, 05/06/2011 11:22am (12 months ago)
RSS feed for comments on this page | RSS feed for all comments