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