From 84b6f7700134c4f37acd1bb73d1fe5222772bccb Mon Sep 17 00:00:00 2001 From: Adrian Cocorev Date: Fri, 26 Apr 2024 20:29:00 +0300 Subject: [PATCH 1/2] ETRANS-0000: Block with eTrans - Language Concept 2023. --- .../oe_webtools_etrans_lc2023/README.md | 51 +++ .../oe_webtools_etrans_lc2023.schema.yml | 25 ++ .../js/oe-webtools-lc2023.js | 56 +++ .../oe_webtools_etrans_lc2023.info.yml | 8 + .../oe_webtools_etrans_lc2023.libraries.yml | 7 + .../oe_webtools_etrans_lc2023.module | 41 +++ .../oe_webtools_etrans_lc2023.services.yml | 10 + .../ETransLanguagesEventSubscriber.php | 323 ++++++++++++++++++ .../src/Plugin/Block/ETransBlockLc2023.php | 313 +++++++++++++++++ .../src/Service/ETransService.php | 112 ++++++ .../oe-webtools-etrans-lc2023.html.twig | 50 +++ 11 files changed, 996 insertions(+) create mode 100644 modules/oe_webtools_etrans/modules/oe_webtools_etrans_lc2023/README.md create mode 100644 modules/oe_webtools_etrans/modules/oe_webtools_etrans_lc2023/config/schema/oe_webtools_etrans_lc2023.schema.yml create mode 100644 modules/oe_webtools_etrans/modules/oe_webtools_etrans_lc2023/js/oe-webtools-lc2023.js create mode 100644 modules/oe_webtools_etrans/modules/oe_webtools_etrans_lc2023/oe_webtools_etrans_lc2023.info.yml create mode 100644 modules/oe_webtools_etrans/modules/oe_webtools_etrans_lc2023/oe_webtools_etrans_lc2023.libraries.yml create mode 100644 modules/oe_webtools_etrans/modules/oe_webtools_etrans_lc2023/oe_webtools_etrans_lc2023.module create mode 100644 modules/oe_webtools_etrans/modules/oe_webtools_etrans_lc2023/oe_webtools_etrans_lc2023.services.yml create mode 100644 modules/oe_webtools_etrans/modules/oe_webtools_etrans_lc2023/src/EventSubscriber/ETransLanguagesEventSubscriber.php create mode 100755 modules/oe_webtools_etrans/modules/oe_webtools_etrans_lc2023/src/Plugin/Block/ETransBlockLc2023.php create mode 100644 modules/oe_webtools_etrans/modules/oe_webtools_etrans_lc2023/src/Service/ETransService.php create mode 100644 modules/oe_webtools_etrans/modules/oe_webtools_etrans_lc2023/templates/oe-webtools-etrans-lc2023.html.twig diff --git a/modules/oe_webtools_etrans/modules/oe_webtools_etrans_lc2023/README.md b/modules/oe_webtools_etrans/modules/oe_webtools_etrans_lc2023/README.md new file mode 100644 index 00000000..c87adfb7 --- /dev/null +++ b/modules/oe_webtools_etrans/modules/oe_webtools_etrans_lc2023/README.md @@ -0,0 +1,51 @@ +# OpenEuropa Webtools eTrans - Language Concept 2023 + +This Drupal 10 custom module provides a block that adds machine translation +capabilities for the preferred EU language of a user. It is based on the +eTrans - Language Concept 2023 from OpenEuropa Webtools. + +More information about this concept can be found +[here](https://webtools.europa.eu/showcase-demo/resources/etrans/demo/links/demo/lc2023/lc2023_live.html). + +## Features + +When a user attempts to change the language of the page and the selected +language is one of the 24 languages supported by eTranslation, the user +stays on the same page. A banner for the selected language appears where +the user can use machine translation for this page. +The module also supports live translation. + +## Dependencies + +This module depends on the `oe_multilingual` module. + +## How to use + +1. Install the module as you would any other Drupal module. +2. Navigate to the block layout page (`/admin/structure/block`) +3. Locate the region where you want to place the block and click the + "Place block" button. +4. From the list of available blocks, find "OpenEuropa Webtools eTrans - + Language Concept 2023" and click the "Place block" button next to it. +5. Configure the block settings as needed and click "Save block". + +The OpenEuropa Webtools eTrans - Language Concept 2023 block should now +appear on your site in the chosen region and provide machine translation +capabilities for the preferred EU language of a user. + +## Configuration + +The block has a number of settings that control its behavior: + +- **Receiver**: The ID of a HTML element in which the eTrans component + will be rendered. +- **Domain**: The domain for translation, can be 'General text' or 'EU + formal language'. +- **Delay**: The time in milliseconds to delay rendering the translation. +- **Live translation**: When enabled, all pages visited by a user will be + automatically translated to the selected language until the user cancels + the translation process. +- **Include**: A list of CSS selectors indicating the page elements to be + translated. +- **Exclude**: A list of CSS selectors indicating page elements to be + excluded from the translation. diff --git a/modules/oe_webtools_etrans/modules/oe_webtools_etrans_lc2023/config/schema/oe_webtools_etrans_lc2023.schema.yml b/modules/oe_webtools_etrans/modules/oe_webtools_etrans_lc2023/config/schema/oe_webtools_etrans_lc2023.schema.yml new file mode 100644 index 00000000..6938a552 --- /dev/null +++ b/modules/oe_webtools_etrans/modules/oe_webtools_etrans_lc2023/config/schema/oe_webtools_etrans_lc2023.schema.yml @@ -0,0 +1,25 @@ +block.block.oe_webtools_etrans_lc2023: + type: block_settings + label: 'OpenEuropa Webtools eTrans - Language Concept 2023 Block settings' + mapping: + receiver: + type: string + label: 'Receiver' + domain: + type: string + label: 'Domain' + delay: + type: integer + label: 'Delay' + source: + type: string + label: 'Source' + include: + type: string + label: 'Include' + exclude: + type: string + label: 'Exclude' + live: + type: boolean + label: 'Live' diff --git a/modules/oe_webtools_etrans/modules/oe_webtools_etrans_lc2023/js/oe-webtools-lc2023.js b/modules/oe_webtools_etrans/modules/oe_webtools_etrans_lc2023/js/oe-webtools-lc2023.js new file mode 100644 index 00000000..b22b82f5 --- /dev/null +++ b/modules/oe_webtools_etrans/modules/oe_webtools_etrans_lc2023/js/oe-webtools-lc2023.js @@ -0,0 +1,56 @@ +(function ($, Drupal) { + "use strict"; + + Drupal.behaviors.oeWTlc2023Behavior = { + attach: function (context, settings) { + // Initialize jQuery objects + const $etransMessage = $(once('wt-ecl-etrans-translate-message', '.wt-ecl-etrans-message', context)); + const $translateLink = $(once('wt-ecl-translate-link', '.wt-ecl-etrans-message a#_translate', context)); + const $closeLink = $(once('wt-ecl-translate-close', 'button.wt-ecl-message__close', context)); + const $defaultLanguage = $('a.oe-webtools-lc2023-default-language', context); + + // Attach event handlers + if ($translateLink.length) handleTranslateLink($translateLink, $etransMessage, settings); + if ($etransMessage.length) handleEtransMessage($etransMessage, $closeLink, context); + if ($defaultLanguage.length) handleDefaultLanguage($defaultLanguage); + } + }; + + // Translate link click event handler + function handleTranslateLink($translateLink, $etransMessage, settings) { + $translateLink.on('click', function (e) { + e.preventDefault(); + $wt.etrans.translate("body", settings.oe_wt_etrans.preferred_language); + $etransMessage.hide(); + }); + } + + // Etrans message event handlers + function handleEtransMessage($etransMessage, $closeLink, context) { + $(window, context).on('wtTranslationAbort wtTranslationStart', function (event) { + $etransMessage.toggle(event.type === 'wtTranslationAbort'); + }); + + $closeLink.on('click', function () { + $etransMessage.hide(); + removePrefLangFromUrl(); + }); + } + + // Default language click event handler + function handleDefaultLanguage($defaultLanguage) { + $defaultLanguage.on('click', function () { + if ($wt && $wt.etrans.isTranslated()) { + $wt.etrans.removeLiveCookie(); + } + }); + } + + // Function to remove 'prefLang' parameter from URL + function removePrefLangFromUrl() { + const url = new URL(window.location.href); + url.searchParams.delete('prefLang'); + window.history.pushState({}, '', url); + } + +})(jQuery, Drupal); diff --git a/modules/oe_webtools_etrans/modules/oe_webtools_etrans_lc2023/oe_webtools_etrans_lc2023.info.yml b/modules/oe_webtools_etrans/modules/oe_webtools_etrans_lc2023/oe_webtools_etrans_lc2023.info.yml new file mode 100644 index 00000000..5d97a6d8 --- /dev/null +++ b/modules/oe_webtools_etrans/modules/oe_webtools_etrans_lc2023/oe_webtools_etrans_lc2023.info.yml @@ -0,0 +1,8 @@ +name: OpenEuropa Webtools eTrans - Language Concept 2023 +description: Provides a block that adds machine translation possibility for preferred EU language using Language Concept 2023 +package: OpenEuropa Webtools +type: module +core_version_requirement: ^10 +dependencies: + - oe_webtools:oe_webtools + - oe_multilingual:oe_multilingual diff --git a/modules/oe_webtools_etrans/modules/oe_webtools_etrans_lc2023/oe_webtools_etrans_lc2023.libraries.yml b/modules/oe_webtools_etrans/modules/oe_webtools_etrans_lc2023/oe_webtools_etrans_lc2023.libraries.yml new file mode 100644 index 00000000..127ee860 --- /dev/null +++ b/modules/oe_webtools_etrans/modules/oe_webtools_etrans_lc2023/oe_webtools_etrans_lc2023.libraries.yml @@ -0,0 +1,7 @@ +oe_wt_etrans_lc2023: + js: + js/oe-webtools-lc2023.js: {} + dependencies: + - core/jquery + - core/once + - core/drupal diff --git a/modules/oe_webtools_etrans/modules/oe_webtools_etrans_lc2023/oe_webtools_etrans_lc2023.module b/modules/oe_webtools_etrans/modules/oe_webtools_etrans_lc2023/oe_webtools_etrans_lc2023.module new file mode 100644 index 00000000..96532194 --- /dev/null +++ b/modules/oe_webtools_etrans/modules/oe_webtools_etrans_lc2023/oe_webtools_etrans_lc2023.module @@ -0,0 +1,41 @@ + [ + 'variables' => [ + 'oe_wt_etrans_script' => NULL, + 'oe_wt_etrans_lc2023' => NULL, + ], + ], + ]; +} + +/** + * Implements hook_language_switch_links_alter(). + */ +function oe_webtools_etrans_lc2023_language_switch_links_alter(array &$links, $type, Url $url) { + $default_language_code = \Drupal::languageManager()->getDefaultLanguage()->getId(); + foreach ($links as $lang_code => $link) { + if ($lang_code !== $default_language_code) { + continue; + } + // Set default language class. + $links[$default_language_code]['attributes']['class'][] = 'oe-webtools-lc2023-default-language'; + if (isset($links[$default_language_code]['query']['prefLang'])) { + unset($links[$default_language_code]['query']['prefLang']); + } + } +} diff --git a/modules/oe_webtools_etrans/modules/oe_webtools_etrans_lc2023/oe_webtools_etrans_lc2023.services.yml b/modules/oe_webtools_etrans/modules/oe_webtools_etrans_lc2023/oe_webtools_etrans_lc2023.services.yml new file mode 100644 index 00000000..951c9135 --- /dev/null +++ b/modules/oe_webtools_etrans/modules/oe_webtools_etrans_lc2023/oe_webtools_etrans_lc2023.services.yml @@ -0,0 +1,10 @@ +services: + oe_webtools_etrans_lc2023.etrans_service: + class: Drupal\oe_webtools_etrans_lc2023\Service\ETransService + arguments: [ '@entity_type.manager', '@language_manager' , '@module_handler'] + + oe_webtools_etrans_lc2023.etrans_eu_languages_event_subscriber: + class: Drupal\oe_webtools_etrans_lc2023\EventSubscriber\ETransLanguagesEventSubscriber + arguments : ['@stream_wrapper_manager', '@language_manager', '@entity_type.manager', '@oe_webtools_etrans_lc2023.etrans_service', '@block.repository'] + tags: + - { name: event_subscriber } diff --git a/modules/oe_webtools_etrans/modules/oe_webtools_etrans_lc2023/src/EventSubscriber/ETransLanguagesEventSubscriber.php b/modules/oe_webtools_etrans/modules/oe_webtools_etrans_lc2023/src/EventSubscriber/ETransLanguagesEventSubscriber.php new file mode 100644 index 00000000..b6fdfc40 --- /dev/null +++ b/modules/oe_webtools_etrans/modules/oe_webtools_etrans_lc2023/src/EventSubscriber/ETransLanguagesEventSubscriber.php @@ -0,0 +1,323 @@ +schemes = array_keys($streamWrapperManager->getWrappers()); + $this->languageManager = $languageManager; + $this->entityTypeManager = $entityTypeManager; + $this->etransService = $etransService; + $this->blockRepository = $blockRepository; + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + // On a normal request. + $events[KernelEvents::REQUEST][] = ['checkForEuLanguageAndRedirect']; + // On access denied request. + $events[KernelEvents::EXCEPTION][] = [ + 'checkForEuLanguageAndRedirect', + 0, + ]; + return $events; + } + + /** + * Check if the current request should be redirected. + * + * @param \Symfony\Component\HttpKernel\Event\RequestEvent $event + * The request event. + */ + public function checkForEuLanguageAndRedirect(RequestEvent $event): void { + if (!$this->isBlockOnPage()) { + return; + } + // Do not redirect if this is a file. + if ($this->isFileRequest($event)) { + return; + } + // Do not redirect aggregated CSS/JS files. + if ($this->isAssetRoute($event)) { + return; + } + // Do not redirect admin route. + if ($this->isAdminRoute($event)) { + return; + } + // Do nothing for default language. + if ($this->isDefaultLanguage()) { + return; + } + // Check if current language is European and it should be handled + // through e-translation component. + $european_lang = $this->etransService->isLanguageEuropean(); + if (!$european_lang instanceof LanguageInterface) { + return; + } + + // Get redirect URL. + $url = $this->getRedirectUrl($event, $european_lang); + + // Set the response. + $this->setResponse($event, $url, $european_lang); + } + + /** + * Check if it's file request. + * + * @param \Symfony\Component\HttpKernel\Event\RequestEvent $event + * Event object. + * + * @return bool + * Returns true or false. + */ + private function isFileRequest(RequestEvent $event): bool { + $params = $event->getRequest()->attributes->all(); + return isset($params['scheme']) && in_array($params['scheme'], $this->schemes); + } + + /** + * Check if it's asset route. + * + * @param \Symfony\Component\HttpKernel\Event\RequestEvent $event + * Event object. + * + * @return bool + * Returns true or false. + */ + private function isAssetRoute(RequestEvent $event): bool { + $route_name = RouteMatch::createFromRequest($event->getRequest())->getRouteName(); + return in_array($route_name, self::ASSET_ROUTES); + } + + /** + * Check if it's admin route. + * + * @param \Symfony\Component\HttpKernel\Event\RequestEvent $event + * Event object. + * + * @return bool + * Returns true or false. + */ + private function isAdminRoute(RequestEvent $event): bool { + $route_match = RouteMatch::createFromRequest($event->getRequest()); + if (!$route_match instanceof RouteMatch) { + return FALSE; + } + $route = $route_match->getRouteObject(); + return (bool) $route->getOption('_admin_route'); + } + + /** + * Check if is default language. + * + * @return bool + * Returns true or false. + */ + private function isDefaultLanguage(): bool { + return $this->languageManager->getCurrentLanguage()->isDefault(); + } + + /** + * Check if block is on the page. + * + * @return bool + * Returns true or false. + */ + private function isBlockOnPage(): bool { + // Make sure our block is on the page. + $blocks_per_region = $this->blockRepository->getVisibleBlocksPerRegion(); + + return array_reduce($blocks_per_region, function ($carry, $blocks) { + foreach ($blocks as $block) { + if ($block->getPluginId() === 'oe_webtools_etrans_lc2023') { + return TRUE; + } + } + return $carry; + }, FALSE); + } + + /** + * Get redirect URL for european languages. + * + * @param \Symfony\Component\HttpKernel\Event\RequestEvent $event + * Event object. + * @param \Drupal\Core\Language\LanguageInterface $european_lang + * European language. + * + * @return \Drupal\Core\Url + * Returns an URL object. + */ + private function getRedirectUrl(RequestEvent $event, LanguageInterface $european_lang): Url { + // Default language will be the default redirect language. + $redirect_language = $this->languageManager->getDefaultLanguage(); + $pref_user_lang_code = mb_substr($this->languageManager->getCurrentLanguage() + ->getId(), 0, 2); + + // Get route match from request. + $route_match = RouteMatch::createFromRequest($event->getRequest()); + $params = $event->getRequest()->query->all(); + $keys_to_remove = ['etransnolive', 'etrans']; + foreach ($keys_to_remove as $key) { + if (array_key_exists($key, $params)) { + unset($params[$key]); + } + } + // Check if we have a route. + $route = $route_match->getRouteName(); + // Create URL object to redirect to in the correct language. + if ($route) { + return Url::fromRoute( + $route_match->getRouteName(), + $route_match->getRawParameters()->all(), + [ + 'language' => $redirect_language, + 'query' => array_merge($params, [ + 'prefLang' => $pref_user_lang_code, + ]), + ], + ); + } + // Create from current path. + $current_path = $event->getRequest()->getPathInfo(); + $path_elements = explode('/', trim($current_path, '/')); + if ($path_elements[0] === $european_lang->getId()) { + $path_elements[0] = '/' . $redirect_language->getId(); + return Url::fromUserInput(implode('/', $path_elements), [ + 'language' => $redirect_language, + 'query' => array_merge($params, [ + 'prefLang' => $pref_user_lang_code, + ]), + ]); + } + // If we couldn't identify correct path or route, redirect to homepage. + return Url::fromRoute('', [], [ + 'language' => $redirect_language, + ]); + } + + /** + * Set trusted redirect response. + * + * @param \Symfony\Component\HttpKernel\Event\RequestEvent $event + * Event object. + * @param \Drupal\Core\Url $url + * Url object. + * @param \Drupal\Core\Language\LanguageInterface $european_lang + * European language object. + */ + private function setResponse(RequestEvent $event, Url $url, LanguageInterface $european_lang): void { + // Set the response. + $cache = new CacheableMetadata(); + $cache->addCacheContexts(['languages', 'url']); + $cache->addCacheableDependency($european_lang); + // @todo Make redirect permanent?. + $response = new TrustedRedirectResponse($url->toString(), '307'); + $response = $this->removeEtransLiveCookieIfDifferent($response, $event, $european_lang->getId()); + $response->addCacheableDependency($cache); + $event->setResponse($response); + } + + /** + * If user changes the language from switcher, remove the "etranslive" cookie. + * + * @param \Drupal\Core\Routing\TrustedRedirectResponse $response + * Redirect response. + * @param \Symfony\Component\HttpKernel\Event\RequestEvent $event + * Event request. + * @param string $pref_lang_code + * Preferred language code. + * + * @return mixed + * Returns the response. + */ + private function removeEtransLiveCookieIfDifferent(TrustedRedirectResponse $response, RequestEvent $event, string $pref_lang_code) { + // Check if the cookie exists. + if ($event->getRequest()->cookies->has('etranslive')) { + $cookie = $event->getRequest()->cookies->get('etranslive'); + if (!empty($cookie) && $cookie !== $pref_lang_code) { + $response->headers->clearCookie('etranslive'); + } + } + return $response; + } + +} diff --git a/modules/oe_webtools_etrans/modules/oe_webtools_etrans_lc2023/src/Plugin/Block/ETransBlockLc2023.php b/modules/oe_webtools_etrans/modules/oe_webtools_etrans_lc2023/src/Plugin/Block/ETransBlockLc2023.php new file mode 100755 index 00000000..0f603186 --- /dev/null +++ b/modules/oe_webtools_etrans/modules/oe_webtools_etrans_lc2023/src/Plugin/Block/ETransBlockLc2023.php @@ -0,0 +1,313 @@ + '', + 'domain' => 'gen', + 'delay' => 0, + 'source' => '', + 'include' => '', + 'exclude' => '', + 'live' => FALSE, + ]; + + /** + * The language manager. + * + * @var \Drupal\Core\Language\LanguageManagerInterface + */ + protected LanguageManagerInterface $languageManager; + + /** + * The request service. + * + * @var \Symfony\Component\HttpFoundation\RequestStack + */ + protected RequestStack $request; + + /** + * The config factory. + * + * @var \Drupal\Core\Config\ConfigFactoryInterface + */ + protected ConfigFactoryInterface $configFactory; + + /** + * E-translation service. + * + * @var \Drupal\oe_webtools_etrans_lc2023\Service\ETransService + */ + protected ETransService $etransService; + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): self { + $instance = new static($configuration, $plugin_id, $plugin_definition); + $instance->languageManager = $container->get('language_manager'); + $instance->request = $container->get('request_stack'); + $instance->configFactory = $container->get('config.factory'); + $instance->etransService = $container->get('oe_webtools_etrans_lc2023.etrans_service'); + + return $instance; + } + + /** + * {@inheritdoc} + */ + public function build(): array { + // Display only for default language. + if (!$this->languageManager->getCurrentLanguage()->isDefault()) { + return []; + } + $json = $this->generateEtransJson(); + + $build['content'] = [ + '#theme' => 'oe_webtools_etrans_lc2023', + '#attached' => [ + 'library' => [ + 'oe_webtools/drupal.webtools-smartloader', + 'oe_webtools_etrans_lc2023/oe_wt_etrans_lc2023', + ], + ], + '#oe_wt_etrans_script' => [ + '#type' => 'html_tag', + '#tag' => 'script', + '#attributes' => [ + 'type' => 'application/json', + ], + '#value' => $json, + ], + ]; + $cache_contexts = [ + 'url.query_args:prefLang', + 'languages:' . LanguageInterface::TYPE_INTERFACE, + ]; + if ($this->configuration['live']) { + $cache_contexts[] = 'cookies:etranslive'; + } + // Set cache context. + $build['content']['#cache']['contexts'] = $cache_contexts; + + $pref_lang = $this->request->getCurrentRequest()->query->get('prefLang'); + if (!$pref_lang) { + return $build; + } + // Do not continue for non-eu language. + $eu_language = $this->etransService->isLanguageEuropean($pref_lang); + if (!$eu_language) { + return $build; + } + // Set additional variables. + $build['content']['#oe_wt_etrans_lc2023'] = [ + 'language' => $eu_language->getName(), + 'language_id' => $eu_language->getId(), + ]; + $build['content']['#attached']['drupalSettings'] = [ + 'oe_wt_etrans' => [ + 'preferred_language' => $pref_lang, + 'default_language' => $this->languageManager->getDefaultLanguage() + ->getId(), + ], + ]; + + return $build; + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration(): array { + return self::DEFAULT_CONFIGURATION + parent::defaultConfiguration(); + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state): array { + $form = parent::buildConfigurationForm($form, $form_state); + // eTrans - Language Concept 2023 url. + $options = ['attributes' => ['target' => '_blank']]; + $url = Link::fromTextAndUrl( + $this->t('More info'), + Url::fromUri('https://webtools.europa.eu/showcase-demo/resources/etrans/demo/links/demo/lc2023/lc2023.html', + $options))->toString(); + // Render as. + $form['info'] = [ + '#type' => 'markup', + '#markup' => $this->t('Language Concept 2023. @more_link', ['@more_link' => $url]), + '#description' => $this->t('Choose how to display the component. If you select Language Concept 2023, you need to specify a "Render to" ID, also known as a Receiver.'), + ]; + + // Render to. + $form['receiver'] = [ + '#type' => 'textfield', + '#title' => $this->t('Receiver'), + '#description' => $this->t('The ID of a HTML element in which the eTrans component will be rendered. Required for Language Concept 2023'), + '#maxlength' => 64, + '#default_value' => (string) $this->configuration['receiver'], + '#required' => TRUE, + ]; + + // Domain. + $form['domain'] = [ + '#type' => 'radios', + '#title' => $this->t('Domain'), + '#options' => [ + 'gen' => $this->t('General text'), + 'spd' => $this->t('EU formal language'), + ], + '#default_value' => $this->configuration['domain'], + ]; + + // Live. + $form['live'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Live translation'), + '#description' => $this->t('When live is set to true, after an user is translating a page to a specific language, all pages visited by user will be automatically translated to the selected language until the user is canceling the translation process.'), + '#default_value' => $this->configuration['live'] ?? FALSE, + ]; + + // Delay. + $form['delay'] = [ + '#type' => 'number', + '#title' => $this->t('Delay'), + '#min' => 0, + '#description' => $this->t('The time in milliseconds to delay rendering the translation. Use this on dynamic pages if the HTML element that contains the translation is not immediately available.'), + '#default_value' => $this->configuration['delay'], + ]; + + // Include. + $form['include'] = [ + '#type' => 'textarea', + '#title' => $this->t('Include'), + '#description' => $this->t('A list of CSS selectors indicating the page elements to be translated, one selector per line. If omitted the entire page will be translated.'), + '#default_value' => (string) $this->configuration['include'], + ]; + + // Exclude. + $form['exclude'] = [ + '#type' => 'textarea', + '#title' => $this->t('Exclude'), + '#description' => $this->t('A list of CSS selectors indicating page elements to be excluded from the translation even if they are inside an "include" element. One selector per line.'), + '#default_value' => (string) $this->configuration['exclude'], + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function blockValidate($form, FormStateInterface $form_state): void { + $receiver_value = $form_state->getValue('receiver'); + if (!empty($receiver_value) && $receiver_value !== Html::cleanCssIdentifier($receiver_value)) { + $form_state->setErrorByName('receiver', $this->t('Please provide a valid HTML ID.')); + } + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state): void { + parent::submitConfigurationForm($form, $form_state); + + foreach (array_keys(self::DEFAULT_CONFIGURATION) as $key) { + $this->configuration[$key] = $form_state->getValue($key); + } + + $this->configuration['delay'] = (int) $form_state->getValue('delay'); + } + + /** + * Generate eTrans json based on block configuration. + * + * @return false|string + * JSON string. + */ + private function generateEtransJson(): bool|string { + // Only pass the first two characters of the language code. The eTrans + // documentation is not clear on the standard used for language codes + // but all examples are showing two letters which seems to indicate + // ISO 639-1:2002. Drupal uses the IETF BCP 47 standard which uses more + // characters. For example this will convert 'pt-pt' to 'pt'. + $current_language_id = mb_substr($this->languageManager->getCurrentLanguage() + ->getId(), 0, 2); + $json = [ + 'service' => 'etrans', + 'languages' => [ + 'exclude' => [ + $current_language_id, + ], + 'source' => $this->languageManager->getDefaultLanguage()->getId(), + ], + 'domain' => $this->configuration['domain'], + 'delay' => (int) $this->configuration['delay'], + 'config' => [ + 'live' => (bool) $this->configuration['live'], + 'mode' => 'lc2023', + 'targets' => [ + 'receiver' => Html::cleanCssIdentifier($this->configuration['receiver']), + ], + ], + ]; + + foreach (['include', 'exclude'] as $option) { + if (!empty($this->configuration[$option])) { + $json[$option] = $this->formatSelectors($option); + } + } + return Json::encode($json); + } + + /** + * Format include/exclude selectors. + * + * @param string $option + * Option group. + * + * @return string|null + * Returns string or null. + */ + private function formatSelectors(string $option): ?string { + $selectors = []; + foreach (explode("\n", $this->configuration[$option]) as $selector) { + if ($selector = trim($selector)) { + $selectors[] = $selector; + } + } + return !empty($selectors) ? implode(',', $selectors) : NULL; + } + +} diff --git a/modules/oe_webtools_etrans/modules/oe_webtools_etrans_lc2023/src/Service/ETransService.php b/modules/oe_webtools_etrans/modules/oe_webtools_etrans_lc2023/src/Service/ETransService.php new file mode 100644 index 00000000..2f5af574 --- /dev/null +++ b/modules/oe_webtools_etrans/modules/oe_webtools_etrans_lc2023/src/Service/ETransService.php @@ -0,0 +1,112 @@ +entityTypeManager = $entity_type_manager; + $this->languageManager = $language_manager; + $this->moduleHandler = $module_handler; + } + + /** + * Check if language is european, excluding default lang. + * + * @param string|null $language_code + * Language code. + * + * @return \Drupal\Core\Language\LanguageInterface|null + * Returns language or null. + */ + public function isLanguageEuropean(?string $language_code = NULL): ?LanguageInterface { + if (!$this->moduleHandler->moduleExists('oe_multilingual')) { + return NULL; + } + if ($language_code == 'pt') { + $language_code = 'pt-pt'; + } + // Load language by ID or fallback to current language. + $language = $language_code ? $this->languageManager->getLanguage($language_code) : $this->languageManager->getCurrentLanguage(); + if (!$language instanceof LanguageInterface) { + return NULL; + } + $config_manager = $this->entityTypeManager->getStorage('configurable_language'); + $is_eu = $config_manager->load($language->getId()) + ->getThirdPartySetting('oe_multilingual', 'category'); + if ($is_eu === 'eu' && !$language->isDefault()) { + return $language; + } + return NULL; + } + + /** + * Get all european languages. + * + * @return array + * Array with all languages marked as european. + */ + public function getEuropeanLanguages(): ?array { + if (!$this->moduleHandler->moduleExists('oe_multilingual')) { + return NULL; + } + $config_manager = $this->entityTypeManager->getStorage('configurable_language'); + // European languages. + $european_languages = []; + $languages = $this->languageManager->getLanguages(); + foreach ($languages as $language_code => $language) { + $etrans_lang = mb_substr($language_code, 0, 2); + $is_eu = $config_manager->load($language->getId()) + ->getThirdPartySetting('oe_multilingual', 'category'); + if ($is_eu === 'eu' && !$language->isDefault()) { + $european_languages[$etrans_lang] = $language; + } + } + return $european_languages; + } + +} diff --git a/modules/oe_webtools_etrans/modules/oe_webtools_etrans_lc2023/templates/oe-webtools-etrans-lc2023.html.twig b/modules/oe_webtools_etrans/modules/oe_webtools_etrans_lc2023/templates/oe-webtools-etrans-lc2023.html.twig new file mode 100644 index 00000000..80a2d61b --- /dev/null +++ b/modules/oe_webtools_etrans/modules/oe_webtools_etrans_lc2023/templates/oe-webtools-etrans-lc2023.html.twig @@ -0,0 +1,50 @@ +{# +/** + * @file + * Theme implementation for the eTranslation - 2023 Language Concept + * + * Available variables: + * - oe_wt_etrans_script: The script for eTranslation. + * - oe_wt_etrans_lc2023: Contains information related to the translation service. + * - language: The language available for translation. + * - language_id: The id of the language available for translation. + */ +#} + +{# Set the script #} +{{ oe_wt_etrans_script }} + +{# Disclimer message #} +{% if oe_wt_etrans_lc2023 %} + +{% endif %} From 1f01807219547fd7a6cffb5eaf90989b508196f8 Mon Sep 17 00:00:00 2001 From: Adrian Cocorev Date: Tue, 7 May 2024 10:45:08 +0300 Subject: [PATCH 2/2] ETRANS-0000: Handle Non-live e-translations. --- .../js/oe-webtools-lc2023.js | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/modules/oe_webtools_etrans/modules/oe_webtools_etrans_lc2023/js/oe-webtools-lc2023.js b/modules/oe_webtools_etrans/modules/oe_webtools_etrans_lc2023/js/oe-webtools-lc2023.js index b22b82f5..5d4b4042 100644 --- a/modules/oe_webtools_etrans/modules/oe_webtools_etrans_lc2023/js/oe-webtools-lc2023.js +++ b/modules/oe_webtools_etrans/modules/oe_webtools_etrans_lc2023/js/oe-webtools-lc2023.js @@ -13,6 +13,14 @@ if ($translateLink.length) handleTranslateLink($translateLink, $etransMessage, settings); if ($etransMessage.length) handleEtransMessage($etransMessage, $closeLink, context); if ($defaultLanguage.length) handleDefaultLanguage($defaultLanguage); + + // Update language elements and add prefLang query parameter. + $(window, context).on('wtTranslationEnd', function () { + // Make sure we do not have a live cookie. + if ($wt && $wt.etrans && !$wt.etrans.getLiveCookie()) { + addPrefLanguageQueryParam(context); + } + }); } }; @@ -53,4 +61,23 @@ window.history.pushState({}, '', url); } + // Add prefLang query parameter to links. + function addPrefLanguageQueryParam(context) { + $(context).find('a:not(.ecl-language-list__container a)').each(function () { + const href = $(this).attr('href'); + if (href && href.startsWith('/') && !href.includes('prefLang=')) { + const newHref = appendPrefLang(href, $wt.etrans.toLanguage); + $(this).attr('href', newHref); + } + }); + } + + // Append 'prefLang' parameter to the URL. + function appendPrefLang(href, language) { + if (language) { + return href + (href.includes('?') ? '&prefLang=' : '?prefLang=') + language; + } + return href; + } + })(jQuery, Drupal);