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