drupal/core/modules/locale/locale.module

955 lines
38 KiB
Plaintext

<?php
/**
* @file
* Add language handling functionality and 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, as well as nodes can have languages
* assigned. The setup of languages and translations is completely web based.
* Gettext portable object files are supported.
*/
use Drupal\locale\LocaleLookup;
use Drupal\locale\LocaleConfigSubscriber;
use Drupal\locale\TranslationsStream;
/**
* 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;
// ---------------------------------------------------------------------------------
// Hook implementations
/**
* 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("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('Importing files from a set of existing translations, known as a translation package. A translation package enables the display of a specific version of Drupal in a specific language, and contains files in the Gettext Portable Object (<em>.po</em>) format. Although not all languages are available for every version of Drupal, translation packages for many languages are available for download from the <a href="@translations">Drupal translations page</a>.', array('@translations' => 'http://localize.drupal.org')) . '</li>';
$output .= '<li>' . t("If an existing translation package does not meet your needs, the Gettext Portable Object (<em>.po</em>) files within a package may be modified, or new <em>.po</em> files may be created, using a desktop Gettext editor. The Locale module's <a href='@import'>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 text can be <a href="@translate">translated</a>. <a href="@translations">Download contributed translations</a> from Drupal.org.', array('@translations' => 'http://localize.drupal.org/download', '@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: For translation tasks involving 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('This page imports the translated strings contained in an individual Gettext Portable Object (<em>.po</em>) file. Normally distributed as part of a translation package (each translation package may contain several <em>.po</em> files), a <em>.po</em> file may need to be imported after offline editing in a Gettext translation editor. Importing an individual <em>.po</em> file may be a lengthy process.') . '</p>';
$output .= '<p>' . t('Note that the <em>.po</em> files within a translation package are imported automatically (if available) when new modules or themes are enabled, or as new languages are added. Since this page only allows the import of one <em>.po</em> file at a time, it may be simpler to download and extract a translation package into your Drupal installation directory and <a href="@language-add">add the language</a> (which automatically imports all <em>.po</em> files within the package). Translation packages are available for download on the <a href="@translations">Drupal translation page</a>.', array('@language-add' => url('admin/config/regional/language/add'), '@translations' => 'http://localize.drupal.org')) . '</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',
'weight' => -10,
'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',
);
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',
),
);
}
/**
* 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_NORMAL,
),
);
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')->flush();
// Force JavaScript translation file re-creation for the new language.
_locale_invalidate_js($language->langcode);
}
/**
* 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')->flush();
// Force JavaScript translation file re-creation for the modified language.
_locale_invalidate_js($language->langcode);
}
/**
* Implements hook_language_delete().
*/
function locale_language_delete($language) {
// Remove translations.
db_delete('locales_target')
->condition('language', $language->langcode)
->execute();
// Remove interface translation files.
module_load_include('inc', 'locale', 'locale.bulk');
locale_translate_delete_translation_files($language->langcode);
_locale_invalidate_js($language->langcode);
// Changing the language settings impacts the interface:
cache('page')->flush();
// Clearing all locale cache from database
cache()->delete('locale:' . $language->langcode);
}
// ---------------------------------------------------------------------------------
// Locale core functionality
/**
* Provides interface translation services.
*
* This function is called from t() to translate a string if needed.
*
* @param $string
* A string to look up translation for. If omitted, all the
* cached strings will be returned in all languages already
* used on the page.
* @param $context
* The context of this string.
* @param $langcode
* Language code to use for the lookup.
*/
function locale($string = NULL, $context = NULL, $langcode = NULL) {
$language_interface = language(LANGUAGE_TYPE_INTERFACE);
// Use the advanced drupal_static() pattern, since this is called very often.
static $drupal_static_fast;
if (!isset($drupal_static_fast)) {
$drupal_static_fast['locale'] = &drupal_static(__FUNCTION__, array('cache' => array(), 'exists' => NULL));
}
$locale_t = &$drupal_static_fast['locale']['cache'];
$locale_exists = &$drupal_static_fast['locale']['exists'];
// Check whether Locale module is actually installed and operational.
// The mere existence of locale() does not imply that Locale module is
// actually enabled and its database tables are installed. Since PHP code
// cannot be unloaded, this is typically the case in the environment that
// is executing a test.
if (!isset($locale_exists)) {
$locale_exists = function_exists('module_exists') && module_exists('locale');
}
if (!$locale_exists) {
return $string;
}
if (!isset($string)) {
// Return all cached strings if no string was specified
return $locale_t;
}
$langcode = isset($langcode) ? $langcode : $language_interface->langcode;
// Strings are cached by langcode, context and roles, using instances of the
// LocaleLookup class to handle string lookup and caching.
if (!isset($locale_t[$langcode][$context]) && isset($language_interface)) {
$locale_t[$langcode][$context] = new LocaleLookup($langcode, $context);
}
return ($locale_t[$langcode][$context][$string] === TRUE ? $string : $locale_t[$langcode][$context][$string]);
}
/**
* Reset static variables used by locale().
*/
function locale_reset() {
drupal_static_reset('locale');
}
/**
* 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->langcode;
if (!isset($plural_indexes[$langcode][$count])) {
// Retrieve and statically cache the plural formulas for all languages.
if (empty($plural_formulas)) {
$plural_formulas = variable_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])) {
// $n is used inside the expression in the eval().
$n = $count;
$plural_indexes[$langcode][$count] = @eval('return intval(' . $plural_formulas[$langcode]['formula'] . ');');
}
// 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) {
locale_system_update($modules);
}
/**
* 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) {
locale_system_update($themes);
}
/**
* Imports translations when new modules or themes are installed.
*
* This function will either import translation for the component change
* right away, or start a batch if more files need to be imported.
*
* @param $components
* An array of component (theme and/or module) names to import
* translations for.
*
* @todo
* This currently imports all .po files available, independent of
* $components. Once we integrated with update status for project
* identification, we should revisit and only import files for the
* identified projects for the components.
*/
function locale_system_update($components) {
// 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()) {
include_once drupal_get_path('module', 'locale') . '/locale.bulk.inc';
if ($batch = locale_translate_batch_import_files(array(), TRUE)) {
batch_set($batch);
}
}
}
/**
* 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://' . variable_get('locale_js_directory', 'languages');
$parsed = variable_get('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->langcode])) {
// 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->langcode]);
}
// Store any changes after refresh was attempted.
variable_set('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) {
variable_set('javascript_parsed', $parsed);
}
// Add the translation JavaScript file to the page.
$locale_javascripts = variable_get('locale_translation_javascript', array());
if ($files && !empty($locale_javascripts[$language_interface->langcode])) {
// Add the translation JavaScript file to the page.
$file = $dir . '/' . $language_interface->langcode . '_' . $locale_javascripts[$language_interface->langcode] . '.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_RTL,
'firstDay' => variable_get('date_first_day', 0),
),
),
),
),
'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 = db_query("SELECT COUNT(*) FROM {locales_source}")->fetchField();
$stats = array_fill_keys(array_keys($languages), array());
// If we have source strings, count translations and calculate progress.
if (!empty($total_strings)) {
$translations = db_query("SELECT COUNT(*) AS translated, t.language FROM {locales_source} s INNER JOIN {locales_target} t ON s.lid = t.lid GROUP BY language");
foreach ($translations as $data) {
$stats[$data->language]['translated'] = $data->translated;
if ($data->translated > 0) {
$stats[$data->language]['ratio'] = round($data->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'),
);
}
}
}
/**
* 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';
}
/**
* Set a batch for newly added language.
*/
function locale_form_language_admin_add_form_alter_submit($form, $form_state) {
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'];
}
include_once drupal_get_path('module', 'locale') . '/locale.bulk.inc';
locale_translate_add_language_set_batch(array('langcode' => $langcode));
}
/**
* 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';
}
}
/**
* Submission handler to record our custom setting.
*/
function locale_form_language_admin_edit_form_alter_submit($form, $form_state) {
variable_set('locale_translate_english', $form_state['values']['locale_translate_english']);
}
/**
* Utility function to tell if locale translates to English.
*/
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['locale_translate_file_directory'] = array(
'#type' => 'textfield',
'#title' => t('Interface translations directory'),
'#default_value' => variable_get('locale_translate_file_directory', conf_path() . '/files/translations'),
'#maxlength' => 255,
'#description' => t('A local file system path where interface translation files are looked for. This directory must exist.'),
'#after_build' => array('system_check_directory'),
'#weight' => 10,
);
if ($form['file_default_scheme']) {
$form['file_default_scheme']['#weight'] = 20;
}
}
/**
* Implements hook_preprocess_HOOK() for node.tpl.php.
*/
function locale_preprocess_node(&$variables) {
if ($variables['langcode'] != LANGUAGE_NOT_SPECIFIED) {
$language_interface = language(LANGUAGE_TYPE_INTERFACE);
$node_language = language_load($variables['langcode']);
if ($node_language->langcode != $language_interface->langcode) {
// 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['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];
}
}
}
}
/**
* 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 &amp; in the translation would get encoded to &amp;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')));
}
/**
* Parses a JavaScript file, extracts strings wrapped in Drupal.t() and
* Drupal.formatPlural() and inserts them into the database.
*/
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 = db_query("SELECT lid, location FROM {locales_source} WHERE source = :source AND context = :context", array(':source' => $string, ':context' => $context))->fetchObject();
if ($source) {
// We already have this source string and now have to add the location
// to the location column, if this file is not yet present in there.
$locations = preg_split('~\s*;\s*~', $source->location);
if (!in_array($filepath, $locations)) {
$locations[] = $filepath;
$locations = implode('; ', $locations);
// Save the new locations string to the database.
db_update('locales_source')
->fields(array(
'location' => $locations,
))
->condition('lid', $source->lid)
->execute();
}
}
else {
// We don't have the source string yet, thus we insert it into the database.
db_insert('locales_source')
->fields(array(
'location' => $filepath,
'source' => $string,
'context' => $context,
))
->execute();
}
}
}
/**
* 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 'javascript_parsed' variable.
*/
function _locale_invalidate_js($langcode = NULL) {
$parsed = variable_get('javascript_parsed', array());
if (empty($langcode)) {
// Invalidate all languages.
$languages = language_list();
if (!locale_translate_english()) {
unset($languages['en']);
}
foreach ($languages as $lcode => $data) {
$parsed['refresh:' . $lcode] = 'waiting';
}
}
else {
// Invalidate single language.
$parsed['refresh:' . $langcode] = 'waiting';
}
variable_set('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) {
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.
$result = db_query("SELECT s.lid, s.source, s.context, t.translation FROM {locales_source} s INNER JOIN {locales_target} t ON s.lid = t.lid AND t.language = :language WHERE s.location LIKE '%.js%'", array(':language' => $language->langcode));
$translations = array();
foreach ($result 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 = variable_get('locale_translation_plurals', array());
if (!empty($locale_plurals[$language->langcode])) {
$data .= "'pluralFormula': function (\$n) { return Number({$locale_plurals[$language->langcode]['formula']}); }, ";
}
$data .= "'strings': " . drupal_json_encode($translations) . " };";
$data_hash = drupal_hash_base64($data);
}
// Construct the filepath where JS translation files are stored.
// There is (on purpose) no front end to edit that variable.
$dir = 'public://' . variable_get('locale_js_directory', 'languages');
// Delete old file, if we have no translations anymore, or a different file to be saved.
$locale_javascripts = variable_get('locale_translation_javascript', array());
$changed_hash = !isset($locale_javascripts[$language->langcode]) || ($locale_javascripts[$language->langcode] != $data_hash);
if (!empty($locale_javascripts[$language->langcode]) && (!$data || $changed_hash)) {
file_unmanaged_delete($dir . '/' . $language->langcode . '_' . $locale_javascripts[$language->langcode] . '.js');
$locale_javascripts[$language->langcode] = '';
$status = 'deleted';
}
// Only create a new file if the content has changed or the original file got
// lost.
$dest = $dir . '/' . $language->langcode . '_' . $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->langcode] = $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->langcode] = '';
$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) {
variable_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->langcode]), 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;
}
}
/**
* Implements hook_language_init().
*/
function locale_language_init() {
// Add locale helper to configuration subscribers.
drupal_container()->get('dispatcher')->addSubscriber(new LocaleConfigSubscriber());
}