1433 lines
54 KiB
Plaintext
1433 lines
54 KiB
Plaintext
<?php
|
|
|
|
/**
|
|
* @file
|
|
* Enables the translation of the user interface to languages other than English.
|
|
*
|
|
* When enabled, multiple languages can be set up. The site interface can be
|
|
* displayed in different languages, and nodes can have languages assigned. The
|
|
* setup of languages and translations is completely web based. Gettext portable
|
|
* object files are supported.
|
|
*/
|
|
|
|
use Drupal\Core\Language\Language;
|
|
use Drupal\Component\Utility\Crypt;
|
|
|
|
/**
|
|
* 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 = 'http://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';
|
|
|
|
/**
|
|
* Implements hook_help().
|
|
*/
|
|
function locale_help($path, $arg) {
|
|
switch ($path) {
|
|
case 'admin/help#locale':
|
|
$output = '';
|
|
$output .= '<h3>' . t('About') . '</h3>';
|
|
$output .= '<p>' . t('The Locale module allows your Drupal site to be presented in languages other than the default English, and to be multilingual. The Locale module works by maintaining a database of translations, and examining text as it is about to be displayed. When a translation of the text is available in the language to be displayed, the translation is displayed rather than the original text. When a translation is unavailable, the original text is displayed, and then stored for review by a translator. For more information, see the online handbook entry for <a href="@locale">Locale module</a>.', array('@locale' => 'http://drupal.org/documentation/modules/locale/')) . '</p>';
|
|
$output .= '<h3>' . t('Uses') . '</h3>';
|
|
$output .= '<dl>';
|
|
$output .= '<dt>' . t('Translating interface text') . '</dt>';
|
|
$output .= '<dd>' . t('Translations of text in the Drupal interface may be provided by:');
|
|
$output .= '<ul>';
|
|
$output .= '<li>' . t('<a href="@update">Automatic import</a> of translations when you add a language or enable a module or theme. These translations are obtained from the <a href="@url">Drupal translation server</a>. Although Drupal modules and themes may not be fully translated in all languages, new translations become available frequently. Interface translation updates can be downloaded and installed automatically at <a href="@config">regular intervals</a>.', array('@url' => 'http://localize.drupal.org', '@update' => url('admin/reports/translations'), '@config' => url('admin/config/regional/translate/settings'))) . '</li>';
|
|
$output .= '<li>' . t("Translating within your site, using the Locale module's integrated <a href='@translate'>translation interface</a>.", array('@translate' => url('admin/config/regional/translate'))) . '</li>';
|
|
$output .= '<li>' . t("If an existing translations do not meet your needs, the interface translations files in Gettext Portable Object (<em>.po</em>) format may be modified, or new <em>.po</em> files may be created, using a desktop Gettext editor. The Locale module's <a href='@import'>manual import</a> feature allows the translated strings from a new or modified <em>.po</em> file to be added to your site. The Locale module's <a href='@export'>export</a> feature generates files from your site's translated strings, that can either be shared with others or edited offline by a Gettext translation editor.", array('@import' => url('admin/config/regional/translate/import'), '@export' => url('admin/config/regional/translate/export'))) . '</li>';
|
|
$output .= '</ul></dd>';
|
|
$output .= '</dl>';
|
|
return $output;
|
|
|
|
case 'admin/config/regional/language':
|
|
return '<p>' . t('Interface translations are automatically imported when a language is added, or when new modules or themes are enabled. The report, <a href="@update">Available translation updates</a>, shows the status. Interface text can be <a href="@translate">customized</a>.', array('@update' => url('admin/reports/translations'), '@translate' => url('admin/config/regional/translate'))) . '</p>';
|
|
|
|
case 'admin/config/regional/translate':
|
|
$output = '<p>' . t('This page allows a translator to search for specific translated and untranslated strings, and is used when creating or editing translations. (Note: Because translation tasks involves many strings, it may be more convenient to <a href="@export">export</a> strings for offline editing in a desktop Gettext translation editor). Searches may be limited to strings in a specific language.', array('@export' => url('admin/config/regional/translate/export'))) . '</p>';
|
|
return $output;
|
|
|
|
case 'admin/config/regional/translate/import':
|
|
$output = '<p>' . t('Translation files are automatically downloaded and imported when <a href="@language">languages</a> are added, or when modules or themes are enabled.', array('@language' => url('admin/config/regional/language'))). '</p>';
|
|
$output .= '<p>' . t('This page allows translators to manually import translated strings contained in a Gettext Portable Object (.po) file. Manual import may be used for customized translations or for the translation of custom modules and themes. To customize translations you can download a translation file from the <a href="@url">Drupal translation server</a> or <a href="@export">export</a> translations from the site, customize the translations using a Gettext translation editor, and import the result using this page.', array('@url' => 'http://localize.drupal.org', '@export' => url('admin/config/regional/translate/export'))) . '</p>';
|
|
$output .= '<p>' . t('Note that importing large .po files may take several minutes.') . '</p>';
|
|
return $output;
|
|
|
|
case 'admin/config/regional/translate/export':
|
|
return '<p>' . t('This page exports the translated strings used by your site. An export file may be in Gettext Portable Object (<em>.po</em>) form, which includes both the original string and the translation (used to share translations with others), or in Gettext Portable Object Template (<em>.pot</em>) form, which includes the original strings only (used to create new translations with a Gettext translation editor).') . '</p>';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Implements hook_menu().
|
|
*/
|
|
function locale_menu() {
|
|
// Translation functionality.
|
|
$items['admin/config/regional/translate'] = array(
|
|
'title' => 'User interface translation',
|
|
'description' => 'Translate the built-in user interface.',
|
|
'page callback' => 'locale_translate_page',
|
|
'access arguments' => array('translate interface'),
|
|
'file' => 'locale.pages.inc',
|
|
'weight' => -5,
|
|
);
|
|
$items['admin/config/regional/translate/translate'] = array(
|
|
'title' => 'Translate',
|
|
'type' => MENU_DEFAULT_LOCAL_TASK,
|
|
);
|
|
$items['admin/config/regional/translate/import'] = array(
|
|
'title' => 'Import',
|
|
'page callback' => 'drupal_get_form',
|
|
'page arguments' => array('locale_translate_import_form'),
|
|
'access arguments' => array('translate interface'),
|
|
'weight' => 20,
|
|
'type' => MENU_LOCAL_TASK,
|
|
'file' => 'locale.bulk.inc',
|
|
);
|
|
$items['admin/config/regional/translate/export'] = array(
|
|
'title' => 'Export',
|
|
'page callback' => 'drupal_get_form',
|
|
'page arguments' => array('locale_translate_export_form'),
|
|
'access arguments' => array('translate interface'),
|
|
'weight' => 30,
|
|
'type' => MENU_LOCAL_TASK,
|
|
'file' => 'locale.bulk.inc',
|
|
);
|
|
$items['admin/config/regional/translate/settings'] = array(
|
|
'title' => 'Settings',
|
|
'route_name' => 'locale_settings',
|
|
'access arguments' => array('translate interface'),
|
|
'weight' => 100,
|
|
'type' => MENU_LOCAL_TASK,
|
|
);
|
|
$items['admin/reports/translations'] = array(
|
|
'title' => 'Available translation updates',
|
|
'description' => 'Get a status report about available interface translations for your installed modules and themes.',
|
|
'page callback' => 'drupal_get_form',
|
|
'page arguments' => array('locale_translation_status_form'),
|
|
'access arguments' => array('translate interface'),
|
|
'file' => 'locale.pages.inc',
|
|
);
|
|
$items['admin/reports/translations/check'] = array(
|
|
'title' => 'Manual translation update check',
|
|
'page callback' => 'locale_translation_manual_status',
|
|
'access arguments' => array('translate interface'),
|
|
'type' => MENU_CALLBACK,
|
|
'file' => 'locale.pages.inc',
|
|
);
|
|
|
|
return $items;
|
|
}
|
|
|
|
/**
|
|
* Implements hook_permission().
|
|
*/
|
|
function locale_permission() {
|
|
return array(
|
|
'translate interface' => array(
|
|
'title' => t('Translate interface texts'),
|
|
'restrict access' => TRUE,
|
|
),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Implements hook_theme().
|
|
*/
|
|
function locale_theme() {
|
|
return array(
|
|
'locale_translate_edit_form_strings' => array(
|
|
'render element' => 'form',
|
|
'file' => 'locale.pages.inc',
|
|
),
|
|
'locale_translation_last_check' => array(
|
|
'variables' => array('last' => NULL),
|
|
'file' => 'locale.pages.inc',
|
|
'template' => 'locale-translation-last-check',
|
|
),
|
|
'locale_translation_update_info' => array(
|
|
'arguments' => array('updates' => array(), 'not_found' => array()),
|
|
'file' => 'locale.pages.inc',
|
|
'template' => 'locale-translation-update-info',
|
|
),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Implements hook_stream_wrappers().
|
|
*/
|
|
function locale_stream_wrappers() {
|
|
$wrappers = array(
|
|
'translations' => array(
|
|
'name' => t('Translation files'),
|
|
'class' => 'Drupal\locale\TranslationsStream',
|
|
'description' => t('Translation files'),
|
|
'type' => STREAM_WRAPPERS_LOCAL_HIDDEN,
|
|
),
|
|
);
|
|
return $wrappers;
|
|
}
|
|
|
|
/**
|
|
* Implements hook_language_insert().
|
|
*/
|
|
function locale_language_insert($language) {
|
|
// @todo move these two cache clears out. See http://drupal.org/node/1293252
|
|
// Changing the language settings impacts the interface.
|
|
cache('page')->deleteAll();
|
|
// Force JavaScript translation file re-creation for the new language.
|
|
_locale_invalidate_js($language->id);
|
|
}
|
|
|
|
/**
|
|
* Implements hook_language_update().
|
|
*/
|
|
function locale_language_update($language) {
|
|
// @todo move these two cache clears out. See http://drupal.org/node/1293252
|
|
// Changing the language settings impacts the interface.
|
|
cache('page')->deleteAll();
|
|
// Force JavaScript translation file re-creation for the modified language.
|
|
_locale_invalidate_js($language->id);
|
|
}
|
|
|
|
/**
|
|
* Implements hook_language_delete().
|
|
*/
|
|
function locale_language_delete($language) {
|
|
// Remove translations.
|
|
Drupal::service('locale.storage')->deleteTranslations(array('language' => $language->id));
|
|
|
|
// Remove interface translation files.
|
|
module_load_include('inc', 'locale', 'locale.bulk');
|
|
locale_translate_delete_translation_files(array(), array($language->id));
|
|
|
|
// Remove translated configuration objects.
|
|
\Drupal\locale\Locale::config()->deleteLanguageTranslations($language->id);
|
|
|
|
// Changing the language settings impacts the interface:
|
|
_locale_invalidate_js($language->id);
|
|
cache('page')->deleteAll();
|
|
|
|
// Clear locale translation caches.
|
|
locale_translation_status_delete_languages(array($language->id));
|
|
cache()->delete('locale:' . $language->id);
|
|
}
|
|
|
|
/**
|
|
* 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 = language_list();
|
|
if (!locale_translate_english()) {
|
|
unset($languages['en']);
|
|
}
|
|
return $languages;
|
|
}
|
|
|
|
/**
|
|
* Returns plural form index for a specific number.
|
|
*
|
|
* The index is computed from the formula of this language.
|
|
*
|
|
* @param $count
|
|
* Number to return plural for.
|
|
* @param $langcode
|
|
* Optional language code to translate to a language other than
|
|
* what is used to display the page.
|
|
* @return
|
|
* 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 = language(Language::TYPE_INTERFACE);
|
|
|
|
// Used to locally cache the plural formulas for all languages.
|
|
$plural_formulas = &drupal_static(__FUNCTION__, array());
|
|
// Used to store precomputed plural indexes corresponding to numbers
|
|
// individually for each language.
|
|
$plural_indexes = &drupal_static(__FUNCTION__ . ':plurals', array());
|
|
|
|
$langcode = $langcode ? $langcode : $language_interface->id;
|
|
|
|
if (!isset($plural_indexes[$langcode][$count])) {
|
|
// Retrieve and statically cache the plural formulas for all languages.
|
|
if (empty($plural_formulas)) {
|
|
$plural_formulas = Drupal::state()->get('locale.translation.plurals') ?: array();
|
|
}
|
|
// 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[$langcode])) {
|
|
// 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] = isset($plural_formulas[$langcode]['formula'][$index]) ? $plural_formulas[$langcode]['formula'][$index] : $plural_formulas[$langcode]['formula']['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];
|
|
}
|
|
|
|
|
|
/**
|
|
* Implements hook_modules_installed().
|
|
*/
|
|
function locale_modules_installed($modules) {
|
|
$components['module'] = $modules;
|
|
locale_system_update($components);
|
|
}
|
|
|
|
/**
|
|
* Implements hook_modules_uninstalled().
|
|
*/
|
|
function locale_modules_uninstalled($modules) {
|
|
$components['module'] = $modules;
|
|
locale_system_remove($components);
|
|
}
|
|
|
|
/**
|
|
* Implements hook_themes_enabled().
|
|
*
|
|
* @todo This is technically wrong. We must not import upon enabling, but upon
|
|
* initial installation. The theme system is missing an installation hook.
|
|
*/
|
|
function locale_themes_enabled($themes) {
|
|
$components['theme'] = $themes;
|
|
locale_system_update($components);
|
|
}
|
|
|
|
/**
|
|
* Implements hook_themes_disabled().
|
|
*/
|
|
function locale_themes_disabled($themes) {
|
|
$components['theme'] = $themes;
|
|
locale_system_remove($components);
|
|
}
|
|
|
|
/**
|
|
* Implements hook_cron().
|
|
*
|
|
* @see drupal_cron_run()
|
|
* @see locale_queue_info()
|
|
*/
|
|
function locale_cron() {
|
|
// Update translations only when an update frequency was set by the admin
|
|
// and a translatable language was set.
|
|
// Update tasks are added to the queue here but processed by Drupal's cron
|
|
// using the cron worker defined in locale_queue_info().
|
|
if ($frequency = config('locale.settings')->get('translation.update_interval_days') && locale_translatable_language_list()) {
|
|
module_load_include('translation.inc', 'locale');
|
|
locale_cron_fill_queue();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Implements hook_queue_info().
|
|
*/
|
|
function locale_queue_info() {
|
|
$queues['locale_translation'] = array(
|
|
'title' => t('Update translations'),
|
|
'worker callback' => 'locale_translation_worker',
|
|
'cron' => array(
|
|
'time' => 30,
|
|
),
|
|
);
|
|
return $queues;
|
|
}
|
|
|
|
/**
|
|
* Callback: Executes interface translation queue tasks.
|
|
*
|
|
* The translation update functions executed here are batch operations which
|
|
* are also used in translation update batches. The batch functions may need to
|
|
* be executed multiple times to complete their task, typically this is the
|
|
* translation import function. When a batch function is not finished, a new
|
|
* queue task is created and added to the end of the queue. The batch context
|
|
* data is needed to continue the batch task is stored in the queue with the
|
|
* queue data.
|
|
*
|
|
* @param array $data
|
|
* Queue data array containing:
|
|
* - Function name.
|
|
* - Array of function arguments. Optionally contains the batch context data.
|
|
*
|
|
* @see locale_queue_info()
|
|
*/
|
|
function locale_translation_worker($data) {
|
|
module_load_include('batch.inc', 'locale');
|
|
list($function, $args) = $data;
|
|
|
|
// We execute batch operation functions here to check, download and import the
|
|
// translation files. Batch functions use a context variable as last argument
|
|
// which is passed by reference. When a batch operation is called for the
|
|
// first time a default batch context is created. When called iterative
|
|
// (usually the batch import function) the batch context is passed through via
|
|
// the queue and is part of the $data.
|
|
$last = count($args) - 1;
|
|
if (!is_array($args[$last]) || !isset($args[$last]['finished'])) {
|
|
$batch_context = array(
|
|
'sandbox' => array(),
|
|
'results' => array(),
|
|
'finished' => 1,
|
|
'message' => '',
|
|
);
|
|
}
|
|
else {
|
|
$batch_context = $args[$last];
|
|
unset ($args[$last]);
|
|
}
|
|
$args = array_merge($args, array(&$batch_context));
|
|
|
|
// Call the batch operation function.
|
|
call_user_func_array($function, $args);
|
|
|
|
// If the batch operation is not finished we create a new queue task to
|
|
// continue the task. This is typically the translation import task.
|
|
if ($batch_context['finished'] < 1) {
|
|
unset($batch_context['strings']);
|
|
$queue = Drupal::queue('locale_translation', TRUE);
|
|
$queue->createItem(array($function, $args));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 += array('module' => array(), 'theme' => array());
|
|
$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 (!drupal_installation_attempted() && locale_translatable_language_list() && config('locale.settings')->get('translation.import_enabled')) {
|
|
module_load_include('compare.inc', 'locale');
|
|
|
|
// 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_load_include('fetch.inc', 'locale');
|
|
// Get translation status of the projects, download and update translations.
|
|
$options = _locale_translation_default_update_options();
|
|
$batch = locale_translation_batch_update_build($list, array(), $options);
|
|
batch_set($batch);
|
|
}
|
|
Drupal::moduleHandler()->loadInclude('locale', 'bulk.inc');
|
|
if ($batch = locale_config_batch_update_components(array(), array(), $components)) {
|
|
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 += array('module' => array(), 'theme' => array());
|
|
$list = array_merge($components['module'], $components['theme']);
|
|
if ($language_list = locale_translatable_language_list()) {
|
|
module_load_include('compare.inc', 'locale');
|
|
Drupal::moduleHandler()->loadInclude('locale', 'bulk.inc');
|
|
// Delete configuration translations.
|
|
\Drupal\locale\Locale::config()->deleteComponentTranslations($components, array_keys($language_list));
|
|
|
|
// 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, array());
|
|
|
|
// Remove translatable projects.
|
|
// Followup issue http://drupal.org/node/1842362 to replace the
|
|
// {locale_project} table. Then change this to a function call.
|
|
db_delete('locale_project')
|
|
->condition('name', $list)
|
|
->execute();
|
|
|
|
// Clear the translation status.
|
|
locale_translation_status_delete_projects($list);
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Implements hook_js_alter().
|
|
*
|
|
* This function checks all JavaScript files currently added via drupal_add_js()
|
|
* and invokes parsing if they have not yet been parsed for Drupal.t()
|
|
* and Drupal.formatPlural() calls. Also refreshes the JavaScript translation
|
|
* file if necessary, and adds it to the page.
|
|
*/
|
|
function locale_js_alter(&$javascript) {
|
|
|
|
$language_interface = language(Language::TYPE_INTERFACE);
|
|
|
|
$dir = 'public://' . config('local.settings')->get('javascript.directory');
|
|
$parsed = Drupal::state()->get('system.javascript_parsed') ?: array();
|
|
$files = $new_files = FALSE;
|
|
|
|
foreach ($javascript as $item) {
|
|
if (isset($item['type']) && $item['type'] == 'file') {
|
|
$files = TRUE;
|
|
$filepath = $item['data'];
|
|
if (!in_array($filepath, $parsed)) {
|
|
// Don't parse our own translations files.
|
|
if (substr($filepath, 0, strlen($dir)) != $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->id])) {
|
|
// 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->id]);
|
|
}
|
|
// 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('translation.javascript') ?: array();
|
|
if ($files && !empty($locale_javascripts[$language_interface->id])) {
|
|
// Add the translation JavaScript file to the page.
|
|
$file = $dir . '/' . $language_interface->id . '_' . $locale_javascripts[$language_interface->id] . '.js';
|
|
$javascript[$file] = drupal_js_defaults($file);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Implements hook_library_info().
|
|
*/
|
|
function locale_library_info() {
|
|
$libraries['drupal.locale.admin'] = array(
|
|
'title' => 'Locale',
|
|
'version' => VERSION,
|
|
'js' => array(
|
|
drupal_get_path('module', 'locale') . '/locale.admin.js' => array(),
|
|
),
|
|
'dependencies' => array(
|
|
array('system', 'jquery'),
|
|
array('system', 'drupal'),
|
|
array('system', 'jquery.once'),
|
|
),
|
|
);
|
|
$libraries['drupal.locale.datepicker'] = array(
|
|
'title' => 'Locale Datepicker UI',
|
|
'version' => VERSION,
|
|
'js' => array(
|
|
drupal_get_path('module', 'locale') . '/locale.datepicker.js' => array(),
|
|
),
|
|
'dependencies' => array(
|
|
array('system', 'jquery'),
|
|
array('system', 'drupal'),
|
|
array('system', 'drupalSettings'),
|
|
),
|
|
);
|
|
|
|
return $libraries;
|
|
}
|
|
|
|
/**
|
|
* Implement hook_library_info_alter().
|
|
*
|
|
* Provides the language support for the jQuery UI Date Picker.
|
|
*/
|
|
function locale_library_info_alter(&$libraries, $module) {
|
|
if ($module == 'system' && isset($libraries['jquery.ui.datepicker'])) {
|
|
$language_interface = language(Language::TYPE_INTERFACE);
|
|
// locale.datepicker.js should be added in the JS_LIBRARY group, so that
|
|
// this attach behavior will execute early. JS_LIBRARY is the default for
|
|
// hook_library_info_alter(), thus does not have to be specified explicitly.
|
|
$libraries['jquery.ui.datepicker']['dependencies'][] = array('locale', 'drupal.locale.datepicker');
|
|
$libraries['jquery.ui.datepicker']['js'][] = array(
|
|
'data' => array(
|
|
'jquery' => array(
|
|
'ui' => array(
|
|
'datepicker' => array(
|
|
'isRTL' => $language_interface->direction == Language::DIRECTION_RTL,
|
|
'firstDay' => config('system.date')->get('first_day'),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
'type' => 'setting',
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Implements hook_form_FORM_ID_alter() for language_admin_overview_form().
|
|
*/
|
|
function locale_form_language_admin_overview_form_alter(&$form, &$form_state) {
|
|
$languages = $form['languages']['#languages'];
|
|
|
|
$total_strings = Drupal::service('locale.storage')->countStrings();
|
|
$stats = array_fill_keys(array_keys($languages), array());
|
|
|
|
// If we have source strings, count translations and calculate progress.
|
|
if (!empty($total_strings)) {
|
|
$translations = Drupal::service('locale.storage')->countTranslations();
|
|
foreach ($translations as $langcode => $translated) {
|
|
$stats[$langcode]['translated'] = $translated;
|
|
if ($translated > 0) {
|
|
$stats[$langcode]['ratio'] = round($translated / $total_strings * 100, 2);
|
|
}
|
|
}
|
|
}
|
|
|
|
array_splice($form['languages']['#header'], -1, 0, t('Interface translation'));
|
|
|
|
foreach ($languages as $langcode => $language) {
|
|
$stats[$langcode] += array(
|
|
'translated' => 0,
|
|
'ratio' => 0,
|
|
);
|
|
if (!$language->locked && ($langcode != 'en' || locale_translate_english())) {
|
|
$form['languages'][$langcode]['locale_statistics'] = array(
|
|
'#markup' => l(
|
|
t('@translated/@total (@ratio%)', array(
|
|
'@translated' => $stats[$langcode]['translated'],
|
|
'@total' => $total_strings,
|
|
'@ratio' => $stats[$langcode]['ratio'],
|
|
)),
|
|
'admin/config/regional/translate/translate',
|
|
array('query' => array('langcode' => $langcode))
|
|
),
|
|
);
|
|
}
|
|
else {
|
|
$form['languages'][$langcode]['locale_statistics'] = array(
|
|
'#markup' => t('not applicable'),
|
|
);
|
|
}
|
|
// #type = link doesn't work with #weight on table.
|
|
// reset and set it back after locale_statistics to get it at the right end.
|
|
$operations = $form['languages'][$langcode]['operations'];
|
|
unset($form['languages'][$langcode]['operations']);
|
|
$form['languages'][$langcode]['operations'] = $operations;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Implements hook_form_FORM_ID_alter() for language_admin_add_form(().
|
|
*/
|
|
function locale_form_language_admin_add_form_alter(&$form, &$form_state) {
|
|
$form['predefined_submit']['#submit'][] = 'locale_form_language_admin_add_form_alter_submit';
|
|
$form['custom_language']['submit']['#submit'][] = 'locale_form_language_admin_add_form_alter_submit';
|
|
}
|
|
|
|
/**
|
|
* 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, $form_state) {
|
|
if (config('locale.settings')->get('translation.import_enabled')) {
|
|
if (empty($form_state['values']['predefined_langcode']) || $form_state['values']['predefined_langcode'] == 'custom') {
|
|
$langcode = $form_state['values']['langcode'];
|
|
}
|
|
else {
|
|
$langcode = $form_state['values']['predefined_langcode'];
|
|
}
|
|
|
|
// Download and import translations for the newly added language.
|
|
module_load_include('fetch.inc', 'locale');
|
|
$options = _locale_translation_default_update_options();
|
|
$batch = locale_translation_batch_update_build(array(), array($langcode), $options);
|
|
batch_set($batch);
|
|
|
|
// Create or update all configuration translations for this language.
|
|
Drupal::moduleHandler()->loadInclude('locale', 'bulk.inc');
|
|
if ($batch = locale_config_batch_update_components($options, array($langcode))) {
|
|
batch_set($batch);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Implements hook_form_FORM_ID_alter() for language_admin_edit_form().
|
|
*/
|
|
function locale_form_language_admin_edit_form_alter(&$form, &$form_state) {
|
|
if ($form['langcode']['#type'] == 'value' && $form['langcode']['#value'] == 'en') {
|
|
$form['locale_translate_english'] = array(
|
|
'#title' => t('Enable interface translation to English'),
|
|
'#type' => 'checkbox',
|
|
'#default_value' => locale_translate_english(),
|
|
);
|
|
$form['#submit'][] = 'locale_form_language_admin_edit_form_alter_submit';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Form submission handler for language_admin_edit_form().
|
|
*/
|
|
function locale_form_language_admin_edit_form_alter_submit($form, $form_state) {
|
|
variable_set('locale_translate_english', $form_state['values']['locale_translate_english']);
|
|
}
|
|
|
|
/**
|
|
* Checks whether locale translates to English.
|
|
*
|
|
* @return bool
|
|
* Returns TRUE if content should be translated to English, FALSE otherwise.
|
|
*/
|
|
function locale_translate_english() {
|
|
return variable_get('locale_translate_english', FALSE);
|
|
}
|
|
|
|
/**
|
|
* Implements hook_form_FORM_ID_alter() for system_file_system_settings().
|
|
*
|
|
* Add interface translation directory setting to directories configuration.
|
|
*/
|
|
function locale_form_system_file_system_settings_alter(&$form, $form_state) {
|
|
$form['translation_path'] = array(
|
|
'#type' => 'textfield',
|
|
'#title' => t('Interface translations directory'),
|
|
'#default_value' => config('locale.settings')->get('translation.path'),
|
|
'#maxlength' => 255,
|
|
'#description' => t('A local file system path where interface translation files will be stored.'),
|
|
'#required' => TRUE,
|
|
'#after_build' => array('system_check_directory'),
|
|
'#weight' => 10,
|
|
);
|
|
if ($form['file_default_scheme']) {
|
|
$form['file_default_scheme']['#weight'] = 20;
|
|
}
|
|
$form['#submit'][] = 'locale_system_file_system_settings_submit';
|
|
}
|
|
|
|
/**
|
|
* 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, $form_state) {
|
|
if ($form['translation_path']['#default_value'] != $form_state['values']['translation_path']) {
|
|
locale_translation_clear_status();
|
|
}
|
|
|
|
config('locale.settings')
|
|
->set('translation.path', $form_state['values']['translation_path'])
|
|
->save();
|
|
}
|
|
|
|
/**
|
|
* Implements hook_preprocess_HOOK() for node.html.twig.
|
|
*/
|
|
function locale_preprocess_node(&$variables) {
|
|
if ($variables['node']->langcode != Language::LANGCODE_NOT_SPECIFIED) {
|
|
$language_interface = language(Language::TYPE_INTERFACE);
|
|
|
|
$node_language = language_load($variables['node']->langcode);
|
|
if ($node_language->id != $language_interface->id) {
|
|
// 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'] = $variables['node']->langcode;
|
|
if ($node_language->direction != $language_interface->direction) {
|
|
// If text direction is different form the page's text direction, add
|
|
// direction information as well.
|
|
$dir = array('ltr', 'rtl');
|
|
$variables['attributes']['dir'] = $dir[$node_language->direction];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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__, array());
|
|
|
|
if (empty($history)) {
|
|
// Get file history from the database.
|
|
$result = db_query('SELECT project, langcode, filename, version, uri, timestamp, last_checked FROM {locale_file}');
|
|
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 integer
|
|
* FALSE on failure. Otherwise SAVED_NEW or SAVED_UPDATED.
|
|
*
|
|
* @see drupal_write_record()
|
|
*/
|
|
function locale_translation_update_file_history($file) {
|
|
// Update or write new record.
|
|
if (db_query("SELECT project FROM {locale_file} WHERE project = :project AND langcode = :langcode", array(':project' => $file->project, ':langcode' => $file->langcode))->fetchField()) {
|
|
$update = array('project', 'langcode');
|
|
}
|
|
else {
|
|
$update = array();
|
|
}
|
|
$result = drupal_write_record('locale_file', $file, $update);
|
|
// 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 $result;
|
|
}
|
|
|
|
/**
|
|
* 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 $langcode
|
|
* Language code(s) to be deleted from the file history.
|
|
*/
|
|
function locale_translation_file_history_delete($projects = array(), $langcodes = array()) {
|
|
$query = db_delete('locale_file');
|
|
if (!empty($projects)) {
|
|
$query->condition('project', $projects);
|
|
}
|
|
if (!empty($langcodes)) {
|
|
$query->condition('langcode', $langcodes);
|
|
}
|
|
$query->execute();
|
|
}
|
|
|
|
/**
|
|
* Gets the current translation status.
|
|
*
|
|
* @todo What is 'translation status'?
|
|
*/
|
|
function locale_translation_get_status($projects = NULL, $langcodes = NULL) {
|
|
$result = array();
|
|
$status = Drupal::state()->get('locale.translation_status');
|
|
module_load_include('translation.inc', 'locale');
|
|
$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(array($project), array($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 array $data
|
|
* File object also containing timestamp when the translation is last updated.
|
|
*/
|
|
function locale_translation_status_save($project, $langcode, $type, $data) {
|
|
// Followup issue: http://drupal.org/node/1842362
|
|
// Split status storage per module/language and expire individually. This will
|
|
// improve performance for large sites.
|
|
|
|
// Load the translation status or build it if not already available.
|
|
module_load_include('translation.inc', 'locale');
|
|
$status = locale_translation_get_status();
|
|
if (empty($status)) {
|
|
$projects = locale_translation_get_projects(array($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])) {
|
|
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::state()->set('locale.translation_status', $status);
|
|
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::state()->set('locale.translation_status', $status);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
$status = locale_translation_get_status();
|
|
|
|
foreach ($status as $project => $languages) {
|
|
if (in_array($project, $projects)) {
|
|
unset($status[$project]);
|
|
}
|
|
}
|
|
Drupal::state()->set('locale.translation_status', $status);
|
|
}
|
|
|
|
/**
|
|
* Clear the translation status cache.
|
|
*/
|
|
function locale_translation_clear_status() {
|
|
Drupal::state()->delete('locale.translation_status');
|
|
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 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,
|
|
* a & in the translation would get encoded to &amp; by filter_xss()
|
|
* before being put in the database, and thus would be displayed incorrectly.
|
|
*
|
|
* The allowed tag list is like filter_xss_admin(), 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) {
|
|
return decode_entities($string) == decode_entities(filter_xss($string, array('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.
|
|
*
|
|
* @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 = array()) {
|
|
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) || ($strings = Drupal::service('locale.storage')->getStrings(array('lid' => $lids, 'type' => 'javascript')))) {
|
|
array_map('_locale_invalidate_js', $langcodes);
|
|
}
|
|
}
|
|
// Clear locale cache.
|
|
cache()->deleteTags(array('locale' => TRUE));
|
|
}
|
|
|
|
/**
|
|
* Refreshes configuration after string translations have been updated.
|
|
*
|
|
* The information that will be refreshed includes:
|
|
* - JavaScript translations.
|
|
* - Locale cache.
|
|
*
|
|
* @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) {
|
|
if ($lids && $langcodes && $names = \Drupal\locale\Locale::config()->getStringNames($lids)) {
|
|
Drupal::moduleHandler()->loadInclude('locale', 'bulk.inc');
|
|
locale_config_update_multiple($names, $langcodes);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parses a JavaScript file, extracts strings wrapped in Drupal.t() and
|
|
* Drupal.formatPlural() and inserts them into the database.
|
|
*
|
|
* @param string $filepath
|
|
* File name to parse.
|
|
*
|
|
* @return array
|
|
* Array of string objects to update indexed by context and source.
|
|
*/
|
|
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 = drupal_parse_url($filepath);
|
|
$filepath = $parsed_url['path'];
|
|
// 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 start of single-quoted string
|
|
(?:\\\\\'|[^\'])* # match any character except unescaped single-quote
|
|
@count # match "@count"
|
|
(?:\\\\\'|[^\'])* # match any character except unescaped single-quote
|
|
\' # match end of single-quoted string
|
|
|
|
|
" # match start of double-quoted string
|
|
(?:\\\\"|[^"])* # match any character except unescaped double-quote
|
|
@count # match "@count"
|
|
(?:\\\\"|[^"])* # match any character except unescaped double-quote
|
|
" # match end of double-quoted string
|
|
)
|
|
(?:\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 = array();
|
|
|
|
// Add strings from Drupal.t().
|
|
foreach ($t_matches[1] as $key => $string) {
|
|
$matches[] = array(
|
|
'string' => $string,
|
|
'context' => $t_matches[2][$key],
|
|
);
|
|
}
|
|
|
|
// Add string from Drupal.formatPlural().
|
|
foreach ($plural_matches[1] as $key => $string) {
|
|
$matches[] = array(
|
|
'string' => $string,
|
|
'context' => $plural_matches[3][$key],
|
|
);
|
|
|
|
// If there is also a plural version of this string, add it to the strings array.
|
|
if (isset($plural_matches[2][$key])) {
|
|
$matches[] = array(
|
|
'string' => $plural_matches[2][$key],
|
|
'context' => $plural_matches[3][$key],
|
|
);
|
|
}
|
|
}
|
|
|
|
// Loop through all matches and process them.
|
|
foreach ($matches as $key => $match) {
|
|
|
|
// Remove the quotes and string concatenations from the string and context.
|
|
$string = implode('', preg_split('~(?<!\\\\)[\'"]\s*\+\s*[\'"]~s', substr($match['string'], 1, -1)));
|
|
$context = implode('', preg_split('~(?<!\\\\)[\'"]\s*\+\s*[\'"]~s', substr($match['context'], 1, -1)));
|
|
|
|
$source = Drupal::service('locale.storage')->findString(array('source' => $string, 'context' => $context));
|
|
|
|
if (!$source) {
|
|
// We don't have the source string yet, thus we insert it into the database.
|
|
$source = Drupal::service('locale.storage')->createString(array(
|
|
'source' => $string,
|
|
'context' => $context,
|
|
));
|
|
}
|
|
// 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 $langcode
|
|
* The language code for which the file needs to be refreshed.
|
|
*
|
|
* @return
|
|
* New content of the 'system.javascript_parsed' variable.
|
|
*/
|
|
function _locale_invalidate_js($langcode = NULL) {
|
|
$parsed = Drupal::state()->get('system.javascript_parsed') ?: array();
|
|
|
|
if (empty($langcode)) {
|
|
// Invalidate all languages.
|
|
$languages = locale_translatable_language_list();
|
|
foreach ($languages as $lcode => $data) {
|
|
$parsed['refresh:' . $lcode] = '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 $langcode
|
|
* The language, the translation file should be (re)created for.
|
|
*/
|
|
function _locale_rebuild_js($langcode = NULL) {
|
|
$config = config('locale.settings');
|
|
if (!isset($langcode)) {
|
|
$language = language(Language::TYPE_INTERFACE);
|
|
}
|
|
else {
|
|
// Get information about the locale.
|
|
$languages = language_list();
|
|
$language = $languages[$langcode];
|
|
}
|
|
|
|
// Construct the array for JavaScript translations.
|
|
// Only add strings with a translation to the translations array.
|
|
$conditions = array(
|
|
'type' => 'javascript',
|
|
'language' => $language->id,
|
|
'translated' => TRUE,
|
|
);
|
|
$translations = array();
|
|
foreach (Drupal::service('locale.storage')->getTranslations($conditions) as $data) {
|
|
$translations[$data->context][$data->source] = $data->translation;
|
|
}
|
|
|
|
// Construct the JavaScript file, if there are translations.
|
|
$data_hash = NULL;
|
|
$data = $status = '';
|
|
if (!empty($translations)) {
|
|
|
|
$data = "Drupal.locale = { ";
|
|
$locale_plurals = Drupal::state()->get('locale.translation.plurals') ?: array();
|
|
if (!empty($locale_plurals[$language->id])) {
|
|
$data .= "'pluralFormula': function (\$n) { return Number({$locale_plurals[$language->id]['formula']}); }, ";
|
|
}
|
|
|
|
$data .= "'strings': " . drupal_json_encode($translations) . " };";
|
|
$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') ?: array();
|
|
$changed_hash = !isset($locale_javascripts[$language->id]) || ($locale_javascripts[$language->id] != $data_hash);
|
|
if (!empty($locale_javascripts[$language->id]) && (!$data || $changed_hash)) {
|
|
file_unmanaged_delete($dir . '/' . $language->id . '_' . $locale_javascripts[$language->id] . '.js');
|
|
$locale_javascripts[$language->id] = '';
|
|
$status = 'deleted';
|
|
}
|
|
|
|
// Only create a new file if the content has changed or the original file got
|
|
// lost.
|
|
$dest = $dir . '/' . $language->id . '_' . $data_hash . '.js';
|
|
if ($data && ($changed_hash || !file_exists($dest))) {
|
|
// Ensure that the directory exists and is writable, if possible.
|
|
file_prepare_directory($dir, FILE_CREATE_DIRECTORY);
|
|
|
|
// Save the file.
|
|
if (file_unmanaged_save_data($data, $dest)) {
|
|
$locale_javascripts[$language->id] = $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->id] = '';
|
|
$status = 'error';
|
|
}
|
|
}
|
|
|
|
// 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.
|
|
switch ($status) {
|
|
case 'updated':
|
|
watchdog('locale', 'Updated JavaScript translation file for the language %language.', array('%language' => $language->name));
|
|
return TRUE;
|
|
case 'rebuilt':
|
|
watchdog('locale', 'JavaScript translation file %file.js was lost.', array('%file' => $locale_javascripts[$language->id]), WATCHDOG_WARNING);
|
|
// Proceed to the 'created' case as the JavaScript translation file has
|
|
// been created again.
|
|
case 'created':
|
|
watchdog('locale', 'Created JavaScript translation file for the language %language.', array('%language' => $language->name));
|
|
return TRUE;
|
|
case 'deleted':
|
|
watchdog('locale', 'Removed JavaScript translation file for the language %language because no translations currently exist for that language.', array('%language' => $language->name));
|
|
return TRUE;
|
|
case 'error':
|
|
watchdog('locale', 'An error occurred during creation of the JavaScript translation file for the language %language.', array('%language' => $language->name), WATCHDOG_ERROR);
|
|
return FALSE;
|
|
default:
|
|
// No operation needed.
|
|
return TRUE;
|
|
}
|
|
}
|