From a6898417e7ff5ffa14c812ccca864706b8f7a874 Mon Sep 17 00:00:00 2001 From: helpfulrobot Date: Wed, 18 Nov 2015 17:07:47 +1300 Subject: [PATCH] Converted to PSR-2 --- .../TranslatableCMSMainExtension.php | 445 +- .../TranslatableEditorToolbarExtension.php | 20 +- code/forms/LanguageDropdownField.php | 206 +- code/model/Translatable.php | 3748 +++++++++-------- code/tasks/MigrateTranslatableTask.php | 284 +- tests/unit/TranslatableSearchFormTest.php | 184 +- tests/unit/TranslatableSiteConfigTest.php | 120 +- tests/unit/TranslatableTest.php | 2454 +++++------ 8 files changed, 3866 insertions(+), 3595 deletions(-) diff --git a/code/controller/TranslatableCMSMainExtension.php b/code/controller/TranslatableCMSMainExtension.php index 8ed5dd3..cb00fa5 100644 --- a/code/controller/TranslatableCMSMainExtension.php +++ b/code/controller/TranslatableCMSMainExtension.php @@ -2,217 +2,244 @@ /** * @package translatable */ -class TranslatableCMSMainExtension extends Extension { +class TranslatableCMSMainExtension extends Extension +{ + private static $allowed_actions = array( + 'createtranslation', + ); - private static $allowed_actions = array( - 'createtranslation', - ); + public function init() + { + $req = $this->owner->getRequest(); + + // Ignore being called on LeftAndMain base class, + // which is the case when requests are first routed through AdminRootController + // as an intermediary rather than the endpoint controller + if (!$this->owner->stat('tree_class')) { + return; + } - function init() { - $req = $this->owner->getRequest(); - - // Ignore being called on LeftAndMain base class, - // which is the case when requests are first routed through AdminRootController - // as an intermediary rather than the endpoint controller - if(!$this->owner->stat('tree_class')) return; - - // 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. - $id = $req->param('ID'); - if($req->requestVar("Locale")) { - $this->owner->Locale = $req->requestVar("Locale"); - } else if($id && is_numeric($id)) { - $record = DataObject::get_by_id($this->owner->stat('tree_class'), $id); - if($record && $record->Locale) $this->owner->Locale = $record->Locale; - } else { - $this->owner->Locale = Translatable::default_locale(); - if ($this->owner->class == 'CMSPagesController') { - // the CMSPagesController always needs to have the locale set, - // otherwise page editing will cause an extra - // ajax request which looks weird due to multiple "loading"-flashes - $getVars = $req->getVars(); - if(isset($getVars['url'])) unset($getVars['url']); - return $this->owner->redirect(Controller::join_links( - $this->owner->Link(), - $req->param('Action'), - $req->param('ID'), - $req->param('OtherID'), - ($query = http_build_query($getVars)) ? "?$query" : null - )); - } - } - Translatable::set_current_locale($this->owner->Locale); - - // If a locale is set, it needs to match to the current record - $requestLocale = $req->requestVar("Locale"); - $page = $this->owner->currentPage(); - if( - $req->httpMethod() == 'GET' // leave form submissions alone - && $requestLocale - && $page - && $page->hasExtension('Translatable') - && $page->Locale != $requestLocale - && $req->latestParam('Action') != 'EditorToolbar' - ) { - $transPage = $page->getTranslation($requestLocale); - if($transPage) { - Translatable::set_current_locale($transPage->Locale); - return $this->owner->redirect(Controller::join_links( - $this->owner->Link('show'), - $transPage->ID - // ?locale will automatically be added - )); - } else if ($this->owner->class != 'CMSPagesController') { - // If the record is not translated, redirect to pages overview - return $this->owner->redirect(Controller::join_links( - singleton('CMSPagesController')->Link(), - '?Locale=' . $requestLocale - )); - } - } - - // 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) { - if($form->getName() == 'RootForm' && SiteConfig::has_extension("Translatable")) { - $siteConfig = SiteConfig::current_site_config(); - $form->Fields()->push(new HiddenField('Locale','', $siteConfig->Locale)); - } - } - - function updatePageOptions(&$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($data, $form) { - $request = $this->owner->getRequest(); - - // Protect against CSRF on destructive action - if(!SecurityToken::inst()->checkRequest($request)) return $this->owner->httpError(400); - - $langCode = Convert::raw2sql($request->postVar('NewTransLang')); - $record = $this->owner->getRecord($request->postVar('ID')); - if(!$record) return $this->owner->httpError(404); - - $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 = Controller::join_links( - $this->owner->Link('show'), - $translatedRecord->ID - ); - - // set the X-Pjax header to Content, so that the whole admin panel will be refreshed - $this->owner->getResponse()->addHeader('X-Pjax', 'Content'); - - return $this->owner->redirect($url); - } - - function updateLink(&$link) { - $locale = $this->owner->Locale ? $this->owner->Locale : Translatable::get_current_locale(); - if($locale) $link = Controller::join_links($link, '?Locale=' . $locale); - } - - function updateLinkWithSearch(&$link) { - $locale = $this->owner->Locale ? $this->owner->Locale : Translatable::get_current_locale(); - if($locale) $link = Controller::join_links($link, '?Locale=' . $locale); - } - - function updateExtraTreeTools(&$html) { - $locale = $this->owner->Locale ? $this->owner->Locale : Translatable::get_current_locale(); - $html = $this->LangForm()->forTemplate() . $html; - } - - function updateLinkPageAdd(&$link) { - $locale = $this->owner->Locale ? $this->owner->Locale : Translatable::get_current_locale(); - if($locale) $link = Controller::join_links($link, '?Locale=' . $locale); - } - - /** - * 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()); + // 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. + $id = $req->param('ID'); + if ($req->requestVar("Locale")) { + $this->owner->Locale = $req->requestVar("Locale"); + } elseif ($id && is_numeric($id)) { + $record = DataObject::get_by_id($this->owner->stat('tree_class'), $id); + if ($record && $record->Locale) { + $this->owner->Locale = $record->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 FieldList( - $field - ), - new FieldList( - new FormAction('selectlang', _t('CMSMain_left.GO','Go')) - ) - ); - $form->unsetValidator(); - $form->addExtraClass('nostyle'); - - 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'); + $this->owner->Locale = Translatable::default_locale(); + if ($this->owner->class == 'CMSPagesController') { + // the CMSPagesController always needs to have the locale set, + // otherwise page editing will cause an extra + // ajax request which looks weird due to multiple "loading"-flashes + $getVars = $req->getVars(); + if (isset($getVars['url'])) { + unset($getVars['url']); + } + return $this->owner->redirect(Controller::join_links( + $this->owner->Link(), + $req->param('Action'), + $req->param('ID'), + $req->param('OtherID'), + ($query = http_build_query($getVars)) ? "?$query" : null + )); + } + } + Translatable::set_current_locale($this->owner->Locale); - return (count($langs) > 1); - } - - /** - * @return boolean - */ - function IsTranslatableEnabled() { - return SiteTree::has_extension('Translatable'); - } - + // If a locale is set, it needs to match to the current record + $requestLocale = $req->requestVar("Locale"); + $page = $this->owner->currentPage(); + if ( + $req->httpMethod() == 'GET' // leave form submissions alone + && $requestLocale + && $page + && $page->hasExtension('Translatable') + && $page->Locale != $requestLocale + && $req->latestParam('Action') != 'EditorToolbar' + ) { + $transPage = $page->getTranslation($requestLocale); + if ($transPage) { + Translatable::set_current_locale($transPage->Locale); + return $this->owner->redirect(Controller::join_links( + $this->owner->Link('show'), + $transPage->ID + // ?locale will automatically be added + )); + } elseif ($this->owner->class != 'CMSPagesController') { + // If the record is not translated, redirect to pages overview + return $this->owner->redirect(Controller::join_links( + singleton('CMSPagesController')->Link(), + '?Locale=' . $requestLocale + )); + } + } + + // 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'); + } + + public function updateEditForm(&$form) + { + if ($form->getName() == 'RootForm' && SiteConfig::has_extension("Translatable")) { + $siteConfig = SiteConfig::current_site_config(); + $form->Fields()->push(new HiddenField('Locale', '', $siteConfig->Locale)); + } + } + + public function updatePageOptions(&$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. + */ + public function createtranslation($data, $form) + { + $request = $this->owner->getRequest(); + + // Protect against CSRF on destructive action + if (!SecurityToken::inst()->checkRequest($request)) { + return $this->owner->httpError(400); + } + + $langCode = Convert::raw2sql($request->postVar('NewTransLang')); + $record = $this->owner->getRecord($request->postVar('ID')); + if (!$record) { + return $this->owner->httpError(404); + } + + $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 = Controller::join_links( + $this->owner->Link('show'), + $translatedRecord->ID + ); + + // set the X-Pjax header to Content, so that the whole admin panel will be refreshed + $this->owner->getResponse()->addHeader('X-Pjax', 'Content'); + + return $this->owner->redirect($url); + } + + public function updateLink(&$link) + { + $locale = $this->owner->Locale ? $this->owner->Locale : Translatable::get_current_locale(); + if ($locale) { + $link = Controller::join_links($link, '?Locale=' . $locale); + } + } + + public function updateLinkWithSearch(&$link) + { + $locale = $this->owner->Locale ? $this->owner->Locale : Translatable::get_current_locale(); + if ($locale) { + $link = Controller::join_links($link, '?Locale=' . $locale); + } + } + + public function updateExtraTreeTools(&$html) + { + $locale = $this->owner->Locale ? $this->owner->Locale : Translatable::get_current_locale(); + $html = $this->LangForm()->forTemplate() . $html; + } + + public function updateLinkPageAdd(&$link) + { + $locale = $this->owner->Locale ? $this->owner->Locale : Translatable::get_current_locale(); + if ($locale) { + $link = Controller::join_links($link, '?Locale=' . $locale); + } + } + + /** + * Returns a form with all languages with languages already used appearing first. + * + * @return Form + */ + public 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 FieldList( + $field + ), + new FieldList( + new FormAction('selectlang', _t('CMSMain_left.GO', 'Go')) + ) + ); + $form->unsetValidator(); + $form->addExtraClass('nostyle'); + + return $form; + } + + public function selectlang($data, $form) + { + return $this->owner; + } + + /** + * Determine if there are more than one languages in our site tree. + * + * @return boolean + */ + public function MultipleLanguages() + { + $langs = Translatable::get_existing_content_languages('SiteTree'); + + return (count($langs) > 1); + } + + /** + * @return boolean + */ + public function IsTranslatableEnabled() + { + return SiteTree::has_extension('Translatable'); + } } diff --git a/code/controller/TranslatableEditorToolbarExtension.php b/code/controller/TranslatableEditorToolbarExtension.php index 97c309d..85293f9 100644 --- a/code/controller/TranslatableEditorToolbarExtension.php +++ b/code/controller/TranslatableEditorToolbarExtension.php @@ -1,13 +1,13 @@ setValue(Translatable::get_current_locale()); - $field->setForm($form); - $form->Fields()->insertBefore($field, 'internal'); - Requirements::javascript('translatable/javascript/HtmlEditorField.Translatable.js'); - } - +class TranslatableEditorToolbarExtension extends DataExtension +{ + public function updateLinkForm(&$form) + { + $field = new LanguageDropdownField('Language', _t('CMSMain.LANGUAGEDROPDOWNLABEL', 'Language')); + $field->setValue(Translatable::get_current_locale()); + $field->setForm($form); + $form->Fields()->insertBefore($field, 'internal'); + Requirements::javascript('translatable/javascript/HtmlEditorField.Translatable.js'); + } } diff --git a/code/forms/LanguageDropdownField.php b/code/forms/LanguageDropdownField.php index d81a1b8..a36a107 100755 --- a/code/forms/LanguageDropdownField.php +++ b/code/forms/LanguageDropdownField.php @@ -5,108 +5,116 @@ * * @package translatable */ -class LanguageDropdownField extends GroupedDropdownField { +class LanguageDropdownField extends GroupedDropdownField +{ + private static $allowed_actions = array( + 'getLocaleForObject' + ); + + /** + * Create a new LanguageDropdownField + * @param string $name + * @param string $title + * @param array $excludeLocales List of locales that won't be included + * @param string $translatingClass Name of the class with translated instances + * where to look for used languages + * @param string $list Indicates the source language list. + * Can be either Common-English, Common-Native, Locale-English, Locale-Native + */ + public function __construct($name, $title, $excludeLocales = array(), + $translatingClass = 'SiteTree', $list = 'Common-English', $instance = null + ) { + $usedLocalesWithTitle = Translatable::get_existing_content_languages($translatingClass); + $usedLocalesWithTitle = array_diff_key($usedLocalesWithTitle, $excludeLocales); - private static $allowed_actions = array( - 'getLocaleForObject' - ); - - /** - * Create a new LanguageDropdownField - * @param string $name - * @param string $title - * @param array $excludeLocales List of locales that won't be included - * @param string $translatingClass Name of the class with translated instances - * where to look for used languages - * @param string $list Indicates the source language list. - * Can be either Common-English, Common-Native, Locale-English, Locale-Native - */ - function __construct($name, $title, $excludeLocales = array(), - $translatingClass = 'SiteTree', $list = 'Common-English', $instance = null - ) { - $usedLocalesWithTitle = Translatable::get_existing_content_languages($translatingClass); - $usedLocalesWithTitle = array_diff_key($usedLocalesWithTitle, $excludeLocales); + if ('Common-English' == $list) { + $allLocalesWithTitle = i18n::get_common_languages(); + } elseif ('Common-Native' == $list) { + $allLocalesWithTitle = i18n::get_common_languages(true); + } elseif ('Locale-English' == $list) { + $allLocalesWithTitle = i18n::get_common_locales(); + } elseif ('Locale-Native' == $list) { + $allLocalesWithTitle = i18n::get_common_locales(true); + } else { + $allLocalesWithTitle = i18n::config()->all_locales; + } - if('Common-English' == $list) $allLocalesWithTitle = i18n::get_common_languages(); - else if('Common-Native' == $list) $allLocalesWithTitle = i18n::get_common_languages(true); - else if('Locale-English' == $list) $allLocalesWithTitle = i18n::get_common_locales(); - else if('Locale-Native' == $list) $allLocalesWithTitle = i18n::get_common_locales(true); - else $allLocalesWithTitle = i18n::config()->all_locales; + if (isset($allLocales[Translatable::default_locale()])) { + unset($allLocales[Translatable::default_locale()]); + } + + // Limit to allowed locales if defined + // Check for canTranslate() if an $instance is given + $allowedLocales = Translatable::get_allowed_locales(); + foreach ($allLocalesWithTitle as $locale => $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]); + } + } - if(isset($allLocales[Translatable::default_locale()])) { - unset($allLocales[Translatable::default_locale()]); - } - - // Limit to allowed locales if defined - // Check for canTranslate() if an $instance is given - $allowedLocales = Translatable::get_allowed_locales(); - foreach($allLocalesWithTitle as $locale => $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; + } - // 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); + } - parent::__construct($name, $title, $source); - } - - function Type() { - return 'languagedropdown dropdown'; - } - - public function getAttributes() { - return array_merge( - parent::getAttributes(), - array('data-locale-url' => $this->Link('getLocaleForObject')) - ); - } - - /** - * Get the locale for an object that has the Translatable extension. - * - * @return locale - */ - function getLocaleForObject() { - $id = (int)$this->getRequest()->requestVar('id'); - $class = Convert::raw2sql($this->getRequest()->requestVar('class')); - $locale = Translatable::get_current_locale(); - if ($id && $class && class_exists($class) && $class::has_extension('Translatable')) { - // temporarily disable locale filter so that we won't filter out the object - Translatable::disable_locale_filter(); - $object = DataObject::get_by_id($class, $id); - Translatable::enable_locale_filter(); - if ($object) { - $locale = $object->Locale; - } - } - return $locale; - } - + public function Type() + { + return 'languagedropdown dropdown'; + } + + public function getAttributes() + { + return array_merge( + parent::getAttributes(), + array('data-locale-url' => $this->Link('getLocaleForObject')) + ); + } + + /** + * Get the locale for an object that has the Translatable extension. + * + * @return locale + */ + public function getLocaleForObject() + { + $id = (int)$this->getRequest()->requestVar('id'); + $class = Convert::raw2sql($this->getRequest()->requestVar('class')); + $locale = Translatable::get_current_locale(); + if ($id && $class && class_exists($class) && $class::has_extension('Translatable')) { + // temporarily disable locale filter so that we won't filter out the object + Translatable::disable_locale_filter(); + $object = DataObject::get_by_id($class, $id); + Translatable::enable_locale_filter(); + if ($object) { + $locale = $object->Locale; + } + } + return $locale; + } } diff --git a/code/model/Translatable.php b/code/model/Translatable.php index 351786b..2df2b86 100755 --- a/code/model/Translatable.php +++ b/code/model/Translatable.php @@ -151,1715 +151,1887 @@ * * @package translatable */ -class Translatable extends DataExtension implements PermissionProvider { +class Translatable extends DataExtension implements PermissionProvider +{ + const QUERY_LOCALE_FILTER_ENABLED = 'Translatable.LocaleFilterEnabled'; - const QUERY_LOCALE_FILTER_ENABLED = 'Translatable.LocaleFilterEnabled'; - - /** - * 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; - - /** - * @var boolean Check other languages for URLSegment values (only applies to {@link SiteTree}). - * Turn this off to handle language setting yourself, e.g. through language-specific subdomains - * or URL path prefixes like "/en/mypage". - */ - private static $enforce_global_unique_urls = true; - - /** - * Exclude these fields from translation - * - * @var array - * @config - */ - private static $translate_excluded_fields = array( - 'ViewerGroups', - 'EditorGroups', - 'CanViewType', - 'CanEditType', - 'NewTransLang', - 'createtranslation' - ); - - /** - * 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($_REQUEST['locale']) && !$langsAvailable) - || (isset($_REQUEST['locale']) - && in_array($_REQUEST['locale'], $langsAvailable)) - ) { - // get from request parameter - self::set_current_locale($_REQUEST['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::config()->all_locales; - 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 = $class::get() - ->where($filter) - ->where(sprintf('"Locale" = \'%s\'', Convert::raw2sql($locale))) - ->sort($orderby) - ->First(); - 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 = "") { - 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 = $class::get(); - if($filter) $result = $result->where($filter); - if($sort) $result = $result->sort($sort); - if($join) $result = $result->leftJoin($join); - if($limit) $result = $result->limit($limit); - 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()}. - * - * @param $enabled (default true), if false this call is a no-op - see {@link disable_locale_filter()} - */ - public static function enable_locale_filter($enabled = true) { - if ($enabled) { - self::$locale_filter_enabled = true; - } - } - - /** - * Disables automatic locale filtering in {@link augmentSQL()}. This can be re-enabled - * using {@link enable_locale_filter()}. - * - * Note that all places that disable the locale filter should generally re-enable it - * before returning from that block of code (function, etc). This is made easier by - * using the following pattern: - * - * - * $enabled = Translatable::disable_locale_filter(); - * // do some work here - * Translatable::enable_locale_filter($enabled); - * return $whateverYouNeedTO; - * - * - * By using this pattern, the call to enable the filter will not re-enable it if it - * was not enabled initially. That will keep code that called your function from - * breaking if it had already disabled the locale filter since it will not expect - * calling your function to change the global state by re-enabling the filter. - * - * @return boolean true if the locale filter was enabled, false if it was not - */ - public static function disable_locale_filter() { - $enabled = self::$locale_filter_enabled; - self::$locale_filter_enabled = false; - return $enabled; - } - - /** - * 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 SQLSelect( - '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 SiteTree::add_extension('Translatable') - */ - static function enable() { - if(class_exists('SiteTree')) SiteTree::add_extension('Translatable'); - } - - /** - * Disable the multilingual feature - * - * @deprecated 2.4 Use SiteTree::remove_extension('Translatable') - */ - static function disable() { - if(class_exists('SiteTree')) SiteTree::remove_extension('Translatable'); - } - - /** - * Check whether multilingual support has been enabled - * - * @deprecated 2.4 Use SiteTree::has_extension('Translatable') - * @return boolean True if enabled - */ - static function is_enabled() { - if(class_exists('SiteTree')){ - return SiteTree::has_extension('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->db()), - array_keys($this->owner->hasMany()), - array_keys($this->owner->manyMany()) - ); - foreach (array_keys($this->owner->hasOne()) as $fieldname) { - $this->translatableFields[] = $fieldname.'ID'; - } - } - } - - static function get_extra_config($class, $extensionClass, $args = null) { - $config = array(); - $config['defaults'] = array( - "Locale" => Translatable::default_locale() // as an overloaded getter as well: getLang() - ); - $config['db'] = array( - "Locale" => "DBLocale", - //"TranslationMasterID" => "Int" // optional relation to a "translation master" - ); - return $config; - } - - /** - * Check if a given SQLSelect filters on the Locale field - * - * @param SQLSelect $query - * @return boolean - */ - protected function filtersOnLocale($query) { - foreach($query->getWhere() as $condition) { - // Compat for 3.1/3.2 where syntax - if(is_array($condition)) { - // In >=3.2 each $condition is a single length array('condition' => array('params')) - reset($condition); - $condition = key($condition); - } - - // >=3.2 allows conditions to be expressed as evaluatable objects - if(interface_exists('SQLConditionGroup') && ($condition instanceof SQLConditionGroup)) { - $condition = $condition->conditionSQL($params); - } - - if(preg_match('/("|\'|`)Locale("|\'|`)/', $condition)) return true; - } - } - - /** - * 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(SQLSelect $query, DataQuery $dataQuery = null) { - // 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 - if($this->owner->ID && !empty($this->owner->Locale)) { - $locale = $this->owner->Locale; - } else { - $locale = Translatable::get_current_locale(); - } - - $baseTable = ClassInfo::baseDataClass($this->owner->class); - if( - $locale - // unless the filter has been temporarily disabled - && self::locale_filter_enabled() - // or it was disabled when the DataQuery was created - && $dataQuery->getQueryParam(self::QUERY_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->getFrom())) !== false - //&& !$query->filtersOnFK() - ) { - // Or we're already filtering by Lang (either from an earlier augmentSQL() - // call or through custom SQL filters) - $filtersOnLocale = array_filter($query->getWhere(), function($predicates) { - foreach($predicates as $predicate => $params) { - if(preg_match('/("|\'|`)Locale("|\'|`)/', $predicate)) return true; - } - }); - if(!$filtersOnLocale) { - $qry = sprintf('"%s"."Locale" = \'%s\'', $baseTable, Convert::raw2sql($locale)); - $query->addWhere($qry); - } - } - } - - function augmentDataQueryCreation(SQLSelect $sqlQuery, DataQuery $dataQuery) { - $enabled = self::locale_filter_enabled(); - $dataQuery->setQueryParam(self::QUERY_LOCALE_FILTER_ENABLED, $enabled); - } - - /** - * 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 - */ - public 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 || $obj->ObsoleteClassName) continue; - - $obj->Locale = Translatable::default_locale(); - - $oldMode = Versioned::get_reading_mode(); - Versioned::reading_stage($stage); - $obj->writeWithoutVersion(); - Versioned::set_reading_mode($oldMode); - - $obj->addTranslationGroup($obj->ID); - $obj->destroy(); - unset($obj); - } - } - } else { - foreach($idsWithoutLocale as $id) { - $obj = DataObject::get_by_id($this->owner->class, $id); - if(!$obj || $obj->ObsoleteClassName) 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(); - $changed = isset($changedFields['ClassName']); - - if ($changed && $this->owner->hasExtension('Versioned')) { - // this is required because when publishing a node the before/after - // values of $changedFields['ClassName'] will be the same because - // the record was already written to the stage/draft table and thus - // the record was updated, and then publish('Stage', 'Live') is - // called, which uses forceChange, which will make all the fields - // act as though they are changed, although the before/after values - // will be the same - // So, we load one from the current stage and test against it - // This is to prevent the overhead of writing all translations when - // the class didn't actually change. - $baseDataClass = ClassInfo::baseDataClass($this->owner->class); - $currentStage = Versioned::current_stage(); - $fresh = Versioned::get_one_by_stage( - $baseDataClass, - Versioned::current_stage(), - '"ID" = ' . $this->owner->ID, - null - ); - if ($fresh) { - $changed = $changedFields['ClassName']['after'] != $fresh->ClassName; - } - } - - if($changed) { - $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 = SiteTree::get()->where(sprintf ( - '"URLSegment" = \'%s\'%s', - Convert::raw2sql($URLSegment), - (is_int($parentID) ? " AND \"ParentID\" = $parentID" : null) - ))->First(); - self::enable_locale_filter(); - - return $default; - } - - //-----------------------------------------------------------------------------------------------// - - function applyTranslatableFieldsUpdate($fields, $type) { - if (method_exists($this, $type)) { - $this->$type($fields); - } else { - throw new InvalidArgumentException("Method $type does not exist on object of type ". get_class($this)); - } - } - - /** - * 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. - * - * This method can be called multiple times on the same FieldList - * because it checks which fields have already been added or modified. - * - * @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(FieldList $fields) { - $this->addTranslatableFields($fields); - - // 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; - $alreadyTranslatedLocales = array_combine($alreadyTranslatedLocales, $alreadyTranslatedLocales); - - // Check if fields exist already to avoid adding them twice on repeat invocations - $tab = $fields->findOrMakeTab('Root.Translations', _t('Translatable.TRANSLATIONS', 'Translations')); - if(!$tab->fieldByName('CreateTransHeader')) { - $tab->push(new HeaderField( - 'CreateTransHeader', - _t('Translatable.CREATE', 'Create new translation'), - 2 - )); - } - if(!$tab->fieldByName('NewTransLang') && !$tab->fieldByName('AllTransCreated')) { - $langDropdown = LanguageDropdownField::create( - "NewTransLang", - _t('Translatable.NEWLANGUAGE', 'New language'), - $alreadyTranslatedLocales, - 'SiteTree', - 'Locale-English', - $this->owner - )->addExtraClass('languageDropdown no-change-track'); - $tab->push($langDropdown); - $canAddLocale = (count($langDropdown->getSource()) > 0); - - if($canAddLocale) { - // Only add create button if new languages are available - $tab->push( - $createButton = InlineFormAction::create( - 'createtranslation', - _t('Translatable.CREATEBUTTON', 'Create') - )->addExtraClass('createTranslationButton') - ); - $createButton->includeDefaultJS(false); // not fluent API... - } else { - $tab->removeByName('NewTransLang'); - $tab->push(new LiteralField( - 'AllTransCreated', - _t('Translatable.ALLCREATED', 'All allowed translations have been created.') - )); - } - } - if($alreadyTranslatedLocales) { - if(!$tab->fieldByName('ExistingTransHeader')) { - $tab->push(new HeaderField( - 'ExistingTransHeader', - _t('Translatable.EXISTING', 'Existing translations'), - 3 - )); - if (!$tab->fieldByName('existingtrans')) { - $existingTransHTML = '
    '; - if ($existingTranslations = $this->getTranslations()) { - foreach ($existingTranslations as $existingTranslation) { - if ($existingTranslation && $existingTranslation->hasMethod('CMSEditLink')) { - $existingTransHTML .= sprintf( - '
  • %s
  • ', - Controller::join_links( - $existingTranslation->CMSEditLink(), - '?Locale=' . $existingTranslation->Locale - ), - i18n::get_locale_name($existingTranslation->Locale) - ); - } - } - } - $existingTransHTML .= '
'; - $tab->push(new LiteralField('existingtrans', $existingTransHTML)); - } - } - } - } - - function updateSettingsFields(&$fields) { - $this->addTranslatableFields($fields); - } - - public function updateRelativeLink(&$base, &$action) { - // Prevent home pages for non-default locales having their urlsegments - // reduced to the site root. - if($base === null && $this->owner->Locale != self::default_locale()){ - $base = $this->owner->URLSegment; - } - } - - /** - * This method can be called multiple times on the same FieldList - * because it checks which fields have already been added or modified. - */ - protected function addTranslatableFields(&$fields) { - // used in LeftAndMain->init() to set language state when reading/writing record - $fields->push(new HiddenField("Locale", "Locale", $this->owner->Locale)); - - // Don't apply these modifications for normal DataObjects - they rely on CMSMain logic - if(!class_exists('SiteTree')) return; - if(!($this->owner instanceof SiteTree)) return; - - // 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; - } - } - - // Get excluded fields from translation - $excludeFields = $this->owner->config()->translate_excluded_fields; - - // if a language other than default language is used, we're in "translation mode", - // hence have to modify the original fields - $baseClass = $this->owner->class; - 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(); - - if($originalRecord && $isTranslationMode) { - // 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) { - // Transformation is a visual helper for CMS authors, so ignore hidden fields - if($dataField instanceof HiddenField) continue; - // Some fields are explicitly excluded from transformation - if(in_array($dataField->getName(), $excludeFields)) continue; - // Readonly field which has been added previously - if(preg_match('/_original$/', $dataField->getName())) continue; - // Field already has been transformed - if(isset($allDataFields[$dataField->getName() . '_original'])) continue; - // CheckboxField which is already transformed - if(preg_match('/class=\"originalvalue\"/', $dataField->Title())) continue; - - if(in_array($dataField->getName(), $translatableFieldNames)) { - // if the field is translatable, perform transformation - $fields->replaceField($dataField->getName(), $transformation->transformFormField($dataField)); - } elseif(!$dataField->isReadonly()) { - // else field shouldn't be editable in translation-mode, make readonly - $fields->replaceField($dataField->getName(), $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') - ) - ) - ) - ); - } - } - - /** - * 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()) return new ArrayList(); - - // HACK need to disable language filtering in augmentSQL(), - // as we purposely want to get different language - // also save state of locale-filter, revert to this state at the - // end of this method - $localeFilterEnabled = false; - if(self::locale_filter_enabled()) { - self::disable_locale_filter(); - $localeFilterEnabled = true; - } - - $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); - } - $currentStage = Versioned::current_stage(); - $joinOnClause = sprintf('"%s_translationgroups"."OriginalID" = "%s"."ID"', $baseDataClass, $baseDataClass); - if($this->owner->hasExtension("Versioned")) { - if($stage) Versioned::reading_stage($stage); - $translations = Versioned::get_by_stage( - $baseDataClass, - Versioned::current_stage(), - $filter, - null - )->leftJoin("{$baseDataClass}_translationgroups", $joinOnClause); - if($stage) Versioned::reading_stage($currentStage); - } else { - $class = $this->owner->class; - $translations = $baseDataClass::get() - ->where($filter) - ->leftJoin("{$baseDataClass}_translationgroups", $joinOnClause); - } - - // only re-enable locale-filter if it was enabled at the beginning of this method - if($localeFilterEnabled) { - 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; - } - - /** - * When the SiteConfig object is automatically instantiated, we should ensure that - * 1. All SiteConfig objects belong to the same group - * 2. Defaults are correctly initiated from the base object - * 3. The creation mechanism uses the createTranslation function in order to be consistent - * This function ensures that any already created "vanilla" SiteConfig object is populated - * correctly with translated values. - * This function DOES populate the ID field with the newly created object ID - * @see SiteConfig - */ - protected function populateSiteConfigDefaults() { - - // Work-around for population of defaults during database initialisation. - // When the database is being setup singleton('SiteConfig') is called. - if(!DB::getConn()->hasTable($this->owner->class)) return; - if(!DB::getConn()->hasField($this->owner->class, 'Locale')) return; - if(DB::getConn()->isSchemaUpdating()) return; - - // Find the best base translation for SiteConfig - $enabled = Translatable::locale_filter_enabled(); - Translatable::disable_locale_filter(); - $existingConfig = SiteConfig::get()->filter(array( - 'Locale' => Translatable::default_locale() - ))->first(); - if(!$existingConfig) $existingConfig = SiteConfig::get()->first(); - if ($enabled) { - Translatable::enable_locale_filter(); - } - - // Stage this SiteConfig and copy into the current object - if( - $existingConfig - // Double-up of SiteConfig in the same locale can be ignored. Often caused by singleton(SiteConfig) - && !$existingConfig->getTranslation(Translatable::get_current_locale()) - // If translation is not allowed by the current user then do not - // allow this code to attempt any behind the scenes translation. - && $existingConfig->canTranslate(null, Translatable::get_current_locale()) - ) { - // Create an unsaved "staging" translated object using the correct createTranslation mechanism - $stagingConfig = $existingConfig->createTranslation(Translatable::get_current_locale(), false); - $this->owner->update($stagingConfig->toMap()); - } - - // Maintain single translation group for SiteConfig - if($existingConfig) { - $this->owner->_TranslationGroupID = $existingConfig->getTranslationGroup(); - } - - $this->owner->Locale = Translatable::get_current_locale(); - } - - /** - * Enables automatic population of SiteConfig fields using createTranslation if - * created outside of the Translatable module - * @var boolean - */ - public static $enable_siteconfig_generation = true; - - /** - * Hooks into the DataObject::populateDefaults() method - */ - public function populateDefaults() { - if ( - empty($this->owner->ID) - && ($this->owner instanceof SiteConfig) - && self::$enable_siteconfig_generation - ) { - // Use enable_siteconfig_generation to prevent infinite loop during object creation - self::$enable_siteconfig_generation = false; - $this->populateSiteConfigDefaults(); - self::$enable_siteconfig_generation = true; - } - } - - /** - * 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 Target locale to translate this object into - * @param boolean $saveTranslation Flag indicating whether the new record - * should be saved to the database. - * @return DataObject The translated object - */ - function createTranslation($locale, $saveTranslation = true) { - 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(array_diff_key($this->owner->toMap(), array('Version' => null))); - - // 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; - $newTranslation->Version = 0; - - $originalPage = $this->getTranslation(self::default_locale()); - if ($originalPage) { - $urlSegment = $originalPage->URLSegment; - } else { - $urlSegment = $newTranslation->URLSegment; - } - - // Only make segment unique if it should be enforced - if(Config::inst()->get('Translatable', 'enforce_global_unique_urls')) { - $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; - if($saveTranslation) $newTranslation->write(); - - // run callback on page for translation related hooks - $newTranslation->invokeWithExtensions('onTranslatableCreate', $saveTranslation); - - 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) { - $translations = $translations->toArray(); - $translations[] = $this->owner; - - foreach($translations as $translation) { - $tags .= sprintf($template, - Convert::raw2xml($translation->Title), - i18n::convert_rfc1766($translation->Locale), - $translation->AbsoluteLink() - ); - } - } - } - - function providePermissions() { - if(!SiteTree::has_extension('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', - 'Translate pages into a language' - ), - $localeName - ); - } - - $permissions['TRANSLATE_ALL'] = _t( - 'Translatable.TRANSLATEALLPERMISSION', - 'Translate into all available languages' - ); - - $permissions['VIEW_LANGS'] = _t( - 'Translatable.TRANSLATEVIEWLANGS', - 'View language dropdown' - ); - - 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 SQLSelect("Distinct \"Locale\"","\"$baseTable\"",$where, '', "\"Locale\""); - $dbLangs = $query->execute()->column(); - $langlist = array_merge((array)Translatable::default_locale(), (array)$dbLangs); - $returnMap = array(); - $allCodes = array_merge( - Config::inst()->get('i18n', 'all_locales'), - Config::inst()->get('i18n', 'common_locales') - ); - foreach ($langlist as $langCode) { - if($langCode && isset($allCodes[$langCode])) { - if(is_array($allCodes[$langCode])) { - $returnMap[$langCode] = $allCodes[$langCode]['name']; - } else { - $returnMap[$langCode] = $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::config()->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 + /** + * The 'default' language. + * @var string */ - public function augmentValidURLSegment() { - $reEnableFilter = false; - if(!Config::inst()->get('Translatable', 'enforce_global_unique_urls')) { - self::enable_locale_filter(); - } elseif(self::locale_filter_enabled()) { - self::disable_locale_filter(); - $reEnableFilter = true; - } + 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; - $IDFilter = ($this->owner->ID) ? "AND \"SiteTree\".\"ID\" <> {$this->owner->ID}" : null; - $parentFilter = null; + /** + * An array of fields that can be translated. + * @var array + */ + protected $translatableFields = null; - if (Config::inst()->get('SiteTree', 'nested_urls')) { - if($this->owner->ParentID) { - $parentFilter = " AND \"SiteTree\".\"ParentID\" = {$this->owner->ParentID}"; - } else { - $parentFilter = ' AND "SiteTree"."ParentID" = 0'; - } - } + /** + * 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; - $existingPage = SiteTree::get() - // disable get_one cache, as this otherwise may pick up results from when locale_filter was on - ->where("\"URLSegment\" = '{$this->owner->URLSegment}' $IDFilter $parentFilter")->First(); - if($reEnableFilter) self::enable_locale_filter(); - - // By returning TRUE or FALSE, we overrule the base SiteTree->validateURLSegment() logic - return !$existingPage; - } - + /** + * @var boolean Check other languages for URLSegment values (only applies to {@link SiteTree}). + * Turn this off to handle language setting yourself, e.g. through language-specific subdomains + * or URL path prefixes like "/en/mypage". + */ + private static $enforce_global_unique_urls = true; + + /** + * Exclude these fields from translation + * + * @var array + * @config + */ + private static $translate_excluded_fields = array( + 'ViewerGroups', + 'EditorGroups', + 'CanViewType', + 'CanEditType', + 'NewTransLang', + 'createtranslation' + ); + + /** + * Reset static configuration variables to their default values + */ + public 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). + */ + public static function choose_site_locale($langsAvailable = array()) + { + if (self::$current_locale) { + return self::$current_locale; + } + + if ( + (isset($_REQUEST['locale']) && !$langsAvailable) + || (isset($_REQUEST['locale']) + && in_array($_REQUEST['locale'], $langsAvailable)) + ) { + // get from request parameter + self::set_current_locale($_REQUEST['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 + */ + public 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 + */ + public static function set_default_locale($locale) + { + if ($locale && !i18n::validate_locale($locale)) { + throw new InvalidArgumentException(sprintf('Invalid locale "%s"', $locale)); + } + + $localeList = i18n::config()->all_locales; + 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 + */ + public 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. + */ + public 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 + */ + public 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 = $class::get() + ->where($filter) + ->where(sprintf('"Locale" = \'%s\'', Convert::raw2sql($locale))) + ->sort($orderby) + ->First(); + 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. + */ + public static function get_by_locale($class, $locale, $filter = '', $sort = '', $join = "", $limit = "") + { + 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 = $class::get(); + if ($filter) { + $result = $result->where($filter); + } + if ($sort) { + $result = $result->sort($sort); + } + if ($join) { + $result = $result->leftJoin($join); + } + if ($limit) { + $result = $result->limit($limit); + } + 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()}. + * + * @param $enabled (default true), if false this call is a no-op - see {@link disable_locale_filter()} + */ + public static function enable_locale_filter($enabled = true) + { + if ($enabled) { + self::$locale_filter_enabled = true; + } + } + + /** + * Disables automatic locale filtering in {@link augmentSQL()}. This can be re-enabled + * using {@link enable_locale_filter()}. + * + * Note that all places that disable the locale filter should generally re-enable it + * before returning from that block of code (function, etc). This is made easier by + * using the following pattern: + * + * + * $enabled = Translatable::disable_locale_filter(); + * // do some work here + * Translatable::enable_locale_filter($enabled); + * return $whateverYouNeedTO; + * + * + * By using this pattern, the call to enable the filter will not re-enable it if it + * was not enabled initially. That will keep code that called your function from + * breaking if it had already disabled the locale filter since it will not expect + * calling your function to change the global state by re-enabling the filter. + * + * @return boolean true if the locale filter was enabled, false if it was not + */ + public static function disable_locale_filter() + { + $enabled = self::$locale_filter_enabled; + self::$locale_filter_enabled = false; + return $enabled; + } + + /** + * 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. + */ + public 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 SQLSelect( + '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 + */ + public 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 + */ + public 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 SiteTree::add_extension('Translatable') + */ + public static function enable() + { + if (class_exists('SiteTree')) { + SiteTree::add_extension('Translatable'); + } + } + + /** + * Disable the multilingual feature + * + * @deprecated 2.4 Use SiteTree::remove_extension('Translatable') + */ + public static function disable() + { + if (class_exists('SiteTree')) { + SiteTree::remove_extension('Translatable'); + } + } + + /** + * Check whether multilingual support has been enabled + * + * @deprecated 2.4 Use SiteTree::has_extension('Translatable') + * @return boolean True if enabled + */ + public static function is_enabled() + { + if (class_exists('SiteTree')) { + return SiteTree::has_extension('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()}). + */ + public 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() + } + + public 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->db()), + array_keys($this->owner->hasMany()), + array_keys($this->owner->manyMany()) + ); + foreach (array_keys($this->owner->hasOne()) as $fieldname) { + $this->translatableFields[] = $fieldname.'ID'; + } + } + } + + public static function get_extra_config($class, $extensionClass, $args = null) + { + $config = array(); + $config['defaults'] = array( + "Locale" => Translatable::default_locale() // as an overloaded getter as well: getLang() + ); + $config['db'] = array( + "Locale" => "DBLocale", + //"TranslationMasterID" => "Int" // optional relation to a "translation master" + ); + return $config; + } + + /** + * Check if a given SQLSelect filters on the Locale field + * + * @param SQLSelect $query + * @return boolean + */ + protected function filtersOnLocale($query) + { + foreach ($query->getWhere() as $condition) { + // Compat for 3.1/3.2 where syntax + if (is_array($condition)) { + // In >=3.2 each $condition is a single length array('condition' => array('params')) + reset($condition); + $condition = key($condition); + } + + // >=3.2 allows conditions to be expressed as evaluatable objects + if (interface_exists('SQLConditionGroup') && ($condition instanceof SQLConditionGroup)) { + $condition = $condition->conditionSQL($params); + } + + if (preg_match('/("|\'|`)Locale("|\'|`)/', $condition)) { + return true; + } + } + } + + /** + * 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". + */ + public function augmentSQL(SQLSelect $query, DataQuery $dataQuery = null) + { + // 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 + if ($this->owner->ID && !empty($this->owner->Locale)) { + $locale = $this->owner->Locale; + } else { + $locale = Translatable::get_current_locale(); + } + + $baseTable = ClassInfo::baseDataClass($this->owner->class); + if ( + $locale + // unless the filter has been temporarily disabled + && self::locale_filter_enabled() + // or it was disabled when the DataQuery was created + && $dataQuery->getQueryParam(self::QUERY_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->getFrom())) !== false + //&& !$query->filtersOnFK() + ) { + // Or we're already filtering by Lang (either from an earlier augmentSQL() + // call or through custom SQL filters) + $filtersOnLocale = array_filter($query->getWhere(), function ($predicates) { + foreach ($predicates as $predicate => $params) { + if (preg_match('/("|\'|`)Locale("|\'|`)/', $predicate)) { + return true; + } + } + }); + if (!$filtersOnLocale) { + $qry = sprintf('"%s"."Locale" = \'%s\'', $baseTable, Convert::raw2sql($locale)); + $query->addWhere($qry); + } + } + } + + public function augmentDataQueryCreation(SQLSelect $sqlQuery, DataQuery $dataQuery) + { + $enabled = self::locale_filter_enabled(); + $dataQuery->setQueryParam(self::QUERY_LOCALE_FILTER_ENABLED, $enabled); + } + + /** + * 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. + */ + public 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 + */ + public 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 || $obj->ObsoleteClassName) { + continue; + } + + $obj->Locale = Translatable::default_locale(); + + $oldMode = Versioned::get_reading_mode(); + Versioned::reading_stage($stage); + $obj->writeWithoutVersion(); + Versioned::set_reading_mode($oldMode); + + $obj->addTranslationGroup($obj->ID); + $obj->destroy(); + unset($obj); + } + } + } else { + foreach ($idsWithoutLocale as $id) { + $obj = DataObject::get_by_id($this->owner->class, $id); + if (!$obj || $obj->ObsoleteClassName) { + 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 + */ + public function isVersionedTable($table) + { + return false; + } + + /** + * Note: The bulk of logic is in ModelAsController->getNestedController() + * and ContentController->handleRequest() + */ + public function contentcontrollerInit($controller) + { + $controller->Locale = Translatable::choose_site_locale(); + } + + public function modelascontrollerInit($controller) + { + //$this->contentcontrollerInit($controller); + } + + public 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()}. + */ + public 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(); + $changed = isset($changedFields['ClassName']); + + if ($changed && $this->owner->hasExtension('Versioned')) { + // this is required because when publishing a node the before/after + // values of $changedFields['ClassName'] will be the same because + // the record was already written to the stage/draft table and thus + // the record was updated, and then publish('Stage', 'Live') is + // called, which uses forceChange, which will make all the fields + // act as though they are changed, although the before/after values + // will be the same + // So, we load one from the current stage and test against it + // This is to prevent the overhead of writing all translations when + // the class didn't actually change. + $baseDataClass = ClassInfo::baseDataClass($this->owner->class); + $currentStage = Versioned::current_stage(); + $fresh = Versioned::get_one_by_stage( + $baseDataClass, + Versioned::current_stage(), + '"ID" = ' . $this->owner->ID, + null + ); + if ($fresh) { + $changed = $changedFields['ClassName']['after'] != $fresh->ClassName; + } + } + + if ($changed) { + $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; + } + } + + public 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. + */ + public 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 = SiteTree::get()->where(sprintf( + '"URLSegment" = \'%s\'%s', + Convert::raw2sql($URLSegment), + (is_int($parentID) ? " AND \"ParentID\" = $parentID" : null) + ))->First(); + self::enable_locale_filter(); + + return $default; + } + + //-----------------------------------------------------------------------------------------------// + + public function applyTranslatableFieldsUpdate($fields, $type) + { + if (method_exists($this, $type)) { + $this->$type($fields); + } else { + throw new InvalidArgumentException("Method $type does not exist on object of type ". get_class($this)); + } + } + + /** + * 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. + * + * This method can be called multiple times on the same FieldList + * because it checks which fields have already been added or modified. + * + * @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. + */ + public function updateCMSFields(FieldList $fields) + { + $this->addTranslatableFields($fields); + + // 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; + $alreadyTranslatedLocales = array_combine($alreadyTranslatedLocales, $alreadyTranslatedLocales); + + // Check if fields exist already to avoid adding them twice on repeat invocations + $tab = $fields->findOrMakeTab('Root.Translations', _t('Translatable.TRANSLATIONS', 'Translations')); + if (!$tab->fieldByName('CreateTransHeader')) { + $tab->push(new HeaderField( + 'CreateTransHeader', + _t('Translatable.CREATE', 'Create new translation'), + 2 + )); + } + if (!$tab->fieldByName('NewTransLang') && !$tab->fieldByName('AllTransCreated')) { + $langDropdown = LanguageDropdownField::create( + "NewTransLang", + _t('Translatable.NEWLANGUAGE', 'New language'), + $alreadyTranslatedLocales, + 'SiteTree', + 'Locale-English', + $this->owner + )->addExtraClass('languageDropdown no-change-track'); + $tab->push($langDropdown); + $canAddLocale = (count($langDropdown->getSource()) > 0); + + if ($canAddLocale) { + // Only add create button if new languages are available + $tab->push( + $createButton = InlineFormAction::create( + 'createtranslation', + _t('Translatable.CREATEBUTTON', 'Create') + )->addExtraClass('createTranslationButton') + ); + $createButton->includeDefaultJS(false); // not fluent API... + } else { + $tab->removeByName('NewTransLang'); + $tab->push(new LiteralField( + 'AllTransCreated', + _t('Translatable.ALLCREATED', 'All allowed translations have been created.') + )); + } + } + if ($alreadyTranslatedLocales) { + if (!$tab->fieldByName('ExistingTransHeader')) { + $tab->push(new HeaderField( + 'ExistingTransHeader', + _t('Translatable.EXISTING', 'Existing translations'), + 3 + )); + if (!$tab->fieldByName('existingtrans')) { + $existingTransHTML = '
    '; + if ($existingTranslations = $this->getTranslations()) { + foreach ($existingTranslations as $existingTranslation) { + if ($existingTranslation && $existingTranslation->hasMethod('CMSEditLink')) { + $existingTransHTML .= sprintf( + '
  • %s
  • ', + Controller::join_links( + $existingTranslation->CMSEditLink(), + '?Locale=' . $existingTranslation->Locale + ), + i18n::get_locale_name($existingTranslation->Locale) + ); + } + } + } + $existingTransHTML .= '
'; + $tab->push(new LiteralField('existingtrans', $existingTransHTML)); + } + } + } + } + + public function updateSettingsFields(&$fields) + { + $this->addTranslatableFields($fields); + } + + public function updateRelativeLink(&$base, &$action) + { + // Prevent home pages for non-default locales having their urlsegments + // reduced to the site root. + if ($base === null && $this->owner->Locale != self::default_locale()) { + $base = $this->owner->URLSegment; + } + } + + /** + * This method can be called multiple times on the same FieldList + * because it checks which fields have already been added or modified. + */ + protected function addTranslatableFields(&$fields) + { + // used in LeftAndMain->init() to set language state when reading/writing record + $fields->push(new HiddenField("Locale", "Locale", $this->owner->Locale)); + + // Don't apply these modifications for normal DataObjects - they rely on CMSMain logic + if (!class_exists('SiteTree')) { + return; + } + if (!($this->owner instanceof SiteTree)) { + return; + } + + // 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; + } + } + } + + // Get excluded fields from translation + $excludeFields = $this->owner->config()->translate_excluded_fields; + + // if a language other than default language is used, we're in "translation mode", + // hence have to modify the original fields + $baseClass = $this->owner->class; + 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(); + + if ($originalRecord && $isTranslationMode) { + // 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) { + // Transformation is a visual helper for CMS authors, so ignore hidden fields + if ($dataField instanceof HiddenField) { + continue; + } + // Some fields are explicitly excluded from transformation + if (in_array($dataField->getName(), $excludeFields)) { + continue; + } + // Readonly field which has been added previously + if (preg_match('/_original$/', $dataField->getName())) { + continue; + } + // Field already has been transformed + if (isset($allDataFields[$dataField->getName() . '_original'])) { + continue; + } + // CheckboxField which is already transformed + if (preg_match('/class=\"originalvalue\"/', $dataField->Title())) { + continue; + } + + if (in_array($dataField->getName(), $translatableFieldNames)) { + // if the field is translatable, perform transformation + $fields->replaceField($dataField->getName(), $transformation->transformFormField($dataField)); + } elseif (!$dataField->isReadonly()) { + // else field shouldn't be editable in translation-mode, make readonly + $fields->replaceField($dataField->getName(), $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') + ) + ) + ) + ); + } + } + + /** + * 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 + */ + public function getTranslatableFields() + { + return $this->translatableFields; + } + + /** + * Return the base table - the class that directly extends DataObject. + * @return string + */ + public function baseTable($stage = null) + { + $tableClasses = ClassInfo::dataClassesFor($this->owner->class); + $baseClass = array_shift($tableClasses); + return (!$stage || $stage == $this->defaultStage) ? $baseClass : $baseClass . "_$stage"; + } + + public 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 + */ + public function getTranslations($locale = null, $stage = null) + { + if ($locale && !i18n::validate_locale($locale)) { + throw new InvalidArgumentException(sprintf('Invalid locale "%s"', $locale)); + } + + if (!$this->owner->exists()) { + return new ArrayList(); + } + + // HACK need to disable language filtering in augmentSQL(), + // as we purposely want to get different language + // also save state of locale-filter, revert to this state at the + // end of this method + $localeFilterEnabled = false; + if (self::locale_filter_enabled()) { + self::disable_locale_filter(); + $localeFilterEnabled = true; + } + + $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); + } + $currentStage = Versioned::current_stage(); + $joinOnClause = sprintf('"%s_translationgroups"."OriginalID" = "%s"."ID"', $baseDataClass, $baseDataClass); + if ($this->owner->hasExtension("Versioned")) { + if ($stage) { + Versioned::reading_stage($stage); + } + $translations = Versioned::get_by_stage( + $baseDataClass, + Versioned::current_stage(), + $filter, + null + )->leftJoin("{$baseDataClass}_translationgroups", $joinOnClause); + if ($stage) { + Versioned::reading_stage($currentStage); + } + } else { + $class = $this->owner->class; + $translations = $baseDataClass::get() + ->where($filter) + ->leftJoin("{$baseDataClass}_translationgroups", $joinOnClause); + } + + // only re-enable locale-filter if it was enabled at the beginning of this method + if ($localeFilterEnabled) { + 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 + */ + public 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; + } + + /** + * When the SiteConfig object is automatically instantiated, we should ensure that + * 1. All SiteConfig objects belong to the same group + * 2. Defaults are correctly initiated from the base object + * 3. The creation mechanism uses the createTranslation function in order to be consistent + * This function ensures that any already created "vanilla" SiteConfig object is populated + * correctly with translated values. + * This function DOES populate the ID field with the newly created object ID + * @see SiteConfig + */ + protected function populateSiteConfigDefaults() + { + + // Work-around for population of defaults during database initialisation. + // When the database is being setup singleton('SiteConfig') is called. + if (!DB::getConn()->hasTable($this->owner->class)) { + return; + } + if (!DB::getConn()->hasField($this->owner->class, 'Locale')) { + return; + } + if (DB::getConn()->isSchemaUpdating()) { + return; + } + + // Find the best base translation for SiteConfig + $enabled = Translatable::locale_filter_enabled(); + Translatable::disable_locale_filter(); + $existingConfig = SiteConfig::get()->filter(array( + 'Locale' => Translatable::default_locale() + ))->first(); + if (!$existingConfig) { + $existingConfig = SiteConfig::get()->first(); + } + if ($enabled) { + Translatable::enable_locale_filter(); + } + + // Stage this SiteConfig and copy into the current object + if ( + $existingConfig + // Double-up of SiteConfig in the same locale can be ignored. Often caused by singleton(SiteConfig) + && !$existingConfig->getTranslation(Translatable::get_current_locale()) + // If translation is not allowed by the current user then do not + // allow this code to attempt any behind the scenes translation. + && $existingConfig->canTranslate(null, Translatable::get_current_locale()) + ) { + // Create an unsaved "staging" translated object using the correct createTranslation mechanism + $stagingConfig = $existingConfig->createTranslation(Translatable::get_current_locale(), false); + $this->owner->update($stagingConfig->toMap()); + } + + // Maintain single translation group for SiteConfig + if ($existingConfig) { + $this->owner->_TranslationGroupID = $existingConfig->getTranslationGroup(); + } + + $this->owner->Locale = Translatable::get_current_locale(); + } + + /** + * Enables automatic population of SiteConfig fields using createTranslation if + * created outside of the Translatable module + * @var boolean + */ + public static $enable_siteconfig_generation = true; + + /** + * Hooks into the DataObject::populateDefaults() method + */ + public function populateDefaults() + { + if ( + empty($this->owner->ID) + && ($this->owner instanceof SiteConfig) + && self::$enable_siteconfig_generation + ) { + // Use enable_siteconfig_generation to prevent infinite loop during object creation + self::$enable_siteconfig_generation = false; + $this->populateSiteConfigDefaults(); + self::$enable_siteconfig_generation = true; + } + } + + /** + * 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 Target locale to translate this object into + * @param boolean $saveTranslation Flag indicating whether the new record + * should be saved to the database. + * @return DataObject The translated object + */ + public function createTranslation($locale, $saveTranslation = true) + { + 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(array_diff_key($this->owner->toMap(), array('Version' => null))); + + // 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; + $newTranslation->Version = 0; + + $originalPage = $this->getTranslation(self::default_locale()); + if ($originalPage) { + $urlSegment = $originalPage->URLSegment; + } else { + $urlSegment = $newTranslation->URLSegment; + } + + // Only make segment unique if it should be enforced + if (Config::inst()->get('Translatable', 'enforce_global_unique_urls')) { + $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; + if ($saveTranslation) { + $newTranslation->write(); + } + + // run callback on page for translation related hooks + $newTranslation->invokeWithExtensions('onTranslatableCreate', $saveTranslation); + + return $newTranslation; + } + + /** + * Caution: Does not consider the {@link canEdit()} permissions. + * + * @param DataObject|int $member + * @param string $locale + * @return boolean + */ + public 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 + */ + public 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 + */ + public 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 + ); + } + + public 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 + */ + public function MetaTags(&$tags) + { + $template = '' . "\n"; + $translations = $this->owner->getTranslations(); + if ($translations) { + $translations = $translations->toArray(); + $translations[] = $this->owner; + + foreach ($translations as $translation) { + $tags .= sprintf($template, + Convert::raw2xml($translation->Title), + i18n::convert_rfc1766($translation->Locale), + $translation->AbsoluteLink() + ); + } + } + } + + public function providePermissions() + { + if (!SiteTree::has_extension('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', + 'Translate pages into a language' + ), + $localeName + ); + } + } + + $permissions['TRANSLATE_ALL'] = _t( + 'Translatable.TRANSLATEALLPERMISSION', + 'Translate into all available languages' + ); + + $permissions['VIEW_LANGS'] = _t( + 'Translatable.TRANSLATEVIEWLANGS', + 'View language dropdown' + ); + + 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 + */ + public static function get_existing_content_languages($className = 'SiteTree', $where = '') + { + $baseTable = ClassInfo::baseDataClass($className); + $query = new SQLSelect("Distinct \"Locale\"", "\"$baseTable\"", $where, '', "\"Locale\""); + $dbLangs = $query->execute()->column(); + $langlist = array_merge((array)Translatable::default_locale(), (array)$dbLangs); + $returnMap = array(); + $allCodes = array_merge( + Config::inst()->get('i18n', 'all_locales'), + Config::inst()->get('i18n', 'common_locales') + ); + foreach ($langlist as $langCode) { + if ($langCode && isset($allCodes[$langCode])) { + if (is_array($allCodes[$langCode])) { + $returnMap[$langCode] = $allCodes[$langCode]['name']; + } else { + $returnMap[$langCode] = $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::config()->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()} + */ + public 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') + */ + public 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 + */ + public static function get_allowed_locales() + { + return self::$allowed_locales; + } + + /** + * @deprecated 2.4 Use get_homepage_urlsegment_by_locale() + */ + public 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() + */ + public static function is_default_lang() + { + return (self::$default_locale == self::get_current_locale()); + } + + /** + * @deprecated 2.4 Use set_default_locale() + */ + public static function set_default_lang($lang) + { + self::set_default_locale(i18n::get_locale_from_lang($lang)); + } + + /** + * @deprecated 2.4 Use get_default_locale() + */ + public static function get_default_lang() + { + return i18n::get_lang_from_locale(self::default_locale()); + } + + /** + * @deprecated 2.4 Use get_current_locale() + */ + public static function current_lang() + { + return i18n::get_lang_from_locale(self::get_current_locale()); + } + + /** + * @deprecated 2.4 Use set_current_locale() + */ + public static function set_reading_lang($lang) + { + self::set_current_locale(i18n::get_locale_from_lang($lang)); + } + + /** + * @deprecated 2.4 Use get_reading_locale() + */ + public static function get_reading_lang() + { + return i18n::get_lang_from_locale(self::get_reading_locale()); + } + + /** + * @deprecated 2.4 Use default_locale() + */ + public static function default_lang() + { + return i18n::get_lang_from_locale(self::default_locale()); + } + + /** + * @deprecated 2.4 Use get_by_locale() + */ + public 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() + */ + public 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 + */ + public function isTranslation() + { + return ($this->owner->Locale && ($this->owner->Locale != Translatable::default_locale())); + } + + /** + * @deprecated 2.4 Use choose_site_locale() + */ + public static function choose_site_lang($langsAvail=null) + { + return self::choose_site_locale($langsAvail); + } + + /** + * @deprecated 2.4 Use getTranslatedLocales() + */ + public function getTranslatedLangs() + { + return $this->getTranslatedLocales(); + } + + /** + * Return a piece of text to keep DataObject cache keys appropriately specific + */ + public function cacheKeyComponent() + { + return 'locale-'.self::get_current_locale(); + } + + /** + * Extends the SiteTree::validURLSegment() method, to do checks appropriate + * to Translatable + * + * @return bool + */ + public function augmentValidURLSegment() + { + $reEnableFilter = false; + if (!Config::inst()->get('Translatable', 'enforce_global_unique_urls')) { + self::enable_locale_filter(); + } elseif (self::locale_filter_enabled()) { + self::disable_locale_filter(); + $reEnableFilter = true; + } + + $IDFilter = ($this->owner->ID) ? "AND \"SiteTree\".\"ID\" <> {$this->owner->ID}" : null; + $parentFilter = null; + + if (Config::inst()->get('SiteTree', 'nested_urls')) { + if ($this->owner->ParentID) { + $parentFilter = " AND \"SiteTree\".\"ParentID\" = {$this->owner->ParentID}"; + } else { + $parentFilter = ' AND "SiteTree"."ParentID" = 0'; + } + } + + $existingPage = SiteTree::get() + // disable get_one cache, as this otherwise may pick up results from when locale_filter was on + ->where("\"URLSegment\" = '{$this->owner->URLSegment}' $IDFilter $parentFilter")->First(); + if ($reEnableFilter) { + self::enable_locale_filter(); + } + + // By returning TRUE or FALSE, we overrule the base SiteTree->validateURLSegment() logic + return !$existingPage; + } } /** @@ -1873,86 +2045,90 @@ class Translatable extends DataExtension implements PermissionProvider { * @package translatable * @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; - } - - public function transformFormField(FormField $field) { - $newfield = $field->performReadOnlyTransformation(); - $fn = 'transform' . $field->class; - return $this->hasMethod($fn) ? $this->$fn($newfield, $field) : $this->baseTransform($newfield, $field); - } - - /** - * Transform a translatable CheckboxField to show the field value from the default language - * in the label. - * - * @param FormField $nonEditableField The readonly field to contain the original value - * @param FormField $originalField The original editable field containing the translated value - * @return CheckboxField The field with a modified label - */ - protected function transformCheckboxField(CheckboxField $nonEditableField, CheckboxField $originalField) { - $label = $originalField->Title(); - $fieldName = $originalField->getName(); - $value = ($this->original->$fieldName) - ? _t('Translatable_Transform.CheckboxValueYes', 'Yes') - : _t('Translatable_Transform.CheckboxValueNo', 'No'); - $originalLabel = _t( - 'Translatable_Transform.OriginalCheckboxLabel', - 'Original: {value}', - 'Addition to a checkbox field label showing the original value of the translatable field.', - array('value'=>$value) - ); - $originalField->setTitle($label . ' (' . $originalLabel . ')'); - return $originalField; - } - - /** - * Transform a translatable field to show the field value from the default language - * DataObject below the translated field. - * - * This is a fallback function which handles field types that aren't transformed by - * $this->transform{FieldType} functions. - * - * @param FormField $nonEditableField The readonly field to contain the original value - * @param FormField $originalField The original editable field containing the translated value - * @return \CompositeField The transformed field - */ - protected function baseTransform($nonEditableField, $originalField) { - $fieldname = $originalField->getName(); - - $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(_t( - 'Translatable_Transform.OriginalFieldLabel', - 'Original {title}', - 'Label for the original value of the translatable field.', - array('title'=>$originalField->Title()) - )); - - $nonEditableField_holder->insertBefore($originalField, $fieldname.'_original'); - return $nonEditableField_holder; - } - +class Translatable_Transformation extends FormTransformation +{ + /** + * @var DataObject + */ + private $original = null; + + public function __construct(DataObject $original) + { + $this->original = $original; + parent::__construct(); + } + + /** + * Returns the original DataObject attached to the Transformation + * + * @return DataObject + */ + public function getOriginal() + { + return $this->original; + } + + public function transformFormField(FormField $field) + { + $newfield = $field->performReadOnlyTransformation(); + $fn = 'transform' . $field->class; + return $this->hasMethod($fn) ? $this->$fn($newfield, $field) : $this->baseTransform($newfield, $field); + } + + /** + * Transform a translatable CheckboxField to show the field value from the default language + * in the label. + * + * @param FormField $nonEditableField The readonly field to contain the original value + * @param FormField $originalField The original editable field containing the translated value + * @return CheckboxField The field with a modified label + */ + protected function transformCheckboxField(CheckboxField $nonEditableField, CheckboxField $originalField) + { + $label = $originalField->Title(); + $fieldName = $originalField->getName(); + $value = ($this->original->$fieldName) + ? _t('Translatable_Transform.CheckboxValueYes', 'Yes') + : _t('Translatable_Transform.CheckboxValueNo', 'No'); + $originalLabel = _t( + 'Translatable_Transform.OriginalCheckboxLabel', + 'Original: {value}', + 'Addition to a checkbox field label showing the original value of the translatable field.', + array('value'=>$value) + ); + $originalField->setTitle($label . ' (' . $originalLabel . ')'); + return $originalField; + } + + /** + * Transform a translatable field to show the field value from the default language + * DataObject below the translated field. + * + * This is a fallback function which handles field types that aren't transformed by + * $this->transform{FieldType} functions. + * + * @param FormField $nonEditableField The readonly field to contain the original value + * @param FormField $originalField The original editable field containing the translated value + * @return \CompositeField The transformed field + */ + protected function baseTransform($nonEditableField, $originalField) + { + $fieldname = $originalField->getName(); + + $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(_t( + 'Translatable_Transform.OriginalFieldLabel', + 'Original {title}', + 'Label for the original value of the translatable field.', + array('title'=>$originalField->Title()) + )); + + $nonEditableField_holder->insertBefore($originalField, $fieldname.'_original'); + return $nonEditableField_holder; + } } diff --git a/code/tasks/MigrateTranslatableTask.php b/code/tasks/MigrateTranslatableTask.php index aa94243..790fb60 100644 --- a/code/tasks/MigrateTranslatableTask.php +++ b/code/tasks/MigrateTranslatableTask.php @@ -35,145 +35,153 @@ * * @package translatable */ -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)); +class MigrateTranslatableTask extends BuildTask +{ + protected $title = "Migrate Translatable Task"; + + protected $description = "Migrates site translations from SilverStripe 2.1/2.2 to new database structure."; + + public function init() + { + parent::init(); + + $canAccess = (Director::isDev() || Director::is_cli() || Permission::check("ADMIN")); + if (!$canAccess) { + return Security::permissionFailure($this); + } + } + + public 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; - } - } - - } + 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 - // 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); + $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); - - if($stage == 'Stage') { - $ids[$original->ID] = $newtrans->ID; - } - } - } - - echo "\n\n#################################\n"; - echo "Done!\n"; - } + $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/tests/unit/TranslatableSearchFormTest.php b/tests/unit/TranslatableSearchFormTest.php index afabdd4..6156a3e 100644 --- a/tests/unit/TranslatableSearchFormTest.php +++ b/tests/unit/TranslatableSearchFormTest.php @@ -2,100 +2,104 @@ /** * @package translatable */ -class TranslatableSearchFormTest extends FunctionalTest { - - protected static $fixture_file = 'translatable/tests/unit/TranslatableSearchFormTest.yml'; - - protected $mockController; +class TranslatableSearchFormTest extends FunctionalTest +{ + protected static $fixture_file = 'translatable/tests/unit/TranslatableSearchFormTest.yml'; + + protected $mockController; - protected $requiredExtensions = array( - 'SiteTree' => array( - 'Translatable', - "FulltextSearchable('Title,MenuTitle,Content,MetaDescription')", - ), - "File" => array( - "FulltextSearchable('Filename,Title,Content')", - ), - "ContentController" => array( - "ContentControllerSearchExtension", - ), - ); + protected $requiredExtensions = array( + 'SiteTree' => array( + 'Translatable', + "FulltextSearchable('Title,MenuTitle,Content,MetaDescription')", + ), + "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 - // MySQL will need a different table type - self::kill_temp_db(); - FulltextSearchable::enable(); - self::create_temp_db(); - $this->resetDBSchema(true); - 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(); + public function waitUntilIndexingFinished() + { + $db = DB::getConn(); + if (method_exists($db, 'waitUntilIndexingFinished')) { + DB::getConn()->waitUntilIndexingFinished(); + } + } + + public function setUpOnce() + { + // HACK Postgres doesn't refresh TSearch indexes when the schema changes after CREATE TABLE + // MySQL will need a different table type + self::kill_temp_db(); + FulltextSearchable::enable(); + self::create_temp_db(); + $this->resetDBSchema(true); + parent::setUpOnce(); + } + + public 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'); + $this->waitUntilIndexingFinished(); + } + + + + public 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(); + $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', 'searchlocale'=>$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', 'searchlocale'=>$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' - ); - } + // 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', 'searchlocale'=>$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', 'searchlocale'=>$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/TranslatableSiteConfigTest.php b/tests/unit/TranslatableSiteConfigTest.php index 3476613..6453cc7 100644 --- a/tests/unit/TranslatableSiteConfigTest.php +++ b/tests/unit/TranslatableSiteConfigTest.php @@ -2,63 +2,67 @@ /** * @package translatable */ -class TranslatableSiteConfigTest extends SapphireTest { - - protected static $fixture_file = 'translatable/tests/unit/TranslatableSiteConfigTest.yml'; - - protected $requiredExtensions = array( - 'SiteTree' => array('Translatable'), - 'SiteConfig' => array('Translatable'), - ); - - protected $illegalExtensions = array( - 'SiteTree' => array('SiteTreeSubsites') - ); - - private $origLocale; +class TranslatableSiteConfigTest extends SapphireTest +{ + protected static $fixture_file = 'translatable/tests/unit/TranslatableSiteConfigTest.yml'; + + protected $requiredExtensions = array( + 'SiteTree' => 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); + public function setUp() + { + parent::setUp(); + + $this->origLocale = Translatable::default_locale(); + Translatable::set_default_locale("en_US"); + } + + public function tearDown() + { + Translatable::set_default_locale($this->origLocale); + Translatable::set_current_locale($this->origLocale); - parent::tearDown(); - } - - function testCurrentCreatesDefaultForLocale() { - Translatable::set_current_locale(Translatable::default_locale()); - $configEn = SiteConfig::current_site_config(); - Translatable::set_current_locale('fr_FR'); - $configFr = SiteConfig::current_site_config(); - Translatable::set_current_locale(Translatable::default_locale()); - - $this->assertInstanceOf('SiteConfig', $configFr); - $this->assertEquals($configFr->Locale, 'fr_FR'); - $this->assertEquals($configFr->Title, $configEn->Title, 'Copies title from existing config'); - $this->assertEquals( - $configFr->getTranslationGroup(), - $configEn->getTranslationGroup(), - 'Created in the same translation group' - ); - } - - 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 + parent::tearDown(); + } + + public function testCurrentCreatesDefaultForLocale() + { + Translatable::set_current_locale(Translatable::default_locale()); + $configEn = SiteConfig::current_site_config(); + Translatable::set_current_locale('fr_FR'); + $configFr = SiteConfig::current_site_config(); + Translatable::set_current_locale(Translatable::default_locale()); + + $this->assertInstanceOf('SiteConfig', $configFr); + $this->assertEquals($configFr->Locale, 'fr_FR'); + $this->assertEquals($configFr->Title, $configEn->Title, 'Copies title from existing config'); + $this->assertEquals( + $configFr->getTranslationGroup(), + $configEn->getTranslationGroup(), + 'Created in the same translation group' + ); + } + + public 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)); + } +} diff --git a/tests/unit/TranslatableTest.php b/tests/unit/TranslatableTest.php index 1f653c1..c475916 100755 --- a/tests/unit/TranslatableTest.php +++ b/tests/unit/TranslatableTest.php @@ -4,1265 +4,1309 @@ * * @package translatable */ -class TranslatableTest extends FunctionalTest { - - protected static $fixture_file = 'translatable/tests/unit/TranslatableTest.yml'; +class TranslatableTest extends FunctionalTest +{ + protected static $fixture_file = 'translatable/tests/unit/TranslatableTest.yml'; - protected $extraDataObjects = array( - 'TranslatableTest_DataObject', - 'TranslatableTest_OneByLocaleDataObject', - 'TranslatableTest_Page', - ); - - protected $requiredExtensions = array( - 'SiteTree' => array('Translatable', 'Versioned', 'EveryoneCanPublish'), - 'SiteConfig' => array('Translatable'), - 'TranslatableTest_DataObject' => array('Translatable'), - 'TranslatableTest_OneByLocaleDataObject' => array('Translatable'), - ); - - private $origLocale; - - protected $autoFollowRedirection = false; + protected $extraDataObjects = array( + 'TranslatableTest_DataObject', + 'TranslatableTest_OneByLocaleDataObject', + 'TranslatableTest_Page', + ); + + protected $requiredExtensions = array( + 'SiteTree' => array('Translatable', 'Versioned', 'EveryoneCanPublish'), + 'SiteConfig' => array('Translatable'), + 'TranslatableTest_DataObject' => array('Translatable'), + 'TranslatableTest_OneByLocaleDataObject' => 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); + public 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"); + } + + public function tearDown() + { + Translatable::set_default_locale($this->origLocale); + Translatable::set_current_locale($this->origLocale); - parent::tearDown(); - } + parent::tearDown(); + } - function assertArrayEqualsAfterSort($expected, $actual, $message = null) { - sort($expected); - sort($actual); - return $this->assertEquals($expected, $actual, $message); - } + public function assertArrayEqualsAfterSort($expected, $actual, $message = null) + { + sort($expected); + sort($actual); + return $this->assertEquals($expected, $actual, $message); + } - function testGetOneByLocale() { - Translatable::disable_locale_filter(); - $this->assertEquals( - 0, - TranslatableTest_OneByLocaleDataObject::get()->count(), - 'should not be any test objects yet' - ); - Translatable::enable_locale_filter(); + public function testGetOneByLocale() + { + Translatable::disable_locale_filter(); + $this->assertEquals( + 0, + TranslatableTest_OneByLocaleDataObject::get()->count(), + 'should not be any test objects yet' + ); + Translatable::enable_locale_filter(); - $obj = new TranslatableTest_OneByLocaleDataObject(); - $obj->TranslatableProperty = 'test - en'; - $obj->write(); + $obj = new TranslatableTest_OneByLocaleDataObject(); + $obj->TranslatableProperty = 'test - en'; + $obj->write(); - Translatable::disable_locale_filter(); - $this->assertEquals( - 1, - TranslatableTest_OneByLocaleDataObject::get()->count(), - 'should not be any test objects yet' - ); - Translatable::enable_locale_filter(); + Translatable::disable_locale_filter(); + $this->assertEquals( + 1, + TranslatableTest_OneByLocaleDataObject::get()->count(), + 'should not be any test objects yet' + ); + Translatable::enable_locale_filter(); - $found = Translatable::get_one_by_locale('TranslatableTest_OneByLocaleDataObject', $obj->Locale); - $this->assertNotNull($found, 'should have found one for ' . $obj->Locale); - $this->assertEquals($obj->ID, $found->ID); + $found = Translatable::get_one_by_locale('TranslatableTest_OneByLocaleDataObject', $obj->Locale); + $this->assertNotNull($found, 'should have found one for ' . $obj->Locale); + $this->assertEquals($obj->ID, $found->ID); - $translated = $obj->createTranslation('de_DE'); - $translated->write(); + $translated = $obj->createTranslation('de_DE'); + $translated->write(); - Translatable::disable_locale_filter(); - $this->assertEquals( - 2, - TranslatableTest_OneByLocaleDataObject::get()->count(), - 'should not be any test objects yet' - ); - Translatable::enable_locale_filter(); + Translatable::disable_locale_filter(); + $this->assertEquals( + 2, + TranslatableTest_OneByLocaleDataObject::get()->count(), + 'should not be any test objects yet' + ); + Translatable::enable_locale_filter(); - $found = Translatable::get_one_by_locale( - 'TranslatableTest_OneByLocaleDataObject', - $translated->Locale - ); - $this->assertNotNull($found, 'should have found one for ' . $translated->Locale); - $this->assertEquals($translated->ID, $found->ID); + $found = Translatable::get_one_by_locale( + 'TranslatableTest_OneByLocaleDataObject', + $translated->Locale + ); + $this->assertNotNull($found, 'should have found one for ' . $translated->Locale); + $this->assertEquals($translated->ID, $found->ID); - // test again to make sure that get_one_by_locale works when locale filter disabled - Translatable::disable_locale_filter(); - $found = Translatable::get_one_by_locale( - 'TranslatableTest_OneByLocaleDataObject', - $translated->Locale - ); - $this->assertEquals($translated->ID, $found->ID); - Translatable::enable_locale_filter(); - } + // test again to make sure that get_one_by_locale works when locale filter disabled + Translatable::disable_locale_filter(); + $found = Translatable::get_one_by_locale( + 'TranslatableTest_OneByLocaleDataObject', + $translated->Locale + ); + $this->assertEquals($translated->ID, $found->ID); + Translatable::enable_locale_filter(); + } - function testLocaleFilteringEnabledAndDisabled() { - $this->assertTrue(Translatable::locale_filter_enabled()); + public function testLocaleFilteringEnabledAndDisabled() + { + $this->assertTrue(Translatable::locale_filter_enabled()); - // get our base page to use for testing - $origPage = $this->objFromFixture('Page', 'testpage_en'); - $origPage->MenuTitle = 'unique-key-used-in-my-query'; - $origPage->write(); - $origPage->publish('Stage', 'Live'); + // get our base page to use for testing + $origPage = $this->objFromFixture('Page', 'testpage_en'); + $origPage->MenuTitle = 'unique-key-used-in-my-query'; + $origPage->write(); + $origPage->publish('Stage', 'Live'); - // create a translation of it so that we can see if translations are filtered - $translatedPage = $origPage->createTranslation('de_DE'); - $translatedPage->MenuTitle = $origPage->MenuTitle; - $translatedPage->write(); - $translatedPage->publish('Stage', 'Live'); + // create a translation of it so that we can see if translations are filtered + $translatedPage = $origPage->createTranslation('de_DE'); + $translatedPage->MenuTitle = $origPage->MenuTitle; + $translatedPage->write(); + $translatedPage->publish('Stage', 'Live'); - $where = sprintf("\"MenuTitle\" = '%s'", Convert::raw2sql($origPage->MenuTitle)); + $where = sprintf("\"MenuTitle\" = '%s'", Convert::raw2sql($origPage->MenuTitle)); - // make sure that our query was filtered - $this->assertEquals(1, Page::get()->where($where)->count()); + // make sure that our query was filtered + $this->assertEquals(1, Page::get()->where($where)->count()); - // test no filtering with disabled locale filter - Translatable::disable_locale_filter(); - $this->assertEquals(2, Page::get()->where($where)->count()); - Translatable::enable_locale_filter(); + // test no filtering with disabled locale filter + Translatable::disable_locale_filter(); + $this->assertEquals(2, Page::get()->where($where)->count()); + Translatable::enable_locale_filter(); - // make sure that our query was filtered after re-enabling the filter - $this->assertEquals(1, Page::get()->where($where)->count()); + // make sure that our query was filtered after re-enabling the filter + $this->assertEquals(1, Page::get()->where($where)->count()); - // test effectiveness of disabling locale filter with 3.x delayed querying - // see https://github.com/silverstripe/silverstripe-translatable/issues/113 - Translatable::disable_locale_filter(); - // create the DataList while the locale filter is disabled - $dataList = Page::get()->where($where); - Translatable::enable_locale_filter(); - // but don't use it until later - after the filter is re-enabled - $this->assertEquals(2, $dataList->count()); - } - - 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'); - - // Need to log out, otherwise pages redirect to CMS views - $this->session()->inst_set('loggedInAs', null); - - $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 - - $this->assertArrayEqualsAfterSort( - array('en_US','es_ES'), - $frPage->getTranslations()->column('Locale') - ); - $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 - $this->assertArrayEqualsAfterSort( - array('es_ES', 'fr_FR'), - $enPage->getTranslations()->column('Locale') - ); - $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 - $this->assertArrayEqualsAfterSort( - array('en_US', 'fr_FR'), - $esPage->getTranslations()->column('Locale') - ); - $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 - ); - } + // test effectiveness of disabling locale filter with 3.x delayed querying + // see https://github.com/silverstripe/silverstripe-translatable/issues/113 + Translatable::disable_locale_filter(); + // create the DataList while the locale filter is disabled + $dataList = Page::get()->where($where); + Translatable::enable_locale_filter(); + // but don't use it until later - after the filter is re-enabled + $this->assertEquals(2, $dataList->count()); + } + + public 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'); + + // Need to log out, otherwise pages redirect to CMS views + $this->session()->inst_set('loggedInAs', null); + + $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' + ); + } + + public 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 - function assertClass($class, $node) { - $this->assertNotNull($node); - $this->assertEquals($class, $node->ClassName); - $this->assertEquals($class, get_class($node)); - } + $this->assertArrayEqualsAfterSort( + array('en_US', 'es_ES'), + $frPage->getTranslations()->column('Locale') + ); + $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 + $this->assertArrayEqualsAfterSort( + array('es_ES', 'fr_FR'), + $enPage->getTranslations()->column('Locale') + ); + $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 + $this->assertArrayEqualsAfterSort( + array('en_US', 'fr_FR'), + $esPage->getTranslations()->column('Locale') + ); + $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 testChangingClassOfDefaultLocaleTranslationChangesOthers() { - // see https://github.com/silverstripe/silverstripe-translatable/issues/97 - // create an English SiteTree - $enST = new SiteTree(); - $enST->Locale = 'en_US'; - $enST->write(); + public function assertClass($class, $node) + { + $this->assertNotNull($node); + $this->assertEquals($class, $node->ClassName); + $this->assertEquals($class, get_class($node)); + } - // create French and Spanish translations - $frST = $enST->createTranslation('fr_FR'); - $esST = $enST->createTranslation('es_ES'); + public function testChangingClassOfDefaultLocaleTranslationChangesOthers() + { + // see https://github.com/silverstripe/silverstripe-translatable/issues/97 + // create an English SiteTree + $enST = new SiteTree(); + $enST->Locale = 'en_US'; + $enST->write(); - // change the class name of the default locale's translation (as CMS admin would) - $enST->setClassName('Page'); - $enST->write(); + // create French and Spanish translations + $frST = $enST->createTranslation('fr_FR'); + $esST = $enST->createTranslation('es_ES'); - // reload them all to get fresh instances - $enPg = DataObject::get_by_id('Page', $enST->ID, $cache = false); - $frPg = DataObject::get_by_id('Page', $frST->ID, $cache = false); - $esPg = DataObject::get_by_id('Page', $esST->ID, $cache = false); + // change the class name of the default locale's translation (as CMS admin would) + $enST->setClassName('Page'); + $enST->write(); - // make sure they are all the right class - $this->assertClass('Page', $enPg); - $this->assertClass('Page', $frPg); - $this->assertClass('Page', $esPg); + // reload them all to get fresh instances + $enPg = DataObject::get_by_id('Page', $enST->ID, $cache = false); + $frPg = DataObject::get_by_id('Page', $frST->ID, $cache = false); + $esPg = DataObject::get_by_id('Page', $esST->ID, $cache = false); - // test that we get the right translations back from each instance - $this->assertArrayEqualsAfterSort( - array('fr_FR', 'es_ES'), - $enPg->getTranslations()->column('Locale') - ); - $this->assertArrayEqualsAfterSort( - array('en_US', 'es_ES'), - $frPg->getTranslations()->column('Locale') - ); - $this->assertArrayEqualsAfterSort( - array('en_US', 'fr_FR'), - $esPg->getTranslations()->column('Locale') - ); - } + // make sure they are all the right class + $this->assertClass('Page', $enPg); + $this->assertClass('Page', $frPg); + $this->assertClass('Page', $esPg); - function testChangingClassOfDefaultLocaleTranslationChangesOthersWhenPublished() { - // create an English SiteTree - $enST = new SiteTree(); - $enST->Locale = 'en_US'; - $enST->write(); - $enST->doPublish(); + // test that we get the right translations back from each instance + $this->assertArrayEqualsAfterSort( + array('fr_FR', 'es_ES'), + $enPg->getTranslations()->column('Locale') + ); + $this->assertArrayEqualsAfterSort( + array('en_US', 'es_ES'), + $frPg->getTranslations()->column('Locale') + ); + $this->assertArrayEqualsAfterSort( + array('en_US', 'fr_FR'), + $esPg->getTranslations()->column('Locale') + ); + } - // create and publish French and Spanish translations - $frST = $enST->createTranslation('fr_FR'); - $this->assertTrue($frST->doPublish(), 'should have been able to publish French translation'); - $esST = $enST->createTranslation('es_ES'); - $this->assertTrue($esST->doPublish(), 'should have been able to publish Spanish translation'); + public function testChangingClassOfDefaultLocaleTranslationChangesOthersWhenPublished() + { + // create an English SiteTree + $enST = new SiteTree(); + $enST->Locale = 'en_US'; + $enST->write(); + $enST->doPublish(); - // change the class name of the default locale's translation (as CMS admin would) - // and publish the change - we should see both versions of English change class - $enST->setClassName('Page'); - $enST->doPublish(); - $this->assertClass('Page', Versioned::get_one_by_stage('SiteTree', 'Stage', '"ID" = ' . $enST->ID)); - $this->assertClass('Page', Versioned::get_one_by_stage('SiteTree', 'Live', '"ID" = ' . $enST->ID)); + // create and publish French and Spanish translations + $frST = $enST->createTranslation('fr_FR'); + $this->assertTrue($frST->doPublish(), 'should have been able to publish French translation'); + $esST = $enST->createTranslation('es_ES'); + $this->assertTrue($esST->doPublish(), 'should have been able to publish Spanish translation'); - // and all of the draft versions of translations: - $this->assertClass('Page', Versioned::get_one_by_stage('SiteTree', 'Stage', '"ID" = ' . $frST->ID)); - $this->assertClass('Page', Versioned::get_one_by_stage('SiteTree', 'Stage', '"ID" = ' . $esST->ID)); + // change the class name of the default locale's translation (as CMS admin would) + // and publish the change - we should see both versions of English change class + $enST->setClassName('Page'); + $enST->doPublish(); + $this->assertClass('Page', Versioned::get_one_by_stage('SiteTree', 'Stage', '"ID" = ' . $enST->ID)); + $this->assertClass('Page', Versioned::get_one_by_stage('SiteTree', 'Live', '"ID" = ' . $enST->ID)); - // and all of the live versions of translations as well: - $this->assertClass('Page', Versioned::get_one_by_stage('SiteTree', 'Live', '"ID" = ' . $frST->ID)); - $this->assertClass('Page', Versioned::get_one_by_stage('SiteTree', 'Live', '"ID" = ' . $esST->ID)); + // and all of the draft versions of translations: + $this->assertClass('Page', Versioned::get_one_by_stage('SiteTree', 'Stage', '"ID" = ' . $frST->ID)); + $this->assertClass('Page', Versioned::get_one_by_stage('SiteTree', 'Stage', '"ID" = ' . $esST->ID)); - } - - function testTranslationGroupsWhenTranslationIsSubclass() { - // create an English SiteTree - $enST = new SiteTree(); - $enST->Locale = 'en_US'; - $enST->write(); + // and all of the live versions of translations as well: + $this->assertClass('Page', Versioned::get_one_by_stage('SiteTree', 'Live', '"ID" = ' . $frST->ID)); + $this->assertClass('Page', Versioned::get_one_by_stage('SiteTree', 'Live', '"ID" = ' . $esST->ID)); + } + + public function testTranslationGroupsWhenTranslationIsSubclass() + { + // create an English SiteTree + $enST = new SiteTree(); + $enST->Locale = 'en_US'; + $enST->write(); - // create French and Spanish translations - $frST = $enST->createTranslation('fr_FR'); - $esST = $enST->createTranslation('es_ES'); + // create French and Spanish translations + $frST = $enST->createTranslation('fr_FR'); + $esST = $enST->createTranslation('es_ES'); - // test that we get the right translations back from each instance - $this->assertArrayEqualsAfterSort( - array('fr_FR', 'es_ES'), - $enST->getTranslations()->column('Locale') - ); - $this->assertArrayEqualsAfterSort( - array('en_US', 'es_ES'), - $frST->getTranslations()->column('Locale') - ); - $this->assertArrayEqualsAfterSort( - array('en_US', 'fr_FR'), - $esST->getTranslations()->column('Locale') - ); + // test that we get the right translations back from each instance + $this->assertArrayEqualsAfterSort( + array('fr_FR', 'es_ES'), + $enST->getTranslations()->column('Locale') + ); + $this->assertArrayEqualsAfterSort( + array('en_US', 'es_ES'), + $frST->getTranslations()->column('Locale') + ); + $this->assertArrayEqualsAfterSort( + array('en_US', 'fr_FR'), + $esST->getTranslations()->column('Locale') + ); - // this should be considered an edge-case, but on some sites translations - // may be allowed to be a subclass of the default locale's translation of - // the same page. In this case, we need to support getTranslations returning - // all of the translations, even if one of the translations is a different - // class from others - $esST->setClassName('Page'); - $esST->write(); - $esPg = DataObject::get_by_id('Page', $esST->ID, $cache = false); + // this should be considered an edge-case, but on some sites translations + // may be allowed to be a subclass of the default locale's translation of + // the same page. In this case, we need to support getTranslations returning + // all of the translations, even if one of the translations is a different + // class from others + $esST->setClassName('Page'); + $esST->write(); + $esPg = DataObject::get_by_id('Page', $esST->ID, $cache = false); - // make sure we successfully changed the class - $this->assertClass('Page', $esPg); + // make sure we successfully changed the class + $this->assertClass('Page', $esPg); - // and make sure that the class of the others did not change - $frST = DataObject::get_by_id('SiteTree', $frST->ID, $cache = false); - $this->assertClass('SiteTree', $frST); - $enST = DataObject::get_by_id('SiteTree', $enST->ID, $cache = false); - $this->assertClass('SiteTree', $enST); + // and make sure that the class of the others did not change + $frST = DataObject::get_by_id('SiteTree', $frST->ID, $cache = false); + $this->assertClass('SiteTree', $frST); + $enST = DataObject::get_by_id('SiteTree', $enST->ID, $cache = false); + $this->assertClass('SiteTree', $enST); - // now that we know our edge case is successfully configured, we need to - // make sure that we get the right translations back from everything - $this->assertArrayEqualsAfterSort( - array('fr_FR', 'es_ES'), - $enST->getTranslations()->column('Locale') - ); - $this->assertArrayEqualsAfterSort( - array('en_US', 'es_ES'), - $frST->getTranslations()->column('Locale') - ); - $this->assertArrayEqualsAfterSort( - array('en_US', 'fr_FR'), - $esPg->getTranslations()->column('Locale') - ); - $this->assertEquals($enST->ID, $esPg->getTranslation('en_US')->ID); - $this->assertEquals($frST->ID, $esPg->getTranslation('fr_FR')->ID); - $this->assertEquals($esPg->ID, $enST->getTranslation('es_ES')->ID); - $this->assertEquals($esPg->ID, $frST->getTranslation('es_ES')->ID); - } + // now that we know our edge case is successfully configured, we need to + // make sure that we get the right translations back from everything + $this->assertArrayEqualsAfterSort( + array('fr_FR', 'es_ES'), + $enST->getTranslations()->column('Locale') + ); + $this->assertArrayEqualsAfterSort( + array('en_US', 'es_ES'), + $frST->getTranslations()->column('Locale') + ); + $this->assertArrayEqualsAfterSort( + array('en_US', 'fr_FR'), + $esPg->getTranslations()->column('Locale') + ); + $this->assertEquals($enST->ID, $esPg->getTranslation('en_US')->ID); + $this->assertEquals($frST->ID, $esPg->getTranslation('fr_FR')->ID); + $this->assertEquals($esPg->ID, $enST->getTranslation('es_ES')->ID); + $this->assertEquals($esPg->ID, $frST->getTranslation('es_ES')->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'; // de_DE clashes with en_US - $translatedPage->write(); - $this->assertNotEquals($origPage->URLSegment, $translatedPage->URLSegment); + public 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); + } + + public 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); + } + + public 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()' + ); + } + + public function testTranslationCantHaveSameURLSegmentAcrossLanguages() + { + $origPage = $this->objFromFixture('Page', 'testpage_en'); + $translatedPage = $origPage->createTranslation('de_DE'); + $this->assertEquals($translatedPage->URLSegment, 'testpage-de-de'); + + $translatedPage->URLSegment = 'testpage'; // de_DE clashes with en_US + $translatedPage->write(); + $this->assertNotEquals($origPage->URLSegment, $translatedPage->URLSegment); - Translatable::set_current_locale('de_DE'); - Config::inst()->update('Translatable', 'enforce_global_unique_urls', false); - $translatedPage->URLSegment = 'testpage'; // de_DE clashes with en_US - $translatedPage->write(); - $this->assertEquals('testpage', $translatedPage->URLSegment); - Config::inst()->update('Translatable', 'enforce_global_unique_urls', true); - Translatable::set_current_locale('en_US'); - } - - function testUpdateCMSFieldsOnSiteTree() { - $pageOrigLang = new TranslatableTest_Page(); - $pageOrigLang->write(); - - // first test with default language - $fields = $pageOrigLang->getCMSFields(); - // title - $this->assertInstanceOf( - '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")' - ); - // custom property - $this->assertInstanceOf( - 'TextField', - $fields->dataFieldByName('TranslatableProperty'), - 'Has custom field' - ); - // custom has_one - $this->assertInstanceOf( - 'DropdownField', - $fields->dataFieldByName('TranslatableObjectID'), - 'Has custom dropdown field' - ); - - // then in "translation mode" - $pageTranslated = $pageOrigLang->createTranslation('fr_FR'); - $fields = $pageTranslated->getCMSFields(); - // title - $this->assertInstanceOf( - 'TextField', - $fields->dataFieldByName('Title'), - 'Translatable leaves original formfield intact in "translation mode"' - ); - $readonlyField = $fields->dataFieldByName('Title')->performReadonlyTransformation(); - $this->assertInstanceOf( - $readonlyField->class, - $fields->dataFieldByName('Title_original'), - 'Translatable adds the original value as a ReadonlyField in "translation mode"' - ); - // custom property - $this->assertInstanceOf( - 'ReadonlyField', - $fields->dataFieldByName('TranslatableProperty_original'), - 'Adds original value for custom field as ReadonlyField' - ); - $this->assertInstanceOf( - 'TextField', - $fields->dataFieldByName('TranslatableProperty'), - 'Retains custom field as TextField' - ); - // custom has_one - $this->assertInstanceOf( - 'LookupField', - $fields->dataFieldByName('TranslatableObjectID_original'), - 'Adds original value for custom dropdown field as LookupField (= readonly version of DropdownField)' - ); - $this->assertInstanceOf( - 'DropdownField', - $fields->dataFieldByName('TranslatableObjectID'), - 'Retains custom dropdown field as DropdownField' - ); - - } - - 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'); - foreach($resultPagesDefaultLangIDs as $key => $val) - $resultPagesDefaultLangIDs[$key] = intval($val); - $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'); - foreach($resultPagesCustomLangIDs as $key => $val) - $resultPagesCustomLangIDs[$key] = intval($val); - $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'); - $this->assertArrayEqualsAfterSort( - array( - $child1Page->ID, - $child2Page->ID, - $child3Page->ID - ), - $parentPage->Children()->column('ID'), - "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()); - $this->assertArrayEqualsAfterSort( - array( - $child1Page->ID, - $child2Page->ID, - $child3Page->ID - ), - $parentPage->stageChildren()->column('ID'), - "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(); - $this->assertArrayEqualsAfterSort( - array( - $child2PageID, - $child3PageID, - $child1PageID // $child1Page was deleted from stage, so the original record doesn't have the ID set - ), - $parentPage->AllChildrenIncludingDeleted()->column('ID'), - "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, - // $child1PageTranslated was deleted from stage, so the original record doesn't have the ID set - $child1PageTranslatedID - ), - "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 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->assertInstanceOf( - '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 CMSPageEditController(); - - $origLocale = Translatable::get_current_locale(); - Translatable::set_current_locale('fr_FR'); - - $form = $cmsMain->getEditForm($frPage->ID); - $form->loadDataFrom(array( - 'Title' => 'Translated', // $db field - )); - $form->saveInto($frPage); - $frPage->write(); - - $this->assertEquals('Translated', $frPage->Title); - - $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(); - - 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' - ); - } + Translatable::set_current_locale('de_DE'); + Config::inst()->update('Translatable', 'enforce_global_unique_urls', false); + $translatedPage->URLSegment = 'testpage'; // de_DE clashes with en_US + $translatedPage->write(); + $this->assertEquals('testpage', $translatedPage->URLSegment); + Config::inst()->update('Translatable', 'enforce_global_unique_urls', true); + Translatable::set_current_locale('en_US'); + } + + public function testUpdateCMSFieldsOnSiteTree() + { + $pageOrigLang = new TranslatableTest_Page(); + $pageOrigLang->write(); + + // first test with default language + $fields = $pageOrigLang->getCMSFields(); + // title + $this->assertInstanceOf( + '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")' + ); + // custom property + $this->assertInstanceOf( + 'TextField', + $fields->dataFieldByName('TranslatableProperty'), + 'Has custom field' + ); + // custom has_one + $this->assertInstanceOf( + 'DropdownField', + $fields->dataFieldByName('TranslatableObjectID'), + 'Has custom dropdown field' + ); + + // then in "translation mode" + $pageTranslated = $pageOrigLang->createTranslation('fr_FR'); + $fields = $pageTranslated->getCMSFields(); + // title + $this->assertInstanceOf( + 'TextField', + $fields->dataFieldByName('Title'), + 'Translatable leaves original formfield intact in "translation mode"' + ); + $readonlyField = $fields->dataFieldByName('Title')->performReadonlyTransformation(); + $this->assertInstanceOf( + $readonlyField->class, + $fields->dataFieldByName('Title_original'), + 'Translatable adds the original value as a ReadonlyField in "translation mode"' + ); + // custom property + $this->assertInstanceOf( + 'ReadonlyField', + $fields->dataFieldByName('TranslatableProperty_original'), + 'Adds original value for custom field as ReadonlyField' + ); + $this->assertInstanceOf( + 'TextField', + $fields->dataFieldByName('TranslatableProperty'), + 'Retains custom field as TextField' + ); + // custom has_one + $this->assertInstanceOf( + 'LookupField', + $fields->dataFieldByName('TranslatableObjectID_original'), + 'Adds original value for custom dropdown field as LookupField (= readonly version of DropdownField)' + ); + $this->assertInstanceOf( + 'DropdownField', + $fields->dataFieldByName('TranslatableObjectID'), + 'Retains custom dropdown field as DropdownField' + ); + } + + public 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'); + foreach ($resultPagesDefaultLangIDs as $key => $val) { + $resultPagesDefaultLangIDs[$key] = intval($val); + } + $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'); + foreach ($resultPagesCustomLangIDs as $key => $val) { + $resultPagesCustomLangIDs[$key] = intval($val); + } + $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'); + } + + public 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' + ); + } + + public 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'); + } + + public 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' + ); + } + + public 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' + ); + } + + public 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)); + } + + public 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'); + $this->assertArrayEqualsAfterSort( + array( + $child1Page->ID, + $child2Page->ID, + $child3Page->ID + ), + $parentPage->Children()->column('ID'), + "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'); + } + + public 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()); + $this->assertArrayEqualsAfterSort( + array( + $child1Page->ID, + $child2Page->ID, + $child3Page->ID + ), + $parentPage->stageChildren()->column('ID'), + "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'); + } + + public 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' + ); + } + + public 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' + ); + } + + public 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' + ); + } + + public 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'); + } + + public 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')); + } + + public 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(); + $this->assertArrayEqualsAfterSort( + array( + $child2PageID, + $child3PageID, + $child1PageID // $child1Page was deleted from stage, so the original record doesn't have the ID set + ), + $parentPage->AllChildrenIncludingDeleted()->column('ID'), + "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, + // $child1PageTranslated was deleted from stage, so the original record doesn't have the ID set + $child1PageTranslatedID + ), + "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'); + } + + public 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'); + } + + public 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' + ); + } + + public 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'); + } + + public 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->assertInstanceOf( + 'Page', + $testPage->createTranslation('ja_JP') + ); + try { + $testPage->createTranslation('de_DE'); + $this->setExpectedException("Exception"); + } catch (Exception $e) { + } + + Translatable::set_allowed_locales($origAllowedLocales); + } + + public 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); + } + + public 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); + } + + public 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 CMSPageEditController(); + + $origLocale = Translatable::get_current_locale(); + Translatable::set_current_locale('fr_FR'); + + $form = $cmsMain->getEditForm($frPage->ID); + $form->loadDataFrom(array( + 'Title' => 'Translated', // $db field + )); + $form->saveInto($frPage); + $frPage->write(); + + $this->assertEquals('Translated', $frPage->Title); + + $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(); + + 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_OneByLocaleDataObject extends DataObject implements TestOnly { - private static $db = array( - 'TranslatableProperty' => 'Text' - ); +class TranslatableTest_OneByLocaleDataObject extends DataObject implements TestOnly +{ + private static $db = array( + 'TranslatableProperty' => 'Text' + ); } -class TranslatableTest_DataObject extends DataObject implements TestOnly { - // add_extension() used to add decorator at end of file - - private static $db = array( - 'TranslatableProperty' => 'Text' - ); +class TranslatableTest_DataObject extends DataObject implements TestOnly +{ + // add_extension() used to add decorator at end of file + + private static $db = array( + 'TranslatableProperty' => 'Text' + ); } -class TranslatableTest_Extension extends DataExtension implements TestOnly { - - private static $db = array( - 'TranslatableDecoratedProperty' => 'Text' - ); - +class TranslatableTest_Extension extends DataExtension implements TestOnly +{ + private static $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 - - private static $db = array( - 'TranslatableProperty' => 'Text' - ); +class TranslatableTest_Page extends Page implements TestOnly +{ + // static $extensions is inherited from SiteTree, + // we don't need to explicitly specify the fields - private static $has_one = array( - 'TranslatableObject' => 'TranslatableTest_DataObject' - ); - - function getCMSFields() { - $fields = parent::getCMSFields(); - $fields->addFieldToTab( - 'Root.Main', - new TextField('TranslatableProperty') - ); - $fields->addFieldToTab( - 'Root.Main', - new DropdownField('TranslatableObjectID') - ); - - $this->applyTranslatableFieldsUpdate($fields, 'updateCMSFields'); + private static $db = array( + 'TranslatableProperty' => 'Text' + ); - return $fields; - } + private static $has_one = array( + 'TranslatableObject' => 'TranslatableTest_DataObject' + ); + + public function getCMSFields() + { + $fields = parent::getCMSFields(); + $fields->addFieldToTab( + 'Root.Main', + new TextField('TranslatableProperty') + ); + $fields->addFieldToTab( + 'Root.Main', + new DropdownField('TranslatableObjectID') + ); + + $this->applyTranslatableFieldsUpdate($fields, 'updateCMSFields'); + + return $fields; + } } -class EveryoneCanPublish extends DataExtension { - - function canPublish($member = null) { - return true; - } +class EveryoneCanPublish extends DataExtension +{ + public function canPublish($member = null) + { + return true; + } } TranslatableTest_DataObject::add_extension('TranslatableTest_Extension');