Elgg  Version 4.3
Translator.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Elgg\I18n;
4 
5 use Elgg\Config;
6 use Elgg\Includer;
8 
16 class Translator {
17 
18  use Loggable;
19 
23  private $config;
24 
28  private $locale;
29 
33  private $translations = [];
34 
38  private $defaultPath = null;
39 
43  private $current_language = null;
44 
48  protected $allowed_languages = null;
49 
64  private $language_paths = [];
65 
69  private $was_reloaded = false;
70 
77  public function __construct(Config $config, LocaleService $locale) {
78  $this->config = $config;
79  $this->locale = $locale;
80 
81  $this->defaultPath = dirname(dirname(dirname(dirname(__DIR__)))) . "/languages/";
82 
83  $this->registerLanguagePath($this->defaultPath);
84  }
85 
91  public function getLoadedTranslations() {
92  return $this->translations;
93  }
94 
106  public function translate($message_key, array $args = [], $language = "") {
107  if (!is_string($message_key) || strlen($message_key) < 1) {
108  $this->getLogger()->warning('$message_key needs to be a string in ' . __METHOD__ . '(), ' . gettype($message_key) . ' provided');
109  return '';
110  }
111 
112  if (!$language) {
113  // no language provided, get current language
114  // based on detection, user setting or site
115  $language = $this->getCurrentLanguage();
116  }
117 
118  // build language array for different trys
119  // avoid dupes without overhead of array_unique
120  $langs = [
121  $language => true,
122  ];
123 
124  // load site language
125  $site_language = $this->config->language;
126  if (!empty($site_language)) {
127  $langs[$site_language] = true;
128  }
129 
130  // ultimate language fallback
131  $langs['en'] = true;
132 
133  $langs = array_intersect_key($langs, array_flip($this->getAllowedLanguages()));
134 
135  // try to translate
136  $string = $message_key;
137  foreach (array_keys($langs) as $try_lang) {
138  $this->ensureTranslationsLoaded($try_lang);
139 
140  if (isset($this->translations[$try_lang][$message_key])) {
141  $string = $this->translations[$try_lang][$message_key];
142 
143  // only pass through if we have arguments to allow backward compatibility
144  // with manual sprintf() calls.
145  if (!empty($args)) {
146  try {
147  $string = vsprintf($string, $args);
148 
149  if ($string === false) {
150  $string = $message_key;
151 
152  $this->getLogger()->warning("Translation error for key '{$message_key}': Too few arguments provided (" . var_export($args, true) . ')');
153  }
154  } catch (\ValueError $e) {
155  // PHP 8 throws errors
156  $string = $message_key;
157 
158  $this->getLogger()->warning("Translation error for key '{$message_key}': " . $e->getMessage());
159  }
160  }
161 
162  break;
163  } else {
164  $message = sprintf(
165  'Missing %s translation for "%s" language key',
166  ($try_lang === 'en') ? 'English' : $try_lang,
167  $message_key
168  );
169 
170  if ($try_lang === 'en') {
171  $this->getLogger()->notice($message);
172  } else {
173  $this->getLogger()->info($message);
174  }
175  }
176  }
177 
178  return $string;
179  }
180 
195  public function addTranslation($country_code, $language_array, $ensure_translations_loaded = true) {
196  $country_code = strtolower($country_code);
197  $country_code = trim($country_code);
198 
199  if (!is_array($language_array) || empty($language_array) || $country_code === "") {
200  return false;
201  }
202 
203  if (!isset($this->translations[$country_code])) {
204  $this->translations[$country_code] = [];
205 
206  if ($ensure_translations_loaded) {
207  // make sure all existing paths are included first before adding language arrays
208  $this->loadTranslations($country_code);
209  }
210  }
211 
212  // Note that we are using union operator instead of array_merge() due to performance implications
213  $this->translations[$country_code] = $language_array + $this->translations[$country_code];
214 
215  return true;
216  }
217 
223  public function getCurrentLanguage() {
224  if (!isset($this->current_language)) {
225  $this->current_language = $this->detectLanguage();
226  }
227 
228  if (!$this->current_language) {
229  $this->current_language = 'en';
230  }
231 
232  return $this->current_language;
233  }
234 
242  public function setCurrentLanguage($language = null) {
243  $this->current_language = $language;
244  }
245 
253  public function detectLanguage() {
254  // detect from URL
255  $url_lang = _elgg_services()->request->getParam('hl');
256  $user = _elgg_services()->session->getLoggedInUser();
257 
258  if (!empty($url_lang)) {
259  // store language for logged out users
260  if (empty($user)) {
261  $cookie = new \ElggCookie('language');
262  $cookie->value = $url_lang;
263  elgg_set_cookie($cookie);
264  }
265  return $url_lang;
266  }
267 
268  // detect from cookie
269  $cookie = _elgg_services()->request->cookies->get('language');
270  if (!empty($cookie)) {
271  return $cookie;
272  }
273 
274  // check logged in user
275  if (!empty($user) && !empty($user->language)) {
276  return $user->language;
277  }
278 
279  // detect from browser if not logged in
280  if ($this->config->language_detect_from_browser) {
281  $browserlangs = _elgg_services()->request->getLanguages();
282  if (!empty($browserlangs)) {
283  $browserlang = explode('_', $browserlangs[0]);
284 
285  return $browserlang[0];
286  }
287  }
288 
289  // get site setting
290  $site_language = $this->config->language;
291  if (!empty($site_language)) {
292  return $site_language;
293  }
294 
295  return false;
296  }
297 
307  public function bootTranslations() {
308  $languages = array_unique(['en', $this->getCurrentLanguage()]);
309 
310  foreach ($languages as $language) {
311  $this->loadTranslations($language);
312  }
313  }
314 
328  public function loadTranslations(string $language) {
329 
330  $data = elgg_load_system_cache("{$language}.lang");
331  if (is_array($data)) {
332  $this->addTranslation($language, $data, false);
333  return;
334  }
335 
336  foreach ($this->getLanguagePaths() as $path) {
337  $this->registerTranslations($path, false, $language);
338  }
339 
340  $translations = elgg_extract($language, $this->translations, []);
341  elgg_save_system_cache("{$language}.lang", $translations);
342  }
343 
356  public function registerTranslations(string $path, bool $load_all = false, string $language = null) {
357  $path = \Elgg\Project\Paths::sanitize($path);
358 
359  // don't need to register translations as the folder is missing
360  if (!is_dir($path)) {
361  $this->getLogger()->info("No translations could be loaded from: {$path}");
362  return true;
363  }
364 
365  // Make a note of this path just in case we need to register this language later
366  $this->registerLanguagePath($path);
367 
368  $this->getLogger()->info("Translations loaded from: {$path}");
369 
370  if ($language) {
371  $load_language_files = ["$language.php"];
372  $load_all = false;
373  } else {
374  // Get the current language based on site defaults and user preference
375  $current_language = $this->getCurrentLanguage();
376 
377  $load_language_files = [
378  'en.php',
379  "$current_language.php"
380  ];
381 
382  $load_language_files = array_unique($load_language_files);
383  }
384 
385  $return = true;
386  if ($handle = opendir($path)) {
387  while (false !== ($language_file = readdir($handle))) {
388  // ignore bad files
389  if (substr($language_file, 0, 1) == '.' || substr($language_file, -4) !== '.php') {
390  continue;
391  }
392 
393  if (in_array($language_file, $load_language_files) || $load_all) {
394  $return = $return && $this->includeLanguageFile($path . $language_file);
395  }
396  }
397  closedir($handle);
398  } else {
399  $this->getLogger()->error("Could not open language path: {$path}");
400  $return = false;
401  }
402 
403  return $return;
404  }
405 
414  protected function includeLanguageFile(string $path) {
416 
417  if (is_array($result)) {
418  $this->addTranslation(basename($path, '.php'), $result);
419  return true;
420  }
421 
422  $this->getLogger()->warning("Language file did not return an array: {$path}");
423 
424  return false;
425  }
426 
436  public function reloadAllTranslations() {
437  if ($this->was_reloaded) {
438  return;
439  }
440 
441  $languages = $this->getAvailableLanguages();
442 
443  foreach ($languages as $language) {
444  $this->ensureTranslationsLoaded($language);
445  }
446 
447  _elgg_services()->events->triggerAfter('reload', 'translations');
448 
449  $this->was_reloaded = true;
450  }
451 
460  public function getInstalledTranslations(bool $calculate_completeness = false) {
461  if ($calculate_completeness) {
462  // Ensure that all possible translations are loaded
463  $this->reloadAllTranslations();
464  }
465 
466  $result = [];
467 
468  $languages = $this->getAvailableLanguages();
469  foreach ($languages as $language) {
470  if ($this->languageKeyExists($language, $language)) {
471  $value = $this->translate($language, [], $language);
472  } else {
473  $value = $this->translate($language);
474  }
475 
476  if (($language !== 'en') && $calculate_completeness) {
477  $completeness = $this->getLanguageCompleteness($language);
478  $value .= " (" . $completeness . "% " . $this->translate('complete') . ")";
479  }
480 
482  }
483 
484  natcasesort($result);
485 
486  return $result;
487  }
488 
497 
498  if ($language == 'en') {
499  return (float) 100;
500  }
501 
502  // Ensure that all possible translations are loaded
503  $this->reloadAllTranslations();
504 
505  $en = count($this->translations['en']);
506 
507  $missing = $this->getMissingLanguageKeys($language);
508  if ($missing) {
509  $missing = count($missing);
510  } else {
511  $missing = 0;
512  }
513 
514  $lang = $en - $missing;
515 
516  return round(($lang / $en) * 100, 2);
517  }
518 
529  public function getMissingLanguageKeys(string $language) {
530 
531  // Ensure that all possible translations are loaded
532  $this->reloadAllTranslations();
533 
534  $missing = [];
535 
536  foreach ($this->translations['en'] as $k => $v) {
537  if ((!isset($this->translations[$language][$k]))
538  || ($this->translations[$language][$k] == $this->translations['en'][$k])) {
539  $missing[] = $k;
540  }
541  }
542 
543  if (count($missing)) {
544  return $missing;
545  }
546 
547  return false;
548  }
549 
559  public function languageKeyExists($key, $language = 'en') {
560  if (empty($key)) {
561  return false;
562  }
563 
565 
566  if (!array_key_exists($language, $this->translations)) {
567  return false;
568  }
569 
570  return array_key_exists($key, $this->translations[$language]);
571  }
572 
579  public function getAvailableLanguages() {
580  $languages = [];
581 
582  $allowed_languages = $this->locale->getLanguageCodes();
583 
584  foreach ($this->getLanguagePaths() as $path) {
585  try {
586  $iterator = new \DirectoryIterator($path);
587  } catch (\Exception $e) {
588  continue;
589  }
590 
591  foreach ($iterator as $file) {
592  if ($file->isDir()) {
593  continue;
594  }
595 
596  if ($file->getExtension() !== 'php') {
597  continue;
598  }
599 
600  $language = $file->getBasename('.php');
601  if (empty($language) || !in_array($language, $allowed_languages)) {
602  continue;
603  }
604 
605  $languages[$language] = true;
606  }
607  }
608 
609  $languages = array_keys($languages);
610 
611  return _elgg_services()->hooks->trigger('languages', 'translations', [], $languages);
612  }
613 
620  public function getAllowedLanguages() {
621  if (isset($this->allowed_languages)) {
623  }
624 
625  $allowed_languages = $this->config->allowed_languages;
626  if (!empty($allowed_languages)) {
627  $allowed_languages = explode(',', $allowed_languages);
628  $allowed_languages = array_filter(array_unique($allowed_languages));
629  } else {
631  }
632 
633  if (!in_array($this->config->language, $allowed_languages)) {
634  // site language is always allowed
635  $allowed_languages[] = $this->config->language;
636  }
637  if (!in_array('en', $allowed_languages)) {
638  // 'en' language is always allowed
639  $allowed_languages[] = 'en';
640  }
641 
642  $this->allowed_languages = $allowed_languages;
643 
644  return $allowed_languages;
645  }
646 
656  public function registerLanguagePath(string $path) {
657  if (isset($this->language_paths[$path])) {
658  return;
659  }
660 
661  if (!is_dir($path)) {
662  return;
663  }
664 
665  $this->language_paths[$path] = true;
666  }
667 
675  public function getLanguagePaths() {
676  return array_keys($this->language_paths);
677  }
678 
686  protected function ensureTranslationsLoaded(string $language) {
687  if (isset($this->translations[$language])) {
688  return;
689  }
690 
691  // The language being requested is not the same as the language of the
692  // logged in user, so we will have to load it separately. (Most likely
693  // we're sending a notification and the recipient is using a different
694  // language than the logged in user.)
695  $this->loadTranslations($language);
696  }
697 }
static includeFile($file)
Include a file with as little context as possible.
Definition: Includer.php:18
setCurrentLanguage($language=null)
Sets current system language.
Definition: Translator.php:242
loadTranslations(string $language)
Load both core and plugin translations.
Definition: Translator.php:328
registerTranslations(string $path, bool $load_all=false, string $language=null)
When given a full path, finds translation files and loads them.
Definition: Translator.php:356
translate($message_key, array $args=[], $language="")
Given a message key, returns an appropriately translated full-text string.
Definition: Translator.php:106
if(elgg_trigger_plugin_hook('usersettings:save', 'user', $hooks_params, true)) foreach($request->validation() ->all() as $item) $data
Definition: save.php:53
addTranslation($country_code, $language_array, $ensure_translations_loaded=true)
Add a translation.
Definition: Translator.php:195
getAvailableLanguages()
Returns an array of all available language keys.
Definition: Translator.php:579
$args
Some servers don&#39;t allow PHP to check the rewrite, so try via AJAX.
$value
Definition: generic.php:51
Provides locale related features.
$path
Definition: details.php:68
registerLanguagePath(string $path)
Registers a path for potential translation files.
Definition: Translator.php:656
trait Loggable
Enables adding a logger.
Definition: Loggable.php:14
detectLanguage()
Detect the current system/user language or false.
Definition: Translator.php:253
reloadAllTranslations()
Reload all translations from all registered paths.
Definition: Translator.php:436
$language
Definition: useradd.php:19
getLanguagePaths()
Returns a unique array with locations of translation files.
Definition: Translator.php:675
$lang
Definition: html.php:13
getLoadedTranslations()
Get a map of all loaded translations.
Definition: Translator.php:91
$user
Definition: ban.php:7
bootTranslations()
Ensures all needed translations are loaded.
Definition: Translator.php:307
__construct(Config $config, LocaleService $locale)
Constructor.
Definition: Translator.php:77
getCurrentLanguage()
Get the current system/user language or "en".
Definition: Translator.php:223
getLanguageCompleteness($language)
Return the level of completeness for a given language code (compared to english)
Definition: Translator.php:496
elgg_extract($key, $array, $default=null, $strict=true)
Checks for $array[$key] and returns its value if it exists, else returns $default.
Definition: elgglib.php:547
elgg_set_cookie(\ElggCookie $cookie)
Set a cookie, but allow plugins to customize it first.
Definition: sessions.php:65
if($container instanceof ElggGroup &&$container->guid!=elgg_get_page_owner_guid()) $key
Definition: summary.php:44
$site_language
getLogger()
Returns logger.
Definition: Loggable.php:37
elgg_load_system_cache($type)
Retrieve the contents of a system cache.
Definition: cache.php:45
getAllowedLanguages()
Returns an array of allowed languages as configured by the site admin.
Definition: Translator.php:620
includeLanguageFile(string $path)
Load cached or include a language file by its path.
Definition: Translator.php:414
ensureTranslationsLoaded(string $language)
Make sure translations are loaded.
Definition: Translator.php:686
languageKeyExists($key, $language= 'en')
Check if a given language key exists.
Definition: Translator.php:559
getMissingLanguageKeys(string $language)
Return the translation keys missing from a given language, or those that are identical to the english...
Definition: Translator.php:529
static sanitize($path, $append_slash=true)
Sanitize file paths ensuring that they begin and end with slashes etc.
Definition: Paths.php:76
_elgg_services()
Get the global service provider.
Definition: elgglib.php:638
elgg_save_system_cache($type, $data, int $expire_after=null)
Saves a system cache.
Definition: cache.php:34
getInstalledTranslations(bool $calculate_completeness=false)
Return an array of installed translations as an associative array "two letter code" => "native langua...
Definition: Translator.php:460