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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1013
  $fd = fopen($file->filepath, "rb"); // File will get closed by PHP on return
Dries's avatar
   
Dries committed
1014
  if (!$fd) {
drumm's avatar
drumm committed
1015
    _locale_import_message('The translation import failed, because the file %filename could not be read.', $file);
Dries's avatar
   
Dries committed
1016
1017
1018
1019
1020
1021
    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
1022
  $lineno = 0;          // Current line
Dries's avatar
   
Dries committed
1023

1024
1025
  while (!feof($fd)) {
    $line = fgets($fd, 10*1024); // A line should not be this long
1026
    if ($lineno == 0) {