true);
if (ini_get('apc.enabled')) {
self::$cache=Zend_Cache::factory('Core', 'Apc', $cache_options, array());
} else {
if (!is_dir(WT_DATA_DIR.'cache')) {
// We may not have permission - especially during setup, before we instruct
// the user to "chmod 777 /data"
@mkdir(WT_DATA_DIR.'cache');
}
if (is_dir(WT_DATA_DIR.DIRECTORY_SEPARATOR.'cache')) {
self::$cache=Zend_Cache::factory('Core', 'File', $cache_options, array('cache_dir'=>WT_DATA_DIR.'cache'));
} else {
// No cache available :-(
self::$cache=Zend_Cache::factory('Core', 'Zend_Cache_Backend_BlackHole', $cache_options, array(), false, true);
}
}
// If we created a cache, use it.
if (self::$cache) {
Zend_Locale::setCache(self::$cache);
Zend_Translate::setCache(self::$cache);
}
$installed_languages=self::installed_languages();
if (is_null($locale) || !array_key_exists($locale, $installed_languages)) {
// Automatic locale selection.
if (isset($_GET['lang']) && array_key_exists($_GET['lang'], $installed_languages)) {
// Requested in the URL?
$locale=$_GET['lang'];
unset($_GET['lang']);
if (WT_USER_ID) {
set_user_setting(WT_USER_ID, 'language', $locale);
}
} elseif (array_key_exists($WT_SESSION->locale, $installed_languages)) {
// Rembered from a previous visit?
$locale=$WT_SESSION->locale;
} else {
// Browser preference takes priority over gedcom default
if (!empty($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
$prefs=explode(',', str_replace(' ', '', $_SERVER['HTTP_ACCEPT_LANGUAGE']));
} else {
$prefs=array();
}
if (WT_GED_ID) {
// Add the gedcom's default language as a low-priority
$locale=get_gedcom_setting(WT_GED_ID, 'LANGUAGE');
$prefs[]=$locale.';q=0.2';
}
$prefs2=array();
foreach ($prefs as $pref) {
list($l, $q)=explode(';q=', $pref.';q=1.0');
$l=preg_replace(
array('/-/', '/_[a-z][a-z]$/e'),
array ('_', 'strtoupper("$0")'),
$l
); // en-gb => en_GB
if (array_key_exists($l, $prefs2)) {
$prefs2[$l]=max((float)$q, $prefs2[$l]);
} else {
$prefs2[$l]=(float)$q;
}
}
// Ensure there is a fallback.
if (!array_key_exists('en_US', $prefs2)) {
$prefs2['en_US']=0.01;
}
arsort($prefs2);
foreach (array_keys($prefs2) as $pref) {
if (array_key_exists($pref, $installed_languages)) {
$locale=$pref;
break;
}
}
}
}
// Load the translation file
$translate=new Zend_Translate('gettext', WT_ROOT.'language/'.$locale.'.mo', $locale);
// Make the locale and translation adapter available to the rest of the Zend Framework
Zend_Registry::set('Zend_Locale', $locale);
Zend_Registry::set('Zend_Translate', $translate);
// Load any local user translations
if (is_dir(WT_DATA_DIR.'language')) {
if (file_exists(WT_DATA_DIR.'language/'.$locale.'.mo')) {
$translate->addTranslation(
new Zend_Translate('gettext', WT_DATA_DIR.'language/'.$locale.'.mo', $locale)
);
}
if (file_exists(WT_DATA_DIR.'language/'.$locale.'.php')) {
$translate->addTranslation(
new Zend_Translate('array', WT_DATA_DIR.'language/'.$locale.'.php', $locale)
);
}
if (file_exists(WT_DATA_DIR.'language/'.$locale.'.csv')) {
$translate->addTranslation(
new Zend_Translate('csv', WT_DATA_DIR.'language/'.$locale.'.csv', $locale)
);
}
}
// Extract language settings from the translation file
global $DATE_FORMAT; // I18N: This is the format string for full dates. See http://php.net/date for codes
$DATE_FORMAT=self::noop('%j %F %Y');
global $TIME_FORMAT; // I18N: This a the format string for the time-of-day. See http://php.net/date for codes
$TIME_FORMAT=self::noop('%g:%i:%s %a');
global $ALPHABET_upper; // Alphabetic sorting sequence (upper-case letters), used by webtrees to sort strings
$ALPHABET_upper=self::noop('ALPHABET_upper=ABCDEFGHIJKLMNOPQRSTUVWXYZ');
list(, $ALPHABET_upper)=explode('=', $ALPHABET_upper);
global $ALPHABET_lower; // Alphabetic sorting sequence (lower-case letters), used by webtrees to sort strings
$ALPHABET_lower=self::noop('ALPHABET_lower=abcdefghijklmnopqrstuvwxyz');
list(, $ALPHABET_lower)=explode('=', $ALPHABET_lower);
global $WEEK_START; // I18N: This is the first day of the week on calendars. 0=Sunday, 1=Monday...
$WEEK_START=self::noop('WEEK_START=0');
list(, $WEEK_START)=explode('=', $WEEK_START);
global $TEXT_DIRECTION;
$localeData=Zend_Locale_Data::getList($locale, 'layout');
$TEXT_DIRECTION=$localeData['characters']=='right-to-left' ? 'rtl' : 'ltr';
self::$locale=$locale;
self::$dir=$TEXT_DIRECTION;
// I18N: This punctuation is used to separate lists of items.
self::$list_separator=self::translate(', ');
// I18N: This is the name of the MySQL collation that applies to your language. A list is available at http://dev.mysql.com/doc/refman/5.0/en/charset-unicode-sets.html
self::$collation=self::translate('utf8_unicode_ci');
return $locale;
}
// Check which languages are installed
static public function installed_languages() {
$mo_files=glob(WT_ROOT.'language'.DIRECTORY_SEPARATOR.'*.mo');
$cache_key=md5(serialize($mo_files));
if (!($installed_languages=self::$cache->load($cache_key))) {
$installed_languages=array();
foreach ($mo_files as $mo_file) {
if (preg_match('/^(([a-z][a-z][a-z]?)(_[A-Z][A-Z])?)\.mo$/', basename($mo_file), $match)) {
// launchpad does not support language variants.
// Until it does, we cannot support languages such as sr@latin
// See http://zendframework.com/issues/browse/ZF-7485
// Sort by the transation of the base language, then the variant.
// e.g. English|British English, Portuguese|Brazilian Portuguese
$tmp1=Zend_Locale::getTranslation($match[1], 'language', $match[1]);
if ($match[1]==$match[2]) {
$tmp2=$tmp1;
} else {
$tmp2=Zend_Locale::getTranslation($match[2], 'language', $match[2]);
}
$installed_languages[$match[1]]=$tmp2.'|'.$tmp1;
}
}
if (empty($installed_languages)) {
// We cannot translate this
die('There are no languages installed. You must include at least one xx.mo file in /language/');
}
// Sort by the combined language/language name...
uasort($installed_languages, 'utf8_strcasecmp');
foreach ($installed_languages as &$value) {
// The locale database doesn't have translations for certain
// "default" languages, such as zn_CH.
if (substr($value, -1)=='|') {
list($value,)=explode('|', $value);
} else {
list(,$value)=explode('|', $value);
}
}
self::$cache->save($installed_languages, $cache_key);
}
return $installed_languages;
}
// Generate i18n markup for the tag, e.g lang="ar" dir="RTL"
static public function html_markup() {
$localeData=Zend_Locale_Data::getList(self::$locale, 'layout');
$dir=$localeData['characters']=='right-to-left' ? 'rtl' : 'ltr';
list($lang)=explode('_', self::$locale);
return 'lang="'.$lang.'" dir="'.$dir.'"';
}
// Translate a number into the local representation. e.g. 12345.67 becomes
// en: 12,345.67
// fr: 12 345,67
// de: 12.345,67
static public function number($n, $precision=0) {
// Add "punctuation" and convert digits
$n=Zend_Locale_Format::toNumber($n, array('locale'=>WT_LOCALE, 'precision'=>$precision));
$n=self::digits($n);
return $n;
}
// Convert the digits 0-9 into the local script
// Used for years, etc., where we do not want thousands-separators, decimals, etc.
static public function digits($n) {
if (WT_NUMBERING_SYSTEM!='latn') {
return Zend_Locale_Format::convertNumerals($n, 'latn', WT_NUMBERING_SYSTEM);
} else {
return $n;
}
}
// Translate a fraction into a percentage. e.g. 0.123 becomes
// en: 12.3%
// fr: 12,3 %
// de: 12,3%
static public function percentage($n, $precision=0) {
return
/* I18N: This is a percentage, such as “32.5%”. “%s” is the number, “%%” is the percent symbol. Some languages require a (non-breaking) space between the two, or a different symbol. */
self::translate('%s%%', self::number($n*100.0, $precision));
}
// echo self::translate('Hello World!');
// echo self::translate('The %s sat on the mat', 'cat');
static public function translate(/* var_args */) {
$args=func_get_args();
if (WT_DEBUG_LANG) {
$args[0]=WT_Debug::pseudoTranslate($args[0]);
} else {
$args[0]=Zend_Registry::get('Zend_Translate')->_($args[0]);
}
return call_user_func_array('sprintf', $args);
}
// Context sensitive version of translate.
// echo self::translate_c('NOMINATIVE', 'January');
// echo self::translate_c('GENITIVE', 'January');
static public function translate_c(/* var_args */) {
$args=func_get_args();
if (WT_DEBUG_LANG) {
$msgtxt=WT_Debug::pseudoTranslate($args[1]);
} else {
$msgid=$args[0]."\x04".$args[1];
$msgtxt=Zend_Registry::get('Zend_Translate')->_($msgid);
if ($msgtxt==$msgid) {
$msgtxt=$args[1];
}
}
$args[0]=$msgtxt;
unset ($args[1]);
return call_user_func_array('sprintf', $args);
}
// Similar to translate, but do perform "no operation" on it.
// This is necessary to fetch a format string (containing % characters) without
// performing sustitution of arguments.
static public function noop($string) {
return Zend_Registry::get('Zend_Translate')->_($string);
}
// echo self::plural('There is an error', 'There are errors', $num_errors);
// echo self::plural('There is one error', 'There are %s errors', $num_errors);
// echo self::plural('There is %1$d %2$s cat', 'There are %1$d %2$s cats', $num, $num, $colour);
static public function plural(/* var_args */) {
$args=func_get_args();
if (WT_DEBUG_LANG) {
if ($args[2]==1) {
$string=WT_Debug::pseudoTranslate($args[0]);
} else {
$string=WT_Debug::pseudoTranslate($args[1]);
}
} else {
$string=Zend_Registry::get('Zend_Translate')->plural($args[0], $args[1], $args[2]);
}
array_splice($args, 0, 3, array($string));
return call_user_func_array('sprintf', $args);
}
// Convert a GEDCOM age string into translated_text
// NB: The import function will have normalised this, so we don't need
// to worry about badly formatted strings
// NOTE: this function is not yet complete - eventually it will replace get_age_at_event()
static public function gedcom_age($string) {
switch ($string) {
case 'STILLBORN':
// I18N: Description of an individual’s age at an event. e.g Died 14 Jan 1900 (stillborn)
return self::translate('(stillborn)');
case 'INFANT':
// I18N: Description of an individual’s age at an event. e.g Died 14 Jan 1900 (in infancy)
return self::translate('(in infancy)');
case 'CHILD':
// I18N: Description of an individual’s age at an event. e.g Died 14 Jan 1900 (in childhood)
return self::translate('(in childhood)');
}
$age=array();
if (preg_match('/(\d+)y/', $string, $match)) {
// I18N: Part of an age string. e.g 5 years, 4 months and 3 days
$years=$match[1];
$age[]=self::plural('%s year', '%s years', $years, self::number($years));
} else {
$years=-1;
}
if (preg_match('/(\d+)m/', $string, $match)) {
// I18N: Part of an age string. e.g 5 years, 4 months and 3 days
$age[]=self::plural('%s month', '%s months', $match[1], self::number($match[1]));
}
if (preg_match('/(\d+)w/', $string, $match)) {
// I18N: Part of an age string. e.g 7 weeks and 3 days
$age[]=self::plural('%s week', '%s weeks', $match[1], self::number($match[1]));
}
if (preg_match('/(\d+)d/', $string, $match)) {
// I18N: Part of an age string. e.g 5 years, 4 months and 3 days
$age[]=self::plural('%s day', '%s days', $match[1], self::number($match[1]));
}
// If an age is just a number of years, only show the number
if (count($age)==1 && $years>=0) {
$age=$years;
}
if ($age) {
if (!substr_compare($string, '<', 0, 1)) {
// I18N: Description of an individual’s age at an event. e.g Died 14 Jan 1900 (aged less than 21 years)
return self::translate('(aged less than %s)', $age);
} elseif (!substr_compare($string, '>', 0, 1)) {
// I18N: Description of an individual’s age at an event. e.g Died 14 Jan 1900 (aged more than 21 years)
return self::translate('(aged more than %s)', $age);
} else {
// I18N: Description of an individual’s age at an event. e.g Died 14 Jan 1900 (aged 43 years)
return self::translate('(aged %s)', $age);
}
} else {
// Not a valid string?
return self::translate('(aged %s)', $string);
}
}
// Convert a number of seconds into a relative time. e.g. 630 => "10 hours, 30 minutes ago"
static function time_ago($seconds) {
$year=365*24*60*60;
$month=30*24*60*60;
$day=24*60*60;
$hour=60*60;
$minute=60;
// TODO: Display two units (years+months), (months+days), etc.
// This requires "contexts". i.e. "%s months" has a different translation
// in different contexts.
// We must AVOID combining phrases to make sentences.
if ($seconds>$year) {
$years=(int)($seconds/$year);
return self::plural('%s year ago', '%s years ago', $years, self::number($years));
} elseif ($seconds>$month) {
$months=(int)($seconds/$month);
return self::plural('%s month ago', '%s months ago', $months, self::number($months));
} elseif ($seconds>$day) {
$days=(int)($seconds/$day);
return self::plural('%s day ago', '%s days ago', $days, self::number($days));
} elseif ($seconds>$hour) {
$hours=(int)($seconds/$hour);
return self::plural('%s hour ago', '%s hours ago', $hours, self::number($hours));
} elseif ($seconds>$minute) {
$minutes=(int)($seconds/$minute);
return self::plural('%s minute ago', '%s minutes ago', $minutes, self::number($minutes));
} else {
return self::plural('%s second ago', '%s seconds ago', $seconds, self::number($seconds));
}
}
// Generate consistent I18N for datatables.js
static function datatablesI18N(array $lengths=null) {
if ($lengths===null) {
$lengths=array(10, 20, 30, 50, 100, -1);
}
$length_menu='';
foreach ($lengths as $length) {
$length_menu.=
'';
}
$length_menu='';
$length_menu=/* I18N: Display %s [records per page], %s is a placeholder for listbox containing numeric options */ self::translate('Display %s', $length_menu);
// Which symbol is used for separating numbers into groups
$symbols = Zend_Locale_Data::getList(WT_LOCALE, 'symbols');
// Which digits are used for numbers
$numbering_system=Zend_Locale_Data::getContent(WT_LOCALE, 'defaultnumberingsystem');
$digits=Zend_Locale_Data::getContent(WT_LOCALE, 'numberingsystem', $numbering_system);
if ($digits=='0123456789') {
$callback='';
} else {
$callback=',
"fnInfoCallback": function(oSettings, iStart, iEnd, iMax, iTotal, sPre) {
return sPre
.replace(/0/g, "'.utf8_substr($digits, 0, 1).'")
.replace(/1/g, "'.utf8_substr($digits, 1, 1).'")
.replace(/2/g, "'.utf8_substr($digits, 2, 1).'")
.replace(/3/g, "'.utf8_substr($digits, 3, 1).'")
.replace(/4/g, "'.utf8_substr($digits, 4, 1).'")
.replace(/5/g, "'.utf8_substr($digits, 5, 1).'")
.replace(/6/g, "'.utf8_substr($digits, 6, 1).'")
.replace(/7/g, "'.utf8_substr($digits, 7, 1).'")
.replace(/8/g, "'.utf8_substr($digits, 8, 1).'")
.replace(/9/g, "'.utf8_substr($digits, 9, 1).'");
},
"fnFormatNumber": function(iIn) {
return String(iIn)
.replace(/0/g, "'.utf8_substr($digits, 0, 1).'")
.replace(/1/g, "'.utf8_substr($digits, 1, 1).'")
.replace(/2/g, "'.utf8_substr($digits, 2, 1).'")
.replace(/3/g, "'.utf8_substr($digits, 3, 1).'")
.replace(/4/g, "'.utf8_substr($digits, 4, 1).'")
.replace(/5/g, "'.utf8_substr($digits, 5, 1).'")
.replace(/6/g, "'.utf8_substr($digits, 6, 1).'")
.replace(/7/g, "'.utf8_substr($digits, 7, 1).'")
.replace(/8/g, "'.utf8_substr($digits, 8, 1).'")
.replace(/9/g, "'.utf8_substr($digits, 9, 1).'");
}
';
}
return
'"oLanguage": {'.
' "oPaginate": {'.
' "sFirst": "'./* I18N: button label, first page */ self::translate('first').'",'.
' "sLast": "'./* I18N: button label, last page */ self::translate('last').'",'.
' "sNext": "'./* I18N: button label, next page */ self::translate('next').'",'.
' "sPrevious": "'./* I18N: button label, previous page */ self::translate('previous').'"'.
' },'.
' "sEmptyTable": "'.self::translate('No records to display').'",'.
' "sInfo": "'./* I18N: %s are placeholders for numbers */ self::translate('Showing %1$s to %2$s of %3$s', '_START_', '_END_', '_TOTAL_').'",'.
' "sInfoEmpty": "'.self::translate('Showing %1$s to %2$s of %3$s', 0, 0, 0).'",'.
' "sInfoFiltered": "'./* I18N: %s is a placeholder for a number */ self::translate('(filtered from %s total entries)', '_MAX_').'",'.
' "sInfoPostfix": "",'.
' "sInfoThousands": "'.$symbols['group'].'",'.
' "sLengthMenu": "'.addslashes($length_menu).'",'.
' "sLoadingRecords": "'.self::translate('Loading...').'",'.
' "sProcessing": "'.self::translate('Loading...').'",'.
' "sSearch": "'.self::translate('Filter').'",'.
' "sUrl": "",'.
' "sZeroRecords": "'.self::translate('No records to display').'"'.
'}'.
$callback;
}
}