diff --git a/README.md b/README.md index a3c72e2..d5e2a9f 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,6 @@ Then: $ docker-compose exec -u web web composer install $ docker-compose exec -u node node npm install $ docker-compose exec -u node node npm run build -$ docker-compose exec -u web web ./vendor/bin/run drupal:site-setup $ docker-compose exec -u web web ./vendor/bin/run drupal:site-install ``` @@ -170,7 +169,7 @@ This is due to the following [Drupal Console issue][11]. ## Demo module The OpenEuropa Multilingual module ships with a demo module which provides all the necessary configuration and code needed -to showcase the modules's most important features. +to showcase the modules' most important features. The demo module includes a translatable content type with automatic URL path generation. diff --git a/behat.yml.dist b/behat.yml.dist index 167d81f..bec0232 100644 --- a/behat.yml.dist +++ b/behat.yml.dist @@ -7,6 +7,7 @@ default: - Drupal\DrupalExtension\Context\MinkContext - Drupal\DrupalExtension\Context\DrupalContext - Drupal\Tests\oe_multilingual\Behat\DrupalContext + - Drupal\Tests\oe_multilingual\Behat\MinkContext extensions: Behat\MinkExtension: goutte: ~ diff --git a/composer.json b/composer.json index 93b11eb..28090a9 100644 --- a/composer.json +++ b/composer.json @@ -6,10 +6,11 @@ "minimum-stability": "dev", "prefer-stable": true, "require": { + "php": "^7.1", "drupal/administration_language_negotiation": "1.4", "drupal/core": "~8.6", - "drupal/pathauto": "1.2", - "php": "^7.1" + "drupal/language_selection_page": "^2.2", + "drupal/pathauto": "^1.2" }, "require-dev": { "cweagans/composer-patches": "~1.0", @@ -18,10 +19,11 @@ "drupal/console": "^1.6", "drupal/config_devel": "~1.2", "drush/drush": "^9", + "drupal/devel": "^1.2", "drupal/drupal-extension": "^4.0.0@alpha", "openeuropa/code-review": "^0.2", "openeuropa/task-runner": "^0.5", - "openeuropa/oe_theme": "dev-master", + "openeuropa/oe_theme": "^0.2", "webflo/drupal-core-require-dev": "8.6.x" }, "scripts": { diff --git a/modules/oe_multilingual_selection_page/README.md b/modules/oe_multilingual_selection_page/README.md new file mode 100644 index 0000000..7d3d88f --- /dev/null +++ b/modules/oe_multilingual_selection_page/README.md @@ -0,0 +1,7 @@ +# OpenEuropa Multilingual Selection Page + +The OpenEuropa Multilingual Selection Page module will present users with a language selection page, as soon as their +visit the site. + +**Disclaimer**: when installed this module will update existing configuration by enabling the language selection page +negotiation method, make sure you review these changes before deploying your configuration. diff --git a/modules/oe_multilingual_selection_page/oe_multilingual_selection_page.info.yml b/modules/oe_multilingual_selection_page/oe_multilingual_selection_page.info.yml new file mode 100644 index 0000000..a263f21 --- /dev/null +++ b/modules/oe_multilingual_selection_page/oe_multilingual_selection_page.info.yml @@ -0,0 +1,9 @@ +name: 'OpenEuropa Multilingual Selection Page' +type: module +description: 'Generates a Language Selection Page used on first access to a multilingual site.' +core: 8.x +package: 'OpenEuropa' + +dependencies: + - drupal:language_selection_page + - oe_multilingual:oe_multilingual diff --git a/modules/oe_multilingual_selection_page/oe_multilingual_selection_page.install b/modules/oe_multilingual_selection_page/oe_multilingual_selection_page.install new file mode 100644 index 0000000..0ac8893 --- /dev/null +++ b/modules/oe_multilingual_selection_page/oe_multilingual_selection_page.install @@ -0,0 +1,42 @@ +getEditable('language_selection_page.negotiation') + ->set('path', '/select-language') + ->set('type', 'standalone') + ->set('ignore_neutral', FALSE) + ->set('blacklisted_paths', [ + '/admin', + '/user', + '/admin/*', + '/admin*', + '/node/add/*', + '/node/*/edit', + ])->save(); + + /** @var \Drupal\oe_multilingual\LanguageNegotiationSetterInterface $setter */ + $setter = \Drupal::service('oe_multilingual.language_negotiation_setter'); + + // Add language selection page negotiation method. + // Since this is an optional module setting configuration in its + // hook_install() might cause unexpected behaviors. + // We are discussing implications in the following ticket: + // https://webgate.ec.europa.eu/CITnet/jira/browse/OPENEUROPA-600 + $setter->addInterfaceSettings([ + LanguageNegotiationLanguageSelectionPage::METHOD_ID => -18, + ]); +} diff --git a/modules/oe_multilingual_selection_page/oe_multilingual_selection_page.module b/modules/oe_multilingual_selection_page/oe_multilingual_selection_page.module new file mode 100644 index 0000000..278e10d --- /dev/null +++ b/modules/oe_multilingual_selection_page/oe_multilingual_selection_page.module @@ -0,0 +1,27 @@ + $value) { + $language_code = $key; + $url = $value['#url']->toString(); + + $variables['languages'][] = [ + 'href' => $url, + 'hreflang' => $language_code, + 'label' => $value['#title'], + 'lang' => $language_code, + ]; + } +} diff --git a/oe_multilingual.install b/oe_multilingual.install index 32dd1a7..7f0a5ee 100644 --- a/oe_multilingual.install +++ b/oe_multilingual.install @@ -36,29 +36,31 @@ function oe_multilingual_install() { '/node/*/translations', ])->save(); + // Make sure that English language prefix is set to "en". + \Drupal::configFactory() + ->getEditable('language.negotiation') + ->set('url.prefixes.en', 'en') + ->save(); + + /** @var \Drupal\oe_multilingual\LanguageNegotiationSetterInterface $setter */ + $setter = \Drupal::service('oe_multilingual.language_negotiation_setter'); + + // Set default language negotiation methods. + $setter->enableNegotiationMethods([ + LanguageInterface::TYPE_INTERFACE, + LanguageInterface::TYPE_CONTENT, + ]); + // For interface negotiation make sure administrative pages are in English. - $interface_settings = [ + $setter->setInterfaceSettings([ LanguageNegotiationAdministrationLanguage::METHOD_ID => -20, LanguageNegotiationUrl::METHOD_ID => -19, LanguageNegotiationSelected::METHOD_ID => 20, - ]; + ]); // For content negotiation make sure that content respects URL language. - $content_settings = [ + $setter->setContentSettings([ LanguageNegotiationUrl::METHOD_ID => -19, LanguageNegotiationSelected::METHOD_ID => 20, - ]; - - // Set default language negotiation methods with related weights. - \Drupal::configFactory() - ->getEditable('language.types') - ->set('configurable', [ - LanguageInterface::TYPE_INTERFACE, - LanguageInterface::TYPE_CONTENT, - ]) - ->set('negotiation.' . LanguageInterface::TYPE_INTERFACE . '.enabled', $interface_settings) - ->set('negotiation.' . LanguageInterface::TYPE_INTERFACE . '.method_weights', $interface_settings) - ->set('negotiation.' . LanguageInterface::TYPE_CONTENT . '.enabled', $content_settings) - ->set('negotiation.' . LanguageInterface::TYPE_CONTENT . '.method_weights', $content_settings) - ->save(); + ]); } diff --git a/oe_multilingual.services.yml b/oe_multilingual.services.yml index 41e2341..2e8c606 100644 --- a/oe_multilingual.services.yml +++ b/oe_multilingual.services.yml @@ -1,4 +1,7 @@ services: + oe_multilingual.language_negotiation_setter: + class: Drupal\oe_multilingual\LanguageNegotiationSetter + arguments: ['@config.factory'] oe_multilingual.helper: class: Drupal\oe_multilingual\MultilingualHelper arguments: ['@entity.repository', '@current_route_match', '@language_manager'] diff --git a/runner.yml.dist b/runner.yml.dist index 8813e30..2dc93d9 100644 --- a/runner.yml.dist +++ b/runner.yml.dist @@ -9,6 +9,7 @@ drupal: password: "" post_install: - "./vendor/bin/drush en config_devel -y" + - "./vendor/bin/drush en devel -y" - "./vendor/bin/drush en oe_multilingual -y" - "./vendor/bin/drush en oe_multilingual_demo -y" - "./vendor/bin/drush pmu big_pipe -y" diff --git a/src/LanguageNegotiationSetter.php b/src/LanguageNegotiationSetter.php new file mode 100644 index 0000000..e7b3985 --- /dev/null +++ b/src/LanguageNegotiationSetter.php @@ -0,0 +1,94 @@ +configFactory = $config_factory; + } + + /** + * {@inheritdoc} + */ + public function enableNegotiationMethods(array $methods): void { + $this->configFactory + ->getEditable(self::CONFIG_NAME) + ->set('configurable', $methods) + ->save(); + } + + /** + * {@inheritdoc} + */ + public function setInterfaceSettings(array $settings): void { + $this->configFactory + ->getEditable(self::CONFIG_NAME) + ->set('negotiation.' . LanguageInterface::TYPE_INTERFACE . '.enabled', $settings) + ->set('negotiation.' . LanguageInterface::TYPE_INTERFACE . '.method_weights', $settings) + ->save(); + } + + /** + * {@inheritdoc} + */ + public function addInterfaceSettings(array $settings): void { + $current = $this->configFactory + ->get(self::CONFIG_NAME) + ->get('negotiation.' . LanguageInterface::TYPE_INTERFACE . '.enabled'); + + $settings = array_merge($current, $settings); + asort($settings); + + $this->setInterfaceSettings($settings); + } + + /** + * {@inheritdoc} + */ + public function setContentSettings(array $settings): void { + $this->configFactory + ->getEditable(self::CONFIG_NAME) + ->set('negotiation.' . LanguageInterface::TYPE_CONTENT . '.enabled', $settings) + ->set('negotiation.' . LanguageInterface::TYPE_CONTENT . '.method_weights', $settings) + ->save(); + } + + /** + * {@inheritdoc} + */ + public function addContentSettings(array $settings): void { + $current = $this->configFactory + ->get(self::CONFIG_NAME) + ->get('negotiation.' . LanguageInterface::TYPE_CONTENT . '.enabled'); + + $settings = array_merge($current, $settings); + asort($settings); + + $this->setContentSettings($settings); + } + +} diff --git a/src/LanguageNegotiationSetterInterface.php b/src/LanguageNegotiationSetterInterface.php new file mode 100644 index 0000000..3001b4b --- /dev/null +++ b/src/LanguageNegotiationSetterInterface.php @@ -0,0 +1,82 @@ +setInterfaceSettings([ + * LanguageNegotiationUrl::METHOD_ID => -19, + * LanguageNegotiationSelected::METHOD_ID => 20, + * ]); + * + * @param array $settings + * Array of language negotiation method names with their weights. + */ + public function setInterfaceSettings(array $settings): void; + + /** + * Add given settings to current interface language negotiation. + * + * Usage: + * + * \Drupal::service('oe_multilingual.language_negotiation_setter') + * ->addInterfaceSettings([ + * LanguageNegotiationUrl::METHOD_ID => -19, + * ]); + * + * @param array $settings + * Array of language negotiation method names with their weights. + */ + public function addInterfaceSettings(array $settings): void; + + /** + * Set content language negotiation settings. + * + * Usage: + * + * \Drupal::service('oe_multilingual.language_negotiation_setter') + * ->setContentSettings([ + * LanguageNegotiationUrl::METHOD_ID => -19, + * LanguageNegotiationSelected::METHOD_ID => 20, + * ]); + * + * @param array $settings + * Array of language negotiation method names with their weights. + */ + public function setContentSettings(array $settings): void; + + /** + * Add given settings to current content language negotiation. + * + * Usage: + * + * \Drupal::service('oe_multilingual.language_negotiation_setter') + * ->addContentSettings([ + * LanguageNegotiationUrl::METHOD_ID => -19, + * ]); + * + * @param array $settings + * Array of language negotiation method names with their weights. + */ + public function addContentSettings(array $settings): void; + +} diff --git a/tests/Behat/DrupalContext.php b/tests/Behat/DrupalContext.php index e8543b2..be1c7a1 100644 --- a/tests/Behat/DrupalContext.php +++ b/tests/Behat/DrupalContext.php @@ -4,6 +4,8 @@ namespace Drupal\Tests\oe_multilingual\Behat; +use Behat\Behat\Hook\Scope\AfterScenarioScope; +use Behat\Behat\Hook\Scope\BeforeScenarioScope; use Behat\Gherkin\Node\TableNode; use Drupal\Core\Entity\ContentEntityInterface; use Drupal\DrupalExtension\Context\RawDrupalContext; @@ -15,6 +17,33 @@ */ class DrupalContext extends RawDrupalContext { + /** + * Enable OpenEuropa Multilingual Selection Page module. + * + * @param \Behat\Behat\Hook\Scope\BeforeScenarioScope $scope + * The Hook scope. + * + * @BeforeScenario @selection-page + */ + public function setupSelectionPage(BeforeScenarioScope $scope): void { + \Drupal::service('module_installer')->install(['oe_multilingual_selection_page']); + } + + /** + * Disable OpenEuropa Multilingual Selection Page module. + * + * @param \Behat\Behat\Hook\Scope\AfterScenarioScope $scope + * The Hook scope. + * + * @AfterScenario @selection-page + */ + public function revertSelectionPage(AfterScenarioScope $scope): void { + \Drupal::service('module_installer')->uninstall([ + 'oe_multilingual_selection_page', + 'language_selection_page', + ]); + } + /** * Create content given its type and fields. * diff --git a/tests/Behat/MinkContext.php b/tests/Behat/MinkContext.php new file mode 100644 index 0000000..8131cf7 --- /dev/null +++ b/tests/Behat/MinkContext.php @@ -0,0 +1,21 @@ +assertSession()->addressMatches("/.*\/select-language/"); + } + +} diff --git a/tests/Kernel/LanguageNegotiationSetterTest.php b/tests/Kernel/LanguageNegotiationSetterTest.php new file mode 100644 index 0000000..2a10b0e --- /dev/null +++ b/tests/Kernel/LanguageNegotiationSetterTest.php @@ -0,0 +1,129 @@ +installConfig([ + 'locale', + 'language', + 'oe_multilingual', + ]); + + $this->service = $this->container->get('oe_multilingual.language_negotiation_setter'); + } + + /** + * Test setting language negotiation settings. + */ + public function testSetSettings(): void { + $this->service->setInterfaceSettings([ + 'foo' => -20, + 'bar' => -19, + ]); + $this->service->setContentSettings([ + 'bar' => -20, + 'foo' => -19, + ]); + + $settings = $this + ->config('language.types') + ->get('negotiation.language_interface'); + + $this->assertEquals([ + 'enabled' => [ + 'foo' => -20, + 'bar' => -19, + ], + 'method_weights' => [ + 'foo' => -20, + 'bar' => -19, + ], + ], $settings); + + $settings = $this + ->config('language.types') + ->get('negotiation.language_content'); + + $this->assertEquals([ + 'enabled' => [ + 'bar' => -20, + 'foo' => -19, + ], + 'method_weights' => [ + 'bar' => -20, + 'foo' => -19, + ], + ], $settings); + } + + /** + * Test adding language negotiation settings to existing ones. + */ + public function testAddSettings(): void { + $this->service->setInterfaceSettings([ + 'foo' => -20, + 'bar' => -18, + ]); + $this->service->setContentSettings([ + 'foo' => -20, + 'bar' => -18, + ]); + + $this->service->addInterfaceSettings(['boo' => -19]); + $this->service->addContentSettings(['boo' => -19]); + + foreach (['language_interface', 'language_content'] as $type) { + $settings = $this + ->config('language.types') + ->get('negotiation.' . $type); + + $this->assertEquals([ + 'enabled' => [ + 'foo' => -20, + 'boo' => -19, + 'bar' => -18, + ], + 'method_weights' => [ + 'foo' => -20, + 'boo' => -19, + 'bar' => -18, + ], + ], $settings); + } + } + +} diff --git a/tests/features/language-selection.feature b/tests/features/language-selection.feature new file mode 100644 index 0000000..9e90ee9 --- /dev/null +++ b/tests/features/language-selection.feature @@ -0,0 +1,33 @@ +@api @selection-page +Feature: Language selection + In order to be able choose the initial language of the site + As a visitor + I want to be presented with a language selection page + + Background: + Given the following "Demo translatable page" content item: + | Title | Test page | + | Body | Hello world | + And the following "French" translation for the "Demo translatable page" with title "Test page": + | Title | Page de test | + | Body | Bonjour le monde | + And the following "Spanish" translation for the "Demo translatable page" with title "Test page": + | Title | Página de prueba | + | Body | Hola Mundo | + + Scenario: When I visit the homepage I'm presented with a language selection page + + Given I am on the homepage + Then I should be redirected to the language selection page + + When I click "French" + Then the url should match "/fr" + + Scenario: Users visiting a page should be presented with the language selection page, + if no language is detected in the URL. + + Given I visit the "Test page" content + Then I should be redirected to the language selection page + + When I click "French" + Then the url should match "/fr/page-de-test"