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