2 if(!defined('sugarEntry') || !sugarEntry) die('Not A Valid Entry Point');
3 /*********************************************************************************
4 * SugarCRM Community Edition is a customer relationship management program developed by
5 * SugarCRM, Inc. Copyright (C) 2004-2012 SugarCRM Inc.
7 * This program is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU Affero General Public License version 3 as published by the
9 * Free Software Foundation with the addition of the following permission added
10 * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
11 * IN WHICH THE COPYRIGHT IS OWNED BY SUGARCRM, SUGARCRM DISCLAIMS THE WARRANTY
12 * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
14 * This program is distributed in the hope that it will be useful, but WITHOUT
15 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
16 * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
19 * You should have received a copy of the GNU Affero General Public License along with
20 * this program; if not, see http://www.gnu.org/licenses or write to the Free
21 * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
24 * You can contact SugarCRM, Inc. headquarters at 10050 North Wolfe Road,
25 * SW2-130, Cupertino, CA 95014, USA. or at email address contact@sugarcrm.com.
27 * The interactive user interfaces in modified source and object code versions
28 * of this program must display Appropriate Legal Notices, as required under
29 * Section 5 of the GNU Affero General Public License version 3.
31 * In accordance with Section 7(b) of the GNU Affero General Public License version 3,
32 * these Appropriate Legal Notices must retain the display of the "Powered by
33 * SugarCRM" logo. If the display of the logo is not reasonably feasible for
34 * technical reasons, the Appropriate Legal Notices must display the words
35 * "Powered by SugarCRM".
36 ********************************************************************************/
39 * Localization manager
43 var $availableCharsets = array(
44 'BIG-5', //Taiwan and Hong Kong
45 /*'CP866' // ms-dos Cyrillic */
46 /*'CP949' //Microsoft Korean */
47 'CP1251', //MS Cyrillic
48 'CP1252', //MS Western European & US
49 'EUC-CN', //Simplified Chinese GB2312
50 'EUC-JP', //Unix Japanese
53 'ISO-2022-JP', //Japanese
54 'ISO-2022-KR', //Korean
55 'ISO-8859-1', //Western European and US
56 'ISO-8859-2', //Central and Eastern European
57 'ISO-8859-3', //Latin 3
58 'ISO-8859-4', //Latin 4
59 'ISO-8859-5', //Cyrillic
60 'ISO-8859-6', //Arabic
62 'ISO-8859-8', //Hebrew
63 'ISO-8859-9', //Latin 5
64 'ISO-8859-10', //Latin 6
65 'ISO-8859-13', //Latin 7
66 'ISO-8859-14', //Latin 8
67 'ISO-8859-15', //Latin 9
68 'KOI8-R', //Cyrillic Russian
69 'KOI8-U', //Cyrillic Ukranian
73 var $localeNameFormat;
74 var $localeNameFormatDefault;
75 var $default_export_charset = 'UTF-8';
76 var $default_email_charset = 'UTF-8';
77 var $currencies = array(); // array loaded with current currencies
78 var $invalidNameFormatUpgradeFilename = 'upgradeInvalidLocaleNameFormat.php';
79 /* Charset mappings for iconv */
80 var $iconvCharsetMap = array(
81 'KS_C_5601-1987' => 'CP949',
82 'ISO-8859-8-I' => 'ISO-8859-8'
88 function Localization() {
90 $this->localeNameFormatDefault = empty($sugar_config['locale_name_format_default']) ? 's f l' : $sugar_config['default_name_format'];
91 $this->loadCurrencies();
95 * returns an array of Sugar Config defaults that are determined by locale settings
98 function getLocaleConfigDefaults() {
99 $coreDefaults = array(
103 'default_currency_significant_digits' => 2,
104 'default_currency_symbol' => '$',
105 'default_export_charset' => $this->default_export_charset,
106 'default_locale_name_format' => 's f l',
107 'name_formats' => array('s f l' => 's f l', 'f l' => 'f l', 's l' => 's l', 'l, s f' => 'l, s f',
108 'l, f' => 'l, f', 's l, f' => 's l, f', 'l s f' => 'l s f', 'l f s' => 'l f s'),
109 'default_number_grouping_seperator' => ',',
110 'default_decimal_seperator' => '.',
111 'export_delimiter' => ',',
112 'default_email_charset' => $this->default_email_charset,
115 return $coreDefaults;
119 * abstraction of precedence
120 * @param string prefName Name of preference to retrieve based on overrides
121 * @param object user User in focus, default null (current_user)
122 * @return string pref Most significant preference
124 function getPrecedentPreference($prefName, $user=null, $sugarConfigPrefName = '') {
125 global $current_user;
126 global $sugar_config;
129 $coreDefaults = $this->getLocaleConfigDefaults();
130 $pref = isset($coreDefaults[$prefName]) ? $coreDefaults[$prefName] : ''; // defaults, even before config.php
133 $userPref = $user->getPreference($prefName);
134 } elseif(!empty($current_user)) {
135 $userPref = $current_user->getPreference($prefName);
137 // Bug 39171 - If we are asking for default_email_charset, check in emailSettings['defaultOutboundCharset'] as well
138 if ( $prefName == 'default_email_charset' ) {
140 $emailSettings = $user->getPreference('emailSettings', 'Emails');
141 } elseif(!empty($current_user)) {
142 $emailSettings = $current_user->getPreference('emailSettings', 'Emails');
144 if ( isset($emailSettings['defaultOutboundCharset']) ) {
145 $userPref = $emailSettings['defaultOutboundCharset'];
149 // set fallback defaults defined in this class
150 if(isset($this->$prefName)) {
151 $pref = $this->$prefName;
153 //rrs: 33086 - give the ability to pass in the preference name as stored in $sugar_config.
154 if(!empty($sugarConfigPrefName)){
155 $prefName = $sugarConfigPrefName;
157 // cn: 9549 empty() call on a value of 0 (0 significant digits) resulted in a false-positive. changing to "isset()"
158 $pref = (!isset($sugar_config[$prefName]) || (empty($sugar_config[$prefName]) && $sugar_config[$prefName] !== '0')) ? $pref : $sugar_config[$prefName];
159 $pref = (empty($userPref) && $userPref !== '0') ? $pref : $userPref;
163 ///////////////////////////////////////////////////////////////////////////
164 //// CURRENCY HANDLING
166 * wrapper for whatever currency system we implement
168 function loadCurrencies() {
169 // doing it dirty here
171 global $sugar_config;
177 $load = sugar_cache_retrieve('currency_list');
178 if ( !is_array($load) ) {
179 // load default from config.php
180 $this->currencies['-99'] = array(
181 'name' => $sugar_config['default_currency_name'],
182 'symbol' => $sugar_config['default_currency_symbol'],
183 'conversion_rate' => 1
186 $q = "SELECT id, name, symbol, conversion_rate FROM currencies WHERE status = 'Active' and deleted = 0";
189 while($a = $db->fetchByAssoc($r)) {
191 $load['name'] = $a['name'];
192 $load['symbol'] = $a['symbol'];
193 $load['conversion_rate'] = $a['conversion_rate'];
195 $this->currencies[$a['id']] = $load;
197 sugar_cache_put('currency_list',$this->currencies);
199 $this->currencies = $load;
206 * getter for currencies array
207 * @return array $this->currencies returns array( id => array(name => X, etc
209 function getCurrencies() {
210 return $this->currencies;
214 * retrieves default OOTB currencies for sugar_config and installer.
215 * @return array ret Array of default currencies keyed by ISO4217 code
217 function getDefaultCurrencies() {
219 'AUD' => array( 'name' => 'Australian Dollars',
222 'BRL' => array( 'name' => 'Brazilian Reais',
225 'GBP' => array( 'name' => 'British Pounds',
228 'CAD' => array( 'name' => 'Canadian Dollars',
231 'CNY' => array( 'name' => 'Chinese Yuan',
234 'EUR' => array( 'name' => 'Euro',
237 'HKD' => array( 'name' => 'Hong Kong Dollars',
240 'INR' => array( 'name' => 'Indian Rupees',
243 'KRW' => array( 'name' => 'Korean Won',
246 'YEN' => array( 'name' => 'Japanese Yen',
249 'MXM' => array( 'name' => 'Mexican Pesos',
252 'SGD' => array( 'name' => 'Singaporean Dollars',
255 'CHF' => array( 'name' => 'Swiss Franc',
258 'THB' => array( 'name' => 'Thai Baht',
261 'USD' => array( 'name' => 'US Dollars',
268 //// END CURRENCY HANDLING
269 ///////////////////////////////////////////////////////////////////////////
272 ///////////////////////////////////////////////////////////////////////////
273 //// CHARSET TRANSLATION
275 * returns a mod|app_strings array in the target charset
276 * @param array strings $mod_string, et.al.
277 * @param string charset Target charset
278 * @return array Translated string pack
280 function translateStringPack($strings, $charset) {
282 foreach($strings as $k => $v) {
284 $strings[$k] = $this->translateStringPack($v, $charset);
286 $strings[$k] = $this->translateCharset($v, 'UTF-8', $charset);
294 * translates the passed variable for email sending (export)
295 * @param mixed the var (array or string) to translate
296 * @return mixed the translated variable
298 function translateForEmail($var) {
300 foreach($var as $k => $v) {
301 $var[$k] = $this->translateForEmail($v);
304 } elseif(!empty($var)) {
305 return $this->translateCharset($var, 'UTF-8', $this->getOutboundEmailCharset());
310 * prepares a bean for export by translating any text fields into the export
312 * @param bean object A SugarBean
313 * @return bean object The bean with translated strings
315 function prepBeanForExport($bean)
317 foreach($bean->field_defs as $k => $field)
319 if (is_string($bean->$k))
321 // $bean->$k = $this->translateCharset($bean->$k, 'UTF-8', $this->getExportCharset());
333 * translates a character set from one encoding to another encoding
334 * @param string string the string to be translated
335 * @param string fromCharset the charset the string is currently in
336 * @param string toCharset the charset to translate into (defaults to UTF-8)
337 * @param bool forceIconv force using the iconv library instead of mb_string
338 * @return string the translated string
340 function translateCharset($string, $fromCharset, $toCharset='UTF-8', $forceIconv = false)
342 $GLOBALS['log']->debug("Localization: translating [{$string}] from {$fromCharset} into {$toCharset}");
344 // Bug #35413 Function has to use iconv if $fromCharset is not in mb_list_encodings
345 $isMb = function_exists('mb_convert_encoding') && !$forceIconv;
346 $isIconv = function_exists('iconv');
349 $fromCharset = strtoupper($fromCharset);
350 $listEncodings = mb_list_encodings();
352 foreach ($listEncodings as $encoding)
354 if (strtoupper($encoding) == $fromCharset)
365 return mb_convert_encoding($string, $toCharset, $fromCharset);
369 $newFromCharset = $fromCharset;
370 if (isset($this->iconvCharsetMap[$fromCharset])) {
371 $newFromCharset = $this->iconvCharsetMap[$fromCharset];
372 $GLOBALS['log']->debug("Localization: iconv using charset {$newFromCharset} instead of {$fromCharset}");
374 $newToCharset = $toCharset;
375 if (isset($this->iconvCharsetMap[$toCharset])) {
376 $newToCharset = $this->iconvCharsetMap[$toCharset];
377 $GLOBALS['log']->debug("Localization: iconv using charset {$newToCharset} instead of {$toCharset}");
379 return iconv($newFromCharset, $newToCharset, $string);
388 * translates a character set from one to another, and the into MIME-header friendly format
390 function translateCharsetMIME($string, $fromCharset, $toCharset='UTF-8', $encoding="Q") {
391 $previousEncoding = mb_internal_encoding();
392 mb_internal_encoding($toCharset);
393 $result = mb_encode_mimeheader($string, $toCharset, $encoding);
394 mb_internal_encoding($previousEncoding);
398 function normalizeCharset($charset) {
399 $charset = strtolower(preg_replace("/[\-\_]*/", "", $charset));
404 * returns an array of charsets with keys for available translations; appropriate for get_select_options_with_id()
406 function getCharsetSelect() {
407 //jc:12293 - the "labels" or "human-readable" representations of the various charsets
408 //should be translatable
409 $translated = array();
410 foreach($this->availableCharsets as $key)
412 //$translated[$key] = translate($value);
413 $translated[$key] = translate($key);
421 * returns the charset preferred in descending order: User, Sugar Config, DEFAULT
422 * @param string charset to override ALL, pass a valid charset here
423 * @return string charset the chosen character set
425 function getExportCharset($charset='', $user=null) {
426 $charset = $this->getPrecedentPreference('default_export_charset', $user);
431 * returns the charset preferred in descending order: User, Sugar Config, DEFAULT
432 * @return string charset the chosen character set
434 function getOutboundEmailCharset($user=null) {
435 $charset = $this->getPrecedentPreference('default_email_charset', $user);
438 //// END CHARSET TRANSLATION
439 ///////////////////////////////////////////////////////////////////////////
441 ///////////////////////////////////////////////////////////////////////////
442 //// NUMBER DISPLAY FORMATTING CODE
443 function getDecimalSeparator($user=null) {
444 // Bug50887 this is purposefully misspelled as ..._seperator to match the way it's defined throughout the app.
445 $dec = $this->getPrecedentPreference('default_decimal_seperator', $user);
449 function getNumberGroupingSeparator($user=null) {
450 $sep = $this->getPrecedentPreference('default_number_grouping_seperator', $user);
454 function getPrecision($user=null) {
455 $precision = $this->getPrecedentPreference('default_currency_significant_digits', $user);
459 function getCurrencySymbol($user=null) {
460 $dec = $this->getPrecedentPreference('default_currency_symbol', $user);
465 * returns a number formatted by user preference or system default
466 * @param string number Number to be formatted and returned
467 * @param string currencySymbol Currency symbol if override is necessary
468 * @param bool is_currency Flag to also return the currency symbol
469 * @return string Formatted number
471 function getLocaleFormattedNumber($number, $currencySymbol='', $is_currency=true, $user=null) {
475 $dec = $this->getDecimalSeparator($user);
476 $thou = $this->getNumberGroupingSeparator($user);
477 $precision = $this->getPrecision($user);
478 $symbol = empty($currencySymbol) ? $this->getCurrencySymbol($user) : $currencySymbol;
480 $exNum = explode($dec, $number);
482 if(is_array($exNum) && count($exNum) > 0) {
483 if(strlen($exNum[0]) > 3) {
484 $offset = strlen($exNum[0]) % 3;
486 for($i=0; $i<$offset; $i++) {
487 $majorDigits .= $exNum[0]{$i};
492 for($i=$offset; $i<strlen($exNum[0]); $i++) {
493 if($tic % 3 == 0 && $i != 0) {
494 $majorDigits .= $thou; // add separator
497 $majorDigits .= $exNum[0]{$i};
501 $majorDigits = $exNum[0]; // no formatting needed
503 $fnum = $majorDigits;
507 if($precision > 0) { // we toss the minor digits otherwise
508 if(is_array($exNum) && isset($exNum[1])) {
515 $fnum = $symbol.$fnum;
521 * returns Javascript to format numbers and currency for ***DISPLAY***
523 function getNumberJs() {
526 var exampleDigits = '123456789.000000';
528 // round parameter can be negative for decimal, precision has to be postive
529 function formatNumber(n, sep, dec, precision) {
532 var formattedMajor = '';
533 var formattedMinor = '';
535 var nArray = n.split('.');
536 majorDigits = nArray[0];
537 if(nArray.length < 2) {
540 minorDigits = nArray[1];
545 var strlength = majorDigits.length;
548 var offset = strlength % 3; // find how many to lead off by
550 for(j=0; j<offset; j++) {
551 formattedMajor += majorDigits[j];
555 for(i=offset; i<strlength; i++) {
556 if(tic % 3 == 0 && i != 0)
557 formattedMajor += sep;
559 formattedMajor += majorDigits.substr(i,1);
564 formattedMajor = majorDigits; // no grouping marker
567 // handle decimal precision
569 for(i=0; i<precision; i++) {
570 if(minorDigits[i] != undefined)
571 formattedMinor += minorDigits[i];
573 formattedMinor += '0';
576 // we're just returning the major digits, no decimal marker
577 dec = ''; // just in case
580 return formattedMajor + dec + formattedMinor;
583 function setSigDigits() {
584 var sym = document.getElementById('symbol').value;
585 var thou = document.getElementById('default_number_grouping_seperator').value;
586 var dec = document.getElementById('default_decimal_seperator').value;
587 var precision = document.getElementById('sigDigits').value;
588 //umber(n, num_grp_sep, dec_sep, round, precision)
589 var newNumber = sym + formatNumber(exampleDigits, thou, dec, precision, precision);
590 document.getElementById('sigDigitsExample').value = newNumber;
596 //// END NUMBER DISPLAY FORMATTING CODE
597 ///////////////////////////////////////////////////////////////////////////
599 ///////////////////////////////////////////////////////////////////////////
600 //// NAME DISPLAY FORMATTING CODE
602 * get's the Name format macro string, preferring $current_user
603 * @return string format Name Format macro for locale
605 function getLocaleFormatMacro($user=null) {
606 $returnFormat = $this->getPrecedentPreference('default_locale_name_format', $user);
607 return $returnFormat;
611 * returns formatted name according to $current_user's locale settings
613 * @param string firstName
614 * @param string lastName
615 * @param string salutation
616 * @param string title
617 * @param string format If a particular format is desired, then pass this optional parameter as a simple string.
618 * sfl is "Salutation FirstName LastName", "l, f s" is "LastName[comma][space]FirstName[space]Salutation"
619 * @param object user object
620 * @param bool returnEmptyStringIfEmpty true if we should return back an empty string rather than a single space
621 * when the formatted name would be blank
622 * @return string formattedName
624 function getLocaleFormattedName($firstName, $lastName, $salutationKey='', $title='', $format="", $user=null, $returnEmptyStringIfEmpty = false) {
625 global $current_user;
626 global $app_list_strings;
628 if ( $user == null ) {
629 $user = $current_user;
632 $salutation = $salutationKey;
633 if(!empty($salutationKey) && !empty($app_list_strings['salutation_dom'][$salutationKey])) {
634 $salutation = (!empty($app_list_strings['salutation_dom'][$salutationKey]) ? $app_list_strings['salutation_dom'][$salutationKey] : $salutationKey);
637 //check to see if passed in variables are set, if so, then populate array with value,
638 //if not, then populate array with blank ''
640 $names['f'] = (empty($firstName) && $firstName != 0) ? '' : $firstName;
641 $names['l'] = (empty($lastName) && $lastName != 0) ? '' : $lastName;
642 $names['s'] = (empty($salutation) && $salutation != 0) ? '' : $salutation;
643 $names['t'] = (empty($title) && $title != 0) ? '' : $title;
645 //Bug: 39936 - if all of the inputs are empty, then don't try to format the name.
647 foreach($names as $key => $val){
654 return $returnEmptyStringIfEmpty ? '' : ' ';
659 $this->localeNameFormat = $this->getLocaleFormatMacro($user);
661 $this->localeNameFormat = $format;
664 // parse localeNameFormat
666 for($i=0; $i<strlen($this->localeNameFormat); $i++) {
667 $formattedName .= array_key_exists($this->localeNameFormat{$i}, $names) ? $names[$this->localeNameFormat{$i}] : $this->localeNameFormat{$i};
670 $formattedName = trim($formattedName);
671 if (strlen($formattedName)==0) {
672 return $returnEmptyStringIfEmpty ? '' : ' ';
675 if(strpos($formattedName,',',strlen($formattedName)-1)) { // remove trailing commas
676 $formattedName = substr($formattedName, 0, strlen($formattedName)-1);
678 return trim($formattedName);
682 * outputs some simple Javascript to show a preview of Name format in "My Account" and "Admin->Localization"
683 * @param string first First Name, use app_strings default if not specified
684 * @param string last Last Name, use app_strings default if not specified
685 * @param string salutation Saluation, use app_strings default if not specified
686 * @return string some Javascript
688 function getNameJs($first='', $last='', $salutation='', $title='') {
691 $salutation = !empty($salutation) ? $salutation : $app_strings['LBL_LOCALE_NAME_EXAMPLE_SALUTATION'];
692 $first = !empty($first) ? $first : $app_strings['LBL_LOCALE_NAME_EXAMPLE_FIRST'];
693 $last = !empty($last) ? $last : $app_strings['LBL_LOCALE_NAME_EXAMPLE_LAST'];
694 $title = !empty($title) ? $title : $app_strings['LBL_LOCALE_NAME_EXAMPLE_TITLE'];
697 function setPreview() {
698 format = document.getElementById('default_locale_name_format').value;
699 field = document.getElementById('nameTarget');
701 stuff = new Object();
703 stuff['s'] = '{$salutation}';
704 stuff['f'] = '{$first}';
705 stuff['l'] = '{$last}';
706 stuff['t'] = '{$title}';
709 for(i=0; i<format.length; i++) {
710 if(stuff[format.substr(i,1)] != undefined) {
711 name += stuff[format.substr(i,1)];
713 name += format.substr(i,1);
727 * Checks to see that the characters in $name_format are allowed: s, f, l, space/tab or punctuation
728 * @param $name_format
731 public function isAllowedNameFormat($name_format) {
732 // will result in a match as soon as a disallowed char is hit in $name_format
733 $match = preg_match('/[^sfl[:punct:][:^alnum:]\s]/', $name_format);
734 if ($match !== false && $match === 0) {
741 * Checks to see if there was an invalid Name Format encountered during the upgrade
742 * @return bool true if there was an invalid name, false if all went well.
744 public function invalidLocaleNameFormatUpgrade() {
745 return file_exists($this->invalidNameFormatUpgradeFilename);
749 * Creates the file that is created when there is an invalid name format during an upgrade
751 public function createInvalidLocaleNameFormatUpgradeNotice() {
752 $fh = fopen($this->invalidNameFormatUpgradeFilename,'w');
757 * Removes the file that is created when there is an invalid name format during an upgrade
759 public function removeInvalidLocaleNameFormatUpgradeNotice() {
760 if ($this->invalidLocaleNameFormatUpgrade()) {
761 unlink($this->invalidNameFormatUpgradeFilename);
767 * Creates dropdown items that have localized example names while filtering out invalid formats
769 * @param array un-prettied dropdown list
770 * @return array array of dropdown options
772 public function getUsableLocaleNameOptions($options) {
775 $examples = array('s' => $app_strings['LBL_LOCALE_NAME_EXAMPLE_SALUTATION'],
776 'f' => $app_strings['LBL_LOCALE_NAME_EXAMPLE_FIRST'],
777 'l' => $app_strings['LBL_LOCALE_NAME_EXAMPLE_LAST']);
779 foreach ($options as $key => $val) {
780 if ($this->isAllowedNameFormat($key) && $this->isAllowedNameFormat($val)) {
782 $pieces = str_split($val);
783 foreach ($pieces as $piece) {
784 if (isset($examples[$piece])) {
785 $newVal .= $examples[$piece];
790 $newOpts[$key] = $newVal;
795 //// END NAME DISPLAY FORMATTING CODE
796 ///////////////////////////////////////////////////////////////////////////
799 * Attempts to detect the charset used in the string
802 * @param $strict bool default false (use strict encoding?)
805 public function detectCharset($str, $strict=false)
807 if ( function_exists('mb_convert_encoding') )
808 return mb_detect_encoding($str,'ASCII,JIS,UTF-8,EUC-JP,SJIS,ISO-8859-1',$strict);