diff --git a/core/core.services.yml b/core/core.services.yml index 589447d..28b7108 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -600,7 +600,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 ebb20e4..0946709 100644 --- a/core/lib/Drupal/Core/StringTranslation/StringTranslationTrait.php +++ b/core/lib/Drupal/Core/StringTranslation/StringTranslationTrait.php @@ -64,6 +64,17 @@ protected function formatPluralTranslated($count, $translated, array $args = arr } /** + * 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 3e866bb..f505dd8 100644 --- a/core/lib/Drupal/Core/StringTranslation/TranslationInterface.php +++ b/core/lib/Drupal/Core/StringTranslation/TranslationInterface.php @@ -120,4 +120,15 @@ public function formatPlural($count, $singular, $plural, array $args = array(), */ 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 edb391c..547bc96 100644 --- a/core/lib/Drupal/Core/StringTranslation/TranslationManager.php +++ b/core/lib/Drupal/Core/StringTranslation/TranslationManager.php @@ -9,6 +9,7 @@ use Drupal\Component\Utility\SafeMarkup; use Drupal\Core\Language\LanguageManagerInterface; +use Drupal\Core\State\StateInterface; use Drupal\Core\StringTranslation\Translator\TranslatorInterface; /** @@ -53,14 +54,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; } /** @@ -229,4 +240,20 @@ public function reset() { } } + /** + * @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..e13cc31 100644 --- a/core/modules/config_translation/src/FormElement/FormElementBase.php +++ b/core/modules/config_translation/src/FormElement/FormElementBase.php @@ -91,6 +91,7 @@ public function getTranslationBuild(LanguageInterface $source_language, Language * A render array for the source value. */ protected function getSourceElement(LanguageInterface $source_language, $source_config) { + // @todo Should support singular+plurals https://www.drupal.org/node/2454829 if ($source_config) { $value = '' . nl2br($source_config) . ''; } @@ -161,6 +162,7 @@ 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 Should support singular+plurals https://www.drupal.org/node/2454829 return array( '#title' => $this->t('!label (!source_language)', array( '!label' => $this->t($this->definition['label']), diff --git a/core/modules/file/config/optional/views.view.files.yml b/core/modules/file/config/optional/views.view.files.yml index 6783c1b..526d9fd 100644 --- a/core/modules/file/config/optional/views.view.files.yml +++ b/core/modules/file/config/optional/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 0b0e227..b8d3c4a 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/NumericField.php b/core/modules/views/src/Plugin/views/field/NumericField.php index 5e2db03..3f895f7 100644 --- a/core/modules/views/src/Plugin/views/field/NumericField.php +++ b/core/modules/views/src/Plugin/views/field/NumericField.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') diff --git a/core/modules/views/src/Tests/Plugin/NumericFormatPluralTest.php b/core/modules/views/src/Tests/Plugin/NumericFormatPluralTest.php new file mode 100644 index 0000000..a673ef4 --- /dev/null +++ b/core/modules/views/src/Tests/Plugin/NumericFormatPluralTest.php @@ -0,0 +1,158 @@ +web_user = $this->drupalCreateUser(array('administer views', 'administer languages')); + $this->drupalLogin($this->web_user); + } + + /** + * Test plural formatting setting on a numeric views handler. + */ + function testNumericFormatPlural() { + // Create a file. + $file = $this->createFile(); + + // Assert that the starting configuration is correct. + $config = $this->config('views.view.numeric_test'); + $field_config_prefix = 'display.default.display_options.fields.count.'; + $this->assertEqual($config->get($field_config_prefix . 'format_plural'), TRUE); + $this->assertEqual($config->get($field_config_prefix . 'format_plural_string'), '1' . LOCALE_PLURAL_DELIMITER . '@count'); + + // Assert that the value is displayed. + $this->drupalGet('numeric-test'); + $this->assertRaw('0'); + + // Assert that the user interface has controls to change it. + $this->drupalGet('admin/structure/views/nojs/handler/numeric_test/page_1/field/count'); + $this->assertFieldByName('options[format_plural_values][0]', '1'); + $this->assertFieldByName('options[format_plural_values][1]', '@count'); + + // Assert that changing the settings will change configuration properly. + $edit = ['options[format_plural_values][0]' => '1 time', 'options[format_plural_values][1]' => '@count times']; + $this->drupalPostForm(NULL, $edit, t('Apply')); + $this->drupalPostForm(NULL, array(), t('Save')); + + $config = $this->config('views.view.numeric_test'); + $field_config_prefix = 'display.default.display_options.fields.count.'; + $this->assertEqual($config->get($field_config_prefix . 'format_plural'), TRUE); + $this->assertEqual($config->get($field_config_prefix . 'format_plural_string'), '1 time' . LOCALE_PLURAL_DELIMITER . '@count times'); + + // Assert that the value is displayed with some sample values. + $numbers = [0, 1, 2, 3, 4, 42]; + foreach ($numbers as $i => $number) { + \Drupal::service('file.usage')->add($file, 'views_ui', 'dummy', $i, $number); + } + $this->drupalGet('numeric-test'); + foreach ($numbers as $i => $number) { + $this->assertRaw('' . $number . ($number == 1 ? ' time' : ' times') . ''); + } + + // Add Slovenian and set its plural formula to test multiple plural forms. + $edit = ['predefined_langcode' => 'sl']; + $this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language')); + $formula = 'nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);'; + $header = new PoHeader(); + list($nplurals, $formula) = $header->parsePluralForms($formula); + debug($formula); + \Drupal::state()->set('locale.translation.plurals', ['sl' => ['plurals' => $nplurals, 'formula' => $formula]]); + + // @todo change the language of the view here once the handler considers that + + // Assert that the user interface has controls with more inputs now. + $this->drupalGet('sl/admin/structure/views/nojs/handler/numeric_test/page_1/field/count'); + $this->assertFieldByName('options[format_plural_values][0]', '1 time'); + $this->assertFieldByName('options[format_plural_values][1]', '@count times'); + $this->assertFieldByName('options[format_plural_values][2]', ''); + $this->assertFieldByName('options[format_plural_values][3]', ''); + + // Assert that changing the settings will change configuration properly. + $edit = [ + 'options[format_plural_values][0]' => '@count time0', + 'options[format_plural_values][1]' => '@count time1', + 'options[format_plural_values][2]' => '@count time2', + 'options[format_plural_values][3]' => '@count time3', + ]; + $this->drupalPostForm(NULL, $edit, t('Apply')); + $this->drupalPostForm(NULL, array(), t('Save')); + $config = $this->config('views.view.numeric_test'); + $field_config_prefix = 'display.default.display_options.fields.count.'; + $this->assertEqual($config->get($field_config_prefix . 'format_plural'), TRUE); + $this->assertEqual($config->get($field_config_prefix . 'format_plural_string'), implode(LOCALE_PLURAL_DELIMITER, array_values($edit))); + + // The view should now use the new plural configuration. + $this->drupalGet('sl/numeric-test'); + $this->assertRaw('0 time3'); + $this->assertRaw('1 time0'); + $this->assertRaw('2 time1'); + $this->assertRaw('3 time2'); + $this->assertRaw('4 time2'); + $this->assertRaw('42 time3'); + } + + /** + * Creates and saves a test file. + * + * @return \Drupal\Core\Entity\EntityInterface + * A file entity. + */ + protected function createFile() { + // Create a new file entity. + $file = entity_create('file', array( + 'uid' => 1, + 'filename' => 'druplicon.txt', + 'uri' => 'public://druplicon.txt', + 'filemime' => 'text/plain', + 'created' => 1, + 'changed' => 1, + 'status' => FILE_STATUS_PERMANENT, + )); + file_put_contents($file->getFileUri(), 'hello world'); + + // Save it, inserting a new record. + $file->save(); + + return $file; + } +} diff --git a/core/modules/views/tests/modules/views_test_config/test_views/views.view.numeric_test.yml b/core/modules/views/tests/modules/views_test_config/test_views/views.view.numeric_test.yml new file mode 100644 index 0000000..e8e99ad --- /dev/null +++ b/core/modules/views/tests/modules/views_test_config/test_views/views.view.numeric_test.yml @@ -0,0 +1,189 @@ +uuid: 6f602122-2918-44c7-8b05-5d6c1e93e6ac +langcode: en +status: true +dependencies: + module: + - file + - user +id: numeric_test +label: 'Numeric test' +module: views +description: '' +tag: '' +base_table: file_managed +base_field: fid +core: 8.x +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + display_options: + access: + type: perm + options: + perm: 'administer views' + cache: + type: none + options: { } + query: + type: views_query + options: + disable_sql_rewrite: false + distinct: false + replica: false + query_comment: '' + query_tags: { } + exposed_form: + type: basic + options: + submit_button: Apply + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + pager: + type: full + options: + items_per_page: 10 + offset: 0 + id: 0 + total_pages: null + expose: + items_per_page: false + items_per_page_label: 'Items per page' + items_per_page_options: '5, 10, 25, 50' + items_per_page_options_all: false + items_per_page_options_all_label: '- All -' + offset: false + offset_label: Offset + tags: + previous: '‹ previous' + next: 'next ›' + first: '« first' + last: 'last »' + quantity: 9 + style: + type: default + row: + type: fields + fields: + filename: + id: filename + table: file_managed + field: filename + entity_type: file + entity_field: filename + label: '' + alter: + alter_text: false + make_link: false + absolute: false + trim: false + word_boundary: false + ellipsis: false + strip_tags: false + html: false + hide_empty: false + empty_zero: false + link_to_file: true + plugin_id: file + relationship: none + group_type: group + admin_label: '' + exclude: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_alter_empty: true + count: + id: count + table: file_usage + field: count + relationship: none + group_type: group + admin_label: '' + label: '' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: false + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + set_precision: false + precision: 0 + decimal: . + separator: ',' + format_plural: true + format_plural_string: "1\x03@count" + prefix: '' + suffix: '' + plugin_id: numeric + filters: { } + sorts: { } + title: 'Numeric test' + header: { } + footer: { } + empty: { } + relationships: { } + arguments: { } + display_extenders: { } + cache_metadata: + contexts: + - language + cacheable: false + page_1: + display_plugin: page + id: page_1 + display_title: Page + position: 1 + display_options: + display_extenders: { } + path: numeric-test + cache_metadata: + contexts: + - language + cacheable: false