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