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