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

92
  return $output;
Dries's avatar
   
Dries committed
93
94
95
}

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

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

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

148
149
150
/**
 * Predefined language setup form.
 */
151
function locale_languages_predefined_form() {
152
  $predefined = _locale_prepare_predefined_list();
153
  $form = array();
154
  $form['language list'] = array('#type' => 'fieldset',
155
    '#title' => t('Predefined language'),
156
157
158
159
    '#collapsible' => TRUE,
  );
  $form['language list']['langcode'] = array('#type' => 'select',
    '#title' => t('Language name'),
160
161
162
    '#default_value' => key($predefined),
    '#options' => $predefined,
    '#description' => t('Select the desired language here, or add it below, if you are unable to find it in the list.'),
163
164
  );
  $form['language list']['submit'] = array('#type' => 'submit', '#value' => t('Add language'));
165
166
  return $form;
}
Dries's avatar
Dries committed
167

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

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

/**
213
 * Common elements of the language addition and editing form.
214
215
216
217
218
219
 *
 * @param $form
 *   A parent form item (or empty array) to add items below.
 * @param $language
 *   Language object to edit.
 */
220
function _locale_languages_common_controls(&$form, $language = NULL) {
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
  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)),
      '#description' => t('This should be an <a href="@rfc4646">RFC 4646</a> compliant language identifier. Basic tags use a country code with an optional script or regional variant name, like "en", "en-US" and "zh-Hant".', array('@rfc4646' => 'http://www.ietf.org/rfc/rfc4646.txt')),
    );
  }
246
  $form['name'] = array('#type' => 'textfield',
247
248
    '#title' => t('Language name in English'),
    '#maxlength' => 64,
249
    '#default_value' => @$language->name,
250
251
252
    '#required' => TRUE,
    '#description' => t('Name of the language. Will be available for translation in all languages.'),
  );
253
  $form['native'] = array('#type' => 'textfield',
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
    '#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,
    '#description' => t('Optional path prefix, for example "deutsch" for the German version. This value is not used in the "None" and "Domain" negotiation schemes. You can leave this empty if you use "Path only" negotiation and this is the default language. Changing this will break existing URLs.')
  );
  $form['domain'] = array('#type' => 'textfield',
    '#title' => t('Language domain'),
    '#maxlength' => 64,
    '#default_value' => @$language->domain,
    '#description' => t('Optional custom domain with protocol (eg. "http://example.de" or "http://de.example.com" for the German version). This value is only used in the "Domain" negotiation mode. If left empty and in "Domain" mode, this language will not be accessible.'),
  );
  $form['direction'] = array('#type' => 'radios',
    '#title' => t('Direction'),
    '#required' => TRUE,
    '#description' => t('Direction of the text being written in this language.'),
    '#default_value' => @$language->direction,
277
    '#options' => array(LANGUAGE_LTR => t('Left to right'), LANGUAGE_RTL => t('Right to left'))
278
  );
279
280
  return $form;
}
Dries's avatar
   
Dries committed
281
282

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

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

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

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

322
323
324
325
326
327
  // 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);
  }

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

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

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

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

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

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

388
  if (language_default('language') == $langcode) {
389
390
391
392
393
394
395
396
397
398
399
400
    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);
401
    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'));
402
403
404
405
406
407
  }
}

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

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

/**
 * @defgroup locale-languages-negotiation Language negotiation options screen
 * @{
 */
438
439
440
441

/**
 * Setting for language negotiation options
 */
442
function locale_languages_configure_form() {
443
444
445
446
447
448
449
450
451
  $form['language_negotiation'] = array(
    '#title' => t('Language negotiation'),
    '#type' => 'radios',
    '#options' => array(
      LANGUAGE_NEGOTIATION_NONE => t('None. Language will be independent of visitor preferences and language prefixes or domains.'),
      LANGUAGE_NEGOTIATION_PATH_DEFAULT => t('Path prefix only. If a suitable path prefix is not identified, the default language is used.'),
      LANGUAGE_NEGOTIATION_PATH => t('Path prefix with language fallback. If a suitable  path prefix is not identified, language is based on user preferences and browser language settings.'),
      LANGUAGE_NEGOTIATION_DOMAIN => t('Domain name only. If a suitable domain name is not identified, the default language is used.')),
    '#default_value' => variable_get('language_negotiation', LANGUAGE_NEGOTIATION_NONE),
452
    '#description' => t('The used language detection mode. Changing this also changes how paths are constructed, so setting a different value breaks all incoming links. Do not change on a live site without thinking twice!')
453
454
455
456
457
458
459
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Save settings')
  );
  return $form;
}
Dries's avatar
Dries committed
460

461
462
463
/**
 * Submit function for language negotiation settings.
 */
464
465
function locale_languages_configure_form_submit($form, &$form_state) {
  variable_set('language_negotiation', $form_state['values']['language_negotiation']);
466
  drupal_set_message(t('Language negotiation configuration saved.'));
467
468
  $form_state['redirect'] = 'admin/settings/language';
  return;
469
470
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
}
/**
 * @} 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;
  }

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

  // Languages with at least one record in the locale table.
506
  $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");
507
508
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
  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'),
559
    '#options' => array('all' => t('Both translated and untranslated strings'), 'translated' => t('Only translated strings'), 'untranslated' => t('Only untranslated strings')),
560
561
562
563
564
565
566
567
568
569
570
571
  );
  $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;
572
}
573
574
575
/**
 * @} End of "locale-translate-seek"
 */
576

577
578
579
580
/**
 * @defgroup locale-translate-import Translation import screen.
 * @{
 */
581

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

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

602
  $form = array();
603
  $form['import'] = array('#type' => 'fieldset',
drumm's avatar
drumm committed
604
    '#title' => t('Import translation'),
605
606
607
608
609
610
611
612
  );
  $form['import']['file'] = array('#type' => 'file',
    '#title' => t('Language file'),
    '#size' => 50,
    '#description' => t('A gettext Portable Object (.po) file.'),
  );
  $form['import']['langcode'] = array('#type' => 'select',
    '#title' => t('Import into'),
613
614
    '#options' => $languages,
    '#default_value' => $default,
615
616
617
618
619
620
621
    '#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.'),
622
623
624
625
  );
  $form['import']['mode'] = array('#type' => 'radios',
    '#title' => t('Mode'),
    '#default_value' => 'overwrite',
626
    '#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')),
627
628
  );
  $form['import']['submit'] = array('#type' => 'submit', '#value' => t('Import'));
629
  $form['#attributes']['enctype'] = 'multipart/form-data';
630

631
  return $form;
Dries's avatar
   
Dries committed
632
633
}

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

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

650
    // Now import strings into the language
651
    if ($ret = _locale_import_po($file, $langcode, $form_state['values']['mode'], $form_state['values']['group']) == FALSE) {
652
653
654
      $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);
655
656
657
658
    }
  }
  else {
    drupal_set_message(t('File to import not found.'), 'error');
659
    return 'admin/build/translate/import';
660
661
  }

662
663
  $form_state['redirect'] = 'admin/build/translate';
  return;
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
}
/**
 * @} 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;
688
689
}

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

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

737
/**
738
 * Process a translation (or template) export form submission.
739
 */
740
function locale_translate_export_po_form_submit($form, &$form_state) {
741
  // If template is required, language code is not given.
742
743
744
745
746
747
  $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'])));
748
749
}
/**
750
 * @} End of "locale-translate-export"
751
 */
752

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

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

769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
  // 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
    ),
788
789
  );

790
791
792
793
794
795
796
797
798
799
  // 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);
800
801
  foreach ($languages as $langcode => $language) {
    $form['translations'][$langcode] = array(
802
      '#type' => 'textarea',
803
      '#title' => t($language->name),
804
      '#rows' => $rows,
805
      '#default_value' => '',
806
807
    );
  }
808

809
810
811
812
813
  // 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;
  }
814
815

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

/**
 * Process string editing form submissions.
 * Saves all translations of one string submitted from a form.
 */
823
824
825
function locale_translate_edit_form_submit($form, &$form_state) {
  $lid = $form_state['values']['lid'];
  foreach ($form_state['values']['translations'] as $key => $value) {
826
827
828
829
830
831
832
833
834
    $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);
      }
835
    }
836
837
838
    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);
839
    }
840

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

845
846
  drupal_set_message(t('The string has been saved.'));

847
848
  // Clear locale cache.
  cache_clear_all('locale:', 'cache', TRUE);
849

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

/**
 * @defgroup locale-translate-delete Translation delete interface.
 * @{
 */
861
862
863
864

/**
 * Delete a language string.
 */
865
function locale_translate_delete($lid) {
866
867
  db_query('DELETE FROM {locales_source} WHERE lid = %d', $lid);
  db_query('DELETE FROM {locales_target} WHERE lid = %d', $lid);
868
869
  // Force JavaScript translation file recreation for all languages.
  _locale_invalidate_js();
870
  cache_clear_all('locale:', 'cache', TRUE);
871
  drupal_set_message(t('The string has been removed.'));
872
873
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
  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.
900
901
902
 * @param $enabled
 *   Optionally TRUE to enable the language when created or FALSE to disable.
 * @param $default
Dries's avatar
Dries committed
903
 *   Optionally set this language to be the default.
904
 */
905
function locale_add_language($langcode, $name = NULL, $native = NULL, $direction = LANGUAGE_LTR, $domain = '', $prefix = '', $enabled = TRUE, $default = FALSE) {
906
907
  // Default prefix on language code.
  if (empty($prefix)) {
908
    $prefix = $langcode;
909
910
  }

911
912
913
914
915
  // 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];
916
    $direction = isset($predefined[$langcode][2]) ? $predefined[$langcode][2] : LANGUAGE_LTR;
917
918
919
  }

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

921
922
  // Only set it as default if enabled.
  if ($enabled && $default) {
923
    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' => ''));
924
925
  }

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

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

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

940
941
942
943
/**
 * @defgroup locale-api-import Translation import API.
 * @{
 */
944

Dries's avatar
   
Dries committed
945
946
947
/**
 * Parses Gettext Portable Object file information and inserts into database
 *
drumm's avatar
drumm committed
948
949
 * @param $file
 *   Drupal file object corresponding to the PO file to import
950
 * @param $langcode
drumm's avatar
drumm committed
951
952
 *   Language code
 * @param $mode
953
 *   Should existing translations be replaced LOCALE_IMPORT_KEEP or LOCALE_IMPORT_OVERWRITE
954
955
 * @param $group
 *   Text group to import PO file into (eg. 'default' for interface translations)
Dries's avatar
   
Dries committed
956
 */
957
958
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
959
960
  if (!ini_get('safe_mode')) {
    set_time_limit(240);
Dries's avatar
   
Dries committed
961
  }
Steven Wittens's avatar
Locale:    
Steven Wittens committed
962

963
964
  // Check if we have the language already in the database.
  if (!db_fetch_object(db_query("SELECT language FROM {languages} WHERE language = '%s'", $langcode))) {
965
    drupal_set_message(t('The language selected for import is not supported.'), 'error');
Dries's avatar
   
Dries committed
966
967
    return FALSE;
  }
Dries's avatar
   
Dries committed
968

969
  // Get strings from file (returns on failure after a partial import, or on success)
970
  $status = _locale_import_read_po('db-store', $file, $mode, $langcode, $group);
971
  if ($status === FALSE) {
972
    // Error messages are set in _locale_import_read_po().
973
974
    return FALSE;
  }
Dries's avatar
   
Dries committed
975

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

979
  if (!$headerdone) {
980
    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
981
  }
Dries's avatar
   
Dries committed
982

983
984
  // Clear cache and force refresh of JavaScript translations.
  _locale_invalidate_js($langcode);
985
  cache_clear_all('locale:', 'cache', TRUE);
986

987
  // Rebuild the menu, strings may have changed.
Dries's avatar
   
Dries committed
988
  menu_rebuild();
989

990
991
  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
992
993
994
995
996
997
  return TRUE;
}

/**
 * Parses Gettext Portable Object file into an array
 *
drumm's avatar
drumm committed
998
999
1000
1001
1002
 * @param $op
 *   Storage operation type: db-store or mem-store
 * @param $file
 *   Drupal file object corresponding to the PO file to import
 * @param $mode
1003
 *   Should existing translations be replaced LOCALE_IMPORT_KEEP or LOCALE_IMPORT_OVERWRITE
drumm's avatar
drumm committed
1004
1005
 * @param $lang
 *   Language code
1006
1007
 * @param $group
 *   Text group to import PO file into (eg. 'default' for interface translations)
Dries's avatar
   
Dries committed
1008
 */
1009
function _locale_import_read_po($op, $file, $mode = NULL, $lang = NULL, $group = 'default') {
Dries's avatar
   
Dries committed
1010

1011
  $fd = fopen($file->filepath, "rb"); // File will get closed by PHP on return
Dries's avatar
   
Dries committed
1012
  if (!$fd) {
drumm's avatar
drumm committed
1013
    _locale_import_message('The translation import failed, because the file %filename could not be read.', $file);
Dries's avatar
   
Dries committed
1014
1015
1016
1017
1018
1019
    return FALSE;
  }

  $context = "COMMENT"; // Parser context: COMMENT, MSGID, MSGID_PLURAL, MSGSTR and MSGSTR_ARR
  $current = array();   // Current entry being read
  $plural = 0;          // Current plural form
1020
  $lineno = 0;          // Current line
Dries's avatar
   
Dries committed
1021

1022
1023
  while (!feof($fd)) {
    $line = fgets($fd, 10*1024); // A line should not be this long
1024
    if ($lineno == 0) {
Dries's avatar
Dries committed
1025
      // The first line might come with a UTF-8 BOM, which should be removed.
1026
1027
      $line = str_replace("\xEF\xBB\xBF", '', $line);
    }
Dries's avatar
   
Dries committed
1028
    $lineno++;
1029
    $line = trim(strtr($line, array("\\\n" => "")));
Dries's avatar
   
Dries committed
1030
1031
1032
1033
1034
1035

    if (!strncmp("#", $line, 1)) { // A comment
      if ($context == "COMMENT") { // Already in comment context: add
        $current["#"][] = substr($line, 1);
      }
      elseif (($context == "MSGSTR") || ($context == "MSGSTR_ARR")) { // End current entry, start a new one
1036
        _locale_import_one_string($op, $current, $mode, $lang, $file, $group);
Dries's avatar
   
Dries committed
1037
1038
1039
1040
1041
        $current = array();
        $current["#"][] = substr($line, 1);
        $context = "COMMENT";
      }
      else { // Parse error
drumm's avatar
drumm committed
1042
        _locale_import_message('The translation file %filename contains an error: "msgstr" was expected but not found on line %line.', $file, $lineno);
Dries's avatar
   
Dries committed
1043
1044
1045
1046
1047
        return FALSE;
      }
    }
    elseif (!strncmp("msgid_plural", $line, 12)) {
      if ($context != "MSGID") { // Must be plural form for current entry
drumm's avatar
drumm committed
1048
        _locale_import_message('The translation file %filename contains an error: "msgid_plural" was expected but not found on line %line.', $file, $lineno);
Dries's avatar
   
Dries committed
1049
1050
1051
1052
        return FALSE;
      }
      $line = trim(substr($line, 12));
      $quoted = _locale_import_parse_quoted($line);
1053
      if ($quoted === FALSE) {