From d3d6ae833d0c298f5a0b0ce6d7991cff08f48c0c Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Sat, 10 Jan 2009 12:15:30 +0000 Subject: [PATCH] Merged, debugged and enhanced Translatable patches from branches/translatable at r64523, r64523, 64523, thanks wakeless! API CHANGE Changed Translatable schema from auxilliary tables (SiteTree_lang, SiteTree_lang_Live) to automatically filtered records on the original table (SiteTree, SiteTree_Live), using $Lang and $OriginalID properties. Incompatible update to old schema, migration script is in the works. API CHANGE Removed Translatable::get_one(), Translatable::write() ENHANCEMENT Simplified Translatable tree generation by using getSiteTreeFor() in CMSMain->createtranslation() ENHANCEMENT Added AllChildrenIncludingDeleted(), augmentNumChildrenCountQuery(), augmentAllChildrenIncludingDeleted(), augmentStageChildren() to Translatable class to allow for more stable tree generation. ENHANCEMENT Moved definition of Translatable schema from augmentDatabase() to Translatable->extraStatics() ENHANCEMENT Changes to the CMS language selection refresh the whole admin interface instead of the tree only. This way we can add a URL parameter ?lang= to /admin, which makes the specific language bookmarkable and reloadable. Changes to LangSelector.js ENHANCEMENT Added fallback to ModelAsController->getNestedController() to fetch page with matching URLSegment but different language in case no page is found in the current language. ENHANCEMENT Added helper methods to Translatable: getTranslation(), hasTranslation(), isTranslation(), findOriginalIDs() ENHANCEMENT Getters and setters for Translatable->getOriginalPage() etc. ENHANCEMENT Hooking Translatable into ModelAsController and ContentController initialization in order to call choose_site_lang() ENHANCEMENT Simplified Translatable->augmentSQL(), augmentWrite() by not using auxilliary tables ENHANCEMENT Showing clickable links for Translations in Translatable->updateCMSFields() BUGFIX Modifying Hierarchy/SiteTree Children getters to accept optional "context" which can be used to set a language explicitly through the $Lang property, rather than implicitly reyling on the static Translatable::current_lang() BUGFIX Fixed TranslatableTest to work with new datamodel BUGFIX Temporarily disabled cookie/session selection in Translatable::choose_site_lang() until we have a good test suite for the side effects. MINOR Added "untranslated" CSS styles to tree nodes and marking them as inactive/grey git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@69959 467b73ca-7a2a-4603-9d3b-597d59a354a9 --- core/control/ModelAsController.php | 6 + core/i18n.php | 1 + core/model/SiteTree.php | 6 +- core/model/Translatable.php | 764 +++++++++++++++-------------- 4 files changed, 418 insertions(+), 359 deletions(-) diff --git a/core/control/ModelAsController.php b/core/control/ModelAsController.php index 11cd4b7dc..7f5455193 100644 --- a/core/control/ModelAsController.php +++ b/core/control/ModelAsController.php @@ -34,6 +34,12 @@ class ModelAsController extends Controller implements NestedController { $SQL_URLSegment = Convert::raw2sql($this->urlParams['URLSegment']); $child = SiteTree::get_by_url($SQL_URLSegment); + // fallback to default language + // @todo Migrate into extension point and module + if(!$child && Translatable::is_enabled()) { + $child = Translatable::get_one_by_lang('SiteTree', Translatable::default_lang(), "URLSegment = '{$SQL_URLSegment}'"); + } + if(!$child) { if($child = $this->findOldPage($SQL_URLSegment)) { $url = Controller::join_links( diff --git a/core/i18n.php b/core/i18n.php index 8f37ab0b6..58a35e188 100755 --- a/core/i18n.php +++ b/core/i18n.php @@ -991,6 +991,7 @@ class i18n extends Object { * @param string $locale Locale to be set */ static function set_locale($locale) { + if(strlen($locale) == 2) Debug::backtrace(); if ($locale) self::$current_locale = $locale; } diff --git a/core/model/SiteTree.php b/core/model/SiteTree.php index 63ab4466f..64e1b4b09 100644 --- a/core/model/SiteTree.php +++ b/core/model/SiteTree.php @@ -169,8 +169,8 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid static $extensions = array( "Hierarchy", + "Versioned('Stage', 'Live')", "Translatable('Title', 'MenuTitle', 'Content', 'URLSegment', 'MetaTitle', 'MetaDescription', 'MetaKeywords', 'Status')", - "Versioned('Stage', 'Live')" ); /** @@ -1694,6 +1694,10 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid if(!$this->ShowInMenus) $classes .= " notinmenu"; + + //TODO: Add integration + if(Translatable::is_enabled() && $controller->Lang != Translatable::default_lang() && !$this->isTranslation()) + $classes .= " untranslated "; $classes .= $this->markingClasses(); diff --git a/core/model/Translatable.php b/core/model/Translatable.php index 1160b4da3..439e3e072 100755 --- a/core/model/Translatable.php +++ b/core/model/Translatable.php @@ -89,6 +89,15 @@ class Translatable extends DataObjectDecorator { */ protected $original_values = null; + /** + * Overloaded getter for $Lang property. + * Not all pages in the database have their language property explicitly set, + * so we fall back to {@link Translatable::default_lang()}. + */ + function getLang() { + $record = $this->owner->toMap(); + return (isset($record["Lang"])) ? $record["Lang"] : Translatable::default_lang(); + } /** * Checks if a table given table exists in the db @@ -113,12 +122,20 @@ class Translatable extends DataObjectDecorator { * @param $langsAvailable array A numerical array of languages which are valid choices (optional) * @return string Selected language (also saved in $reading_lang). */ - static function choose_site_lang($langsAvailable = null) { + static function choose_site_lang($langsAvailable = array()) { $siteMode = Director::get_site_mode(); // either 'cms' or 'site' + if(self::$reading_lang) { + self::$language_decided = true; + return self::$reading_lang; + } - if(isset($_GET['lang']) && (!isset($langsAvailable) || in_array($_GET['lang'], $langsAvailable))) { + if( + (isset($_GET['lang']) && !$langsAvailable) + || (isset($_GET['lang']) && in_array($_GET['lang'], $langsAvailable)) + ) { // get from GET parameter self::set_reading_lang($_GET['lang']); + /* } elseif(isset($_COOKIE['lang.' . $siteMode]) && $siteMode && (!isset($langsAvailable) || in_array($_COOKIE['lang.' . $siteMode], $langsAvailable))) { // get from namespaced cookie self::set_reading_lang($_COOKIE[$siteMode . '.lang']); @@ -132,10 +149,11 @@ class Translatable extends DataObjectDecorator { // get from global session self::set_reading_lang(Session::get('lang.global')); } else { - // get default lang stored in class + get default lang stored in class self::set_reading_lang(self::default_lang()); + */ } - + self::$language_decided = true; return self::$reading_lang; } @@ -180,8 +198,7 @@ class Translatable extends DataObjectDecorator { * @param string $lang New reading language. */ static function set_reading_lang($lang) { - $key = (Director::get_site_mode()) ? 'lang.' . Director::get_site_mode() : 'lang.global'; - Session::set($key, $lang); + //Session::set('currentLang',$lang); self::$reading_lang = $lang; } @@ -195,44 +212,11 @@ class Translatable extends DataObjectDecorator { * @return DataObject */ static function get_one_by_lang($class, $lang, $filter = '', $cache = false, $orderby = "") { - $oldLang = self::current_lang(); - self::set_reading_lang($lang); - $result = DataObject::get_one($class, $filter, $cache, $orderby); - self::set_reading_lang($oldLang); - return $result; - } - - /** - * Get a singleton instance of a class in the most convenient language (@see choose_site_lang()) - * - * @param string $callerClass The name of the class - * @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($callerClass, $filter = "", $cache = false, $orderby = "") { - self::$language_decided = true; - self::$reading_lang = self::default_lang(); - $record = DataObject::get_one($callerClass, $filter); - if (!$record) { - self::$bypass = true; - $record = DataObject::get_one($callerClass, $filter, $cache, $orderby); - self::$bypass = false; - if ($record) self::set_reading_lang($record->Lang); - } else { - $langsAvailable = (array)self::get_langs_by_id($callerClass, $record->ID); - $langsAvailable[] = self::default_lang(); - $lang = self::choose_site_lang($langsAvailable); - if (isset($lang)) { - $transrecord = self::get_one_by_lang($callerClass, $lang, "\"$callerClass\".\"ID\" = $record->ID"); - if ($transrecord) { - self::set_reading_lang($lang); - $record = $transrecord; - } - } - } - return $record; + $orig = Translatable::current_lang(); + Translatable::set_reading_lang($lang); + $do = DataObject::get_one($class, $filter, $cache, $orderby); + Translatable::set_reading_lang($orig); + return $do; } /** @@ -265,7 +249,21 @@ class Translatable extends DataObjectDecorator { static function get_original($class, $originalLangID) { $baseClass = $class; while( ($p = get_parent_class($baseClass)) != "DataObject") $baseClass = $p; - return self::get_one_by_lang($class,self::default_lang(),"\"$baseClass\".ID = $originalLangID"); + return self::get_one_by_lang($class,self::default_lang(),"\"$baseClass\".\"ID\" = $originalLangID"); + } + + function getTranslatedLangs() { + $class = ClassInfo::baseDataClass($this->owner->class); //Base Class + if($this->owner->hasExtension("Versioned") && Versioned::current_stage() == "Live") { + $class = $class."_Live"; + } + + $id = $this->owner->ID; + if(is_numeric($id)) { + $query = new SQLQuery('distinct Lang',"$class","(\"$class\".\"OriginalID\" =$id)"); + $langs = $query->execute()->column(); + } + return ($langs) ? array_values($langs) : array(); } /** @@ -276,23 +274,8 @@ class Translatable extends DataObjectDecorator { * @return array List of languages */ static function get_langs_by_id($class, $id) { - $query = new SQLQuery('Lang',"{$class}_lang","(\"{$class}_lang\".OriginalLangID =$id)"); - $langs = $query->execute()->column(); - return ($langs) ? array_values($langs) : false; - } - - /** - * Writes an object in a certain language. Use this instead of $object->write() if you want to write - * an instance in a determinated language independently of the currently set working language - * - * @param DataObject $object Object to be written - * @param string $lang The name of the language - */ - static function write(DataObject $object, $lang) { - $oldLang = self::current_lang(); - self::set_reading_lang($lang); - $result = $object->write(); - self::set_reading_lang($oldLang); + $do = DataObject::get_by_id($class, $id); + return ($do ? $do->getTranslatedLangs() : array()); } /** @@ -340,99 +323,92 @@ class Translatable extends DataObjectDecorator { function __construct($translatableFields) { 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. + $this->createMethod("AllChildrenIncludingDeleted", + " + \$context = (isset(\$args[0])) ? \$args[0] : null; + if(\$context && \$obj->getLang() == \$context->Lang && \$obj->isTranslation()) { + // if the language matches the context (e.g. CMSMain), and object is translated, + // then call method on original language instead + return \$obj->getOwner()->getOriginalPage()->doAllChildrenIncludingDeleted(\$context); + } else if(\$obj->getOwner()->hasExtension('Hierarchy') ) { + return \$obj->getOwner()->extInstance('Hierarchy')->doAllChildrenIncludingDeleted(\$context); + } else { + return null; + }" + ); + } + + function setOwner(Object $owner) { + parent::setOwner($owner); + + // setting translatable fields by inspecting owner - this should really be done in the constructor + $this->translatableFields = array_keys($this->owner->inheritedDatabaseFields()); + } + + function extraDBFields() { + if(!Translatable::is_enabled()) return; + + if(get_class($this->owner) == ClassInfo::baseDataClass(get_class($this->owner))) { + return array( + "db" => array( + "Lang" => "Varchar(12)", + "OriginalID" => "Int" + ), + "defaults" => array( + "Lang" => Translatable::default_lang() + ) + ); + } else { + return array(); + } + } + + function findOriginalIDs() { + if(!$this->isTranslation()) { + $query = new SQLQuery("ID", + ClassInfo::baseDataClass($this->owner->class), + array("OriginalID = ".$this->owner->ID) + ); + $ret = $query->execute()->column(); + + } else { + return array(); + } } function augmentSQL(SQLQuery &$query) { - if (! $this->stat('enabled')) return false; - if((($lang = self::current_lang()) && !self::is_default_lang()) || self::$bypass) { - foreach($query->from as $table => $dummy) { - if(!isset($baseTable)) { - $baseTable = $table; - } - - if (self::table_exists("{$table}_lang")) { - $query->renameTable($table, $table . '_lang'); - if (stripos($query->sql(),'.ID')) { - // Every reference to ID is now OriginalLangID - $query->replaceText(".ID",".OriginalLangID"); - $query->where = str_replace("\"ID\"", "\"OriginalLangID\"",$query->where); - $query->select[] = "\"{$baseTable}_lang\".OriginalLangID AS ID"; - } - if ($query->where) foreach ($query->where as $i => $wherecl) { - if (substr($wherecl,0,4) == 'ID =') - // Another reference to ID to be changed - $query->where[$i] = str_replace('ID =','OriginalLangID =',$wherecl); - else { - $parts = explode(' AND ',$wherecl); - foreach ($parts as $j => $part) { - // Divide this clause between the left ($innerparts[1]) and right($innerparts[2]) part of the condition - ereg('(\"?[[:alnum:]_-]*\"?\.?\"?[[:alnum:]_-]*\"?)(.*)', $part, $innerparts); - if (strpos($innerparts[1],'.') === false) - //it may be ambiguous, so sometimes we will need to add the table - $parts[$j] = ($this->isInAugmentedTable($innerparts[1], $table) ? "\"{$table}_lang\"." : "")."$part"; - else { - /* if the table has been specified we have to determine if the original (without _lang) name has to be used - * because we don't have the queried field in the augmented table (which usually means - * that is not a translatable field) - */ - $clauseparts = explode('.',$innerparts[1]); - $originalTable = str_replace('"','',str_replace('_lang','',$clauseparts[0])); - $parts[$j] = ($this->isInAugmentedTable($clauseparts[1], $originalTable) ? "\"{$originalTable}_lang\"" : "\"$originalTable\"") - . ".{$clauseparts[1]}{$innerparts[2]}"; - } - } - $query->where[$i] = implode(' AND ',$parts); - } - } - - if($table != $baseTable) { - $query->from["{$table}_lang"] = $query->from[$table]; - } else { - // _lang is now the base table (the first one) - $query->from = array("{$table}_lang" => $query->from[$table]) + $query->from; - } - - // unless we are bypassing this query, add the language filter - if (!self::$bypass) $query->where[] = "\"{$table}_lang\".Lang = '$lang'"; - - // unless this is a deletion, the query is applied to the joined table - if (!$query->delete) { - $query->from[$table] = "INNER JOIN \"$table\"". - " ON \"{$table}_lang\".OriginalLangID = \"$table\".ID"; - /* if we are selecting fields (not doing counts for example) we need to select everything from - * the original table (was renamed to _lang) since some fields that we require may be there - */ - if ($query->select[0][0] == '"') $query->select = array_merge(array("\"$table\".*"),$query->select); - } else unset($query->from[$table]); - } else { - $query->from[$table] = str_replace("\"{$table}\".OriginalLangID","\"{$table}\".ID",$query->from[$table]); - } + if(!Translatable::is_enabled()) return; + + $lang = Translatable::current_lang(); + $baseTable = ClassInfo::baseDataClass($this->owner->class); + $where = $query->where; + if ( + $lang + && !$query->filtersOnID() + && array_search($baseTable, array_keys($query->from)) !== false + && !$this->isTranslation() + //&& !$query->filtersOnFK() + ) { + $qry = "\"Lang\" = '$lang'"; + if(Translatable::is_default_lang()) { + $qry .= " OR \"Lang\" = '' "; + $qry .= " OR \"Lang\" IS NULL "; } + $query->where[] = $qry; } } - - /** - * Check whether a WHERE clause should be applied to the augmented table - * - * @param string $clause Where clause that need to know if can be applied to the augmented (suffixed) table - * @param string $table Name of the non-augmented table - * @return boolean True if the clause can be applied to the augmented table - */ - function isInAugmentedTable($clause, $table) { - $clause = str_replace('\"','',$clause); - $table = str_replace('_lang','',$table); - if (strpos($table,'_') !== false) return false; - $field = ereg_replace('[[:blank:]]*([[:alnum:]]*).*','\\1',$clause); - $field = trim($field); - $allFields = $this->allFieldsInTable($table); - return (array_search($field,$allFields) !== false); - } - /** * Determine if the DataObject has any own translatable field (not inherited). @@ -447,6 +423,50 @@ class Translatable extends DataObjectDecorator { return false; } + function augmentNumChildrenCountQuery(SQLQuery $query) { + if(!Translatable::is_enabled()) return; + + if($this->isTranslation()) { + $query->where[0] = '\"ParentID\" = '.$this->getOriginalPage()->ID; + } + } + + /** + * @var SiteTree $cache_originalPage Cached representation of the original page for this translation + * (if at all in translation mode) + */ + private $cache_originalPage = null; + + function setOriginalPage($original) { + if($original instanceof DataObject) { + $this->owner->OriginalID = $original->ID; + } else { + $this->owner->OriginalID = $original; + } + } + + function getOriginalPage() { + if($this->isTranslation()) { + if(!$this->cache_originalPage) { + $orig = Translatable::current_lang(); + Translatable::set_reading_lang(Translatable::default_lang()); + $this->cache_originalPage = DataObject::get_by_id($this->owner->class, $this->owner->OriginalID); + Translatable::set_reading_lang($orig); + } + return $this->cache_originalPage; + } else { + return $this->owner; + } + } + + function isTranslation() { + if($this->getLang() && ($this->getLang() != Translatable::default_lang()) && $this->owner->exists()) { + return true; + } else { + return false; + } + } + /** * Determine if a table needs Versioned support * This is called at db/build time @@ -455,220 +475,138 @@ class Translatable extends DataObjectDecorator { * @return boolean */ function isVersionedTable($table) { + return false; // Every _lang table wants Versioned support return ($this->owner->databaseFields() && $this->hasOwnTranslatableFields()); } - function augmentDatabase() { - if (! $this->stat('enabled')) return false; - self::set_reading_lang(self::default_lang()); - $table = $this->owner->class; - - if(($fields = $this->owner->databaseFields()) && $this->hasOwnTranslatableFields()) { - //Calculate the required fields - foreach ($fields as $field => $type) { - if (array_search($field,$this->translatableFields) === false) unset($fields[$field]); - } - $metaFields = array_diff((array)$this->owner->databaseFields(), (array)$this->owner->customDatabaseFields()); - $indexes = $this->owner->databaseIndexes(); - - $langFields = array_merge( - array( - "Lang" => "Varchar(12)", - "OriginalLangID" => "Int" - ), - $fields, - $metaFields - ); - - foreach ($indexes as $index => $type) { - if (true === $type && array_search($index,$langFields) === false) unset($indexes[$index]); - } - - $langIndexes = array_merge( - array( - 'OriginalLangID_Lang' => '(OriginalLangID, Lang)', - 'OriginalLangID' => true, - 'Lang' => true, - ), - (array)$indexes - ); - - // Create table for translated instances - DB::requireTable("{$table}_lang", $langFields, $langIndexes); - - } else { - DB::dontRequireTable("{$table}_lang"); - } + function contentcontrollerInit($controller) { + if(!Translatable::is_enabled()) return; + Translatable::choose_site_lang(); + $controller->Lang = Translatable::current_lang(); } + function modelascontrollerInit($controller) { + if(!Translatable::is_enabled()) return; + + //$this->contentcontrollerInit($controller); + } + + function initgetEditForm($controller) { + if(!Translatable::is_enabled()) return; + + $this->contentcontrollerInit($controller); + } - /** - * Augment a write-record request. - * @param SQLQuery $manipulation Query to augment. - */ - function augmentWrite(&$manipulation) { - if (! $this->stat('enabled')) return false; - if(($lang = self::current_lang()) && !self::is_default_lang()) { - $tables = array_keys($manipulation); - foreach($tables as $table) { - if (self::table_exists("{$table}_lang")) { - $manipulation["{$table}_lang"] = $manipulation[$table]; - if ($manipulation[$table]['command'] == 'insert') { - $fakeID = $this->owner->ID; - // In an insert we've to populate our fields and generate a new id (since the passed one it's relative to $table) - $SessionOrigID = Session::get($this->owner->ID.'_originalLangID'); - $manipulation["{$table}_lang"]['fields']['OriginalLangID'] = $this->owner->ID = - ( $SessionOrigID ? $SessionOrigID : self::$creatingFromID); - $manipulation["{$table}_lang"]['RecordID'] = $manipulation["{$table}_lang"]['fields']['OriginalLangID']; - // populate lang field - $manipulation["{$table}_lang"]['fields']['Lang'] = "'$lang'" ; - // get a valid id, pre-inserting - DB::query("INSERT INTO \"{$table}_lang\" (\"Created\", \"Lang\") VALUES (NOW(), '$lang')"); - $manipulation["{$table}_lang"]['id'] = $manipulation["{$table}_lang"]['fields']['ID'] = DB::getGeneratedID("{$table}_lang"); - $manipulation["{$table}_lang"]['command'] = 'update'; - // we don't have to insert anything in $table if we are inserting in $table_lang - unset($manipulation[$table]); - // now dataobjects may create a record before the real write in the base table, so we have to delete it - 20/08/2007 - if (is_numeric($fakeID)) DB::query("DELETE FROM \"$table\" WHERE \"ID\"=$fakeID"); - } - else { - if (!isset($manipulation[$table]['fields']['OriginalLangID'])) { - // for those updates that may become inserts populate these fields - $manipulation["{$table}_lang"]['fields']['OriginalLangID'] = $this->owner->ID; - $manipulation["{$table}_lang"]['fields']['Lang'] = "'$lang'"; + function augmentWrite(&$manipulation) { + if(!Translatable::is_enabled()) return; + + if(!$this->isTranslation()) { + $ids = $this->findOriginalIDs(); + if(!$ids || count($ids) == 0) return; + } + $newManip = array(); + foreach($manipulation as $table => $manip) { + if(strpos($table, "_versions") !== false) continue; + /* + foreach($this->fieldBlackList as $blackField) { + if(isset($manip["fields"][$blackField])) { + if($this->isTranslation()) { + unset($manip["fields"][$blackField]); + } else { + if(!isset($newManip[$table])) { + $newManip[$table] = array("command" =>"update", + "where" => "ID in (".implode(",", $ids).")", + "fields" => array()); } - $id = $manipulation["{$table}_lang"]['id']; - if(!$id) user_error("Couldn't find ID in manipulation", E_USER_ERROR); - if (isset($manipulation["{$table}_lang"]['where'])) { - $manipulation["{$table}_lang"]['where'] .= "AND (Lang = '$lang') AND (OriginalLangID = $id)"; - } else { - $manipulation["{$table}_lang"]['where'] = "(Lang = '$lang') AND (OriginalLangID = $id)"; - } - $realID = DB::query("SELECT \"ID\" FROM \"{$table}_lang\" WHERE (\"OriginalLangID\" = $id) AND (\"Lang\" = '$lang') LIMIT 1")->value(); - $manipulation["{$table}_lang"]['id'] = $realID; - $manipulation["{$table}_lang"]['RecordID'] = $manipulation["{$table}_lang"]['fields']['OriginalLangID']; - // we could be updating non-translatable fields at the same time, so these will remain - foreach ($manipulation[$table]['fields'] as $field => $dummy) { - if ($this->isInAugmentedTable($field, $table) ) unset($manipulation[$table]['fields'][$field]); - } - if (count($manipulation[$table]['fields']) == 0) unset($manipulation[$table]); - } - foreach ($manipulation["{$table}_lang"]['fields'] as $field => $dummy) { - if (! $this->isInAugmentedTable($field, $table) ) unset($manipulation["{$table}_lang"]['fields'][$field]); + $newManip[$table]["fields"][$blackField] = $manip["fields"][$blackField]; } } } + */ } - } + DB::manipulate($newManip); + } //-----------------------------------------------------------------------------------------------// - /** - * Change the member dialog in the CMS - * - * This method updates the forms in the cms to allow the translations for - * the defined translatable fields. - */ function updateCMSFields(FieldSet &$fields) { - if (! $this->stat('enabled')) return false; + if(!Translatable::is_enabled()) return; + + // used in CMSMain->init() to set language state when reading/writing record + $fields->push(new HiddenField("Lang", "Lang", $this->getLang()) ); + $fields->push(new HiddenField("OriginalID", "OriginalID", $this->owner->OriginalID) ); + + // if a language other than default language is used, we're in "translation mode", + // hence have to modify the original fields $creating = false; $baseClass = $this->owner->class; + $allFields = $fields->toArray(); while( ($p = get_parent_class($baseClass)) != "DataObject") $baseClass = $p; - $allFields = $this->owner->getAllFields(); - if(!self::is_default_lang()) { - // Get the original version record, to show the original values - if (!is_numeric($allFields['ID'])) { - $originalLangID = Session::get($this->owner->ID . '_originalLangID'); - $creating = true; - } else { - $originalLangID = $allFields['ID']; - } - $originalRecord = self::get_one_by_lang( - $this->owner->class, - self::$default_lang, - "\"$baseClass\".ID = ".$originalLangID - ); - $this->original_values = $originalRecord->getAllFields(); - $alltasks = array( 'dup' => array()); - foreach($fields as $field) { - if ($field->isComposite()) { - $innertasks = $this->duplicateOrReplaceFields($field->FieldSet()); - // more efficient and safe than array_merge_recursive - $alltasks['dup'] = array_merge($alltasks['dup'],$innertasks['dup']); - } - } - foreach ($alltasks['dup'] as $fieldname => $newfield) { - // Duplicate the field - $fields->replaceField($fieldname,$newfield); + $isTranslationMode = (Translatable::default_lang() != $this->getLang() && $this->getLang()); + + if($isTranslationMode) { + $originalLangID = Session::get($this->owner->ID . '_originalLangID'); + + $translatableFieldNames = $this->getTranslatableFields(); + $allDataFields = $fields->dataFields(); + $originalRecord = $this->owner->getOriginalPage(); + $transformation = new Translatable_Transformation($originalRecord); + + // iterate through sequential list of all datafields in fieldset + // (fields are object references, so we can replace them with the translatable CompositeField) + foreach($allDataFields as $dataField) { + + if(in_array($dataField->Name(), $translatableFieldNames)) { + // if the field is translatable, perform transformation + $fields->replaceField($dataField->Name(), $transformation->transformFormField($dataField)); + } else { + // else field shouldn't be editable in translation-mode, make readonly + $fields->replaceField($dataField->Name(), $dataField->performReadonlyTransformation()); + } } } else { - $alreadyTranslatedLangs = null; - if (is_numeric($allFields['ID'])) { - $alreadyTranslatedLangs = self::get_langs_by_id($baseClass,$allFields['ID']); - } - if (!$alreadyTranslatedLangs) $alreadyTranslatedLangs = array(); - foreach ($alreadyTranslatedLangs as $i => $langCode) { - $alreadyTranslatedLangs[$i] = i18n::get_language_name($langCode); - } + // if we're not in "translation mode", show a dropdown to create a new translation. + // this action should just be possible when showing the default language, + // you can't create new translations from within a "translation mode" form. + $alreadyTranslatedLangs = $this->getTranslatedLangs(); + $fields->addFieldsToTab( 'Root', new Tab(_t('Translatable.TRANSLATIONS', 'Translations'), - new HeaderField('CreateTransHeader',_t('Translatable.CREATE', 'Create new translation'), 2), + new HeaderField('CreateTransHeader', _t('Translatable.CREATE', 'Create new translation'), 2), $langDropdown = new LanguageDropdownField("NewTransLang", _t('Translatable.NEWLANGUAGE', 'New language'), $alreadyTranslatedLangs), $createButton = new InlineFormAction('createtranslation',_t('Translatable.CREATEBUTTON', 'Create')) ) ); - if (count($alreadyTranslatedLangs)) { - $fields->addFieldsToTab( + + if($alreadyTranslatedLangs) { + $fields->addFieldToTab( 'Root.Translations', - new FieldSet( - new HeaderField('ExistingTransHeader',_t('Translatable.EXISTING', 'Existing translations:'), 3), - new LiteralField('existingtrans',implode(', ',$alreadyTranslatedLangs)) - ) + new HeaderField('ExistingTransHeader', _t('Translatable.EXISTING', 'Existing translations:'), 3) + ); + $existingTransHTML = ''; + $fields->addFieldToTab( + 'Root.Translations', + new LiteralField('existingtrans',$existingTransHTML) ); } + + $langDropdown->addExtraClass('languageDropdown'); $createButton->addExtraClass('createTranslationButton'); + + // disable creation of new pages via javascript $createButton->includeDefaultJS(false); } } - - protected function duplicateOrReplaceFields(&$fields) { - $tasks = array( - 'dup' => array(), - ); - foreach($fields as $field) { - if ($field->isComposite()) { - $innertasks = $this->duplicateOrReplaceFields($field->FieldSet()); - $tasks['dup'] = array_merge($tasks['dup'],$innertasks['dup']); - } - else if(($fieldname = $field->Name()) && array_key_exists($fieldname,$this->original_values)) { - // Get a copy of the original field to show the untranslated value - if($field instanceof TextareaField) { - $nonEditableField = new ToggleField($fieldname,$field->Title(),'','+','-'); - $nonEditableField->labelMore = '+'; - $nonEditableField->labelLess = '-'; - } else { - $nonEditableField = $field->performDisabledTransformation(); - } - - $nonEditableField_holder = new CompositeField($nonEditableField); - $nonEditableField_holder->setName($fieldname.'_holder'); - $nonEditableField_holder->addExtraClass('originallang_holder'); - $nonEditableField->setValue($this->original_values[$fieldname]); - $nonEditableField->setName($fieldname.'_original'); - $nonEditableField->addExtraClass('originallang'); - if (array_search($fieldname,$this->translatableFields) !== false) { - // Duplicate the field - if ($field->Title()) $nonEditableField->setTitle('Original'); - $nonEditableField_holder->insertBefore($field, $fieldname.'_original'); - $tasks['dup'][$fieldname] = $nonEditableField_holder; - } - } - } - return $tasks; - } /** * Get a list of fields from the tables created by this extension @@ -677,30 +615,7 @@ class Translatable extends DataObjectDecorator { * @return array Map where the keys are db, indexes and the values are the table fields */ function fieldsInExtraTables($table){ - - if(($fields = $this->owner->databaseFields()) && $this->hasOwnTranslatableFields()) { - //Calculate the required fields - foreach ($fields as $field => $type) { - if (array_search($field,$this->translatableFields) === false) unset($fields[$field]); - } - $metaFields = array_diff((array)$this->owner->databaseFields(), (array)$this->owner->customDatabaseFields()); - $indexes = $this->owner->databaseIndexes(); - - $langFields = array_merge( - array( - "Lang" => "Varchar(12)", - "OriginalLangID" => "Int" - ), - $fields, - $metaFields - ); - - foreach ($indexes as $index => $type) { - if (true === $type && array_search($index,$langFields) === false) unset($indexes[$index]); - } - - return array('db' => $langFields, 'indexes' => $indexes); - } + return array('db'=>null,'indexes'=>null); } /** @@ -735,6 +650,17 @@ class Translatable extends DataObjectDecorator { ); return $langFields; } + + /** + * 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. @@ -746,19 +672,74 @@ class Translatable extends DataObjectDecorator { return (!$stage || $stage == $this->defaultStage) ? $baseClass : $baseClass . "_$stage"; } - /** - * Extends $table with a suffix if required - * - * @param string $table Name of the table - * @return string Extended table name - */ function extendWithSuffix($table) { - if((($lang = self::current_lang()) && !self::is_default_lang())) { - if (self::table_exists("{$table}_lang")) return $table.'_lang'; - } return $table; } + function getTranslation($lang, $create=true) { + if($this->owner->exists() && !$this->owner->isTranslation()) { + $orig = Translatable::current_lang(); + $this->owner->flushCache(); + Translatable::set_reading_lang($lang); + + $filter = array("`OriginalID` = '".$this->owner->ID."'"); + + if($this->owner->hasExtension("Versioned") && Versioned::current_stage()) { + $translation = Versioned::get_one_by_stage($this->owner->class, Versioned::current_stage(), $filter); + } else { + $translation = DataObject::get_one($this->owner->class, $filter); + } + + Translatable::set_reading_lang($orig); + + if($create && !$translation) { + $class = $this->owner->class; + $translation = new $class; + $translation->update($this->owner->toMap()); + $translation->ID = 0; + $translation->setOriginalPage($this->owner->ID); + $translation->Lang = $lang; + } + return $translation; + } + } + + function hasTranslation($lang) { + return ($this->owner->exists()) && (array_search($lang, $this->getTranslatedLangs()) !== false); + } + + function augmentStageChildren(DataObjectSet $children, $showall = false) { + if(!Translatable::is_enabled()) return; + + if($this->isTranslation()) { + $children->merge($this->getOriginalPage()->stageChildren($showall)); + } + } + + function augmentAllChildrenIncludingDeleted(DataObjectSet $children, $context = null) { + if(!Translatable::is_enabled()) return false; + + $find = array(); + $replace = array(); + + // @todo check usage of $context + if($context && $context->Lang /*&& $this->owner->Lang != $context->Lang */&& $context->Lang != Translatable::default_lang()) { + if($children) { + foreach($children as $child) { + if($child->hasTranslation($context->Lang)) { + $trans = $child->getTranslation($context->Lang); + $find[] = $child; + $replace[] = $trans; + } + } + foreach($find as $i => $found) { + $children->replace($found, $replace[$i]); + } + } + } + + } + /** * Get a list of languages with at least one element translated in (including the default language) * @@ -768,7 +749,7 @@ class Translatable extends DataObjectDecorator { static function get_existing_content_languages($className = 'SiteTree', $where = '') { if(!Translatable::is_enabled()) return false; $baseTable = ClassInfo::baseDataClass($className); - $query = new SQLQuery('Lang',$baseTable.'_lang',$where,"",'Lang'); + $query = new SQLQuery('Distinct Lang',$baseTable,$where,"",'Lang'); $dbLangs = $query->execute()->column(); $langlist = array_merge((array)Translatable::default_lang(), (array)$dbLangs); $returnMap = array(); @@ -781,4 +762,71 @@ class Translatable extends DataObjectDecorator { } } -?> + +/** + * Transform a formfield to a "translatable" representation, + * consisting of the original formfield plus a readonly-version + * of the original value, wrapped in a CompositeField. + * + * @param DataObject $original Needs the original record as we populate the readonly formfield with the original value + * + * @package sapphire + * @subpackage misc + */ +class Translatable_Transformation extends FormTransformation { + + /** + * @var DataObject + */ + private $original = null; + + function __construct(DataObject $original) { + $this->original = $original; + parent::__construct(); + } + + /** + * Returns the original DataObject attached to the Transformation + * + * @return DataObject + */ + function getOriginal() { + return $this->original; + } + + /** + * @todo transformTextareaField() not used at the moment + */ + function transformTextareaField(TextareaField $field) { + $nonEditableField = new ToggleField($fieldname,$field->Title(),'','+','-'); + $nonEditableField->labelMore = '+'; + $nonEditableField->labelLess = '-'; + return $this->baseTransform($nonEditableField, $field); + + return $nonEditableField; + } + + function transformFormField(FormField $field) { + $newfield = $field->performReadOnlyTransformation(); + return $this->baseTransform($newfield, $field); + } + + protected function baseTransform($nonEditableField, $originalField) { + $fieldname = $originalField->Name(); + + $nonEditableField_holder = new CompositeField($nonEditableField); + $nonEditableField_holder->setName($fieldname.'_holder'); + $nonEditableField_holder->addExtraClass('originallang_holder'); + $nonEditableField->setValue($this->original->$fieldname); + $nonEditableField->setName($fieldname.'_original'); + $nonEditableField->addExtraClass('originallang'); + $nonEditableField->setTitle('Original '.$originalField->Title()); + + $nonEditableField_holder->insertBefore($originalField, $fieldname.'_original'); + return $nonEditableField_holder; + } + + +} + +?> \ No newline at end of file