'Languages Support',
'version' => 103,
'summary' => 'ProcessWire multi-language support.',
'author' => 'Ryan Cramer',
'autoload' => true,
'singular' => true,
'installs' => array(
'ProcessLanguage',
'ProcessLanguageTranslator',
)
);
}
/**
* Name of template used for language pages
*
*/
const languageTemplateName = 'language';
/**
* Name of field used to store the language page ref
*
*/
const languageFieldName = 'language';
/**
* This module can possibly be init'd before PW's Modules class fully loads, so we keep this to prevent double initialization
*
*/
protected $initialized = false;
/**
* Reference to the default language page
*
*/
protected $defaultLanguagePage = null;
/**
* Array of pages that were cached before this module was loaded.
*
*/
protected $earlyCachedPages = array();
/**
* Instanceof LanguageSupportFields, if installed
*
*/
protected $LanguageSupportFields = null;
/**
* Instanceof LanguageTabs, if installed
*
*/
protected $languageTabs = null;
/**
* Construct and set our dynamic config vars
*
*/
public function __construct() {
$this->set('initialized', false);
// load other required classes
$dirname = dirname(__FILE__);
require_once($dirname . '/FieldtypeLanguageInterface.php');
require_once($dirname . '/Language.php');
require_once($dirname . '/Languages.php');
require_once($dirname . '/LanguageTranslator.php');
require_once($dirname . '/LanguagesValueInterface.php');
require_once($dirname . '/LanguagesPageFieldValue.php');
// set our config var placeholders
$this->set('languagesPageID', 0);
$this->set('defaultLanguagePageID', 0);
$this->set('languageTranslatorPageID', 0);
// quick reference to non-default language IDs, for when needed before languages loaded
$this->set('otherLanguagePageIDs', array());
}
/**
* Initialize the language support API vars
*
*/
public function init() {
// document which pages were already cached at this point, as their values may need
// to be reloaded to account for language fields.
foreach(wire('pages')->getCache() as $id => $value) $this->earlyCachedPages[$id] = $value;
// prevent possible double init
if($this->initialized) return;
$this->initialized = true;
FieldtypePageTitle::$languageSupport = true;
$defaultLanguagePageID = $this->defaultLanguagePageID;
// create the $languages API var
$languageTemplate = $this->templates->get('language');
if(!$languageTemplate) return;
$languages = new Languages($languageTemplate, $this->languagesPageID);
$_default = null; // just in case
// ensure all languages are loaded and get instantiated versions of system/default languages
foreach($languages as $language) {
if($language->id == $defaultLanguagePageID) {
$this->defaultLanguagePage = $language;
} else if($language->name == 'default') {
$_default = $language; // backup plan
}
}
if(!$this->defaultLanguagePage) {
if($_default) $this->defaultLanguagePage = $_default;
else $this->defaultLanguagePage = $languages->getAll()->first();
}
$this->defaultLanguagePage->setIsDefaultLanguage();
$languages->setDefault($this->defaultLanguagePage);
Wire::setFuel('languages', $languages);
// identify the current language from the user, or set one if it's not already
if($this->user->language && $this->user->language->id) {
$language = $this->user->language;
} else {
$language = $this->defaultLanguagePage;
$this->user->language = $language;
}
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);
// setup our hooks handled by this class
$this->addHookBefore('Inputfield::render', $this, 'hookInputfieldBeforeRender');
$this->addHookAfter('Inputfield::render', $this, 'hookInputfieldAfterRender');
$this->addHookAfter('Inputfield::processInput', $this, 'hookInputfieldAfterProcessInput');
$this->addHookBefore('Inputfield::processInput', $this, 'hookInputfieldBeforeProcessInput');
$this->addHookAfter('Field::getInputfield', $this, 'hookFieldGetInputfield');
$this->pages->addHook('added', $this, 'hookPageAdded');
$this->pages->addHook('deleteReady', $this, 'hookPageDeleteReady');
$this->addHook('Page::setLanguageValue', $this, 'hookPageSetLanguageValue');
$this->addHook('Page::getLanguageValue', $this, 'hookPageGetLanguageValue');
if($this->wire('modules')->isInstalled('LanguageSupportFields')) {
$this->LanguageSupportFields = wire('modules')->get('LanguageSupportFields');
$this->LanguageSupportFields->LS_init();
}
}
/**
* Called by ProcessWire when API is fully ready with known $page
*
*/
public function ready() {
// styles used by our Inputfield hooks
if($this->wire('page')->template == 'admin') {
$this->config->styles->add($this->config->urls->LanguageSupport . "LanguageSupport.css");
$language = $this->wire('user')->language;
$this->config->js('LanguageSupport', array(
'language' => array(
'id' => $language->id,
'name' => $language->name,
'title' => (string) $language->title,
)
));
if($this->wire('modules')->isInstalled('LanguageTabs')) {
$this->languageTabs = $this->wire('modules')->get('LanguageTabs');
}
}
// if languageSupportFields is here, then we have to deal with pages that loaded before this module did
if($this->LanguageSupportFields) {
$fieldNames = array();
// save the names of all fields that support languages
foreach(wire('fields') as $field) {
if($field->type instanceof FieldtypeLanguageInterface) $fieldNames[] = $field->name;
}
// unset the values from all the early cached pages since they didn't recognize languages
// this will force them to reload when accessed
foreach($this->earlyCachedPages as $id => $p) {
$t = $p->trackChanges();
if($t) $p->setTrackChanges(false);
foreach($fieldNames as $name) unset($p->$name);
if($t) $p->setTrackChanges(true);
}
}
// release this as we don't need it anymore
$this->earlyCachedPages = array();
if($this->LanguageSupportFields) $this->LanguageSupportFields->LS_ready();
}
/**
* Hook before Inputfield::render to set proper default language value
*
* Only applies to Inputfields that have: useLanguages == true
*
*/
public function hookInputfieldBeforeRender(HookEvent $event) {
$inputfield = $event->object;
if(!$inputfield->useLanguages) return;
$userLanguage = $this->wire('user')->language;
if(!$userLanguage) return;
// set 'value' attribute to default language values
if($userLanguage->id !== $this->defaultLanguagePageID) {
$t = $inputfield->trackChanges();
if($t) $inputfield->setTrackChanges(false);
$inputfield->attr('value', $inputfield->get('value' . $this->defaultLanguagePageID));
if($t) $inputfield->setTrackChanges(true);
}
}
/**
* Wrap the inputfield output with a language name label
*
* @param string $out Existing inputfield output
* @param string $id ID attribute to use
* @param Language $language
* @return string
*
*/
protected function wrapInputfieldOutput($out, $id, Language $language) {
$label = (string) $language->title;
if(!strlen($label)) $label = $language->name;
$out = "\n
" .
"\n" . $out .
"\n
";
return $out;
}
/**
* Hook into Inputfield::render to duplicate inputs for other languages
*
* Only applies to Inputfields that have: useLanguages == true
*
*/
public function hookInputfieldAfterRender(HookEvent $event) {
static $numLanguages = null;
$inputfield = $event->object;
$name = $inputfield->attr('name');
$languages = $this->wire('languages');
if(is_null($numLanguages)) $numLanguages = $languages->count();
// provide an automatic translation for some system/default fields if they've not been overridden in the fields editor
if($name == 'language' && $inputfield->label == 'Language') $inputfield->label = $this->_('Language'); // Label for 'language' field in user profile
else if($name == 'email' && $inputfield->label == 'E-Mail Address') $inputfield->label = $this->_('E-Mail Address'); // Label for 'email' field in user profile
else if($name == 'title' && $inputfield->label == 'Title') $inputfield->label = $this->_('Title'); // Label for 'title' field used throughout ProcessWire
// check if this is a language alternate field (i.e. title_es or title)
if($this->LanguageSupportFields && strpos($name, '_') && wire('fields')->get($name)) {
$language = $this->LanguageSupportFields->getAlternateFieldLanguage($name);
if($language && $language->id) {
$event->return = $this->wrapInputfieldOutput($event->return, $inputfield->attr('id'), $language);
return;
}
}
// if inputfield doesn't have a 'useLanguages' var set in it, then we're done, abort
if(!$inputfield->useLanguages || $numLanguages < 2) return;
// keep originals to restore later (including $name, which we already got above)
$id = $inputfield->attr('id');
$value = $inputfield->attr('value');
$required = $inputfield->required;
$trackChanges = $inputfield->trackChanges();
$inputfield->setTrackChanges(false);
if($this->languageTabs) $this->languageTabs->resetTabs();
$out = '';
foreach($languages as $language) {
$languageID = (int) $language->id;
if($language->isDefault) {
// default language
$newID = $id;
$o = $event->return;
} else {
// non-default language
$newID = $id . "__$languageID";
$newName = $name . "__$languageID";
$inputfield->attr('id', $newID);
$inputfield->attr('name', $newName);
$valueAttr = "value$languageID";
$inputfield->required = false;
$inputfield->setAttribute('value', $inputfield->$valueAttr);
$o = $inputfield->___render();
}
$out .= $this->wrapInputfieldOutput($o, $newID, $language);
if($this->languageTabs) $this->languageTabs->addTab($inputfield, $language);
}
$inputfield->setAttribute('name', $name);
$inputfield->setAttribute('id', $id);
$inputfield->setAttribute('value', $value);
$inputfield->required = $required;
$inputfield->setTrackChanges($trackChanges);
if($this->languageTabs) {
$out = $this->languageTabs->renderTabs($inputfield, $out);
}
$event->return = $out;
}
/**
* Hook before Inputfield::processInput to process input for other languages
*
* Only applies to Inputfields that have: useLanguages == true
*
*/
public function hookInputfieldBeforeProcessInput(HookEvent $event) {
// ensures default language values are populated
$this->hookInputfieldBeforeRender($event);
}
/**
* Hook into Inputfield::processInput to process input for other languages
*
* Only applies to Inputfields that have: useLanguages == true
*
*/
public function hookInputfieldAfterProcessInput(HookEvent $event) {
$inputfield = $event->object;
if(!$inputfield->useLanguages) return;
$post = $event->arguments[0];
$languages = $this->wire('languages');
// originals
$name = $inputfield->attr('name');
$id = $inputfield->attr('id');
$value = $inputfield->attr('value');
$required = $inputfield->required;
// process and set value for each language
foreach($languages as $language) {
if($language->isDefault) continue;
$languageID = (int) $language->id;
$newID = $id . "__$languageID";
$newName = $name . "__$languageID";
$inputfield->setTrackChanges(false);
$inputfield->attr('id', $newID);
$inputfield->attr('name', $newName);
// other language values not required, even if default language value is
$inputfield->required = false;
$valueAttr = "value$languageID";
$inputfield->attr('value', $inputfield->$valueAttr);
$inputfield->setTrackChanges(true);
$inputfield->___processInput($post);
$inputfield->set($valueAttr, $inputfield->attr('value'));
}
// restore originals
$inputfield->setTrackChanges(false);
$inputfield->setAttribute('name', $name);
$inputfield->setAttribute('id', $id);
$inputfield->setAttribute('value', $value);
$inputfield->required = $required;
$inputfield->setTrackChanges(true);
}
/**
* Hook into Field::getInputfield to change label/description to proper language
*
*/
public function hookFieldGetInputfield(HookEvent $event) {
$languages = $this->wire('languages');
$language = $this->wire('user')->language;
if(!$language || !$language->id) return;
$field = $event->object;
$inputfield = $event->return;
$translatable = array('label', 'description', 'notes');
// populate language versions where available
foreach($translatable as $key) {
$langKey = $key . $language->id; // i.e. label1234
$value = $field->$langKey;
if(!$value) continue;
$inputfield->$key = $value;
}
// see if this fieldtype supports languages natively
if($field->type instanceof FieldtypeLanguageInterface) {
// populate useLanguages in the inputfield so we can detect it elsehwere
$inputfield->set('useLanguages', true);
$page = $event->arguments[0];
$value = $page->get($field->name);
// set values in this field specific to each language
foreach($languages as $language) {
$languageValue = '';
if(is_object($value) && $value instanceof LanguagesPageFieldValue) {
$languageValue = $value->getLanguageValue($language->id);
} else {
if($language->isDefault) $languageValue = $value;
}
$inputfield->set('value' . $language->id, $languageValue);
}
}
$event->return = $inputfield;
}
/**
* Hook called when new language added
*
*/
public function hookPageAdded(HookEvent $event) {
$page = $event->arguments[0];
if($page->template->name != self::languageTemplateName) return;
// trigger hook in $languages
$ids = $this->otherLanguagePageIDs;
$ids[] = $page->id;
$this->set('otherLanguagePageIDs', $ids);
wire('languages')->added($page);
// save this as a known language page with module settings
// this is a shortcut used to identify language pages before the API is fully ready
$configData = wire('modules')->getModuleConfigData('LanguageSupport');
$configData['otherLanguagePageIDs'][] = $page->id;
wire('modules')->saveModuleConfigData('LanguageSupport', $configData);
}
/**
* Hook called when language is deleted
*
*/
public function hookPageDeleteReady(HookEvent $event) {
$page = $event->arguments[0];
if($page->template->name != self::languageTemplateName) return;
$language = $page;
// remove any language-specific values from any fields
foreach(wire('fields') as $field) {
$changed = false;
foreach(array('label', 'description', 'notes') as $name) {
$name = $name . $language->id;
if(!isset($field->$name)) continue;
$field->remove($name);
$this->message("Removed {$language->name} $name from field {$field->name}");
$changed = true;
}
if($changed) $field->save();
}
// remove template labels
foreach(wire('templates') as $template) {
$name = 'label' . $page->id;
if(isset($template->$name)) {
$template->remove($name);
$template->save();
$this->message("Removed {$language->name} label from template {$template->name}");
}
}
// trigger hook in $languages
wire('languages')->deleted($page);
// update the other language module IDs to remove the uninstalled language
$configData = wire('modules')->getModuleConfigData('LanguageSupport');
$key = array_search($page->id, $configData['otherLanguagePageIDs']);
if($key !== false) {
unset($configData['otherLanguagePageIDs'][$key]);
wire('modules')->saveModuleConfigData('LanguageSupport', $configData);
}
}
/**
* Adds a Page::setLanguageValue($language, $fieldName, $value) method
*
* Provides a common interface for setting all language values to a Page.
*
* This method exists in this class rather than one of the field-specific classes
* because it deals with both language fields and page names, and potentially
* other types of unknown types that implement LanguagesValueInterface.
*
*/
public function hookPageSetLanguageValue(HookEvent $event) {
$page = $event->object;
$language = $event->arguments(0);
$field = $event->arguments(1);
$value = $event->arguments(2);
$event->return = $page;
if(!is_object($language)) {
if(ctype_digit("$language")) $language = (int) $language;
$language = $this->wire('languages')->get($language);
}
if(!$language instanceof Language) throw new WireException('Unknown language set to Page::setLanguageValue');
if($field == 'name') {
// set page name
if(!$this->wire('modules')->isInstalled('LanguageSupportPageNames')) {
throw new WireException("Please install LanguageSupportPageNames module before attempting to set multi-language names/paths/URLs.");
}
if($language->isDefault()) {
$page->set("name", $value);
} else {
$page->set("name$language->id", $value);
}
} else {
if(is_object($field)) $field = $field->name;
$previousValue = $page->get($field);
if(is_object($previousValue) && $previousValue instanceof LanguagesValueInterface) {
// utilize existing set methods available in LanguagesValueInterface (which might be slightly quicker than the else condition method
if(is_object($value) && $value instanceof LanguagesValueInterface) {
// if given a LanguagesPageFieldValue, then just set it to the page
$page->set($field, $value);
} else {
// otherwise use existing setLanguageValue method provided by LanguagesValueInterface
$previousValue->setLanguageValue($language->id, $value);
}
} else {
// temporarily set user's language to field language, set the field value, then set user's language back
// we don't know what exactly $field might be, whether custom field or some other field, but we'll set it anyway
$user = $this->wire('user');
$userLanguage = $user->language->id != $language->id ? $user->language : null;
if($userLanguage) $user->language = $language;
$page->set($field, $value);
if($userLanguage) $user->language = $userLanguage;
}
}
}
/**
* Adds a Page::getLanguageValue($language, $fieldName) method
*
* Provides a common interface for getting all language values from a Page.
*
* This method exists in this class rather than one of the field-specific classes
* because it deals with both language fields and page names, and potentially
* other types of unknown types that implement LanguagesValueInterface.
*
*/
public function hookPageGetLanguageValue(HookEvent $event) {
$page = $event->object;
$language = $event->arguments(0);
$field = $event->arguments(1);
$value = null;
if(!is_object($language)) {
if(ctype_digit("$language")) $language = (int) $language;
$language = $this->wire('languages')->get($language);
}
if(!$language instanceof Language) throw new WireException('Unknown language sent to Page::getLanguageValue');
if($field == 'name') {
// get a page name
if($language->isDefault()) {
$value = $page->name;
} else {
$value = $page->get("name$language->id");
}
} else {
if(is_object($field)) $field = $field->name;
$value = $page->get($field);
if(is_object($value) && $value instanceof LanguagesValueInterface) {
$value = $value->getLanguageValue($language->id);
} else {
// temporarily set user's language to field language, get the field value, then set user's language back
$user = $this->wire('user');
$userLanguage = $user->language->id != $language->id ? $user->language : null;
if($userLanguage) $user->language = $language;
$value = $page->get($field);
if($userLanguage) $user->language = $userLanguage;
}
}
$event->return = $value;
}
/**
* Module configuration screen
*
*/
public static function getModuleConfigInputfields(array $data) {
$form = new InputfieldWrapper();
// TBA
return $form;
}
/**
* Install or uninstall by loading the LanguageSupportInstall script
*
*/
protected function installer($install = true) {
require_once($this->config->paths->LanguageSupport . 'LanguageSupportInstall.php');
$installer = new LanguageSupportInstall();
if($install) $installer->install();
else $installer->uninstall();
}
/**
* Install the module
*
*/
public function ___install() {
$this->installer(true);
}
/**
* Uninstall the module
*
*/
public function ___uninstall() {
$this->installer(false);
}
}