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