Version: current

       1 <?php
       2 /**
       3  * The Translatable decorator allows your DataObjects to have versions in different languages,
       4  * defining which fields are can be translated. Translatable can be applied
       5  * to any {@link DataObject} subclass, but is mostly used with {@link SiteTree}.
       6  * Translatable is compatible with the {@link Versioned} extension.
       7  * To avoid cluttering up the database-schema of the 99% of sites without multiple languages,
       8  * the translation-feature is disabled by default.
       9  *
      10  * Locales (e.g. 'en_US') are used in Translatable for identifying a record by language,
      11  * see section "Locales and Language Tags".
      12  *
      13  * <h2>Configuration</h2>
      14  *
      15  * <h3>Through Object::add_extension()</h3>
      16  * Enabling Translatable through {@link Object::add_extension()} in your _config.php:
      17  * <code>
      18  * Object::add_extension('MyClass', 'Translatable');
      19  * </code>
      20  * This is the recommended approach for enabling Translatable.
      21  *
      22  * <h3>Through $extensions</h3>
      23  * <code>
      24  * class MyClass extends DataObject {
      25  *   static $extensions = array(
      26  *     "Translatable"
      27  *   );
      28  * }
      29  * </code>
      30  *
      31  * Make sure to rebuild the database through /dev/build after enabling translatable.
      32  * Use the correct {@link set_default_locale()} before building the database
      33  * for the first time, as this locale will be written on all new records.
      34  *
      35  * <h3>"Default" locales</h3>
      36  *
      37  * Important: If the "default language" of your site is not US-English (en_US),
      38  * please ensure to set the appropriate default language for
      39  * your content before building the database with Translatable enabled:
      40  * <code>
      41  * Translatable::set_default_locale(<locale>); // e.g. 'de_DE' or 'fr_FR'
      42  * </code>
      43  *
      44  * For the Translatable class, a "locale" consists of a language code plus a region code separated by an underscore,
      45  * for example "de_AT" for German language ("de") in the region Austria ("AT").
      46  * See http://www.w3.org/International/articles/language-tags/ for a detailed description.
      47  *
      48  * <h2>Usage</h2>
      49  *
      50  * Getting a translation for an existing instance:
      51  * <code>
      52  * $translatedObj = Translatable::get_one_by_locale('MyObject', 'de_DE');
      53  * </code>
      54  *
      55  * Getting a translation for an existing instance:
      56  * <code>
      57  * $obj = DataObject::get_by_id('MyObject', 99); // original language
      58  * $translatedObj = $obj->getTranslation('de_DE');
      59  * </code>
      60  *
      61  * Getting translations through {@link Translatable::set_current_locale()}.
      62  * This is *not* a recommended approach, but sometimes inavoidable (e.g. for {@link Versioned} methods).
      63  * <code>
      64  * $origLocale = Translatable::get_current_locale();
      65  * Translatable::set_current_locale('de_DE');
      66  * $obj = Versioned::get_one_by_stage('MyObject', "ID = 99");
      67  * Translatable::set_current_locale($origLocale);
      68  * </code>
      69  *
      70  * Creating a translation:
      71  * <code>
      72  * $obj = new MyObject();
      73  * $translatedObj = $obj->createTranslation('de_DE');
      74  * </code>
      75  *
      76  * <h2>Usage for SiteTree</h2>
      77  *
      78  * Translatable can be used for subclasses of {@link SiteTree} as well.
      79  *
      80  * <code>
      81  * Object::add_extension('SiteTree', 'Translatable');
      82  * Object::add_extension('SiteConig', 'Translatable');
      83  * </code>
      84  *
      85  * If a child page translation is requested without the parent
      86  * page already having a translation in this language, the extension
      87  * will recursively create translations up the tree.
      88  * Caution: The "URLSegment" property is enforced to be unique across
      89  * languages by auto-appending the language code at the end.
      90  * You'll need to ensure that the appropriate "reading language" is set
      91  * before showing links to other pages on a website through $_GET['locale'].
      92  * Pages in different languages can have different publication states
      93  * through the {@link Versioned} extension.
      94  *
      95  * Note: You can't get Children() for a parent page in a different language
      96  * through set_current_locale(). Get the translated parent first.
      97  *
      98  * <code>
      99  * // wrong
     100  * Translatable::set_current_locale('de_DE');
     101  * $englishParent->Children();
     102  * // right
     103  * $germanParent = $englishParent->getTranslation('de_DE');
     104  * $germanParent->Children();
     105  * </code>
     106  *
     107  * <h2>Translation groups</h2>
     108  *
     109  * Each translation can have one or more related pages in other languages.
     110  * This relation is optional, meaning you can
     111  * create translations which have no representation in the "default language".
     112  * This means you can have a french translation with a german original,
     113  * without either of them having a representation
     114  * in the default english language tree.
     115  * Caution: There is no versioning for translation groups,
     116  * meaning associating an object with a group will affect both stage and live records.
     117  *
     118  * SiteTree database table (abbreviated)
     119  * ^ ID ^ URLSegment ^ Title ^ Locale ^
     120  * | 1 | about-us | About us | en_US |
     121  * | 2 | ueber-uns | Über uns | de_DE |
     122  * | 3 | contact | Contact | en_US |
     123  *
     124  * SiteTree_translationgroups database table
     125  * ^ TranslationGroupID ^ OriginalID ^
     126  * | 99 | 1 |
     127  * | 99 | 2 |
     128  * | 199 | 3 |
     129  *
     130  * <h2>Character Sets</h2>
     131  *
     132  * Caution: Does not apply any character-set conversion, it is assumed that all content
     133  * is stored and represented in UTF-8 (Unicode). Please make sure your database and
     134  * HTML-templates adjust to this.
     135  *
     136  * <h2>Permissions</h2>
     137  *
     138  * Authors without administrative access need special permissions to edit locales other than
     139  * the default locale.
     140  *
     141  * - TRANSLATE_ALL: Translate into all locales
     142  * - Translate_<locale>: Translate a specific locale. Only available for all locales set in
     143  *   `Translatable::set_allowed_locales()`.
     144  *
     145  * Note: If user-specific view permissions are required, please overload `SiteTree->canView()`.
     146  *
     147  * <h2>Uninstalling/Disabling</h2>
     148  *
     149  * Disabling Translatable after creating translations will lead to all
     150  * pages being shown in the default sitetree regardless of their language.
     151  * It is advised to start with a new database after uninstalling Translatable,
     152  * or manually filter out translated objects through their "Locale" property
     153  * in the database.
     154  *
     155  * @see http://doc.silverstripe.org/doku.php?id=multilingualcontent
     156  *
     157  * @author Ingo Schommer <ingo (at) silverstripe (dot) com>
     158  * @author Michael Gall <michael (at) wakeless (dot) net>
     159  * @author Bernat Foj Capell <bernat@silverstripe.com>
     160  *
     161  * @package sapphire
     162  * @subpackage i18n
     163  */
     164 class Translatable extends DataObjectDecorator implements PermissionProvider {
     165
     166     /**
     167      * The 'default' language.
     168      * @var string
     169      */
     170     protected static $default_locale = 'en_US';
     171
     172     /**
     173      * The language in which we are reading dataobjects.
     174      *
     175      * @var string
     176      */
     177     protected static $current_locale = null;
     178
     179     /**
     180      * A cached list of existing tables
     181      *
     182      * @var mixed
     183      */
     184     protected static $tableList = null;
     185
     186     /**
     187      * An array of fields that can be translated.
     188      * @var array
     189      */
     190     protected $translatableFields = null;
     191
     192     /**
     193      * A map of the field values of the original (untranslated) DataObject record
     194      * @var array
     195      */
     196     protected $original_values = null;
     197
     198     /**
     199      * If this is set to TRUE then {@link augmentSQL()} will automatically add a filter
     200      * clause to limit queries to the current {@link get_current_locale()}. This camn be
     201      * disabled using {@link disable_locale_filter()}
     202      *
     203      * @var bool
     204      */
     205     protected static $locale_filter_enabled = true;
     206
     207     /**
     208      * @var array All locales in which a translation can be created.
     209      * This limits the choice in the CMS language dropdown in the
     210      * "Translation" tab, as well as the language dropdown above
     211      * the CMS tree. If not set, it will default to showing all
     212      * common locales.
     213      */
     214     protected static $allowed_locales = null;
     215
     216     /**
     217      * Reset static configuration variables to their default values
     218      */
     219     static function reset() {
     220         self::enable_locale_filter();
     221         self::$default_locale = 'en_US';
     222         self::$current_locale = null;
     223         self::$allowed_locales = null;
     224     }
     225
     226     /**
     227      * Choose the language the site is currently on.
     228      *
     229      * If $_GET['locale'] is currently set, then that locale will be used. Otherwise the member preference (if logged
     230      * in) or default locale will be used.
     231      *
     232      * @todo Re-implement cookie and member option
     233      *
     234      * @param $langsAvailable array A numerical array of languages which are valid choices (optional)
     235      * @return string Selected language (also saved in $current_locale).
     236      */
     237     static function choose_site_locale($langsAvailable = array()) {
     238         if(self::$current_locale) {
     239             return self::$current_locale;
     240         }
     241         if((isset($_GET['locale']) && !$langsAvailable) || (isset($_GET['locale']) && in_array($_GET['locale'], $langsAvailable))) {
     242             // get from GET parameter
     243             self::set_current_locale($_GET['locale']);
     244         } else {
     245             self::set_current_locale(self::default_locale());
     246         }
     247
     248         return self::$current_locale;
     249     }
     250
     251     /**
     252      * Get the current reading language.
     253      * This value has to be set before the schema is built with translatable enabled,
     254      * any changes after this can cause unintended side-effects.
     255      *
     256      * @return string
     257      */
     258     static function default_locale() {
     259         return self::$default_locale;
     260     }
     261
     262     /**
     263      * Set default language. Please set this value *before* creating
     264      * any database records (like pages), as this locale will be attached
     265      * to all new records.
     266      *
     267      * @param $locale String
     268      */
     269     static function set_default_locale($locale) {
     270         $localeList = i18n::get_locale_list();
     271         if(isset($localeList[$locale])) {
     272             self::$default_locale = $locale;
     273         } else {
     274             user_error("Translatable::set_default_locale(): '$locale' is not a valid locale.", E_USER_WARNING);
     275         }
     276     }
     277
     278     /**
     279      * Get the current reading language.
     280      * If its not chosen, call {@link choose_site_locale()}.
     281      *
     282      * @return string
     283      */
     284     static function get_current_locale() {
     285         return (self::$current_locale) ? self::$current_locale : self::choose_site_locale();
     286     }
     287
     288     /**
     289      * Set the reading language, either namespaced to 'site' (website content)
     290      * or 'cms' (management backend). This value is used in {@link augmentSQL()}
     291      * to "auto-filter" all SELECT queries by this language.
     292      * See {@link disable_locale_filter()} on how to override this behaviour temporarily.
     293      *
     294      * @param string $lang New reading language.
     295      */
     296     static function set_current_locale($locale) {
     297         self::$current_locale = $locale;
     298     }
     299
     300     /**
     301      * Get a singleton instance of a class in the given language.
     302      * @param string $class The name of the class.
     303      * @param string $locale  The name of the language.
     304      * @param string $filter A filter to be inserted into the WHERE clause.
     305      * @param boolean $cache Use caching (default: false)
     306      * @param string $orderby A sort expression to be inserted into the ORDER BY clause.
     307      * @return DataObject
     308      */
     309     static function get_one_by_locale($class, $locale, $filter = '', $cache = false, $orderby = "") {
     310         $orig = Translatable::get_current_locale();
     311         Translatable::set_current_locale($locale);
     312         $do = DataObject::get_one($class, $filter, $cache, $orderby);
     313         Translatable::set_current_locale($orig);
     314         return $do;
     315     }
     316
     317     /**
     318      * Get all the instances of the given class translated to the given language
     319      *
     320      * @param string $class The name of the class
     321      * @param string $locale  The name of the language
     322      * @param string $filter A filter to be inserted into the WHERE clause.
     323      * @param string $sort A sort expression to be inserted into the ORDER BY clause.
     324      * @param string $join A single join clause.  This can be used for filtering, only 1 instance of each DataObject will be returned.
     325      * @param string $limit A limit expression to be inserted into the LIMIT clause.
     326      * @param string $containerClass The container class to return the results in.
     327      * @param string $having A filter to be inserted into the HAVING clause.
     328      * @return mixed The objects matching the conditions.
     329      */
     330     static function get_by_locale($class, $locale, $filter = '', $sort = '', $join = "", $limit = "", $containerClass = "DataObjectSet", $having = "") {
     331         $oldLang = self::get_current_locale();
     332         self::set_current_locale($locale);
     333         $result = DataObject::get($class, $filter, $sort, $join, $limit, $containerClass, $having);
     334         self::set_current_locale($oldLang);
     335         return $result;
     336     }
     337
     338     /**
     339      * @return bool
     340      */
     341     public static function locale_filter_enabled() {
     342         return self::$locale_filter_enabled;
     343     }
     344
     345     /**
     346      * Enables automatic filtering by locale. This is normally called after is has been
     347      * disabled using {@link disable_locale_filter()}.
     348      */
     349     public static function enable_locale_filter() {
     350         self::$locale_filter_enabled = true;
     351     }
     352
     353     /**
     354      * Disables automatic locale filtering in {@link augmentSQL()}. This can be re-enabled
     355      * using {@link enable_locale_filter()}.
     356      */
     357     public static function disable_locale_filter() {
     358         self::$locale_filter_enabled = false;
     359     }
     360
     361     /**
     362      * Gets all translations for this specific page.
     363      * Doesn't include the language of the current record.
     364      *
     365      * @return array Numeric array of all locales, sorted alphabetically.
     366      */
     367     function getTranslatedLocales() {
     368         $langs = array();
     369
     370         $baseDataClass = ClassInfo::baseDataClass($this->owner->class); //Base Class
     371         $translationGroupClass = $baseDataClass . "_translationgroups";
     372         if($this->owner->hasExtension("Versioned")  && Versioned::current_stage() == "Live") {
     373             $baseDataClass = $baseDataClass . "_Live";
     374         }
     375
     376         $translationGroupID = $this->getTranslationGroup();
     377         if(is_numeric($translationGroupID)) {
     378             $query = new SQLQuery(
     379                 'DISTINCT "Locale"',
     380                 sprintf(
     381                     '"%s" LEFT JOIN "%s" ON "%s"."OriginalID" = "%s"."ID"',
     382                     $baseDataClass,
     383                     $translationGroupClass,
     384                     $translationGroupClass,
     385                     $baseDataClass
     386                 ), // from
     387                 sprintf(
     388                     '"%s"."TranslationGroupID" = %d AND "%s"."Locale" != \'%s\'',
     389                     $translationGroupClass,
     390                     $translationGroupID,
     391                     $baseDataClass,
     392                     $this->owner->Locale
     393                 ) // where
     394             );
     395             $langs = $query->execute()->column();
     396         }
     397         if($langs) {
     398             $langCodes = array_values($langs);
     399             sort($langCodes);
     400             return $langCodes;
     401         } else {
     402             return array();
     403         };
     404     }
     405
     406     /**
     407      * Gets all locales that a member can access
     408      * as defined by {@link $allowed_locales}
     409      * and {@link canTranslate()}.
     410      * If {@link $allowed_locales} is not set and
     411      * the user has the `TRANSLATE_ALL` permission,
     412      * the method will return all available locales in the system.
     413      *
     414      * @param Member $member
     415      * @return array Map of locales
     416      */
     417     function getAllowedLocalesForMember($member) {
     418         $locales = self::get_allowed_locales();
     419         if(!$locales) $locales = i18n::get_common_locales();
     420         if($locales) foreach($locales as $k => $locale) {
     421             if(!$this->canTranslate($member, $locale)) unset($locales[$k]);
     422         }
     423
     424         return $locales;
     425     }
     426
     427     /**
     428      * Get a list of languages in which a given element has been translated.
     429      *
     430      * @deprecated 2.4 Use {@link getTranslations()}
     431      *
     432      * @param string $class Name of the class of the element
     433      * @param int $id ID of the element
     434      * @return array List of languages
     435      */
     436     static function get_langs_by_id($class, $id) {
     437         $do = DataObject::get_by_id($class, $id);
     438         return ($do ? $do->getTranslatedLocales() : array());
     439     }
     440
     441     /**
     442      * Enables the multilingual feature
     443      *
     444      * @deprecated 2.4 Use Object::add_extension('SiteTree', 'Translatable')
     445      */
     446     static function enable() {
     447         Object::add_extension('SiteTree', 'Translatable');
     448     }
     449
     450     /**
     451      * Disable the multilingual feature
     452      *
     453      * @deprecated 2.4 Use Object::remove_extension('SiteTree', 'Translatable')
     454      */
     455     static function disable() {
     456         Object::remove_extension('SiteTree', 'Translatable');
     457     }
     458
     459     /**
     460      * Check whether multilingual support has been enabled
     461      *
     462      * @deprecated 2.4 Use Object::has_extension('SiteTree', 'Translatable')
     463      * @return boolean True if enabled
     464      */
     465     static function is_enabled() {
     466         return Object::has_extension('SiteTree', 'Translatable');
     467     }
     468
     469
     470     /**
     471      * Construct a new Translatable object.
     472      * @var array $translatableFields The different fields of the object that can be translated.
     473      * This is currently not implemented, all fields are marked translatable (see {@link setOwner()}).
     474      */
     475     function __construct($translatableFields = null) {
     476         parent::__construct();
     477
     478         // @todo Disabled selection of translatable fields - we're setting all fields as translatable in setOwner()
     479         /*
     480         if(!is_array($translatableFields)) {
     481             $translatableFields = func_get_args();
     482         }
     483         $this->translatableFields = $translatableFields;
     484         */
     485
     486         // workaround for extending a method on another decorator (Hierarchy):
     487         // split the method into two calls, and overwrite the wrapper AllChildrenIncludingDeleted()
     488         // Has to be executed even with Translatable disabled, as it overwrites the method with same name
     489         // on Hierarchy class, and routes through to Hierarchy->doAllChildrenIncludingDeleted() instead.
     490         // Caution: There's an additional method for augmentAllChildrenIncludingDeleted()
     491
     492     }
     493
     494     function setOwner($owner, $ownerBaseClass = null) {
     495         parent::setOwner($owner, $ownerBaseClass);
     496
     497         // setting translatable fields by inspecting owner - this should really be done in the constructor
     498         if($this->owner && $this->translatableFields === null) {
     499             $this->translatableFields = array_merge(
     500                 array_keys($this->owner->inheritedDatabaseFields()),
     501                 array_keys($this->owner->has_many()),
     502                 array_keys($this->owner->many_many())
     503             );
     504         }
     505     }
     506
     507     function extraStatics() {
     508         return array(
     509             "db" => array(
     510                 "Locale" => "DBLocale",
     511                 //"TranslationMasterID" => "Int" // optional relation to a "translation master"
     512             ),
     513             "defaults" => array(
     514                 "Locale" => Translatable::default_locale() // as an overloaded getter as well: getLang()
     515             )
     516         );
     517     }
     518
     519     /**
     520      * Changes any SELECT query thats not filtering on an ID
     521      * to limit by the current language defined in {@link get_current_locale()}.
     522      * It falls back to "Locale='' OR Lang IS NULL" and assumes that
     523      * this implies querying for the default language.
     524      *
     525      * Use {@link disable_locale_filter()} to temporarily disable this "auto-filtering".
     526      */
     527     function augmentSQL(SQLQuery &$query) {
     528         // If the record is saved (and not a singleton), and has a locale,
     529         // limit the current call to its locale. This fixes a lot of problems
     530         // with other extensions like Versioned
     531         $locale = ($this->owner->ID && $this->owner->Locale) ? $this->owner->Locale : Translatable::get_current_locale();
     532         $baseTable = ClassInfo::baseDataClass($this->owner->class);
     533         $where = $query->where;
     534         if(
     535             $locale
     536             // unless the filter has been temporarily disabled
     537             && self::locale_filter_enabled()
     538             // DataObject::get_by_id() should work independently of language
     539             && !$query->filtersOnID()
     540             // the query contains this table
     541             // @todo Isn't this always the case?!
     542             && array_search($baseTable, array_keys($query->from)) !== false
     543             // or we're already filtering by Lang (either from an earlier augmentSQL() call or through custom SQL filters)
     544             && !preg_match('/("|\'|`)Locale("|\'|`)/', $query->getFilter())
     545             //&& !$query->filtersOnFK()
     546         )  {
     547             $qry = sprintf('"%s"."Locale" = \'%s\'', $baseTable, $locale);
     548             $query->where[] = $qry;
     549         }
     550     }
     551
     552     /**
     553      * Create <table>_translation database table to enable
     554      * tracking of "translation groups" in which each related
     555      * translation of an object acts as a sibling, rather than
     556      * a parent->child relation.
     557      */
     558     function augmentDatabase() {
     559         $baseDataClass = ClassInfo::baseDataClass($this->owner->class);
     560         if($this->owner->class != $baseDataClass) return;
     561
     562         $fields = array(
     563             'OriginalID' => 'Int',
     564             'TranslationGroupID' => 'Int',
     565         );
     566         $indexes = array(
     567             'OriginalID' => true,
     568             'TranslationGroupID' => true
     569         );
     570
     571         // Add new tables if required
     572         DB::requireTable("{$baseDataClass}_translationgroups", $fields, $indexes);
     573
     574         // Remove 2.2 style tables
     575         DB::dontRequireTable("{$baseDataClass}_lang");
     576         if($this->owner->hasExtension('Versioned')) {
     577             DB::dontRequireTable("{$baseDataClass}_lang_Live");
     578             DB::dontRequireTable("{$baseDataClass}_lang_versions");
     579         }
     580     }
     581
     582     /**
     583      * @todo Find more appropriate place to hook into database building
     584      */
     585     function requireDefaultRecords() {
     586         // @todo This relies on the Locale attribute being on the base data class, and not any subclasses
     587         if($this->owner->class != ClassInfo::baseDataClass($this->owner->class)) return false;
     588
     589         // Permissions: If a group doesn't have any specific TRANSLATE_<locale> edit rights,
     590         // but has CMS_ACCESS_CMSMain (general CMS access), then assign TRANSLATE_ALL permissions as a default.
     591         // Auto-setting permissions based on these intransparent criteria is a bit hacky,
     592         // but unavoidable until we can determine when a certain permission code was made available first
     593         // (see http://open.silverstripe.org/ticket/4940)
     594         $groups = Permission::get_groups_by_permission(array('CMS_ACCESS_CMSMain','CMS_ACCESS_LeftAndMain','ADMIN'));
     595         if($groups) foreach($groups as $group) {
     596             $codes = $group->Permissions()->column('Code');
     597             $hasTranslationCode = false;
     598             foreach($codes as $code) {
     599                 if(preg_match('/^TRANSLATE_/', $code)) $hasTranslationCode = true;
     600             }
     601             // Only add the code if no more restrictive code exists
     602             if(!$hasTranslationCode) Permission::grant($group->ID, 'TRANSLATE_ALL');
     603         }
     604
     605         // If the Translatable extension was added after the first records were already
     606         // created in the database, make sure to update the Locale property if
     607         // if wasn't set before
     608         $idsWithoutLocale = DB::query(sprintf(
     609             'SELECT "ID" FROM "%s" WHERE "Locale" IS NULL OR "Locale" = \'\'',
     610             ClassInfo::baseDataClass($this->owner->class)
     611         ))->column();
     612         if(!$idsWithoutLocale) return;
     613
     614             if($this->owner->class == 'SiteTree') {
     615             foreach(array('Stage', 'Live') as $stage) {
     616                 foreach($idsWithoutLocale as $id) {
     617                     $obj = Versioned::get_one_by_stage(
     618                         $this->owner->class,
     619                         $stage,
     620                         sprintf('"SiteTree"."ID" = %d', $id)
     621                     );
     622                     if(!$obj) continue;
     623
     624                     $obj->Locale = Translatable::default_locale();
     625                     $obj->writeToStage($stage);
     626                     $obj->addTranslationGroup($obj->ID);
     627                     $obj->destroy();
     628                     unset($obj);
     629                 }
     630             }
     631         } else {
     632             foreach($idsWithoutLocale as $id) {
     633                 $obj = DataObject::get_by_id($this->owner->class, $id);
     634                 if(!$obj) continue;
     635
     636                 $obj->Locale = Translatable::default_locale();
     637                 $obj->write();
     638                 $obj->addTranslationGroup($obj->ID);
     639                 $obj->destroy();
     640                 unset($obj);
     641             }
     642         }
     643         DB::alteration_message(sprintf(
     644             "Added default locale '%s' to table %s","changed",
     645             Translatable::default_locale(),
     646             $this->owner->class
     647         ));
     648     }
     649
     650     /**
     651      * Add a record to a "translation group",
     652      * so its relationship to other translations
     653      * based off the same object can be determined later on.
     654      * See class header for further comments.
     655      *
     656      * @param int $originalID Either the primary key of the record this new translation is based on,
     657      *  or the primary key of this record, to create a new translation group
     658      * @param boolean $overwrite
     659      */
     660     public function addTranslationGroup($originalID, $overwrite = false) {
     661         if(!$this->owner->exists()) return false;
     662
     663         $baseDataClass = ClassInfo::baseDataClass($this->owner->class);
     664         $existingGroupID = $this->getTranslationGroup($originalID);
     665
     666         // Remove any existing groups if overwrite flag is set
     667         if($existingGroupID && $overwrite) {
     668             $sql = sprintf(
     669                 'DELETE FROM "%s_translationgroups" WHERE "TranslationGroupID" = %d AND "OriginalID" = %d',
     670                 $baseDataClass,
     671                 $existingGroupID,
     672                 $this->owner->ID
     673             );
     674             DB::query($sql);
     675             $existingGroupID = null;
     676         }
     677
     678         // Add to group (only if not in existing group or $overwrite flag is set)
     679         if(!$existingGroupID) {
     680             $sql = sprintf(
     681                 'INSERT INTO "%s_translationgroups" ("TranslationGroupID","OriginalID") VALUES (%d,%d)',
     682                 $baseDataClass,
     683                 $originalID,
     684                 $this->owner->ID
     685             );
     686             DB::query($sql);
     687         }
     688     }
     689
     690     /**
     691      * Gets the translation group for the current record.
     692      * This ID might equal the record ID, but doesn't have to -
     693      * it just points to one "original" record in the list.
     694      *
     695      * @return int Numeric ID of the translationgroup in the <classname>_translationgroup table
     696      */
     697     public function getTranslationGroup() {
     698         if(!$this->owner->exists()) return false;
     699
     700         $baseDataClass = ClassInfo::baseDataClass($this->owner->class);
     701         return DB::query(
     702             sprintf('SELECT "TranslationGroupID" FROM "%s_translationgroups" WHERE "OriginalID" = %d', $baseDataClass, $this->owner->ID)
     703         )->value();
     704     }
     705
     706     /**
     707      * Removes a record from the translation group lookup table.
     708      * Makes no assumptions on other records in the group - meaning
     709      * if this happens to be the last record assigned to the group,
     710      * this group ceases to exist.
     711      */
     712     public function removeTranslationGroup() {
     713         $baseDataClass = ClassInfo::baseDataClass($this->owner->class);
     714         DB::query(
     715             sprintf('DELETE FROM "%s_translationgroups" WHERE "OriginalID" = %d', $baseDataClass, $this->owner->ID)
     716         );
     717     }
     718
     719     /**
     720      * Determine if a table needs Versioned support
     721      * This is called at db/build time
     722      *
     723      * @param string $table Table name
     724      * @return boolean
     725      */
     726     function isVersionedTable($table) {
     727         return false;
     728     }
     729
     730     /**
     731      * Note: The bulk of logic is in ModelAsController->getNestedController()
     732      * and ContentController->handleRequest()
     733      */
     734     function contentcontrollerInit($controller) {
     735         $controller->Locale = Translatable::choose_site_locale();
     736         i18n::set_locale($controller->Locale);
     737     }
     738
     739     function modelascontrollerInit($controller) {
     740         //$this->contentcontrollerInit($controller);
     741     }
     742
     743     function initgetEditForm($controller) {
     744         $this->contentcontrollerInit($controller);
     745     }
     746
     747     /**
     748      * Recursively creates translations for parent pages in this language
     749      * if they aren't existing already. This is a necessity to make
     750      * nested pages accessible in a translated CMS page tree.
     751      * It would be more userfriendly to grey out untranslated pages,
     752      * but this involves complicated special cases in AllChildrenIncludingDeleted().
     753      *
     754      * {@link SiteTree->onBeforeWrite()} will ensure that each translation will get
     755      * a unique URL across languages, by means of {@link SiteTree::get_by_link()}
     756      * and {@link Translatable->alternateGetByURL()}.
     757      */
     758     function onBeforeWrite() {
     759         // If language is not set explicitly, set it to current_locale.
     760         // This might be a bit overzealous in assuming the language
     761         // of the content, as a "single language" website might be expanded
     762         // later on. See {@link requireDefaultRecords()} for batch setting
     763         // of empty Locale columns on each dev/build call.
     764         if(!$this->owner->Locale) {
     765             $this->owner->Locale = Translatable::get_current_locale();
     766         }
     767
     768         // Specific logic for SiteTree subclasses.
     769         // If page has untranslated parents, create (unpublished) translations
     770         // of those as well to avoid having inaccessible children in the sitetree.
     771         // Caution: This logic is very sensitve to infinite loops when translation status isn't determined properly
     772         // If a parent for the newly written translation was existing before this
     773         // onBeforeWrite() call, it will already have been linked correctly through createTranslation()
     774         if($this->owner->hasField('ParentID')) {
     775             if(
     776                 !$this->owner->ID
     777                 && $this->owner->ParentID
     778                 && !$this->owner->Parent()->hasTranslation($this->owner->Locale)
     779             ) {
     780                 $parentTranslation = $this->owner->Parent()->createTranslation($this->owner->Locale);
     781                 $this->owner->ParentID = $parentTranslation->ID;
     782             }
     783         }
     784
     785         // Has to be limited to the default locale, the assumption is that the "page type"
     786         // dropdown is readonly on all translations.
     787         if($this->owner->ID && $this->owner->Locale == Translatable::default_locale()) {
     788             $changedFields = $this->owner->getChangedFields();
     789             if(isset($changedFields['ClassName'])) {
     790                 $this->owner->ClassName = $changedFields['ClassName']['before'];
     791                 $translations = $this->owner->getTranslations();
     792                 $this->owner->ClassName = $changedFields['ClassName']['after'];
     793                 if($translations) foreach($translations as $translation) {
     794                     $translation->setClassName($this->owner->ClassName);
     795                     $translation = $translation->newClassInstance($translation->ClassName);
     796                     $translation->populateDefaults();
     797                     $translation->forceChange();
     798                     $translation->write();
     799                 }
     800             }
     801         }
     802
     803         // see onAfterWrite()
     804         if(!$this->owner->ID) {
     805             $this->owner->_TranslatableIsNewRecord = true;
     806         }
     807     }
     808
     809     function onAfterWrite() {
     810         // hacky way to determine if the record was created in the database,
     811         // or just updated
     812         if($this->owner->_TranslatableIsNewRecord) {
     813             // this would kick in for all new records which are NOT
     814             // created through createTranslation(), meaning they don't
     815             // have the translation group automatically set.
     816             $translationGroupID = $this->getTranslationGroup();
     817             if(!$translationGroupID) $this->addTranslationGroup($this->owner->_TranslationGroupID ? $this->owner->_TranslationGroupID : $this->owner->ID);
     818             unset($this->owner->_TranslatableIsNewRecord);
     819             unset($this->owner->_TranslationGroupID);
     820         }
     821
     822     }
     823
     824     /**
     825      * Remove the record from the translation group mapping.
     826      */
     827     function onBeforeDelete() {
     828         // @todo Coupling to Versioned, we need to avoid removing
     829         // translation groups if records are just deleted from a stage
     830         // (="unpublished"). Ideally the translation group tables would
     831         // be specific to different Versioned changes, making this restriction unnecessary.
     832         // This will produce orphaned translation group records for SiteTree subclasses.
     833         if(!$this->owner->hasExtension('Versioned')) {
     834             $this->removeTranslationGroup();
     835         }
     836
     837         parent::onBeforeDelete();
     838     }
     839
     840     /**
     841      * Attempt to get the page for a link in the default language that has been translated.
     842      *
     843      * @param string $URLSegment
     844      * @param int|null $parentID
     845      * @return SiteTree
     846      */
     847     public function alternateGetByLink($URLSegment, $parentID) {
     848         // If the parentID value has come from a translated page, then we need to find the corresponding parentID value
     849         // in the default Locale.
     850         if (
     851             is_int($parentID)
     852             && $parentID > 0
     853             && ($parent = DataObject::get_by_id('SiteTree', $parentID))
     854             && ($parent->isTranslation())
     855         ) {
     856             $parentID = $parent->getTranslationGroup();
     857         }
     858
     859         // Find the locale language-independent of the page
     860         self::disable_locale_filter();
     861         $default = DataObject::get_one (
     862             'SiteTree',
     863             sprintf (
     864                 '"URLSegment" = \'%s\'%s',
     865                 Convert::raw2sql($URLSegment),
     866                 (is_int($parentID) ? " AND \"ParentID\" = $parentID" : null)
     867             ),
     868             false
     869         );
     870         self::enable_locale_filter();
     871
     872         return $default;
     873     }
     874
     875     //-----------------------------------------------------------------------------------------------//
     876
     877     /**
     878      * If the record is not shown in the default language, this method
     879      * will try to autoselect a master language which is shown alongside
     880      * the normal formfields as a readonly representation.
     881      * This gives translators a powerful tool for their translation workflow
     882      * without leaving the translated page interface.
     883      * Translatable also adds a new tab "Translation" which shows existing
     884      * translations, as well as a formaction to create new translations based
     885      * on a dropdown with available languages.
     886      *
     887      * @todo This is specific to SiteTree and CMSMain
     888      * @todo Implement a special "translation mode" which triggers display of the
     889      * readonly fields, so you can translation INTO the "default language" while
     890      * seeing readonly fields as well.
     891      */
     892     function updateCMSFields(FieldSet &$fields) {
     893         // Don't apply these modifications for normal DataObjects - they rely on CMSMain logic
     894         if(!($this->owner instanceof SiteTree)) return;
     895
     896         // used in CMSMain->init() to set language state when reading/writing record
     897         $fields->push(new HiddenField("Locale", "Locale", $this->owner->Locale) );
     898
     899         // Don't allow translation of virtual pages because of data inconsistencies (see #5000)
     900         $excludedPageTypes = array('VirtualPage');
     901         foreach($excludedPageTypes as $excludedPageType) {
     902             if(is_a($this->owner, $excludedPageType)) return;
     903         }
     904
     905         $excludeFields = array(
     906             'ViewerGroups',
     907             'EditorGroups',
     908             'CanViewType',
     909             'CanEditType'
     910         );
     911
     912         // if a language other than default language is used, we're in "translation mode",
     913         // hence have to modify the original fields
     914         $creating = false;
     915         $baseClass = $this->owner->class;
     916         $allFields = $fields->toArray();
     917         while( ($p = get_parent_class($baseClass)) != "DataObject") $baseClass = $p;
     918
     919         // try to get the record in "default language"
     920         $originalRecord = $this->owner->getTranslation(Translatable::default_locale());
     921         // if no translation in "default language", fall back to first translation
     922         if(!$originalRecord) {
     923             $translations = $this->owner->getTranslations();
     924             $originalRecord = ($translations) ? $translations->First() : null;
     925         }
     926
     927         $isTranslationMode = $this->owner->Locale != Translatable::default_locale();
     928
     929         // Show a dropdown to create a new translation.
     930         // This action is possible both when showing the "default language"
     931         // and a translation. Include the current locale (record might not be saved yet).
     932         $alreadyTranslatedLocales = $this->getTranslatedLocales();
     933         $alreadyTranslatedLocales[$this->owner->Locale] = $this->owner->Locale;
     934
     935         if($originalRecord && $isTranslationMode) {
     936             $originalLangID = Session::get($this->owner->ID . '_originalLangID');
     937
     938             // Remove parent page dropdown
     939             $fields->removeByName("ParentType");
     940             $fields->removeByName("ParentID");
     941
     942             $translatableFieldNames = $this->getTranslatableFields();
     943             $allDataFields = $fields->dataFields();
     944
     945             $transformation = new Translatable_Transformation($originalRecord);
     946
     947             // iterate through sequential list of all datafields in fieldset
     948             // (fields are object references, so we can replace them with the translatable CompositeField)
     949             foreach($allDataFields as $dataField) {
     950                 if($dataField instanceof HiddenField) continue;
     951                 if(in_array($dataField->Name(), $excludeFields)) continue;
     952                 if(array_key_exists($dataField->Name(), $this->owner->has_one())) continue; // ignore has_one relations
     953
     954                 if(in_array($dataField->Name(), $translatableFieldNames)) {
     955                     // if the field is translatable, perform transformation
     956                     $fields->replaceField($dataField->Name(), $transformation->transformFormField($dataField));
     957                 } else {
     958                     // else field shouldn't be editable in translation-mode, make readonly
     959                     $fields->replaceField($dataField->Name(), $dataField->performReadonlyTransformation());
     960                 }
     961             }
     962
     963         } elseif($this->owner->isNew()) {
     964             $fields->addFieldsToTab(
     965                 'Root',
     966                 new Tab(_t('Translatable.TRANSLATIONS', 'Translations'),
     967                     new LiteralField('SaveBeforeCreatingTranslationNote',
     968                         sprintf('<p class="message">%s</p>',
     969                             _t('Translatable.NOTICENEWPAGE', 'Please save this page before creating a translation')
     970                         )
     971                     )
     972                 )
     973             );
     974         }
     975
     976         $fields->addFieldsToTab(
     977             'Root',
     978             new Tab('Translations', _t('Translatable.TRANSLATIONS', 'Translations'),
     979                 new HeaderField('CreateTransHeader', _t('Translatable.CREATE', 'Create new translation'), 2),
     980                 $langDropdown = new LanguageDropdownField(
     981                     "NewTransLang",
     982                     _t('Translatable.NEWLANGUAGE', 'New language'),
     983                     $alreadyTranslatedLocales,
     984                     'SiteTree',
     985                     'Locale-English',
     986                     $this->owner
     987                 ),
     988                 $createButton = new InlineFormAction('createtranslation',_t('Translatable.CREATEBUTTON', 'Create'))
     989             )
     990         );
     991         $createButton->includeDefaultJS(false);
     992
     993         if($alreadyTranslatedLocales) {
     994             $fields->addFieldToTab(
     995                 'Root.Translations',
     996                 new HeaderField('ExistingTransHeader', _t('Translatable.EXISTING', 'Existing translations:'), 3)
     997             );
     998             $existingTransHTML = '<ul>';
     999             foreach($alreadyTranslatedLocales as $i => $langCode) {
    1000                 $existingTranslation = $this->owner->getTranslation($langCode);
    1001                 if($existingTranslation) {
    1002                     $existingTransHTML .= sprintf('<li><a href="%s">%s</a></li>',
    1003                         sprintf('admin/show/%d/?locale=%s', $existingTranslation->ID, $langCode),
    1004                         i18n::get_locale_name($langCode)
    1005                     );
    1006                 }
    1007             }
    1008             $existingTransHTML .= '</ul>';
    1009             $fields->addFieldToTab(
    1010                 'Root.Translations',
    1011                 new LiteralField('existingtrans',$existingTransHTML)
    1012             );
    1013         }
    1014
    1015         $langDropdown->addExtraClass('languageDropdown');
    1016         $createButton->addExtraClass('createTranslationButton');
    1017     }
    1018
    1019     /**
    1020      * Get the names of all translatable fields on this class
    1021      * as a numeric array.
    1022      * @todo Integrate with blacklist once branches/translatable is merged back.
    1023      *
    1024      * @return array
    1025      */
    1026     function getTranslatableFields() {
    1027         return $this->translatableFields;
    1028     }
    1029
    1030     /**
    1031      * Return the base table - the class that directly extends DataObject.
    1032      * @return string
    1033      */
    1034     function baseTable($stage = null) {
    1035         $tableClasses = ClassInfo::dataClassesFor($this->owner->class);
    1036         $baseClass = array_shift($tableClasses);
    1037         return (!$stage || $stage == $this->defaultStage) ? $baseClass : $baseClass . "_$stage";
    1038     }
    1039
    1040     function extendWithSuffix($table) {
    1041         return $table;
    1042     }
    1043
    1044     /**
    1045      * Gets all related translations for the current object,
    1046      * excluding itself. See {@link getTranslation()} to retrieve
    1047      * a single translated object.
    1048      *
    1049      * Getter with $stage parameter is specific to {@link Versioned} extension,
    1050      * mostly used for {@link SiteTree} subclasses.
    1051      *
    1052      * @param string $locale
    1053      * @param string $stage
    1054      * @return DataObjectSet
    1055      */
    1056     function getTranslations($locale = null, $stage = null) {
    1057         if($this->owner->exists()) {
    1058             // HACK need to disable language filtering in augmentSQL(),
    1059             // as we purposely want to get different language
    1060             self::disable_locale_filter();
    1061
    1062             $translationGroupID = $this->getTranslationGroup();
    1063
    1064             $baseDataClass = ClassInfo::baseDataClass($this->owner->class);
    1065             $filter = sprintf('"%s_translationgroups"."TranslationGroupID" = %d', $baseDataClass, $translationGroupID);
    1066             if($locale) {
    1067                 $filter .= sprintf(' AND "%s"."Locale" = \'%s\'', $baseDataClass, Convert::raw2sql($locale));
    1068             } else {
    1069                 // exclude the language of the current owner
    1070                 $filter .= sprintf(' AND "%s"."Locale" != \'%s\'', $baseDataClass, $this->owner->Locale);
    1071             }
    1072             $join = sprintf('LEFT JOIN "%s_translationgroups" ON "%s_translationgroups"."OriginalID" = "%s"."ID"',
    1073                 $baseDataClass,
    1074                 $baseDataClass,
    1075                 $baseDataClass
    1076             );
    1077             $currentStage = Versioned::current_stage();
    1078             if($this->owner->hasExtension("Versioned")) {
    1079                 if($stage) Versioned::reading_stage($stage);
    1080                 $translations = Versioned::get_by_stage(
    1081                     $this->owner->class,
    1082                     Versioned::current_stage(),
    1083                     $filter,
    1084                     null,
    1085                     $join
    1086                 );
    1087                 if($stage) Versioned::reading_stage($currentStage);
    1088             } else {
    1089                 $translations = DataObject::get($this->owner->class, $filter, null, $join);
    1090             }
    1091
    1092             self::enable_locale_filter();
    1093
    1094             return $translations;
    1095         }
    1096     }
    1097
    1098     /**
    1099      * Gets an existing translation based on the language code.
    1100      * Use {@link hasTranslation()} as a quicker alternative to check
    1101      * for an existing translation without getting the actual object.
    1102      *
    1103      * @param String $locale
    1104      * @return DataObject Translated object
    1105      */
    1106     function getTranslation($locale, $stage = null) {
    1107         $translations = $this->getTranslations($locale, $stage);
    1108         return ($translations) ? $translations->First() : null;
    1109     }
    1110
    1111     /**
    1112      * Creates a new translation for the owner object of this decorator.
    1113      * Checks {@link getTranslation()} to return an existing translation
    1114      * instead of creating a duplicate. Writes the record to the database before
    1115      * returning it. Use this method if you want the "translation group"
    1116      * mechanism to work, meaning that an object knows which group of translations
    1117      * it belongs to. For "original records" which are not created through this
    1118      * method, the "translation group" is set in {@link onAfterWrite()}.
    1119      *
    1120      * @param string $locale
    1121      * @return DataObject The translated object
    1122      */
    1123     function createTranslation($locale) {
    1124         if(!$this->owner->exists()) {
    1125             user_error('Translatable::createTranslation(): Please save your record before creating a translation', E_USER_ERROR);
    1126         }
    1127
    1128         // permission check
    1129         if(!$this->owner->canTranslate(null, $locale)) {
    1130             throw new Exception(sprintf(
    1131                 'Creating a new translation in locale "%s" is not allowed for this user',
    1132                 $locale
    1133             ));
    1134             return;
    1135         }
    1136
    1137         $existingTranslation = $this->getTranslation($locale);
    1138         if($existingTranslation) return $existingTranslation;
    1139
    1140         $class = $this->owner->class;
    1141         $newTranslation = new $class;
    1142
    1143         // copy all fields from owner (apart from ID)
    1144         $newTranslation->update($this->owner->toMap());
    1145
    1146         // If the object has Hierarchy extension,
    1147         // check for existing translated parents and assign
    1148         // their ParentID (and overwrite any existing ParentID relations
    1149         // to parents in other language). If no parent translations exist,
    1150         // they are automatically created in onBeforeWrite()
    1151         if($newTranslation->hasField('ParentID')) {
    1152             $origParent = $this->owner->Parent();
    1153             $newTranslationParent = $origParent->getTranslation($locale);
    1154             if($newTranslationParent) $newTranslation->ParentID = $newTranslationParent->ID;
    1155         }
    1156
    1157         $newTranslation->ID = 0;
    1158         $newTranslation->Locale = $locale;
    1159
    1160         $originalPage = $this->getTranslation(self::default_locale());
    1161         if ($originalPage) {
    1162             $urlSegment = $originalPage->URLSegment;
    1163         } else {
    1164             $urlSegment = $newTranslation->URLSegment;
    1165         }
    1166         $newTranslation->URLSegment = $urlSegment . '-' . i18n::convert_rfc1766($locale);
    1167         // hacky way to set an existing translation group in onAfterWrite()
    1168         $translationGroupID = $this->getTranslationGroup();
    1169         $newTranslation->_TranslationGroupID = $translationGroupID ? $translationGroupID : $this->owner->ID;
    1170         $newTranslation->write();
    1171
    1172         return $newTranslation;
    1173     }
    1174
    1175     /**
    1176      * Caution: Does not consider the {@link canEdit()} permissions.
    1177      *
    1178      * @param DataObject|int $member
    1179      * @param string $locale
    1180      * @return boolean
    1181      */
    1182     function canTranslate($member = null, $locale) {
    1183         if (Director::is_cli()) return true;
    1184
    1185         if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) $member = Member::currentUser();
    1186
    1187         // check for locale
    1188         $allowedLocale = (
    1189             !is_array(self::get_allowed_locales())
    1190             || in_array($locale, self::get_allowed_locales())
    1191         );
    1192
    1193         if(!$allowedLocale) return false;
    1194
    1195         // By default, anyone who can edit a page can edit the default locale
    1196         if($locale == self::default_locale()) return true;
    1197
    1198         // check for generic translation permission
    1199         if(Permission::checkMember($member, 'TRANSLATE_ALL')) return true;
    1200
    1201         // check for locale specific translate permission
    1202         if(!Permission::checkMember($member, 'TRANSLATE_' . $locale)) return false;
    1203
    1204         return true;
    1205     }
    1206
    1207     /**
    1208      * @return boolean
    1209      */
    1210     function canEdit($member) {
    1211         if(!$this->owner->Locale) return true;
    1212         return $this->owner->canTranslate($member, $this->owner->Locale);
    1213     }
    1214
    1215     /**
    1216      * Returns TRUE if the current record has a translation in this language.
    1217      * Use {@link getTranslation()} to get the actual translated record from
    1218      * the database.
    1219      *
    1220      * @param string $locale
    1221      * @return boolean
    1222      */
    1223     function hasTranslation($locale) {
    1224         return (
    1225             $this->owner->Locale == $locale
    1226             || array_search($locale, $this->getTranslatedLocales()) !== false
    1227         );
    1228     }
    1229
    1230     function AllChildrenIncludingDeleted($context = null) {
    1231         $children = $this->owner->doAllChildrenIncludingDeleted($context);
    1232
    1233         return $children;
    1234     }
    1235
    1236     /**
    1237      * Returns <link rel="alternate"> markup for insertion into
    1238      * a HTML4/XHTML compliant <head> section, listing all available translations
    1239      * of a page.
    1240      *
    1241      * @see http://www.w3.org/TR/html4/struct/links.html#edef-LINK
    1242      * @see http://www.w3.org/International/articles/language-tags/
    1243      *
    1244      * @return string HTML
    1245      */
    1246     function MetaTags(&$tags) {
    1247         $template = '<link rel="alternate" type="text/html" title="%s" hreflang="%s" href="%s" />' . "\n";
    1248         $translations = $this->owner->getTranslations();
    1249         if($translations) foreach($translations as $translation) {
    1250             $tags .= sprintf($template,
    1251                 $translation->Title,
    1252                 i18n::convert_rfc1766($translation->Locale),
    1253                 $translation->Link()
    1254             );
    1255         }
    1256     }
    1257
    1258     function providePermissions() {
    1259         if(!Object::has_extension('SiteTree', 'Translatable')) return false;
    1260
    1261         $locales = self::get_allowed_locales();
    1262
    1263         // Fall back to any locales used in existing translations (see #4939)
    1264         if(!$locales) {
    1265             $locales = DB::query('SELECT "Locale" FROM "SiteTree" GROUP BY "Locale"')->column();
    1266         }
    1267
    1268         $permissions = array();
    1269         if($locales) foreach($locales as $locale) {
    1270             $localeName = i18n::get_locale_name($locale);
    1271             $permissions['TRANSLATE_' . $locale] = sprintf(
    1272                 _t(
    1273                     'Translatable.TRANSLATEPERMISSION',
    1274                     'Translate %s',
    1275                     PR_MEDIUM,
    1276                     'Translate pages into a language'
    1277                 ),
    1278                 $localeName
    1279             );
    1280         }
    1281
    1282         $permissions['TRANSLATE_ALL'] = _t(
    1283             'Translatable.TRANSLATEALLPERMISSION',
    1284             'Translate into all available languages'
    1285         );
    1286
    1287         return $permissions;
    1288     }
    1289
    1290     /**
    1291      * Get a list of languages with at least one element translated in (including the default language)
    1292      *
    1293      * @param string $className Look for languages in elements of this class
    1294      * @return array Map of languages in the form locale => langName
    1295      */
    1296     static function get_existing_content_languages($className = 'SiteTree', $where = '') {
    1297         $baseTable = ClassInfo::baseDataClass($className);
    1298         //We don't quote $where if it is empty:
    1299         if($where!='')
    1300             $where="\"$where\"";
    1301         $query = new SQLQuery("Distinct \"Locale\"","\"$baseTable\"",$where, '', "\"Locale\"");
    1302         $dbLangs = $query->execute()->column();
    1303         $langlist = array_merge((array)Translatable::default_locale(), (array)$dbLangs);
    1304         $returnMap = array();
    1305         $allCodes = array_merge(i18n::$all_locales, i18n::$common_locales);
    1306         foreach ($langlist as $langCode) {
    1307             if($langCode && isset($allCodes[$langCode])) {
    1308                 $returnMap[$langCode] = (is_array($allCodes[$langCode])) ? $allCodes[$langCode][0] : $allCodes[$langCode];
    1309             }
    1310         }
    1311         return $returnMap;
    1312     }
    1313
    1314     /**
    1315      * Get the RelativeLink value for a home page in another locale. This is found by searching for the default home
    1316      * page in the default language, then returning the link to the translated version (if one exists).
    1317      *
    1318      * @return string
    1319      */
    1320     public static function get_homepage_link_by_locale($locale) {
    1321         $originalLocale = self::get_current_locale();
    1322
    1323         self::set_current_locale(self::default_locale());
    1324         $original = SiteTree::get_by_link(RootURLController::get_default_homepage_link());
    1325         self::set_current_locale($originalLocale);
    1326
    1327         if($original) {
    1328             if($translation = $original->getTranslation($locale)) return trim($translation->RelativeLink(true), '/');
    1329         }
    1330     }
    1331
    1332     /**
    1333      * @deprecated 2.4 Use {@link Translatable::get_homepage_link_by_locale()}
    1334      */
    1335     static function get_homepage_urlsegment_by_locale($locale) {
    1336         user_error (
    1337             'Translatable::get_homepage_urlsegment_by_locale() is deprecated, please use get_homepage_link_by_locale()',
    1338             E_USER_NOTICE
    1339         );
    1340
    1341         return self::get_homepage_link_by_locale($locale);
    1342     }
    1343
    1344     /**
    1345      * Define all locales which in which a new translation is allowed.
    1346      * Checked in {@link canTranslate()}.
    1347      *
    1348      * @param array List of allowed locale codes (see {@link i18n::$all_locales}).
    1349      *  Example: array('de_DE','ja_JP')
    1350      */
    1351     static function set_allowed_locales($locales) {
    1352         self::$allowed_locales = $locales;
    1353     }
    1354
    1355     /**
    1356      * Get all locales which are generally permitted to be translated.
    1357      * Use {@link canTranslate()} to check if a specific member has permission
    1358      * to translate a record.
    1359      *
    1360      * @return array
    1361      */
    1362     static function get_allowed_locales() {
    1363         return self::$allowed_locales;
    1364     }
    1365
    1366     /**
    1367      * @deprecated 2.4 Use get_homepage_urlsegment_by_locale()
    1368      */
    1369     static function get_homepage_urlsegment_by_language($locale) {
    1370         return self::get_homepage_urlsegment_by_locale($locale);
    1371     }
    1372
    1373     /**
    1374      * @deprecated 2.4 Use custom check: self::$default_locale == self::get_current_locale()
    1375      */
    1376     static function is_default_lang() {
    1377         return (self::$default_locale == self::get_current_locale());
    1378     }
    1379
    1380     /**
    1381      * @deprecated 2.4 Use set_default_locale()
    1382      */
    1383     static function set_default_lang($lang) {
    1384         self::set_default_locale(i18n::get_locale_from_lang($lang));
    1385     }
    1386
    1387     /**
    1388      * @deprecated 2.4 Use get_default_locale()
    1389      */
    1390     static function get_default_lang() {
    1391         return i18n::get_lang_from_locale(self::default_locale());
    1392     }
    1393
    1394     /**
    1395      * @deprecated 2.4 Use get_current_locale()
    1396      */
    1397     static function current_lang() {
    1398         return i18n::get_lang_from_locale(self::get_current_locale());
    1399     }
    1400
    1401     /**
    1402      * @deprecated 2.4 Use set_current_locale()
    1403      */
    1404     static function set_reading_lang($lang) {
    1405         self::set_current_locale(i18n::get_locale_from_lang($lang));
    1406     }
    1407
    1408     /**
    1409      * @deprecated 2.4 Use get_reading_locale()
    1410      */
    1411     static function get_reading_lang() {
    1412         return i18n::get_lang_from_locale(self::get_reading_locale());
    1413     }
    1414
    1415     /**
    1416      * @deprecated 2.4 Use default_locale()
    1417      */
    1418     static function default_lang() {
    1419         return i18n::get_lang_from_locale(self::default_locale());
    1420     }
    1421
    1422     /**
    1423      * @deprecated 2.4 Use get_by_locale()
    1424      */
    1425     static function get_by_lang($class, $lang, $filter = '', $sort = '', $join = "", $limit = "", $containerClass = "DataObjectSet", $having = "") {
    1426         return self::get_by_locale($class, i18n::get_locale_from_lang($lang), $filter, $sort, $join, $limit, $containerClass, $having);
    1427     }
    1428
    1429     /**
    1430      * @deprecated 2.4 Use get_one_by_locale()
    1431      */
    1432     static function get_one_by_lang($class, $lang, $filter = '', $cache = false, $orderby = "") {
    1433         return self::get_one_by_locale($class, i18n::get_locale_from_lang($lang), $filter, $cache, $orderby);
    1434     }
    1435
    1436     /**
    1437      * Determines if the record has a locale,
    1438      * and if this locale is different from the "default locale"
    1439      * set in {@link Translatable::default_locale()}.
    1440      * Does not look at translation groups to see if the record
    1441      * is based on another record.
    1442      *
    1443      * @return boolean
    1444      * @deprecated 2.4
    1445      */
    1446     function isTranslation() {
    1447         return ($this->owner->Locale && ($this->owner->Locale != Translatable::default_locale()));
    1448     }
    1449
    1450     /**
    1451      * @deprecated 2.4 Use choose_site_locale()
    1452      */
    1453     static function choose_site_lang($langsAvail=null) {
    1454         return self::choose_site_locale($langsAvail);
    1455     }
    1456
    1457     /**
    1458      * @deprecated 2.4 Use getTranslatedLocales()
    1459      */
    1460     function getTranslatedLangs() {
    1461         return $this->getTranslatedLocales();
    1462     }
    1463
    1464     /**
    1465      * Return a piece of text to keep DataObject cache keys appropriately specific
    1466      */
    1467     function cacheKeyComponent() {
    1468         return 'locale-'.self::get_current_locale();
    1469     }
    1470
    1471     /**
    1472      * Extends the SiteTree::validURLSegment() method, to do checks appropriate
    1473      * to Translatable
    1474      *
    1475      * @return bool
    1476      */
    1477     public function augmentValidURLSegment() {
    1478         if (self::locale_filter_enabled()) {
    1479             self::disable_locale_filter();
    1480             $reEnableFilter = true;
    1481         }
    1482         $IDFilter     = ($this->owner->ID) ? "AND \"SiteTree\".\"ID\" <> {$this->owner->ID}" :  null;
    1483         $parentFilter = null;
    1484
    1485         if(SiteTree::nested_urls()) {
    1486             if($this->owner->ParentID) {
    1487                 $parentFilter = " AND \"SiteTree\".\"ParentID\" = {$this->owner->ParentID}";
    1488             } else {
    1489                 $parentFilter = ' AND "SiteTree"."ParentID" = 0';
    1490             }
    1491         }
    1492
    1493         $existingPage = DataObject::get_one(
    1494             'SiteTree',
    1495             "\"URLSegment\" = '{$this->owner->URLSegment}' $IDFilter $parentFilter",
    1496             false // disable get_one cache, as this otherwise may pick up results from when locale_filter was on
    1497         );
    1498         if ($reEnableFilter) {
    1499             self::enable_locale_filter();
    1500         }
    1501         return !$existingPage;
    1502     }
    1503
    1504 }
    1505
    1506 /**
    1507  * Transform a formfield to a "translatable" representation,
    1508  * consisting of the original formfield plus a readonly-version
    1509  * of the original value, wrapped in a CompositeField.
    1510  *
    1511  * @param DataObject $original Needs the original record as we populate the readonly formfield with the original value
    1512  *
    1513  * @package sapphire
    1514  * @subpackage misc
    1515  */
    1516 class Translatable_Transformation extends FormTransformation {
    1517
    1518     /**
    1519      * @var DataObject
    1520      */
    1521     private $original = null;
    1522
    1523     function __construct(DataObject $original) {
    1524         $this->original = $original;
    1525         parent::__construct();
    1526     }
    1527
    1528     /**
    1529      * Returns the original DataObject attached to the Transformation
    1530      *
    1531      * @return DataObject
    1532      */
    1533     function getOriginal() {
    1534         return $this->original;
    1535     }
    1536
    1537     /**
    1538      * @todo transformTextareaField() not used at the moment
    1539      */
    1540     function transformTextareaField(TextareaField $field) {
    1541         $nonEditableField = new ToggleField($fieldname,$field->Title(),'','+','-');
    1542         $nonEditableField->labelMore = '+';
    1543         $nonEditableField->labelLess = '-';
    1544         return $this->baseTransform($nonEditableField, $field);
    1545
    1546         return $nonEditableField;
    1547     }
    1548
    1549     function transformFormField(FormField $field) {
    1550         $newfield = $field->performReadOnlyTransformation();
    1551         return $this->baseTransform($newfield, $field);
    1552     }
    1553
    1554     protected function baseTransform($nonEditableField, $originalField) {
    1555         $fieldname = $originalField->Name();
    1556
    1557         $nonEditableField_holder = new CompositeField($nonEditableField);
    1558         $nonEditableField_holder->setName($fieldname.'_holder');
    1559         $nonEditableField_holder->addExtraClass('originallang_holder');
    1560         $nonEditableField->setValue($this->original->$fieldname);
    1561         $nonEditableField->setName($fieldname.'_original');
    1562         $nonEditableField->addExtraClass('originallang');
    1563         $nonEditableField->setTitle('Original '.$originalField->Title());
    1564
    1565         $nonEditableField_holder->insertBefore($originalField, $fieldname.'_original');
    1566         return $nonEditableField_holder;
    1567     }
    1568
    1569
    1570 }
    1571
    1572 ?>
    1573

[Raise a SilverStripe Framework issue/bug](https://github.com/silverstripe/silverstripe-framework/issues/new)
- [Raise a SilverStripe CMS issue/bug](https://github.com/silverstripe/silverstripe-cms/issues/new)
- Please use the Silverstripe Forums to ask development related questions. -