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

Dries's avatar
 
Dries committed
4 5
/**
 * @file
6
 *   Administration functions for locale.module.
Dries's avatar
 
Dries committed
7 8
 */

9 10
define('LOCALE_JS_STRING', '(?:(?:\'(?:\\\\\'|[^\'])*\'|"(?:\\\\"|[^"])*")(?:\s*\+\s*)?)+');

11 12 13 14 15 16 17 18 19 20 21 22
/**
 * Translation import mode overwriting all existing translations
 * if new translated version available.
 */
define('LOCALE_IMPORT_OVERWRITE', 0);

/**
 * Translation import mode keeping existing translations and only
 * inserting new strings.
 */
define('LOCALE_IMPORT_KEEP', 1);

Dries's avatar
 
Dries committed
23
/**
24 25
 * @defgroup locale-language-overview Language overview functionality
 * @{
Dries's avatar
 
Dries committed
26 27 28
 */

/**
29
 * User interface for the language overview screen.
Dries's avatar
 
Dries committed
30
 */
31
function locale_languages_overview_form() {
32
  $languages = language_list('language', TRUE);
Dries's avatar
 
Dries committed
33

34
  $options = array();
35 36
  $form['weight'] = array('#tree' => TRUE);
  foreach ($languages as $langcode => $language) {
Dries's avatar
 
Dries committed
37

38 39 40
    $options[$langcode] = '';
    if ($language->enabled) {
      $enabled[] = $langcode;
Dries's avatar
 
Dries committed
41
    }
42 43 44 45 46 47
    $form['weight'][$langcode] = array(
      '#type' => 'weight',
      '#default_value' => $language->weight
    );
    $form['name'][$langcode] = array('#value' => check_plain($language->name));
    $form['native'][$langcode] = array('#value' => check_plain($language->native));
48
    $form['direction'][$langcode] = array('#value' => ($language->direction == LANGUAGE_RTL ? 'Right to left' : 'Left to right'));
Dries's avatar
 
Dries committed
49
  }
50 51 52 53 54 55
  $form['enabled'] = array('#type' => 'checkboxes',
    '#options' => $options,
    '#default_value' => $enabled,
  );
  $form['site_default'] = array('#type' => 'radios',
    '#options' => $options,
56
    '#default_value' => language_default('language'),
57
  );
58
  $form['submit'] = array('#type' => 'submit', '#value' => t('Save configuration'));
59
  $form['#theme'] = 'locale_languages_overview_form';
60

61
  return $form;
62
}
Dries's avatar
 
Dries committed
63

64
/**
Dries's avatar
Dries committed
65
 * Theme the language overview form.
66 67
 *
 * @ingroup themeable
68
 */
69
function theme_locale_languages_overview_form($form) {
70
  $default = language_default();
71
  foreach ($form['name'] as $key => $element) {
72
    // Do not take form control structures.
73
    if (is_array($element) && element_child($key)) {
74 75
      // Disable checkbox for the default language, because it cannot be disabled.
      if ($key == $default->language) {
76
        $form['enabled'][$key]['#attributes']['disabled'] = 'disabled';
77 78 79 80 81 82 83 84 85
      }
      $rows[] = array(
        array('data' => drupal_render($form['enabled'][$key]), 'align' => 'center'),
        check_plain($key),
        '<strong>'. drupal_render($form['name'][$key]) .'</strong>',
        drupal_render($form['native'][$key]),
        drupal_render($form['direction'][$key]),
        drupal_render($form['site_default'][$key]),
        drupal_render($form['weight'][$key]),
86
        l(t('edit'), 'admin/settings/language/edit/'. $key) . (($key != 'en' && $key != $default->language) ? ' '. l(t('delete'), 'admin/settings/language/delete/'. $key) : '')
87
      );
88 89
    }
  }
90
  $header = array(array('data' => t('Enabled')), array('data' => t('Code')), array('data' => t('English name')), array('data' => t('Native name')), array('data' => t('Direction')), array('data' => t('Default')), array('data' => t('Weight')), array('data' => t('Operations')));
91
  $output = theme('table', $header, $rows);
92
  $output .= drupal_render($form);
Dries's avatar
Dries committed
93

94
  return $output;
Dries's avatar
 
Dries committed
95 96 97
}

/**
98
 * Process language overview form submissions, updating existing languages.
Dries's avatar
 
Dries committed
99
 */
100
function locale_languages_overview_form_submit($form, &$form_state) {
101
  $languages = language_list();
102
  $default = language_default();
103 104
  $enabled_count = 0;
  foreach ($languages as $langcode => $language) {
105 106 107 108
    if ($form_state['values']['site_default'] == $langcode || $default->language == $langcode) {
      // Automatically enable the default language and the language
      // which was default previously (because we will not get the
      // value from that disabled checkox).
109
      $form_state['values']['enabled'][$langcode] = 1;
110
    }
111
    if ($form_state['values']['enabled'][$langcode]) {
112 113
      $enabled_count++;
      $language->enabled = 1;
114 115
    }
    else {
116
      $language->enabled = 0;
117
    }
118
    $language->weight = $form_state['values']['weight'][$langcode];
119 120
    db_query("UPDATE {languages} SET enabled = %d, weight = %d WHERE language = '%s'", $language->enabled, $language->weight, $langcode);
    $languages[$langcode] = $language;
121 122
  }
  drupal_set_message(t('Configuration saved.'));
123
  variable_set('language_default', $languages[$form_state['values']['site_default']]);
124
  variable_set('language_count', $enabled_count);
Dries's avatar
Dries committed
125

126
  // Changing the language settings impacts the interface.
127
  cache_clear_all('*', 'cache_page', TRUE);
Dries's avatar
 
Dries committed
128

129 130
  $form_state['redirect'] = 'admin/settings/language';
  return;
131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147
}
/**
 * @} End of "locale-language-overview"
 */

/**
 * @defgroup locale-language-add-edit Language addition and editing functionality
 * @{
 */

/**
 * User interface for the language addition screen.
 */
function locale_languages_add_screen() {
  $output = drupal_get_form('locale_languages_predefined_form');
  $output .= drupal_get_form('locale_languages_custom_form');
  return $output;
148 149
}

150 151 152
/**
 * Predefined language setup form.
 */
153
function locale_languages_predefined_form() {
154
  $predefined = _locale_prepare_predefined_list();
155
  $form = array();
156
  $form['language list'] = array('#type' => 'fieldset',
157
    '#title' => t('Predefined language'),
158 159 160 161
    '#collapsible' => TRUE,
  );
  $form['language list']['langcode'] = array('#type' => 'select',
    '#title' => t('Language name'),
162 163
    '#default_value' => key($predefined),
    '#options' => $predefined,
164
    '#description' => t('Select the desired language and click the <em>Add language</em> button. (Use the <em>Custom language</em> options if your desired language does not appear in this list.)'),
165 166
  );
  $form['language list']['submit'] = array('#type' => 'submit', '#value' => t('Add language'));
167 168
  return $form;
}
Dries's avatar
Dries committed
169

170 171 172
/**
 * Custom language addition form.
 */
173
function locale_languages_custom_form() {
174
  $form = array();
175 176 177
  $form['custom language'] = array('#type' => 'fieldset',
    '#title' => t('Custom language'),
    '#collapsible' => TRUE,
178
    '#collapsed' => TRUE,
179
  );
180
  _locale_languages_common_controls($form['custom language']);
181 182 183
  $form['custom language']['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Add custom language')
184
  );
185
  // Reuse the validation and submit functions of the predefined language setup form.
186 187
  $form['#submit'][] = 'locale_languages_predefined_form_submit';
  $form['#validate'][] = 'locale_languages_predefined_form_validate';
188 189 190 191 192 193 194
  return $form;
}

/**
 * Editing screen for a particular language.
 *
 * @param $langcode
195
 *   Language code of the language to edit.
196
 */
197
function locale_languages_edit_form(&$form_state, $langcode) {
198 199
  if ($language = db_fetch_object(db_query("SELECT * FROM {languages} WHERE language = '%s'", $langcode))) {
    $form = array();
200
    _locale_languages_common_controls($form, $language);
201 202 203 204
    $form['submit'] = array(
      '#type' => 'submit',
      '#value' => t('Save language')
    );
205 206
    $form['#submit'][] = 'locale_languages_edit_form_submit';
    $form['#validate'][] = 'locale_languages_edit_form_validate';
207 208 209 210 211 212 213 214
    return $form;
  }
  else {
    drupal_not_found();
  }
}

/**
215
 * Common elements of the language addition and editing form.
216 217 218 219 220 221
 *
 * @param $form
 *   A parent form item (or empty array) to add items below.
 * @param $language
 *   Language object to edit.
 */
222
function _locale_languages_common_controls(&$form, $language = NULL) {
223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244
  if (!is_object($language)) {
    $language = new stdClass();
  }
  if (isset($language->language)) {
    $form['langcode_view'] = array(
      '#type' => 'item',
      '#title' => t('Language code'),
      '#value' => $language->language
    );
    $form['langcode'] = array(
      '#type' => 'value',
      '#value' => $language->language
    );
  }
  else {
    $form['langcode'] = array('#type' => 'textfield',
      '#title' => t('Language code'),
      '#size' => 12,
      '#maxlength' => 60,
      '#required' => TRUE,
      '#default_value' => @$language->language,
      '#disabled' => (isset($language->language)),
245
      '#description' => t('<a href="@rfc4646">RFC 4646</a> compliant language identifier. Language codes typically use a country code, and optionally, a script or regional variant name. <em>Examples: "en", "en-US" and "zh-Hant".</em>', array('@rfc4646' => 'http://www.ietf.org/rfc/rfc4646.txt')),
246 247
    );
  }
248
  $form['name'] = array('#type' => 'textfield',
249 250
    '#title' => t('Language name in English'),
    '#maxlength' => 64,
251
    '#default_value' => @$language->name,
252
    '#required' => TRUE,
253
    '#description' => t('Name of the language in English. Will be available for translation in all languages.'),
254
  );
255
  $form['native'] = array('#type' => 'textfield',
256 257 258 259 260 261 262 263 264 265
    '#title' => t('Native language name'),
    '#maxlength' => 64,
    '#default_value' => @$language->native,
    '#required' => TRUE,
    '#description' => t('Name of the language in the language being added.'),
  );
  $form['prefix'] = array('#type' => 'textfield',
    '#title' => t('Path prefix'),
    '#maxlength' => 64,
    '#default_value' => @$language->prefix,
266
    '#description' => t('Language code or other custom string for pattern matching within the path. With language negotiation set to <em>Path prefix only</em> or <em>Path prefix with language fallback</em>, this site is presented in this language when the Path prefix value matches an element in the path. For the default language, this value may be left blank. <strong>Modifying this value will break existing URLs and should be used with caution in a production environment.</strong> <em>Example: Specifying "deutsch" as the path prefix for German results in URLs in the form "www.example.com/deutsch/node".</em>')
267 268 269 270 271
  );
  $form['domain'] = array('#type' => 'textfield',
    '#title' => t('Language domain'),
    '#maxlength' => 64,
    '#default_value' => @$language->domain,
272
    '#description' => t('Language-specific URL, with protocol. With language negotiation set to <em>Domain name only</em>, the site is presented in this language when the URL accessing the site references this domain. For the default language, this value may be left blank. <strong>This value must include a protocol as part of the string.</strong> <em>Example: Specifying "http://example.de" or "http://de.example.com" as language domains for German results in URLs in the forms "http://example.de/node" and "http://de.example.com/node", respectively.</em>'),
273 274 275 276
  );
  $form['direction'] = array('#type' => 'radios',
    '#title' => t('Direction'),
    '#required' => TRUE,
277
    '#description' => t('Direction that text in this language is presented.'),
278
    '#default_value' => @$language->direction,
279
    '#options' => array(LANGUAGE_LTR => t('Left to right'), LANGUAGE_RTL => t('Right to left'))
280
  );
281 282
  return $form;
}
Dries's avatar
 
Dries committed
283 284

/**
285 286
 * Validate the language addition form.
 */
287 288
function locale_languages_predefined_form_validate($form, &$form_state) {
  $langcode = $form_state['values']['langcode'];
289

290
  if ($duplicate = db_result(db_query("SELECT COUNT(*) FROM {languages} WHERE language = '%s'", $langcode)) != 0) {
291
    form_set_error('langcode', t('The language %language (%code) already exists.', array('%language' => $form_state['values']['name'], '%code' => $langcode)));
292 293
  }

294
  if (!isset($form_state['values']['name'])) {
295 296
    // Predefined language selection.
    $predefined = _locale_get_predefined_list();
297
    if (!isset($predefined[$langcode])) {
298 299 300
      form_set_error('langcode', t('Invalid language code.'));
    }
  }
301
  else {
302
    // Reuse the editing form validation routine if we add a custom language.
303
    locale_languages_edit_form_validate($form, $form_state);
304
  }
305 306 307 308 309
}

/**
 * Process the language addition form submission.
 */
310 311 312
function locale_languages_predefined_form_submit($form, &$form_state) {
  $langcode = $form_state['values']['langcode'];
  if (isset($form_state['values']['name'])) {
313
    // Custom language form.
314 315
    locale_add_language($langcode, $form_state['values']['name'], $form_state['values']['native'], $form_state['values']['direction'], $form_state['values']['domain'], $form_state['values']['prefix']);
    drupal_set_message(t('The language %language has been created and can now be used. More information is available on the <a href="@locale-help">help screen</a>.', array('%language' => t($form_state['values']['name']), '@locale-help' => url('admin/help/locale'))));
316 317
  }
  else {
318 319
    // Predefined language selection.
    $predefined = _locale_get_predefined_list();
320 321
    locale_add_language($langcode);
    drupal_set_message(t('The language %language has been created and can now be used. More information is available on the <a href="@locale-help">help screen</a>.', array('%language' => t($predefined[$langcode][0]), '@locale-help' => url('admin/help/locale'))));
322 323
  }

324 325 326 327 328 329
  // See if we have language files to import for the newly added
  // language, collect and import them.
  if ($batch = locale_batch_by_language($langcode, '_locale_batch_language_finished')) {
    batch_set($batch);
  }

330 331
  $form_state['redirect'] = 'admin/settings/language';
  return;
332 333 334 335 336
}

/**
 * Validate the language editing form. Reused for custom language addition too.
 */
337 338
function locale_languages_edit_form_validate($form, &$form_state) {
  if (!empty($form_state['values']['domain']) && !empty($form_state['values']['prefix'])) {
339 340
    form_set_error('prefix', t('Domain and path prefix values should not be set at the same time.'));
  }
341 342
  if (!empty($form_state['values']['domain']) && $duplicate = db_fetch_object(db_query("SELECT language FROM {languages} WHERE domain = '%s' AND language != '%s'", $form_state['values']['domain'], $form_state['values']['langcode']))) {
    form_set_error('domain', t('The domain (%domain) is already tied to a language (%language).', array('%domain' => $form_state['values']['domain'], '%language' => $duplicate->language)));
343
  }
344
  if (empty($form_state['values']['prefix']) && language_default('language') != $form_state['values']['langcode'] && empty($form_state['values']['domain'])) {
345 346
    form_set_error('prefix', t('Only the default language can have both the domain and prefix empty.'));
  }
347 348
  if (!empty($form_state['values']['prefix']) && $duplicate = db_fetch_object(db_query("SELECT language FROM {languages} WHERE prefix = '%s' AND language != '%s'", $form_state['values']['prefix'], $form_state['values']['langcode']))) {
    form_set_error('prefix', t('The prefix (%prefix) is already tied to a language (%language).', array('%prefix' => $form_state['values']['prefix'], '%language' => $duplicate->language)));
349 350 351 352 353 354
  }
}

/**
 * Process the language editing form submission.
 */
355 356
function locale_languages_edit_form_submit($form, &$form_state) {
  db_query("UPDATE {languages} SET name = '%s', native = '%s', domain = '%s', prefix = '%s', direction = %d WHERE language = '%s'", $form_state['values']['name'], $form_state['values']['native'], $form_state['values']['domain'], $form_state['values']['prefix'], $form_state['values']['direction'], $form_state['values']['langcode']);
357
  $default = language_default();
358
  if ($default->language == $form_state['values']['langcode']) {
359
    $properties = array('name', 'native', 'direction', 'enabled', 'plurals', 'formula', 'domain', 'prefix', 'weight');
360
    foreach ($properties as $keyname) {
361 362 363
      if (isset($form_state['values'][$keyname])) {
        $default->$keyname = $form_state['values'][$keyname];
      }
364
    }
365
    variable_set('language_default', $default);
366
  }
367 368
  $form_state['redirect'] = 'admin/settings/language';
  return;
369
}
370 371 372 373 374 375 376 377 378 379 380 381
/**
 * @} End of "locale-language-add-edit"
 */

/**
 * @defgroup locale-language-delete Language deletion functionality
 * @{
 */

/**
 * User interface for the language deletion confirmation screen.
 */
382
function locale_languages_delete_form(&$form_state, $langcode) {
383 384 385 386 387 388 389

  // Do not allow deletion of English locale.
  if ($langcode == 'en') {
    drupal_set_message(t('The English language cannot be deleted.'));
    drupal_goto('admin/settings/language');
  }

390
  if (language_default('language') == $langcode) {
391 392 393 394 395 396 397 398 399 400 401 402
    drupal_set_message(t('The default language cannot be deleted.'));
    drupal_goto('admin/settings/language');
  }

  // For other languages, warn user that data loss is ahead.
  $languages = language_list();

  if (!isset($languages[$langcode])) {
    drupal_not_found();
  }
  else {
    $form['langcode'] = array('#type' => 'value', '#value' => $langcode);
403
    return confirm_form($form, t('Are you sure you want to delete the language %name?', array('%name' => t($languages[$langcode]->name))), 'admin/settings/language', t('Deleting a language will remove all interface translations associated with it, and posts in this language will be set to be language neutral. This action cannot be undone.'), t('Delete'), t('Cancel'));
404 405 406 407 408 409
  }
}

/**
 * Process language deletion submissions.
 */
410
function locale_languages_delete_form_submit($form, &$form_state) {
411
  $languages = language_list();
412
  if (isset($languages[$form_state['values']['langcode']])) {
413
    // Remove translations first.
414
    db_query("DELETE FROM {locales_target} WHERE language = '%s'", $form_state['values']['langcode']);
415 416 417 418 419
    cache_clear_all('locale:'. $form_state['values']['langcode'], 'cache');
    // With no translations, this removes existing JavaScript translations file.
    _locale_rebuild_js($form_state['values']['langcode']);
    // Remove the language.
    db_query("DELETE FROM {languages} WHERE language = '%s'", $form_state['values']['langcode']);
420 421
    db_query("UPDATE {node} SET language = '' WHERE language = '%s'", $form_state['values']['langcode']);
    $variables = array('%locale' => $languages[$form_state['values']['langcode']]->name);
422 423 424 425 426 427 428
    drupal_set_message(t('The language %locale has been removed.', $variables));
    watchdog('locale', 'The language %locale has been removed.', $variables);
  }

  // Changing the language settings impacts the interface:
  cache_clear_all('*', 'cache_page', TRUE);

429 430
  $form_state['redirect'] = 'admin/settings/language';
  return;
431 432 433 434 435 436 437 438 439
}
/**
 * @} End of "locale-language-add-edit"
 */

/**
 * @defgroup locale-languages-negotiation Language negotiation options screen
 * @{
 */
440 441 442 443

/**
 * Setting for language negotiation options
 */
444
function locale_languages_configure_form() {
445 446 447 448
  $form['language_negotiation'] = array(
    '#title' => t('Language negotiation'),
    '#type' => 'radios',
    '#options' => array(
449 450 451 452
      LANGUAGE_NEGOTIATION_NONE => t('None.'),
      LANGUAGE_NEGOTIATION_PATH_DEFAULT => t('Path prefix only.'),
      LANGUAGE_NEGOTIATION_PATH => t('Path prefix with language fallback.'),
      LANGUAGE_NEGOTIATION_DOMAIN => t('Domain name only.')),
453
    '#default_value' => variable_get('language_negotiation', LANGUAGE_NEGOTIATION_NONE),
454
    '#description' => t("Select the mechanism used to determine your site's presentation language. <strong>Modifying this setting may break all incoming URLs and should be used with caution in a production environment.</strong>")
455 456 457 458 459 460 461
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Save settings')
  );
  return $form;
}
Dries's avatar
Dries committed
462

463 464 465
/**
 * Submit function for language negotiation settings.
 */
466 467
function locale_languages_configure_form_submit($form, &$form_state) {
  variable_set('language_negotiation', $form_state['values']['language_negotiation']);
468
  drupal_set_message(t('Language negotiation configuration saved.'));
469 470
  $form_state['redirect'] = 'admin/settings/language';
  return;
471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497
}
/**
 * @} End of "locale-languages-negotiation"
 */

/**
 * @defgroup locale-translate-overview Translation overview screen.
 * @{
 */

/**
 * Overview screen for translations.
 */
function locale_translate_overview_screen() {
  $languages = language_list('language', TRUE);
  $groups = module_invoke_all('locale', 'groups');

  // Build headers with all groups in order.
  $headers = array_merge(array(t('Language')), array_values($groups));

  // Collect summaries of all source strings in all groups.
  $sums = db_query("SELECT COUNT(*) AS strings, textgroup FROM {locales_source} GROUP BY textgroup");
  $groupsums = array();
  while ($group = db_fetch_object($sums)) {
    $groupsums[$group->textgroup] = $group->strings;
  }

498
  // Set up overview table with default values, ensuring common order for values.
499 500 501 502
  $rows = array();
  foreach ($languages as $langcode => $language) {
    $rows[$langcode] = array('name' => ($langcode == 'en' ? t('English (built-in)') : t($language->name)));
    foreach ($groups as $group => $name) {
503
      $rows[$langcode][$group] = ($langcode == 'en' ? t('n/a') : '0/'. (isset($groupsums[$group]) ? $groupsums[$group] : 0) .' (0%)');
504 505 506 507
    }
  }

  // Languages with at least one record in the locale table.
508
  $translations = db_query("SELECT COUNT(*) AS translation, t.language, s.textgroup FROM {locales_source} s INNER JOIN {locales_target} t ON s.lid = t.lid GROUP BY textgroup, language");
509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560
  while ($data = db_fetch_object($translations)) {
    $ratio = (!empty($groupsums[$data->textgroup]) && $data->translation > 0) ? round(($data->translation/$groupsums[$data->textgroup])*100., 2) : 0;
    $rows[$data->language][$data->textgroup] = $data->translation .'/'. $groupsums[$data->textgroup] ." ($ratio%)";
  }

  return theme('table', $headers, $rows);
}
/**
 * @} End of "locale-translate-overview"
 */

/**
 * @defgroup locale-translate-seek Translation search screen.
 * @{
 */

/**
 * String search screen.
 */
function locale_translate_seek_screen() {
  $output = _locale_translate_seek();
  $output .= drupal_get_form('locale_translate_seek_form');
  return $output;
}

/**
 * User interface for the string search screen.
 */
function locale_translate_seek_form() {
  // Get all languages, except English
  $languages = locale_language_list('name', TRUE);
  unset($languages['en']);

  // Present edit form preserving previous user settings
  $query = _locale_translate_seek_query();
  $form = array();
  $form['search'] = array('#type' => 'fieldset',
    '#title' => t('Search'),
  );
  $form['search']['string'] = array('#type' => 'textfield',
    '#title' => t('String contains'),
    '#default_value' => @$query['string'],
    '#description' => t('Leave blank to show all strings. The search is case sensitive.'),
  );
  $form['search']['language'] = array('#type' => 'radios',
    '#title' => t('Language'),
    '#default_value' => (!empty($query['language']) ? $query['language'] : 'all'),
    '#options' => array_merge(array('all' => t('All languages'), 'en' => t('English (provided by Drupal)')), $languages),
  );
  $form['search']['translation'] = array('#type' => 'radios',
    '#title' => t('Search in'),
    '#default_value' => (!empty($query['translation']) ? $query['translation'] : 'all'),
561
    '#options' => array('all' => t('Both translated and untranslated strings'), 'translated' => t('Only translated strings'), 'untranslated' => t('Only untranslated strings')),
562 563 564 565 566 567 568 569 570 571 572 573
  );
  $groups = module_invoke_all('locale', 'groups');
  $form['search']['group'] = array('#type' => 'radios',
    '#title' => t('Limit search to'),
    '#default_value' => (!empty($query['group']) ? $query['group'] : 'all'),
    '#options' => array_merge(array('all' => t('All text groups')), $groups),
  );

  $form['search']['submit'] = array('#type' => 'submit', '#value' => t('Search'));
  $form['#redirect'] = FALSE;

  return $form;
574
}
575 576 577
/**
 * @} End of "locale-translate-seek"
 */
578

579 580 581 582
/**
 * @defgroup locale-translate-import Translation import screen.
 * @{
 */
583

584 585
/**
 * User interface for the translation import screen.
Dries's avatar
 
Dries committed
586
 */
587 588
function locale_translate_import_form() {
  // Get all languages, except English
589 590
  $names = locale_language_list('name', TRUE);
  unset($names['en']);
Dries's avatar
 
Dries committed
591

592
  if (!count($names)) {
593
    $languages = _locale_prepare_predefined_list();
594
    $default = array_shift(array_keys($languages));
Dries's avatar
 
Dries committed
595 596 597
  }
  else {
    $languages = array(
598
      t('Already added languages') => $names,
599
      t('Languages not yet added') => _locale_prepare_predefined_list()
Dries's avatar
 
Dries committed
600
    );
601
    $default = array_shift(array_keys($names));
Dries's avatar
 
Dries committed
602
  }
Steven Wittens's avatar
Locale:  
Steven Wittens committed
603

604
  $form = array();
605
  $form['import'] = array('#type' => 'fieldset',
drumm's avatar
drumm committed
606
    '#title' => t('Import translation'),
607 608 609 610
  );
  $form['import']['file'] = array('#type' => 'file',
    '#title' => t('Language file'),
    '#size' => 50,
611
    '#description' => t('A Gettext Portable Object (<em>.po</em>) file.'),
612 613 614
  );
  $form['import']['langcode'] = array('#type' => 'select',
    '#title' => t('Import into'),
615 616
    '#options' => $languages,
    '#default_value' => $default,
617 618 619 620 621 622 623
    '#description' => t('Choose the language you want to add strings into. If you choose a language which is not yet set up, it will be added.'),
  );
  $form['import']['group'] = array('#type' => 'radios',
    '#title' => t('Text group'),
    '#default_value' => 'default',
    '#options' => module_invoke_all('locale', 'groups'),
    '#description' => t('Imported translations will be added to this text group.'),
624 625 626 627
  );
  $form['import']['mode'] = array('#type' => 'radios',
    '#title' => t('Mode'),
    '#default_value' => 'overwrite',
628
    '#options' => array(LOCALE_IMPORT_OVERWRITE => t('Strings in the uploaded file replace existing ones, new ones are added'), LOCALE_IMPORT_KEEP => t('Existing strings are kept, only new strings are added')),
629 630
  );
  $form['import']['submit'] = array('#type' => 'submit', '#value' => t('Import'));
631
  $form['#attributes']['enctype'] = 'multipart/form-data';
632

633
  return $form;
Dries's avatar
 
Dries committed
634 635
}

636 637 638
/**
 * Process the locale import form submission.
 */
639
function locale_translate_import_form_submit($form, &$form_state) {
640
  // Ensure we have the file uploaded
641
  if ($file = file_save_upload('file')) {
642 643 644

    // Add language, if not yet supported
    $languages = language_list('language', TRUE);
645
    $langcode = $form_state['values']['langcode'];
646
    if (!isset($languages[$langcode])) {
647
      $predefined = _locale_get_predefined_list();
648 649
      locale_add_language($langcode);
      drupal_set_message(t('The language %language has been created.', array('%language' => t($predefined[$langcode][0]))));
650
    }
651

652
    // Now import strings into the language
653
    if ($ret = _locale_import_po($file, $langcode, $form_state['values']['mode'], $form_state['values']['group']) == FALSE) {
654 655 656
      $variables = array('%filename' => $file->filename);
      drupal_set_message(t('The translation import of %filename failed.', $variables), 'error');
      watchdog('locale', 'The translation import of %filename failed.', $variables, WATCHDOG_ERROR);
657 658 659 660
    }
  }
  else {
    drupal_set_message(t('File to import not found.'), 'error');
661
    return 'admin/build/translate/import';
662 663
  }

664 665
  $form_state['redirect'] = 'admin/build/translate';
  return;
666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689
}
/**
 * @} End of "locale-translate-import"
 */

/**
 * @defgroup locale-translate-export Translation export screen.
 * @{
 */

/**
 * User interface for the translation export screen.
 */
function locale_translate_export_screen() {
  // Get all languages, except English
  $names = locale_language_list('name', TRUE);
  unset($names['en']);
  $output = '';
  // Offer translation export if any language is set up.
  if (count($names)) {
    $output = drupal_get_form('locale_translate_export_po_form', $names);
  }
  $output .= drupal_get_form('locale_translate_export_pot_form');
  return $output;
690 691
}

692 693 694 695 696 697
/**
 * Form to export PO files for the languages provided.
 *
 * @param $names
 *   An associate array with localized language names
 */
698
function locale_translate_export_po_form(&$form_state, $names) {
699 700 701 702 703 704
  $form['export'] = array('#type' => 'fieldset',
    '#title' => t('Export translation'),
    '#collapsible' => TRUE,
  );
  $form['export']['langcode'] = array('#type' => 'select',
    '#title' => t('Language name'),
705
    '#options' => $names,
706
    '#description' => t('Select the language to export in Gettext Portable Object (<em>.po</em>) format.'),
707
  );
708 709 710 711 712
  $form['export']['group'] = array('#type' => 'radios',
    '#title' => t('Text group'),
    '#default_value' => 'default',
    '#options' => module_invoke_all('locale', 'groups'),
  );
713 714 715 716
  $form['export']['submit'] = array('#type' => 'submit', '#value' => t('Export'));
  return $form;
}

717 718 719 720
/**
 * Translation template export form.
 */
function locale_translate_export_pot_form() {
721 722 723 724
  // Complete template export of the strings
  $form['export'] = array('#type' => 'fieldset',
    '#title' => t('Export template'),
    '#collapsible' => TRUE,
725
    '#description' => t('Generate a Gettext Portable Object Template (<em>.pot</em>) file with all strings from the Drupal locale database.'),
726 727 728 729 730
  );
  $form['export']['group'] = array('#type' => 'radios',
    '#title' => t('Text group'),
    '#default_value' => 'default',
    '#options' => module_invoke_all('locale', 'groups'),
731 732
  );
  $form['export']['submit'] = array('#type' => 'submit', '#value' => t('Export'));
733
  // Reuse PO export submission callback.
734 735
  $form['#submit'][] = 'locale_translate_export_po_form_submit';
  $form['#validate'][] = 'locale_translate_export_po_form_validate';
736 737 738
  return $form;
}

739
/**
740
 * Process a translation (or template) export form submission.
741
 */
742
function locale_translate_export_po_form_submit($form, &$form_state) {
743
  // If template is required, language code is not given.
744 745 746 747 748 749
  $language = NULL;
  if (isset($form_state['values']['langcode'])) {
    $languages = language_list();
    $language = $languages[$form_state['values']['langcode']];
  }
  _locale_export_po($language, _locale_export_po_generate($language, _locale_export_get_strings($language, $form_state['values']['group'])));
750 751
}
/**
752
 * @} End of "locale-translate-export"
753
 */
754

755
/**
756 757
 * @defgroup locale-translate-edit Translation text editing
 * @{
758 759 760 761 762
 */

/**
 * User interface for string editing.
 */
763
function locale_translate_edit_form(&$form_state, $lid) {
764 765 766
  // Fetch source string, if possible.
  $source = db_fetch_object(db_query('SELECT source, textgroup, location FROM {locales_source} WHERE lid = %d', $lid));
  if (!$source) {
767 768
    drupal_set_message(t('String not found.'), 'error');
    drupal_goto('admin/build/translate/search');
769 770
  }

771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789
  // Add original text to the top and some values for form altering.
  $form = array(
    'original' => array(
      '#type'  => 'item',
      '#title' => t('Original text'),
      '#value' => check_plain(wordwrap($source->source, 0)),
    ),
    'lid' => array(
      '#type'  => 'value',
      '#value' => $lid
    ),
    'textgroup' => array(
      '#type'  => 'value',
      '#value' => $source->textgroup,
    ),
    'location' => array(
      '#type'  => 'value',
      '#value' => $source->location
    ),
790 791
  );

792 793 794 795 796 797 798 799 800 801
  // Include default form controls with empty values for all languages.
  // This ensures that the languages are always in the same order in forms.
  $languages = language_list();
  $default = language_default();
  // We don't need the default language value, that value is in $source.
  $omit = $source->textgroup == 'default' ? 'en' : $default->language;
  unset($languages[($omit)]);
  $form['translations'] = array('#tree' => TRUE);
  // Approximate the number of rows to use in the default textarea.
  $rows = min(ceil(str_word_count($source->source) / 12), 10);
802 803
  foreach ($languages as $langcode => $language) {
    $form['translations'][$langcode] = array(
804
      '#type' => 'textarea',
805
      '#title' => t($language->name),
806
      '#rows' => $rows,
807
      '#default_value' => '',
808 809
    );
  }
810

811 812 813 814 815
  // Fetch translations and fill in default values in the form.
  $result = db_query("SELECT DISTINCT translation, language FROM {locales_target} WHERE lid = %d AND language != '%s'", $lid, $omit);
  while ($translation = db_fetch_object($result)) {
    $form['translations'][$translation->language]['#default_value'] = $translation->translation;
  }
816 817

  $form['submit'] = array('#type' => 'submit', '#value' => t('Save translations'));
818
  return $form;
819 820 821 822 823 824
}

/**
 * Process string editing form submissions.
 * Saves all translations of one string submitted from a form.
 */
825 826 827
function locale_translate_edit_form_submit($form, &$form_state) {
  $lid = $form_state['values']['lid'];
  foreach ($form_state['values']['translations'] as $key => $value) {
828 829 830 831 832 833 834 835 836
    $translation = db_result(db_query("SELECT translation FROM {locales_target} WHERE lid = %d AND language = '%s'", $lid, $key));
    if (!empty($value)) {
      // Only update or insert if we have a value to use.
      if (!empty($translation)) {
        db_query("UPDATE {locales_target} SET translation = '%s' WHERE lid = %d AND language = '%s'", $value, $lid, $key);
      }
      else {
        db_query("INSERT INTO {locales_target} (lid, translation, language) VALUES (%d, '%s', '%s')", $lid, $value, $key);
      }
837
    }
838 839 840
    elseif (!empty($translation)) {
      // Empty translation entered: remove existing entry from database.
      db_query("DELETE FROM {locales_target} WHERE lid = %d AND language = '%s'", $lid, $key);
841
    }
842

843 844
    // Force JavaScript translation file recreation for this language.
    _locale_invalidate_js($key);
845
  }
846

847 848
  drupal_set_message(t('The string has been saved.'));

849 850
  // Clear locale cache.
  cache_clear_all('locale:', 'cache', TRUE);
851

852 853
  $form_state['redirect'] = 'admin/build/translate/search';
  return;
854
}
855 856 857 858 859 860 861 862
/**
 * @} End of "locale-translate-edit"
 */

/**
 * @defgroup locale-translate-delete Translation delete interface.
 * @{
 */
863 864 865 866

/**
 * Delete a language string.
 */
867
function locale_translate_delete($lid) {
868 869
  db_query('DELETE FROM {locales_source} WHERE lid = %d', $lid);
  db_query('DELETE FROM {locales_target} WHERE lid = %d', $lid);
870 871
  // Force JavaScript translation file recreation for all languages.
  _locale_invalidate_js();
872
  cache_clear_all('locale:', 'cache', TRUE);
873
  drupal_set_message(t('The string has been removed.'));
874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901
  drupal_goto('admin/build/translate/search');
}
/**
 * @} End of "locale-translate-delete"
 */

/**
 * @defgroup locale-api-add Language addition API.
 * @{
 */

/**
 * API function to add a language.
 *
 * @param $langcode
 *   Language code.
 * @param $name
 *   English name of the language
 * @param $native
 *   Native name of the language
 * @param $direction
 *   LANGUAGE_LTR or LANGUAGE_RTL
 * @param $domain
 *   Optional custom domain name with protocol, without
 *   trailing slash (eg. http://de.example.com).
 * @param $prefix
 *   Optional path prefix for the language. Defaults to the
 *   language code if omitted.
902 903 904
 * @param $enabled
 *   Optionally TRUE to enable the language when created or FALSE to disable.
 * @param $default
Dries's avatar
Dries committed
905
 *   Optionally set this language to be the default.
906
 */
907
function locale_add_language($langcode, $name = NULL, $native = NULL, $direction = LANGUAGE_LTR, $domain = '', $prefix = '', $enabled = TRUE, $default = FALSE) {
908 909
  // Default prefix on language code.
  if (empty($prefix)) {
910
    $prefix = $langcode;
911 912
  }

913 914 915 916 917
  // If name was not set, we add a predefined language.
  if (!isset($name)) {
    $predefined = _locale_get_predefined_list();
    $name = $predefined[$langcode][0];
    $native = isset($predefined[$langcode][1]) ? $predefined[$langcode][1] : $predefined[$langcode][0];
918
    $direction = isset($predefined[$langcode][2]) ? $predefined[$langcode][2] : LANGUAGE_LTR;
919 920 921
  }

  db_query("INSERT INTO {languages} (language, name, native, direction, domain, prefix, enabled) VALUES ('%s', '%s', '%s', %d, '%s', '%s', %d)", $langcode, $name, $native, $direction, $domain, $prefix, $enabled);
922

923 924
  // Only set it as default if enabled.
  if ($enabled && $default) {
925
    variable_set('language_default', (object) array('language' => $langcode, 'name' => $name, 'native' => $native, 'direction' => $direction, 'enabled' => (int) $enabled, 'plurals' => 0, 'formula' => '', 'domain' => '', 'prefix' => $prefix, 'weight' => 0, 'javascript' => ''));
926 927
  }

928 929 930 931
  if ($enabled) {
    // Increment enabled language count if we are adding an enabled language.
    variable_set('language_count', variable_get('language_count', 1) + 1);
  }
932

933 934 935
  // Force JavaScript translation file creation for the newly added language.
  _locale_invalidate_js($langcode);

936
  watchdog('locale', 'The %language language (%code) has been created.', array('%language' => $name, '%code' => $langcode));
937
}
938 939 940
/**
 * @} End of "locale-api-add"
 */
941

942 943 944 945
/**
 * @defgroup locale-api-import Translation import API.
 * @{
 */
946

Dries's avatar
 
Dries committed
947 948 949
/**
 * Parses Gettext Portable Object file information and inserts into database
 *
drumm's avatar
drumm committed
950 951
 * @param $file
 *   Drupal file object corresponding to the PO file to import
952
 * @param $langcode
drumm's avatar
drumm committed
953 954
 *   Language code
 * @param $mode
955
 *   Should existing translations be replaced LOCALE_IMPORT_KEEP or LOCALE_IMPORT_OVERWRITE
956 957
 * @param $group
 *   Text group to import PO file into (eg. 'default' for interface translations)
Dries's avatar
 
Dries committed
958
 */
959 960
function _locale_import_po($file, $langcode, $mode, $group = NULL) {
  // If not in 'safe mode', increase the maximum execution time.
Steven Wittens's avatar
Locale:  
Steven Wittens committed
961 962
  if (!ini_get('safe_mode')) {
    set_time_limit(240);
Dries's avatar
 
Dries committed
963
  }