Project

General

Profile

Paste
Download (36.4 KB) Statistics
| Branch: | Revision:

root / drupal7 / sites / all / modules / i18n / i18n_string / i18n_string.module @ 01f36513

1
<?php
2

    
3
/**
4
 * @file
5
 * Internationalization (i18n) package - translatable strings.
6
 *
7
 * Object oriented string translation using locale and textgroups. As opposed to core locale strings,
8
 * all strings handled by this module will have a unique id (name), composed by several parts
9
 *
10
 * A string name or string id will have the form 'textgroup:type:objectid:property'. Examples:
11
 *
12
 * - 'profile:field:profile_name:title', will be the title for the profile field 'profile_name'
13
 * - 'taxonomy:term:tid:name', will be the name for the taxonomy term tid
14
 * - 'views:view_name:display_id:footer', footer text
15
 *
16
 * Notes:
17
 * - The object id must be an integer. This is intended for quick indexing of some properties
18
 *
19
 * Some concepts
20
 * - Textgroup. Group the string belongs to, defined by locale hook.
21
 * - Location. Unique id of the string for this textgroup.
22
 * - Name. Unique absolute id of the string: textgroup + location.
23
 * - Context. Object with textgroup, type, objectid, property.
24
 *
25
 * Default language
26
 * - Default language may be English or not. It will be the language set as default.
27
 *   Source strings will be stored in default language.
28
 * - In the traditional i18n use case you shouldn't change the default language once defined.
29
 *
30
 * Default language changes
31
 * - You might result in the need to change the default language at a later point.
32
 * - Enabling translation of default language will curcumvent previous limitations.
33
 * - Check i18n_string_translate_langcode() for more details.
34
 *
35
 * The API other modules to translate/update/remove user defined strings consists of
36
 *
37
 * @see i18n_string($name, $string, $langcode)
38
 * @see i18n_string_update($name, $string, $format)
39
 * @see i18n_string_remove($name, $string)
40
 *
41
 * @author Jose A. Reyero, 2007
42
 */
43

    
44
/**
45
 * Translated string is current.
46
 */
47
define('I18N_STRING_STATUS_CURRENT', 0);
48

    
49
/**
50
 * Translated string needs updating as the source has been edited.
51
 */
52
define('I18N_STRING_STATUS_UPDATE', 1);
53

    
54
/**
55
 * Source string is obsoleted, cannot be found anymore. To be deleted.
56
 */
57
define('I18N_STRING_STATUS_DELETE', 2);
58

    
59
/**
60
 * Special string formats/filters: Run through filter_xss
61
 */
62
define('I18N_STRING_FILTER_XSS', 'FILTER_XSS');
63

    
64
/**
65
 * Special string formats/filters: Run through filter_xss_admin
66
 */
67
define('I18N_STRING_FILTER_XSS_ADMIN', 'FILTER_XSS_ADMIN');
68

    
69
/**
70
 * Implements hook_help().
71
 */
72
function i18n_string_help($path, $arg) {
73
  switch ($path) {
74
    case 'admin/help#i18n_string':
75
      $output = '<p>' . t('This module adds support for other modules to translate user defined strings. Depending on which modules you have enabled that use this feature you may see different text groups to translate.') . '<p>';
76
      $output .= '<p>' . t('This works differently to Drupal standard localization system: The strings will be translated from the <a href="@configure-strings">source language</a>, which defaults to the site default language (it may not be English), so changing the default language may cause all these translations to be broken.', array('@configure-strings' => url('admin/config/regional/i18n/strings'))) . '</p>';
77
      $output .= '<ul>';
78
      $output .= '<li>' . t('To search and translate strings, use the <a href="@translate-interface">translation interface</a> pages.', array('@translate-interface' => url('admin/config/regional/translate'))) . '</li>';
79
      $output .= '<li>' . t('If you are missing strings to translate, use the <a href="@refresh-strings">refresh strings</a> page.', array('@refresh-strings' => url('admin/build/translate/refresh'))) . '</li>';
80
      $output .= '</ul>';
81
      $output .= '<p>' . t('Read more on the <em>Internationalization handbook</em>: <a href="http://drupal.org/node/313293">Translating user defined strings</a>.') . '</p>';
82
      return $output;
83

    
84
    case 'admin/config/regional/translate/i18n_string':
85
      $output = '<p>' . t('On this page you can refresh and update values for user defined strings.') . '</p>';
86
      $output .= '<ul>';
87
      $output .= '<li>' . t('Use the refresh option when you are missing strings to translate for a given text group. All the strings will be re-created keeping existing translations.') . '</li>';
88
      $output .= '<li>' . t('Use the update option when some of the strings had been previously translated with the localization system, but the translations are not showing up for the configurable strings.') . '</li>';
89
      $output .= '</ul>';
90
      $output .= '<p>' . t('To search and translate strings, use the <a href="@translate-interface">translation interface</a> pages.', array('@translate-interface' => url('admin/config/regional/translate'))) . '</p>';
91
      $output .= '<p>' . t('<strong>Important:</strong> To configure which text formats are safe for translation, visit the <a href="@configure-strings">configure strings</a> page before refreshing your strings.', array('@configure-strings' => url('admin/config/regional/i18n/strings'))) . '</p>';
92
      return $output;
93

    
94
    case 'admin/config/regional/language':
95
      $output = '<p>' . t('<strong>Warning</strong>: Changing the default language may have unwanted effects on string translations. Check also the <a href="@configure-strings">source language</a> for translations and read more about <a href="@i18n_string-help">String translation</a>', array('@configure-strings' => url('admin/config/regional/i18n/strings'), '@i18n_string-help' => url('admin/help/i18n_string'))) . '</p>';
96
      return $output;
97
    case 'admin/config/regional/i18n/strings':
98
      $output = '<p>' . t('When translating user defined strings that have a text format associated, translators will be able to edit the text before it is filtered which may be a security risk for some filters. An obvious example is when using the PHP filter but other filters may also be dangerous.') . '</p>';
99
      $output .= '<p>' . t('As a general rule <strong>do not allow any filtered text to be translated unless the translators already have access to that text format</strong>. However if you are doing all your translations through this site\'s translation UI or the Localization client, and never importing translations for other textgroups than <i>default</i>, filter access will be checked for translators on every translation page.') . '</p>';
100
      $output .= '<p>' . t('<strong>Important:</strong> After disallowing some text format, use the <a href="@refresh-strings">refresh strings</a> page so forbidden strings are deleted and not allowed anymore for translators.', array('@refresh-strings' => url('admin/config/regional/translate/i18n_string'))) . '</p>';
101
      return $output;
102
    case 'admin/config/filters':
103
      return '<p>' . t('After updating your text formats do not forget to review the list of formats allowed for string translations on the <a href="@configure-strings">configure translatable strings</a> page.', array('@configure-strings' => url('admin/config/regional/i18n/strings'))) . '</p>';
104
  }
105
}
106

    
107
/**
108
 * Implements hook_menu().
109
 */
110
function i18n_string_menu() {
111
  $items['admin/config/regional/translate/i18n_string'] = array(
112
    'title' => 'Strings',
113
    'description' => 'Refresh user defined strings.',
114
    'weight' => 20,
115
    'type' => MENU_LOCAL_TASK,
116
    'page callback' => 'drupal_get_form',
117
    'page arguments' => array('i18n_string_admin_refresh_form'),
118
    'file' => 'i18n_string.admin.inc',
119
    'access arguments' => array('translate interface'),
120
  );
121

    
122
  $items['admin/config/regional/i18n/strings'] = array(
123
    'title' => 'Strings',
124
    'description' => 'Options for user defined strings.',
125
    'weight' => 20,
126
    'type' => MENU_LOCAL_TASK,
127
    'page callback' => 'drupal_get_form',
128
    'page arguments' => array('variable_edit_form', array('i18n_string_allowed_formats', 'i18n_string_source_language', 'i18n_string_textgroup_class_[textgroup]')),
129
    'access arguments' => array('administer site configuration'),
130
  );
131
  // AJAX callback path for strings.
132
  $items['i18n_string/save'] = array(
133
    'title' => 'Save string',
134
    'page callback' => 'i18n_string_l10n_client_save_string',
135
    'access arguments' => array('use on-page translation'),
136
    'file' => 'i18n_string.pages.inc',
137
    'type' => MENU_CALLBACK,
138
  );
139
  return $items;
140
}
141

    
142
/**
143
 * Implements hook_menu_alter().
144
 *
145
 * Take over the locale translation page.
146
 */
147
function i18n_string_menu_alter(&$items) {
148
  $items['admin/config/regional/translate/edit/%'] = array(
149
    'title' => 'Edit string',
150
    'page callback' => 'drupal_get_form',
151
    'page arguments' => array('i18n_string_locale_translate_edit_form', 5),
152
    'access arguments' => array('translate interface'),
153
    'file' => 'i18n_string.pages.inc',
154
    'file path' => drupal_get_path('module', 'i18n_string'),
155
  );
156
}
157

    
158
/**
159
 * Implements hook_hook_info().
160
 */
161
function i18n_string_hook_info() {
162
  $hooks['i18n_string_info'] =
163
  $hooks['i18n_string_list'] =
164
  $hooks['i18n_string_refresh'] =
165
  $hooks['i18n_string_objects'] = array(
166
    'group' => 'i18n',
167
  );
168
  return $hooks;
169
}
170

    
171
/**
172
 * Implements hook_locale().
173
 *
174
 * Provide the information from i18n_string groups to locale module
175
 */
176
function i18n_string_locale($op = 'groups') {
177
  if ($op == 'groups') {
178
    $groups = array();
179
    foreach (i18n_string_group_info() as $name => $info) {
180
      $groups[$name] = $info['title'];
181
    }
182
    return $groups;
183
  }
184
}
185

    
186
/**
187
 * Implements hook_permission().
188
 */
189
function i18n_string_permission() {
190
  return array(
191
    'translate user-defined strings' => array(
192
      'title' => t('Translate user-defined strings'),
193
      'description' => t('Translate user-defined strings that are created as part of content or configuration.'),
194
      'restrict access' => TRUE,
195
    ),
196
    'translate admin strings' => array(
197
      'title' => t('Translate admin strings'),
198
      'description' => t('Translate administrative strings with a very permissive XSS/HTML filter that allows all HTML tags.'),
199
      'restrict access' => TRUE,
200
    ),
201
  );
202
}
203

    
204
/**
205
 * Implements hook_modules_enabled().
206
 */
207
function i18n_string_modules_enabled($modules) {
208
  module_load_include('admin.inc', 'i18n_string');
209
  i18n_string_refresh_enabled_modules($modules);
210
}
211

    
212
/**
213
 * Implements hook_modules_uninstalled().
214
 */
215
function i18n_string_modules_uninstalled($modules) {
216
  module_load_include('admin.inc', 'i18n_string');
217
  i18n_string_refresh_uninstalled_modules($modules);
218
}
219

    
220
/**
221
 * Implements hook_form_FORM_ID_alter()
222
 */
223
function i18n_string_form_l10n_client_form_alter(&$form, &$form_state) {
224
  $form['#action'] = url('i18n_string/save');
225
}
226

    
227
/**
228
 * Implements hook_form_FORM_ID_alter()
229
 */
230
function i18n_string_form_locale_translate_export_po_form_alter(&$form, $form_state) {
231
  $names = locale_language_list('name', TRUE);
232
  if (i18n_string_source_language() != 'en' && array_key_exists('en', $names)) {
233
    $form['langcode']['#options']['en'] = $names['en'];
234
  }
235
}
236

    
237
/**
238
 * Implements hook_form_FORM_ID_alter()
239
 */
240
function i18n_string_form_locale_translate_import_form_alter(&$form, $form_state) {
241
  if (i18n_string_source_language() != 'en') {
242
    $names = locale_language_list('name', TRUE);
243
    if (array_key_exists('en', $names)) {
244
      $form['import']['langcode']['#options'][t('Already added languages')]['en'] = $names['en'];
245
    }
246
    else {
247
      $form['import']['langcode']['#options'][t('Languages not yet added')]['en'] = t('English');
248
    }
249
  }
250
  $form['#submit'][] = 'i18n_string_locale_translate_import_form_submit';
251
}
252

    
253
/**
254
 * Update string data after import form submit
255
 */
256
function i18n_string_locale_translate_import_form_submit($form, &$form_state) {
257
  if (!drupal_get_messages('error', FALSE) && i18n_string_group_info($form_state['values']['group'])) {
258
    i18n_string_textgroup($form_state['values']['group'])->update_check();
259
  }
260
}
261

    
262
/**
263
 * Implements hook_element_info_alter().
264
 *
265
 * We need to do this on the element info level as wysiwyg also does so and form
266
 * API (incorrectly) does not merge in the defaults for values that are arrays.
267
 */
268
function i18n_string_element_info_alter(&$types) {
269
  $types['text_format']['#pre_render'][] = 'i18n_string_pre_render_text_format';
270
}
271

    
272
/**
273
 * The '#pre_render' function to alter the text format element in a translation.
274
 * The text format for a translation is taken form the original, so the text
275
 * format drop down should be disabled.
276
 *
277
 * @param array $element
278
 *   The text_format element which will be rendered.
279
 *
280
 * @return array
281
 *   The altered text_format element with a disabled "Text format" select.
282
 */
283
function i18n_string_pre_render_text_format($element) {
284
  if (!empty($element['#i18n_string_is_translation'])) {
285
      $element['format']['format']['#attributes']['disabled'] = TRUE;
286
  }
287
  return $element;
288
}
289

    
290
/**
291
 * Check if translation is required for this language code.
292
 *
293
 * Translation is required when default language is different from the given
294
 * language, or when default language translation is explicitly enabled.
295
 *
296
 * No UI is provided to enable translation of default language. On the other
297
 * hand, you can enable/disable translation for a specific language by adding
298
 * the following to your settings.php
299
 *
300
 * @param $langcode
301
 *   Optional language code to check. It will default to current request language.
302
 * @code
303
 *   // Enable translation of specific language. Language code is 'xx'
304
 *   $conf['i18n_string_translate_langcode_xx'] = TRUE;
305
 *   // Disable translation of specific language. Language code is 'yy'
306
 *   $conf['i18n_string_translate_langcode_yy'] = FALSE;
307
 * @endcode
308
 */
309
function i18n_string_translate_langcode($langcode = NULL) {
310
  $translate = &drupal_static(__FUNCTION__);
311
  $langcode = isset($langcode) ? $langcode : i18n_langcode();
312
  if (!isset($translate[$langcode])) {
313
    $translate[$langcode] = variable_get('i18n_string_translate_langcode_' . $langcode, i18n_string_source_language() != $langcode);
314
  }
315
  return $translate[$langcode];
316
}
317

    
318
/**
319
 * Create string class from string name
320
 */
321
function i18n_string_build($name, $string = NULL) {
322
  list ($group, $context) = i18n_string_context($name);
323
  return i18n_string_textgroup($group)->build_string($context, $string);
324
}
325

    
326
/**
327
 * Update / create translation source for user defined strings.
328
 *
329
 * @param $name
330
 *   Array or string concatenated with ':' that contains textgroup and string context
331
 * @param $string
332
 *   Source string in default language. Default language may or may not be English.
333
 *   Array of key => string to update multiple strings at once
334
 * @param $options
335
 *   Array with additional options:
336
 *   - 'format', String format if the string has text format
337
 *   - 'messages', Whether to print out status messages
338
 */
339
function i18n_string_update($name, $string, $options = array()) {
340
  if (is_array($string)) {
341
    return i18n_string_multiple('update', $name, $string, $options);
342
  }
343
  else {
344
    list($textgroup, $context) = i18n_string_context($name);
345
    return i18n_string_textgroup($textgroup)->context_update($context, $string, $options);
346
  }
347
}
348

    
349
/**
350
 * Update context for strings.
351
 *
352
 * As some string locations depend on configurable values, the field needs sometimes to be updated
353
 * without losing existing translations. I.e:
354
 * - profile fields indexed by field name.
355
 * - content types indexted by low level content type name.
356
 *
357
 * Example:
358
 *  'profile:field:oldfield:*' -> 'profile:field:newfield:*'
359
 */
360
function i18n_string_update_context($oldname, $newname) {
361
  module_load_install('i18n_string');
362
  return i18n_string_install_update_context($oldname, $newname);
363
}
364

    
365
/**
366
 * Get textgroup handler.
367
 *
368
 * @return i18n_string_textgroup_default
369
 *
370
 */
371
function i18n_string_textgroup($textgroup) {
372
  $groups = &drupal_static(__FUNCTION__);
373
  if (!isset($groups[$textgroup])) {
374
    $class = i18n_string_group_info($textgroup, 'class', 'i18n_string_textgroup_default');
375
    $groups[$textgroup] = new $class($textgroup);
376
  }
377
  return $groups[$textgroup];
378
}
379

    
380
/**
381
 * Check whether a string format is allowed for translation.
382
 */
383
function i18n_string_allowed_format($format_id = NULL) {
384
  if (!$format_id || $format_id === I18N_STRING_FILTER_XSS || $format_id === I18N_STRING_FILTER_XSS_ADMIN) {
385
    return TRUE;
386
  }
387
  else {
388
    // Check the format still exists an it is in the allowed formats list.
389
    return filter_format_load($format_id) && in_array($format_id, variable_get('i18n_string_allowed_formats', array(filter_fallback_format())), TRUE);
390
  }
391
}
392

    
393
/**
394
 * Convert string name into textgroup and string context
395
 *
396
 * @param $name
397
 *   Array or string concatenated with ':' that contains textgroup and string context
398
 * @param $replace
399
 *   Parameter to replace the placeholder ('*') if we are dealing with multiple strings
400
 *   Or parameter to append to context if there's no placeholder
401
 *
402
 * @return array
403
 *   The first element will be the text group name
404
 *   The second one will be an array with string name elements, without textgroup
405
 */
406
function i18n_string_context($name, $replace = NULL) {
407
  $parts = is_array($name) ? $name : explode(':', $name);
408
  if ($replace) {
409
    $key = array_search('*', $parts);
410
    if ($key !== FALSE) {
411
      $parts[$key] = $replace;
412
    }
413
    elseif (count($parts) < 4) {
414
      array_push($parts, $replace);
415
    }
416
  }
417
  $textgroup = array_shift($parts);
418
  $context = $parts;
419
  return array($textgroup, $context);
420
}
421

    
422
/**
423
 * Mark form element as localizable
424
 */
425
function i18n_string_element_mark(&$element) {
426
  $description = '<strong>' . t('This string will be localizable. You can translate it using the <a href="@translate-interface">translate interface</a> pages.', array('@translate-interface' => url('admin/config/regional/translate/translate'))) . '</strong>';
427
  if (empty($element['#description'])) {
428
    $element['#description'] = $description;
429
  }
430
  else {
431
    $element['#description'] .= ' ' . $description;
432
  }
433
}
434

    
435

    
436
/**
437
 * Get source string object.
438
 *
439
 * This returns the i18nstring object only when it has a source.
440
 *
441
 * @return i18n_string_object
442
 */
443
function i18n_string_get_source($name) {
444
  return i18n_string_build($name)->get_source();
445
}
446

    
447
/**
448
 * Get full string object.
449
 *
450
 * Builds string and loads the source if available.
451
 *
452
 * @return i18n_string_object
453
 */
454
function i18n_string_get_string($name, $default = NULL) {
455
  $i18nstring = i18n_string_build($name, $default);
456
  $i18nstring->get_source();
457
  return $i18nstring;
458
}
459

    
460
/**
461
 * Get full string object by lid.
462
 */
463
function i18n_string_get_by_lid($lid) {
464
  $strings = i18n_string_load_multiple(array('lid' => $lid));
465
  return reset($strings);
466
}
467

    
468
/**
469
 * Load multiple strings, including string source
470
 *
471
 * @param $conditions
472
 *   Array of conditions for i18n_string table.
473
 *
474
 * @return $strings
475
 *   List of i18n string objects
476
 */
477
function i18n_string_load_multiple($conditions) {
478
  // The primary table here will be i18n_string, though we add source too.
479
  $query = db_select('i18n_string', 'i')
480
    ->fields('i');
481
  $query->leftJoin('locales_source', 's', 'i.lid = s.lid');
482
  $query->fields('s', array('source', 'version', 'location'));
483
  // Add text group condition and add conditions to the query
484
  foreach ($conditions as $field => $value) {
485
    $alias = in_array($field, array('source', 'version', 'location')) ? 's' : 'i';
486
    $query->condition($alias . '.' . $field, $value);
487
  }
488
  // this patch  is a workaround for core file bug in file
489
  // include/database/prefetch.inc (see: http://drupal.org/node/1567216)
490
  // return $query->execute()->fetchAll(PDO::FETCH_CLASS, 'i18n_string_object');
491
  $stmt = $query->execute();
492
  $stmt->setFetchMode(PDO::FETCH_CLASS, 'i18n_string_object');
493
  return $stmt->fetchAll();
494
}
495

    
496
/**
497
 * Get textgroup info, from hook_locale('info')
498
 *
499
 * @param $group
500
 *   Text group name.
501
 * @param $default
502
 *   Default value to return for a property if not set.
503
 */
504
function i18n_string_group_info($group = NULL, $property = NULL, $default = NULL) {
505
  $info = &drupal_static(__FUNCTION__ , NULL);
506

    
507
  if (!isset($info)) {
508
    $info = module_invoke_all('i18n_string_info');
509
    drupal_alter('i18n_string_info', $info);
510
  }
511

    
512
  if ($group && $property) {
513
    return isset($info[$group][$property]) ? $info[$group][$property] : $default;
514
  }
515
  elseif ($group) {
516
    return isset($info[$group]) ? $info[$group] : array();
517
  }
518
  else {
519
    return $info;
520
  }
521
}
522

    
523
/**
524
 * Implements hook_i18n_string_info_alter().
525
 *
526
 * Set determined classes to use for the text group.
527
 */
528
function i18n_string_i18n_string_info_alter(&$info) {
529
  foreach (array_keys($info) as $name) {
530
    // If class is not defined. Classes from other modules, fixed classes and etc.
531
    if (!isset($info[$name]['class'])) {
532
      $info[$name]['class'] = variable_get('i18n_string_textgroup_class_' . $name, 'i18n_string_textgroup_default');
533
    }
534
  }
535
}
536

    
537
/**
538
 * Translate / update multiple strings
539
 *
540
 * @param $strings
541
 *   Array of name => string pairs
542
 */
543
function i18n_string_multiple($operation, $name, $strings, $options = array()) {
544
  $result = array();
545
  // Strings may be an array of properties, we need to shift it
546
  if ($operation == 'remove') {
547
    $strings = array_flip($strings);
548
  }
549
  foreach ($strings as $key => $string) {
550
    list($textgroup, $context) = i18n_string_context($name, $key);
551
    array_unshift($context, $textgroup);
552
    $result[$key] = call_user_func('i18n_string_' . $operation, $context, $string, $options);
553
  }
554
  return $result;
555
}
556

    
557
/**
558
 * @ingroup i18napi
559
 * @{
560
 */
561

    
562
/**
563
 * Get translation for user defined string.
564
 *
565
 * This function is intended to return translations for plain strings that have NO text format
566
 *
567
 * @param array|string name
568
 *   Array or string concatenated with ':' that contains textgroup and string context
569
 * @param array|string $string
570
 *   A string in the default language, a string wth format (array with keys
571
 *   value and format),or an array of strings (without format) to be translated.
572
 * @param array $options
573
 *   An associative array of additional options, with the following keys:
574
 *   - 'langcode' (defaults to the current language) The language code to translate to a language other than what is used to display the page.
575
 *   - 'filter' Filtering callback to apply to the translated string only
576
 *   - 'format' Input format to apply to the translated string only
577
 *   - 'callback' Callback to apply to the result (both to translated or untranslated string
578
 *   - 'sanitize' Whether to filter the translation applying the text format if any, default is TRUE
579
 *   - 'sanitize default' Whether to filter the default value if no translation found, default is FALSE
580
 *
581
 * @return string
582
 */
583
function i18n_string_translate($name, $string, $options = array()) {
584
  if (is_array($string) && isset($string['value'])) {
585
    $string = $string['value'];
586
  }
587
  if (is_array($string)) {
588
    return i18n_string_translate_list($name, $string, $options);
589
  }
590
  else {
591
    $options['langcode'] = $langcode = isset($options['langcode']) ? $options['langcode'] : i18n_langcode();
592
    if (i18n_string_translate_langcode($langcode)) {
593
      list($textgroup, $context) = i18n_string_context($name);
594
      $translation = i18n_string_textgroup($textgroup)->context_translate($context, $string, $options);
595
      // Add for l10n client if available, we pass translation object that contains the format
596
      i18n_string_l10n_client_add($translation, $langcode);
597
      return $translation->format_translation($langcode, $options);
598
    }
599
    else {
600
      // If we don't want to translate to this language, format and return
601
      $options['sanitize'] = !empty($options['sanitize default']);
602
      return i18n_string_format($string, $options);
603
    }
604
  }
605
}
606

    
607
/**
608
 * Check user access to translate a specific string.
609
 *
610
 * If the string has a format the user is not allowed to edit, it will return FALSE
611
 *
612
 * @param $string_format;
613
 *   String object or $format_id
614
 */
615
function i18n_string_translate_access($string_format, $account = NULL) {
616
  $format_id = is_object($string_format) ? i18n_object_field($string_format, 'format') : $string_format;
617
  return user_access('translate interface', $account) &&
618
    (empty($format_id) || i18n_string_allowed_format($format_id) && ($format = filter_format_load($format_id)) && filter_access($format, $account));
619
}
620

    
621
/**
622
 * Check whether there is any problem for the  user to translate a specific string.
623
 *
624
 * Here we assume the user has 'translate interface' access that should have
625
 * been checked for the page. Possible reasons a user cannot translate a string:
626
 *
627
 * @param $i18nstring
628
 *   String object.
629
 * @param $account
630
 *   Optional user account, defaults to current user.
631
 *
632
 * @return
633
 *   None or empty string if the user has access to translate the string.
634
 *   Message if the user cannot translate that string.
635
 */
636
function i18n_string_translate_check_string($i18nstring, $account = NULL) {
637
  // Check block translation permissions.
638
  if ($i18nstring->textgroup == 'blocks') {
639
    if (!user_access('translate interface', $account) && !user_access('translate blocks', $account)) {
640
      return t('This is a user-defined string within a block. You are not allowed to translate blocks.');
641
    }
642
  }
643
  elseif (!user_access('translate interface', $account) || !user_access('translate user-defined strings', $account)) {
644
    return t('This is a user-defined string. You are not allowed to translate these strings.');
645
  }
646

    
647
  if (!empty($i18nstring->format)) {
648
    if (!i18n_string_allowed_format($i18nstring->format)) {
649
      $format = filter_format_load($i18nstring->format);
650
      return t('This string uses the %name text format. Strings with this format are not allowed for translation.', array('%name' => $format->name));
651
    }
652
    elseif ($format = filter_format_load($i18nstring->format)) {
653
      // It is a text format, check user access to that text format.
654
      if (!filter_access($format, $account)) {
655
        return t('This string uses the %name text format. You are not allowed to translate or edit texts with this format.', array('%name' => $format->name));
656
      }
657
    }
658
    else {
659
      // This is one of our special formats, I18N_STRING_FILTER_*
660
      if ($i18nstring->format == I18N_STRING_FILTER_XSS_ADMIN && !user_access('translate admin strings', $account)) {
661
        return t('The source string is an administrative string. You are not allowed to translate these strings.');
662
      }
663
    }
664
  }
665
  // No error message, it should be OK to translate.
666
  return '';
667
}
668

    
669
/**
670
 * Format the resulting translation or the default string applying callbacks
671
 *
672
 * @param $string
673
 *   Text string.
674
 * @param $options
675
 *   Array of options for string formatting:
676
 *   - 'format', text format to apply to the string, defaults to none.
677
 *   - 'sanitize', whether to apply the text format, defaults to TRUE.
678
 *   - 'cache', text format parameter.
679
 *   - 'langcode', text format parameter, defaults to current page language.
680
 *   - 'allowed_tags', allowed HTML tags when format is I18N_STRING_FILTER_XSS
681
 */
682
function i18n_string_format($string, $options = array()) {
683
  $options += array('langcode' => i18n_langcode(), 'format' => FALSE, 'sanitize' => TRUE, 'cache' => FALSE);
684
  // Apply format and callback
685
  if ($string) {
686
    if ($options['sanitize']) {
687
      if ($options['format']) {
688
        // Handle special format values (xss, xss_admin)
689
        switch ($options['format']) {
690
          case I18N_STRING_FILTER_XSS:
691
            $string = !empty($options['allowed_tags']) ? filter_xss($string, $options['allowed_tags']) : filter_xss($string);
692
            break;
693
          case I18N_STRING_FILTER_XSS_ADMIN:
694
            $string = filter_xss_admin($string);
695
            break;
696
          default:
697
            $string = check_markup($string, $options['format'], $options['langcode'], $options['cache']);
698
        }
699
      }
700
      else {
701
        $string = check_plain($string);
702
      }
703
    }
704
    if (isset($options['callback'])) {
705
      $string = call_user_func($options['callback'], $string);
706
    }
707
  }
708
  // Finally, apply prefix and suffix
709
  $options += array('prefix' => '', 'suffix' => '');
710
  return $options['prefix'] . $string . $options['suffix'];
711
}
712

    
713
/**
714
 * Get filtered translation.
715
 *
716
 * This function is intended to return translations for strings that have a text format
717
 *
718
 * @param $name
719
 *   Array or string concatenated with ':' that contains textgroup and string context
720
 * @param $default
721
 *   Default string to return if not found, already filtered
722
 * @param $options
723
 *   Array with additional options.
724
 */
725
function i18n_string_text($name, $default, $options = array()) {
726
  $options += array('format' => filter_fallback_format(), 'sanitize' => TRUE);
727
  return i18n_string_translate($name, $default, $options);
728
}
729

    
730
/**
731
 * Translation for plain string. In case it finds a translation it applies check_plain() to it
732
 *
733
 * @param $name
734
 *   Array or string concatenated with ':' that contains textgroup and string context
735
 * @param $default
736
 *   Default string to return if not found
737
 * @param $options
738
 *   Array with additional options
739
 */
740
function i18n_string_plain($name, $default, $options = array()) {
741
  $options += array('filter' => 'check_plain');
742
  return i18n_string_translate($name, $default, $options);
743
}
744

    
745
/**
746
 * Get source language code for translations
747
 */
748
function i18n_string_source_language() {
749
  return variable_get('i18n_string_source_language', language_default('language'));
750
}
751

    
752
/**
753
 * Translation for list of options
754
 *
755
 * @param $options
756
 *   Array with additional options, some changes
757
 *   - 'index' => field that will be mapped to the array key (defaults to 'property')
758
 */
759
function i18n_string_translate_list($name, $strings, $options = array()) {
760
  $options['langcode'] = $langcode = isset($options['langcode']) ? $options['langcode'] : i18n_langcode();
761
  // If language is default, just return
762
  if (i18n_string_translate_langcode($langcode)) {
763
    // Get textgroup context, preserve placeholder
764
    list($textgroup, $context) = i18n_string_context($name, '*');
765
    $translations = i18n_string_textgroup($textgroup)->multiple_translate($context, $strings, $options);
766
    // Add for l10n client if available, we pass translation object that contains the format
767
    foreach ($translations as $index => $translation) {
768
      i18n_string_l10n_client_add($translation, $langcode);
769
      $strings[$index] = $translation->format_translation($langcode, $options);
770
    }
771
  }
772
  else {
773
    // Format and return
774
    foreach ($strings as $key => $string) {
775
      $strings[$key] = i18n_string_format($string, $options);
776
    }
777
  }
778
  return $strings;
779
}
780

    
781
/**
782
 * Remove source and translations for user defined string.
783
 *
784
 * Though for most strings the 'name' or 'string id' uniquely identifies that string,
785
 * there are some exceptions (like profile categories) for which we need to use the
786
 * source string itself as a search key.
787
 *
788
 * @param $name
789
 *   String name
790
 * @param $string
791
 *   Optional source string (string in default language).
792
 *   Array of string properties to remove
793
 */
794
function i18n_string_remove($name, $string = NULL, $options = array()) {
795
  if (is_array($string)) {
796
    return i18n_string_multiple('remove', $name, $string, $options);
797
  }
798
  else {
799
    list($textgroup, $context) = i18n_string_context($name);
800
    return i18n_string_textgroup($textgroup)->context_remove($context, $string, $options);
801
  }
802
}
803

    
804
/**
805
 * @} End of "ingroup i18napi".
806
 */
807

    
808
/*** l10n client related functions ***/
809

    
810
/**
811
 * Add string to l10n strings if enabled and allowed for this string
812
 *
813
 * @param $context
814
 *   String object
815
 */
816
function i18n_string_l10n_client_add($string, $langcode) {
817
  // If current language add to l10n client list for later on page translation.
818
  // If langcode translation was disabled we are not supossed to reach here.
819
  if (($langcode == i18n_langcode()) && function_exists('l10_client_add_string_to_page') && user_access('translate interface')) {
820
    if (!$string->check_translate_access()) {
821
      $translation = $string->get_translation($langcode);
822
      $source = !empty($string->source) ? $string->source : $string->string;
823
      l10_client_add_string_to_page($source, $translation ? $translation : TRUE, $string->textgroup, $string->context);
824
    }
825
  }
826
}
827

    
828
/**
829
 * Get information about object string translation
830
 */
831
function i18n_string_object_info($type = NULL, $property = NULL) {
832
  if ($type) {
833
    if (($info = i18n_object_info($type, 'string translation'))) {
834
      if ($property) {
835
        return isset($info[$property]) ? $info[$property] : NULL;
836
      }
837
      else {
838
        return $info;
839
      }
840
    }
841
  }
842
  else {
843
    $list = array();
844
    foreach (i18n_object_info() as $type => $info) {
845
      if (!empty($info['string translation'])) {
846
        $list[$type] = $info;
847
      }
848
    }
849
    return $list;
850
  }
851
}
852

    
853
/**
854
 * Implements hook_i18n_object_info_alter().
855
 *
856
 * Set a different default object wrapper for objects that have string translation.
857
 */
858
function i18n_string_i18n_object_info_alter(&$object_info) {
859
  foreach ($object_info as $type => &$info) {
860
    if (!empty($info['string translation']) && (empty($info['class']) || $info['class'] == 'i18n_object_wrapper')) {
861
      $info['class'] = 'i18n_string_object_wrapper';
862
    }
863
  }
864
}
865

    
866
/**
867
 * Translate object properties
868
 *
869
 * We clone the object previously so we don't risk translated properties being saved
870
 *
871
 * @param $type
872
 *   Object type
873
 * @param $object
874
 *   Object or array
875
 */
876
function i18n_string_object_translate($type, $object, $options = array()) {
877
  $langcode = isset($options['langcode']) ? $options['langcode'] : i18n_langcode();
878
  if (i18n_string_translate_langcode($langcode)) {
879
    // Object properties will be returned without filtering as in the original one.
880
    $options += array('sanitize' => FALSE);
881
    return i18n_object($type, $object)->translate($langcode, $options);
882
  }
883
  else {
884
    return $object;
885
  }
886
}
887

    
888
/**
889
 * Remove object strings, because object is deleted
890
 *
891
 * @param $type
892
 *   Object type
893
 * @param $object
894
 *   Object or array
895
 */
896
function i18n_string_object_remove($type, $object, $options = array()) {
897
  return i18n_object($type, $object)->strings_remove($options);
898
}
899

    
900
/**
901
 * Update object properties.
902
 *
903
 * @param $type
904
 *   Object type
905
 * @param $object
906
 *   Object or array
907
 */
908
function i18n_string_object_update($type, $object, $options = array()) {
909
  return i18n_object($type, $object)->strings_update($options);
910
}
911

    
912
/**
913
 * Generic translation page for i18n_strings objects.
914
 */
915
function i18n_string_object_translate_page($object_type, $object_value, $language = NULL) {
916
  module_load_include('inc', 'i18n_string', 'i18n_string.pages');
917
  return i18n_string_translate_page_object($object_type, $object_value, $language);
918
}
919

    
920
/**
921
 * Preload all strings for this textroup/context.
922
 *
923
 * This is a performance optimization to load all needed strings with a single query.
924
 *
925
 * Examples of valid string name to search are:
926
 *  - 'taxonomy:term:*:title'
927
 *    This will find all titles for taxonomy terms
928
 *  - array('taxonomy', 'term', array(1,2), '*')
929
 *    This will find all properties for taxonomy terms 1 and 2
930
 *
931
 * @param $name
932
 *   Specially crafted string name, it may take '*' and array parameters for each element.
933
 * @param $langcode
934
 *   Language code to search translations. Defaults to current language.
935
 *
936
 * @return array()
937
 *   String objects indexed by context.
938
 */
939
function i18n_string_translation_search($name, $langcode = NULL) {
940
  $langcode = isset($langcode) ? $langcode : i18n_langcode();
941
  list ($textgroup, $context) = i18n_string_context($name);
942
  return i18n_string_textgroup($textgroup)->multiple_translation_search($context, $langcode);
943
}
944

    
945
/**
946
 * Update / create translation for a certain source.
947
 *
948
 * @param $name
949
 *   Array or string concatenated with ':' that contains textgroup and string context
950
 * @param $translation
951
 *   Translation string for this language code
952
 * @param $langcode
953
 *   The language code to translate to a language other than what is used to display the page.
954
 * @param $source_string
955
 *   Optional source string, just in case it needs to be created.
956
 *
957
 * @return mixed
958
 *   Source string object if update was successful.
959
 *   Null if source string not found.
960
 *   FALSE if use doesn't have permission to edit this translation.
961
 */
962
function i18n_string_translation_update($name, $translation, $langcode, $source_string = NULL) {
963
  if (is_array($translation)) {
964
    return i18n_string_multiple('translation_update', $name, $translation, $langcode);
965
  }
966
  elseif ($source = i18n_string_get_source($name)) {
967
    if ($langcode == i18n_string_source_language()) {
968
      // It's the default language so we should update the string source as well.
969
      i18n_string_update($name, $translation);
970
    }
971
    else {
972
      list ($textgroup, $context) = i18n_string_context($name);
973
      i18n_string_textgroup($textgroup)->update_translation($context, $langcode, $translation);
974
    }
975
    return $source;
976
  }
977
  elseif ($source_string) {
978
    // We don't have a source in the database, so we need to create it, but only if we've got the source too.
979
    // Note this string won't have any format.
980
    i18n_string_update($name, $source_string);
981
    return i18n_string_translation_update($name, $translation, $langcode);
982
  }
983
  else {
984
    return NULL;
985
  }
986
}
987

    
988
/**
989
 * Count operation results by result value
990
 */
991
function _i18n_string_result_count($list) {
992
  $result = array();
993
  foreach ($list as $value) {
994
    $key = (string)$value;
995
    $result[$key] = isset($result[$key]) ? $result[$key] +1 : 1;
996
  }
997
  return $result;
998
}