language.php 11.4 KB
Newer Older
1
<?php
2
/**
3
 * @file include/language.php
4
 *
5
 * @brief Translation support.
6 7 8
 *
 * This file contains functions to work with translations and other
 * language related tasks.
9 10
 */

Klaus Weidenbach's avatar
Klaus Weidenbach committed
11
use CommerceGuys\Intl\Language\LanguageRepository;
12 13

/**
14 15 16 17
 * @brief Get the browser's submitted preferred languages.
 *
 * This functions parses the HTTP_ACCEPT_LANGUAGE header sent by the browser and
 * extracts the preferred languages and their priority.
18 19 20
 *
 * Get the language setting directly from system variables, bypassing get_config()
 * as database may not yet be configured.
21
 *
22 23
 * If possible, we use the value from the browser.
 *
24
 * @return array with ordered list of preferred languages from browser
25 26
 */
function get_browser_language() {
27
	$langs = array();
28
	$lang_parse = array();
29

30
	if (x($_SERVER, 'HTTP_ACCEPT_LANGUAGE')) {
31
		// break up string into pieces (languages and q factors)
32
		preg_match_all('/([a-z]{1,8}(-[a-z]{1,8})?)\s*(;\s*q\s*=\s*(1|0\.[0-9]+))?/i',
33 34
			$_SERVER['HTTP_ACCEPT_LANGUAGE'], $lang_parse);

35 36 37
		if (count($lang_parse[1])) {
			// create a list like "en" => 0.8
			$langs = array_combine($lang_parse[1], $lang_parse[4]);
38

39 40 41 42 43
			// set default to 1 for any without q factor
			foreach ($langs as $lang => $val) {
				if ($val === '') $langs[$lang] = 1;
			}

44
			// sort list based on value
45 46
			arsort($langs, SORT_NUMERIC);
		}
47
	}
48 49

	return $langs;
friendica's avatar
.  
friendica committed
50
}
51

52 53 54 55
/**
 * @brief Returns the best language for which also a translation exists.
 *
 * This function takes the results from get_browser_language() and compares it
56
 * with the available translations and returns the best fitting language for
57 58 59 60 61 62
 * which there exists a translation.
 *
 * If there is no match fall back to config['system']['language']
 *
 * @return Language code in 2-letter ISO 639-1 (en).
 */
63 64
function get_best_language() {
	$langs = get_browser_language();
65 66 67

	if(isset($langs) && count($langs)) {
		foreach ($langs as $lang => $v) {
68
			$lang = strtolower($lang);
69
			if(is_dir("view/$lang")) {
70 71 72 73 74 75
				$preferred = $lang;
				break;
			}
		}
	}

76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102

	if(! isset($preferred)) {

		/*
		 * We could find no perfect match for any of the preferred languages. 
		 * For cases where the preference is fr-fr and we have fr but *not* fr-fr
		 * run the test again and only look for the language base
		 * which should provide an interface they can sort of understand
		 */

		if(isset($langs) && count($langs)) {
			foreach ($langs as $lang => $v) {
				if(strlen($lang) ===  2) {
					/* we have already checked this language */
					continue;
				}
				/* Check the base */
				$lang = strtolower(substr($lang,0,2));
				if(is_dir("view/$lang")) {
					$preferred = $lang;
					break;
				}
			}
		}
	}

	if(! isset($preferred)) {
103
		$preferred = 'unset';
104
	}
105 106 107 108 109 110 111

	$arr = array('langs' => $langs, 'preferred' => $preferred);

	call_hooks('get_best_language',$arr);

	if($arr['preferred'] !== 'unset')
		return $arr['preferred'];
112

redmatrix's avatar
redmatrix committed
113
	return ((isset(App::$config['system']['language'])) ? App::$config['system']['language'] : 'en');
114
}
115

116 117 118 119 120 121
/*
 * push_lang and pop_lang let you temporarily override the default language.
 * Often used to email the administrator during a session created in another language.
 * The stack is one level deep - so you must pop after every push.
 */

122 123 124

function push_lang($language) {

redmatrix's avatar
redmatrix committed
125
	App::$langsave = App::$language;
126

redmatrix's avatar
redmatrix committed
127
	if($language === App::$language)
128 129
		return;

redmatrix's avatar
redmatrix committed
130 131
	if(isset(App::$strings) && count(App::$strings)) {
		App::$stringsave = App::$strings;
132
	}
redmatrix's avatar
redmatrix committed
133
	App::$strings = array();
134
	load_translation_table($language);
redmatrix's avatar
redmatrix committed
135
	App::$language = $language;
136 137 138 139
}

function pop_lang() {

redmatrix's avatar
redmatrix committed
140
	if(App::$language === App::$langsave)
141 142
		return;

143
	if(isset(App::$stringsave) && is_array(App::$stringsave))
redmatrix's avatar
redmatrix committed
144
		App::$strings = App::$stringsave;
145
	else
redmatrix's avatar
redmatrix committed
146
		App::$strings = array();
147

redmatrix's avatar
redmatrix committed
148
	App::$language = App::$langsave;
149 150
}

151 152 153 154 155 156
/**
 * @brief Load string translation table for alternate language.
 *
 * @param string $lang language code in 2-letter ISO 639-1 (en, de, fr) format
 * @param boolean $install (optional) default false
 */
157
function load_translation_table($lang, $install = false) {
158

redmatrix's avatar
redmatrix committed
159
	App::$strings = array();
160 161 162 163 164

	if(file_exists("view/$lang/hstrings.php")) {
		include("view/$lang/hstrings.php");
	}
	elseif(file_exists("view/$lang/strings.php")) {
165 166
		include("view/$lang/strings.php");
	}
friendica's avatar
.  
friendica committed
167

168
	if(! $install) {
169
		$plugins = q("SELECT aname FROM addon WHERE installed=1;");
170
		if ($plugins !== false) {
171
			foreach($plugins as $p) {
172
				$name = $p['aname'];
173 174 175 176
				if(file_exists("addon/$name/lang/$lang/hstrings.php")) {
					include("addon/$name/lang/$lang/hstrings.php");
				}
				elseif(file_exists("addon/$name/lang/$lang/strings.php")) {
177 178 179 180 181
					include("addon/$name/lang/$lang/strings.php");
				}
			}
		}
	}
182 183 184 185

	// Allow individual strings to be over-ridden on this site
	// Either for the default language or for all languages

186 187 188 189
	if(file_exists("view/local-$lang/hstrings.php")) {
		include("view/local-$lang/hstrings.php");
	}
	elseif(file_exists("view/local-$lang/strings.php")) {
190 191
		include("view/local-$lang/strings.php");
	}
friendica's avatar
.  
friendica committed
192
}
193

194
/**
195
 * @brief Translate string if translation exists.
196
 *
197
 * @param string $s string that should get translated
198
 * @param string $ctx (optional) context to appear in po file
199
 * @return translated string if exists, otherwise return $s
200
 *
201
 */
202
function t($s, $ctx = '') {
203

204
	$cs = $ctx ? '__ctx:' . $ctx . '__ ' . $s : $s;
redmatrix's avatar
redmatrix committed
205 206
	if (x(App::$strings, $cs)) {
		$t = App::$strings[$cs];
207

208
		return ((is_array($t)) ? translate_projectname($t[0]) : translate_projectname($t));
209
	}
210

211
	return translate_projectname($s);
friendica's avatar
.  
friendica committed
212 213
}

214 215 216 217 218
/**
 * @brief translate product name
 *  Merging strings from different project names is problematic so we'll do that with a string replacement
 */

219
function translate_projectname($s) {
220

221
	return str_replace(array('$projectname','$Projectname'),array(Zotlabs\Lib\System::get_platform_name(),ucfirst(Zotlabs\Lib\System::get_platform_name())),$s);
222 223 224 225 226

}



227 228 229 230 231 232 233 234 235
/**
 * @brief
 *
 * @param string $singular
 * @param string $plural
 * @param int $count
 * @param string $ctx
 * @return string
 */
236
function tt($singular, $plural, $count, $ctx = ''){
237

238
	$cs = $ctx ? "__ctx:" . $ctx . "__ " . $singular : $singular;
redmatrix's avatar
redmatrix committed
239 240 241
	if (x(App::$strings,$cs)) {
		$t = App::$strings[$cs];
		$f = 'string_plural_select_' . str_replace('-', '_', App::$language);
242
		if (! function_exists($f))
243
			$f = 'string_plural_select_default';
244

245
		$k = $f(intval($count));
246

247
		return is_array($t) ? $t[$k] : $t;
248
	}
249 250

	if ($count != 1) {
251 252 253 254
		return $plural;
	} else {
		return $singular;
	}
friendica's avatar
.  
friendica committed
255
}
256

257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279
/**
 * @brief Return slash separated string of plurals translation forms
 *
 * @param string $k key in translations array
 * @return string
 */
function ta($k){

        $t = App::$strings[$k];
        if (is_array($t))
                $t = implode("/", $t);
        return ($t == "" ? $k : $t);
}

/**
 * @brief Return string_plural_select_xx function code
 *
 * @return string
 */

function tf() {

        $s = "plural_function_code";
280
        return (x(App::$strings, $s) ? App::$strings[$s] : "0");
281 282
}

283 284 285 286 287 288 289
/**
 * @brief Provide a fallback which will not collide with a function defined in
 * any language file.
 *
 * @param int $n
 * @return boolean
 */
290 291
function string_plural_select_default($n) {
	return ($n != 1);
friendica's avatar
.  
friendica committed
292
}
293

294 295 296 297 298 299 300 301 302 303 304
/**
 * @brief Takes a string and tries to identify the language.
 *
 * It uses the pear library Text_LanguageDetect and it can identify 52 human languages.
 * It returns the identified languges and a confidence score for each.
 *
 * Strings need to have a min length config['system']['language_detect_min_length']
 * and you can influence the confidence that must be met before a result will get
 * returned through config['system']['language_detect_min_confidence'].
 *
 * @see http://pear.php.net/package/Text_LanguageDetect
305
 * @param string $s A string to examine
306
 * @return string Language code in 2-letter ISO 639-1 (en, de, fr) format
307
 */
308
function detect_language($s) {
309
	$min_length = get_config('system', 'language_detect_min_length');
310
	if ($min_length === false)
311 312
		$min_length = LANGUAGE_DETECT_MIN_LENGTH;

313
	$min_confidence = get_config('system', 'language_detect_min_confidence');
314
	if ($min_confidence === false)
315 316
		$min_confidence = LANGUAGE_DETECT_MIN_CONFIDENCE;

317
	// embedded apps have long base64 strings which will trip up the detector.
318
	$naked_body = preg_replace('/\[app\](.*?)\[\/app\]/', '', $s);
319
	// strip off bbcode
320
	$naked_body = preg_replace('/\[(.+?)\]/', '', $naked_body);
321
	if (mb_strlen($naked_body) < intval($min_length)) {
322
		logger('string length less than ' . intval($min_length), LOGGER_DATA);
323
		return '';
324
	}
325 326

	$l = new Text_LanguageDetect;
327 328 329 330 331 332 333 334
	try {
		// return 2-letter ISO 639-1 (en) language code
		$l->setNameMode(2);
		$lng = $l->detectConfidence($naked_body);
		logger('detect language: ' . print_r($lng, true) . $naked_body, LOGGER_DATA);
	} catch (Text_LanguageDetect_Exception $e) {
		logger('detect language exception: ' . $e->getMessage(), LOGGER_DATA);
	}
335

336
	if ((! $lng) || (! (x($lng,'language')))) {
337 338 339
		return '';
	}

340
	if ($lng['confidence'] < (float) $min_confidence) {
341 342 343 344
		logger('detect language: confidence less than ' . (float) $min_confidence, LOGGER_DATA);
		return '';
	}

345 346 347 348 349 350 351 352 353
	return($lng['language']);
}

/**
 * @brief Returns the display name of a given language code.
 *
 * By default we use the localized language name. You can switch the result
 * to any language with the optional 2nd parameter $l.
 *
354
 * $s and $l should be in 2-letter ISO 639-1 format.
355 356 357
 *
 * If nothing could be looked up it returns $s.
 *
358 359
 * @param string $s Language code to look up
 * @param string $l (optional) In which language to return the name
360 361 362
 * @return string with the language name, or $s if unrecognized
 */
function get_language_name($s, $l = null) {
363
	// get() expects the second part to be in upper case
364 365
	if (strpos($s, '-') !== false) $s = substr($s, 0, 2) . strtoupper(substr($s, 2));
	if ($l !== null && strpos($l, '-') !== false) $l = substr($l, 0, 2) . strtoupper(substr($l, 2));
366

367 368 369 370 371
	$languageRepository = new LanguageRepository;

	// Sometimes intl doesn't like the second part at all ...
	try {
		$language = $languageRepository->get($s, $l);
372 373 374
	} catch(CommerceGuys\Intl\Exception\UnknownLanguageException $e) {
		$s = substr($s, 0, 2);
		if($l !== null) $l = substr($s, 0, 2);
375 376
		try {
			$language = $languageRepository->get($s, $l);
377
		} catch (CommerceGuys\Intl\Exception\UnknownLanguageException $e) {
378
			return $s; // Give up
379 380
		} catch (CommerceGuys\Intl\Exception\UnknownLocaleException $e) {
			return $s; // Give up
381
		}
382
	}
383 384

	return $language->getName();
385
}
386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437



function language_list() {

	$langs = glob('view/*/hstrings.php');

	$lang_options = array();
	$selected = "";

	if(is_array($langs) && count($langs)) {
		if(! in_array('view/en/hstrings.php',$langs))
			$langs[] = 'view/en/';
		asort($langs);
		foreach($langs as $l) {
			$ll = substr($l,5);
			$ll = substr($ll,0,strrpos($ll,'/'));
			$lang_options[$ll] = get_language_name($ll, $ll) . " ($ll)";
		}
	}
	return $lang_options;
}

function lang_selector() {

	$langs = glob('view/*/hstrings.php');

	$lang_options = array();
	$selected = "";

	if(is_array($langs) && count($langs)) {
		$langs[] = '';
		if(! in_array('view/en/hstrings.php',$langs))
			$langs[] = 'view/en/';
		asort($langs);
		foreach($langs as $l) {
			if($l == '') {
				$lang_options[""] = t('default');
				continue;
			}
			$ll = substr($l,5);
			$ll = substr($ll,0,strrpos($ll,'/'));
			$selected = (($ll === App::$language && (x($_SESSION, 'language'))) ? $ll : $selected);
			$lang_options[$ll] = get_language_name($ll, $ll) . " ($ll)";
		}
	}

	$tpl = get_markup_template('lang_selector.tpl');

	$o = replace_macros($tpl, array(
		'$title' => t('Select an alternate language'),
		'$langs' => array($lang_options, $selected),
438

439 440 441 442 443
	));

	return $o;
}