locale.module 21.4 KB
Newer Older
Dries's avatar
 
Dries committed
1
<?php
2
// $Id$
Dries's avatar
 
Dries committed
3

Dries's avatar
 
Dries committed
4 5
/**
 * @file
6 7
 *   Add language handling functionality and enables the translation of the
 *   user interface to languages other than English.
Dries's avatar
 
Dries committed
8
 *
9 10 11 12
 *   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.
Dries's avatar
 
Dries committed
13 14
 */

15 16 17 18 19 20 21 22 23 24 25
/**
 * Language written left to right. Possible value of $language->direction.
 */
define('LANGUAGE_LTR', 0);

/**
 * Language written right to left. Possible value of $language->direction.
 */
define('LANGUAGE_RTL', 1);


Dries's avatar
 
Dries committed
26
// ---------------------------------------------------------------------------------
27
// Hook implementations
Dries's avatar
 
Dries committed
28

Dries's avatar
 
Dries committed
29 30 31
/**
 * Implementation of hook_help().
 */
32 33
function locale_help($path, $arg) {
  switch ($path) {
34
    case 'admin/help#locale':
35
      $output = '<p>'. t('The locale module allows you to present your Drupal site in a language other than the default English. You can use it to set up a multi-lingual website or replace given <em>built-in</em> text with text which has been customized for your site. Whenever the locale module encounters text which needs to be displayed, it tries to translate it into the currently selected language. If a translation is not available, then the string is remembered, so you can look up untranslated strings easily.') .'</p>';
36
      $output .= '<p>'. t('The locale module provides two options for providing translations. The first is the integrated web interface, via which you can search for untranslated strings, and specify their translations. An easier and less time-consuming method is to import existing translations for your language. These translations are available as <em>GNU gettext Portable Object files</em> (<em>.po</em> files for short). Translations for many languages are available for download from the translation page.') .'</p>';
37
      $output .= '<p>'. t("If an existing translation does not meet your needs, the <em>.po</em> files are easily edited with special editing tools. The locale module's import feature allows you to add strings from such files into your site's database. The export functionality enables you to share your translations with others, generating Portable Object files from your site strings.") .'</p>';
38
      $output .= '<p>'. t('For more information please read the configuration and customization handbook <a href="@locale">Locale page</a>.', array('@locale' => 'http://drupal.org/handbook/modules/locale/')) .'</p>';
39
      return $output;
40 41 42 43 44 45 46 47 48 49 50

    case 'admin/settings/language':
      return t("<p>Drupal provides support for the translation of its interface text into different languages. This page provides an overview of the installed languages. You can add a language on the <a href=\"@add-language\">add language page</a>, or directly by <a href=\"@import\">importing a translation</a>. If multiple languages are enabled, registered users will be able to set their preferred language. The site default will be used for anonymous visitors and for users without their own settings.</p><p>Drupal interface translations may be added or extended by several courses: by <a href=\"@import\">importing</a> an existing translation, by <a href=\"@search\">translating everything</a> from scratch, or by a combination of these approaches.</p>", array("@search" => url("admin/build/translate/search"), "@import" => url("admin/build/translate/import"), "@add-language" => url("admin/settings/language/add")));
    case 'admin/settings/language/add':
      return '<p>'. t("You need to add all languages in which you would like to display the site interface. If you can't find the desired language in the quick-add dropdown, then you will need to provide the proper language code yourself. The language code may be used to negotiate with browsers and to present flags, etc., so it is important to pick a code that is standardised for the desired language. You can also add a language by <a href=\"@import\">importing a translation</a>.", array("@import" => url("admin/build/translate/import"))) .'</p>';
    case 'admin/settings/language/configure':
      return '<p>'. t('The language used to display a web page is determined with a negotiation algorithm. You can choose how this algorithm should work. By default, there is no negotiation and the default language is used. You can use path prefixes (like "de" and "it" for German and Italian content) with different fallback options, so you can have web addresses like /de/contact and /it/contact. Alternatively you can use custom domains like de.example.com and it.example.com. Customize path prefixes and set domain names on the <a href="@languages">language editing pages</a>.', array('@languages' => url('admin/settings/language'))) .'</p>';

    case 'admin/build/translate':
      return '<p>'. t("This page provides an overview of interface translation on the site. Drupal groups all translatable strings in so called 'text groups'. Text groups are useful, because you can focus your translation efforts on the groups of text you care most about.  For example, a translation team could choose not to fully translate the text group that includes all the long help texts on the administration pages. Similarly, text groups are useful to ensure that certain aspects of the site are always fully translated.") . '</p>';
    case 'admin/build/translate/import':
51
      return '<p>'. t("This page allows you to import a translation provided in the gettext Portable Object (.po) format. The easiest way to get your site translated is to obtain an existing Drupal translation and to import it. You can find existing translations on the <a href=\"@url\">Drupal translation page</a>. Note that importing a translation file might take a while.", array('@url' => 'http://drupal.org/project/translations')) .'</p>';
52
    case 'admin/build/translate/export':
53
      return '<p>'. t("This page allows you to export Drupal strings. The first option is to export a translation so it can be shared. The second option generates a translation template, which contains all Drupal strings, but without their translations. You can use this template to start a new translation using various software packages designed for this task.") .'</p>';
54 55
    case 'admin/build/translate/search':
      return '<p>'. t("It is often convenient to get the strings from your setup on the <a href=\"@export\">export page</a>, and use a desktop Gettext translation editor to edit the translations. On this page you can search in the translated and untranslated strings, and the default English texts provided by Drupal.", array("@export" => url("admin/build/translate/export"))) .'</p>';
Dries's avatar
 
Dries committed
56
  }
Dries's avatar
 
Dries committed
57 58
}

Dries's avatar
 
Dries committed
59
/**
Dries's avatar
 
Dries committed
60
 * Implementation of hook_menu().
61 62 63
 *
 * Locale module only provides administrative menu items, so all
 * menu items are invoked through locale_inc_callback().
Dries's avatar
 
Dries committed
64
 */
65
function locale_menu() {
66 67
  // Manage languages
  $items['admin/settings/language'] = array(
68
    'title' => 'Languages',
69 70 71 72
    'description' => 'Configure languages for content and the user interface.',
    'page callback' => 'locale_inc_callback',
    'page arguments' => array('drupal_get_form', 'locale_languages_overview_form'),
    'access arguments' => array('administer languages'),
73
  );
74
  $items['admin/settings/language/overview'] = array(
75
    'title' => 'List',
76 77 78
    'weight' => 0,
    'type' => MENU_DEFAULT_LOCAL_TASK,
  );
79
  $items['admin/settings/language/add'] = array(
80
    'title' => 'Add language',
81 82
    'page callback' => 'locale_inc_callback',
    'page arguments' => array('locale_languages_add_screen'), // two forms concatenated
83 84 85
    'weight' => 5,
    'type' => MENU_LOCAL_TASK,
  );
86
  $items['admin/settings/language/configure'] = array(
87
    'title' => 'Configure',
88 89
    'page callback' => 'locale_inc_callback',
    'page arguments' => array('drupal_get_form', 'locale_languages_configure_form'),
90 91 92
    'weight' => 10,
    'type' => MENU_LOCAL_TASK,
  );
93
  $items['admin/settings/language/edit/%'] = array(
94
    'title' => 'Edit language',
95 96 97 98 99 100 101 102
    'page callback' => 'locale_inc_callback',
    'page arguments' => array('drupal_get_form', 'locale_languages_edit_form', 4),
    'type' => MENU_CALLBACK,
  );
  $items['admin/settings/language/delete/%'] = array(
    'title' => 'Confirm',
    'page callback' => 'locale_inc_callback',
    'page arguments' => array('drupal_get_form', 'locale_languages_delete_form', 4),
103 104 105
    'type' => MENU_CALLBACK,
  );

106 107 108 109 110 111 112 113 114 115
  // Translation functionality
  $items['admin/build/translate'] = array(
    'title' => 'Translate interface',
    'description' => 'Translate the built in interface as well as menu items and taxonomies.',
    'page callback' => 'locale_inc_callback',
    'page arguments' => array('locale_translate_overview_screen'), // not a form, just a table
    'access arguments' => array('translate interface'),
  );
  $items['admin/build/translate/overview'] = array(
    'title' => 'Overview',
116 117 118
    'weight' => 0,
    'type' => MENU_DEFAULT_LOCAL_TASK,
  );
119 120
  $items['admin/build/translate/search'] = array(
    'title' => 'Search',
121 122
    'weight' => 10,
    'type' => MENU_LOCAL_TASK,
123 124
    'page callback' => 'locale_inc_callback',
    'page arguments' => array('locale_translate_seek_screen'), // search results and form concatenated
125
  );
126 127 128 129
  $items['admin/build/translate/import'] = array(
    'title' => 'Import',
    'page callback' => 'locale_inc_callback',
    'page arguments' => array('drupal_get_form', 'locale_translate_import_form'),
130 131 132
    'weight' => 20,
    'type' => MENU_LOCAL_TASK,
  );
133 134 135 136 137 138
  $items['admin/build/translate/export'] = array(
    'title' => 'Export',
    'page callback' => 'locale_inc_callback',
    'page arguments' => array('locale_translate_export_screen'), // possibly multiple forms concatenated
    'weight' => 30,
    'type' => MENU_LOCAL_TASK,
139
  );
140
  $items['admin/build/translate/edit/%'] = array(
141
    'title' => 'Edit string',
142 143
    'page callback' => 'locale_inc_callback',
    'page arguments' => array('drupal_get_form', 'locale_translate_edit_form', 4),
144 145
    'type' => MENU_CALLBACK,
  );
146
  $items['admin/build/translate/delete/%'] = array(
147
    'title' => 'Delete string',
148 149
    'page callback' => 'locale_inc_callback',
    'page arguments' => array('locale_translate_delete', 4),  // directly deletes, no confirmation
150 151
    'type' => MENU_CALLBACK,
  );
Dries's avatar
 
Dries committed
152

Dries's avatar
 
Dries committed
153
  return $items;
Dries's avatar
 
Dries committed
154 155
}

156 157 158 159 160 161 162 163 164 165
/**
 * Wrapper function to be able to set callbacks in locale.inc
 */
function locale_inc_callback() {
  $args = func_get_args();
  $function = array_shift($args);
  include_once './includes/locale.inc';
  return call_user_func_array($function, $args);
}

Dries's avatar
 
Dries committed
166
/**
Dries's avatar
 
Dries committed
167
 * Implementation of hook_perm().
Dries's avatar
 
Dries committed
168
 */
Dries's avatar
 
Dries committed
169
function locale_perm() {
170 171 172 173 174 175 176 177 178 179 180
  return array('administer languages', 'translate interface');
}

/**
 * Implementation of hook_locale().
 */
function locale_locale($op = 'groups') {
  switch ($op) {
    case 'groups':
      return array('default' => t('Built-in interface'));
  }
Dries's avatar
 
Dries committed
181 182 183 184 185 186
}

/**
 * Implementation of hook_user().
 */
function locale_user($type, $edit, &$user, $category = NULL) {
187 188 189
  if ($type == 'form' && $category == 'account' && variable_get('language_count', 1) > 1 && variable_get('language_negotiation', LANGUAGE_NEGOTIATION_NONE) == LANGUAGE_NEGOTIATION_PATH) {
    $languages = language_list('enabled');
    $languages = $languages['1'];
Dries's avatar
 
Dries committed
190
    if ($user->language == '') {
191
      $user->language = language_default('language');
192 193
    }
    $names = array();
194
    foreach ($languages as $langcode => $language) {
195
      $names[$langcode] = t($language->name) .' ('. $language->native .')';
Dries's avatar
 
Dries committed
196
    }
197 198 199 200 201 202 203
    $form['locale'] = array('#type' => 'fieldset',
      '#title' => t('Interface language settings'),
      '#weight' => 1,
    );
    $form['locale']['language'] = array('#type' => 'radios',
      '#title' => t('Language'),
      '#default_value' => $user->language,
204
      '#options' => $names,
205 206
      '#description' => t('Selecting a different locale will change the interface language of the site.'),
    );
207
    return $form;
Dries's avatar
 
Dries committed
208 209 210
  }
}

211 212 213
/**
 * Implementation of hook_form_alter(). Adds language fields to forms.
 */
214
function locale_form_alter(&$form, $form_state, $form_id) {
215
  switch ($form_id) {
216 217

    // Language field for paths
218 219
    case 'path_admin_edit':
      $form['language'] = array(
220
        '#type' => 'select',
221 222
        '#title' => t('Language'),
        '#options' => array('' => t('All languages')) + locale_language_list('name'),
223 224 225
        '#default_value' => $form['language']['#value'],
        '#weight' => -10,
        '#description' => t('Path aliases added for languages take precedence over path aliases added for all languages for the same Drupal path.'),
226 227
      );
      break;
228 229 230 231 232 233 234 235 236

    // Language setting for content types
    case 'node_type_form':
      if (isset($form['identity']['type'])) {
        $form['workflow']['language'] = array(
          '#type' => 'radios',
          '#title' => t('Multilingual support'),
          '#default_value' => variable_get('language_'. $form['#node_type']->type, 0),
          '#options' => array(t('Disabled'), t('Enabled')),
237
          '#description' => t('Enable multilingual support for this content type. If enabled, a language selection field will be added to the editing form, allowing you to select from one of the <a href="!languages">enabled languages</a>. If disabled, new posts are saved with the default language. Existing content will not be affected by changing this option.', array('!languages' => url('admin/settings/language'))),
238 239 240 241 242 243 244
        );
      }
      break;

    // Language field for nodes
    default:
      if ($form['#id'] == 'node-form') {
245
        if (isset($form['#node']->type) && variable_get('language_' . $form['#node']->type, 0)) {
246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261
          $form['language'] = array(
            '#type' => 'select',
            '#title' => t('Language'),
            '#default_value' => (isset($form['#node']->language) ? $form['#node']->language : ''),
            '#options' => array('' => t('Language neutral')) + locale_language_list('name'),
          );
        }
        // Node type without language selector: assign the default for new nodes
        elseif (!isset($form['#node']->nid)) {
          $default = language_default();
          $form['language'] = array(
            '#type' => 'value',
            '#value' => $default->language
          );
        }
      }
262 263 264
  }
}

265 266 267 268 269 270 271 272 273 274
/**
 * Implementation of hook_theme()
 */
function locale_theme() {
  return array(
    'locale_languages_overview_form' => array(
      'arguments' => array('form' => array()),
    ),
  );
}
275

Dries's avatar
 
Dries committed
276
// ---------------------------------------------------------------------------------
277
// Locale core functionality
Dries's avatar
 
Dries committed
278

Dries's avatar
 
Dries committed
279
/**
280
 * Provides interface translation services.
Dries's avatar
 
Dries committed
281 282
 *
 * This function is called from t() to translate a string if needed.
283 284 285
 *
 * @param $string
 *   A string to look up translation for. If omitted, all the
286
 *   cached strings will be returned in all languages already
287 288 289
 *   used on the page.
 * @param $langcode
 *   Language code to use for the lookup.
Dries's avatar
 
Dries committed
290
 */
291
function locale($string = NULL, $langcode = NULL) {
292
  global $language;
Dries's avatar
 
Dries committed
293
  static $locale_t;
Dries's avatar
 
Dries committed
294

295 296 297 298 299
  // Return all cached strings if no string was specified
  if (!isset($string)) {
    return $locale_t;
  }

300 301
  $langcode = isset($langcode) ? $langcode : $language->language;

302
  // Store database cached translations in a static var.
303 304
  if (!isset($locale_t[$langcode])) {
    $locale_t[$langcode] = array();
305 306 307
    // Disabling the usage of string caching allows a module to watch for
    // the exact list of strings used on a page. From a performance
    // perspective that is a really bad idea, so we have no user
308 309 310 311 312 313 314 315 316
    // interface for this. Be careful when turning this option off!
    if (variable_get('locale_cache_strings', 1) == 1) {
      if (!($cache = cache_get('locale:'. $langcode, 'cache'))) {
        locale_refresh_cache();
        $cache = cache_get('locale:'. $langcode, 'cache');
      }
      if ($cache) {
        $locale_t[$langcode] = $cache->data;
      }
317
    }
Dries's avatar
 
Dries committed
318 319
  }

Dries's avatar
 
Dries committed
320 321
  // We have the translation cached (if it is TRUE, then there is no
  // translation, so there is no point in checking the database)
322 323
  if (isset($locale_t[$langcode][$string])) {
    $string = ($locale_t[$langcode][$string] === TRUE ? $string : $locale_t[$langcode][$string]);
Dries's avatar
 
Dries committed
324
  }
Dries's avatar
 
Dries committed
325

326
  // We do not have this translation cached, so get it from the DB.
Dries's avatar
 
Dries committed
327
  else {
328 329 330 331 332 333 334
    $translation = db_fetch_object(db_query("SELECT s.lid, t.translation FROM {locales_source} s LEFT JOIN {locales_target} t ON s.lid = t.lid WHERE s.source = '%s' AND t.language = '%s' AND s.textgroup = 'default'", $string, $langcode));
    if ($translation) {
      // We have the source string at least.
      if ($translation->lid) {
        // Cache translation string or TRUE if no translation exists.
        $translation = (empty($translation->translation) ? TRUE : $translation->translation);
        $locale_t[$langcode][$string] = $translation;
335
      }
Dries's avatar
 
Dries committed
336 337
    }
    else {
338 339 340 341
      // We don't have the source string, cache this as untranslated.
      db_query("INSERT INTO {locales_source} (location, source, textgroup) VALUES ('%s', '%s', 'default')", request_uri(), $string);
      $locale_t[$langcode][$string] = TRUE;
      // Clear locale cache so this string can be added in a later request.
342
      cache_clear_all('locale:'. $langcode, 'cache');
Dries's avatar
 
Dries committed
343
    }
Dries's avatar
 
Dries committed
344 345
  }

Dries's avatar
 
Dries committed
346 347
  return $string;
}
Dries's avatar
 
Dries committed
348

Dries's avatar
 
Dries committed
349
/**
350
 * Refreshes database stored cache of translations.
Dries's avatar
 
Dries committed
351 352 353 354
 *
 * We only store short strings to improve performance and consume less memory.
 */
function locale_refresh_cache() {
355 356
  $languages = language_list('enabled');
  $languages = $languages['1'];
Dries's avatar
 
Dries committed
357

358
  foreach ($languages as $language) {
359
    $result = db_query("SELECT s.source, t.translation, t.language FROM {locales_source} s LEFT JOIN {locales_target} t ON s.lid = t.lid WHERE t.language = '%s' AND s.textgroup = 'default' AND LENGTH(s.source) < 75", $language->language);
360
    $t = array();
Dries's avatar
 
Dries committed
361 362
    while ($data = db_fetch_object($result)) {
      $t[$data->source] = (empty($data->translation) ? TRUE : $data->translation);
Dries's avatar
 
Dries committed
363
    }
364
    cache_set('locale:'. $language->language, $t);
Dries's avatar
 
Dries committed
365 366 367
  }
}

Dries's avatar
 
Dries committed
368
/**
369
 * Returns plural form index for a specific number.
Dries's avatar
 
Dries committed
370
 *
371
 * The index is computed from the formula of this language.
372
 *
373 374 375 376 377
 * @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.
Dries's avatar
 
Dries committed
378
 */
379
function locale_get_plural($count, $langcode = NULL) {
380
  global $language;
Dries's avatar
 
Dries committed
381 382
  static $locale_formula, $plurals = array();

383
  $langcode = $langcode ? $langcode : $language->language;
384

385
  if (!isset($plurals[$langcode][$count])) {
Dries's avatar
 
Dries committed
386
    if (!isset($locale_formula)) {
387 388
      $language_list = language_list();
      $locale_formula[$langcode] = $language_list[$langcode]->formula;
Dries's avatar
 
Dries committed
389
    }
390
    if ($locale_formula[$langcode]) {
Dries's avatar
 
Dries committed
391
      $n = $count;
392 393
      $plurals[$langcode][$count] = @eval('return intval('. $locale_formula[$langcode] .');');
      return $plurals[$langcode][$count];
Dries's avatar
 
Dries committed
394 395
    }
    else {
396
      $plurals[$langcode][$count] = -1;
Dries's avatar
 
Dries committed
397
      return -1;
398
    }
Dries's avatar
 
Dries committed
399
  }
400
  return $plurals[$langcode][$count];
401
}
Dries's avatar
 
Dries committed
402

403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420

/**
 * Returns a language name
 */
function locale_language_name($lang) {
  static $list = NULL;
  if (!isset($list)) {
    $list = locale_language_list();
  }
  return ($lang && isset($list[$lang])) ? $list[$lang] : t('All');
}

/**
 * Returns array of language names
 *
 * @param $field
 *   'name' => names in current language, localized
 *   'native' => native names
421 422
 * @param $all
 *   Boolean to return all languages or only enabled ones
423
 */
424 425 426 427 428 429 430 431
function locale_language_list($field = 'name', $all = FALSE) {
  if ($all) {
    $languages = language_list();
  }
  else {
    $languages = language_list('enabled');
    $languages = $languages[1];
  }
432
  $list = array();
433
  foreach ($languages as $language) {
434 435 436 437
    $list[$language->language] = ($field == 'name') ? t($language->name) : $language->$field;
  }
  return $list;
}
438 439 440 441 442 443 444 445 446 447 448 449 450

/**
 * Imports translations when new modules or themes are installed or enabled.
 *
 * 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.
 */
function locale_system_update($components) {
  include_once 'includes/locale.inc';
451
  if ($batch = locale_batch_by_component($components)) {
452 453 454 455 456 457 458 459 460 461 462 463 464 465
    batch_set($batch);
  }
}

/**
 * Finished callback of system page locale import batch.
 * Inform the user of translation files imported.
 */
function _locale_batch_system_finished($success, $results) {
  if ($success) {
    drupal_set_message(format_plural(count($results), 'One translation file imported for the newly installed modules.', '@count translation files imported for the newly installed modules.'));
  }
}

466 467 468 469 470 471 472 473 474 475
/**
 * Finished callback of language addition locale import batch.
 * Inform the user of translation files imported.
 */
function _locale_batch_language_finished($success, $results) {
  if ($success) {
    drupal_set_message(format_plural(count($results), 'One translation file imported for the enabled modules.', '@count translation files imported for the enabled modules.'));
  }
}

476 477 478 479 480 481 482 483 484 485 486 487 488 489
/**
 * Perform interface translation import as a batch step.
 *
 * @param $filepath
 *   Path to a file to import.
 * @param $results
 *   Contains a list of files imported.
 */
function _locale_batch_import($filepath, &$context) {
  include_once 'includes/locale.inc';
  // The filename is either {langcode}.po or {prefix}.{langcode}.po, so
  // we can extract the language code to use for the import from the end.
  if (preg_match('!(/|\.)([^\.]+)\.po$!', $filepath, $langcode)) {
    $file = (object) array('filename' => basename($filepath), 'filepath' => $filepath);
490
    _locale_import_read_po('db-store', $file, LOCALE_IMPORT_KEEP, $langcode[2]);
491 492 493
    $context['results'][] = $filepath;
  }
}