Blame | Last modification | View Log | Download
<?php namespace ProcessWire;/*** ProcessWire Language Process, displays languages in Setup > Languages >** It also contains the hooks for altering the output of the InputfieldFile to hold language info and links.* This is the process assigned to processwire/setup/languages/.** ProcessWire 3.x, Copyright 2019 by Ryan Cramer* https://processwire.com***/class ProcessLanguage extends ProcessPageType {static public function getModuleInfo() {return array('title' => __('Languages', __FILE__),'version' => 103,'summary' => __('Manage system languages', __FILE__),'author' => 'Ryan Cramer','requires' => 'LanguageSupport','icon' => 'language','useNavJSON' => true,'permission' => 'lang-edit','permissions' => array('lang-edit' => 'Administer languages and static translation files'));}/*** The URL to the language-translator page (typically admin/setup/language-translator/)**/protected $translationUrl = '';/*** Array of messages for language_files, indexed by file basename**/protected $fileMessages = array();/*** CSV file to process, if present** @var Pagefile|null**/protected $csvImportLabel = '';/*** Populate the fields shown in the default language list output**/public function __construct() {parent::__construct();$showFields = array('name', 'title', 'language_files', 'language_files_site');$this->set('showFields', $showFields);$this->set('jsonListLabel', 'title|name');require_once(dirname(__FILE__) . '/LanguageParser.php');// make sure our files fields have CSV supportforeach(array('language_files', 'language_files_site') as $fieldName) {$field = $this->wire('fields')->get($fieldName);if(!$field) continue;$extensions = $field->get('extensions');if(strpos($extensions, 'csv') === false) {$field->set('extensions', "$extensions csv");$field->save();$this->message("Added CSV support to field $fieldName", Notice::debug);}}$this->csvImportLabel = $this->_('CSV Import:') . ' ';}/*** Add InputfieldFile hooks**/public function init() {$this->addHookBefore('InputfieldFile::render', $this, 'renderInputfieldFile');$this->addHookAfter('InputfieldFile::renderItem', $this, 'renderInputfieldFileItem');$this->addHookAfter('InputfieldFile::renderUpload', $this, 'renderInputfieldFileUpload');$this->addHookBefore('InputfieldFile::processInput', $this, 'processInputfieldFileInput');if(!$this->wire('config')->ajax) {$this->addHookBefore('InputfieldForm::render', $this, 'renderInputfieldForm');}parent::init();}protected function translationUrl() {if(!$this->translationUrl) {$support = $this->wire('modules')->get('LanguageSupport');$this->translationUrl = $this->wire('pages')->get($support->languageTranslatorPageID)->url;}return $this->translationUrl;}public function processInputfieldFileInput(HookEvent $event) {/** @var InputfieldFile $inputfield */$inputfield = $event->object;$inputfield->overwrite = true;}/*** Hook for before InputfieldFile::render** In this case we add an 'edit' link to the translator and some info about the translation file.** @param HookEvent $event**/public function renderInputfieldFile(HookEvent $event) {/** @var InputfieldFile $inputfield */$inputfield = $event->object;$language = $this->wire('process')->getPage();/** @var Pagefiles $pagefiles */$pagefiles = $inputfield->attr('value');foreach($pagefiles as $pagefile) {/** Pagefile $pagefile */if($pagefile->ext() != 'csv') continue;$pagefiles->remove($pagefile);$this->processCSV($pagefile->filename(), $language);}$inputfield->descriptionRows = 0;$inputfield->overwrite = true;$inputfield->noCollapseItem = true;$inputfield->noShortName = true;}public function renderInputfieldForm(HookEvent $event) {/** @var InputfieldForm $form */$form = $event->object;$language = $this->getPage();if(!$language->id) return;$file = $language->filesManager()->path . '.phrase-index.txt';$inputfield = $this->wire('modules')->get('InputfieldMarkup');$inputfield->label = $this->_('Live Search');$inputfield->icon = 'search';$placeholder = $this->_('Text to search for');$refreshUrl = "../../language-translator/add/?language_id=$language->id&refresh=1";$refreshLabel = $this->_('Refresh search phrase index');if(!is_file($file)) {$inputfield->value = "<p><a href='$refreshUrl'>" .$this->_('Click here to build search phrase index') . "</a></p>";} else {$phrases = file_get_contents($file);$phrases = str_replace(array('"', "\n", "<", ">"), ' ', $phrases);$inputfield->value ="<script>" ."var phraseIndex = \"$phrases\"; " ."var phraseLanguageID = $language->id;" ."</script>" ."<p class='description' style='margin:0'>" .$this->_('Search all translatable files for specific text/phrase.') . ' ' .$this->_('Click found matches to edit translation or add file (if not already present).') ."<p>" ."<p>" ."<input type='text' class='language-phrase-search InputfieldIgnoreChanges' style='width:50%' name='_q' placeholder='$placeholder' /> " ."<span class='detail language-phrase-search-cnt'></span> " ."<a class='pw-tooltip' title='$refreshLabel' href='$refreshUrl'>" . wireIconMarkup('refresh') . "</a>" ."</p>";}$field = $form->getChildByName('language_files_site');if($field) $form->insertBefore($inputfield, $field);}/*** Hook for InputfieldFile::renderItem** In this case we add an 'edit' link to the translator and some info about the translation file.** @param HookEvent $event**/public function renderInputfieldFileItem(HookEvent $event) {$translationUrl = $this->translationUrl();/** @var Pagefile $pagefile */$pagefile = $event->arguments[0];/** @var Language $page */$page = $pagefile->get('page');if($pagefile->ext() == 'csv') {$event->return .="<div class='InputfieldFileData InputfieldFileLanguageInfo'>" ."<span class='InputfieldFileLanguageFilename description'>" .$this->_('CSV translation file to be imported after save') ."</span>" ."</div>";return;}$textdomain = basename($pagefile->basename, '.json');$data = $page->translator->getTextdomain($textdomain);$file = $data['file'];$pathname = $this->wire('config')->paths->root . $file;$translations =& $data['translations'];$total = count($translations);$parser = $this->wire(new LanguageParser($page->translator, $pathname));$untranslated = $parser->getUntranslated();$alternates = $parser->getAlternates();$numPending = 0;$numAbandoned = 0;$numFallback = 0;foreach($untranslated as $hash => $text) {if(!isset($translations[$hash]) || !strlen($translations[$hash]['text'])) $numPending++;}foreach($translations as $hash => $translation) {if(isset($untranslated[$hash])) continue;$numAbandoned++;if($page->isDefault()) continue;foreach($alternates as $srcHash => $values) {if(isset($values[$hash]) && isset($untranslated[$srcHash])) {$numFallback++;$numAbandoned--;break;}}}$total += $numAbandoned;$message = sprintf($this->_n("%d phrase", "%d phrases", $total), $total);if($numAbandoned || $numPending || $numFallback) {$a = array();if($numAbandoned) $a[] = sprintf($this->_('%d abandoned'), $numAbandoned);if($numPending) $a[] = sprintf($this->_('%d blank'), $numPending);if($numFallback) $a[] = sprintf($this->_('%d fallback'), $numFallback);$message = " <span class='ui-state-error-text'>(" . implode(' / ', $a) . ")</span>";}$editLabel = $this->_x('Edit', 'edit-language-file');$out ="<div class='InputfieldFileData InputfieldFileLanguageInfo'>" ."<span class='InputfieldFileLanguageFilename description'>/$file —</span> <span class='notes'>$message</span> " ."<a class='action' href='{$translationUrl}edit/?language_id={$page->id}&textdomain=$textdomain'> " ."<i class='fa fa-edit'></i> $editLabel <i class='fa fa-angle-double-right hover-only'></i></a>" ."</div>";$page->translator->unloadTextdomain($textdomain);$event->return .= $out;}/*** Hook for InputfieldFile::renderUpload** This just adds a 'new' link to add a new translation file.** @param HookEvent $event**/public function renderInputfieldFileUpload(HookEvent $event) {$translationUrl = $this->translationUrl();/** @var Pagefiles $pagefiles */$pagefiles = $event->arguments(0);/** @var Page $page */$page = $pagefiles->get('page');/** @var InputfieldFile $inputfield */$inputfield = $event->object;$out = '';/** @var InputfieldButton $btn1 */$btn1 = $this->wire('modules')->get('InputfieldButton');$btn1->href = "{$translationUrl}add/?language_id={$page->id}";$btn1->value = $this->_('Find Files to Translate');$btn1->icon = 'plane';if($inputfield->name == 'language_files') $btn1->showInHeader();$out .= $btn1->render();if(count($inputfield->attr('value'))) {/** @var InputfieldButton $btn2 */$btn2 = $this->wire('modules')->get('InputfieldButton');$btn2->href = "../download/?language_id={$page->id}&field=" . $inputfield->attr('name');$btn2->value = $this->_('Download ZIP');$btn2->icon = 'file-zip-o';$btn2->setSecondary();$btn2->addClass('download-button');$out .= $btn2->render();$btn2->href .= '&csv=1';$btn2->value = $this->_('Download CSV');$btn2->icon = 'file-excel-o';$out .= $btn2->render();}$event->return .= "<p>$out</p>";}/*** Modify the output per-field in the PageType list (template-method)** In this case we make it return a count for the language_files** @param string $name* @param mixed $value* @return string**/protected function renderListFieldValue($name, $value) {if($name == 'language_files' || $name == 'language_files_site') {return count($value);} else if($name == 'title') {if(!$value) return '(blank)';return (string) $value;} else {return parent::renderListFieldValue($name, $value);}}public function ___execute() {// check if 2.5 update needed to add new language_files_site fieldif(!$this->wire('fields')->get('language_files_site')) {require_once(dirname(__FILE__) . '/LanguageSupportInstall.php');$installer = $this->wire(new LanguageSupportInstall());$installer->addFilesFields($this->wire('fieldgroups')->get(LanguageSupport::languageTemplateName));}return parent::___execute();}/*** Create and send a ZIP of translation files or CSV of translations**/public function ___executeDownload() {$id = (int) $this->input->get('language_id');if(!$id) throw new WireException("No language specified");$language = $this->wire('languages')->get($id);if(!$language->id) throw new WireException("Unknown language");$fieldName = $this->input->get('field') == 'language_files_site' ? 'language_files_site' : 'language_files';$csv = (int) $this->wire('input')->get('csv');$path = $language->$fieldName->path();$files = array();foreach($language->$fieldName as $file) {$files[] = $file->filename;}if($csv) {// CSV$filename = $language->name . "-" . (strpos($fieldName, 'site') ? 'site' : 'wire') . ".csv";header("Content-type: application/force-download");header("Content-Transfer-Encoding: Binary");header("Content-disposition: attachment; filename=$filename");$fp = fopen('php://output', 'w');$defaultCol = $language->name == 'en' ? 'default' : 'en';$fields = array($defaultCol, $language->name, 'description', 'file', 'hash');fputcsv($fp, $fields);foreach($files as $f) {$textdomain = basename($f, '.json');$data = $language->translator->getTextdomain($textdomain);$file = $data['file'];$pathname = $this->wire('config')->paths->root . $file;$translated =& $data['translations'];$parser = $this->wire(new LanguageParser($language->translator, $pathname));$untranslated = $parser->getUntranslated();$comments = $parser->getComments();foreach($untranslated as $hash => $text1) {$text2 = isset($translated[$hash]) ? $translated[$hash]['text'] : '';$comment = isset($comments[$hash]) ? $comments[$hash] : '';if(strpos($comment, '//') !== false) list(, $comment) = explode('//', $comment);$fields = array($text1, $text2, trim($comment), $file, $hash);fputcsv($fp, $fields);}}fclose($fp);} else {// ZIP$zipname = $language->name . "-";$zipname .= $fieldName == 'language_files' ? 'wire' : 'site';$zipfile = "$path$zipname.zip";$info = wireZipFile($zipfile, $files, array("overwrite" => true));if(!count($info['files'])) {$this->error("Error adding files to ZIP");$this->session->redirect('../');} else {wireSendFile($zipfile);}}exit(0);}/*** Process a CSV file to import changes from it** Must be in the same format as the one provied by the executeDownload() method** @param string $csvFile* @param Language $language* @return bool* @throws WireException**/public function processCSV($csvFile, Language $language) {$fp = fopen($csvFile, "r");if($fp === false) {$this->error($this->csvImportLabel . "Unable to open: $csvFile");return false;}$keys = array('original','translated','file','hash',);$n = 0;$header = array();$translator = new LanguageTranslator($language);$textdomain = '';$lastTextdomain = '';$lastFile = '';$numChanges = 0;$numTotal = 0;$numGross = 0;$translations = null;$halt = false;while(($csvData = fgetcsv($fp, 8192, ",")) !== FALSE) {if(++$n === 1) {// header row$header = $csvData;foreach($header as $key => $value) {$header[$key] = strtolower($value);}// make sure everything we need is presentforeach($keys as $k => $key) {if($k > 1 && !in_array($key, $header)) {$this->error($this->csvImportLabel . "CSV data missing required column '$key'");$halt = true;}}if($halt) break;continue;}$row = array();foreach($header as $key => $name) {if($key === 0) $name = 'original';if($key === 1) $name = 'translated';$row[$name] = $csvData[$key];}if(empty($row['file']) || empty($row['original'])) continue;$file = $row['file'];$hash = $row['hash'];// $textOriginal = $row['original'];$textTranslated = $row['translated'];$textdomain = $translator->filenameToTextdomain($file);if(!$translator->textdomainFileExists($textdomain)) {$textdomain = $translator->addFileToTranslate($file, false, false);//$translator->loadTextdomain($textdomain);}if(is_null($translations)) $translations = $translator->getTranslations($textdomain);if(!$textdomain) {$this->warning($this->csvImportLabel . sprintf($this->_('Unrecognized textdomain for file: %s'),$this->wire('sanitizer')->entities($file)));continue;}if($textdomain != $lastTextdomain) {if(!$lastFile) $lastFile = $file;if(!$lastTextdomain) $lastTextdomain = $textdomain;$this->processCSV_saveTextdomain($translator, $lastTextdomain, $lastFile, $numChanges);$translations = $translator->getTranslations($textdomain);$numChanges = 0;}$translation = isset($translations[$hash]) ? $translations[$hash] : array('text' => '');if($translation['text'] != $textTranslated) {$translator->setTranslationFromHash($textdomain, $hash, $textTranslated);$numChanges++;$numTotal++;}$lastTextdomain = $textdomain;$lastFile = $file;$numGross++;}if($numChanges) {$this->processCSV_saveTextdomain($translator, $textdomain, $lastFile, $numChanges);}$language->save();fclose($fp);$this->message($this->csvImportLabel . sprintf($this->_('%d total translations, %d total changes'), $numGross, $numTotal));return $halt ? false : true;}/*** Save a textdomain, helper for processCSV method** @param LanguageTranslator $translator* @param string $textdomain* @param string $filename* @param int $numChanges**/protected function processCSV_saveTextdomain(LanguageTranslator $translator, $textdomain, $filename, $numChanges) {if($filename) { /* ignore, not currently used */ }$file = $translator->textdomainToFilename($textdomain);if($numChanges) {try {$translator->saveTextdomain($textdomain);$this->message($this->csvImportLabel . sprintf($this->_('Saved %d change(s) for file: %s'), $numChanges, $file));} catch(\Exception $e) {$this->error($e->getMessage());}} else {// no changes}$translator->unloadTextdomain($textdomain);}}