'Languages Support - Fields', 'version' => 101, 'summary' => 'Required to use multi-language fields.', 'author' => 'Ryan Cramer', 'autoload' => true, 'singular' => true, 'requires' => array( 'LanguageSupport' ), 'installs' => array( 'FieldtypePageTitleLanguage', 'FieldtypeTextareaLanguage', 'FieldtypeTextLanguage', ) ); } /** * Cached names of fields that are dealing in multiple languages. * */ protected $multilangFields = array(); /** * Cached names of fields that have alternate language-specific versions * * Indexed by original field name and value is an array of the language-alternate versions * */ protected $multilangAlternateFields = array(); /** * Construct and set our dynamic config vars * */ public function __construct() { // load other required classes $dirname = dirname(__FILE__); require_once($dirname . '/LanguagesValueInterface.php'); require_once($dirname . '/FieldtypeLanguageInterface.php'); require_once($dirname . '/LanguagesPageFieldValue.php'); } public function wired() { $this->addHookAfter('FieldtypeLanguageInterface::loadPageField', $this, 'fieldtypeLoadPageField'); $this->addHookAfter('FieldtypeLanguageInterface::wakeupValue', $this, 'fieldtypeWakeupValue'); $this->addHookAfter('FieldtypeLanguageInterface::getConfigInputfields', $this, 'fieldtypeGetConfigInputfields'); parent::wired(); } public function init() { // intentionally left blank } /** * Initialize the language support API vars * */ public function LS_init() { $fields = $this->wire()->fields; $this->addHookBefore('FieldtypeLanguageInterface::sleepValue', $this, 'fieldtypeSleepValue'); $this->addHookBefore('PageFinder::getQuery', $this, 'pageFinderGetQuery'); $this->addHookBefore('Fieldtype::formatValue', $this, 'hookFieldtypeFormatValue'); $languageNames = array(); $fieldNames = $fields->getAllNames('name'); foreach($this->wire()->languages as $language) { $languageNames[] = $language->name; } // keep track of which fields are multilanguage $this->multilangFields = $fields->findByType('FieldtypeLanguageInterface', array( 'inherit' => true, 'valueType' => 'name', 'indexType' => '', )); // determine which fields have language alternates, i.e. 'title_es' is an alternate for 'title' foreach($fieldNames as $fieldName) { foreach($languageNames as $languageName) { $altName = $fieldName . '_' . $languageName; if(isset($fieldNames[$altName])) { if(!isset($this->multilangAlternateFields[$fieldName])) $this->multilangAlternateFields[$fieldName] = array(); $this->multilangAlternateFields[$fieldName][] = $altName; } } } } /** * Called by ProcessWire when the API and known $page is ready * */ public function LS_ready() { $languages = $this->wire()->languages; $languages->addHook('added', $this, 'hookLanguageAdded'); $languages->addHook('deleted', $this, 'hookLanguageDeleted'); } /** * Hook into FieldtypeText::formatValue * * Replace a value of one field with the value from another field that has the same name, but with the language name appended to it. * Example: title and title_es * * @param HookEvent $event * */ public function hookFieldtypeFormatValue(HookEvent $event) { /** @var Field $field */ $field = $event->arguments[1]; if($field->name === 'language') return; $language = $this->wire()->user->get('language'); if(!$language || !$language->id || $language->isDefault()) return; // exit quickly if we can determine now we don't need to continue if(!isset($this->multilangAlternateFields[$field->name])) return; /** @var Page $page */ $page = $event->arguments[0]; // determine name of language field, if present. // note that if the language name contains dashes or dots (- or .) the field name should contain underscores there instead $newName = $field->name . '_' . str_replace(array('-', '.'), '_', $language->name); $newField = $page->fieldgroup->getField($newName); if(!$newField) return; // unformatted so nothing can modify it first $value = $page->getUnformatted($newName); // if the page doesn't have a populated language-specific field, then exit // this will make it fallback to the default language value if($field->type->isEmptyValue($field, $value)) return; if(is_object($value)) { if($value instanceof WireArray && !$value->count()) return; if($value instanceof NullPage) return; } // we have a new field: swap $field with $newField in the arguments $newValue = $page->get($newName); // get formatted version $arguments = $event->arguments; $arguments[1] = $newField; $arguments[2] = $newValue; $event->arguments = $arguments; } /** * Hook called when new language added * * @param HookEvent $event * */ public function hookLanguageAdded(HookEvent $event) { $fields = $this->wire()->fields; $language = $event->arguments[0]; if($language->template->name != LanguageSupport::languageTemplateName) return; foreach($this->multilangFields as $name) { $field = $fields->get($name); if($field) $this->fieldLanguageAdded($field, $language); } $this->languageAdded($language); } /** * Hookable function called when a new language is added * * @param Page|Language $language * */ public function ___languageAdded(Page $language) { // hookable, intentionally blank } /** * Hook called when languag is deleted * * @param HookEvent $event * */ public function hookLanguageDeleted(HookEvent $event) { $fields = $this->wire()->fields; $language = $event->arguments[0]; if($language->template->name != LanguageSupport::languageTemplateName) return; foreach($this->multilangFields as $name) { $field = $fields->get($name); if($field) $this->fieldLanguageRemoved($field, $language); } $this->languageDeleted($language); } /** * Hookable function called when a language is deleted * * @param Page|Language $language * */ public function ___languageDeleted(Page $language) { // hookable, intentionally blank } /** * Called when a new language is added to the system for each field that implements FieldtypeLanguageInterface * * @param Field $field * @param Page|Language $language * */ public function ___fieldLanguageAdded(Field $field, Page $language) { if($language->isDefault) return; if(!($field->type instanceof FieldtypeLanguageInterface)) return; $schema = $field->type->getDatabaseSchema($field); $database = $this->wire()->database; $table = $database->escapeTable($field->table); foreach($schema as $name => $value) { if(!preg_match('/[^\d]+' . $language->id . '$/', $name)) continue; // field in schema ends with the language ID try { $database->exec("ALTER TABLE `{$table}` ADD `$name` $value"); } catch(\Exception $e) { $this->error($e->getMessage(), Notice::log); } } foreach($schema['keys'] as $name => $value) { if(!preg_match('/[^\d]+' . $language->id . '$/', $name)) continue; // index in schema ends with the language ID try { $database->exec("ALTER TABLE `{$table}` ADD $value"); } catch(\Exception $e) { $this->error($e->getMessage(), Notice::log); } } } /** * Called when a language is removed from the system for each field that implements FieldtypeLanguageInterface * * @param Field $field * @param Page|Language $language * */ protected function ___fieldLanguageRemoved(Field $field, Page $language) { if($language->isDefault) return; if(!($field->type instanceof FieldtypeLanguageInterface)) return; $schema = $field->type->getDatabaseSchema($field); $database = $this->wire()->database; $table = $database->escapeTable($field->table); foreach($schema as $name => $value) { if(!preg_match('/[^\d]+' . $language->id . '$/', $name)) continue; try { $database->exec("ALTER TABLE `{$table}` DROP COLUMN `$name`"); } catch(\Exception $e) { // just catch, no need for fatal errors here } } } /** * Hook into PageFinder::getQuery * * Adjusts the selectors passed to the query so that find operations search in user's native language version, as well as the default version. * * We may make this behavior configurable later on, as one may want to limit the search to 1 language only. * * @param HookEvent $event * */ public function pageFinderGetQuery(HookEvent $event) { $user = $this->wire()->user; $language = $user->language; $database = $this->wire()->database; if(!$language || !$language->id || $language->isDefault()) return; $arguments = $event->arguments; $selectors = $arguments[0]; foreach($selectors as $selector) { $changed = false; $fields = $selector->field; $fields = is_array($fields) ? $fields : array($fields); foreach($fields as $key => $field) { $subfield = ''; if(strpos($field, '.')) list($field, $subfield) = explode('.', $field); $field = $database->escapeCol($field); $subfield = $database->escapeCol($subfield); if(isset($this->multilangAlternateFields[$field])) { // account for multilang alternates like 'title_es' for 'title' $altName = $field . '_' . $database->escapeCol($user->language->name); if(in_array($altName, $this->multilangAlternateFields[$field])) { if($subfield) $altName .= ".$subfield"; array_unshift($fields, $altName); $changed = true; } } // next we account for actual multilang fields if(!in_array($field, $this->multilangFields)) continue; if(!$subfield) $subfield = 'data'; if($subfield === 'data') { array_unshift($fields, "$field.$subfield" . (int) $user->language->id); $changed = true; } } if($changed) $selector->field = $fields; } $arguments[0] = $selectors; $event->arguments = $arguments; } /** * Hook into FieldtypeLanguageInterface::loadPageField * * Converts the value to a LanguagesPageFieldValue * * @param HookEvent $event * */ public function fieldtypeLoadPageField(HookEvent $event) { $page = $event->arguments[0]; $field = $event->arguments[1]; $value = $event->return; if($value instanceof LanguagesPageFieldValue) return; $v = new LanguagesPageFieldValue($page, $field, $value); $this->wire($v); $event->return = $v; } /** * Hook into FieldtypeLanguageInterface::wakeupValue * * Converts the value to a LanguagesPageFieldValue * * @param HookEvent $event * */ public function fieldtypeWakeupValue(HookEvent $event) { $page = $event->arguments[0]; $field = $event->arguments[1]; $value = $event->return; if($value instanceof LanguagesPageFieldValue) { $value->setTrackChanges(true); $value->setField($field); // good } else if(is_array($value)) { $value = new LanguagesPageFieldValue($page, $field, $value); $this->wire($value); $value->setTrackChanges(true); $value->setField($field); $event->return = $value; } } /** * Hook into FieldtypeLanguageInterface::sleepValue * * Converts a LanguagesPageFieldValue to an array * * @param HookEvent $event * */ public function fieldtypeSleepValue(HookEvent $event) { // $page = $event->arguments[0]; // $field = $event->arguments[1]; $value = $event->arguments[2]; //$value = $event->return; $values = array(); if(!$value instanceof LanguagesPageFieldValue) return; foreach($this->wire()->languages as $language) { if($language->isDefault()) $key = 'data'; else $key = 'data' . $language->id; $values[$key] = $value->getLanguageValue($language->id); } /* if(!strlen($values['data'])) foreach($values as $k => $v) { // prevent the possibility of the default language having // a blank value while some other language has a populated value if(!strlen($v)) continue; $values['data'] = $v; break; } */ // ensure that sleepValue is getting an array $event->setArgument(2, $values); // $event->return = $values; } public function fieldtypeGetConfigInputfields(HookEvent $event) { /** @var Field $field */ $field = $event->arguments(0); /** @var InputfieldWrapper $inputfields */ $inputfields = $event->return; /** @var InputfieldRadios $f */ $f = $this->wire()->modules->get('InputfieldRadios'); $f->attr('name', 'langBlankInherit'); $f->label = $this->_('Language Support / Blank Behavior'); $f->description = $this->_("What should happen when this field's value is blank?"); $f->notes = $this->_('Applies only to non-default language values on the front-end of your site.'); $f->addOption(LanguagesPageFieldValue::langBlankInheritDefault, $this->_('Inherit value from default language')); $f->addOption(LanguagesPageFieldValue::langBlankInheritNone, $this->_('Remain blank')); $f->attr('value', (int) $field->get('langBlankInherit')); $f->collapsed = Inputfield::collapsedBlank; $inputfields->add($f); } /** * Given a field name, return an array of alternate language field names * * Returns a blank array if none found * * @param string $fieldName * @return array * */ public function getAlternateFields($fieldName) { if(isset($this->multilangAlternateFields[$fieldName])) { return $this->multilangAlternateFields[$fieldName]; } return array(); } /** * Given an alternate field name, return the parent (default-language) version of it * * @param string $altFieldName * @param bool $returnLanguage Specify true if you want this function to return the language rather than the parent field name. * @return string|Language Returns blank string if none found * */ public function getAlternateFieldParent($altFieldName, $returnLanguage = false) { $pos = strrpos($altFieldName, '_'); if(!$pos) return ''; $parentName = substr($altFieldName, 0, $pos); if(!isset($this->multilangAlternateFields[$parentName])) return ''; if(!in_array($altFieldName, $this->multilangAlternateFields[$parentName])) return ''; if(!$returnLanguage) return $parentName; $languageName = substr($altFieldName, $pos+1); return $this->wire()->languages->get($languageName); } /** * Get the language associated with the alternate field name * * @param string $altFieldName * @return Page|false Language page associated with the field, or blank string or false if not found * */ public function getAlternateFieldLanguage($altFieldName) { return $this->getAlternateFieldParent($altFieldName, true); } /** * Get multi-language field names (fields that implement FieldtypeLanguageInterface) * * @return array * @since 3.0.194 * */ public function getMultilangFieldNames() { return $this->multilangFields; } /** * Is the given field name a language alternate field? * * If it is, the Language of the field is returned. * If it isn't, then boolean false is returned. * * This method also accounts for default language. * * @param string $name * @return bool|Language * */ public function isAlternateField($name) { $name = (string) $name; if(isset($this->multilangAlternateFields[$name])) { // default language for an alternate field set return $this->wire()->languages->getDefault(); } if(!strpos($name, '_')) return false; $language = $this->getAlternateFieldParent($name, true); if($language && $language->id) return $language; return false; } /** * Install the module * */ public function ___install() { $this->modules->get('FieldtypeTextLanguage'); } /** * Uninstall the module * */ public function ___uninstall() { // first check if there are any fields using the LanguageInterface $errors = ''; foreach($this->wire()->fields as $field) { if($field->type instanceof FieldtypeLanguageInterface) $errors .= $field->name . ", "; } if($errors) { throw new WireException("Can't uninstall because these fields use the language interface: " . rtrim($errors, ", ")); } } }