xref: /webtrees/app/I18N.php (revision 5331c5ea113ac4a3b341c881b1a62b7f9f823399)
1a25f0a04SGreg Roach<?php
2dd04c183SGreg 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
19c999a340SGreg Roachuse Fisharebest\Localization\Locale;
20a25f0a04SGreg Roachuse Patchwork\TurkishUtf8;
21a25f0a04SGreg Roachuse Zend_Cache;
22a25f0a04SGreg Roachuse Zend_Cache_Core;
23a25f0a04SGreg Roachuse Zend_Registry;
24a25f0a04SGreg Roachuse Zend_Translate;
25a25f0a04SGreg Roach
26a25f0a04SGreg Roach/**
27a25f0a04SGreg Roach * Class I18N - Functions to support internationalization (i18n) functionality.
28a25f0a04SGreg Roach */
29a25f0a04SGreg Roachclass I18N {
30c999a340SGreg Roach	/** @var Locale The current locale (e.g. LocaleEnGb) */
31c999a340SGreg Roach	private static $locale;
32c999a340SGreg Roach
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 MySQL collation sequence used by this language, typically utf8_unicode_ci */
94a25f0a04SGreg Roach	public static $collation;
95a25f0a04SGreg Roach
96a25f0a04SGreg Roach	/** @var string Punctuation used to separate list items, typically a comma */
97a25f0a04SGreg Roach	public static $list_separator;
98a25f0a04SGreg Roach
99a25f0a04SGreg Roach	/** @var Zend_Cache_Core */
100a25f0a04SGreg Roach	private static $cache;
101a25f0a04SGreg Roach
102a25f0a04SGreg Roach	/** @var Zend_Translate */
103a25f0a04SGreg Roach	private static $translation_adapter;
104a25f0a04SGreg Roach
105a25f0a04SGreg Roach	/**
106a25f0a04SGreg Roach	 * Initialise the translation adapter with a locale setting.
107a25f0a04SGreg Roach	 *
108a25f0a04SGreg Roach	 * @param string|null $locale If no locale specified, choose one automatically
109a25f0a04SGreg Roach	 *
110a25f0a04SGreg Roach	 * @return string $string
111a25f0a04SGreg Roach	 */
112a25f0a04SGreg Roach	public static function init($locale = null) {
113a25f0a04SGreg Roach		global $WT_SESSION, $WT_TREE;
114a25f0a04SGreg Roach
115a25f0a04SGreg Roach		// The translation libraries only work with a cache.
116a25f0a04SGreg Roach		$cache_options = array(
117a25f0a04SGreg Roach			'automatic_serialization' => true,
118a25f0a04SGreg Roach			'cache_id_prefix'         => md5(WT_BASE_URL),
119a25f0a04SGreg Roach		);
120a25f0a04SGreg Roach
121a25f0a04SGreg Roach		if (ini_get('apc.enabled')) {
122a25f0a04SGreg Roach			self::$cache = Zend_Cache::factory('Core', 'Apc', $cache_options, array());
123a25f0a04SGreg Roach		} elseif (File::mkdir(WT_DATA_DIR . 'cache')) {
124a25f0a04SGreg Roach			self::$cache = Zend_Cache::factory('Core', 'File', $cache_options, array('cache_dir' => WT_DATA_DIR . 'cache'));
125a25f0a04SGreg Roach		} else {
126a25f0a04SGreg Roach			self::$cache = Zend_Cache::factory('Core', 'Zend_Cache_Backend_BlackHole', $cache_options, array(), false, true);
127a25f0a04SGreg Roach		}
128a25f0a04SGreg Roach
129a25f0a04SGreg Roach		Zend_Translate::setCache(self::$cache);
130a25f0a04SGreg Roach
131c999a340SGreg Roach
132c999a340SGreg Roach		$installed_locales = array();
133c999a340SGreg Roach		foreach (self::installedLocales() as $installed_locale) {
134c999a340SGreg Roach			$installed_locales[$installed_locale->languageTag()] = $installed_locale->endonym();
135c999a340SGreg Roach		}
136c999a340SGreg Roach
137c999a340SGreg Roach		if (is_null($locale) || !array_key_exists($locale, $installed_locales)) {
138a25f0a04SGreg Roach			// Automatic locale selection.
139c999a340SGreg Roach			if (array_key_exists(Filter::get('lang'), $installed_locales)) {
140a25f0a04SGreg Roach				// Requested in the URL?
141a25f0a04SGreg Roach				$locale = Filter::get('lang');
142c999a340SGreg Roach			} elseif (array_key_exists($WT_SESSION->locale, $installed_locales)) {
143a25f0a04SGreg Roach				// Rembered from a previous visit?
144a25f0a04SGreg Roach				$locale = $WT_SESSION->locale;
145a25f0a04SGreg Roach			} else {
146a25f0a04SGreg Roach				// Browser preference takes priority over gedcom default
147a25f0a04SGreg Roach				if (empty($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
148a25f0a04SGreg Roach					$prefs = array();
149a25f0a04SGreg Roach				} else {
150a25f0a04SGreg Roach					$prefs = explode(',', str_replace(' ', '', $_SERVER['HTTP_ACCEPT_LANGUAGE']));
151a25f0a04SGreg Roach				}
152a25f0a04SGreg Roach				if ($WT_TREE) {
153a25f0a04SGreg Roach					// Add the tree’s default language as a low-priority
154a25f0a04SGreg Roach					$locale  = $WT_TREE->getPreference('LANGUAGE');
155a25f0a04SGreg Roach					$prefs[] = $locale . ';q=0.2';
156a25f0a04SGreg Roach				}
157a25f0a04SGreg Roach				$prefs2 = array();
158a25f0a04SGreg Roach				foreach ($prefs as $pref) {
159a25f0a04SGreg Roach					list($l, $q) = explode(';q=', $pref . ';q=1.0');
160a25f0a04SGreg Roach					$l = preg_replace_callback(
161a25f0a04SGreg Roach						'/_[a-z][a-z]$/',
162a25f0a04SGreg Roach						function($x) { return strtoupper($x[0]); },
163a25f0a04SGreg Roach						str_replace('-', '_', $l)
164a25f0a04SGreg Roach					); // en-gb => en_GB
165a25f0a04SGreg Roach					if (array_key_exists($l, $prefs2)) {
166a25f0a04SGreg Roach						$prefs2[$l] = max((float) $q, $prefs2[$l]);
167a25f0a04SGreg Roach					} else {
168a25f0a04SGreg Roach						$prefs2[$l] = (float) $q;
169a25f0a04SGreg Roach					}
170a25f0a04SGreg Roach				}
171a25f0a04SGreg Roach				// Ensure there is a fallback.
172c999a340SGreg Roach				if (!array_key_exists('en-US', $prefs2)) {
173c999a340SGreg Roach					$prefs2['en-US'] = 0.01;
174a25f0a04SGreg Roach				}
175a25f0a04SGreg Roach				arsort($prefs2);
176a25f0a04SGreg Roach				foreach (array_keys($prefs2) as $pref) {
177c999a340SGreg Roach					if (array_key_exists($pref, $installed_locales)) {
178a25f0a04SGreg Roach						$locale = $pref;
179a25f0a04SGreg Roach						break;
180a25f0a04SGreg Roach					}
181a25f0a04SGreg Roach				}
182a25f0a04SGreg Roach			}
183a25f0a04SGreg Roach		}
184a25f0a04SGreg Roach
185a25f0a04SGreg Roach		// Load the translation file
186a25f0a04SGreg Roach		self::$translation_adapter = new Zend_Translate('gettext', WT_ROOT . 'language/' . $locale . '.mo', $locale);
187a25f0a04SGreg Roach
188a25f0a04SGreg Roach		// Deprecated - some custom modules use this to add translations
189a25f0a04SGreg Roach		Zend_Registry::set('Zend_Translate', self::$translation_adapter);
190a25f0a04SGreg Roach
191a25f0a04SGreg Roach		// Load any local user translations
192a25f0a04SGreg Roach		if (is_dir(WT_DATA_DIR . 'language')) {
193a25f0a04SGreg Roach			if (file_exists(WT_DATA_DIR . 'language/' . $locale . '.mo')) {
194a25f0a04SGreg Roach				self::addTranslation(
195a25f0a04SGreg Roach					new Zend_Translate('gettext', WT_DATA_DIR . 'language/' . $locale . '.mo', $locale)
196a25f0a04SGreg Roach				);
197a25f0a04SGreg Roach			}
198a25f0a04SGreg Roach			if (file_exists(WT_DATA_DIR . 'language/' . $locale . '.php')) {
199a25f0a04SGreg Roach				self::addTranslation(
200a25f0a04SGreg Roach					new Zend_Translate('array', WT_DATA_DIR . 'language/' . $locale . '.php', $locale)
201a25f0a04SGreg Roach				);
202a25f0a04SGreg Roach			}
203a25f0a04SGreg Roach			if (file_exists(WT_DATA_DIR . 'language/' . $locale . '.csv')) {
204a25f0a04SGreg Roach				self::addTranslation(
205a25f0a04SGreg Roach					new Zend_Translate('csv', WT_DATA_DIR . 'language/' . $locale . '.csv', $locale)
206a25f0a04SGreg Roach				);
207a25f0a04SGreg Roach			}
208a25f0a04SGreg Roach		}
209a25f0a04SGreg Roach
210a25f0a04SGreg Roach		// Extract language settings from the translation file
211a25f0a04SGreg Roach		global $DATE_FORMAT; // I18N: This is the format string for full dates.  See http://php.net/date for codes
212a25f0a04SGreg Roach		$DATE_FORMAT = self::noop('%j %F %Y');
213a25f0a04SGreg Roach
214a25f0a04SGreg Roach		global $TIME_FORMAT; // I18N: This is the format string for the time-of-day.  See http://php.net/date for codes
215a25f0a04SGreg Roach		$TIME_FORMAT = self::noop('%H:%i:%s');
216a25f0a04SGreg Roach
217a25f0a04SGreg Roach		// Alphabetic sorting sequence (upper-case letters), used by webtrees to sort strings
218a25f0a04SGreg Roach		list(, self::$alphabet_upper) = explode('=', self::noop('ALPHABET_upper=ABCDEFGHIJKLMNOPQRSTUVWXYZ'));
219a25f0a04SGreg Roach		// Alphabetic sorting sequence (lower-case letters), used by webtrees to sort strings
220a25f0a04SGreg Roach		list(, self::$alphabet_lower) = explode('=', self::noop('ALPHABET_lower=abcdefghijklmnopqrstuvwxyz'));
221a25f0a04SGreg Roach
222a25f0a04SGreg Roach		global $WEEK_START; // I18N: This is the first day of the week on calendars. 0=Sunday, 1=Monday...
223a25f0a04SGreg Roach		list(, $WEEK_START) = explode('=', self::noop('WEEK_START=0'));
224a25f0a04SGreg Roach
225a25f0a04SGreg Roach
2263cfaf201SGreg Roach		// Save the current locale, and some attributes of it
227c999a340SGreg Roach		self::$locale = Locale::create($locale);
228c999a340SGreg Roach
2293cfaf201SGreg Roach		self::$list_separator = /* I18N: This punctuation is used to separate lists of items */ self::translate(', ');
2303cfaf201SGreg Roach		self::$collation      = /* 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::translate('utf8_unicode_ci');
231a25f0a04SGreg Roach
232*5331c5eaSGreg Roach		return self::$locale->languageTag();
233a25f0a04SGreg Roach	}
234a25f0a04SGreg Roach
235a25f0a04SGreg Roach	/**
236a25f0a04SGreg Roach	 * Add a translation file
237a25f0a04SGreg Roach	 *
238a25f0a04SGreg Roach	 * @param Zend_Translate $translation
239a25f0a04SGreg Roach	 */
240a25f0a04SGreg Roach	public static function addTranslation(Zend_Translate $translation) {
241a25f0a04SGreg Roach		self::$translation_adapter->getAdapter()->addTranslation(array('content' => $translation));
242a25f0a04SGreg Roach	}
243a25f0a04SGreg Roach
244a25f0a04SGreg Roach	/**
245c999a340SGreg Roach	 * The prefered locales for this site, or a default list if no preference.
246a25f0a04SGreg Roach	 *
247c999a340SGreg Roach	 * @return Locale[]
248a25f0a04SGreg Roach	 */
249c999a340SGreg Roach	public static function activeLocales() {
250c999a340SGreg Roach		$code_list = Site::getPreference('LANGUAGES');
251a25f0a04SGreg Roach
252c999a340SGreg Roach		if ($code_list) {
253c999a340SGreg Roach			$codes = explode(',', $code_list);
254a25f0a04SGreg Roach		} else {
255c999a340SGreg Roach			$codes = array(
256c999a340SGreg Roach				'ar', 'bg', 'bs', 'ca', 'cs', 'da', 'de', 'el', 'en-GB', 'en-US', 'es',
257c999a340SGreg Roach				'et', 'fi', 'fr', 'he', 'hr', 'hu', 'is', 'it', 'ka', 'lt', 'mr', 'nb',
258c999a340SGreg Roach				'nl', 'nn', 'pl', 'pt', 'ru', 'sk', 'sv', 'tr', 'uk', 'vi', 'zh-Hans',
259c999a340SGreg Roach			);
260a25f0a04SGreg Roach		}
261c999a340SGreg Roach
262c999a340SGreg Roach		$locales = array();
263c999a340SGreg Roach		foreach ($codes as $code) {
264c999a340SGreg Roach			if (file_exists(WT_ROOT . 'language/' . $code . '.mo')) {
265c999a340SGreg Roach				try {
266c999a340SGreg Roach					$locales[] = Locale::create($code);
267c999a340SGreg Roach				} catch (\Exception $ex) {
268c999a340SGreg Roach					// No such locale exists?
269a25f0a04SGreg Roach				}
270a25f0a04SGreg Roach			}
271c999a340SGreg Roach		}
272c999a340SGreg Roach		usort($locales, '\Fisharebest\Localization\Locale::compare');
273c999a340SGreg Roach
274c999a340SGreg Roach		return $locales;
275c999a340SGreg Roach	}
276c999a340SGreg Roach
277c999a340SGreg Roach	/**
278c999a340SGreg Roach	 * All locales for which a translation file exists.
279c999a340SGreg Roach	 *
280c999a340SGreg Roach	 * @return Locale[]
281c999a340SGreg Roach	 */
282c999a340SGreg Roach	public static function installedLocales() {
283c999a340SGreg Roach		$locales = array();
284c999a340SGreg Roach		foreach (glob(WT_ROOT . 'language/*.mo') as $file) {
285c999a340SGreg Roach			try {
286c999a340SGreg Roach				$locales[] = Locale::create(basename($file, '.mo'));
287c999a340SGreg Roach			} catch (\Exception $ex) {
288c999a340SGreg Roach				// No such locale exists?
289a25f0a04SGreg Roach			}
290a25f0a04SGreg Roach		}
291c999a340SGreg Roach		usort($locales, '\Fisharebest\Localization\Locale::compare');
292c999a340SGreg Roach
293c999a340SGreg Roach		return $locales;
294a25f0a04SGreg Roach	}
295a25f0a04SGreg Roach
296a25f0a04SGreg Roach	/**
297a25f0a04SGreg Roach	 * Generate i18n markup for the <html> tag, e.g. lang="ar" dir="rtl"
298a25f0a04SGreg Roach	 *
299a25f0a04SGreg Roach	 * @return string
300a25f0a04SGreg Roach	 */
301764a01d9SGreg Roach	public static function htmlAttributes() {
302c999a340SGreg Roach		return self::$locale->htmlAttributes();
303a25f0a04SGreg Roach	}
304a25f0a04SGreg Roach
305a25f0a04SGreg Roach	/**
306a25f0a04SGreg Roach	 * Translate a number into the local representation.
307a25f0a04SGreg Roach	 *
308a25f0a04SGreg Roach	 * e.g. 12345.67 becomes
309a25f0a04SGreg Roach	 * en: 12,345.67
310a25f0a04SGreg Roach	 * fr: 12 345,67
311a25f0a04SGreg Roach	 * de: 12.345,67
312a25f0a04SGreg Roach	 *
313a25f0a04SGreg Roach	 * @param float   $n
314a25f0a04SGreg Roach	 * @param integer $precision
315a25f0a04SGreg Roach	 *
316a25f0a04SGreg Roach	 * @return string
317a25f0a04SGreg Roach	 */
318a25f0a04SGreg Roach	public static function number($n, $precision = 0) {
319c999a340SGreg Roach		return self::$locale->number(round($n, $precision));
320a25f0a04SGreg Roach	}
321a25f0a04SGreg Roach
322a25f0a04SGreg Roach	/**
323a25f0a04SGreg Roach	 * Convert the digits 0-9 into the local script
324a25f0a04SGreg Roach	 *
325a25f0a04SGreg Roach	 * Used for years, etc., where we do not want thousands-separators, decimals, etc.
326a25f0a04SGreg Roach	 *
327a25f0a04SGreg Roach	 * @param integer $n
328a25f0a04SGreg Roach	 *
329a25f0a04SGreg Roach	 * @return string
330a25f0a04SGreg Roach	 */
331a25f0a04SGreg Roach	public static function digits($n) {
332c999a340SGreg Roach		return self::$locale->digits($n);
333a25f0a04SGreg Roach	}
334a25f0a04SGreg Roach
335a25f0a04SGreg Roach	/**
336a25f0a04SGreg Roach	 * Translate a fraction into a percentage.
337a25f0a04SGreg Roach	 *
338a25f0a04SGreg Roach	 * e.g. 0.123 becomes
339a25f0a04SGreg Roach	 * en: 12.3%
340a25f0a04SGreg Roach	 * fr: 12,3 %
341a25f0a04SGreg Roach	 * de: 12,3%
342a25f0a04SGreg Roach	 *
343a25f0a04SGreg Roach	 * @param float   $n
344a25f0a04SGreg Roach	 * @param integer $precision
345a25f0a04SGreg Roach	 *
346a25f0a04SGreg Roach	 * @return string
347a25f0a04SGreg Roach	 */
348a25f0a04SGreg Roach	public static function percentage($n, $precision = 0) {
349a25f0a04SGreg Roach		return
350a25f0a04SGreg 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. */
351a25f0a04SGreg Roach			self::translate('%s%%', self::number($n * 100.0, $precision));
352a25f0a04SGreg Roach	}
353a25f0a04SGreg Roach
354a25f0a04SGreg Roach	/**
3553cfaf201SGreg Roach	 * What is the direction of the current locale
3563cfaf201SGreg Roach	 *
3573cfaf201SGreg Roach	 * @return string "ltr" or "rtl"
3583cfaf201SGreg Roach	 */
359c0af647eSGreg Roach	public static function direction() {
360c999a340SGreg Roach		return self::$locale->direction();
3613cfaf201SGreg Roach	}
3623cfaf201SGreg Roach
3633cfaf201SGreg Roach	/**
364a25f0a04SGreg Roach	 * Translate a string, and then substitute placeholders
365a25f0a04SGreg Roach	 *
366a25f0a04SGreg Roach	 * echo I18N::translate('Hello World!');
367a25f0a04SGreg Roach	 * echo I18N::translate('The %s sat on the mat', 'cat');
368a25f0a04SGreg Roach	 *
369a25f0a04SGreg Roach	 * @return string
370a25f0a04SGreg Roach	 */
371a25f0a04SGreg Roach	public static function translate(/* var_args */) {
372a25f0a04SGreg Roach		$args = func_get_args();
373a25f0a04SGreg Roach		$args[0] = self::$translation_adapter->getAdapter()->_($args[0]);
374a25f0a04SGreg Roach
375a25f0a04SGreg Roach		return call_user_func_array('sprintf', $args);
376a25f0a04SGreg Roach	}
377a25f0a04SGreg Roach
378a25f0a04SGreg Roach	/**
379a25f0a04SGreg Roach	 * Context sensitive version of translate.
380a25f0a04SGreg Roach	 *
381a25f0a04SGreg Roach	 * echo I18N::translate_c('NOMINATIVE', 'January');
382a25f0a04SGreg Roach	 * echo I18N::translate_c('GENITIVE',   'January');
383a25f0a04SGreg Roach	 *
384a25f0a04SGreg Roach	 * @return string
385a25f0a04SGreg Roach	 */
386764a01d9SGreg Roach	public static function translateContext(/* var_args */) {
387a25f0a04SGreg Roach		$args = func_get_args();
388a25f0a04SGreg Roach		$msgid = $args[0] . "\x04" . $args[1];
389a25f0a04SGreg Roach		$msgtxt = self::$translation_adapter->getAdapter()->_($msgid);
390c0af647eSGreg Roach		if ($msgtxt === $msgid) {
391a25f0a04SGreg Roach			$msgtxt = $args[1];
392a25f0a04SGreg Roach		}
393a25f0a04SGreg Roach		$args[0] = $msgtxt;
394a25f0a04SGreg Roach		unset($args[1]);
395a25f0a04SGreg Roach
396a25f0a04SGreg Roach		return call_user_func_array('sprintf', $args);
397a25f0a04SGreg Roach	}
398a25f0a04SGreg Roach
399a25f0a04SGreg Roach	/**
400a25f0a04SGreg Roach	 * Similar to translate, but do perform "no operation" on it.
401a25f0a04SGreg Roach	 *
402a25f0a04SGreg Roach	 * This is necessary to fetch a format string (containing % characters) without
403a25f0a04SGreg Roach	 * performing sustitution of arguments.
404a25f0a04SGreg Roach	 *
405a25f0a04SGreg Roach	 * @param string $string
406a25f0a04SGreg Roach	 *
407a25f0a04SGreg Roach	 * @return string
408a25f0a04SGreg Roach	 */
409a25f0a04SGreg Roach	public static function noop($string) {
410a25f0a04SGreg Roach		return self::$translation_adapter->getAdapter()->_($string);
411a25f0a04SGreg Roach	}
412a25f0a04SGreg Roach
413a25f0a04SGreg Roach	/**
414a25f0a04SGreg Roach	 * Translate a plural string
415a25f0a04SGreg Roach	 *
416a25f0a04SGreg Roach	 * echo self::plural('There is an error', 'There are errors', $num_errors);
417a25f0a04SGreg Roach	 * echo self::plural('There is one error', 'There are %s errors', $num_errors);
418a25f0a04SGreg Roach	 * echo self::plural('There is %1$d %2$s cat', 'There are %1$d %2$s cats', $num, $num, $colour);
419a25f0a04SGreg Roach	 *
420a25f0a04SGreg Roach	 * @return string
421a25f0a04SGreg Roach	 */
422a25f0a04SGreg Roach	public static function plural(/* var_args */) {
423a25f0a04SGreg Roach		$args = func_get_args();
424a25f0a04SGreg Roach		$string = self::$translation_adapter->getAdapter()->plural($args[0], $args[1], $args[2]);
425a25f0a04SGreg Roach		array_splice($args, 0, 3, array($string));
426a25f0a04SGreg Roach
427a25f0a04SGreg Roach		return call_user_func_array('sprintf', $args);
428a25f0a04SGreg Roach	}
429a25f0a04SGreg Roach
430a25f0a04SGreg Roach	/**
431a25f0a04SGreg Roach	 * Convert a GEDCOM age string into translated_text
432a25f0a04SGreg Roach	 *
433a25f0a04SGreg Roach	 * NB: The import function will have normalised this, so we don't need
434a25f0a04SGreg Roach	 * to worry about badly formatted strings
435a25f0a04SGreg Roach	 * NOTE: this function is not yet complete - eventually it will replace get_age_at_event()
436a25f0a04SGreg Roach	 *
437a25f0a04SGreg Roach	 * @param $string
438a25f0a04SGreg Roach	 *
439a25f0a04SGreg Roach	 * @return string
440a25f0a04SGreg Roach	 */
441764a01d9SGreg Roach	public static function gedcomAge($string) {
442a25f0a04SGreg Roach		switch ($string) {
443a25f0a04SGreg Roach		case 'STILLBORN':
444a25f0a04SGreg Roach			// I18N: Description of an individual’s age at an event.  For example, Died 14 Jan 1900 (stillborn)
445a25f0a04SGreg Roach			return self::translate('(stillborn)');
446a25f0a04SGreg Roach		case 'INFANT':
447a25f0a04SGreg Roach			// I18N: Description of an individual’s age at an event.  For example, Died 14 Jan 1900 (in infancy)
448a25f0a04SGreg Roach			return self::translate('(in infancy)');
449a25f0a04SGreg Roach		case 'CHILD':
450a25f0a04SGreg Roach			// I18N: Description of an individual’s age at an event.  For example, Died 14 Jan 1900 (in childhood)
451a25f0a04SGreg Roach			return self::translate('(in childhood)');
452a25f0a04SGreg Roach		}
453a25f0a04SGreg Roach		$age = array();
454a25f0a04SGreg Roach		if (preg_match('/(\d+)y/', $string, $match)) {
455a25f0a04SGreg Roach			// I18N: Part of an age string. e.g. 5 years, 4 months and 3 days
456a25f0a04SGreg Roach			$years = $match[1];
457a25f0a04SGreg Roach			$age[] = self::plural('%s year', '%s years', $years, self::number($years));
458a25f0a04SGreg Roach		} else {
459a25f0a04SGreg Roach			$years = -1;
460a25f0a04SGreg Roach		}
461a25f0a04SGreg Roach		if (preg_match('/(\d+)m/', $string, $match)) {
462a25f0a04SGreg Roach			// I18N: Part of an age string. e.g. 5 years, 4 months and 3 days
463a25f0a04SGreg Roach			$age[] = self::plural('%s month', '%s months', $match[1], self::number($match[1]));
464a25f0a04SGreg Roach		}
465a25f0a04SGreg Roach		if (preg_match('/(\d+)w/', $string, $match)) {
466a25f0a04SGreg Roach			// I18N: Part of an age string. e.g. 7 weeks and 3 days
467a25f0a04SGreg Roach			$age[] = self::plural('%s week', '%s weeks', $match[1], self::number($match[1]));
468a25f0a04SGreg Roach		}
469a25f0a04SGreg Roach		if (preg_match('/(\d+)d/', $string, $match)) {
470a25f0a04SGreg Roach			// I18N: Part of an age string. e.g. 5 years, 4 months and 3 days
471a25f0a04SGreg Roach			$age[] = self::plural('%s day', '%s days', $match[1], self::number($match[1]));
472a25f0a04SGreg Roach		}
473a25f0a04SGreg Roach		// If an age is just a number of years, only show the number
474c0af647eSGreg Roach		if (count($age) === 1 && $years >= 0) {
475a25f0a04SGreg Roach			$age = $years;
476a25f0a04SGreg Roach		}
477a25f0a04SGreg Roach		if ($age) {
478a25f0a04SGreg Roach			if (!substr_compare($string, '<', 0, 1)) {
479a25f0a04SGreg Roach				// I18N: Description of an individual’s age at an event.  For example, Died 14 Jan 1900 (aged less than 21 years)
480a25f0a04SGreg Roach				return self::translate('(aged less than %s)', $age);
481a25f0a04SGreg Roach			} elseif (!substr_compare($string, '>', 0, 1)) {
482a25f0a04SGreg Roach				// I18N: Description of an individual’s age at an event.  For example, Died 14 Jan 1900 (aged more than 21 years)
483a25f0a04SGreg Roach				return self::translate('(aged more than %s)', $age);
484a25f0a04SGreg Roach			} else {
485a25f0a04SGreg Roach				// I18N: Description of an individual’s age at an event.  For example, Died 14 Jan 1900 (aged 43 years)
486a25f0a04SGreg Roach				return self::translate('(aged %s)', $age);
487a25f0a04SGreg Roach			}
488a25f0a04SGreg Roach		} else {
489a25f0a04SGreg Roach			// Not a valid string?
490a25f0a04SGreg Roach			return self::translate('(aged %s)', $string);
491a25f0a04SGreg Roach		}
492a25f0a04SGreg Roach	}
493a25f0a04SGreg Roach
494a25f0a04SGreg Roach	/**
495a25f0a04SGreg Roach	 * Convert a number of seconds into a relative time.  For example, 630 => "10 hours, 30 minutes ago"
496a25f0a04SGreg Roach	 *
497a25f0a04SGreg Roach	 * @param integer $seconds
498a25f0a04SGreg Roach	 *
499a25f0a04SGreg Roach	 * @return string
500a25f0a04SGreg Roach	 */
501a25f0a04SGreg Roach	public static function timeAgo($seconds) {
502a25f0a04SGreg Roach		$minute = 60;
503a25f0a04SGreg Roach		$hour   = 60 * $minute;
504a25f0a04SGreg Roach		$day    = 24 * $hour;
505a25f0a04SGreg Roach		$month  = 30 * $day;
506a25f0a04SGreg Roach		$year   = 365 * $day;
507a25f0a04SGreg Roach
508a25f0a04SGreg Roach		if ($seconds > $year) {
509a25f0a04SGreg Roach			$years = (int) ($seconds / $year);
510a25f0a04SGreg Roach			return self::plural('%s year ago', '%s years ago', $years, self::number($years));
511a25f0a04SGreg Roach		} elseif ($seconds > $month) {
512a25f0a04SGreg Roach			$months = (int) ($seconds / $month);
513a25f0a04SGreg Roach			return self::plural('%s month ago', '%s months ago', $months, self::number($months));
514a25f0a04SGreg Roach		} elseif ($seconds > $day) {
515a25f0a04SGreg Roach			$days = (int) ($seconds / $day);
516a25f0a04SGreg Roach			return self::plural('%s day ago', '%s days ago', $days, self::number($days));
517a25f0a04SGreg Roach		} elseif ($seconds > $hour) {
518a25f0a04SGreg Roach			$hours = (int) ($seconds / $hour);
519a25f0a04SGreg Roach			return self::plural('%s hour ago', '%s hours ago', $hours, self::number($hours));
520a25f0a04SGreg Roach		} elseif ($seconds > $minute) {
521a25f0a04SGreg Roach			$minutes = (int) ($seconds / $minute);
522a25f0a04SGreg Roach			return self::plural('%s minute ago', '%s minutes ago', $minutes, self::number($minutes));
523a25f0a04SGreg Roach		} else {
524a25f0a04SGreg Roach			return self::plural('%s second ago', '%s seconds ago', $seconds, self::number($seconds));
525a25f0a04SGreg Roach		}
526a25f0a04SGreg Roach	}
527a25f0a04SGreg Roach
528a25f0a04SGreg Roach	/**
529a25f0a04SGreg Roach	 * Return the endonym for a given language - as per http://cldr.unicode.org/
530a25f0a04SGreg Roach	 *
531a25f0a04SGreg Roach	 * @param string $locale
532a25f0a04SGreg Roach	 *
533a25f0a04SGreg Roach	 * @return string
534a25f0a04SGreg Roach	 */
535a25f0a04SGreg Roach	public static function languageName($locale) {
536c999a340SGreg Roach		return Locale::create($locale)->endonym();
537a25f0a04SGreg Roach	}
538a25f0a04SGreg Roach
539a25f0a04SGreg Roach	/**
540a25f0a04SGreg Roach	 * Return the script used by a given language
541a25f0a04SGreg Roach	 *
542a25f0a04SGreg Roach	 * @param string $locale
543a25f0a04SGreg Roach	 *
544a25f0a04SGreg Roach	 * @return string
545a25f0a04SGreg Roach	 */
546a25f0a04SGreg Roach	public static function languageScript($locale) {
547c999a340SGreg Roach		return Locale::create($locale)->script()->code();
548a25f0a04SGreg Roach	}
549a25f0a04SGreg Roach
550a25f0a04SGreg Roach	/**
551a25f0a04SGreg Roach	 * Identify the script used for a piece of text
552a25f0a04SGreg Roach	 *
553a25f0a04SGreg Roach	 * @param $string
554a25f0a04SGreg Roach	 *
555a25f0a04SGreg Roach	 * @return string
556a25f0a04SGreg Roach	 */
557a25f0a04SGreg Roach	public static function textScript($string) {
558a25f0a04SGreg Roach		$string = strip_tags($string); // otherwise HTML tags show up as latin
559a25f0a04SGreg Roach		$string = html_entity_decode($string, ENT_QUOTES, 'UTF-8'); // otherwise HTML entities show up as latin
560a25f0a04SGreg Roach		$string = str_replace(array('@N.N.', '@P.N.'), '', $string); // otherwise unknown names show up as latin
561a25f0a04SGreg Roach		$pos = 0;
562a25f0a04SGreg Roach		$strlen = strlen($string);
563a25f0a04SGreg Roach		while ($pos < $strlen) {
564a25f0a04SGreg Roach			// get the Unicode Code Point for the character at position $pos
565a25f0a04SGreg Roach			$byte1 = ord($string[$pos]);
566a25f0a04SGreg Roach			if ($byte1 < 0x80) {
567a25f0a04SGreg Roach				$code_point = $byte1;
568a25f0a04SGreg Roach				$chrlen = 1;
569a25f0a04SGreg Roach			} elseif ($byte1 < 0xC0) {
570a25f0a04SGreg Roach				// Invalid continuation character
571a25f0a04SGreg Roach				return 'Latn';
572a25f0a04SGreg Roach			} elseif ($byte1 < 0xE0) {
573a25f0a04SGreg Roach				$code_point = (($byte1 & 0x1F) << 6) + (ord($string[$pos + 1]) & 0x3F);
574a25f0a04SGreg Roach				$chrlen = 2;
575a25f0a04SGreg Roach			} elseif ($byte1 < 0xF0) {
576a25f0a04SGreg Roach				$code_point = (($byte1 & 0x0F) << 12) + ((ord($string[$pos + 1]) & 0x3F) << 6) + (ord($string[$pos + 2]) & 0x3F);
577a25f0a04SGreg Roach				$chrlen = 3;
578a25f0a04SGreg Roach			} elseif ($byte1 < 0xF8) {
579a25f0a04SGreg Roach				$code_point = (($byte1 & 0x07) << 24) + ((ord($string[$pos + 1]) & 0x3F) << 12) + ((ord($string[$pos + 2]) & 0x3F) << 6) + (ord($string[$pos + 3]) & 0x3F);
580a25f0a04SGreg Roach				$chrlen = 3;
581a25f0a04SGreg Roach 			} else {
582a25f0a04SGreg Roach				// Invalid UTF
583a25f0a04SGreg Roach				return 'Latn';
584a25f0a04SGreg Roach			}
585a25f0a04SGreg Roach
586a25f0a04SGreg Roach			foreach (self::$scripts as $range) {
587a25f0a04SGreg Roach				if ($code_point >= $range[1] && $code_point <= $range[2]) {
588a25f0a04SGreg Roach					return $range[0];
589a25f0a04SGreg Roach				}
590a25f0a04SGreg Roach			}
591a25f0a04SGreg Roach			// Not a recognised script.  Maybe punctuation, spacing, etc.  Keep looking.
592a25f0a04SGreg Roach			$pos += $chrlen;
593a25f0a04SGreg Roach		}
594a25f0a04SGreg Roach
595a25f0a04SGreg Roach		return 'Latn';
596a25f0a04SGreg Roach	}
597a25f0a04SGreg Roach
598a25f0a04SGreg Roach	/**
599a25f0a04SGreg Roach	 * Return the direction (ltr or rtl) for a given script
600a25f0a04SGreg Roach	 *
601a25f0a04SGreg Roach	 * The PHP/intl library does not provde this information, so we need
602a25f0a04SGreg Roach	 * our own lookup table.
603a25f0a04SGreg Roach	 *
604a25f0a04SGreg Roach	 * @param string $script
605a25f0a04SGreg Roach	 *
606a25f0a04SGreg Roach	 * @return string
607a25f0a04SGreg Roach	 */
608a25f0a04SGreg Roach	public static function scriptDirection($script) {
609a25f0a04SGreg Roach		switch ($script) {
610a25f0a04SGreg Roach		case 'Arab':
611a25f0a04SGreg Roach		case 'Hebr':
612a25f0a04SGreg Roach		case 'Mong':
613a25f0a04SGreg Roach		case 'Thaa':
614a25f0a04SGreg Roach			return 'rtl';
615a25f0a04SGreg Roach		default:
616a25f0a04SGreg Roach			return 'ltr';
617a25f0a04SGreg Roach		}
618a25f0a04SGreg Roach	}
619a25f0a04SGreg Roach
620a25f0a04SGreg Roach	/**
621a25f0a04SGreg Roach	 * UTF8 version of PHP::strtoupper()
622a25f0a04SGreg Roach	 *
623a25f0a04SGreg Roach	 * Convert a string to upper case, using the rules from the current locale
624a25f0a04SGreg Roach	 *
625a25f0a04SGreg Roach	 * @param string $string
626a25f0a04SGreg Roach	 *
627a25f0a04SGreg Roach	 * @return string
628a25f0a04SGreg Roach	 */
629a25f0a04SGreg Roach	public static function strtoupper($string) {
630c999a340SGreg Roach		if (self::$locale->language()->code() === 'tr' || self::$locale->language()->code() === 'az') {
631a25f0a04SGreg Roach			return TurkishUtf8::strtoupper($string);
632a25f0a04SGreg Roach		} else {
633a25f0a04SGreg Roach			return mb_strtoupper($string);
634a25f0a04SGreg Roach		}
635a25f0a04SGreg Roach	}
636a25f0a04SGreg Roach
637a25f0a04SGreg Roach	/**
638a25f0a04SGreg Roach	 * UTF8 version of PHP::strtolower()
639a25f0a04SGreg Roach	 *
640a25f0a04SGreg Roach	 * Convert a string to lower case, using the rules from the current locale
641a25f0a04SGreg Roach	 *
642a25f0a04SGreg Roach	 * @param string $string
643a25f0a04SGreg Roach	 *
644a25f0a04SGreg Roach	 * @return string
645a25f0a04SGreg Roach	 */
646a25f0a04SGreg Roach	public static function strtolower($string) {
647c999a340SGreg Roach		if (self::$locale->language()->code() === 'tr' || self::$locale->language()->code() === 'az') {
648a25f0a04SGreg Roach			return TurkishUtf8::strtolower($string);
649a25f0a04SGreg Roach		} else {
650a25f0a04SGreg Roach			return mb_strtolower($string);
651a25f0a04SGreg Roach		}
652a25f0a04SGreg Roach	}
653a25f0a04SGreg Roach
654a25f0a04SGreg Roach	/**
655a25f0a04SGreg Roach	 * UTF8 version of PHP::strcasecmp()
656a25f0a04SGreg Roach	 *
657a25f0a04SGreg Roach	 * Perform a case-insensitive comparison of two strings, using rules from the current locale
658a25f0a04SGreg Roach	 *
659a25f0a04SGreg Roach	 * @param string $string1
660a25f0a04SGreg Roach	 * @param string $string2
661a25f0a04SGreg Roach	 *
662a25f0a04SGreg Roach	 * @return integer
663a25f0a04SGreg Roach	 */
664a25f0a04SGreg Roach	public static function strcasecmp($string1, $string2) {
665a25f0a04SGreg Roach		$strpos1 = 0;
666a25f0a04SGreg Roach		$strpos2 = 0;
667a25f0a04SGreg Roach		$strlen1 = strlen($string1);
668a25f0a04SGreg Roach		$strlen2 = strlen($string2);
669a25f0a04SGreg Roach		while ($strpos1 < $strlen1 && $strpos2 < $strlen2) {
670a25f0a04SGreg Roach			$byte1 = ord($string1[$strpos1]);
671a25f0a04SGreg Roach			$byte2 = ord($string2[$strpos2]);
672c0af647eSGreg Roach			if (($byte1 & 0xE0) === 0xC0) {
673a25f0a04SGreg Roach				$chr1 = $string1[$strpos1++] . $string1[$strpos1++];
674c0af647eSGreg Roach			} elseif (($byte1 & 0xF0) === 0xE0) {
675a25f0a04SGreg Roach				$chr1 = $string1[$strpos1++] . $string1[$strpos1++] . $string1[$strpos1++];
676a25f0a04SGreg Roach			} else {
677a25f0a04SGreg Roach				$chr1 = $string1[$strpos1++];
678a25f0a04SGreg Roach			}
679c0af647eSGreg Roach			if (($byte2 & 0xE0) === 0xC0) {
680a25f0a04SGreg Roach				$chr2 = $string2[$strpos2++] . $string2[$strpos2++];
681c0af647eSGreg Roach			} elseif (($byte2 & 0xF0) === 0xE0) {
682a25f0a04SGreg Roach				$chr2 = $string2[$strpos2++] . $string2[$strpos2++] . $string2[$strpos2++];
683a25f0a04SGreg Roach			} else {
684a25f0a04SGreg Roach				$chr2 = $string2[$strpos2++];
685a25f0a04SGreg Roach			}
686c0af647eSGreg Roach			if ($chr1 === $chr2) {
687a25f0a04SGreg Roach				continue;
688a25f0a04SGreg Roach			}
689a25f0a04SGreg Roach			// Try the local alphabet first
690a25f0a04SGreg Roach			$offset1 = strpos(self::$alphabet_lower, $chr1);
691a25f0a04SGreg Roach			if ($offset1 === false) {
692a25f0a04SGreg Roach				$offset1 = strpos(self::$alphabet_upper, $chr1);
693a25f0a04SGreg Roach			}
694a25f0a04SGreg Roach			$offset2 = strpos(self::$alphabet_lower, $chr2);
695a25f0a04SGreg Roach			if ($offset2 === false) {
696a25f0a04SGreg Roach				$offset2 = strpos(self::$alphabet_upper, $chr2);
697a25f0a04SGreg Roach			}
698a25f0a04SGreg Roach			if ($offset1 !== false && $offset2 !== false) {
699c0af647eSGreg Roach				if ($offset1 === $offset2) {
700a25f0a04SGreg Roach					continue;
701a25f0a04SGreg Roach				} else {
702a25f0a04SGreg Roach					return $offset1 - $offset2;
703a25f0a04SGreg Roach				}
704a25f0a04SGreg Roach			}
705a25f0a04SGreg Roach			// Try the global alphabet next
706a25f0a04SGreg Roach			$offset1 = strpos(self::ALPHABET_LOWER, $chr1);
707a25f0a04SGreg Roach			if ($offset1 === false) {
708a25f0a04SGreg Roach				$offset1 = strpos(self::ALPHABET_UPPER, $chr1);
709a25f0a04SGreg Roach			}
710a25f0a04SGreg Roach			$offset2 = strpos(self::ALPHABET_LOWER, $chr2);
711a25f0a04SGreg Roach			if ($offset2 === false) {
712a25f0a04SGreg Roach				$offset2 = strpos(self::ALPHABET_UPPER, $chr2);
713a25f0a04SGreg Roach			}
714a25f0a04SGreg Roach			if ($offset1 !== false && $offset2 !== false) {
715c0af647eSGreg Roach				if ($offset1 === $offset2) {
716a25f0a04SGreg Roach					continue;
717a25f0a04SGreg Roach				} else {
718a25f0a04SGreg Roach					return $offset1 - $offset2;
719a25f0a04SGreg Roach				}
720a25f0a04SGreg Roach			}
721a25f0a04SGreg Roach			// Just compare by unicode order
722a25f0a04SGreg Roach			return strcmp($chr1, $chr2);
723a25f0a04SGreg Roach		}
724a25f0a04SGreg Roach		// Shortest string comes first.
725a25f0a04SGreg Roach		return ($strlen1 - $strpos1) - ($strlen2 - $strpos2);
726a25f0a04SGreg Roach	}
727a25f0a04SGreg Roach
728a25f0a04SGreg Roach	/**
729a25f0a04SGreg Roach	 * UTF8 version of PHP::strrev()
730a25f0a04SGreg Roach	 *
731a25f0a04SGreg Roach	 * Reverse RTL text for third-party libraries such as GD2 and googlechart.
732a25f0a04SGreg Roach	 *
733a25f0a04SGreg Roach	 * These do not support UTF8 text direction, so we must mimic it for them.
734a25f0a04SGreg Roach	 *
735a25f0a04SGreg Roach	 * Numbers are always rendered LTR, even in RTL text.
736a25f0a04SGreg Roach	 * The visual direction of characters such as parentheses should be reversed.
737a25f0a04SGreg Roach	 *
738a25f0a04SGreg Roach	 * @param string $text Text to be reversed
739a25f0a04SGreg Roach	 *
740a25f0a04SGreg Roach	 * @return string
741a25f0a04SGreg Roach	 */
742a25f0a04SGreg Roach	public static function reverseText($text) {
743a25f0a04SGreg Roach		// Remove HTML markup - we can't display it and it is LTR.
744a25f0a04SGreg Roach		$text = Filter::unescapeHtml($text);
745a25f0a04SGreg Roach
746a25f0a04SGreg Roach		// LTR text doesn't need reversing
747c0af647eSGreg Roach		if (self::scriptDirection(self::textScript($text)) === 'ltr') {
748a25f0a04SGreg Roach			return $text;
749a25f0a04SGreg Roach		}
750a25f0a04SGreg Roach
751a25f0a04SGreg Roach		// Mirrored characters
752a25f0a04SGreg Roach		$text = strtr($text, self::$mirror_characters);
753a25f0a04SGreg Roach
754a25f0a04SGreg Roach		$reversed = '';
755a25f0a04SGreg Roach		$digits = '';
756a25f0a04SGreg Roach		while ($text != '') {
757a25f0a04SGreg Roach			$letter = mb_substr($text, 0, 1);
758a25f0a04SGreg Roach			$text = mb_substr($text, 1);
759a25f0a04SGreg Roach			if (strpos(self::DIGITS, $letter) !== false) {
760a25f0a04SGreg Roach				$digits .= $letter;
761a25f0a04SGreg Roach			} else {
762a25f0a04SGreg Roach				$reversed = $letter . $digits . $reversed;
763a25f0a04SGreg Roach				$digits = '';
764a25f0a04SGreg Roach			}
765a25f0a04SGreg Roach		}
766a25f0a04SGreg Roach
767a25f0a04SGreg Roach		return $digits . $reversed;
768a25f0a04SGreg Roach	}
769a25f0a04SGreg Roach
770a25f0a04SGreg Roach	/**
771a25f0a04SGreg Roach	 * Generate consistent I18N for datatables.js
772a25f0a04SGreg Roach	 *
773a25f0a04SGreg Roach	 * @param array|null $lengths An optional array of page lengths
774a25f0a04SGreg Roach	 *
775a25f0a04SGreg Roach	 * @return string
776a25f0a04SGreg Roach	 */
777a25f0a04SGreg Roach	public static function datatablesI18N(array $lengths = null) {
778a25f0a04SGreg Roach		if ($lengths === null) {
779a25f0a04SGreg Roach			$lengths = array(10, 20, 30, 50, 100, -1);
780a25f0a04SGreg Roach		}
781a25f0a04SGreg Roach
782a25f0a04SGreg Roach		$length_menu = '';
783a25f0a04SGreg Roach		foreach ($lengths as $length) {
784a25f0a04SGreg Roach			$length_menu .=
785a25f0a04SGreg Roach				'<option value="' . $length . '">' .
786c0af647eSGreg Roach				($length === -1 ? /* I18N: listbox option, e.g. “10,25,50,100,all” */ self::translate('All') : self::number($length)) .
787a25f0a04SGreg Roach				'</option>';
788a25f0a04SGreg Roach		}
789a25f0a04SGreg Roach		$length_menu = '<select>' . $length_menu . '</select>';
790a25f0a04SGreg Roach		$length_menu = /* I18N: Display %s [records per page], %s is a placeholder for listbox containing numeric options */ self::translate('Display %s', $length_menu);
791a25f0a04SGreg Roach
792c999a340SGreg Roach		$digits = self::$locale->digits('0123456789');
793c0af647eSGreg Roach		if ($digits === '0123456789') {
794a25f0a04SGreg Roach			$callback = '';
795a25f0a04SGreg Roach		} else {
796a25f0a04SGreg Roach			$callback = ',
797a25f0a04SGreg Roach				"infoCallback": function(oSettings, iStart, iEnd, iMax, iTotal, sPre) {
798a25f0a04SGreg Roach					return sPre
799a25f0a04SGreg Roach						.replace(/0/g, "' . mb_substr($digits, 0, 1) . '")
800a25f0a04SGreg Roach						.replace(/1/g, "' . mb_substr($digits, 1, 1) . '")
801a25f0a04SGreg Roach						.replace(/2/g, "' . mb_substr($digits, 2, 1) . '")
802a25f0a04SGreg Roach						.replace(/3/g, "' . mb_substr($digits, 3, 1) . '")
803a25f0a04SGreg Roach						.replace(/4/g, "' . mb_substr($digits, 4, 1) . '")
804a25f0a04SGreg Roach						.replace(/5/g, "' . mb_substr($digits, 5, 1) . '")
805a25f0a04SGreg Roach						.replace(/6/g, "' . mb_substr($digits, 6, 1) . '")
806a25f0a04SGreg Roach						.replace(/7/g, "' . mb_substr($digits, 7, 1) . '")
807a25f0a04SGreg Roach						.replace(/8/g, "' . mb_substr($digits, 8, 1) . '")
808a25f0a04SGreg Roach						.replace(/9/g, "' . mb_substr($digits, 9, 1) . '");
809a25f0a04SGreg Roach				},
810a25f0a04SGreg Roach				"formatNumber": function(iIn) {
811a25f0a04SGreg Roach					return String(iIn)
812a25f0a04SGreg Roach						.replace(/0/g, "' . mb_substr($digits, 0, 1) . '")
813a25f0a04SGreg Roach						.replace(/1/g, "' . mb_substr($digits, 1, 1) . '")
814a25f0a04SGreg Roach						.replace(/2/g, "' . mb_substr($digits, 2, 1) . '")
815a25f0a04SGreg Roach						.replace(/3/g, "' . mb_substr($digits, 3, 1) . '")
816a25f0a04SGreg Roach						.replace(/4/g, "' . mb_substr($digits, 4, 1) . '")
817a25f0a04SGreg Roach						.replace(/5/g, "' . mb_substr($digits, 5, 1) . '")
818a25f0a04SGreg Roach						.replace(/6/g, "' . mb_substr($digits, 6, 1) . '")
819a25f0a04SGreg Roach						.replace(/7/g, "' . mb_substr($digits, 7, 1) . '")
820a25f0a04SGreg Roach						.replace(/8/g, "' . mb_substr($digits, 8, 1) . '")
821a25f0a04SGreg Roach						.replace(/9/g, "' . mb_substr($digits, 9, 1) . '");
822a25f0a04SGreg Roach				}
823a25f0a04SGreg Roach			';
824a25f0a04SGreg Roach		}
825a25f0a04SGreg Roach
826a25f0a04SGreg Roach		return
827a25f0a04SGreg Roach			'"language": {' .
828a25f0a04SGreg Roach			' "paginate": {' .
829a25f0a04SGreg Roach			'  "first":    "' . /* I18N: button label, first page    */ self::translate('first') . '",' .
830a25f0a04SGreg Roach			'  "last":     "' . /* I18N: button label, last page     */ self::translate('last') . '",' .
831a25f0a04SGreg Roach			'  "next":     "' . /* I18N: button label, next page     */ self::translate('next') . '",' .
832a25f0a04SGreg Roach			'  "previous": "' . /* I18N: button label, previous page */ self::translate('previous') . '"' .
833a25f0a04SGreg Roach			' },' .
834a25f0a04SGreg Roach			' "emptyTable":     "' . self::translate('No records to display') . '",' .
835a25f0a04SGreg Roach			' "info":           "' . /* I18N: %s are placeholders for numbers */ self::translate('Showing %1$s to %2$s of %3$s', '_START_', '_END_', '_TOTAL_') . '",' .
836a25f0a04SGreg Roach			' "infoEmpty":      "' . self::translate('Showing %1$s to %2$s of %3$s', 0, 0, 0) . '",' .
837a25f0a04SGreg Roach			' "infoFiltered":   "' . /* I18N: %s is a placeholder for a number */ self::translate('(filtered from %s total entries)', '_MAX_') . '",' .
838a25f0a04SGreg Roach			' "infoPostfix":    "",' .
839a25f0a04SGreg Roach			' "lengthMenu":     "' . Filter::escapeJs($length_menu) . '",' .
840a25f0a04SGreg Roach			' "loadingRecords": "' . self::translate('Loading…') . '",' .
841a25f0a04SGreg Roach			' "processing":     "' . self::translate('Loading…') . '",' .
842a25f0a04SGreg Roach			' "search":         "' . self::translate('Filter') . '",' .
843a25f0a04SGreg Roach			' "url":            "",' .
844a25f0a04SGreg Roach			' "zeroRecords":    "' . self::translate('No records to display') . '"' .
845a25f0a04SGreg Roach			'}' .
846a25f0a04SGreg Roach			$callback;
847a25f0a04SGreg Roach	}
848a25f0a04SGreg Roach}
849