diff --git a/core/config/schema/core.entity.schema.yml b/core/config/schema/core.entity.schema.yml index df2a2fd02d..15f92bc85f 100644 --- a/core/config/schema/core.entity.schema.yml +++ b/core/config/schema/core.entity.schema.yml @@ -362,3 +362,16 @@ field.formatter.settings.entity_reference_label: type: boolean label: 'Link label to the referenced entity' +bundle_entity_with_plural_labels: + type: config_entity + label: 'Bundle' + mapping: + label_singular: + type: label + label: 'Indefinite singular name' + label_plural: + type: label + label: 'Indefinite plural name' + label_count: + type: plural_label + label: 'Count label' diff --git a/core/lib/Drupal/Core/Config/Entity/EntityBundleWithPluralLabelsInterface.php b/core/lib/Drupal/Core/Config/Entity/EntityBundleWithPluralLabelsInterface.php new file mode 100644 index 0000000000..a7362d24ac --- /dev/null +++ b/core/lib/Drupal/Core/Config/Entity/EntityBundleWithPluralLabelsInterface.php @@ -0,0 +1,37 @@ +label_singular)) { + if ($label = $this->label()) { + $this->label_singular = Unicode::strtolower($label); + } + } + return $this->label_singular; + } + + /** + * {@inheritdoc} + */ + public function getPluralLabel() { + // Provide a fallback in case label_plural is not set yet. + if (empty($this->label_plural)) { + if ($label = $this->label()) { + $arguments = ['@label' => Unicode::strtolower($label)]; + $options = ['langcode' => $this->language()->getId()]; + $this->label_plural = new TranslatableMarkup('@label items', $arguments, $options); + } + } + return $this->label_plural; + } + + /** + * {@inheritdoc} + */ + public function getCountLabel($count) { + if (($index = static::getPluralIndex($count)) === -1) { + // If the index cannot be computed, fallback to a single plural variant. + $index = $count > 1 ? 1 : 0; + } + + $label_count = empty($this->label_count) ? [] : explode(PluralTranslatableMarkup::DELIMITER, $this->label_count); + if (isset($label_count[$index])) { + return new FormattableMarkup($label_count[$index], ['@count' => $count]); + } + $arguments = [ + '@singular' => $this->getSingularLabel(), + '@plural' => $this->getPluralLabel(), + ]; + return new PluralTranslatableMarkup($count, '1 @singular', '@count @plural', $arguments); + } + + /** + * Gets the plural index through the gettext formula. + * + * @param int $count + * Number to return plural for. + * + * @return int + * The numeric index of the plural variant to use for this $langcode and + * $count combination or -1 if the language was not found or does not have a + * plural formula. + * + * @todo Remove this method when https://www.drupal.org/node/2766857 gets in. + */ + protected static function getPluralIndex($count) { + // We have to test both if the function and the service exist since in + // certain situations it is possible that locale code might be loaded but + // the service does not exist. For example, where the parent test site has + // locale installed but the child site does not. + // @todo Refactor in https://www.drupal.org/node/2660338 so this code does + // not depend on knowing that the Locale module exists. + if (function_exists('locale_get_plural') && \Drupal::hasService('locale.plural.formula')) { + return locale_get_plural($count); + } + return -1; + } + +} diff --git a/core/modules/book/config/optional/node.type.book.yml b/core/modules/book/config/optional/node.type.book.yml index 0c07a79e8f..4deaa971bd 100644 --- a/core/modules/book/config/optional/node.type.book.yml +++ b/core/modules/book/config/optional/node.type.book.yml @@ -11,3 +11,6 @@ help: '' new_revision: true preview_mode: 1 display_submitted: true +label_singular: 'book page' +label_plural: 'book pages' +label_count: "1 book page\x03@count book pages" diff --git a/core/modules/forum/config/optional/node.type.forum.yml b/core/modules/forum/config/optional/node.type.forum.yml index 8ed965df3f..a6f19e8f3a 100644 --- a/core/modules/forum/config/optional/node.type.forum.yml +++ b/core/modules/forum/config/optional/node.type.forum.yml @@ -11,3 +11,6 @@ help: '' new_revision: false preview_mode: 1 display_submitted: true +label_singular: 'forum topic' +label_plural: 'forum topics' +label_count: "1 forum topic\x03@count forum topics" diff --git a/core/modules/node/config/schema/node.schema.yml b/core/modules/node/config/schema/node.schema.yml index 11a93c5310..14d8939c87 100644 --- a/core/modules/node/config/schema/node.schema.yml +++ b/core/modules/node/config/schema/node.schema.yml @@ -9,7 +9,7 @@ node.settings: label: 'Use administration theme when editing or creating content' node.type.*: - type: config_entity + type: bundle_entity_with_plural_labels label: 'Content type' mapping: name: diff --git a/core/modules/node/node.post_update.php b/core/modules/node/node.post_update.php index 43e3cd6acc..6f15718fa6 100644 --- a/core/modules/node/node.post_update.php +++ b/core/modules/node/node.post_update.php @@ -6,6 +6,7 @@ */ use Drupal\Core\Entity\Entity\EntityFormDisplay; +use Drupal\node\Entity\NodeType; /** * Load all form displays for nodes, add status with these settings, save. @@ -27,3 +28,17 @@ function node_post_update_configure_status_field_widget() { ])->save(); } } + +/** + * Add plural label variants to node-type entities. + */ +function node_post_update_plural_variants() { + /** @var \Drupal\node\NodeTypeInterface $node_type */ + foreach (NodeType::loadMultiple() as $node_type) { + $node_type + ->set('label_singular', NULL) + ->set('label_plural', NULL) + ->set('label_count', NULL) + ->save(); + } +} diff --git a/core/modules/node/src/Entity/NodeType.php b/core/modules/node/src/Entity/NodeType.php index e4a4c3b6c1..f48dcc4ab7 100644 --- a/core/modules/node/src/Entity/NodeType.php +++ b/core/modules/node/src/Entity/NodeType.php @@ -3,6 +3,8 @@ namespace Drupal\node\Entity; use Drupal\Core\Config\Entity\ConfigEntityBundleBase; +use Drupal\Core\Config\Entity\EntityBundleWithPluralLabelsInterface; +use Drupal\Core\Config\Entity\EntityBundleWithPluralLabelsTrait; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\node\NodeTypeInterface; @@ -41,10 +43,15 @@ * "new_revision", * "preview_mode", * "display_submitted", + * "label_singular", + * "label_plural", + * "label_count", * } * ) */ -class NodeType extends ConfigEntityBundleBase implements NodeTypeInterface { +class NodeType extends ConfigEntityBundleBase implements NodeTypeInterface, EntityBundleWithPluralLabelsInterface { + + use EntityBundleWithPluralLabelsTrait; /** * The machine name of this node type. diff --git a/core/modules/node/src/NodeTypeForm.php b/core/modules/node/src/NodeTypeForm.php index 9eec40c700..adf7b58023 100644 --- a/core/modules/node/src/NodeTypeForm.php +++ b/core/modules/node/src/NodeTypeForm.php @@ -6,6 +6,7 @@ use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\StringTranslation\PluralTranslatableMarkup; use Drupal\language\Entity\ContentLanguageSettings; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -184,6 +185,39 @@ public function form(array $form, FormStateInterface $form_state) { '#default_value' => $type->displaySubmitted(), '#description' => t('Author username and publish date will be displayed.'), ]; + $form['display']['plural'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Singular and plural labels'), + '#description' => $this->t('These are lowercase alternatives to the node type label for singular and plural cases.'), + ]; + $form['display']['plural']['label_singular'] = [ + '#type' => 'textfield', + '#title' => $this->t('Singular label'), + '#description' => $this->t("Enter a lowercase label for the singular case. Examples: 'article', 'page'."), + '#default_value' => $type->getSingularLabel(), + ]; + $form['display']['plural']['label_plural'] = [ + '#type' => 'textfield', + '#title' => $this->t('Plural label'), + '#description' => $this->t("Enter a lowercase label for the plural case. Examples: 'articles', 'pages'."), + '#default_value' => $type->getPluralLabel(), + ]; + $arguments = ['@plural' => $type->getPluralLabel() ?: $this->t('items')]; + $form['display']['label_count'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Count labels'), + '#description' => $this->t('Count labels are used to build a text representation of a certain number of @plural. The @count placeholder is available and will be replaced with the number of @plural.', $arguments), + '#tree' => TRUE, + ]; + $plurals = $this->getNumberOfPlurals($type->language()->getId()); + for ($i = 0; $i < $plurals; $i++) { + $form['display']['label_count'][$i] = [ + '#type' => 'textfield', + '#title' => $this->getPluralVariantLabel($plurals, $i), + '#description' => $this->t('Text to use for this variant, @count will be replaced with the number of @plural.', $arguments), + '#default_value' => $this->getPluralVariantDefaultValue($i), + ]; + } return $this->protectBundleIdElement($form); } @@ -209,6 +243,9 @@ public function validateForm(array &$form, FormStateInterface $form_state) { if ($id == '0') { $form_state->setErrorByName('type', $this->t("Invalid machine-readable name. Enter a name other than %invalid.", ['%invalid' => $id])); } + + // Pack the label_count values as a plural_label. + $form_state->setValue('label_count', implode(PluralTranslatableMarkup::DELIMITER, $form_state->getValue('label_count'))); } /** @@ -256,4 +293,64 @@ public function save(array $form, FormStateInterface $form_state) { $form_state->setRedirectUrl($type->urlInfo('collection')); } + /** + * Returns a plural variant label given the variant delta and variants count. + * + * @param int $plurals + * The total number of plural variants. + * @param int $delta + * The plural variant delta. + * + * @return \Drupal\Component\Render\MarkupInterface + * The variant label. + */ + protected function getPluralVariantLabel($plurals, $delta) { + if ($delta == 0) { + return $this->t('Singular form'); + } + elseif ($plurals == 2 && $delta == 1) { + return $this->t('Plural form'); + } + return $this->formatPlural($delta, 'First plural form', '@count. plural form'); + } + + /** + * Retrieves a plural variant from the backend or assures a decent fallback. + * + * @param int $delta + * The plural variant delta. + * + * @return \Drupal\Component\Render\MarkupInterface|string + * The plural variant default value. + */ + protected function getPluralVariantDefaultValue($delta) { + static $label_count; + + /** @var \Drupal\node\NodeTypeInterface $type */ + $type = $this->getEntity(); + + if (!isset($label_count)) { + $label_count = $type->get('label_count'); + $label_count = $label_count ? explode(PluralTranslatableMarkup::DELIMITER, $label_count) : []; + } + + if (isset($label_count[$delta])) { + // A value was stored in the backend. + return $label_count[$delta]; + } + + $singular = $type->getSingularLabel(); + $plural = $type->getPluralLabel(); + + if ($type->isNew() || ($delta == 0 && empty($singular) || ($delta > 0 && empty($plural)))) { + // Either the node type is not yet saved (we don't even know the entity + // label) or we cannot assure a decent fallback. + return ''; + } + + // Provide a fallback default value. + $arguments = ['@singular' => $singular, '@plural' => $plural]; + return $delta == 0 ? $this->t('1 @singular', $arguments) : $this->t('@count @plural', $arguments); + } + } diff --git a/core/modules/node/src/Tests/NodeTypeTest.php b/core/modules/node/src/Tests/NodeTypeTest.php index 70d3ac40b5..fec15562b7 100644 --- a/core/modules/node/src/Tests/NodeTypeTest.php +++ b/core/modules/node/src/Tests/NodeTypeTest.php @@ -256,4 +256,42 @@ public function testNodeTypeNoContentType() { $this->assertEqual(0, count($bundle_info->getBundleInfo('node')), 'The bundle information service has 0 bundles for the Node entity type.'); } + /** + * Tests singular/plural labels. + */ + public function testPluralLabels() { + $account = $this->drupalCreateUser(['administer content types', 'administer node fields']); + $this->drupalLogin($account); + + $edit = [ + 'name' => 'Mouse', + 'type' => 'mouse', + ]; + $this->drupalPostForm('admin/structure/types/add', $edit, 'Save and manage fields'); + + $type = NodeType::load('mouse'); + + // Test fallback values. + $this->assertEqual($type->getSingularLabel(), 'mouse'); + $this->assertEqual($type->getPluralLabel(), 'mouse items'); + $this->assertEqual($type->getCountLabel(1), '1 mouse'); + $this->assertEqual($type->getCountLabel(5), '5 mouse items'); + + $edit = [ + 'label_singular' => 'mouse', + 'label_plural' => 'mice', + 'label_count[0]' => '1 mouse', + 'label_count[1]' => '@count mice', + ]; + $this->drupalPostForm('admin/structure/types/manage/mouse', $edit, 'Save content type'); + + $type = NodeType::load('mouse'); + + // Test user entered values. + $this->assertEqual($type->getSingularLabel(), 'mouse'); + $this->assertEqual($type->getPluralLabel(), 'mice'); + $this->assertEqual($type->getCountLabel(1), '1 mouse'); + $this->assertEqual($type->getCountLabel(5), '5 mice'); + } + } diff --git a/core/modules/node/tests/src/Functional/NodeTypePluralLabelsTranslationTest.php b/core/modules/node/tests/src/Functional/NodeTypePluralLabelsTranslationTest.php new file mode 100644 index 0000000000..1898e1ae7b --- /dev/null +++ b/core/modules/node/tests/src/Functional/NodeTypePluralLabelsTranslationTest.php @@ -0,0 +1,136 @@ +save(); + + // Create and log in user. + $account = $this->drupalCreateUser([ + 'administer content types', + 'administer node fields', + 'translate configuration', + ]); + $this->drupalLogin($account); + + /** @var \Drupal\locale\PluralFormulaInterface $plural_formula */ + $plural_formula = $this->container->get('locale.plural.formula'); + + // Workaround to register the Romanian and English language formulae in + // state. Without this, the number of plurals is not correctly parsed. + $po_header = new PoHeader('ro'); + $plural = $po_header->parsePluralForms('nplurals=3; plural=((n==1)?(0):(((n==0)||(((n%100)>0)&&((n%100)<20)))?(1):2));\n'); + $plural_formula->setPluralFormula('ro', $plural[0], $plural[1]); + $plural = $po_header->parsePluralForms('nplurals=2; plural=(n > 1);\n'); + $plural_formula->setPluralFormula('en', $plural[0], $plural[1]); + } + + /** + * {@inheritdoc} + */ + protected function installParameters() { + $parameters = parent::installParameters(); + // Install the site in Romanian language. + $parameters['parameters']['langcode'] = 'ro'; + return $parameters; + } + + /** + * Tests the node type translation. + */ + public function testNodeTypeTranslation() { + /** @var \Drupal\Core\Language\LanguageManagerInterface $language_manager */ + $language_manager = $this->container->get('language_manager'); + + $this->drupalGet('admin/structure/types/add'); + + // Check that 3 plural variants are exposed for the Romanian language. + $this->assertSession()->fieldExists('label_count[0]'); + $this->assertSession()->fieldExists('label_count[1]'); + $this->assertSession()->fieldExists('label_count[2]'); + $this->assertSession()->fieldNotExists('label_count[3]'); + + $edit = [ + 'name' => 'Copil', + 'type' => 'child', + 'label_singular' => 'copil', + 'label_plural' => 'copii', + 'label_count[0]' => '1 copil', + 'label_count[1]' => '@count copii', + 'label_count[2]' => '@count de copii', + ]; + $this->drupalPostForm(NULL, $edit, 'Save and manage fields'); + + $edit = [ + 'translation[config_names][node.type.child][name]' => 'Child', + 'translation[config_names][node.type.child][label_singular]' => 'child', + 'translation[config_names][node.type.child][label_plural]' => 'children', + 'translation[config_names][node.type.child][label_count][0]' => '1 child', + 'translation[config_names][node.type.child][label_count][1]' => '@count children', + ]; + $this->drupalPostForm('admin/structure/types/manage/child/translate/en/add', $edit, 'Save translation'); + + /** @var \Drupal\node\NodeTypeInterface $node_type */ + $node_type = NodeType::load('child'); + + // Check that the default language (Romanian) original labels were stored. + self::assertEquals($node_type->label(), 'Copil'); + self::assertEquals($node_type->getSingularLabel(), 'copil'); + self::assertEquals($node_type->getPluralLabel(), 'copii'); + self::assertEquals($node_type->getCountLabel(1), '1 copil'); + self::assertEquals($node_type->getCountLabel(5), '5 copii'); + self::assertEquals($node_type->getCountLabel(20), '20 de copii'); + + // Load the English version of the 'child' node-type. + $original_language = $language_manager->getConfigOverrideLanguage(); + $language_manager->setConfigOverrideLanguage(ConfigurableLanguage::load('en')); + /** @var \Drupal\node\NodeTypeInterface $node_type */ + $node_type = NodeType::load('child'); + $language_manager->setConfigOverrideLanguage($original_language); + + // Check that the English translated labels were stored. + self::assertEquals($node_type->label(), 'Child'); + self::assertEquals($node_type->getSingularLabel(), 'child'); + self::assertEquals($node_type->getPluralLabel(), 'children'); + self::assertEquals($node_type->getCountLabel(1), '1 child'); + self::assertEquals($node_type->getCountLabel(5), '5 children'); + self::assertEquals($node_type->getCountLabel(20), '20 children'); + + // Clear the count label second variant to test variant delta preserving. + $edit = ['label_count[1]' => '']; + $this->drupalPostForm($node_type->urlInfo('edit-form'), $edit, 'Save content type'); + + $node_type = NodeType::load('child'); + + // Check that the variants delta has been preserved. + $variants = explode(PluralTranslatableMarkup::DELIMITER, $node_type->get('label_count')); + self::assertEquals($variants[0], '1 copil'); + self::assertEquals($variants[1], ''); + self::assertEquals($variants[2], '@count de copii'); + } + +} diff --git a/core/modules/node/tests/src/Functional/Update/NodeUpdateTest.php b/core/modules/node/tests/src/Functional/Update/NodeUpdateTest.php index 223676b31e..2dc8cb8279 100644 --- a/core/modules/node/tests/src/Functional/Update/NodeUpdateTest.php +++ b/core/modules/node/tests/src/Functional/Update/NodeUpdateTest.php @@ -64,4 +64,28 @@ public function testStatusCheckbox() { } } + /** + * Tests node_post_update_plural_variants(). + * + * @see node_post_update_plural_variants() + */ + public function testPostUpdatePluralVariants() { + $properties = ['label_singular', 'label_plural', 'label_count']; + + // Check that plural label variant properties are not present before update. + $node_type = $this->config('node.type.page')->getRawData(); + foreach ($properties as $property) { + $this->assertArrayNotHasKey($property, $node_type); + } + + $this->runUpdates(); + + // Check that plural label variant properties were added as NULL. + $node_type = $this->config('node.type.page')->getRawData(); + foreach ($properties as $property) { + $this->assertArrayHasKey($property, $node_type); + $this->assertNull($node_type[$property]); + } + } + } diff --git a/core/modules/rest/tests/src/Functional/EntityResource/NodeType/NodeTypeResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/NodeType/NodeTypeResourceTestBase.php index c374bfb7c0..12df01f571 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/NodeType/NodeTypeResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/NodeType/NodeTypeResourceTestBase.php @@ -59,6 +59,9 @@ protected function getExpectedNormalizedEntity() { 'description' => 'Camelids are large, strictly herbivorous animals with slender necks and long legs.', 'display_submitted' => TRUE, 'help' => NULL, + 'label_count' => NULL, + 'label_plural' => NULL, + 'label_singular' => NULL, 'langcode' => 'en', 'name' => 'Camelids', 'new_revision' => TRUE, diff --git a/core/profiles/standard/config/install/node.type.article.yml b/core/profiles/standard/config/install/node.type.article.yml index 1fd439ce71..9d6f11a0b9 100644 --- a/core/profiles/standard/config/install/node.type.article.yml +++ b/core/profiles/standard/config/install/node.type.article.yml @@ -8,3 +8,6 @@ help: '' new_revision: true preview_mode: 1 display_submitted: true +label_singular: article +label_plural: articles +label_count: "1 article\x03@count articles" diff --git a/core/profiles/standard/config/install/node.type.page.yml b/core/profiles/standard/config/install/node.type.page.yml index 57dcc0c992..2c1de802da 100644 --- a/core/profiles/standard/config/install/node.type.page.yml +++ b/core/profiles/standard/config/install/node.type.page.yml @@ -8,3 +8,6 @@ help: '' new_revision: true preview_mode: 1 display_submitted: false +label_singular: page +label_plural: pages +label_count: "1 page\x03@count pages"