0011984: printing events with tags is broken for daysview
[tine20] / tine20 / Tinebase / Translation.php
1 <?php
2 /**
3  * Tine 2.0
4  *
5  * @package     Tinebase
6  * @subpackage  Translation
7  * @license     http://www.gnu.org/licenses/agpl.html AGPL3
8  * @copyright   Copyright (c) 2008-2011 Metaways Infosystems GmbH (http://www.metaways.de)
9  * @author      Lars Kneschke <l.kneschke@metaways.de>
10  */
11
12 /**
13  * primary class to handle translations
14  *
15  * @package     Tinebase
16  * @subpackage  Translation
17  */
18 class Tinebase_Translation
19 {
20     /**
21      * Lazy loading for {@see getCountryList()}
22      * 
23      * @var array
24      */
25     protected static $_countryLists = array();
26     
27     /**
28      * cached instances of Zend_Translate
29      * 
30      * @var array
31      */
32     protected static $_applicationTranslations = array();
33     
34     /**
35      * returns list of all available translations
36      * 
37      * NOTE available are those, having a Tinebase translation
38      * 
39      * @return array list of all available translation
40      *
41      * @todo add test
42      */
43     public static function getAvailableTranslations($appName = 'Tinebase')
44     {
45         $availableTranslations = array();
46
47         // look for po files in Tinebase 
48         $officialTranslationsDir = dirname(__FILE__) . "/../$appName/translations";
49         foreach(scandir($officialTranslationsDir) as $poFile) {
50             list ($localestring, $suffix) = explode('.', $poFile);
51             if ($suffix == 'po') {
52                 $availableTranslations[$localestring] = array(
53                     'path' => "$officialTranslationsDir/$poFile" 
54                 );
55             }
56         }
57         
58         // lookup/merge custom translations
59         if (Tinebase_Config::isReady() === TRUE) {
60             $logger = Tinebase_Core::getLogger();
61             $customTranslationsDir = Tinebase_Config::getInstance()->translations;
62             if ($customTranslationsDir) {
63                 foreach((array) @scandir($customTranslationsDir) as $dir) {
64                     $poFile = "$customTranslationsDir/$dir/$appName/translations/$dir.po";
65                     if (is_readable($poFile)) {
66                         $availableTranslations[$dir] = array(
67                             'path' => $poFile
68                         );
69                     }
70                 }
71             }
72         } else {
73             $logger = null;
74         }
75         
76         $filesToWatch = array();
77         
78         // compute information
79         foreach ($availableTranslations as $localestring => $info) {
80             if (! Zend_Locale::isLocale($localestring, TRUE, FALSE)) {
81                 if ($logger) {
82                     $logger->WARN(__METHOD__ . '::' . __LINE__ . " $localestring is not supported, removing translation form list");
83                 }
84                 unset($availableTranslations[$localestring]);
85                 continue;
86             }
87             
88             $filesToWatch[] = $info['path'];
89         }
90         
91         if (Tinebase_Config::isReady()) {
92             $cache = new Zend_Cache_Frontend_File(array(
93                 'master_files' => $filesToWatch
94             ));
95             $cache->setBackend(Tinebase_Core::get(Tinebase_Core::CACHE)->getBackend());
96         } else {
97             $cache = null;
98         }
99         
100         if ($cache) {
101             $cacheId = Tinebase_Helper::convertCacheId(__FUNCTION__ . $appName . sha1(serialize($filesToWatch)));
102             $cache = new Zend_Cache_Frontend_File(array(
103                 'master_files' => $filesToWatch
104             ));
105             $cache->setBackend(Tinebase_Core::get(Tinebase_Core::CACHE)->getBackend());
106             
107             if ($cachedTranslations = $cache->load($cacheId)) {
108                 $cachedTranslations = unserialize($cachedTranslations);
109                 
110                 if ($cachedTranslations !== null) {
111                     return $cachedTranslations;
112                 }
113             }
114         }
115         
116         // compute information
117         foreach ($availableTranslations as $localestring => $info) {
118             // fetch header grep for X-Poedit-Language, X-Poedit-Country
119             $fh = fopen($info['path'], 'r');
120             $header = fread($fh, 1024);
121             fclose($fh);
122             
123             preg_match('/X-Tine20-Language: (.+)(?:\\\\n?)(?:"?)/', $header, $language);
124             preg_match('/X-Tine20-Country: (.+)(?:\\\\n?)(?:"?)/', $header, $region);
125             
126             $locale = new Zend_Locale($localestring);
127             $availableTranslations[$localestring]['locale'] = $localestring;
128             $availableTranslations[$localestring]['language'] = isset($language[1]) ? 
129                 $language[1] : Zend_Locale::getTranslation($locale->getLanguage(), 'language', $locale);
130             $availableTranslations[$localestring]['region'] = isset($region[1]) ? 
131                 $region[1] : Zend_Locale::getTranslation($locale->getRegion(), 'country', $locale);
132         }
133
134         ksort($availableTranslations);
135         
136         if ($cache) {
137             $cache->save(serialize($availableTranslations), $cacheId, array(), /* 1 day */ 86400);
138         }
139         
140         return $availableTranslations;
141     }
142     
143     /**
144      * get list of translated country names
145      *
146      * @return array list of countrys
147      */
148     public static function getCountryList()
149     {
150         $locale = Tinebase_Core::get('locale');
151         $language = $locale->getLanguage();
152         
153         //try lazy loading of translated country list
154         if (empty(self::$_countryLists[$language])) {
155             $countries = Zend_Locale::getTranslationList('territory', $locale, 2);
156             asort($countries);
157             foreach($countries as $shortName => $translatedName) {
158                 $results[] = array(
159                     'shortName'         => $shortName, 
160                     'translatedName'    => $translatedName
161                 );
162             }
163     
164             self::$_countryLists[$language] = $results;
165         }
166
167         return array('results' => self::$_countryLists[$language]);
168     }
169     
170     /**
171      * Get translated country name for a given ISO {@param $_regionCode}
172      * 
173      * @param String $regionCode [e.g. DE, US etc.]
174      * @return String | null [e.g. Germany, United States etc.]
175      */
176     public static function getCountryNameByRegionCode($_regionCode)
177     {
178         $countries = self::getCountryList();
179         foreach($countries['results'] as $country) {
180             if ($country['shortName'] === $_regionCode) {
181                 return $country['translatedName'];
182             }
183         } 
184
185         return null;
186     }
187     
188     /**
189      * Get translated country name for a given ISO {@param $_regionCode}
190      * 
191      * @param String $regionCode [e.g. DE, US etc.]
192      * @return String | null [e.g. Germany, United States etc.]
193      */
194     public static function getRegionCodeByCountryName($_countryName)
195     {
196         $countries = self::getCountryList();
197         foreach($countries['results'] as $country) {
198             if ($country['translatedName'] === $_countryName) {
199                 return $country['shortName'];
200             }
201         } 
202
203         return null;
204     }
205     
206     /**
207      * gets a supported locale
208      *
209      * @param   string $_localeString
210      * @return  Zend_Locale
211      * @throws  Tinebase_Exception_NotFound
212      */
213     public static function getLocale($_localeString = 'auto')
214     {
215         Zend_Locale::$compatibilityMode = false;
216         
217         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " given localeString '$_localeString'");
218         try {
219             $locale = new Zend_Locale($_localeString);
220             
221             // check if we suppot the locale
222             $supportedLocales = array();
223             $availableTranslations = self::getAvailableTranslations();
224             foreach ($availableTranslations as $translation) {
225                 $supportedLocales[] = $translation['locale'];
226             }
227             
228             if (! in_array($_localeString, $supportedLocales)) {
229                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " '$locale' is not supported, checking fallback");
230                 
231                 // check if we find suiteable fallback
232                 $language = $locale->getLanguage();
233                 switch ($language) {
234                     case 'zh':
235                         $locale = new Zend_Locale('zh_CN');
236                         break;
237                     default: 
238                         if (in_array($language, $supportedLocales)) {
239                             $locale = new Zend_Locale($language);
240                         } else {
241                             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " no suiteable lang fallback found within this locales: " . print_r($supportedLocales, true) );
242                             throw new Tinebase_Exception_NotFound('No suiteable lang fallback found.');
243                         }
244                         break;
245                 }
246             }
247         } catch (Exception $e) {
248             if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . 
249                 ' ' . $e->getMessage() . ', falling back to locale en.');
250             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . 
251                 ' ' . $e->getTraceAsString());
252             $locale = new Zend_Locale('en');
253         }
254         
255         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " selected locale: '$locale'");
256         return $locale;
257     }
258     
259     /**
260      * get zend translate for an application
261      * 
262      * @param  string $_applicationName
263      * @param  Zend_Locale $_locale [optional]
264      * @return Zend_Translate
265      * 
266      * @todo return 'void' if locale = en
267     */
268     public static function getTranslation($_applicationName, Zend_Locale $_locale = NULL)
269     {
270         $locale = ($_locale !== NULL) ? $_locale : Tinebase_Core::get('locale');
271         
272         $cacheId = (string) $locale . $_applicationName;
273         
274         // get translation from internal class member?
275         if ((isset(self::$_applicationTranslations[$cacheId]) || array_key_exists($cacheId, self::$_applicationTranslations))) {
276             return self::$_applicationTranslations[$cacheId];
277         }
278         
279         // get translation from filesystem
280         $availableTranslations = self::getAvailableTranslations(ucfirst($_applicationName));
281         $info = (isset($availableTranslations[(string) $locale]) || array_key_exists((string) $locale, $availableTranslations)) 
282             ? $availableTranslations[(string) $locale] 
283             : $availableTranslations['en'];
284         
285         // create new translation
286         $options = array(
287             'disableNotices' => true
288         );
289         
290         // TODO remove workaround for server tests, maybe we should fix/rework bootstrap of tests
291         $buildtype = (! defined('TINE20_BUILDTYPE')) ? 'DEVELOPMENT' : TINE20_BUILDTYPE;
292         
293         // Switch between Po and Mo adapter depending on the mode
294         switch ($buildtype) {
295             case 'DEVELOPMENT':
296                 $translate = new Zend_Translate('gettextPo', $info['path'], $info['locale'], $options);
297                 break;
298             case 'DEBUG':
299             case 'RELEASE':
300                 $translate = new Zend_Translate('gettext', str_replace('.po', '.mo', $info['path']), $info['locale'], $options);
301                 break;
302         }
303         
304         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) 
305             Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .' locale used: ' . $_applicationName . '/' . $info['locale']);
306         
307         self::$_applicationTranslations[$cacheId] = $translate;
308         
309         return $translate;
310     }
311     
312     /**
313      * Returns collection of all javascript translations data for requested language
314      * 
315      * This is a javascript special function!
316      * The data will be preseted to be included as javascript on client side!
317      *
318      * NOTE: This function is called from release.php cli script. In this case no 
319      *       tine 2.0 core initialisation took place beforehand
320      *       
321      * @param  Zend_Locale|string $_locale
322      * @return string      javascript
323      */
324     public static function getJsTranslations($_locale, $_appName = 'all')
325     {
326         $locale = ($_locale instanceof Zend_Locale) ? $_locale : new Zend_Locale($_locale);
327         $localeString = (string) $_locale;
328         
329         $availableTranslations = self::getAvailableTranslations();
330         $info = (isset($availableTranslations[$localeString]) || array_key_exists($localeString, $availableTranslations)) ? $availableTranslations[$localeString] : array('locale' => $localeString);
331         $baseDir = ((isset($info['path']) || array_key_exists('path', $info)) ? dirname($info['path']) . '/..' : dirname(__FILE__)) . '/..';
332         
333         $defaultDir = dirname(__FILE__) . "/..";
334         
335         $genericTranslationFile = "$baseDir/Tinebase/js/Locale/static/generic-$localeString.js";
336         $genericTranslationFile = is_readable($genericTranslationFile) ? $genericTranslationFile : "$defaultDir/Tinebase/js/Locale/static/generic-$localeString.js";
337         
338         $extjsTranslationFile   = "$baseDir/library/ExtJS/src/locale/ext-lang-$localeString.js";
339         $extjsTranslationFile   = is_readable($extjsTranslationFile) ? $extjsTranslationFile : "$defaultDir/library/ExtJS/src/locale/ext-lang-$localeString.js";
340         if (! is_readable($extjsTranslationFile)) {
341             // trying language as fallback if lang_region file can not be found, @see 0008242: Turkish does not work / throws an error
342             $language = $locale->getLanguage();
343             $extjsTranslationFile   = "$baseDir/library/ExtJS/src/locale/ext-lang-$language.js";
344             $extjsTranslationFile   = is_readable($extjsTranslationFile) ? $extjsTranslationFile : "$defaultDir/library/ExtJS/src/locale/ext-lang-$language.js";
345         }
346         $tine20TranslationFiles = self::getPoTranslationFiles($info);
347         
348         $allTranslationFiles    = array_merge(array($genericTranslationFile, $extjsTranslationFile), $tine20TranslationFiles);
349         
350         $jsTranslations = NULL;
351         
352         if (Tinebase_Core::get(Tinebase_Core::CACHE) && $_appName == 'all') {
353             // setup cache (saves about 20% @2010/01/28)
354             $cache = new Zend_Cache_Frontend_File(array(
355                 'master_files' => $allTranslationFiles
356             ));
357             $cache->setBackend(Tinebase_Core::get(Tinebase_Core::CACHE)->getBackend());
358             
359             $cacheId = __CLASS__ . "_". __FUNCTION__ . "_{$localeString}";
360             
361             $jsTranslations = $cache->load($cacheId);
362         }
363         
364         if (! $jsTranslations) {
365             $jsTranslations  = "";
366             
367             if (in_array($_appName, array('Tinebase', 'all'))) {
368                 $jsTranslations .= "/************************** generic translations **************************/ \n";
369                 
370                 $jsTranslations .= file_get_contents($genericTranslationFile);
371                 
372                 $jsTranslations  .= "/*************************** extjs translations ***************************/ \n";
373                 if (file_exists($extjsTranslationFile)) {
374                     $jsTranslations  .= file_get_contents($extjsTranslationFile);
375                 } else {
376                     $jsTranslations  .= "console.error('Translation Error: extjs changed their lang file name again ;-(');";
377                 }
378             }
379             
380             $poFiles = self::getPoTranslationFiles($info);
381             
382             foreach ($poFiles as $appName => $poPath) {
383                 if ($_appName !='all' && $_appName != $appName) continue;
384                 $poObject = self::po2jsObject($poPath);
385                 
386                 //if (! json_decode($poObject)) {
387                 //    $jsTranslations .= "console.err('tanslations for application $appName are broken');";
388                 //} else {
389                     $jsTranslations  .= "/********************** tine translations of $appName**********************/ \n";
390                     $jsTranslations .= "Locale.Gettext.prototype._msgs['./LC_MESSAGES/$appName'] = new Locale.Gettext.PO($poObject); \n";
391                 //}
392             }
393             
394             if (isset($cache)) {
395                 $cache->save($jsTranslations, $cacheId);
396             }
397         }
398         
399         return $jsTranslations;
400     }
401     
402     /**
403      * gets array of lang dirs from all applications having translations
404      * 
405      * Note: This functions must not query the database! 
406      *       It's only used in the development and release building process
407      * 
408      * @return array appName => translationDir
409      */
410     public static function getTranslationDirs($_customPath = NULL)
411     {
412         $tine20path = dirname(__File__) . "/..";
413         
414         $langDirs = array();
415         $d = dir($tine20path);
416         while (false !== ($appName = $d->read())) {
417             $appPath = "$tine20path/$appName";
418             if ($appName{0} != '.' && is_dir($appPath)) {
419                 $translationPath = "$appPath/translations";
420                 if (is_dir($translationPath)) {
421                     $langDirs[$appName] = $translationPath;
422                 }
423             }
424         }
425         
426         // evaluate customPath
427         if ($_customPath) {
428             $d = dir($_customPath);
429             while (false !== ($appName = $d->read())) {
430                 $appPath = "$_customPath/$appName";
431                 if ($appName{0} != '.' && is_dir($appPath)) {
432                     $translationPath = "$appPath/translations";
433                     if (is_dir($translationPath)) {
434                         $langDirs[$appName] = $translationPath;
435                     }
436                 }
437             }
438         }
439         
440         return $langDirs;
441     }
442     
443     /**
444      * gets all available po files for a given locale
445      *
446      * @param  array $_info translation info
447      * @return array appName => pofile path
448      */
449     public static function getPoTranslationFiles($_info)
450     {
451         $localeString = $_info['locale'];
452         $poFiles = array();
453         
454         $translationDirs = self::getTranslationDirs(isset($_info['path']) ? dirname($_info['path']) . '/../..': NULL);
455         foreach ($translationDirs as $appName => $translationDir) {
456             $poPath = "$translationDir/$localeString.po";
457             if (file_exists($poPath)) {
458                 $poFiles[$appName] = $poPath;
459             }
460         }
461         
462         return $poFiles;
463     }
464     
465     /**
466      * convertes po file to js object
467      *
468      * @param  string $filePath
469      * @return string
470      */
471     public static function po2jsObject($filePath)
472     {
473         $po = file_get_contents($filePath);
474         
475         global $first, $plural;
476         $first = true;
477         $plural = false;
478         
479         $po = preg_replace('/\r?\n/', "\n", $po);
480         $po = preg_replace('/^#.*\n/m', '', $po);
481         // 2008-08-25 \s -> \n as there are situations when whitespace like space breaks the thing!
482         $po = preg_replace('/"(\n+)"/', '', $po);
483         // Create a singular version of plural defined words
484         preg_match_all('/msgid "(.*?)"\nmsgid_plural ".*"\nmsgstr\[0\] "(.*?)"\n/', $po, $plurals);
485         for ($i = 0; $i < count($plurals[0]); $i++) {
486             $po = $po . "\n".'msgid "' . $plurals[1][$i] . '"' . "\n" . 'msgstr "' . $plurals[2][$i] . '"' . "\n";
487         }
488         $po = preg_replace('/msgid "(.*?)"\nmsgid_plural "(.*?)"/', 'msgid "$1, $2"', $po);
489         $po = preg_replace_callback('/msg(\S+) /', create_function('$matches','
490             global $first, $plural;
491             switch ($matches[1]) {
492                 case "id":
493                     if ($first) {
494                         $first = false;
495                         return "";
496                     }
497                     if ($plural) {
498                         $plural = false;
499                         return "]\n, ";
500                     }
501                     return ", ";
502                 case "str":
503                     return ": ";
504                 case "str[0]":
505                     $plural = true;
506                     return ": [\n  ";
507                 default:
508                     return " ,";
509             }
510         '), $po);
511         $po = "({\n" . (string)$po . ($plural ? "]\n})" : "\n})");
512         return $po;
513     }
514     
515     /**
516      * convert date to string
517      * 
518      * @param Tinebase_DateTime $date [optional]
519      * @param string            $timezone [optional]
520      * @param Zend_Locale       $locale [optional]
521      * @param string            $part one of date, time or datetime [optional]
522      * @param boolean           $addWeekday should the weekday be added (only works with $part = 'date[time]') [optional] 
523      * @return string
524      */
525     public static function dateToStringInTzAndLocaleFormat(DateTime $date = null, $timezone = null, Zend_Locale $locale = null, $part = 'datetime', $addWeekday = false)
526     {
527         $date = ($date !== null) ? clone($date) : Tinebase_DateTime::now();
528         $timezone = ($timezone !== null) ? $timezone : Tinebase_Core::getUserTimezone();
529         $locale = ($locale !== null) ? $locale : Tinebase_Core::get(Tinebase_Core::LOCALE);
530         
531         $date = new Zend_Date($date->getTimestamp());
532         $date->setTimezone($timezone);
533
534         if (in_array($part, array('date', 'time', 'datetime'))) {
535             $dateString = $date->toString(Zend_Locale_Format::getDateFormat($locale), $locale);
536             if ($addWeekday) {
537                 $dateString = $date->toString('EEEE', $locale) . ', ' . $dateString;
538             }
539             $timeString = $date->toString(Zend_Locale_Format::getTimeFormat($locale), $locale);
540
541             switch ($part) {
542                 case 'date':
543                     return $dateString;
544                 case 'time':
545                     return $timeString;
546                 default:
547                     return $dateString . ' ' . $timeString;
548             }
549         } else {
550             return $date->toString($part, $locale);
551         }
552
553     }
554 }