INFRA-132 - Civi - PHPStorm cleanup
[civicrm-core.git] / CRM / Core / I18n.php
CommitLineData
6a488035
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
39de6fd5 4 | CiviCRM version 4.6 |
6a488035 5 +--------------------------------------------------------------------+
06b69b18 6 | Copyright CiviCRM LLC (c) 2004-2014 |
6a488035
TO
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
9 | |
10 | CiviCRM is free software; you can copy, modify, and distribute it |
11 | under the terms of the GNU Affero General Public License |
12 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
13 | |
14 | CiviCRM is distributed in the hope that it will be useful, but |
15 | WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
17 | See the GNU Affero General Public License for more details. |
18 | |
19 | You should have received a copy of the GNU Affero General Public |
20 | License and the CiviCRM Licensing Exception along |
21 | with this program; if not, contact CiviCRM LLC |
22 | at info[AT]civicrm[DOT]org. If you have questions about the |
23 | GNU Affero General Public License or the licensing of CiviCRM, |
24 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
25 +--------------------------------------------------------------------+
26*/
27
28/**
29 *
30 * @package CRM
06b69b18 31 * @copyright CiviCRM LLC (c) 2004-2014
6a488035
TO
32 * $Id$
33 *
34 */
35class CRM_Core_I18n {
36
37 /**
2ba175b6
DL
38 * A PHP-gettext instance for string translation;
39 * should stay null if the strings are not to be translated (en_US).
6a488035
TO
40 */
41 private $_phpgettext = NULL;
42
43 /**
44 * Whether we are using native gettext or not.
45 */
46 private $_nativegettext = FALSE;
47
74eb462f
ML
48 /**
49 * Gettext cache for extension domains/streamers, depending on if native or phpgettext.
50 * - native gettext: we cache the value for textdomain()
51 * - phpgettext: we cache the file streamer.
52 */
53 private $_extensioncache = array();
54
6a488035
TO
55 /**
56 * A locale-based constructor that shouldn't be called from outside of this class (use singleton() instead).
57 *
5a4f6742
CW
58 * @param string $locale
59 * the base of this certain object's existence.
6a488035 60 *
77b97be7 61 * @return \CRM_Core_I18n
6a488035 62 */
00be9182 63 public function __construct($locale) {
6a488035
TO
64 if ($locale != '' and $locale != 'en_US') {
65 $config = CRM_Core_Config::singleton();
66
67 if (defined('CIVICRM_GETTEXT_NATIVE') && CIVICRM_GETTEXT_NATIVE && function_exists('gettext')) {
68 // Note: the file hierarchy for .po must be, for example: l10n/fr_FR/LC_MESSAGES/civicrm.mo
69
70 $this->_nativegettext = TRUE;
71
72 $locale .= '.utf8';
73 putenv("LANG=$locale");
74
75 // CRM-11833 Avoid LC_ALL because of LC_NUMERIC and potential DB error.
76 setlocale(LC_TIME, $locale);
77 setlocale(LC_MESSAGES, $locale);
78 setlocale(LC_CTYPE, $locale);
79
80 bindtextdomain('civicrm', $config->gettextResourceDir);
81 bind_textdomain_codeset('civicrm', 'UTF-8');
82 textdomain('civicrm');
83
84 $this->_phpgettext = new CRM_Core_I18n_NativeGettext();
74eb462f 85 $this->_extensioncache['civicrm'] = 'civicrm';
6a488035
TO
86 return;
87 }
88
89 // Otherwise, use PHP-gettext
b395e3c0
ML
90 // we support both the old file hierarchy format and the new:
91 // pre-4.5: civicrm/l10n/xx_XX/civicrm.mo
92 // post-4.5: civicrm/l10n/xx_XX/LC_MESSAGES/civicrm.mo
6a488035
TO
93 require_once 'PHPgettext/streams.php';
94 require_once 'PHPgettext/gettext.php';
95
b395e3c0
ML
96 $mo_file = $config->gettextResourceDir . $locale . DIRECTORY_SEPARATOR . 'LC_MESSAGES' . DIRECTORY_SEPARATOR . 'civicrm.mo';
97
98 if (! file_exists($mo_file)) {
99 // fallback to pre-4.5 mode
100 $mo_file = $config->gettextResourceDir . $locale . DIRECTORY_SEPARATOR . 'civicrm.mo';
101 }
102
103 $streamer = new FileReader($mo_file);
6a488035 104 $this->_phpgettext = new gettext_reader($streamer);
74eb462f 105 $this->_extensioncache['civicrm'] = $this->_phpgettext;
6a488035
TO
106 }
107 }
108
109 /**
110 * Returns whether gettext is running natively or using PHP-Gettext.
111 *
a6c01b45
CW
112 * @return bool
113 * True if gettext is native
6a488035 114 */
00be9182 115 public function isNative() {
6a488035
TO
116 return $this->_nativegettext;
117 }
118
119 /**
120 * Return languages available in this instance of CiviCRM.
121 *
5a4f6742
CW
122 * @param bool $justEnabled
123 * whether to return all languages or just the enabled ones.
6a488035 124 *
a6c01b45
CW
125 * @return array
126 * of code/language name mappings
6a488035 127 */
00be9182 128 public static function languages($justEnabled = FALSE) {
6a488035
TO
129 static $all = NULL;
130 static $enabled = NULL;
131
132 if (!$all) {
c0c9cd82 133 $all = CRM_Contact_BAO_Contact::buildOptions('preferred_language');
6a488035
TO
134
135 // check which ones are available; add them to $all if not there already
136 $config = CRM_Core_Config::singleton();
137 $codes = array();
948d11bf 138 if (is_dir($config->gettextResourceDir) && $dir = opendir($config->gettextResourceDir)) {
6a488035
TO
139 while ($filename = readdir($dir)) {
140 if (preg_match('/^[a-z][a-z]_[A-Z][A-Z]$/', $filename)) {
141 $codes[] = $filename;
142 if (!isset($all[$filename])) {
143 $all[$filename] = $filename;
144 }
145 }
146 }
147 closedir($dir);
148 }
149
150 // drop the unavailable languages (except en_US)
151 foreach (array_keys($all) as $code) {
152 if ($code == 'en_US') {
153 continue;
154 }
155 if (!in_array($code, $codes))unset($all[$code]);
156 }
157 }
158
159 if ($enabled === NULL) {
160 $config = CRM_Core_Config::singleton();
161 $enabled = array();
162 if (isset($config->languageLimit) and $config->languageLimit) {
163 foreach ($all as $code => $name) {
164 if (in_array($code, array_keys($config->languageLimit))) {
165 $enabled[$code] = $name;
166 }
167 }
168 }
169 }
170
171 return $justEnabled ? $enabled : $all;
172 }
173
174 /**
175 * Replace arguments in a string with their values. Arguments are represented by % followed by their number.
176 *
5a4f6742
CW
177 * @param string $str
178 * source string.
6a0b768e 179 * @param mixed arguments, can be passed in an array or through single variables
6a488035 180 *
a6c01b45
CW
181 * @return string
182 * modified string
6a488035 183 */
00be9182 184 public function strarg($str) {
6a488035
TO
185 $tr = array();
186 $p = 0;
187 for ($i = 1; $i < func_num_args(); $i++) {
188 $arg = func_get_arg($i);
189 if (is_array($arg)) {
190 foreach ($arg as $aarg) {
191 $tr['%' . ++$p] = $aarg;
192 }
193 }
194 else {
195 $tr['%' . ++$p] = $arg;
196 }
197 }
198 return strtr($str, $tr);
199 }
200
201 /**
202 * Smarty block function, provides gettext support for smarty.
203 *
204 * The block content is the text that should be translated.
205 *
206 * Any parameter that is sent to the function will be represented as %n in the translation text,
207 * where n is 1 for the first parameter. The following parameters are reserved:
208 * - escape - sets escape mode:
209 * - 'html' for HTML escaping, this is the default.
210 * - 'js' for javascript escaping.
211 * - 'no'/'off'/0 - turns off escaping
212 * - plural - The plural version of the text (2nd parameter of ngettext())
213 * - count - The item count for plural mode (3rd parameter of ngettext())
214 * - context - gettext context of that string (for homonym handling)
215 *
5a4f6742
CW
216 * @param string $text
217 * the original string.
218 * @param array $params
219 * the params of the translation (if any).
6a488035 220 *
a6c01b45
CW
221 * @return string
222 * the translated string
6a488035 223 */
00be9182 224 public function crm_translate($text, $params = array()) {
6a488035
TO
225 if (isset($params['escape'])) {
226 $escape = $params['escape'];
227 unset($params['escape']);
228 }
229
230 // sometimes we need to {ts}-tag a string, but don’t want to
231 // translate it in the template (like civicrm_navigation.tpl),
232 // because we handle the translation in a different way (CRM-6998)
233 // in such cases we return early, only doing SQL/JS escaping
234 if (isset($params['skip']) and $params['skip']) {
235 if (isset($escape) and ($escape == 'sql')) {
74032946 236 $text = CRM_Core_DAO::escapeString($text);
6a488035
TO
237 }
238 if (isset($escape) and ($escape == 'js')) {
239 $text = addcslashes($text, "'");
240 }
241 return $text;
242 }
243
244 if (isset($params['plural'])) {
245 $plural = $params['plural'];
246 unset($params['plural']);
247 if (isset($params['count'])) {
248 $count = $params['count'];
249 }
250 }
251
252 if (isset($params['context'])) {
253 $context = $params['context'];
254 unset($params['context']);
255 }
256 else {
257 $context = NULL;
258 }
259
1a3cba0e
ML
260 // gettext domain for extensions
261 $domain_changed = FALSE;
82bcff63 262 if (! empty($params['domain']) && $this->_phpgettext) {
1a3cba0e
ML
263 if ($this->setGettextDomain($params['domain'])) {
264 $domain_changed = TRUE;
265 }
266 }
267
6a488035
TO
268 // do all wildcard translations first
269 $config = CRM_Core_Config::singleton();
6cf5bb6f
DL
270 $stringTable = CRM_Utils_Array::value(
271 $config->lcMessages,
6a488035
TO
272 $config->localeCustomStrings
273 );
274
275 $exactMatch = FALSE;
276 if (isset($stringTable['enabled']['exactMatch'])) {
277 foreach ($stringTable['enabled']['exactMatch'] as $search => $replace) {
278 if ($search === $text) {
279 $exactMatch = TRUE;
280 $text = $replace;
281 break;
282 }
283 }
284 }
285
6cf5bb6f
DL
286 if (
287 !$exactMatch &&
6a488035
TO
288 isset($stringTable['enabled']['wildcardMatch'])
289 ) {
290 $search = array_keys($stringTable['enabled']['wildcardMatch']);
291 $replace = array_values($stringTable['enabled']['wildcardMatch']);
6cf5bb6f 292 $text = str_replace($search, $replace, $text);
6a488035
TO
293 }
294
295 // dont translate if we've done exactMatch already
296 if (!$exactMatch) {
297 // use plural if required parameters are set
298 if (isset($count) && isset($plural)) {
299
300 if ($this->_phpgettext) {
301 $text = $this->_phpgettext->ngettext($text, $plural, $count);
302 }
303 else {
304 // if the locale's not set, we do ngettext work by hand
305 // if $count == 1 then $text = $text, else $text = $plural
306 if ($count != 1) {
307 $text = $plural;
308 }
309 }
310
311 // expand %count in translated string to $count
312 $text = strtr($text, array('%count' => $count));
313
314 // if not plural, but the locale's set, translate
315 }
316 elseif ($this->_phpgettext) {
317 if ($context) {
318 $text = $this->_phpgettext->pgettext($context, $text);
319 }
320 else {
321 $text = $this->_phpgettext->translate($text);
322 }
323 }
324 }
325
326 // replace the numbered %1, %2, etc. params if present
327 if (count($params)) {
328 $text = $this->strarg($text, $params);
329 }
330
331 // escape SQL if we were asked for it
332 if (isset($escape) and ($escape == 'sql')) {
74032946 333 $text = CRM_Core_DAO::escapeString($text);
6a488035
TO
334 }
335
336 // escape for JavaScript (if requested)
337 if (isset($escape) and ($escape == 'js')) {
338 $text = addcslashes($text, "'");
339 }
340
1a3cba0e
ML
341 if ($domain_changed) {
342 $this->setGettextDomain('civicrm');
343 }
344
6a488035
TO
345 return $text;
346 }
347
348 /**
349 * Translate a string to the current locale.
350 *
5a4f6742
CW
351 * @param string $string
352 * this string should be translated.
6a488035 353 *
a6c01b45
CW
354 * @return string
355 * the translated string
6a488035 356 */
00be9182 357 public function translate($string) {
6a488035
TO
358 return ($this->_phpgettext) ? $this->_phpgettext->translate($string) : $string;
359 }
360
361 /**
362 * Localize (destructively) array values.
363 *
5a4f6742
CW
364 * @param array $array
365 * the array for localization (in place).
366 * @param array $params
367 * an array of additional parameters.
6a488035 368 *
2b37475d 369 * @return void
6a488035
TO
370 */
371 function localizeArray(
372 &$array,
373 $params = array()
374 ) {
375 global $tsLocale;
376
377 if ($tsLocale == 'en_US') {
378 return;
379 }
380
381 foreach ($array as & $value) {
382 if ($value) {
383 $value = ts($value, $params);
384 }
385 }
386 }
387
388 /**
389 * Localize (destructively) array elements with keys of 'title'.
390 *
5a4f6742
CW
391 * @param array $array
392 * the array for localization (in place).
6a488035 393 *
2b37475d 394 * @return void
6a488035 395 */
00be9182 396 public function localizeTitles(&$array) {
6a488035
TO
397 foreach ($array as $key => $value) {
398 if (is_array($value)) {
399 $this->localizeTitles($value);
400 $array[$key] = $value;
401 }
402 elseif ((string ) $key == 'title') {
eb7d6f39 403 $array[$key] = ts($value, array('context' => 'menu'));
6a488035
TO
404 }
405 }
406 }
407
1a3cba0e
ML
408 /**
409 * Binds a gettext domain, wrapper over bindtextdomain().
410 *
6a0b768e
TO
411 * @param $key
412 * Key of the extension (can be 'civicrm', or 'org.example.foo').
1a3cba0e 413 *
a6c01b45
CW
414 * @return Boolean
415 * True if the domain was changed for an extension.
1a3cba0e 416 */
00be9182 417 public function setGettextDomain($key) {
82bcff63
ML
418 /* No domain changes for en_US */
419 if (! $this->_phpgettext) {
420 return FALSE;
421 }
1a3cba0e 422
74eb462f
ML
423 // It's only necessary to find/bind once
424 if (! isset($this->_extensioncache[$key])) {
1a3cba0e
ML
425 $config = CRM_Core_Config::singleton();
426
427 try {
428 $mapper = CRM_Extension_System::singleton()->getMapper();
429 $path = $mapper->keyToBasePath($key);
430 $info = $mapper->keyToInfo($key);
431 $domain = $info->file;
432
74eb462f
ML
433 if ($this->_nativegettext) {
434 bindtextdomain($domain, $path . DIRECTORY_SEPARATOR . 'l10n');
435 bind_textdomain_codeset($domain, 'UTF-8');
436 $this->_extensioncache[$key] = $domain;
437 }
438 else {
439 // phpgettext
440 $mo_file = $path . DIRECTORY_SEPARATOR . 'l10n' . DIRECTORY_SEPARATOR . $config->lcMessages . DIRECTORY_SEPARATOR . 'LC_MESSAGES' . DIRECTORY_SEPARATOR . $domain . '.mo';
441 $streamer = new FileReader($mo_file);
442 $this->_extensioncache[$key] = new gettext_reader($streamer);
443 }
1a3cba0e
ML
444 }
445 catch (CRM_Extension_Exception $e) {
74eb462f
ML
446 // Intentionally not translating this string to avoid possible infinit loops
447 // Only developers should see this string, if they made a mistake in their ts() usage.
448 CRM_Core_Session::setStatus('Unknown extension key in a translation string: ' . $key, '', 'error');
449 $this->_extensioncache[$key] = FALSE;
1a3cba0e
ML
450 }
451 }
452
74eb462f
ML
453 if (isset($this->_extensioncache[$key]) && $this->_extensioncache[$key]) {
454 if ($this->_nativegettext) {
455 textdomain($this->_extensioncache[$key]);
456 }
457 else {
458 $this->_phpgettext = $this->_extensioncache[$key];
459 }
460
461 return TRUE;
1a3cba0e 462 }
74eb462f
ML
463
464 return FALSE;
1a3cba0e
ML
465 }
466
6a488035
TO
467 /**
468 * Static instance provider - return the instance for the current locale.
469 */
00be9182 470 public static function &singleton() {
6a488035
TO
471 static $singleton = array();
472
473 global $tsLocale;
474 if (!isset($singleton[$tsLocale])) {
475 $singleton[$tsLocale] = new CRM_Core_I18n($tsLocale);
476 }
477
478 return $singleton[$tsLocale];
479 }
480
481 /**
482 * Set the LC_TIME locale if it's not set already (for a given language choice).
483 *
a6c01b45
CW
484 * @return string
485 * the final LC_TIME that got set
6a488035 486 */
00be9182 487 public static function setLcTime() {
6a488035
TO
488 static $locales = array();
489
490 global $tsLocale;
491 if (!isset($locales[$tsLocale])) {
492 // with the config being set to pl_PL: try pl_PL.UTF-8,
493 // then pl_PL, if neither present fall back to C
494 $locales[$tsLocale] = setlocale(LC_TIME, $tsLocale . '.UTF-8', $tsLocale, 'C');
495 }
496
497 return $locales[$tsLocale];
498 }
499}
500
501/**
502 * Short-named function for string translation, defined in global scope so it's available everywhere.
503 *
6a0b768e
TO
504 * @param $text
505 * String string for translating.
506 * @param $params
507 * Array an array of additional parameters.
6a488035 508 *
a6c01b45
CW
509 * @return string
510 * the translated string
6a488035
TO
511 */
512function ts($text, $params = array()) {
513 static $config = NULL;
514 static $locale = NULL;
515 static $i18n = NULL;
516 static $function = NULL;
517
518 if ($text == '') {
519 return '';
520 }
521
522 if (!$config) {
523 $config = CRM_Core_Config::singleton();
524 }
525
526 global $tsLocale;
527 if (!$i18n or $locale != $tsLocale) {
528 $i18n = CRM_Core_I18n::singleton();
529 $locale = $tsLocale;
530 if (isset($config->customTranslateFunction) and function_exists($config->customTranslateFunction)) {
531 $function = $config->customTranslateFunction;
532 }
533 }
534
535 if ($function) {
536 return $function($text, $params);
537 }
538 else {
539 return $i18n->crm_translate($text, $params);
540 }
541}