diff --git a/core/core.services.yml b/core/core.services.yml index 2b81197..cffc627 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -510,7 +510,7 @@ services: - { name: string_translator, priority: 30 } string_translation: class: Drupal\Core\StringTranslation\TranslationManager - arguments: ['@language_manager'] + arguments: ['@language_manager', '@state'] calls: - [initLanguageManager] tags: diff --git a/core/lib/Drupal/Core/StringTranslation/StringTranslationTrait.php b/core/lib/Drupal/Core/StringTranslation/StringTranslationTrait.php index 85a6e3f..0946709 100644 --- a/core/lib/Drupal/Core/StringTranslation/StringTranslationTrait.php +++ b/core/lib/Drupal/Core/StringTranslation/StringTranslationTrait.php @@ -53,6 +53,28 @@ protected function formatPlural($count, $singular, $plural, array $args = array( } /** + * Formats a translated string containing a count of items. + * + * See the + * \Drupal\Core\StringTranslation\TranslationInterface::formatPluralTranslated() + * documentation for details. + */ + protected function formatPluralTranslated($count, $translated, array $args = array(), array $options = array()) { + return $this->getStringTranslation()->formatPluralTranslated($count, $translated, $args, $options); + } + + /** + * Returns number of plurals supported by a given language. + * + * See the + * \Drupal\Core\StringTranslation\TranslationInterface::getNumberOfPlurals() + * documentation for details. + */ + protected function getNumberOfPlurals($langcode = NULL) { + return $this->getStringTranslation()->getNumberOfPlurals($langcode); + } + + /** * Gets the string translation service. * * @return \Drupal\Core\StringTranslation\TranslationInterface diff --git a/core/lib/Drupal/Core/StringTranslation/TranslationInterface.php b/core/lib/Drupal/Core/StringTranslation/TranslationInterface.php index bc560cf..d3626a6 100644 --- a/core/lib/Drupal/Core/StringTranslation/TranslationInterface.php +++ b/core/lib/Drupal/Core/StringTranslation/TranslationInterface.php @@ -41,7 +41,7 @@ public function translate($string, array $args = array(), array $options = array * * This function ensures that the string is pluralized correctly. Since t() is * called by this function, make sure not to pass already-localized strings to - * it. + * it. See formatPluralTranslated() for that. * * For example: * @code @@ -71,7 +71,8 @@ public function translate($string, array $args = array(), array $options = array * of any key in this array are replaced with the corresponding value. * Based on the first character of the key, the value is escaped and/or * themed. See format_string(). Note that you do not need to include @count - * in this array; this replacement is done automatically for the plural case. + * in this array; this replacement is done automatically for the plural + * cases. * @param array $options * An associative array of additional options. See t() for allowed keys. * @@ -82,7 +83,53 @@ public function translate($string, array $args = array(), array $options = array * @see \Drupal\Component\Utility\String * @see t() * @see format_string() + * @see self::formatPluralTranslated */ public function formatPlural($count, $singular, $plural, array $args = array(), array $options = array()); + /** + * Formats an already translated string containing a count of items. + * + * This function ensures that the string is pluralized correctly. As opposed + * to the formatPlural() method, this method is designed to be invoked with + * a string already translated (such as with configuration translation). + * + * @param int $count + * The item count to display. + * @param string $translation + * The string containing the translation of a singular/plural pair. It may + * contain any number of possible variants (depending on the language + * translated to) separated by the value of the LOCALE_PLURAL_DELIMITER + * constant. + * @param array $args + * Associative array of replacements to make in the translation. Instances + * of any key in this array are replaced with the corresponding value. + * Based on the first character of the key, the value is escaped and/or + * themed. See format_string(). Note that you do not need to include @count + * in this array; this replacement is done automatically for the plural + * cases. + * @param array $options + * An associative array of additional options. t() defines all allowed keys. + * The 'context' key is not supported because the passed string is already + * translated. + * + * @return string + * The correct substring for the given $count with $args replaced. + * + * @see self::formatPlural + * @see format_string() + */ + public function formatPluralTranslated($count, $translation, array $args = array(), array $options = array()); + + /** + * Returns number of plurals supported by a given language. + * + * @param null $langcode + * (optional) The language code. If not provided, the current language + * will be used. + * @return int + * Number of plural variants supported by the given language. + */ + public function getNumberOfPlurals($langcode = NULL); + } diff --git a/core/lib/Drupal/Core/StringTranslation/TranslationManager.php b/core/lib/Drupal/Core/StringTranslation/TranslationManager.php index cdfc2df..9820d4c 100644 --- a/core/lib/Drupal/Core/StringTranslation/TranslationManager.php +++ b/core/lib/Drupal/Core/StringTranslation/TranslationManager.php @@ -10,6 +10,7 @@ use Drupal\Component\Utility\SafeMarkup; use Drupal\Component\Utility\String; use Drupal\Core\Language\LanguageManagerInterface; +use Drupal\Core\State\StateInterface; use Drupal\Core\StringTranslation\Translator\TranslatorInterface; /** @@ -54,14 +55,24 @@ class TranslationManager implements TranslationInterface, TranslatorInterface { protected $defaultLangcode; /** + * The state service. + * + * @var \Drupal\Core\State\StateInterface + */ + protected $state; + + /** * Constructs a TranslationManager object. * - * @param \Drupal\Core\Language\LanguageManagerInterface + * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager * The language manager. + * @param \Drupal\Core\State\StateInterface $state + * (optional) The state service. */ - public function __construct(LanguageManagerInterface $language_manager) { + public function __construct(LanguageManagerInterface $language_manager, StateInterface $state = NULL) { $this->languageManager = $language_manager; $this->defaultLangcode = $language_manager->getDefaultLanguage()->getId(); + $this->state = $state; } /** @@ -152,13 +163,20 @@ public function translate($string, array $args = array(), array $options = array * {@inheritdoc} */ public function formatPlural($count, $singular, $plural, array $args = array(), array $options = array()) { - $args['@count'] = $count; // Join both forms to search a translation. $translatable_string = implode(LOCALE_PLURAL_DELIMITER, array($singular, $plural)); // Translate as usual. $translated_strings = $this->translate($translatable_string, $args, $options); + return $this->formatPluralTranslated($count, $translated_strings, $args, $options); + } + + /** + * {@inheritdoc} + */ + public function formatPluralTranslated($count, $translation, array $args = array(), array $options = array()) { + $args['@count'] = $count; // Split joined translation strings into array. - $translated_array = explode(LOCALE_PLURAL_DELIMITER, $translated_strings); + $translated_array = explode(LOCALE_PLURAL_DELIMITER, $translation); if ($count == 1) { return SafeMarkup::set($translated_array[0]); @@ -183,7 +201,29 @@ public function formatPlural($count, $singular, $plural, array $args = array(), $return = $translated_array[1]; } } - return SafeMarkup::set($return); + + if (empty($args)) { + return SafeMarkup::set($return); + } + else { + return String::format($return, $args); + } + } + + /** + * @inheritdoc. + */ + public function getNumberOfPlurals($langcode = NULL) { + // If the state service is not injected, we assume 2 plural variants are + // allowed. This may happen in the installer for simplicity. + if (isset($this->state)) { + $langcode = $langcode ?: $this->languageManager->getCurrentLanguage()->getId(); + $plural_formulas = $this->state->get('locale.translation.plurals') ?: array(); + if (isset($plural_formulas[$langcode]['plurals'])) { + return $plural_formulas[$langcode]['plurals']; + } + } + return 2; } /** diff --git a/core/modules/config_translation/src/FormElement/FormElementBase.php b/core/modules/config_translation/src/FormElement/FormElementBase.php index 7db01e1..19114ee 100644 --- a/core/modules/config_translation/src/FormElement/FormElementBase.php +++ b/core/modules/config_translation/src/FormElement/FormElementBase.php @@ -91,6 +91,9 @@ public function getTranslationBuild(LanguageInterface $source_language, Language * A render array for the source value. */ protected function getSourceElement(LanguageInterface $source_language, $source_config) { + // @todo Should be able to render source as singular+plurals. Similar + // to TranslateEditForm::buildForm(), but here the source may also be + // multi-plural. if ($source_config) { $value = '' . nl2br($source_config) . ''; } @@ -161,6 +164,8 @@ protected function getSourceElement(LanguageInterface $source_language, $source_ */ protected function getTranslationElement(LanguageInterface $translation_language, $source_config, $translation_config) { // Add basic properties that apply to all form elements. + // @todo Needs support for possibly singular+plurals input if the source + // was singular/plural. As in TranslateEditForm::buildForm(). return array( '#title' => $this->t('!label (!source_language)', array( '!label' => $this->t($this->definition['label']), diff --git a/core/modules/file/config/install/views.view.files.yml b/core/modules/file/config/install/views.view.files.yml index 6783c1b..526d9fd 100644 --- a/core/modules/file/config/install/views.view.files.yml +++ b/core/modules/file/config/install/views.view.files.yml @@ -529,8 +529,7 @@ display: decimal: . separator: ',' format_plural: true - format_plural_singular: '1 place' - format_plural_plural: '@count places' + format_plural_string: "1 place\x03@count places" prefix: '' suffix: '' plugin_id: numeric @@ -952,8 +951,7 @@ display: decimal: . separator: ',' format_plural: false - format_plural_singular: '1' - format_plural_plural: '@count' + format_plural_string: "1\x03@count" prefix: '' suffix: '' plugin_id: numeric diff --git a/core/modules/forum/tests/modules/forum_test_views/test_views/views.view.test_forum_index.yml b/core/modules/forum/tests/modules/forum_test_views/test_views/views.view.test_forum_index.yml index 1f917f0..32a9306 100644 --- a/core/modules/forum/tests/modules/forum_test_views/test_views/views.view.test_forum_index.yml +++ b/core/modules/forum/tests/modules/forum_test_views/test_views/views.view.test_forum_index.yml @@ -141,8 +141,7 @@ display: decimal: . separator: ',' format_plural: false - format_plural_singular: '1' - format_plural_plural: '@count' + format_plural_string: "1\x03@count" prefix: '' suffix: '' plugin_id: numeric diff --git a/core/modules/statistics/tests/modules/statistics_test_views/test_views/views.view.test_statistics_integration.yml b/core/modules/statistics/tests/modules/statistics_test_views/test_views/views.view.test_statistics_integration.yml index e8099fa..680648b 100644 --- a/core/modules/statistics/tests/modules/statistics_test_views/test_views/views.view.test_statistics_integration.yml +++ b/core/modules/statistics/tests/modules/statistics_test_views/test_views/views.view.test_statistics_integration.yml @@ -160,8 +160,7 @@ display: decimal: . separator: '' format_plural: false - format_plural_singular: '1' - format_plural_plural: '@count' + format_plural_string: "1\x03@count" prefix: '' suffix: '' plugin_id: numeric @@ -218,8 +217,7 @@ display: decimal: . separator: '' format_plural: false - format_plural_singular: '1' - format_plural_plural: '@count' + format_plural_string: "1\x03@count" prefix: '' suffix: '' plugin_id: numeric diff --git a/core/modules/views/config/schema/views.field.schema.yml b/core/modules/views/config/schema/views.field.schema.yml index 0d636ac..69365e0 100644 --- a/core/modules/views/config/schema/views.field.schema.yml +++ b/core/modules/views/config/schema/views.field.schema.yml @@ -116,12 +116,9 @@ views.field.numeric: format_plural: type: boolean label: 'Format plural' - format_plural_singular: + format_plural_string: type: label - label: 'Singular form' - format_plural_plural: - type: label - label: 'Plural form' + label: 'Singular and one or more plurals' prefix: type: label label: 'Prefix' diff --git a/core/modules/views/src/Plugin/views/field/Numeric.php b/core/modules/views/src/Plugin/views/field/Numeric.php index da99d15..6b57d19 100644 --- a/core/modules/views/src/Plugin/views/field/Numeric.php +++ b/core/modules/views/src/Plugin/views/field/Numeric.php @@ -34,8 +34,7 @@ protected function defineOptions() { $options['decimal'] = array('default' => '.'); $options['separator'] = array('default' => ','); $options['format_plural'] = array('default' => FALSE); - $options['format_plural_singular'] = array('default' => '1'); - $options['format_plural_plural'] = array('default' => '@count'); + $options['format_plural_string'] = array('default' => '1' . LOCALE_PLURAL_DELIMITER . '@count'); $options['prefix'] = array('default' => ''); $options['suffix'] = array('default' => ''); @@ -93,28 +92,55 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) { '#description' => $this->t('If checked, special handling will be used for plurality.'), '#default_value' => $this->options['format_plural'], ); - $form['format_plural_singular'] = array( - '#type' => 'textfield', - '#title' => $this->t('Singular form'), - '#default_value' => $this->options['format_plural_singular'], - '#description' => $this->t('Text to use for the singular form.'), - '#states' => array( - 'visible' => array( - ':input[name="options[format_plural]"]' => array('checked' => TRUE), - ), - ), + $form['format_plural_string'] = array( + '#type' => 'value', + '#default_value' => $this->options['format_plural_string'], ); - $form['format_plural_plural'] = array( - '#type' => 'textfield', - '#title' => $this->t('Plural form'), - '#default_value' => $this->options['format_plural_plural'], - '#description' => $this->t('Text to use for the plural form, @count will be replaced with the value.'), - '#states' => array( - 'visible' => array( - ':input[name="options[format_plural]"]' => array('checked' => TRUE), + + // @todo Figure out how to pass in the language of the view. + $plural_array = explode(LOCALE_PLURAL_DELIMITER, $this->options['format_plural_string']); + $plurals = $this->getNumberOfPlurals(); + if ($plurals > 2) { + for ($i = 0; $i < $plurals; $i++) { + $form['format_plural_values'][$i] = array( + '#type' => 'textfield', + '#title' => ($i == 0 ? $this->t('Singular form') : $this->formatPlural($i, 'First plural form', '@count. plural form')), + '#default_value' => isset($plural_array[$i]) ? $plural_array[$i] : '', + '#description' => $this->t('Text to use for this variant, @count will be replaced with the value.'), + '#states' => array( + 'visible' => array( + ':input[name="options[format_plural]"]' => array('checked' => TRUE), + ), + ), + ); + } + } + else { + // Fallback for unknown number of plurals. + $form['format_plural_values'][0] = array( + '#type' => 'textfield', + '#title' => $this->t('Singular form'), + '#default_value' => $plural_array[0], + '#description' => $this->t('Text to use for the singular form.'), + '#states' => array( + 'visible' => array( + ':input[name="options[format_plural]"]' => array('checked' => TRUE), + ), ), - ), - ); + ); + $form['format_plural_values'][1] = array( + '#type' => 'textfield', + '#title' => $this->t('Plural form'), + '#default_value' => isset($plural_array[1]) ? $plural_array[1] : '', + '#description' => $this->t('Text to use for the plural form, @count will be replaced with the value.'), + '#states' => array( + 'visible' => array( + ':input[name="options[format_plural]"]' => array('checked' => TRUE), + ), + ), + ); + } + $form['prefix'] = array( '#type' => 'textfield', '#title' => $this->t('Prefix'), @@ -132,6 +158,18 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) { } /** + * @inheritdoc + */ + public function submitOptionsForm(&$form, FormStateInterface $form_state) { + // Merge plural format options into one string and drop the individual + // option values. + $options = &$form_state->getValue('options'); + $options['format_plural_string'] = implode(LOCALE_PLURAL_DELIMITER, $options['format_plural_values']); + unset($options['format_plural_values']); + parent::submitOptionsForm($form, $form_state); + } + + /** * {@inheritdoc} */ public function render(ResultRow $values) { @@ -156,7 +194,7 @@ public function render(ResultRow $values) { // Should we format as a plural. if (!empty($this->options['format_plural'])) { - $value = $this->formatPlural($value, $this->options['format_plural_singular'], $this->options['format_plural_plural']); + $value = $this->formatPluralTranslated($value, $this->options['format_plural_string']); } return $this->sanitizeValue($this->options['prefix'], 'xss')