From 2a3cc650dc55cef276e3b1690b9b1dfb4e51a054 Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Tue, 22 Mar 2011 21:50:26 +1300 Subject: [PATCH] MINOR Initial commit, moved files from 'sapphire' and 'cms' modules --- LICENSE | 24 + README.md | 35 + _config.php | 4 + .../TranslatableCMSMainExtension.php | 143 ++ code/forms/LanguageDropdownField.php | 71 + code/model/Translatable.php | 1593 +++++++++++++++++ code/tasks/MigrateTranslatableTask.php | 176 ++ css/CMSMain.Translatable.css | 13 + docs/en/_images/translatable1.png | Bin 0 -> 24062 bytes docs/en/_images/translatable2.png | Bin 0 -> 5657 bytes docs/en/_images/translatable3.png | Bin 0 -> 14011 bytes docs/en/_images/translatable4_small.png | Bin 0 -> 79692 bytes docs/en/index.md | 417 +++++ javascript/CMSMain.Translatable.js | 77 + tests/unit/TranslatableSearchFormTest.php | 101 ++ tests/unit/TranslatableSearchFormTest.yml | 18 + tests/unit/TranslatableSiteConfigTest.php | 53 + tests/unit/TranslatableSiteConfigTest.yml | 86 + tests/unit/TranslatableTest.php | 987 ++++++++++ tests/unit/TranslatableTest.yml | 82 + 20 files changed, 3880 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 _config.php create mode 100644 code/controller/TranslatableCMSMainExtension.php create mode 100755 code/forms/LanguageDropdownField.php create mode 100755 code/model/Translatable.php create mode 100644 code/tasks/MigrateTranslatableTask.php create mode 100644 css/CMSMain.Translatable.css create mode 100644 docs/en/_images/translatable1.png create mode 100644 docs/en/_images/translatable2.png create mode 100644 docs/en/_images/translatable3.png create mode 100644 docs/en/_images/translatable4_small.png create mode 100644 docs/en/index.md create mode 100755 javascript/CMSMain.Translatable.js create mode 100644 tests/unit/TranslatableSearchFormTest.php create mode 100644 tests/unit/TranslatableSearchFormTest.yml create mode 100644 tests/unit/TranslatableSiteConfigTest.php create mode 100644 tests/unit/TranslatableSiteConfigTest.yml create mode 100755 tests/unit/TranslatableTest.php create mode 100644 tests/unit/TranslatableTest.yml diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a5e0cfd --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +* Copyright (c) 2007-2011, Silverstripe Ltd. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* * Redistributions of source code must retain the above copyright +* notice, this list of conditions and the following disclaimer. +* * Redistributions in binary form must reproduce the above copyright +* notice, this list of conditions and the following disclaimer in the +* documentation and/or other materials provided with the distribution. +* * Neither the name of the nor the +* names of its contributors may be used to endorse or promote products +* derived from this software without specific prior written permission. +* +* THIS SOFTWARE IS PROVIDED BY Silverstripe Ltd. ``AS IS'' AND ANY +* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +* DISCLAIMED. IN NO EVENT SHALL Silverstripe Ltd. BE LIABLE FOR ANY +* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..f202582 --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +# Translatable module for SilverStripe CMS # + +## Introduction ## + +Allows translation of DataObject and SiteTree records into multiple languages. +See `/docs/en/index.md` for details. + +## Requirements ## + + * SilverStripe 3.0 (both cms and sapphire modules) + +## Maintainers ## + + * Ingo Schommer + +## TODO ## + +This module was originally part of the SilverStripe CMS core codebase. +While the bulk of the logic has been separated out into this module, +there are still many places across SilverStripe CMS which this modules relies on: + +* CMSBatchActionHandler->handleAction() +* ContentController->handleRequest() +* ContentController->ContentLocale() +* ErrorPage::response_for() +* LeftAndMain->init() +* ModelAsController->getNestedController() +* RootURLController::get_homepage_link() +* SearchForm +* SiteConfig +* SiteTree->RelativeLink() +* SiteTree->getSiteConfig() + +These APIs mostly require either hooks for an Extension subclass, +or refactoring to allow better customization. \ No newline at end of file diff --git a/_config.php b/_config.php new file mode 100644 index 0000000..e30c975 --- /dev/null +++ b/_config.php @@ -0,0 +1,4 @@ + array( + 'createtranslation', + ) + ); + } + + function init() { + + // Locale" attribute is either explicitly added by LeftAndMain Javascript logic, + // or implied on a translated record (see {@link Translatable->updateCMSFields()}). + // $Lang serves as a "context" which can be inspected by Translatable - hence it + // has the same name as the database property on Translatable. + $req = $this->owner->getRequest(); + if($req->requestVar("Locale")) { + $this->owner->Locale = $req->requestVar("Locale"); + } elseif($req->requestVar("locale")) { + $this->owner->Locale = $req->requestVar("locale"); + } else { + $this->owner->Locale = Translatable::default_locale(); + } + Translatable::set_current_locale($this->owner->Locale); + + // collect languages for TinyMCE spellchecker plugin. + // see http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/spellchecker + $langName = i18n::get_locale_name($this->owner->Locale); + HtmlEditorConfig::get('cms')->setOption('spellchecker_languages', "+{$langName}={$this->owner->Locale}"); + + Requirements::javascript('translatable/javascript/CMSMain.Translatable.js'); + Requirements::css('translatable/css/CMSMain.Translatable.css'); + } + + function updateEditForm(&$form) { + $siteConfig = SiteConfig::current_site_config(); + if($form->Name() == 'RootForm' && Object::has_extension('SiteConfig',"Translatable")) { + $form->Fields()->push(new HiddenField('Locale','', $siteConfig->Locale)); + } + } + + function updatePageOption(&$fields) { + $fields->push(new HiddenField("Locale", 'Locale', Translatable::get_current_locale())); + } + + /** + * Create a new translation from an existing item, switch to this language and reload the tree. + */ + function createtranslation($request) { + // Protect against CSRF on destructive action + if(!SecurityToken::inst()->checkRequest($request)) return $this->owner->httpError(400); + + $langCode = Convert::raw2sql($request->getVar('newlang')); + $originalLangID = (int)$request->getVar('ID'); + + $record = $this->owner->getRecord($originalLangID); + + $this->owner->Locale = $langCode; + Translatable::set_current_locale($langCode); + + // Create a new record in the database - this is different + // to the usual "create page" pattern of storing the record + // in-memory until a "save" is performed by the user, mainly + // to simplify things a bit. + // @todo Allow in-memory creation of translations that don't persist in the database before the user requests it + $translatedRecord = $record->createTranslation($langCode); + + $url = sprintf( + "%s/%d/?locale=%s", + $this->owner->Link('show'), + $translatedRecord->ID, + $langCode + ); + + return Director::redirect($url); + } + + /** + * Returns a form with all languages with languages already used appearing first. + * + * @return Form + */ + function LangForm() { + $member = Member::currentUser(); //check to see if the current user can switch langs or not + if(Permission::checkMember($member, 'VIEW_LANGS')) { + $field = new LanguageDropdownField( + 'Locale', + _t('CMSMAIN.LanguageDropdownLabel', 'Language'), + array(), + 'SiteTree', + 'Locale-English', + singleton('SiteTree') + ); + $field->setValue(Translatable::get_current_locale()); + } else { + // user doesn't have permission to switch langs + // so just show a string displaying current language + $field = new LiteralField( + 'Locale', + i18n::get_locale_name( Translatable::get_current_locale()) + ); + } + + $form = new Form( + $this->owner, + 'LangForm', + new FieldSet( + $field + ), + new FieldSet( + new FormAction('selectlang', _t('CMSMain_left.ss.GO','Go')) + ) + ); + $form->unsetValidator(); + + return $form; + } + + function selectlang($data, $form) { + return $this->owner; + } + + /** + * Determine if there are more than one languages in our site tree. + * + * @return boolean + */ + function MultipleLanguages() { + $langs = Translatable::get_existing_content_languages('SiteTree'); + + return (count($langs) > 1); + } + + /** + * @return boolean + */ + function IsTranslatableEnabled() { + return Object::has_extension('SiteTree', 'Translatable'); + } + +} \ No newline at end of file diff --git a/code/forms/LanguageDropdownField.php b/code/forms/LanguageDropdownField.php new file mode 100755 index 0000000..c2ef796 --- /dev/null +++ b/code/forms/LanguageDropdownField.php @@ -0,0 +1,71 @@ + $localeTitle) { + if( + ($allowedLocales && !in_array($locale, $allowedLocales)) + || ($excludeLocales && in_array($locale, $excludeLocales)) + || ($usedLocalesWithTitle && array_key_exists($locale, $usedLocalesWithTitle)) + ) { + unset($allLocalesWithTitle[$locale]); + } + } + // instance specific permissions + foreach($allLocalesWithTitle as $locale => $localeTitle) { + if($instance && !$instance->canTranslate(null, $locale)) { + unset($allLocalesWithTitle[$locale]); + } + } + foreach($usedLocalesWithTitle as $locale => $localeTitle) { + if($instance && !$instance->canTranslate(null, $locale)) { + unset($usedLocalesWithTitle[$locale]); + } + } + + // Sort by title (array value) + asort($allLocalesWithTitle); + + if(count($usedLocalesWithTitle)) { + asort($usedLocalesWithTitle); + $source = array( + _t('Form.LANGAVAIL', "Available languages") => $usedLocalesWithTitle, + _t('Form.LANGAOTHER', "Other languages") => $allLocalesWithTitle + ); + } else { + $source = $allLocalesWithTitle; + } + + parent::__construct($name, $title, $source); + } +} + +?> \ No newline at end of file diff --git a/code/model/Translatable.php b/code/model/Translatable.php new file mode 100755 index 0000000..4246609 --- /dev/null +++ b/code/model/Translatable.php @@ -0,0 +1,1593 @@ +Configuration + * + *

Through Object::add_extension()

+ * Enabling Translatable through {@link Object::add_extension()} in your _config.php: + * + * Object::add_extension('MyClass', 'Translatable'); + * + * This is the recommended approach for enabling Translatable. + * + *

Through $extensions

+ * + * class MyClass extends DataObject { + * static $extensions = array( + * "Translatable" + * ); + * } + * + * + * Make sure to rebuild the database through /dev/build after enabling translatable. + * Use the correct {@link set_default_locale()} before building the database + * for the first time, as this locale will be written on all new records. + * + *

"Default" locales

+ * + * Important: If the "default language" of your site is not US-English (en_US), + * please ensure to set the appropriate default language for + * your content before building the database with Translatable enabled: + * + * Translatable::set_default_locale(); // e.g. 'de_DE' or 'fr_FR' + * + * + * For the Translatable class, a "locale" consists of a language code plus a region code separated by an underscore, + * for example "de_AT" for German language ("de") in the region Austria ("AT"). + * See http://www.w3.org/International/articles/language-tags/ for a detailed description. + * + *

Usage

+ * + * Getting a translation for an existing instance: + * + * $translatedObj = Translatable::get_one_by_locale('MyObject', 'de_DE'); + * + * + * Getting a translation for an existing instance: + * + * $obj = DataObject::get_by_id('MyObject', 99); // original language + * $translatedObj = $obj->getTranslation('de_DE'); + * + * + * Getting translations through {@link Translatable::set_current_locale()}. + * This is *not* a recommended approach, but sometimes inavoidable (e.g. for {@link Versioned} methods). + * + * $origLocale = Translatable::get_current_locale(); + * Translatable::set_current_locale('de_DE'); + * $obj = Versioned::get_one_by_stage('MyObject', "ID = 99"); + * Translatable::set_current_locale($origLocale); + * + * + * Creating a translation: + * + * $obj = new MyObject(); + * $translatedObj = $obj->createTranslation('de_DE'); + * + * + *

Usage for SiteTree

+ * + * Translatable can be used for subclasses of {@link SiteTree} as well. + * + * + * Object::add_extension('SiteTree', 'Translatable'); + * Object::add_extension('SiteConig', 'Translatable'); + * + * + * If a child page translation is requested without the parent + * page already having a translation in this language, the extension + * will recursively create translations up the tree. + * Caution: The "URLSegment" property is enforced to be unique across + * languages by auto-appending the language code at the end. + * You'll need to ensure that the appropriate "reading language" is set + * before showing links to other pages on a website through $_GET['locale']. + * Pages in different languages can have different publication states + * through the {@link Versioned} extension. + * + * Note: You can't get Children() for a parent page in a different language + * through set_current_locale(). Get the translated parent first. + * + * + * // wrong + * Translatable::set_current_locale('de_DE'); + * $englishParent->Children(); + * // right + * $germanParent = $englishParent->getTranslation('de_DE'); + * $germanParent->Children(); + * + * + *

Translation groups

+ * + * Each translation can have one or more related pages in other languages. + * This relation is optional, meaning you can + * create translations which have no representation in the "default language". + * This means you can have a french translation with a german original, + * without either of them having a representation + * in the default english language tree. + * Caution: There is no versioning for translation groups, + * meaning associating an object with a group will affect both stage and live records. + * + * SiteTree database table (abbreviated) + * ^ ID ^ URLSegment ^ Title ^ Locale ^ + * | 1 | about-us | About us | en_US | + * | 2 | ueber-uns | Über uns | de_DE | + * | 3 | contact | Contact | en_US | + * + * SiteTree_translationgroups database table + * ^ TranslationGroupID ^ OriginalID ^ + * | 99 | 1 | + * | 99 | 2 | + * | 199 | 3 | + * + *

Character Sets

+ * + * Caution: Does not apply any character-set conversion, it is assumed that all content + * is stored and represented in UTF-8 (Unicode). Please make sure your database and + * HTML-templates adjust to this. + * + *

Permissions

+ * + * Authors without administrative access need special permissions to edit locales other than + * the default locale. + * + * - TRANSLATE_ALL: Translate into all locales + * - Translate_: Translate a specific locale. Only available for all locales set in + * `Translatable::set_allowed_locales()`. + * + * Note: If user-specific view permissions are required, please overload `SiteTree->canView()`. + * + *

Uninstalling/Disabling

+ * + * Disabling Translatable after creating translations will lead to all + * pages being shown in the default sitetree regardless of their language. + * It is advised to start with a new database after uninstalling Translatable, + * or manually filter out translated objects through their "Locale" property + * in the database. + * + * @see http://doc.silverstripe.org/doku.php?id=multilingualcontent + * + * @author Ingo Schommer + * @author Michael Gall + * @author Bernat Foj Capell + * + * @package sapphire + * @subpackage i18n + */ +class Translatable extends DataObjectDecorator implements PermissionProvider { + + /** + * The 'default' language. + * @var string + */ + protected static $default_locale = 'en_US'; + + /** + * The language in which we are reading dataobjects. + * + * @var string + */ + protected static $current_locale = null; + + /** + * A cached list of existing tables + * + * @var mixed + */ + protected static $tableList = null; + + /** + * An array of fields that can be translated. + * @var array + */ + protected $translatableFields = null; + + /** + * A map of the field values of the original (untranslated) DataObject record + * @var array + */ + protected $original_values = null; + + /** + * If this is set to TRUE then {@link augmentSQL()} will automatically add a filter + * clause to limit queries to the current {@link get_current_locale()}. This camn be + * disabled using {@link disable_locale_filter()} + * + * @var bool + */ + protected static $locale_filter_enabled = true; + + /** + * @var array All locales in which a translation can be created. + * This limits the choice in the CMS language dropdown in the + * "Translation" tab, as well as the language dropdown above + * the CMS tree. If not set, it will default to showing all + * common locales. + */ + protected static $allowed_locales = null; + + /** + * Reset static configuration variables to their default values + */ + static function reset() { + self::enable_locale_filter(); + self::$default_locale = 'en_US'; + self::$current_locale = null; + self::$allowed_locales = null; + } + + /** + * Choose the language the site is currently on. + * + * If $_GET['locale'] is currently set, then that locale will be used. Otherwise the member preference (if logged + * in) or default locale will be used. + * + * @todo Re-implement cookie and member option + * + * @param $langsAvailable array A numerical array of languages which are valid choices (optional) + * @return string Selected language (also saved in $current_locale). + */ + static function choose_site_locale($langsAvailable = array()) { + if(self::$current_locale) { + return self::$current_locale; + } + + if((isset($_GET['locale']) && !$langsAvailable) || (isset($_GET['locale']) && in_array($_GET['locale'], $langsAvailable))) { + // get from GET parameter + self::set_current_locale($_GET['locale']); + } else { + self::set_current_locale(self::default_locale()); + } + + return self::$current_locale; + } + + /** + * Get the current reading language. + * This value has to be set before the schema is built with translatable enabled, + * any changes after this can cause unintended side-effects. + * + * @return string + */ + static function default_locale() { + return self::$default_locale; + } + + /** + * Set default language. Please set this value *before* creating + * any database records (like pages), as this locale will be attached + * to all new records. + * + * @param $locale String + */ + static function set_default_locale($locale) { + if($locale && !i18n::validate_locale($locale)) throw new InvalidArgumentException(sprintf('Invalid locale "%s"', $locale)); + + $localeList = i18n::get_locale_list(); + if(isset($localeList[$locale])) { + self::$default_locale = $locale; + } else { + user_error("Translatable::set_default_locale(): '$locale' is not a valid locale.", E_USER_WARNING); + } + } + + /** + * Get the current reading language. + * If its not chosen, call {@link choose_site_locale()}. + * + * @return string + */ + static function get_current_locale() { + return (self::$current_locale) ? self::$current_locale : self::choose_site_locale(); + } + + /** + * Set the reading language, either namespaced to 'site' (website content) + * or 'cms' (management backend). This value is used in {@link augmentSQL()} + * to "auto-filter" all SELECT queries by this language. + * See {@link disable_locale_filter()} on how to override this behaviour temporarily. + * + * @param string $lang New reading language. + */ + static function set_current_locale($locale) { + if($locale && !i18n::validate_locale($locale)) throw new InvalidArgumentException(sprintf('Invalid locale "%s"', $locale)); + + self::$current_locale = $locale; + } + + /** + * Get a singleton instance of a class in the given language. + * @param string $class The name of the class. + * @param string $locale The name of the language. + * @param string $filter A filter to be inserted into the WHERE clause. + * @param boolean $cache Use caching (default: false) + * @param string $orderby A sort expression to be inserted into the ORDER BY clause. + * @return DataObject + */ + static function get_one_by_locale($class, $locale, $filter = '', $cache = false, $orderby = "") { + if($locale && !i18n::validate_locale($locale)) throw new InvalidArgumentException(sprintf('Invalid locale "%s"', $locale)); + + $orig = Translatable::get_current_locale(); + Translatable::set_current_locale($locale); + $do = DataObject::get_one($class, $filter, $cache, $orderby); + Translatable::set_current_locale($orig); + return $do; + } + + /** + * Get all the instances of the given class translated to the given language + * + * @param string $class The name of the class + * @param string $locale The name of the language + * @param string $filter A filter to be inserted into the WHERE clause. + * @param string $sort A sort expression to be inserted into the ORDER BY clause. + * @param string $join A single join clause. This can be used for filtering, only 1 instance of each DataObject will be returned. + * @param string $limit A limit expression to be inserted into the LIMIT clause. + * @param string $containerClass The container class to return the results in. + * @param string $having A filter to be inserted into the HAVING clause. + * @return mixed The objects matching the conditions. + */ + static function get_by_locale($class, $locale, $filter = '', $sort = '', $join = "", $limit = "", $containerClass = "DataObjectSet", $having = "") { + if($locale && !i18n::validate_locale($locale)) throw new InvalidArgumentException(sprintf('Invalid locale "%s"', $locale)); + + $oldLang = self::get_current_locale(); + self::set_current_locale($locale); + $result = DataObject::get($class, $filter, $sort, $join, $limit, $containerClass, $having); + self::set_current_locale($oldLang); + return $result; + } + + /** + * @return bool + */ + public static function locale_filter_enabled() { + return self::$locale_filter_enabled; + } + + /** + * Enables automatic filtering by locale. This is normally called after is has been + * disabled using {@link disable_locale_filter()}. + */ + public static function enable_locale_filter() { + self::$locale_filter_enabled = true; + } + + /** + * Disables automatic locale filtering in {@link augmentSQL()}. This can be re-enabled + * using {@link enable_locale_filter()}. + */ + public static function disable_locale_filter() { + self::$locale_filter_enabled = false; + } + + /** + * Gets all translations for this specific page. + * Doesn't include the language of the current record. + * + * @return array Numeric array of all locales, sorted alphabetically. + */ + function getTranslatedLocales() { + $langs = array(); + + $baseDataClass = ClassInfo::baseDataClass($this->owner->class); //Base Class + $translationGroupClass = $baseDataClass . "_translationgroups"; + if($this->owner->hasExtension("Versioned") && Versioned::current_stage() == "Live") { + $baseDataClass = $baseDataClass . "_Live"; + } + + $translationGroupID = $this->getTranslationGroup(); + if(is_numeric($translationGroupID)) { + $query = new SQLQuery( + 'DISTINCT "Locale"', + sprintf( + '"%s" LEFT JOIN "%s" ON "%s"."OriginalID" = "%s"."ID"', + $baseDataClass, + $translationGroupClass, + $translationGroupClass, + $baseDataClass + ), // from + sprintf( + '"%s"."TranslationGroupID" = %d AND "%s"."Locale" != \'%s\'', + $translationGroupClass, + $translationGroupID, + $baseDataClass, + $this->owner->Locale + ) // where + ); + $langs = $query->execute()->column(); + } + if($langs) { + $langCodes = array_values($langs); + sort($langCodes); + return $langCodes; + } else { + return array(); + }; + } + + /** + * Gets all locales that a member can access + * as defined by {@link $allowed_locales} + * and {@link canTranslate()}. + * If {@link $allowed_locales} is not set and + * the user has the `TRANSLATE_ALL` permission, + * the method will return all available locales in the system. + * + * @param Member $member + * @return array Map of locales + */ + function getAllowedLocalesForMember($member) { + $locales = self::get_allowed_locales(); + if(!$locales) $locales = i18n::get_common_locales(); + if($locales) foreach($locales as $k => $locale) { + if(!$this->canTranslate($member, $locale)) unset($locales[$k]); + } + + return $locales; + } + + /** + * Get a list of languages in which a given element has been translated. + * + * @deprecated 2.4 Use {@link getTranslations()} + * + * @param string $class Name of the class of the element + * @param int $id ID of the element + * @return array List of languages + */ + static function get_langs_by_id($class, $id) { + $do = DataObject::get_by_id($class, $id); + return ($do ? $do->getTranslatedLocales() : array()); + } + + /** + * Enables the multilingual feature + * + * @deprecated 2.4 Use Object::add_extension('SiteTree', 'Translatable') + */ + static function enable() { + if(class_exists('SiteTree')) Object::add_extension('SiteTree', 'Translatable'); + } + + /** + * Disable the multilingual feature + * + * @deprecated 2.4 Use Object::remove_extension('SiteTree', 'Translatable') + */ + static function disable() { + if(class_exists('SiteTree')) Object::remove_extension('SiteTree', 'Translatable'); + } + + /** + * Check whether multilingual support has been enabled + * + * @deprecated 2.4 Use Object::has_extension('SiteTree', 'Translatable') + * @return boolean True if enabled + */ + static function is_enabled() { + if(class_exists('SiteTree')){ + return Object::has_extension('SiteTree', 'Translatable'); + }else{ + return false; + } + } + + + /** + * Construct a new Translatable object. + * @var array $translatableFields The different fields of the object that can be translated. + * This is currently not implemented, all fields are marked translatable (see {@link setOwner()}). + */ + function __construct($translatableFields = null) { + parent::__construct(); + + // @todo Disabled selection of translatable fields - we're setting all fields as translatable in setOwner() + /* + if(!is_array($translatableFields)) { + $translatableFields = func_get_args(); + } + $this->translatableFields = $translatableFields; + */ + + // workaround for extending a method on another decorator (Hierarchy): + // split the method into two calls, and overwrite the wrapper AllChildrenIncludingDeleted() + // Has to be executed even with Translatable disabled, as it overwrites the method with same name + // on Hierarchy class, and routes through to Hierarchy->doAllChildrenIncludingDeleted() instead. + // Caution: There's an additional method for augmentAllChildrenIncludingDeleted() + + } + + function setOwner($owner, $ownerBaseClass = null) { + parent::setOwner($owner, $ownerBaseClass); + + // setting translatable fields by inspecting owner - this should really be done in the constructor + if($this->owner && $this->translatableFields === null) { + $this->translatableFields = array_merge( + array_keys($this->owner->inheritedDatabaseFields()), + array_keys($this->owner->has_many()), + array_keys($this->owner->many_many()) + ); + } + } + + function extraStatics() { + return array( + "db" => array( + "Locale" => "DBLocale", + //"TranslationMasterID" => "Int" // optional relation to a "translation master" + ), + "defaults" => array( + "Locale" => Translatable::default_locale() // as an overloaded getter as well: getLang() + ) + ); + } + + /** + * Changes any SELECT query thats not filtering on an ID + * to limit by the current language defined in {@link get_current_locale()}. + * It falls back to "Locale='' OR Lang IS NULL" and assumes that + * this implies querying for the default language. + * + * Use {@link disable_locale_filter()} to temporarily disable this "auto-filtering". + */ + function augmentSQL(SQLQuery &$query) { + // If the record is saved (and not a singleton), and has a locale, + // limit the current call to its locale. This fixes a lot of problems + // with other extensions like Versioned + $locale = ($this->owner->ID && $this->owner->Locale) ? $this->owner->Locale : Translatable::get_current_locale(); + $baseTable = ClassInfo::baseDataClass($this->owner->class); + $where = $query->where; + if( + $locale + // unless the filter has been temporarily disabled + && self::locale_filter_enabled() + // DataObject::get_by_id() should work independently of language + && !$query->filtersOnID() + // the query contains this table + // @todo Isn't this always the case?! + && array_search($baseTable, array_keys($query->from)) !== false + // or we're already filtering by Lang (either from an earlier augmentSQL() call or through custom SQL filters) + && !preg_match('/("|\'|`)Locale("|\'|`)/', $query->getFilter()) + //&& !$query->filtersOnFK() + ) { + $qry = sprintf('"%s"."Locale" = \'%s\'', $baseTable, Convert::raw2sql($locale)); + $query->where[] = $qry; + } + } + + /** + * Create _translation database table to enable + * tracking of "translation groups" in which each related + * translation of an object acts as a sibling, rather than + * a parent->child relation. + */ + function augmentDatabase() { + $baseDataClass = ClassInfo::baseDataClass($this->owner->class); + if($this->owner->class != $baseDataClass) return; + + $fields = array( + 'OriginalID' => 'Int', + 'TranslationGroupID' => 'Int', + ); + $indexes = array( + 'OriginalID' => true, + 'TranslationGroupID' => true + ); + + // Add new tables if required + DB::requireTable("{$baseDataClass}_translationgroups", $fields, $indexes); + + // Remove 2.2 style tables + DB::dontRequireTable("{$baseDataClass}_lang"); + if($this->owner->hasExtension('Versioned')) { + DB::dontRequireTable("{$baseDataClass}_lang_Live"); + DB::dontRequireTable("{$baseDataClass}_lang_versions"); + } + } + + /** + * @todo Find more appropriate place to hook into database building + */ + function requireDefaultRecords() { + // @todo This relies on the Locale attribute being on the base data class, and not any subclasses + if($this->owner->class != ClassInfo::baseDataClass($this->owner->class)) return false; + + // Permissions: If a group doesn't have any specific TRANSLATE_ edit rights, + // but has CMS_ACCESS_CMSMain (general CMS access), then assign TRANSLATE_ALL permissions as a default. + // Auto-setting permissions based on these intransparent criteria is a bit hacky, + // but unavoidable until we can determine when a certain permission code was made available first + // (see http://open.silverstripe.org/ticket/4940) + $groups = Permission::get_groups_by_permission(array('CMS_ACCESS_CMSMain','CMS_ACCESS_LeftAndMain','ADMIN')); + if($groups) foreach($groups as $group) { + $codes = $group->Permissions()->column('Code'); + $hasTranslationCode = false; + foreach($codes as $code) { + if(preg_match('/^TRANSLATE_/', $code)) $hasTranslationCode = true; + } + // Only add the code if no more restrictive code exists + if(!$hasTranslationCode) Permission::grant($group->ID, 'TRANSLATE_ALL'); + } + + // If the Translatable extension was added after the first records were already + // created in the database, make sure to update the Locale property if + // if wasn't set before + $idsWithoutLocale = DB::query(sprintf( + 'SELECT "ID" FROM "%s" WHERE "Locale" IS NULL OR "Locale" = \'\'', + ClassInfo::baseDataClass($this->owner->class) + ))->column(); + if(!$idsWithoutLocale) return; + + if(class_exists('SiteTree') && $this->owner->class == 'SiteTree') { + foreach(array('Stage', 'Live') as $stage) { + foreach($idsWithoutLocale as $id) { + $obj = Versioned::get_one_by_stage( + $this->owner->class, + $stage, + sprintf('"SiteTree"."ID" = %d', $id) + ); + if(!$obj) continue; + + $obj->Locale = Translatable::default_locale(); + $obj->writeToStage($stage); + $obj->addTranslationGroup($obj->ID); + $obj->destroy(); + unset($obj); + } + } + } else { + foreach($idsWithoutLocale as $id) { + $obj = DataObject::get_by_id($this->owner->class, $id); + if(!$obj) continue; + + $obj->Locale = Translatable::default_locale(); + $obj->write(); + $obj->addTranslationGroup($obj->ID); + $obj->destroy(); + unset($obj); + } + } + DB::alteration_message(sprintf( + "Added default locale '%s' to table %s","changed", + Translatable::default_locale(), + $this->owner->class + )); + } + + /** + * Add a record to a "translation group", + * so its relationship to other translations + * based off the same object can be determined later on. + * See class header for further comments. + * + * @param int $originalID Either the primary key of the record this new translation is based on, + * or the primary key of this record, to create a new translation group + * @param boolean $overwrite + */ + public function addTranslationGroup($originalID, $overwrite = false) { + if(!$this->owner->exists()) return false; + + $baseDataClass = ClassInfo::baseDataClass($this->owner->class); + $existingGroupID = $this->getTranslationGroup($originalID); + + // Remove any existing groups if overwrite flag is set + if($existingGroupID && $overwrite) { + $sql = sprintf( + 'DELETE FROM "%s_translationgroups" WHERE "TranslationGroupID" = %d AND "OriginalID" = %d', + $baseDataClass, + $existingGroupID, + $this->owner->ID + ); + DB::query($sql); + $existingGroupID = null; + } + + // Add to group (only if not in existing group or $overwrite flag is set) + if(!$existingGroupID) { + $sql = sprintf( + 'INSERT INTO "%s_translationgroups" ("TranslationGroupID","OriginalID") VALUES (%d,%d)', + $baseDataClass, + $originalID, + $this->owner->ID + ); + DB::query($sql); + } + } + + /** + * Gets the translation group for the current record. + * This ID might equal the record ID, but doesn't have to - + * it just points to one "original" record in the list. + * + * @return int Numeric ID of the translationgroup in the _translationgroup table + */ + public function getTranslationGroup() { + if(!$this->owner->exists()) return false; + + $baseDataClass = ClassInfo::baseDataClass($this->owner->class); + return DB::query( + sprintf('SELECT "TranslationGroupID" FROM "%s_translationgroups" WHERE "OriginalID" = %d', $baseDataClass, $this->owner->ID) + )->value(); + } + + /** + * Removes a record from the translation group lookup table. + * Makes no assumptions on other records in the group - meaning + * if this happens to be the last record assigned to the group, + * this group ceases to exist. + */ + public function removeTranslationGroup() { + $baseDataClass = ClassInfo::baseDataClass($this->owner->class); + DB::query( + sprintf('DELETE FROM "%s_translationgroups" WHERE "OriginalID" = %d', $baseDataClass, $this->owner->ID) + ); + } + + /** + * Determine if a table needs Versioned support + * This is called at db/build time + * + * @param string $table Table name + * @return boolean + */ + function isVersionedTable($table) { + return false; + } + + /** + * Note: The bulk of logic is in ModelAsController->getNestedController() + * and ContentController->handleRequest() + */ + function contentcontrollerInit($controller) { + $controller->Locale = Translatable::choose_site_locale(); + } + + function modelascontrollerInit($controller) { + //$this->contentcontrollerInit($controller); + } + + function initgetEditForm($controller) { + $this->contentcontrollerInit($controller); + } + + /** + * Recursively creates translations for parent pages in this language + * if they aren't existing already. This is a necessity to make + * nested pages accessible in a translated CMS page tree. + * It would be more userfriendly to grey out untranslated pages, + * but this involves complicated special cases in AllChildrenIncludingDeleted(). + * + * {@link SiteTree->onBeforeWrite()} will ensure that each translation will get + * a unique URL across languages, by means of {@link SiteTree::get_by_link()} + * and {@link Translatable->alternateGetByURL()}. + */ + function onBeforeWrite() { + // If language is not set explicitly, set it to current_locale. + // This might be a bit overzealous in assuming the language + // of the content, as a "single language" website might be expanded + // later on. See {@link requireDefaultRecords()} for batch setting + // of empty Locale columns on each dev/build call. + if(!$this->owner->Locale) { + $this->owner->Locale = Translatable::get_current_locale(); + } + + // Specific logic for SiteTree subclasses. + // If page has untranslated parents, create (unpublished) translations + // of those as well to avoid having inaccessible children in the sitetree. + // Caution: This logic is very sensitve to infinite loops when translation status isn't determined properly + // If a parent for the newly written translation was existing before this + // onBeforeWrite() call, it will already have been linked correctly through createTranslation() + if(class_exists('SiteTree') && $this->owner->hasField('ParentID') && $this->owner instanceof SiteTree) { + if( + !$this->owner->ID + && $this->owner->ParentID + && !$this->owner->Parent()->hasTranslation($this->owner->Locale) + ) { + $parentTranslation = $this->owner->Parent()->createTranslation($this->owner->Locale); + $this->owner->ParentID = $parentTranslation->ID; + } + } + + // Has to be limited to the default locale, the assumption is that the "page type" + // dropdown is readonly on all translations. + if($this->owner->ID && $this->owner->Locale == Translatable::default_locale()) { + $changedFields = $this->owner->getChangedFields(); + if(isset($changedFields['ClassName'])) { + $this->owner->ClassName = $changedFields['ClassName']['before']; + $translations = $this->owner->getTranslations(); + $this->owner->ClassName = $changedFields['ClassName']['after']; + if($translations) foreach($translations as $translation) { + $translation->setClassName($this->owner->ClassName); + $translation = $translation->newClassInstance($translation->ClassName); + $translation->populateDefaults(); + $translation->forceChange(); + $translation->write(); + } + } + } + + // see onAfterWrite() + if(!$this->owner->ID) { + $this->owner->_TranslatableIsNewRecord = true; + } + } + + function onAfterWrite() { + // hacky way to determine if the record was created in the database, + // or just updated + if($this->owner->_TranslatableIsNewRecord) { + // this would kick in for all new records which are NOT + // created through createTranslation(), meaning they don't + // have the translation group automatically set. + $translationGroupID = $this->getTranslationGroup(); + if(!$translationGroupID) $this->addTranslationGroup($this->owner->_TranslationGroupID ? $this->owner->_TranslationGroupID : $this->owner->ID); + unset($this->owner->_TranslatableIsNewRecord); + unset($this->owner->_TranslationGroupID); + } + + } + + /** + * Remove the record from the translation group mapping. + */ + function onBeforeDelete() { + // @todo Coupling to Versioned, we need to avoid removing + // translation groups if records are just deleted from a stage + // (="unpublished"). Ideally the translation group tables would + // be specific to different Versioned changes, making this restriction unnecessary. + // This will produce orphaned translation group records for SiteTree subclasses. + if(!$this->owner->hasExtension('Versioned')) { + $this->removeTranslationGroup(); + } + + parent::onBeforeDelete(); + } + + /** + * Attempt to get the page for a link in the default language that has been translated. + * + * @param string $URLSegment + * @param int|null $parentID + * @return SiteTree + */ + public function alternateGetByLink($URLSegment, $parentID) { + // If the parentID value has come from a translated page, then we need to find the corresponding parentID value + // in the default Locale. + if ( + is_int($parentID) + && $parentID > 0 + && ($parent = DataObject::get_by_id('SiteTree', $parentID)) + && ($parent->isTranslation()) + ) { + $parentID = $parent->getTranslationGroup(); + } + + // Find the locale language-independent of the page + self::disable_locale_filter(); + $default = DataObject::get_one ( + 'SiteTree', + sprintf ( + '"URLSegment" = \'%s\'%s', + Convert::raw2sql($URLSegment), + (is_int($parentID) ? " AND \"ParentID\" = $parentID" : null) + ), + false + ); + self::enable_locale_filter(); + + return $default; + } + + //-----------------------------------------------------------------------------------------------// + + /** + * If the record is not shown in the default language, this method + * will try to autoselect a master language which is shown alongside + * the normal formfields as a readonly representation. + * This gives translators a powerful tool for their translation workflow + * without leaving the translated page interface. + * Translatable also adds a new tab "Translation" which shows existing + * translations, as well as a formaction to create new translations based + * on a dropdown with available languages. + * + * @todo This is specific to SiteTree and CMSMain + * @todo Implement a special "translation mode" which triggers display of the + * readonly fields, so you can translation INTO the "default language" while + * seeing readonly fields as well. + */ + function updateCMSFields(FieldSet &$fields) { + if(!class_exists('SiteTree')) return; + // Don't apply these modifications for normal DataObjects - they rely on CMSMain logic + if(!($this->owner instanceof SiteTree)) return; + + // used in CMSMain->init() to set language state when reading/writing record + $fields->push(new HiddenField("Locale", "Locale", $this->owner->Locale) ); + + // Don't allow translation of virtual pages because of data inconsistencies (see #5000) + if(class_exists('VirtualPage')){ + $excludedPageTypes = array('VirtualPage'); + foreach($excludedPageTypes as $excludedPageType) { + if(is_a($this->owner, $excludedPageType)) return; + } + } + + $excludeFields = array( + 'ViewerGroups', + 'EditorGroups', + 'CanViewType', + 'CanEditType' + ); + + // if a language other than default language is used, we're in "translation mode", + // hence have to modify the original fields + $creating = false; + $baseClass = $this->owner->class; + $allFields = $fields->toArray(); + while( ($p = get_parent_class($baseClass)) != "DataObject") $baseClass = $p; + + // try to get the record in "default language" + $originalRecord = $this->owner->getTranslation(Translatable::default_locale()); + // if no translation in "default language", fall back to first translation + if(!$originalRecord) { + $translations = $this->owner->getTranslations(); + $originalRecord = ($translations) ? $translations->First() : null; + } + + $isTranslationMode = $this->owner->Locale != Translatable::default_locale(); + + // Show a dropdown to create a new translation. + // This action is possible both when showing the "default language" + // and a translation. Include the current locale (record might not be saved yet). + $alreadyTranslatedLocales = $this->getTranslatedLocales(); + $alreadyTranslatedLocales[$this->owner->Locale] = $this->owner->Locale; + + if($originalRecord && $isTranslationMode) { + $originalLangID = Session::get($this->owner->ID . '_originalLangID'); + + // Remove parent page dropdown + $fields->removeByName("ParentType"); + $fields->removeByName("ParentID"); + + $translatableFieldNames = $this->getTranslatableFields(); + $allDataFields = $fields->dataFields(); + + $transformation = new Translatable_Transformation($originalRecord); + + // iterate through sequential list of all datafields in fieldset + // (fields are object references, so we can replace them with the translatable CompositeField) + foreach($allDataFields as $dataField) { + if($dataField instanceof HiddenField) continue; + if(in_array($dataField->Name(), $excludeFields)) continue; + + if(in_array($dataField->Name(), $translatableFieldNames)) { + // if the field is translatable, perform transformation + $fields->replaceField($dataField->Name(), $transformation->transformFormField($dataField)); + } else { + // else field shouldn't be editable in translation-mode, make readonly + $fields->replaceField($dataField->Name(), $dataField->performReadonlyTransformation()); + } + } + + } elseif($this->owner->isNew()) { + $fields->addFieldsToTab( + 'Root', + new Tab(_t('Translatable.TRANSLATIONS', 'Translations'), + new LiteralField('SaveBeforeCreatingTranslationNote', + sprintf('

%s

', + _t('Translatable.NOTICENEWPAGE', 'Please save this page before creating a translation') + ) + ) + ) + ); + } + + $fields->addFieldsToTab( + 'Root', + new Tab('Translations', _t('Translatable.TRANSLATIONS', 'Translations'), + new HeaderField('CreateTransHeader', _t('Translatable.CREATE', 'Create new translation'), 2), + $langDropdown = new LanguageDropdownField( + "NewTransLang", + _t('Translatable.NEWLANGUAGE', 'New language'), + $alreadyTranslatedLocales, + 'SiteTree', + 'Locale-English', + $this->owner + ), + $createButton = new InlineFormAction('createtranslation',_t('Translatable.CREATEBUTTON', 'Create')) + ) + ); + $createButton->includeDefaultJS(false); + + if($alreadyTranslatedLocales) { + $fields->addFieldToTab( + 'Root.Translations', + new HeaderField('ExistingTransHeader', _t('Translatable.EXISTING', 'Existing translations:'), 3) + ); + $existingTransHTML = '
    '; + foreach($alreadyTranslatedLocales as $i => $langCode) { + $existingTranslation = $this->owner->getTranslation($langCode); + if($existingTranslation) { + $existingTransHTML .= sprintf('
  • %s
  • ', + sprintf('admin/show/%d/?locale=%s', $existingTranslation->ID, $langCode), + i18n::get_locale_name($langCode) + ); + } + } + $existingTransHTML .= '
'; + $fields->addFieldToTab( + 'Root.Translations', + new LiteralField('existingtrans',$existingTransHTML) + ); + } + + $langDropdown->addExtraClass('languageDropdown'); + $createButton->addExtraClass('createTranslationButton'); + } + + /** + * Get the names of all translatable fields on this class + * as a numeric array. + * @todo Integrate with blacklist once branches/translatable is merged back. + * + * @return array + */ + function getTranslatableFields() { + return $this->translatableFields; + } + + /** + * Return the base table - the class that directly extends DataObject. + * @return string + */ + function baseTable($stage = null) { + $tableClasses = ClassInfo::dataClassesFor($this->owner->class); + $baseClass = array_shift($tableClasses); + return (!$stage || $stage == $this->defaultStage) ? $baseClass : $baseClass . "_$stage"; + } + + function extendWithSuffix($table) { + return $table; + } + + /** + * Gets all related translations for the current object, + * excluding itself. See {@link getTranslation()} to retrieve + * a single translated object. + * + * Getter with $stage parameter is specific to {@link Versioned} extension, + * mostly used for {@link SiteTree} subclasses. + * + * @param string $locale + * @param string $stage + * @return DataObjectSet + */ + function getTranslations($locale = null, $stage = null) { + if($locale && !i18n::validate_locale($locale)) throw new InvalidArgumentException(sprintf('Invalid locale "%s"', $locale)); + + if($this->owner->exists()) { + // HACK need to disable language filtering in augmentSQL(), + // as we purposely want to get different language + self::disable_locale_filter(); + + $translationGroupID = $this->getTranslationGroup(); + + $baseDataClass = ClassInfo::baseDataClass($this->owner->class); + $filter = sprintf('"%s_translationgroups"."TranslationGroupID" = %d', $baseDataClass, $translationGroupID); + if($locale) { + $filter .= sprintf(' AND "%s"."Locale" = \'%s\'', $baseDataClass, Convert::raw2sql($locale)); + } else { + // exclude the language of the current owner + $filter .= sprintf(' AND "%s"."Locale" != \'%s\'', $baseDataClass, $this->owner->Locale); + } + $join = sprintf('LEFT JOIN "%s_translationgroups" ON "%s_translationgroups"."OriginalID" = "%s"."ID"', + $baseDataClass, + $baseDataClass, + $baseDataClass + ); + $currentStage = Versioned::current_stage(); + if($this->owner->hasExtension("Versioned")) { + if($stage) Versioned::reading_stage($stage); + $translations = Versioned::get_by_stage( + $this->owner->class, + Versioned::current_stage(), + $filter, + null, + $join + ); + if($stage) Versioned::reading_stage($currentStage); + } else { + $translations = DataObject::get($this->owner->class, $filter, null, $join); + } + + self::enable_locale_filter(); + + return $translations; + } + } + + /** + * Gets an existing translation based on the language code. + * Use {@link hasTranslation()} as a quicker alternative to check + * for an existing translation without getting the actual object. + * + * @param String $locale + * @return DataObject Translated object + */ + function getTranslation($locale, $stage = null) { + if($locale && !i18n::validate_locale($locale)) throw new InvalidArgumentException(sprintf('Invalid locale "%s"', $locale)); + + $translations = $this->getTranslations($locale, $stage); + return ($translations) ? $translations->First() : null; + } + + /** + * Creates a new translation for the owner object of this decorator. + * Checks {@link getTranslation()} to return an existing translation + * instead of creating a duplicate. Writes the record to the database before + * returning it. Use this method if you want the "translation group" + * mechanism to work, meaning that an object knows which group of translations + * it belongs to. For "original records" which are not created through this + * method, the "translation group" is set in {@link onAfterWrite()}. + * + * @param string $locale + * @return DataObject The translated object + */ + function createTranslation($locale) { + if($locale && !i18n::validate_locale($locale)) throw new InvalidArgumentException(sprintf('Invalid locale "%s"', $locale)); + + if(!$this->owner->exists()) { + user_error('Translatable::createTranslation(): Please save your record before creating a translation', E_USER_ERROR); + } + + // permission check + if(!$this->owner->canTranslate(null, $locale)) { + throw new Exception(sprintf( + 'Creating a new translation in locale "%s" is not allowed for this user', + $locale + )); + return; + } + + $existingTranslation = $this->getTranslation($locale); + if($existingTranslation) return $existingTranslation; + + $class = $this->owner->class; + $newTranslation = new $class; + + // copy all fields from owner (apart from ID) + $newTranslation->update($this->owner->toMap()); + + // If the object has Hierarchy extension, + // check for existing translated parents and assign + // their ParentID (and overwrite any existing ParentID relations + // to parents in other language). If no parent translations exist, + // they are automatically created in onBeforeWrite() + if($newTranslation->hasField('ParentID')) { + $origParent = $this->owner->Parent(); + $newTranslationParent = $origParent->getTranslation($locale); + if($newTranslationParent) $newTranslation->ParentID = $newTranslationParent->ID; + } + + $newTranslation->ID = 0; + $newTranslation->Locale = $locale; + + $originalPage = $this->getTranslation(self::default_locale()); + if ($originalPage) { + $urlSegment = $originalPage->URLSegment; + } else { + $urlSegment = $newTranslation->URLSegment; + } + $newTranslation->URLSegment = $urlSegment . '-' . i18n::convert_rfc1766($locale); + // hacky way to set an existing translation group in onAfterWrite() + $translationGroupID = $this->getTranslationGroup(); + $newTranslation->_TranslationGroupID = $translationGroupID ? $translationGroupID : $this->owner->ID; + $newTranslation->write(); + + return $newTranslation; + } + + /** + * Caution: Does not consider the {@link canEdit()} permissions. + * + * @param DataObject|int $member + * @param string $locale + * @return boolean + */ + function canTranslate($member = null, $locale) { + if($locale && !i18n::validate_locale($locale)) throw new InvalidArgumentException(sprintf('Invalid locale "%s"', $locale)); + + if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) $member = Member::currentUser(); + + // check for locale + $allowedLocale = ( + !is_array(self::get_allowed_locales()) + || in_array($locale, self::get_allowed_locales()) + ); + + if(!$allowedLocale) return false; + + // By default, anyone who can edit a page can edit the default locale + if($locale == self::default_locale()) return true; + + // check for generic translation permission + if(Permission::checkMember($member, 'TRANSLATE_ALL')) return true; + + // check for locale specific translate permission + if(!Permission::checkMember($member, 'TRANSLATE_' . $locale)) return false; + + return true; + } + + /** + * @return boolean + */ + function canEdit($member) { + if(!$this->owner->Locale) return null; + return $this->owner->canTranslate($member, $this->owner->Locale) ? null : false; + } + + /** + * Returns TRUE if the current record has a translation in this language. + * Use {@link getTranslation()} to get the actual translated record from + * the database. + * + * @param string $locale + * @return boolean + */ + function hasTranslation($locale) { + if($locale && !i18n::validate_locale($locale)) throw new InvalidArgumentException(sprintf('Invalid locale "%s"', $locale)); + + return ( + $this->owner->Locale == $locale + || array_search($locale, $this->getTranslatedLocales()) !== false + ); + } + + function AllChildrenIncludingDeleted($context = null) { + $children = $this->owner->doAllChildrenIncludingDeleted($context); + + return $children; + } + + /** + * Returns markup for insertion into + * a HTML4/XHTML compliant section, listing all available translations + * of a page. + * + * @see http://www.w3.org/TR/html4/struct/links.html#edef-LINK + * @see http://www.w3.org/International/articles/language-tags/ + * + * @return string HTML + */ + function MetaTags(&$tags) { + $template = '' . "\n"; + $translations = $this->owner->getTranslations(); + if($translations) foreach($translations as $translation) { + $tags .= sprintf($template, + Convert::raw2xml($translation->Title), + i18n::convert_rfc1766($translation->Locale), + $translation->Link() + ); + } + } + + function providePermissions() { + if(!Object::has_extension('SiteTree', 'Translatable') || !class_exists('SiteTree')) return false; + + $locales = self::get_allowed_locales(); + + // Fall back to any locales used in existing translations (see #4939) + if(!$locales) { + $locales = DB::query('SELECT "Locale" FROM "SiteTree" GROUP BY "Locale"')->column(); + } + + $permissions = array(); + if($locales) foreach($locales as $locale) { + $localeName = i18n::get_locale_name($locale); + $permissions['TRANSLATE_' . $locale] = sprintf( + _t( + 'Translatable.TRANSLATEPERMISSION', + 'Translate %s', + PR_MEDIUM, + 'Translate pages into a language' + ), + $localeName + ); + } + + $permissions['TRANSLATE_ALL'] = _t( + 'Translatable.TRANSLATEALLPERMISSION', + 'Translate into all available languages' + ); + + return $permissions; + } + + /** + * Get a list of languages with at least one element translated in (including the default language) + * + * @param string $className Look for languages in elements of this class + * @param string $where Optional SQL WHERE statement + * @return array Map of languages in the form locale => langName + */ + static function get_existing_content_languages($className = 'SiteTree', $where = '') { + $baseTable = ClassInfo::baseDataClass($className); + $query = new SQLQuery("Distinct \"Locale\"","\"$baseTable\"",$where, '', "\"Locale\""); + $dbLangs = $query->execute()->column(); + $langlist = array_merge((array)Translatable::default_locale(), (array)$dbLangs); + $returnMap = array(); + $allCodes = array_merge(i18n::$all_locales, i18n::$common_locales); + foreach ($langlist as $langCode) { + if($langCode && isset($allCodes[$langCode])) { + $returnMap[$langCode] = (is_array($allCodes[$langCode])) ? $allCodes[$langCode][0] : $allCodes[$langCode]; + } + } + return $returnMap; + } + + /** + * Get the RelativeLink value for a home page in another locale. This is found by searching for the default home + * page in the default language, then returning the link to the translated version (if one exists). + * + * @return string + */ + public static function get_homepage_link_by_locale($locale) { + $originalLocale = self::get_current_locale(); + + self::set_current_locale(self::default_locale()); + $original = SiteTree::get_by_link(RootURLController::get_default_homepage_link()); + self::set_current_locale($originalLocale); + + if($original) { + if($translation = $original->getTranslation($locale)) return trim($translation->RelativeLink(true), '/'); + } + } + + + /** + * @deprecated 2.4 Use {@link Translatable::get_homepage_link_by_locale()} + */ + static function get_homepage_urlsegment_by_locale($locale) { + user_error ( + 'Translatable::get_homepage_urlsegment_by_locale() is deprecated, please use get_homepage_link_by_locale()', + E_USER_NOTICE + ); + + return self::get_homepage_link_by_locale($locale); + } + + /** + * Define all locales which in which a new translation is allowed. + * Checked in {@link canTranslate()}. + * + * @param array List of allowed locale codes (see {@link i18n::$all_locales}). + * Example: array('de_DE','ja_JP') + */ + static function set_allowed_locales($locales) { + self::$allowed_locales = $locales; + } + + /** + * Get all locales which are generally permitted to be translated. + * Use {@link canTranslate()} to check if a specific member has permission + * to translate a record. + * + * @return array + */ + static function get_allowed_locales() { + return self::$allowed_locales; + } + + /** + * @deprecated 2.4 Use get_homepage_urlsegment_by_locale() + */ + static function get_homepage_urlsegment_by_language($locale) { + return self::get_homepage_urlsegment_by_locale($locale); + } + + /** + * @deprecated 2.4 Use custom check: self::$default_locale == self::get_current_locale() + */ + static function is_default_lang() { + return (self::$default_locale == self::get_current_locale()); + } + + /** + * @deprecated 2.4 Use set_default_locale() + */ + static function set_default_lang($lang) { + self::set_default_locale(i18n::get_locale_from_lang($lang)); + } + + /** + * @deprecated 2.4 Use get_default_locale() + */ + static function get_default_lang() { + return i18n::get_lang_from_locale(self::default_locale()); + } + + /** + * @deprecated 2.4 Use get_current_locale() + */ + static function current_lang() { + return i18n::get_lang_from_locale(self::get_current_locale()); + } + + /** + * @deprecated 2.4 Use set_current_locale() + */ + static function set_reading_lang($lang) { + self::set_current_locale(i18n::get_locale_from_lang($lang)); + } + + /** + * @deprecated 2.4 Use get_reading_locale() + */ + static function get_reading_lang() { + return i18n::get_lang_from_locale(self::get_reading_locale()); + } + + /** + * @deprecated 2.4 Use default_locale() + */ + static function default_lang() { + return i18n::get_lang_from_locale(self::default_locale()); + } + + /** + * @deprecated 2.4 Use get_by_locale() + */ + static function get_by_lang($class, $lang, $filter = '', $sort = '', $join = "", $limit = "", $containerClass = "DataObjectSet", $having = "") { + return self::get_by_locale($class, i18n::get_locale_from_lang($lang), $filter, $sort, $join, $limit, $containerClass, $having); + } + + /** + * @deprecated 2.4 Use get_one_by_locale() + */ + static function get_one_by_lang($class, $lang, $filter = '', $cache = false, $orderby = "") { + return self::get_one_by_locale($class, i18n::get_locale_from_lang($lang), $filter, $cache, $orderby); + } + + /** + * Determines if the record has a locale, + * and if this locale is different from the "default locale" + * set in {@link Translatable::default_locale()}. + * Does not look at translation groups to see if the record + * is based on another record. + * + * @return boolean + * @deprecated 2.4 + */ + function isTranslation() { + return ($this->owner->Locale && ($this->owner->Locale != Translatable::default_locale())); + } + + /** + * @deprecated 2.4 Use choose_site_locale() + */ + static function choose_site_lang($langsAvail=null) { + return self::choose_site_locale($langsAvail); + } + + /** + * @deprecated 2.4 Use getTranslatedLocales() + */ + function getTranslatedLangs() { + return $this->getTranslatedLocales(); + } + + /** + * Return a piece of text to keep DataObject cache keys appropriately specific + */ + function cacheKeyComponent() { + return 'locale-'.self::get_current_locale(); + } + + /** + * Extends the SiteTree::validURLSegment() method, to do checks appropriate + * to Translatable + * + * @return bool + */ + public function augmentValidURLSegment() { + if (self::locale_filter_enabled()) { + self::disable_locale_filter(); + $reEnableFilter = true; + } + $IDFilter = ($this->owner->ID) ? "AND \"SiteTree\".\"ID\" <> {$this->owner->ID}" : null; + $parentFilter = null; + + if(SiteTree::nested_urls()) { + if($this->owner->ParentID) { + $parentFilter = " AND \"SiteTree\".\"ParentID\" = {$this->owner->ParentID}"; + } else { + $parentFilter = ' AND "SiteTree"."ParentID" = 0'; + } + } + + $existingPage = DataObject::get_one( + 'SiteTree', + "\"URLSegment\" = '{$this->owner->URLSegment}' $IDFilter $parentFilter", + false // disable get_one cache, as this otherwise may pick up results from when locale_filter was on + ); + if ($reEnableFilter) { + self::enable_locale_filter(); + } + return !$existingPage; + } + +} + +/** + * Transform a formfield to a "translatable" representation, + * consisting of the original formfield plus a readonly-version + * of the original value, wrapped in a CompositeField. + * + * @param DataObject $original Needs the original record as we populate the readonly formfield with the original value + * + * @package sapphire + * @subpackage misc + */ +class Translatable_Transformation extends FormTransformation { + + /** + * @var DataObject + */ + private $original = null; + + function __construct(DataObject $original) { + $this->original = $original; + parent::__construct(); + } + + /** + * Returns the original DataObject attached to the Transformation + * + * @return DataObject + */ + function getOriginal() { + return $this->original; + } + + /** + * @todo transformTextareaField() not used at the moment + */ + function transformTextareaField(TextareaField $field) { + $nonEditableField = new ToggleField($fieldname,$field->Title(),'','+','-'); + $nonEditableField->labelMore = '+'; + $nonEditableField->labelLess = '-'; + return $this->baseTransform($nonEditableField, $field); + + return $nonEditableField; + } + + function transformFormField(FormField $field) { + $newfield = $field->performReadOnlyTransformation(); + return $this->baseTransform($newfield, $field); + } + + protected function baseTransform($nonEditableField, $originalField) { + $fieldname = $originalField->Name(); + + $nonEditableField_holder = new CompositeField($nonEditableField); + $nonEditableField_holder->setName($fieldname.'_holder'); + $nonEditableField_holder->addExtraClass('originallang_holder'); + $nonEditableField->setValue($this->original->$fieldname); + $nonEditableField->setName($fieldname.'_original'); + $nonEditableField->addExtraClass('originallang'); + $nonEditableField->setTitle('Original '.$originalField->Title()); + + $nonEditableField_holder->insertBefore($originalField, $fieldname.'_original'); + return $nonEditableField_holder; + } + + +} + +?> diff --git a/code/tasks/MigrateTranslatableTask.php b/code/tasks/MigrateTranslatableTask.php new file mode 100644 index 0000000..19b941c --- /dev/null +++ b/code/tasks/MigrateTranslatableTask.php @@ -0,0 +1,176 @@ +Limitations + * + * - Information from the {@link Versioned} extension (e.g. in "SiteTree_versions" table) + * will be discarded for translated records. + * - Custom translatable fields on your own {@link Page} class or subclasses thereof won't + * be migrated into the translation. + * - 2.1-style subtags of a language (e.g. "en") will be automatically disambiguated to their full + * locale value (e.g. "en_US"), by the lookup defined in {@link i18n::get_locale_from_lang()}. + * - Doesn't detect published translations when the script is run twice on the same data set + * + *

Usage

+ * + * PLEASE BACK UP YOUR DATABASE BEFORE RUNNING THIS SCRIPT. + * + * Warning: Please run dev/build on your 2.2 database to update the schema before running this task. + * The dev/build command will rename tables like "SiteTree_lang" to "_obsolete_SiteTree_lang". + * + *

Commandline

+ * Requires "sake" tool (see http://doc.silverstripe.com/?id=sake) + * + * sake dev/tasks/MigrateTranslatableTask + * + * + *

Browser

+ * + * http://mydomain.com/dev/tasks/MigrateTranslatableTask + * + * + * @package sapphire + * @subpackage tasks + */ +class MigrateTranslatableTask extends BuildTask { + protected $title = "Migrate Translatable Task"; + + protected $description = "Migrates site translations from SilverStripe 2.1/2.2 to new database structure."; + + function init() { + parent::init(); + + $canAccess = (Director::isDev() || Director::is_cli() || Permission::check("ADMIN")); + if(!$canAccess) return Security::permissionFailure($this); + } + + function run($request) { + $ids = array(); + + echo "#################################\n"; + echo "# Adding translation groups to existing records" . "\n"; + echo "#################################\n"; + + $allSiteTreeIDs = DB::query('SELECT "ID" FROM "SiteTree"')->column(); + if($allSiteTreeIDs) foreach($allSiteTreeIDs as $id) { + $original = DataObject::get_by_id('SiteTree', $id); + $existingGroupID = $original->getTranslationGroup(); + if(!$existingGroupID) $original->addTranslationGroup($original->ID); + $original->destroy(); + unset($original); + } + + DataObject::flush_and_destroy_cache(); + + echo sprintf("Created translation groups for %d records\n", count($allSiteTreeIDs)); + + foreach(array('Stage', 'Live') as $stage) { + echo "\n\n#################################\n"; + echo "# Migrating stage $stage" . "\n"; + echo "#################################\n"; + + $suffix = ($stage == 'Live') ? '_Live' : ''; + + // First get all entries in SiteTree_lang + // This should be all translated pages + $trans = DB::query(sprintf('SELECT * FROM "_obsolete_SiteTree_lang%s"', $suffix)); + + // Iterate over each translated pages + foreach($trans as $oldtrans) { + $newLocale = i18n::get_locale_from_lang($oldtrans['Lang']); + + echo sprintf( + "Migrating from %s to %s translation of '%s' (#%d)\n", + $oldtrans['Lang'], + $newLocale, + Convert::raw2xml($oldtrans['Title']), + $oldtrans['OriginalLangID'] + ); + + // Get the untranslated page + + $original = Versioned::get_one_by_stage( + $oldtrans['ClassName'], + $stage, + '"SiteTree"."ID" = ' . $oldtrans['OriginalLangID'] + ); + + if(!$original) { + echo sprintf("Couldn't find original for #%d", $oldtrans['OriginalLangID']); + continue; + } + + // write locale to $original + $original->Locale = i18n::get_locale_from_lang(Translatable::default_lang()); + $original->writeToStage($stage); + + // Clone the original, and set it up as a translation + $existingTrans = $original->getTranslation($newLocale, $stage); + + if($existingTrans) { + echo sprintf("Found existing new-style translation for #%d. Already merged? Skipping.\n", $oldtrans['OriginalLangID']); + continue; + } + + // Doesn't work with stage/live split + //$newtrans = $original->createTranslation($newLocale); + + $newtrans = $original->duplicate(false); + $newtrans->OriginalID = $original->ID; + // we have to "guess" a locale based on the language + $newtrans->Locale = $newLocale; + if($stage == 'Live' && array_key_exists($original->ID, $ids)) { + $newtrans->ID = $ids[$original->ID]; + } + + // Look at each class in the ancestry, and see if there is a _lang table for it + foreach(ClassInfo::ancestry($oldtrans['ClassName']) as $classname) { + $oldtransitem = false; + + // If the class is SiteTree, we already have the DB record, else check for the table and get the record + if($classname == 'SiteTree') { + $oldtransitem = $oldtrans; + } elseif(in_array(strtolower($classname) . '_lang', DB::tableList())) { + $oldtransitem = DB::query(sprintf( + 'SELECT * FROM "_obsolete_%s_lang%s" WHERE "OriginalLangID" = %d AND "Lang" = \'%s\'', + $classname, + $suffix, + $original->ID, + $oldtrans['Lang'] + ))->first(); + } + + // Copy each translated field into the new translation + if($oldtransitem) foreach($oldtransitem as $key => $value) { + if(!in_array($key, array('ID', 'OriginalLangID'))) { + $newtrans->$key = $value; + } + } + + } + + // Write the new translation to the database + $sitelang = Translatable::get_current_locale(); + Translatable::set_current_locale($newtrans->Locale); + $newtrans->writeToStage($stage); + Translatable::set_current_locale($sitelang); + + $newtrans->addTranslationGroup($original->getTranslationGroup(), true); + + + if($stage == 'Stage') { + $ids[$original->ID] = $newtrans->ID; + } + } + } + + echo "\n\n#################################\n"; + echo "Done!\n"; + } +} + +?> \ No newline at end of file diff --git a/css/CMSMain.Translatable.css b/css/CMSMain.Translatable.css new file mode 100644 index 0000000..82aaf03 --- /dev/null +++ b/css/CMSMain.Translatable.css @@ -0,0 +1,13 @@ +ul.tree span.untranslated a:link, +ul.tree span.untranslated a:hover, +ul.tree span.untranslated a:visited { + color: #ccc +} + +.right form .createTranslationButton .middleColumn { + background: none; +} + +.right form#Form_EditForm div.createTranslation { + margin-left: 0; +} diff --git a/docs/en/_images/translatable1.png b/docs/en/_images/translatable1.png new file mode 100644 index 0000000000000000000000000000000000000000..dbe4fd0202829b82a8b1048d5b2db44e5c0b8aa8 GIT binary patch literal 24062 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 Pd;t|j4F$Bk>AU|2U!feu literal 0 HcmV?d00001 diff --git a/docs/en/_images/translatable3.png b/docs/en/_images/translatable3.png new file mode 100644 index 0000000000000000000000000000000000000000..6b6f59a38165a980a92237950aa713d245bbd462 GIT binary patch literal 14011 zcmaibWmH_4m*DP_;O-KvfdGv*?ixIS;O@bl#v5teJ$P_;Yuw?H@7#0m zdH4NzfA-j8FWF0~X4R}!>xYVxG#WA?@~c;`&}3yK)Ly-M9slw^{toWtX(~CH{^}J^ zfULx44fpw@43LVRcIwWhiVai`V^-vzUkHuJee| z;u0sLi?YDCW_F1VYN_8p%!mWN68ocl@1=wee);q3Ppyp(0;TEO>YR7}@OVZ@nB+gd z$wX|LWjP(UtnZ^?`o3P@$x@GWb^&DGFFN^N^Y65h!?IQArzqlI8GIAV#stvgAP2+5 zUOolkBfI?J=2oftC37DCd^r?N`mt5i_~ye}u$T`~|CajGNS3X)-Q>+<5=F3W3;MnM z72!(}iIw8R8lB(Wx>uT^@AH;yWn1N}m;WqmmTJUsF_za*^ae>mI4_ZG!axv;E~=k+ zUY=lBvgCdH)+>Vp+HNiE104H#{!HRtUdH5K9Mk3fok=vW+0~uIw}C0D3U94W&%I-B z7c=6y2Oi^t_Xp#Rgaxa@@2Iit`ZXDx>k&F}nACc4CL(sQ zH%0`GEtbH`tH;as^*Bb{rkQW1lL+=seo>k{8*!W%Sg~t;Rsby@j4LC9D?cAZ7n4wi zu^A^QE?a>~&qT4^HR*!2zsRO?ro0BMX-c-#P3($(iP6S_L?%pDe4*EN)xSHAl#4o9n;5UY-yykR&m z76u%SZRgpwFYa1hUiXowT#0d9vspu8+S`kC?vgsqleG>Sf~c?(*N}mvU&@r+1-L0e zw{I*jIVX9vwceu*hAC9H_I=8~O3kk2)RIm30jub=HJ~%&Do^C1wJ)tC@=?BtN#bn^ z`Q{WGepaUSyb&9v5WApq1T+F_Ird7_ps~L~YbXf1Ef7jjl;KFr^NbzYNq;XJQQM$8 z(TwS-6ocPg!EU)f>cN)@k7mfxCel>O#3F3x%v@H{WXC&Ly*EC}+;aFO#BeFKI{i@J zlAlCly;C`4Cy{vs-eyyMu^KkgnNh$wJXl;ozSf?@7p@descaqP1T+ zA{MUD{C>g6fP8I_Yt)e!DV)QJskFA?K$nN4Di0y)$%chnpQ+_Y-^dA=#l%Wwvf;i4 ze?ZIyv9T)U8%4nEnJqtF2n!fKRPW_iOH-s88pg3Cj;hek9A<9gK)hu2Te82RHgWaX zAlt)pvUJ_PCjs`i8CegF;&dD{>FRa&)ztJ*_Pbp*xqzGNhvPkMS`^Y(?4t%Y8p@e| zH78_vZP!!HxC|-z6emBV>nR)#w@9Gx<~#yDSS}w=(-92fg}o}RY#TDC7Q1ImifNuS zK9H4Hj4pu^*%9k%*YG|i-cWEh|GL2e=}o!2{jBFJwdU)(5rUKrw93>wCvFpqvfzog z9_FN%N1j^T+7mw7EUJJtu?qC`=__CfR_FZjr^l~`_ zj-u-=cBNkT4EOhoEC9bmFeK>M*Z0&lp;)^GB-Mqt%OAf}b7uysG?;4YY|`41dW&qZ z{aN&Q`zdjp4XyYLkwe!&u{=*hDrxM*IrgEP6Ns{=A$4Fw4+pLkEkIaE*Xy5J5!iYP zN#m(t1*(b-co|?3A_TB2)CD}O6|~BRwNW@lr|h6x+k_u6Sop|5V4HJY;SOetdy(yV z<5+?pl8@G_!$D|jo~!#ittp^ibl^o4W|A;C+KH6knrzal*#zD&cna-PnvtVO<#%nK zy~)R!DIV3ig+I`t#FJ2ij-LgcCnIor(Yt8LxLx`;^ksafn{`z2Eama@i1gc3*qyo}s)34J@^g!2#_v+$% z`H8)b6H!k7vEhcX?vWe;b0hKvA9;BEjf!uEL9=H7w+Iq{1yEyrzs<<{M14J>l{|Wv zPDKdPS0~_Au#zIkm`Bg*SYDtCZ`!psBVPm&C988Srk}C!;xMK9I^{H-lMQm%B-?-K zd&F%(w1=8Mdz4P*m|6jWD!a${oR6Q`cgZ#(a+5^l4@>+(BONsp5I>zujYNrwJZ+cZ z*~#QZ-(y@8iuW?6oZp^$lznu1=)8&dpVI$bt>CCM6P8lpwFM`fTR$hgQQu$}`$N_h zG)A_-Fu-C+mPOk_d2SlzP+5a@MK8@;)!+RyEb~e5;-~v(-qxDB;x0x-?x6>qGtrrg zrcDOgNUL#?B&!duPI`D`aejq_ z{qIYXe-WK%h|yCJCOgHkryFd9L`0pii{p!^km`r<9iY6EHP!v7x5$up4+*MKSBO&h+L-oQ&cSRXUw%g(MW!;&phPD}W00k27rLw_ z%gLofS3I|qI}9*z)w6S{pVr3PVY6@rN5(xdS3ocz(N==xHrTJg{1Y2BW6OC*N&xaX zt3_fcB?Zzi|67&fH18&Z@JMG1>;ifIpiykSjO`8_Z-^9Bkvbqv2RR#WA=zDNzNgH z2=tm*vpWe43hQ{*1C16Rtb5!VY?*3j>X8 z_om{7O>0bh4DgD98D$RqJ^8p9Z(RFrPK0`F80ZrL`&`3Ez4KFR3FDIZ<6Tjb@qE*s zkm_EUuURh3azZ1r_%~U6%<3Eo+<_^iv4&JJ+5vXjuwsC@}iY6 z0r@-0&t{5U`Kc)QqlQ*Z+#?ZIam z0v^TLPZPoC_$*V41X(|My~dZtLg!LOS-JA(4#8LT>M^BwMsyeeA+9nd11!4^lx=6d zjkrMVz=22S=0qHT05VE_Jk`znaqZH?h#n049}ILI*16zii&+8 zomx%`5RG%8{O^oW$WoQ4tDYdys`r4B6KhPDuuEZ0YxM>7B&rvAJ%q=_y=s>DqqNc1 zkg%XK0T(JZ#ND0$5)X@a4}Xc1i|EHtgUx_dKks0zEhH4%w;R^7F$=46-K z)7s!qkf282Pb&)xnOh~` zw%~7+(D+*gr-C1i-wEC?tScAFGG!U}jun?!acPXQXGR97TBMhfCD{z!&$V zwc7xVhAL16?)G<`>uvG&(_6ZRC2GugEplxORu98A;9NdWClDFnhsy|RHYMn&;WCw8 z656rJ2UeyHKLNNzHi^>jDys66=N;PzN{2?5qb%R5lsD&!NTy<6ifksiXiPk6UoNYT z`y=OTkuLvo&1lMLbBIZNo%*6b@4fnG2}m|d8F`{46UbZ^7o?KYQ&J>nPZskY>1&48 zrZsynij?O_X)3bvFQo;f+Oks`uFB?5VXa|b(38E8{YDqFrT3v*+1MAqvvXtAV|sRO zf=EI6S0S+i<;FL@rt)u=a)?tcXo8(8KAaqA(!kUg`pNlweu(f0khl>ljduyV2HQ4$ zt)07YL4~^3yGpxPp)fe=^p|)NL3?Q(=M&;uz?i#WOm0% z$^-^7QmfDPB9xH1LrX45TyuA+HpEYm5m_gup^v6b;2___=|PL@;fhQH2SAL2q@xIF zs#$aM#7*=;>d6Q5B=5*l1rtqYK~eUkxQ!jSAH$id89xE1gCEBQgAFS!xPL?y7+RMiEcGy)SZYK4={lS=mB!rAq#L(-#&=~>2NyIJtA5W8 zDrTQ^Ky0g2G$dH8kY+cJ#P#j7-%p6i7-7;}@*o@nY0GFzbTK@33b}z+nrWM<&)4N$ zGQ~XV&f9`FuKQ6!rw_a`wZ{E+g!K%1oWOWe4v)gSN||p-7Ye*!f(W+{a7=VhDVMfw zW>9S@mA@2ijeo|EcQ&O?OURYzCR{<)51)&rm`$aiQq_bCwvwXO%$)i1xGS z;4X}$t#_D@K{oG;7a9mROk+2~AAvB_$xy#ePO(BbU|;$5MRTKk4o7tCRRCW(nc zk={~x$2-<32t2@q!QriD^=Qo2r*^?{RzbWuS`U$$@K=IxlTmZ%lZRX6$s-*HyA`Xr z=Jx%ZJYuG4y_Qt_Y+a08u9RXG01WWh0-ra@LFT(p&INoF2%7bm^;Jf>3%tC-1@+j{ zbNxrVRk6$L%X>nb)1{_;4VB0JF(x!&wipEL{s2;vC{k>BoV!=dRR*AOlWILF)Yz%L zJ_XQMo_Ib6)r&I}>I!DA*Y}QaYWbN2EkjfcMaU)Uy95|P3NBc&;!HJ?YZNha1441m zD{kfw`x0q~64c}(j)xKk5awu9K=CdA?^1ex3Vw>>L%@=NiU~tXkkj;g2ic4fBDsNS z`YDvqSw~UGj3x)kW4O7Ejc3Ziaw%2cIxrrD?O(+s`=C%0EBVQ^6N~fx5jv=1rl+iO z{Nr$aU5^x*OCQ#lQ%k_XsXieD6M8uXg%qJ5sV-RV#cFBuGsjSZkd@I!rwPnU5QxsL( zTY_78ucOEMmNdqJ6AuoqRuaJ=ZFy>iga9tKO68LwL^9C?OE$kmv=#)CEmWFH=V<|G zcKVq2Zr0aD)Kzu0ME)M?(mRV&h&GC^dnlp=+S76f#+VB&9Nmxhf+OkwbS5lelSU~T zx=|0}s>0;v#;Oj}SmrpMgGxX{cxvKC_!UywSBQZ6CK>5P8tA-56ne1HRQf6XbID6HXgaM%g)2GM(ChE& zCG5kgAK3!(->_dZp^U2+3|9DjrYXFYdD`jE%h_h*iV+1 z;E+Y6#1rR=O3_jsYF1`g%cHWgLvV2B#26NfdF*K9!2;?Krv2mS(I)|ibr0tY;;?4- zq<&7p57I?vr!>)c?C2V!;agh1_}Gin=dO~;I{rqb4`MLxRcy4()t zI;S!k=~W8&A(83y#YFzWix~A?`C%)8{TTe=jykhO^_&f8CQkJL8WSLzz z1@>y9m;XL~n=L4e6*MMGxRioz?SX&(v~EcvGlZ?V{5-hEKJ+#jo+{;yQ1l`PrtO+) z>vg+4mO2wa71OEj-W}=dseLC_L;#amZ7}1G+hkx*w$MBf*{ESDzD0MRR8=&eg8Ra9r<^Sa`1tlY06dK*Sa1+aM%5Lt-gOES$lwHaD|0 zXQ{hfU$3!-`=^oTNkCYi3>DZVsV=P!{t=dc3dZEd4=6xgFh(Rm#e)@)5l z3Mjtg$D9;6vD#DpVP!fz^D&(G(WGEYF$jNuwKIHqhFZxnu;owfuo877O+!1*_w<}x zl)BR$s?!?Yrel=C>ty+xh3R@CcS+XcL;W;a2d2A#?HLyc6^9RpVNYttiv;|Y!md>9 z*olP9&GS}+d;a>%J%CQ<8BNP%vlBORogFO4;d`+iQq$^x@4(m&puG%@t>? zy>V(Zpvl8cPOhAF#qP+2sgKkTTM6wF*E5vZ&6(ATpyn^D5(8iZzVH;5Sf19%Bu46>M2kQ5OxnONgd0-Hb>p8y5?H<@`T%SI>re@xl`Wyv~G{#v>JuPOVtA)GgKhwp2a zpp2Ps?eu}*e@8wBAN53Do&r^s`UFTZagZF zsEP-8xpxVEst+IP%eTjh{5EBg3HuLr6#ihW5!b;>*1WBit^pi$`ke z9SzYw*#PEH4=B_<#=P11iowDAsYi@g+v(}O_X-zD;x_NZ!&TnH=y%88RJ1c-e58f2 zmX|B+XAcGd?J$d@u@^r}U^N#f);x7j!k zQ-2oU6({o&xm8Z1$;*s4=HLH>9wz~E?pAB9r@hZXWs@~hdYturD;$f^C;xcr^g(7T z_@%XEO&S#32NNP9(ydg3=$4xhh%ca<JggaZ4`I@CU4W{ zYs%SM)|JHI1f_#%;zsJqZLIkWC0;al%y6>Bkx+E&_cu%Xq*WxPYYt=lh#Ep+W1+hj zXZ9eNv0*bMGyP&1oTqE!>FMa{N!szBUhIE4xN>D4N>PipJDXkaOF{M)o5xAC3S+GP zp&etZAlR*UZ6$9mCZMDvPpyyb;S@8UhQ8F1?k56;CV%V6f$< zKAmPc`JS;hHp})&z_&3Ayje!b>Vu(paU%b7)H5ibbqa{POBI_-H`MUl-Z5+`-h^bX_xckZ?~m48nmU~=W=)>oi7QMe7+=j2w@0eJcZ_dgT0Um zwLtt~9YT>d%8%ukElz6F6YRIJwZ+9nEC!c4gZqu6*G>v|Wmgz0K9^vYDIOT_)gqzA z(SAbwz{Jiv+5m;*QO5Y)_uQ!W4<+}t`H!FZ$X;wD4o!$gaw zg(0^{EVPLUeuLCE#BhEJ6uF_-Xw};oW-BxiMzO$il9gM?xRnWe*h82}=t=pNA~&qN*`FRUFyjW?11*Yu8$+y1Kq0j86PTEvqV~r>-L`%$@}kc zL_Qb~{g@FsXqjoleJ64>Lli+(PWSW#nl>=8UUPe1J1T3#HO){JA0Vm=e%6|oD^W>8 zeoJ>JWs3uBLEAFwO7X*lmhT5Xz;!Hmmbz^A_E85`y{^2hDzM8_R@Z$f7FkXVEKz3R zW@K?Yrq$$j_EuXsTTUs-dOVQ*D|(l;FaaHn`3VR3Csd4|UE>yW&W>2`TFwZ+2g!h2 zc=-gJ?{L3&uRb%2dLOk=q$*~xdKdQa({*1Vz_45c?) z;Irh5V@n-VuZaD953~DX&aB7qBFq;L#$A{hiX85@GBM6CB=m4OYbcmI9dli>ihqf| z9p1R%1?PVlQhcNNIlK1|y5g`oEynpMsSfLcqM!RHu3aNHlf2P{>nKe^%-|NqhU%74 zY;j?FY@wk}y?eVd8I3obFa5TyRp`^%j!X1eEyLvMR@P&gbF^=82uoT%K5m zdUN*7c9;8w{Kz&Is?NRzCGUJ3Yhj6Y)+;>J_Pe;VWL#Qiz7>rv)$)RDUakOw!Mvj+ zxKPhKDM6%efY0UhtnrJJo?EIlxku(e1i*!|G$P;nNt`=r>WDthYNz};x`g1)6pQCd zUE*uEvccly${!DE+io#dZbuDU@7m(aXDJt+f6Zta9y5vD&z(ZT|H9p@`cj^*ebYk= zmQLv%L)na|_YI6-H z_mjpQFWw70w_sdcx31eA9+6odwQ6eV&p>hI=9s-tilg9{H> zAbC6)YEz^OT@}Q0)!!smG4kH$lNsV<@xf;zK5qohy-WxI_>tQ@`K;rk zu4Fv07sUX!eC9j4Dyt|*e7>Ml_(&56a&)#b?z@=oK3rO>n%}Mnz!>!(>v+LJZyNL$ zE0viM(#kd#G>jM5qel)y(bu#xhvl6lR@$F3Sy0WXJp= z9XtPl+-KIm=t!GZt&+~p8Beleo(on3{k-lsS)U%e$NUmP_dKX!%SQ-=s!DS{#TD1h8QnKDD&!sXFbby@mh5L`(L93V+H2^lhW~@-oXDu z1VD`Lq5Hpm5AH4x-1$`hv4n80hpcVi^trjY|Mg`M23HQgy=I(NSzG(-ki6t}%7BsuOwq|+#B1H9J zLQqXVxK?f*VO0;;nC)Y4Sl}eYbvWcDb>(GHp-FZQBC)`Wt-U=_-Vw=q1-DGb6JlcC zY5b=%3DRzpwlW^~KHHXSTC^cw*n!eHwW-&iYkvGvB*vx?AKR|BN!I8&UUO}^<*$Bf zX*ZzH|2+pwW0u%EQ6I*~h(XKc#PXv(QM;qYWjppvT8+D}zQ(p+9(_IUz#-;}-%3{M zlgStCwyrjT)!PM4r{O!L#p%%zvYh?nfkmaDX!P z!aJJ zMWVc$sqzV4(cdhdWZrjDo_BiiJA?9L3HYD3vp(9MmrDM74~&;dWLW~4gbjo?BW+U3 z?mg~UtxlF{VNJ623M+4keQp|?c1m=eG?&gohkU3WF%9zDD4s4bnD@uJ*#wNKvdcJSn+GS`RI^FYBJW17-?0EA80fD@s|V95+KJcBsJ>q3Q25nry& zbJOZc)6*01C{EAkaE`yXN2U)sOB4V1RD5kivK9?j=)J?6)hQqQvJA+AB0_3JeK!u# zXuC}PmWUR|g3;WzcaHt87%9##%MH!MV@eIj8bXN%XxCB(ke2TCk9m=a=yDhSdv`}U zakEH;Yn6p6MddEA72{wBQx`Ld#k*j3B;O2oK7uxq7hrk;ICR#r+|c`kf6JyP*U=(4 z=)WXyw$|Fa4eQ@KS|Qu_Ke|DiVR-Ui>LA=1?h*drySLsG{?E$2V9r&N{(IklDbC!R7xQwPqy-ye^fAme7%Vu9qyI)@dH-}0asBwh@ zC*V`IX%4s#LQ(MD%L(&mQko84R8)KY)O=J10nF8Cw~zyDwC9}@1DF5^2k3bxfBNP| z7XyIaA@^hpgD3&o+)D{RGgfgY2EL~SJ37f#h`)?%;Vt7f69SFLv{cP_d))XT@)4%o z2QI>h8MY>$dQ8Di`01HfL-Ne=lOHn1I1ayCZ++@u{FPVu{il`nzd8`?V$AjA>Hv$F z0HmZ6=^5oUjtUyN)ox-FVkjmW#g!uTsj;>OYEhZJb9H4Cqr>NeVy((~YY7|=s_Vu0 zv)^%q>Q8%kcu>aZ?@)3JaO>nDrHuY&!J7E4A;I6a?uQi{T#S_`IJ4Cl!*T+H(G#}PM0 z2BY(@v`oLp3z&qK^bcnt-kJIQ* zL0}&IXXlAI z_J;l!#cdH=P{5BuiDsKbNxRtFh93-^q3gYVWvRQ-P6v5Nei11|cX1G`n2Y+4tgShy zX5_bvI`nkd3*!ZEO9MD^dGP=g=COA^XSGFh4dY}-O^S9d0o#&V&)Z5*4nq@IIK*gd zA~8+{>x(#-Q2gh)3W^U{wB{Rr8_k6|(R z`wQq`uFO@vy6G`R#ra!0cN$2BD4b77`5)0R$ROK2P6pphv@_!jUZGSm7sxI(pJR0K{7_iVtD`JZVO>eZccWm+p69IJ z{v1n5aC7~Fz#c9|~ z?dvJE5a9&pH~aO^NJzebTl(q1K`4xwO6SoRJ#wO-C8D|`6SEzX;m0H1yMObsy#MF5 z;V(cn{C%FA0lZXN4qi1^#amd;o1wBvjiIw3XjO$nu=|(O6I9kGu&;>}mpbL?Qavop ziUv!xiR&;nUZ#XAvPpW*<9HNCX?n%)LimMXOeVZFD#-4^^-_8@f&6T}Am5VX_^zE6 zGD`3N#YXU1rBOQKNREG{_GnC;ehOmSA);)_l@=jH@YDIol+$-azUaI+(*8~JW_%x0 zr9@);Hx6IUaNLjjZ|uFq1v|nKnIQ4a&f1_4E-4E&vE?2ymwVmi5{{``hc0k$@@27` zee1EIV!0kRIK^f+19&*Soz~5F{O+IW_pdx3$E^Wcwv7M{&^Jx)!pO95;au6E$Bn4& zY%UJ8#yW*K9h?~ZjWh+y1UXX#-sLF&JFQOZv^0$Ym#SzbhE2R+1Ay%SQb+|aApr1{ zsx4&-Cp+uljX)u4bb5Y@!^@ZJx3o1aMavLw6-=}llfEpYX(*s`@pj{L zK$({zucRmg&hO1C6Kds+uhQhr$avLOo8IW1$kqNWdIPiZ_>J-<^J8@~V7gvmW9&!9 zwWQA}J?-S;^UK{*{)$Kyzj|sr6^S5hJ3HmFNHiRYgIMK)t>Z-oD2>LwAGYw1@hUAg z;%3KCB#ULAEM`BWNLk`x*sTg2yOtk4Zn*7J6IiOcx!FU$Nmlquu~hz2Sz&P(HVJ+Z zVeQ`u4-Q{J6up5f9x;MDvX8PMRq%};9(mGtj=PAwBGE&-xi&)Z8-qT9z&jvyKl;_e zp169FoZyz$cpYpb#q+fX%wU)R`PZbdk}K;dP>2R#HHyCANPL-qKoj~fumRKKBP`N$Xe^eLfmX^mbdEeGK0LM+J1U1 zA=_}#`XPp|Y|Ze&iOdEVA7t<-OEI4-J;yabE0+Y7e|oGNt}GYO<<@_w$!Z|BHA_-2 zb`$im8OdIi`dZUz>}S7Y;EFc#!AH3=D_aE-9^e})9xX~Cuj%a@H9hZAqOhotHvJXiY4lSL zIt`UXyQy+JAKsGCx3{!9vw3B`)Zg&TvuqH22yUgiJmH)_<|FATZx(T~=-Z2DqoYF8 zHv)3HW7J+w@#nOi4pd~PzO5if8bzDevcxiX{NbZnb>!GsDdWkH5n{^QlzhV>&89+I9G?qvaSL$=jdrFSaF(b$-@9xaZ5&E@b%P>>fVY)7SHB;gK zb2_T;AB+2uu^HPYk+LQ&wxNJ0VKn7h?szKMRa|=|vG{JB_+7HN{?g8~t!mbATU7yG zXr%16_UGV9#oyR7b?9+mwRhlTDGnjS*$uQR$SM>_krm??CToFn;~iX3bC*~`iK(w_h9Q#`k%R4T#*gTAjEb}_sW zrY$%C+D574@#i1RJ5Jb zc``jKhoz4K8!kL67X4g6o>40a+xyfEtW81hw2nb6EM?URW}6V>W!~SLL4PdYkkcQY z!M}4o%JW@;M(g%0FlLo?c1O$PWl{_oAD}3Zeu|ZlPrZ7tCB}%8QBR(q+=DD{_qmPQ z@6)k<;6RzoGanARRb3pgXUix60A{FZjKAEqjVwdIUgt zHau_lXC2Kh0`m|_?nI~-B;WdbDK#-S2o-U-zVFaTFDb( z3Ob*7KSDL5b0*(A6M@>zRD0~Df@#2ml)QJ3oHba4xk#nyxd-nJ`C$k;IQy&ZLu=?F z6hoi5g6W~CdN5?MU(`h@6ephbKl`KsAzI;!ct*5=l^V()y34>$&{au5@0AWq&;TUW z=k0d@)@wAKz%)Jh+1#~hE1XY@_-}2|E%r`wJq3pu-%tbdVv_tQ0#{US*t;z}JPlu! zVA$ zl-PiUnvGSZFCy|55V!ahFRdiQ+S`4gztj8I5B+)~%uIhj5M~hj!fSwC3DmbR+#qa_ zw{EhZ(F6q`4qV1%ZVrGpSVkxLe%d5N`Br@ zsb#iakDB5!Db;38OI0ncCK!&rbuiU;#|nd<6MKOf0T5(BDRAJoXZsu=L2m7) zG=j*Vn%6((3&#z$15HP_n@#XKm`5s=>P5Ynq_S1+k-*87D3g&$&4kD<7L?$nKCR|r zI!vMX>*CM=cQ0XN7y1`o@?Y62p0u#DQKow9jA;Xw43kUvrJ%;A27|HXLPFTB%iX{} zi~0TU_3&nY;a$5Ey>3Az)gp0JRz@FZTJUu7VRe@xhGqFVYrDQd1bftSYR_URMVSNF zrNDrRlw>5n3!NtBak=(rP}RXNl~8QJlgF#X;m&2aPbUwHp5ORgV?>A}z0vHiF}p?E zj-n966c7f{8jg(I<4sZe!`vJ4+;Rpph++c9A)keQct=22Xs;xB?ZI81Q-husFKCG3 z36-)w!#Db+QWS{BnuT`!;0#-~t})(Cqrv!ahj*6TSAHIfhO(K!L3xER8j6NXOW|Wn zTR!-Ch5rr2K|p`=wxh^A6I*C?WB_fdC>gnGmy6KdNpdGTO~|N$Kt-!cb6`L{Q3cn_ z%gPNxqbNtjT&+=>axW9=Y_tYc$t47ay|XUxrp~FYET3#PUz@YZPW7l8Ey=Txk$wUm z+UU0S{HS`l6!T%N$7`6)CMcNh$h6rPI0bpl3hdvf@sk^R$(?oEj$YV85wnw+#MjWu|PEB5?#(thZGse*$@vnp_eI-g|KWQr6`nFZ(bmz)2mz zi!<|6&}TD-M3i%NtqKH}gVs1Dv0~RFvOs)2>@@sl`MrDa0%L==#rt1u=Y5J`Yk+kM z9mZ)9p7W(jm%UbxidKT8hS~I}Zx=Z1BMKR{_}kBif()GpikDr#J*9fs&O?Zg=V(7| z^7&YCEII=}aa??O+WJY6c(Sb+ME?RZ2Q@2B?*;?43yf4Ema`xoZmS5^~}vjcFxZN#nhEV|9Bs`k7! z$h75^Q3%)?b(j8~LraKp5_HCUca%^rTwrhvYK(O2;|06ya=2%I&3Y>QPpC>!*E_J{ zS>lHuTxU1oss)BY?!BTLkwOdlAiW*ZL?u+#SJ8`AYxUhi=fpp~+SA&$xx0FdG1)=F zc^T6JTj;SwA1T42!~445J;sB72I8N;mZ8(=5_Ed!Y8=C;C?LV|`A-1!Ww@JZP&60v z+$66CPZF)gkn)D@mm5-Q?p5zlLN=a8;XSD7qhoOgvOFU3pbJAdmr}P97?yFR5ygso zt~cr=6rxCjl(UFNW{O3)wR#Hi#Ld+=O{{V-u!ejsd literal 0 HcmV?d00001 diff --git a/docs/en/_images/translatable4_small.png b/docs/en/_images/translatable4_small.png new file mode 100644 index 0000000000000000000000000000000000000000..77999a691677193e2a23158800ad8734544d7c43 GIT binary patch literal 79692 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

#Tz5XGQWffJ+N2Di8yH!2+w|aU?1V9V2m3a6 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 T(&i}cp|4kp8VZ$imLdNK2aS|? literal 0 HcmV?d00001 diff --git a/docs/en/index.md b/docs/en/index.md new file mode 100644 index 0000000..5470e2d --- /dev/null +++ b/docs/en/index.md @@ -0,0 +1,417 @@ +# Translation + +## Introduction + +This page introduces developers to using the CMS for creating content in multiple languages. + +Please refer to the `i18n` class in `sapphire` for a internationalization, globalization and localization support of built-in datatypes as well as translating templates and PHP code. + +Translations can be enabled for all subclasses of `[api:DataObject]`, so it can easily be implemented into existing code +with minimal interference. + +Warning: If you're upgrading from a SilverStripe version prior to 2.3.2, please migrate your datamodel before using the +extension (see below). + +## Requirements + +*SilverStripe 2.3.2* + +## Screenshots + +![](_images/translatable4_small.png) + +*Translated website* + + +![](_images/translatable1.png) + +*CMS: Language dropdown* + +![](_images/translatable2.png) + +*CMS: Translatable field with original value* + +![](_images/translatable3.png) + +*CMS: Create a new translation* + + +## Usage + +### Configuration + +#### ThroughObject::add_extension() + +Enabling Translatable through *Object::add_extension()* in your *mysite/_config.php*: + + :::php + Object::add_extension('SiteTree', 'Translatable'); + Object::add_extension('SiteConfig', 'Translatable'); // 2.4 or newer only + + +#### Through $extensions + + :::php + class Page extends SiteTree { + static $extensions = array( + "Translatable" + ); + } + + +Make sure to rebuild the database through /dev/build after enabling `[api:Translatable]`. +Use the correct set_default_locale() before building the database +for the first time, as this locale will be written on all new records. + +#### Setting the default locale + +

+**Important:** If the "default language" of your site is not english (en_US), please ensure to set the appropriate default +language for your content before building the database with Translatable enabled +
+ +Example: + + :::php + Translatable::set_default_locale(); + // Important: Call add_extension() after setting the default locale + Object::add_extension('SiteTree', 'Translatable'); + + +For the Translatable class, a "locale" consists of a language code plus a region code separated by an underscore, +for example "de_AT" for German language ("de") in the region Austria ("AT"). +See http://www.w3.org/International/articles/language-tags/ for a detailed description. + +To ensure that your template declares the correct content language, please see [i18n](i18n#declaring_the_content_language_in_html). + +### Usage + +Getting a translation for an existing instance: + + :::php + $translatedObj = Translatable::get_one_by_locale('MyObject', 'de_DE'); + + +Getting a translation for an existing instance: + + :::php + $obj = DataObject::get_by_id('MyObject', 99); // original language + $translatedObj = $obj->getTranslation('de_DE'); + + +Getting translations through Translatable::set_reading_locale(). +This is *not* a recommended approach, but sometimes unavoidable (e.g. for `[api:Versioned]` methods). + + :::php + $origLocale = Translatable::get_reading_locale(); + Translatable::set_reading_locale('de_DE'); + $obj = Versioned::get_one_by_stage('MyObject', "ID = 99"); + Translatable::set_reading_locale($origLocale); + + +Creating a translation: + + :::php + $obj = new MyObject(); + $translatedObj = $obj->createTranslation('de_DE'); + + + +### Usage for SiteTree + +`[api:Translatable]` can be used for subclasses of SiteTree as well. +If a child page translation is requested without the parent +page already having a translation in this language, the extension +will recursively create translations up the tree. +Caution: The "URLSegment" property is enforced to be unique across +languages by auto-appending the language code at the end. +You'll need to ensure that the appropriate "reading language" is set +before showing links to other pages on a website through $_GET['locale']. +Pages in different languages can have different publication states +through the `[api:Versioned]` extension. + +Note: You can't get Children() for a parent page in a different language +through set_reading_locale(). Get the translated parent first. + + :::php + // wrong + Translatable::set_reading_lang('de_DE'); + $englishParent->Children(); + // right + $germanParent = $englishParent->getTranslation('de_DE'); + $germanParent->Children(); + + + + + +### Translating custom properties + +Keep in mind that the `[api:Translatable]` extension currently doesn't support the exclusion of properties from being +translated - all custom properties will automatically be fetched from their translated record on the database. This means +you don't have to explicitly mark any custom properties as being translatable. + +The `[api:Translatable]` decorator applies only to the getCMSFields() method on DataObject or SiteTree, not to any fields +added in overloaded getCMSFields() implementations. See Translatable->updateCMSFields() for details. By default, custom +fields in the CMS won't show an original readonly value on a translated record, although they will save correctly. You can +attach this behaviour to custom fields by using Translatable_Transformation as shown below. + + :::php + class Page extends SiteTree { + + public static $db = array( + 'AdditionalProperty' => 'Text', + ); + + function getCMSFields() { + $fields = parent::getCMSFields(); + + // Add fields as usual + $additionalField = new TextField('AdditionalProperty'); + $fields->addFieldToTab('Root.Content.Main', $additionalField); + + // If a translation exists, exchange them with + // original/translation field pairs + $translation = $this->getTranslation(Translatable::default_locale()); + if($translation && $this->Locale != Translatable::default_locale()) { + $transformation = new Translatable_Transformation($translation); + $fields->replaceField( + 'AdditionalProperty', + $transformation->transformFormField($additionalField) + ); + } + + return $fields; + } + + } + + + +### Translating theHomepage + +Every homepage has a distinct URL, the default language is /home, a German translation by default would be /home-de_DE. +They can be accessed like any other translated page. If you want to access different homepages from the "root" without a +URL, add a "locale" GET parameter. The German homepage would also be accessible through /?locale=de_DE. + +For this to work, please ensure that the translated homepage is a direct translation of the default homepage, and not a +new page created through "Create page...". + +### Translation groups + +Each translation can have an associated "master" object in another language which it is based on, +as defined by the "MasterTranslationID" property. This relation is optional, meaning you can +create translations which have no representation in the "default language". +This "original" doesn't have to be in a default language, meaning +a french translation can have a german original, without either of them having a representation +in the default english language tree. +Caution: There is no versioning for translation groups, +meaning associating an object with a group will affect both stage and live records. + +SiteTree database table (abbreviated) + | ID | URLSegment | Title | Locale | + | -- | ---------- | ----- | ------ | + | 1 | about-us | About us | en_US | + | 2 | ueber-uns | Über uns | de_DE | + | 3 | contact | Contact | en_US | + +SiteTree_translationgroups database table + | TranslationGroupID | OriginalID | + | ------------------ | ---------- | + | 99 | 1 | + | 99 | 2 | + | 199 | 3 | + + +### CharacterSets + +
+**Caution:** Does not apply any character-set conversion, it is assumed that all content +is stored and represented in UTF-8 (Unicode). Please make sure your database and +HTML-templates adjust to this. +
+ +### "Default" languages + +
+**Important:** If the "default language" of your site is not english (en_US), +please ensure to set the appropriate default language for +your content before building the database with Translatable enabled +
+ +Example: + + :::php + Translatable::set_default_locale(); + + + +### Locales and language tags + +For the Translatable class, a "locale" consists of a language code plus a region code separated by an underscore, +for example "de_AT" for German language ("de") in the region Austria ("AT"). +See [http://www.w3.org/International/articles/language-tags/](http://www.w3.org/International/articles/language-tags/) +for a detailed description. + +Uninstalling/Disabling + +Disabling Translatable after creating translations will lead to all +pages being shown in the default sitetree regardless of their language. +It is advised to start with a new database after uninstalling Translatable, +or manually filter out translated objects through their "Locale" property +in the database. + +## Recipes + + +### Switching languages + +A widget now exists to switch between languages, and is [available here](http://www.silverstripe.org/Language-Chooser-Widget/). +You can easily make your own switchers with the following basic tools. To stay friendly to caches and search engines, each +translation of a page must have a unique URL. + +By URL: + + :::php + http:///mypage/?locale=de_DE + + +By user preference (place this in your Page_Controller->init() method): + + :::php + $member = Member::currentUser(); + if($member && $member->Locale) { + Translatable::set_reading_locale($member->Locale); + } + +### Templates + +As every page has its own unique URL, language selection mostly happens explicitly: A user requests a page, which always +has only one language. But how does a user coming to your English default language know that there's a Japanese version +of this page? +By default, SilverStripe core doesn't provide any switching of languages through sessions or browser cookies. As a +SEO-friendly CMS, it contains all this information in the URL. Each page in SilverStripe is aware of its translations +through the *getTranslations()* method. We can use this method in our template to build a simple language switcher. It +shows all available translations in an unordered list with links to the same page in a different language. The example +below can be inserted in any of your templates, for example `themes/blackcandy/templates/Layout/Page.ss`. + + :::php + <% if Translations %> +
+ <% end_if %> + + +Keep in mind that this will only show you available translations for the current page. The $Locale.Nice casting will +just work if your locale value is registered in i18n::get_common_locales(). + +### Page-control + +If you want to put static links in your template, which link to a site by their url, normally you can use the `<% control +Page(page-url) %>`. For sites which use Translatable, this is not possible for more than one language, because the url's +of different pages differ. + +For this case place the following function in your Page_Controller: + + :::php + public function PageByLang($url, $lang) { + $SQL_url = Convert::raw2sql($url); + $SQL_lang = Convert::raw2sql($lang); + + $page = Translatable::get_one_by_lang('SiteTree', $SQL_lang, "URLSegment = '$SQL_url'"); + + if ($page->Locale != Translatable::get_current_locale()) { + $page = $page->getTranslation(Translatable::get_current_locale()); + } + return $page; + } + +So, for example if you have a german page "Kontakt", which should be translated to english as "Contact", you may use: + + <% control PageByLang(Kontakt,de_DE) %> + +The control displays the link in the right language, depending on the current locale. + +Example: + + <% control PageByLang(Kontakt,de_DE) %> +

$Title

+ <% end_control %> + + +### Language Chooser Widget + +You can use a widget on your website to provide a list of links for switching languages: +[download](http://silverstripe.org/Language-Chooser-Widget-2/) + + +### Enabling the _t() function in templates + +If you're looking to use [the _t() function](http://doc.silverstripe.com/doku.php?id=i18n#the_t_function) in template +files, you'll need to [set the i18n locale](/topics/translation#setting_the_i18n_locale) first. + +(The reasoning is as follows: Translatable doesn't set the i18n locale. Historically these were two separate systems, +but they're reasonably interchangeable for a front-end website. The distinction is mainly valid for the CMS, because you +want the CMS to be in English (`[api:i18n]`), but edit pages in different languages (`[api:Translatable]`).) + +### Migrating from 2.1 datamodel + +The datamodel of `[api:Translatable]` changed significantly between its original release in SilverStripe 2.1 and SilverStripe +2.3.2. See our [discussion on the +mailinglist](http://groups.google.com/group/silverstripe-dev/browse_thread/thread/91e26e1f78d3c1b4/bd276dd5bbc56283?lnk=gst&q=translatable#bd276dd5bbc56283). + +To migrate a database that was built with SilverStripe 2.1.x or 2.2.x, follow these steps: + +* Upgrade your SilverStripe installation to at least 2.3.2 (see [upgrading](/installation/upgrading)) +* Backup your database content +* Login as an administrator +* Run `http://mysite.com/dev/build` +* Run `http://mysite.com/dev/tasks/MigrateTranslatableTask` + +Please see the `[api:MigrateTranslatableTask]` for +limitations of this migration task - not all your data will be preserved. + + +### Setting the i18n locale + +You can set the `[api:i18n]` locale value which is used to format dates, currencies and other regionally different values to +the same as your current page locale. + + :::php + class Page_Controller extends ContentController { + public function init() { + parent::init(); + + if($this->dataRecord->hasExtension('Translatable')) { + i18n::set_locale($this->dataRecord->Locale); + } + } + } + + +### Adding a new locale + +The `[api:i18n]` logic has lookup tables for common locales in i18n::$common_locales, which is a subset of i18n::$all_locales. +If your locale is not present here, you can simply add it through `mysite/_config.php`: + + :::php + i18n::$common_locales['de_AT'] = 'Deutsch (Oestereich)'; + +This should e.g. enable you to use `$Locale.Nice` in template code. + + +## Related + +* [translate.silverstripe.org](http://translate.silverstripe.org): Starting point for community-driven translation of the Silverstripe UI +* [i18n](i18n): Developer-level documentation of Silverstripe's i18n capabilities +* `[api:Translatable]`: DataObject-interface powering the website-content translations +* ["Translatable ModelAdmin" module](http://silverstripe.org/translatablemodeladmin-module/): An extension which allows +translations of `[api:DataObject]`s inside `[api:ModelAdmin]` diff --git a/javascript/CMSMain.Translatable.js b/javascript/CMSMain.Translatable.js new file mode 100755 index 0000000..4a34ad7 --- /dev/null +++ b/javascript/CMSMain.Translatable.js @@ -0,0 +1,77 @@ +/** + * File: CMSMain.Translatable.js + */ +(function($) { + $.entwine('ss', function($){ + + /** + * Class: .CMSMain #Form_LangForm + * + * Dropdown with languages above CMS tree, causing a redirect upon translation + */ + $('.CMSMain #Form_LangForm').entwine({ + /** + * Constructor: onmatch + */ + onmatch: function() { + var self = this; + + // monitor form loading for any locale changes + $('#Form_EditForm').bind('loadnewpage', function(e) { + var newLocale = $(this).find(':input[name=Locale]').val(); + if(newLocale) self.val(newLocale); + }); + + // whenever a new value is selected, reload the whole CMS in the new locale + this.find(':input[name=Locale]').bind('change', function(e) { + var url = document.location.href; + url += (url.indexOf('?') != -1) ? '&' : '?'; + // TODO Replace existing locale GET params + url += 'locale=' + $(e.target).val(); + document.location = url; + return false; + }); + + this._super(); + } + }); + + /** + * Class: .CMSMain .createTranslation + * + * Loads /admin/createtranslation, which will create the new record, + * and redirect to an edit form. + * + * Dropdown in "Translation" tab in CMS forms, with button to + * trigger translating the currently loaded record. + * + * Requires: + * jquery.metadata + */ + $('.CMSMain .createTranslation').entwine({ + + /** + * Constructor: onmatch + */ + onmatch: function() { + var self = this; + + this.find(':input[name=action_createtranslation]').bind('click', function(e) { + var form = self.parents('form'); + // redirect to new URL + // TODO This should really be a POST request + + document.location.href = $('base').attr('href') + + jQuery(self).metadata().url + + '?ID=' + form.find(':input[name=ID]').val() + + '&newlang=' + self.find(':input[name=NewTransLang]').val() + + '&locale=' + form.find(':input[name=Locale]').val(); + + return false; + }); + + this._super(); + } + }); + }); +}(jQuery)); \ No newline at end of file diff --git a/tests/unit/TranslatableSearchFormTest.php b/tests/unit/TranslatableSearchFormTest.php new file mode 100644 index 0000000..d17e6e8 --- /dev/null +++ b/tests/unit/TranslatableSearchFormTest.php @@ -0,0 +1,101 @@ + array( + 'Translatable', + "FulltextSearchable('Title,MenuTitle,Content,MetaTitle,MetaDescription,MetaKeywords')", + ), + "File" => array( + "FulltextSearchable('Filename,Title,Content')", + ), + "ContentController" => array( + "ContentControllerSearchExtension", + ), + ); + + function waitUntilIndexingFinished() { + $db = DB::getConn(); + if (method_exists($db, 'waitUntilIndexingFinished')) DB::getConn()->waitUntilIndexingFinished(); + } + + function setUpOnce() { + // HACK Postgres doesn't refresh TSearch indexes when the schema changes after CREATE TABLE + if(is_a(DB::getConn(), 'PostgreSQLDatabase')) { + self::kill_temp_db(); + } + + parent::setUpOnce(); + } + + function setUp() { + parent::setUp(); + + $holderPage = $this->objFromFixture('SiteTree', 'searchformholder'); + $this->mockController = new ContentController($holderPage); + + // whenever a translation is created, canTranslate() is checked + $admin = $this->objFromFixture('Member', 'admin'); + $admin->logIn(); + + $this->waitUntilIndexingFinished(); + } + + + + function testPublishedPagesMatchedByTitleInDefaultLanguage() { + $sf = new SearchForm($this->mockController, 'SearchForm'); + + $publishedPage = $this->objFromFixture('SiteTree', 'publishedPage'); + $publishedPage->publish('Stage', 'Live'); + $translatedPublishedPage = $publishedPage->createTranslation('de_DE'); + $translatedPublishedPage->Title = 'translatedPublishedPage'; + $translatedPublishedPage->Content = 'German content'; + $translatedPublishedPage->write(); + $translatedPublishedPage->publish('Stage', 'Live'); + + $this->waitUntilIndexingFinished(); + + // Translatable::set_current_locale() can't be used because the context + // from the holder is not present here - we set the language explicitly + // through a pseudo GET variable in getResults() + + $lang = 'en_US'; + $results = $sf->getResults(null, array('Search'=>'content', 'locale'=>$lang)); + $this->assertContains( + $publishedPage->ID, + $results->column('ID'), + 'Published pages are found by searchform in default language' + ); + $this->assertNotContains( + $translatedPublishedPage->ID, + $results->column('ID'), + 'Published pages in another language are not found when searching in default language' + ); + + $lang = 'de_DE'; + $results = $sf->getResults(null, array('Search'=>'content', 'locale'=>$lang)); + $this->assertNotContains( + $publishedPage->ID, + $results->column('ID'), + 'Published pages in default language are not found when searching in another language' + ); + $actual = $results->column('ID'); + array_walk($actual, 'intval'); + $this->assertContains( + (int)$translatedPublishedPage->ID, + $actual, + 'Published pages in another language are found when searching in this language' + ); + } + +} +?> diff --git a/tests/unit/TranslatableSearchFormTest.yml b/tests/unit/TranslatableSearchFormTest.yml new file mode 100644 index 0000000..2fbf060 --- /dev/null +++ b/tests/unit/TranslatableSearchFormTest.yml @@ -0,0 +1,18 @@ +SiteTree: + searchformholder: + URLSegment: searchformholder + Title: searchformholder + publishedPage: + Title: publishedPage + Content: English content +Group: + admingroup: + Code: admingroup +Member: + admin: + FirstName: Admin + Groups: =>Group.admingroup +Permission: + admincode: + Code: ADMIN + Group: =>Group.admingroup \ No newline at end of file diff --git a/tests/unit/TranslatableSiteConfigTest.php b/tests/unit/TranslatableSiteConfigTest.php new file mode 100644 index 0000000..33bde9d --- /dev/null +++ b/tests/unit/TranslatableSiteConfigTest.php @@ -0,0 +1,53 @@ + array('Translatable'), + 'SiteConfig' => array('Translatable'), + ); + + protected $illegalExtensions = array( + 'SiteTree' => array('SiteTreeSubsites') + ); + + private $origLocale; + + function setUp() { + parent::setUp(); + + $this->origLocale = Translatable::default_locale(); + Translatable::set_default_locale("en_US"); + } + + function tearDown() { + Translatable::set_default_locale($this->origLocale); + Translatable::set_current_locale($this->origLocale); + + parent::tearDown(); + } + + function testCurrentCreatesDefaultForLocale() { + $configEn = SiteConfig::current_site_config(); + $configFr = SiteConfig::current_site_config('fr_FR'); + + $this->assertType('SiteConfig', $configFr); + $this->assertEquals($configFr->Locale, 'fr_FR'); + $this->assertEquals($configFr->Title, $configEn->Title, 'Copies title from existing config'); + } + + function testCanEditTranslatedRootPages() { + $configEn = $this->objFromFixture('SiteConfig', 'en_US'); + $configDe = $this->objFromFixture('SiteConfig', 'de_DE'); + + $pageEn = $this->objFromFixture('Page', 'root_en'); + $pageDe = $pageEn->createTranslation('de_DE'); + + $translatorDe = $this->objFromFixture('Member', 'translator_de'); + $translatorEn = $this->objFromFixture('Member', 'translator_en'); + + $this->assertFalse($pageEn->canEdit($translatorDe)); + $this->assertTrue($pageEn->canEdit($translatorEn)); + } +} \ No newline at end of file diff --git a/tests/unit/TranslatableSiteConfigTest.yml b/tests/unit/TranslatableSiteConfigTest.yml new file mode 100644 index 0000000..7f5151d --- /dev/null +++ b/tests/unit/TranslatableSiteConfigTest.yml @@ -0,0 +1,86 @@ +Permission: + cmsmain1: + Code: CMS_ACCESS_CMSMain + cmsmain2: + Code: CMS_ACCESS_CMSMain + translate_all1: + Code: TRANSLATE_ALL + translate_all2: + Code: TRANSLATE_ALL +Group: + translators_de: + Code: translators_de + Permissions: =>Permission.cmsmain1,=>Permission.translate_all1 + translators_en: + Code: translators_en + Permissions: =>Permission.cmsmain2,=>Permission.translate_all2 +Member: + translator_de: + Email: translator_de@test.com + Password: test + Groups: =>Group.translators_de + translator_en: + Email: translator_en@test.com + Password: test + Groups: =>Group.translators_en + websiteuser: + Email: websiteuser@test.com + Password: test +Page: + root_en: + URLSegment: root-en + Locale: en_US +SiteConfig: + en_US: + Title: My test site + Locale: en_US + CanEditType: OnlyTheseUsers + EditorGroups: =>Group.translators_en + de_DE: + Title: Meine Test Seite + Locale: de_DE + CanEditType: OnlyTheseUsers + EditorGroups: =>Group.translators_de +Permission: + cmsmain1: + Code: CMS_ACCESS_CMSMain + cmsmain2: + Code: CMS_ACCESS_CMSMain + translate_all1: + Code: TRANSLATE_ALL + translate_all2: + Code: TRANSLATE_ALL +Group: + translators_de: + Code: translators_de + Permissions: =>Permission.cmsmain1,=>Permission.translate_all1 + translators_en: + Code: translators_en + Permissions: =>Permission.cmsmain2,=>Permission.translate_all2 +Member: + translator_de: + Email: translator_de@test.com + Password: test + Groups: =>Group.translators_de + translator_en: + Email: translator_en@test.com + Password: test + Groups: =>Group.translators_en + websiteuser: + Email: websiteuser@test.com + Password: test +Page: + root_en: + URLSegment: root-en + Locale: en_US +SiteConfig: + en_US: + Title: My test site + Locale: en_US + CanEditType: OnlyTheseUsers + EditorGroups: =>Group.translators_en + de_DE: + Title: Meine Test Seite + Locale: de_DE + CanEditType: OnlyTheseUsers + EditorGroups: =>Group.translators_de \ No newline at end of file diff --git a/tests/unit/TranslatableTest.php b/tests/unit/TranslatableTest.php new file mode 100755 index 0000000..d97eaf4 --- /dev/null +++ b/tests/unit/TranslatableTest.php @@ -0,0 +1,987 @@ + array('Translatable'), + 'SiteConfig' => array('Translatable'), + 'TranslatableTest_DataObject' => array('Translatable'), + ); + + private $origLocale; + + protected $autoFollowRedirection = false; + + function setUp() { + parent::setUp(); + + // whenever a translation is created, canTranslate() is checked + $cmseditor = $this->objFromFixture('Member', 'cmseditor'); + $cmseditor->logIn(); + + $this->origLocale = Translatable::default_locale(); + Translatable::set_default_locale("en_US"); + } + + function tearDown() { + Translatable::set_default_locale($this->origLocale); + Translatable::set_current_locale($this->origLocale); + + parent::tearDown(); + } + + function testLocaleGetParamRedirectsToTranslation() { + $origPage = $this->objFromFixture('Page', 'testpage_en'); + $origPage->publish('Stage', 'Live'); + $translatedPage = $origPage->createTranslation('de_DE'); + $translatedPage->URLSegment = 'ueber-uns'; + $translatedPage->write(); + $translatedPage->publish('Stage', 'Live'); + + $response = $this->get($origPage->URLSegment); + $this->assertEquals(200, $response->getStatusCode(), 'Page request without Locale GET param doesnt redirect'); + + $response = $this->get(Controller::join_links($origPage->URLSegment, '?locale=de_DE')); + $this->assertEquals(301, $response->getStatusCode(), 'Locale GET param causes redirect if it exists'); + $this->assertContains($translatedPage->URLSegment, $response->getHeader('Location')); + + $response = $this->get(Controller::join_links($origPage->URLSegment, '?locale=fr_FR')); + $this->assertEquals(200, $response->getStatusCode(), 'Locale GET param without existing translation shows original page'); + } + + function testTranslationGroups() { + // first in french + $frPage = new SiteTree(); + $frPage->Locale = 'fr_FR'; + $frPage->write(); + + // second in english (from french "original") + $enPage = $frPage->createTranslation('en_US'); + + // third in spanish (from the english translation) + $esPage = $enPage->createTranslation('es_ES'); + + // test french + + $array1=$frPage->getTranslations()->column('Locale'); + $array2=array('en_US','es_ES'); + sort($array1); + sort($array2); + $this->assertEquals( + $array1, + $array2 + ); + $this->assertNotNull($frPage->getTranslation('en_US')); + $this->assertEquals( + $frPage->getTranslation('en_US')->ID, + $enPage->ID + ); + $this->assertNotNull($frPage->getTranslation('es_ES')); + $this->assertEquals( + $frPage->getTranslation('es_ES')->ID, + $esPage->ID + ); + + // test english + $expected = array('es_ES', 'fr_FR'); + sort($expected); + $actual = $enPage->getTranslations()->column('Locale'); + sort($actual); + $this->assertEquals( + $expected, + $actual + ); + $this->assertNotNull($frPage->getTranslation('fr_FR')); + $this->assertEquals( + $enPage->getTranslation('fr_FR')->ID, + $frPage->ID + ); + $this->assertNotNull($frPage->getTranslation('es_ES')); + $this->assertEquals( + $enPage->getTranslation('es_ES')->ID, + $esPage->ID + ); + + // test spanish + $actual = $esPage->getTranslations()->column('Locale'); + sort($actual); + $expected = array('en_US', 'fr_FR'); + sort($expected); + $this->assertEquals( + $actual, + $expected + ); + $this->assertNotNull($esPage->getTranslation('fr_FR')); + $this->assertEquals( + $esPage->getTranslation('fr_FR')->ID, + $frPage->ID + ); + $this->assertNotNull($esPage->getTranslation('en_US')); + $this->assertEquals( + $esPage->getTranslation('en_US')->ID, + $enPage->ID + ); + } + + function testTranslationGroupNotRemovedWhenSiteTreeUnpublished() { + $enPage = new Page(); + $enPage->Locale = 'en_US'; + $enPage->write(); + $enPage->publish('Stage', 'Live'); + $enTranslationGroup = $enPage->getTranslationGroup(); + + $frPage = $enPage->createTranslation('fr_FR'); + $frPage->write(); + $frPage->publish('Stage', 'Live'); + $frTranslationGroup = $frPage->getTranslationGroup(); + + $enPage->doUnpublish(); + $this->assertEquals($enPage->getTranslationGroup(), $enTranslationGroup); + + $frPage->doUnpublish(); + $this->assertEquals($frPage->getTranslationGroup(), $frTranslationGroup); + } + + function testGetTranslationOnSiteTree() { + $origPage = $this->objFromFixture('Page', 'testpage_en'); + + $translatedPage = $origPage->createTranslation('fr_FR'); + $getTranslationPage = $origPage->getTranslation('fr_FR'); + + $this->assertNotNull($getTranslationPage); + $this->assertEquals($getTranslationPage->ID, $translatedPage->ID); + } + + function testGetTranslatedLanguages() { + $origPage = $this->objFromFixture('Page', 'testpage_en'); + + // through createTranslation() + $translationAf = $origPage->createTranslation('af_ZA'); + + // create a new language on an unrelated page which shouldnt be returned from $origPage + $otherPage = new Page(); + $otherPage->write(); + $otherTranslationEs = $otherPage->createTranslation('es_ES'); + + $this->assertEquals( + $origPage->getTranslatedLangs(), + array( + 'af_ZA', + //'en_US', // default language is not included + ), + 'Language codes are returned specifically for the queried page through getTranslatedLangs()' + ); + + $pageWithoutTranslations = new Page(); + $pageWithoutTranslations->write(); + $this->assertEquals( + $pageWithoutTranslations->getTranslatedLangs(), + array(), + 'A page without translations returns empty array through getTranslatedLangs(), ' . + 'even if translations for other pages exist in the database' + ); + + // manual creation of page without an original link + $translationDeWithoutOriginal = new Page(); + $translationDeWithoutOriginal->Locale = 'de_DE'; + $translationDeWithoutOriginal->write(); + $this->assertEquals( + $translationDeWithoutOriginal->getTranslatedLangs(), + array(), + 'A translated page without an original doesn\'t return anything through getTranslatedLang()' + ); + } + + function testTranslationCantHaveSameURLSegmentAcrossLanguages() { + $origPage = $this->objFromFixture('Page', 'testpage_en'); + $translatedPage = $origPage->createTranslation('de_DE'); + $this->assertEquals($translatedPage->URLSegment, 'testpage-de-DE'); + + $translatedPage->URLSegment = 'testpage'; + $translatedPage->write(); + + $this->assertNotEquals($origPage->URLSegment, $translatedPage->URLSegment); + } + + function testUpdateCMSFieldsOnSiteTree() { + $pageOrigLang = $this->objFromFixture('Page', 'testpage_en'); + + // first test with default language + $fields = $pageOrigLang->getCMSFields(); + $this->assertType( + 'TextField', + $fields->dataFieldByName('Title'), + 'Translatable doesnt modify fields if called in default language (e.g. "non-translation mode")' + ); + $this->assertNull( + $fields->dataFieldByName('Title_original'), + 'Translatable doesnt modify fields if called in default language (e.g. "non-translation mode")' + ); + + // then in "translation mode" + $pageTranslated = $pageOrigLang->createTranslation('fr_FR'); + $fields = $pageTranslated->getCMSFields(); + $this->assertType( + 'TextField', + $fields->dataFieldByName('Title'), + 'Translatable leaves original formfield intact in "translation mode"' + ); + $readonlyField = $fields->dataFieldByName('Title')->performReadonlyTransformation(); + $this->assertType( + $readonlyField->class, + $fields->dataFieldByName('Title_original'), + 'Translatable adds the original value as a ReadonlyField in "translation mode"' + ); + + } + + function testDataObjectGetWithReadingLanguage() { + $origTestPage = $this->objFromFixture('Page', 'testpage_en'); + $otherTestPage = $this->objFromFixture('Page', 'othertestpage_en'); + $translatedPage = $origTestPage->createTranslation('de_DE'); + + // test in default language + $resultPagesDefaultLang = DataObject::get( + 'Page', + sprintf("\"SiteTree\".\"MenuTitle\" = '%s'", 'A Testpage') + ); + $resultPagesDefaultLangIDs = $resultPagesDefaultLang->column('ID'); + array_walk($resultPagesDefaultLangIDs, 'intval'); + $this->assertEquals($resultPagesDefaultLang->Count(), 2); + $this->assertContains((int)$origTestPage->ID, $resultPagesDefaultLangIDs); + $this->assertContains((int)$otherTestPage->ID, $resultPagesDefaultLangIDs); + $this->assertNotContains((int)$translatedPage->ID, $resultPagesDefaultLangIDs); + + // test in custom language + Translatable::set_current_locale('de_DE'); + $resultPagesCustomLang = DataObject::get( + 'Page', + sprintf("\"SiteTree\".\"MenuTitle\" = '%s'", 'A Testpage') + ); + $resultPagesCustomLangIDs = $resultPagesCustomLang->column('ID'); + array_walk($resultPagesCustomLangIDs, 'intval'); + $this->assertEquals($resultPagesCustomLang->Count(), 1); + $this->assertNotContains((int)$origTestPage->ID, $resultPagesCustomLangIDs); + $this->assertNotContains((int)$otherTestPage->ID, $resultPagesCustomLangIDs); + $this->assertContains((int)$translatedPage->ID, $resultPagesCustomLangIDs); + + Translatable::set_current_locale('en_US'); + } + + function testDataObjectGetByIdWithReadingLanguage() { + $origPage = $this->objFromFixture('Page', 'testpage_en'); + $translatedPage = $origPage->createTranslation('de_DE'); + $compareOrigPage = DataObject::get_by_id('Page', $origPage->ID); + + $this->assertEquals( + $origPage->ID, + $compareOrigPage->ID, + 'DataObject::get_by_id() should work independently of the reading language' + ); + } + + function testDataObjectGetOneWithReadingLanguage() { + $origPage = $this->objFromFixture('Page', 'testpage_en'); + $translatedPage = $origPage->createTranslation('de_DE'); + + // running the same query twice with different + Translatable::set_current_locale('de_DE'); + $compareTranslatedPage = DataObject::get_one( + 'Page', + sprintf("\"SiteTree\".\"Title\" = '%s'", $translatedPage->Title) + ); + $this->assertNotNull($compareTranslatedPage); + $this->assertEquals( + $translatedPage->ID, + $compareTranslatedPage->ID, + "Translated page is found through get_one() when reading lang is not the default language" + ); + + // reset language to default + Translatable::set_current_locale('en_US'); + } + + function testModifyTranslationWithDefaultReadingLang() { + $origPage = $this->objFromFixture('Page', 'testpage_en'); + $translatedPage = $origPage->createTranslation('de_DE'); + + Translatable::set_current_locale('en_US'); + $translatedPage->Title = 'De Modified'; + $translatedPage->write(); + $savedTranslatedPage = $origPage->getTranslation('de_DE'); + $this->assertEquals( + $savedTranslatedPage->Title, + 'De Modified', + 'Modifying a record in language which is not the reading language should still write the record correctly' + ); + $this->assertEquals( + $origPage->Title, + 'Home', + 'Modifying a record in language which is not the reading language does not modify the original record' + ); + } + + function testSiteTreePublication() { + $origPage = $this->objFromFixture('Page', 'testpage_en'); + $translatedPage = $origPage->createTranslation('de_DE'); + + Translatable::set_current_locale('en_US'); + $origPage->Title = 'En Modified'; + $origPage->write(); + // modifying a record in language which is not the reading language should still write the record correctly + $translatedPage->Title = 'De Modified'; + $translatedPage->write(); + $origPage->publish('Stage', 'Live'); + $liveOrigPage = Versioned::get_one_by_stage('Page', 'Live', "\"SiteTree\".\"ID\" = {$origPage->ID}"); + $this->assertEquals( + $liveOrigPage->Title, + 'En Modified', + 'Publishing a record in its original language publshes correct properties' + ); + } + + function testDeletingTranslationKeepsOriginal() { + $origPage = $this->objFromFixture('Page', 'testpage_en'); + $translatedPage = $origPage->createTranslation('de_DE'); + $translatedPageID = $translatedPage->ID; + $translatedPage->delete(); + + $translatedPage->flushCache(); + $origPage->flushCache(); + + $this->assertNull($origPage->getTranslation('de_DE')); + $this->assertNotNull(DataObject::get_by_id('Page', $origPage->ID)); + } + + function testHierarchyChildren() { + $parentPage = $this->objFromFixture('Page', 'parent'); + $child1Page = $this->objFromFixture('Page', 'child1'); + $child2Page = $this->objFromFixture('Page', 'child2'); + $child3Page = $this->objFromFixture('Page', 'child3'); + $grandchildPage = $this->objFromFixture('Page', 'grandchild1'); + + $parentPageTranslated = $parentPage->createTranslation('de_DE'); + $child4PageTranslated = new SiteTree(); + $child4PageTranslated->Locale = 'de_DE'; + $child4PageTranslated->ParentID = $parentPageTranslated->ID; + $child4PageTranslated->write(); + + Translatable::set_current_locale('en_US'); + $actual = $parentPage->Children()->column('ID'); + sort($actual); + $expected = array( + $child1Page->ID, + $child2Page->ID, + $child3Page->ID + ); + $this->assertEquals( + $actual, + $expected, + "Showing Children() in default language doesnt show children in other languages" + ); + + Translatable::set_current_locale('de_DE'); + $parentPage->flushCache(); + $this->assertEquals( + $parentPageTranslated->Children()->column('ID'), + array($child4PageTranslated->ID), + "Showing Children() in translation mode doesnt show children in default languages" + ); + + // reset language + Translatable::set_current_locale('en_US'); + } + + function testHierarchyLiveStageChildren() { + $parentPage = $this->objFromFixture('Page', 'parent'); + $child1Page = $this->objFromFixture('Page', 'child1'); + $child1Page->publish('Stage', 'Live'); + $child2Page = $this->objFromFixture('Page', 'child2'); + $child3Page = $this->objFromFixture('Page', 'child3'); + $grandchildPage = $this->objFromFixture('Page', 'grandchild1'); + + $parentPageTranslated = $parentPage->createTranslation('de_DE'); + + $child4PageTranslated = new SiteTree(); + $child4PageTranslated->Locale = 'de_DE'; + $child4PageTranslated->ParentID = $parentPageTranslated->ID; + $child4PageTranslated->write(); + $child4PageTranslated->publish('Stage', 'Live'); + + $child5PageTranslated = new SiteTree(); + $child5PageTranslated->Locale = 'de_DE'; + $child5PageTranslated->ParentID = $parentPageTranslated->ID; + $child5PageTranslated->write(); + + Translatable::set_current_locale('en_US'); + $this->assertNotNull($parentPage->liveChildren()); + $this->assertEquals( + $parentPage->liveChildren()->column('ID'), + array( + $child1Page->ID + ), + "Showing liveChildren() in default language doesnt show children in other languages" + ); + $this->assertNotNull($parentPage->stageChildren()); + $actual = $parentPage->stageChildren()->column('ID'); + sort($actual); + $expected = array( + $child1Page->ID, + $child2Page->ID, + $child3Page->ID + ); + sort($expected); + $this->assertEquals( + $actual, + $expected, + "Showing stageChildren() in default language doesnt show children in other languages" + ); + + Translatable::set_current_locale('de_DE'); + $parentPage->flushCache(); + $this->assertNotNull($parentPageTranslated->liveChildren()); + $this->assertEquals( + $parentPageTranslated->liveChildren()->column('ID'), + array($child4PageTranslated->ID), + "Showing liveChildren() in translation mode doesnt show children in default languages" + ); + $this->assertNotNull($parentPageTranslated->stageChildren()); + $this->assertEquals( + $parentPageTranslated->stageChildren()->column('ID'), + array( + $child4PageTranslated->ID, + $child5PageTranslated->ID, + ), + "Showing stageChildren() in translation mode doesnt show children in default languages" + ); + + // reset language + Translatable::set_current_locale('en_US'); + } + + function testTranslatablePropertiesOnSiteTree() { + $origObj = $this->objFromFixture('TranslatableTest_Page', 'testpage_en'); + + $translatedObj = $origObj->createTranslation('fr_FR'); + $translatedObj->TranslatableProperty = 'fr_FR'; + $translatedObj->write(); + + $this->assertEquals( + $origObj->TranslatableProperty, + 'en_US', + 'Creating a translation doesnt affect database field on original object' + ); + $this->assertEquals( + $translatedObj->TranslatableProperty, + 'fr_FR', + 'Translated object saves database field independently of original object' + ); + } + + function testCreateTranslationOnSiteTree() { + $origPage = $this->objFromFixture('Page', 'testpage_en'); + $translatedPage = $origPage->createTranslation('de_DE'); + + $this->assertEquals($translatedPage->Locale, 'de_DE'); + $this->assertNotEquals($translatedPage->ID, $origPage->ID); + + $subsequentTranslatedPage = $origPage->createTranslation('de_DE'); + $this->assertEquals( + $translatedPage->ID, + $subsequentTranslatedPage->ID, + 'Subsequent calls to createTranslation() dont cause new records in database' + ); + } + + function testTranslatablePropertiesOnDataObject() { + $origObj = $this->objFromFixture('TranslatableTest_DataObject', 'testobject_en'); + $translatedObj = $origObj->createTranslation('fr_FR'); + $translatedObj->TranslatableProperty = 'fr_FR'; + $translatedObj->TranslatableDecoratedProperty = 'fr_FR'; + $translatedObj->write(); + + $this->assertEquals( + $origObj->TranslatableProperty, + 'en_US', + 'Creating a translation doesnt affect database field on original object' + ); + $this->assertEquals( + $origObj->TranslatableDecoratedProperty, + 'en_US', + 'Creating a translation doesnt affect decorated database field on original object' + ); + $this->assertEquals( + $translatedObj->TranslatableProperty, + 'fr_FR', + 'Translated object saves database field independently of original object' + ); + $this->assertEquals( + $translatedObj->TranslatableDecoratedProperty, + 'fr_FR', + 'Translated object saves decorated database field independently of original object' + ); + } + + function testCreateTranslationWithoutOriginal() { + $origParentPage = $this->objFromFixture('Page', 'testpage_en'); + $translatedParentPage = $origParentPage->createTranslation('de_DE'); + + $translatedPageWithoutOriginal = new SiteTree(); + $translatedPageWithoutOriginal->ParentID = $translatedParentPage->ID; + $translatedPageWithoutOriginal->Locale = 'de_DE'; + $translatedPageWithoutOriginal->write(); + + Translatable::set_current_locale('de_DE'); + $this->assertEquals( + $translatedParentPage->stageChildren()->column('ID'), + array( + $translatedPageWithoutOriginal->ID + ), + "Children() still works on a translated page even if no translation group is set" + ); + + Translatable::set_current_locale('en_US'); + } + + function testCreateTranslationTranslatesUntranslatedParents() { + $parentPage = $this->objFromFixture('Page', 'parent'); + $child1Page = $this->objFromFixture('Page', 'child1'); + $child1PageOrigID = $child1Page->ID; + $grandChild1Page = $this->objFromFixture('Page', 'grandchild1'); + $grandChild2Page = $this->objFromFixture('Page', 'grandchild2'); + + $this->assertFalse($grandChild1Page->hasTranslation('de_DE')); + $this->assertFalse($child1Page->hasTranslation('de_DE')); + $this->assertFalse($parentPage->hasTranslation('de_DE')); + + $translatedGrandChild1Page = $grandChild1Page->createTranslation('de_DE'); + $translatedGrandChild2Page = $grandChild2Page->createTranslation('de_DE'); + $translatedChildPage = $child1Page->getTranslation('de_DE'); + $translatedParentPage = $parentPage->getTranslation('de_DE'); + + $this->assertTrue($grandChild1Page->hasTranslation('de_DE')); + $this->assertEquals($translatedGrandChild1Page->ParentID, $translatedChildPage->ID); + + $this->assertTrue($grandChild2Page->hasTranslation('de_DE')); + $this->assertEquals($translatedGrandChild2Page->ParentID, $translatedChildPage->ID); + + $this->assertTrue($child1Page->hasTranslation('de_DE')); + $this->assertEquals($translatedChildPage->ParentID, $translatedParentPage->ID); + + $this->assertTrue($parentPage->hasTranslation('de_DE')); + } + + function testHierarchyAllChildrenIncludingDeleted() { + // Original tree in 'en_US': + // parent + // child1 (Live only, deleted from stage) + // child2 (Stage only, never published) + // child3 (Stage only, never published, untranslated) + // Translated tree in 'de_DE': + // parent + // child1 (Live only, deleted from stage) + // child2 (Stage only) + + // Create parent + $parentPage = $this->objFromFixture('Page', 'parent'); + $parentPageID = $parentPage->ID; + + // Create parent translation + $translatedParentPage = $parentPage->createTranslation('de_DE'); + $translatedParentPageID = $translatedParentPage->ID; + + // Create child1 + $child1Page = $this->objFromFixture('Page', 'child1'); + $child1PageID = $child1Page->ID; + $child1Page->publish('Stage', 'Live'); + + // Create child1 translation + $child1PageTranslated = $child1Page->createTranslation('de_DE'); + $child1PageTranslatedID = $child1PageTranslated->ID; + $child1PageTranslated->publish('Stage', 'Live'); + $child1PageTranslated->deleteFromStage('Stage'); // deleted from stage only, record still exists on live + $child1Page->deleteFromStage('Stage'); // deleted from stage only, record still exists on live + + // Create child2 + $child2Page = $this->objFromFixture('Page', 'child2'); + $child2PageID = $child2Page->ID; + + // Create child2 translation + $child2PageTranslated = $child2Page->createTranslation('de_DE'); + $child2PageTranslatedID = $child2PageTranslated->ID; + + // Create child3 + $child3Page = $this->objFromFixture('Page', 'child3'); + $child3PageID = $child3Page->ID; + + // on original parent in default language + Translatable::set_current_locale('en_US'); + SiteTree::flush_and_destroy_cache(); + $parentPage = $this->objFromFixture('Page', 'parent'); + $children = $parentPage->AllChildrenIncludingDeleted(); + $expected = array( + $child2PageID, + $child3PageID, + $child1PageID // $child1Page was deleted from stage, so the original record doesn't have the ID set + ); + sort($expected); + $actual = $parentPage->AllChildrenIncludingDeleted()->column('ID'); + sort($actual); + $this->assertEquals( + $actual, + $expected, + "Showing AllChildrenIncludingDeleted() in default language doesnt show deleted children in other languages" + ); + + // on original parent in translation mode + Translatable::set_current_locale('de_DE'); + SiteTree::flush_and_destroy_cache(); + $parentPage = $this->objFromFixture('Page', 'parent'); + $this->assertEquals( + $translatedParentPage->AllChildrenIncludingDeleted()->column('ID'), + array( + $child2PageTranslatedID, + $child1PageTranslatedID // $child1PageTranslated was deleted from stage, so the original record doesn't have the ID set + ), + "Showing AllChildrenIncludingDeleted() in translation mode with parent page in translated language shows children in translated language" + ); + + Translatable::set_current_locale('de_DE'); + SiteTree::flush_and_destroy_cache(); + $parentPage = $this->objFromFixture('Page', 'parent'); + $this->assertEquals( + $parentPage->AllChildrenIncludingDeleted()->column('ID'), + array(), + "Showing AllChildrenIncludingDeleted() in translation mode with parent page in translated language shows children in default language" + ); + + // reset language + Translatable::set_current_locale('en_US'); + } + + function testRootUrlDefaultsToTranslatedLink() { + $origPage = $this->objFromFixture('Page', 'homepage_en'); + $origPage->publish('Stage', 'Live'); + $translationDe = $origPage->createTranslation('de_DE'); + $translationDe->URLSegment = 'heim'; + $translationDe->write(); + $translationDe->publish('Stage', 'Live'); + + // test with translatable + Translatable::set_current_locale('de_DE'); + $this->assertEquals( + RootURLController::get_homepage_link(), + 'heim', + 'Homepage with different URLSegment in non-default language is found' + ); + + // @todo Fix add/remove extension + // test with translatable disabled + // Object::remove_extension('Page', 'Translatable'); + // $_SERVER['HTTP_HOST'] = '/'; + // $this->assertEquals( + // RootURLController::get_homepage_urlsegment(), + // 'home', + // 'Homepage is showing in default language if ?lang GET variable is left out' + // ); + // Object::add_extension('Page', 'Translatable'); + + // setting back to default + Translatable::set_current_locale('en_US'); + } + + function testSiteTreeChangePageTypeInMaster() { + // create original + $origPage = new SiteTree(); + $origPage->Locale = 'en_US'; + $origPage->write(); + $origPageID = $origPage->ID; + + // create translation + $translatedPage = $origPage->createTranslation('de_DE'); + $translatedPageID = $translatedPage->ID; + + // change page type + $newPage = $origPage->newClassInstance('RedirectorPage'); + $newPage->write(); + + // re-fetch original page with new instance + $origPageChanged = DataObject::get_by_id('RedirectorPage', $origPageID); + $this->assertEquals($origPageChanged->ClassName, 'RedirectorPage', + 'A ClassName change to an original page doesnt change original classname' + ); + + // re-fetch the translation with new instance + Translatable::set_current_locale('de_DE'); + $translatedPageChanged = DataObject::get_by_id('RedirectorPage', $translatedPageID); + $translatedPageChanged = $origPageChanged->getTranslation('de_DE'); + $this->assertEquals($translatedPageChanged->ClassName, 'RedirectorPage', + 'ClassName change on an original page also changes ClassName attribute of translation' + ); + } + + function testLocalePersistsInAllPageTypes() { + $types = ClassInfo::subclassesFor('SiteTree'); + foreach($types as $type) { + if(singleton($type) instanceof TestOnly) continue; + + $enPage = new $type(); + $enPage->Locale = 'en_US'; + $enPage->write(); + + $dePage = $enPage->createTranslation('de_DE'); + $dePage->write(); + $this->assertEquals('de_DE', $dePage->Locale, "Page type $type retains Locale property"); + } + } + + function testGetTranslationByStage() { + $publishedPage = new SiteTree(); + $publishedPage->Locale = 'en_US'; + $publishedPage->Title = 'Published'; + $publishedPage->write(); + $publishedPage->publish('Stage', 'Live'); + $publishedPage->Title = 'Unpublished'; + $publishedPage->write(); + + $publishedTranslatedPage = $publishedPage->createTranslation('de_DE'); + $publishedTranslatedPage->Title = 'Publiziert'; + $publishedTranslatedPage->write(); + $publishedTranslatedPage->publish('Stage', 'Live'); + $publishedTranslatedPage->Title = 'Unpubliziert'; + $publishedTranslatedPage->write(); + + $compareStage = $publishedPage->getTranslation('de_DE', 'Stage'); + $this->assertNotNull($compareStage); + $this->assertEquals($compareStage->Title, 'Unpubliziert'); + + $compareLive = $publishedPage->getTranslation('de_DE', 'Live'); + $this->assertNotNull($compareLive); + $this->assertEquals($compareLive->Title, 'Publiziert'); + } + + function testCanTranslateAllowedLocales() { + $origAllowedLocales = Translatable::get_allowed_locales(); + + $cmseditor = $this->objFromFixture('Member', 'cmseditor'); + + $testPage = $this->objFromFixture('Page', 'testpage_en'); + $this->assertTrue( + $testPage->canTranslate($cmseditor, 'de_DE'), + "Users with canEdit() and TRANSLATE_ALL permission can create a new translation if locales are not limited" + ); + + Translatable::set_allowed_locales(array('ja_JP')); + $this->assertTrue( + $testPage->canTranslate($cmseditor, 'ja_JP'), + "Users with canEdit() and TRANSLATE_ALL permission can create a new translation if locale is in Translatable::get_allowed_locales()" + ); + $this->assertFalse( + $testPage->canTranslate($cmseditor, 'de_DE'), + "Users with canEdit() and TRANSLATE_ALL permission can't create a new translation if locale is not in Translatable::get_allowed_locales()" + ); + + $this->assertType( + 'Page', + $testPage->createTranslation('ja_JP') + ); + try { + $testPage->createTranslation('de_DE'); + $this->setExpectedException("Exception"); + } catch(Exception $e) {} + + Translatable::set_allowed_locales($origAllowedLocales); + } + + function testCanTranslatePermissionCodes() { + $origAllowedLocales = Translatable::get_allowed_locales(); + + Translatable::set_allowed_locales(array('ja_JP','de_DE')); + + $cmseditor = $this->objFromFixture('Member', 'cmseditor'); + + $testPage = $this->objFromFixture('Page', 'testpage_en'); + $this->assertTrue( + $testPage->canTranslate($cmseditor, 'de_DE'), + "Users with TRANSLATE_ALL permission can create a new translation" + ); + + $translator = $this->objFromFixture('Member', 'germantranslator'); + + $testPage = $this->objFromFixture('Page', 'testpage_en'); + $this->assertTrue( + $testPage->canTranslate($translator, 'de_DE'), + "Users with TRANSLATE_ permission can create a new translation" + ); + + $this->assertFalse( + $testPage->canTranslate($translator, 'ja_JP'), + "Users without TRANSLATE_ permission can create a new translation" + ); + + Translatable::set_allowed_locales($origAllowedLocales); + } + + function testLocalesForMember() { + $origAllowedLocales = Translatable::get_allowed_locales(); + Translatable::set_allowed_locales(array('de_DE', 'ja_JP')); + + $cmseditor = $this->objFromFixture('Member', 'cmseditor'); + $translator = $this->objFromFixture('Member', 'germantranslator'); + + $this->assertEquals( + array('de_DE', 'ja_JP'), + singleton('SiteTree')->getAllowedLocalesForMember($cmseditor), + 'Members with TRANSLATE_ALL permission can edit all locales' + ); + + $this->assertEquals( + array('de_DE'), + singleton('SiteTree')->getAllowedLocalesForMember($translator), + 'Members with TRANSLATE_ permission cant edit all locales' + ); + + Translatable::set_allowed_locales($origAllowedLocales); + } + + function testSavePageInCMS() { + $adminUser = $this->objFromFixture('Member', 'admin'); + $enPage = $this->objFromFixture('Page', 'testpage_en'); + + $group = new Group(); + $group->Title = 'Example Group'; + $group->write(); + + $frPage = $enPage->createTranslation('fr_FR'); + $frPage->write(); + + $adminUser->logIn(); + + $cmsMain = new CMSMain(); + + $origLocale = Translatable::get_current_locale(); + Translatable::set_current_locale('fr_FR'); + + $form = $cmsMain->getEditForm($frPage->ID); + $form->loadDataFrom(array( + 'Title' => 'Translated', // $db field + 'ViewerGroups' => $group->ID // $many_many field + )); + $form->saveInto($frPage); + $frPage->write(); + + $this->assertEquals('Translated', $frPage->Title); + $this->assertEquals(array($group->ID), $frPage->ViewerGroups()->column('ID')); + + $adminUser->logOut(); + Translatable::set_current_locale($origLocale); + } + + public function testAlternateGetByLink() { + $parent = $this->objFromFixture('Page', 'parent'); + $child = $this->objFromFixture('Page', 'child1'); + $grandchild = $this->objFromFixture('Page', 'grandchild1'); + + $parentTranslation = $parent->createTranslation('en_AU'); + $parentTranslation->write(); + + $childTranslation = $child->createTranslation('en_AU'); + $childTranslation->write(); + + $grandchildTranslation = $grandchild->createTranslation('en_AU'); + $grandchildTranslation->write(); + + SiteTree::enable_nested_urls(); + Translatable::set_current_locale('en_AU'); + + $this->assertEquals ( + $parentTranslation->ID, + Sitetree::get_by_link($parentTranslation->Link())->ID, + 'Top level pages can be found.' + ); + + $this->assertEquals ( + $childTranslation->ID, + SiteTree::get_by_link($childTranslation->Link())->ID, + 'Child pages can be found.' + ); + + $this->assertEquals ( + $grandchildTranslation->ID, + SiteTree::get_by_link($grandchildTranslation->Link())->ID, + 'Grandchild pages can be found.' + ); + + // TODO Re-enable test after clarifying with ajshort (see r88503). + // Its unclear if this is valid behaviour, and/or necessary for translated nested URLs + // to work properly + // + // $this->assertEquals ( + // $child->ID, + // SiteTree::get_by_link($parentTranslation->Link($child->URLSegment))->ID, + // 'Links can be made up of multiple languages' + // ); + } + + public function testSiteTreeGetByLinkFindsTranslationWithoutLocale() { + $parent = $this->objFromFixture('Page', 'parent'); + + $parentTranslation = $parent->createTranslation('en_AU'); + $parentTranslation->URLSegment = 'parent-en-AU'; + $parentTranslation->write(); + + $match = Sitetree::get_by_link($parentTranslation->URLSegment); + $this->assertNotNull( + $match, + 'SiteTree::get_by_link() doesnt need a locale setting to find translated pages' + ); + $this->assertEquals( + $parentTranslation->ID, + $match->ID, + 'SiteTree::get_by_link() doesnt need a locale setting to find translated pages' + ); + } +} + +class TranslatableTest_DataObject extends DataObject implements TestOnly { + // add_extension() used to add decorator at end of file + + static $db = array( + 'TranslatableProperty' => 'Text' + ); +} + +class TranslatableTest_Decorator extends DataObjectDecorator implements TestOnly { + + function extraStatics() { + return array( + 'db' => array( + 'TranslatableDecoratedProperty' => 'Text' + ) + ); + } +} + +class TranslatableTest_Page extends Page implements TestOnly { + // static $extensions is inherited from SiteTree, + // we don't need to explicitly specify the fields + + static $db = array( + 'TranslatableProperty' => 'Text' + ); +} + +DataObject::add_extension('TranslatableTest_DataObject', 'TranslatableTest_Decorator'); +?> diff --git a/tests/unit/TranslatableTest.yml b/tests/unit/TranslatableTest.yml new file mode 100644 index 0000000..43707e4 --- /dev/null +++ b/tests/unit/TranslatableTest.yml @@ -0,0 +1,82 @@ +Page: + homepage_en: + Title: Home + URLSegment: home + Locale: en_US + testpage_en: + Title: Home + MenuTitle: A Testpage + URLSegment: testpage + Locale: en_US + othertestpage_en: + Title: Other Testpage + MenuTitle: A Testpage + URLSegment: othertestpage + Locale: en_US + parent: + Title: Parent + URLSegment: parent + child1: + Title: Child 1 + URLSegment: child1 + Parent: =>Page.parent + child2: + Title: Child 2 + URLSegment: child2 + Parent: =>Page.parent + child3: + Title: Child 3 + URLSegment: child3 + Parent: =>Page.parent + grandchild1: + Title: Grandchild + URLSegment: grandchild1 + Parent: =>Page.child1 + grandchild2: + Title: Grandchild + URLSegment: grandchild2 + Parent: =>Page.child1 +TranslatableTest_DataObject: + testobject_en: + TranslatableProperty: en_US + TranslatableDecoratedProperty: en_US +TranslatableTest_Page: + testpage_en: + Title: En + TranslatableProperty: en_US + URLSegment: testpage-en +Group: + cmseditorgroup: + Code: cmseditorgroup + admingroup: + Code: admingroup + germantranslators: + Code: germantranslators +Member: + cmseditor: + FirstName: Editor + Groups: =>Group.cmseditorgroup + websiteuser: + FirstName: Website User + admin: + FirstName: Admin + Groups: =>Group.admingroup + germantranslator: + FirstName: German + Groups: =>Group.germantranslators +Permission: + admincode: + Code: ADMIN + Group: =>Group.admingroup + cmsmaincode: + Code: CMS_ACCESS_CMSMain + Group: =>Group.cmseditorgroup + translateAllCode: + Code: TRANSLATE_ALL + Group: =>Group.cmseditorgroup + cmsmaincode2: + Code: CMS_ACCESS_CMSMain + Group: =>Group.germantranslators + translateDeCode2: + Code: TRANSLATE_de_DE + Group: =>Group.germantranslators \ No newline at end of file