From 05973cee55a9a80b30421c88a3c12baddb63a4f0 Mon Sep 17 00:00:00 2001 From: Damian Mooyman Date: Thu, 7 Apr 2016 12:48:40 +1200 Subject: [PATCH] API Add i18n pluralisation --- admin/code/CampaignAdmin.php | 6 ++ i18n/i18n.php | 124 +++++++++++++++++++++++---------- model/DataObject.php | 18 +++++ model/versioning/ChangeSet.php | 113 ++++++++++++++++++++++++++++++ 4 files changed, 226 insertions(+), 35 deletions(-) diff --git a/admin/code/CampaignAdmin.php b/admin/code/CampaignAdmin.php index 1f0a6855b..257919d98 100644 --- a/admin/code/CampaignAdmin.php +++ b/admin/code/CampaignAdmin.php @@ -234,6 +234,7 @@ JSON; ], 'ID' => $changeSet->ID, 'Name' => $changeSet->Name, + 'Description' => $changeSet->getDescription(), 'Created' => $changeSet->Created, 'LastEdited' => $changeSet->LastEdited, 'State' => $changeSet->State, @@ -258,6 +259,7 @@ JSON; * @return array */ protected function getChangeSetItemResource(ChangeSetItem $changeSetItem) { + $objectSingleton = DataObject::singleton($changeSetItem->ObjectClass); $hal = [ '_links' => [ 'self' => [ @@ -270,6 +272,10 @@ JSON; 'Title' => $changeSetItem->getTitle(), 'ChangeType' => $changeSetItem->getChangeType(), 'Added' => $changeSetItem->Added, + 'ObjectClass' => $changeSetItem->ObjectClass, + 'ObjectID' => $changeSetItem->ObjectID, + 'ObjectSingular' => $objectSingleton->i18n_singular_name(), + 'ObjectPlural' => $objectSingleton->i18n_plural_name(), ]; // Depending on whether the object was added implicitly or explicitly, set // other related objects. diff --git a/i18n/i18n.php b/i18n/i18n.php index dcc07a5bf..0350c79a4 100644 --- a/i18n/i18n.php +++ b/i18n/i18n.php @@ -2053,43 +2053,21 @@ class i18n extends Object implements TemplateGlobalProvider, Flushable { } } - // get current locale (either default or user preference) + // Find best translation $locale = i18n::get_locale(); - $lang = i18n::get_lang_from_locale($locale); - - // Only call getter if static isn't already defined (for performance reasons) - $translatorsByPrio = self::$translators; - if(!$translatorsByPrio) $translatorsByPrio = self::get_translators(); - - $returnValue = (is_string($string)) ? $string : ''; // Fall back to default string argument - - foreach($translatorsByPrio as $priority => $translators) { - foreach($translators as $name => $translator) { - $adapter = $translator->getAdapter(); - - // at this point, we need to ensure the language and locale are loaded - // as include_by_locale() doesn't load a fallback. - - // TODO Remove reliance on global state, by refactoring into an i18nTranslatorManager - // which is instanciated by core with a $clean instance variable. - - if(!$adapter->isAvailable($lang)) { - i18n::include_by_locale($lang); - } - - if(!$adapter->isAvailable($locale)) { - i18n::include_by_locale($locale); - } - - $translation = $adapter->translate($entity, $locale); - - // Return translation only if we found a match thats not the entity itself (Zend fallback) - if($translation && $translation != $entity) { - $returnValue = $translation; - break 2; - } - } + $returnValue = static::with_translators(function(Zend_Translate_Adapter $adapter) use ($entity, $locale) { + // Return translation only if we found a match thats not the entity itself (Zend fallback) + $translation = $adapter->translate($entity, $locale); + if($translation && $translation != $entity) { + return $translation; } + return null; + }); + + // Fall back to default string argument + if($returnValue === null) { + $returnValue = (is_string($string)) ? $string : ''; + } // inject the variables from injectionArray (if present) if($injectionArray) { @@ -2136,6 +2114,82 @@ class i18n extends Object implements TemplateGlobalProvider, Flushable { return $returnValue; } + /** + * Pluralise an item or items. + * + * @param string $singular Singular form + * @param string $plural Plural form + * @param int $number Number of items (natural number only) + * @param bool $prependNumber Include number in result + * @return string Result with the number and pluralised form appended. E.g. '1 page' + */ + public static function pluralise($singular, $plural, $number, $prependNumber = true) { + $locale = static::get_locale(); + $form = static::with_translators( + function(Zend_Translate_Adapter $adapter) use ($singular, $plural, $number, $locale) { + // Return translation only if we found a match thats not the entity itself (Zend fallback) + $result = $adapter->plural($singular, $plural, $number, $locale); + if($result) { + return $result; + } + return null; + } + ); + if($prependNumber) { + return _t('i18n.PLURAL', '{number} {form}', [ + 'number' => $number, + 'form' => $form + ]); + } else { + return $form; + } + } + + /** + * Loop over all translators in order of precedence, and return the first non-null value + * returned via $callback + * + * @param callable $callback Callback which is given the translator + * @return mixed First non-null result from $callback, or null if none matched + */ + protected static function with_translators($callback) { + // get current locale (either default or user preference) + $locale = i18n::get_locale(); + $lang = i18n::get_lang_from_locale($locale); + + // Only call getter if static isn't already defined (for performance reasons) + $translatorsByPrio = self::$translators ?: self::get_translators(); + + foreach($translatorsByPrio as $priority => $translators) { + /** @var Zend_Translate $translator */ + foreach($translators as $name => $translator) { + $adapter = $translator->getAdapter(); + + // at this point, we need to ensure the language and locale are loaded + // as include_by_locale() doesn't load a fallback. + + // TODO Remove reliance on global state, by refactoring into an i18nTranslatorManager + // which is instanciated by core with a $clean instance variable. + + if(!$adapter->isAvailable($lang)) { + i18n::include_by_locale($lang); + } + + if(!$adapter->isAvailable($locale)) { + i18n::include_by_locale($locale); + } + + $result = call_user_func($callback, $adapter); + if($result !== null) { + return $result; + } + } + } + + // Nothing matched + return null; + } + /** * @return array Array of priority keys to instances of Zend_Translate, mapped by name. diff --git a/model/DataObject.php b/model/DataObject.php index dccbe45b3..fbca06e59 100644 --- a/model/DataObject.php +++ b/model/DataObject.php @@ -722,6 +722,24 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity return true; } + /** + * Pluralise this item given a specific count. + * + * E.g. "0 Pages", "1 File", "3 Images" + * + * @param string $count + * @param bool $prependNumber Include number in result. Defaults to true. + * @return string + */ + public function i18n_pluralise($count, $prependNumber = true) { + return i18n::pluralise( + $this->i18n_singular_name(), + $this->i18n_plural_name(), + $count, + $prependNumber + ); + } + /** * Get the user friendly singular name of this DataObject. * If the name is not defined (by redefining $singular_name in the subclass), diff --git a/model/versioning/ChangeSet.php b/model/versioning/ChangeSet.php index 91fa8479f..9d1e25b77 100644 --- a/model/versioning/ChangeSet.php +++ b/model/versioning/ChangeSet.php @@ -338,4 +338,117 @@ class ChangeSet extends DataObject { $this->extend('updateCMSFields', $fields); return $fields; } + + /** + * Gets summary of items in changeset + * + * @return string + */ + public function getDescription() { + // Initialise list of items to count + $counted = []; + $countedOther = 0; + foreach($this->config()->important_classes as $type) { + if(class_exists($type)) { + $counted[$type] = 0; + } + } + + // Check each change item + /** @var ChangeSetItem $change */ + foreach($this->Changes() as $change) { + $found = false; + foreach($counted as $class => $num) { + if(is_a($change->ObjectClass, $class, true)) { + $counted[$class]++; + $found = true; + break; + } + } + if(!$found) { + $countedOther++; + } + } + + // Describe set based on this output + $counted = array_filter($counted); + + // Empty state + if(empty($counted) && empty($countedOther)) { + return ''; + } + + // Put all parts together + $parts = []; + foreach($counted as $class => $count) { + $parts[] = DataObject::singleton($class)->i18n_pluralise($count); + } + + // Describe non-important items + if($countedOther) { + if ($counted) { + $parts[] = i18n::pluralise( + _t('ChangeSet.DESCRIPTION_OTHER_ITEM', 'other item'), + _t('ChangeSet.DESCRIPTION_OTHER_ITEMS', 'other items'), + $countedOther + ); + } else { + $parts[] = i18n::pluralise( + _t('ChangeSet.DESCRIPTION_ITEM', 'item'), + _t('ChangeSet.DESCRIPTION_ITEMS', 'items'), + $countedOther + ); + } + } + + // Figure out how to join everything together + if(empty($parts)) { + return ''; + } + if(count($parts) === 1) { + return $parts[0]; + } + + // Non-comma list + if(count($parts) === 2) { + return _t( + 'ChangeSet.DESCRIPTION_AND', + '{first} and {second}', + [ + 'first' => $parts[0], + 'second' => $parts[1], + ] + ); + } + + // First item + $string = _t( + 'ChangeSet.DESCRIPTION_LIST_FIRST', + '{item}', + ['item' => $parts[0]] + ); + + // Middle items + for($i = 1; $i < count($parts) - 1; $i++) { + $string = _t( + 'ChangeSet.DESCRIPTION_LIST_MID', + '{list}, {item}', + [ + 'list' => $string, + 'item' => $parts[$i] + ] + ); + } + + // Oxford comma + $string = _t( + 'ChangeSet.DESCRIPTION_LIST_LAST', + '{list}, and {item}', + [ + 'list' => $string, + 'item' => end($parts) + ] + ); + return $string; + } }