diff --git a/core/config/schema/core.entity.schema.yml b/core/config/schema/core.entity.schema.yml
index 8850d40ef7..aa64a5655b 100644
--- a/core/config/schema/core.entity.schema.yml
+++ b/core/config/schema/core.entity.schema.yml
@@ -375,6 +375,24 @@ 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: sequence
+ nullable: true
+ label: 'Count labels'
+ sequence:
+ type: plural_label
+ label: 'Count label'
+
block.settings.field_block:*:*:*:
type: block_settings
mapping:
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..1be187211c
--- /dev/null
+++ b/core/lib/Drupal/Core/Config/Entity/EntityBundleWithPluralLabelsInterface.php
@@ -0,0 +1,40 @@
+ "1 article\x03@count article",
+ * 'items_found' => "1 article was found\x03@count articles were found",
+ * ]
+ * @endcode
+ * Note that the context ('default', 'items_found') is an arbitrary string
+ * identifier used to retrieve the desired version. If the array keys are
+ * missed, the array item integer index is used as variant ID:
+ * @code
+ * [
+ * "1 item\x03@count items",
+ * ]
+ * @endcode
+ * Each value is a definite singular/plural count label with the plural
+ * variants separated by ETX (PoItem::DELIMITER).
+ *
+ * @var string[]|null
+ *
+ * @see \Drupal\Component\Gettext\PoItem::DELIMITER
+ */
+ protected $label_count;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setSingularLabel(string $singular_label): EntityBundleWithPluralLabelsInterface {
+ $this->label_singular = $singular_label;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setPluralLabel(string $plural_label): EntityBundleWithPluralLabelsInterface {
+ $this->label_plural = $plural_label;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setCountLabel(array $count_label): EntityBundleWithPluralLabelsInterface {
+ $this->label_count = $count_label;
+ return $this;
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Entity/EntityTypeBundleInfo.php b/core/lib/Drupal/Core/Entity/EntityTypeBundleInfo.php
index aa917c845c..4f268cc96d 100644
--- a/core/lib/Drupal/Core/Entity/EntityTypeBundleInfo.php
+++ b/core/lib/Drupal/Core/Entity/EntityTypeBundleInfo.php
@@ -2,9 +2,12 @@
namespace Drupal\Core\Entity;
+use Drupal\Component\Gettext\PoItem;
+use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Cache\UseCacheBackendTrait;
+use Drupal\Core\Config\Entity\EntityBundleWithPluralLabelsInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\TypedData\TypedDataManagerInterface;
@@ -98,6 +101,13 @@ public function getAllBundleInfo() {
if ($bundle_entity_type = $entity_type->getBundleEntityType()) {
foreach ($this->entityTypeManager->getStorage($bundle_entity_type)->loadMultiple() as $entity) {
$this->bundleInfo[$type][$entity->id()]['label'] = $entity->label();
+ if ($entity instanceof EntityBundleWithPluralLabelsInterface) {
+ $this->bundleInfo[$type][$entity->id()] += [
+ 'label_singular' => $entity->get('label_singular'),
+ 'label_plural' => $entity->get('label_plural'),
+ 'label_count' => $entity->get('label_count'),
+ ];
+ }
}
}
// If entity type bundles are not supported and
@@ -125,4 +135,62 @@ public function clearCachedBundles() {
$this->typedDataManager->clearCachedDefinitions();
}
+ /**
+ * {@inheritdoc}
+ */
+ public function getBundleCountLabel(string $entity_type_id, string $bundle, int $count, $variant): ?string {
+ if (!$bundles_info = $this->getBundleInfo($entity_type_id)) {
+ throw new \InvalidArgumentException("The '{$entity_type_id}' doesn't exist.");
+ }
+ $bundle_info = $bundles_info[$bundle] ?? NULL;
+ if (!$bundle_info) {
+ throw new \InvalidArgumentException("The '{$entity_type_id}' entity type bundle '{$bundle}' doesn't exist.");
+ }
+
+ if (!empty($bundle_info['label_count'])) {
+ if (empty($bundle_info['label_count'][$variant])) {
+ throw new \InvalidArgumentException("There's no variant '{$variant}' defined in label_count for bundle '{$bundle}' of '{$entity_type_id}' entity type.");
+ }
+
+ $index = static::getPluralIndex($count);
+ if ($index === -1) {
+ // If the index cannot be computed, fallback to a single plural variant.
+ $index = $count > 1 ? 1 : 0;
+ }
+
+ $label_count = explode(PoItem::DELIMITER, $bundle_info['label_count'][$variant]);
+ if (!empty($label_count[$index])) {
+ return new FormattableMarkup($label_count[$index], ['@count' => $count]);
+ }
+ }
+
+ return NULL;
+ }
+
+ /**
+ * 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 the current language
+ * and the given $count number 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(int $count): int {
+ // 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/lib/Drupal/Core/Entity/EntityTypeBundleInfoInterface.php b/core/lib/Drupal/Core/Entity/EntityTypeBundleInfoInterface.php
index 2789b651e5..7baea06b5e 100644
--- a/core/lib/Drupal/Core/Entity/EntityTypeBundleInfoInterface.php
+++ b/core/lib/Drupal/Core/Entity/EntityTypeBundleInfoInterface.php
@@ -37,4 +37,30 @@ public function getBundleInfo($entity_type_id);
*/
public function clearCachedBundles();
+ /**
+ * Gets the count label for a given bundle.
+ *
+ * @param string $entity_type_id
+ * The bundle's entity type ID.
+ * @param string $bundle
+ * The bundle.
+ * @param int $count
+ * The item count to display if the plural form was requested.
+ * @param string|int $variant
+ * The variant of the count label. This is actually the, string or integer,
+ * array item key corresponding to the count label variant. A bundle can
+ * define unlimited definite singular/plural count labels in order to cover
+ * various contexts where they are used. Pass the variant, as a string or
+ * integer identifier, to get the appropriate version of the count label.
+ *
+ * @return string|null
+ * The count label. NULL is returned in one of the following cases:
+ * - The bundle didn't define a 'label_count' variant list.
+ * - There's no plural formula for the given $count.
+ *
+ * @throws \InvalidArgumentException
+ * If the passed entity type, bundle or count label variant doesn't exist.
+ */
+ public function getBundleCountLabel(string $entity_type_id, string $bundle, int $count, $variant): ?string;
+
}
diff --git a/core/lib/Drupal/Core/Entity/entity.api.php b/core/lib/Drupal/Core/Entity/entity.api.php
index 0b163dc3a0..b47d3a2a93 100644
--- a/core/lib/Drupal/Core/Entity/entity.api.php
+++ b/core/lib/Drupal/Core/Entity/entity.api.php
@@ -840,6 +840,31 @@ function hook_entity_view_mode_info_alter(&$view_modes) {
* An associative array of all entity bundles, keyed by the entity
* type name, and then the bundle name, with the following keys:
* - label: The human-readable name of the bundle.
+ * - label_singular: (optional) A translated string with the indefinite
+ * singular label of the bundle. This is typically a lowercase label but it
+ * can be also translated as a capitalized label for languages such as
+ * German. For instance, given a bundle with the ID 'fruit', the singular
+ * label would be 'fruit' but for German translation will be capitalized as
+ * 'Obst'.
+ * - label_plural: (optional) A translated string with the indefinite plural
+ * label of the bundle. The same capitalization rules should be applied as
+ * for 'label_singular'.
+ * - label_count: (optional) An array containing one or more count label
+ * variants for this bundle. Depending on context a site might need
+ * different versions of the count label. For instance, a site will define
+ * the following variants, depending on the use-case:
+ * - On a regular page: @code 1 article\x03@count articles @endcode.
+ * - On a search results page: @code 1 article was found\x03@count articles
+ * were found @encode. Note that we cannot build this version by deriving
+ * the first one because of the was/were forms.
+ * - On a different page: @code 1
+ * article\x03@count articles @endcode.
+ * - On other pages the count might be displayed in a different @code
+ * @encode or even in a different theme hook, so it needs a variant
+ * without the count part: @code Article\x03Articles @encode.
+ * A meaningful key may be added to each variant in order to allow a more
+ * developer friendly identification. If the key is missed, the array index
+ * value should be used to identify which count plural variant to be used.
* - uri_callback: (optional) The same as the 'uri_callback' key defined for
* the entity type in the EntityTypeManager, but for the bundle only. When
* determining the URI of an entity, if a 'uri_callback' is defined for both
@@ -852,6 +877,16 @@ function hook_entity_view_mode_info_alter(&$view_modes) {
*/
function hook_entity_bundle_info() {
$bundles['user']['user']['label'] = t('User');
+ $bundles['user']['user']['label_singular'] = t('user');
+ $bundles['user']['user']['label_plural'] = t('users');
+ $bundles['user']['user']['label_count'] = [
+ // This variant misses a key and is identifiable by its array delta.
+ t('1 user\x03@count users'),
+ // The following variants have a meaningful identifier as key.
+ 'search results' => t('1 user is found\x03@count users were found'),
+ 'with_markup' => t('1 user\x03@count users'),
+ 'no count' => t('User\x03Users'),
+ ];
return $bundles;
}
@@ -859,13 +894,16 @@ function hook_entity_bundle_info() {
* Alter the bundles for entity types.
*
* @param array $bundles
- * An array of bundles, keyed first by entity type, then by bundle name.
+ * An array of bundles, keyed first by entity type, then by bundle name. Each
+ * value is an array with the same structure as the hook_entity_bundle_info()
+ * return array element.
*
* @see Drupal\Core\Entity\EntityTypeBundleInfo::getBundleInfo()
* @see hook_entity_bundle_info()
*/
function hook_entity_bundle_info_alter(&$bundles) {
$bundles['user']['user']['label'] = t('Full account');
+ $bundles['user']['user']['label_count']['search results'] = t('1 user is registered\x03@count users are registered');
}
/**
diff --git a/core/modules/book/config/optional/node.type.book.yml b/core/modules/book/config/optional/node.type.book.yml
index 0c07a79e8f..ada815bb6d 100644
--- a/core/modules/book/config/optional/node.type.book.yml
+++ b/core/modules/book/config/optional/node.type.book.yml
@@ -11,3 +11,7 @@ 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..5e0db03df4 100644
--- a/core/modules/forum/config/optional/node.type.forum.yml
+++ b/core/modules/forum/config/optional/node.type.forum.yml
@@ -11,3 +11,7 @@ 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/jsonapi/tests/src/Functional/NodeTypeTest.php b/core/modules/jsonapi/tests/src/Functional/NodeTypeTest.php
index c7bc3e2223..85a9afe77b 100644
--- a/core/modules/jsonapi/tests/src/Functional/NodeTypeTest.php
+++ b/core/modules/jsonapi/tests/src/Functional/NodeTypeTest.php
@@ -91,6 +91,9 @@ protected function getExpectedDocument() {
'display_submitted' => TRUE,
'help' => NULL,
'langcode' => 'en',
+ 'label_count' => NULL,
+ 'label_plural' => NULL,
+ 'label_singular' => NULL,
'name' => 'Camelids',
'new_revision' => TRUE,
'preview_mode' => 1,
diff --git a/core/modules/node/config/schema/node.schema.yml b/core/modules/node/config/schema/node.schema.yml
index 2a831d7678..0fa84e78be 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/migrations/d6_node_type.yml b/core/modules/node/migrations/d6_node_type.yml
index 3afe4bc475..063fb16fb8 100644
--- a/core/modules/node/migrations/d6_node_type.yml
+++ b/core/modules/node/migrations/d6_node_type.yml
@@ -8,6 +8,7 @@ source:
constants:
preview: 1 # DRUPAL_OPTIONAL
create_body: false
+ null_value: null
process:
type: type
name: name
@@ -23,5 +24,8 @@ process:
create_body_label: body_label
'third_party_settings/menu_ui/available_menus': available_menus
'third_party_settings/menu_ui/parent': parent
+ label_singular: 'constants/null_value'
+ label_plural: 'constants/null_value'
+ label_count: 'constants/null_value'
destination:
plugin: entity:node_type
diff --git a/core/modules/node/migrations/d7_node_type.yml b/core/modules/node/migrations/d7_node_type.yml
index 1be9cd1ccc..23deba9c82 100644
--- a/core/modules/node/migrations/d7_node_type.yml
+++ b/core/modules/node/migrations/d7_node_type.yml
@@ -7,6 +7,7 @@ source:
plugin: d7_node_type
constants:
preview: 1 # DRUPAL_OPTIONAL
+ null_value: null
process:
type: type
name: name
@@ -20,5 +21,8 @@ process:
create_body_label: body_label
'third_party_settings/menu_ui/available_menus': available_menus
'third_party_settings/menu_ui/parent': parent
+ label_singular: 'constants/null_value'
+ label_plural: 'constants/null_value'
+ label_count: 'constants/null_value'
destination:
plugin: entity:node_type
diff --git a/core/modules/node/node.post_update.php b/core/modules/node/node.post_update.php
index 913137757c..ab24dc40f7 100644
--- a/core/modules/node/node.post_update.php
+++ b/core/modules/node/node.post_update.php
@@ -5,7 +5,9 @@
* Post update functions for Node.
*/
+use Drupal\Core\Config\Entity\ConfigEntityUpdater;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
+use Drupal\node\NodeTypeInterface;
/**
* Load all form displays for nodes, add status with these settings, save.
@@ -34,3 +36,16 @@ function node_post_update_configure_status_field_widget() {
function node_post_update_node_revision_views_data() {
// Empty post-update hook.
}
+
+/**
+ * Add plural label variants to node-type entities.
+ */
+function node_post_update_plural_variants(array &$sandbox): void {
+ \Drupal::classResolver(ConfigEntityUpdater::class)->update($sandbox, 'node_type', function (NodeTypeInterface $node_type): bool {
+ $node_type
+ ->set('label_singular', NULL)
+ ->set('label_plural', NULL)
+ ->set('label_count', NULL);
+ return TRUE;
+ });
+}
diff --git a/core/modules/node/src/Entity/NodeType.php b/core/modules/node/src/Entity/NodeType.php
index 60bfc7df77..69c187ecf9 100644
--- a/core/modules/node/src/Entity/NodeType.php
+++ b/core/modules/node/src/Entity/NodeType.php
@@ -3,6 +3,7 @@
namespace Drupal\node\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBundleBase;
+use Drupal\Core\Config\Entity\EntityBundleWithPluralLabelsTrait;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\node\NodeTypeInterface;
@@ -48,11 +49,16 @@
* "new_revision",
* "preview_mode",
* "display_submitted",
+ * "label_singular",
+ * "label_plural",
+ * "label_count",
* }
* )
*/
class NodeType extends ConfigEntityBundleBase implements NodeTypeInterface {
+ use EntityBundleWithPluralLabelsTrait;
+
/**
* The machine name of this node type.
*
diff --git a/core/modules/node/src/NodeTypeInterface.php b/core/modules/node/src/NodeTypeInterface.php
index 17dcd63db2..39b7b6f154 100644
--- a/core/modules/node/src/NodeTypeInterface.php
+++ b/core/modules/node/src/NodeTypeInterface.php
@@ -3,12 +3,13 @@
namespace Drupal\node;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
+use Drupal\Core\Config\Entity\EntityBundleWithPluralLabelsInterface;
use Drupal\Core\Entity\RevisionableEntityBundleInterface;
/**
* Provides an interface defining a node type entity.
*/
-interface NodeTypeInterface extends ConfigEntityInterface, RevisionableEntityBundleInterface {
+interface NodeTypeInterface extends ConfigEntityInterface, RevisionableEntityBundleInterface, EntityBundleWithPluralLabelsInterface {
/**
* Determines whether the node type is locked.
diff --git a/core/modules/node/tests/src/Functional/Rest/NodeTypeResourceTestBase.php b/core/modules/node/tests/src/Functional/Rest/NodeTypeResourceTestBase.php
index d74824dd13..dbc36cfc08 100644
--- a/core/modules/node/tests/src/Functional/Rest/NodeTypeResourceTestBase.php
+++ b/core/modules/node/tests/src/Functional/Rest/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/modules/node/tests/src/Functional/Update/NodeUpdateBundlePluralLabelsTest.php b/core/modules/node/tests/src/Functional/Update/NodeUpdateBundlePluralLabelsTest.php
new file mode 100644
index 0000000000..b828451048
--- /dev/null
+++ b/core/modules/node/tests/src/Functional/Update/NodeUpdateBundlePluralLabelsTest.php
@@ -0,0 +1,47 @@
+databaseDumpFiles[] = __DIR__ . '/../../../../../system/tests/fixtures/update/drupal-8.8.0.bare.standard.php.gz';
+ }
+
+ /**
+ * 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/system/tests/modules/entity_bundle_test/entity_bundle_test.info.yml b/core/modules/system/tests/modules/entity_bundle_test/entity_bundle_test.info.yml
new file mode 100644
index 0000000000..660bf0c41e
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_bundle_test/entity_bundle_test.info.yml
@@ -0,0 +1,4 @@
+name: 'Entity bundle test'
+type: module
+description: 'Provides support for testing entity bundles.'
+package: Testing
diff --git a/core/modules/system/tests/modules/entity_bundle_test/entity_bundle_test.module b/core/modules/system/tests/modules/entity_bundle_test/entity_bundle_test.module
new file mode 100644
index 0000000000..bc85eb9027
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_bundle_test/entity_bundle_test.module
@@ -0,0 +1,60 @@
+set('bundle_entity_type', 'entity_test_bundle_plural_labels');
+}
+
+/**
+ * Implements hook_entity_bundle_info().
+ */
+function entity_bundle_test_entity_bundle_info(): array {
+ return [
+ 'entity_test' => [
+ 'artist' => [
+ 'label' => t('Artist'),
+ 'label_singular' => t('artist'),
+ 'label_plural' => t('artists'),
+ 'label_count' => [
+ "1 artist\x03@count artists",
+ 'search results' => "1 artist was awarded\x03@count artists were awarded",
+ ],
+ ],
+ ],
+ ];
+}
+
+/**
+ * Implements hook_entity_bundle_info_alter().
+ */
+function entity_bundle_test_entity_bundle_info_alter(array &$bundles): void {
+ // Allow the test to trigger altering.
+ // @see \Drupal\KernelTests\Core\Entity\EntityTypeBundleInfoPluralLabelTest
+ if (\Drupal::state()->get('entity_bundle_test.allow_alter', FALSE)) {
+ // Bundle defined via config entity.
+ $bundles['entity_test_with_bundle']['article']['label_singular'] = 'article item';
+ $bundles['entity_test_with_bundle']['article']['label_plural'] = 'article items';
+ $bundles['entity_test_with_bundle']['article']['label_count'] = [
+ "1 article item\x03@count article items",
+ 'search results' => "1 article item was found\x03@count article items were found",
+ ];
+
+ // Bundle defined via hook_entity_bundle_info().
+ $bundles['entity_test']['artist']['label_singular'] = 'creator';
+ $bundles['entity_test']['artist']['label_plural'] = 'creators';
+ $bundles['entity_test']['artist']['label_count'] = [
+ "1 creator\x03@count creators",
+ 'search results' => "1 creator was awarded\x03@count creators were awarded",
+ ];
+ }
+}
diff --git a/core/modules/system/tests/modules/entity_test/config/schema/entity_test.schema.yml b/core/modules/system/tests/modules/entity_test/config/schema/entity_test.schema.yml
index d34d94940d..2c57636cbb 100644
--- a/core/modules/system/tests/modules/entity_test/config/schema/entity_test.schema.yml
+++ b/core/modules/system/tests/modules/entity_test/config/schema/entity_test.schema.yml
@@ -27,3 +27,13 @@ entity_test.entity_test_bundle.*:
description:
type: text
label: 'Description'
+
+entity_test.entity_test_bundle_plural_labels.*:
+ type: bundle_entity_with_plural_labels
+ mapping:
+ label:
+ type: label
+ label: 'Label'
+ id:
+ type: string
+ label: 'Machine-readable name'
diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestBundleWithPluralLabels.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestBundleWithPluralLabels.php
new file mode 100644
index 0000000000..93a7a19349
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestBundleWithPluralLabels.php
@@ -0,0 +1,33 @@
+bundleInfo = $this->container->get('entity_type.bundle.info');
+ }
+
+ /**
+ * Tests singular, plural and count labels for bundles as config entities.
+ *
+ * @covers ::getAllBundleInfo
+ */
+ public function testLabelsForBundlesAsConfigEntities(): void {
+ // Test bundles stored in config entities.
+ $this->setBundle('entity_test_with_bundle', 'article');
+
+ $bundle_entity = EntityTestBundleWithPluralLabels::create([
+ 'id' => 'article',
+ 'label' => 'Article',
+ ]);
+ $bundle_entity->save();
+
+ // Check that bundle info returns no label with bundle undefined labels.
+ $this->assertSingularLabel(NULL);
+ $this->assertPluralLabel(NULL);
+ $this->assertCountLabel(1, 0, NULL);
+ $this->assertCountLabel(100, 0, NULL);
+ $this->assertCountLabel(1, 'search results', NULL);
+ $this->assertCountLabel(100, 'search results', NULL);
+
+ // Set singular, plural and count labels on the bundle entity.
+ $bundle_entity
+ ->setSingularLabel('article')
+ ->setPluralLabel('articles')
+ ->setCountLabel([
+ "1 article\x03@count articles",
+ 'search results' => "1 article was found\x03@count articles were found",
+ ])
+ ->save();
+
+ // Check that labels are correctly returned.
+ $this->assertSingularLabel('article');
+ $this->assertPluralLabel('articles');
+ $this->assertCountLabel(1, 0, '1 article');
+ $this->assertCountLabel(100, 0, '100 articles');
+ $this->assertCountLabel(1, 'search results', '1 article was found');
+ $this->assertCountLabel(100, 'search results', '100 articles were found');
+
+ // Allow altering the labels via hook_entity_bundle_info_alter().
+ // @see \entity_bundle_test_entity_bundle_info_alter()
+ \Drupal::state()->set('entity_bundle_test.allow_alter', TRUE);
+ // Also, clear the bundle info cache, to get fresh bundle definitions.
+ $this->bundleInfo->clearCachedBundles();
+
+ // Check that altered labels are correctly returned.
+ $this->assertSingularLabel('article item');
+ $this->assertPluralLabel('article items');
+ $this->assertCountLabel(1, 0, '1 article item');
+ $this->assertCountLabel(100, 0, '100 article items');
+ $this->assertCountLabel(1, 'search results', '1 article item was found');
+ $this->assertCountLabel(100, 'search results', '100 article items were found');
+ }
+
+ /**
+ * Tests singular, plural and count labels for bundles defined in code.
+ *
+ * @covers ::getAllBundleInfo
+ */
+ public function testLabelsForBundlesWithoutConfigEntities(): void {
+ // Test entities with bundles not stored in config entities.
+ // @see entity_bundle_test_entity_bundle_info()
+ $this->setBundle('entity_test', 'artist');
+
+ // Check that labels are correctly returned.
+ $this->assertSingularLabel('artist');
+ $this->assertPluralLabel('artists');
+ $this->assertCountLabel(1, 0, '1 artist');
+ $this->assertCountLabel(100, 0, '100 artists');
+ $this->assertCountLabel(1, 'search results', '1 artist was awarded');
+ $this->assertCountLabel(100, 'search results', '100 artists were awarded');
+
+ // Allow altering the labels via hook_entity_bundle_info_alter().
+ // @see \entity_bundle_test_entity_bundle_info_alter()
+ \Drupal::state()->set('entity_bundle_test.allow_alter', TRUE);
+ // Also, clear the bundle info cache, to get fresh bundle definitions.
+ $this->bundleInfo->clearCachedBundles();
+
+ // Check that altered labels are correctly returned.
+ $this->assertSingularLabel('creator');
+ $this->assertPluralLabel('creators');
+ $this->assertCountLabel(1, 0, '1 creator');
+ $this->assertCountLabel(100, 0, '100 creators');
+ $this->assertCountLabel(1, 'search results', '1 creator was awarded');
+ $this->assertCountLabel(100, 'search results', '100 creators were awarded');
+ }
+
+ /**
+ * Test passing a wrong entity type to ::getBundleCountLabel().
+ *
+ * @covers ::getBundleCountLabel
+ */
+ public function testGetBundleCountLabelWithWrongEntityType(): void {
+ $this->expectExceptionObject(new \InvalidArgumentException("The 'nonexistent_entity_type' doesn't exist."));
+ $this->bundleInfo->getBundleCountLabel('nonexistent_entity_type', 'article', 123, 'default');
+ }
+
+ /**
+ * Test passing a wrong bundle type to ::getBundleCountLabel().
+ *
+ * @covers ::getBundleCountLabel
+ */
+ public function testGetBundleCountLabelWithWrongBundle(): void {
+ $this->expectExceptionObject(new \InvalidArgumentException("The 'entity_test' entity type bundle 'nonexistent_bundle' doesn't exist."));
+ $this->bundleInfo->getBundleCountLabel('entity_test', 'nonexistent_bundle', 123, 'default');
+ }
+
+ /**
+ * Test passing a wrong bundle count label variant to ::getBundleCountLabel().
+ *
+ * @covers ::getBundleCountLabel
+ */
+ public function testGetBundleCountLabelWithWrongVariant(): void {
+ EntityTestBundleWithPluralLabels::create([
+ 'id' => 'article',
+ 'label' => 'Article',
+ 'label_count' => [
+ 'default' => "1 item\x03@count items",
+ ],
+ ])->save();
+
+ $this->expectExceptionObject(new \InvalidArgumentException("There's no variant 'nonexistent_variant' defined in label_count for bundle 'article' of 'entity_test_with_bundle' entity type."));
+ $this->bundleInfo->getBundleCountLabel('entity_test_with_bundle', 'article', 123, 'nonexistent_variant');
+ }
+
+ /**
+ * Asserts that a given bundle has an expected singular label.
+ *
+ * @param string|null $expected_singular_label
+ * The expected bundle singular label.
+ */
+ protected function assertSingularLabel(?string $expected_singular_label): void {
+ $bundle_info = $this->bundleInfo->getBundleInfo($this->entityTypeId)[$this->bundle];
+ $this->assertEquals($expected_singular_label, $bundle_info['label_singular']);
+ }
+
+ /**
+ * Asserts that a given bundle has an expected plural label.
+ *
+ * @param string|null $expected_plural_label
+ * The expected bundle plural label.
+ *
+ * @throws \Exception
+ */
+ protected function assertPluralLabel(?string $expected_plural_label): void {
+ $bundle_info = $this->bundleInfo->getBundleInfo($this->entityTypeId)[$this->bundle];
+ $this->assertEquals($expected_plural_label, $bundle_info['label_plural']);
+ }
+
+ /**
+ * Asserts an expected count label on a given bundle, count and variant.
+ *
+ * @param int $count
+ * The count.
+ * @param string|int $variant
+ * The count label variant ID.
+ * @param string|null $expected_count_label
+ * The expected count label.
+ */
+ protected function assertCountLabel(int $count, $variant, ?string $expected_count_label): void {
+ $actual_count_label = $this->bundleInfo->getBundleCountLabel($this->entityTypeId, $this->bundle, $count, $variant);
+ $this->assertEquals($expected_count_label, $actual_count_label);
+ }
+
+ /**
+ * Sets the bundle being tested.
+ *
+ * @param string $entity_type_id
+ * The bundle's entity type ID.
+ * @param string $bundle
+ * The bundle.
+ */
+ protected function setBundle(string $entity_type_id, string $bundle): void {
+ $this->entityTypeId = $entity_type_id;
+ $this->bundle = $bundle;
+ }
+
+}