Language.php 13 KB

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