Language.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507
  1. <?php
  2. /**
  3. * @package Grav.Common.Language
  4. *
  5. * @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
  6. * @license MIT License; see LICENSE file for details.
  7. */
  8. namespace Grav\Common\Language;
  9. use Grav\Common\Grav;
  10. use Grav\Common\Config\Config;
  11. class Language
  12. {
  13. protected $grav;
  14. protected $enabled = true;
  15. protected $languages = [];
  16. protected $page_extensions = [];
  17. protected $fallback_languages = [];
  18. protected $default;
  19. protected $active = null;
  20. /** @var Config $config */
  21. protected $config;
  22. protected $http_accept_language;
  23. protected $lang_in_url = false;
  24. /**
  25. * Constructor
  26. *
  27. * @param \Grav\Common\Grav $grav
  28. */
  29. public function __construct(Grav $grav)
  30. {
  31. $this->grav = $grav;
  32. $this->config = $grav['config'];
  33. $this->languages = $this->config->get('system.languages.supported', []);
  34. $this->init();
  35. }
  36. /**
  37. * Initialize the default and enabled languages
  38. */
  39. public function init()
  40. {
  41. $this->default = reset($this->languages);
  42. if (empty($this->languages)) {
  43. $this->enabled = false;
  44. }
  45. }
  46. /**
  47. * Ensure that languages are enabled
  48. *
  49. * @return bool
  50. */
  51. public function enabled()
  52. {
  53. return $this->enabled;
  54. }
  55. /**
  56. * Gets the array of supported languages
  57. *
  58. * @return array
  59. */
  60. public function getLanguages()
  61. {
  62. return $this->languages;
  63. }
  64. /**
  65. * Sets the current supported languages manually
  66. *
  67. * @param $langs
  68. */
  69. public function setLanguages($langs)
  70. {
  71. $this->languages = $langs;
  72. $this->init();
  73. }
  74. /**
  75. * Gets a pipe-separated string of available languages
  76. *
  77. * @return string
  78. */
  79. public function getAvailable()
  80. {
  81. $languagesArray = $this->languages; //Make local copy
  82. sort($languagesArray);
  83. return implode('|', array_reverse($languagesArray));
  84. }
  85. /**
  86. * Gets language, active if set, else default
  87. *
  88. * @return mixed
  89. */
  90. public function getLanguage()
  91. {
  92. return $this->active ? $this->active : $this->default;
  93. }
  94. /**
  95. * Gets current default language
  96. *
  97. * @return mixed
  98. */
  99. public function getDefault()
  100. {
  101. return $this->default;
  102. }
  103. /**
  104. * Sets default language manually
  105. *
  106. * @param $lang
  107. *
  108. * @return bool
  109. */
  110. public function setDefault($lang)
  111. {
  112. if ($this->validate($lang)) {
  113. $this->default = $lang;
  114. return $lang;
  115. }
  116. return false;
  117. }
  118. /**
  119. * Gets current active language
  120. *
  121. * @return mixed
  122. */
  123. public function getActive()
  124. {
  125. return $this->active;
  126. }
  127. /**
  128. * Sets active language manually
  129. *
  130. * @param $lang
  131. *
  132. * @return bool
  133. */
  134. public function setActive($lang)
  135. {
  136. if ($this->validate($lang)) {
  137. $this->active = $lang;
  138. return $lang;
  139. }
  140. return false;
  141. }
  142. /**
  143. * Sets the active language based on the first part of the URL
  144. *
  145. * @param $uri
  146. *
  147. * @return mixed
  148. */
  149. public function setActiveFromUri($uri)
  150. {
  151. $regex = '/(^\/(' . $this->getAvailable() . '))(?:\/|\?|$)/i';
  152. // if languages set
  153. if ($this->enabled()) {
  154. // Try setting language from prefix of URL (/en/blah/blah).
  155. if (preg_match($regex, $uri, $matches)) {
  156. $this->lang_in_url = true;
  157. $this->active = $matches[2];
  158. $uri = preg_replace("/\\" . $matches[1] . '/', '', $uri, 1);
  159. // Store in session if language is different.
  160. if (isset($this->grav['session']) && $this->grav['session']->isStarted()
  161. && $this->config->get('system.languages.session_store_active', true)
  162. && $this->grav['session']->active_language != $this->active
  163. ) {
  164. $this->grav['session']->active_language = $this->active;
  165. }
  166. } else {
  167. // Try getting language from the session, else no active.
  168. if (isset($this->grav['session']) && $this->grav['session']->isStarted()
  169. && $this->config->get('system.languages.session_store_active', true)) {
  170. $this->active = $this->grav['session']->active_language ?: null;
  171. }
  172. // if still null, try from http_accept_language header
  173. if ($this->active === null && $this->config->get('system.languages.http_accept_language')) {
  174. $preferred = $this->getBrowserLanguages();
  175. foreach ($preferred as $lang) {
  176. if ($this->validate($lang)) {
  177. $this->active = $lang;
  178. break;
  179. }
  180. }
  181. // Repeat if not found, try base language only - fixes Safari sending the language code always
  182. // with a locale (e.g. it-it or fr-fr).
  183. foreach ($preferred as $lang) {
  184. $lang = substr($lang, 0, 2);
  185. if ($this->validate($lang)) {
  186. $this->active = $lang;
  187. break;
  188. }
  189. }
  190. }
  191. }
  192. }
  193. return $uri;
  194. }
  195. /**
  196. * Get's a URL prefix based on configuration
  197. *
  198. * @param null $lang
  199. * @return string
  200. */
  201. public function getLanguageURLPrefix($lang = null)
  202. {
  203. // if active lang is not passed in, use current active
  204. if (!$lang) {
  205. $lang = $this->getLanguage();
  206. }
  207. return $this->isIncludeDefaultLanguage($lang) ? '/' . $lang : '';
  208. }
  209. /**
  210. * Test to see if language is default and language should be included in the URL
  211. *
  212. * @param null $lang
  213. * @return bool
  214. */
  215. public function isIncludeDefaultLanguage($lang = null)
  216. {
  217. // if active lang is not passed in, use current active
  218. if (!$lang) {
  219. $lang = $this->getLanguage();
  220. }
  221. if ($this->default == $lang && $this->config->get('system.languages.include_default_lang') === false) {
  222. return false;
  223. } else {
  224. return true;
  225. }
  226. }
  227. /**
  228. * Simple getter to tell if a language was found in the URL
  229. *
  230. * @return bool
  231. */
  232. public function isLanguageInUrl()
  233. {
  234. return (bool) $this->lang_in_url;
  235. }
  236. /**
  237. * Gets an array of valid extensions with active first, then fallback extensions
  238. *
  239. * @param string|null $file_ext
  240. *
  241. * @return array
  242. */
  243. public function getFallbackPageExtensions($file_ext = null)
  244. {
  245. if (empty($this->page_extensions)) {
  246. if (empty($file_ext)) {
  247. $file_ext = CONTENT_EXT;
  248. }
  249. if ($this->enabled()) {
  250. $valid_lang_extensions = [];
  251. foreach ($this->languages as $lang) {
  252. $valid_lang_extensions[] = '.' . $lang . $file_ext;
  253. }
  254. if ($this->active) {
  255. $active_extension = '.' . $this->active . $file_ext;
  256. $key = array_search($active_extension, $valid_lang_extensions);
  257. unset($valid_lang_extensions[$key]);
  258. array_unshift($valid_lang_extensions, $active_extension);
  259. }
  260. $this->page_extensions = array_merge($valid_lang_extensions, (array)$file_ext);
  261. } else {
  262. $this->page_extensions = (array)$file_ext;
  263. }
  264. }
  265. return $this->page_extensions;
  266. }
  267. /**
  268. * Resets the page_extensions value.
  269. *
  270. * Useful to re-initialize the pages and change site language at runtime, example:
  271. *
  272. * ```
  273. * $this->grav['language']->setActive('it');
  274. * $this->grav['language']->resetFallbackPageExtensions();
  275. * $this->grav['pages']->init();
  276. * ```
  277. */
  278. public function resetFallbackPageExtensions() {
  279. $this->page_extensions = null;
  280. }
  281. /**
  282. * Gets an array of languages with active first, then fallback languages
  283. *
  284. * @return array
  285. */
  286. public function getFallbackLanguages()
  287. {
  288. if (empty($this->fallback_languages)) {
  289. if ($this->enabled()) {
  290. $fallback_languages = $this->languages;
  291. if ($this->active) {
  292. $active_extension = $this->active;
  293. $key = array_search($active_extension, $fallback_languages);
  294. unset($fallback_languages[$key]);
  295. array_unshift($fallback_languages, $active_extension);
  296. }
  297. $this->fallback_languages = $fallback_languages;
  298. }
  299. // always add english in case a translation doesn't exist
  300. $this->fallback_languages[] = 'en';
  301. }
  302. return $this->fallback_languages;
  303. }
  304. /**
  305. * Ensures the language is valid and supported
  306. *
  307. * @param $lang
  308. *
  309. * @return bool
  310. */
  311. public function validate($lang)
  312. {
  313. if (in_array($lang, $this->languages)) {
  314. return true;
  315. }
  316. return false;
  317. }
  318. /**
  319. * Translate a key and possibly arguments into a string using current lang and fallbacks
  320. *
  321. * @param mixed $args The first argument is the lookup key value
  322. * Other arguments can be passed and replaced in the translation with sprintf syntax
  323. * @param array $languages
  324. * @param bool $array_support
  325. * @param bool $html_out
  326. *
  327. * @return string
  328. */
  329. public function translate($args, array $languages = null, $array_support = false, $html_out = false)
  330. {
  331. if (is_array($args)) {
  332. $lookup = array_shift($args);
  333. } else {
  334. $lookup = $args;
  335. $args = [];
  336. }
  337. if ($this->config->get('system.languages.translations', true)) {
  338. if ($this->enabled() && $lookup) {
  339. if (empty($languages)) {
  340. if ($this->config->get('system.languages.translations_fallback', true)) {
  341. $languages = $this->getFallbackLanguages();
  342. } else {
  343. $languages = (array)$this->getLanguage();
  344. }
  345. }
  346. } else {
  347. $languages = ['en'];
  348. }
  349. foreach ((array)$languages as $lang) {
  350. $translation = $this->getTranslation($lang, $lookup, $array_support);
  351. if ($translation) {
  352. if (count($args) >= 1) {
  353. return vsprintf($translation, $args);
  354. } else {
  355. return $translation;
  356. }
  357. }
  358. }
  359. }
  360. if ($html_out) {
  361. return '<span class="untranslated">' . $lookup . '</span>';
  362. } else {
  363. return $lookup;
  364. }
  365. }
  366. /**
  367. * Translate Array
  368. *
  369. * @param $key
  370. * @param $index
  371. * @param null $languages
  372. * @param bool $html_out
  373. *
  374. * @return string
  375. */
  376. public function translateArray($key, $index, $languages = null, $html_out = false)
  377. {
  378. if ($this->config->get('system.languages.translations', true)) {
  379. if ($this->enabled() && $key) {
  380. if (empty($languages)) {
  381. if ($this->config->get('system.languages.translations_fallback', true)) {
  382. $languages = $this->getFallbackLanguages();
  383. } else {
  384. $languages = (array)$this->getDefault();
  385. }
  386. }
  387. } else {
  388. $languages = ['en'];
  389. }
  390. foreach ((array)$languages as $lang) {
  391. $translation_array = (array)Grav::instance()['languages']->get($lang . '.' . $key, null);
  392. if ($translation_array && array_key_exists($index, $translation_array)) {
  393. return $translation_array[$index];
  394. }
  395. }
  396. }
  397. if ($html_out) {
  398. return '<span class="untranslated">' . $key . '[' . $index . ']</span>';
  399. } else {
  400. return $key . '[' . $index . ']';
  401. }
  402. }
  403. /**
  404. * Lookup the translation text for a given lang and key
  405. *
  406. * @param string $lang lang code
  407. * @param string $key key to lookup with
  408. * @param bool $array_support
  409. *
  410. * @return string
  411. */
  412. public function getTranslation($lang, $key, $array_support = false)
  413. {
  414. $translation = Grav::instance()['languages']->get($lang . '.' . $key, null);
  415. if (!$array_support && is_array($translation)) {
  416. return (string)array_shift($translation);
  417. }
  418. return $translation;
  419. }
  420. /**
  421. * Get the browser accepted languages
  422. *
  423. * @param array $accept_langs
  424. *
  425. * @return array
  426. */
  427. public function getBrowserLanguages($accept_langs = [])
  428. {
  429. if (empty($this->http_accept_language)) {
  430. if (empty($accept_langs) && isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
  431. $accept_langs = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
  432. } else {
  433. return $accept_langs;
  434. }
  435. foreach (explode(',', $accept_langs) as $k => $pref) {
  436. // split $pref again by ';q='
  437. // and decorate the language entries by inverted position
  438. if (false !== ($i = strpos($pref, ';q='))) {
  439. $langs[substr($pref, 0, $i)] = [(float)substr($pref, $i + 3), -$k];
  440. } else {
  441. $langs[$pref] = [1, -$k];
  442. }
  443. }
  444. arsort($langs);
  445. // no need to undecorate, because we're only interested in the keys
  446. $this->http_accept_language = array_keys($langs);
  447. }
  448. return $this->http_accept_language;
  449. }
  450. }