diff --git a/wire/core/Modules.php b/wire/core/Modules.php
index 5afb2420..a2d09949 100644
--- a/wire/core/Modules.php
+++ b/wire/core/Modules.php
@@ -4974,9 +4974,9 @@ class Modules extends WireArray {
if(!in_array($id, $this->moduleIDs)) unset($this->modulesLastVersions[$id]);
}
if(count($this->modulesLastVersions)) {
- $this->wire('cache')->save(self::moduleLastVersionsCacheName, $this->modulesLastVersions, WireCache::expireReserved);
+ $this->wire()->cache->save(self::moduleLastVersionsCacheName, $this->modulesLastVersions, WireCache::expireReserved);
} else {
- $this->wire('cache')->delete(self::moduleLastVersionsCacheName);
+ $this->wire()->cache->delete(self::moduleLastVersionsCacheName);
}
}
@@ -5355,6 +5355,48 @@ class Modules extends WireArray {
return $cnt;
}
+ /**
+ * Get module language translation files
+ *
+ * @param Module|string $module
+ * @return array Array of translation files including full path, indexed by basename without extension
+ * @since 3.0.181
+ *
+ */
+ public function getModuleLanguageFiles($module) {
+
+ $module = $this->getModuleClass($module);
+ if(empty($module)) return array();
+
+ $path = $this->wire()->config->paths($module);
+ if(empty($path)) return array();
+
+ $pathHidden = $path . '.languages/';
+ $pathVisible = $path . 'languages/';
+
+ if(is_dir($pathVisible)) {
+ $path = $pathVisible;
+ } else if(is_dir($pathHidden)) {
+ $path = $pathHidden;
+ } else {
+ return array();
+ }
+
+ $items = array();
+ $options = array(
+ 'extensions' => array('csv'),
+ 'recursive' => false,
+ 'excludeHidden' => true,
+ );
+
+ foreach($this->wire()->files->find($path, $options) as $file) {
+ $basename = basename($file, '.csv');
+ $items[$basename] = $file;
+ }
+
+ return $items;
+ }
+
/**
* Enables use of $modules('ModuleName')
*
diff --git a/wire/modules/LanguageSupport/LanguageTranslator.php b/wire/modules/LanguageSupport/LanguageTranslator.php
index 3bd9bd71..0e64cf74 100644
--- a/wire/modules/LanguageSupport/LanguageTranslator.php
+++ b/wire/modules/LanguageSupport/LanguageTranslator.php
@@ -170,7 +170,23 @@ class LanguageTranslator extends Wire {
} else {
$reflection = new \ReflectionClass($o);
- $filename = $reflection->getFileName();
+ $filename = $reflection->getFileName();
+
+ if($o instanceof Module) {
+ $ds = \DIRECTORY_SEPARATOR;
+ if(strpos($filename, "{$ds}wire{$ds}modules{$ds}") === false) {
+ // not a core module
+ $config = $this->wire()->config;
+ $filename = $this->wire()->files->unixFileName($filename);
+ if(strpos($filename, $config->urls($o)) === false && strpos($filename, "/$class/") !== false) {
+ // module likely in a symbolic link directory, so determine our own path for textdomain
+ // rather than using the one provided by ReflectionClass
+ list(, $filename) = explode("/$class/", $filename, 2);
+ $filename = $config->paths($class) . $filename;
+ }
+ }
+ }
+
$textdomain = $this->filenameToTextdomain($filename);
$this->classNamesToTextdomains[$class] = $textdomain;
$parentTextdomains = array();
@@ -286,7 +302,7 @@ class LanguageTranslator extends Wire {
*
*/
public function getTranslation($textdomain, $text, $context = '') {
- if($this->wire('hooks')->isHooked('LanguageTranslator::getTranslation()')) {
+ if($this->wire()->hooks->isHooked('LanguageTranslator::getTranslation()')) {
// if method has hooks, we let them run
return $this->__call('getTranslation', array($textdomain, $text, $context));
} else {
@@ -474,7 +490,7 @@ class LanguageTranslator extends Wire {
*/
public function textdomainFileExists($textdomain) {
$file = $this->getTextdomainTranslationFile($textdomain);
- return is_file($file);
+ return file_exists($file);
}
/**
diff --git a/wire/modules/LanguageSupport/Languages.php b/wire/modules/LanguageSupport/Languages.php
index 6ea65b06..5c2d1e99 100644
--- a/wire/modules/LanguageSupport/Languages.php
+++ b/wire/modules/LanguageSupport/Languages.php
@@ -767,6 +767,27 @@ class Languages extends PagesType {
public function get($key) {
return parent::get($key);
}
+
+ /**
+ * Import a language translations file
+ *
+ * @param Language|string $language
+ * @param string $file Full path to .csv translations file
+ * The .csv file must be one generated by ProcessWire’s language translation tools.
+ * @param bool $quiet Specify true to suppress error/success notifications being generated (default=false)
+ * @return bool|int Returns integer with number of translations imported or boolean false on error
+ * @throws WireException
+ * @since 3.0.181
+ *
+ */
+ public function importTranslationsFile($language, $file, $quiet = false) {
+ if(!wireInstanceOf($language, 'Language')) $language = $this->get($language);
+ if(!$language || !$language->id) throw new WireException("Unknown language");
+ $process = $this->wire()->modules->getModule('ProcessLanguage', array('noInit' => true)); /** @var ProcessLanguage $process */
+ if(!$this->wire()->files->exists($file)) throw new WireException("Language file does not exist: $file");
+ if(pathinfo($file, PATHINFO_EXTENSION) !== 'csv') throw new WireException("Language file does not have .csv extension");
+ return $process->processCSV($file, $language, array('quiet' => $quiet));
+ }
/**
* Hook to WireDatabasePDO::unknownColumnError
diff --git a/wire/modules/LanguageSupport/ProcessLanguage.css b/wire/modules/LanguageSupport/ProcessLanguage.css
index d7ecffcf..aa7b2edd 100644
--- a/wire/modules/LanguageSupport/ProcessLanguage.css
+++ b/wire/modules/LanguageSupport/ProcessLanguage.css
@@ -8,14 +8,16 @@
text-transform: none;
}
-.Inputfields .InputfieldFile .InputfieldFileLanguageInfo {
+
+.AdminThemeReno .Inputfields .InputfieldFile .InputfieldFileLanguageInfo,
+.AdminThemeDefault .Inputfields .InputfieldFile .InputfieldFileLanguageInfo {
position: relative;
- margin-top: 0;
+ margin-top: 0;
padding-top: 0;
}
-.InputfieldFileList .InputfieldFileLanguageInfo {
- margin-top: -1em;
+.Inputfields .InputfieldFile a.action:hover {
+ text-decoration: none;
}
.InputfieldFileList .InputfieldFileLanguageInfo a i.hover-only {
diff --git a/wire/modules/LanguageSupport/ProcessLanguage.module b/wire/modules/LanguageSupport/ProcessLanguage.module
index 9cc030ed..d0c62d54 100644
--- a/wire/modules/LanguageSupport/ProcessLanguage.module
+++ b/wire/modules/LanguageSupport/ProcessLanguage.module
@@ -253,9 +253,9 @@ class ProcessLanguage extends ProcessPageType {
$out =
"
";
$page->translator->unloadTextdomain($textdomain);
@@ -347,27 +347,62 @@ class ProcessLanguage extends ProcessPageType {
*/
public function ___executeDownload() {
- $id = (int) $this->input->get('language_id');
+ $config = $this->wire()->config;
+ $input = $this->wire()->input;
+
+ $id = (int) $input->get('language_id');
if(!$id) throw new WireException("No language specified");
- $language = $this->wire('languages')->get($id);
+ $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');
+ $fieldName = $input->get('field') == 'language_files_site' ? 'language_files_site' : 'language_files';
+ $textdomain = $this->wire()->sanitizer->textdomain($input->get('textdomain'));
+ $textdomains = array();
+ $csv = (int) $input->get('csv');
$path = $language->$fieldName->path();
$files = array();
- foreach($language->$fieldName as $file) {
- $files[] = $file->filename;
+ if($textdomain) {
+ $file = $language->translator->textdomainToFilename($textdomain);
+ if($file) {
+ $files[] = $file;
+ $textdomains[$file] = $textdomain;
+ } else {
+ $textdomain = '';
+ }
+ }
+
+ if(!count($files) && $fieldName) {
+ foreach($language->$fieldName as $file) {
+ $files[] = $file->filename;
+ }
+ }
+
+ if(!count($files)) {
+ throw new WireException('No translation files specified to download');
}
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");
+ if($textdomain) {
+ // i.e. es-modulename.csv
+ $parts = explode('--', $textdomain);
+ $basename = array_pop($parts);
+ $parts = explode('-', $basename);
+ $basename = array_shift($parts);
+ $filename = "$language->name-$basename.csv";
+ } else {
+ // i.e. es-site.csv or es-wire.csv
+ $filename = $language->name . "-" . (strpos($fieldName, 'site') ? 'site' : 'wire') . ".csv";
+ }
+ if($input->get('view')) {
+ header("Content-type: text/plain");
+ } else {
+ 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';
@@ -375,13 +410,20 @@ class ProcessLanguage extends ProcessPageType {
fputcsv($fp, $fields);
foreach($files as $f) {
+
+ if(isset($textdomains[$f])) {
+ $textdomain = $textdomains[$f];
+ } else {
+ $textdomain = basename($f, '.json');
+ }
- $textdomain = basename($f, '.json');
$data = $language->translator->getTextdomain($textdomain);
+ if(empty($data)) continue;
+
$file = $data['file'];
- $pathname = $this->wire('config')->paths->root . $file;
+ $pathname = $config->paths->root . $file;
$translated =& $data['translations'];
- $parser = $this->wire(new LanguageParser($language->translator, $pathname));
+ $parser = $this->wire(new LanguageParser($language->translator, $pathname)); /** @var LanguageParser $parser */
$untranslated = $parser->getUntranslated();
$comments = $parser->getComments();
@@ -420,16 +462,26 @@ class ProcessLanguage extends ProcessPageType {
*
* @param string $csvFile
* @param Language $language
- * @return bool
+ * @param array $options Additional options (3.0.181+)
+ * - `file` (string): Use this path/file (relative to install root)
+ * - `quiet` (bool): Suppress generating notifications? (default=false)
+ * @return bool|int Returns false on error or integer on success, where value is number of translations imported
* @throws WireException
*
*/
- public function processCSV($csvFile, Language $language) {
+ public function processCSV($csvFile, Language $language, array $options = array()) {
+
+ $defaults = array(
+ 'file' => '',
+ 'quiet' => false,
+ );
+
+ $options = array_merge($defaults, $options);
$fp = fopen($csvFile, "r");
if($fp === false) {
- $this->error($this->csvImportLabel . "Unable to open: $csvFile");
+ if(!$options['quiet']) $this->error($this->csvImportLabel . "Unable to open: $csvFile");
return false;
}
@@ -450,8 +502,16 @@ class ProcessLanguage extends ProcessPageType {
$numTotal = 0;
$numGross = 0;
$translations = null;
+ $optionsFileBasename = '';
$halt = false;
+ $this->wire($translator);
+
+ if(!empty($options['file'])) {
+ $options['file'] = ltrim($this->wire()->files->unixFileName($options['file']), '/');
+ $optionsFileBasename = basename($options['file']);
+ }
+
while(($csvData = fgetcsv($fp, 8192, ",")) !== FALSE) {
if(++$n === 1) {
@@ -463,13 +523,16 @@ class ProcessLanguage extends ProcessPageType {
// make sure everything we need is present
foreach($keys as $k => $key) {
if($k > 1 && !in_array($key, $header)) {
- $this->error($this->csvImportLabel . "CSV data missing required column '$key'");
- $halt = true;
+ if($key === 'file' && !empty($options['file'])) {
+ // default file provided so not required in CSV data
+ } else {
+ if(!$options['quiet']) $this->error($this->csvImportLabel . "CSV data missing required column '$key'");
+ $halt = true;
+ }
}
}
if($halt) break;
continue;
-
}
$row = array();
@@ -479,7 +542,22 @@ class ProcessLanguage extends ProcessPageType {
$row[$name] = $csvData[$key];
}
- if(empty($row['file']) || empty($row['original'])) continue;
+ if($options['file']) {
+ if(empty($row['file'])) {
+ $row['file'] = $options['file'];
+ } else {
+ $rowFileBasename = basename($row['file']);
+ if($rowFileBasename === $optionsFileBasename) {
+ // i.e. site/modules/Hello/Hello.module
+ $row['file'] = $options['file'];
+ } else {
+ // i.e. site/modules/Hello/World.module
+ $row['file'] = dirname($options['file']) . '/' . $rowFileBasename;
+ }
+ }
+ }
+
+ if(empty($row['original']) || empty($row['file'])) continue;
$file = $row['file'];
$hash = $row['hash'];
@@ -489,17 +567,17 @@ class ProcessLanguage extends ProcessPageType {
if(!$translator->textdomainFileExists($textdomain)) {
$textdomain = $translator->addFileToTranslate($file, false, false);
- //$translator->loadTextdomain($textdomain);
}
- if(is_null($translations)) $translations = $translator->getTranslations($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)
- )
- );
+ if(!$options['quiet']) $this->warning($this->csvImportLabel . sprintf(
+ $this->_('Unrecognized textdomain for file: %s'),
+ $this->wire()->sanitizer->entities($file)
+ ));
continue;
}
@@ -529,9 +607,11 @@ class ProcessLanguage extends ProcessPageType {
$language->save();
fclose($fp);
- $this->message($this->csvImportLabel . sprintf($this->_('%d total translations, %d total changes'), $numGross, $numTotal));
+ if(!$options['quiet']) {
+ $this->message($this->csvImportLabel . sprintf($this->_('%d total translations, %d total changes'), $numGross, $numTotal), Notice::noGroup);
+ }
- return $halt ? false : true;
+ return $halt ? false : $numGross;
}
/**
@@ -549,7 +629,7 @@ class ProcessLanguage extends ProcessPageType {
if($numChanges) {
try {
$translator->saveTextdomain($textdomain);
- $this->message($this->csvImportLabel . sprintf($this->_('Saved %d change(s) for file: %s'), $numChanges, $file));
+ $this->message($this->csvImportLabel . sprintf($this->_('Saved %d change(s) for file: %s'), $numChanges, $file), Notice::noGroup);
} catch(\Exception $e) {
$this->error($e->getMessage());
}
diff --git a/wire/modules/Process/ProcessModule/ProcessModule.module b/wire/modules/Process/ProcessModule/ProcessModule.module
index ce8b48fa..504af829 100644
--- a/wire/modules/Process/ProcessModule/ProcessModule.module
+++ b/wire/modules/Process/ProcessModule/ProcessModule.module
@@ -139,6 +139,7 @@ class ProcessModule extends Process {
$this->labels['download_install'] = $this->_('Download and Install');
}
$this->labels['get_module_info'] = $this->_('Get Module Info');
+ $this->labels['modules'] = $this->_('Modules');
$this->labels['module_information'] = $this->_x("Module Information", 'edit');
$this->labels['download_now'] = $this->_('Download Now');
$this->labels['download_dir'] = $this->_('Add Module From Directory');
@@ -197,7 +198,17 @@ class ProcessModule extends Process {
*
*/
public function ___executeNavJSON(array $options = array()) {
- $page = $this->wire('page');
+
+ $page = $this->wire()->page;
+ $input = $this->wire()->input;
+ $modules = $this->wire()->modules;
+
+ $site = (int) $input->get('site');
+ $core = (int) $input->get('core');
+ $configurable = (int) $input->get('configurable');
+ $install = (int) $input->get('install');
+ $moduleNames = array();
+
$data = array(
'url' => $page->url,
'label' => (string) $page->get('title|name'),
@@ -205,36 +216,32 @@ class ProcessModule extends Process {
'list' => array(),
);
- $site = $this->wire('input')->get('site');
- $core = $this->wire('input')->get('core');
- $configurable = $this->wire('input')->get('configurable');
- $install = $this->wire('input')->get('install');
-
if($site || $install) $data['add'] = array(
'url' => "?new#tab_new_modules",
'label' => __('Add New', '/wire/templates-admin/default.php'),
'icon' => 'plus-circle',
);
- $modules = $this->wire('modules');
- $moduleNames = array();
if($install) {
$moduleNames = array_keys($modules->getInstallable());
} else {
- foreach($modules as $module) $moduleNames[] = $module->className();
+ foreach($modules as $module) {
+ $moduleNames[] = $module->className();
+ }
}
+
sort($moduleNames);
foreach($moduleNames as $moduleName) {
- $info = $this->wire('modules')->getModuleInfoVerbose($moduleName);
+ $info = $modules->getModuleInfoVerbose($moduleName);
if($site && $info['core']) continue;
if($core && !$info['core']) continue;
if($configurable) {
if(!$info['configurable'] || !$info['installed']) continue;
- $flags = $this->wire('modules')->getFlags($moduleName);
+ $flags = $modules->getFlags($moduleName);
if($flags & Modules::flagsNoUserConfig) continue;
}
@@ -242,7 +249,7 @@ class ProcessModule extends Process {
// exclude already installed modules
if($info['installed']) continue;
// check that it can be installed NOW (i.e. all dependencies met)
- if(!$this->wire('modules')->isInstallable($moduleName, true)) continue;
+ if(!$modules->isInstallable($moduleName, true)) continue;
}
$label = $info['name'];
@@ -264,7 +271,8 @@ class ProcessModule extends Process {
ksort($data['list']);
$data['list'] = array_values($data['list']);
- if($this->wire('config')->ajax) header("Content-Type: application/json");
+ if($this->wire()->config->ajax) header("Content-Type: application/json");
+
return json_encode($data);
}
@@ -863,6 +871,12 @@ class ProcessModule extends Process {
$summary .= "" . $this->labels['requires'] . " - " . implode(', ', $requires) . "";
}
} else $requires = array();
+
+ $nsClassName = $modules->getModuleClass($name, true);
+ if(!wireInstanceOf($nsClassName, 'Module')) {
+ $summary .= "" . $this->_('Module class must implement the “ProcessWire\Module” interface.') . "";
+ $requires[] = 'Module interface';
+ }
if(count($info['installs'])) {
$summary .= "" . $this->labels['installs'] . " - " . implode(', ', $info['installs']) . "";
@@ -1697,6 +1711,13 @@ class ProcessModule extends Process {
if($hooksStr) {
$table->row(array($this->_x('Hooks To', 'edit'), $hooksStr));
}
+
+ $languageFiles = $languages ? $modules->getModuleLanguageFiles($moduleName) : array();
+ if(count($languageFiles)) {
+ $languages = wireIconMarkup('language') . ' ' . $sanitizer->entities(implode(', ', array_keys($languageFiles)));
+ $languages .= " - " . $this->_('install translations') . "";
+ $table->row(array($this->_x('Languages', 'edit'), $languages));
+ }
if(!empty($moduleInfo['href'])) {
$table->row(array($this->_x('More Information', 'edit'), "$moduleInfo[href]"));
@@ -1750,10 +1771,9 @@ class ProcessModule extends Process {
public function ___executeInstallConfirm() {
- $name = $this->wire('input')->get('name');
+ $name = $this->wire('input')->get->name('name');
if(!$name) throw new WireException("No module name specified");
- $name = $this->wire('sanitizer')->fieldName($name);
- if(!$this->wire('modules')->isInstallable($name, true)) throw new WireException("Module is not currently installable");
+ if(!$this->wire()->modules->isInstallable($name, true)) throw new WireException("Module is not currently installable");
$this->headline($this->labels['install']);
@@ -1778,6 +1798,84 @@ class ProcessModule extends Process {
return $form->render();
}
+ /**
+ * Languages translations import
+ *
+ * @return string
+ * @since 3.0.181
+ *
+ */
+ public function ___executeTranslation() {
+
+ $languages = $this->wire()->languages;
+ $modules = $this->wire()->modules;
+ $session = $this->wire()->session;
+ $input = $this->wire()->input;
+ $config = $this->wire()->config;
+ $moduleName = $input->get->name('name');
+
+ if(empty($moduleName)) throw new WireException('No module name specified');
+ if(!$modules->isInstalled($moduleName)) throw new WireException("Unknown module: $moduleName");
+
+ $moduleEditUrl = $modules->getModuleEditUrl($moduleName);
+ $languageFiles = $modules->getModuleLanguageFiles($moduleName);
+
+ if(!$languages || !count($languageFiles)){
+ $session->message($this->_('No module language files available'));
+ $session->location($moduleEditUrl);
+ }
+
+ $this->headline($this->_('Module language translations'));
+ $this->breadcrumb($config->urls->admin . 'modules/', $this->labels['modules']);
+ $this->breadcrumb($moduleEditUrl, $moduleName);
+
+ /** @var InputfieldForm $form */
+ $form = $modules->get('InputfieldForm');
+ $form->attr('id', 'ModuleImportTranslationForm');
+ $form->attr('action', $config->urls->admin . "module/translation/?name=$moduleName");
+ $form->attr('method', 'post');
+ $form->description = sprintf($this->_('Import translations for module %s'), $moduleName);
+
+ foreach($languages as $language) {
+ /** @var InputfieldSelect $lang */
+ $langLabel = $language->get('title');
+ $langLabel .= $langLabel ? " ($language->name)" : $language->name;
+
+ /** @var InputfieldSelect $f */
+ $f = $modules->get('InputfieldSelect');
+ $f->attr('name', "language_$language->name");
+ $f->label = sprintf($this->_('Import into %s'), $langLabel);
+ $f->addOption('');
+ foreach($languageFiles as $basename => $filename) {
+ $f->addOption($basename);
+ }
+ $form->append($f);
+ }
+
+ if($input->post('submit_import_translations')) {
+ foreach($languages as $language) {
+ $basename = $input->post->pageName("language_$language->name");
+ if(empty($basename)) continue;
+ if(empty($languageFiles[$basename])) continue;
+ $file = $languageFiles[$basename];
+ if(!is_file($file)) {
+ $session->error($this->_('Cannot find CSV file') . " - " . basename($file));
+ continue;
+ }
+ $languages->importTranslationsFile($language, $file);
+ }
+ $session->location($moduleEditUrl);
+ }
+
+ /** @var InputfieldSubmit $f */
+ $f = $modules->get('InputfieldSubmit');
+ $f->attr('name', 'submit_import_translations');
+ $f->showInHeader(true);
+ $form->add($f);
+
+ return $form->render();
+ }
+
/**
* URL to redirect to after non-authenticated user is logged-in, or false if module does not support
*