xref: /webtrees/app/I18N.php (revision 3bdc890b36a371d2e439cb152023b7eb79257527)
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;
20*3bdc890bSGreg Roachuse Fisharebest\Localization\LocaleEnUs;
21*3bdc890bSGreg Roachuse Fisharebest\Localization\Translation;
22*3bdc890bSGreg Roachuse Fisharebest\Localization\Translator;
23a25f0a04SGreg Roachuse Patchwork\TurkishUtf8;
24a25f0a04SGreg Roach
25a25f0a04SGreg Roach/**
26a25f0a04SGreg Roach * Class I18N - Functions to support internationalization (i18n) functionality.
27a25f0a04SGreg Roach */
28a25f0a04SGreg Roachclass I18N {
29c999a340SGreg Roach	/** @var Locale The current locale (e.g. LocaleEnGb) */
30c999a340SGreg Roach	private static $locale;
31c999a340SGreg Roach
32*3bdc890bSGreg Roach	/** @var Translator */
33*3bdc890bSGreg Roach	private static $translator;
34*3bdc890bSGreg Roach
35a25f0a04SGreg Roach	// Digits are always rendered LTR, even in RTL text.
36a25f0a04SGreg Roach	const DIGITS = '0123456789٠١٢٣٤٥٦٧٨٩۰۱۲۳۴۵۶۷۸۹';
37a25f0a04SGreg Roach
38a25f0a04SGreg Roach	// Reversable character conversions from the UNICODE 5.1 database.
39a25f0a04SGreg Roach	// It excludes ambiguous (turkish dotless i) and mixed-case (Dz) characters.
40a25f0a04SGreg Roach	// The characters should be arranged in default unicode-collation order.
41a25f0a04SGreg 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ƶȥǯʒƹȝþƿƨƽƅάαἀἁἂἃἄἅἆἇὰάᾀᾁᾂᾃᾄᾅᾆᾇᾰᾱᾳβγδέεἐἑἒἓἔἕὲέϝϛζήηἠἡἢἣἤἥἦἧὴήᾐᾑᾒᾓᾔᾕᾖᾗῃθϊἰἱἲἳἴἵἶἷὶίῐῑκϗλμνξοόὀὁὂὃὄὅὸόπϟϙρῥσϲτυϋύὑὓὕὗὺύῠῡφχψωώὠὡὢὣὤὥὦὧὼώᾠᾡᾢᾣᾤᾥᾦᾧῳϡϸϻϣϥϧϩϫϭϯаӑӓәӛӕбвгґғҕдԁђԃѓҙеѐёӗєжӂӝҗзԅӟѕӡԇиѝӣҋӥіїйјкқӄҡҟҝлӆљԉмӎнӊңӈҥњԋоӧөӫпҧҁрҏсԍҫтԏҭћќуӯўӱӳүұѹфхҳһѡѿѽѻцҵчӵҷӌҹҽҿџшщъыӹьҍѣэӭюяѥѧѫѩѭѯѱѳѵѷҩաբգդեզէըթժիլխծկհձղճմյնշոչպջռսվտրցւփքօֆȼɂɇɉɋɍɏͱͳͷͻͼͽӏӷӻӽӿԑԓԕԗԙԛԝԟԡԣԥᵹᵽỻỽỿⅎↄⰰⰱⰲⰳⰴⰵⰶⰷⰸⰹⰺⰻⰼⰽⰾⰿⱀⱁⱂⱃⱄⱅⱆⱇⱈⱉⱊⱋⱌⱍⱎⱏⱐⱑⱒⱓⱔⱕⱖⱗⱘⱙⱚⱛⱜⱝⱞⱡⱨⱪⱬⱳⱶⲁⲃⲅⲇⲉⲋⲍⲏⲑⲓⲕⲗⲙⲛⲝⲟⲡⲣⲥⲧⲩⲫⲭⲯⲱⲳⲵⲷⲹⲻⲽⲿⳁⳃⳅⳇⳉⳋⳍⳏⳑⳓⳕⳗⳙⳛⳝⳟⳡⳣⳬⳮⴀⴁⴂⴃⴄⴅⴆⴇⴈⴉⴊⴋⴌⴍⴎⴏⴐⴑⴒⴓⴔⴕⴖⴗⴘⴙⴚⴛⴜⴝⴞⴟⴠⴡⴢⴣⴤⴥꙁꙃꙅꙇꙉꙋꙍꙏꙑꙓꙕꙗꙙꙛꙝꙟꙣꙥꙧꙩꙫꙭꚁꚃꚅꚇꚉꚋꚍꚏꚑꚓꚕꚗꜣꜥꜧꜩꜫꜭꜯꜳꜵꜷꜹꜻꜽꜿꝁꝃꝅꝇꝉꝋꝍꝏꝑꝓꝕꝗꝙꝛꝝꝟꝡꝣꝥꝧꝩꝫꝭꝯꝺꝼꝿꞁꞃꞅꞇꞌ';
42a25f0a04SGreg 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ƵȤǮƷƸȜÞǷƧƼƄΆΑἈἉἊἋἌἍἎἏᾺΆᾈᾉᾊᾋᾌᾍᾎᾏᾸᾹᾼΒΓΔΈΕἘἙἚἛἜἝῈΈϜϚΖΉΗἨἩἪἫἬἭἮἯῊΉᾘᾙᾚᾛᾜᾝᾞᾟῌΘΪἸἹἺἻἼἽἾἿῚΊῘῙΚϏΛΜΝΞΟΌὈὉὊὋὌὍῸΌΠϞϘΡῬΣϹΤΥΫΎὙὛὝὟῪΎῨῩΦΧΨΩΏὨὩὪὫὬὭὮὯῺΏᾨᾩᾪᾫᾬᾭᾮᾯῼϠϷϺϢϤϦϨϪϬϮАӐӒӘӚӔБВГҐҒҔДԀЂԂЃҘЕЀЁӖЄЖӁӜҖЗԄӞЅӠԆИЍӢҊӤІЇЙЈКҚӃҠҞҜЛӅЉԈМӍНӉҢӇҤЊԊОӦӨӪПҦҀРҎСԌҪТԎҬЋЌУӮЎӰӲҮҰѸФХҲҺѠѾѼѺЦҴЧӴҶӋҸҼҾЏШЩЪЫӸЬҌѢЭӬЮЯѤѦѪѨѬѮѰѲѴѶҨԱԲԳԴԵԶԷԸԹԺԻԼԽԾԿՀՁՂՃՄՅՆՇՈՉՊՋՌՍՎՏՐՑՒՓՔՕՖȻɁɆɈɊɌɎͰͲͶϽϾϿӀӶӺӼӾԐԒԔԖԘԚԜԞԠԢԤꝽⱣỺỼỾℲↃⰀⰁⰂⰃⰄⰅⰆⰇⰈⰉⰊⰋⰌⰍⰎⰏⰐⰑⰒⰓⰔⰕⰖⰗⰘⰙⰚⰛⰜⰝⰞⰟⰠⰡⰢⰣⰤⰥⰦⰧⰨⰩⰪⰫⰬⰭⰮⱠⱧⱩⱫⱲⱵⲀⲂⲄⲆⲈⲊⲌⲎⲐⲒⲔⲖⲘⲚⲜⲞⲠⲢⲤⲦⲨⲪⲬⲮⲰⲲⲴⲶⲸⲺⲼⲾⳀⳂⳄⳆⳈⳊⳌⳎⳐⳒⳔⳖⳘⳚⳜⳞⳠⳢⳫⳭႠႡႢႣႤႥႦႧႨႩႪႫႬႭႮႯႰႱႲႳႴႵႶႷႸႹႺႻႼႽႾႿჀჁჂჃჄჅꙀꙂꙄꙆꙈꙊꙌꙎꙐꙒꙔꙖꙘꙚꙜꙞꙢꙤꙦꙨꙪꙬꚀꚂꚄꚆꚈꚊꚌꚎꚐꚒꚔꚖꜢꜤꜦꜨꜪꜬꜮꜲꜴꜶꜸꜺꜼꜾꝀꝂꝄꝆꝈꝊꝌꝎꝐꝒꝔꝖꝘꝚꝜꝞꝠꝢꝤꝦꝨꝪꝬꝮꝹꝻꝾꞀꞂꞄꞆꞋ';
43a25f0a04SGreg Roach
44a25f0a04SGreg Roach	// Alphabet for the currently selected locale
45a25f0a04SGreg Roach	private static $alphabet_lower = 'abcdefghijklmnopqrstuvwxyz';
46a25f0a04SGreg Roach	private static $alphabet_upper = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
47a25f0a04SGreg Roach
48a25f0a04SGreg Roach	// Lookup table to convert unicode code-points into scripts.
49a25f0a04SGreg Roach	// See https://en.wikipedia.org/wiki/Unicode_block
50a25f0a04SGreg Roach	// Note: we only need details for scripts of languages into which webtrees is translated.
51a25f0a04SGreg Roach	private static $scripts = array(
52a25f0a04SGreg Roach		array('Latn', 0x0041, 0x005A), // a-z
53a25f0a04SGreg Roach		array('Latn', 0x0061, 0x007A), // A-Z
54a25f0a04SGreg Roach		array('Latn', 0x0100, 0x02AF),
55a25f0a04SGreg Roach		array('Grek', 0x0370, 0x03FF),
56a25f0a04SGreg Roach		array('Cyrl', 0x0400, 0x052F),
57a25f0a04SGreg Roach		array('Hebr', 0x0590, 0x05FF),
58a25f0a04SGreg Roach		array('Arab', 0x0600, 0x06FF),
59a25f0a04SGreg Roach		array('Arab', 0x0750, 0x077F),
60a25f0a04SGreg Roach		array('Arab', 0x08A0, 0x08FF),
61a25f0a04SGreg Roach		array('Deva', 0x0900, 0x097F),
62a25f0a04SGreg Roach		array('Taml', 0x0B80, 0x0BFF),
63a25f0a04SGreg Roach		array('Sinh', 0x0D80, 0x0DFF),
64a25f0a04SGreg Roach		array('Thai', 0x0E00, 0x0E7F),
65a25f0a04SGreg Roach		array('Geor', 0x10A0, 0x10FF),
66a25f0a04SGreg Roach		array('Grek', 0x1F00, 0x1FFF),
67a25f0a04SGreg Roach		array('Deva', 0xA8E0, 0xA8FF),
68a25f0a04SGreg Roach		array('Hans', 0x3000, 0x303F), // Mixed CJK, not just Hans
69a25f0a04SGreg Roach		array('Hans', 0x3400, 0xFAFF), // Mixed CJK, not just Hans
70a25f0a04SGreg Roach		array('Hans', 0x20000, 0x2FA1F), // Mixed CJK, not just Hans
71a25f0a04SGreg Roach	);
72a25f0a04SGreg Roach
73a25f0a04SGreg Roach	// Characters that are displayed in mirror form in RTL text.
74a25f0a04SGreg Roach	private static $mirror_characters = array(
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	);
94a25f0a04SGreg Roach
95a25f0a04SGreg Roach	/** @var string The MySQL collation sequence used by this language, typically utf8_unicode_ci */
96a25f0a04SGreg Roach	public static $collation;
97a25f0a04SGreg Roach
98a25f0a04SGreg Roach	/** @var string Punctuation used to separate list items, typically a comma */
99a25f0a04SGreg Roach	public static $list_separator;
100a25f0a04SGreg Roach
101a25f0a04SGreg Roach	/**
102a25f0a04SGreg Roach	 * Initialise the translation adapter with a locale setting.
103a25f0a04SGreg Roach	 *
104*3bdc890bSGreg Roach	 * @param string|null $code Use this locale/language code, or choose one automatically
105a25f0a04SGreg Roach	 *
106a25f0a04SGreg Roach	 * @return string $string
107a25f0a04SGreg Roach	 */
108*3bdc890bSGreg Roach	public static function init($code = null) {
109a25f0a04SGreg Roach		global $WT_SESSION, $WT_TREE;
110a25f0a04SGreg Roach
111*3bdc890bSGreg Roach		if ($code !== null) {
112*3bdc890bSGreg Roach			// Create the specified locale
113*3bdc890bSGreg Roach			self::$locale = Locale::create($code);
114*3bdc890bSGreg Roach		} else {
115*3bdc890bSGreg Roach			// Negotiate a locale, but if we can't then use a failsafe
116*3bdc890bSGreg Roach			self::$locale = new LocaleEnUs;
117*3bdc890bSGreg Roach			if (Filter::get('lang')) {
118*3bdc890bSGreg Roach				// A request in the URL
119*3bdc890bSGreg Roach				try {
120*3bdc890bSGreg Roach					$locale = Locale::create(Filter::get('lang'));
121*3bdc890bSGreg Roach					if (file_exists(WT_ROOT . 'language/' . $locale->languageTag() . '.mo')) {
122*3bdc890bSGreg Roach						self::$locale = $locale;
123*3bdc890bSGreg Roach					}
124*3bdc890bSGreg Roach				} catch (\Exception $ex) {
125*3bdc890bSGreg Roach				}
126*3bdc890bSGreg Roach			} elseif ($WT_SESSION->locale) {
127*3bdc890bSGreg Roach				// Previously used
128*3bdc890bSGreg Roach				self::$locale = Locale::create($WT_SESSION->locale);
129*3bdc890bSGreg Roach			} elseif (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
130*3bdc890bSGreg Roach				// Browser negotiation
131*3bdc890bSGreg Roach				$http_accept_language = strtolower(str_replace(' ', '', $_SERVER['HTTP_ACCEPT_LANGUAGE']));
132*3bdc890bSGreg Roach				if (preg_match_all('/(?:([a-z][a-z0-9_-]+)(?:;q=([0-9.]+))?)/', $http_accept_language, $match)) {
133*3bdc890bSGreg Roach					$preferences = array_combine($match[1], $match[2]);
134*3bdc890bSGreg Roach					array_walk($preferences, function(&$x) { $x = $x === '' ? 1.0 : (float) $x; });
135*3bdc890bSGreg Roach				} else {
136*3bdc890bSGreg Roach					$preferences = array();
137*3bdc890bSGreg Roach				}
138*3bdc890bSGreg Roach				// Add the tree’s default language as a low-priority
139*3bdc890bSGreg Roach				if ($WT_TREE && !isset($preferences[$WT_TREE->getPreference('LANGUAGE')])) {
140*3bdc890bSGreg Roach					$preferences[$WT_TREE->getPreference('LANGUAGE')] = 0.2;
141*3bdc890bSGreg Roach				}
142*3bdc890bSGreg Roach				arsort($preferences);
143*3bdc890bSGreg Roach				foreach (array_keys($preferences) as $code) {
144*3bdc890bSGreg Roach					try {
145*3bdc890bSGreg Roach						$locale = Locale::create($code);
146*3bdc890bSGreg Roach						if (file_exists(WT_ROOT . 'language/' . $locale->languageTag() . '.mo')) {
147*3bdc890bSGreg Roach							self::$locale = $locale;
148*3bdc890bSGreg Roach							break;
149*3bdc890bSGreg Roach						}
150*3bdc890bSGreg Roach					} catch (\Exception $ex) {
151*3bdc890bSGreg Roach						// The user's prefered locale does not exist
152*3bdc890bSGreg Roach					}
153*3bdc890bSGreg Roach				}
154*3bdc890bSGreg Roach			}
155*3bdc890bSGreg Roach		}
156*3bdc890bSGreg Roach
157*3bdc890bSGreg Roach		File::mkdir( WT_DATA_DIR . 'cache');
158*3bdc890bSGreg Roach		$cache_file = WT_DATA_DIR . 'cache/language-' . self::$locale->languageTag() . '-cache.php';
159*3bdc890bSGreg Roach		if (file_exists($cache_file)) {
160*3bdc890bSGreg Roach			$filemtime = filemtime($cache_file);
161*3bdc890bSGreg Roach		} else {
162*3bdc890bSGreg Roach			$filemtime = 0;
163*3bdc890bSGreg Roach		}
164*3bdc890bSGreg Roach
165*3bdc890bSGreg Roach		// Load the translation file(s)
166*3bdc890bSGreg Roach		$translation_files = array_merge(
167*3bdc890bSGreg Roach			array(WT_ROOT . 'language/' . self::$locale->languageTag() . '.mo'),
168*3bdc890bSGreg Roach			glob(WT_MODULES_DIR . '*/language/*.{csv,php,mo}', GLOB_BRACE),
169*3bdc890bSGreg Roach			glob(WT_DATA_DIR . 'language/*.{csv,php,mo}', GLOB_BRACE)
170a25f0a04SGreg Roach		);
171a25f0a04SGreg Roach
172*3bdc890bSGreg Roach		$rebuild_cache = false;
173*3bdc890bSGreg Roach		foreach ($translation_files as $translation_file) {
174*3bdc890bSGreg Roach			if (filemtime($translation_file) > $filemtime) {
175*3bdc890bSGreg Roach				$rebuild_cache = true;
176a25f0a04SGreg Roach				break;
177a25f0a04SGreg Roach			}
178a25f0a04SGreg Roach		}
179*3bdc890bSGreg Roach
180*3bdc890bSGreg Roach		if ($rebuild_cache) {
181*3bdc890bSGreg Roach			$translations = array();
182*3bdc890bSGreg Roach			foreach ($translation_files as $translation_file) {
183*3bdc890bSGreg Roach				$translation = new Translation($translation_file);
184*3bdc890bSGreg Roach				$translations = array_merge($translations, $translation->asArray());
185a25f0a04SGreg Roach			}
186*3bdc890bSGreg Roach			file_put_contents($cache_file, '<' . '?php return ' . var_export($translations, true) . ';');
187*3bdc890bSGreg Roach		} else {
188*3bdc890bSGreg Roach			$translations = include $cache_file;
189a25f0a04SGreg Roach		}
190a25f0a04SGreg Roach
191*3bdc890bSGreg Roach		// Create a translator
192*3bdc890bSGreg Roach		self::$translator = new Translator($translations, self::$locale->pluralRule());
193a25f0a04SGreg Roach
194a25f0a04SGreg Roach		// Extract language settings from the translation file
195a25f0a04SGreg Roach		global $DATE_FORMAT; // I18N: This is the format string for full dates.  See http://php.net/date for codes
196*3bdc890bSGreg Roach		$DATE_FORMAT = self::$translator->translate('%j %F %Y');
197a25f0a04SGreg Roach
198a25f0a04SGreg Roach		global $TIME_FORMAT; // I18N: This is the format string for the time-of-day.  See http://php.net/date for codes
199*3bdc890bSGreg Roach		$TIME_FORMAT = self::$translator->translate('%H:%i:%s');
200a25f0a04SGreg Roach
201a25f0a04SGreg Roach		// Alphabetic sorting sequence (upper-case letters), used by webtrees to sort strings
202*3bdc890bSGreg Roach		list(, self::$alphabet_upper) = explode('=', self::$translator->translate('ALPHABET_upper=ABCDEFGHIJKLMNOPQRSTUVWXYZ'));
203a25f0a04SGreg Roach		// Alphabetic sorting sequence (lower-case letters), used by webtrees to sort strings
204*3bdc890bSGreg Roach		list(, self::$alphabet_lower) = explode('=', self::$translator->translate('ALPHABET_lower=abcdefghijklmnopqrstuvwxyz'));
205a25f0a04SGreg Roach
206*3bdc890bSGreg Roach		global $WEEK_START;
207*3bdc890bSGreg Roach		$WEEK_START = self::$locale->territory()->firstDay();
208c999a340SGreg Roach
2093cfaf201SGreg Roach		self::$list_separator = /* I18N: This punctuation is used to separate lists of items */ self::translate(', ');
210*3bdc890bSGreg Roach		self::$collation = 'utf8_' . self::$locale->collation();
211a25f0a04SGreg Roach
2125331c5eaSGreg Roach		return self::$locale->languageTag();
213a25f0a04SGreg Roach	}
214a25f0a04SGreg Roach
215a25f0a04SGreg Roach	/**
216c999a340SGreg Roach	 * The prefered locales for this site, or a default list if no preference.
217a25f0a04SGreg Roach	 *
218c999a340SGreg Roach	 * @return Locale[]
219a25f0a04SGreg Roach	 */
220c999a340SGreg Roach	public static function activeLocales() {
221c999a340SGreg Roach		$code_list = Site::getPreference('LANGUAGES');
222a25f0a04SGreg Roach
223c999a340SGreg Roach		if ($code_list) {
224c999a340SGreg Roach			$codes = explode(',', $code_list);
225a25f0a04SGreg Roach		} else {
226c999a340SGreg Roach			$codes = array(
227c999a340SGreg Roach				'ar', 'bg', 'bs', 'ca', 'cs', 'da', 'de', 'el', 'en-GB', 'en-US', 'es',
228c999a340SGreg Roach				'et', 'fi', 'fr', 'he', 'hr', 'hu', 'is', 'it', 'ka', 'lt', 'mr', 'nb',
229c999a340SGreg Roach				'nl', 'nn', 'pl', 'pt', 'ru', 'sk', 'sv', 'tr', 'uk', 'vi', 'zh-Hans',
230c999a340SGreg Roach			);
231a25f0a04SGreg Roach		}
232c999a340SGreg Roach
233c999a340SGreg Roach		$locales = array();
234c999a340SGreg Roach		foreach ($codes as $code) {
235c999a340SGreg Roach			if (file_exists(WT_ROOT . 'language/' . $code . '.mo')) {
236c999a340SGreg Roach				try {
237c999a340SGreg Roach					$locales[] = Locale::create($code);
238c999a340SGreg Roach				} catch (\Exception $ex) {
239c999a340SGreg Roach					// No such locale exists?
240a25f0a04SGreg Roach				}
241a25f0a04SGreg Roach			}
242c999a340SGreg Roach		}
243c999a340SGreg Roach		usort($locales, '\Fisharebest\Localization\Locale::compare');
244c999a340SGreg Roach
245c999a340SGreg Roach		return $locales;
246c999a340SGreg Roach	}
247c999a340SGreg Roach
248c999a340SGreg Roach	/**
249c999a340SGreg Roach	 * All locales for which a translation file exists.
250c999a340SGreg Roach	 *
251c999a340SGreg Roach	 * @return Locale[]
252c999a340SGreg Roach	 */
253c999a340SGreg Roach	public static function installedLocales() {
254c999a340SGreg Roach		$locales = array();
255c999a340SGreg Roach		foreach (glob(WT_ROOT . 'language/*.mo') as $file) {
256c999a340SGreg Roach			try {
257c999a340SGreg Roach				$locales[] = Locale::create(basename($file, '.mo'));
258c999a340SGreg Roach			} catch (\Exception $ex) {
259*3bdc890bSGreg Roach				// Not a recognised locale
260a25f0a04SGreg Roach			}
261a25f0a04SGreg Roach		}
262c999a340SGreg Roach		usort($locales, '\Fisharebest\Localization\Locale::compare');
263c999a340SGreg Roach
264c999a340SGreg Roach		return $locales;
265a25f0a04SGreg Roach	}
266a25f0a04SGreg Roach
267a25f0a04SGreg Roach	/**
268a25f0a04SGreg Roach	 * Generate i18n markup for the <html> tag, e.g. lang="ar" dir="rtl"
269a25f0a04SGreg Roach	 *
270a25f0a04SGreg Roach	 * @return string
271a25f0a04SGreg Roach	 */
272764a01d9SGreg Roach	public static function htmlAttributes() {
273c999a340SGreg Roach		return self::$locale->htmlAttributes();
274a25f0a04SGreg Roach	}
275a25f0a04SGreg Roach
276a25f0a04SGreg Roach	/**
277a25f0a04SGreg Roach	 * Translate a number into the local representation.
278a25f0a04SGreg Roach	 *
279a25f0a04SGreg Roach	 * e.g. 12345.67 becomes
280a25f0a04SGreg Roach	 * en: 12,345.67
281a25f0a04SGreg Roach	 * fr: 12 345,67
282a25f0a04SGreg Roach	 * de: 12.345,67
283a25f0a04SGreg Roach	 *
284a25f0a04SGreg Roach	 * @param float   $n
285a25f0a04SGreg Roach	 * @param integer $precision
286a25f0a04SGreg Roach	 *
287a25f0a04SGreg Roach	 * @return string
288a25f0a04SGreg Roach	 */
289a25f0a04SGreg Roach	public static function number($n, $precision = 0) {
290c999a340SGreg Roach		return self::$locale->number(round($n, $precision));
291a25f0a04SGreg Roach	}
292a25f0a04SGreg Roach
293a25f0a04SGreg Roach	/**
294a25f0a04SGreg Roach	 * Convert the digits 0-9 into the local script
295a25f0a04SGreg Roach	 *
296a25f0a04SGreg Roach	 * Used for years, etc., where we do not want thousands-separators, decimals, etc.
297a25f0a04SGreg Roach	 *
298a25f0a04SGreg Roach	 * @param integer $n
299a25f0a04SGreg Roach	 *
300a25f0a04SGreg Roach	 * @return string
301a25f0a04SGreg Roach	 */
302a25f0a04SGreg Roach	public static function digits($n) {
303c999a340SGreg Roach		return self::$locale->digits($n);
304a25f0a04SGreg Roach	}
305a25f0a04SGreg Roach
306a25f0a04SGreg Roach	/**
307a25f0a04SGreg Roach	 * Translate a fraction into a percentage.
308a25f0a04SGreg Roach	 *
309a25f0a04SGreg Roach	 * e.g. 0.123 becomes
310a25f0a04SGreg Roach	 * en: 12.3%
311a25f0a04SGreg Roach	 * fr: 12,3 %
312a25f0a04SGreg Roach	 * de: 12,3%
313a25f0a04SGreg Roach	 *
314a25f0a04SGreg Roach	 * @param float   $n
315a25f0a04SGreg Roach	 * @param integer $precision
316a25f0a04SGreg Roach	 *
317a25f0a04SGreg Roach	 * @return string
318a25f0a04SGreg Roach	 */
319a25f0a04SGreg Roach	public static function percentage($n, $precision = 0) {
320*3bdc890bSGreg Roach		return self::$locale->percent($n, $precision);
321a25f0a04SGreg Roach	}
322a25f0a04SGreg Roach
323a25f0a04SGreg Roach	/**
3243cfaf201SGreg Roach	 * What is the direction of the current locale
3253cfaf201SGreg Roach	 *
3263cfaf201SGreg Roach	 * @return string "ltr" or "rtl"
3273cfaf201SGreg Roach	 */
328c0af647eSGreg Roach	public static function direction() {
329c999a340SGreg Roach		return self::$locale->direction();
3303cfaf201SGreg Roach	}
3313cfaf201SGreg Roach
3323cfaf201SGreg Roach	/**
333a25f0a04SGreg Roach	 * Translate a string, and then substitute placeholders
334a25f0a04SGreg Roach	 *
335a25f0a04SGreg Roach	 * echo I18N::translate('Hello World!');
336a25f0a04SGreg Roach	 * echo I18N::translate('The %s sat on the mat', 'cat');
337a25f0a04SGreg Roach	 *
338a25f0a04SGreg Roach	 * @return string
339a25f0a04SGreg Roach	 */
340a25f0a04SGreg Roach	public static function translate(/* var_args */) {
341a25f0a04SGreg Roach		$args = func_get_args();
342*3bdc890bSGreg Roach		$args[0] = self::$translator->translate($args[0]);
343a25f0a04SGreg Roach
344a25f0a04SGreg Roach		return call_user_func_array('sprintf', $args);
345a25f0a04SGreg Roach	}
346a25f0a04SGreg Roach
347a25f0a04SGreg Roach	/**
348a25f0a04SGreg Roach	 * Context sensitive version of translate.
349a25f0a04SGreg Roach	 *
350a25f0a04SGreg Roach	 * echo I18N::translate_c('NOMINATIVE', 'January');
351a25f0a04SGreg Roach	 * echo I18N::translate_c('GENITIVE',   'January');
352a25f0a04SGreg Roach	 *
353a25f0a04SGreg Roach	 * @return string
354a25f0a04SGreg Roach	 */
355764a01d9SGreg Roach	public static function translateContext(/* var_args */) {
356a25f0a04SGreg Roach		$args = func_get_args();
357*3bdc890bSGreg Roach		$args[0] = self::$translator->translateContext($args[0], $args[1]);
358a25f0a04SGreg Roach		unset($args[1]);
359a25f0a04SGreg Roach
360a25f0a04SGreg Roach		return call_user_func_array('sprintf', $args);
361a25f0a04SGreg Roach	}
362a25f0a04SGreg Roach
363a25f0a04SGreg Roach	/**
364a25f0a04SGreg Roach	 * Similar to translate, but do perform "no operation" on it.
365a25f0a04SGreg Roach	 *
366a25f0a04SGreg Roach	 * This is necessary to fetch a format string (containing % characters) without
367a25f0a04SGreg Roach	 * performing sustitution of arguments.
368a25f0a04SGreg Roach	 *
369a25f0a04SGreg Roach	 * @param string $string
370a25f0a04SGreg Roach	 *
371a25f0a04SGreg Roach	 * @return string
372a25f0a04SGreg Roach	 */
373a25f0a04SGreg Roach	public static function noop($string) {
374*3bdc890bSGreg Roach		return self::$translator->translate($string);
375a25f0a04SGreg Roach	}
376a25f0a04SGreg Roach
377a25f0a04SGreg Roach	/**
378a25f0a04SGreg Roach	 * Translate a plural string
379a25f0a04SGreg Roach	 *
380a25f0a04SGreg Roach	 * echo self::plural('There is an error', 'There are errors', $num_errors);
381a25f0a04SGreg Roach	 * echo self::plural('There is one error', 'There are %s errors', $num_errors);
382409b0734SGreg Roach	 * echo self::plural('There is %1$s %2$s cat', 'There are %1$s %2$s cats', $num, $num, $colour);
383a25f0a04SGreg Roach	 *
384a25f0a04SGreg Roach	 * @return string
385a25f0a04SGreg Roach	 */
386a25f0a04SGreg Roach	public static function plural(/* var_args */) {
387a25f0a04SGreg Roach		$args = func_get_args();
388*3bdc890bSGreg Roach		$args[0] = self::$translator->plural($args[0], $args[1], $args[2]);
389*3bdc890bSGreg Roach		unset($args[1], $args[2]);
390a25f0a04SGreg Roach
391a25f0a04SGreg Roach		return call_user_func_array('sprintf', $args);
392a25f0a04SGreg Roach	}
393a25f0a04SGreg Roach
394a25f0a04SGreg Roach	/**
395a25f0a04SGreg Roach	 * Convert a GEDCOM age string into translated_text
396a25f0a04SGreg Roach	 *
397a25f0a04SGreg Roach	 * NB: The import function will have normalised this, so we don't need
398a25f0a04SGreg Roach	 * to worry about badly formatted strings
399a25f0a04SGreg Roach	 * NOTE: this function is not yet complete - eventually it will replace get_age_at_event()
400a25f0a04SGreg Roach	 *
401a25f0a04SGreg Roach	 * @param $string
402a25f0a04SGreg Roach	 *
403a25f0a04SGreg Roach	 * @return string
404a25f0a04SGreg Roach	 */
405764a01d9SGreg Roach	public static function gedcomAge($string) {
406a25f0a04SGreg Roach		switch ($string) {
407a25f0a04SGreg Roach		case 'STILLBORN':
408a25f0a04SGreg Roach			// I18N: Description of an individual’s age at an event.  For example, Died 14 Jan 1900 (stillborn)
409a25f0a04SGreg Roach			return self::translate('(stillborn)');
410a25f0a04SGreg Roach		case 'INFANT':
411a25f0a04SGreg Roach			// I18N: Description of an individual’s age at an event.  For example, Died 14 Jan 1900 (in infancy)
412a25f0a04SGreg Roach			return self::translate('(in infancy)');
413a25f0a04SGreg Roach		case 'CHILD':
414a25f0a04SGreg Roach			// I18N: Description of an individual’s age at an event.  For example, Died 14 Jan 1900 (in childhood)
415a25f0a04SGreg Roach			return self::translate('(in childhood)');
416a25f0a04SGreg Roach		}
417a25f0a04SGreg Roach		$age = array();
418a25f0a04SGreg Roach		if (preg_match('/(\d+)y/', $string, $match)) {
419a25f0a04SGreg Roach			// I18N: Part of an age string. e.g. 5 years, 4 months and 3 days
420a25f0a04SGreg Roach			$years = $match[1];
421a25f0a04SGreg Roach			$age[] = self::plural('%s year', '%s years', $years, self::number($years));
422a25f0a04SGreg Roach		} else {
423a25f0a04SGreg Roach			$years = -1;
424a25f0a04SGreg Roach		}
425a25f0a04SGreg Roach		if (preg_match('/(\d+)m/', $string, $match)) {
426a25f0a04SGreg Roach			// I18N: Part of an age string. e.g. 5 years, 4 months and 3 days
427a25f0a04SGreg Roach			$age[] = self::plural('%s month', '%s months', $match[1], self::number($match[1]));
428a25f0a04SGreg Roach		}
429a25f0a04SGreg Roach		if (preg_match('/(\d+)w/', $string, $match)) {
430a25f0a04SGreg Roach			// I18N: Part of an age string. e.g. 7 weeks and 3 days
431a25f0a04SGreg Roach			$age[] = self::plural('%s week', '%s weeks', $match[1], self::number($match[1]));
432a25f0a04SGreg Roach		}
433a25f0a04SGreg Roach		if (preg_match('/(\d+)d/', $string, $match)) {
434a25f0a04SGreg Roach			// I18N: Part of an age string. e.g. 5 years, 4 months and 3 days
435a25f0a04SGreg Roach			$age[] = self::plural('%s day', '%s days', $match[1], self::number($match[1]));
436a25f0a04SGreg Roach		}
437a25f0a04SGreg Roach		// If an age is just a number of years, only show the number
438c0af647eSGreg Roach		if (count($age) === 1 && $years >= 0) {
439a25f0a04SGreg Roach			$age = $years;
440a25f0a04SGreg Roach		}
441a25f0a04SGreg Roach		if ($age) {
442a25f0a04SGreg Roach			if (!substr_compare($string, '<', 0, 1)) {
443a25f0a04SGreg Roach				// I18N: Description of an individual’s age at an event.  For example, Died 14 Jan 1900 (aged less than 21 years)
444a25f0a04SGreg Roach				return self::translate('(aged less than %s)', $age);
445a25f0a04SGreg Roach			} elseif (!substr_compare($string, '>', 0, 1)) {
446a25f0a04SGreg Roach				// I18N: Description of an individual’s age at an event.  For example, Died 14 Jan 1900 (aged more than 21 years)
447a25f0a04SGreg Roach				return self::translate('(aged more than %s)', $age);
448a25f0a04SGreg Roach			} else {
449a25f0a04SGreg Roach				// I18N: Description of an individual’s age at an event.  For example, Died 14 Jan 1900 (aged 43 years)
450a25f0a04SGreg Roach				return self::translate('(aged %s)', $age);
451a25f0a04SGreg Roach			}
452a25f0a04SGreg Roach		} else {
453a25f0a04SGreg Roach			// Not a valid string?
454a25f0a04SGreg Roach			return self::translate('(aged %s)', $string);
455a25f0a04SGreg Roach		}
456a25f0a04SGreg Roach	}
457a25f0a04SGreg Roach
458a25f0a04SGreg Roach	/**
459a25f0a04SGreg Roach	 * Convert a number of seconds into a relative time.  For example, 630 => "10 hours, 30 minutes ago"
460a25f0a04SGreg Roach	 *
461a25f0a04SGreg Roach	 * @param integer $seconds
462a25f0a04SGreg Roach	 *
463a25f0a04SGreg Roach	 * @return string
464a25f0a04SGreg Roach	 */
465a25f0a04SGreg Roach	public static function timeAgo($seconds) {
466a25f0a04SGreg Roach		$minute = 60;
467a25f0a04SGreg Roach		$hour   = 60 * $minute;
468a25f0a04SGreg Roach		$day    = 24 * $hour;
469a25f0a04SGreg Roach		$month  = 30 * $day;
470a25f0a04SGreg Roach		$year   = 365 * $day;
471a25f0a04SGreg Roach
472a25f0a04SGreg Roach		if ($seconds > $year) {
473a25f0a04SGreg Roach			$years = (int) ($seconds / $year);
474a25f0a04SGreg Roach			return self::plural('%s year ago', '%s years ago', $years, self::number($years));
475a25f0a04SGreg Roach		} elseif ($seconds > $month) {
476a25f0a04SGreg Roach			$months = (int) ($seconds / $month);
477a25f0a04SGreg Roach			return self::plural('%s month ago', '%s months ago', $months, self::number($months));
478a25f0a04SGreg Roach		} elseif ($seconds > $day) {
479a25f0a04SGreg Roach			$days = (int) ($seconds / $day);
480a25f0a04SGreg Roach			return self::plural('%s day ago', '%s days ago', $days, self::number($days));
481a25f0a04SGreg Roach		} elseif ($seconds > $hour) {
482a25f0a04SGreg Roach			$hours = (int) ($seconds / $hour);
483a25f0a04SGreg Roach			return self::plural('%s hour ago', '%s hours ago', $hours, self::number($hours));
484a25f0a04SGreg Roach		} elseif ($seconds > $minute) {
485a25f0a04SGreg Roach			$minutes = (int) ($seconds / $minute);
486a25f0a04SGreg Roach			return self::plural('%s minute ago', '%s minutes ago', $minutes, self::number($minutes));
487a25f0a04SGreg Roach		} else {
488a25f0a04SGreg Roach			return self::plural('%s second ago', '%s seconds ago', $seconds, self::number($seconds));
489a25f0a04SGreg Roach		}
490a25f0a04SGreg Roach	}
491a25f0a04SGreg Roach
492a25f0a04SGreg Roach	/**
493a25f0a04SGreg Roach	 * Return the endonym for a given language - as per http://cldr.unicode.org/
494a25f0a04SGreg Roach	 *
495a25f0a04SGreg Roach	 * @param string $locale
496a25f0a04SGreg Roach	 *
497a25f0a04SGreg Roach	 * @return string
498a25f0a04SGreg Roach	 */
499a25f0a04SGreg Roach	public static function languageName($locale) {
500c999a340SGreg Roach		return Locale::create($locale)->endonym();
501a25f0a04SGreg Roach	}
502a25f0a04SGreg Roach
503a25f0a04SGreg Roach	/**
504a25f0a04SGreg Roach	 * Return the script used by a given language
505a25f0a04SGreg Roach	 *
506a25f0a04SGreg Roach	 * @param string $locale
507a25f0a04SGreg Roach	 *
508a25f0a04SGreg Roach	 * @return string
509a25f0a04SGreg Roach	 */
510a25f0a04SGreg Roach	public static function languageScript($locale) {
511c999a340SGreg Roach		return Locale::create($locale)->script()->code();
512a25f0a04SGreg Roach	}
513a25f0a04SGreg Roach
514a25f0a04SGreg Roach	/**
515a25f0a04SGreg Roach	 * Identify the script used for a piece of text
516a25f0a04SGreg Roach	 *
517a25f0a04SGreg Roach	 * @param $string
518a25f0a04SGreg Roach	 *
519a25f0a04SGreg Roach	 * @return string
520a25f0a04SGreg Roach	 */
521a25f0a04SGreg Roach	public static function textScript($string) {
522a25f0a04SGreg Roach		$string = strip_tags($string); // otherwise HTML tags show up as latin
523a25f0a04SGreg Roach		$string = html_entity_decode($string, ENT_QUOTES, 'UTF-8'); // otherwise HTML entities show up as latin
524a25f0a04SGreg Roach		$string = str_replace(array('@N.N.', '@P.N.'), '', $string); // otherwise unknown names show up as latin
525a25f0a04SGreg Roach		$pos = 0;
526a25f0a04SGreg Roach		$strlen = strlen($string);
527a25f0a04SGreg Roach		while ($pos < $strlen) {
528a25f0a04SGreg Roach			// get the Unicode Code Point for the character at position $pos
529a25f0a04SGreg Roach			$byte1 = ord($string[$pos]);
530a25f0a04SGreg Roach			if ($byte1 < 0x80) {
531a25f0a04SGreg Roach				$code_point = $byte1;
532a25f0a04SGreg Roach				$chrlen = 1;
533a25f0a04SGreg Roach			} elseif ($byte1 < 0xC0) {
534a25f0a04SGreg Roach				// Invalid continuation character
535a25f0a04SGreg Roach				return 'Latn';
536a25f0a04SGreg Roach			} elseif ($byte1 < 0xE0) {
537a25f0a04SGreg Roach				$code_point = (($byte1 & 0x1F) << 6) + (ord($string[$pos + 1]) & 0x3F);
538a25f0a04SGreg Roach				$chrlen = 2;
539a25f0a04SGreg Roach			} elseif ($byte1 < 0xF0) {
540a25f0a04SGreg Roach				$code_point = (($byte1 & 0x0F) << 12) + ((ord($string[$pos + 1]) & 0x3F) << 6) + (ord($string[$pos + 2]) & 0x3F);
541a25f0a04SGreg Roach				$chrlen = 3;
542a25f0a04SGreg Roach			} elseif ($byte1 < 0xF8) {
543a25f0a04SGreg Roach				$code_point = (($byte1 & 0x07) << 24) + ((ord($string[$pos + 1]) & 0x3F) << 12) + ((ord($string[$pos + 2]) & 0x3F) << 6) + (ord($string[$pos + 3]) & 0x3F);
544a25f0a04SGreg Roach				$chrlen = 3;
545a25f0a04SGreg Roach 			} else {
546a25f0a04SGreg Roach				// Invalid UTF
547a25f0a04SGreg Roach				return 'Latn';
548a25f0a04SGreg Roach			}
549a25f0a04SGreg Roach
550a25f0a04SGreg Roach			foreach (self::$scripts as $range) {
551a25f0a04SGreg Roach				if ($code_point >= $range[1] && $code_point <= $range[2]) {
552a25f0a04SGreg Roach					return $range[0];
553a25f0a04SGreg Roach				}
554a25f0a04SGreg Roach			}
555a25f0a04SGreg Roach			// Not a recognised script.  Maybe punctuation, spacing, etc.  Keep looking.
556a25f0a04SGreg Roach			$pos += $chrlen;
557a25f0a04SGreg Roach		}
558a25f0a04SGreg Roach
559a25f0a04SGreg Roach		return 'Latn';
560a25f0a04SGreg Roach	}
561a25f0a04SGreg Roach
562a25f0a04SGreg Roach	/**
563a25f0a04SGreg Roach	 * Return the direction (ltr or rtl) for a given script
564a25f0a04SGreg Roach	 *
565a25f0a04SGreg Roach	 * The PHP/intl library does not provde this information, so we need
566a25f0a04SGreg Roach	 * our own lookup table.
567a25f0a04SGreg Roach	 *
568a25f0a04SGreg Roach	 * @param string $script
569a25f0a04SGreg Roach	 *
570a25f0a04SGreg Roach	 * @return string
571a25f0a04SGreg Roach	 */
572a25f0a04SGreg Roach	public static function scriptDirection($script) {
573a25f0a04SGreg Roach		switch ($script) {
574a25f0a04SGreg Roach		case 'Arab':
575a25f0a04SGreg Roach		case 'Hebr':
576a25f0a04SGreg Roach		case 'Mong':
577a25f0a04SGreg Roach		case 'Thaa':
578a25f0a04SGreg Roach			return 'rtl';
579a25f0a04SGreg Roach		default:
580a25f0a04SGreg Roach			return 'ltr';
581a25f0a04SGreg Roach		}
582a25f0a04SGreg Roach	}
583a25f0a04SGreg Roach
584a25f0a04SGreg Roach	/**
585a25f0a04SGreg Roach	 * UTF8 version of PHP::strtoupper()
586a25f0a04SGreg Roach	 *
587a25f0a04SGreg Roach	 * Convert a string to upper case, using the rules from the current locale
588a25f0a04SGreg Roach	 *
589a25f0a04SGreg Roach	 * @param string $string
590a25f0a04SGreg Roach	 *
591a25f0a04SGreg Roach	 * @return string
592a25f0a04SGreg Roach	 */
593a25f0a04SGreg Roach	public static function strtoupper($string) {
594c999a340SGreg Roach		if (self::$locale->language()->code() === 'tr' || self::$locale->language()->code() === 'az') {
595a25f0a04SGreg Roach			return TurkishUtf8::strtoupper($string);
596a25f0a04SGreg Roach		} else {
597a25f0a04SGreg Roach			return mb_strtoupper($string);
598a25f0a04SGreg Roach		}
599a25f0a04SGreg Roach	}
600a25f0a04SGreg Roach
601a25f0a04SGreg Roach	/**
602a25f0a04SGreg Roach	 * UTF8 version of PHP::strtolower()
603a25f0a04SGreg Roach	 *
604a25f0a04SGreg Roach	 * Convert a string to lower case, using the rules from the current locale
605a25f0a04SGreg Roach	 *
606a25f0a04SGreg Roach	 * @param string $string
607a25f0a04SGreg Roach	 *
608a25f0a04SGreg Roach	 * @return string
609a25f0a04SGreg Roach	 */
610a25f0a04SGreg Roach	public static function strtolower($string) {
611c999a340SGreg Roach		if (self::$locale->language()->code() === 'tr' || self::$locale->language()->code() === 'az') {
612a25f0a04SGreg Roach			return TurkishUtf8::strtolower($string);
613a25f0a04SGreg Roach		} else {
614a25f0a04SGreg Roach			return mb_strtolower($string);
615a25f0a04SGreg Roach		}
616a25f0a04SGreg Roach	}
617a25f0a04SGreg Roach
618a25f0a04SGreg Roach	/**
619a25f0a04SGreg Roach	 * UTF8 version of PHP::strcasecmp()
620a25f0a04SGreg Roach	 *
621a25f0a04SGreg Roach	 * Perform a case-insensitive comparison of two strings, using rules from the current locale
622a25f0a04SGreg Roach	 *
623a25f0a04SGreg Roach	 * @param string $string1
624a25f0a04SGreg Roach	 * @param string $string2
625a25f0a04SGreg Roach	 *
626a25f0a04SGreg Roach	 * @return integer
627a25f0a04SGreg Roach	 */
628a25f0a04SGreg Roach	public static function strcasecmp($string1, $string2) {
629a25f0a04SGreg Roach		$strpos1 = 0;
630a25f0a04SGreg Roach		$strpos2 = 0;
631a25f0a04SGreg Roach		$strlen1 = strlen($string1);
632a25f0a04SGreg Roach		$strlen2 = strlen($string2);
633a25f0a04SGreg Roach		while ($strpos1 < $strlen1 && $strpos2 < $strlen2) {
634a25f0a04SGreg Roach			$byte1 = ord($string1[$strpos1]);
635a25f0a04SGreg Roach			$byte2 = ord($string2[$strpos2]);
636c0af647eSGreg Roach			if (($byte1 & 0xE0) === 0xC0) {
637a25f0a04SGreg Roach				$chr1 = $string1[$strpos1++] . $string1[$strpos1++];
638c0af647eSGreg Roach			} elseif (($byte1 & 0xF0) === 0xE0) {
639a25f0a04SGreg Roach				$chr1 = $string1[$strpos1++] . $string1[$strpos1++] . $string1[$strpos1++];
640a25f0a04SGreg Roach			} else {
641a25f0a04SGreg Roach				$chr1 = $string1[$strpos1++];
642a25f0a04SGreg Roach			}
643c0af647eSGreg Roach			if (($byte2 & 0xE0) === 0xC0) {
644a25f0a04SGreg Roach				$chr2 = $string2[$strpos2++] . $string2[$strpos2++];
645c0af647eSGreg Roach			} elseif (($byte2 & 0xF0) === 0xE0) {
646a25f0a04SGreg Roach				$chr2 = $string2[$strpos2++] . $string2[$strpos2++] . $string2[$strpos2++];
647a25f0a04SGreg Roach			} else {
648a25f0a04SGreg Roach				$chr2 = $string2[$strpos2++];
649a25f0a04SGreg Roach			}
650c0af647eSGreg Roach			if ($chr1 === $chr2) {
651a25f0a04SGreg Roach				continue;
652a25f0a04SGreg Roach			}
653a25f0a04SGreg Roach			// Try the local alphabet first
654a25f0a04SGreg Roach			$offset1 = strpos(self::$alphabet_lower, $chr1);
655a25f0a04SGreg Roach			if ($offset1 === false) {
656a25f0a04SGreg Roach				$offset1 = strpos(self::$alphabet_upper, $chr1);
657a25f0a04SGreg Roach			}
658a25f0a04SGreg Roach			$offset2 = strpos(self::$alphabet_lower, $chr2);
659a25f0a04SGreg Roach			if ($offset2 === false) {
660a25f0a04SGreg Roach				$offset2 = strpos(self::$alphabet_upper, $chr2);
661a25f0a04SGreg Roach			}
662a25f0a04SGreg Roach			if ($offset1 !== false && $offset2 !== false) {
663c0af647eSGreg Roach				if ($offset1 === $offset2) {
664a25f0a04SGreg Roach					continue;
665a25f0a04SGreg Roach				} else {
666a25f0a04SGreg Roach					return $offset1 - $offset2;
667a25f0a04SGreg Roach				}
668a25f0a04SGreg Roach			}
669a25f0a04SGreg Roach			// Try the global alphabet next
670a25f0a04SGreg Roach			$offset1 = strpos(self::ALPHABET_LOWER, $chr1);
671a25f0a04SGreg Roach			if ($offset1 === false) {
672a25f0a04SGreg Roach				$offset1 = strpos(self::ALPHABET_UPPER, $chr1);
673a25f0a04SGreg Roach			}
674a25f0a04SGreg Roach			$offset2 = strpos(self::ALPHABET_LOWER, $chr2);
675a25f0a04SGreg Roach			if ($offset2 === false) {
676a25f0a04SGreg Roach				$offset2 = strpos(self::ALPHABET_UPPER, $chr2);
677a25f0a04SGreg Roach			}
678a25f0a04SGreg Roach			if ($offset1 !== false && $offset2 !== false) {
679c0af647eSGreg Roach				if ($offset1 === $offset2) {
680a25f0a04SGreg Roach					continue;
681a25f0a04SGreg Roach				} else {
682a25f0a04SGreg Roach					return $offset1 - $offset2;
683a25f0a04SGreg Roach				}
684a25f0a04SGreg Roach			}
685a25f0a04SGreg Roach			// Just compare by unicode order
686a25f0a04SGreg Roach			return strcmp($chr1, $chr2);
687a25f0a04SGreg Roach		}
688a25f0a04SGreg Roach		// Shortest string comes first.
689a25f0a04SGreg Roach		return ($strlen1 - $strpos1) - ($strlen2 - $strpos2);
690a25f0a04SGreg Roach	}
691a25f0a04SGreg Roach
692a25f0a04SGreg Roach	/**
693a25f0a04SGreg Roach	 * UTF8 version of PHP::strrev()
694a25f0a04SGreg Roach	 *
695a25f0a04SGreg Roach	 * Reverse RTL text for third-party libraries such as GD2 and googlechart.
696a25f0a04SGreg Roach	 *
697a25f0a04SGreg Roach	 * These do not support UTF8 text direction, so we must mimic it for them.
698a25f0a04SGreg Roach	 *
699a25f0a04SGreg Roach	 * Numbers are always rendered LTR, even in RTL text.
700a25f0a04SGreg Roach	 * The visual direction of characters such as parentheses should be reversed.
701a25f0a04SGreg Roach	 *
702a25f0a04SGreg Roach	 * @param string $text Text to be reversed
703a25f0a04SGreg Roach	 *
704a25f0a04SGreg Roach	 * @return string
705a25f0a04SGreg Roach	 */
706a25f0a04SGreg Roach	public static function reverseText($text) {
707a25f0a04SGreg Roach		// Remove HTML markup - we can't display it and it is LTR.
708a25f0a04SGreg Roach		$text = Filter::unescapeHtml($text);
709a25f0a04SGreg Roach
710a25f0a04SGreg Roach		// LTR text doesn't need reversing
711c0af647eSGreg Roach		if (self::scriptDirection(self::textScript($text)) === 'ltr') {
712a25f0a04SGreg Roach			return $text;
713a25f0a04SGreg Roach		}
714a25f0a04SGreg Roach
715a25f0a04SGreg Roach		// Mirrored characters
716a25f0a04SGreg Roach		$text = strtr($text, self::$mirror_characters);
717a25f0a04SGreg Roach
718a25f0a04SGreg Roach		$reversed = '';
719a25f0a04SGreg Roach		$digits = '';
720a25f0a04SGreg Roach		while ($text != '') {
721a25f0a04SGreg Roach			$letter = mb_substr($text, 0, 1);
722a25f0a04SGreg Roach			$text = mb_substr($text, 1);
723a25f0a04SGreg Roach			if (strpos(self::DIGITS, $letter) !== false) {
724a25f0a04SGreg Roach				$digits .= $letter;
725a25f0a04SGreg Roach			} else {
726a25f0a04SGreg Roach				$reversed = $letter . $digits . $reversed;
727a25f0a04SGreg Roach				$digits = '';
728a25f0a04SGreg Roach			}
729a25f0a04SGreg Roach		}
730a25f0a04SGreg Roach
731a25f0a04SGreg Roach		return $digits . $reversed;
732a25f0a04SGreg Roach	}
733a25f0a04SGreg Roach
734a25f0a04SGreg Roach	/**
735a25f0a04SGreg Roach	 * Generate consistent I18N for datatables.js
736a25f0a04SGreg Roach	 *
737a25f0a04SGreg Roach	 * @param array|null $lengths An optional array of page lengths
738a25f0a04SGreg Roach	 *
739a25f0a04SGreg Roach	 * @return string
740a25f0a04SGreg Roach	 */
741a25f0a04SGreg Roach	public static function datatablesI18N(array $lengths = null) {
742a25f0a04SGreg Roach		if ($lengths === null) {
743a25f0a04SGreg Roach			$lengths = array(10, 20, 30, 50, 100, -1);
744a25f0a04SGreg Roach		}
745a25f0a04SGreg Roach
746a25f0a04SGreg Roach		$length_menu = '';
747a25f0a04SGreg Roach		foreach ($lengths as $length) {
748a25f0a04SGreg Roach			$length_menu .=
749a25f0a04SGreg Roach				'<option value="' . $length . '">' .
750c0af647eSGreg Roach				($length === -1 ? /* I18N: listbox option, e.g. “10,25,50,100,all” */ self::translate('All') : self::number($length)) .
751a25f0a04SGreg Roach				'</option>';
752a25f0a04SGreg Roach		}
753a25f0a04SGreg Roach		$length_menu = '<select>' . $length_menu . '</select>';
754a25f0a04SGreg Roach		$length_menu = /* I18N: Display %s [records per page], %s is a placeholder for listbox containing numeric options */ self::translate('Display %s', $length_menu);
755a25f0a04SGreg Roach
756c999a340SGreg Roach		$digits = self::$locale->digits('0123456789');
757c0af647eSGreg Roach		if ($digits === '0123456789') {
758a25f0a04SGreg Roach			$callback = '';
759a25f0a04SGreg Roach		} else {
760a25f0a04SGreg Roach			$callback = ',
761a25f0a04SGreg Roach				"infoCallback": function(oSettings, iStart, iEnd, iMax, iTotal, sPre) {
762a25f0a04SGreg Roach					return sPre
763a25f0a04SGreg Roach						.replace(/0/g, "' . mb_substr($digits, 0, 1) . '")
764a25f0a04SGreg Roach						.replace(/1/g, "' . mb_substr($digits, 1, 1) . '")
765a25f0a04SGreg Roach						.replace(/2/g, "' . mb_substr($digits, 2, 1) . '")
766a25f0a04SGreg Roach						.replace(/3/g, "' . mb_substr($digits, 3, 1) . '")
767a25f0a04SGreg Roach						.replace(/4/g, "' . mb_substr($digits, 4, 1) . '")
768a25f0a04SGreg Roach						.replace(/5/g, "' . mb_substr($digits, 5, 1) . '")
769a25f0a04SGreg Roach						.replace(/6/g, "' . mb_substr($digits, 6, 1) . '")
770a25f0a04SGreg Roach						.replace(/7/g, "' . mb_substr($digits, 7, 1) . '")
771a25f0a04SGreg Roach						.replace(/8/g, "' . mb_substr($digits, 8, 1) . '")
772a25f0a04SGreg Roach						.replace(/9/g, "' . mb_substr($digits, 9, 1) . '");
773a25f0a04SGreg Roach				},
774a25f0a04SGreg Roach				"formatNumber": function(iIn) {
775a25f0a04SGreg Roach					return String(iIn)
776a25f0a04SGreg Roach						.replace(/0/g, "' . mb_substr($digits, 0, 1) . '")
777a25f0a04SGreg Roach						.replace(/1/g, "' . mb_substr($digits, 1, 1) . '")
778a25f0a04SGreg Roach						.replace(/2/g, "' . mb_substr($digits, 2, 1) . '")
779a25f0a04SGreg Roach						.replace(/3/g, "' . mb_substr($digits, 3, 1) . '")
780a25f0a04SGreg Roach						.replace(/4/g, "' . mb_substr($digits, 4, 1) . '")
781a25f0a04SGreg Roach						.replace(/5/g, "' . mb_substr($digits, 5, 1) . '")
782a25f0a04SGreg Roach						.replace(/6/g, "' . mb_substr($digits, 6, 1) . '")
783a25f0a04SGreg Roach						.replace(/7/g, "' . mb_substr($digits, 7, 1) . '")
784a25f0a04SGreg Roach						.replace(/8/g, "' . mb_substr($digits, 8, 1) . '")
785a25f0a04SGreg Roach						.replace(/9/g, "' . mb_substr($digits, 9, 1) . '");
786a25f0a04SGreg Roach				}
787a25f0a04SGreg Roach			';
788a25f0a04SGreg Roach		}
789a25f0a04SGreg Roach
790a25f0a04SGreg Roach		return
791a25f0a04SGreg Roach			'"language": {' .
792a25f0a04SGreg Roach			' "paginate": {' .
793a25f0a04SGreg Roach			'  "first":    "' . /* I18N: button label, first page    */ self::translate('first') . '",' .
794a25f0a04SGreg Roach			'  "last":     "' . /* I18N: button label, last page     */ self::translate('last') . '",' .
795a25f0a04SGreg Roach			'  "next":     "' . /* I18N: button label, next page     */ self::translate('next') . '",' .
796a25f0a04SGreg Roach			'  "previous": "' . /* I18N: button label, previous page */ self::translate('previous') . '"' .
797a25f0a04SGreg Roach			' },' .
798a25f0a04SGreg Roach			' "emptyTable":     "' . self::translate('No records to display') . '",' .
799a25f0a04SGreg Roach			' "info":           "' . /* I18N: %s are placeholders for numbers */ self::translate('Showing %1$s to %2$s of %3$s', '_START_', '_END_', '_TOTAL_') . '",' .
800a25f0a04SGreg Roach			' "infoEmpty":      "' . self::translate('Showing %1$s to %2$s of %3$s', 0, 0, 0) . '",' .
801a25f0a04SGreg Roach			' "infoFiltered":   "' . /* I18N: %s is a placeholder for a number */ self::translate('(filtered from %s total entries)', '_MAX_') . '",' .
802a25f0a04SGreg Roach			' "infoPostfix":    "",' .
803a25f0a04SGreg Roach			' "lengthMenu":     "' . Filter::escapeJs($length_menu) . '",' .
804a25f0a04SGreg Roach			' "loadingRecords": "' . self::translate('Loading…') . '",' .
805a25f0a04SGreg Roach			' "processing":     "' . self::translate('Loading…') . '",' .
806a25f0a04SGreg Roach			' "search":         "' . self::translate('Filter') . '",' .
807a25f0a04SGreg Roach			' "url":            "",' .
808a25f0a04SGreg Roach			' "zeroRecords":    "' . self::translate('No records to display') . '"' .
809a25f0a04SGreg Roach			'}' .
810a25f0a04SGreg Roach			$callback;
811a25f0a04SGreg Roach	}
812a25f0a04SGreg Roach}
813