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 tree’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_callback(
'/_[a-z][a-z]$/',
function($x) { return strtoupper($x[0]); },
str_replace('-', '_', $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
self::$translation_adapter = new Zend_Translate('gettext', WT_ROOT.'language/'.$locale.'.mo', $locale);
// Deprecated - some custom modules use this to add translations
Zend_Registry::set('Zend_Translate', self::$translation_adapter);
// Load any local user translations
if (is_dir(WT_DATA_DIR.'language')) {
if (file_exists(WT_DATA_DIR.'language/'.$locale.'.mo')) {
self::$translation_adapter->addTranslation(
new Zend_Translate('gettext', WT_DATA_DIR.'language/'.$locale.'.mo', $locale)
);
}
if (file_exists(WT_DATA_DIR.'language/'.$locale.'.php')) {
self::$translation_adapter->addTranslation(
new Zend_Translate('array', WT_DATA_DIR.'language/'.$locale.'.php', $locale)
);
}
if (file_exists(WT_DATA_DIR.'language/'.$locale.'.csv')) {
self::$translation_adapter->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 is the format string for the time-of-day. See http://php.net/date for codes
$TIME_FORMAT=self::noop('%H:%i:%s');
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;
$TEXT_DIRECTION = WT_I18N::scriptDirection(WT_I18N::languageScript($locale));
self::$locale=$locale;
self::$dir=$TEXT_DIRECTION;
// I18N: This punctuation is used to separate lists of items.
self::$list_separator=WT_I18N::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=WT_I18N::translate('utf8_unicode_ci');
// Non-latin numbers may require non-latin digits
self::$numbering_system = Zend_Locale_Data::getContent($locale, 'defaultnumberingsystem');
return $locale;
}
// Check which languages are installed
public static 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])?([-_][A-Za-z]+)*)\.mo$/', basename($mo_file), $match)) {
// Sort by the transation of the base language, then the variant.
// e.g. English|British English, Portuguese|Brazilian Portuguese
$tmp1 = WT_I18N::languageName($match[1]);
if ($match[1]==$match[2]) {
$tmp2=$tmp1;
} else {
$tmp2 = WT_I18N::languageName($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"
public static function html_markup() {
$localeData=Zend_Locale_Data::getList(self::$locale, 'layout');
$dir=$localeData['characters']=='right-to-left' ? 'rtl' : 'ltr';
list($lang) = preg_split('/[-_@]/', 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
public static 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.
public static function digits($n) {
if (self::$numbering_system != 'latn') {
return Zend_Locale_Format::convertNumerals($n, 'latn', self::$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%
public static 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. */
WT_I18N::translate('%s%%', self::number($n*100.0, $precision));
}
// echo WT_I18N::translate('Hello World!');
// echo WT_I18N::translate('The %s sat on the mat', 'cat');
public static function translate(/* var_args */) {
$args=func_get_args();
if (WT_DEBUG_LANG) {
$args[0]=WT_Debug::pseudoTranslate($args[0]);
} else {
$args[0]=self::$translation_adapter->_($args[0]);
}
return call_user_func_array('sprintf', $args);
}
// Context sensitive version of translate.
// echo WT_I18N::translate_c('NOMINATIVE', 'January');
// echo WT_I18N::translate_c('GENITIVE', 'January');
public static 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=self::$translation_adapter->_($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.
public static function noop($string) {
return self::$translation_adapter->_($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);
public static 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=self::$translation_adapter->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()
public static 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 WT_I18N::translate('(stillborn)');
case 'INFANT':
// I18N: Description of an individual’s age at an event. e.g. Died 14 Jan 1900 (in infancy)
return WT_I18N::translate('(in infancy)');
case 'CHILD':
// I18N: Description of an individual’s age at an event. e.g. Died 14 Jan 1900 (in childhood)
return WT_I18N::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 WT_I18N::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 WT_I18N::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 WT_I18N::translate('(aged %s)', $age);
}
} else {
// Not a valid string?
return WT_I18N::translate('(aged %s)', $string);
}
}
// Convert a number of seconds into a relative time. e.g. 630 => "10 hours, 30 minutes ago"
public 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));
}
}
// Return the endonym for a given language - as per http://cldr.unicode.org/
public static function languageName($language) {
switch (str_replace(array('_', '@'), '-', $language)) {
case 'af': return 'Afrikaans';
case 'ar': return 'العربية';
case 'bg': return 'български';
case 'bs': return 'bosanski';
case 'ca': return 'català';
case 'cs': return 'čeština';
case 'da': return 'dansk';
case 'de': return 'Deutsch';
case 'dv': return 'ދިވެހިބަސް';
case 'el': return 'Ελληνικά';
case 'en': return 'English';
case 'en-AU': return 'Australian English';
case 'en-GB': return 'British English';
case 'en-US': return 'U.S. English';
case 'es': return 'español';
case 'et': return 'eesti';
case 'fa': return 'فارسی';
case 'fi': return 'suomi';
case 'fo': return 'føroyskt';
case 'fr': return 'français';
case 'fr-CA': return 'français canadien';
case 'gl': return 'galego';
case 'haw': return 'ʻŌlelo Hawaiʻi';
case 'he': return 'עברית';
case 'hr': return 'hrvatski';
case 'hu': return 'magyar';
case 'id': return 'Bahasa Indonesia';
case 'is': return 'íslenska';
case 'it': return 'italiano';
case 'ja': return '日本語';
case 'ka': return 'ქართული';
case 'ko': return '한국어';
case 'lt': return 'lietuvių';
case 'lv': return 'latviešu';
case 'mi': return 'Māori';
case 'mr': return 'मराठी';
case 'ms': return 'Bahasa Melayu';
case 'nb': return 'norsk bokmål';
case 'ne': return 'नेपाली';
case 'nl': return 'Nederlands';
case 'nn': return 'nynorsk';
case 'oc': return 'occitan';
case 'pl': return 'polski';
case 'pt': return 'português';
case 'pt-BR': return 'português do Brasil';
case 'ro': return 'română';
case 'ru': return 'русский';
case 'sk': return 'slovenčina';
case 'sl': return 'slovenščina';
case 'sr': return 'Српски';
case 'sr-Latn': return 'srpski';
case 'sv': return 'svenska';
case 'ta': return 'தமிழ்';
case 'tr': return 'Türkçe';
case 'tt': return 'Татар';
case 'uk': return 'українська';
case 'vi': return 'Tiếng Việt';
case 'yi': return 'ייִדיש';
case 'zh': return '中文';
case 'zh-CN': return '简体中文'; // Simplified Chinese
case 'zh-TW': return '繁體中文'; // Traditional Chinese
default:
// Use the PHP/intl library, if it exists
if (class_exists('\\Locale')) {
return Locale::getDisplayName($language, $language);
}
return $language;
}
}
// Return the script used by a given language
// The PHP/intl library does not provde this information.
public static function languageScript($language) {
switch (str_replace(array('_', '@'), '-', $language)) {
case 'af': return 'Latn';
case 'ar': return 'Arab';
case 'bg': return 'Cyrl';
case 'bs': return 'Latn';
case 'ca': return 'Latn';
case 'cs': return 'Latn';
case 'da': return 'Latn';
case 'de': return 'Latn';
case 'dv': return 'Thaa';
case 'el': return 'Grek';
case 'en': return 'Latn';
case 'en-AU': return 'Latn';
case 'en-GB': return 'Latn';
case 'en-US': return 'Latn';
case 'es': return 'Latn';
case 'et': return 'Latn';
case 'fa': return 'Arab';
case 'fi': return 'Latn';
case 'fo': return 'Latn';
case 'fr': return 'Latn';
case 'fr-CA': return 'Latn';
case 'gl': return 'Latn';
case 'haw': return 'Latn';
case 'he': return 'Hebr';
case 'hr': return 'Latn';
case 'hu': return 'Latn';
case 'id': return 'Latn';
case 'is': return 'Latn';
case 'it': return 'Latn';
case 'ja': return 'Kana';
case 'ka': return 'Geor';
case 'ko': return 'Kore';
case 'lt': return 'Latn';
case 'lv': return 'Latn';
case 'mi': return 'Latn';
case 'mr': return 'Mymr';
case 'ms': return 'Latn';
case 'nb': return 'Latn';
case 'ne': return 'Deva';
case 'nl': return 'Latn';
case 'nn': return 'Latn';
case 'oc': return 'Latn';
case 'pl': return 'Latn';
case 'pt': return 'Latn';
case 'pt-BR': return 'Latn';
case 'ro': return 'Latn';
case 'ru': return 'Cyrl';
case 'sk': return 'Latn';
case 'sl': return 'Latn';
case 'sr': return 'Cyrl';
case 'sr-Latn': return 'Latn';
case 'sv': return 'Latn';
case 'ta': return 'Taml';
case 'tr': return 'Latn';
case 'tt': return 'Cyrl';
case 'uk': return 'Cyrl';
case 'vi': return 'Latn';
case 'yi': return 'Hebr';
case 'zh': return 'Hans';
case 'zh-CN': return 'Hans';
case 'zh-TW': return 'Hant';
default:
return 'Latn';
}
}
// Identify the script used for a piece of text
public static function textScript($string) {
$string = strip_tags($string); // otherwise HTML tags show up as latin
$string = html_entity_decode($string, ENT_QUOTES, 'UTF-8'); // otherwise HTML entities show up as latin
$string = str_replace(array('@N.N.', '@P.N.'), '', $string); // otherwise unknown names show up as latin
$pos = 0;
$strlen = strlen($string);
while ($pos < $strlen) {
$byte = ord($string[$pos]);
if (($byte & 0xE0) == 0xC0) {
$chrlen = 2;
$chr = $string[$pos] . $string[$pos + 1];
} elseif (($byte & 0xF0) == 0xE0) {
$chrlen = 3;
$chr = $string[$pos] . $string[$pos + 1] . $string[$pos + 2];
} else {
$chrlen = 1;
$chr = $string[$pos];
}
if (strpos(WT_I18N::WT_UTF8_LATIN_CHARACTERS, $chr) !== false) {
return 'Latn';
} elseif (strpos(WT_I18N::WT_UTF8_CYRILLIC_CHARACTERS, $chr) !== false) {
return 'Cyrl';
} elseif (strpos(WT_I18N::WT_UTF8_HEBREW_CHARACTERS, $chr) !== false) {
return 'Hebr';
} elseif (strpos(WT_I18N::WT_UTF8_GREEK_CHARACTERS, $chr) !== false) {
return 'Grek';
} elseif (strpos(WT_I18N::WT_UTF8_ARABIC_CHARACTERS, $chr) !== false) {
return 'Arab';
}
$pos += $chrlen;
}
return 'Latn';
}
// Return the direction (ltr or rtl) for a given script
// The PHP/intl library does not provde this information.
public static function scriptDirection($script) {
switch ($script) {
case 'Arab':
case 'Hebr':
case 'Mong':
case 'Thaa':
return 'rtl';
default:
return 'ltr';
}
}
// Generate consistent I18N for datatables.js
public 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 */ WT_I18N::translate('Display %s', $length_menu);
// Which symbol is used for separating numbers into groups
$symbols = Zend_Locale_Data::getList(self::$locale, 'symbols');
// Which digits are used for numbers
$digits = Zend_Locale_Data::getContent(self::$locale, 'numberingsystem', self::$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 */ WT_I18N::translate('first').'",'.
' "sLast": "'./* I18N: button label, last page */ WT_I18N::translate('last').'",'.
' "sNext": "'./* I18N: button label, next page */ WT_I18N::translate('next').'",'.
' "sPrevious": "'./* I18N: button label, previous page */ WT_I18N::translate('previous').'"'.
' },'.
' "sEmptyTable": "'.WT_I18N::translate('No records to display').'",'.
' "sInfo": "'./* I18N: %s are placeholders for numbers */ WT_I18N::translate('Showing %1$s to %2$s of %3$s', '_START_', '_END_', '_TOTAL_').'",'.
' "sInfoEmpty": "'.WT_I18N::translate('Showing %1$s to %2$s of %3$s', 0, 0, 0).'",'.
' "sInfoFiltered": "'./* I18N: %s is a placeholder for a number */ WT_I18N::translate('(filtered from %s total entries)', '_MAX_').'",'.
' "sInfoPostfix": "",'.
' "sInfoThousands": "'.$symbols['group'].'",'.
' "sLengthMenu": "'.WT_Filter::escapeJs($length_menu).'",'.
' "sLoadingRecords": "'.WT_I18N::translate('Loading…').'",'.
' "sProcessing": "'.WT_I18N::translate('Loading…').'",'.
' "sSearch": "'.WT_I18N::translate('Filter').'",'.
' "sUrl": "",'.
' "sZeroRecords": "'.WT_I18N::translate('No records to display').'"'.
'}'.
$callback;
}
}