Language.php 15 KB

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