xref: /webtrees/app/I18N.php (revision dd04c183d8beed05be2226b30b7dda485ea4538a)
1a25f0a04SGreg Roach<?php
2*dd04c183SGreg Roachnamespace Fisharebest\Webtrees;
3a25f0a04SGreg Roach
4a25f0a04SGreg Roach/**
5a25f0a04SGreg Roach * webtrees: online genealogy
6a25f0a04SGreg Roach * Copyright (C) 2015 webtrees development team
7a25f0a04SGreg Roach * This program is free software: you can redistribute it and/or modify
8a25f0a04SGreg Roach * it under the terms of the GNU General Public License as published by
9a25f0a04SGreg Roach * the Free Software Foundation, either version 3 of the License, or
10a25f0a04SGreg Roach * (at your option) any later version.
11a25f0a04SGreg Roach * This program is distributed in the hope that it will be useful,
12a25f0a04SGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of
13a25f0a04SGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14a25f0a04SGreg Roach * GNU General Public License for more details.
15a25f0a04SGreg Roach * You should have received a copy of the GNU General Public License
16a25f0a04SGreg Roach * along with this program. If not, see <http://www.gnu.org/licenses/>.
17a25f0a04SGreg Roach */
18a25f0a04SGreg Roach
19a25f0a04SGreg Roachuse Patchwork\TurkishUtf8;
20a25f0a04SGreg Roachuse Zend_Cache;
21a25f0a04SGreg Roachuse Zend_Cache_Core;
22a25f0a04SGreg Roachuse Zend_Locale;
23a25f0a04SGreg Roachuse Zend_Locale_Data;
24a25f0a04SGreg Roachuse Zend_Locale_Exception;
25a25f0a04SGreg Roachuse Zend_Locale_Format;
26a25f0a04SGreg Roachuse Zend_Registry;
27a25f0a04SGreg Roachuse Zend_Translate;
28a25f0a04SGreg Roach
29a25f0a04SGreg Roach/**
30a25f0a04SGreg Roach * Class I18N - Functions to support internationalization (i18n) functionality.
31a25f0a04SGreg Roach */
32a25f0a04SGreg Roachclass I18N {
33a25f0a04SGreg Roach	// Digits are always rendered LTR, even in RTL text.
34a25f0a04SGreg Roach	const DIGITS = '0123456789٠١٢٣٤٥٦٧٨٩۰۱۲۳۴۵۶۷۸۹';
35a25f0a04SGreg Roach
36a25f0a04SGreg Roach	// Reversable character conversions from the UNICODE 5.1 database.
37a25f0a04SGreg Roach	// It excludes ambiguous (turkish dotless i) and mixed-case (Dz) characters.
38a25f0a04SGreg Roach	// The characters should be arranged in default unicode-collation order.
39a25f0a04SGreg Roach	const ALPHABET_LOWER = 'aàáâãäåāăąǎǟǡǻȁȃȧḁạảấầẩẫậắằẳẵặⓐaæǣǽbḃḅḇⓑbƀɓƃcçćĉċčḉⅽⓒcƈdďḋḍḏḑḓⅾⓓddždzđɖɗƌðeèéêëēĕėęěȅȇȩḕḗḙḛḝẹẻẽếềểễệⓔeǝəɛfḟⓕfƒgĝğġģǧǵḡⓖgǥɠɣƣhĥȟḣḥḧḩḫⓗhƕħiìíîïĩīĭįǐȉȋḭḯỉịⅰⓘiⅱⅲijⅳⅸɨɩjĵⓙjkķǩḱḳḵⓚkƙlĺļľḷḹḻḽⅼⓛlŀljłƚmḿṁṃⅿⓜmnñńņňǹṅṇṉṋⓝnnjɲƞŋoòóôõöōŏőơǒǫǭȍȏȫȭȯȱṍṏṑṓọỏốồổỗộớờởỡợⓞoœøǿɔɵȣpṕṗⓟpƥqⓠqrŕŗřȑȓṙṛṝṟⓡrʀsśŝşšșṡṣṥṧṩⓢsʃtţťțṫṭṯṱⓣtŧƭʈuùúûüũūŭůűųưǔǖǘǚǜȕȗṳṵṷṹṻụủứừửữựⓤuʉɯʊvṽṿⅴⓥvⅵⅶⅷʋʌwŵẁẃẅẇẉⓦwxẋẍⅹⓧxⅺⅻyýÿŷȳẏỳỵỷỹⓨyƴzźżžẑẓẕⓩzƶȥǯʒƹȝþƿƨƽƅάαἀἁἂἃἄἅἆἇὰάᾀᾁᾂᾃᾄᾅᾆᾇᾰᾱᾳβγδέεἐἑἒἓἔἕὲέϝϛζήηἠἡἢἣἤἥἦἧὴήᾐᾑᾒᾓᾔᾕᾖᾗῃθϊἰἱἲἳἴἵἶἷὶίῐῑκϗλμνξοόὀὁὂὃὄὅὸόπϟϙρῥσϲτυϋύὑὓὕὗὺύῠῡφχψωώὠὡὢὣὤὥὦὧὼώᾠᾡᾢᾣᾤᾥᾦᾧῳϡϸϻϣϥϧϩϫϭϯаӑӓәӛӕбвгґғҕдԁђԃѓҙеѐёӗєжӂӝҗзԅӟѕӡԇиѝӣҋӥіїйјкқӄҡҟҝлӆљԉмӎнӊңӈҥњԋоӧөӫпҧҁрҏсԍҫтԏҭћќуӯўӱӳүұѹфхҳһѡѿѽѻцҵчӵҷӌҹҽҿџшщъыӹьҍѣэӭюяѥѧѫѩѭѯѱѳѵѷҩաբգդեզէըթժիլխծկհձղճմյնշոչպջռսվտրցւփքօֆȼɂɇɉɋɍɏͱͳͷͻͼͽӏӷӻӽӿԑԓԕԗԙԛԝԟԡԣԥᵹᵽỻỽỿⅎↄⰰⰱⰲⰳⰴⰵⰶⰷⰸⰹⰺⰻⰼⰽⰾⰿⱀⱁⱂⱃⱄⱅⱆⱇⱈⱉⱊⱋⱌⱍⱎⱏⱐⱑⱒⱓⱔⱕⱖⱗⱘⱙⱚⱛⱜⱝⱞⱡⱨⱪⱬⱳⱶⲁⲃⲅⲇⲉⲋⲍⲏⲑⲓⲕⲗⲙⲛⲝⲟⲡⲣⲥⲧⲩⲫⲭⲯⲱⲳⲵⲷⲹⲻⲽⲿⳁⳃⳅⳇⳉⳋⳍⳏⳑⳓⳕⳗⳙⳛⳝⳟⳡⳣⳬⳮⴀⴁⴂⴃⴄⴅⴆⴇⴈⴉⴊⴋⴌⴍⴎⴏⴐⴑⴒⴓⴔⴕⴖⴗⴘⴙⴚⴛⴜⴝⴞⴟⴠⴡⴢⴣⴤⴥꙁꙃꙅꙇꙉꙋꙍꙏꙑꙓꙕꙗꙙꙛꙝꙟꙣꙥꙧꙩꙫꙭꚁꚃꚅꚇꚉꚋꚍꚏꚑꚓꚕꚗꜣꜥꜧꜩꜫꜭꜯꜳꜵꜷꜹꜻꜽꜿꝁꝃꝅꝇꝉꝋꝍꝏꝑꝓꝕꝗꝙꝛꝝꝟꝡꝣꝥꝧꝩꝫꝭꝯꝺꝼꝿꞁꞃꞅꞇꞌ';
40a25f0a04SGreg Roach	const ALPHABET_UPPER = 'AÀÁÂÃÄÅĀĂĄǍǞǠǺȀȂȦḀẠẢẤẦẨẪẬẮẰẲẴẶⒶAÆǢǼBḂḄḆⒷBɃƁƂCÇĆĈĊČḈⅭⒸCƇDĎḊḌḎḐḒⅮⒹDDŽDZĐƉƊƋÐEÈÉÊËĒĔĖĘĚȄȆȨḔḖḘḚḜẸẺẼẾỀỂỄỆⒺEƎƏƐFḞⒻFƑGĜĞĠĢǦǴḠⒼGǤƓƔƢHĤȞḢḤḦḨḪⒽHǶĦIÌÍÎÏĨĪĬĮǏȈȊḬḮỈỊⅠⒾIⅡⅢIJⅣⅨƗƖJĴⒿJKĶǨḰḲḴⓀKƘLĹĻĽḶḸḺḼⅬⓁLĿLJŁȽMḾṀṂⅯⓂMNÑŃŅŇǸṄṆṈṊⓃNNJƝȠŊOÒÓÔÕÖŌŎŐƠǑǪǬȌȎȪȬȮȰṌṎṐṒỌỎỐỒỔỖỘỚỜỞỠỢⓄOŒØǾƆƟȢPṔṖⓅPƤQⓆQRŔŖŘȐȒṘṚṜṞⓇRƦSŚŜŞŠȘṠṢṤṦṨⓈSƩTŢŤȚṪṬṮṰⓉTŦƬƮUÙÚÛÜŨŪŬŮŰŲƯǓǕǗǙǛȔȖṲṴṶṸṺỤỦỨỪỬỮỰⓊUɄƜƱVṼṾⅤⓋVⅥⅦⅧƲɅWŴẀẂẄẆẈⓌWXẊẌⅩⓍXⅪⅫYÝŸŶȲẎỲỴỶỸⓎYƳZŹŻŽẐẒẔⓏZƵȤǮƷƸȜÞǷƧƼƄΆΑἈἉἊἋἌἍἎἏᾺΆᾈᾉᾊᾋᾌᾍᾎᾏᾸᾹᾼΒΓΔΈΕἘἙἚἛἜἝῈΈϜϚΖΉΗἨἩἪἫἬἭἮἯῊΉᾘᾙᾚᾛᾜᾝᾞᾟῌΘΪἸἹἺἻἼἽἾἿῚΊῘῙΚϏΛΜΝΞΟΌὈὉὊὋὌὍῸΌΠϞϘΡῬΣϹΤΥΫΎὙὛὝὟῪΎῨῩΦΧΨΩΏὨὩὪὫὬὭὮὯῺΏᾨᾩᾪᾫᾬᾭᾮᾯῼϠϷϺϢϤϦϨϪϬϮАӐӒӘӚӔБВГҐҒҔДԀЂԂЃҘЕЀЁӖЄЖӁӜҖЗԄӞЅӠԆИЍӢҊӤІЇЙЈКҚӃҠҞҜЛӅЉԈМӍНӉҢӇҤЊԊОӦӨӪПҦҀРҎСԌҪТԎҬЋЌУӮЎӰӲҮҰѸФХҲҺѠѾѼѺЦҴЧӴҶӋҸҼҾЏШЩЪЫӸЬҌѢЭӬЮЯѤѦѪѨѬѮѰѲѴѶҨԱԲԳԴԵԶԷԸԹԺԻԼԽԾԿՀՁՂՃՄՅՆՇՈՉՊՋՌՍՎՏՐՑՒՓՔՕՖȻɁɆɈɊɌɎͰͲͶϽϾϿӀӶӺӼӾԐԒԔԖԘԚԜԞԠԢԤꝽⱣỺỼỾℲↃⰀⰁⰂⰃⰄⰅⰆⰇⰈⰉⰊⰋⰌⰍⰎⰏⰐⰑⰒⰓⰔⰕⰖⰗⰘⰙⰚⰛⰜⰝⰞⰟⰠⰡⰢⰣⰤⰥⰦⰧⰨⰩⰪⰫⰬⰭⰮⱠⱧⱩⱫⱲⱵⲀⲂⲄⲆⲈⲊⲌⲎⲐⲒⲔⲖⲘⲚⲜⲞⲠⲢⲤⲦⲨⲪⲬⲮⲰⲲⲴⲶⲸⲺⲼⲾⳀⳂⳄⳆⳈⳊⳌⳎⳐⳒⳔⳖⳘⳚⳜⳞⳠⳢⳫⳭႠႡႢႣႤႥႦႧႨႩႪႫႬႭႮႯႰႱႲႳႴႵႶႷႸႹႺႻႼႽႾႿჀჁჂჃჄჅꙀꙂꙄꙆꙈꙊꙌꙎꙐꙒꙔꙖꙘꙚꙜꙞꙢꙤꙦꙨꙪꙬꚀꚂꚄꚆꚈꚊꚌꚎꚐꚒꚔꚖꜢꜤꜦꜨꜪꜬꜮꜲꜴꜶꜸꜺꜼꜾꝀꝂꝄꝆꝈꝊꝌꝎꝐꝒꝔꝖꝘꝚꝜꝞꝠꝢꝤꝦꝨꝪꝬꝮꝹꝻꝾꞀꞂꞄꞆꞋ';
41a25f0a04SGreg Roach
42a25f0a04SGreg Roach	// Alphabet for the currently selected locale
43a25f0a04SGreg Roach	private static $alphabet_lower = 'abcdefghijklmnopqrstuvwxyz';
44a25f0a04SGreg Roach	private static $alphabet_upper = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
45a25f0a04SGreg Roach
46a25f0a04SGreg Roach	// Lookup table to convert unicode code-points into scripts.
47a25f0a04SGreg Roach	// See https://en.wikipedia.org/wiki/Unicode_block
48a25f0a04SGreg Roach	// Note: we only need details for scripts of languages into which webtrees is translated.
49a25f0a04SGreg Roach	private static $scripts = array(
50a25f0a04SGreg Roach		array('Latn', 0x0041, 0x005A), // a-z
51a25f0a04SGreg Roach		array('Latn', 0x0061, 0x007A), // A-Z
52a25f0a04SGreg Roach		array('Latn', 0x0100, 0x02AF),
53a25f0a04SGreg Roach		array('Grek', 0x0370, 0x03FF),
54a25f0a04SGreg Roach		array('Cyrl', 0x0400, 0x052F),
55a25f0a04SGreg Roach		array('Hebr', 0x0590, 0x05FF),
56a25f0a04SGreg Roach		array('Arab', 0x0600, 0x06FF),
57a25f0a04SGreg Roach		array('Arab', 0x0750, 0x077F),
58a25f0a04SGreg Roach		array('Arab', 0x08A0, 0x08FF),
59a25f0a04SGreg Roach		array('Deva', 0x0900, 0x097F),
60a25f0a04SGreg Roach		array('Taml', 0x0B80, 0x0BFF),
61a25f0a04SGreg Roach		array('Sinh', 0x0D80, 0x0DFF),
62a25f0a04SGreg Roach		array('Thai', 0x0E00, 0x0E7F),
63a25f0a04SGreg Roach		array('Geor', 0x10A0, 0x10FF),
64a25f0a04SGreg Roach		array('Grek', 0x1F00, 0x1FFF),
65a25f0a04SGreg Roach		array('Deva', 0xA8E0, 0xA8FF),
66a25f0a04SGreg Roach		array('Hans', 0x3000, 0x303F), // Mixed CJK, not just Hans
67a25f0a04SGreg Roach		array('Hans', 0x3400, 0xFAFF), // Mixed CJK, not just Hans
68a25f0a04SGreg Roach		array('Hans', 0x20000, 0x2FA1F), // Mixed CJK, not just Hans
69a25f0a04SGreg Roach	);
70a25f0a04SGreg Roach
71a25f0a04SGreg Roach	// Characters that are displayed in mirror form in RTL text.
72a25f0a04SGreg Roach	private static $mirror_characters = array(
73a25f0a04SGreg Roach		'(' => ')',
74a25f0a04SGreg Roach		')' => '(',
75a25f0a04SGreg Roach		'[' => ']',
76a25f0a04SGreg Roach		']' => '[',
77a25f0a04SGreg Roach		'{' => '}',
78a25f0a04SGreg Roach		'}' => '{',
79a25f0a04SGreg Roach		'<' => '>',
80a25f0a04SGreg Roach		'>' => '<',
81a25f0a04SGreg Roach		'‹' => '›',
82a25f0a04SGreg Roach		'›' => '‹',
83a25f0a04SGreg Roach		'«' => '»',
84a25f0a04SGreg Roach		'»' => '«',
85a25f0a04SGreg Roach		'﴾' => '﴿',
86a25f0a04SGreg Roach		'﴿' => '﴾',
87a25f0a04SGreg Roach		'“' => '”',
88a25f0a04SGreg Roach		'”' => '“',
89a25f0a04SGreg Roach		'‘' => '’',
90a25f0a04SGreg Roach		'’' => '‘',
91a25f0a04SGreg Roach	);
92a25f0a04SGreg Roach
93a25f0a04SGreg Roach	/** @var string[] The names of all currently supported languages */
94a25f0a04SGreg Roach	private static $language_data = array(
95a25f0a04SGreg Roach		'af' => array('Latn', 'Afrikaans'),
96a25f0a04SGreg Roach		'ar' => array('Arab', 'العربية'),
97a25f0a04SGreg Roach		'bg' => array('Cyrl', 'български'),
98a25f0a04SGreg Roach		'bs' => array('Latn', 'bosanski'),
99a25f0a04SGreg Roach		'ca' => array('Latn', 'català'),
100a25f0a04SGreg Roach		'cs' => array('Latn', 'čeština'),
101a25f0a04SGreg Roach		'da' => array('Latn', 'dansk'),
102a25f0a04SGreg Roach		'de' => array('Latn', 'Deutsch'),
103a25f0a04SGreg Roach		'dv' => array('Thaa', 'ދިވެހިބަސް'),
104a25f0a04SGreg Roach		'el' => array('Grek', 'Ελληνικά'),
105a25f0a04SGreg Roach		'en' => array('Latn', 'English'),
106a25f0a04SGreg Roach		'en-AU' => array('Latn', 'Australian English'),
107a25f0a04SGreg Roach		'en-GB' => array('Latn', 'British English'),
108a25f0a04SGreg Roach		'en-US' => array('Latn', 'U.S. English'),
109a25f0a04SGreg Roach		'es' => array('Latn', 'español'),
110a25f0a04SGreg Roach		'et' => array('Latn', 'eesti'),
111a25f0a04SGreg Roach		'fa' => array('Arab', 'فارسی'),
112a25f0a04SGreg Roach		'fi' => array('Latn', 'suomi'),
113a25f0a04SGreg Roach		'fo' => array('Latn', 'føroyskt'),
114a25f0a04SGreg Roach		'fr' => array('Latn', 'français'),
115a25f0a04SGreg Roach		'fr-CA' => array('Latn', 'français canadien'),
116a25f0a04SGreg Roach		'gl' => array('Latn', 'galego'),
117a25f0a04SGreg Roach		'haw' => array('Latn', 'ʻŌlelo Hawaiʻi'),
118a25f0a04SGreg Roach		'he' => array('Hebr', 'עברית'),
119a25f0a04SGreg Roach		'hr' => array('Latn', 'hrvatski'),
120a25f0a04SGreg Roach		'hu' => array('Latn', 'magyar'),
121a25f0a04SGreg Roach		'id' => array('Latn', 'Bahasa Indonesia'),
122a25f0a04SGreg Roach		'is' => array('Latn', 'íslenska'),
123a25f0a04SGreg Roach		'it' => array('Latn', 'italiano'),
124a25f0a04SGreg Roach		'ja' => array('Kana', '日本語'),
125a25f0a04SGreg Roach		'ka' => array('Geor', 'ქართული'),
126a25f0a04SGreg Roach		'ko' => array('Kore', '한국어'),
127a25f0a04SGreg Roach		'lt' => array('Latn', 'lietuvių'),
128a25f0a04SGreg Roach		'lv' => array('Latn', 'latviešu'),
129a25f0a04SGreg Roach		'mi' => array('Latn', 'Māori'),
130a25f0a04SGreg Roach		'mr' => array('Mymr', 'मराठी'),
131a25f0a04SGreg Roach		'ms' => array('Latn', 'Bahasa Melayu'),
132a25f0a04SGreg Roach		'nb' => array('Latn', 'norsk bokmål'),
133a25f0a04SGreg Roach		'ne' => array('Deva', 'नेपाली'),
134a25f0a04SGreg Roach		'nl' => array('Latn', 'Nederlands'),
135a25f0a04SGreg Roach		'nn' => array('Latn', 'nynorsk'),
136a25f0a04SGreg Roach		'oc' => array('Latn', 'occitan'),
137a25f0a04SGreg Roach		'pl' => array('Latn', 'polski'),
138a25f0a04SGreg Roach		'pt' => array('Latn', 'português'),
139a25f0a04SGreg Roach		'pt-BR' => array('Latn', 'português do Brasil'),
140a25f0a04SGreg Roach		'ro' => array('Latn', 'română'),
141a25f0a04SGreg Roach		'ru' => array('Cyrl', 'русский'),
142a25f0a04SGreg Roach		'sk' => array('Latn', 'slovenčina'),
143a25f0a04SGreg Roach		'sl' => array('Latn', 'slovenščina'),
144a25f0a04SGreg Roach		'sr' => array('Cyrl', 'Српски'),
145a25f0a04SGreg Roach		'sr-Latn' => array('Latn', 'srpski'),
146a25f0a04SGreg Roach		'sv' => array('Latn', 'svenska'),
147a25f0a04SGreg Roach		'ta' => array('Taml', 'தமிழ்'),
148a25f0a04SGreg Roach		'tr' => array('Latn', 'Türkçe'),
149a25f0a04SGreg Roach		'tt' => array('Cyrl', 'Татар'),
150a25f0a04SGreg Roach		'uk' => array('Cyrl', 'українська'),
151a25f0a04SGreg Roach		'vi' => array('Latn', 'Tiếng Việt'),
152a25f0a04SGreg Roach		'yi' => array('Hebr', 'ייִדיש'),
153a25f0a04SGreg Roach		'zh' => array('Hans', '中文'),
154a25f0a04SGreg Roach		'zh-CN' => array('Hans', '简体中文'),
155a25f0a04SGreg Roach		'zh-TW' => array('Hant', '繁體中文'),
156a25f0a04SGreg Roach	);
157a25f0a04SGreg Roach
158a25f0a04SGreg Roach	/** @var string the name of the current locale, such as fr or en_GB */
159a25f0a04SGreg Roach	public  static $locale;
160a25f0a04SGreg Roach
161a25f0a04SGreg Roach	/** @var string The MySQL collation sequence used by this language, typically utf8_unicode_ci */
162a25f0a04SGreg Roach	public  static $collation;
163a25f0a04SGreg Roach
164a25f0a04SGreg Roach	/** @var string Punctuation used to separate list items, typically a comma */
165a25f0a04SGreg Roach	public  static $list_separator;
166a25f0a04SGreg Roach
167a25f0a04SGreg Roach	/** @var string Text direction; ltr or rtl */
168a25f0a04SGreg Roach	private static $dir;
169a25f0a04SGreg Roach
170a25f0a04SGreg Roach	/** @var Zend_Cache_Core */
171a25f0a04SGreg Roach	private static $cache;
172a25f0a04SGreg Roach
173a25f0a04SGreg Roach	/** @var string The numbering system used by this language; typically latin digits */
174a25f0a04SGreg Roach	private static $numbering_system;
175a25f0a04SGreg Roach
176a25f0a04SGreg Roach	/** @var Zend_Translate */
177a25f0a04SGreg Roach	private static $translation_adapter;
178a25f0a04SGreg Roach
179a25f0a04SGreg Roach	/**
180a25f0a04SGreg Roach	 * Initialise the translation adapter with a locale setting.
181a25f0a04SGreg Roach	 *
182a25f0a04SGreg Roach	 * @param string|null $locale If no locale specified, choose one automatically
183a25f0a04SGreg Roach	 *
184a25f0a04SGreg Roach	 * @return string $string
185a25f0a04SGreg Roach	 */
186a25f0a04SGreg Roach	public static function init($locale = null) {
187a25f0a04SGreg Roach		global $WT_SESSION, $WT_TREE;
188a25f0a04SGreg Roach
189a25f0a04SGreg Roach		// The translation libraries only work with a cache.
190a25f0a04SGreg Roach		$cache_options = array(
191a25f0a04SGreg Roach			'automatic_serialization' => true,
192a25f0a04SGreg Roach			'cache_id_prefix'         => md5(WT_BASE_URL),
193a25f0a04SGreg Roach		);
194a25f0a04SGreg Roach
195a25f0a04SGreg Roach		if (ini_get('apc.enabled')) {
196a25f0a04SGreg Roach			self::$cache = Zend_Cache::factory('Core', 'Apc', $cache_options, array());
197a25f0a04SGreg Roach		} elseif (File::mkdir(WT_DATA_DIR . 'cache')) {
198a25f0a04SGreg Roach			self::$cache = Zend_Cache::factory('Core', 'File', $cache_options, array('cache_dir' => WT_DATA_DIR . 'cache'));
199a25f0a04SGreg Roach		} else {
200a25f0a04SGreg Roach			self::$cache = Zend_Cache::factory('Core', 'Zend_Cache_Backend_BlackHole', $cache_options, array(), false, true);
201a25f0a04SGreg Roach		}
202a25f0a04SGreg Roach
203a25f0a04SGreg Roach		Zend_Locale::setCache(self::$cache);
204a25f0a04SGreg Roach		Zend_Translate::setCache(self::$cache);
205a25f0a04SGreg Roach
206a25f0a04SGreg Roach		$installed_languages = self::installed_languages();
207a25f0a04SGreg Roach		if (is_null($locale) || !array_key_exists($locale, $installed_languages)) {
208a25f0a04SGreg Roach			// Automatic locale selection.
209a25f0a04SGreg Roach			if (array_key_exists(Filter::get('lang'), $installed_languages)) {
210a25f0a04SGreg Roach				// Requested in the URL?
211a25f0a04SGreg Roach				$locale = Filter::get('lang');
212a25f0a04SGreg Roach			} elseif (array_key_exists($WT_SESSION->locale, $installed_languages)) {
213a25f0a04SGreg Roach				// Rembered from a previous visit?
214a25f0a04SGreg Roach				$locale = $WT_SESSION->locale;
215a25f0a04SGreg Roach			} else {
216a25f0a04SGreg Roach				// Browser preference takes priority over gedcom default
217a25f0a04SGreg Roach				if (empty($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
218a25f0a04SGreg Roach					$prefs = array();
219a25f0a04SGreg Roach				} else {
220a25f0a04SGreg Roach					$prefs = explode(',', str_replace(' ', '', $_SERVER['HTTP_ACCEPT_LANGUAGE']));
221a25f0a04SGreg Roach				}
222a25f0a04SGreg Roach				if ($WT_TREE) {
223a25f0a04SGreg Roach					// Add the tree’s default language as a low-priority
224a25f0a04SGreg Roach					$locale  = $WT_TREE->getPreference('LANGUAGE');
225a25f0a04SGreg Roach					$prefs[] = $locale . ';q=0.2';
226a25f0a04SGreg Roach				}
227a25f0a04SGreg Roach				$prefs2 = array();
228a25f0a04SGreg Roach				foreach ($prefs as $pref) {
229a25f0a04SGreg Roach					list($l, $q) = explode(';q=', $pref . ';q=1.0');
230a25f0a04SGreg Roach					$l = preg_replace_callback(
231a25f0a04SGreg Roach						'/_[a-z][a-z]$/',
232a25f0a04SGreg Roach						function($x) { return strtoupper($x[0]); },
233a25f0a04SGreg Roach						str_replace('-', '_', $l)
234a25f0a04SGreg Roach					); // en-gb => en_GB
235a25f0a04SGreg Roach					if (array_key_exists($l, $prefs2)) {
236a25f0a04SGreg Roach						$prefs2[$l] = max((float) $q, $prefs2[$l]);
237a25f0a04SGreg Roach					} else {
238a25f0a04SGreg Roach						$prefs2[$l] = (float) $q;
239a25f0a04SGreg Roach					}
240a25f0a04SGreg Roach				}
241a25f0a04SGreg Roach				// Ensure there is a fallback.
242a25f0a04SGreg Roach				if (!array_key_exists('en_US', $prefs2)) {
243a25f0a04SGreg Roach					$prefs2['en_US'] = 0.01;
244a25f0a04SGreg Roach				}
245a25f0a04SGreg Roach				arsort($prefs2);
246a25f0a04SGreg Roach				foreach (array_keys($prefs2) as $pref) {
247a25f0a04SGreg Roach					if (array_key_exists($pref, $installed_languages)) {
248a25f0a04SGreg Roach						$locale = $pref;
249a25f0a04SGreg Roach						break;
250a25f0a04SGreg Roach					}
251a25f0a04SGreg Roach				}
252a25f0a04SGreg Roach			}
253a25f0a04SGreg Roach		}
254a25f0a04SGreg Roach
255a25f0a04SGreg Roach		// Load the translation file
256a25f0a04SGreg Roach		self::$translation_adapter = new Zend_Translate('gettext', WT_ROOT . 'language/' . $locale . '.mo', $locale);
257a25f0a04SGreg Roach
258a25f0a04SGreg Roach		// Deprecated - some custom modules use this to add translations
259a25f0a04SGreg Roach		Zend_Registry::set('Zend_Translate', self::$translation_adapter);
260a25f0a04SGreg Roach
261a25f0a04SGreg Roach		// Load any local user translations
262a25f0a04SGreg Roach		if (is_dir(WT_DATA_DIR . 'language')) {
263a25f0a04SGreg Roach			if (file_exists(WT_DATA_DIR . 'language/' . $locale . '.mo')) {
264a25f0a04SGreg Roach				self::addTranslation(
265a25f0a04SGreg Roach					new Zend_Translate('gettext', WT_DATA_DIR . 'language/' . $locale . '.mo', $locale)
266a25f0a04SGreg Roach				);
267a25f0a04SGreg Roach			}
268a25f0a04SGreg Roach			if (file_exists(WT_DATA_DIR . 'language/' . $locale . '.php')) {
269a25f0a04SGreg Roach				self::addTranslation(
270a25f0a04SGreg Roach					new Zend_Translate('array', WT_DATA_DIR . 'language/' . $locale . '.php', $locale)
271a25f0a04SGreg Roach				);
272a25f0a04SGreg Roach			}
273a25f0a04SGreg Roach			if (file_exists(WT_DATA_DIR . 'language/' . $locale . '.csv')) {
274a25f0a04SGreg Roach				self::addTranslation(
275a25f0a04SGreg Roach					new Zend_Translate('csv', WT_DATA_DIR . 'language/' . $locale . '.csv', $locale)
276a25f0a04SGreg Roach				);
277a25f0a04SGreg Roach			}
278a25f0a04SGreg Roach		}
279a25f0a04SGreg Roach
280a25f0a04SGreg Roach		// Extract language settings from the translation file
281a25f0a04SGreg Roach		global $DATE_FORMAT; // I18N: This is the format string for full dates.  See http://php.net/date for codes
282a25f0a04SGreg Roach		$DATE_FORMAT = self::noop('%j %F %Y');
283a25f0a04SGreg Roach
284a25f0a04SGreg Roach		global $TIME_FORMAT; // I18N: This is the format string for the time-of-day.  See http://php.net/date for codes
285a25f0a04SGreg Roach		$TIME_FORMAT = self::noop('%H:%i:%s');
286a25f0a04SGreg Roach
287a25f0a04SGreg Roach		// Alphabetic sorting sequence (upper-case letters), used by webtrees to sort strings
288a25f0a04SGreg Roach		list(, self::$alphabet_upper) = explode('=', self::noop('ALPHABET_upper=ABCDEFGHIJKLMNOPQRSTUVWXYZ'));
289a25f0a04SGreg Roach		// Alphabetic sorting sequence (lower-case letters), used by webtrees to sort strings
290a25f0a04SGreg Roach		list(, self::$alphabet_lower) = explode('=', self::noop('ALPHABET_lower=abcdefghijklmnopqrstuvwxyz'));
291a25f0a04SGreg Roach
292a25f0a04SGreg Roach		global $WEEK_START; // I18N: This is the first day of the week on calendars. 0=Sunday, 1=Monday...
293a25f0a04SGreg Roach		list(, $WEEK_START) = explode('=', self::noop('WEEK_START=0'));
294a25f0a04SGreg Roach
295a25f0a04SGreg Roach		global $TEXT_DIRECTION;
296a25f0a04SGreg Roach		$TEXT_DIRECTION = self::scriptDirection(self::languageScript($locale));
297a25f0a04SGreg Roach
298a25f0a04SGreg Roach		self::$locale = $locale;
299a25f0a04SGreg Roach		self::$dir    = $TEXT_DIRECTION;
300a25f0a04SGreg Roach
301a25f0a04SGreg Roach		// I18N: This punctuation is used to separate lists of items.
302a25f0a04SGreg Roach		self::$list_separator = self::translate(', ');
303a25f0a04SGreg Roach
304a25f0a04SGreg Roach		// 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
305a25f0a04SGreg Roach		self::$collation = self::translate('utf8_unicode_ci');
306a25f0a04SGreg Roach
307a25f0a04SGreg Roach		// Non-latin numbers may require non-latin digits
308a25f0a04SGreg Roach		try {
309a25f0a04SGreg Roach			self::$numbering_system = Zend_Locale_Data::getContent($locale, 'defaultnumberingsystem');
310a25f0a04SGreg Roach		} catch (Zend_Locale_Exception $ex) {
311a25f0a04SGreg Roach			// The latest CLDR database omits some languges such as Tatar (tt)
312a25f0a04SGreg Roach			self::$numbering_system = 'latin';
313a25f0a04SGreg Roach		}
314a25f0a04SGreg Roach
315a25f0a04SGreg Roach		return $locale;
316a25f0a04SGreg Roach	}
317a25f0a04SGreg Roach
318a25f0a04SGreg Roach	/**
319a25f0a04SGreg Roach	 * Add a translation file
320a25f0a04SGreg Roach	 *
321a25f0a04SGreg Roach	 * @param Zend_Translate $translation
322a25f0a04SGreg Roach	 */
323a25f0a04SGreg Roach	public static function addTranslation(Zend_Translate $translation) {
324a25f0a04SGreg Roach		self::$translation_adapter->getAdapter()->addTranslation(array('content' => $translation));
325a25f0a04SGreg Roach	}
326a25f0a04SGreg Roach
327a25f0a04SGreg Roach	/**
328a25f0a04SGreg Roach	 * Check which languages are installed
329a25f0a04SGreg Roach	 *
330a25f0a04SGreg Roach	 * @return array
331a25f0a04SGreg Roach	 */
332a25f0a04SGreg Roach	public static function installed_languages() {
333a25f0a04SGreg Roach		$mo_files = glob(WT_ROOT . 'language' . DIRECTORY_SEPARATOR . '*.mo');
334a25f0a04SGreg Roach		$cache_key = md5(serialize($mo_files));
335a25f0a04SGreg Roach
336a25f0a04SGreg Roach		if (!($installed_languages = self::$cache->load($cache_key))) {
337a25f0a04SGreg Roach			$installed_languages = array();
338a25f0a04SGreg Roach			foreach ($mo_files as $mo_file) {
339a25f0a04SGreg Roach				if (preg_match('/^(([a-z][a-z][a-z]?)([-_][A-Z][A-Z])?([-_][A-Za-z]+)*)\.mo$/', basename($mo_file), $match)) {
340a25f0a04SGreg Roach					// Sort by the transation of the base language, then the variant.
341a25f0a04SGreg Roach					// e.g. English|British English, Portuguese|Brazilian Portuguese
342a25f0a04SGreg Roach					$tmp1 = self::languageName($match[1]);
343a25f0a04SGreg Roach					if ($match[1] == $match[2]) {
344a25f0a04SGreg Roach						$tmp2 = $tmp1;
345a25f0a04SGreg Roach					} else {
346a25f0a04SGreg Roach						$tmp2 = self::languageName($match[2]);
347a25f0a04SGreg Roach					}
348a25f0a04SGreg Roach					$installed_languages[$match[1]] = $tmp2 . '|' . $tmp1;
349a25f0a04SGreg Roach				}
350a25f0a04SGreg Roach			}
351a25f0a04SGreg Roach			// Sort by the combined language/language name...
352*dd04c183SGreg Roach			uasort($installed_languages, __NAMESPACE__ . '\I18N::strcasecmp');
353a25f0a04SGreg Roach			foreach ($installed_languages as &$value) {
354a25f0a04SGreg Roach				// The locale database doesn't have translations for certain
355a25f0a04SGreg Roach				// "default" languages, such as zn_CH.
356a25f0a04SGreg Roach				if (substr($value, -1) == '|') {
357a25f0a04SGreg Roach					list($value,) = explode('|', $value);
358a25f0a04SGreg Roach				} else {
359a25f0a04SGreg Roach					list(,$value) = explode('|', $value);
360a25f0a04SGreg Roach				}
361a25f0a04SGreg Roach			}
362a25f0a04SGreg Roach			self::$cache->save($installed_languages, $cache_key);
363a25f0a04SGreg Roach		}
364a25f0a04SGreg Roach		return $installed_languages;
365a25f0a04SGreg Roach	}
366a25f0a04SGreg Roach
367a25f0a04SGreg Roach	/**
368a25f0a04SGreg Roach	 * Generate i18n markup for the <html> tag, e.g. lang="ar" dir="rtl"
369a25f0a04SGreg Roach	 *
370a25f0a04SGreg Roach	 * @return string
371a25f0a04SGreg Roach	 */
372a25f0a04SGreg Roach	public static function html_markup() {
373a25f0a04SGreg Roach		$localeData = Zend_Locale_Data::getList(self::$locale, 'layout');
374a25f0a04SGreg Roach		$dir = $localeData['characterOrder'] == 'right-to-left' ? 'rtl' : 'ltr';
375a25f0a04SGreg Roach		list($lang) = preg_split('/[-_@]/', self::$locale);
376a25f0a04SGreg Roach		return 'lang="' . $lang . '" dir="' . $dir . '"';
377a25f0a04SGreg Roach	}
378a25f0a04SGreg Roach
379a25f0a04SGreg Roach	/**
380a25f0a04SGreg Roach	 * Translate a number into the local representation.
381a25f0a04SGreg Roach	 *
382a25f0a04SGreg Roach	 * e.g. 12345.67 becomes
383a25f0a04SGreg Roach	 * en: 12,345.67
384a25f0a04SGreg Roach	 * fr: 12 345,67
385a25f0a04SGreg Roach	 * de: 12.345,67
386a25f0a04SGreg Roach	 *
387a25f0a04SGreg Roach	 * @param float   $n
388a25f0a04SGreg Roach	 * @param integer $precision
389a25f0a04SGreg Roach	 *
390a25f0a04SGreg Roach	 * @return string
391a25f0a04SGreg Roach	 */
392a25f0a04SGreg Roach	public static function number($n, $precision = 0) {
393a25f0a04SGreg Roach		// Add "punctuation" and convert digits
394a25f0a04SGreg Roach		$n = Zend_Locale_Format::toNumber($n, array('locale'=>WT_LOCALE, 'precision'=>$precision));
395a25f0a04SGreg Roach		$n = self::digits($n);
396a25f0a04SGreg Roach		return $n;
397a25f0a04SGreg Roach	}
398a25f0a04SGreg Roach
399a25f0a04SGreg Roach	/**
400a25f0a04SGreg Roach	 * Convert the digits 0-9 into the local script
401a25f0a04SGreg Roach	 *
402a25f0a04SGreg Roach	 * Used for years, etc., where we do not want thousands-separators, decimals, etc.
403a25f0a04SGreg Roach	 *
404a25f0a04SGreg Roach	 * @param integer $n
405a25f0a04SGreg Roach	 *
406a25f0a04SGreg Roach	 * @return string
407a25f0a04SGreg Roach	 */
408a25f0a04SGreg Roach	public static function digits($n) {
409a25f0a04SGreg Roach		if (self::$numbering_system != 'latn') {
410a25f0a04SGreg Roach			return Zend_Locale_Format::convertNumerals($n, 'latn', self::$numbering_system);
411a25f0a04SGreg Roach		} else {
412a25f0a04SGreg Roach			return $n;
413a25f0a04SGreg Roach		}
414a25f0a04SGreg Roach	}
415a25f0a04SGreg Roach
416a25f0a04SGreg Roach	/**
417a25f0a04SGreg Roach	 * Translate a fraction into a percentage.
418a25f0a04SGreg Roach	 *
419a25f0a04SGreg Roach	 * e.g. 0.123 becomes
420a25f0a04SGreg Roach	 * en: 12.3%
421a25f0a04SGreg Roach	 * fr: 12,3 %
422a25f0a04SGreg Roach	 * de: 12,3%
423a25f0a04SGreg Roach	 *
424a25f0a04SGreg Roach	 * @param float   $n
425a25f0a04SGreg Roach	 * @param integer $precision
426a25f0a04SGreg Roach	 *
427a25f0a04SGreg Roach	 * @return string
428a25f0a04SGreg Roach	 */
429a25f0a04SGreg Roach	public static function percentage($n, $precision = 0) {
430a25f0a04SGreg Roach		return
431a25f0a04SGreg Roach			/* 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. */
432a25f0a04SGreg Roach			self::translate('%s%%', self::number($n * 100.0, $precision));
433a25f0a04SGreg Roach	}
434a25f0a04SGreg Roach
435a25f0a04SGreg Roach	/**
436a25f0a04SGreg Roach	 * Translate a string, and then substitute placeholders
437a25f0a04SGreg Roach	 *
438a25f0a04SGreg Roach	 * echo I18N::translate('Hello World!');
439a25f0a04SGreg Roach	 * echo I18N::translate('The %s sat on the mat', 'cat');
440a25f0a04SGreg Roach	 *
441a25f0a04SGreg Roach	 * @return string
442a25f0a04SGreg Roach	 */
443a25f0a04SGreg Roach	public static function translate(/* var_args */) {
444a25f0a04SGreg Roach		$args = func_get_args();
445a25f0a04SGreg Roach		$args[0] = self::$translation_adapter->getAdapter()->_($args[0]);
446a25f0a04SGreg Roach
447a25f0a04SGreg Roach		return call_user_func_array('sprintf', $args);
448a25f0a04SGreg Roach	}
449a25f0a04SGreg Roach
450a25f0a04SGreg Roach	/**
451a25f0a04SGreg Roach	 * Context sensitive version of translate.
452a25f0a04SGreg Roach	 *
453a25f0a04SGreg Roach	 * echo I18N::translate_c('NOMINATIVE', 'January');
454a25f0a04SGreg Roach	 * echo I18N::translate_c('GENITIVE',   'January');
455a25f0a04SGreg Roach	 *
456a25f0a04SGreg Roach	 * @return string
457a25f0a04SGreg Roach	 */
458a25f0a04SGreg Roach	public static function translate_c(/* var_args */) {
459a25f0a04SGreg Roach		$args = func_get_args();
460a25f0a04SGreg Roach		$msgid = $args[0] . "\x04" . $args[1];
461a25f0a04SGreg Roach		$msgtxt = self::$translation_adapter->getAdapter()->_($msgid);
462a25f0a04SGreg Roach		if ($msgtxt == $msgid) {
463a25f0a04SGreg Roach			$msgtxt = $args[1];
464a25f0a04SGreg Roach		}
465a25f0a04SGreg Roach		$args[0] = $msgtxt;
466a25f0a04SGreg Roach		unset($args[1]);
467a25f0a04SGreg Roach
468a25f0a04SGreg Roach		return call_user_func_array('sprintf', $args);
469a25f0a04SGreg Roach	}
470a25f0a04SGreg Roach
471a25f0a04SGreg Roach	/**
472a25f0a04SGreg Roach	 * Similar to translate, but do perform "no operation" on it.
473a25f0a04SGreg Roach	 *
474a25f0a04SGreg Roach	 * This is necessary to fetch a format string (containing % characters) without
475a25f0a04SGreg Roach	 * performing sustitution of arguments.
476a25f0a04SGreg Roach	 *
477a25f0a04SGreg Roach	 * @param string $string
478a25f0a04SGreg Roach	 *
479a25f0a04SGreg Roach	 * @return string
480a25f0a04SGreg Roach	 */
481a25f0a04SGreg Roach	public static function noop($string) {
482a25f0a04SGreg Roach		return self::$translation_adapter->getAdapter()->_($string);
483a25f0a04SGreg Roach	}
484a25f0a04SGreg Roach
485a25f0a04SGreg Roach	/**
486a25f0a04SGreg Roach	 * Translate a plural string
487a25f0a04SGreg Roach	 *
488a25f0a04SGreg Roach	 * echo self::plural('There is an error', 'There are errors', $num_errors);
489a25f0a04SGreg Roach	 * echo self::plural('There is one error', 'There are %s errors', $num_errors);
490a25f0a04SGreg Roach	 * echo self::plural('There is %1$d %2$s cat', 'There are %1$d %2$s cats', $num, $num, $colour);
491a25f0a04SGreg Roach	 *
492a25f0a04SGreg Roach	 * @return string
493a25f0a04SGreg Roach	 */
494a25f0a04SGreg Roach	public static function plural(/* var_args */) {
495a25f0a04SGreg Roach		$args = func_get_args();
496a25f0a04SGreg Roach		$string = self::$translation_adapter->getAdapter()->plural($args[0], $args[1], $args[2]);
497a25f0a04SGreg Roach		array_splice($args, 0, 3, array($string));
498a25f0a04SGreg Roach
499a25f0a04SGreg Roach		return call_user_func_array('sprintf', $args);
500a25f0a04SGreg Roach	}
501a25f0a04SGreg Roach
502a25f0a04SGreg Roach	/**
503a25f0a04SGreg Roach	 * Convert a GEDCOM age string into translated_text
504a25f0a04SGreg Roach	 *
505a25f0a04SGreg Roach	 * NB: The import function will have normalised this, so we don't need
506a25f0a04SGreg Roach	 * to worry about badly formatted strings
507a25f0a04SGreg Roach	 * NOTE: this function is not yet complete - eventually it will replace get_age_at_event()
508a25f0a04SGreg Roach	 *
509a25f0a04SGreg Roach	 * @param $string
510a25f0a04SGreg Roach	 *
511a25f0a04SGreg Roach	 * @return string
512a25f0a04SGreg Roach	 */
513a25f0a04SGreg Roach	public static function gedcom_age($string) {
514a25f0a04SGreg Roach		switch ($string) {
515a25f0a04SGreg Roach		case 'STILLBORN':
516a25f0a04SGreg Roach			// I18N: Description of an individual’s age at an event.  For example, Died 14 Jan 1900 (stillborn)
517a25f0a04SGreg Roach			return self::translate('(stillborn)');
518a25f0a04SGreg Roach		case 'INFANT':
519a25f0a04SGreg Roach			// I18N: Description of an individual’s age at an event.  For example, Died 14 Jan 1900 (in infancy)
520a25f0a04SGreg Roach			return self::translate('(in infancy)');
521a25f0a04SGreg Roach		case 'CHILD':
522a25f0a04SGreg Roach			// I18N: Description of an individual’s age at an event.  For example, Died 14 Jan 1900 (in childhood)
523a25f0a04SGreg Roach			return self::translate('(in childhood)');
524a25f0a04SGreg Roach		}
525a25f0a04SGreg Roach		$age = array();
526a25f0a04SGreg Roach		if (preg_match('/(\d+)y/', $string, $match)) {
527a25f0a04SGreg Roach			// I18N: Part of an age string. e.g. 5 years, 4 months and 3 days
528a25f0a04SGreg Roach			$years = $match[1];
529a25f0a04SGreg Roach			$age[] = self::plural('%s year', '%s years', $years, self::number($years));
530a25f0a04SGreg Roach		} else {
531a25f0a04SGreg Roach			$years = -1;
532a25f0a04SGreg Roach		}
533a25f0a04SGreg Roach		if (preg_match('/(\d+)m/', $string, $match)) {
534a25f0a04SGreg Roach			// I18N: Part of an age string. e.g. 5 years, 4 months and 3 days
535a25f0a04SGreg Roach			$age[] = self::plural('%s month', '%s months', $match[1], self::number($match[1]));
536a25f0a04SGreg Roach		}
537a25f0a04SGreg Roach		if (preg_match('/(\d+)w/', $string, $match)) {
538a25f0a04SGreg Roach			// I18N: Part of an age string. e.g. 7 weeks and 3 days
539a25f0a04SGreg Roach			$age[] = self::plural('%s week', '%s weeks', $match[1], self::number($match[1]));
540a25f0a04SGreg Roach		}
541a25f0a04SGreg Roach		if (preg_match('/(\d+)d/', $string, $match)) {
542a25f0a04SGreg Roach			// I18N: Part of an age string. e.g. 5 years, 4 months and 3 days
543a25f0a04SGreg Roach			$age[] = self::plural('%s day', '%s days', $match[1], self::number($match[1]));
544a25f0a04SGreg Roach		}
545a25f0a04SGreg Roach		// If an age is just a number of years, only show the number
546a25f0a04SGreg Roach		if (count($age) == 1 && $years >= 0) {
547a25f0a04SGreg Roach			$age = $years;
548a25f0a04SGreg Roach		}
549a25f0a04SGreg Roach		if ($age) {
550a25f0a04SGreg Roach			if (!substr_compare($string, '<', 0, 1)) {
551a25f0a04SGreg Roach				// I18N: Description of an individual’s age at an event.  For example, Died 14 Jan 1900 (aged less than 21 years)
552a25f0a04SGreg Roach				return self::translate('(aged less than %s)', $age);
553a25f0a04SGreg Roach			} elseif (!substr_compare($string, '>', 0, 1)) {
554a25f0a04SGreg Roach				// I18N: Description of an individual’s age at an event.  For example, Died 14 Jan 1900 (aged more than 21 years)
555a25f0a04SGreg Roach				return self::translate('(aged more than %s)', $age);
556a25f0a04SGreg Roach			} else {
557a25f0a04SGreg Roach				// I18N: Description of an individual’s age at an event.  For example, Died 14 Jan 1900 (aged 43 years)
558a25f0a04SGreg Roach				return self::translate('(aged %s)', $age);
559a25f0a04SGreg Roach			}
560a25f0a04SGreg Roach		} else {
561a25f0a04SGreg Roach			// Not a valid string?
562a25f0a04SGreg Roach			return self::translate('(aged %s)', $string);
563a25f0a04SGreg Roach		}
564a25f0a04SGreg Roach	}
565a25f0a04SGreg Roach
566a25f0a04SGreg Roach	/**
567a25f0a04SGreg Roach	 * Convert a number of seconds into a relative time.  For example, 630 => "10 hours, 30 minutes ago"
568a25f0a04SGreg Roach	 *
569a25f0a04SGreg Roach	 * @param integer $seconds
570a25f0a04SGreg Roach	 *
571a25f0a04SGreg Roach	 * @return string
572a25f0a04SGreg Roach	 */
573a25f0a04SGreg Roach	public static function timeAgo($seconds) {
574a25f0a04SGreg Roach		$minute = 60;
575a25f0a04SGreg Roach		$hour   = 60 * $minute;
576a25f0a04SGreg Roach		$day    = 24 * $hour;
577a25f0a04SGreg Roach		$month  = 30 * $day;
578a25f0a04SGreg Roach		$year   = 365 * $day;
579a25f0a04SGreg Roach
580a25f0a04SGreg Roach		if ($seconds > $year) {
581a25f0a04SGreg Roach			$years = (int) ($seconds / $year);
582a25f0a04SGreg Roach			return self::plural('%s year ago', '%s years ago', $years, self::number($years));
583a25f0a04SGreg Roach		} elseif ($seconds > $month) {
584a25f0a04SGreg Roach			$months = (int) ($seconds / $month);
585a25f0a04SGreg Roach			return self::plural('%s month ago', '%s months ago', $months, self::number($months));
586a25f0a04SGreg Roach		} elseif ($seconds > $day) {
587a25f0a04SGreg Roach			$days = (int) ($seconds / $day);
588a25f0a04SGreg Roach			return self::plural('%s day ago', '%s days ago', $days, self::number($days));
589a25f0a04SGreg Roach		} elseif ($seconds > $hour) {
590a25f0a04SGreg Roach			$hours = (int) ($seconds / $hour);
591a25f0a04SGreg Roach			return self::plural('%s hour ago', '%s hours ago', $hours, self::number($hours));
592a25f0a04SGreg Roach		} elseif ($seconds > $minute) {
593a25f0a04SGreg Roach			$minutes = (int) ($seconds / $minute);
594a25f0a04SGreg Roach			return self::plural('%s minute ago', '%s minutes ago', $minutes, self::number($minutes));
595a25f0a04SGreg Roach		} else {
596a25f0a04SGreg Roach			return self::plural('%s second ago', '%s seconds ago', $seconds, self::number($seconds));
597a25f0a04SGreg Roach		}
598a25f0a04SGreg Roach	}
599a25f0a04SGreg Roach
600a25f0a04SGreg Roach	/**
601a25f0a04SGreg Roach	 * Return the endonym for a given language - as per http://cldr.unicode.org/
602a25f0a04SGreg Roach	 *
603a25f0a04SGreg Roach	 * @param string $locale
604a25f0a04SGreg Roach	 *
605a25f0a04SGreg Roach	 * @return string
606a25f0a04SGreg Roach	 */
607a25f0a04SGreg Roach	public static function languageName($locale) {
608a25f0a04SGreg Roach		$language_tag = str_replace(array('_', '@'), '-', $locale);
609a25f0a04SGreg Roach
610a25f0a04SGreg Roach		if (array_key_exists($language_tag, self::$language_data)) {
611a25f0a04SGreg Roach			return self::$language_data[$language_tag][1];
612a25f0a04SGreg Roach		} elseif (class_exists('\Locale')) {
613a25f0a04SGreg Roach			return \Locale::getDisplayName($locale, $locale);
614a25f0a04SGreg Roach		} else {
615a25f0a04SGreg Roach			return $locale;
616a25f0a04SGreg Roach		}
617a25f0a04SGreg Roach	}
618a25f0a04SGreg Roach
619a25f0a04SGreg Roach	/**
620a25f0a04SGreg Roach	 * Return the script used by a given language
621a25f0a04SGreg Roach	 *
622a25f0a04SGreg Roach	 * @param string $locale
623a25f0a04SGreg Roach	 *
624a25f0a04SGreg Roach	 * @return string
625a25f0a04SGreg Roach	 */
626a25f0a04SGreg Roach	public static function languageScript($locale) {
627a25f0a04SGreg Roach		$language_tag = str_replace(array('_', '@'), '-', $locale);
628a25f0a04SGreg Roach
629a25f0a04SGreg Roach		if (array_key_exists($language_tag, self::$language_data)) {
630a25f0a04SGreg Roach			return self::$language_data[$language_tag][0];
631a25f0a04SGreg Roach		} else {
632a25f0a04SGreg Roach			return 'Latn';
633a25f0a04SGreg Roach		}
634a25f0a04SGreg Roach	}
635a25f0a04SGreg Roach
636a25f0a04SGreg Roach	/**
637a25f0a04SGreg Roach	 * Identify the script used for a piece of text
638a25f0a04SGreg Roach	 *
639a25f0a04SGreg Roach	 * @param $string
640a25f0a04SGreg Roach	 *
641a25f0a04SGreg Roach	 * @return string
642a25f0a04SGreg Roach	 */
643a25f0a04SGreg Roach	public static function textScript($string) {
644a25f0a04SGreg Roach		$string = strip_tags($string); // otherwise HTML tags show up as latin
645a25f0a04SGreg Roach		$string = html_entity_decode($string, ENT_QUOTES, 'UTF-8'); // otherwise HTML entities show up as latin
646a25f0a04SGreg Roach		$string = str_replace(array('@N.N.', '@P.N.'), '', $string); // otherwise unknown names show up as latin
647a25f0a04SGreg Roach		$pos = 0;
648a25f0a04SGreg Roach		$strlen = strlen($string);
649a25f0a04SGreg Roach		while ($pos < $strlen) {
650a25f0a04SGreg Roach			// get the Unicode Code Point for the character at position $pos
651a25f0a04SGreg Roach			$byte1 = ord($string[$pos]);
652a25f0a04SGreg Roach			if ($byte1 < 0x80) {
653a25f0a04SGreg Roach				$code_point = $byte1;
654a25f0a04SGreg Roach				$chrlen = 1;
655a25f0a04SGreg Roach			} elseif ($byte1 < 0xC0) {
656a25f0a04SGreg Roach				// Invalid continuation character
657a25f0a04SGreg Roach				return 'Latn';
658a25f0a04SGreg Roach			} elseif ($byte1 < 0xE0) {
659a25f0a04SGreg Roach				$code_point = (($byte1 & 0x1F) << 6) + (ord($string[$pos + 1]) & 0x3F);
660a25f0a04SGreg Roach				$chrlen = 2;
661a25f0a04SGreg Roach			} elseif ($byte1 < 0xF0) {
662a25f0a04SGreg Roach				$code_point = (($byte1 & 0x0F) << 12) + ((ord($string[$pos + 1]) & 0x3F) << 6) + (ord($string[$pos + 2]) & 0x3F);
663a25f0a04SGreg Roach				$chrlen = 3;
664a25f0a04SGreg Roach			} elseif ($byte1 < 0xF8) {
665a25f0a04SGreg Roach				$code_point = (($byte1 & 0x07) << 24) + ((ord($string[$pos + 1]) & 0x3F) << 12) + ((ord($string[$pos + 2]) & 0x3F) << 6) + (ord($string[$pos + 3]) & 0x3F);
666a25f0a04SGreg Roach				$chrlen = 3;
667a25f0a04SGreg Roach 			} else {
668a25f0a04SGreg Roach				// Invalid UTF
669a25f0a04SGreg Roach				return 'Latn';
670a25f0a04SGreg Roach			}
671a25f0a04SGreg Roach
672a25f0a04SGreg Roach			foreach (self::$scripts as $range) {
673a25f0a04SGreg Roach				if ($code_point >= $range[1] && $code_point <= $range[2]) {
674a25f0a04SGreg Roach					return $range[0];
675a25f0a04SGreg Roach				}
676a25f0a04SGreg Roach			}
677a25f0a04SGreg Roach			// Not a recognised script.  Maybe punctuation, spacing, etc.  Keep looking.
678a25f0a04SGreg Roach			$pos += $chrlen;
679a25f0a04SGreg Roach		}
680a25f0a04SGreg Roach
681a25f0a04SGreg Roach		return 'Latn';
682a25f0a04SGreg Roach	}
683a25f0a04SGreg Roach
684a25f0a04SGreg Roach	/**
685a25f0a04SGreg Roach	 * Return the direction (ltr or rtl) for a given script
686a25f0a04SGreg Roach	 *
687a25f0a04SGreg Roach	 * The PHP/intl library does not provde this information, so we need
688a25f0a04SGreg Roach	 * our own lookup table.
689a25f0a04SGreg Roach	 *
690a25f0a04SGreg Roach	 * @param string $script
691a25f0a04SGreg Roach	 *
692a25f0a04SGreg Roach	 * @return string
693a25f0a04SGreg Roach	 */
694a25f0a04SGreg Roach	public static function scriptDirection($script) {
695a25f0a04SGreg Roach		switch ($script) {
696a25f0a04SGreg Roach		case 'Arab':
697a25f0a04SGreg Roach		case 'Hebr':
698a25f0a04SGreg Roach		case 'Mong':
699a25f0a04SGreg Roach		case 'Thaa':
700a25f0a04SGreg Roach			return 'rtl';
701a25f0a04SGreg Roach		default:
702a25f0a04SGreg Roach			return 'ltr';
703a25f0a04SGreg Roach		}
704a25f0a04SGreg Roach	}
705a25f0a04SGreg Roach
706a25f0a04SGreg Roach	/**
707a25f0a04SGreg Roach	 * UTF8 version of PHP::strtoupper()
708a25f0a04SGreg Roach	 *
709a25f0a04SGreg Roach	 * Convert a string to upper case, using the rules from the current locale
710a25f0a04SGreg Roach	 *
711a25f0a04SGreg Roach	 * @param string $string
712a25f0a04SGreg Roach	 *
713a25f0a04SGreg Roach	 * @return string
714a25f0a04SGreg Roach	 */
715a25f0a04SGreg Roach	public static function strtoupper($string) {
716a25f0a04SGreg Roach		if (self::$locale == 'tr' || self::$locale == 'az') {
717a25f0a04SGreg Roach			return TurkishUtf8::strtoupper($string);
718a25f0a04SGreg Roach		} else {
719a25f0a04SGreg Roach			return mb_strtoupper($string);
720a25f0a04SGreg Roach		}
721a25f0a04SGreg Roach	}
722a25f0a04SGreg Roach
723a25f0a04SGreg Roach	/**
724a25f0a04SGreg Roach	 * UTF8 version of PHP::strtolower()
725a25f0a04SGreg Roach	 *
726a25f0a04SGreg Roach	 * Convert a string to lower case, using the rules from the current locale
727a25f0a04SGreg Roach	 *
728a25f0a04SGreg Roach	 * @param string $string
729a25f0a04SGreg Roach	 *
730a25f0a04SGreg Roach	 * @return string
731a25f0a04SGreg Roach	 */
732a25f0a04SGreg Roach	public static function strtolower($string) {
733a25f0a04SGreg Roach		if (self::$locale == 'tr' || self::$locale == 'az') {
734a25f0a04SGreg Roach			return TurkishUtf8::strtolower($string);
735a25f0a04SGreg Roach		} else {
736a25f0a04SGreg Roach			return mb_strtolower($string);
737a25f0a04SGreg Roach		}
738a25f0a04SGreg Roach	}
739a25f0a04SGreg Roach
740a25f0a04SGreg Roach	/**
741a25f0a04SGreg Roach	 * UTF8 version of PHP::strcasecmp()
742a25f0a04SGreg Roach	 *
743a25f0a04SGreg Roach	 * Perform a case-insensitive comparison of two strings, using rules from the current locale
744a25f0a04SGreg Roach	 *
745a25f0a04SGreg Roach	 * @param string $string1
746a25f0a04SGreg Roach	 * @param string $string2
747a25f0a04SGreg Roach	 *
748a25f0a04SGreg Roach	 * @return integer
749a25f0a04SGreg Roach	 */
750a25f0a04SGreg Roach	public static function strcasecmp($string1, $string2) {
751a25f0a04SGreg Roach		$strpos1 = 0;
752a25f0a04SGreg Roach		$strpos2 = 0;
753a25f0a04SGreg Roach		$strlen1 = strlen($string1);
754a25f0a04SGreg Roach		$strlen2 = strlen($string2);
755a25f0a04SGreg Roach		while ($strpos1 < $strlen1 && $strpos2 < $strlen2) {
756a25f0a04SGreg Roach			$byte1 = ord($string1[$strpos1]);
757a25f0a04SGreg Roach			$byte2 = ord($string2[$strpos2]);
758a25f0a04SGreg Roach			if (($byte1 & 0xE0) == 0xC0) {
759a25f0a04SGreg Roach				$chr1 = $string1[$strpos1++] . $string1[$strpos1++];
760a25f0a04SGreg Roach			} elseif (($byte1 & 0xF0) == 0xE0) {
761a25f0a04SGreg Roach				$chr1 = $string1[$strpos1++] . $string1[$strpos1++] . $string1[$strpos1++];
762a25f0a04SGreg Roach			} else {
763a25f0a04SGreg Roach				$chr1 = $string1[$strpos1++];
764a25f0a04SGreg Roach			}
765a25f0a04SGreg Roach			if (($byte2 & 0xE0) == 0xC0) {
766a25f0a04SGreg Roach				$chr2 = $string2[$strpos2++] . $string2[$strpos2++];
767a25f0a04SGreg Roach			} elseif (($byte2 & 0xF0) == 0xE0) {
768a25f0a04SGreg Roach				$chr2 = $string2[$strpos2++] . $string2[$strpos2++] . $string2[$strpos2++];
769a25f0a04SGreg Roach			} else {
770a25f0a04SGreg Roach				$chr2 = $string2[$strpos2++];
771a25f0a04SGreg Roach			}
772a25f0a04SGreg Roach			if ($chr1 == $chr2) {
773a25f0a04SGreg Roach				continue;
774a25f0a04SGreg Roach			}
775a25f0a04SGreg Roach			// Try the local alphabet first
776a25f0a04SGreg Roach			$offset1 = strpos(self::$alphabet_lower, $chr1);
777a25f0a04SGreg Roach			if ($offset1 === false) {
778a25f0a04SGreg Roach				$offset1 = strpos(self::$alphabet_upper, $chr1);
779a25f0a04SGreg Roach			}
780a25f0a04SGreg Roach			$offset2 = strpos(self::$alphabet_lower, $chr2);
781a25f0a04SGreg Roach			if ($offset2 === false) {
782a25f0a04SGreg Roach				$offset2 = strpos(self::$alphabet_upper, $chr2);
783a25f0a04SGreg Roach			}
784a25f0a04SGreg Roach			if ($offset1 !== false && $offset2 !== false) {
785a25f0a04SGreg Roach				if ($offset1 == $offset2) {
786a25f0a04SGreg Roach					continue;
787a25f0a04SGreg Roach				} else {
788a25f0a04SGreg Roach					return $offset1 - $offset2;
789a25f0a04SGreg Roach				}
790a25f0a04SGreg Roach			}
791a25f0a04SGreg Roach			// Try the global alphabet next
792a25f0a04SGreg Roach			$offset1 = strpos(self::ALPHABET_LOWER, $chr1);
793a25f0a04SGreg Roach			if ($offset1 === false) {
794a25f0a04SGreg Roach				$offset1 = strpos(self::ALPHABET_UPPER, $chr1);
795a25f0a04SGreg Roach			}
796a25f0a04SGreg Roach			$offset2 = strpos(self::ALPHABET_LOWER, $chr2);
797a25f0a04SGreg Roach			if ($offset2 === false) {
798a25f0a04SGreg Roach				$offset2 = strpos(self::ALPHABET_UPPER, $chr2);
799a25f0a04SGreg Roach			}
800a25f0a04SGreg Roach			if ($offset1 !== false && $offset2 !== false) {
801a25f0a04SGreg Roach				if ($offset1 == $offset2) {
802a25f0a04SGreg Roach					continue;
803a25f0a04SGreg Roach				} else {
804a25f0a04SGreg Roach					return $offset1 - $offset2;
805a25f0a04SGreg Roach				}
806a25f0a04SGreg Roach			}
807a25f0a04SGreg Roach			// Just compare by unicode order
808a25f0a04SGreg Roach			return strcmp($chr1, $chr2);
809a25f0a04SGreg Roach		}
810a25f0a04SGreg Roach		// Shortest string comes first.
811a25f0a04SGreg Roach		return ($strlen1 - $strpos1) - ($strlen2 - $strpos2);
812a25f0a04SGreg Roach	}
813a25f0a04SGreg Roach
814a25f0a04SGreg Roach	/**
815a25f0a04SGreg Roach	 * UTF8 version of PHP::strrev()
816a25f0a04SGreg Roach	 *
817a25f0a04SGreg Roach	 * Reverse RTL text for third-party libraries such as GD2 and googlechart.
818a25f0a04SGreg Roach	 *
819a25f0a04SGreg Roach	 * These do not support UTF8 text direction, so we must mimic it for them.
820a25f0a04SGreg Roach	 *
821a25f0a04SGreg Roach	 * Numbers are always rendered LTR, even in RTL text.
822a25f0a04SGreg Roach	 * The visual direction of characters such as parentheses should be reversed.
823a25f0a04SGreg Roach	 *
824a25f0a04SGreg Roach	 * @param string $text Text to be reversed
825a25f0a04SGreg Roach	 *
826a25f0a04SGreg Roach	 * @return string
827a25f0a04SGreg Roach	 */
828a25f0a04SGreg Roach	public static function reverseText($text) {
829a25f0a04SGreg Roach		// Remove HTML markup - we can't display it and it is LTR.
830a25f0a04SGreg Roach		$text = Filter::unescapeHtml($text);
831a25f0a04SGreg Roach
832a25f0a04SGreg Roach		// LTR text doesn't need reversing
833a25f0a04SGreg Roach		if (self::scriptDirection(self::textScript($text)) == 'ltr') {
834a25f0a04SGreg Roach			return $text;
835a25f0a04SGreg Roach		}
836a25f0a04SGreg Roach
837a25f0a04SGreg Roach		// Mirrored characters
838a25f0a04SGreg Roach		$text = strtr($text, self::$mirror_characters);
839a25f0a04SGreg Roach
840a25f0a04SGreg Roach		$reversed = '';
841a25f0a04SGreg Roach		$digits = '';
842a25f0a04SGreg Roach		while ($text != '') {
843a25f0a04SGreg Roach			$letter = mb_substr($text, 0, 1);
844a25f0a04SGreg Roach			$text = mb_substr($text, 1);
845a25f0a04SGreg Roach			if (strpos(self::DIGITS, $letter) !== false) {
846a25f0a04SGreg Roach				$digits .= $letter;
847a25f0a04SGreg Roach			} else {
848a25f0a04SGreg Roach				$reversed = $letter . $digits . $reversed;
849a25f0a04SGreg Roach				$digits = '';
850a25f0a04SGreg Roach			}
851a25f0a04SGreg Roach		}
852a25f0a04SGreg Roach
853a25f0a04SGreg Roach		return $digits . $reversed;
854a25f0a04SGreg Roach	}
855a25f0a04SGreg Roach
856a25f0a04SGreg Roach	/**
857a25f0a04SGreg Roach	 * Generate consistent I18N for datatables.js
858a25f0a04SGreg Roach	 *
859a25f0a04SGreg Roach	 * @param array|null $lengths An optional array of page lengths
860a25f0a04SGreg Roach	 *
861a25f0a04SGreg Roach	 * @return string
862a25f0a04SGreg Roach	 */
863a25f0a04SGreg Roach	public static function datatablesI18N(array $lengths = null) {
864a25f0a04SGreg Roach		if ($lengths === null) {
865a25f0a04SGreg Roach			$lengths = array(10, 20, 30, 50, 100, -1);
866a25f0a04SGreg Roach		}
867a25f0a04SGreg Roach
868a25f0a04SGreg Roach		$length_menu = '';
869a25f0a04SGreg Roach		foreach ($lengths as $length) {
870a25f0a04SGreg Roach			$length_menu .=
871a25f0a04SGreg Roach				'<option value="' . $length . '">' .
872a25f0a04SGreg Roach				($length == -1 ? /* I18N: listbox option, e.g. “10,25,50,100,all” */ self::translate('All') : self::number($length)) .
873a25f0a04SGreg Roach				'</option>';
874a25f0a04SGreg Roach		}
875a25f0a04SGreg Roach		$length_menu = '<select>' . $length_menu . '</select>';
876a25f0a04SGreg Roach		$length_menu = /* I18N: Display %s [records per page], %s is a placeholder for listbox containing numeric options */ self::translate('Display %s', $length_menu);
877a25f0a04SGreg Roach
878a25f0a04SGreg Roach		// Which symbol is used for separating numbers into groups
879a25f0a04SGreg Roach		$symbols = Zend_Locale_Data::getList(self::$locale, 'symbols');
880a25f0a04SGreg Roach		// Which digits are used for numbers
881a25f0a04SGreg Roach		$digits = Zend_Locale_Data::getContent(self::$locale, 'numberingsystem', self::$numbering_system);
882a25f0a04SGreg Roach
883a25f0a04SGreg Roach		if ($digits == '0123456789') {
884a25f0a04SGreg Roach			$callback = '';
885a25f0a04SGreg Roach		} else {
886a25f0a04SGreg Roach			$callback = ',
887a25f0a04SGreg Roach				"infoCallback": function(oSettings, iStart, iEnd, iMax, iTotal, sPre) {
888a25f0a04SGreg Roach					return sPre
889a25f0a04SGreg Roach						.replace(/0/g, "'.mb_substr($digits, 0, 1) . '")
890a25f0a04SGreg Roach						.replace(/1/g, "'.mb_substr($digits, 1, 1) . '")
891a25f0a04SGreg Roach						.replace(/2/g, "'.mb_substr($digits, 2, 1) . '")
892a25f0a04SGreg Roach						.replace(/3/g, "'.mb_substr($digits, 3, 1) . '")
893a25f0a04SGreg Roach						.replace(/4/g, "'.mb_substr($digits, 4, 1) . '")
894a25f0a04SGreg Roach						.replace(/5/g, "'.mb_substr($digits, 5, 1) . '")
895a25f0a04SGreg Roach						.replace(/6/g, "'.mb_substr($digits, 6, 1) . '")
896a25f0a04SGreg Roach						.replace(/7/g, "'.mb_substr($digits, 7, 1) . '")
897a25f0a04SGreg Roach						.replace(/8/g, "'.mb_substr($digits, 8, 1) . '")
898a25f0a04SGreg Roach						.replace(/9/g, "'.mb_substr($digits, 9, 1) . '");
899a25f0a04SGreg Roach				},
900a25f0a04SGreg Roach				"formatNumber": function(iIn) {
901a25f0a04SGreg Roach					return String(iIn)
902a25f0a04SGreg Roach						.replace(/0/g, "'.mb_substr($digits, 0, 1) . '")
903a25f0a04SGreg Roach						.replace(/1/g, "'.mb_substr($digits, 1, 1) . '")
904a25f0a04SGreg Roach						.replace(/2/g, "'.mb_substr($digits, 2, 1) . '")
905a25f0a04SGreg Roach						.replace(/3/g, "'.mb_substr($digits, 3, 1) . '")
906a25f0a04SGreg Roach						.replace(/4/g, "'.mb_substr($digits, 4, 1) . '")
907a25f0a04SGreg Roach						.replace(/5/g, "'.mb_substr($digits, 5, 1) . '")
908a25f0a04SGreg Roach						.replace(/6/g, "'.mb_substr($digits, 6, 1) . '")
909a25f0a04SGreg Roach						.replace(/7/g, "'.mb_substr($digits, 7, 1) . '")
910a25f0a04SGreg Roach						.replace(/8/g, "'.mb_substr($digits, 8, 1) . '")
911a25f0a04SGreg Roach						.replace(/9/g, "'.mb_substr($digits, 9, 1) . '");
912a25f0a04SGreg Roach				}
913a25f0a04SGreg Roach			';
914a25f0a04SGreg Roach		}
915a25f0a04SGreg Roach
916a25f0a04SGreg Roach		return
917a25f0a04SGreg Roach			'"language": {' .
918a25f0a04SGreg Roach			' "paginate": {' .
919a25f0a04SGreg Roach			'  "first":    "' . /* I18N: button label, first page    */ self::translate('first') . '",' .
920a25f0a04SGreg Roach			'  "last":     "' . /* I18N: button label, last page     */ self::translate('last') . '",' .
921a25f0a04SGreg Roach			'  "next":     "' . /* I18N: button label, next page     */ self::translate('next') . '",' .
922a25f0a04SGreg Roach			'  "previous": "' . /* I18N: button label, previous page */ self::translate('previous') . '"' .
923a25f0a04SGreg Roach			' },' .
924a25f0a04SGreg Roach			' "emptyTable":     "' . self::translate('No records to display') . '",' .
925a25f0a04SGreg Roach			' "info":           "' . /* I18N: %s are placeholders for numbers */ self::translate('Showing %1$s to %2$s of %3$s', '_START_', '_END_', '_TOTAL_') . '",' .
926a25f0a04SGreg Roach			' "infoEmpty":      "' . self::translate('Showing %1$s to %2$s of %3$s', 0, 0, 0) . '",' .
927a25f0a04SGreg Roach			' "infoFiltered":   "' . /* I18N: %s is a placeholder for a number */ self::translate('(filtered from %s total entries)', '_MAX_') . '",' .
928a25f0a04SGreg Roach			' "infoPostfix":    "",' .
929a25f0a04SGreg Roach			' "infoThousands":  "' . $symbols['group'] . '",' .
930a25f0a04SGreg Roach			' "lengthMenu":     "' . Filter::escapeJs($length_menu) . '",' .
931a25f0a04SGreg Roach			' "loadingRecords": "' . self::translate('Loading…') . '",' .
932a25f0a04SGreg Roach			' "processing":     "' . self::translate('Loading…') . '",' .
933a25f0a04SGreg Roach			' "search":         "' . self::translate('Filter') . '",' .
934a25f0a04SGreg Roach			' "url":            "",' .
935a25f0a04SGreg Roach			' "zeroRecords":    "' . self::translate('No records to display') . '"' .
936a25f0a04SGreg Roach			'}' .
937a25f0a04SGreg Roach			$callback;
938a25f0a04SGreg Roach	}
939a25f0a04SGreg Roach}
940