diff --git a/core/model/Translatable.php b/core/model/Translatable.php
index 5fc2d302f..005631004 100755
--- a/core/model/Translatable.php
+++ b/core/model/Translatable.php
@@ -193,6 +193,15 @@ class Translatable extends DataObjectDecorator {
*/
protected static $enable_lang_filter = true;
+ /**
+ * @var array All locales in which a translation can be created.
+ * This limits the choice in the CMS language dropdown in the
+ * "Translation" tab, as well as the language dropdown above
+ * the CMS tree. If not set, it will default to showing all
+ * common locales.
+ */
+ protected static $allowed_locales = null;
+
/**
* Choose the language the site is currently on.
* If $_GET['locale'] is set, then it will use that language, and store it in the session.
@@ -311,9 +320,9 @@ class Translatable extends DataObjectDecorator {
* Gets all translations for this specific page.
* Doesn't include the language of the current record.
*
- * @return array Numeric array of all language codes, sorted alphabetically.
+ * @return array Numeric array of all locales, sorted alphabetically.
*/
- function getTranslatedLangs() {
+ function getTranslatedLocales() {
$langs = array();
$baseDataClass = ClassInfo::baseDataClass($this->owner->class); //Base Class
@@ -363,7 +372,7 @@ class Translatable extends DataObjectDecorator {
*/
static function get_langs_by_id($class, $id) {
$do = DataObject::get_by_id($class, $id);
- return ($do ? $do->getTranslatedLangs() : array());
+ return ($do ? $do->getTranslatedLocales() : array());
}
/**
@@ -774,6 +783,12 @@ class Translatable extends DataObjectDecorator {
}
$isTranslationMode = $this->owner->Locale != Translatable::default_locale();
+
+ // Show a dropdown to create a new translation.
+ // This action is possible both when showing the "default language"
+ // and a translation. Include the current locale (record might not be saved yet).
+ $alreadyTranslatedLocales = $this->getTranslatedLocales();
+ $alreadyTranslatedLocales[$this->owner->Locale] = $this->owner->Locale;
if($originalRecord && $isTranslationMode) {
$originalLangID = Session::get($this->owner->ID . '_originalLangID');
@@ -813,12 +828,6 @@ class Translatable extends DataObjectDecorator {
);
}
- // Show a dropdown to create a new translation.
- // This action is possible both when showing the "default language"
- // and a translation. Include the current locale (record might not be saved yet).
- $alreadyTranslatedLangs = $this->getTranslatedLangs();
- $alreadyTranslatedLangs[$this->owner->Locale] = $this->owner->Locale;
-
$fields->addFieldsToTab(
'Root',
new Tab('Translations', _t('Translatable.TRANSLATIONS', 'Translations'),
@@ -826,22 +835,22 @@ class Translatable extends DataObjectDecorator {
$langDropdown = new LanguageDropdownField(
"NewTransLang",
_t('Translatable.NEWLANGUAGE', 'New language'),
- $alreadyTranslatedLangs,
- 'SiteTree',
- 'Locale-English'
+ $alreadyTranslatedLocales,
+ 'SiteTree',
+ $this->owner
),
$createButton = new InlineFormAction('createtranslation',_t('Translatable.CREATEBUTTON', 'Create'))
)
);
$createButton->includeDefaultJS(false);
- if($alreadyTranslatedLangs) {
+ if($alreadyTranslatedLocales) {
$fields->addFieldToTab(
'Root.Translations',
new HeaderField('ExistingTransHeader', _t('Translatable.EXISTING', 'Existing translations:'), 3)
);
$existingTransHTML = '
';
- foreach($alreadyTranslatedLangs as $i => $langCode) {
+ foreach($alreadyTranslatedLocales as $i => $langCode) {
$existingTranslation = $this->owner->getTranslation($langCode);
if($existingTranslation) {
$existingTransHTML .= sprintf('- %s
',
@@ -856,8 +865,7 @@ class Translatable extends DataObjectDecorator {
new LiteralField('existingtrans',$existingTransHTML)
);
}
-
-
+
$langDropdown->addExtraClass('languageDropdown');
$createButton->addExtraClass('createTranslationButton');
}
@@ -966,6 +974,15 @@ class Translatable extends DataObjectDecorator {
user_error('Translatable::createTranslation(): Please save your record before creating a translation', E_USER_ERROR);
}
+ // permission check
+ if(!$this->owner->canTranslate(null, $locale)) {
+ throw new Exception(sprintf(
+ 'Creating a new translation in locale "%s" is not allowed for this user',
+ $locale
+ ));
+ return;
+ }
+
$existingTranslation = $this->getTranslation($locale);
if($existingTranslation) return $existingTranslation;
@@ -996,6 +1013,31 @@ class Translatable extends DataObjectDecorator {
return $newTranslation;
}
+ /**
+ * Caution: Does not consider the {@link canEdit()} permissions.
+ *
+ * @param DataObject|int $member
+ * @param string $locale
+ * @return boolean
+ */
+ function canTranslate($member = null, $locale) {
+ if(!$member || !(is_a($member, 'Member')) || is_numeric($member)) $member = Member::currentUser();
+
+ return (
+ !is_array(self::get_allowed_locales())
+ || in_array($locale, self::get_allowed_locales())
+ );
+ }
+
+ /**
+ * @return boolean
+ */
+ function canEdit($member) {
+ if(!$this->owner->Locale) return true;
+
+ return $this->owner->canTranslate($member, $this->owner->Locale);
+ }
+
/**
* Returns TRUE if the current record has a translation in this language.
* Use {@link getTranslation()} to get the actual translated record from
@@ -1005,7 +1047,7 @@ class Translatable extends DataObjectDecorator {
* @return boolean
*/
function hasTranslation($locale) {
- return (array_search($locale, $this->getTranslatedLangs()) !== false);
+ return (array_search($locale, $this->getTranslatedLocales()) !== false);
}
function AllChildrenIncludingDeleted($context = null) {
@@ -1082,6 +1124,28 @@ class Translatable extends DataObjectDecorator {
return null;
}
+ /**
+ * Define all locales which in which a new translation is allowed.
+ * Checked in {@link canTranslate()}.
+ *
+ * @param array List of allowed locale codes (see {@link i18n::$all_locales}).
+ * Example: array('de_DE','ja_JP')
+ */
+ static function set_allowed_locales($locales) {
+ self::$allowed_locales = $locales;
+ }
+
+ /**
+ * Get all locales which are generally permitted to be translated.
+ * Use {@link canTranslate()} to check if a specific member has permission
+ * to translate a record.
+ *
+ * @return array
+ */
+ static function get_allowed_locales() {
+ return self::$allowed_locales;
+ }
+
/**
* @deprecated 2.4 Use get_homepage_urlsegment_by_locale()
*/
@@ -1172,6 +1236,13 @@ class Translatable extends DataObjectDecorator {
static function choose_site_lang($langsAvail=null) {
return self::choose_site_locale($langAvail);
}
+
+ /**
+ * @deprecated 2.4 Use getTranslatedLocales()
+ */
+ function getTranslatedLangs() {
+ return $this->getTranslatedLocales();
+ }
}
diff --git a/forms/LanguageDropdownField.php b/forms/LanguageDropdownField.php
index a6cea42bb..c9d680b8a 100755
--- a/forms/LanguageDropdownField.php
+++ b/forms/LanguageDropdownField.php
@@ -11,56 +11,50 @@ class LanguageDropdownField extends GroupedDropdownField {
* Create a new LanguageDropdownField
* @param string $name
* @param string $title
- * @param array $dontInclude list of languages that won't be included
+ * @param array $excludeLocales List of locales that won't be included
* @param string $translatingClass Name of the class with translated instances where to look for used languages
* @param string $list Indicates the source language list. Can be either Common-English, Common-Native, Locale-English, Locale-Native
*/
- function __construct($name, $title, $dontInclude = array(), $translatingClass = 'SiteTree', $list = 'Common-English' ) {
- $usedlangs = array_diff(
- Translatable::get_existing_content_languages($translatingClass),
- $dontInclude
- );
+ function __construct($name, $title, $excludeLocales = array(), $translatingClass = 'SiteTree', $list = 'Common-English', $instance = null) {
+ $usedLocalesWithTitle = Translatable::get_existing_content_languages($translatingClass);
+ $usedLocalesWithTitle = array_diff_key($usedLocalesWithTitle, $excludeLocales);
- // we accept in dontInclude both language codes and names, so another diff is required
- $usedlangs = array_diff_key(
- $usedlangs,
- array_flip($dontInclude)
- );
+ if('Common-English' == $list) $allLocalesWithTitle = i18n::get_common_languages();
+ else if('Common-Native' == $list) $allLocalesWithTitle = i18n::get_common_languages(true);
+ else if('Locale-English' == $list) $allLocalesWithTitle = i18n::get_common_locales();
+ else if('Locale-Native' == $list) $allLocalesWithTitle = i18n::get_common_locales(true);
+ else $allLocalesWithTitle = i18n::get_locale_list();
- //if (isset($usedlangs[Translatable::default_locale()])) unset($usedlangs[Translatable::default_locale()]);
-
- if ('Common-English' == $list) $languageList = i18n::get_common_languages();
- else if ('Common-Native' == $list) $languageList = i18n::get_common_languages(true);
- else if ('Locale-English' == $list) $languageList = i18n::get_common_locales();
- else if ('Locale-Native' == $list) $languageList = i18n::get_common_locales(true);
- else $languageList = i18n::get_locale_list();
+ if(isset($allLocales[Translatable::default_locale()])) unset($allLocales[Translatable::default_locale()]);
+
+ // Limit to allowed locales if defined
+ // Check for canTranslate() if an $instance is given
+ $allowedLocales = Translatable::get_allowed_locales();
+ foreach($allLocalesWithTitle as $locale => $localeTitle) {
+ if(
+ ($allowedLocales && !in_array($locale, $allowedLocales))
+ || ($excludeLocales && in_array($locale, $excludeLocales))
+ || ($usedLocalesWithTitle && array_key_exists($locale, $usedLocalesWithTitle))
+ || ($instance && !$instance->canTranslate(null, $locale))
+ ) {
+ unset($allLocalesWithTitle[$locale]);
+ }
+ }
- $alllangs = array_diff(
- $languageList,
- (array)$usedlangs,
- $dontInclude
- );
- $alllangs = array_flip(array_diff(
- array_flip($alllangs),
- $dontInclude
- ));
- if (isset($alllangs[Translatable::default_locale()])) unset($alllangs[Translatable::default_locale()]);
-
- asort($alllangs);
- if (count($usedlangs)) {
- asort($usedlangs);
- $labelAvail = _t('Form.LANGAVAIL', "Available languages");
- $labelOther = _t('Form.LANGAOTHER', "Other languages");
- parent::__construct($name, $title, array(
- $labelAvail => $usedlangs,
- $labelOther => $alllangs
- ),
- reset($usedlangs)
+ // Sort by title (array value)
+ asort($allLocalesWithTitle);
+
+ if(count($usedLocalesWithTitle)) {
+ asort($usedLocalesWithTitle);
+ $source = array(
+ _t('Form.LANGAVAIL', "Available languages") => $usedLocalesWithTitle,
+ _t('Form.LANGAOTHER', "Other languages") => $allLocalesWithTitle
);
+ } else {
+ $source = $allLocalesWithTitle;
}
- else {
- parent::__construct($name, $title, $alllangs);
- }
+
+ parent::__construct($name, $title, $source);
}
}
diff --git a/tests/model/TranslatableTest.php b/tests/model/TranslatableTest.php
index de7292056..dc49cb3a0 100644
--- a/tests/model/TranslatableTest.php
+++ b/tests/model/TranslatableTest.php
@@ -62,6 +62,14 @@ class TranslatableTest extends FunctionalTest {
parent::tear_down_once();
}
+
+ function setUp() {
+ parent::setUp();
+
+ // whenever a translation is created, canTranslate() is checked
+ $cmseditor = $this->objFromFixture('Member', 'cmseditor');
+ $cmseditor->logIn();
+ }
function testTranslationGroups() {
// first in french
@@ -462,7 +470,7 @@ class TranslatableTest extends FunctionalTest {
'Subsequent calls to createTranslation() dont cause new records in database'
);
}
-
+
function testTranslatablePropertiesOnDataObject() {
$origObj = $this->objFromFixture('TranslatableTest_DataObject', 'testobject_en');
$translatedObj = $origObj->createTranslation('fr_FR');
@@ -711,6 +719,38 @@ class TranslatableTest extends FunctionalTest {
$this->assertEquals($compareLive->Title, 'Publiziert');
}
+ function testCanTranslate() {
+ $origAllowedLocales = Translatable::get_allowed_locales();
+
+ $cmseditor = $this->objFromFixture('Member', 'cmseditor');
+
+ $testPage = $this->objFromFixture('Page', 'testpage_en');
+ $this->assertTrue(
+ $testPage->canTranslate($cmseditor, 'de_DE'),
+ "Users with canEdit() permission can create a new translation if locales are not limited"
+ );
+
+ Translatable::set_allowed_locales(array('ja_JP'));
+ $this->assertTrue(
+ $testPage->canTranslate($cmseditor, 'ja_JP'),
+ "Users with canEdit() permission can create a new translation if locale is in Translatable::get_allowed_locales()"
+ );
+ $this->assertFalse(
+ $testPage->canTranslate($cmseditor, 'de_DE'),
+ "Users with canEdit() permission can't create a new translation if locale is not in Translatable::get_allowed_locales()"
+ );
+
+ $this->assertType(
+ 'Page',
+ $testPage->createTranslation('ja_JP')
+ );
+ try {
+ $testPage->createTranslation('de_DE');
+ $this->setExpectedException("Exception");
+ } catch(Exception $e) {}
+
+ Translatable::set_allowed_locales($origAllowedLocales);
+ }
}
class TranslatableTest_DataObject extends DataObject implements TestOnly {
diff --git a/tests/model/TranslatableTest.yml b/tests/model/TranslatableTest.yml
index efa160b26..51c4ad792 100644
--- a/tests/model/TranslatableTest.yml
+++ b/tests/model/TranslatableTest.yml
@@ -44,4 +44,17 @@ TranslatableTest_Page:
testpage_en:
Title: En
TranslatableProperty: en_US
- URLSegment: testpage-en
\ No newline at end of file
+ URLSegment: testpage-en
+Group:
+ cmseditorgroup:
+ Code: cmseditorgroup
+Member:
+ cmseditor:
+ FirstName: Editor
+ Groups: =>Group.cmseditorgroup
+ websiteuser:
+ FirstName: Website User
+Permission:
+ cmsmaincode:
+ Code: CMS_ACCESS_CMSMain
+ Group: =>Group.cmseditorgroup
\ No newline at end of file