locale.module
Same filename in other branches
File
-
core/
modules/ locale/ locale.module
View source
<?php
/**
* @file
*/
use Drupal\Component\Gettext\PoItem;
use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Component\Utility\Xss;
use Drupal\Core\File\Exception\FileException;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Installer\InstallerKernel;
use Drupal\Core\Site\Settings;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Component\Utility\Crypt;
use Drupal\locale\LocaleEvent;
use Drupal\locale\LocaleEvents;
/**
* Regular expression pattern used to localize JavaScript strings.
*/
const LOCALE_JS_STRING = '(?:(?:\'(?:\\\\\'|[^\'])*\'|"(?:\\\\"|[^"])*")(?:\\s*\\+\\s*)?)+';
/**
* Regular expression pattern used to match simple JS object literal.
*
* This pattern matches a basic JS object, but will fail on an object with
* nested objects. Used in JS file parsing for string arg processing.
*/
const LOCALE_JS_OBJECT = '\\{.*?\\}';
/**
* Regular expression to match an object containing a key 'context'.
*
* Pattern to match a JS object containing a 'context key' with a string value,
* which is captured. Will fail if there are nested objects.
*/
define('LOCALE_JS_OBJECT_CONTEXT', '
\\{ # match object literal start
.*? # match anything, non-greedy
(?: # match a form of "context"
\'context\'
|
"context"
|
context
)
\\s*:\\s* # match key-value separator ":"
(' . LOCALE_JS_STRING . ') # match context string
.*? # match anything, non-greedy
\\} # match end of object literal
');
/**
* Flag for locally not customized interface translation.
*
* Such translations are imported from .po files downloaded from
* localize.drupal.org for example.
*/
const LOCALE_NOT_CUSTOMIZED = 0;
/**
* Flag for locally customized interface translation.
*
* Such translations are edited from their imported originals on the user
* interface or are imported as customized.
*/
const LOCALE_CUSTOMIZED = 1;
/**
* Translation update mode: Use local files only.
*
* When checking for available translation updates, only local files will be
* used. Any remote translation file will be ignored. Also custom modules and
* themes which have set a "server pattern" to use a remote translation server
* will be ignored.
*/
const LOCALE_TRANSLATION_USE_SOURCE_LOCAL = 'local';
/**
* Translation update mode: Use both remote and local files.
*
* When checking for available translation updates, both local and remote files
* will be checked.
*/
const LOCALE_TRANSLATION_USE_SOURCE_REMOTE_AND_LOCAL = 'remote_and_local';
/**
* Default location of gettext file on the translation server.
*
* @see locale_translation_default_translation_server().
*/
const LOCALE_TRANSLATION_DEFAULT_SERVER_PATTERN = 'https://ftp.drupal.org/files/translations/%core/%project/%project-%version.%language.po';
/**
* The number of seconds that the translations status entry should be considered.
*/
const LOCALE_TRANSLATION_STATUS_TTL = 600;
/**
* UI option for override of existing translations. Override any translation.
*/
const LOCALE_TRANSLATION_OVERWRITE_ALL = 'all';
/**
* UI option for override of existing translations.
*
* Only override non-customized translations.
*/
const LOCALE_TRANSLATION_OVERWRITE_NON_CUSTOMIZED = 'non_customized';
/**
* UI option for override of existing translations.
*
* Don't override existing translations.
*/
const LOCALE_TRANSLATION_OVERWRITE_NONE = 'none';
/**
* Translation source is a remote file.
*/
const LOCALE_TRANSLATION_REMOTE = 'remote';
/**
* Translation source is a local file.
*/
const LOCALE_TRANSLATION_LOCAL = 'local';
/**
* Translation source is the current translation.
*/
const LOCALE_TRANSLATION_CURRENT = 'current';
/**
* Returns list of translatable languages.
*
* @return array
* Array of installed languages keyed by language name. English is omitted
* unless it is marked as translatable.
*/
function locale_translatable_language_list() {
$languages = \Drupal::languageManager()->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 ? $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'];
}
elseif ($langcode == 'en') {
$plural_indexes[$langcode][$count] = (int) ($count != 1);
}
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() {
\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) {
$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) {
$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);
}
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) {
\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) {
\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) {
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();
}
/**
* Implements hook_preprocess_HOOK() for node templates.
*/
function locale_preprocess_node(&$variables) : void {
/** @var \Drupal\node\NodeInterface $node */
$node = $variables['node'];
if ($node->language()
->getId() != LanguageInterface::LANGCODE_NOT_SPECIFIED) {
$interface_language = \Drupal::languageManager()->getCurrentLanguage();
$node_language = $node->language();
if ($node_language->getId() != $interface_language->getId()) {
// If the node language was different from the page language, we should
// add markup to identify the language. Otherwise the page language is
// inherited.
$variables['attributes']['lang'] = $node_language->getId();
if ($node_language->getDirection() != $interface_language->getDirection()) {
// If text direction is different form the page's text direction, add
// direction information as well.
$variables['attributes']['dir'] = $node_language->getDirection();
}
}
}
}
/**
* 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 = []) {
$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) {
$result = [];
$status = \Drupal::keyValue('locale.translation_status')->getAll();
\Drupal::moduleHandler()->loadInclude('locale', 'inc', 'locale.translation');
$projects = $projects ? $projects : array_keys(locale_translation_get_projects());
$langcodes = $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) {
// 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) {
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) {
\Drupal::keyValue('locale.translation_status')->deleteMultiple($projects);
}
/**
* Clear the translation status cache.
*/
function locale_translation_clear_status() {
\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 &amp; 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',
]));
}
/**
* 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 = []) {
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) {
$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('~(?<!\\\\)[\'"]\\s*\\+\\s*[\'"]~s', substr($string, 1, -1)));
}
/**
* Parses a JavaScript file, extracts translatable strings, and saves them.
*
* Strings are extracted from both Drupal.t() and Drupal.formatPlural().
*
* @param string $filepath
* File name to parse.
*
* @throws Exception
* If a non-local file is attempted to be parsed.
*/
function _locale_parse_js_file($filepath) {
// The file path might contain a query string, so make sure we only use the
// actual file.
$parsed_url = UrlHelper::parse($filepath);
$filepath = $parsed_url['path'];
// If there is still a protocol component in the path, reject that.
if (strpos($filepath, ':')) {
throw new Exception('Only local files should be passed to _locale_parse_js_file().');
}
// Load the JavaScript file.
$file = file_get_contents($filepath);
// Match all calls to Drupal.t() in an array.
// Note: \s also matches newlines with the 's' modifier.
preg_match_all('~
[^\\w]Drupal\\s*\\.\\s*t\\s* # match "Drupal.t" with whitespace
\\(\\s* # match "(" argument list start
(' . LOCALE_JS_STRING . ')\\s* # capture string argument
(?:,\\s*' . LOCALE_JS_OBJECT . '\\s* # optionally capture str args
(?:,\\s*' . LOCALE_JS_OBJECT_CONTEXT . '\\s*) # optionally capture context
?)? # close optional args
[,\\)] # match ")" or "," to finish
~sx', $file, $t_matches);
// Match all Drupal.formatPlural() calls in another array.
preg_match_all('~
[^\\w]Drupal\\s*\\.\\s*formatPlural\\s* # match "Drupal.formatPlural" with whitespace
\\( # match "(" argument list start
\\s*.+?\\s*,\\s* # match count argument
(' . LOCALE_JS_STRING . ')\\s*,\\s* # match singular string argument
( # capture plural string argument
(?: # non-capturing group to repeat string pieces
(?:
\'(?:\\\\\'|[^\'])*\' # match single-quoted string with any character except unescaped single-quote
|
"(?:\\\\"|[^"])*" # match double-quoted string with any character except unescaped double-quote
)
(?:\\s*\\+\\s*)? # match "+" with possible whitespace, for str concat
)+ # match multiple because we supports concatenating strs
)\\s* # end capturing of plural string argument
(?:,\\s*' . LOCALE_JS_OBJECT . '\\s* # optionally capture string args
(?:,\\s*' . LOCALE_JS_OBJECT_CONTEXT . '\\s*)? # optionally capture context
)?
[,\\)]
~sx', $file, $plural_matches);
$matches = [];
// Add strings from Drupal.t().
foreach ($t_matches[1] as $key => $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;
}
/**
* (Re-)Creates 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';
}
elseif ($changed_hash) {
$status = 'created';
}
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;
}
Functions
Title | Deprecated | Summary |
---|---|---|
locale_form_language_admin_add_form_alter_submit | Form submission handler for language_admin_add_form(). | |
locale_form_language_admin_edit_form_alter_submit | Form submission handler for language_admin_edit_form(). | |
locale_get_plural | Returns plural form index for a specific number. | |
locale_is_translatable | Checks whether $langcode is a language supported as a locale target. | |
locale_js_translate | Returns a list of translation files given a list of JavaScript files. | |
locale_preprocess_node | Implements hook_preprocess_HOOK() for node templates. | |
locale_string_is_safe | Check that a string is safe to be added or imported as a translation. | |
locale_system_file_system_settings_submit | Submit handler for the file system settings form. | |
locale_system_remove | Delete translation history of modules and themes. | |
locale_system_set_config_langcodes | Updates default configuration when new modules or themes are installed. | |
locale_system_update | Imports translations when new modules or themes are installed. | |
locale_translatable_language_list | Returns list of translatable languages. | |
locale_translation_clear_status | Clear the translation status cache. | |
locale_translation_file_history_delete | Deletes the history of downloaded translations. | |
locale_translation_get_file_history | Gets current translation status from the {locale_file} table. | |
locale_translation_get_status | Gets the current translation status. | |
locale_translation_language_table | Form element callback: After build changes to the language update table. | |
locale_translation_status_delete_languages | Delete language entries from the status cache. | |
locale_translation_status_delete_projects | Delete project entries from the status cache. | |
locale_translation_status_save | Saves the status of translation sources in static cache. | |
locale_translation_update_file_history | Updates the {locale_file} table. | |
locale_translation_use_remote_source | Checks whether remote translation sources are used. | |
_locale_invalidate_js | Force the JavaScript translation file(s) to be refreshed. | |
_locale_parse_js_file | Parses a JavaScript file, extracts translatable strings, and saves them. | |
_locale_rebuild_js | (Re-)Creates the JavaScript translation file for a language. | |
_locale_refresh_configuration | Refreshes configuration after string translations have been updated. | |
_locale_refresh_translations | Refresh related information after string translations have been updated. | |
_locale_strip_quotes | Removes the quotes and string concatenations from the string. |
Constants
Title | Deprecated | Summary |
---|---|---|
LOCALE_CUSTOMIZED | Flag for locally customized interface translation. | |
LOCALE_JS_OBJECT | Regular expression pattern used to match simple JS object literal. | |
LOCALE_JS_OBJECT_CONTEXT | Regular expression to match an object containing a key 'context'. | |
LOCALE_JS_STRING | Regular expression pattern used to localize JavaScript strings. | |
LOCALE_NOT_CUSTOMIZED | Flag for locally not customized interface translation. | |
LOCALE_TRANSLATION_CURRENT | Translation source is the current translation. | |
LOCALE_TRANSLATION_DEFAULT_SERVER_PATTERN | Default location of gettext file on the translation server. | |
LOCALE_TRANSLATION_LOCAL | Translation source is a local file. | |
LOCALE_TRANSLATION_OVERWRITE_ALL | UI option for override of existing translations. Override any translation. | |
LOCALE_TRANSLATION_OVERWRITE_NONE | UI option for override of existing translations. | |
LOCALE_TRANSLATION_OVERWRITE_NON_CUSTOMIZED | UI option for override of existing translations. | |
LOCALE_TRANSLATION_REMOTE | Translation source is a remote file. | |
LOCALE_TRANSLATION_STATUS_TTL | The number of seconds that the translations status entry should be considered. | |
LOCALE_TRANSLATION_USE_SOURCE_LOCAL | Translation update mode: Use local files only. | |
LOCALE_TRANSLATION_USE_SOURCE_REMOTE_AND_LOCAL | Translation update mode: Use both remote and local files. |
Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.