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