locale.inc 91.4 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 11
/**
 * Regular expression pattern used to localize JavaScript strings.
 */
12 13
define('LOCALE_JS_STRING', '(?:(?:\'(?:\\\\\'|[^\'])*\'|"(?:\\\\"|[^"])*")(?:\s*\+\s*)?)+');

14 15 16 17 18 19 20 21 22 23 24 25
/**
 * 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
26
/**
27 28
 * @defgroup locale-language-overview Language overview functionality
 * @{
Dries's avatar
 
Dries committed
29 30 31
 */

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

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

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

64
  return $form;
65
}
Dries's avatar
 
Dries committed
66

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

97
  return $output;
Dries's avatar
 
Dries committed
98 99 100
}

/**
101
 * Process language overview form submissions, updating existing languages.
Dries's avatar
 
Dries committed
102
 */
103
function locale_languages_overview_form_submit($form, &$form_state) {
104
  $languages = language_list();
105
  $default = language_default();
106 107
  $enabled_count = 0;
  foreach ($languages as $langcode => $language) {
108 109 110 111
    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).
112
      $form_state['values']['enabled'][$langcode] = 1;
113
    }
114
    if ($form_state['values']['enabled'][$langcode]) {
115 116
      $enabled_count++;
      $language->enabled = 1;
117 118
    }
    else {
119
      $language->enabled = 0;
120
    }
121
    $language->weight = $form_state['values']['weight'][$langcode];
122 123
    db_query("UPDATE {languages} SET enabled = %d, weight = %d WHERE language = '%s'", $language->enabled, $language->weight, $langcode);
    $languages[$langcode] = $language;
124 125
  }
  drupal_set_message(t('Configuration saved.'));
126
  variable_set('language_default', $languages[$form_state['values']['site_default']]);
127
  variable_set('language_count', $enabled_count);
Dries's avatar
Dries committed
128

129
  // Changing the language settings impacts the interface.
130
  cache_clear_all('*', 'cache_page', TRUE);
Dries's avatar
 
Dries committed
131

132 133
  $form_state['redirect'] = 'admin/settings/language';
  return;
134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150
}
/**
 * @} 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;
151 152
}

153 154 155
/**
 * Predefined language setup form.
 */
156
function locale_languages_predefined_form() {
157
  $predefined = _locale_prepare_predefined_list();
158
  $form = array();
159
  $form['language list'] = array('#type' => 'fieldset',
160
    '#title' => t('Predefined language'),
161 162 163 164
    '#collapsible' => TRUE,
  );
  $form['language list']['langcode'] = array('#type' => 'select',
    '#title' => t('Language name'),
165 166
    '#default_value' => key($predefined),
    '#options' => $predefined,
167
    '#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.)'),
168 169
  );
  $form['language list']['submit'] = array('#type' => 'submit', '#value' => t('Add language'));
170 171
  return $form;
}
Dries's avatar
Dries committed
172

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

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

/**
218
 * Common elements of the language addition and editing form.
219 220 221 222 223 224
 *
 * @param $form
 *   A parent form item (or empty array) to add items below.
 * @param $language
 *   Language object to edit.
 */
225
function _locale_languages_common_controls(&$form, $language = NULL) {
226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247
  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)),
248
      '#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')),
249 250
    );
  }
251
  $form['name'] = array('#type' => 'textfield',
252 253
    '#title' => t('Language name in English'),
    '#maxlength' => 64,
254
    '#default_value' => @$language->name,
255
    '#required' => TRUE,
256
    '#description' => t('Name of the language in English. Will be available for translation in all languages.'),
257
  );
258
  $form['native'] = array('#type' => 'textfield',
259 260 261 262 263 264 265 266 267 268
    '#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,
269
    '#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>')
270 271 272 273 274
  );
  $form['domain'] = array('#type' => 'textfield',
    '#title' => t('Language domain'),
    '#maxlength' => 64,
    '#default_value' => @$language->domain,
275
    '#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>'),
276 277 278 279
  );
  $form['direction'] = array('#type' => 'radios',
    '#title' => t('Direction'),
    '#required' => TRUE,
280
    '#description' => t('Direction that text in this language is presented.'),
281
    '#default_value' => @$language->direction,
282
    '#options' => array(LANGUAGE_LTR => t('Left to right'), LANGUAGE_RTL => t('Right to left'))
283
  );
284 285
  return $form;
}
Dries's avatar
 
Dries committed
286 287

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

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

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

/**
 * Process the language addition form submission.
 */
313 314 315
function locale_languages_predefined_form_submit($form, &$form_state) {
  $langcode = $form_state['values']['langcode'];
  if (isset($form_state['values']['name'])) {
316
    // Custom language form.
317 318
    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'))));
319 320
  }
  else {
321 322
    // Predefined language selection.
    $predefined = _locale_get_predefined_list();
323 324
    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'))));
325 326
  }

327 328 329 330 331 332
  // 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);
  }

333 334
  $form_state['redirect'] = 'admin/settings/language';
  return;
335 336 337 338 339
}

/**
 * Validate the language editing form. Reused for custom language addition too.
 */
340 341
function locale_languages_edit_form_validate($form, &$form_state) {
  if (!empty($form_state['values']['domain']) && !empty($form_state['values']['prefix'])) {
342 343
    form_set_error('prefix', t('Domain and path prefix values should not be set at the same time.'));
  }
344 345
  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)));
346
  }
347
  if (empty($form_state['values']['prefix']) && language_default('language') != $form_state['values']['langcode'] && empty($form_state['values']['domain'])) {
348 349
    form_set_error('prefix', t('Only the default language can have both the domain and prefix empty.'));
  }
350 351
  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)));
352 353 354 355 356 357
  }
}

/**
 * Process the language editing form submission.
 */
358 359
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']);
360
  $default = language_default();
361
  if ($default->language == $form_state['values']['langcode']) {
362
    $properties = array('name', 'native', 'direction', 'enabled', 'plurals', 'formula', 'domain', 'prefix', 'weight');
363
    foreach ($properties as $keyname) {
364 365 366
      if (isset($form_state['values'][$keyname])) {
        $default->$keyname = $form_state['values'][$keyname];
      }
367
    }
368
    variable_set('language_default', $default);
369
  }
370 371
  $form_state['redirect'] = 'admin/settings/language';
  return;
372
}
373 374 375 376 377 378 379 380 381 382 383 384
/**
 * @} End of "locale-language-add-edit"
 */

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

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

  // 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');
  }

393
  if (language_default('language') == $langcode) {
394 395 396 397 398 399 400 401 402 403 404 405
    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);
406
    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'));
407 408 409 410 411 412
  }
}

/**
 * Process language deletion submissions.
 */
413
function locale_languages_delete_form_submit($form, &$form_state) {
414
  $languages = language_list();
415
  if (isset($languages[$form_state['values']['langcode']])) {
416
    // Remove translations first.
417
    db_query("DELETE FROM {locales_target} WHERE language = '%s'", $form_state['values']['langcode']);
418
    cache_clear_all('locale:' . $form_state['values']['langcode'], 'cache');
419 420 421 422
    // 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']);
423 424
    db_query("UPDATE {node} SET language = '' WHERE language = '%s'", $form_state['values']['langcode']);
    $variables = array('%locale' => $languages[$form_state['values']['langcode']]->name);
425 426 427 428 429 430 431
    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);

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

/**
 * @defgroup locale-languages-negotiation Language negotiation options screen
 * @{
 */
443 444 445 446

/**
 * Setting for language negotiation options
 */
447
function locale_languages_configure_form() {
448 449 450 451
  $form['language_negotiation'] = array(
    '#title' => t('Language negotiation'),
    '#type' => 'radios',
    '#options' => array(
452 453 454 455
      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.')),
456
    '#default_value' => variable_get('language_negotiation', LANGUAGE_NEGOTIATION_NONE),
457
    '#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>")
458 459 460 461 462 463 464
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Save settings')
  );
  return $form;
}
Dries's avatar
Dries committed
465

466 467 468
/**
 * Submit function for language negotiation settings.
 */
469 470
function locale_languages_configure_form_submit($form, &$form_state) {
  variable_set('language_negotiation', $form_state['values']['language_negotiation']);
471
  drupal_set_message(t('Language negotiation configuration saved.'));
472 473
  $form_state['redirect'] = 'admin/settings/language';
  return;
474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500
}
/**
 * @} 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;
  }

501
  // Set up overview table with default values, ensuring common order for values.
502 503 504 505
  $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) {
506
      $rows[$langcode][$group] = ($langcode == 'en' ? t('n/a') : '0/' . (isset($groupsums[$group]) ? $groupsums[$group] : 0) . ' (0%)');
507 508 509 510
    }
  }

  // Languages with at least one record in the locale table.
511
  $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");
512 513
  while ($data = db_fetch_object($translations)) {
    $ratio = (!empty($groupsums[$data->textgroup]) && $data->translation > 0) ? round(($data->translation/$groupsums[$data->textgroup])*100., 2) : 0;
514
    $rows[$data->language][$data->textgroup] = $data->translation . '/' . $groupsums[$data->textgroup] . " ($ratio%)";
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
  }

  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.'),
  );
556
  $form['search']['language'] = array(
557 558 559
    // Change type of form widget if more the 5 options will
    // be present (2 of the options are added below).
    '#type' => (count($languages) <= 3 ? 'radios' : 'select'),
560 561 562 563 564 565 566
    '#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'),
567
    '#options' => array('all' => t('Both translated and untranslated strings'), 'translated' => t('Only translated strings'), 'untranslated' => t('Only untranslated strings')),
568 569 570 571 572 573 574 575 576 577 578 579
  );
  $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;
580
}
581 582 583
/**
 * @} End of "locale-translate-seek"
 */
584

585 586 587 588
/**
 * @defgroup locale-translate-import Translation import screen.
 * @{
 */
589

590 591
/**
 * User interface for the translation import screen.
Dries's avatar
 
Dries committed
592
 */
593 594
function locale_translate_import_form() {
  // Get all languages, except English
595 596
  $names = locale_language_list('name', TRUE);
  unset($names['en']);
Dries's avatar
 
Dries committed
597

598
  if (!count($names)) {
599
    $languages = _locale_prepare_predefined_list();
600
    $default = array_shift(array_keys($languages));
Dries's avatar
 
Dries committed
601 602 603
  }
  else {
    $languages = array(
604
      t('Already added languages') => $names,
605
      t('Languages not yet added') => _locale_prepare_predefined_list()
Dries's avatar
 
Dries committed
606
    );
607
    $default = array_shift(array_keys($names));
Dries's avatar
 
Dries committed
608
  }
Steven Wittens's avatar
Locale:  
Steven Wittens committed
609

610
  $form = array();
611
  $form['import'] = array('#type' => 'fieldset',
drumm's avatar
drumm committed
612
    '#title' => t('Import translation'),
613 614 615 616
  );
  $form['import']['file'] = array('#type' => 'file',
    '#title' => t('Language file'),
    '#size' => 50,
617
    '#description' => t('A Gettext Portable Object (<em>.po</em>) file.'),
618 619 620
  );
  $form['import']['langcode'] = array('#type' => 'select',
    '#title' => t('Import into'),
621 622
    '#options' => $languages,
    '#default_value' => $default,
623 624 625 626 627 628 629
    '#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.'),
630 631 632
  );
  $form['import']['mode'] = array('#type' => 'radios',
    '#title' => t('Mode'),
633 634 635 636 637
    '#default_value' => LOCALE_IMPORT_KEEP,
    '#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')
    ),
638 639
  );
  $form['import']['submit'] = array('#type' => 'submit', '#value' => t('Import'));
640
  $form['#attributes']['enctype'] = 'multipart/form-data';
641

642
  return $form;
Dries's avatar
 
Dries committed
643 644
}

645 646 647
/**
 * Process the locale import form submission.
 */
648
function locale_translate_import_form_submit($form, &$form_state) {
649
  // Ensure we have the file uploaded
650
  if ($file = file_save_upload('file')) {
651 652 653

    // Add language, if not yet supported
    $languages = language_list('language', TRUE);
654
    $langcode = $form_state['values']['langcode'];
655
    if (!isset($languages[$langcode])) {
656
      $predefined = _locale_get_predefined_list();
657 658
      locale_add_language($langcode);
      drupal_set_message(t('The language %language has been created.', array('%language' => t($predefined[$langcode][0]))));
659
    }
660

661
    // Now import strings into the language
662
    if ($ret = _locale_import_po($file, $langcode, $form_state['values']['mode'], $form_state['values']['group']) == FALSE) {
663 664 665
      $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);
666 667 668 669
    }
  }
  else {
    drupal_set_message(t('File to import not found.'), 'error');
670
    return 'admin/build/translate/import';
671 672
  }

673 674
  $form_state['redirect'] = 'admin/build/translate';
  return;
675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698
}
/**
 * @} 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;
699 700
}

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

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

748
/**
749
 * Process a translation (or template) export form submission.
750
 */
751
function locale_translate_export_po_form_submit($form, &$form_state) {
752
  // If template is required, language code is not given.
753 754 755 756 757 758
  $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'])));
759 760
}
/**
761
 * @} End of "locale-translate-export"
762
 */
763

764
/**
765 766
 * @defgroup locale-translate-edit Translation text editing
 * @{
767 768 769 770 771
 */

/**
 * User interface for string editing.
 */
772
function locale_translate_edit_form(&$form_state, $lid) {
773 774 775
  // Fetch source string, if possible.
  $source = db_fetch_object(db_query('SELECT source, textgroup, location FROM {locales_source} WHERE lid = %d', $lid));
  if (!$source) {
776 777
    drupal_set_message(t('String not found.'), 'error');
    drupal_goto('admin/build/translate/search');
778 779
  }

780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798
  // 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
    ),
799 800
  );

801 802 803 804 805 806 807 808 809 810
  // 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);
811 812
  foreach ($languages as $langcode => $language) {
    $form['translations'][$langcode] = array(
813
      '#type' => 'textarea',
814
      '#title' => t($language->name),
815
      '#rows' => $rows,
816
      '#default_value' => '',
817 818
    );
  }
819

820 821 822 823 824
  // 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;
  }
825 826

  $form['submit'] = array('#type' => 'submit', '#value' => t('Save translations'));
827
  return $form;
828 829 830 831 832 833
}

/**
 * Process string editing form submissions.
 * Saves all translations of one string submitted from a form.
 */
834 835 836
function locale_translate_edit_form_submit($form, &$form_state) {
  $lid = $form_state['values']['lid'];
  foreach ($form_state['values']['translations'] as $key => $value) {
837 838 839 840 841 842 843 844 845
    $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);
      }
846
    }
847 848 849
    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);
850
    }
851

852 853
    // Force JavaScript translation file recreation for this language.
    _locale_invalidate_js($key);
854
  }
855

856 857
  drupal_set_message(t('The string has been saved.'));

858 859
  // Clear locale cache.
  cache_clear_all('locale:', 'cache', TRUE);
860

861 862
  $form_state['redirect'] = 'admin/build/translate/search';
  return;
863
}
864 865 866 867 868 869 870 871
/**
 * @} End of "locale-translate-edit"
 */

/**
 * @defgroup locale-translate-delete Translation delete interface.
 * @{
 */
872 873 874 875

/**
 * Delete a language string.
 */
876
function locale_translate_delete($lid) {
877 878
  db_query('DELETE FROM {locales_source} WHERE lid = %d', $lid);
  db_query('DELETE FROM {locales_target} WHERE lid = %d', $lid);
879 880
  // Force JavaScript translation file recreation for all languages.
  _locale_invalidate_js();
881
  cache_clear_all('locale:', 'cache', TRUE);
882
  drupal_set_message(t('The string has been removed.'));
883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910
  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.
911 912 913
 * @param $enabled
 *   Optionally TRUE to enable the language when created or FALSE to disable.
 * @param $default
Dries's avatar
Dries committed
914
 *   Optionally set this language to be the default.
915
 */
916
function locale_add_language($langcode, $name = NULL, $native = NULL, $direction = LANGUAGE_LTR, $domain = '', $prefix = '', $enabled = TRUE, $default = FALSE) {
917 918
  // Default prefix on language code.
  if (empty($prefix)) {
919
    $prefix = $langcode;
920 921
  }

922 923 924 925 926
  // 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];
927
    $direction = isset($predefined[$langcode][2]) ? $predefined[$langcode][2] : LANGUAGE_LTR;
928 929 930
  }

  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);
931

932 933
  // Only set it as default if enabled.
  if ($enabled && $default) {
934
    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' => ''));
935 936
  }

937 938 939 940
  if ($enabled) {
    // Increment enabled language count if we are adding an enabled language.
    variable_set('language_count', variable_get('language_count', 1) + 1);
  }
941

942 943 944
  // Force JavaScript translation file creation for the newly added language.
  _locale_invalidate_js($langcode);

945
  watchdog('locale', 'The %language language (%code) has been created.', array('%language' => $name, '%code' => $langcode));
946
}
947 948 949
/**
 * @} End of "locale-api-add"
 */
950

951 952 953 954
/**
 * @defgroup locale-api-import Translation import API.
 * @{
 */
955

Dries's avatar
 
Dries committed
956 957 958
/**
 * Parses Gettext Portable Object file information and inserts into database
 *
drumm's avatar
drumm committed
959