CRM-16373 - Simplify loading of enableComponents
[civicrm-core.git] / CRM / Core / I18n.php
CommitLineData
6a488035
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
7e9e8871 4 | CiviCRM version 4.7 |
6a488035 5 +--------------------------------------------------------------------+
e7112fa7 6 | Copyright CiviCRM LLC (c) 2004-2015 |
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 +--------------------------------------------------------------------+
d25dd0ee 26 */
6a488035
TO
27
28/**
29 *
30 * @package CRM
e7112fa7 31 * @copyright CiviCRM LLC (c) 2004-2015
6a488035
TO
32 */
33class CRM_Core_I18n {
34
35 /**
2ba175b6
DL
36 * A PHP-gettext instance for string translation;
37 * should stay null if the strings are not to be translated (en_US).
6a488035
TO
38 */
39 private $_phpgettext = NULL;
40
41 /**
42 * Whether we are using native gettext or not.
43 */
44 private $_nativegettext = FALSE;
45
74eb462f
ML
46 /**
47 * Gettext cache for extension domains/streamers, depending on if native or phpgettext.
48 * - native gettext: we cache the value for textdomain()
49 * - phpgettext: we cache the file streamer.
50 */
51 private $_extensioncache = array();
52
6a488035
TO
53 /**
54 * A locale-based constructor that shouldn't be called from outside of this class (use singleton() instead).
55 *
5a4f6742
CW
56 * @param string $locale
57 * the base of this certain object's existence.
6a488035 58 *
77b97be7 59 * @return \CRM_Core_I18n
6a488035 60 */
00be9182 61 public function __construct($locale) {
6a488035
TO
62 if ($locale != '' and $locale != 'en_US') {
63 $config = CRM_Core_Config::singleton();
64
65 if (defined('CIVICRM_GETTEXT_NATIVE') && CIVICRM_GETTEXT_NATIVE && function_exists('gettext')) {
66 // Note: the file hierarchy for .po must be, for example: l10n/fr_FR/LC_MESSAGES/civicrm.mo
67
68 $this->_nativegettext = TRUE;
69
70 $locale .= '.utf8';
71 putenv("LANG=$locale");
72
73 // CRM-11833 Avoid LC_ALL because of LC_NUMERIC and potential DB error.
74 setlocale(LC_TIME, $locale);
75 setlocale(LC_MESSAGES, $locale);
76 setlocale(LC_CTYPE, $locale);
77
4d8ce7f0 78 bindtextdomain('civicrm', CRM_Core_I18n::getResourceDir());
6a488035
TO
79 bind_textdomain_codeset('civicrm', 'UTF-8');
80 textdomain('civicrm');
81
82 $this->_phpgettext = new CRM_Core_I18n_NativeGettext();
74eb462f 83 $this->_extensioncache['civicrm'] = 'civicrm';
6a488035
TO
84 return;
85 }
86
87 // Otherwise, use PHP-gettext
b395e3c0
ML
88 // we support both the old file hierarchy format and the new:
89 // pre-4.5: civicrm/l10n/xx_XX/civicrm.mo
90 // post-4.5: civicrm/l10n/xx_XX/LC_MESSAGES/civicrm.mo
6a488035
TO
91 require_once 'PHPgettext/streams.php';
92 require_once 'PHPgettext/gettext.php';
93
4d8ce7f0 94 $mo_file = CRM_Core_I18n::getResourceDir() . $locale . DIRECTORY_SEPARATOR . 'LC_MESSAGES' . DIRECTORY_SEPARATOR . 'civicrm.mo';
b395e3c0 95
353ffa53 96 if (!file_exists($mo_file)) {
b395e3c0 97 // fallback to pre-4.5 mode
4d8ce7f0 98 $mo_file = CRM_Core_I18n::getResourceDir() . $locale . DIRECTORY_SEPARATOR . 'civicrm.mo';
b395e3c0
ML
99 }
100
101 $streamer = new FileReader($mo_file);
6a488035 102 $this->_phpgettext = new gettext_reader($streamer);
74eb462f 103 $this->_extensioncache['civicrm'] = $this->_phpgettext;
6a488035
TO
104 }
105 }
106
107 /**
108 * Returns whether gettext is running natively or using PHP-Gettext.
109 *
a6c01b45
CW
110 * @return bool
111 * True if gettext is native
6a488035 112 */
00be9182 113 public function isNative() {
6a488035
TO
114 return $this->_nativegettext;
115 }
116
117 /**
118 * Return languages available in this instance of CiviCRM.
119 *
5a4f6742
CW
120 * @param bool $justEnabled
121 * whether to return all languages or just the enabled ones.
6a488035 122 *
a6c01b45 123 * @return array
16b10e64 124 * Array of code/language name mappings
6a488035 125 */
00be9182 126 public static function languages($justEnabled = FALSE) {
6a488035
TO
127 static $all = NULL;
128 static $enabled = NULL;
129
130 if (!$all) {
c0c9cd82 131 $all = CRM_Contact_BAO_Contact::buildOptions('preferred_language');
6a488035
TO
132
133 // check which ones are available; add them to $all if not there already
134 $config = CRM_Core_Config::singleton();
135 $codes = array();
4d8ce7f0 136 if (is_dir(CRM_Core_I18n::getResourceDir()) && $dir = opendir(CRM_Core_I18n::getResourceDir())) {
6a488035
TO
137 while ($filename = readdir($dir)) {
138 if (preg_match('/^[a-z][a-z]_[A-Z][A-Z]$/', $filename)) {
139 $codes[] = $filename;
140 if (!isset($all[$filename])) {
141 $all[$filename] = $filename;
142 }
143 }
144 }
145 closedir($dir);
146 }
147
148 // drop the unavailable languages (except en_US)
149 foreach (array_keys($all) as $code) {
150 if ($code == 'en_US') {
151 continue;
152 }
408b79bf 153 if (!in_array($code, $codes)) {
154 unset($all[$code]);
155 }
6a488035
TO
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.
6a488035 179 *
a6c01b45
CW
180 * @return string
181 * modified string
6a488035 182 */
00be9182 183 public function strarg($str) {
6a488035
TO
184 $tr = array();
185 $p = 0;
186 for ($i = 1; $i < func_num_args(); $i++) {
187 $arg = func_get_arg($i);
188 if (is_array($arg)) {
189 foreach ($arg as $aarg) {
190 $tr['%' . ++$p] = $aarg;
191 }
192 }
193 else {
194 $tr['%' . ++$p] = $arg;
195 }
196 }
197 return strtr($str, $tr);
198 }
199
4d8ce7f0
TO
200 public static function getResourceDir() {
201 static $dir = NULL;
202 if ($dir === NULL) {
203 $dir = dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR . 'l10n' . DIRECTORY_SEPARATOR;
204 }
205 return $dir;
206 }
207
6a488035
TO
208 /**
209 * Smarty block function, provides gettext support for smarty.
210 *
211 * The block content is the text that should be translated.
212 *
213 * Any parameter that is sent to the function will be represented as %n in the translation text,
214 * where n is 1 for the first parameter. The following parameters are reserved:
215 * - escape - sets escape mode:
216 * - 'html' for HTML escaping, this is the default.
217 * - 'js' for javascript escaping.
218 * - 'no'/'off'/0 - turns off escaping
219 * - plural - The plural version of the text (2nd parameter of ngettext())
220 * - count - The item count for plural mode (3rd parameter of ngettext())
221 * - context - gettext context of that string (for homonym handling)
222 *
5a4f6742
CW
223 * @param string $text
224 * the original string.
225 * @param array $params
3f3bba82
TO
226 * The params of the translation (if any).
227 * - domain: string|array a list of translation domains to search (in order)
228 * - context: string
e3b7fc11 229 *
a6c01b45
CW
230 * @return string
231 * the translated string
6a488035 232 */
00be9182 233 public function crm_translate($text, $params = array()) {
6a488035
TO
234 if (isset($params['escape'])) {
235 $escape = $params['escape'];
236 unset($params['escape']);
237 }
238
239 // sometimes we need to {ts}-tag a string, but don’t want to
240 // translate it in the template (like civicrm_navigation.tpl),
241 // because we handle the translation in a different way (CRM-6998)
242 // in such cases we return early, only doing SQL/JS escaping
243 if (isset($params['skip']) and $params['skip']) {
244 if (isset($escape) and ($escape == 'sql')) {
74032946 245 $text = CRM_Core_DAO::escapeString($text);
6a488035
TO
246 }
247 if (isset($escape) and ($escape == 'js')) {
248 $text = addcslashes($text, "'");
249 }
250 return $text;
251 }
252
3f3bba82 253 $plural = $count = NULL;
6a488035
TO
254 if (isset($params['plural'])) {
255 $plural = $params['plural'];
256 unset($params['plural']);
257 if (isset($params['count'])) {
258 $count = $params['count'];
259 }
260 }
261
262 if (isset($params['context'])) {
263 $context = $params['context'];
264 unset($params['context']);
265 }
266 else {
267 $context = NULL;
268 }
269
3f3bba82
TO
270 if (isset($params['domain'])) {
271 $domain = $params['domain'];
272 unset($params['domain']);
273 }
274 else {
275 $domain = NULL;
276 }
277
1b4710da
TO
278 $raw = !empty($params['raw']);
279 unset($params['raw']);
280
3f3bba82
TO
281 if (!empty($domain)) {
282 // It might be prettier to cast to an array, but this is high-traffic stuff.
283 if (is_array($domain)) {
284 foreach ($domain as $d) {
285 $candidate = $this->crm_translate_raw($text, $d, $count, $plural, $context);
286 if ($candidate != $text) {
287 $text = $candidate;
288 break;
289 }
290 }
291 }
292 else {
293 $text = $this->crm_translate_raw($text, $domain, $count, $plural, $context);
294 }
295 }
296 else {
297 $text = $this->crm_translate_raw($text, NULL, $count, $plural, $context);
298 }
299
300 // replace the numbered %1, %2, etc. params if present
1b4710da 301 if (count($params) && !$raw) {
3f3bba82
TO
302 $text = $this->strarg($text, $params);
303 }
304
305 // escape SQL if we were asked for it
306 if (isset($escape) and ($escape == 'sql')) {
307 $text = CRM_Core_DAO::escapeString($text);
308 }
309
310 // escape for JavaScript (if requested)
311 if (isset($escape) and ($escape == 'js')) {
312 $text = addcslashes($text, "'");
313 }
314
315 return $text;
316 }
317
318 /**
319 * Lookup the raw translation of a string (without any extra escaping or interpolation).
320 *
321 * @param string $text
322 * @param string|NULL $domain
323 * @param int|NULL $count
324 * @param string $plural
325 * @param string $context
e3b7fc11 326 *
327 * @return string
3f3bba82
TO
328 */
329 protected function crm_translate_raw($text, $domain, $count, $plural, $context) {
1a3cba0e
ML
330 // gettext domain for extensions
331 $domain_changed = FALSE;
3f3bba82
TO
332 if (!empty($domain) && $this->_phpgettext) {
333 if ($this->setGettextDomain($domain)) {
1a3cba0e
ML
334 $domain_changed = TRUE;
335 }
336 }
337
6a488035
TO
338 // do all wildcard translations first
339 $config = CRM_Core_Config::singleton();
6cf5bb6f
DL
340 $stringTable = CRM_Utils_Array::value(
341 $config->lcMessages,
6a488035
TO
342 $config->localeCustomStrings
343 );
344
345 $exactMatch = FALSE;
346 if (isset($stringTable['enabled']['exactMatch'])) {
347 foreach ($stringTable['enabled']['exactMatch'] as $search => $replace) {
348 if ($search === $text) {
349 $exactMatch = TRUE;
350 $text = $replace;
351 break;
352 }
353 }
354 }
355
6cf5bb6f
DL
356 if (
357 !$exactMatch &&
6a488035
TO
358 isset($stringTable['enabled']['wildcardMatch'])
359 ) {
353ffa53 360 $search = array_keys($stringTable['enabled']['wildcardMatch']);
6a488035 361 $replace = array_values($stringTable['enabled']['wildcardMatch']);
353ffa53 362 $text = str_replace($search, $replace, $text);
6a488035
TO
363 }
364
365 // dont translate if we've done exactMatch already
366 if (!$exactMatch) {
367 // use plural if required parameters are set
368 if (isset($count) && isset($plural)) {
369
370 if ($this->_phpgettext) {
371 $text = $this->_phpgettext->ngettext($text, $plural, $count);
372 }
373 else {
374 // if the locale's not set, we do ngettext work by hand
375 // if $count == 1 then $text = $text, else $text = $plural
376 if ($count != 1) {
377 $text = $plural;
378 }
379 }
380
381 // expand %count in translated string to $count
382 $text = strtr($text, array('%count' => $count));
383
384 // if not plural, but the locale's set, translate
385 }
386 elseif ($this->_phpgettext) {
387 if ($context) {
388 $text = $this->_phpgettext->pgettext($context, $text);
389 }
390 else {
391 $text = $this->_phpgettext->translate($text);
392 }
393 }
394 }
395
1a3cba0e
ML
396 if ($domain_changed) {
397 $this->setGettextDomain('civicrm');
398 }
399
6a488035
TO
400 return $text;
401 }
402
403 /**
404 * Translate a string to the current locale.
405 *
5a4f6742
CW
406 * @param string $string
407 * this string should be translated.
6a488035 408 *
a6c01b45
CW
409 * @return string
410 * the translated string
6a488035 411 */
00be9182 412 public function translate($string) {
6a488035
TO
413 return ($this->_phpgettext) ? $this->_phpgettext->translate($string) : $string;
414 }
415
416 /**
417 * Localize (destructively) array values.
418 *
5a4f6742
CW
419 * @param array $array
420 * the array for localization (in place).
421 * @param array $params
422 * an array of additional parameters.
6a488035 423 */
408b79bf 424 public function localizeArray(
6a488035
TO
425 &$array,
426 $params = array()
427 ) {
428 global $tsLocale;
429
430 if ($tsLocale == 'en_US') {
431 return;
432 }
433
434 foreach ($array as & $value) {
435 if ($value) {
436 $value = ts($value, $params);
437 }
438 }
439 }
440
441 /**
442 * Localize (destructively) array elements with keys of 'title'.
443 *
5a4f6742
CW
444 * @param array $array
445 * the array for localization (in place).
6a488035 446 */
00be9182 447 public function localizeTitles(&$array) {
6a488035
TO
448 foreach ($array as $key => $value) {
449 if (is_array($value)) {
450 $this->localizeTitles($value);
451 $array[$key] = $value;
452 }
453 elseif ((string ) $key == 'title') {
eb7d6f39 454 $array[$key] = ts($value, array('context' => 'menu'));
6a488035
TO
455 }
456 }
457 }
458
1a3cba0e
ML
459 /**
460 * Binds a gettext domain, wrapper over bindtextdomain().
461 *
6a0b768e
TO
462 * @param $key
463 * Key of the extension (can be 'civicrm', or 'org.example.foo').
1a3cba0e 464 *
408b79bf 465 * @return Bool
a6c01b45 466 * True if the domain was changed for an extension.
1a3cba0e 467 */
00be9182 468 public function setGettextDomain($key) {
82bcff63 469 /* No domain changes for en_US */
353ffa53 470 if (!$this->_phpgettext) {
82bcff63
ML
471 return FALSE;
472 }
1a3cba0e 473
74eb462f 474 // It's only necessary to find/bind once
353ffa53 475 if (!isset($this->_extensioncache[$key])) {
1a3cba0e
ML
476 $config = CRM_Core_Config::singleton();
477
478 try {
479 $mapper = CRM_Extension_System::singleton()->getMapper();
480 $path = $mapper->keyToBasePath($key);
481 $info = $mapper->keyToInfo($key);
482 $domain = $info->file;
483
74eb462f
ML
484 if ($this->_nativegettext) {
485 bindtextdomain($domain, $path . DIRECTORY_SEPARATOR . 'l10n');
486 bind_textdomain_codeset($domain, 'UTF-8');
487 $this->_extensioncache[$key] = $domain;
488 }
489 else {
490 // phpgettext
491 $mo_file = $path . DIRECTORY_SEPARATOR . 'l10n' . DIRECTORY_SEPARATOR . $config->lcMessages . DIRECTORY_SEPARATOR . 'LC_MESSAGES' . DIRECTORY_SEPARATOR . $domain . '.mo';
492 $streamer = new FileReader($mo_file);
493 $this->_extensioncache[$key] = new gettext_reader($streamer);
494 }
1a3cba0e
ML
495 }
496 catch (CRM_Extension_Exception $e) {
b44e3f84 497 // Intentionally not translating this string to avoid possible infinite loops
74eb462f
ML
498 // Only developers should see this string, if they made a mistake in their ts() usage.
499 CRM_Core_Session::setStatus('Unknown extension key in a translation string: ' . $key, '', 'error');
500 $this->_extensioncache[$key] = FALSE;
1a3cba0e
ML
501 }
502 }
503
74eb462f
ML
504 if (isset($this->_extensioncache[$key]) && $this->_extensioncache[$key]) {
505 if ($this->_nativegettext) {
506 textdomain($this->_extensioncache[$key]);
507 }
508 else {
509 $this->_phpgettext = $this->_extensioncache[$key];
510 }
511
512 return TRUE;
1a3cba0e 513 }
74eb462f
ML
514
515 return FALSE;
1a3cba0e
ML
516 }
517
6a488035
TO
518 /**
519 * Static instance provider - return the instance for the current locale.
c4c311c1
CW
520 *
521 * @return CRM_Core_I18n
6a488035 522 */
00be9182 523 public static function &singleton() {
6a488035
TO
524 static $singleton = array();
525
526 global $tsLocale;
527 if (!isset($singleton[$tsLocale])) {
528 $singleton[$tsLocale] = new CRM_Core_I18n($tsLocale);
529 }
530
531 return $singleton[$tsLocale];
532 }
533
534 /**
535 * Set the LC_TIME locale if it's not set already (for a given language choice).
536 *
a6c01b45
CW
537 * @return string
538 * the final LC_TIME that got set
6a488035 539 */
00be9182 540 public static function setLcTime() {
6a488035
TO
541 static $locales = array();
542
543 global $tsLocale;
544 if (!isset($locales[$tsLocale])) {
545 // with the config being set to pl_PL: try pl_PL.UTF-8,
546 // then pl_PL, if neither present fall back to C
547 $locales[$tsLocale] = setlocale(LC_TIME, $tsLocale . '.UTF-8', $tsLocale, 'C');
548 }
549
550 return $locales[$tsLocale];
551 }
96025800 552
9747df8a 553 /**
554 * Get the default language for contacts where no language is provided.
555 *
556 * Note that NULL is a valid option so be careful with checking for empty etc.
557 *
558 * NULL would mean 'we don't know & we don't want to hazard a guess'.
559 *
560 * @return string
561 */
562 public static function getContactDefaultLanguage() {
563 $language = CRM_Core_BAO_Setting::getItem(CRM_Core_BAO_Setting::LOCALIZATION_PREFERENCES_NAME, 'contact_default_language');
564 if ($language == 'undefined') {
565 return NULL;
566 }
567 if (empty($language) || $language === '*default*') {
568 $language = civicrm_api3('setting', 'getvalue', array(
569 'name' => 'lcMessages',
570 'group' => CRM_Core_BAO_Setting::LOCALIZATION_PREFERENCES_NAME,
571 ));
572 }
573 elseif ($language == 'current_site_language') {
574 global $tsLocale;
575 return $tsLocale;
576 }
577
578 return $language;
579 }
580
6a488035
TO
581}
582
583/**
584 * Short-named function for string translation, defined in global scope so it's available everywhere.
585 *
e3b7fc11 586 * @param string $text
6a0b768e 587 * String string for translating.
16b10e64 588 * @param array $params
6a0b768e 589 * Array an array of additional parameters.
6a488035 590 *
a6c01b45 591 * @return string
353ffa53 592 * the translated string
6a488035
TO
593 */
594function ts($text, $params = array()) {
595 static $config = NULL;
596 static $locale = NULL;
597 static $i18n = NULL;
598 static $function = NULL;
599
600 if ($text == '') {
601 return '';
602 }
603
604 if (!$config) {
605 $config = CRM_Core_Config::singleton();
606 }
607
608 global $tsLocale;
609 if (!$i18n or $locale != $tsLocale) {
610 $i18n = CRM_Core_I18n::singleton();
611 $locale = $tsLocale;
612 if (isset($config->customTranslateFunction) and function_exists($config->customTranslateFunction)) {
613 $function = $config->customTranslateFunction;
614 }
615 }
616
617 if ($function) {
618 return $function($text, $params);
619 }
620 else {
621 return $i18n->crm_translate($text, $params);
622 }
623}