grav = $grav;
$this->config = $grav['config'];
$languages = $this->config->get('system.languages.supported', []);
foreach ($languages as &$language) {
$language = (string)$language;
}
unset($language);
$this->languages = $languages;
$this->init();
}
/**
* Initialize the default and enabled languages
*
* @return void
*/
public function init()
{
$default = $this->config->get('system.languages.default_lang');
if (null !== $default) {
$default = (string)$default;
}
// Note that reset returns false on empty languages.
$this->default = $default ?? reset($this->languages);
$this->resetFallbackPageExtensions();
if (empty($this->languages)) {
// If no languages are set, turn of multi-language support.
$this->enabled = false;
} elseif ($default && !in_array($default, $this->languages, true)) {
// If default language isn't in the language list, we need to add it.
array_unshift($this->languages, $default);
}
}
/**
* Ensure that languages are enabled
*
* @return bool
*/
public function enabled()
{
return $this->enabled;
}
/**
* Returns true if language debugging is turned on.
*
* @return bool
*/
public function isDebug(): bool
{
return !$this->config->get('system.languages.translations', true);
}
/**
* Gets the array of supported languages
*
* @return array
*/
public function getLanguages()
{
return $this->languages;
}
/**
* Sets the current supported languages manually
*
* @param array $langs
* @return void
*/
public function setLanguages($langs)
{
$this->languages = $langs;
$this->init();
}
/**
* Gets a pipe-separated string of available languages
*
* @param string|null $delimiter Delimiter to be quoted.
* @return string
*/
public function getAvailable($delimiter = null)
{
$languagesArray = $this->languages; //Make local copy
$languagesArray = array_map(static function ($value) use ($delimiter) {
return preg_quote($value, $delimiter);
}, $languagesArray);
sort($languagesArray);
return implode('|', array_reverse($languagesArray));
}
/**
* Gets language, active if set, else default
*
* @return string|false
*/
public function getLanguage()
{
return $this->active ?: $this->default;
}
/**
* Gets current default language
*
* @return string|false
*/
public function getDefault()
{
return $this->default;
}
/**
* Sets default language manually
*
* @param string $lang
* @return string|bool
*/
public function setDefault($lang)
{
$lang = (string)$lang;
if ($this->validate($lang)) {
$this->default = $lang;
return $lang;
}
return false;
}
/**
* Gets current active language
*
* @return string|false
*/
public function getActive()
{
return $this->active;
}
/**
* Sets active language manually
*
* @param string|false $lang
* @return string|false
*/
public function setActive($lang)
{
$lang = (string)$lang;
if ($this->validate($lang)) {
/** @var Debugger $debugger */
$debugger = $this->grav['debugger'];
$debugger->addMessage('Active language set to ' . $lang, 'debug');
$this->active = $lang;
return $lang;
}
return false;
}
/**
* Sets the active language based on the first part of the URL
*
* @param string $uri
* @return string
*/
public function setActiveFromUri($uri)
{
$regex = '/(^\/(' . $this->getAvailable() . '))(?:\/|\?|$)/i';
// if languages set
if ($this->enabled()) {
// Try setting language from prefix of URL (/en/blah/blah).
if (preg_match($regex, $uri, $matches)) {
$this->lang_in_url = true;
$this->setActive($matches[2]);
$uri = preg_replace("/\\" . $matches[1] . '/', '', $uri, 1);
// Store in session if language is different.
if (isset($this->grav['session']) && $this->grav['session']->isStarted()
&& $this->config->get('system.languages.session_store_active', true)
&& $this->grav['session']->active_language != $this->active
) {
$this->grav['session']->active_language = $this->active;
}
} else {
// Try getting language from the session, else no active.
if (isset($this->grav['session']) && $this->grav['session']->isStarted() &&
$this->config->get('system.languages.session_store_active', true)) {
$this->setActive($this->grav['session']->active_language ?: null);
}
// if still null, try from http_accept_language header
if ($this->active === null &&
$this->config->get('system.languages.http_accept_language') &&
$accept = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? false) {
$negotiator = new LanguageNegotiator();
$best_language = $negotiator->getBest($accept, $this->languages);
if ($best_language instanceof AcceptLanguage) {
$this->setActive($best_language->getType());
} else {
$this->setActive($this->getDefault());
}
}
}
}
return $uri;
}
/**
* Get a URL prefix based on configuration
*
* @param string|null $lang
* @return string
*/
public function getLanguageURLPrefix($lang = null)
{
if (!$this->enabled()) {
return '';
}
// if active lang is not passed in, use current active
if (!$lang) {
$lang = $this->getLanguage();
}
return $this->isIncludeDefaultLanguage($lang) ? '/' . $lang : '';
}
/**
* Test to see if language is default and language should be included in the URL
*
* @param string|null $lang
* @return bool
*/
public function isIncludeDefaultLanguage($lang = null)
{
if (!$this->enabled()) {
return false;
}
// if active lang is not passed in, use current active
if (!$lang) {
$lang = $this->getLanguage();
}
return !($this->default === $lang && $this->config->get('system.languages.include_default_lang') === false);
}
/**
* Simple getter to tell if a language was found in the URL
*
* @return bool
*/
public function isLanguageInUrl()
{
return (bool) $this->lang_in_url;
}
/**
* Get full list of used language page extensions: [''=>'.md', 'en'=>'.en.md', ...]
*
* @param string|null $fileExtension
* @return array
*/
public function getPageExtensions($fileExtension = null)
{
$fileExtension = $fileExtension ?: CONTENT_EXT;
if (!isset($this->fallback_extensions[$fileExtension])) {
$extensions[''] = $fileExtension;
foreach ($this->languages as $code) {
$extensions[$code] = ".{$code}{$fileExtension}";
}
$this->fallback_extensions[$fileExtension] = $extensions;
}
return $this->fallback_extensions[$fileExtension];
}
/**
* Gets an array of valid extensions with active first, then fallback extensions
*
* @param string|null $fileExtension
* @param string|null $languageCode
* @param bool $assoc Return values in ['en' => '.en.md', ...] format.
* @return array Key is the language code, value is the file extension to be used.
*/
public function getFallbackPageExtensions(string $fileExtension = null, string $languageCode = null, bool $assoc = false)
{
$fileExtension = $fileExtension ?: CONTENT_EXT;
$key = $fileExtension . '-' . ($languageCode ?? 'default') . '-' . (int)$assoc;
if (!isset($this->fallback_extensions[$key])) {
$all = $this->getPageExtensions($fileExtension);
$list = [];
$fallback = $this->getFallbackLanguages($languageCode, true);
foreach ($fallback as $code) {
$ext = $all[$code] ?? null;
if (null !== $ext) {
$list[$code] = $ext;
}
}
if (!$assoc) {
$list = array_values($list);
}
$this->fallback_extensions[$key] = $list;
}
return $this->fallback_extensions[$key];
}
/**
* Resets the fallback_languages value.
*
* Useful to re-initialize the pages and change site language at runtime, example:
*
* ```
* $this->grav['language']->setActive('it');
* $this->grav['language']->resetFallbackPageExtensions();
* $this->grav['pages']->init();
* ```
*
* @return void
*/
public function resetFallbackPageExtensions()
{
$this->fallback_languages = [];
$this->fallback_extensions = [];
$this->page_extensions = [];
}
/**
* Gets an array of languages with active first, then fallback languages.
*
*
* @param string|null $languageCode
* @param bool $includeDefault If true, list contains '', which can be used for default
* @return array
*/
public function getFallbackLanguages(string $languageCode = null, bool $includeDefault = false)
{
// Handle default.
if ($languageCode === '' || !$this->enabled()) {
return [''];
}
$default = $this->getDefault() ?? 'en';
$active = $languageCode ?? $this->getActive() ?? $default;
$key = $active . '-' . (int)$includeDefault;
if (!isset($this->fallback_languages[$key])) {
$fallback = $this->config->get('system.languages.content_fallback.' . $active);
$fallback_languages = [];
if (null === $fallback && $this->config->get('system.languages.pages_fallback_only', false)) {
user_error('Configuration option `system.languages.pages_fallback_only` is deprecated since Grav 1.7, use `system.languages.content_fallback` instead', E_USER_DEPRECATED);
// Special fallback list returns itself and all the previous items in reverse order:
// active: 'v2', languages: ['v1', 'v2', 'v3', 'v4'] => ['v2', 'v1', '']
if ($includeDefault) {
$fallback_languages[''] = '';
}
foreach ($this->languages as $code) {
$fallback_languages[$code] = $code;
if ($code === $active) {
break;
}
}
$fallback_languages = array_reverse($fallback_languages);
} else {
if (null === $fallback) {
$fallback = [$default];
} elseif (!is_array($fallback)) {
$fallback = is_string($fallback) && $fallback !== '' ? explode(',', $fallback) : [];
}
array_unshift($fallback, $active);
$fallback = array_unique($fallback);
foreach ($fallback as $code) {
// Default fallback list has active language followed by default language and extensionless file:
// active: 'fi', default: 'en', languages: ['sv', 'en', 'de', 'fi'] => ['fi', 'en', '']
$fallback_languages[$code] = $code;
if ($includeDefault && $code === $default) {
$fallback_languages[''] = '';
}
}
}
$fallback_languages = array_values($fallback_languages);
$this->fallback_languages[$key] = $fallback_languages;
}
return $this->fallback_languages[$key];
}
/**
* Ensures the language is valid and supported
*
* @param string $lang
* @return bool
*/
public function validate($lang)
{
return in_array($lang, $this->languages, true);
}
/**
* Translate a key and possibly arguments into a string using current lang and fallbacks
*
* @param string|array $args The first argument is the lookup key value
* Other arguments can be passed and replaced in the translation with sprintf syntax
* @param array|null $languages
* @param bool $array_support
* @param bool $html_out
* @return string|string[]
*/
public function translate($args, array $languages = null, $array_support = false, $html_out = false)
{
if (is_array($args)) {
$lookup = array_shift($args);
} else {
$lookup = $args;
$args = [];
}
if (!$this->isDebug()) {
if ($lookup && $this->enabled() && empty($languages)) {
$languages = $this->getTranslatedLanguages();
}
$languages = $languages ?: ['en'];
foreach ((array)$languages as $lang) {
$translation = $this->getTranslation($lang, $lookup, $array_support);
if ($translation) {
if (is_string($translation) && count($args) >= 1) {
return vsprintf($translation, $args);
}
return $translation;
}
}
} elseif ($array_support) {
return [$lookup];
}
if ($html_out) {
return '' . $lookup . '';
}
return $lookup;
}
/**
* Translate Array
*
* @param string $key
* @param string $index
* @param array|null $languages
* @param bool $html_out
* @return string
*/
public function translateArray($key, $index, $languages = null, $html_out = false)
{
if ($this->isDebug()) {
return $key . '[' . $index . ']';
}
if ($key && empty($languages) && $this->enabled()) {
$languages = $this->getTranslatedLanguages();
}
$languages = $languages ?: ['en'];
foreach ((array)$languages as $lang) {
$translation_array = (array)Grav::instance()['languages']->get($lang . '.' . $key, null);
if ($translation_array && array_key_exists($index, $translation_array)) {
return $translation_array[$index];
}
}
if ($html_out) {
return '' . $key . '[' . $index . ']';
}
return $key . '[' . $index . ']';
}
/**
* Lookup the translation text for a given lang and key
*
* @param string $lang lang code
* @param string $key key to lookup with
* @param bool $array_support
* @return string|string[]
*/
public function getTranslation($lang, $key, $array_support = false)
{
if ($this->isDebug()) {
return $key;
}
$translation = Grav::instance()['languages']->get($lang . '.' . $key, null);
if (!$array_support && is_array($translation)) {
return (string)array_shift($translation);
}
return $translation;
}
/**
* Get the browser accepted languages
*
* @param array $accept_langs
* @return array
* @deprecated 1.6 No longer used - using content negotiation.
*/
public function getBrowserLanguages($accept_langs = [])
{
user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, no longer used', E_USER_DEPRECATED);
if (empty($this->http_accept_language)) {
if (empty($accept_langs) && isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
$accept_langs = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
} else {
return $accept_langs;
}
$langs = [];
foreach (explode(',', $accept_langs) as $k => $pref) {
// split $pref again by ';q='
// and decorate the language entries by inverted position
if (false !== ($i = strpos($pref, ';q='))) {
$langs[substr($pref, 0, $i)] = [(float)substr($pref, $i + 3), -$k];
} else {
$langs[$pref] = [1, -$k];
}
}
arsort($langs);
// no need to undecorate, because we're only interested in the keys
$this->http_accept_language = array_keys($langs);
}
return $this->http_accept_language;
}
/**
* Accessible wrapper to LanguageCodes
*
* @param string $code
* @param string $type
* @return string|false
*/
public function getLanguageCode($code, $type = 'name')
{
return LanguageCodes::get($code, $type);
}
/**
* @return array
*/
public function __debugInfo()
{
$vars = get_object_vars($this);
unset($vars['grav'], $vars['config']);
return $vars;
}
/**
* @return array
*/
protected function getTranslatedLanguages(): array
{
if ($this->config->get('system.languages.translations_fallback', true)) {
$languages = $this->getFallbackLanguages();
} else {
$languages = [$this->getLanguage()];
}
$languages[] = 'en';
return array_values(array_unique($languages));
}
}