locale.inc 92.7 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
    $form['weight'][$langcode] = array(
      '#type' => 'weight',
47
48
      '#default_value' => $language->weight,
      '#attributes' => array('class' => 'language-order-weight'),
49
    );
50
51
52
    $form['name'][$langcode] = array('#markup' => check_plain($language->name));
    $form['native'][$langcode] = array('#markup' => check_plain($language->native));
    $form['direction'][$langcode] = array('#markup' => ($language->direction == LANGUAGE_RTL ? t('Right to left') : t('Left to right')));
Dries's avatar
   
Dries committed
53
  }
54
55
56
57
58
59
  $form['enabled'] = array('#type' => 'checkboxes',
    '#options' => $options,
    '#default_value' => $enabled,
  );
  $form['site_default'] = array('#type' => 'radios',
    '#options' => $options,
60
    '#default_value' => language_default('language'),
61
  );
62
  $form['submit'] = array('#type' => 'submit', '#value' => t('Save configuration'));
63
  $form['#theme'] = 'locale_languages_overview_form';
64

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

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

103
  return $output;
Dries's avatar
   
Dries committed
104
105
106
}

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

135
  // Changing the language settings impacts the interface.
136
  cache_clear_all('*', 'cache_page', TRUE);
Dries's avatar
   
Dries committed
137

138
139
  $form_state['redirect'] = 'admin/settings/language';
  return;
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
}
/**
 * @} 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;
157
158
}

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

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

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

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

/**
294
295
 * Validate the language addition form.
 */
296
297
function locale_languages_predefined_form_validate($form, &$form_state) {
  $langcode = $form_state['values']['langcode'];
298

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

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

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

333
334
335
336
337
338
  // 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);
  }

339
340
  $form_state['redirect'] = 'admin/settings/language';
  return;
341
342
343
344
345
}

/**
 * Validate the language editing form. Reused for custom language addition too.
 */
346
347
function locale_languages_edit_form_validate($form, &$form_state) {
  if (!empty($form_state['values']['domain']) && !empty($form_state['values']['prefix'])) {
348
349
    form_set_error('prefix', t('Domain and path prefix values should not be set at the same time.'));
  }
350
  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']))) {
351
    form_set_error('domain', t('The domain (%domain) is already tied to a language (%language).', array('%domain' => $form_state['values']['domain'], '%language' => $duplicate->language)));
352
  }
353
  if (empty($form_state['values']['prefix']) && language_default('language') != $form_state['values']['langcode'] && empty($form_state['values']['domain'])) {
354
355
    form_set_error('prefix', t('Only the default language can have both the domain and prefix empty.'));
  }
356
  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']))) {
357
    form_set_error('prefix', t('The prefix (%prefix) is already tied to a language (%language).', array('%prefix' => $form_state['values']['prefix'], '%language' => $duplicate->language)));
358
359
360
361
362
363
  }
}

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

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

/**
 * User interface for the language deletion confirmation screen.
 */
391
function locale_languages_delete_form(&$form_state, $langcode) {
392
393
394
395
396
397
398

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

399
  if (language_default('language') == $langcode) {
400
401
402
403
404
405
406
407
408
409
410
411
    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);
412
    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'));
413
414
415
416
417
418
  }
}

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

438
439
  $form_state['redirect'] = 'admin/settings/language';
  return;
440
441
442
443
444
445
446
447
448
}
/**
 * @} End of "locale-language-add-edit"
 */

/**
 * @defgroup locale-languages-negotiation Language negotiation options screen
 * @{
 */
449
450
451
452

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

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

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

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

  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.'),
  );
562
  $form['search']['language'] = array(
563
564
565
    // 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'),
566
567
568
569
570
571
572
    '#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'),
573
    '#options' => array('all' => t('Both translated and untranslated strings'), 'translated' => t('Only translated strings'), 'untranslated' => t('Only untranslated strings')),
574
575
576
577
578
579
580
581
582
583
584
585
  );
  $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;
586
}
587
588
589
/**
 * @} End of "locale-translate-seek"
 */
590

591
592
593
594
/**
 * @defgroup locale-translate-import Translation import screen.
 * @{
 */
595

596
597
/**
 * User interface for the translation import screen.
Dries's avatar
   
Dries committed
598
 */
599
600
function locale_translate_import_form() {
  // Get all languages, except English
601
602
  $names = locale_language_list('name', TRUE);
  unset($names['en']);
Dries's avatar
   
Dries committed
603

604
  if (!count($names)) {
605
    $languages = _locale_prepare_predefined_list();
606
    $default = array_shift(array_keys($languages));
Dries's avatar
   
Dries committed
607
608
609
  }
  else {
    $languages = array(
610
      t('Already added languages') => $names,
611
      t('Languages not yet added') => _locale_prepare_predefined_list()
Dries's avatar
   
Dries committed
612
    );
613
    $default = array_shift(array_keys($names));
Dries's avatar
   
Dries committed
614
  }
Steven Wittens's avatar
Locale:    
Steven Wittens committed
615

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

648
  return $form;
Dries's avatar
   
Dries committed
649
650
}

651
652
653
/**
 * Process the locale import form submission.
 */
654
function locale_translate_import_form_submit($form, &$form_state) {
655
  // Ensure we have the file uploaded
656
  if ($file = file_save_upload('file')) {
657
658
659

    // Add language, if not yet supported
    $languages = language_list('language', TRUE);
660
    $langcode = $form_state['values']['langcode'];
661
    if (!isset($languages[$langcode])) {
662
      $predefined = _locale_get_predefined_list();
663
664
      locale_add_language($langcode);
      drupal_set_message(t('The language %language has been created.', array('%language' => t($predefined[$langcode][0]))));
665
    }
666

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

679
680
  $form_state['redirect'] = 'admin/build/translate';
  return;
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
}
/**
 * @} 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;
705
706
}

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

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

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

770
/**
771
772
 * @defgroup locale-translate-edit Translation text editing
 * @{
773
774
775
776
777
 */

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

786
787
788
789
790
  // Add original text to the top and some values for form altering.
  $form = array(
    'original' => array(
      '#type'  => 'item',
      '#title' => t('Original text'),
791
      '#markup' => check_plain(wordwrap($source->source, 0)),
792
793
794
795
796
797
798
799
800
801
802
803
804
    ),
    'lid' => array(
      '#type'  => 'value',
      '#value' => $lid
    ),
    'textgroup' => array(
      '#type'  => 'value',
      '#value' => $source->textgroup,
    ),
    'location' => array(
      '#type'  => 'value',
      '#value' => $source->location
    ),
805
806
  );

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

826
  // Fetch translations and fill in default values in the form.
827
  $result = db_query("SELECT DISTINCT translation, language FROM {locales_target} WHERE lid = %d AND language <> '%s'", $lid, $omit);
828
829
830
  while ($translation = db_fetch_object($result)) {
    $form['translations'][$translation->language]['#default_value'] = $translation->translation;
  }
831
832

  $form['submit'] = array('#type' => 'submit', '#value' => t('Save translations'));
833
  return $form;
834
835
836
837
838
839
}

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

858
859
    // Force JavaScript translation file recreation for this language.
    _locale_invalidate_js($key);
860
  }
861

862
863
  drupal_set_message(t('The string has been saved.'));

864
  // Clear locale cache.
865
  _locale_invalidate_js();
866
  cache_clear_all('locale:', 'cache', TRUE);
867

868
869
  $form_state['redirect'] = 'admin/build/translate/search';
  return;
870
}
871
872
873
874
875
876
877
878
/**
 * @} End of "locale-translate-edit"
 */

/**
 * @defgroup locale-translate-delete Translation delete interface.
 * @{
 */
879
880

/**
881
 * String deletion confirmation page.
882
 */
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
function locale_translate_delete_page($lid) {
  if ($source = db_fetch_object(db_query('SELECT * FROM {locales_source} WHERE lid = %d', $lid))) {
    return drupal_get_form('locale_translate_delete_form', $source);
  }
  else {
    return drupal_not_found();
  }
}

/**
 * User interface for the string deletion confirmation screen.
 */
function locale_translate_delete_form(&$form_state, $source) {
  $form['lid'] = array('#type' => 'value', '#value' => $source->lid);
  return confirm_form($form, t('Are you sure you want to delete the string "%source"?', array('%source' => $source->source)), 'admin/build/translate/search', t('Deleting the string will remove all translations of this string in all languages. This action cannot be undone.'), t('Delete'), t('Cancel'));
}

/**
 * Process string deletion submissions.
 */
function locale_translate_delete_form_submit($form, &$form_state) {
  db_query('DELETE FROM {locales_source} WHERE lid = %d', $form_state['values']['lid']);
  db_query('DELETE FROM {locales_target} WHERE lid = %d', $form_state['values']['lid']);
906
907
  // Force JavaScript translation file recreation for all languages.
  _locale_invalidate_js();
908
  cache_clear_all('locale:', 'cache', TRUE);
909
  drupal_set_message(t('The string has been removed.'));
910
  $form_state['redirect'] = 'admin/build/translate/search';
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
}
/**
 * @} 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.
938
939
940
 * @param $enabled
 *   Optionally TRUE to enable the language when created or FALSE to disable.
 * @param $default
Dries's avatar
Dries committed
941
 *   Optionally set this language to be the default.
942
 */
943
function locale_add_language($langcode, $name = NULL, $native = NULL, $direction = LANGUAGE_LTR, $domain = '', $prefix = '', $enabled = TRUE, $default = FALSE) {
944
945
  // Default prefix on language code.
  if (empty($prefix)) {
946
    $prefix = $langcode;
947
948
  }

949
950
951
952
953
  // 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];
954
    $direction = isset($predefined[$langcode][2]) ? $predefined[$langcode][2] : LANGUAGE_LTR;
955
956
957
  }

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

959
960
  // Only set it as default if enabled.
  if ($enabled && $default) {
961
    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' => ''));
962
963
  }

964
965
966
967
  if ($enabled) {
    // Increment enabled language count if we are adding an enabled language.
    variable_set('language_count', variable_get('language_count', 1) + 1);
  }
968

969
970
971
  // Force JavaScript translation file creation for the newly added language.
  _locale_invalidate_js($langcode);

972
  watchdog('locale', 'The %language language (%code) has been created.', array('%language' => $name, '%code' => $langcode));
973
}
974
975
976
/**
 * @} End of "locale-api-add"
 */
977

978
979
980
981
/**
 * @defgroup locale-api-import Translation import API.
 * @{
 */
982

Dries's avatar
   
Dries committed
983
984
985
/**
 * Parses Gettext Portable Object file information and inserts into database
 *
drumm's avatar
drumm committed
986
987
 * @param $file
 *   Drupal file object corresponding to the PO file to import
988
 * @param $langcode
drumm's avatar
drumm committed
989
990
 *   Language code
 * @param $mode
991
 *   Should existing translations be replaced LOCALE_IMPORT_KEEP or LOCALE_IMPORT_OVERWRITE
992
993
 * @param $group
 *   Text group to import PO file into (eg. 'default' for interface translations)
Dries's avatar
   
Dries committed
994
 */
995
996
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
997
998
  if (!ini_get('safe_mode')) {
    set_time_limit(240);
Dries's avatar
   
Dries committed
999
  }
Steven Wittens's avatar
Locale:    
Steven Wittens committed
1000

1001
1002
  // Check if we have the language already in the database.
  if (!db_fetch_object(db_query("SELECT language FROM {languages} WHERE language = '%s'", $langcode))) {
1003
    drupal_set_message(t('The language selected for import is not supported.'), 'error');
Dries's avatar
   
Dries committed
1004
1005
    return FALSE;
  }
Dries's avatar
   
Dries committed
1006

1007
  // Get strings from file (returns on failure after a partial import, or on success)
1008
  $status = _locale_import_read_po('db-store', $file, $mode, $langcode, $group);
1009
  if ($status === FALSE) {
1010
    // Error messages are set in _locale_import_read_po().
1011
1012
    return FALSE;
  }
Dries's avatar
   
Dries committed
1013

1014
  // Get status information on import process.
1015
  list($headerdone, $additions, $updates, $deletes) = _locale_import_one_string('db-report');
Dries's avatar
   
Dries committed
1016

1017
  if (!$headerdone) {
1018
    drupal_set_message(t('The translation file %filename appears to have a missing or malformed header.', array('%filename' => $file->filename)), 'error');
Dries's avatar
   
Dries committed
1019
  }
Dries's avatar
   
Dries committed
1020

1021
1022
  // Clear cache and force refresh of JavaScript translations.
  _locale_invalidate_js($langcode);
1023
  cache_clear_all('locale:', 'cache', TRUE);
1024

1025
  // Rebuild the menu, strings may have changed.
Dries's avatar
   
Dries committed
1026
  menu_rebuild();
1027

1028
1029
  drupal_set_message(t('The translation was successfully imported. There are %number newly created translated strings, %update strings were updated and %delete strings were removed.', array('%number' => $additions, '%update' => $updates, '%delete' => $deletes)));
  watchdog('locale', 'Imported %file into %locale: %number new strings added, %update updated and %delete removed.', array('%file' => $file->filename, '%locale' => $langcode, '%number' => $additions, '%update' => $updates, '%delete' => $deletes));
Dries's avatar
   
Dries committed
1030
1031
1032
1033
1034
1035
  return TRUE;
}

/**
 * Parses Gettext Portable Object file into an array
 *
drumm's avatar
drumm committed
1036
1037
1038
1039
1040
 * @param $op
 *   Storage operation type: db-store or mem-store
 * @param $file
 *   Drupal file object corresponding to the PO file to import
 * @param $mode
1041
 *   Should existing translations be replaced LOCALE_IMPORT_KEEP or LOCALE_IMPORT_OVERWRITE
drumm's avatar
drumm committed
1042
1043
 * @param $lang
 *   Language code
1044
1045
 * @param $group
 *   Text group to import PO file into (eg. 'default' for interface translations)
Dries's avatar
   
Dries committed