diff --git a/system/src/Grav/Common/Config/Config.php b/system/src/Grav/Common/Config/Config.php index f2c6abcbc..f1d2d611b 100644 --- a/system/src/Grav/Common/Config/Config.php +++ b/system/src/Grav/Common/Config/Config.php @@ -60,6 +60,12 @@ class Config extends Data '' => ['user://themes'], ] ], + 'languages' => [ + 'type' => 'ReadOnlyStream', + 'prefixes' => [ + '' => ['user://languages', 'system/languages'], + ] + ], 'cache' => [ 'type' => 'Stream', 'prefixes' => [ @@ -85,17 +91,21 @@ class Config extends Data protected $blueprintFiles = []; protected $configFiles = []; + protected $languageFiles = []; protected $checksum; protected $timestamp; protected $configLookup; protected $blueprintLookup; protected $pluginLookup; + protected $languagesLookup; protected $finder; protected $environment; protected $messages = []; + protected $languages; + public function __construct(array $setup = array(), Grav $grav = null, $environment = null) { $this->grav = $grav ?: Grav::instance(); @@ -164,42 +174,61 @@ class Config extends Data $this->blueprintLookup = $locator->findResources('blueprints://config'); $this->pluginLookup = $locator->findResources('plugins://'); + $this->loadCompiledBlueprints($this->blueprintLookup, $this->pluginLookup, 'master'); $this->loadCompiledConfig($this->configLookup, $this->pluginLookup, 'master'); + // process languages if supported + if ($this->get('languages', false)) { + $this->languagesLookup = $locator->findResources('languages://'); + $this->loadCompiledLanguages($this->languagesLookup, $this->pluginLookup, 'master'); + } + $this->initializeLocator($locator); } public function checksum() { - $checkBlueprints = $this->get('system.cache.check.blueprints', false); - $checkConfig = $this->get('system.cache.check.config', true); - $checkSystem = $this->get('system.cache.check.system', true); + if (empty($this->checksum)) { + $checkBlueprints = $this->get('system.cache.check.blueprints', false); + $checkLanguages = $this->get('system.cache.check.languages', false); + $checkConfig = $this->get('system.cache.check.config', true); + $checkSystem = $this->get('system.cache.check.system', true); - if (!$checkBlueprints && !$checkConfig && !$checkSystem) { - $this->messages[] = 'Skip configuration timestamp check.'; - return false; + if (!$checkBlueprints && !!$checkLanguages && $checkConfig && !$checkSystem) { + $this->messages[] = 'Skip configuration timestamp check.'; + return false; + } + + // Generate checksum according to the configuration settings. + if (!$checkConfig) { + $this->messages[] = 'Check configuration timestamps from system.yaml files.'; + // Just check changes in system.yaml files and ignore all the other files. + $cc = $checkSystem ? $this->finder->locateConfigFile($this->configLookup, 'system') : []; + } else { + $this->messages[] = 'Check configuration timestamps from all configuration files.'; + // Check changes in all configuration files. + $cc = $this->finder->locateConfigFiles($this->configLookup, $this->pluginLookup); + } + + if ($checkBlueprints) { + $this->messages[] = 'Check blueprint timestamps from all blueprint files.'; + $cb = $this->finder->locateBlueprintFiles($this->blueprintLookup, $this->pluginLookup); + } else { + $cb = []; + } + + if ($checkLanguages) { + $this->messages[] = 'Check language timestamps from all language files.'; + $cl = $this->finder->locateLanguageFiles($this->languagesLookup, $this->pluginLookup); + } else { + $cl = []; + } + + $this->checksum = md5(json_encode([$cc, $cb, $cl])); } - // Generate checksum according to the configuration settings. - if (!$checkConfig) { - $this->messages[] = 'Check configuration timestamps from system.yaml files.'; - // Just check changes in system.yaml files and ignore all the other files. - $cc = $checkSystem ? $this->finder->locateConfigFile($this->configLookup, 'system') : []; - } else { - $this->messages[] = 'Check configuration timestamps from all configuration files.'; - // Check changes in all configuration files. - $cc = $this->finder->locateConfigFiles($this->configLookup, $this->pluginLookup); - } - - if ($checkBlueprints) { - $this->messages[] = 'Check blueprint timestamps from all blueprint files.'; - $cb = $this->finder->locateBlueprintFiles($this->blueprintLookup, $this->pluginLookup); - } else { - $cb = []; - } - - return md5(json_encode([$cc, $cb])); + return $this->checksum; } protected function autoDetectEnvironmentConfig($items) @@ -312,6 +341,54 @@ class Config extends Data $this->items = $cache['data']; } + protected function loadCompiledLanguages($languages, $plugins, $filename = null) + { + $checksum = md5(json_encode($languages)); + $filename = $filename + ? CACHE_DIR . 'compiled/languages/' . $filename . '-' . $this->environment . '.php' + : CACHE_DIR . 'compiled/languages/' . $checksum . '-' . $this->environment . '.php'; + $file = PhpFile::instance($filename); + $cache = $file->exists() ? $file->content() : null; + $languageFiles = $this->finder->locateLanguageFiles($languages, $plugins); + $checksum .= ':' . md5(json_encode($languageFiles)); + $class = get_class($this); + + // Load real file if cache isn't up to date (or is invalid). + if ( + !is_array($cache) + || !isset($cache['checksum']) + || !isset($cache['@class']) + || $cache['checksum'] != $checksum + || $cache['@class'] != $class + ) { + // Attempt to lock the file for writing. + $file->lock(false); + + // Load languages. + $this->languages = new Languages; + foreach ($languageFiles as $files) { + $this->loadLanguagesFiles($files); + } + + $this->languages->reformat(); + + $cache = [ + '@class' => $class, + 'checksum' => $checksum, + 'files' => $languageFiles, + 'data' => $this->languages->toArray() + ]; + // If compiled file wasn't already locked by another process, save it. + if ($file->locked() !== false) { + $this->messages[] = 'Saving compiled languages.'; + $file->save($cache); + $file->unlock(); + } + } else { + $this->languages = new Languages($cache['data']); + } + } + /** * Load blueprints. * @@ -338,6 +415,14 @@ class Config extends Data } } + public function loadLanguagesFiles(array $files) + { + foreach ($files as $name => $item) { + $file = CompiledYamlFile::instance($item['file']); + $this->languages->join($name, $file->content(), '/'); + } + } + /** * Initialize resource locator by using the configuration. * @@ -380,4 +465,9 @@ class Config extends Data return $schemes; } + + public function getLanguages() + { + return $this->languages; + } } diff --git a/system/src/Grav/Common/Config/ConfigFinder.php b/system/src/Grav/Common/Config/ConfigFinder.php index 243ddc5aa..7d79b765b 100644 --- a/system/src/Grav/Common/Config/ConfigFinder.php +++ b/system/src/Grav/Common/Config/ConfigFinder.php @@ -49,6 +49,18 @@ class ConfigFinder return $list; } + public function locateLanguageFiles(array $languages, array $plugins) + { + $list = []; + foreach (array_reverse($plugins) as $folder) { + $list += $this->detectLanguagesInFolder($folder, 'languages'); + } + foreach (array_reverse($languages) as $folder) { + $list += $this->detectRecursive($folder); + } + return $list; + } + /** * Get all locations for a single configuration file. * @@ -90,11 +102,11 @@ class ConfigFinder $list = []; if (is_dir($folder)) { - $iterator = new \DirectoryIterator($folder); + $iterator = new \FilesystemIterator($folder); /** @var \DirectoryIterator $directory */ foreach ($iterator as $directory) { - if (!$directory->isDir() || $directory->isDot()) { + if (!$directory->isDir()) { continue; } @@ -111,6 +123,34 @@ class ConfigFinder return [$path => $list]; } + protected function detectLanguagesInFolder($folder, $lookup = null) + { + $path = trim(Folder::getRelativePath($folder), '/'); + + $list = []; + + if (is_dir($folder)) { + $iterator = new \FilesystemIterator($folder); + + /** @var \DirectoryIterator $directory */ + foreach ($iterator as $directory) { + if (!$directory->isDir()) { + continue; + } + + $name = $directory->getBasename(); + $find = ($lookup ?: $name) . '.yaml'; + $filename = "{$path}/{$name}/$find"; + + if (file_exists($filename)) { + $list["plugins"] = ['file' => $filename, 'modified' => filemtime($filename)]; + } + } + } + + return [$path => $list]; + } + /** * Detects all plugins with a configuration file and returns them with last modification time. * diff --git a/system/src/Grav/Common/Config/Languages.php b/system/src/Grav/Common/Config/Languages.php new file mode 100644 index 000000000..c0141292e --- /dev/null +++ b/system/src/Grav/Common/Config/Languages.php @@ -0,0 +1,27 @@ +items['plugins'])) { + $this->items = array_merge_recursive($this->items, $this->items['plugins']); + unset($this->items['plugins']); + } + } + + public function mergeRecursive(array $data) + { + $this->items = array_merge_recursive($this->items, $data); + } +} diff --git a/system/src/Grav/Common/Language.php b/system/src/Grav/Common/Language.php index 77c5c8892..5dd4d5b04 100644 --- a/system/src/Grav/Common/Language.php +++ b/system/src/Grav/Common/Language.php @@ -248,6 +248,13 @@ class Language $lookup = array_shift($args); if ($this->enabled() && $lookup) { + + if ($this->config->get('system.languages.translations.fallback', true)) { + $languages = $this->getFallbackLanguages(); + } else { + $languages = (array) $this->getDefault(); + } + foreach ($this->getFallbackLanguages() as $lang) { $translation = $this->getTranslation($lang, $lookup); @@ -272,6 +279,13 @@ class Language * @return string */ public function getTranslation($lang, $key) { - return $this->config->get('languages.'.$lang.'.'.$key, null); + $languages = $this->config->getLanguages(); + + $translation = $languages->get($lang.'.'.$key, null); + if (is_array($translation)) { + return (string) array_shift($translation); + } + + return $translation; } } diff --git a/system/src/Grav/Common/Themes.php b/system/src/Grav/Common/Themes.php index ab4696a88..dc4810f10 100644 --- a/system/src/Grav/Common/Themes.php +++ b/system/src/Grav/Common/Themes.php @@ -221,7 +221,13 @@ class Themes extends Iterator protected function loadConfiguration($name, Config $config) { $themeConfig = CompiledYamlFile::instance("themes://{$name}/{$name}" . YAML_EXT)->content(); - $config->joinDefaults("themes.{$name}", $themeConfig); + + if ($this->grav['language']->enabled()) { + $languages = CompiledYamlFile::instance("themes://{$name}/languages". YAML_EXT)->content(); + if ($languages) { + $config->getLanguages()->mergeRecursive($languages); + } + } } } diff --git a/system/src/Grav/Common/Uri.php b/system/src/Grav/Common/Uri.php index 2bd667fa1..65d4df667 100644 --- a/system/src/Grav/Common/Uri.php +++ b/system/src/Grav/Common/Uri.php @@ -95,9 +95,13 @@ class Uri $uri = $language->setActiveFromUri($uri); // redirect to language specific homepage if configured to do so - if ($uri == '/' && $language->enabled() && $config->get('system.languages.home.redirect', true) && !$language->getActive()) { - $prefix = $config->get('system.languages.home.include_lang', true) ? $language->getDefault() . '/' : ''; - $grav->redirect($prefix . Pages::getHomeRoute()); + if ($uri == '/' && $language->enabled() && !$language->getActive()) { + if ($config->get('system.languages.home_redirect.include_route', true)) { + $prefix = $config->get('system.languages.home_redirect.include_lang', true) ? $language->getDefault() . '/' : ''; + $grav->redirect($prefix . Pages::getHomeRoute()); + } elseif ($config->get('system.languages.home_redirect.include_lang', true)) { + $grav->redirect($language->getDefault() . '/'); + } } // split the URL and params