Language.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537
  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. $languagesArray = array_map(function($value) {
  94. return preg_quote($value);
  95. }, $languagesArray);
  96. sort($languagesArray);
  97. return implode('|', array_reverse($languagesArray));
  98. }
  99. /**
  100. * Gets language, active if set, else default
  101. *
  102. * @return string
  103. */
  104. public function getLanguage()
  105. {
  106. return $this->active ?: $this->default;
  107. }
  108. /**
  109. * Gets current default language
  110. *
  111. * @return mixed
  112. */
  113. public function getDefault()
  114. {
  115. return $this->default;
  116. }
  117. /**
  118. * Sets default language manually
  119. *
  120. * @param string $lang
  121. *
  122. * @return bool
  123. */
  124. public function setDefault($lang)
  125. {
  126. if ($this->validate($lang)) {
  127. $this->default = $lang;
  128. return $lang;
  129. }
  130. return false;
  131. }
  132. /**
  133. * Gets current active language
  134. *
  135. * @return string
  136. */
  137. public function getActive()
  138. {
  139. return $this->active;
  140. }
  141. /**
  142. * Sets active language manually
  143. *
  144. * @param string $lang
  145. *
  146. * @return string|bool
  147. */
  148. public function setActive($lang)
  149. {
  150. if ($this->validate($lang)) {
  151. $this->active = $lang;
  152. return $lang;
  153. }
  154. return false;
  155. }
  156. /**
  157. * Sets the active language based on the first part of the URL
  158. *
  159. * @param string $uri
  160. *
  161. * @return string
  162. */
  163. public function setActiveFromUri($uri)
  164. {
  165. $regex = '/(^\/(' . $this->getAvailable() . '))(?:\/|\?|$)/i';
  166. // if languages set
  167. if ($this->enabled()) {
  168. // Try setting language from prefix of URL (/en/blah/blah).
  169. if (preg_match($regex, $uri, $matches)) {
  170. $this->lang_in_url = true;
  171. $this->active = $matches[2];
  172. $uri = preg_replace("/\\" . $matches[1] . '/', '', $uri, 1);
  173. // Store in session if language is different.
  174. if (isset($this->grav['session']) && $this->grav['session']->isStarted()
  175. && $this->config->get('system.languages.session_store_active', true)
  176. && $this->grav['session']->active_language != $this->active
  177. ) {
  178. $this->grav['session']->active_language = $this->active;
  179. }
  180. } else {
  181. // Try getting language from the session, else no active.
  182. if (isset($this->grav['session']) && $this->grav['session']->isStarted() &&
  183. $this->config->get('system.languages.session_store_active', true)) {
  184. $this->active = $this->grav['session']->active_language ?: null;
  185. }
  186. // if still null, try from http_accept_language header
  187. if ($this->active === null &&
  188. $this->config->get('system.languages.http_accept_language') &&
  189. $accept = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? false) {
  190. $negotiator = new LanguageNegotiator();
  191. $best_language = $negotiator->getBest($accept, $this->languages);
  192. if ($best_language instanceof AcceptLanguage) {
  193. $this->active = $best_language->getType();
  194. } else {
  195. $this->active = $this->getDefault();
  196. }
  197. }
  198. }
  199. }
  200. return $uri;
  201. }
  202. /**
  203. * Get a URL prefix based on configuration
  204. *
  205. * @param string|null $lang
  206. * @return string
  207. */
  208. public function getLanguageURLPrefix($lang = null)
  209. {
  210. // if active lang is not passed in, use current active
  211. if (!$lang) {
  212. $lang = $this->getLanguage();
  213. }
  214. return $this->isIncludeDefaultLanguage($lang) ? '/' . $lang : '';
  215. }
  216. /**
  217. * Test to see if language is default and language should be included in the URL
  218. *
  219. * @param string|null $lang
  220. * @return bool
  221. */
  222. public function isIncludeDefaultLanguage($lang = null)
  223. {
  224. // if active lang is not passed in, use current active
  225. if (!$lang) {
  226. $lang = $this->getLanguage();
  227. }
  228. return !($this->default === $lang && $this->config->get('system.languages.include_default_lang') === false);
  229. }
  230. /**
  231. * Simple getter to tell if a language was found in the URL
  232. *
  233. * @return bool
  234. */
  235. public function isLanguageInUrl()
  236. {
  237. return (bool) $this->lang_in_url;
  238. }
  239. /**
  240. * Gets an array of valid extensions with active first, then fallback extensions
  241. *
  242. * @param string|null $file_ext
  243. *
  244. * @return array
  245. */
  246. public function getFallbackPageExtensions($file_ext = null)
  247. {
  248. if (empty($this->page_extensions)) {
  249. if (!$file_ext) {
  250. $file_ext = CONTENT_EXT;
  251. }
  252. if ($this->enabled()) {
  253. $valid_lang_extensions = [];
  254. foreach ($this->languages as $lang) {
  255. $valid_lang_extensions[] = '.' . $lang . $file_ext;
  256. }
  257. if ($this->active) {
  258. $active_extension = '.' . $this->active . $file_ext;
  259. $key = \array_search($active_extension, $valid_lang_extensions, true);
  260. // Default behavior is to find any language other than active
  261. if ($this->config->get('system.languages.pages_fallback_only')) {
  262. $slice = \array_slice($valid_lang_extensions, 0, $key+1);
  263. $valid_lang_extensions = array_reverse($slice);
  264. } else {
  265. unset($valid_lang_extensions[$key]);
  266. array_unshift($valid_lang_extensions, $active_extension);
  267. }
  268. }
  269. $valid_lang_extensions[] = $file_ext;
  270. $this->page_extensions = $valid_lang_extensions;
  271. } else {
  272. $this->page_extensions = (array)$file_ext;
  273. }
  274. }
  275. return $this->page_extensions;
  276. }
  277. /**
  278. * Resets the page_extensions value.
  279. *
  280. * Useful to re-initialize the pages and change site language at runtime, example:
  281. *
  282. * ```
  283. * $this->grav['language']->setActive('it');
  284. * $this->grav['language']->resetFallbackPageExtensions();
  285. * $this->grav['pages']->init();
  286. * ```
  287. */
  288. public function resetFallbackPageExtensions()
  289. {
  290. $this->page_extensions = null;
  291. }
  292. /**
  293. * Gets an array of languages with active first, then fallback languages
  294. *
  295. * @return array
  296. */
  297. public function getFallbackLanguages()
  298. {
  299. if (empty($this->fallback_languages)) {
  300. if ($this->enabled()) {
  301. $fallback_languages = $this->languages;
  302. if ($this->active) {
  303. $active_extension = $this->active;
  304. $key = \array_search($active_extension, $fallback_languages, true);
  305. unset($fallback_languages[$key]);
  306. array_unshift($fallback_languages, $active_extension);
  307. }
  308. $this->fallback_languages = $fallback_languages;
  309. }
  310. // always add english in case a translation doesn't exist
  311. $this->fallback_languages[] = 'en';
  312. }
  313. return $this->fallback_languages;
  314. }
  315. /**
  316. * Ensures the language is valid and supported
  317. *
  318. * @param string $lang
  319. *
  320. * @return bool
  321. */
  322. public function validate($lang)
  323. {
  324. return \in_array($lang, $this->languages, true);
  325. }
  326. /**
  327. * Translate a key and possibly arguments into a string using current lang and fallbacks
  328. *
  329. * @param string|array $args The first argument is the lookup key value
  330. * Other arguments can be passed and replaced in the translation with sprintf syntax
  331. * @param array $languages
  332. * @param bool $array_support
  333. * @param bool $html_out
  334. *
  335. * @return string
  336. */
  337. public function translate($args, array $languages = null, $array_support = false, $html_out = false)
  338. {
  339. if (\is_array($args)) {
  340. $lookup = array_shift($args);
  341. } else {
  342. $lookup = $args;
  343. $args = [];
  344. }
  345. if ($this->config->get('system.languages.translations', true)) {
  346. if ($this->enabled() && $lookup) {
  347. if (empty($languages)) {
  348. if ($this->config->get('system.languages.translations_fallback', true)) {
  349. $languages = $this->getFallbackLanguages();
  350. } else {
  351. $languages = (array)$this->getLanguage();
  352. }
  353. }
  354. } else {
  355. $languages = ['en'];
  356. }
  357. foreach ((array)$languages as $lang) {
  358. $translation = $this->getTranslation($lang, $lookup, $array_support);
  359. if ($translation) {
  360. if (\count($args) >= 1) {
  361. return vsprintf($translation, $args);
  362. }
  363. return $translation;
  364. }
  365. }
  366. }
  367. if ($html_out) {
  368. return '<span class="untranslated">' . $lookup . '</span>';
  369. }
  370. return $lookup;
  371. }
  372. /**
  373. * Translate Array
  374. *
  375. * @param string $key
  376. * @param string $index
  377. * @param array|null $languages
  378. * @param bool $html_out
  379. *
  380. * @return string
  381. */
  382. public function translateArray($key, $index, $languages = null, $html_out = false)
  383. {
  384. if ($this->config->get('system.languages.translations', true)) {
  385. if ($this->enabled() && $key) {
  386. if (empty($languages)) {
  387. if ($this->config->get('system.languages.translations_fallback', true)) {
  388. $languages = $this->getFallbackLanguages();
  389. } else {
  390. $languages = (array)$this->getDefault();
  391. }
  392. }
  393. } else {
  394. $languages = ['en'];
  395. }
  396. foreach ((array)$languages as $lang) {
  397. $translation_array = (array)Grav::instance()['languages']->get($lang . '.' . $key, null);
  398. if ($translation_array && array_key_exists($index, $translation_array)) {
  399. return $translation_array[$index];
  400. }
  401. }
  402. }
  403. if ($html_out) {
  404. return '<span class="untranslated">' . $key . '[' . $index . ']</span>';
  405. }
  406. return $key . '[' . $index . ']';
  407. }
  408. /**
  409. * Lookup the translation text for a given lang and key
  410. *
  411. * @param string $lang lang code
  412. * @param string $key key to lookup with
  413. * @param bool $array_support
  414. *
  415. * @return string
  416. */
  417. public function getTranslation($lang, $key, $array_support = false)
  418. {
  419. $translation = Grav::instance()['languages']->get($lang . '.' . $key, null);
  420. if (!$array_support && is_array($translation)) {
  421. return (string)array_shift($translation);
  422. }
  423. return $translation;
  424. }
  425. /**
  426. * Get the browser accepted languages
  427. *
  428. * @param array $accept_langs
  429. * @return array
  430. * @deprecated 1.6 No longer used - using content negotiation.
  431. */
  432. public function getBrowserLanguages($accept_langs = [])
  433. {
  434. user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, no longer used', E_USER_DEPRECATED);
  435. if (empty($this->http_accept_language)) {
  436. if (empty($accept_langs) && isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
  437. $accept_langs = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
  438. } else {
  439. return $accept_langs;
  440. }
  441. $langs = [];
  442. foreach (explode(',', $accept_langs) as $k => $pref) {
  443. // split $pref again by ';q='
  444. // and decorate the language entries by inverted position
  445. if (false !== ($i = strpos($pref, ';q='))) {
  446. $langs[substr($pref, 0, $i)] = [(float)substr($pref, $i + 3), -$k];
  447. } else {
  448. $langs[$pref] = [1, -$k];
  449. }
  450. }
  451. arsort($langs);
  452. // no need to undecorate, because we're only interested in the keys
  453. $this->http_accept_language = array_keys($langs);
  454. }
  455. return $this->http_accept_language;
  456. }
  457. /**
  458. * Accessible wrapper to LanguageCodes
  459. *
  460. * @param string $code
  461. * @param string $type
  462. * @return bool
  463. */
  464. public function getLanguageCode($code, $type = 'name')
  465. {
  466. return LanguageCodes::get($code, $type);
  467. }
  468. }