getLanguages(); if (!locale_is_translatable('en')) { unset($languages['en']); } return $languages; } /** * Returns plural form index for a specific number. * * The index is computed from the formula of this language. * * @param int $count * Number to return plural for. * @param string|null $langcode * (optional) Language code to translate to a language other than what is used * to display the page, or NULL for current language. Defaults to NULL. * * @return int * The numeric index of the plural variant to use for this $langcode and * $count combination or -1 if the language was not found or does not have a * plural formula. */ function locale_get_plural($count, $langcode = NULL) { $language_interface = \Drupal::languageManager()->getCurrentLanguage(); // Used to store precomputed plural indexes corresponding to numbers // individually for each language. $plural_indexes = &drupal_static(__FUNCTION__ . ':plurals', []); $langcode = $langcode ?: $language_interface->getId(); if (!isset($plural_indexes[$langcode][$count])) { // Retrieve and statically cache the plural formulas for all languages. $plural_formulas = \Drupal::service('locale.plural.formula')->getFormula($langcode); // If there is a plural formula for the language, evaluate it for the given // $count and statically cache the result for the combination of language // and count, since the result will always be identical. if (!empty($plural_formulas)) { // Plural formulas are stored as an array for 0-199. 100 is the highest // modulo used but storing 0-99 is not enough because below 100 we often // find exceptions (1, 2, etc). $index = $count > 199 ? 100 + ($count % 100) : $count; $plural_indexes[$langcode][$count] = $plural_formulas[$index] ?? $plural_formulas['default']; } // In case there is no plural formula for English (no imported translation // for English), use a default formula. elseif ($langcode == 'en') { $plural_indexes[$langcode][$count] = (int) ($count != 1); } // Otherwise, return -1 (unknown). else { $plural_indexes[$langcode][$count] = -1; } } return $plural_indexes[$langcode][$count]; } /** * Updates default configuration when new modules or themes are installed. */ function locale_system_set_config_langcodes(): void { \Drupal::service('locale.config_manager')->updateDefaultConfigLangcodes(); } /** * Imports translations when new modules or themes are installed. * * This function will start a batch to import translations for the added * components. * * @param array $components * An array of arrays of component (theme and/or module) names to import * translations for, indexed by type. */ function locale_system_update(array $components): void { $components += ['module' => [], 'theme' => []]; $list = array_merge($components['module'], $components['theme']); // Skip running the translation imports if in the installer, // because it would break out of the installer flow. We have // built-in support for translation imports in the installer. if (!InstallerKernel::installationAttempted() && locale_translatable_language_list()) { $module_handler = \Drupal::moduleHandler(); if (\Drupal::config('locale.settings')->get('translation.import_enabled')) { $module_handler->loadInclude('locale', 'inc', 'locale.compare'); // Update the list of translatable projects and start the import batch. // Only when new projects are added the update batch will be triggered. // Not each enabled module will introduce a new project. E.g. sub modules. $projects = array_keys(locale_translation_build_projects()); if ($list = array_intersect($list, $projects)) { $module_handler->loadInclude('locale', 'inc', 'locale.fetch'); // Get translation status of the projects, download and update // translations. $options = _locale_translation_default_update_options(); $batch = locale_translation_batch_update_build($list, [], $options); batch_set($batch); } } // Construct a batch to update configuration for all components. Installing // this component may have installed configuration from any number of other // components. Do this even if import is not enabled because parsing new // configuration may expose new source strings. $module_handler->loadInclude('locale', 'inc', 'locale.bulk'); if ($batch = locale_config_batch_update_components([], [], [], TRUE)) { batch_set($batch); } } } /** * Delete translation history of modules and themes. * * Only the translation history is removed, not the source strings or * translations. This is not possible because strings are shared between * modules and we have no record of which string is used by which module. * * @param array $components * An array of arrays of component (theme and/or module) names to import * translations for, indexed by type. */ function locale_system_remove($components): void { $components += ['module' => [], 'theme' => []]; $list = array_merge($components['module'], $components['theme']); if (locale_translatable_language_list()) { $module_handler = \Drupal::moduleHandler(); $module_handler->loadInclude('locale', 'inc', 'locale.compare'); $module_handler->loadInclude('locale', 'inc', 'locale.bulk'); // Only when projects are removed, the translation files and records will be // deleted. Not each disabled module will remove a project, e.g., sub // modules. $projects = array_keys(locale_translation_get_projects()); if ($list = array_intersect($list, $projects)) { locale_translation_file_history_delete($list); // Remove translation files. locale_translate_delete_translation_files($list, []); // Remove translatable projects. // Follow-up issue https://www.drupal.org/node/1842362 to replace the // {locale_project} table. Then change this to a function call. \Drupal::service('locale.project')->deleteMultiple($list); // Clear the translation status. locale_translation_status_delete_projects($list); } } } /** * Returns a list of translation files given a list of JavaScript files. * * This function checks all JavaScript files passed and invokes parsing if they * have not yet been parsed for Drupal.t() and Drupal.formatPlural() calls. * Also refreshes the JavaScript translation files if necessary, and returns * the filepath to the translation file (if any). * * @param array $files * An array of local file paths. * @param \Drupal\Core\Language\LanguageInterface $language_interface * The interface language the files should be translated into. * * @return string|null * The filepath to the translation file or NULL if no translation is * applicable. */ function locale_js_translate(array $files = [], $language_interface = NULL) { if (!isset($language_interface)) { $language_interface = \Drupal::languageManager()->getCurrentLanguage(); } $dir = 'public://' . \Drupal::config('locale.settings')->get('javascript.directory'); $parsed = \Drupal::state()->get('system.javascript_parsed', []); $new_files = FALSE; foreach ($files as $filepath) { if (!in_array($filepath, $parsed)) { // Don't parse our own translations files. if (!str_starts_with($filepath, $dir)) { _locale_parse_js_file($filepath); $parsed[] = $filepath; $new_files = TRUE; } } } // If there are any new source files we parsed, invalidate existing // JavaScript translation files for all languages, adding the refresh // flags into the existing array. if ($new_files) { $parsed += _locale_invalidate_js(); } // If necessary, rebuild the translation file for the current language. if (!empty($parsed['refresh:' . $language_interface->getId()])) { // Don't clear the refresh flag on failure, so that another try will // be performed later. if (_locale_rebuild_js()) { unset($parsed['refresh:' . $language_interface->getId()]); } // Store any changes after refresh was attempted. \Drupal::state()->set('system.javascript_parsed', $parsed); } // If no refresh was attempted, but we have new source files, we need // to store them too. This occurs if current page is in English. elseif ($new_files) { \Drupal::state()->set('system.javascript_parsed', $parsed); } // Add the translation JavaScript file to the page. $locale_javascripts = \Drupal::state()->get('locale.translation.javascript', []); $translation_file = NULL; if (!empty($files) && !empty($locale_javascripts[$language_interface->getId()])) { // Add the translation JavaScript file to the page. $translation_file = $dir . '/' . $language_interface->getId() . '_' . $locale_javascripts[$language_interface->getId()] . '.js'; } return $translation_file; } /** * Form submission handler for language_admin_add_form(). * * Set a batch for a newly-added language. */ function locale_form_language_admin_add_form_alter_submit($form, FormStateInterface $form_state): void { \Drupal::moduleHandler()->loadInclude('locale', 'fetch.inc'); $options = _locale_translation_default_update_options(); if ($form_state->isValueEmpty('predefined_langcode') || $form_state->getValue('predefined_langcode') == 'custom') { $langcode = $form_state->getValue('langcode'); } else { $langcode = $form_state->getValue('predefined_langcode'); } if (\Drupal::config('locale.settings')->get('translation.import_enabled')) { // Download and import translations for the newly added language. $batch = locale_translation_batch_update_build([], [$langcode], $options); batch_set($batch); } // Create or update all configuration translations for this language. If we // are adding English then we need to run this even if import is not enabled, // because then we extract English sources from shipped configuration. if (\Drupal::config('locale.settings')->get('translation.import_enabled') || $langcode == 'en') { \Drupal::moduleHandler()->loadInclude('locale', 'bulk.inc'); if ($batch = locale_config_batch_update_components($options, [$langcode])) { batch_set($batch); } } } /** * Form submission handler for language_admin_edit_form(). */ function locale_form_language_admin_edit_form_alter_submit($form, FormStateInterface $form_state): void { \Drupal::configFactory()->getEditable('locale.settings')->set('translate_english', intval($form_state->getValue('locale_translate_english')))->save(); } /** * Checks whether $langcode is a language supported as a locale target. * * @param string $langcode * The language code. * * @return bool * Whether $langcode can be translated to in locale. */ function locale_is_translatable($langcode) { return $langcode != 'en' || \Drupal::config('locale.settings')->get('translate_english'); } /** * Submit handler for the file system settings form. * * Clears the translation status when the Interface translations directory * changes. Without a translations directory local po files in the directory * should be ignored. The old translation status is no longer valid. */ function locale_system_file_system_settings_submit(&$form, FormStateInterface $form_state): void { if ($form['translation_path']['#default_value'] != $form_state->getValue('translation_path')) { locale_translation_clear_status(); } \Drupal::configFactory()->getEditable('locale.settings') ->set('translation.path', $form_state->getValue('translation_path')) ->save(); } /** * Gets current translation status from the {locale_file} table. * * @return array * Array of translation file objects. */ function locale_translation_get_file_history() { $history = &drupal_static(__FUNCTION__, []); if (empty($history)) { // Get file history from the database. $result = \Drupal::database()->select('locale_file') ->fields('locale_file', ['project', 'langcode', 'filename', 'version', 'uri', 'timestamp', 'last_checked']) ->execute() ->fetchAll(); foreach ($result as $file) { $file->type = $file->timestamp ? LOCALE_TRANSLATION_CURRENT : ''; $history[$file->project][$file->langcode] = $file; } } return $history; } /** * Updates the {locale_file} table. * * @param object $file * Object representing the file just imported. * * @return int * FALSE on failure. Otherwise SAVED_NEW or SAVED_UPDATED. */ function locale_translation_update_file_history($file) { $status = \Drupal::database()->merge('locale_file') ->keys([ 'project' => $file->project, 'langcode' => $file->langcode, ]) ->fields([ 'version' => $file->version, 'timestamp' => $file->timestamp, 'last_checked' => $file->last_checked, ]) ->execute(); // The file history has changed, flush the static cache now. // @todo Can we make this more fine grained? drupal_static_reset('locale_translation_get_file_history'); return $status; } /** * Deletes the history of downloaded translations. * * @param array $projects * Project name(s) to be deleted from the file history. If both project(s) and * language code(s) are specified the conditions will be ANDed. * @param array $langcodes * Language code(s) to be deleted from the file history. */ function locale_translation_file_history_delete($projects = [], $langcodes = []): void { $query = \Drupal::database()->delete('locale_file'); if (!empty($projects)) { $query->condition('project', $projects, 'IN'); } if (!empty($langcodes)) { $query->condition('langcode', $langcodes, 'IN'); } $query->execute(); } /** * Gets the current translation status. * * @todo What is 'translation status'? */ function locale_translation_get_status($projects = NULL, $langcodes = NULL): array { $result = []; $status = \Drupal::keyValue('locale.translation_status')->getAll(); \Drupal::moduleHandler()->loadInclude('locale', 'inc', 'locale.translation'); $projects = $projects ?: array_keys(locale_translation_get_projects()); $langcodes = $langcodes ?: array_keys(locale_translatable_language_list()); // Get the translation status of each project-language combination. If no // status was stored, a new translation source is created. foreach ($projects as $project) { foreach ($langcodes as $langcode) { if (isset($status[$project][$langcode])) { $result[$project][$langcode] = $status[$project][$langcode]; } else { $sources = locale_translation_build_sources([$project], [$langcode]); if (isset($sources[$project][$langcode])) { $result[$project][$langcode] = $sources[$project][$langcode]; } } } } return $result; } /** * Saves the status of translation sources in static cache. * * @param string $project * Machine readable project name. * @param string $langcode * Language code. * @param string $type * Type of data to be stored. * @param object $data * File object also containing timestamp when the translation is last updated. */ function locale_translation_status_save($project, $langcode, $type, $data): void { // Load the translation status or build it if not already available. \Drupal::moduleHandler()->loadInclude('locale', 'inc', 'locale.translation'); $status = locale_translation_get_status([$project]); if (empty($status)) { $projects = locale_translation_get_projects([$project]); if (isset($projects[$project])) { $status[$project][$langcode] = locale_translation_source_build($projects[$project], $langcode); } } // Merge the new status data with the existing status. if (isset($status[$project][$langcode])) { $request_time = \Drupal::time()->getRequestTime(); switch ($type) { case LOCALE_TRANSLATION_REMOTE: case LOCALE_TRANSLATION_LOCAL: // Add the source data to the status array. $status[$project][$langcode]->files[$type] = $data; // Check if this translation is the most recent one. Set timestamp and // data type of the most recent translation source. if (isset($data->timestamp) && $data->timestamp) { if ($data->timestamp > $status[$project][$langcode]->timestamp) { $status[$project][$langcode]->timestamp = $data->timestamp; $status[$project][$langcode]->last_checked = $request_time; $status[$project][$langcode]->type = $type; } } break; case LOCALE_TRANSLATION_CURRENT: $data->last_checked = $request_time; $status[$project][$langcode]->timestamp = $data->timestamp; $status[$project][$langcode]->last_checked = $data->last_checked; $status[$project][$langcode]->type = $type; locale_translation_update_file_history($data); break; } \Drupal::keyValue('locale.translation_status')->set($project, $status[$project]); \Drupal::state()->set('locale.translation_last_checked', $request_time); } } /** * Delete language entries from the status cache. * * @param array $langcodes * Language code(s) to be deleted from the cache. */ function locale_translation_status_delete_languages($langcodes): void { if ($status = locale_translation_get_status()) { foreach ($status as $project => $languages) { foreach ($languages as $langcode => $source) { if (in_array($langcode, $langcodes)) { unset($status[$project][$langcode]); } } \Drupal::keyValue('locale.translation_status')->set($project, $status[$project]); } } } /** * Delete project entries from the status cache. * * @param array $projects * Project name(s) to be deleted from the cache. */ function locale_translation_status_delete_projects($projects): void { \Drupal::keyValue('locale.translation_status')->deleteMultiple($projects); } /** * Clear the translation status cache. */ function locale_translation_clear_status(): void { \Drupal::keyValue('locale.translation_status')->deleteAll(); \Drupal::state()->delete('locale.translation_last_checked'); } /** * Checks whether remote translation sources are used. * * @return bool * Returns TRUE if remote translations sources should be taken into account * when checking or importing translation files, FALSE otherwise. */ function locale_translation_use_remote_source() { return \Drupal::config('locale.settings')->get('translation.use_source') == LOCALE_TRANSLATION_USE_SOURCE_REMOTE_AND_LOCAL; } /** * Check that a string is safe to be added or imported as a translation. * * This test can be used to detect possibly bad translation strings. It should * not have any false positives. But it is only a test, not a transformation, * as it destroys valid HTML. We cannot reliably filter translation strings * on import because some strings are irreversibly corrupted. For example, * an & in the translation would get encoded to & by * \Drupal\Component\Utility\Xss::filter() before being put in the database, * and thus would be displayed incorrectly. * * The allowed tag list is like \Drupal\Component\Utility\Xss::filterAdmin(), * but omitting div and img as not needed for translation and likely to cause * layout issues (div) or a possible attack vector (img). */ function locale_string_is_safe($string) { // Some strings have tokens in them. For tokens in the first part of href or // src HTML attributes, \Drupal\Component\Utility\Xss::filter() removes part // of the token, the part before the first colon. // \Drupal\Component\Utility\Xss::filter() assumes it could be an attempt to // inject javascript. When \Drupal\Component\Utility\Xss::filter() removes // part of tokens, it causes the string to not be translatable when it should // be translatable. // @see \Drupal\Tests\locale\Kernel\LocaleStringIsSafeTest::testLocaleStringIsSafe() // // We can recognize tokens since they are wrapped with brackets and are only // composed of alphanumeric characters, colon, underscore, and dashes. We can // be sure these strings are safe to strip out before the string is checked in // \Drupal\Component\Utility\Xss::filter() because no dangerous javascript // will match that pattern. // // Strings with tokens should not be assumed to be dangerous because even if // we evaluate them to be safe here, later replacing the token inside the // string will automatically mark it as unsafe as it is not the same string // anymore. // // @todo Do not strip out the token. Fix // \Drupal\Component\Utility\Xss::filter() to not incorrectly alter the // string. https://www.drupal.org/node/2372127 $string = preg_replace('/\[[a-z0-9_-]+(:[a-z0-9_-]+)+\]/i', '', $string); return Html::decodeEntities($string) == Html::decodeEntities(Xss::filter($string, ['a', 'abbr', 'acronym', 'address', 'b', 'bdo', 'big', 'blockquote', 'br', 'caption', 'cite', 'code', 'col', 'colgroup', 'dd', 'del', 'dfn', 'dl', 'dt', 'em', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'ins', 'kbd', 'li', 'ol', 'p', 'pre', 'q', 'samp', 'small', 'span', 'strong', 'sub', 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr', 'tt', 'ul', 'var', 'wbr'])); } /** * Refresh related information after string translations have been updated. * * The information that will be refreshed includes: * - JavaScript translations. * - Locale cache. * - Render cache. * * @param array $langcodes * Language codes for updated translations. * @param array $lids * (optional) List of string identifiers that have been updated / created. * If not provided, all caches for the affected languages are cleared. */ function _locale_refresh_translations($langcodes, $lids = []): void { if (!empty($langcodes)) { // Update javascript translations if any of the strings has a javascript // location, or if no string ids were provided, update all languages. if (empty($lids) || !empty(\Drupal::service('locale.storage')->getStrings(['lid' => $lids, 'type' => 'javascript']))) { array_map('_locale_invalidate_js', $langcodes); } } // Throw locale.save_translation event. \Drupal::service('event_dispatcher')->dispatch(new LocaleEvent($langcodes, $lids), LocaleEvents::SAVE_TRANSLATION); } /** * Refreshes configuration after string translations have been updated. * * @param array $langcodes * Language codes for updated translations. * @param array $lids * List of string identifiers that have been updated / created. */ function _locale_refresh_configuration(array $langcodes, array $lids): void { $locale_config_manager = \Drupal::service('locale.config_manager'); if ($lids && $langcodes && $names = $locale_config_manager->getStringNames($lids)) { $locale_config_manager->updateConfigTranslations($names, $langcodes); } } /** * Removes the quotes and string concatenations from the string. * * @param string $string * Single or double quoted strings, optionally concatenated by plus (+) sign. * * @return string * String with leading and trailing quotes removed. */ function _locale_strip_quotes($string) { return implode('', preg_split('~(? $string) { $matches[] = [ 'source' => _locale_strip_quotes($string), 'context' => _locale_strip_quotes($t_matches[2][$key]), ]; } // Add string from Drupal.formatPlural(). foreach ($plural_matches[1] as $key => $string) { $matches[] = [ 'source' => _locale_strip_quotes($string) . PoItem::DELIMITER . _locale_strip_quotes($plural_matches[2][$key]), 'context' => _locale_strip_quotes($plural_matches[3][$key]), ]; } // Loop through all matches and process them. foreach ($matches as $match) { $source = \Drupal::service('locale.storage')->findString($match); if (!$source) { // We don't have the source string yet, thus we insert it into the // database. $source = \Drupal::service('locale.storage')->createString($match); } // Besides adding the location this will tag it for current version. $source->addLocation('javascript', $filepath); $source->save(); } } /** * Force the JavaScript translation file(s) to be refreshed. * * This function sets a refresh flag for a specified language, or all * languages except English, if none specified. JavaScript translation * files are rebuilt (with locale_update_js_files()) the next time a * request is served in that language. * * @param string|null $langcode * (optional) The language code for which the file needs to be refreshed, or * NULL to refresh all languages. Defaults to NULL. * * @return array * New content of the 'system.javascript_parsed' variable. */ function _locale_invalidate_js($langcode = NULL) { $parsed = \Drupal::state()->get('system.javascript_parsed', []); if (empty($langcode)) { // Invalidate all languages. $languages = locale_translatable_language_list(); foreach ($languages as $language_code => $data) { $parsed['refresh:' . $language_code] = 'waiting'; } } else { // Invalidate single language. $parsed['refresh:' . $langcode] = 'waiting'; } \Drupal::state()->set('system.javascript_parsed', $parsed); return $parsed; } /** * Creates or recreates the JavaScript translation file for a language. * * @param string|null $langcode * (optional) The language that the translation file should be (re)created * for, or NULL for the current language. Defaults to NULL. * * @return bool * TRUE if translation file exists, FALSE otherwise. */ function _locale_rebuild_js($langcode = NULL) { $config = \Drupal::config('locale.settings'); if (!isset($langcode)) { $language = \Drupal::languageManager()->getCurrentLanguage(); } else { // Get information about the locale. $languages = \Drupal::languageManager()->getLanguages(); $language = $languages[$langcode]; } // Construct the array for JavaScript translations. // Only add strings with a translation to the translations array. $conditions = [ 'type' => 'javascript', 'language' => $language->getId(), 'translated' => TRUE, ]; $translations = []; foreach (\Drupal::service('locale.storage')->getTranslations($conditions) as $data) { $translations[$data->context][$data->source] = $data->translation; } // Include custom string overrides. $custom_strings = Settings::get('locale_custom_strings_' . $language->getId(), []); foreach ($custom_strings as $context => $strings) { foreach ($strings as $source => $translation) { $translations[$context][$source] = $translation; } } // Construct the JavaScript file, if there are translations. $data_hash = NULL; $data = $status = ''; if (!empty($translations)) { $data = [ 'strings' => $translations, ]; $locale_plurals = \Drupal::service('locale.plural.formula')->getFormula($language->getId()); if ($locale_plurals) { $data['pluralFormula'] = $locale_plurals; } $data = 'window.drupalTranslations = ' . Json::encode($data) . ';'; $data_hash = Crypt::hashBase64($data); } // Construct the filepath where JS translation files are stored. // There is (on purpose) no front end to edit that variable. $dir = 'public://' . $config->get('javascript.directory'); // Delete old file, if we have no translations anymore, or a different file to // be saved. $locale_javascripts = \Drupal::state()->get('locale.translation.javascript', []); $changed_hash = !isset($locale_javascripts[$language->getId()]) || ($locale_javascripts[$language->getId()] != $data_hash); /** @var \Drupal\Core\File\FileSystemInterface $file_system */ $file_system = \Drupal::service('file_system'); if (!empty($locale_javascripts[$language->getId()]) && (!$data || $changed_hash)) { try { $file_system->delete($dir . '/' . $language->getId() . '_' . $locale_javascripts[$language->getId()] . '.js'); } catch (FileException) { // Ignore. } $locale_javascripts[$language->getId()] = ''; $status = 'deleted'; } // Only create a new file if the content has changed or the original file got // lost. $dest = $dir . '/' . $language->getId() . '_' . $data_hash . '.js'; if ($data && ($changed_hash || !file_exists($dest))) { // Ensure that the directory exists and is writable, if possible. $file_system->prepareDirectory($dir, FileSystemInterface::CREATE_DIRECTORY); // Save the file. try { if ($file_system->saveData($data, $dest)) { $locale_javascripts[$language->getId()] = $data_hash; // If we deleted a previous version of the file and we replace it with a // new one we have an update. if ($status == 'deleted') { $status = 'updated'; } // If the file did not exist previously and the data has changed we have // a fresh creation. elseif ($changed_hash) { $status = 'created'; } // If the data hash is unchanged the translation was lost and has to be // rebuilt. else { $status = 'rebuilt'; } } else { $locale_javascripts[$language->getId()] = ''; $status = 'error'; } } catch (FileException) { // Do nothing. } } // Save the new JavaScript hash (or an empty value if the file just got // deleted). Act only if some operation was executed that changed the hash // code. if ($status && $changed_hash) { \Drupal::state()->set('locale.translation.javascript', $locale_javascripts); } // Log the operation and return success flag. $logger = \Drupal::logger('locale'); switch ($status) { case 'updated': $logger->notice('Updated JavaScript translation file for the language %language.', ['%language' => $language->getName()]); return TRUE; case 'rebuilt': $logger->warning('JavaScript translation file %file.js was lost.', ['%file' => $locale_javascripts[$language->getId()]]); // Proceed to the 'created' case as the JavaScript translation file has // been created again. case 'created': $logger->notice('Created JavaScript translation file for the language %language.', ['%language' => $language->getName()]); return TRUE; case 'deleted': $logger->notice('Removed JavaScript translation file for the language %language because no translations currently exist for that language.', ['%language' => $language->getName()]); return TRUE; case 'error': $logger->error('An error occurred during creation of the JavaScript translation file for the language %language.', ['%language' => $language->getName()]); return FALSE; default: // No operation needed. return TRUE; } } /** * Form element callback: After build changes to the language update table. * * Adds labels to the languages and removes checkboxes from languages from which * translation files could not be found. */ function locale_translation_language_table($form_element) { // Remove checkboxes of languages without updates. if ($form_element['#not_found']) { foreach ($form_element['#not_found'] as $langcode) { $form_element[$langcode] = []; } } return $form_element; }