From 8872455e1a1d51cb132757bcd483ecfe123420c3 Mon Sep 17 00:00:00 2001 From: Matias Griese Date: Thu, 27 Jun 2019 13:19:30 +0300 Subject: [PATCH 1/2] Much improved multi-language support for pages Added `Admin::redirect()` method to allow redirects to be used outside of controllers Added `$admin->adminRoute()` method and `admin_route()` twig function to create language aware admin page links Renamed `Admin::route()` to `Admin::getCurrentRoute()` and deprecated the old call Admin redirects should now work better with multiple languages enabled Fixed default language being renamed to `page.en.md` (English) instead of keeping existing `page.md` filename --- CHANGELOG.md | 13 ++ classes/Twig/AdminTwigExtension.php | 27 ++-- classes/admin.php | 138 ++++++++++++++++-- classes/adminbasecontroller.php | 42 +----- classes/admincontroller.php | 76 +++++----- languages/en.yaml | 2 + .../forms/fields/parents/parents.html.twig | 2 +- themes/grav/templates/pages.html.twig | 85 ++++++----- .../partials/backups-button.html.twig | 2 +- .../media-list-wrapper__sidebar.html.twig | 14 -- .../partials/page-children.html.twig | 2 +- 11 files changed, 245 insertions(+), 158 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6d199be..ca7250d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +# v1.9.8 +## mm/dd/2019 + +1. [](#new) + * Added `Admin::redirect()` method to allow redirects to be used outside of controllers + * Added `$admin->adminRoute()` method and `admin_route()` twig function to create language aware admin page links + * Renamed `Admin::route()` to `Admin::getCurrentRoute()` and deprecated the old call +1. [](#improved) + * Much improved multi-language support for pages + * Admin redirects should now work better with multiple languages enabled +1. [](#bugfix) + * Fixed default language being renamed to `page.en.md` (English) instead of keeping existing `page.md` filename + # v1.9.7 ## 06/21/2019 diff --git a/classes/Twig/AdminTwigExtension.php b/classes/Twig/AdminTwigExtension.php index 073aef23..43a0d39c 100644 --- a/classes/Twig/AdminTwigExtension.php +++ b/classes/Twig/AdminTwigExtension.php @@ -6,6 +6,7 @@ use Grav\Common\Grav; use Grav\Common\Page\Interfaces\PageInterface; use Grav\Common\Yaml; use Grav\Common\Language\Language; +use Grav\Plugin\Admin\Admin; class AdminTwigExtension extends \Twig_Extension { @@ -39,7 +40,8 @@ class AdminTwigExtension extends \Twig_Extension public function getFunctions() { return [ - new \Twig_SimpleFunction('getPageUrl', [$this, 'getPageUrl'], ['needs_context' => true]), + new \Twig_SimpleFunction('admin_route', [$this, 'adminRouteFunc']), + new \Twig_SimpleFunction('getPageUrl', [$this, 'getPageUrl']), new \Twig_SimpleFunction('clone', [$this, 'cloneFunc']), ]; } @@ -66,21 +68,20 @@ class AdminTwigExtension extends \Twig_Extension return clone $obj; } - public function getPageUrl($context, PageInterface $page) + public function adminRouteFunc(string $route = '', string $languageCode = null) { - $page_route = trim($page->rawRoute(), '/'); - $page_lang = $page->language(); - $base_url = $context['base_url']; - $base_url_simple = $context['base_url_simple']; - $admin_lang = Grav::instance()['session']->admin_lang ?: 'en'; + /** @var Admin $admin */ + $admin = Grav::instance()['admin']; - if ($page_lang && $page_lang !== $admin_lang) { - $page_url = $base_url_simple . '/' . $page_lang . '/' . $context['admin_route'] . '/pages/' . $page_route; - } else { - $page_url = $base_url . '/pages/' . $page_route; - } + return $admin->getAdminRoute($route, $languageCode)->toString(true); + } - return $page_url; + public function getPageUrl(PageInterface $page) + { + /** @var Admin $admin */ + $admin = Grav::instance()['admin']; + + return $admin->getAdminRoute('/pages' . $page->rawRoute(), $page->language())->toString(true); } public static function tuFilter() diff --git a/classes/admin.php b/classes/admin.php index b9bcd846..9410c458 100644 --- a/classes/admin.php +++ b/classes/admin.php @@ -10,6 +10,7 @@ use Grav\Common\GPM\Licenses; use Grav\Common\GPM\Response; use Grav\Common\Grav; use Grav\Common\Helpers\YamlLinter; +use Grav\Common\Language\Language; use Grav\Common\Language\LanguageCodes; use Grav\Common\Page\Collection; use Grav\Common\Page\Interfaces\PageInterface; @@ -24,6 +25,8 @@ use Grav\Common\User\Interfaces\UserCollectionInterface; use Grav\Common\User\User; use Grav\Common\Utils; use Grav\Framework\Collection\ArrayCollection; +use Grav\Framework\Route\Route; +use Grav\Framework\Route\RouteFactory; use Grav\Plugin\Login\Login; use Grav\Plugin\Login\TwoFactorAuth\TwoFactorAuth; use PicoFeed\Parser\MalformedXmlException; @@ -74,8 +77,11 @@ class Admin /** @var bool */ public $multilang; + /** @var string */ + public $language; + /** @var array */ - public $languages_enabled; + public $languages_enabled = []; /** @var Uri $uri */ protected $uri; @@ -128,27 +134,25 @@ class Admin $this->session = $grav['session']; $this->user = $grav['user']; $this->permissions = []; - $language = $grav['language']; + + /** @var Language $language */ + $language = $grav['language']; + + $this->multilang = $language->enabled(); // Load utility class - if ($language->enabled()) { - $this->multilang = true; + if ($this->multilang) { + $this->language = $language->getLanguage(); $this->languages_enabled = (array)$this->grav['config']->get('system.languages.supported', []); //Set the currently active language for the admin - $language = $this->grav['uri']->param('lang'); - if (!$language) { - if (!$this->session->admin_lang) { - $this->session->admin_lang = $this->grav['language']->getLanguage(); - } - $language = $this->session->admin_lang; + $languageCode = $this->grav['uri']->param('lang'); + if (!$languageCode && !$this->session->admin_lang) { + $this->session->admin_lang = $language->getLanguage(); } - $this->grav['language']->setActive($language ?: 'en'); } else { - $this->grav['language']->setActive('en'); - $this->multilang = false; + $this->language = 'en'; } - } /** @@ -180,6 +184,11 @@ class Admin return $languages; } + public function getLanguage(): string + { + return $this->language; + } + /** * Return the found configuration blueprints * @@ -276,15 +285,56 @@ class Admin return Grav::instance()['session']->lastPageRoute ?: self::route(); } + public function getAdminRoute(string $path = '', $languageCode = null): Route + { + /** @var Language $language */ + $language = $this->grav['language']; + $languageCode = $languageCode ?? $language->getLanguage(); + $languagePrefix = $language->getLanguageURLPrefix($languageCode); + + $parts = [ + 'path' => $path, + 'query' => '', + 'query_params' => [], + 'grav' => [ + // TODO: Make URL to be /admin/en, not /en/admin. + 'root' => RouteFactory::getRoot() . $languagePrefix . $this->base, + 'language' => '', //$languageCode, + 'route' => ltrim($path, '/'), + 'params' => '' + ], + ]; + + return RouteFactory::createFromParts($parts); + } + + public function adminUrl(string $route = '', $languageCode = null) + { + /** @var string $base_url */ + $baseUrl = $this->grav['base_url']; + + return $baseUrl . $this->getAdminRoute($route, $languageCode); + } + /** * Static helper method to return current route. * * @return string + * @deprecated 1.9.7 */ public static function route() + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Admin 1.9.7, use $admin->getCurrentRoute() instead', E_USER_DEPRECATED); + + $admin = Grav::instance()['admin']; + + return $admin->getCurrentRoute(); + } + + public function getCurrentRoute() { $pages = Grav::instance()['pages']; - $route = '/' . ltrim(Grav::instance()['admin']->route, '/'); + $route = '/' . ltrim($this->route, '/'); /** @var PageInterface $page */ $page = $pages->dispatch($route); @@ -298,6 +348,64 @@ class Admin return $parent_route; } + /** + * Redirect to the route stored in $this->redirect + * + * Route may or may not be prefixed by /en or /admin or /en/admin. + * + * @param string $redirect + * @param int$redirectCode + */ + public function redirect($redirect, $redirectCode = 303) + { + // No redirect, do nothing. + if (!$redirect) { + return; + } + + $redirect = '/' . ltrim($redirect, '/'); + $base = $this->base; + + // Check if we already have an admin path: /admin. + if (Utils::startsWith($redirect, $base)) { + $this->grav->redirect($redirect, $redirectCode); + } + + if ($this->isMultilang()) { + // Check if URL does not have language prefix. + if (!Utils::pathPrefixedByLangCode($redirect)) { + /** @var Language $language */ + $language = $this->grav['language']; + + // Prefix path with language prefix: /en + // TODO: Use /admin/en instead of /en/admin in the future. + $redirect = $language->getLanguageURLPrefix($this->grav['session']->admin_lang) . $base . $redirect; + } else { + // TODO: Use /admin/en instead of /en/admin in the future. + //$redirect = preg_replace('`^(/[^/]+)/admin`', '\\1', $redirect); + + // Check if we already have language prefixed admin path: /en/admin + $this->grav->redirect($redirect, $redirectCode); + } + } else { + // TODO: Use /admin/en instead of /en/admin in the future. + // Prefix path with /admin + $redirect = $base . $redirect; + } + + $this->grav->redirect($redirect, $redirectCode); + } + + /** + * Return true if multilang is active + * + * @return bool True if multilang is active + */ + protected function isMultilang() + { + return count($this->grav['config']->get('system.languages.supported', [])) > 1; + } + public static function getTempDir() { try { diff --git a/classes/adminbasecontroller.php b/classes/adminbasecontroller.php index 03ee9ab6..8b373a45 100644 --- a/classes/adminbasecontroller.php +++ b/classes/adminbasecontroller.php @@ -609,48 +609,12 @@ class AdminBaseController /** * Redirect to the route stored in $this->redirect + * + * Route may or may not be prefixed by /en or /admin or /en/admin. */ public function redirect() { - if (!$this->redirect) { - return; - } - - $base = $this->admin->base; - $this->redirect = '/' . ltrim($this->redirect, '/'); - $multilang = $this->isMultilang(); - - $redirect = ''; - if ($multilang) { - // if base path does not already contain the lang code, add it - $langPrefix = '/' . $this->grav['session']->admin_lang; - if (!Utils::startsWith($base, $langPrefix . '/')) { - $base = $langPrefix . $base; - } - - // now the first 4 chars of base contain the lang code. - // if redirect path already contains the lang code, and is != than the base lang code, then use redirect path as-is - if (Utils::pathPrefixedByLangCode($base) && Utils::pathPrefixedByLangCode($this->redirect) - && !Utils::startsWith($this->redirect, $base) - ) { - $redirect = $this->redirect; - } else { - if (!Utils::startsWith($this->redirect, $base)) { - $this->redirect = $base . $this->redirect; - } - } - - } else { - if (!Utils::startsWith($this->redirect, $base)) { - $this->redirect = $base . $this->redirect; - } - } - - if (!$redirect) { - $redirect = $this->redirect; - } - - $this->grav->redirect($redirect, $this->redirectCode); + $this->admin->redirect($this->redirect, $this->redirectCode); } /** diff --git a/classes/admincontroller.php b/classes/admincontroller.php index a50cfaa5..6b7fe063 100644 --- a/classes/admincontroller.php +++ b/classes/admincontroller.php @@ -11,6 +11,7 @@ use Grav\Common\GPM\GPM as GravGPM; use Grav\Common\GPM\Installer; use Grav\Common\Grav; use Grav\Common\Data; +use Grav\Common\Language\Language; use Grav\Common\Page\Interfaces\PageInterface; use Grav\Common\Page\Media; use Grav\Common\Page\Medium\ImageMedium; @@ -499,10 +500,7 @@ class AdminController extends AdminBaseController $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.SUCCESSFULLY_SAVED'), 'info'); - $multilang = $this->isMultilang(); - $admin_route = $this->admin->base; - $redirect_url = '/' . ($multilang ? ($this->grav['session']->admin_lang) : '') . $admin_route . '/' . $this->view; - $this->setRedirect($redirect_url); + $this->setRedirect($this->admin->getAdminRoute("/{$this->view}")->toString()); return true; } @@ -660,10 +658,8 @@ class AdminController extends AdminBaseController $multilang = $this->isMultilang(); - if ($multilang) { - if (!$obj->language()) { - $obj->language($this->grav['session']->admin_lang); - } + if ($multilang && !$obj->language()) { + $obj->language($this->admin->language); } $admin_route = $this->admin->base; @@ -1933,9 +1929,9 @@ class AdminController extends AdminBaseController * * @param PageInterface $page * @param bool $clean_header - * @param string $language + * @param string $languageCode */ - protected function preparePage(PageInterface $page, $clean_header = false, $language = '') + protected function preparePage(PageInterface $page, $clean_header = false, $languageCode = '') { $input = (array)$this->data; @@ -1947,18 +1943,30 @@ class AdminController extends AdminBaseController if (isset($input['name']) && !empty($input['name'])) { $type = strtolower($input['name']); + $page->template($type); $name = preg_replace('|.*/|', '', $type); - if ($language) { - $name .= '.' . $language; - } else { - $language = $this->grav['language']; - if ($language->enabled()) { - $name .= '.' . $language->getLanguage(); + + /** @var Language $language */ + $language = $this->grav['language']; + if ($language->enabled()) { + $languageCode = $languageCode ?: $language->getLanguage(); + if ($languageCode) { + $isDefault = $languageCode === $language->getDefault(); + $includeLang = !$isDefault || (bool)$this->grav['config']->get('system.languages.include_default_lang_file_extension', true); + if (!$includeLang) { + // Check if the language specific file exists; use it if it does. + $includeLang = file_exists("{$page->path()}/{$name}.{$languageCode}.md"); + } + + // Keep existing .md file if we're updating default language, otherwise always append the language. + if ($includeLang) { + $name .= '.' . $languageCode; + } } } + $name .= '.md'; $page->name($name); - $page->template($type); } // Special case for Expert mode: build the raw, unset content @@ -2227,16 +2235,12 @@ class AdminController extends AdminBaseController $data = (array)$this->data; - if (isset($data['lang'])) { - $language = $data['lang']; - } else { - $language = $this->grav['uri']->param('lang'); - } + $language = $data['lang'] ?? $this->grav['uri']->param('lang'); if (isset($data['redirect'])) { - $redirect = 'pages/' . $data['redirect']; + $redirect = '/pages/' . $data['redirect']; } else { - $redirect = 'pages'; + $redirect = '/pages'; } @@ -2246,8 +2250,7 @@ class AdminController extends AdminBaseController $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.SUCCESSFULLY_SWITCHED_LANGUAGE'), 'info'); - $admin_route = $this->admin->base; - $this->setRedirect('/' . $language . $admin_route . '/' . $redirect); + $this->setRedirect($this->admin->getAdminRoute($redirect)->toString()); return true; } @@ -2324,27 +2327,30 @@ class AdminController extends AdminBaseController return false; } - $data = (array)$this->data; - $language = $data['lang']; + /** @var Language $language */ + $language = $this->grav['language']; - if ($language) { - $this->grav['session']->admin_lang = $language ?: 'en'; + $data = (array)$this->data; + $lang = $data['lang'] ?? null; + + if ($lang) { + $this->grav['session']->admin_lang = $lang ?: 'en'; } $uri = $this->grav['uri']; $obj = $this->admin->page($uri->route()); - $this->preparePage($obj, false, $language); + $this->preparePage($obj, false, $lang); $file = $obj->file(); if ($file) { - $filename = $this->determineFilenameIncludingLanguage($obj->name(), $language); + $filename = $this->determineFilenameIncludingLanguage($obj->name(), $lang); $path = $obj->path() . DS . $filename; $aFile = File::instance($path); $aFile->save(); $aPage = new Page(); - $aPage->init(new \SplFileInfo($path), $language . '.md'); + $aPage->init(new \SplFileInfo($path), $lang . '.md'); $aPage->header($obj->header()); $aPage->rawMarkdown($obj->rawMarkdown()); $aPage->template($obj->template()); @@ -2357,7 +2363,9 @@ class AdminController extends AdminBaseController } $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.SUCCESSFULLY_SWITCHED_LANGUAGE'), 'info'); - $this->setRedirect('/' . $language . $uri->route()); + + // TODO: better multilanguage support needed. + $this->setRedirect($language->getLanguageURLPrefix($lang) . $uri->route()); return true; } diff --git a/languages/en.yaml b/languages/en.yaml index e719e89c..fa3171bb 100644 --- a/languages/en.yaml +++ b/languages/en.yaml @@ -501,6 +501,8 @@ PLUGIN_ADMIN: PLUGIN_STATUS: "Plugin status" INCLUDE_DEFAULT_LANG: "Include default language" INCLUDE_DEFAULT_LANG_HELP: "This will prepend all URLs in the default language with the default language. e.g. `/en/blog/my-post`" + INCLUDE_DEFAULT_LANG_FILE_EXTENSION: "Include default language in file extension" + INCLUDE_DEFAULT_LANG_HELP_FILE_EXTENSION: "If enabled, it will prepend the default language to the file extension (e.g. `.en.md`). Disable it to keep the default language using `.md` file extension." PAGES_FALLBACK_ONLY: "Pages fallback only" PAGES_FALLBACK_ONLY_HELP: "Only 'fallback' to find page content through supported languages, default behavior is to display any language found if active language is missing" ALLOW_URL_TAXONOMY_FILTERS: "URL Taxonomy Filters" diff --git a/themes/grav/templates/forms/fields/parents/parents.html.twig b/themes/grav/templates/forms/fields/parents/parents.html.twig index 982c6619..8ac59028 100644 --- a/themes/grav/templates/forms/fields/parents/parents.html.twig +++ b/themes/grav/templates/forms/fields/parents/parents.html.twig @@ -12,7 +12,7 @@ {% elseif show_parents == 'fullpath' %} {% set show_fullpath_val = true %} {% endif %} - + {% set limit_levels_val = config.get('plugins.admin.pages.parents_levels') %} {% set show_modular_val = config.get('plugins.admin.pages.show_modular', true) %} diff --git a/themes/grav/templates/pages.html.twig b/themes/grav/templates/pages.html.twig index 9e166d97..f3ef5d6a 100644 --- a/themes/grav/templates/pages.html.twig +++ b/themes/grav/templates/pages.html.twig @@ -10,11 +10,8 @@ {% set config = twig_vars['config'] %} {% set separator = config.system.param_sep %} {% set display_field = config.plugins.admin.pages_list_display_field %} - {% set base_url = twig_vars['base_url_relative'] %} {% set base_url_relative_frontend = twig_vars['base_url_relative_frontend'] %} - {% set base_url_simple = twig_vars['base_url_simple'] %} - {% set admin_route = twig_vars['admin_route'] %} - {% set admin_lang = twig_vars['admin_lang'] %} + {% set admin = twig_vars['admin'] %} {% set warn = twig_vars['warn'] %} {% set uri = twig_vars['uri'] %} @@ -49,7 +46,7 @@ {{ page_label|e }} {% if p.language %} - {{p.language}} + {{p.language}} {% endif %} {% if p.home %} @@ -60,11 +57,11 @@ {% if config.plugins.admin.frontend_preview_target != 'inline' %} {% set preview_target = config.plugins.admin.frontend_preview_target %} - {% set preview_html = (base_url_relative_frontend|rtrim('/') ~ (p.home ? '' : p.route)) ?: '/' %} - {% set preview_link = p.routable ? ' ' : '' %} + {% set preview_route = (base_url_relative_frontend|rtrim('/') ~ (p.home ? '' : p.route)) ?: '/' %} + {% set preview_link = p.routable ? ' ' : '' %} {% else %} - {% set preview_html = (base_url|rtrim('/') ~ '/preview' ~ (p.home ? '' : p.route)) ?: '/' %} - {% set preview_link = p.routable ? ' ' : '' %} + {% set preview_route = admin_route('/preview' ~ (p.home ? '' : p.route), 'fi') %} + {% set preview_link = p.routable ? ' ' : '' %} {% endif %} {{ preview_link|raw }} {% if warn %} @@ -87,6 +84,14 @@ {% if admin.route %} {% set context = admin.page(true) %} + {# + {% if admin.language != admin.session.admin_lang %} + {% do admin.setMessage('Session language does not match') %} + {% endif %} + #} +{% elseif admin.language != admin.session.admin_lang %} + {# Redirect to last set language #} + {% do admin.redirect(admin.adminRoute('/pages', admin.session.admin_lang)) %} {% endif %} {% if uri.param('new') %} @@ -94,7 +99,7 @@ {% elseif context %} {% set mode = 'edit' %} {% if context.exists %} - {% set page_url = base_url ~ '/pages' ~ (context.header.routes.default ?: context.rawRoute) %} + {% set page_url = admin_route('/pages' ~ (context.header.routes.default ?: context.rawRoute)) %} {% set exists = true %} {% set title = (context.exists ? "PLUGIN_ADMIN.EDIT"|tu : "PLUGIN_ADMIN.CREATE"|tu ) ~ " " ~ (context.header.title ?: context.title) %} {% else %} @@ -107,7 +112,6 @@ {% set modular = context.modular ? 'modular_' : '' %} {% set warn = config.plugins.admin.warnings.delete_page %} -{% set admin_lang = admin.session.admin_lang ?: 'en' %} {% set page_lang = context.language %} {% set type = 'page' %} @@ -123,19 +127,19 @@ {% endblock %} {% if config.plugins.admin.frontend_preview_target != 'inline' %} - {% set preview_html = (base_url_relative_frontend|rtrim('/') ~ (context.home ? '' : context.route)) ?: '/' %} + {% set preview_route = (base_url_relative_frontend|rtrim('/') ~ (context.home ? '' : context.route)) ?: '/' %} {% set preview_target = config.plugins.admin.frontend_preview_target %} - {% set preview_link = context.routable ? ' ' : '' %} + {% set preview_link = context.routable ? ' ' : '' %} {% else %} - {% set preview_html = (base_url|rtrim('/') ~ '/preview' ~ (context.home ? '' : context.route)) ?: '/' %} - {% set preview_link = context.routable ? ' ' : '' %} + {% set preview_route = admin_route('/preview' ~ (context.home ? '' : context.route)) %} + {% set preview_link = context.routable ? ' ' : '' %} {% endif %} {% block titlebar %}
{% if mode == 'list' %} - {{ "PLUGIN_ADMIN.BACK"|tu }} + {{ "PLUGIN_ADMIN.BACK"|tu }} {% for key, add_modal in config.plugins.admin.add_modals %} {% if add_modal.show_in|defined('bar') == 'bar' %} @@ -168,7 +172,7 @@
{% if admin.languages_enabled|length > 1 %} @@ -178,7 +182,7 @@