882 lines
36 KiB
Plaintext
882 lines
36 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;
|
|
|
|
/**
|
|
* 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', '@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_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 = drupal_container()->get(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__);
|
|
}
|
|
$locale_t = &$drupal_static_fast['locale'];
|
|
|
|
|
|
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 = drupal_container()->get(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) {
|
|
include_once drupal_get_path('module', 'locale') . '/locale.bulk.inc';
|
|
if ($batch = locale_translate_batch_import_files(NULL, 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 = drupal_container()->get(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 ($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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 = drupal_container()->get(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.
|
|
$datepicker = drupal_get_path('module', 'locale') . '/locale.datepicker.js';
|
|
$libraries['jquery.ui.datepicker']['js'][$datepicker] = array();
|
|
$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($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 = drupal_container()->get(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_array']['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_array']['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 & 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')));
|
|
}
|
|
|
|
/**
|
|
* 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 = drupal_container()->get(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;
|
|
}
|
|
}
|