Translator.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588
  1. <?php
  2. namespace Elgg\I18n;
  3. /**
  4. * WARNING: API IN FLUX. DO NOT USE DIRECTLY.
  5. *
  6. * @access private
  7. *
  8. * @since 1.10.0
  9. */
  10. class Translator {
  11. /**
  12. * Global Elgg configuration
  13. *
  14. * @var \stdClass
  15. */
  16. private $CONFIG;
  17. /**
  18. * Initializes new translator
  19. */
  20. public function __construct() {
  21. global $CONFIG;
  22. $this->CONFIG = $CONFIG;
  23. $this->defaultPath = dirname(dirname(dirname(dirname(__DIR__)))) . "/languages/";
  24. }
  25. /**
  26. * Given a message key, returns an appropriately translated full-text string
  27. *
  28. * @param string $message_key The short message code
  29. * @param array $args An array of arguments to pass through vsprintf().
  30. * @param string $language Optionally, the standard language code
  31. * (defaults to site/user default, then English)
  32. *
  33. * @return string Either the translated string, the English string,
  34. * or the original language string.
  35. */
  36. function translate($message_key, $args = array(), $language = "") {
  37. static $CURRENT_LANGUAGE;
  38. // old param order is deprecated
  39. if (!is_array($args)) {
  40. elgg_deprecated_notice(
  41. 'As of Elgg 1.8, the 2nd arg to elgg_echo() is an array of string replacements and the 3rd arg is the language.',
  42. 1.8
  43. );
  44. $language = $args;
  45. $args = array();
  46. }
  47. if (!isset($this->CONFIG->translations)) {
  48. // this means we probably had an exception before translations were initialized
  49. $this->registerTranslations($this->defaultPath);
  50. }
  51. if (!$CURRENT_LANGUAGE) {
  52. $CURRENT_LANGUAGE = $this->getLanguage();
  53. }
  54. if (!$language) {
  55. $language = $CURRENT_LANGUAGE;
  56. }
  57. if (!isset($this->CONFIG->translations[$language])) {
  58. // The language being requested is not the same as the language of the
  59. // logged in user, so we will have to load it separately. (Most likely
  60. // we're sending a notification and the recipient is using a different
  61. // language than the logged in user.)
  62. _elgg_load_translations_for_language($language);
  63. }
  64. if (isset($this->CONFIG->translations[$language][$message_key])) {
  65. $string = $this->CONFIG->translations[$language][$message_key];
  66. } else if (isset($this->CONFIG->translations["en"][$message_key])) {
  67. $string = $this->CONFIG->translations["en"][$message_key];
  68. _elgg_services()->logger->notice(sprintf('Missing %s translation for "%s" language key', $language, $message_key));
  69. } else {
  70. $string = $message_key;
  71. _elgg_services()->logger->notice(sprintf('Missing English translation for "%s" language key', $message_key));
  72. }
  73. // only pass through if we have arguments to allow backward compatibility
  74. // with manual sprintf() calls.
  75. if ($args) {
  76. $string = vsprintf($string, $args);
  77. }
  78. return $string;
  79. }
  80. /**
  81. * Add a translation.
  82. *
  83. * Translations are arrays in the Zend Translation array format, eg:
  84. *
  85. * $english = array('message1' => 'message1', 'message2' => 'message2');
  86. * $german = array('message1' => 'Nachricht1','message2' => 'Nachricht2');
  87. *
  88. * @param string $country_code Standard country code (eg 'en', 'nl', 'es')
  89. * @param array $language_array Formatted array of strings
  90. *
  91. * @return bool Depending on success
  92. */
  93. function addTranslation($country_code, $language_array) {
  94. if (!isset($this->CONFIG->translations)) {
  95. $this->CONFIG->translations = array();
  96. }
  97. $country_code = strtolower($country_code);
  98. $country_code = trim($country_code);
  99. if (is_array($language_array) && $country_code != "") {
  100. if (sizeof($language_array) > 0) {
  101. if (!isset($this->CONFIG->translations[$country_code])) {
  102. $this->CONFIG->translations[$country_code] = $language_array;
  103. } else {
  104. $this->CONFIG->translations[$country_code] = $language_array + $this->CONFIG->translations[$country_code];
  105. }
  106. }
  107. return true;
  108. }
  109. return false;
  110. }
  111. /**
  112. * Detect the current language being used by the current site or logged in user.
  113. *
  114. * @return string The language code for the site/user or "en" if not set
  115. */
  116. function getCurrentLanguage() {
  117. $language = $this->getLanguage();
  118. if (!$language) {
  119. $language = 'en';
  120. }
  121. return $language;
  122. }
  123. /**
  124. * Gets the current language in use by the system or user.
  125. *
  126. * @return string The language code (eg "en") or false if not set
  127. */
  128. function getLanguage() {
  129. $url_lang = _elgg_services()->input->get('hl');
  130. if ($url_lang) {
  131. return $url_lang;
  132. }
  133. $user = _elgg_services()->session->getLoggedInUser();
  134. $language = false;
  135. if (($user) && ($user->language)) {
  136. $language = $user->language;
  137. }
  138. if ((!$language) && (isset($this->CONFIG->language)) && ($this->CONFIG->language)) {
  139. $language = $this->CONFIG->language;
  140. }
  141. if ($language) {
  142. return $language;
  143. }
  144. return false;
  145. }
  146. /**
  147. * @access private
  148. */
  149. function loadTranslations() {
  150. if ($this->CONFIG->system_cache_enabled) {
  151. $loaded = true;
  152. $languages = array_unique(array('en', $this->getCurrentLanguage()));
  153. foreach ($languages as $language) {
  154. $data = elgg_load_system_cache("$language.lang");
  155. if ($data) {
  156. $this->addTranslation($language, unserialize($data));
  157. } else {
  158. $loaded = false;
  159. }
  160. }
  161. if ($loaded) {
  162. $this->CONFIG->i18n_loaded_from_cache = true;
  163. // this is here to force
  164. $this->CONFIG->language_paths[$this->defaultPath] = true;
  165. return;
  166. }
  167. }
  168. // load core translations from languages directory
  169. $this->registerTranslations($this->defaultPath);
  170. }
  171. /**
  172. * When given a full path, finds translation files and loads them
  173. *
  174. * @param string $path Full path
  175. * @param bool $load_all If true all languages are loaded, if
  176. * false only the current language + en are loaded
  177. *
  178. * @return bool success
  179. */
  180. function registerTranslations($path, $load_all = false) {
  181. $path = sanitise_filepath($path);
  182. // Make a note of this path just incase we need to register this language later
  183. if (!isset($this->CONFIG->language_paths)) {
  184. $this->CONFIG->language_paths = array();
  185. }
  186. $this->CONFIG->language_paths[$path] = true;
  187. // Get the current language based on site defaults and user preference
  188. $current_language = $this->getCurrentLanguage();
  189. _elgg_services()->logger->info("Translations loaded from: $path");
  190. // only load these files unless $load_all is true.
  191. $load_language_files = array(
  192. 'en.php',
  193. "$current_language.php"
  194. );
  195. $load_language_files = array_unique($load_language_files);
  196. $handle = opendir($path);
  197. if (!$handle) {
  198. _elgg_services()->logger->error("Could not open language path: $path");
  199. return false;
  200. }
  201. $return = true;
  202. while (false !== ($language = readdir($handle))) {
  203. // ignore bad files
  204. if (substr($language, 0, 1) == '.' || substr($language, -4) !== '.php') {
  205. continue;
  206. }
  207. if (in_array($language, $load_language_files) || $load_all) {
  208. $result = include_once($path . $language);
  209. if ($result === false) {
  210. $return = false;
  211. continue;
  212. } elseif (is_array($result)) {
  213. $this->addTranslation(basename($language, '.php'), $result);
  214. }
  215. }
  216. }
  217. return $return;
  218. }
  219. /**
  220. * Reload all translations from all registered paths.
  221. *
  222. * This is only called by functions which need to know all possible translations.
  223. *
  224. * @todo Better on demand loading based on language_paths array
  225. *
  226. * @return void
  227. */
  228. function reloadAllTranslations() {
  229. static $LANG_RELOAD_ALL_RUN;
  230. if ($LANG_RELOAD_ALL_RUN) {
  231. return;
  232. }
  233. if ($this->CONFIG->i18n_loaded_from_cache) {
  234. $cache = elgg_get_system_cache();
  235. $cache_dir = $cache->getVariable("cache_path");
  236. $filenames = elgg_get_file_list($cache_dir, array(), array(), array(".lang"));
  237. foreach ($filenames as $filename) {
  238. // Look for files matching for example 'en.lang', 'cmn.lang' or 'pt_br.lang'.
  239. // Note that this regex is just for the system cache. The original language
  240. // files are allowed to have uppercase letters (e.g. pt_BR.php).
  241. if (preg_match('/(([a-z]{2,3})(_[a-z]{2})?)\.lang$/', $filename, $matches)) {
  242. $language = $matches[1];
  243. $data = elgg_load_system_cache("$language.lang");
  244. if ($data) {
  245. $this->addTranslation($language, unserialize($data));
  246. }
  247. }
  248. }
  249. } else {
  250. foreach ($this->CONFIG->language_paths as $path => $dummy) {
  251. $this->registerTranslations($path, true);
  252. }
  253. }
  254. $LANG_RELOAD_ALL_RUN = true;
  255. }
  256. /**
  257. * Return an array of installed translations as an associative
  258. * array "two letter code" => "native language name".
  259. *
  260. * @return array
  261. */
  262. function getInstalledTranslations() {
  263. // Ensure that all possible translations are loaded
  264. $this->reloadAllTranslations();
  265. $installed = array();
  266. $admin_logged_in = _elgg_services()->session->isAdminLoggedIn();
  267. foreach ($this->CONFIG->translations as $k => $v) {
  268. $installed[$k] = $this->translate($k, array(), $k);
  269. if ($admin_logged_in && ($k != 'en')) {
  270. $completeness = $this->getLanguageCompleteness($k);
  271. if ($completeness < 100) {
  272. $installed[$k] .= " (" . $completeness . "% " . $this->translate('complete') . ")";
  273. }
  274. }
  275. }
  276. return $installed;
  277. }
  278. /**
  279. * Return the level of completeness for a given language code (compared to english)
  280. *
  281. * @param string $language Language
  282. *
  283. * @return int
  284. */
  285. function getLanguageCompleteness($language) {
  286. // Ensure that all possible translations are loaded
  287. $this->reloadAllTranslations();
  288. $language = sanitise_string($language);
  289. $en = count($this->CONFIG->translations['en']);
  290. $missing = $this->getMissingLanguageKeys($language);
  291. if ($missing) {
  292. $missing = count($missing);
  293. } else {
  294. $missing = 0;
  295. }
  296. //$lang = count($this->CONFIG->translations[$language]);
  297. $lang = $en - $missing;
  298. return round(($lang / $en) * 100, 2);
  299. }
  300. /**
  301. * Return the translation keys missing from a given language,
  302. * or those that are identical to the english version.
  303. *
  304. * @param string $language The language
  305. *
  306. * @return mixed
  307. */
  308. function getMissingLanguageKeys($language) {
  309. // Ensure that all possible translations are loaded
  310. $this->reloadAllTranslations();
  311. $missing = array();
  312. foreach ($this->CONFIG->translations['en'] as $k => $v) {
  313. if ((!isset($this->CONFIG->translations[$language][$k]))
  314. || ($this->CONFIG->translations[$language][$k] == $this->CONFIG->translations['en'][$k])) {
  315. $missing[] = $k;
  316. }
  317. }
  318. if (count($missing)) {
  319. return $missing;
  320. }
  321. return false;
  322. }
  323. /**
  324. * Check if a give language key exists
  325. *
  326. * @param string $key The translation key
  327. * @param string $language The specific language to check
  328. *
  329. * @return bool
  330. * @since 1.11
  331. */
  332. function languageKeyExists($key, $language = 'en') {
  333. if (empty($key)) {
  334. return false;
  335. }
  336. if (($language !== 'en') && !array_key_exists($language, $this->CONFIG->translations)) {
  337. // Ensure that all possible translations are loaded
  338. $this->reloadAllTranslations();
  339. }
  340. if (!array_key_exists($language, $this->CONFIG->translations)) {
  341. return false;
  342. }
  343. return array_key_exists($key, $this->CONFIG->translations[$language]);
  344. }
  345. /**
  346. * Returns an array of language codes.
  347. *
  348. * @return array
  349. */
  350. public static function getAllLanguageCodes() {
  351. return [
  352. "aa", // "Afar"
  353. "ab", // "Abkhazian"
  354. "af", // "Afrikaans"
  355. "am", // "Amharic"
  356. "ar", // "Arabic"
  357. "as", // "Assamese"
  358. "ay", // "Aymara"
  359. "az", // "Azerbaijani"
  360. "ba", // "Bashkir"
  361. "be", // "Byelorussian"
  362. "bg", // "Bulgarian"
  363. "bh", // "Bihari"
  364. "bi", // "Bislama"
  365. "bn", // "Bengali; Bangla"
  366. "bo", // "Tibetan"
  367. "br", // "Breton"
  368. "ca", // "Catalan"
  369. "cmn", // "Mandarin Chinese" // ISO 639-3
  370. "co", // "Corsican"
  371. "cs", // "Czech"
  372. "cy", // "Welsh"
  373. "da", // "Danish"
  374. "de", // "German"
  375. "dz", // "Bhutani"
  376. "el", // "Greek"
  377. "en", // "English"
  378. "eo", // "Esperanto"
  379. "es", // "Spanish"
  380. "et", // "Estonian"
  381. "eu", // "Basque"
  382. "eu_es", // "Basque (Spain)"
  383. "fa", // "Persian"
  384. "fi", // "Finnish"
  385. "fj", // "Fiji"
  386. "fo", // "Faeroese"
  387. "fr", // "French"
  388. "fy", // "Frisian"
  389. "ga", // "Irish"
  390. "gd", // "Scots / Gaelic"
  391. "gl", // "Galician"
  392. "gn", // "Guarani"
  393. "gu", // "Gujarati"
  394. "he", // "Hebrew"
  395. "ha", // "Hausa"
  396. "hi", // "Hindi"
  397. "hr", // "Croatian"
  398. "hu", // "Hungarian"
  399. "hy", // "Armenian"
  400. "ia", // "Interlingua"
  401. "id", // "Indonesian"
  402. "ie", // "Interlingue"
  403. "ik", // "Inupiak"
  404. "is", // "Icelandic"
  405. "it", // "Italian"
  406. "iu", // "Inuktitut"
  407. "iw", // "Hebrew (obsolete)"
  408. "ja", // "Japanese"
  409. "ji", // "Yiddish (obsolete)"
  410. "jw", // "Javanese"
  411. "ka", // "Georgian"
  412. "kk", // "Kazakh"
  413. "kl", // "Greenlandic"
  414. "km", // "Cambodian"
  415. "kn", // "Kannada"
  416. "ko", // "Korean"
  417. "ks", // "Kashmiri"
  418. "ku", // "Kurdish"
  419. "ky", // "Kirghiz"
  420. "la", // "Latin"
  421. "ln", // "Lingala"
  422. "lo", // "Laothian"
  423. "lt", // "Lithuanian"
  424. "lv", // "Latvian/Lettish"
  425. "mg", // "Malagasy"
  426. "mi", // "Maori"
  427. "mk", // "Macedonian"
  428. "ml", // "Malayalam"
  429. "mn", // "Mongolian"
  430. "mo", // "Moldavian"
  431. "mr", // "Marathi"
  432. "ms", // "Malay"
  433. "mt", // "Maltese"
  434. "my", // "Burmese"
  435. "na", // "Nauru"
  436. "ne", // "Nepali"
  437. "nl", // "Dutch"
  438. "no", // "Norwegian"
  439. "oc", // "Occitan"
  440. "om", // "(Afan) Oromo"
  441. "or", // "Oriya"
  442. "pa", // "Punjabi"
  443. "pl", // "Polish"
  444. "ps", // "Pashto / Pushto"
  445. "pt", // "Portuguese"
  446. "pt_br", // "Portuguese (Brazil)"
  447. "qu", // "Quechua"
  448. "rm", // "Rhaeto-Romance"
  449. "rn", // "Kirundi"
  450. "ro", // "Romanian"
  451. "ro_ro", // "Romanian (Romania)"
  452. "ru", // "Russian"
  453. "rw", // "Kinyarwanda"
  454. "sa", // "Sanskrit"
  455. "sd", // "Sindhi"
  456. "sg", // "Sangro"
  457. "sh", // "Serbo-Croatian"
  458. "si", // "Singhalese"
  459. "sk", // "Slovak"
  460. "sl", // "Slovenian"
  461. "sm", // "Samoan"
  462. "sn", // "Shona"
  463. "so", // "Somali"
  464. "sq", // "Albanian"
  465. "sr", // "Serbian"
  466. "sr_latin", // "Serbian (Latin)"
  467. "ss", // "Siswati"
  468. "st", // "Sesotho"
  469. "su", // "Sundanese"
  470. "sv", // "Swedish"
  471. "sw", // "Swahili"
  472. "ta", // "Tamil"
  473. "te", // "Tegulu"
  474. "tg", // "Tajik"
  475. "th", // "Thai"
  476. "ti", // "Tigrinya"
  477. "tk", // "Turkmen"
  478. "tl", // "Tagalog"
  479. "tn", // "Setswana"
  480. "to", // "Tonga"
  481. "tr", // "Turkish"
  482. "ts", // "Tsonga"
  483. "tt", // "Tatar"
  484. "tw", // "Twi"
  485. "ug", // "Uigur"
  486. "uk", // "Ukrainian"
  487. "ur", // "Urdu"
  488. "uz", // "Uzbek"
  489. "vi", // "Vietnamese"
  490. "vo", // "Volapuk"
  491. "wo", // "Wolof"
  492. "xh", // "Xhosa"
  493. "yi", // "Yiddish"
  494. "yo", // "Yoruba"
  495. "za", // "Zuang"
  496. "zh", // "Chinese"
  497. "zh_hans", // "Chinese Simplified"
  498. "zu", // "Zulu"
  499. ];
  500. }
  501. /**
  502. * Normalize a language code (e.g. from Transifex)
  503. *
  504. * @param string $code Language code
  505. *
  506. * @return string
  507. */
  508. public static function normalizeLanguageCode($code) {
  509. $code = strtolower($code);
  510. $code = preg_replace('~[^a-z0-9]~', '_', $code);
  511. return $code;
  512. }
  513. }