From 9c92ce530577531214f940ec4cab7fa57e2c23ec Mon Sep 17 00:00:00 2001 From: Ryan Cramer Date: Thu, 16 Mar 2017 09:49:47 -0400 Subject: [PATCH] Updates for issue processwire/processwire-issues#215 to better support locale settings on front-end, plus add $languages->setLocale() and $languages->getLocale() methods, and freshen up code and docs in related classes. --- wire/core/Selectors.php | 3 +- .../LanguageSupport/LanguageSupport.module | 4 +- .../LanguageSupportPageNames.module | 83 ++++++++++- wire/modules/LanguageSupport/Languages.php | 129 +++++++++++++++++- 4 files changed, 205 insertions(+), 14 deletions(-) diff --git a/wire/core/Selectors.php b/wire/core/Selectors.php index 49a5957d..4689b19c 100644 --- a/wire/core/Selectors.php +++ b/wire/core/Selectors.php @@ -391,6 +391,7 @@ class Selectors extends WireArray { $operator = $op; $not = true; } else { + if(is_array($value)) $value = implode('|', $value); $debug = $this->wire('config')->debug ? "field='$field', value='$value', selector: '$this->selectorStr'" : ""; throw new WireException("Unknown Selector operator: '$operator' -- was your selector value properly escaped? $debug"); } @@ -434,7 +435,7 @@ class Selectors extends WireArray { } } - if($field || strlen("$value")) { + if($field || $value || strlen("$value")) { $selector = $this->create($field, $operator, $value); if(!is_null($group)) $selector->group = $group; if($quote) $selector->quote = $quote; diff --git a/wire/modules/LanguageSupport/LanguageSupport.module b/wire/modules/LanguageSupport/LanguageSupport.module index 99d9f87d..157a6880 100644 --- a/wire/modules/LanguageSupport/LanguageSupport.module +++ b/wire/modules/LanguageSupport/LanguageSupport.module @@ -182,8 +182,8 @@ class LanguageSupport extends WireData implements Module, ConfigurableModule { } $this->wire('config')->dateFormat = $this->_('Y-m-d H:i:s'); // Sortable date format used in the admin - $locale = $this->_('C'); // Value to pass to PHP's setlocale(LC_ALL, 'value') function when initializing this language // Default is 'C'. Specify '0' to skip the setlocale() call (and carry on system default). - if($locale != '0') setlocale(LC_ALL, $locale); + $locale = $this->_('C'); // Value to pass to PHP's setlocale(LC_ALL, 'value') function when initializing this language // Default is 'C'. Specify '0' to skip the setlocale() call (and carry on system default). Specify CSV string of locales to try multiple locales in order. + if($locale != '0') $languages->setLocale(LC_ALL, $locale); // setup our hooks handled by this class $this->addHookBefore('Inputfield::render', $this, 'hookInputfieldBeforeRender'); diff --git a/wire/modules/LanguageSupport/LanguageSupportPageNames.module b/wire/modules/LanguageSupport/LanguageSupportPageNames.module index c3df79be..27ddb401 100644 --- a/wire/modules/LanguageSupport/LanguageSupportPageNames.module +++ b/wire/modules/LanguageSupport/LanguageSupportPageNames.module @@ -3,8 +3,12 @@ /** * Multi-language support page names module * - * ProcessWire 3.x, Copyright 2016 by Ryan Cramer + * ProcessWire 3.x, Copyright 2017 by Ryan Cramer * https://processwire.com + * + * @property int $moduleVersion + * @property int $inheritInactive + * @property int $useHomeSegment * */ @@ -129,7 +133,10 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM // verify that page path doesn't have mixed languages where it shouldn't $redirectURL = $this->verifyPath($this->requestPath); - if($redirectURL) return $this->wire('session')->redirect($redirectURL); + if($redirectURL) { + $this->wire('session')->redirect($redirectURL); + return; + } $page = $this->wire('page'); @@ -178,6 +185,9 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM * Given a page path, return an updated version that lacks the language segment * * It extracts the language segment and uses that to later set the language + * + * @param string $path + * @return string * */ public function updatePath($path) { @@ -269,7 +279,8 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM // set the language if(!$setLanguage) $setLanguage = $languages->get('default'); $user->language = $setLanguage; - $this->setLanguage = $setLanguage; + $this->setLanguage = $setLanguage; + $languages->setLocale(); // if $page is the 404 page, exit out now if($page->id == $config->http404PageID) return ''; @@ -360,10 +371,14 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM /** * Hook in before ProcesssPageView::execute to capture and modify $_GET[it] as needed + * + * @param HookEvent $event * */ public function hookProcessPageViewExecute(HookEvent $event) { - $event->object->setDelayRedirects(true); + /** @var ProcessPageView $process */ + $process = $event->object; + $process->setDelayRedirects(true); // save now, since ProcessPageView removes $_GET['it'] when it executes $it = isset($_GET['it']) ? $_GET['it'] : ''; $this->requestPath = $it; @@ -377,9 +392,13 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM /** * Hook in before ProcesssPageView::render to throw 404 when appropriate + * + * @param HookEvent $event + * @throws WireException * */ public function hookPageRender(HookEvent $event) { + if($event) {} if($this->force404) { $this->force404 = false; // prevent another 404 on the 404 page throw new Wire404Exception(); @@ -390,12 +409,16 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM * Hook in after ProcesssPageView::viewable account for specific language versions * * May be passed a Language name or page to check viewable for that language + * + * @param HookEvent $event * */ public function hookPageViewable(HookEvent $event) { if(!$event->return) return; + /** @var Page $page */ $page = $event->object; // if(wire('user')->isSuperuser() || $page->editable()) return; + /** @var Language $language */ $language = $event->arguments(0); if(!$language) return; if(is_string($language)) $language = $this->wire('languages')->get($this->wire('sanitizer')->pageNameUTF8($language)); @@ -407,10 +430,14 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM /** * Hook into WirePageEditor (i.e. ProcessPageEdit) to remove the non-applicable default home name of 'home' + * + * @param HookEvent $event * */ public function hookWirePageEditorExecute(HookEvent $event) { - $page = $event->object->getPage(); + /** @var WirePageEditor $editor */ + $editor = $event->object; + $page = $editor->getPage(); if($page && $page->id == 1) { if($page->name == Pages::defaultRootName) $page->name = ''; } @@ -420,6 +447,8 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM * Hook into the page name render for when in ProcessPageEdit * * Adds additional inputs for each language + * + * @param HookEvent $event * */ public function hookInputfieldPageNameRenderAfter(HookEvent $event) { @@ -489,13 +518,19 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM * Process the input data from hookInputfieldPageNameRender * * @todo Just move this to the InputfieldPageName module rather than using hooks + * + * @param HookEvent $event * */ public function hookInputfieldPageNameProcess(HookEvent $event) { + /** @var InputfieldPageName $inputfield */ $inputfield = $event->object; //$page = $this->process == 'ProcessPageEdit' ? $this->process->getPage() : new NullPage(); - $page = $this->process->getPage(); + /** @var WirePageEditor $process */ + $process = $this->process; + /** @var Page $page */ + $page = $process instanceof WirePageEditor ? $process->getPage() : new NullPage(); if($page->id && !$page->editable('name', false)) return; // name is not editable $input = $event->arguments[0]; /** @var Languages $languages */ @@ -533,10 +568,13 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM /** * Hook into PageFinder::getQuery to add language status check + * + * @param HookEvent $event * */ public function hookPageFinderGetQuery(HookEvent $event) { $query = $event->return; + /** @var PageFinder $pageFinder */ $pageFinder = $event->object; $options = $pageFinder->getOptions(); @@ -555,9 +593,12 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM /** * Hook into Page::path to localize path for current language + * + * @param HookEvent $event * */ public function hookPagePath(HookEvent $event) { + /** @var Page $page */ $page = $event->object; if($page->template == 'admin') return; $language = $this->wire('user')->get('language'); @@ -570,6 +611,8 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM * * event param Language|string|int Optional language * event return string Localized language name or blank if not set + * + * @param HookEvent $event * */ public function hookPageLocalName(HookEvent $event) { @@ -586,9 +629,12 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM * * event param Language|string|int Optional language * event return string Localized language path + * + * @param HookEvent $event * */ public function hookPageLocalPath(HookEvent $event) { + /** @var Page $page */ $page = $event->object; $language = $this->getLanguage($event->arguments(0)); $event->return = $this->getPagePath($page, $language); @@ -599,9 +645,12 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM * * event param Language|string|int Optional language * event return string Localized language URL + * + * @param HookEvent $event * */ public function hookPageLocalUrl(HookEvent $event) { + /** @var Page $page */ $page = $event->object; $language = $this->getLanguage($event->arguments(0)); $event->return = $this->wire('config')->urls->root . ltrim($this->getPagePath($page, $language), '/'); @@ -612,6 +661,8 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM * * event param Language|string|int Optional language * event return string Localized language name or blank if not set + * + * @param HookEvent $event * */ public function hookPageLocalHttpUrl(HookEvent $event) { @@ -649,6 +700,8 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM /** * Update pages table for new column when a language is added + * + * @param Language|Page $language * */ public function languageAdded(Page $language) { @@ -668,6 +721,8 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM /** * Hook called when language is added + * + * @param HookEvent $event * */ public function hookLanguageAdded(HookEvent $event) { @@ -677,6 +732,8 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM /** * Update pages table to remove column when a language is deleted + * + * @param Language|Page $language * */ protected function languageDeleted(Page $language) { @@ -695,6 +752,8 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM /** * Hook called when language is deleted + * + * @param HookEvent $event * */ public function hookLanguageDeleted(HookEvent $event) { @@ -707,6 +766,8 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM * * Here we make use of the 'extraData' return property of the saveReady hook * to bundle in the language name fields into the query. + * + * @param HookEvent $event * */ public function hookPageSaveReady(HookEvent $event) { @@ -781,7 +842,8 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM * */ public function hookPageSetupNew(HookEvent $event) { - + + /** @var Page $page */ $page = $event->arguments[0]; // if page already has a name, then no need to continue @@ -821,6 +883,8 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM /** * Hook called immediately after a page is saved * + * @param HookEvent $event + * */ public function hookPageSaved(HookEvent $event) { // The setLanguage may get lost upon some page save events, so this restores that @@ -925,6 +989,8 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM /** * Check to make sure that the status table exists and creates it if not + * + * @param bool $force * */ public function checkModuleVersion($force = false) { @@ -961,6 +1027,9 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM /** * Module interactive configuration fields + * + * @param array $data + * @return InputfieldWrapper * */ public function getModuleConfigInputfields(array $data) { diff --git a/wire/modules/LanguageSupport/Languages.php b/wire/modules/LanguageSupport/Languages.php index 039fb239..b84c2b89 100644 --- a/wire/modules/LanguageSupport/Languages.php +++ b/wire/modules/LanguageSupport/Languages.php @@ -29,6 +29,7 @@ * @method added(Page $language) Hook called when Language is added #pw-hooker * @method deleted(Page $language) Hook called when Language is deleted #pw-hooker * @method updated(Page $language, $what) Hook called when Language is added or deleted #pw-hooker + * @method languageChanged($fromLanguage, $toLanguage) Hook called when User language is changed #pw-hooker * */ @@ -36,6 +37,8 @@ class Languages extends PagesType { /** * Reference to LanguageTranslator instance + * + * @var LanguageTranslator * */ protected $translator = null; @@ -93,7 +96,15 @@ class Languages extends PagesType { * */ protected $editableCache = array(); - + + /** + * Construct + * + * @param ProcessWire $wire + * @param array $templates + * @param array $parents + * + */ public function __construct(ProcessWire $wire, $templates = array(), $parents = array()) { parent::__construct($wire, $templates, $parents); $this->wire('database')->addHookAfter('unknownColumnError', $this, 'hookUnknownColumnError'); @@ -107,9 +118,15 @@ class Languages extends PagesType { * */ public function translator(Language $language) { - if(is_null($this->translator)) $this->translator = $this->wire(new LanguageTranslator($language)); - else $this->translator->setCurrentLanguage($language); - return $this->translator; + /** @var LanguageTranslator $translator */ + $translator = $this->translator; + if(is_null($translator)) { + $translator = $this->wire(new LanguageTranslator($language)); + $this->translator = $translator; + } else { + $translator->setCurrentLanguage($language); + } + return $translator; } /** @@ -348,6 +365,110 @@ class Languages extends PagesType { $user->language = $this->savedLanguage2; return true; } + + /** + * Set the current locale + * + * This function behaves exactly the same way as [PHP setlocale](http://php.net/manual/en/function.setlocale.php) except + * for the following: + * + * - If the $locale argument is omitted, it uses the locale setting translated for the current user language. + * - You can optionally specify a CSV string of locales to try for the $locale argument. + * - You can optionally or a “category=locale;category=locale;category=locale” string for the $locale argument. + * When this type of string is used, the $category argument is ignored. + * - This method does not accept more than the 2 indicated arguments. + * + * See the PHP setlocale link above for a list of constants that can be used for the `$category` argument. + * + * ~~~~~ + * // Set locale to whatever settings defined for current $user language + * $languages->setLocale(); + * + * // Set all locale categories + * $languages->setLocale(LC_ALL, 'en_US.UTF-8'); + * + * // Set locale for specific category (CTYPE) + * $langauges->setLocale(LC_CTYPE, 'en_US.UTF-8'); + * + * // Try multiple locales till one works (in order) using array + * $languages->setLocale(LC_ALL, [ 'en_US.UTF-8', 'en_US', 'en' ]); + * + * // Same as above, except using CSV string + * $languages->setLocale(LC_ALL, 'en_US.UTF-8, en_US, en'); + * + * // Set multiple categories and locales (first argument ignored) + * $languages->setLocale(null, 'LC_CTYPE=en_US;LC_NUMERIC=de_DE;LC_TIME=es_ES'); + * ~~~~~ + * + * @param int $category Specify a PHP “LC_” constant or omit (or null) for default (LC_ALL). + * @param string|array|null $locale Specify string, array or CSV string of locale name(s), or omit (null) for default language locale. + * @return string|bool Returns the locale that was set or boolean false if requested locale cannot be set. + * @see Languages::getLocale() + * + */ + public function setLocale($category = LC_ALL, $locale = null) { + + $setLocale = ''; // return value + if($category === null) $category = LC_ALL; + + if($locale === null) { + // argument omitted means set according to language settings + $locale = __('C', 'wire--modules--languagesupport--languagesupport-module'); + } + + if(is_string($locale)) { + + if(strpos($locale, ',') !== false) { + // convert CSV string to array of locales + $locale = explode(',', $locale); + foreach($locale as $key => $value) { + $locale[$key] = trim($value); + } + + } else if(strpos($locale, ';') !== false) { + // multi-category and locale string, i.e. LC_CTYPE=en_US.UTF-8;LC_NUMERIC=C;LC_TIME=C + foreach(explode(';', $locale) as $s) { + // call setLocale() for each locale item present in the string + if(strpos($s, '=') === false) continue; + list($cats, $loc) = explode('=', $s); + $cat = constant($cats); + if($cat !== null) { + $loc = $this->setLocale($cat, $loc); + if($loc !== false) $setLocale .= trim($cats) . '=' . trim($loc) . ";"; + } + } + $setLocale = rtrim($setLocale, ';'); + if(empty($setLocale)) $setLocale = false; + } + } + + if($setLocale === '') { + if($locale === '0' || $locale === 0) { + // get locale (to be consistent with behavior of PHP setlocale) + $setLocale = $this->getLocale($category); + } else { + // set the locale + $setLocale = setlocale($category, $locale); + } + } + + return $setLocale; + } + + /** + * Return the current locale setting + * + * If using LC_ALL category and locales change by category, the returned string will be in + * the format: “category=locale;category=locale”, and so on. + * + * @param int $category Optionally specify a PHP LC constant (default=LC_ALL) + * @return string|bool Locale(s) string or boolean false if not supported by the system. + * @see Languages::setLocale() + * + */ + public function getLocale($category = LC_ALL) { + return setlocale($category, '0'); + } /** * Hook called when a language is deleted