first commit

This commit is contained in:
2024-07-15 11:28:08 +02:00
commit f52d538ea5
21891 changed files with 6161164 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,5 @@
# Compiled libraries
These files are built from the Loco core. Do not edit!
They've been converted down for PHP 5.2 compatibility in WordPress.

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,5 @@
<?php
/**
* Downgraded for PHP 5.2 compatibility. Do not edit.
*/
function loco_parse_wp_locale( $tag ){ if( ! preg_match( '/^([a-z]{2,3})(?:[-_]([a-z]{2}))?(?:[-_]([a-z0-9]{3,8}))?$/i', $tag, $tags ) ){ throw new InvalidArgumentException('Invalid WordPress locale: '.json_encode($tag) ); } $data = array( 'lang' => strtolower( $tags[1] ), ); if( isset($tags[2]) && ( $subtag = $tags[2] ) ){ $data['region'] = strtoupper($tags[2]); } if( isset($tags[3]) && ( $subtag = $tags[3] ) ){ $data['variant'] = strtolower($tags[3]); } return $data; }

View File

@@ -0,0 +1,6 @@
<?php
/**
* Downgraded for PHP 5.2 compatibility. Do not edit.
*/
class LocoDomQuery extends ArrayIterator { public static function parse( $source ){ $dom = new DOMDocument('1.0', 'UTF-8' ); $dom->preserveWhitespace = true; $dom->formatOutput = false; $source = '<html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /></head><body>'.$source.'</body></html>'; $used_errors = libxml_use_internal_errors(true); $opts = LIBXML_HTML_NODEFDTD; $parsed = $dom->loadHTML( $source, $opts ); $errors = libxml_get_errors(); $used_errors || libxml_use_internal_errors(false); libxml_clear_errors(); if( $errors || ! $parsed ){ $e = new Loco_error_ParseException('Unknown parse error'); foreach( $errors as $error ){ $e = new Loco_error_ParseException( trim($error->message) ); $e->setContext( $error->line, $error->column, $source ); if( LIBXML_ERR_FATAL === $error->level ){ throw $e; } } if( ! $parsed ){ throw $e; } } return $dom; } public function __construct( $value ){ if( is_array($value) ){ $nodes = $value; } else if( $value instanceof DOMDocument ){ $nodes = array( $value->documentElement ); } else if( $value instanceof DOMNodeList ){ $nodes = array(); foreach( $value as $node ){ $nodes[] = $node; } } else { $value = self::parse( $value ); $nodes = array( $value->documentElement ); } parent::__construct( $nodes ); } public function eq( $index ){ $q = new LocoDomQuery(array()); if( $el = $this[$index] ){ $q[] = $el; } return $q; } public function find( $value ){ $q = new LocoDomQuery( array() ); $f = new _LocoDomQueryFilter($value); foreach( $this as $el ){ foreach( $f->filter($el) as $match ){ $q[] = $match; } } return $q; } public function text(){ $s = ''; foreach( $this as $el ){ $s .= $el->textContent; } return $s; } public function html(){ $s = ''; foreach( $this as $outer ){ foreach( $outer->childNodes as $inner ){ $s .= $inner->ownerDocument->saveXML($inner); } break; } return $s; } public function attr( $name ){ foreach( $this as $el ){ return $el->getAttribute($name); } return null; } public function serialize(){ $pairs = array(); foreach( array('input','select','textarea','button') as $type ){ foreach( $this->find($type) as $field ){ $name = $field->getAttribute('name'); if( ! $name ){ continue; } if( $field->hasAttribute('type') ){ $type = $field->getAttribute('type'); } if( 'select' === $type ){ $value = null; $f = new _LocoDomQueryFilter('option'); foreach( $f->filter($field) as $option ){ if( $option->hasAttribute('value') ){ $_value = $option->getAttribute('value'); } else { $_value = $option->nodeValue; } if( $option->hasAttribute('selected') ){ $value = $_value; break; } else if( is_null($value) ){ $value = $_value; } } if( is_null($value) ){ $value = ''; } } else if( 'checkbox' === $type || 'radio' === $type ){ if( $field->hasAttribute('checked') ){ $value = $field->getAttribute('value'); } else { continue; } } else if( 'file' === $type ){ $value = ''; } else if( $field->hasAttribute('value') ){ $value = $field->getAttribute('value'); } else { $value = $field->textContent; } $pairs[] = sprintf('%s=%s', rawurlencode($name), rawurlencode($value) ); } } return implode('&',$pairs); } }
class _LocoDomQueryFilter { private $tag; private $attr = array(); public function __construct( $value ){ $id = '[-_a-z][-_a-z0-9]*'; if( ! preg_match('/^([a-z1-6]*)(#'.$id.')?(\.'.$id.')?(\[(\w+)="(.+)"\])?$/i', $value, $r ) ){ throw new InvalidArgumentException('Bad filter, '.$value ); } if( $r[1] ){ $this->tag = $r[1]; } if( ! empty($r[2]) ){ $this->attr['id'] = substr($r[2],1); } if( ! empty($r[3]) ){ $this->attr['class'] = substr($r[3],1); } if( ! empty($r[4]) ){ $this->attr[ $r[5] ] = $r[6]; } } public function filter( DOMElement $el ){ if( $this->tag ){ $list = $el->getElementsByTagName($this->tag); $recursive = false; } else { $list = $el->childNodes; $recursive = true; } if( $this->attr ){ $list = $this->reduce( $list, new ArrayIterator, $recursive )->getArrayCopy(); } return $list; } public function reduce( DOMNodeList $list, ArrayIterator $reduced, $recursive ){ foreach( $list as $node ){ if( $node instanceof DOMElement ){ $matched = false; foreach( $this->attr as $name => $value ){ if( ! $node->hasAttribute($name) ){ $matched = false; break; } $values = array_flip( explode(' ', $node->getAttribute($name) ) ); if( ! isset($values[$value]) ){ $matched = false; break; } $matched = true; } if( $matched ){ $reduced[] = $node; } if( $recursive && $node->hasChildNodes() ){ $this->reduce( $node->childNodes, $reduced, true ); } } } return $reduced; } }

View File

@@ -0,0 +1,5 @@
<?php
/**
* Compiled data. Do not edit.
*/
return array('aa'=>'Afar','ab'=>'Abkhazian','ae'=>'Avestan','af'=>'Afrikaans','ak'=>'Akan','am'=>'Amharic','an'=>'Aragonese','ar'=>'Arabic','arq'=>'Algerian Arabic','ary'=>'Moroccan Arabic','as'=>'Assamese','ast'=>'Asturian','av'=>'Avaric','ay'=>'Aymara','az'=>'Azerbaijani','azb'=>'South Azerbaijani','ba'=>'Bashkir','bal'=>'Baluchi','bcc'=>'Southern Balochi','be'=>'Belarusian','bg'=>'Bulgarian','bi'=>'Bislama','bm'=>'Bambara','bn'=>'Bengali','bo'=>'Tibetan','br'=>'Breton','bs'=>'Bosnian','ca'=>'Catalan','ce'=>'Chechen','ceb'=>'Cebuano','ch'=>'Chamorro','ckb'=>'Central Kurdish','co'=>'Corsican','cr'=>'Cree','cs'=>'Czech','cu'=>'Church Slavic','cv'=>'Chuvash','cy'=>'Welsh','da'=>'Danish','de'=>'German','dv'=>'Dhivehi','dz'=>'Dzongkha','ee'=>'Ewe','el'=>'Greek','en'=>'English','eo'=>'Esperanto','es'=>'Spanish','et'=>'Estonian','eu'=>'Basque','fa'=>'Persian','ff'=>'Fulah','fi'=>'Finnish','fj'=>'Fijian','fo'=>'Faroese','fr'=>'French','frp'=>'Arpitan','fuc'=>'Pulaar','fur'=>'Friulian','fy'=>'Western Frisian','ga'=>'Irish','gd'=>'Scottish Gaelic','gl'=>'Galician','gn'=>'Guarani','gsw'=>'Swiss German','gu'=>'Gujarati','gv'=>'Manx','ha'=>'Hausa','haw'=>'Hawaiian','haz'=>'Hazaragi','he'=>'Hebrew','hi'=>'Hindi','ho'=>'Hiri Motu','hr'=>'Croatian','ht'=>'Haitian','hu'=>'Hungarian','hy'=>'Armenian','hz'=>'Herero','ia'=>'Interlingua','id'=>'Indonesian','ie'=>'Interlingue','ig'=>'Igbo','ii'=>'Sichuan Yi','ik'=>'Inupiaq','io'=>'Ido','is'=>'Icelandic','it'=>'Italian','iu'=>'Inuktitut','ja'=>'Japanese','jv'=>'Javanese','ka'=>'Georgian','kab'=>'Kabyle','kg'=>'Kongo','ki'=>'Kikuyu','kj'=>'Kuanyama','kk'=>'Kazakh','kl'=>'Kalaallisut','km'=>'Central Khmer','kn'=>'Kannada','ko'=>'Korean','kr'=>'Kanuri','ks'=>'Kashmiri','ku'=>'Kurdish','kv'=>'Komi','kw'=>'Cornish','ky'=>'Kirghiz','la'=>'Latin','lb'=>'Luxembourgish','lg'=>'Ganda','li'=>'Limburgan','ln'=>'Lingala','lo'=>'Lao','lt'=>'Lithuanian','lu'=>'Luba-Katanga','lv'=>'Latvian','mg'=>'Malagasy','mh'=>'Marshallese','mi'=>'Maori','mk'=>'Macedonian','ml'=>'Malayalam','mn'=>'Mongolian','mr'=>'Marathi','ms'=>'Malay','mt'=>'Maltese','my'=>'Burmese','na'=>'Nauru','nb'=>'Norwegian Bokmål','nd'=>'North Ndebele','ne'=>'Nepali','ng'=>'Ndonga','nl'=>'Dutch','nn'=>'Norwegian Nynorsk','no'=>'Norwegian','nr'=>'South Ndebele','nv'=>'Navajo','ny'=>'Nyanja','oc'=>'Occitan (post 1500)','oj'=>'Ojibwa','om'=>'Oromo','or'=>'Oriya','ory'=>'Oriya (individual language)','os'=>'Ossetian','pa'=>'Panjabi','pi'=>'Pali','pl'=>'Polish','ps'=>'Pushto','pt'=>'Portuguese','qu'=>'Quechua','rhg'=>'Rohingya','rm'=>'Romansh','rn'=>'Rundi','ro'=>'Romanian','ru'=>'Russian','rue'=>'Rusyn','rup'=>'Macedo-Romanian','rw'=>'Kinyarwanda','sa'=>'Sanskrit','sah'=>'Yakut','sc'=>'Sardinian','sd'=>'Sindhi','se'=>'Northern Sami','sg'=>'Sango','sh'=>'Serbo-Croatian','si'=>'Sinhala','sk'=>'Slovak','sl'=>'Slovenian','sm'=>'Samoan','sn'=>'Shona','so'=>'Somali','sq'=>'Albanian','sr'=>'Serbian','ss'=>'Swati','st'=>'Southern Sotho','su'=>'Sundanese','sv'=>'Swedish','sw'=>'Swahili','szl'=>'Silesian','ta'=>'Tamil','te'=>'Telugu','tg'=>'Tajik','th'=>'Thai','ti'=>'Tigrinya','tk'=>'Turkmen','tl'=>'Tagalog','tn'=>'Tswana','to'=>'Tonga (Tonga Islands)','tr'=>'Turkish','ts'=>'Tsonga','tt'=>'Tatar','tw'=>'Twi','twd'=>'Twents','ty'=>'Tahitian','tzm'=>'Central Atlas Tamazight','ug'=>'Uighur','uk'=>'Ukrainian','ur'=>'Urdu','uz'=>'Uzbek','ve'=>'Venda','vi'=>'Vietnamese','vo'=>'Volapük','wa'=>'Walloon','wo'=>'Wolof','xh'=>'Xhosa','xmf'=>'Mingrelian','yi'=>'Yiddish','yo'=>'Yoruba','za'=>'Zhuang','zh'=>'Chinese','zu'=>'Zulu');

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,5 @@
<?php
/**
* Compiled data. Do not edit.
*/
return array('ak'=>1,'am'=>1,'ar'=>2,'ary'=>2,'be'=>3,'bm'=>4,'bo'=>4,'br'=>1,'bs'=>3,'cs'=>5,'cy'=>6,'dz'=>4,'ff'=>1,'fr'=>1,'ga'=>7,'gd'=>8,'gv'=>9,'hr'=>10,'id'=>4,'ii'=>4,'iu'=>11,'ja'=>4,'ka'=>4,'kk'=>4,'km'=>4,'kn'=>4,'ko'=>4,'kw'=>11,'ky'=>4,'ln'=>1,'lo'=>4,'lt'=>12,'lv'=>13,'mg'=>1,'mi'=>1,'mk'=>14,'ms'=>4,'mt'=>15,'my'=>4,'nr'=>4,'oc'=>1,'pl'=>16,'ro'=>17,'ru'=>3,'sa'=>11,'sg'=>4,'sk'=>5,'sl'=>18,'sm'=>4,'sr'=>3,'su'=>4,'th'=>4,'ti'=>1,'tl'=>1,'to'=>4,'tt'=>4,'ug'=>4,'uk'=>3,'vi'=>4,'wa'=>1,'wo'=>4,'yo'=>4,'zh'=>4,''=>array(0=>array(0=>'n != 1',1=>array(0=>'one',1=>'other')),1=>array(0=>'n > 1',1=>array(0=>'one',1=>'other')),2=>array(0=>'n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100 >= 3 && n%100<=10 ? 3 : n%100 >= 11 && n%100<=99 ? 4 : 5',1=>array(0=>'zero',1=>'one',2=>'two',3=>'few',4=>'many',5=>'other')),3=>array(0=>'(n%10==1 && n%100!=11 ? 0 : n%10 >= 2 && n%10<=4 &&(n%100<10||n%100 >= 20)? 1 : 2)',1=>array(0=>'one',1=>'few',2=>'other')),4=>array(0=>'0',1=>array(0=>'other')),5=>array(0=>'( n == 1 ) ? 0 : ( n >= 2 && n <= 4 ) ? 1 : 2',1=>array(0=>'one',1=>'few',2=>'other')),6=>array(0=>'n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n==3 ? 3 : n==6 ? 4 : 5',1=>array(0=>'zero',1=>'one',2=>'two',3=>'few',4=>'many',5=>'other')),7=>array(0=>'n==1 ? 0 : n==2 ? 1 : n<7 ? 2 : n<11 ? 3 : 4',1=>array(0=>'one',1=>'two',2=>'few',3=>'many',4=>'other')),8=>array(0=>'n==1||n==11 ? 0 : n==2||n==12 ? 1 :(n >= 3 && n<=10)||(n >= 13 && n<=19)? 2 : 3',1=>array(0=>'one',1=>'two',2=>'few',3=>'other')),9=>array(0=>'n%10==1 ? 0 : n%10==2 ? 1 : n%20==0 ? 2 : 3',1=>array(0=>'one',1=>'two',2=>'few',3=>'other')),10=>array(0=>'n%10==1 && n%100!=11 ? 0 : n%10 >= 2 && n%10<=4 &&(n%100<10||n%100 >= 20)? 1 : 2',1=>array(0=>'one',1=>'few',2=>'other')),11=>array(0=>'n == 1 ? 0 : n == 2 ? 1 : 2',1=>array(0=>'one',1=>'two',2=>'other')),12=>array(0=>'(n%10==1 && n%100!=11 ? 0 : n%10 >= 2 &&(n%100<10||n%100 >= 20)? 1 : 2)',1=>array(0=>'one',1=>'few',2=>'other')),13=>array(0=>'n % 10 == 1 && n % 100 != 11 ? 0 : n != 0 ? 1 : 2',1=>array(0=>'one',1=>'other',2=>'zero')),14=>array(0=>'( n % 10 == 1 && n % 100 != 11 ) ? 0 : 1',1=>array(0=>'one',1=>'other')),15=>array(0=>'(n==1 ? 0 : n==0||( n%100>1 && n%100<11)? 1 :(n%100>10 && n%100<20)? 2 : 3)',1=>array(0=>'one',1=>'few',2=>'many',3=>'other')),16=>array(0=>'(n==1 ? 0 : n%10 >= 2 && n%10<=4 &&(n%100<10||n%100 >= 20)? 1 : 2)',1=>array(0=>'one',1=>'few',2=>'other')),17=>array(0=>'(n==1 ? 0 :(((n%100>19)||(( n%100==0)&&(n!=0)))? 2 : 1))',1=>array(0=>'one',1=>'few',2=>'other')),18=>array(0=>'n%100==1 ? 0 : n%100==2 ? 1 : n%100==3||n%100==4 ? 2 : 3',1=>array(0=>'one',1=>'two',2=>'few',3=>'other'))));

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,180 @@
<?php
/*
Plugin Name: Loco Translate
Plugin URI: https://wordpress.org/plugins/loco-translate/
Description: Translate themes and plugins directly in WordPress
Author: Tim Whitlock
Version: 2.3.1
Author URI: https://localise.biz/wordpress/plugin
Text Domain: loco-translate
Domain Path: /languages/
*/
// disallow execution out of context
if( ! function_exists('is_admin') ){
return;
}
// legacy plugin should not be installed at the same time
if( function_exists('loco_require') ){
return;
}
/**
* Get absolute path to Loco primary plugin file
* @return string
*/
function loco_plugin_file(){
return __FILE__;
}
/**
* Get version of this plugin
* @return string
*/
function loco_plugin_version(){
return '2.3.1';
}
/**
* Get Loco plugin handle, used by WordPress to identify plugin as a relative path
* @return string probably "loco-translate/loco.php"
*/
function loco_plugin_self(){
static $handle;
isset($handle) or $handle = plugin_basename(__FILE__);
return $handle;
}
/**
* Get absolute path to plugin root directory
* @return string __DIR__
*/
function loco_plugin_root(){
static $root;
isset($root) or $root = dirname(__FILE__);
return $root;
}
/**
* Check whether currently running in debug mode
* @return bool
*/
function loco_debugging(){
return apply_filters('loco_debug', WP_DEBUG );
}
/**
* Whether currently processing an Ajax request
* @return bool
*/
function loco_doing_ajax(){
return defined('DOING_AJAX') && DOING_AJAX;
}
/**
* Evaluate a constant by name
* @param string
* @return mixed
*/
function loco_constant( $name ){
$value = defined($name) ? constant($name) : null;
// constant values will only be modified in tests
if( defined('LOCO_TEST') && LOCO_TEST ){
$value = apply_filters('loco_constant', $value, $name );
$value = apply_filters('loco_constant_'.$name, $value );
}
return $value;
}
/**
* Runtime inclusion of any file under plugin root
* @param string PHP file path relative to __DIR__
* @return mixed return value from included file
*/
function loco_include( $relpath ){
$path = loco_plugin_root().'/'.$relpath;
if( ! file_exists($path) ){
throw new Loco_error_Exception('File not found: '.$path);
}
return include $path;
}
/**
* Require dependant library once only
* @param string PHP file path relative to ./lib
* @return void
*/
function loco_require_lib( $path ){
require_once loco_plugin_root().'/lib/'.$path;
}
/**
* Check PHP extension required by Loco and load polyfill if needed
* @param string
* @return bool
*/
function loco_check_extension( $name ) {
static $cache = array();
if ( ! isset( $cache[$name] ) ) {
if ( extension_loaded($name) ) {
$cache[ $name ] = true;
}
else {
Loco_error_AdminNotices::warn( sprintf( __('Loco requires the "%s" PHP extension. Ask your hosting provider to install it','loco-translate'), $name ) );
$class = 'Loco_compat_'.ucfirst($name).'Extension.php';
$cache[$name] = class_exists($class);
}
}
return $cache[ $name ];
}
/**
* Class autoloader for Loco classes under src directory.
* e.g. class "Loco_foo_FooBar" wil be found in "src/foo/FooBar.php"
* Also does autoload for polyfills under "src/compat" if $name < 20 chars
*
* @internal
* @param string
* @return void
*/
function loco_autoload( $name ){
if( 'Loco_' === substr($name,0,5) ){
loco_include( 'src/'.strtr( substr($name,5), '_', '/' ).'.php' );
}
else if( ! isset($name{20}) && file_exists( $path = loco_plugin_root().'/src/compat/'.$name.'.php') ){
require $path;
}
}
spl_autoload_register( 'loco_autoload', false );
// provide safe directory for custom translations that won't be deleted during auto-updates
if( ! defined('LOCO_LANG_DIR') ){
define( 'LOCO_LANG_DIR', trailingslashit(loco_constant('WP_LANG_DIR')).'loco' );
}
// text domain loading helper for custom file locations. disable by setting constant empty
if( LOCO_LANG_DIR ){
new Loco_hooks_LoadHelper;
}
// initialize hooks for admin screens
if( is_admin() ){
new Loco_hooks_AdminHooks;
}

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<bundle name="Loco Translate" min-version="2.0.14">
<domain name="loco-translate">
<project name="Loco Translate" slug="loco-translate">
<source>
<directory>src</directory>
<directory>tpl</directory>
<file>loco.php</file>
</source>
<target>
<directory>languages</directory>
</target>
<template>
<file>languages/loco-translate.pot</file>
</template>
</project>
</domain>
<exclude>
<directory>tmp</directory>
<directory>lib</directory>
<directory>pub</directory>
<directory>test</directory>
</exclude>
</bundle>

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
form.loco-filter{float:right}@media only screen and (max-width: 1024px){table.wp-list-table a.row-title{max-width:100px}}

View File

@@ -0,0 +1 @@
form#loco-conf>div{overflow:visible;border-bottom:solid 1px #ccc;padding-top:2em}form#loco-conf>div h2{margin-top:0}form#loco-conf td.twin>div{float:left;clear:none;width:50%}form#loco-conf td .description:first-child{margin-top:0;margin-bottom:4px}form#loco-conf a.icon-del{display:block;float:right;z-index:99;color:#aaa;outline:none}form#loco-conf a.icon-del:hover{color:#c00}form#loco-conf>div:first-child a.icon-del{display:none}form#loco-conf p.description{color:#aaa;font-size:12px;text-indent:.25em}form#loco-conf tr:hover p.description{color:#666}form#loco-reset{position:absolute;bottom:0;right:0}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
#loco.wrap .notice-info dl{margin-top:0;display:inline-block}#loco.wrap .notice-info dl dt,#loco.wrap .notice-info dl dd{line-height:1.4em}#loco.wrap .notice-info dl dt{font-weight:bold;color:#555}#loco.wrap .notice-info dl dd{margin-left:0;margin-bottom:.8em}#loco.wrap .notice-info dl div.progress .l{display:none}

View File

@@ -0,0 +1 @@
#loco.wrap td.loco-not-active{color:#aaa}#loco.wrap div.loco-projects>h3{float:left}#loco.wrap div.loco-projects form.loco-filter{float:right;margin:1em 0}

View File

@@ -0,0 +1 @@
#loco.wrap .revisions-diff{padding:10px;min-height:20px}#loco.wrap table.diff{border-collapse:collapse}#loco.wrap table.diff td{white-space:nowrap;overflow:hidden;font:normal 12px/17px "Monaco","Menlo","Ubuntu Mono","Consolas","source-code-pro",monospace;padding:2px}#loco.wrap table.diff td>span{color:#aaa}#loco.wrap table.diff td>span:after{content:". "}#loco.wrap table.diff tbody{border-top:1px dashed #ccc}#loco.wrap table.diff tbody:first-child{border-top:none}#loco.wrap .revisions.loading .diff-meta{color:#eee}#loco.wrap .revisions.loading .loading-indicator span.spinner{visibility:visible;background:#fff url(../img/spin-modal.gif?v=2.3.1) center center no-repeat}#loco.wrap .revisions-meta{clear:both;padding:10px 12px;margin:0;position:relative;top:10px}#loco.wrap .revisions-meta .diff-meta{clear:none;float:left;width:50%;padding:0;min-height:auto}#loco.wrap .revisions-meta .diff-meta button{margin-top:5px}#loco.wrap .revisions-meta .diff-meta-current{float:right;text-align:right}#loco.wrap .revisions-meta time{color:#72777c}#loco.wrap .revisions-control-frame{margin:10px 0}#loco.wrap .revisions-diff-frame{margin-top:20px}

View File

@@ -0,0 +1 @@
form#loco-poinit .loco-locales fieldset{float:left;margin-right:2em}form#loco-poinit .loco-locales fieldset.disabled span.lang{visibility:hidden !important}form#loco-poinit .loco-locales fieldset span.nolang{background:#999}form#loco-poinit .loco-locales fieldset>label span{width:20px;text-align:center;display:inline-block;margin-right:4px}form#loco-poinit a.icon-help{color:#999;font-style:normal;text-decoration:none}form#loco-poinit a.icon-help:hover{color:#666}form#loco-poinit .form-table th{padding:15px 10px}form#loco-poinit .form-table tr:first-child td,form#loco-poinit .form-table tr:first-child th{padding-top:25px}form#loco-poinit label.for-disabled input{visibility:hidden}form#loco-poinit label.for-disabled .icon-lock{top:0;left:0;display:block;position:absolute;width:1em;font-size:14px;text-align:center;color:gray}

View File

@@ -0,0 +1 @@
.js #loco.wrap .loco-loading{min-height:100px;background:#fff url(../img/spin-modal.gif?v=2.3.1) center center no-repeat}.js #loco.wrap .loco-loading ol.msgcat{display:none}#loco.wrap #loco-po{padding-right:0;overflow:auto}#loco.wrap ol.msgcat{margin-left:3em;padding-top:1em;border-top:1px dashed #ccc}#loco.wrap ol.msgcat:first-child{padding-top:0;border-top:none}#loco.wrap ol.msgcat li{color:#aaa;margin:0;padding:0 0 0 1em;font:normal 12px/17px "Monaco","Menlo","Ubuntu Mono","Consolas","source-code-pro",monospace;border-left:1px solid #eee}#loco.wrap ol.msgcat li>*{color:#333;white-space:pre}#loco.wrap ol.msgcat li>.po-comment{color:#3cc200}#loco.wrap ol.msgcat li>.po-refs{color:#0073aa}#loco.wrap ol.msgcat li>.po-refs a{color:inherit;text-decoration:none}#loco.wrap ol.msgcat li>.po-refs a:hover{text-decoration:underline}#loco.wrap ol.msgcat li>.po-flags{color:#77904a}#loco.wrap ol.msgcat li>.po-flags em{font-style:normal}#loco.wrap ol.msgcat li>.po-word{color:#000}#loco.wrap ol.msgcat li>.po-junk{font-style:italic;color:#ccc}#loco.wrap ol.msgcat li>.po-string>span{color:#c931c7}#loco.wrap form.loco-filter{top:0;right:0;position:absolute}#loco.wrap .loco-invalid form.loco-filter input[type=text]:focus{border-color:#c00;-webkit-box-shadow:0 0 2px rgba(153,0,0,.5);-moz-box-shadow:0 0 2px rgba(153,0,0,.5);box-shadow:0 0 2px rgba(153,0,0,.5)}#loco.wrap .loco-invalid ol.msgcat{list-style-type:none}#loco.wrap .loco-invalid ol.msgcat li{color:#000}.loco-modal{min-width:90% !important;min-height:100px}#loco-po-ref ol li{color:#aaa;margin:0;white-space:pre;padding:0 0 0 1em;font:normal 12px/17px Consolas,Monaco,monospace;background:transparent;border-left:1px solid #eee}#loco-po-ref ol li code{margin:0;padding:0;display:inline;background:inherit}#loco-po-ref ol li.highlighted{color:#666;background-color:#f8eec7}#loco-po-ref ol li.highlighted code.T_CONSTANT_ENCAPSED_STRING{color:#c931c7}

View File

@@ -0,0 +1 @@
.wrap #loco-editor .is-table .wg-tr:nth-child(even){background-color:rgba(9,100,132,.05)}.wrap #loco-editor .wg-split-x>nav.wg-tabs>a.active,.wrap #loco-editor .is-table .wg-cols>div>div.selected{background-color:#096484}.wrap #loco-editor .is-editable>.wg-content>textarea:focus,.wrap #loco-editor .is-editable>.wg-content.has-focus .ace_scroller,.wrap #loco-editor .is-editable>.wg-content.has-focus .mce-content-body{border-color:#5b9dd9;-webkit-box-shadow:inset 0 0 .6em rgba(30,140,190,.8);-moz-box-shadow:inset 0 0 .6em rgba(30,140,190,.8);box-shadow:inset 0 0 .6em rgba(30,140,190,.8)}form button.loco-loading.button-primary[disabled]:before{background:transparent url(../../img/skins/blue/spin-primary-button.gif?v=2.3.1) 0 0 no-repeat !important}

View File

@@ -0,0 +1 @@
.wrap #loco-editor .is-table .wg-tr:nth-child(even){background-color:rgba(199,165,137,.05)}.wrap #loco-editor .wg-split-x>nav.wg-tabs>a.active,.wrap #loco-editor .is-table .wg-cols>div>div.selected{background-color:#c7a589}.wrap #loco-editor .is-editable>.wg-content>textarea:focus,.wrap #loco-editor .is-editable>.wg-content.has-focus .ace_scroller,.wrap #loco-editor .is-editable>.wg-content.has-focus .mce-content-body{border-color:#5b9dd9;-webkit-box-shadow:inset 0 0 .6em rgba(30,140,190,.8);-moz-box-shadow:inset 0 0 .6em rgba(30,140,190,.8);box-shadow:inset 0 0 .6em rgba(30,140,190,.8)}form button.loco-loading.button-primary[disabled]:before{background:transparent url(../../img/skins/coffee/spin-primary-button.gif?v=2.3.1) 0 0 no-repeat !important}

View File

@@ -0,0 +1 @@
.wrap #loco-editor .is-table .wg-tr:nth-child(even){background-color:rgba(163,183,69,.05)}.wrap #loco-editor .wg-split-x>nav.wg-tabs>a.active,.wrap #loco-editor .is-table .wg-cols>div>div.selected{background-color:#a3b745}.wrap #loco-editor .is-editable>.wg-content>textarea:focus,.wrap #loco-editor .is-editable>.wg-content.has-focus .ace_scroller,.wrap #loco-editor .is-editable>.wg-content.has-focus .mce-content-body{border-color:#5b9dd9;-webkit-box-shadow:inset 0 0 .6em rgba(30,140,190,.8);-moz-box-shadow:inset 0 0 .6em rgba(30,140,190,.8);box-shadow:inset 0 0 .6em rgba(30,140,190,.8)}form button.loco-loading.button-primary[disabled]:before{background:transparent url(../../img/skins/ectoplasm/spin-primary-button.gif?v=2.3.1) 0 0 no-repeat !important}

View File

@@ -0,0 +1 @@
.wrap #loco-editor .is-table .wg-tr:nth-child(even){background-color:rgba(136,136,136,.05)}.wrap #loco-editor .wg-split-x>nav.wg-tabs>a.active,.wrap #loco-editor .is-table .wg-cols>div>div.selected{background-color:#888}.wrap #loco-editor .is-editable>.wg-content>textarea:focus,.wrap #loco-editor .is-editable>.wg-content.has-focus .ace_scroller,.wrap #loco-editor .is-editable>.wg-content.has-focus .mce-content-body{border-color:#5b9dd9;-webkit-box-shadow:inset 0 0 .6em rgba(30,140,190,.8);-moz-box-shadow:inset 0 0 .6em rgba(30,140,190,.8);box-shadow:inset 0 0 .6em rgba(30,140,190,.8)}form button.loco-loading.button-primary[disabled]:before{background:transparent url(../../img/skins/light/spin-primary-button.gif?v=2.3.1) 0 0 no-repeat !important}

View File

@@ -0,0 +1 @@
.wrap #loco-editor .is-table .wg-tr:nth-child(even){background-color:rgba(225,77,67,.05)}.wrap #loco-editor .wg-split-x>nav.wg-tabs>a.active,.wrap #loco-editor .is-table .wg-cols>div>div.selected{background-color:#e14d43}.wrap #loco-editor .is-editable>.wg-content>textarea:focus,.wrap #loco-editor .is-editable>.wg-content.has-focus .ace_scroller,.wrap #loco-editor .is-editable>.wg-content.has-focus .mce-content-body{border-color:#5b9dd9;-webkit-box-shadow:inset 0 0 .6em rgba(30,140,190,.8);-moz-box-shadow:inset 0 0 .6em rgba(30,140,190,.8);box-shadow:inset 0 0 .6em rgba(30,140,190,.8)}form button.loco-loading.button-primary[disabled]:before{background:transparent url(../../img/skins/midnight/spin-primary-button.gif?v=2.3.1) 0 0 no-repeat !important}

View File

@@ -0,0 +1 @@
.wrap #loco-editor .is-table .wg-tr:nth-child(even){background-color:rgba(158,186,160,.05)}.wrap #loco-editor .wg-split-x>nav.wg-tabs>a.active,.wrap #loco-editor .is-table .wg-cols>div>div.selected{background-color:#9ebaa0}.wrap #loco-editor .is-editable>.wg-content>textarea:focus,.wrap #loco-editor .is-editable>.wg-content.has-focus .ace_scroller,.wrap #loco-editor .is-editable>.wg-content.has-focus .mce-content-body{border-color:#5b9dd9;-webkit-box-shadow:inset 0 0 .6em rgba(30,140,190,.8);-moz-box-shadow:inset 0 0 .6em rgba(30,140,190,.8);box-shadow:inset 0 0 .6em rgba(30,140,190,.8)}form button.loco-loading.button-primary[disabled]:before{background:transparent url(../../img/skins/ocean/spin-primary-button.gif?v=2.3.1) 0 0 no-repeat !important}

View File

@@ -0,0 +1 @@
.wrap #loco-editor .is-table .wg-tr:nth-child(even){background-color:rgba(221,130,59,.05)}.wrap #loco-editor .wg-split-x>nav.wg-tabs>a.active,.wrap #loco-editor .is-table .wg-cols>div>div.selected{background-color:#dd823b}.wrap #loco-editor .is-editable>.wg-content>textarea:focus,.wrap #loco-editor .is-editable>.wg-content.has-focus .ace_scroller,.wrap #loco-editor .is-editable>.wg-content.has-focus .mce-content-body{border-color:#5b9dd9;-webkit-box-shadow:inset 0 0 .6em rgba(30,140,190,.8);-moz-box-shadow:inset 0 0 .6em rgba(30,140,190,.8);box-shadow:inset 0 0 .6em rgba(30,140,190,.8)}form button.loco-loading.button-primary[disabled]:before{background:transparent url(../../img/skins/sunrise/spin-primary-button.gif?v=2.3.1) 0 0 no-repeat !important}

Binary file not shown.

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 B

View File

@@ -0,0 +1,83 @@
/**
* Script for bundle configuration screen
*/
!function( window, document, $ ){
// utility for scrolling
function elementTop( el, ancestor ){
var y = el.offsetTop;
while( ( el = el.offsetParent ) && el !== ancestor ){
y += el.offsetTop;
}
return y;
}
// form duplicator for adding another project
function addProject(){
var $divs = $('#loco-conf > div'),
$copy = $divs.eq(0).clone(),
index = $divs.length,
id = 'loco-conf-'+index,
ns = '['+index+']'
;
function clearField( i, input ){
var name = input.name.replace('[0]',ns);
$(input).attr('name', name ).val('');
}
$copy.attr('id', 'loco-conf-'+index );
$copy.find('input').each( clearField );
$copy.find('textarea').each( clearField );
//$copy.find('div.notice').remove();
// TODO translations of this:
$copy.find('h2').eq(0).html('New set <span>(untitled)</span>');
$copy.insertBefore('#loco-form-foot');
createClickRemove( $copy.find('a.icon-del'), index );
// scroll to $copy
$copy.hide().slideDown( 500 );
$('html, body').animate( { scrollTop: elementTop($copy[0]) }, 500 );
}
function createClickRemove( $el, index ){
return $el.click( function(event){
event.preventDefault();
delProject( index );
return false;
} );
}
// remove whole set from form
function delProject( index ){
var $div = $('#loco-conf-'+index),
$fld = $div.find('input[name="conf['+index+'][removed]"]')
;
// setting removed flag saves having to re-index all sets. back end will ignore it.
$fld.val('1');
$div.slideUp( 500, function(){ $(this).hide().find('table').remove(); } );
}
// enable project removal from initial blocks
$('#loco-conf > div').each( function( index, div ){
createClickRemove( $(div).find('a.icon-del'), index );
} );
// enable project addition via button in footer
$('#loco-add-butt').attr('disabled',false).click( function(event){
event.preventDefault();
addProject();
return false;
} );
}( window, document, jQuery );

View File

@@ -0,0 +1,44 @@
/**
* Run Javascript diagnostics
*/
!function( loco, $ ){
// check tick symbol is readable by JavaScript as UTF-8
// E2\x9C\x93 => 0x2713
var span = $('#loco-utf8-check'),
tick = span[0].textContent
;
if( 1 !== tick.length || 0x2713 !== tick.charCodeAt(0) ){
loco.notices.warn("This page has a problem rendering UTF-8").stick();
}
// show custom endpoint if set in global
if( window.ajaxurl ){
$('#loco-ajax-url').text( window.ajaxurl );
}
// check UTF-8 passes to Ajax and back without a problem
function onAjaxTestPass( data, status, xhr ){
if( data && data.ping ){
$('#loco-ajax-check').text( data.ping );
}
else {
onAjaxTestFail( xhr, status, data && data.error && data.error.message );
}
}
function onAjaxTestFail( xhr, error, message ){
if( 'success' !== error ){
message = loco.ajax.parse( loco.ajax.strip( xhr.responseText ) );
}
$('#loco-ajax-check').text( 'FAILED: '+message ).addClass('loco-danger');
}
loco.ajax.post( 'ping', {echo:'\u039F\u039A \u2713'}, onAjaxTestPass, onAjaxTestFail );
}( window.locoScope, window.jQuery );

View File

@@ -0,0 +1,13 @@
/**
* Script for file delete operation
*/
!function( window, document, $ ){
var fsHook = document.getElementById('loco-fs'),
elForm = document.getElementById('loco-del')
;
if( fsHook && elForm ){
window.locoScope.fs.init(fsHook).setForm(elForm);
}
}( window, document, jQuery );

View File

@@ -0,0 +1,586 @@
/**
* Script for PO file editor pages
*/
!function( window, $ ){
var loco = window.locoScope,
conf = window.locoConf,
syncParams = null,
saveParams = null,
ajaxUpload = conf.multipart,
// UI translation
translator = loco.l10n,
sprintf = loco.string.sprintf,
// PO file data
locale = conf.locale,
messages = loco.po.init( locale ).wrap( conf.powrap ),
template = ! locale,
// form containing action buttons
elForm = document.getElementById('loco-actions'),
filePath = conf.popath,
syncPath = conf.potpath,
// file system connect when file is locked
elFilesys = document.getElementById('loco-fs'),
fsConnect = elFilesys && loco.fs.init( elFilesys ),
// prevent all write operations if readonly mode
readonly = conf.readonly,
editable = ! readonly,
// Editor components
editor,
saveButton,
innerDiv = document.getElementById('loco-editor-inner')
;
// warn if ajax uploads are enabled but not supported
if( ajaxUpload && ! ( window.FormData && window.Blob ) ){
ajaxUpload = false;
loco.notices.warn("Your browser doesn't support Ajax file uploads. Falling back to standard postdata");
}
/**
*
*/
function doSyncAction( callback ){
function onSuccess( result ){
var info = [],
doc = messages,
exp = result.po,
src = result.pot,
pot = loco.po.init().load( exp ),
done = doc.merge( pot ),
nadd = done.add.length,
ndel = done.del.length,
t = translator;
// reload even if unchanged, cos indexes could be off
editor.load( doc );
// Show summary
if( nadd || ndel ){
if( src ){
// Translators: Where %s is the name of the POT template file. Message appears after sync
info.push( sprintf( t._('Merged from %s'), src ) );
}
else {
// Translators: Message appears after sync operation
info.push( t._('Merged from source code') );
}
// Translators: Summary of new strings after running in-editor Sync
nadd && info.push( sprintf( t._n('1 new string added','%s new strings added', nadd ), nadd ) );
// Translators: Summary of existing strings that no longer exist after running in-editor Sync
ndel && info.push( sprintf( t._n('1 obsolete string removed','%s obsolete strings removed', ndel ), ndel ) );
// editor thinks it's saved, but we want the UI to appear otherwise
$(innerDiv).trigger('poUnsaved',[]);
updateStatus();
// debug info in lieu of proper merge confirmation:
window.console && debugMerge( console, done );
}
else if( src ){
// Translators: Message appears after sync operation when nothing has changed. %s refers to a POT file.
info.push( sprintf( t._('Already up to date with %s'), src ) );
}
else {
// Translators: Message appears after sync operation when nothing has changed
info.push( t._('Already up to date with source code') );
}
loco.notices.success( info.join('. ') );
$(innerDiv).trigger('poMerge',[result]);
// done sync
callback && callback();
}
loco.ajax.post( 'sync', syncParams, onSuccess, callback );
}
function debugMerge( console, result ){
var i = -1, t = result.add.length;
while( ++i < t ){
console.log(' + '+result.add[i].source() );
}
i = -1, t = result.del.length;
while( ++i < t ){
console.log(' - '+result.del[i].source() );
}
}
/**
* @param params {Object}
* @return FormData
*/
function initMultiPart( params ){
var p, data = new FormData;
for( p in params ){
if( params.hasOwnProperty(p) ) {
data.append(p, params[p]);
}
}
return data;
}
/**
* Post full editor contents to "posave" endpoint
*/
function doSaveAction( callback ){
function onSuccess( result ){
callback && callback();
editor.save( true );
// Update saved time update
$('#loco-po-modified').text( result.datetime||'[datetime error]' );
}
var postData = $.extend( {locale:String(messages.locale()||'')}, saveParams||{} );
if( fsConnect ){
fsConnect.applyCreds(postData);
}
// submit PO as concrete file if configured
if( ajaxUpload ){
postData = initMultiPart(postData);
postData.append('po', new Blob([String(messages)],{type:'application/x-gettext'}), String(postData.path).split('/').pop()||'untitled.po' );
}
else {
postData.data = String(messages);
}
loco.ajax.post( 'save', postData, onSuccess, callback );
}
function saveIfDirty(){
editor.dirty && doSaveAction();
}
function onUnloadWarning(){
// Translators: Warning appears when user tries to refresh or navigate away when editor work is unsaved
return translator._("Your changes will be lost if you continue without saving");
}
function registerSaveButton( button ){
saveButton = button;
// enables and disable according to save/unsave events
editor
.on('poUnsaved', function(){
enable();
$(button).addClass( 'button-primary loco-flagged' );
} )
.on('poSave', function(){
disable();
$(button).removeClass( 'button-primary loco-flagged' );
} )
;
function disable(){
button.disabled = true;
}
function enable(){
button.disabled = false;
}
function think(){
disable();
$(button).addClass('loco-loading');
}
function unthink(){
enable();
$(button).removeClass('loco-loading');
}
saveParams = $.extend( { path: filePath }, conf.project||{} );
$(button).click( function(event){
event.preventDefault();
think();
doSaveAction( unthink );
return false;
} );
return true;
};
function registerSyncButton( button ){
var project = conf.project;
if( project ){
function disable(){
button.disabled = true;
}
function enable(){
button.disabled = false;
}
function think(){
disable();
$(button).addClass('loco-loading');
}
function unthink(){
enable();
$(button).removeClass('loco-loading');
}
// Only permit sync when document is saved
editor
.on('poUnsaved', function(){
disable();
} )
.on('poSave', function(){
enable();
} )
;
// params for sync end point
syncParams = {
bundle: project.bundle,
domain: project.domain,
type: template ? 'pot' : 'po',
sync: syncPath||''
};
// enable syncing on button click
$(button)
.click( function(event){
event.preventDefault();
think();
doSyncAction( unthink );
return false;
} )
//.attr('title', syncPath ? sprintf( translator._('Update from %s'), syncPath ) : translator._('Update from source code') )
;
enable();
}
return true;
}
function registerFuzzyButton( button ){
var toggled = false,
enabled = false
;
function redraw( message, state ){
// fuzziness only makes sense when top-level string is translated
var allowed = message && message.translated(0) || false;
if( enabled !== allowed ){
button.disabled = ! allowed;
enabled = allowed;
}
// toggle on/off according to new fuzziness
if( state !== toggled ){
$(button)[ state ? 'addClass' : 'removeClass' ]('inverted');
toggled = state;
}
}
// state changes depending on whether an asset is selected and is fuzzy
editor
.on('poSelected', function( event, message ){
redraw( message, message && message.fuzzy() || false );
} )
.on( 'poEmpty', function( event, blank, message, pluralIndex ){
if( 0 === pluralIndex && blank === enabled ){
redraw( message, toggled );
}
} )
.on( 'poFuzzy', function( event, message, newState ){
redraw( message, newState );
} )
;
// click toggles current state
$(button).click( function( event ){
event.preventDefault();
editor.fuzzy( ! editor.fuzzy() );
return false;
} );
return true;
};
function registerRevertButton( button ){
// No need for revert when document is saved
editor
.on('poUnsaved', function(){
button.disabled = false;
} )
.on('poSave', function(){
button.disabled = true;
} )
;
// handling unsaved state prompt with onbeforeunload, see below
$(button).click( function( event ){
event.preventDefault();
location.reload();
return false;
} );
return true;
};
function registerInvisiblesButton( button ){
var $button = $(button);
button.disabled = false;
editor.on('poInvs', function( event, state ){
$button[ state ? 'addClass' : 'removeClass' ]('inverted');
});
$button.click( function( event ){
event.preventDefault();
editor.setInvs( ! editor.getInvs() );
return false;
} );
locoScope.tooltip.init($button);
return true;
}
function registerCodeviewButton( button ){
var $button = $(button);
button.disabled = false;
$button.click( function(event){
event.preventDefault();
var state = ! editor.getMono();
editor.setMono( state );
$button[ state ? 'addClass' : 'removeClass' ]('inverted');
return false;
} );
locoScope.tooltip.init($button);
return true;
};
function registerAddButton( button ){
button.disabled = false;
$(button).click( function( event ){
event.preventDefault();
// Need a placeholder guaranteed to be unique for new items
var i = 1, baseid, msgid, regex = /(\d+)$/;
msgid = baseid = 'New message';
while( messages.get( msgid ) ){
i = regex.exec(msgid) ? Math.max(i,RegExp.$1) : i;
msgid = baseid+' '+( ++i );
}
editor.add( msgid );
return false;
} );
return true;
};
function registerDelButton( button ){
button.disabled = false;
$(button).click( function(event){
event.preventDefault();
editor.del();
return false;
} );
return true;
};
function registerDownloadButton( button, id ){
button.disabled = false;
$(button).click( function( event ){
var form = button.form,
path = filePath;
// swap out path
if( 'binary' === id ){
path = path.replace(/\.po$/,'.mo');
}
form.path.value = path;
form.source.value = messages.toString();
// allow form to submit
return true;
} );
return true;
}
// event handler that stops dead
function noop( event ){
event.preventDefault();
return false;
}
/*/ dummy function for enabling buttons that do nothing (or do something inherently)
function registerNoopButton( button ){
return true;
}*/
/**
* Update status message above editor.
* This is dynamic version of PHP Loco_gettext_Metadata::getProgressSummary
* TODO implement progress bar, not just text.
*/
function updateStatus(){
var t = translator,
stats = editor.stats(),
total = stats.t,
fuzzy = stats.f,
empty = stats.u,
// Translators: Shows total string count at top of editor
stext = sprintf( t._n('1 string','%s strings',total ), total.format(0) ),
extra = [];
if( locale ){
// Translators: Shows percentage translated at top of editor
stext = sprintf( t._('%s%% translated'), stats.p.replace('%','') ) +', '+ stext;
// Translators: Shows number of fuzzy strings at top of editor
fuzzy && extra.push( sprintf( t._('%s fuzzy'), fuzzy.format(0) ) );
// Translators: Shows number of untranslated strings at top of editor
empty && extra.push( sprintf( t._('%s untranslated'), empty.format(0) ) );
if( extra.length ){
stext += ' ('+extra.join(', ')+')';
}
}
$('#loco-po-status').text( stext );
}
/**
* Enable text filtering
*/
function initSearchFilter( elSearch ){
editor.searchable( loco.fulltext.init() );
// prep search text field
elSearch.disabled = false;
elSearch.value = '';
function showValidFilter( numFound ){
$(elSearch.parentNode)[ numFound || null == numFound ? 'removeClass' : 'addClass' ]('invalid');
}
var listener = loco.watchtext( elSearch, function( value ){
var numFound = editor.filter( value, true );
showValidFilter( numFound );
} );
editor
.on( 'poFilter', function( event, value, numFound ){
listener.val( value||'' );
showValidFilter( numFound );
} )
.on( 'poMerge', function( event, result ){
var value = listener.val();
value && editor.filter( value );
} )
;
}
// resize function fits editor to screen, accounting for headroom and touching bottom of screen.
var resize = function(){
function top( el, ancestor ){
var y = el.offsetTop||0;
while( ( el = el.offsetParent ) && el !== ancestor ){
y += el.offsetTop||0;
}
return y;
}
var fixHeight,
minHeight = parseInt($(innerDiv).css('min-height')||0)
;
return function(){
var padBottom = 20,
topBanner = top( innerDiv, document.body ),
winHeight = window.innerHeight,
setHeight = Math.max( minHeight, winHeight - topBanner - padBottom )
;
if( fixHeight !== setHeight ){
innerDiv.style.height = String(setHeight)+'px';
fixHeight = setHeight;
}
};
}();
// ensure outer resize is handled before editor's internal resize
resize();
$(window).resize( resize );
// initialize editor
innerDiv.innerHTML = '';
editor = loco.po.ed
.init( innerDiv )
.localise( translator )
;
loco.po.kbd
.init( editor )
.add( 'save', saveIfDirty )
.enable('copy','clear','enter','next','prev','fuzzy','save','invis')
;
// initialize toolbar button actions
var buttons = {
// help: registerNoopButton,
save: editable && registerSaveButton,
sync: editable && registerSyncButton,
revert: registerRevertButton,
// editor mode togglers
invs: registerInvisiblesButton,
code: registerCodeviewButton,
// downloads / post-throughs
source: registerDownloadButton,
binary: template ? null : registerDownloadButton
};
// POT only
if( template ){
buttons.add = editable && registerAddButton;
buttons.del = editable && registerDelButton;
}
// PO only
else {
buttons.fuzzy = registerFuzzyButton;
};
$('#loco-toolbar').find('button').each( function(i,el){
var id = el.getAttribute('data-loco'), register = buttons[id];
register && register(el,id) || $(el).hide();
} );
// disable submit on dummy form
$(elForm).submit( noop );
// enable text filtering
initSearchFilter( document.getElementById('loco-search') );
// editor event behaviours
editor
.on('poUnsaved', function(){
window.onbeforeunload = onUnloadWarning;
} )
.on('poSave', function(){
updateStatus();
window.onbeforeunload = null;
} )
.on( 'poUpdate', updateStatus );
;
// load raw message data
messages.load( conf.podata );
// ready to render editor
editor.load( messages );
// locale should be cast to full object once set in editor
if( locale = editor.targetLocale ){
locale.isRTL() && $(innerDiv).addClass('trg-rtl');
}
// enable template mode when no target locale
else {
editor.unlock();
}
// ok, editor ready
updateStatus();
// clean up
delete window.locoConf;
conf = buttons = null;
}( window, jQuery );

View File

@@ -0,0 +1,242 @@
(function(v,z,h,S){var n=function(){var c={};return{register:function(f,h){c[f]=h},require:function(f,h){var p=c[f];if(!p)throw Error('CommonJS error: failed to require("'+h+'")');return p}}}();n.register("$1",function(c,f,h){function p(b){var e=typeof b;if("string"===e)if(/[^ <>!=()%^&|?:n0-9]/.test(b))console.error("Invalid plural: "+b);else return new Function("n","return "+b);"function"!==e&&(b=function(a){return 1!=a});return b}c.init=function(b){function e(d,b,e){return(d=a[d])&&d[e]?d[e]:b||
""}b=p(b);var a={};return{_:function(a){return e(a,a,0)},_x:function(a,b){return e(b+"\u0004"+a,a,0)},_n:function(a,m,k){k=Number(b(k));isNaN(k)&&(k=0);return e(a,k?m:a,k)},load:function(d){a=d||{};return this},pluraleq:function(a){b=p(a);return this}}};return c}({},v,z));n.register("$2",function(c,f,h){Array.prototype.indexOf||(Array.prototype.indexOf=function(c){if(null==this)throw new TypeError;var b,e=Object(this),a=e.length>>>0;if(0===a)return-1;b=0;1<arguments.length&&(b=Number(arguments[1]),
b!=b?b=0:0!=b&&Infinity!=b&&-Infinity!=b&&(b=(0<b||-1)*Math.floor(Math.abs(b))));if(b>=a)return-1;for(b=0<=b?b:Math.max(a-Math.abs(b),0);b<a;b++)if(b in e&&e[b]===c)return b;return-1});return c}({},v,z));n.register("$3",function(c,f,h){c.trim=function(c,b){for(b||(b=" \n");c&&-1!==b.indexOf(c.substr(0,1));)c=c.substr(1);for(;c&&-1!==b.indexOf(c.substr(-1));)c=c.substr(0,c.length-1);return c};c.sprintf=function(c){var b=0,e,a=[].slice.call(arguments,1);return c.replace(/%(s|u|%)/g,function(d,m){if("%"===
m)return"%";e=a[b++];return String(e)||""})};return c}({},v,z));n.register("$48",function(c,f,h){function p(){var b=a;d&&(b="("+d+") "+b);h.title=b}function b(d){d||(d=e);d!==a&&(a=d,p())}var e=h.title,a=e,d=0;c.set=function(a){b(a);return c};c.get=function(){return a};c.replace=function(d,e){b(a.replace(d,e));return c};c.badge=function(a){isNaN(a=Number(a))&&(a=0);a!==d&&(d=a,p());return c};return c}({},v,z));n.register("$42",function(c,f,h){function p(d){a||f._gat&&(a=_gat._createTracker(b,"loco"));
if(a){var m=d.shift();a[m].apply(a,d)}else e&&e.push(d);return c}var b,e,a;c._init=function(a){if(b=a.code){e=f._gaq||(f._gaq=[]);e.push(["_setAccount",b]);e.push(["_gat._anonymizeIp"]);e.push(["_setDomainName",a.host]);e.push(["_trackPageview"]);a=h.createElement("script");a.type="text/javascript";a.async=!0;a.src=("https:"==h.location.protocol?"https://ssl":"http://www")+".google-analytics.com/ga.js";var m=h.getElementsByTagName("script")[0];m.parentNode.insertBefore(a,m)}return c};c.event=function(a,
b,e,g){return p(["_trackEvent",a||"",b||"",e||"",g||0])};c.page=function(a,b){var e={page:a||location.pathname+location.hash,title:b||n.require("$48","title.js").get()};return p(["_trackPageview",e])};return c}({},v,z));n.register("$43",function(c,f,h){function p(a,d){if(b)b[a](d);else f.ga&&ga(a,d);return c}var b,e;c._init=function(a){a.code&&(function(a,b,e,g,l,A,c){a.GoogleAnalyticsObject=l;a[l]=a[l]||function(){(a[l].q=a[l].q||[]).push(arguments)};a[l].l=1*new Date;A=b.createElement(e);c=b.getElementsByTagName(e)[0];
A.async=1;A.src=g;c.parentNode.insertBefore(A,c)}(f,h,"script","//www.google-analytics.com/analytics.js","ga"),ga("create",a.code,{alwaysSendReferrer:!0,userId:a.uid}),ga("set","anonymizeIp",!0),a.custom&&p("set",a.custom),c.page(),ga(function(a){b=a}));return c};c.event=function(a,d,b,e){return p("send",{hitType:"event",eventCategory:a||"",eventAction:d||"",eventLabel:b||"",eventValue:Number(e||0)})};c.page=function(a,d){var b={hitType:"pageview",page:a||location.pathname+location.hash,title:d||
n.require("$48","title.js").get()};b.location=location.protocol+"//"+location.hostname+b.page;e&&p("set",{referrer:e});e=b.location;return p("send",b)};c.reset=function(){e=location.href;var a={page:location.pathname+location.hash,title:n.require("$48","title.js").get(),location:e};p("set",a);return c};return c}({},v,z));n.register("$31",function(c,f,D){function p(a,e){h(a).click(function(a){b&&b.event(e,"click",this.getAttribute("href")||"");return!0});a=null;return c}var b,e=location.hostname,a=
"/help"===location.pathname.substr(0,5);c.init=function(a){!b&&a&&(e=a.host||(a.host=e),b=a.legacy?n.require("$42","legacy.js"):n.require("$43","universal.js"),b._init(a));return c};c.link=function(d){for(var b=d.getAttribute("href");b&&"#"!==b;){if(0===b.indexOf("#"))return p(d,"anchor");if("/help"===b.substr(0,5)&&!a)d.setAttribute("target","_blank");else if(0===b.indexOf("http")||0===b.indexOf("//")){if(-1!==b.indexOf(e)&&/^(https?:)*\/\/([^\/]+)/.exec(b)&&e===RegExp.$2)break;d.setAttribute("target",
"_blank");p(d,"external")}break}return c};c.page=function(){b&&b.page.apply(b,arguments);return c};c.event=function(){b&&b.event.apply(b,arguments);return c};c.reset=function(){b&&b.reset&&b.reset();return c};return c}({},v,z));n.register("$36",function(c,f,n){function p(a){return 27===a.keyCode&&r&&w?(I(),a.preventDefault(),!1):!0}function b(){if(r){var a=f.innerWidth,d=f.innerHeight;a!==H&&(H=a,K(u));d!==y&&(y=d,s&&e())}return!0}function e(){var a=m.outerHeight(!0)-s.clientHeight;s.style.maxHeight=
String(y-a)+"px"}function a(a){a?(l.show(),k.addClass("has-title")):(l.hide(),k.removeClass("has-title"))}var d,m,k,g,l,A,B,s,r=!1,w=!1,q=!1,C,E,H,u,y,F=c.init=function(){if(!d){d=h('<div id="overlay"></div>');m=h('<div class="overlay-frame"></div>');k=h('<div class="overlay-container"></div>');l=h('<div class="overlay-title"><span class="icon"></span><span class="title">Untitled</span></div>');A=h("<nav></nav>");B=h('<a class="overlay-close" href="#"><span>x</span></a>');g=h('<div class="overlay-bg"></div>');
d.append(m.append(k)).append(g).prependTo(n.body);h(n).on("keydown",p);h(f).resize(b);H=f.innerWidth;y=f.innerHeight;E=k.outerWidth(!0)-k.width()+(m.innerWidth()-m.width());C=parseInt(k.css("width"));if(!C||isNaN(C))C=k.width();A.hide().prependTo(m);l.append(B.hide()).hide().prependTo(m);d.hide()}return d},K=c.width=function(a){var d=F();if(null===a)m.css("width",""),k.css("width","");else{a=a||C||640;x=a+E;u=a;var g=H;x>g?(x=g,a=x-E,d.addClass("spill")):d.removeClass("spill");m.css("width",x+"px");
k.css("width",a+"px")}return c};c.autoSize=function(){var a=F(),d=C||0;k.children().each(function(a,g){d=Math.max(d,h(g).outerWidth(!0))});K(d);if(s=a.find("div.overlay-scroll")[0])y=f.innerHeight,e();return c};c.css=function(a){F().attr("class",a);return c};c.think=function(){F().addClass("loading");return c};c.unthink=function(){F().removeClass("loading");return c};c.html=function(a){F();f.innerShiv&&(a=innerShiv(a,!1));return k.html(a)};c.append=function(a){F();a instanceof jQuery||(a=h(a));k.append(a);
return c};var I=c.close=function(a){if(r){var g=function(){F().hide();h(n.body).removeClass("has-overlay");r=!1;k.html("");r=null;d.trigger("overlayClosed",[c])};null==a&&(a=300);d.trigger("overlayClosing",[c]);a?d.fadeOut(a,g):g()}return c};c.title=function(d,g){F();var b="",e=l.find("span");g&&/^lang lang-(\w+)/.exec(g)&&(b=RegExp.$1);e.eq(0).attr("class",g||"no-icon").attr("lang",b);q=d||"";e.eq(1).text(q);null!=d?a(!0):w||a(!1);return c};c.enableClose=function(){F();w=!0;B.off("click").on("click",
function(){I();return!1});a(!0);B.show();return c};c.disableClose=function(){F();w=!1;B.hide();r&&q||a(!1);return c};c.enableNav=function(a){A.append(a);A.show();return c};c.disableNav=function(){A.html("").hide();return c};c.open=function(){F();k.html("");K(C);d.attr("class","");h(n.body).addClass("has-overlay");F().show();r||(r=!0,b());c.title(null);w&&a(!0);d.trigger("overlayOpened",[c]);return c};c.active=function(){return r};c.listen=function(a){F().on("overlayClosed",a);return c};c.unlisten=
function(a){F().off("overlayClosed",a);return c};return c}({},v,z));n.register("$11",function(c,f,n){function p(a,d){this.$element=h(a);this.options=d;this.enabled=!0;this.fixTitle()}c.init=function(a,d){var m={fade:!0,offset:5,delayIn:b,delayOut:e,anchor:a.attr("data-anchor"),gravity:a.attr("data-gravity")||"s"};d&&(m=h.extend({},m,d));a.tipsy(m)};c.delays=function(a,d){b=a||150;e=d||100};c.kill=function(){h("div.tipsy").remove()};c.text=function(a,d){d.data("tipsy").setTitle(a)};var b,e;c.delays();
h(n.body).on("overlayOpened overlayClosing",function(a){c.kill();return!0});p.prototype={show:function(){var a=this.getTitle();if(a&&this.enabled){var d=this.tip();d.find(".tipsy-inner")[this.options.html?"html":"text"](a);d[0].className="tipsy";d.remove().css({top:0,left:0}).prependTo(n.body);var a=(a=this.options.anchor)?this.$element.find(a):this.$element,a=h.extend({},a.offset(),{width:a[0].offsetWidth,height:a[0].offsetHeight}),b=d[0].offsetWidth,e=d[0].offsetHeight,g="function"==typeof this.options.gravity?
this.options.gravity.call(this.$element[0]):this.options.gravity,l;switch(g.charAt(0)){case "n":l={top:a.top+a.height+this.options.offset,left:a.left+a.width/2-b/2};break;case "s":l={top:a.top-e-this.options.offset,left:a.left+a.width/2-b/2};break;case "e":l={top:a.top+a.height/2-e/2,left:a.left-b-this.options.offset};break;case "w":l={top:a.top+a.height/2-e/2,left:a.left+a.width+this.options.offset}}2==g.length&&("w"==g.charAt(1)?l.left=a.left+a.width/2-15:l.left=a.left+a.width/2-b+15);d.css(l).addClass("tipsy-"+
g);d.find(".tipsy-arrow")[0].className="tipsy-arrow tipsy-arrow-"+g.charAt(0);this.options.className&&d.addClass("function"==typeof this.options.className?this.options.className.call(this.$element[0]):this.options.className);d.addClass("in")}},hide:function(){this.tip().remove()},fixTitle:function(){var a=this.$element,d=a.attr("title")||"";(d||"string"!==typeof a.attr("original-title"))&&a.attr("original-title",d).removeAttr("title")},getTitle:function(){var a,d=this.$element,b=this.options;this.fixTitle();
"string"==typeof b.title?a=d.attr("title"==b.title?"original-title":b.title):"function"==typeof b.title&&(a=b.title.call(d[0]));return(a=(""+a).replace(/(^\s*|\s*$)/,""))||b.fallback},setTitle:function(a){var d=this.$element;d.attr("default-title")||d.attr("default-title",this.getTitle());null==a&&(a=d.attr("default-title")||this.getTitle());d.attr("original-title",a);if(this.$tip)this.$tip.find(".tipsy-inner")[this.options.html?"html":"text"](a)},tip:function(){this.$tip||(this.$tip=h('<div class="tipsy"></div>').html('<div class="tipsy-arrow"></div><div class="tipsy-inner"></div>'),
this.$tip.data("tipsy-pointee",this.$element[0]));return this.$tip},validate:function(){this.$element[0].parentNode||(this.hide(),this.options=this.$element=null)},enable:function(){this.enabled=!0},disable:function(){this.hide();this.enabled=!1},toggleEnabled:function(){this.enabled=!this.enabled}};h.fn.tipsy=function(a){function d(d){var g=h.data(d,"tipsy");g||(g=new p(d,h.fn.tipsy.elementOptions(d,a)),h.data(d,"tipsy",g));return g}function b(){var g=d(this),e=a.delayIn;g.hoverState="in";0==e?g.show():
(g.fixTitle(),setTimeout(function(){"in"==g.hoverState&&g.show()},e))}function e(){var g=d(this),b=a.delayOut;g.hoverState="out";0==b?g.hide():(g.tip().removeClass("in"),setTimeout(function(){"out"==g.hoverState&&g.hide()},b))}a=h.extend({},h.fn.tipsy.defaults,a);a.live||this.each(function(){d(this)});if("manual"!=a.trigger){var g=a.live?"live":"bind",l="hover"==a.trigger?"mouseleave":"blur";this[g]("hover"==a.trigger?"mouseenter":"focus",b)[g](l,e)}return this};h.fn.tipsy.defaults={className:null,
delayIn:0,delayOut:0,fade:!1,fallback:"",gravity:"n",html:!1,live:!1,offset:0,opacity:0.8,title:"title",trigger:"hover",anchor:null};h.fn.tipsy.elementOptions=function(a,d){return h.metadata?h.extend({},d,h(a).metadata()):d};h.fn.tipsy.autoNS=function(){return h(this).offset().top>h(n).scrollTop()+h(f).height()/2?"s":"n"};h.fn.tipsy.autoWE=function(){return h(this).offset().left>h(n).scrollLeft()+h(f).width()/2?"e":"w"};h.fn.tipsy.autoBounds=function(a,d){return function(){var b=d[0],e=1<d.length?
d[1]:!1,g=h(n).scrollTop()+a,l=h(n).scrollLeft()+a,A=h(this);A.offset().top<g&&(b="n");A.offset().left<l&&(e="w");h(f).width()+h(n).scrollLeft()-A.offset().left<a&&(e="e");h(f).height()+h(n).scrollTop()-A.offset().top<a&&(b="s");return b+(e?e:"")}};return c}({},v,z));n.register("$8",function(c,f,n){c.listen=function(c,b){function e(){s[k?"show":"hide"]()}function a(a){B&&c.setAttribute("size",2+a.length);k=a;e();return a}function d(){g=null;b(k)}function m(){var b=c.value;A&&b===A&&(b="");b!==k&&
(g&&clearTimeout(g),a(b),l?g=setTimeout(d,l):d())}var k,g;c=c instanceof jQuery?c[0]:c;var l=150,A=f.attachEvent&&c.getAttribute("placeholder"),B=1===Number(c.size),s=h('<a href="#clear" tabindex="-1" class="icon clear"><span>clear</span></a>').click(function(){c.value="";m();return!1});a(c.value);h(c).on("input blur focus",function(){m();return!0}).after(s);e();return{delay:function(a){l=a},ping:function(b){b?(g&&clearTimeout(g),b=c.value,A&&b===A&&(b=""),a(b),d(),b=void 0):b=m();return b},val:function(b){if(null==
b)return k;g&&clearTimeout(g);c.value=a(b);e()},el:function(){return c},blur:function(a){return h(c).on("blur",a)}}};return c}({},v,z));n.register("$35",function(c,f,n){function p(a,b,m,c,g){function l(){s&&clearTimeout(s);r&&r.fadeOut(400,function(){h(this).remove();r=null});return!1}function A(){B();-1!==c&&(s=setTimeout(l,c||2E3));r.off("mouseleave").on("mouseenter",B)}function B(){s&&clearTimeout(s);s=null;r.off("mouseenter").on("mouseleave",A)}var s;e||(e=n.createElement("div"),e.id="growls",
n.body.appendChild(e));var r=h('<div class="growl growl-'+m+'"><div><a class="close" href="#"><span>X</span></a><span class="badge"></span><p class="message"></p><small class="caption"></small></div></div>');r.find("p").text(a||"Empty message");b?r.find("small").text(b):r.find("small").remove();if(g.length){g.push({label:"Cancel",callback:l,css:"cancel"});var w,q=h('<form action="#" class="dialog"></form>');b=function(a,b){w=h('<input type="button" value="'+b.label+'" class="butt '+(b.css||"")+'" />');
w.click(function(a){"function"===typeof b.callback&&b.callback(a,{close:l})});q.append(w);return w};for(a=0;a<g.length;a++)b(a,g[a]);r.append(q)}h(e).prepend(r.hide().fadeIn(400));r.find("a").click(l);A()}var b,e;c.init=function(){if(!b)return b=f.alert,f.alert=function(a){a=String(a).split("\n");var b=a[1]&&a.slice(1).join("\n");c.alert(a[0],b)},c};c.debug=function(a){b(a);return c};c.alert=function(a,d,e,c,g){try{return p(a,d||"",e||"alert",c||4E3,g||[]),!0}catch(l){return a+="\n\n--\n"+(l.message||
l),b.call(f,a),!1}};c.success=function(a,b,e){return c.alert(a,b,"success",e||2E3)};c.dialog=function(a,b,e,k){return c.alert(a,b,k||"alert",-1,e)};return c}({},v,z));n.register("$22",function(c,f,D){function p(a){var d;(d=a.alert)&&alert(d);(d=a.success)&&n.require("$35","growl.js").success.apply(null,d.push?d:[d]);if(d=a.download)f.location.assign(d);else if(d=a.redirect)if(0===d.indexOf("/modal/"))a.modal={url:d};else return f.location.assign(d),!1;else if(a.reload)return f.location.reload(),!1;
(d=a.modal)&&n.require("$32","modal.js").replace(d);(d=a.async)&&b(d.job_id,d.title)}function b(a,b){var e=n.require("$36","overlay.js");e.open().disableClose().css("modal-processing").html('<div class="wrap"><h2>'+(b||"Please wait")+"</h2></div>");n.require("$37","async.js").init(a).delay(500).timeout(2E4).listen("error",function(a){e.css("modal-error").enableClose().title("Error");e.html("<h2>"+(a||"Unknown error")+"</h2>")}).listen("complete",function(a){e.close();e=null;a&&p(a)}).start()}var e=
{401:"Authorization Required",422:"Invalid data sent to server",404:"Not Found",500:"Server Error",502:"Bad Gateway",503:"Service unavailable",504:"Gateway timeout"};c.jsonLink=function(a){if(!a)return"";a=a.split("?");a[0]=a[0].replace(/(\.[a-z0-9]{1,4})?$/i,".json");return a.join("?")};c.errorData=function(a,b,c){var k,g;c=a.responseText;b=a.status;if(!c&&0===b)return null;try{k=h.parseJSON(c)||{}}catch(l){k={},g=e[a.status]||l.message||l}k.error=g||k.error||k.statusText||a.statusText||e[b]||"Unknown Error";
return k};c.ajax=function(a,b,m,k){function g(a){function b(a){var g=a&&a[0];if(!g||D.contains&&!D.contains(g))a=h(D.body);return a}var g=b(k);g.trigger("locoAjaxSuccess",[a]);var d=a.events;if(d&&d.length)for(var e,q=n.require("$31","ga.js");e=d.shift();)g=b(g),g.trigger(e,[a]),q.event("ajax",e);if(!1===p(a))return!1;m&&m();return!0}function l(l,k,s){if("abort"!==k){var r=l.status,w,q=a.statusCode;if(200===r||!q||!q[r])if(401===r)n.require("$32","modal.js").load("/modal/user/ping.json?r="+encodeURIComponent(location.pathname),
e[r]);else{if("parsererror"===k)f.console&&console.error&&console.error(r,l.responseText),w=404===r?"Ajax service not found":/^\s+Fatal error/.test(l.responseText)?"Fatal server error from Ajax request":"Bad Ajax response";else{var C=c.errorData(l,k,s);C&&C.error&&(w=C.error)}"function"===typeof b&&!1===b(null,w,r,l)?m&&m():C&&C.data&&!g(C.data)||(alert(w||"Unknown Ajax error"),m&&m())}}}(a.headers||(a.headers={}))["X-Loco-Csrf"]=f.loco&&G.csrf||"";a.error=l;a.success=function(a,c,s){if(!a||"object"!==
typeof a)return l(s,"unknown");(a.status||a.error)&&alert(a.error||a.statusText||s.statusText||e[a.status]||"Unknown Error");"function"===typeof b&&!1===b(a&&a.data?a.data:a,null,c,s)?m&&m():a&&a.data?g(a.data):m&&m()};-1!==a.url.indexOf(".json")&&(a.dataType="json");return h.ajax(a)};return c}({},v,z));n.register("$37",function(c,f,h){c.init=function(c){function b(a,b){for(var g=-1,d,e=q[a]||[];++g<e.length;)(d=e[g])&&d.apply&&d.apply(null,b||[])}function e(){var a=B+"ms",g=Math.round(B/1E3);1<g&&
(a=g+" seconds");a="Job timeout after "+a;m();d();b("error",[a])}function a(a,e){if(!a||e)a={state:"error",message:e};try{switch(a.state){case "error":throw Error(a.message||"Unknown error tracking job progress");case "queued":case "starting":case "progress":var q=a.progress||0,r=a.message,y=a.state;q!==l&&(m(),l=q);b("progress",[l,r,y]);g();break;case "done":var A=a.data,c=a.message;l=100;m();d();b("complete",[A,c]);break;default:throw Error("Unexpected job status: "+a.state);}}catch(w){q=w.message||
w,m(),d(),b("error",[q])}}function d(){r&&(clearTimeout(r),r=null)}function m(){s&&(clearTimeout(s),s=null)}function k(){var b={dataType:"json",url:A};n.require("$22","http.js").ajax(b,a)}function g(){d();r=setTimeout(k,w);s||(s=setTimeout(e,B))}var l=0,A="/ajax/async/poll/"+c+".json",B=1E4,s,r,w=100,q={error:[],complete:[],progress:[]};return{start:function(){m();g();return this},stop:function(){m();d();return this},delay:function(a){w=a;return this},timeout:function(a){B=a;return this},listen:function(a,
b){var g=q[a];if(g)g.push(b);else throw Error(a+" is not a valid async event");return this}}};return c}({},v,z));n.register("$32",function(c,f,D){function p(a){a.stopPropagation();a.preventDefault();return!1}function b(){l||(l=n.require("$36","overlay.js"),l.listen(m));return l}function e(){var a=s.length-1,b=s[a],g=h('<a class="has-icon icon-back" data-gravity="w">Back</a>').attr("href",b[0]).attr("title",b[1]).click(function(g){if(s[a]===b&&C())return n.require("$11","tooltip.js").kill(),p(g)});
l.enableNav(g);n.require("$11","tooltip.js").init(g)}function a(a,b,g){a=a.split("#");var d=a[0],d=d+(-1===d.indexOf("?")?"?":"&"),d=d+(encodeURIComponent(b)+"="+encodeURIComponent(g));a[0]=d;return a.join("#")}function d(a,g){var d=b().autoSize(),q=d.init();n.require("$18","html.js").init(q);g&&g.lock&&d.disableClose();s.length&&e();q.find("[data-script]").each(function(a,b){b=h(b);for(var d=-1,e,q=b.attr("data-script").split(" ");++d<q.length;)e=q[d],B[e]?B[e](b,g||{}):alert("Unknown script "+e)});
q.trigger("locoModalLoaded",[d,a||"",g||{}]);var l=q.find("form")[0],r;if(l)a:for(d=0;d<l.elements.length;d++)switch(q=l.elements[d],q.type){case "text":case "email":case "textarea":r=Number(q.getAttribute("tabindex"));if(isNaN(r)||100>r)continue a;h(q).focus();break a}}function m(){r=null;s=[];return!0}function k(a){var b=h(a.currentTarget),d=b.attr("data-modal");if("back"===d){if(C())return p(a);d="close"}if("close"===d){q();var e=b.attr("href");if(e&&-1!==e.indexOf("#!"))return!0}else{var l="submit"===
a.type,r=b.attr("title")||b.attr("data-title"),e=b.attr("href")||b.attr("action"),A=l?b.serialize():"",b=l?b.attr("method"):"get";w(e,r,b,A,"modal "+(d||g(e)))}return p(a)}function g(a){return"http"===a.substr(0,4)?a.split("/").slice(3,6).join("-"):a.split("/").slice(1,4).join("-")}var l,A=n.require("$31","ga.js"),B={},s=[],r,w=c.load=function(g,e,q,y,m){var k=r;r=[].slice.call(arguments);k&&g!==k[0]&&(s.push(k),g=a(g,"r",k[0]));k=b();k.active()||k.open().css("modal").html('<div class="loading"></div>');
k.title("Loading ..").disableClose().disableNav();k.think();var k=g.split("#"),B=k[1];B&&(g=k[0]);var k=n.require("$22","http.js"),C={type:q||"get",data:y||"",url:k.jsonLink(g)};k.ajax(C,function(a,b,c){var k=a&&a.html;if(!k)return a&&a.redirect?(r=s.pop()||null,w(a.redirect,e,q,y,m)):(a=h('<h3 class="error"></h3>').text(b||"Unknown error"),b=h('<footer class="buttonset"><a href="/" data-modal="close" class="has-icon icon-ok">Close</a></footer>'),a=h('<div class="basic"></div>').append(a).append(b),
b="Error",c&&200!==c&&(b+=" "+c),l.unthink().enableClose().title(b).html("").append(a),n.require("$18","html.js").init(l.init())),!1;e=a.title||e||"Untitled";r&&(r[1]=e);l.unthink().enableClose().title(e,a.icon).width(null).css(m||"modal").autoSize().html(k);c=a.js||{};B&&(c["#"]=B);d(g,c);A.page(g,e);l.init().one("overlayClosed",function(){A.reset()});return!0});return c},q=c.close=function(){b().close();return c},C=c.back=function(){if(previous=s.pop())return r=s[s.length-2],w.apply(null,previous),
!0};c.reload=function(){r&&w.apply(null,r)};c.postback=function(a){r&&(r[2]="post",r[3]=a,w.apply(null,r))};c.initLink=function(a){a.click(k)};c.initForm=function(a){h(a).submit(k)};c.replace=function(a){var e=b(),q=a&&a.html,l=a&&a.url,r=a&&a.title,A=a&&a.action;if(l)q=s.length-1,q=0<=q?s[q][0]:void 0,l===q?C():w(l,r,"","",a&&a.css||"modal "+g(l));else if(q)e.open().html(q),r&&e.enableClose().title(r),d("",a&&a.js);else if("function"===typeof c[A])c[A]();return e};c.find=function(a){return b().init().find(a)};
c.script=function(a,b){if(b){if("function"!==typeof b.run)throw Error(a+" macro has no run function");B[a]=b.run;return c}return B[a]};c.hash=function(a){var b=a.hash;"#/modal/"===b.substr(0,8)&&(b=b.substr(1),a.hash="",c.replace({url:b}))};return c}({},v,z));n.register("$21",{"\u00e1":"a","\u00e0":"a","\u0103":"a","\u1eaf":"a","\u1eb1":"a","\u1eb5":"a","\u1eb3":"a","\u00e2":"a","\u1ea5":"a","\u1ea7":"a","\u1eab":"a","\u1ea9":"a","\u01ce":"a","\u00e5":"a","\u01fb":"a","\u00e4":"a","\u01df":"a","\u00e3":"a",
"\u0227":"a","\u01e1":"a","\u0105":"a","\u0101":"a","\u1ea3":"a","\u0201":"a","\u0203":"a","\u1ea1":"a","\u1eb7":"a","\u1ead":"a","\u1e01":"a","\u01fd":"\u00e6","\u01e3":"\u00e6","\u1e03":"b","\u1e05":"b","\u1e07":"b","\u0107":"c","\u0109":"c","\u010d":"c","\u010b":"c","\u00e7":"c","\u1e09":"c","\u010f":"d","\u1e0b":"d","\u1e11":"d","\u0111":"d","\u1e0d":"d","\u1e13":"d","\u1e0f":"d","\u00f0":"d","\ua77a":"d","\u01c6":"\u01f3","\u00e9":"e","\u00e8":"e","\u0115":"e","\u00ea":"e","\u1ebf":"e","\u1ec1":"e",
"\u1ec5":"e","\u1ec3":"e","\u011b":"e","\u00eb":"e","\u1ebd":"e","\u0117":"e","\u0229":"e","\u1e1d":"e","\u0119":"e","\u0113":"e","\u1e17":"e","\u1e15":"e","\u1ebb":"e","\u0205":"e","\u0207":"e","\u1eb9":"e","\u1ec7":"e","\u1e19":"e","\u1e1b":"e","\u1e1f":"f","\ua77c":"f","\u01f5":"g","\u011f":"g","\u011d":"g","\u01e7":"g","\u0121":"g","\u0123":"g","\u1e21":"g","\ua7a1":"g","\u1d79":"g","\u0125":"h","\u021f":"h","\u1e27":"h","\u1e23":"h","\u1e29":"h","\u0127":"h","\u210f":"h","\u1e25":"h","\u1e2b":"h",
"\u1e96":"h","\u00ed":"i","\u00ec":"i","\u012d":"i","\u00ee":"i","\u01d0":"i","\u00ef":"i","\u1e2f":"i","\u0129":"i","\u012f":"i","\u012b":"i","\u1ec9":"i","\u0209":"i","\u020b":"i","\u1ecb":"i","\u1e2d":"i","\u0135":"j","\u01f0":"j","\u1e31":"k","\u01e9":"k","\u0137":"k","\ua7a3":"k","\u1e33":"k","\u1e35":"k","\u013a":"l","\u013e":"l","\u013c":"l","\u0142":"l","\u1e37":"l","\u1e39":"l","\u1e3d":"l","\u1e3b":"l","\u0140":"l","\u1e3f":"m","\u1e41":"m","\u1e43":"m","\u0144":"n","\u01f9":"n","\u0148":"n",
"\u00f1":"n","\u1e45":"n","\u0146":"n","\ua7a5":"n","\u1e47":"n","\u1e4b":"n","\u1e49":"n","\u00f3":"o","\u00f2":"o","\u014f":"o","\u00f4":"o","\u1ed1":"o","\u1ed3":"o","\u1ed7":"o","\u1ed5":"o","\u01d2":"o","\u00f6":"o","\u022b":"o","\u0151":"o","\u00f5":"o","\u1e4d":"o","\u1e4f":"o","\u022d":"o","\u022f":"o","\u0231":"o","\u00f8":"o","\u01ff":"o","\u01eb":"o","\u01ed":"o","\u014d":"o","\u1e53":"o","\u1e51":"o","\u1ecf":"o","\u020d":"o","\u020f":"o","\u01a1":"o","\u1edb":"o","\u1edd":"o","\u1ee1":"o",
"\u1edf":"o","\u1ee3":"o","\u1ecd":"o","\u1ed9":"o","\u1e55":"p","\u1e57":"p","\u0155":"r","\u0159":"r","\u1e59":"r","\u0157":"r","\ua7a7":"r","\u0211":"r","\u0213":"r","\u1e5b":"r","\u1e5d":"r","\u1e5f":"r","\ua783":"r","\u015b":"s","\u1e65":"s","\u015d":"s","\u0161":"s","\u1e67":"s","\u1e61":"s","\u015f":"s","\ua7a9":"s","\u1e63":"s","\u1e69":"s","\u0219":"s","\u017f":"s","\ua785":"s","\u1e9b":"s","\u0165":"t","\u1e97":"t","\u1e6b":"t","\u0163":"t","\u1e6d":"t","\u021b":"t","\u1e71":"t","\u1e6f":"t",
"\ua787":"t","\u00fa":"u","\u00f9":"u","\u016d":"u","\u00fb":"u","\u01d4":"u","\u016f":"u","\u00fc":"u","\u01d8":"u","\u01dc":"u","\u01da":"u","\u01d6":"u","\u0171":"u","\u0169":"u","\u1e79":"u","\u0173":"u","\u016b":"u","\u1e7b":"u","\u1ee7":"u","\u0215":"u","\u0217":"u","\u01b0":"u","\u1ee9":"u","\u1eeb":"u","\u1eef":"u","\u1eed":"u","\u1ef1":"u","\u1ee5":"u","\u1e73":"u","\u1e77":"u","\u1e75":"u","\u1e7d":"v","\u1e7f":"v","\u1e83":"w","\u1e81":"w","\u0175":"w","\u1e98":"w","\u1e85":"w","\u1e87":"w",
"\u1e89":"w","\u1e8d":"x","\u1e8b":"x","\u00fd":"y","\u1ef3":"y","\u0177":"y","\u1e99":"y","\u00ff":"y","\u1ef9":"y","\u1e8f":"y","\u0233":"y","\u1ef7":"y","\u1ef5":"y","\u017a":"z","\u1e91":"z","\u017e":"z","\u017c":"z","\u1e93":"z","\u1e95":"z","\u01ef":"\u0292","\u1f00":"\u03b1","\u1f04":"\u03b1","\u1f84":"\u03b1","\u1f02":"\u03b1","\u1f82":"\u03b1","\u1f06":"\u03b1","\u1f86":"\u03b1","\u1f80":"\u03b1","\u1f01":"\u03b1","\u1f05":"\u03b1","\u1f85":"\u03b1","\u1f03":"\u03b1","\u1f83":"\u03b1","\u1f07":"\u03b1",
"\u1f87":"\u03b1","\u1f81":"\u03b1","\u03ac":"\u03b1","\u1f71":"\u03b1","\u1fb4":"\u03b1","\u1f70":"\u03b1","\u1fb2":"\u03b1","\u1fb0":"\u03b1","\u1fb6":"\u03b1","\u1fb7":"\u03b1","\u1fb1":"\u03b1","\u1fb3":"\u03b1","\u1f10":"\u03b5","\u1f14":"\u03b5","\u1f12":"\u03b5","\u1f11":"\u03b5","\u1f15":"\u03b5","\u1f13":"\u03b5","\u03ad":"\u03b5","\u1f73":"\u03b5","\u1f72":"\u03b5","\u1f20":"\u03b7","\u1f24":"\u03b7","\u1f94":"\u03b7","\u1f22":"\u03b7","\u1f92":"\u03b7","\u1f26":"\u03b7","\u1f96":"\u03b7",
"\u1f90":"\u03b7","\u1f21":"\u03b7","\u1f25":"\u03b7","\u1f95":"\u03b7","\u1f23":"\u03b7","\u1f93":"\u03b7","\u1f27":"\u03b7","\u1f97":"\u03b7","\u1f91":"\u03b7","\u03ae":"\u03b7","\u1f75":"\u03b7","\u1fc4":"\u03b7","\u1f74":"\u03b7","\u1fc2":"\u03b7","\u1fc6":"\u03b7","\u1fc7":"\u03b7","\u1fc3":"\u03b7","\u1f30":"\u03b9","\u1f34":"\u03b9","\u1f32":"\u03b9","\u1f36":"\u03b9","\u1f31":"\u03b9","\u1f35":"\u03b9","\u1f33":"\u03b9","\u1f37":"\u03b9","\u03af":"\u03b9","\u1f77":"\u03b9","\u1f76":"\u03b9",
"\u1fd0":"\u03b9","\u1fd6":"\u03b9","\u03ca":"\u03b9","\u0390":"\u03b9","\u1fd3":"\u03b9","\u1fd2":"\u03b9","\u1fd7":"\u03b9","\u1fd1":"\u03b9","\u1f40":"\u03bf","\u1f44":"\u03bf","\u1f42":"\u03bf","\u1f41":"\u03bf","\u1f45":"\u03bf","\u1f43":"\u03bf","\u03cc":"\u03bf","\u1f79":"\u03bf","\u1f78":"\u03bf","\u1fe4":"\u03c1","\u1fe5":"\u03c1","\u1f50":"\u03c5","\u1f54":"\u03c5","\u1f52":"\u03c5","\u1f56":"\u03c5","\u1f51":"\u03c5","\u1f55":"\u03c5","\u1f53":"\u03c5","\u1f57":"\u03c5","\u03cd":"\u03c5",
"\u1f7b":"\u03c5","\u1f7a":"\u03c5","\u1fe0":"\u03c5","\u1fe6":"\u03c5","\u03cb":"\u03c5","\u03b0":"\u03c5","\u1fe3":"\u03c5","\u1fe2":"\u03c5","\u1fe7":"\u03c5","\u1fe1":"\u03c5","\u1f60":"\u03c9","\u1f64":"\u03c9","\u1fa4":"\u03c9","\u1f62":"\u03c9","\u1fa2":"\u03c9","\u1f66":"\u03c9","\u1fa6":"\u03c9","\u1fa0":"\u03c9","\u1f61":"\u03c9","\u1f65":"\u03c9","\u1fa5":"\u03c9","\u1f63":"\u03c9","\u1fa3":"\u03c9","\u1f67":"\u03c9","\u1fa7":"\u03c9","\u1fa1":"\u03c9","\u03ce":"\u03c9","\u1f7d":"\u03c9",
"\u1ff4":"\u03c9","\u1f7c":"\u03c9","\u1ff2":"\u03c9","\u1ff6":"\u03c9","\u1ff7":"\u03c9","\u1ff3":"\u03c9","\u0491":"\u0433","\u0450":"\u0435","\u0451":"\u0435","\u04c2":"\u0436","\u045d":"\u0438","\u04e3":"\u0438","\u04ef":"\u0443"});n.register("$7",function(c,f,h){c.init=function(){function c(a){return l[a]||a}function b(a,b,g,d){b=a.split(b);for(var e=b.length;0!==e--;)(a=b[e])&&null==d[a]&&(g.push(a),d[a]=!0);return g}function e(a){return b(String(a||"").toLowerCase().replace(k,c),g,[],{})}function a(a,
d){for(var e=[],l={},w,q=d.length,C=g;0!==q--;)(w=d[q])&&b(String(w||"").toLowerCase().replace(k,c),C,e,l);m[a]=e}function d(a,b){var g=[],d=-1,e=m,q=e.length,l,c,k,f,y,h,p=a.length,n=b?!0:!1;a:for(;++d<q;)if(k=e[d],null!=k&&(f=k.length)){y=0;b:for(;y<p;y++){h=a[y];for(l=0;l<f;l++)if(c=k[l],0===c.indexOf(h))continue b;continue a}g.push(n?b[d]:d)}return g}var m=[],k=/[^a-z0-9]/g,g=/[\-_\s.?!;:,*^+=~`"(){}<>\[\]\/\\\u00a0\u1680\u180e\u2000-\u206f\u2e00-\u2e7f\u3000-\u303f]+/,l=n.require("$21","flatten.json");
return{split:e,pull:function(a,b){return d(a,b)},find:function(a,b){return d(e(a),b)},add:function(a,b){m[a]=e(b)},push:function(b){a(m.length,b)},index:function(b,g){a(b,g)},size:function(){return m.length},clear:function(){m=[]},remove:function(a){m[a]=null}}};return c}({},v,z));n.register("$23",function(c,f,h){c.create=function(){var c=[],b=n.require("$7","fulltext.js").init();return{add:function(e,a){b.add(c.length,a);c.push(e);return this},find:function(e){return b.find(e,c)},clear:function(){c=
[];b.clear();return this}}};return c}({},v,z));n.register("$10",function(c,f,D){function p(b){-1===b.indexOf("?")&&(b="/auto/"+b+".json?q=");this.url=b;this.dead={}}function b(){this.dict=n.require("$23","dict.js").create()}c.init=function(e){function a(){if("hint"!==K){var a=y.val()&&!(J&&J.val())&&null==f&&!q;D[a?"addClass":"removeClass"]("error")}}function d(a){J&&J.val(a)}function c(){v.show();var a=y.outerWidth(!1),b=y.outerHeight(!1),g=y.css("margin-top");g&&(g=parseInt(g),isNaN(g)||(b+=g));
a-=2;v.css("top",b+"px").css("minWidth",a+"px");q=!0}function k(){v.hide();q=!1}function g(){v.html("");k();w=0;u=f=null}function l(b){g();var d;for(d=0;d<b.length;d++){var e=d,q=b[d],l=q.value,r=q.lang||l.lang,y=h('<span class="label"></span>').text(q.label),k=h('<div class="auto-comp-result"></div>'),s=void 0,E=void 0;if(E=q.icon)s=h("<span></span>").attr("class",E).appendTo(k),r?(s.attr("lang",r),q.lang=r):-1!==E.indexOf("lang-"+l)&&(s.attr("lang",l),q.lang=l);for(E in q)k.data(E,q[E]);k.append(y);
A(e,k)}(w=b.length)?(C&&c(),B(0)):(B(null),a(),D.trigger("locoAutonone",[]))}function A(a,b){v.append(b);b.click(function(g){g.stopPropagation();B(a,b);r();return!1});return b}function B(a,b){u&&(u.removeClass("selected"),u=null);f=null;null==a?d(""):(b||(b=v.find("div.auto-comp-result").eq(a)),b.length&&(b.addClass("selected"),f=a,u=b))}function s(a){if(w){var b=w-1;null==f?a=0<a?0:b:(a=f+a,0>a?a=b:a>b&&(a=0));return B(a)}}function r(){if(null==f)z.val(""),d("");else{var b=v.find("div.auto-comp-result").eq(f),
e=b.data()||{label:"Error"},q=e.value,l=e.label;d(q);z.val(l);k();b=b.clone();b.data(e);g();A(0,b);w=1;B(0,b);a();b.trigger("locoAutocomp",[q,l,b])}}var w=0,q=!1,C=!1,E=e.form,f=null,u=null,y=h(e),F=y.attr("name"),K=y.attr("data-mode"),I=y.attr("data-provider"),J="hint"!==K&&h('<input type="hidden" value="" name="'+F+'" />').appendTo(E),D=h('<div class="auto-comp-wrap"></div>').replaceAll(y),v=h('<div class="auto-comp-drop"></div>');I&&(I=new p(I));J&&y.attr("name","_"+F);y.attr("autocomplete")||
y.attr("autocomplete","off");D.append(y).append(v);k();y.focus(function(){C=!0;1<w&&c()}).blur(function(){C=!1;a()}).keydown(function(a){function b(){a.preventDefault();a.stopPropagation();return!1}switch(a.keyCode){case 27:q&&(a.stopPropagation(),k(),y.blur());break;case 40:w&&(q?s(1):c());break;case 38:q&&s(-1);break;case 13:if(q)return r(),b();if(!f&&"hint"!==K)return b()}return!0});var z=n.require("$8","LocoTextListener.js").listen(y,function(a){I&&I.fetch(a,l)});(E=y.attr("data-pre"))&&(E=h.parseJSON(E))&&
E.value&&E.label?(l([E]),r()):!e.value||J&&J.val()||!I||I.fetch(e.value,function(a){l(a);r()});return{$:y,val:function(){return J&&J.val()},clear:g,reset:function(){g();y.val("");d("");z.ping()},force:function(a,b){g();d(b||"");z.val(a)},preload:function(a){w&&g();I=new b;var d,e;for(d in a)e=a[d],I.add(e)},mode:function(a){K=a},provide:function(a){I=new p(a)}}};p.prototype.fetch=function(b,a){if(!b)return a&&a([]),this.dead={},this;var d,c=this.dead;for(d in c)if(0===b.indexOf(d))return a&&a([]),
this;d={dataType:"json",url:this.url+encodeURIComponent(b)};n.require("$22","http.js").ajax(d,function(d){var g=d&&d.results;g&&(a&&d.query&&d.query===b&&a(d.results),g.length||(c[b]=0));return!0});return this};b.prototype.add=function(b){var a=b.fulltext||b.label||b.value;a&&this.dict.add(b,a)};b.prototype.fetch=function(b,a){var d=b?this.dict.find(b):[];a&&a(d);return this};return c}({},v,z));n.register("$9",function(c,f,D){function p(a){a.stopPropagation();a.preventDefault();return!1}function b(a,
b,d){d.off().mouseup(function(d){d.stopPropagation();a.selectIndex(b,!0);return!1}).mouseover(function(){h(this).addClass("over");a.hover=b;return!0}).mouseout(function(){h(this).removeClass("over");a.hover=-1;return!0});return d}function e(b){m===b&&(h(f).off("resize scroll",a),m=null);return b}function a(){m&&m.resize()}function d(a){if(a){var b,d=a[0],e=d.id,c=d.title,r=d.disabled,m=[],q=[],k,E=-1,f=d.options.length,u=this.selectedIcon||"";this.id=e||"";this.name=d.name||"";this.prefix=d.getAttribute("data-prefix");
for(this.defaultIcon=d.getAttribute("data-icon")||"icon no-icon";++E<f;)b=d.options[E],k=h(b),b.selected&&m.push(E),q.push([k.val(),k.text(),k.attr("data-icon")||u,b.disabled,k.attr("lang")]);this.hidden||(this.hidden=h('<input type="hidden" name="'+this.name+'" value="" />').appendTo(d.form));this.list=h("<ul></ul>").mouseup(p);this.icon=h('<span class="icon"> </span>');this.selection=h('<span class="label"></span>');this.handle=h('<a class="handle" href="#"></a>').attr("tabindex",a.attr("tabindex")||
"").append(this.icon).append(this.selection);b=h('<div class="selector"></div>').addClass(d.className).append(this.handle).append(this.list).replaceAll(d);e&&b.attr("id",e);c&&a.hasClass("hastip")&&(b.attr("title",c),n.require("$11","tooltip.js").init(b,{gravity:a.attr("data-gravity")||"s",anchor:a.attr("data-anchor")}));this.wrapper=b;this.up=b.hasClass("up");for(this.clearOptions();b=q.shift();)this.addOption.apply(this,b);r?this.disable():this.enable();this.close();if(f=m.length)for(E=0;E<f;E++)this.selectIndex(m[E]);
else E=d.selectedIndex,null!=E&&0<=E&&this.selectIndex(E)}}c.create=function(a){return new d(a)};c.extend=function(a){a.parent=d;a.prototype=new d};var m,k=d.prototype;k.enable=function(){var a=this;a.bound?a.wrapper.removeClass("disabled"):(a.handle.click(function(a){a.preventDefault();return!1}).mouseover(function(b){return a.onRollover(b)}).mouseout(function(b){return a.onRollout(b)}).mousedown(function(b){return a.onPress(b)}).keydown(function(b){return a.onKeydown(b)}),h(D).mouseup(function(b){return a.onRelease(b)}).keydown(function(b){return a.onGlobalKeydown(b)}),
a.bound=!0)};k.disable=function(){this.wrapper.addClass("disabled")};k.isDisabled=function(){return this.wrapper.hasClass("disabled")};k.onRollover=function(){return this.over=!0};k.onRollout=function(){this.over=!1;return!0};k.onPress=function(a){return this.active?this.over?(this.close(),a.stopPropagation(),a.preventDefault(),!1):!0:(this.open(),a.stopPropagation(),a.preventDefault(),this.handle.focus(),!1)};k.onRelease=function(){this.active&&!this.over&&this.close();return!0};k.onGlobalKeydown=
function(a){if(this.active)switch(a.keyCode){case 27:return this.close(),p(a);case 40:return this.hoverNext(1),p(a);case 38:return this.hoverNext(-1),p(a);case 13:if(-1!=this.hover)return this.selectIndex(this.hover,!0),this.hoverItem(-1),this.close(),p(a)}return!0};k.onKeydown=function(a){return this.active||40!==a.keyCode?!0:(this.open(),p(a))};k.resize=function(){var a=this.list,b=this.handle,d=b.outerWidth()||0,e=a.outerWidth()||0,c;this.up?(c=a.outerHeight(),a.css("top","-"+(c+2)+"px")):(c=b.outerHeight()||
0,c+=2,a.css("top",c+"px"),b=c+n.require("$18","html.js").top(b[0]),c=f.innerHeight-Math.max(0,b-f.pageYOffset),c-=4,a.css("max-height",c+"px"));d>e&&(d-=e-a.width(),a.css("min-width",String(d)+"px"))};k.open=function(){var b=this.list,d=this.wrapper,c=d.data("tipsy");this.active=!0;this.hover=-1;d.addClass("active");b.show().scrollTop(0);c&&c.disable();this!==m&&(m&&e(m),h(f).on("resize scroll",a),m=this);this.resize()};k.close=function(){var a=this.wrapper,b=a.data("tipsy");this.list.hide();this.active=
!1;-1!==this.hover&&(this.getElement(this.hover).removeClass("over"),this.hover=-1);a.removeClass("active");b&&b.enable();e(this)};k.hoverItem=function(a,b){-1!==this.hover&&this.getElement(this.hover).removeClass("over");this.hover=a;-1!==a&&(b=b||this.getElement(a),b.addClass("over"))};k.hoverNext=function(a){var b=this.hover,d=this.options.length,e,c=null;for(-1===b&&0>a&&(b=d);d;){b+=a;if(0>b){this.close();break}else b>=d&&(b=0);if((e=this.options[b])&&!e.disabled&&!e.hidden){this.hoverItem(b);
break}if(null==c)c=b;else if(c===b)break}};k.enableChange=function(a){this.eventName=a;this.eventData=[].slice.call(arguments,1);return this};k.enableConfirm=function(a){this.confirm=a;return this};k.clearOptions=function(){this.index={};this.length=0;this.options=[];this.list.html("");this.hidden.val("");this.idx=this.hover=-1;this.dict=null;return this};k.destroy=function(){this.clearOptions();this.hidden.remove()};k.addOption=function(a,d,e,c,m){var r=this.options.length,k=h("<span></span>").addClass(e||
"icon no-icon"),q=h('<span class="label"></span>').text(d||a),q=h("<li></li>").append(k).append(q).appendTo(this.list);14<r&&this.getDict().add(r,d);m?m=m.split("-").shift():e&&-1!==e.indexOf("lang-"+a)&&(m=a);m&&k.attr("lang",m);q.attr("data-option",r);this.options[r]={value:a,text:d,lang:m||"",icon:e};this.index[a]=r;this.length=r+1;c?this.disableIndex(r):b(this,r,q);return r};k.getDict=function(){return this.dict||(this.dict=this.createDict())};k.createDict=function(){var a=this,b=n.require("$7",
"fulltext.js").init(),d=h('<input type="text" placeholder="&#x1F50D;"/>').appendTo(h('<form class="clearable"></form>').submit(p).insertBefore(a.getElement(0)));n.require("$8","LocoTextListener.js").listen(d,function(e){0===(""===e?a.unfilter():a.filter(b.find(e)))?d.addClass("error"):d.removeClass("error")});h.each(a.options,function(a,d){b.add(a,d.text)});return b};k.filter=function(a){var b=-1,d=this.length,e,c=-1,r=a.length;if(r===d)this.unfilter();else{for(;++c<r;){for(e=a[c];++b<e;)this.hideIndex(b);
this.showIndex(b)}for(;++b<d;)this.hideIndex(b)}return r};k.unfilter=function(){for(var a=-1,b=this.length;++a<b;)this.showIndex(a);return b};k.disableOption=function(a){return this.disableIndex(this.index[a])};k.enableOption=function(a){return this.enableIndex(this.index[a])};k.disableIndex=function(a){var b=this.options[a];b&&!b.disabled&&(b.disabled=!0,this.getElement(a).addClass("disabled").off());return this};k.enableIndex=function(a){var d=this.options[a];d&&d.disabled&&(d.disabled=null,b(this,
a,this.getElement(a)).removeClass("disabled"));return this};k.hideIndex=function(a){var b=this.options[a];b&&!b.hidden&&(b.hidden=!0,this.getElement(a).hide())};k.showIndex=function(a){var b=this.options[a];b&&b.hidden&&(b.hidden=null,this.getElement(a).show())};k.each=function(a){for(var b=-1,d=this.options,e=d.length;++b<e;)a(b,d[b]);return this};k.reIndex=function(){var a=-1,d={},e,c=this.dict,m=this.options,r=m.length;for(c&&c.clear();++a<r;)e=m[a],d[e.value]=a,c&&c.add(a,e.text),e.disabled||
b(this,a,this.getElement(a));this.index=d;this.length=r};k.selectValue=function(a,b){return this.selectIndex(this.index[a],b)};k.hasValue=function(a){return null!=this.index[a]};k.hasIndex=function(a){return this.length>a};k.getIndex=function(){return this.idx};k.selectIndex=function(a,b){var d=this.options[a];if(d){var e=this,c=e.idx,r=d.value,m=d.icon||e.defaultIcon,q=function(){null!=c&&e.getElement(c).removeClass("active");e.getElement(a).addClass("active");e.setLabel(d.text).setIcon(m,d.lang);
e.hidden.val(r);e.idx=a;b&&e.change()};c===a?e.hidden.val(r):b?"function"===typeof e.confirm?e.confirm.call(null,d,function(a){a&&q()}):e.beforeChange(r)&&q():q();e.active&&e.close()}return this};k.setLabel=function(a){this.selection.text(a);(a=this.prefix)&&this.selection.prepend(h('<span class="prefix"></span>').text(a));return this};k.setIcon=function(a,b){this.icon.attr("class",a).attr("lang",b||"");return this};k.val=function(){var a=this.options[this.idx];return a&&a.value};k.change=function(){var a=
this.val(),b=this.save,d=this.eventName||"change",e=[a].concat(this.eventData||[]);this.wrapper.trigger(d,e);b&&b(a);this.hidden.triggerHandler("change");return this};k.beforeChange=function(a){var b=h.Event("locoBeforeSelect");this.wrapper.trigger(b,[a]);return!b.isDefaultPrevented()};k.renameOption=function(a,b){var d=this.index[a],e=this.options[d];e&&(e.text=b,this.getElement(d).find("span.label").text(b),d===this.idx&&this.setLabel(b));return this};k.removeOption=function(a){var b=this.index[a],
d=this.options[b];d&&(a=this.val(),this.getElement(b).remove(),this.options.splice(b,1),this.reIndex(),a===d.value?this.selectIndex(0,!0):this.selectValue(a,!1))};k.getElement=function(a){return this.list.find("li").eq(a)};k.getWrapper=function(){return this.wrapper};k.tip=function(){return this.getWrapper().data("tipsy")};k.$=function(a,b){h.fn[a].apply(this.wrapper,b||[]);return this};k.persist=function(a){var b=this.id||this.name,d=a.fetch(b);null!=d&&this.selectValue(d,!0);this.save=function(d){a.store(b,
d)};return this};k.listen=function(a){return this.on(this.eventName||"change",a)};k.on=function(a,b){this.wrapper.on(a,b);return this};k=null;return c}({},v,z));n.register("$33",function(c,f,D){function p(b){var e=[],a=h([]);this.hidden=a;this.selected=e;this.selectedIcon="icon icon-checkbox";this.constructor.call(this,b);this.wrapper.addClass("multi");for(var d,c,k,g=h('<input type="checkbox" name="'+this.name+'[]" />')[0],l=this.options,A=l.length,f=-1;++f<A;)d=this.getElement(f)[0],k=g.cloneNode(!0),
e[f]&&(k.checked=!0),k.setAttribute("value",l[f].value),a.push(d.appendChild(k));(c=this.defaultIcon)&&this.setIcon(c);(c=b.attr("title"))?this.setLabel(c):this.wrapper.addClass("no-title")}c.create=function(b){return new p(b)};n.require("$9","LocoSelector.js").extend(p);f=p.prototype;f.selectIndex=function(b,e){return this.setIndexState(b,!this.selected[b],e)};f.setIndexState=function(b,e,a){var d=this.options[b],c=this.getElement(b),k=this.hidden[b],g=this.selected;d&&e!==g[b]&&(g[b]=e,c[e?"addClass":
"removeClass"]("checked"),k&&(k.checked=e),a&&this.change());return this};f.selectValue=function(b,e){if(null==b.pop)return p.parent.prototype.selectValue.call(this,b,e);for(var a=this.selected.slice(),d=this.options.length,c=this.index||{},k=!1,g=-1,l=-1;++g<d;)a[g]&&(k=!0),a[g]=!1;if(d=b.length)for(;++l<d;)g=c[b[l]],null==g?k=!0:!1===a[g]&&(k=a[g]=!0);if(k){for(g in a)this.setIndexState(g,a[g]);e&&this.change()}return this};f.val=function(){for(var b=[],e=this.options,a=this.selected,d=a.length,
c=-1;++c<d;)a[c]&&b.push(e[c].value);return b};f=null;return c}({},v,z));n.register("$44",function(c,f,h){function p(b,a){for(var d=String(b);a>d.length;)d="0"+d;return d}function b(b){var a=b%10;if(1===a){if(11!==b)return"st"}else if(2===a){if(12!==b)return"nd"}else if(3===a&&13!==b)return"rd";return"th"}c.formatter=function(){return{format:function(b,a){var d=this;return a.replace(/%[YmdHisMjSghAa]/g,function(a){return d[a.charAt(1)](b)})},Y:function(b){return String(b.getFullYear())},m:function(b){return p(1+
b.getFullMonth(),2)},M:function(b){return"Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(" ")[b.getMonth()]},d:function(b){return p(b.getDate(),2)},H:function(b){return p(b.getHours(),2)},i:function(b){return p(b.getMinutes(),2)},s:function(b){return p(b.getSeconds(),2)},j:function(b){return String(b.getDate())},S:function(e){return b(e.getDate())},g:function(b){b=b.getHours();return String(0===b?12:12<b?b-12:b)},h:function(b){return p(this.g(b),2)},a:function(b){return 12>b.getHours()?"am":
"pm"},A:function(b){return 12>b.getHours()?"AM":"PM"}}};return c}({},v,z));n.register("$34",function(c,f,D){function p(a){var e=a.id,c=m||(m=[]),r=k||(k={}),w=a.getAttribute("datetime"),w=l(w);if(!isNaN(w))return e||(e="tick"+ ++g,a.setAttribute("id",e)),a={id:e,el:a,dt:new Date(w),st:a.textContent},r[e]||c.push(a),r[e]=a,d||(d=setTimeout(b,200)),a}function b(){for(var a=m,c=a.length,g={},r=[];0<c--;)ticker=a[c],e(ticker)&&(r.push(ticker),g[ticker.id]=ticker);r.length?(m=r,k=g,d=setTimeout(b,3E4)):
d=k=m=null}function e(b){var d=b.el;if(D.contains&&!D.contains(d))return!1;strDate=a(b.dt,new Date,!0);if(""===strDate)return!1;strDate!==b.st&&(h(d).text(strDate),b.st=strDate);return!0}function a(a,b,d){var e=b.getTime(),c=a.getTime(),q=e-c;if(6E4>q)return"Just now";if(36E5>q)return a=Math.floor(q/6E4),String(a)+" minute"+(1===a?"":"s")+" ago";if(864E5>q&&b.getDate()===a.getDate()&&b.getMonth()===a.getMonth()&&b.getFullYear()===a.getFullYear())return a=Math.round(q/36E5),"About "+String(a)+" hour"+
(1===a?"":"s")+" ago";if(d)return"";d=Math.floor(e/864E5)-Math.floor(c/864E5);e=n.require("$44","date.js").formatter();q=e.format(a," %g:%i %A");return 1>=d&&(b.setHours(0),b.setMinutes(0),b.setSeconds(0),b=b.getTime()-864E5,c>=b)?"Yesterday at"+q:11>d?d+" days ago at"+q:e.format(a,"%M %j%S %Y,")+q}var d,m,k,g=0,l=Date.parse||function(){return Number.NaN};c.onEach=function(a,b){p(b)};c.update=function(a){e(p(a))};c.rel=a;return c}({},v,z));n.register("$18",function(c,f,D){var p=f.ieVersion;c.ie=function(b){return b?
p<=b:p};var b=c.init=function(b){b?b instanceof jQuery||(b=h(b)):b=h(D.body);var a=n.require("$31","ga.js"),d=n.require("$19","forms.js"),m=n.require("$32","modal.js"),k=n.require("$10","LocoAutoComplete.js"),g=n.require("$9","LocoSelector.js"),l=n.require("$33","LocoMultiSelector.js"),f=n.require("$11","tooltip.js");b.find("form").each(function(a,b){var e=h(b);p&&10>p&&d.placeholders(e);b.getAttribute("data-modal")?m.initForm(b):b.hasAttribute("action")&&"#"!==e.attr("action").charAt(0)&&!b.target&&
d.jsonify(b);e.find("input.auto-comp").each(function(a,b){k.init(b)});e.find("select.selector").each(function(a,b){b.hasAttribute("multiple")?l.create(h(b)):g.create(h(b))});e.hasClass("hasreveal")&&d.revealify(e);e.find("button.hastip").each(function(a,b){f.init(h(b))})});b.find("a").each(function(b,d){-1!==d.className.indexOf("hastip")&&f.init(h(d));"/modal/"===String(d.getAttribute("href")).substr(0,7)||d.getAttribute("data-modal")?m.initLink(h(d)):a.link(d)});b.find("time.tick").each(n.require("$34",
"tick.js").onEach);d=m=k=b=null;return c};c.$=function(b){return h(f.innerShiv?innerShiv(b,!1):b)};h.fn._html=function(e){return null!=e?(e=this.html(f.innerShiv?innerShiv(e,!1):e),b(this),e):j.html()};h.fn.macro=function(b,a){if("function"!==typeof b.run)throw Error("macro has no run function");b.run(this,a||{});return this};c.el=function(b,a){var d=D.createElement(b||"div");a&&(d.className=a);return d};c.txt=function(b){return D.createTextNode(b||"")};c.h=function(){function b(){c=/[<>&]/g;k=/(\r\n|\n|\r)/g;
g=/(?:https?):\/\/(\S+)/ig;l=location.hostname;b=null}function a(a){return"&#"+a.charCodeAt(0)+";"}function d(a,b){return'<a href="'+a+'" target="'+(b.indexOf(l)?"_blank":"_top")+'">'+b+"</a>"}var c,k,g,l;return function(l,f){b&&b();var s=l.replace(c,a);f&&(s=s.replace(g,d).replace(k,"<br />"));return s}}();c.noop=function(b){b&&(b.preventDefault(),b.stopPropagation(),h(b.target).blur());return!1};c.top=function(b,a){a||(a=D.body);for(var d=b.offsetTop||0;(b=b.offsetParent)&&b!==a;)d+=b.offsetTop||
0;return d};c.left=function(b,a){a||(a=D.body);for(var d=b.offsetLeft||0;(b=b.offsetParent)&&b!==a;)d+=b.offsetLeft||0;return d};return c}({},v,z));n.register("$19",function(c,f,D){function p(a){function b(){a.value===g&&(a.value="",c.removeClass("placeheld"));return!0}function e(){""===a.value&&(a.value=g,c.addClass("placeheld"));return!0}var c=h(a);if(!c.hasClass("auto-comp")){var g=c.attr("placeholder");if(g)return c.focus(b).blur(e),e(),{kill:function(){b();c.off("focus",b).off("blur",e)}}}}var b=
c.enable=function(a){function b(a,d){d.getAttribute("data-was-disabled")||(d.disabled=!1)}a.find(".button").removeClass("loading");a.find("button").each(b);a.find("input").each(b);a.find("select").each(b);a.find("textarea").each(b);f.attachEvent&&a.hasClass("has-placeholders")&&c.placeholders(a);a.removeClass("disabled").trigger("locoEnable");delete a._disabled},e=c.disable=function(a){function b(a,d){d.disabled?d.setAttribute("data-was-disabled","true"):d.disabled=!0}a._disabled||(a.find(".button").addClass("loading"),
a.find("button").each(b),a.find("input").each(b),a.find("select").each(b),a.find("textarea").each(b),a.addClass("disabled").trigger("locoDisable"),a._disabled=!0)};c.jsonify=function(a,d,c){a instanceof jQuery||(a=h(a));a.disable||(h.fn.disable=function(){e(this);return this},h.fn.enable=function(){b(this);this.placehold&&this.placehold();return this});var k="";a.find('[type="submit"]').click(function(a){a&&a.target&&a.target.name&&(k=encodeURIComponent(a.target.name)+"="+encodeURIComponent(a.target.value));
return!0});a.submit(function(b){if(b&&b.isDefaultPrevented&&b.isDefaultPrevented()||c&&!1===c(b))return!1;var e=a.serialize(),e=e.replace(/%0D%0A/g,"%0A");k&&(e&&(e+="&"),e+=k,k="");a.disable();var f=n.require("$22","http.js"),e={url:f.jsonLink(a.attr("action")),type:a.attr("method"),data:e};f.ajax(e,d,function(){a.enable()},a);b.preventDefault();b.stopPropagation();return!1})};c.revealify=function(a){a=a.closest("form");a.find("div[data-reveal-if]").each(function(b,e){function c(a){var b;b=p;q=q||
a.target;if("."===r)b=Boolean(q&&q[w]),p&&(b=!b);else{var d,e,y=h(q.form).serializeArray();for(e in y)y[e].name===s&&(d=y[e].value);"!="===r&&(b=!b);b=b?w!==d:w===d}if(b!==f)if(f=b,a)g[f?"slideDown":"slideUp"](200);else g[f?"show":"hide"]();return!0}var g=h(e),l=/^(\!?)([_\w\-\[\]]+)(\.|!?=)(.*)$/.exec(g.attr("data-reveal-if"));if(l){var f,p=l[1]?!0:!1,s=l[2],r=l[3],w=l[4],l=a[0][s];if(l.type||null==l.length)l=[l];b=l.length;for(var q;0!==b--;)q=l[b],c(),h(q).change(c).removeClass("jshide");l=l=q=
null}});a=null};c.linkify=function(a){var b=a.getAttribute("data-icon");if(b){var e=h(a),c=h("<a> </a>");c.attr("href",a.form.action);c.attr("class",e.attr("class"));c.attr("tabindex",e.attr("tabindex"));e.attr("tabindex","-1");c.text(e.val());b&&h("<span></span>").prependTo(c).addClass(b);e.hide().after(c);c.click(function(a){e.click();return!1})}};c.placeholders=function(a){var b,e=[];a.find("input[placeholder]").each(function(a,c){"password"!==c.type&&(b=p(c))&&e.push(b)});e.length&&(a.submit(function(){for(var a in e)e[a].kill()}),
a.addClass("has-placeholders"),b=i=null)};return c}({},v,z));n.register("$4",function(c,f,D){function p(a,b,d){function e(){c();g=setTimeout(b,d)}function c(){g&&clearTimeout(g);g=null}var g;e();h(a).mouseenter(c).mouseleave(e);return{die:function(){c();h(a).off("mouseenter mouseleave")}}}function b(a,b){a.fadeTo(b,0,function(){a.slideUp(b,function(){a.remove();h(f).triggerHandler("resize")})});return a}function e(a,d){function e(d){g[r]=null;b(h(a),250);c&&c.die();var q;if(q=d)d.stopPropagation(),
d.preventDefault(),q=!1;return q}var c,r;h(a).addClass("is-dismissible");h('<button type="button" class="notice-dismiss" href="#"> </a>').prependTo(a).click(e);h(f).triggerHandler("resize");l();r=g.length;g.push(e);d&&(c=p(a,e,d));return{stick:function(){c&&c.die();c=null;g[r]=null}}}function a(a,b,d){var e=n.require("$18","html.js");a=h('<div class="notice notice-'+a+'" loco-notice inline></div>').prependTo(h("#loco-notices"));var c=h(e.el("p"));d=h(e.el("span")).text(d);b=h(e.el("strong","has-icon")).text(b+
": ");c.append(b).append(d).appendTo(a);return a}function d(b,d,c,g){b=a(c,d,b).hide().fadeIn(500);h(f).triggerHandler("resize");return e(b,g)}function m(a){return d(a,B,"warning")}function k(){h("#loco-notices").find("div.notice").each(function(a,b){if(-1===b.className.indexOf("jshide")){var d=-1===b.className.indexOf("notice-success")?null:5E3;e(b,d)}})}var g=[],l=Date.now||function(){return(new Date).getTime()},A,B,s,r;c.error=function(a){return d(a,A,"error")};c.warn=m;c.info=function(a){return d(a,
s,"info")};c.success=function(a){return d(a,r,"success",5E3)};c.warning=m;c.log=function(){f.console&&console.log&&console.log.apply(console,arguments)};c.debug=function(a,b){f.console&&console.error&&(console.error("Loco Error: "+a),b&&console.debug&&console.debug(b))};c.clear=function(){for(var a=-1,b,d=g,e=d.length;++a<e;)(b=d[a])&&b();g=[];return c};c.create=a;c.raise=function(a){(c[a.type]||c.error).call(c,a.message)};c.convert=e;c.init=function(a){A=a._("Error");B=a._("Warning");s=a._("Notice");
r=a._("OK");setTimeout(k,1E3);return c};return c}({},v,z));n.register("$5",function(c,f,D){function p(a){var b=h("<pre>"+a+"</pre>").text();b&&(b=b.replace(/[\r\n]+/g,"\n").replace(/(^|\n)\s+/g,"$1").replace(/\s+$/,""));b||(b=a)||(b="Blank response from server");return b}function b(a){return(a=a.split(/[\r\n]/)[0])?(a=a.replace(/ +in +\S+ on line \d+/,""),a=a.replace(/^[()! ]+Fatal error:\s*/,"")):t._("Server returned invalid data")}function e(a,b,d){a[b]=d}function a(a,b,d){a.push({name:b,value:d})}
function d(a,b,d){a.append(b,d)}function m(a,d,e,c){function q(d,c,q){if("abort"!==c){var y=g||{_:function(a){return a}},k=d.status,f=d.responseText,w=p(f),h=d.getResponseHeader("Content-Type")||"text/html",s=d.getResponseHeader("Content-Length")||f.length;"success"===c&&q?m.error(q):(m.error(b(w)+".\n"+y._("Check console output for debugging information")),m.debug("Ajax failure for "+a,{status:k,error:c,message:q,output:f}),"parsererror"===c&&(q="Response not JSON"),m.log([y._("Provide the following text when reporting a problem")+
":","----","Status "+k+' "'+(q||y._("Unknown error"))+'" ('+h+" "+s+" bytes)",w,"===="].join("\n")));e&&e.call&&e(d,c,q);l=d}}c.url=A;c.dataType="json";c.error=q;c.success=function(a,b,e){var c=a&&a.data,g=a&&a.notices,r=g&&g.length;for(!c||a.error?q(e,b,a&&a.error&&a.error.message):d&&d(c,b,e);r--;)m.raise(g[r])};var m=n.require("$4","notices.js").clear();return h.ajax(c)}var k={},g,l,A=f.ajaxurl||"/wp-admin/admin-ajax.php";c.init=function(a){k=a.nonces||k;return c};c.localise=function(a){g=a;return c};
c.xhr=function(){return l};c.strip=p;c.parse=b;c.submit=function(a,b,d){function e(){c.removeClass("loading");n.require("$19","forms.js").enable(c)}var c=h(a),g=c.serialize();c.addClass("loading");n.require("$19","forms.js").disable(c);return m(a.route.value,function(a,d,c){e();b&&b(a,d,c)},function(a,b,c){e();d&&d(a,b,c)},{type:a.method,data:g})};c.post=function(b,c,g,l){var q,C=!0;c=c||{};(q=k[b])||(f.console&&console.debug&&console.debug('No nonce for "'+b+'"'),q="");var E=q;f.FormData&&c instanceof
FormData?(C=!1,q=d):q=Array.isArray(c)?a:e;q(c,"action","loco_json");q(c,"route",b);q(c,"loco-nonce",E);return m(b,g,l,{type:"post",data:c,processData:C,contentType:C?"application/x-www-form-urlencoded; charset=UTF-8":!1})};c.setNonce=function(a,b){k[a]=b;return c};c.hasNonce=function(a){return!!k[a]};return c}({},v,z));n.register("$20",{arab:1,aran:1,hebr:1,nkoo:1,syrc:1,syrn:1,syrj:1,syre:1,samr:1,mand:1,mend:1,thaa:1,adlm:1,cprt:1,phnx:1,armi:1,prti:1,phli:1,phlp:1,phlv:1,avst:1,mani:1,khar:1,
orkh:1,ital:1,lydi:1,ar:1,ary:1,ckb:1,dv:1,fa:1,he:1,nqo:1,ps:1,ur:1,yi:1});n.register("$6",function(c,f,h){function p(){}var b,e=n.require("$20","rtl.json");c.init=function(){return new p};c.cast=function(a){return a instanceof p?a:"string"===typeof a?c.parse(a):c.clone(a)};c.clone=function(a){var b,c=new p;for(b in a)c[b]=a[b];return c};c.parse=function(a){if(!(b||(b=/^([a-z]{2,3})(?:[-_]([a-z]{2}))?(?:[-_]([a-z0-9]{3,8}))?$/i)).exec(a))return null;var d=new p;d.lang=RegExp.$1.toLowerCase();if(a=
RegExp.$2)d.region=a.toUpperCase();if(a=RegExp.$3)d.variant=a.toLowerCase();return d};f=p.prototype;f.isValid=function(){return!!this.lang};f.isKnown=function(){var a=this.lang;return!(!a||"zxx"===a)};f.toString=function(a){a=a||"-";var b,c=this.lang||"zxx";if(b=this.region)c+=a+b;if(b=this.variant)c+=a+b;return c};f.getIcon=function(){for(var a=3,b,c,e=["variant","region","lang"],g=[];0!==a--;)if(b=e[a],c=this[b])g.push(b),g.push(b+"-"+c.toLowerCase());return g.join(" ")};f.isRTL=function(){return!!e[String(this.lang).toLowerCase()]};
f=null;return c}({},v,z));n.register("$38",function(c,f,h){function p(){}var b,e,a=n.require("$20","rtl.json");c.init=function(){return new p};c.cast=function(a){return a instanceof p?a:"string"===typeof a?c.parse(a):c.clone(a)};c.clone=function(a){var b,c=new p;for(b in a)c[b]=a[b];return c};c.parse=function(a){b||(e=/[-_+]/,b=/^([a-z]{2,3})(?:-([a-z]{4}))?(?:-([a-z]{2}|[0-9]{3}))?(?:-([0-9][a-z0-9]{3,8}|[a-z0-9]{5,8}))?(?:-([a-z]-[-a-z]+))?$/i);a=String(a).split(e).join("-");if(!b.exec(a))return null;
var c=new p;c.lang=RegExp.$1.toLowerCase();if(a=RegExp.$2)c.script=a.charAt(0).toUpperCase()+a.substr(1).toLowerCase();if(a=RegExp.$3)c.region=a.toUpperCase();if(a=RegExp.$4)c.variant=a.toLowerCase();if(a=RegExp.$5)c.extension=a;return c};f=p.prototype;f.isValid=function(){return!!this.lang};f.isKnown=function(){var a=this.lang;return!(!a||"zxx"===a)};f.toString=function(a){a=a||"-";var b,c=this.lang||"zxx";if(b=this.script)c+=a+b;if(b=this.region)c+=a+b;if(b=this.variant)c+=a+b;if(b=this.extension)c+=
a+b;return c};f.getIcon=function(){for(var a=4,b,c,e=["variant","region","script","lang"],l=[];0!==a--;)if(b=e[a],c=this[b])c.join&&(c=c.join("-")),1===a&&3===c.length?l.push("region-m49"):l=l.concat([b,b+"-"+c.toLowerCase()]);return l.join(" ")};f.isRTL=function(){return!!a[String(this.script||this.lang).toLowerCase()]};f=null;return c}({},v,z));n.register("$39",function(c,f,h){function p(a){f.console&&console.error&&console.error(a)}function b(){p("Method not implemented")}function e(){}function a(a){}
e.prototype.toString=function(){return"[Undefined]"};a.prototype._validate=function(a){var c,k,g=!0;for(c in this)k=this[c],k===b?(p(a+"."+c+"() must be implemented"),g=!1):k instanceof e&&(p(a+"."+c+" must be defined"),g=!1);return g};c.init=function(d,c){var k,g=new a;if(d)for(k=d.length;0!==k--;)g[d[k]]=b;if(c)for(k=c.length;0!==k--;)g[c[k]]=new e;return g};c.validate=function(a){var b=/function (\w+)\(/.exec(a.toString())?RegExp.$1:"";a.prototype._validate(b||"Object")};return c}({},v,z));n.register("$49",
function(c,f,h){var p=f.requestAnimationFrame,b=f.cancelAnimationFrame,e=0;if(!p||!b)for(var a in{ms:1,moz:1,webkit:1,o:1})if(p=f[a+"RequestAnimationFrame"])if(b=f[a+"CancelAnimationFrame"]||f[a+"CancelRequestAnimationFrame"])break;p&&b||(p=function(a){var b=d();timeToCall=Math.max(0,16-(b-e));nextTime=b+timeToCall;timerId=f.setTimeout(function(){a(nextTime)},timeToCall);e=nextTime;return timerId},b=function(a){clearTimeout(a)});var d=Date.now||function(){return(new Date).getTime()};c.loop=function(a,
d){function c(){f=p(c,d);a(e++)}var e=0,f;c();return{stop:function(){f&&b(f);f=null}}};return c}({},v,z));n.register("$45",function(c,f,h){function p(b,d,c,e){if(a){var g=c;c=function(a){if((a.MSPOINTER_TYPE_TOUCH||"touch")===a.pointerType)return g(a)}}b.addEventListener(d,c,e);return{unbind:function(){b.removeEventListener(d,c,e)}}}function b(a){a.preventDefault();a.stopPropagation();return!1}var e,a=!!f.navigator.msPointerEnabled,d=a?"MSPointerDown":"touchstart",m=a?"MSPointerMove":"touchmove",
k=a?"MSPointerUp":"touchend";c.ok=function(a){null==e&&(e="function"===typeof h.body.addEventListener);e&&a&&a(c);return e};c.ms=function(){return a};c.dragger=function(a,c){function e(b){a.addEventListener(b,l[b],!1)}function r(b){a.removeEventListener(b,l[b],!1)}var l={};l[d]=function(a){g(a,function(b,e){e.type=d;c(a,e,q)});e(m);e(k);return!0};l[k]=function(a){r(m);r(k);g(a,function(b,d){d.type=k;c(a,d,q)});return!0};l[m]=function(a){g(a,function(b,d){d.type=m;c(a,d,q)});return b(a)};e(d);var q=
{kill:function(){r(d);r(m);r(k);a=q=c=null}};return q};c.swiper=function(c,e,f){function r(a){c.addEventListener(a,u[a],!1)}function w(a){c.removeEventListener(a,u[a],!1)}function q(){h&&h.stop();h=null}var h,E,p,u={},y=[],F=[],K=[];u[d]=function(a){E=!1;q();var b=l();g(a,function(a,d){y[a]=b;F[a]=d.clientX;K[a]=d.clientY});p=c.scrollLeft;return!0};u[k]=function(a){g(a,function(a,b){var d=l()-y[a],c=F[a]-b.clientX,d=Math.abs(c)/d;e(d,c?0>c?-1:1:0)});p=null;return!0};u[m]=function(a){var d,e;null==
p||g(a,function(a,b){d=F[a]-b.clientX;e=K[a]-b.clientY});if(e&&Math.abs(e)>Math.abs(d))return E=!0;d&&(E=!0,c.scrollLeft=Math.max(0,p+d));return b(a)};if(!a||f)r(d),r(m),r(k),a&&(c.className+=" mstouch");return{kill:function(){w(d);w(m);w(k);q()},swiped:function(){return E},ms:function(){return a},snap:function(b){a&&!f&&(c.style["-ms-scroll-snap-points-x"]="snapInterval(0px,"+b+"px)",c.style["-ms-scroll-snap-type"]="mandatory",c.style["-ms-scroll-chaining"]="none")},scroll:function(a,b,d){q();var e=
c.scrollLeft,g=a>e?1:-1,r=Math[1===g?"min":"max"],y=Math.round(16*b*g);return h=n.require("$49","fps.js").loop(function(b){b&&(e=Math.max(0,r(a,e+y)),c.scrollLeft=e,a===e&&(q(),d&&d(e)))},c)}}};c.start=function(a,b){return p(a,d,b,!1)};c.move=function(a,b){return p(a,m,b,!1)};c.end=function(a,b){return p(a,k,b,!1)};var g=c.each=function(b,d){if(a)(b.MSPOINTER_TYPE_TOUCH||"touch")===b.pointerType&&d(0,b);else for(var c=-1,e=(b.originalEvent||b).changedTouches||[];++c<e.length;)d(c,e[c])},l=Date.now||
function(){return(new Date).getTime()};return c}({},v,z));n.register("$50",function(c,f,n){c.init=function(c){function b(){f.style.top=String(-c.scrollTop)+"px";return!0}function e(){var b=f;b.textContent=c.value;b.innerHTML=b.innerHTML.replace(/[ \t]/g,a).split(/(?:\n|\r\n?)/).join('<span class="eol crlf"></span>\r\n')+'<span class="eol eof"></span>';return!0}function a(a){return'<span class="x'+a.charCodeAt(0).toString(16)+'">'+a+"</span>"}var d=c.parentNode,f=d.insertBefore(n.createElement("div"),
c);h(c).on("input",e).on("scroll",b);h(d).addClass("has-mirror");f.className="ta-mirror";var k=c.offsetWidth-c.clientWidth;2<k&&(f.style.marginRight=String(k-2)+"px");e();b();return{kill:function(){h(c).off("input",e).off("scroll",b);d.removeChild(f);f=null;h(d).removeClass("has-mirror")}}};return c}({},v,z));n.register("$30",function(c,f,h){function p(a,d){for(var c=0,e=-1,g=d&&f[d],l=b[a]||[],h=l.length;++e<h;)callback=l[e],"function"===typeof callback&&(callback(g),c++);return c}var b={},e;c.load=
function(a,d,c){function e(){A&&(clearTimeout(A),A=null);n&&(n.onreadystatechange=null,n=n=n.onload=null);a&&(delete b[a],a=null)}function g(b,d){var g=n&&n.readyState;if(d||!g||"loaded"===g||"complete"===g)d||p(a,c),e()}function l(){if(0===p(a))throw Error('Failed to load "'+(c||a)+'"');e()}if(c&&f[c])"function"===typeof d&&d(f[c]);else if(null!=b[a])b[a].push(d);else{b[a]=[d];var A=setTimeout(l,4E3),n=h.createElement("script");n.setAttribute("src",a);n.setAttribute("async","true");n.onreadystatechange=
g;n.onload=g;n.onerror=l;n.onabort=e;h.getElementsByTagName("head")[0].appendChild(n)}};c.stat=function(a){var b;if(!(b=e)){for(var c,f,g=h.getElementsByTagName("script"),l=-1,p=g.length;++l<p;)if(b=g[l].getAttribute("src"))if(c=b.indexOf("/lib/vendor"),-1!==c){f=b.substr(0,c);break}b=e=f||"/static"}return b+a};return c}({},v,z));n.register("$15",function(c,f,D){function p(a,b){a.setReadOnly(!1);a.on("change",function(a,c){return b.val(c.getValue())});a.on("focus",function(){return b.focus()});a.on("blur",
function(){return b.blur()})}function b(a){a.off("change");a.off("focus");a.off("blur")}function e(a){b(a);a.setReadOnly(!0);a.setHighlightGutterLine(!1);a.setHighlightActiveLine(!1)}function a(a,b){function c(){this.HighlightRules=e}var e=d(b),g=a.require,f=g("ace/lib/oop");f.inherits(e,g("ace/mode/text_highlight_rules").TextHighlightRules);f.inherits(c,g("ace/mode/text").Mode);return new c}function d(a){return function(){var b={start:[{token:"empty_line",regex:/^$/},{token:"constant.language",regex:"(?:&#[0-9]+;)|(?:&#x[0-9a-fA-F]+;)|(?:&[a-zA-Z0-9_:\\.-]+;)"},
{token:"constant.language",regex:/<!\[CDATA\[/},{token:"constant.language",regex:/\]\]>/},{token:"locked",regex:/<(?:xliff:)?(?:g|ph)[^>]*>[^<]*<\/(?:xliff:)?(?:g|ph)>/},{token:"locked",regex:/<(?:xliff:)?(bx|ex|x)[^\/>]*\/>/},{token:"constant.language",regex:/<\/?[:a-z]+[^>]*>/}]},c=m(a);"icu"===a?b={start:b.start.concat([{token:"icu-quoted",regex:/'([{}][^']*)?'/},{token:"printf",regex:"{[^!-/:-@\\[-^{-~\u00a1\u00a2\u00a3\u00a4\u00a5\u00a6\u00a7\u00a9\u00ab\u00ac\u00ae\u00b0\u00b1\u00b6\u00bb\u00bf\u00d7\u00f7\\u2010-\\u2027\\u2030-\\u203E\\u2041-\\u2053\\u2055-\\u205E\\u2190-\\u245F\\u2500-\\u2775\\u2794-\\u2BFF\\u2E00-\\u2E7F\\u3001-\\u3003\\u3008-\\u3020\\u3030\\uFD3E\\uFD3F\\uFE45\\uFE46]+(,[\\s\\u0085\\u200E\\u200F\\u2028\\u2029]*(?:number|date|time|spellout|ordinal|duration)[\\s\\u0085\\u200E\\u200F\\u2028\\u2029]*(,[\\s\\u0085\\u200E\\u200F\\u2028\\u2029]*[^{}]+)?)?}"},
{token:"icu",regex:/{/,next:"icuName"},{token:"icu",regex:/}/,next:"icuType"}]),icuName:[{token:"icu",regex:"[\\s\\u0085\\u200E\\u200F\\u2028\\u2029]+"},{token:"icu.name",regex:"[^\\s\\u0085\\u200E\\u200F\\u2028\\u2029!-/:-@\\[-^{-~\u00a1\u00a2\u00a3\u00a4\u00a5\u00a6\u00a7\u00a9\u00ab\u00ac\u00ae\u00b0\u00b1\u00b6\u00bb\u00bf\u00d7\u00f7\\u2010-\\u2027\\u2030-\\u203E\\u2041-\\u2053\\u2055-\\u205E\\u2190-\\u245F\\u2500-\\u2775\\u2794-\\u2BFF\\u2E00-\\u2E7F\\u3001-\\u3003\\u3008-\\u3020\\u3030\\uFD3E\\uFD3F\\uFE45\\uFE46]+",
next:"icuType"},{defaultToken:"icu",next:"icuType"}],icuType:[{token:"icu",regex:/[{}]/,next:"start"},{defaultToken:"icu"}]}:c&&b.start.push({token:"printf",regex:c});this.$rules=b}}function m(a){switch(a){case "objc":return/%(?:\d+\$)?[-+'0# ]*\d*(?:\.\d+|\.\*(?:\d+\$)?)?(?:hh?|ll?|[qjzTL])?[sScCdDioOuUxXfFeEgGaAp%@]/;case "java":return/%(?:\d+\$)?[-+,(0# ]*\d*(?:\.\d+)?(?:[bBhHsScCdoxXeEfgGaA%n]|[tT][HIklMSLNpzZsQBbhAaCYyjmdeRTrDFc])/;case "php":return/%(?:\d+\$)?(?:'.|[-+0 ])*\d*(?:\.\d+)?[suxXbcdeEfFgGo%]/;
case "python":return/%(?:\([a-z]+\))?[-+0# ]*(?:\d+|\*)?(?:\.\d+|\.\*)?(?:[hlL])?[sdiouxXeEfFgGcra%]/;case "javascript":return/%(?:[1-9]\d*\$)?\+?(?:0|'[^$])?-?\d*(?:\.\d+)?[b-gijostTuvxX%]/;case "auto":return/%(?:\d+\$|\([a-z]+\))?(?:[-+0]?\d*(\.\d+)?[duxoefgaDUXOEFGA]|[@scSC%])/;case g:return k||"%%"}}var k,g="auto";c.init=function(c,d,k){var m,r=!1,w=k||g,q=c.parentNode,C=q.appendChild(D.createElement("div"));h(q).addClass("has-proxy has-ace");n.require("$30","remote.js").load("https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.1/ace.js",
function(b){if(C){if(!b)throw Error("Failed to load code editor");m=b.edit(C);var q=m.session,g=m.renderer;m.$blockScrolling=Infinity;m.setShowInvisibles(r);m.setWrapBehavioursEnabled(!1);m.setBehavioursEnabled(!1);m.setHighlightActiveLine(!1);q.setUseSoftTabs(!1);g.setShowGutter(!0);g.setPadding(10);g.setScrollMargin(8);q.setMode(a(b,w));m.setValue(c.value,-1);q.setUseWrapMode(!0);d?p(m,d):e(m)}},"ace");return{kill:function(){m&&(b(m),m.destroy(),m=null);C&&(q.removeChild(C),h(q).removeClass("has-proxy has-ace"),
C=null);return this},disable:function(){m&&e(m);d=null;return this},enable:function(a){d=a;m&&p(m,a);return this},resize:function(){m&&m.resize();return this},val:function(a){m&&a!==m.getValue()&&m.setValue(a,-1);return this},invs:function(a){a=a||!1;r!==a&&(r=a,m&&m.setShowInvisibles(a));return this},strf:function(b){b=b||g;b!==w&&(w=b,m&&m.session.setMode(a(f.ace,b)));return this},focus:function(){return this}}};c.strf=function(a,b){g=a;k=b;return c};return c}({},v,z));n.register("$51",function(c,
f,D){function p(a,b){function c(){return b.val(a.getContent())}a.on("input",c);a.on("change",c);a.on("focus",function(){return b.focus()});a.on("blur",function(){return b.blur()});a.setMode("design")}function b(a){a.off("input");a.off("change");a.off("focus");a.off("blur")}function e(a){b(a);a.setMode("readonly")}var a=0;c.load=function(a){var b=n.require("$30","remote.js");b.load(b.stat("/lib/tinymce.min.js"),a,"tinymce");return c};c.init=function(d,f){function k(a){B=a;s="<p>"===a.substr(0,3)&&
"</p>"===a.substr(-4);return a.replace(/(<\/?)script/ig,"$1loco:script")}function g(a){l=a;a._getContent=a.getContent;a.getContent=function(a){a=this._getContent(a);a=a.replace(/(<\/?)loco:script/ig,"$1script");if(!s&&"<p>"===a.substr(0,3)&&"</p>"===a.substr(-4)){var b=a.substr(3,a.length-7);if(b===B||-1===b.indexOf("</p>"))a=b}return a};a._setContent=a.setContent;a.setContent=function(a,b){return this._setContent(k(a),b)};f?(p(a,f),f.reset()):e(a);h(q).removeClass("loading")}var l,n=!1,B="",s=!1,
r=d.parentNode,w=r.parentNode,q=r.appendChild(D.createElement("div")),C=w.insertBefore(D.createElement("nav"),r);C.id="_tb"+String(++a);h(r).addClass("has-proxy has-mce");h(q).addClass("mce-content-body loading").html(k(d.value));c.load(function(a){if(!a)throw Error("Failed to load HTML editor");q&&a.init({inline:!0,target:q,hidden_input:!1,theme:"modern",skin:!1,plugins:"link lists",browser_spellcheck:!0,menubar:!1,fixed_toolbar_container:"#"+C.id,toolbar:"formatselect | bold italic link unlink | bullist numlist outdent indent",
block_formats:"Paragraph=p;Heading 1=h1;Heading 2=h2;Heading 3=h4;Heading 4=h4;Heading 5=h5;Heading 6=h6;",forced_root_block:"p",relative_urls:!1,convert_urls:!1,remove_script_host:!1,document_base_url:"",allow_script_urls:!1,formats:{alignleft:{classes:"alignleft"},alignright:{selector:"p,h1,h2,h3,h4,span,strong,em,a",classes:"alignright"},aligncenter:{selector:"p,h1,h2,h3,h4,span,strong,em,a",classes:"aligncenter"},strikethrough:{inline:"del"}},fix_list_elements:!0,extended_valid_elements:"span,b,i,u,loco:script",
entities:"38,amp,60,lt,62,gt,160,nbsp",entity_encoding:"named",keep_styles:!1,init_instance_callback:g})});return{val:function(a){a=k(a);null==l?(d.value=a,h(q).html(a)):l.getContent()!==a&&l.setContent(a);f&&f.val(a);return this},kill:function(){l&&(f&&f.val(l.getContent()),b(l),l.destroy(),l=null);q&&(r.removeChild(q),h(r).removeClass("has-proxy has-mce"),q=null);C&&(w.removeChild(C),C=null);return this},enable:function(a){f=a;l&&p(l,a);return this},disable:function(){l&&e(l);f=null;return this},
focus:function(){l&&f&&l.focus();return this},invs:function(a){a=a||!1;n!==a&&(n=a,h(r)[a?"addClass":"removeClass"]("show-invs"));return this}}};return c}({},v,z));n.register("$46",function(c,f,D){function p(a){function b(){p&&(n.off("input",c),p=!1)}function c(){var b=a.value;b!==r&&(n.trigger("changing",[b,r]),r=b)}function f(){c();p&&w!==r&&n.trigger("changed",[r])}function g(){e=a;w=r;p||(n.on("input",c),p=!0);n.trigger("editFocus");s.addClass("has-focus");return!0}function l(){e===a&&(e=null);
n.trigger("editBlur");s.removeClass("has-focus");p&&(f(),b());return!0}var p=!1,n=h(a),s=h(a.parentNode),r=a.value,w;n.blur(l).focus(g);return{val:function(b){r!==b&&(a.value=b,n.triggerHandler("input"),r=b);return!0},kill:function(){b();n.off("blur",l).off("focus",g)},fire:function(){r=null;c()},ping:f,blur:l,focus:g,reset:function(){w=r=a.value}}}function b(a){this.e=a}var e;c._new=function(a){return new b(a)};c.init=function(a){var c=new b(a);a.disabled?(a.removeAttribute("disabled"),c.disable()):
a.readOnly?c.disable():c.enable();return c};TextAreaPrototype=b.prototype;TextAreaPrototype.destroy=function(){this.unlisten();var a=this.p;a&&(a.kill(),this.p=null);this.e=null};TextAreaPrototype.reload=function(a,b){var c=this.l;c&&!b&&(this.disable(),c=null);this.val(a||"");b&&!c&&this.enable();return this};TextAreaPrototype.val=function(a){var b=this.e;if(null==a)return b.value;var c=this.l,e=this.p;e&&e.val(a);c&&c.val(a);c||b.value===a||(b.value=a,h(b).triggerHandler("input"));return this};
TextAreaPrototype.fire=function(){this.l&&this.l.fire();return this};TextAreaPrototype.ping=function(){this.l&&this.l.ping();return this};TextAreaPrototype.focus=function(){var a=this.p;a?a.focus():h(this.e).focus()};TextAreaPrototype.focused=function(){return e&&e===this.el};TextAreaPrototype.parent=function(){return this.e.parentNode};TextAreaPrototype.attr=function(a,b){var c=this.e;if(1===arguments.length)return c.getAttribute(a);null==b?c.removeAttribute(a):c.setAttribute(a,b);return this};TextAreaPrototype.editable=
function(){return!!this.l};TextAreaPrototype.enable=function(){var a=this.p;this.e.removeAttribute("readonly");this.listen();a&&a.enable&&a.enable(this.l);return this};TextAreaPrototype.disable=function(){var a=this.p;this.e.setAttribute("readonly",!0);this.unlisten();a&&a.disable&&a.disable();return this};TextAreaPrototype.listen=function(){var a=this.l;a&&a.kill();this.l=p(this.e);return this};TextAreaPrototype.unlisten=function(){var a=this.l;a&&(a.kill(),this.l=null);return this};TextAreaPrototype.setInvs=
function(a,b){var c=this.i||!1;if(b||c!==a)this._i&&(this._i.kill(),delete this._i),(c=this.p)?c.invs&&c.invs(a):a&&(this._i=n.require("$50","mirror.js").init(this.e)),this.i=a;return this};TextAreaPrototype.getInvs=function(){return this.i||!1};TextAreaPrototype.setMode=function(a){var b=this.p,c=this.i||!1;a!==(this.m||"")&&(this.m=a,b&&b.kill(),this.p=b="code"===a?n.require("$15","ace.js").init(this.e,this.l,this["%"]):"html"===a?n.require("$51","mce.js").init(this.e,this.l):null,this.setInvs(c,
!0),e&&this.focus());return this};TextAreaPrototype.setStrf=function(a){this["%"]=a;"code"===this.m&&this.p.strf(a);return this};TextAreaPrototype.name=function(a){this.e.setAttribute("name",a);return this};TextAreaPrototype.placeholder=function(a){this.e.setAttribute("placeholder",a);return this};TextAreaPrototype.redraw=function(){var a=this.p;a&&a.resize&&a.resize()};TextAreaPrototype=null;return c}({},v,z));n.register("$47",function(c,f,n){function p(a){var b=f.console;b&&b.error&&b.error(a)}
function b(a){var b=n.createElement("div");a&&b.setAttribute("class",a);return b}function e(a){return function(){a.resize();return this}}function a(a){return function(b){for(var c=b.target,d=c.$index;null==d&&"DIV"!==c.nodeName&&(c=c.parentElement);)d=c.$index;null!=d&&(b.stopImmediatePropagation(),a.select(d));return!0}}function d(a){return function(){a.redrawDirty()&&a.redraw();return!0}}function m(a){return function(b){var c;c=b.keyCode;if(40===c)c=1;else if(38===c)c=-1;else return!0;if(b.shiftKey||
b.ctrlKey||b.metaKey||b.altKey)return!0;a.selectNext(c);b.stopPropagation();b.preventDefault();return!1}}function k(a,b,c){function d(a){p("row["+a+"] disappeared");return{cellVal:function(){return""}}}return function(e){var g=b||0,f=c?-1:1,y=a.rows||[];e.sort(function(a,b){return f*(y[a]||d(a)).cellVal(g).localeCompare((y[b]||d(b)).cellVal(g))})}}function g(a){this.w=a}function l(a){this.t=a;this.length=0}function A(a,b,c){var d=n.createElement("div");d.className=c||"";this._=d;this.d=b||[];this.i=
a||0;this.length=b.length}function B(a){this.live=a;this.rows=[]}c.create=function(a){return new g(a)};var s=g.prototype;s.init=function(c){var g=this.w,q=g.id,f=g.splity(q+"-thead",q+"-tbody"),l=f[0],f=f[1],k=[],p=[],y=[],F=[];if(c)this.ds=c,this.idxs=p,this._idxs=null;else if(!(c=this.ds))throw Error("No datasource");l.css.push("wg-thead");f.css.push("wg-tbody");c.eachCol(function(a,b,c){y[a]=q+"-col-"+b;F[a]=c||b});for(var n=b(),s=-1,B=y.length,D=b("wg-cols"),v=l.splitx.apply(l,y);++s<B;)v[s].header(F[s]),
D.appendChild(n.cloneNode(!1)).setAttribute("for",y[s]);c.eachRow(function(a,b,c){k[a]=new A(a,b,c);p[a]=a});this.rows=k;this.cols=D;this.ww=null;this.root=n=f.body;this.head=l;l.redraw=e(this);l=f.fixed=v[0].bodyY()||20;g.lock().resize(l,f);g.css.push("is-table");g.restyle();this.sc?this._re_sort(B):c.sort&&c.sort(p);this.redrawDirty();this.render();h(n).attr("tabindex","-1").on("keydown",m(this)).on("mousedown",a(this)).on("scroll",d(this));return this};s.clear=function(){for(var a=this.pages||
[],b=a.length;0!==b--;)a[b].destroy();this.pages=[];this.sy=this.mx=this.mn=this.vh=null;void 0;return this};s.render=function(){for(var a,b,c=[],d=this.rows||[],e=-1,g,f=this.idxs,y=f.length,l=this.idxr={},k=this.r,h=this._r,m=this.root,p=this.cols;++e<y;){0===e%100&&(a=p.cloneNode(!0),b=new B(a),b.h=2200,b.insert(m),c.push(b));g=f[e];l[g]=e;a=d[g];if(null==a)throw Error("Render error, no data at ["+g+"]");a.page=b;b.rows.push(a)}b&&100!==b.size()&&b.sleepH(22);this.pages=c;this.mx=this.mn=null;
this.redrawDirty();this.redraw();null==k?null!=h&&(a=d[h])&&a.page&&(delete this._r,this.select(h,!0)):(a=d[k])&&a.page?this.select(k,!0):(this.deselect(),this._r=k);return this};s.resize=function(){var a=-1,b=this.ww||(this.ww=[]),c=this.w,d=c.cells[0],e=d.body.childNodes,g=e.length,f=this.pages||[],y=f.length;for(c.redraw.call(d);++a<g;)b[a]=e[a].style.width;if(y){c=this.mx;for(a=this.mn;a<=c;a++)f[a].widths(b);this.redrawDirty()&&this.redraw()}};s.redrawDirty=function(){var a=!1,b=this.root,c=
b.scrollTop,b=b.clientHeight;this.sy!==c&&(a=!0,this.sy=c);this.vh!==b&&(a=!0,this.vh=b);return a};s.redraw=function(){for(var a=0,b=-1,c=null,d=null,e=this.ww,g=this.sy,f=this.vh,y=this.mn,l=this.mx,k=Math.max(0,g-100),g=f+g+100,h=this.pages||[],m=h.length;++b<m&&!(a>g);)f=h[b],a+=f.height(),a<k||(null===c&&(c=b),d=b,f.rendered||f.render(e));if(y!==c){if(null!==y&&c>y)for(b=y;b<c;b++){f=h[b];if(!f)throw Error("Shit!");f.rendered&&f.sleep()}this.mn=c}if(l!==d){if(null!==l&&d<l)for(b=l;b>d;b--)f=h[b],
f.rendered&&f.sleep();this.mx=d}};s.selected=function(){return this.r};s.thead=function(){return this.w.cells[0]};s.tbody=function(){return this.w.cells[1]};s.tr=function(a){return(a=this.row(a))?a.cells():[]};s.row=function(a){return this.rows[a]};s.td=function(a,b){return this.tr(a)[b]};s.next=function(a,b,c){null==c&&(c=this.r||0);var d=this.idxs,e=d.length,g=(this.idxr||{})[c];for(c=g;c!==(g+=a)&&!(0<=g&&e>g);)if(b&&e)g=1===a?-1:e,b=!1;else return null;c=d[g];return null==c||null==this.rows[c]?
(p("Bad next: ["+g+"] does not map to data row"),null):c};s.selectNext=function(a,b,c){a=this.next(a,b);null!=a&&this.r!==a&&this.select(a,c);return this};s.deselect=function(a){var b=this.r;null!=b&&(this.r=null,h(this.tr(b)).removeClass("selected"),this.w.fire("wgRowDeselect",[b,a]));return this};s.selectRow=function(a,b){return this.select(this.idxs[a])};s.select=function(a,b){var c=this.rows[a],d=c&&c.page;if(!d)return this.deselect(!1),p("Row is filtered out"),this;this.deselect(!0);var e,g=
this.w.cells[1];d.rendered||(e=d.top(),g.scrollY(e),this.redrawDirty()&&this.redraw());if(!c.rendered)return d.rendered||p("Failed to render page"),p("Row ["+c.i+"] not rendered"),this;d=c.cells();h(d).addClass("selected");this.r=a;b||(e=g.scrollY(),h(this.root).focus(),e!==g.scrollY()&&g.scrollY(e));g.scrollTo(d[0],!0);this.w.fire("wgRowSelect",[a,c.data()]);return this};s.unfilter=function(){this._idxs&&(this.idxs=this._sort(this._idxs),this._idxs=null,this.clear().render());return this};s.filter=
function(a){this._idxs||(this._idxs=this.idxs);this.idxs=this._sort(a);return this.clear().render()};s.each=function(a){for(var b,c=-1,d=this.rows||[],e=this.idxs||[],g=e.length;++c<g;)b=e[c],a(d[b],c,b);return this};s.sortable=function(a){var b=this.sc||(this.sc=new l(this));b.has(a)||b.add(a);return this};s._re_sort=function(a){var b=-1,c=this.sc,d=c.active;for(this.sc=c=new l(this);++b<a;)c.add(b);d&&(b=this.head.indexOf(d.id),-1===b&&(b=Math.min(d.idx,a-1)),this.sort(b,d.desc));return this};s._sort=
function(a,b){b?(this.s=b,b(a)):(b=this.s)&&b(a);return a};s.sort=function(a,b){this._sort(this.idxs,k(this,a,b));this.sc.activate(a,b);return this};s=null;s=l.prototype;s.has=function(a){return null!=this[a]};s.add=function(a){var b=this,c=b.t.head.cells[a];b[a]={desc:null,idx:a,id:c.id};b.length++;c.addClass("wg-sortable").on("click",function(c){if("header"===c.target.nodeName.toLowerCase())return c.stopImmediatePropagation(),b.toggle(a),!1});return b};s.toggle=function(a){this.t.sort(a,!this[a].desc).clear().render();
return this};s.activate=function(a,b){var c,d;c=this.active;var e=this[a],g=this.t.head.cells;c&&(d=g[c.idx])&&(d.removeClass(c.css),c!==e&&d.restyle());(d=g[a])?(e.desc=b,this.active=e,c="wg-"+(b?"desc":"asc"),d.addClass(c).restyle(),e.css=c):this.active=null;return this};s=null;s=A.prototype;s.render=function(a){var b,c=[],d=this._,e=this.length;if(d){for(this.c=c;0!==e--;)b=d.cloneNode(!1),c[e]=this.update(e,b),b.$index=this.i,a[e].appendChild(b);this._=null}else for(c=this.c;0!==e--;)a[e].appendChild(c[e]);
this.rendered=!0;return this};s.update=function(a,b){var c=b||this.c[a]||{},d=(this.d[a]||function(){})()||"\u00a0";null==d.innerHTML?c.textContent=d:c.innerHTML=d.innerHTML;return c};s.cells=function(){return this.c||[]};s.data=function(){for(var a=-1,b=[],c=this.length;++a<c;)b[a]=this.cellVal(a);return b};s.destroy=function(){this.page=null;this.rendered=!1};s.cellVal=function(a){a=this.d[a]()||"";return String(a.textContent||a)};s=null;s=B.prototype;s.size=function(){return this.rows.length};
s.insert=function(a){var c=this.h,d=b("wg-dead");d.style.height=String(c)+"px";a.appendChild(d);return this.dead=d};s.top=function(){return(this.rendered?this.live:this.dead).offsetTop};s.height=function(){var a=this.h;null==a&&(this.h=a=this.rendered?this.live.firstChild.offsetHeight:this.dead.offsetHight);a||p("row has zero height");return a};s.render=function(a){for(var b,c=-1,d=this.rows,e=d.length,g=this.dead,f=this.live,y=f.childNodes;++c<e;)b=d[c],b.rendered||b.render(y);e=a.length;for(c=0;c<
e;c++)y[c].style.width=a[c];g.parentNode.replaceChild(f,g);this.rendered=!0;this.h=null;return this};s.sleep=function(){var a=this.height(),b=this.live,c=this.dead;c.style.height=String(a)+"px";b.parentNode.replaceChild(c,b);this.rendered=!1;this.h=a;return this};s.sleepH=function(a){a*=this.rows.length;var b=this.dead;b&&(b.style.height=String(a)+"px");this.rendered||(this.h=a);return this};s.widths=function(a){for(var b=this.live.childNodes,c=a.length;0!==c--;)b[c].style.width=a[c];return this};
s.destroy=function(){var a=this.rendered?this.live:this.dead,b=this.rows,c=b.length;for(a.parentNode.removeChild(a);0!==c--;)b[c].destroy()};s=null;return c}({},v,z));n.register("$40",function(c,f,D){function p(a,b){var c=a.id,d=c&&s[c],e=d&&d.parent();if(!d||!e)return null;var g=e.dir===B,c=g?"X":"Y",f="page"+c,g=g?A:l,k=g(e.el),c=b["offset"+c],h=e.el,m=h.className;null==c&&(c=b[f]-g(a));c&&(k+=c);h.className=m+" is-resizing";return{done:function(){h.className=m},move:function(a){e.resize(a[f]-k,
d);return!0}}}function b(b,c){function d(){h(D).off("mousemove",e);w&&(w.done(),w=null);return!0}function e(a){w?w.move(a):d();return!0}if(w)return!0;w=p(b.target,b);if(!w)return!0;h(D).one("mouseup",d).on("mousemove",e);return a(b)}function e(a,b){var c=b.type;"touchmove"===c?w&&w.move(b):"touchstart"===c?w=p(a.target,b):"touchend"===c&&w&&(w.done(),w=null)}function a(a){a.stopPropagation();a.preventDefault();return!1}function d(a){var b=r;b&&b.redraw();a&&a.redraw();return r=a}function m(a,b){var c=
h(b).on("editFocus",function(){c.trigger("wgFocus",[d(a)])}).on("editBlur",function(){c.trigger("wgBlur",[d(null)])})}function k(a){var b=a.id,c=a.className;this.id=b;this.el=a;this.pos=this.index=0;this.css=[c||"wg-root","wg-cell"];this._cn=c;s[b]=this;this.clear()}var g=n.require("$18","html.js"),l=g.top,A=g.left,B=1,s={},r,w=!1;c.init=function(a){var c=new k(a);c.redraw();n.require("$45","touch.js").ok(function(b){b.dragger(a,e)});h(a).mousedown(b);return c};f=k.prototype;f.fire=function(a,b){var c=
h.Event(a);c.cell=this;h(this.el).trigger(c,b);return this};f.each=function(a){for(var b=-1,c=this.cells,d=c.length;++b<d;)a(c[b],b);return this};f.indexOf=function(a){return(a=s[a.id||String(a)])&&a.pid===this.id?a.index:-1};f.on=function(){return this.$("on",arguments)};f.off=function(){return this.$("off",arguments)};f.find=function(a){return h(this.el).find(a)};f.$=function(a,b){h.fn[a].apply(h(this.el),b);return this};f.addClass=function(a){this.css.push(a);return this};f.removeClass=function(a){a=
this.css.indexOf(a);-1!==a&&this.css.splice(a,1);return this};f.parent=function(){return this.pid&&s[this.pid]};f.splitx=function(){return this._split(B,arguments)};f.splity=function(){return this._split(2,arguments)};f._split=function(a,b){(this.length||this.field)&&this.clear();for(var c=-1,d,e=b.length,f=1/e,l=0;++c<e;){d=g.el();this.body.appendChild(d);for(var h=d,m=b[c],p=m,n=1;s[m];)m=p+"-"+ ++n;h.id=m;d=new k(d);d.index=c;d.pid=this.id;d._locale(this.lang,this.rtl);d.pos=l;l+=f;this.cells.push(d);
this.length++}this.dir=a;this.redraw();return this.cells};f.destroy=function(){this.clear();delete s[this.id];var a=this.el;a.innerHTML="";this.body=null;a.className=this._cn||"";h(a).off();return this};f.exists=function(){return this===s[this.id]};f.clear=function(){for(var a=this.el,b=this.cells,c=this.field,d=this.body,e=this.nav,f=this.length||0;0!==f--;)delete s[b[f].destroy().id];this.cells=[];this.length=0;e&&(a.removeChild(e),this.nav=null);d&&(c&&(g.ie()&&h(d).triggerHandler("blur"),c.destroy(),
this.field=null),this.table&&(this.table=null),a===d.parentNode&&a.removeChild(d));this.body=a.appendChild(g.el("","wg-body"));this._h=null;return this};f.resize=function(a,b){if(!b&&(b=this.cells[1],!b))return;var c=b.index,d=this.cells,e=h(this.el)[this.dir===B?"width":"height"](),g=d[c+1],c=d[c-1];pad=(b.body||b.el.firstChild).offsetTop||0;max=(g?g.pos*e:e)-pad;min=c?c.pos*e:0;b.pos=Math.min(max,Math.max(min,a))/e;this.redraw();return this};f.distribute=function(a){for(var b=-1,c=0,d,e=this.cells,
g=a.length;++b<g&&(d=e[++c]);)d.pos=Math.max(0,Math.min(1,a[b]));this.redraw();return this};f.distribution=function(){for(var a=[],b=0,c=this.cells,d=c.length-1;b<d;)a[b]=c[++b].pos;return a};f.restyle=function(){var a=this.css.concat();0===this.index?a.push("first"):a.push("not-first");this.dir&&(a.push("wg-split"),2===this.dir?a.push("wg-split-y"):a.push("wg-split-x"));this.t&&a.push("has-title");this.nav&&a.push("has-nav");this.field&&(a.push("is-field"),this.field.editable()?a.push("is-editable"):
a.push("is-readonly"));a=a.join(" ");a!==this._css&&(this._css=a,this.el.className=a);return this};f.redraw=function(a){this.restyle();var b=this.el,c=this.body,d=this.field;if(c){var e,g=b.clientWidth||0,f=b.clientHeight||0,l=c.offsetTop||0,f=l>f?0:f-l;this._h!==f&&(this._h=f,c.style.height=String(f)+"px",e=d);this._w!==g&&(this._w=g,e=d);e&&e.redraw()}c=this.length;g=1;f=this.nav;for(l=2===this.dir?"height":"width";0!==c--;)d=this.cells[c],f?e=1:(d.fixed&&(d.pos=d.fixed/h(b)[l]()),e=g-d.pos,g=d.pos),
d.el.style[l]=String(100*e)+"%",d.redraw(a);return this};f.contents=function(a,b){var c=this.el,d=this.body;if(null==a)return d.innerHTML;this.length?this.clear():d&&(c.removeChild(d),d=null);d||(this.body=d=c.appendChild(g.el("",b||"wg-content")),this._h=null,(c=this.lang)&&this._locale(c,this.rtl,!0));"string"===typeof a?h(d)._html(a):a&&this.append(a);this.redraw();return this};f.textarea=function(a,b){var c=this.field;if(c){var d=c.editable();c.reload(a,b);d!==b&&this.restyle()}else this.length&&
this.clear(),d=g.el("textarea"),d.setAttribute("wrap","virtual"),d.value=a,this.contents(d),c=n.require("$46","field.js")._new(d)[b?"enable":"disable"](),m(this,d),this.field=c,this.restyle();this.lang||this.locale("en");return c};f.locale=function(a){a=n.require("$38","locale.js").cast(a);return this._locale(String(a),a.isRTL())};f._locale=function(a,b,c){var d=this.body;if(c||a!==this.lang)this.lang=a,d&&d.setAttribute("lang",a);if(c||b!==this.rtl)this.rtl=b,d&&d.setAttribute("dir",b?"RTL":"LTR");
return this};f.editable=function(){var a=this.field;if(a)return a.editable()?a:null;var b=this.cells,c=b.length,d=this.navigated();if(null!=d)return b[d].editable();for(;++d<c;){for(d=0;d<c;c++);if(a=b[d].editable())return a}};f.eachTextarea=function(a){var b=this.field;b?a(b):this.each(function(b){b.eachTextarea(a)});return this};f.append=function(a){a&&(a.nodeType?g.init(this.body.appendChild(a)):g.init(h(a).appendTo(this.body)));return this};f.prepend=function(a){var b=this.body;if(a.nodeType){var c=
b.firstChild;g.init(c?b.insertBefore(a,c):b.appendChild(a))}else g.init(h(a).prependTo(b));return this};f.before=function(a){var b=this.body;a.nodeType?g.init(this.el.insertBefore(a,b)):g.init(h(a).insertBefore(b));return this};f.header=function(a,b){if(null==a&&null==b)return this.el.getElementsByTagName("header")[0];this.t=g.txt(a||"");this.el.insertBefore(g.el("header",b),this.body).appendChild(this.t);this.redraw();return this};f.title=function(a){var b=this.t;if(b)return b.nodeValue=a||"",b;
this.header(a);return this.t};f.titled=function(){var a=this.t;return a&&a.nodeValue};f.bodyY=function(){return l(this.body,this.el)};f.scrollY=function(a){if(S===a)return this.body.scrollTop;this.body.scrollTop=a};f.tabulate=function(a){var b=this.table;b?b.clear():b=n.require("$47","wgtable.js").create(this);b.init(a);return this.table=b};f.lock=function(){this.body.className+=" locked";return this};f.scrollTo=function(a,b){var c,d=this.body;c=d.scrollTop;var e=l(a,d);if(c>e)c=e;else{var g=d.clientHeight,
e=e+h(a).outerHeight();if(g+c<e)c=e-g;else return}b?d.scrollTop=c:h(d).stop(!0).animate({scrollTop:c},250)};f.navigize=function(b,c){function d(a){var b=f[a],c=k[a],e=h(b.el).show();c.addClass("active");l=a;m.data("idx",a);b.fire("wgTabSelect",[a]);return e}var e=this,f=e.cells,y=e.nav,l,k=[];y&&e.el.removeChild(y);var y=e.nav=e.el.insertBefore(g.el("nav","wg-tabs"),e.body),m=h(y).on("click",function(b){var c=h(b.target).data("idx");if(null==c)return!0;if(null!=l){var g=k[l];h(f[l].el).hide();g.removeClass("active")}d(c);
e.redraw();return a(b)});null==c&&(c=m.data("idx")||0);e.each(function(a,c){k[c]=h('<a href="#'+a.id+'"></a>').data("idx",c).text(b[c]).appendTo(m);a.pos=0;h(a.el).hide()});d(f[c]?c:0);e.lock();e.redraw();return e};f.navigated=function(){var a=this.nav;if(a)return h(a).data("idx")};f=null;return c}({},v,z));n.register("$24",function(c,f,D){function p(a){var b=[];a&&(a.saved()||b.push("po-unsaved"),a.fuzzy()?b.push("po-fuzzy"):a.flagged()&&b.push("po-flagged"),a.translation()||b.push("po-empty"),a.comment()&&
b.push("po-comment"));return b.join(" ")}function b(a,b,c){b=h(a.title(b).parentNode);var d=b.find("span.lang");c?(c=n.require("$38","locale.js").cast(c),d.length||(d=h("<span></span>").prependTo(b)),d.attr("lang",c.lang).attr("class",c.getIcon()||"lang region region-"+(c.region||"zz").toLowerCase())):(d.remove(),c="en");a.locale(c);return b}function e(){this.dirty=0}var a="poUpdate",d="changing",m="changed",k=0,g=1,l=2,A=3,B=4,s=5,r=/^\s+/,w,q,C=n.require("$3","string.js").sprintf,v=n.require("$18",
"html.js");c.extend=function(a){return a.prototype=new e};c.localise=function(a){q=a;return c};var z=function(){var a=D.createElement("p");return function(b){a.innerHTML=b.replace("src=","x=");return a.textContent}}(),u=e.prototype=n.require("$39","abstract.js").init(["getListColumns","getListHeadings","getListEntry"],["editable","t"]);u.init=function(){this.localise();this.editable={source:!0,target:!0};this.mode="";this.html=!1;return this};u.localise=function(a){a||(a=q||n.require("$1","t.js").init());
var b=[];b[k]=a._x("Source text","Editor")+":";b[A]=a._x("%s translation","Editor")+":";b[B]=a._x("Context","Editor")+":";b[s]=a._x("Comments","Editor")+":";b[g]=a._x("Single","Editor")+":";b[l]=a._x("Plural","Editor")+":";b[6]=a._x("Untranslated","Editor");b[7]=a._x("Translated","Editor");b[8]=a._x("Fuzzy","Editor");this.labels=b;this.t=a;return this};u.setRootCell=function(a){function b(a){c.redraw(!0,a);return!0}var c=n.require("$40","wingrid.js").init(a);h(f).on("resize",b);this.redraw=b;h(a).on("wgFocus wgBlur",
function(a,b){a.stopPropagation();w=b});this.destroy=function(){c.destroy();h(f).off("resize",b)};this.rootDiv=a;return c};u.$=function(){return h(this.rootDiv)};u.setListCell=function(a){var b=this;b.listCell=a;a.on("wgRowSelect",function(a,c){b.loadMessage(b.po.row(c));return!0}).on("wgRowDeselect",function(a,c,d){d||b.loadNothing();return!0})};u.setSourceCell=function(a){this.sourceCell=a};u.setTargetCell=function(a){this.targetCell=a};u.next=function(a,b,c){for(var d=this.listTable,e=d.selected(),
g=e,f,l=this.po;null!=(e=d.next(a,c,e));){if(g===e){e=null;break}if(b&&(f=l.row(e),f.translated(0)))continue;break}null!=e&&d.select(e,!0);return e};u.current=function(a){var b=this.active;if("undefined"===typeof a)return b;a?a.is(b)?this.reloadMessage(a):this.loadMessage(a):this.unloadActive();return this};u.getTargetOffset=function(){if(this.active)return this.targetCell&&this.targetCell.navigated()||0};u.getTargetEditable=function(){return this.editable.target&&this.targetCell&&this.targetCell.editable()};
u.getSourceEditable=function(){return this.editable.source&&this.sourceCell&&this.sourceCell.editable()};u.getContextEditable=function(){return this.editable.context&&this.contextCell&&this.contextCell.editable()};u.getFirstEditable=function(){return this.getTargetEditable()||this.getSourceEditable()||this.getContextEditable()};u.searchable=function(a){a&&(this.dict=a,this.po&&this.rebuildSearch());return this.dict&&!0};u.rebuildSearch=function(){var a=-1,b=this.po.rows,c=b.length,d=this.dict;for(d.clear();++a<
c;)d.add(a,b[a].toText())};u.filtered=function(){return this.lastSearch||""};u.filter=function(a,b){var c,d=this.listTable,e=this.lastFound,g=this.lastSearch;if(a){if(g===a)return e||0;if(g&&!e&&0===a.indexOf(g))return 0;c=this.dict.find(a)}this.lastSearch=g=a;this.lastFound=e=c?c.length:this.po.length;c?d.filter(c):d.unfilter();b||this.fire("poFilter",[g,e]);return e};u.countFiltered=function(){return this.lastSearch?this.lastFound:this.po.length};u.unsave=function(a,b){var c=!1;if(a=a||self.active){if(c=
a.saved(b))this.dirty++,a.unsave(b),this.fire("poUnsaved",[a,b]);this.markUnsaved(a)}return c};u.markUnsaved=function(a){var b=this.po.indexOf(a);if(b=this.listTable.tr(b)){var c=b[0].className;a=c.replace(/(?:^| +)po-[a-z]+/g,"")+" "+p(a);a!==c&&h(b).attr("class",a)}};u.save=function(a){var b=this.po;if(this.dirty||a)b.each(function(a,b){b.save()}),this.listCell.find("div.po-unsaved").removeClass("po-unsaved"),this.dirty=0,this.fire("poSave");return b};u.fire=function(a,b){var c=this.handle;if(c&&
c[a]&&(c=c[a].apply(this,b||[]),!1===c))return!1;c=h.Event(a);this.$().trigger(c,b);return!c.isDefaultPrevented()};u.on=function(a,b){this.$().on(a,b);return this};u.getSorter=function(){return null};u.reload=function(){var a=this,b,c=a.listCell,d=a.listTable,e=a.po,g=e&&e.locale(),f=g&&g.isRTL(),l=e&&e.length||0;if(!e||!e.row)return c&&c.clear().header("Error").contents("Invalid messages list"),!1;a.lastSearch&&(a.lastSearch="",a.lastFound=l,a.fire("poFilter",["",l]));d&&(b=d.thead().distribution());
a.listTable=d=c.tabulate({eachCol:function(b){var c,d,e=a.getListColumns(),g=a.getListHeadings();for(d in e)c=e[d],b(c,d,g[c])},eachRow:function(b){e.each(function(c,d){b(d.idx,a.getListEntry(d),p(d))})},sort:a.getSorter()});var k,c=a.getListColumns();for(k in c)d.sortable(c[k]);b&&d.thead().distribute(b);d.tbody().$(f?"addClass":"removeClass",["is-rtl"]);a.targetLocale=g;a.fire("poLoad");return!!l};u.load=function(a,b){this.po=a;this.dict&&this.rebuildSearch();this.reload()&&(-1!==b?this.listTable.selectRow(b||
0):this.active&&this.unloadActive())};u.pasteMessage=function(a){var b,c=0;(b=this.sourceCell)&&b.eachTextarea(function(b){b.val(a.source(null,c++))});(b=this.contextCell)&&b.eachTextarea(function(b){b.val(a.context())});if(b=this.targetCell)c=0,b.eachTextarea(function(b){b.val(a.translation(c++))});this.updateListCell(a,"source");this.updateListCell(a,"target");return this};u.reloadMessage=function(a){var b=this.sourceCell,c=this.targetCell,d;this.pasteMessage(a);b&&this.setNotes(a,b)&&b.redraw();
c&&(d=c.navigated()||0,d=this.setErrors(a.errors(d),c),!b&&this.setNotes(a,c)&&(d=!0),d&&c.redraw());return this};u.setStatus=function(){return null};u.setNotes=function(a,b){var c=[],d=!1,e=this.$notes,g=this.labels,f=a.notes(),l=a.context(),k=[],m=a.tags(),p=m&&m.length;l&&(k.push("<span>"+v.h(g[B])+"</span>"),k.push("<mark>"+v.h(l)+"</mark>"));if(p&&this.getTag)for(k.push("<span>Tagged:</span>");0<=--p;)(g=this.getTag(m[p]))&&k.push('<mark class="tag">'+v.h(g.mod_name)+"</mark>");k.length&&c.push(k.join(" "));
f&&c.push(v.h(f,!0));c.length?(e||(e=b.find("p.notes"),e.length||(e=h('<p class="notes"></p>').insertAfter(b.header())),this.$notes=e),e.html(c.join("\n")).show(),v.init(e),d=!0):e&&e.text()&&(e.text("").hide(),d=!0);return d};u.setErrors=function(a,b){var c=!1,d=this.$errs;a&&a.length?(d||(d=b.find("p.errors"),d.length||(d=h('<p class="errors"></p>').insertAfter(this.targetCell.header())),this.$errs=d),d.html(v.h(a.join(".\n")+".",!0)).show(),v.init(d),c=!0):d&&d.text()&&(d.text("").hide(),c=!0);
return c};u.loadMessage=function(c){function e(a,b){var c=b?a.split(" "):a.split(" ",1);a=c[0];"="===a.charAt(0)&&(a=a.substr(1),a=["zero","one","two"][Number(a)]||a);c[0]=a.charAt(0).toUpperCase()+a.substr(1).toLowerCase();return c.join(" ")}function f(a,d){var m=S,p=O[k];a.off();a.titled()!==p&&b(a,p,d||"en");p=!1;u.setNotes(c,a)&&(p=!0);if(c.plural()){var p=-1,n=[],r=[],q=a.id+"-",s=c.sourceForms()||d&&d.plurals||["One","Other"],A=s.length;if(2!==A||"="===s[0].charAt(0)&&"=1"!==s[0])for(;++p<A;)n[p]=
q+String(p),r[p]=e(s[p])+":";else n=[q+"-0",q+"-1"],r=[O[g],O[l]];a.splity.apply(a,n);a.each(function(a,b){a.header(r[b]).textarea(c.source(null,b),m).setStrf(H).setMode(v).setInvs(z)});a.lock();m&&a.each(function(a,b){h(a,b)})}else p&&a.redraw(),a.textarea(c.source(),m).setStrf(H).setMode(v).setInvs(z),m&&h(a,0)}function h(b,e){b.on(d,function(a,b){c.source(b,e);0===e&&u.updateListCell(c,"source");u.unsave(c,e)}).on(m,function(){0===e&&u.po.reIndex(c);u.dict&&u.rebuildSearch();u.fire(a,[c])})}function p(a,
d,g){P&&a.eachTextarea(function(a){a.ping()});a.off();var f=d.isKnown()&&d.label||"Target",f=C(O[A],f);a.titled()!==f&&b(a,f,d);f=!1;!this.sourceCell&&u.setNotes(c,a)&&(f=!0);u.setErrors(c.errors(g),a)&&(f=!0);u.setStatus(c,g);if(c.pluralized()){var l=[],k=[],h=a.id+"-",m=c.targetForms()||d.plurals||["One","Other"],f=m.length,r=function(a){var b=m[a];k.push(b?e(b,!0):"Form "+a);l.push(h+String(a))};for(c.each(r);(d=l.length)<f;)r(d);a.splitx.apply(a,l);a.each(function(a,b){var d=P&&!c.disabled(b);
a.textarea(c.translation(b),d).setStrf(H).setMode(v).setInvs(z);P&&n(a,b)});a.navigize(k,g||null).on("wgTabSelect",function(b,d){var e=P&&b.cell.editable();e&&e.focus();u.setErrors(c.errors(d),a);u.setStatus(c,d);u.fire("poTab",[d])})}else f&&a.redraw(),a.textarea(c.translation(),P&&!c.disabled(0)).setStrf(H).setMode(v).setInvs(z),P&&n(a,0)}function n(b,e){b.on(d,function(a,b,d){c.translate(b,e);0===e&&u.updateListCell(c,"target");c.fuzzy(e)?u.fuzzy(!1,c,e):u.unsave(c,e);""===b?u.fire("poEmpty",[!0,
c,e]):""===d&&u.fire("poEmpty",[!1,c,e])}).on(m,function(){u.dict&&u.rebuildSearch();u.fire(a,[c])})}function r(e){e.off();var g=O[B];e.titled()!==g&&(b(e,g),u.setStatus(null));e.textarea(c.context(),!0).setMode(v).setInvs(z);X&&e.on(d,function(a,b){c.context(b);u.updateListCell(c,"source");u.unsave(c,R)}).on(m,function(){u.po.reIndex(c);u.dict&&u.rebuildSearch();u.fire(a,[c])})}function q(a){var e=O[s];a.titled()!==e&&b(a,e);a.off().on(d,function(a,b){c.comment(b);u.fire("poComment",[c,b]);u.unsave(c,
R)}).textarea(c.comment(),!0)}var u=this,v=u.mode,D=c.isHTML(),z=u.inv||!1,E=this.fmt||null,H=c.format()||null,G=c.is(u.active),R=0,L=u.sourceCell,T=u.targetCell,U=u.contextCell,V=u.commentCell,P=u.editable.target,S=u.editable.source,X=u.editable.context,Q=w,Y=u.sourceLocale,W=u.targetLocale,O=u.labels;u.html!==D&&(u.html=D,"code"!==u.mode&&(v=D?"html":"",u.setMode(v)));u.active=c;L&&f(L,Y);U&&r(U);T&&W&&(R=T.navigated()||0,p(T,W,R));V&&q(V);Q&&(Q.exists()||(Q=Q.parent()),(D=Q.editable())&&D.focus());
E!==H&&(this.fmt=H);G||u.fire("poSelected",[c])};u.unloadActive=function(){var a;(a=this.$notes)&&a.text("").hide();(a=this.$errs)&&a.text("").hide();(a=this.sourceCell)&&a.off().clear();(a=this.contextCell)&&a.off().clear();(a=this.targetCell)&&a.off().clear();(a=this.commentCell)&&a.off();this.active&&(this.fire("poDeselected",[this.active]),this.active=null);return this};u.loadNothing=function(){var a,b=this.t,c=this.mode||"",d=this.inv||!1,e=this.fmt;this.unloadActive();this.setStatus(null);(a=
this.commentCell)&&a.textarea("",!1);if(a=this.sourceCell)a.textarea("",!1).setStrf(e).setMode(c).setInvs(d),a.title(b._x("Source text not loaded","Editor")+":");if(a=this.contextCell)a.textarea("",!1).setMode(c).setInvs(d),a.title(b._x("Context not loaded","Editor")+":");if(a=this.targetCell)a.textarea("",!1).setStrf(e).setMode(c).setInvs(d),a.title(b._x("Translation not loaded","Editor")+":");this.fire("poSelected",[null])};u.updateListCell=function(a,b){var c=this.getListColumns()[b],d=this.po.indexOf(a);
(d=this.listTable.row(d))&&d.update(c)};u.cellText=function(a){if(-1!==a.indexOf("<")||-1!==a.indexOf("&"))a=z(a);return a.replace(r,"")||"\u00a0"};u.fuzzy=function(a,b,c){var d=4===this.flag(null,b,c);!0!==a||d?!1===a&&d&&this.flag(0,b,c)&&this.fire("poFuzzy",[b,!1,c]):this.flag(4,b,c)&&this.fire("poFuzzy",[b,!0,c]);return d};u.flag=function(b,c,d){if(!c){c=this.active;d=this.getTargetOffset();if(null==d)return null;d&&c.targetForms()&&(d=0)}var e=c.flagged(d);if(null==b)return e;if(e===b||b&&!c.translated(d)||
!this.fire("poFlag",[b,e,c,d]))return!1;c.flag(b,d);this.fire(a,[c])&&this.unsave(c,d);this.setStatus(c,d);return!0};u.add=function(b,c){var d,e=this.po.get(b,c);e?d=this.po.indexOf(e):(d=this.po.length,e=this.po.add(b,c),this.load(this.po,-1),this.fire("poAdd",[e]),this.fire(a,[e]));this.lastSearch&&this.filter("");this.listTable.select(d);return e};u.del=function(b){if(b=b||this.active){var c=this.lastSearch,d=this.po.del(b);null!=d&&(this.unsave(b),this.fire("poDel",[b]),this.fire(a,[b]),this.reload(),
this.dict&&this.rebuildSearch(),this.active&&this.active.equals(b)&&this.unloadActive(),this.po.length&&(c&&this.filter(c),this.active||(d=Math.min(d,this.po.length-1),this.listTable.select(d))))}};u.setMono=function(a){return this.setMode(a?"code":this.html?"html":"")};u.setMode=function(a){this.mode!==a&&(this.mode=a,this.callTextareas(function(b){b.setMode(a)}));return this};u.getMono=function(){return"code"===this.mode};u.setInvs=function(a){(this.inv||!1)!==a&&(this.inv=a,this.callTextareas(function(b){b.setInvs(a)}),
this.fire("poInvs",[a]));return this};u.getInvs=function(){return this.inv||!1};u.callTextareas=function(a){var b=this.targetCell;b&&b.eachTextarea(a);(b=this.contextCell)&&b.eachTextarea(a);(b=this.sourceCell)&&b.eachTextarea(a);return this};u.focus=function(){var a=this.getTargetEditable();a&&a.focus();return this};u=null;return c}({},v,z));n.register("$25",function(c,f,h){Number.prototype.format=function(c){c=Math.pow(10,c||0);var b=Math.round(c*this)/c;c=[];var b=String(b),e=b.split("."),b=e[0],
e=e[1],a=b.length;do c.unshift(b.substring(a-3,a));while(0<(a-=3));b=c.join(",");if(c=e){c=e;for(var d,e=c.length;"0"===c.charAt(--e);)d=e;d&&(c=substring(0,d));c=e=c}c&&(b+="."+e);return b};Number.prototype.percent=function(c){var b=0,e=this&&c?100*(this/c):0;if(0===e)return"0";if(100===e)return"100";if(99<e)e=Math.min(e,99.9),c=e.format(++b);else if(0.5>e){e=Math.max(e,1E-4);do c=e.format(++b);while("0"===c&&4>b);c=c.substr(1)}else c=e.format(0);return c};return c}({},v,z));n.register("$12",function(c,
f,h){function p(){this.init()._validate();this.sourceLocale={lang:"en",label:"English",plurals:["One","Other"]}}f=n.require("$24","base.js");n.require("$25","number.js");c.init=function(b){var c=new p;b=c.setRootCell(b);var a=b.splity("po-list","po-edit"),d=a[0],f=a[1],a=f.splitx("po-trans","po-comment"),k=a[0],g=a[1].header("Loading.."),a=k.splity("po-source","po-target"),k=a[0].header("Loading.."),a=a[1].header("Loading..");b.distribute([0.34]);f.distribute([0.8]);c.setListCell(d);c.setSourceCell(k);
c.setTargetCell(a);c.commentCell=g;c.editable.source=!1;return c};f=p.prototype=f.extend(p);f.getListHeadings=function(){var b=this.t||{_x:function(b){return b}};return[b._x("Source text","Editor"),b._x("Translation","Editor")]};f.getListColumns=function(){return{source:0,target:1}};f.getListEntry=function(b){var c=this.cellText;return[function(){var a,d=c(b.source()||""),f=b.context();return f?(a=h.createElement("p"),a.appendChild(h.createElement("mark")).innerText=f,a.appendChild(h.createTextNode("\u00a0"+
d)),a):d},function(){return c(b.translation()||"")}]};f.stats=function(){var b=this.po,c=b.length,a=0,d=0,f=0;b.each(function(b,c){c.fuzzy()?f++:c.translated()?a++:d++});return{t:c,p:a.percent(c)+"%",f:f,u:d}};f.unlock=function(){this._unlocked||(this._unlocked=this.targetLocale,delete this.targetLocale,this.po&&this.po.unlock(),this.editable={source:!0,context:!0,target:!1},this.contextCell=this.targetCell,delete this.targetCell,this.fire("poLock",[!1]),this.active&&this.loadMessage(this.active))};
f.lock=function(){var b;this._unlocked&&(b=this.targetLocale=this._unlocked,delete this._unlocked,this.po&&this.po.lock(b),this.editable={source:!1,context:!1,target:!0},this.targetCell=this.contextCell,delete this.contextCell,this.fire("poLock",[!0,b]),this.active&&this.loadMessage(this.active))};f.locked=function(){return!this._unlocked};f.getSorter=function(){function b(a,b){var f=a.weight(),g=b.weight();return f===g?c(a,b):f>g?-1:1}function c(a,b){return a.hash().localeCompare(b.hash())}var a=
this;return function(d){var f=a.po,k=a.locked()?b:c;d.sort(function(a,b){return k(f.row(a),f.row(b))})}};return c}({},v,z));n.register("$13",function(c,f,n){var p={copy:66,clear:75,save:83,fuzzy:85,next:40,prev:38,enter:13,invis:73},b={38:!0,40:!0,73:!0},e={66:function(a,b){var c=b.current();c&&(c.normalize(),b.focus().pasteMessage(c))},75:function(a,b){var c=b.current();c&&(c.untranslate(),b.focus().pasteMessage(c))},85:function(a,b){b.focus().fuzzy(!b.fuzzy())},13:function(a,b){b.getFirstEditable()&&
b.next(1,!0,!0)},40:function(a,b){var c=a.shiftKey;b.next(1,c,c)},38:function(a,b){var c=a.shiftKey;b.next(-1,c,c)},73:function(a,b){if(!a.shiftKey)return!1;b.setInvs(!b.getInvs())}};c.init=function(a,c){function m(c){if(c.isDefaultPrevented()||!c.metaKey&&!c.ctrlKey)return!0;var d=c.which;if(!k[d])return!0;var f=e[d];if(!f)throw Error("command undefined #"+d);if(c.altKey||c.shiftKey&&!b[d]||!1===f(c,a))return!0;c.stopPropagation();c.preventDefault();return!1}var k={};h(c||f).on("keydown",m);return{add:function(a,
b){e[p[a]]=b;return this},enable:function(){var a,b;for(b in arguments)a=p[arguments[b]],k[a]=!0;return this},disable:function(){h(c||f).off("keydown",m);a=c=k=null}}};return c}({},v,z));n.register("$26",function(c,f,h){function p(){this.reIndex([])}c.init=function(){return new p};f=p.prototype;f.reIndex=function(b){for(var c={},a=-1,d=b.length;++a<d;)c[b[a]]=a;this.keys=b;this.length=a;this.ords=c};f.key=function(b,c){if(null==c)return this.keys[b];var a=this.keys[b],d=this.ords[c];if(c!==a){if(null!=
d)throw Error("Clash with item at ["+d+"]");this.keys[b]=c;delete this.ords[a];this.ords[c]=b}return b};f.indexOf=function(b){b=this.ords[b];return null==b?-1:b};f.add=function(b,c){var a=this.ords[b];null==a&&(this.keys[this.length]=b,a=this.ords[b]=this.length++);this[a]=c;return a};f.get=function(b){return this[this.ords[b]]};f.has=function(b){return null!=this.ords[b]};f.del=function(b){this.cut(this.ords[b],1)};f.cut=function(b,c){c=c||1;var a=[].splice.call(this,b,c);this.keys.splice(b,c);this.reIndex(this.keys);
return a};f.each=function(b){for(var c=-1,a=this.keys,d=this.length;++c<d;)b(a[c],this[c],c);return this};f.sort=function(b){for(var c=-1,a=this.length,d,f=this.keys,k=this.ords,g=[];++c<a;)g[c]=[this[c],f[c]];g.sort(function(a,c){return b(a[0],c[0])});for(c=0;c<a;c++)d=g[c],this[c]=d[0],d=d[1],f[c]=d,k[d]=c;return this};f.join=function(b){return[].join.call(this,b)};f=null;return c}({},v,z));n.register("$27",function(c,f,h){function p(b,c){var a=RegExp("^.{0,"+(b-1)+"}["+c+"]"),d=RegExp("^[^"+c+
"]+");return function(c,e){for(var g=c.length,f;g>b;){f=a.exec(c)||d.exec(c);if(null==f)break;f=f[0];e.push(f);f=f.length;g-=f;c=c.substr(f)}0!==g&&e.push(c);return e}}c.create=function(b){function c(a){return l[a]||"\\"+a}var a,d,f=/(?:\r\n|[\r\n\v\f\u2028\u2029])/g,k=/[ \r\n]+/g,g=/[\t\v\f\x07\x08\\\"]/g,l={"\t":"\\t","\v":"\\v","\f":"\\f","\u0007":"\\a","\b":"\\b"};if(null==b||isNaN(b=Number(b)))b=79;0<b&&(a=p(b-3," "),d=p(b-2,"-\u2013 \\.,:;\\?!\\)\\]\\}\\>"));return{pair:function(a,l){if(!l)return a+
' ""';l=l.replace(g,c);var k=0;l=l.replace(f,function(){k++;return"\\n\n"});if(!(k||b&&b<l.length+a.length+3))return a+' "'+l+'"';var h=[a+' "'],p=l.split("\n");if(d)for(var n=-1,v=p.length;++n<v;)d(p[n],h);else h=h.concat(p);return h.join('"\n"')+'"'},prefix:function(a,b){var c=a.split(f);return b+c.join("\n"+b)},refs:function(b){b=b.replace(k," ",b);a&&(b=a(b,[]).join("\n#: "));return"#: "+b}}};return c}({},v,z));n.register("$41",function(c,f,h){function p(){this.length=0}c.init=function(){return new p};
f=p.prototype;f.push=function(b){this[this.length++]=b;return this};f.sort=function(b){[].sort.call(this,b);return this};f.each=function(b){for(var c=-1,a=this.length;++c<a;)b(c,this[c]);return this};return c}({},v,z));n.register("$28",function(c,f,h){function p(){}c.extend=function(b){return b.prototype=new p};f=p.prototype=n.require("$39","abstract.js").init(["add","load"]);f.row=function(b){return this.rows[b]};f.lock=function(b){return this.locale(b||{lang:"zxx",label:"Unknown",nplurals:1,pluraleq:"n!=1"})};
f.unlock=function(){var b=this.loc;this.loc=null;return b};f.locale=function(b){null==b?b=this.loc:this.loc=b=n.require("$38","locale.js").cast(b);return b};f.each=function(b){this.rows.each(b);return this};f.indexOf=function(b){"object"!==typeof b&&(b=this.get(b));if(!b)return-1;null==b.idx&&(b.idx=this.rows.indexOf(b.hash()));return b.idx};f.get=function(b){return this.rows&&this.rows.get(b)};f.del=function(b){b=this.indexOf(b);if(-1!==b){var c=this.rows.cut(b,1);if(c&&c.length)return this.length=
this.rows.length,this.rows.each(function(a,b,c){b.idx=c}),b}};f.reIndex=function(b,c){var a=this.indexOf(b),d=b.hash(),f=this.rows.indexOf(d);return f===a?a:-1!==f?(c=(c||0)+1,b.source("Error, duplicate "+String(c)+": "+b.source()),this.reIndex(b,c)):this.rows.key(a,d)};f.sort=function(b){this.rows.sort(b);return this};f["export"]=function(){for(var b=-1,c=this.rows,a=c.length,d=n.require("$41","list.js").init();++b<a;)d.push(c[b]);return d};f=null;return c}({},v,z));n.register("$29",function(c,f,
h){function p(){this._id=this.id=""}c.extend=function(b){return b.prototype=new p};f=p.prototype;f.flag=function(b,c){var a=this.flg||(this.flg=[]);if(null!=c)a[c]=b;else for(var d=Math.max(a.length,this.src.length,this.msg.length);0!==d--;)a[d]=b;return this};f.flagged=function(b){var c=this.flg||[];if(null!=b)return c[b]||0;for(b=c.length;0!==b--;)if(c[b])return!0;return!1};f.flags=function(){for(var b,c={},a=[],d=this.flg||[],f=d.length;0!==f--;)b=d[f],c[b]||(c[b]=!0,a.push(b));return a};f.flaggedAs=
function(b,c){var a=this.flg||[];if(null!=c)return b===a[c]||0;for(var d=a.length;0!==d--;)if(a[d]===b)return!0;return!1};f.fuzzy=function(b,c){var a=this.flaggedAs(4,b);null!=c&&this.flag(c?4:0,b);return a};f.source=function(b,c){if(null==b)return this.src[c||0]||"";this.src[c||0]=b;return this};f.plural=function(b,c){if(null==b)return this.src[c||1]||"";this.src[c||1]=b||"";return this};f.sourceForms=function(){return this.srcF};f.targetForms=function(){return this.msgF};f.each=function(b){for(var c=
-1,a=this.src,d=this.msg,f=Math.max(a.length,d.length);++c<f;)b(c,a[c],d[c]);return this};f.count=function(){return Math.max(this.src.length,this.msg.length)};f.pluralized=function(){return 1<this.src.length||1<this.msg.length};f.translate=function(b,c){this.msg[c||0]=b||"";return this};f.untranslate=function(b){if(null!=b)this.msg[b]="";else{var c=this.msg,a=c.length;for(b=0;b<a;b++)c[b]=""}return this};f.translation=function(b){return this.msg[b||0]||""};f.errors=function(b){return this.err&&this.err[b||
0]||[]};f.translated=function(b){if(null!=b)return!!this.msg[b];var c=this.msg,a=c.length;for(b=0;b<a;b++)if(!c[b])return!1;return!0};f.untranslated=function(b){if(null!=b)return!this.msg[b];var c=this.msg,a=c.length;for(b=0;b<a;b++)if(c[b])return!1;return!0};f.comment=function(b){if(null==b)return this.cmt||"";this.cmt=b||"";return this};f.notes=function(b){if(null==b)return this.xcmt||"";this.xcmt=b||"";return this};f.refs=function(b){if(null==b)return this.ref||"";this.ref=b||"";return this};f.format=
function(b){if(null==b)return this.fmt||"";this.fmt=b||"";return this};f.context=function(b){if(null==b)return this.ctx||"";this.ctx=b||"";return this};f.tags=function(){return this.tg};f.toString=f.toText=function(){return this.src.concat(this.msg,this.id,this.ctx).join(" ")};f.weight=function(){var b=0;this.translation()||(b+=2);this.fuzzy()&&(b+=1);return b};f.equals=function(b){return this===b||this.hash()===b.hash()};f.hash=function(){return this.id};f.normalize=function(){for(var b=this.msg.length;0!==
b--;)this.msg[b]=this.src[b]||""};f.disabled=function(b){return!!(this.lck||[])[b||0]};f.disable=function(b){(this.lck||(this.lck=[]))[b||0]=!0;return this};f.saved=function(b){var c=this.drt;if(!c)return!0;if(null!=b)return!c[b];for(b=c.length;0!==b--;)if(c[b])return!1;return!0};f.unsave=function(b){(this.drt||(this.drt=[]))[b||0]=!0;return this};f.save=function(b){var c=this.drt;null==b?this.drt=null:c[b]=!1;return this};f.is=function(b){return b&&(b===this||b.idx===this.idx)};f.isHTML=function(b){if(null==
b)return this.htm||!1;this.htm=b};f=null;return c}({},v,z));n.register("$14",function(c,f,h){function p(a){return{"Project-Id-Version":"PACKAGE VERSION","Report-Msgid-Bugs-To":"","POT-Creation-Date":a||"","PO-Revision-Date":a||"","Last-Translator":"","Language-Team":"",Language:"","Plural-Forms":"","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit"}}function b(a,b){var c=a||"";b&&(c+="\x00"+b);return c}function e(){return n.require("$26","collection.js").init()}
function a(a){var b=f.console;b&&b.error&&b.error(a.message||String(a))}function d(a){return n.require("$27","format.js").create(a)}function m(a){this.locale(a);this.length=0;this.rows=e();this.head=p(this.now())}function k(a,b){this.src=[a||""];this.msg=[b||""]}c.create=function(a){return new m(a)};h=n.require("$28","messages.js").extend(m);h.now=function(){function a(b,c){for(var d=String(b);d.length<c;)d="0"+d;return d}var b=new Date,c=b.getUTCFullYear(),d=b.getUTCMonth()+1,e=b.getUTCDate(),f=
b.getUTCHours(),b=b.getUTCMinutes();return a(c,4)+"-"+a(d,2)+"-"+a(e,2)+" "+a(f,2)+":"+a(b,2)+"+0000"};h.header=function(a,b){var c=this.head||(this.head={});if(null==b)return this.headers()[a]||"";c[a]=b||"";return this};h.headers=function(a){var b,c=this.now(),d=this.head||(this.head=p(c));if(null!=a){for(b in a)d[b]=a[b];return this}var e=this.locale();a={};for(b in d)a[b]=String(d[b]);e?(a.Language=String(e)||"zxx",a["Language-Team"]=e.label||a.Language,a["Plural-Forms"]="nplurals="+(e.nplurals||
"2")+"; plural="+(e.pluraleq||"n!=1")+";",b="PO-Revision-Date"):(a.Language="",a["Plural-Forms"]="nplurals=INTEGER; plural=EXPRESSION;",a["PO-Revision-Date"]="YEAR-MO-DA HO:MI+ZONE",b="POT-Creation-Date");a[b]||(a[b]=c);a["X-Generator"]="Loco https://localise.biz/";return a};h.get=function(a,c){var d=b(a,c);return this.rows.get(d)};h.add=function(b,c){b instanceof k||(b=new k(b));c&&b.context(c);var d=b.hash();this.rows.get(d)?a("Duplicate message at index "+this.indexOf(b)):(b.idx=this.rows.add(d,
b),this.length=this.rows.length);return b};h.load=function(b){for(var c=-1,d,e,f,h,m,p,n=(f=this.locale())&&f.nplurals||2,v=[];++c<b.length;)d=b[c],null==d.parent?(e=d.source||d.id,f=d.target||"",h=d.context,e||h?(m=new k(e,f),m._id=d._id,h&&m.context(h),d.flag&&m.flag(d.flag,0),d.comment&&m.comment(d.comment),d.notes&&m.notes(d.notes),d.refs&&m.refs(d.refs),m.format(d.format),d.message=m,this.add(m)):0===c&&"object"===typeof f&&(this.head=f,this.headcmt=d.comment)):v.push(d);for(c=-1;++c<v.length;)try{d=
v[c];e=d.source||d.id;m=b[d.parent]&&b[d.parent].message;if(!m)throw Error("parent missing for plural "+e);p=d.plural;1===p&&m.plural(e);p>=n||(d.flag&&m.flag(d.flag,p),m.translate(d.target||"",p),d.format&&!m.format()&&m.format(d.format))}catch(z){a(z)}return this};h.merge=function(b){var c=this,d,f=b.header("POT-Creation-Date"),h=c.rows,k=[],m=[];b=b.rows;c.rows.each(function(a,c){null==b.get(a)&&m.push(c)});c.rows=e();b.each(function(b,e){try{(d=h.get(b))?(d.ref=e.ref,d.fmt=e.fmt):(d=e,k.push(d)),
c.add(d)}catch(f){a(f)}});f&&c.header("POT-Creation-Date",f);return{add:k,del:m}};h.wrap=function(a){this.fmtr=d(a);return this};h.toString=function(){var a,b=this.locale(),c=[],e=[],f=this.headers(),h=!b,m=b&&b.nplurals||2,p=this.fmtr||d();f[b?"PO-Revision-Date":"POT-Creation-Date"]=this.now();for(a in f)e.push(a+": "+f[a]);e=new k("",e.join("\n"));e.comment(this.headcmt||"");h&&e.fuzzy(0,!0);c.push(e.toString());c.push("");this.rows.each(function(a,b){a&&(c.push(b.cat(p,h,m)),c.push(""))});return c.join("\n")};
h=n.require("$29","message.js").extend(k);h.hash=function(){return b(this.source(),this.context())};h.toString=function(){return this.cat(d())};h.cat=function(a,b,c){var d,e=[],f;(f=this.cmt)&&e.push(a.prefix(f,"# "));(f=this.xcmt)&&e.push(a.prefix(f,"#. "));d=this.ref;if(f=this._id)d+=(d?" ":"")+"loco:"+f;d&&/\S/.test(d)&&e.push(a.refs(d));!b&&this.fuzzy()&&e.push("#, fuzzy");(f=this.fmt)&&e.push("#, "+f+"-format");(f=this.ctx)&&e.push(a.pair("msgctxt",f));e.push(a.pair("msgid",this.src[0]));if(null==
this.src[1])e.push(a.pair("msgstr",b?"":this.msg[0]));else for(d=-1,e.push(a.pair("msgid_plural",this.src[1])),f=this.msg||["",""],c=c||f.length;++d<c;)e.push(a.pair("msgstr["+d+"]",b?"":f[d]||""));return e.join("\n")};h.compare=function(a,b){var c=this.weight(),d=a.weight();if(c>d)return 1;if(c<d)return-1;if(b){c=this.hash().toLowerCase();d=a.hash().toLowerCase();if(c<d)return 1;if(c>d)return-1}return 0};h=h=null;return c}({},v,z));n.register("$16",function(c,f,n){c.init=function(c){function b(){M||
(I.click(l),M=h('<div id="loco-fs-creds"></div>').dialog({dialogClass:"request-filesystem-credentials-dialog loco-modal",minWidth:580,modal:!0,autoOpen:!1,closeOnEscape:!0}).on("change",'input[name="connection_type"]',function(){this.checked&&h("#ssh-keys").toggleClass("hidden","ssh"!==h(this).val())}));return M}function e(){G&&(a(h(q)),G=!1);if(H&&L){var b=L,c=h(J);c.find("span.loco-msg").text(b);N||(c.removeClass("jshide").hide().fadeIn(500),N=!0)}else N&&(a(h(J)),N=!1)}function a(a){a.slideUp(250).fadeOut(250,
function(){h(this).addClass("jshide")})}function d(){if(H)return M&&M.dialog("close"),e(),h(c).find('button[type="submit"]').attr("disabled",!1),h(f).triggerHandler("resize"),w&&w(!0),!0;y&&M?(G||(h(q).removeClass("jshide").hide().fadeIn(500),G=!0),N&&(a(h(J)),N=!1)):e();h(c).find('input[type="submit"]').attr("disabled",!0);w&&w(!1);return!1}function m(a){var b,c,d=r||{};for(b in d)d.hasOwnProperty(b)&&(c=d[b],a[b]?a[b].value=c:h('<input type="hidden" />').attr("name",b).appendTo(a).val(c))}function k(a){a.preventDefault();
a=h(a.target).serializeArray();s(a);E=!0;return!1}function g(a){a.preventDefault();M.dialog("close");return!1}function l(a){a.preventDefault();M.dialog("open").find('input[name="connection_type"]').change();return!1}function v(a){H=a.authed;C=a.method;h(q).find("span.loco-msg").text(a.message||"Something went wrong.");L=a.warning||"";a.notice&&u.notices.info(a.notice);if(H)"direct"!==C&&(r=a.creds,m(c),E&&a.success&&u.notices.success(a.success)),d();else if(a.reason)u.notices.info(a.reason);else if(a=
a.prompt){var e=b();e.html(a).find("form").submit(k);e.dialog("option","title",e.find("h2").remove().text());e.find("button.cancel-button").show().click(g);e.find('input[type="submit"]').addClass("button-primary");d();h(f).triggerHandler("resize")}else u.notices.error("Server didn't return credentials, nor a prompt for credentials")}function z(){d()}function s(a){E=!1;u.ajax.setNonce("fsConnect",K).post("fsConnect",a,v,z);return a}var r,w,q=c,C=null,E=!1,H=!1,u=f.locoScope,y=c.path.value,F=c.auth.value,
K=c["loco-nonce"].value,I=h(q).find("button.button-primary"),J=n.getElementById(q.id+"-warn"),G=!1,N=!1,L="",M;u.notices.convert(J).stick();c.connection_type?(r={},r.connection_type=c.connection_type.value,H=!0):y&&F&&s({path:y,auth:F});d();return{applyCreds:function(a){if(a.nodeType)m(a);else{var b,c=r||{};for(b in c)c.hasOwnProperty(b)&&(a[b]=c[b])}return this},setForm:function(a){c=a;d();m(a);return this},connect:function(){y=c.path.value;F=c.auth.value;s(h(c).serializeArray());return this},listen:function(a){w=
a;H&&a(!0);return this}}};return c}({},v,z));n.register("$17",function(c,f,v){function p(c,f,g,h){f="n"===g?e(f):a(f);h&&(f=d(f));return b([].sort,[f])(c)}function b(a,b){return function(c){a.apply(c,b);return c}}function e(a){return function(b,c){var d=b&&b[a]||0,e=c&&c[a]||0;return d===e?0:d>e?1:-1}}function a(a){return function(b,c){return(b&&b[a]||"").localeCompare(c&&c[a]||"")}}function d(a){return function(b,c){return-1*a(b,c)}}c.init=function(a){function b(a){var c=-1,d=a.length;for(h("tr",
q).remove();++c<d;)q.appendChild(a[c].$)}function c(a){s=a?z.find(a,d):d.slice(0);w&&(a=e[w],s=p(s,w,a.type,a.desc));b(s)}var d=[],e=[],f=0,s,r,w,q=a.getElementsByTagName("tbody")[0],v=a.getElementsByTagName("thead")[0],z=n.require("$7","fulltext.js").init();v&&q&&(h("th",v).each(function(a,c){var g=c.getAttribute("data-sort-type");g&&(a=f,h(c).addClass("loco-sort").click(function(c){c.preventDefault();c=a;var f=e[c],g=f.type,m=!(f.desc=!f.desc);s=p(s||d.slice(0),c,g,m);b(s);r&&r.removeClass("loco-desc loco-asc");
r=h(f.$).addClass(m?"loco-desc":"loco-asc").removeClass(m?"loco-asc":"loco-desc");w=c;return!1}),e[f]={$:c,type:g});c.hasAttribute("colspan")?f+=Number(c.getAttribute("colspan")):f++}),h("tr",q).each(function(a,b){var c,f,g,h=[],k={_:a,$:b},m=b.getElementsByTagName("td");for(f in e){c=m[f];(g=c.textContent.replace(/(^\s+|\s+$)/g,""))&&h.push(g);c.hasAttribute("data-sort-value")&&(g=c.getAttribute("data-sort-value"));switch(e[f].type){case "n":g=Number(g)}k[f]=g}d[a]=k;z.index(a,h)}),a=h('form.loco-filter input[type="text"]',
a.parentNode),a.length&&(a=a[0],v=h(a.form),1<d.length?n.require("$8","LocoTextListener.js").listen(a,c):v.hide(),v.on("submit",function(a){a.preventDefault();return!1})))};return c}({},v,z));var G=v.locoScope||(v.locoScope={});v=v.locoConf||(v.locoConf={});z=n.require("$1","t.js").init();var L=v.wplang;n.require("$2","array.js");G.l10n=z;z.load(v.wpl10n);L&&z.pluraleq(L.pluraleq);G.string=n.require("$3","string.js");G.notices=n.require("$4","notices.js").init(z);G.ajax=n.require("$5","ajax.js").init(v).localise(z);
G.locale=n.require("$6","wplocale.js");G.fulltext=n.require("$7","fulltext.js");G.watchtext=n.require("$8","LocoTextListener.js").listen;G.selector=n.require("$9","LocoSelector.js").create;G.autocomp=n.require("$10","LocoAutoComplete.js").init;G.tooltip=n.require("$11","tooltip.js");G.po={ed:n.require("$12","poedit.js"),kbd:n.require("$13","hotkeys.js"),init:n.require("$14","po.js").create,ace:n.require("$15","ace.js").strf("php")};G.fs=n.require("$16","fsconn.js");h("#loco.wrap table.wp-list-table").each(function(c,
f){n.require("$17","tables.js").init(f)})})(window,document,window.jQuery);

View File

@@ -0,0 +1,52 @@
/**
* Script for file move operation
*/
!function( window, document, $ ){
var fsConn,
destPath,
fsHook = document.getElementById('loco-fs'),
elForm = document.getElementById('loco-move'),
origPath = elForm.path.value
;
function setFormDisabled( disabled ){
$(elForm).find('button.button-primary').each( function(i,button){
button.disabled = disabled;
} );
}
function onFsConnect( valid ){
setFormDisabled( ! ( valid && destPath ) );
}
function validate(event){
var field = event.target||{}, value;
if( 'dest' === field.name && ( field.checked || 'text' === field.type ) ){
value = field.value;
if( value && value !== destPath ){
destPath = value;
setFormDisabled(true);
// check chosen target permissions
if( origPath !== value ){
fsHook.dest.value = value;
fsConn.connect();
}
}
}
}
function process( event ){
if( destPath ){
return true;
}
event.preventDefault();
return false;
}
if( fsHook && elForm ){
fsConn = window.locoScope.fs.init(fsHook).setForm(elForm).listen(onFsConnect);
$(elForm).change(validate).submit(process);
}
}( window, document, jQuery );

View File

@@ -0,0 +1,172 @@
/**
* Script for PO/POT file diff renderer and roll back function
*/
!function( window, document, $ ){
var xhr,
cache = [],
conf = window.locoConf,
loco = window.locoScope,
revOffset = 0,
maxOffset = conf.paths.length - 2,
elRoot = document.getElementById('loco-ui'),
fsHook = document.getElementById('loco-fs'),
elForm = elRoot.getElementsByTagName('form').item(0),
buttons = elRoot.getElementsByTagName('button'),
$metas = $(elRoot).find('div.diff-meta'),
prevBut = buttons.item(0),
nextBut = buttons.item(1)
;
if( fsHook && elForm ){
loco.fs
.init( fsHook )
.setForm( elForm );
}
function showLoading(){
return $(elRoot).addClass('loading');
}
function hideLoading(){
return $(elRoot).removeClass('loading');
}
function setContent( src ){
return $(elRoot).find('div.diff').html( src );
}
function setError( message ){
hideLoading();
return $('<p class="error"></p>').text( message ).appendTo( setContent('') );
}
// applying line numbers in JS is easier than hacking HTML on the back end, because dom is parsed
function applyLineNumbers( i, tbody ){
var i, cells,
rows = tbody.getElementsByTagName('tr'),
nrow = rows.length,
data = tbody.getAttribute('data-diff').split(/\D+/),
xmin = data[0], xmax = data[1],
ymin = data[2], ymax = data[3]
;
/*function lpad( n, max ){
var str = String( n ),
len = String(max).length;
while( str.length < len ){
str = '\xA0'+str;
}
return str;
}*/
function apply( td, num, max ){
if( num <= max ){
$('<span></span>').text( String(num) ).prependTo( td );
}
}
for( i = 0; i < nrow; i++ ){
cells = rows[i].getElementsByTagName('td');
apply( cells[0], xmin++, xmax );
apply( cells[2], ymin++, ymax );
}
}
function loadDiff( offset ){
if( xhr ){
xhr.abort();
}
// use in-code cache
var src = cache[offset];
if( null != src ){
setContent( src );
hideLoading();
return;
}
// remote load required
setContent('');
showLoading();
xhr = loco.ajax.post( 'diff',
{
lhs: conf.paths[offset],
rhs: conf.paths[offset+1]
},
function( r, state, _xhr ){
if( _xhr === xhr ){
if( src = r && r.html ){
cache[offset] = src;
setContent( src ).find('tbody').each( applyLineNumbers );
hideLoading();
}
else {
setError( r && r.error || 'Unknown error' );
}
}
},
function( _xhr, error, message ){
if( _xhr === xhr ){
xhr = null;
// ajax utilty should have extracted meaningful error
setError('Failed to generate diff');
}
}
);
}
/**
* Go to next revision (newer)
*/
function goNext( event ){
event.preventDefault();
go( revOffset - 1 );
return false;
}
/**
* Go to previous revision (older)
*/
function goPrev( event ){
event.preventDefault();
go( revOffset + 1 );
return false;
}
function go( offset ){
if( offset >=0 && offset <= maxOffset ){
revOffset = offset;
loadDiff( offset );
redraw();
}
}
function redraw(){
var i = revOffset, j = i + 1;
// lock navigation to available range
prevBut.disabled = i >= maxOffset;
nextBut.disabled = i <= 0;
//
$metas.addClass('jshide').removeClass('diff-meta-current');
$metas.eq(i).removeClass('jshide').addClass('diff-meta-current');
$metas.eq(j).removeClass('jshide');
}
// enable navigation if there is something to navigate to
if( maxOffset ){
$(prevBut).click( goPrev ).parent().removeClass('jshide');
$(nextBut).click( goNext ).parent().removeClass('jshide');
}
// load default diff, which is current version compared to most recent backup
go( 0 );
}( window, document, jQuery );

View File

@@ -0,0 +1,203 @@
/**
* Script for PO file initializing page
*/
!function( window, document, $ ){
var path,
loco = window.locoScope,
fsHook = document.getElementById('loco-fs'),
elForm = document.getElementById('loco-poinit'),
fsConn = fsHook && loco.fs.init( fsHook )
;
/**
* Abstract selection of twin mode (Select/Custom) locale input
*/
var localeSelector = function( elForm ){
function isSelectMode(){
return elMode[0].checked;
}
function setSelectMode(){
elMode[0].checked = true;
redrawMode( true );
}
function setCustomMode(){
if( ! elCode.value ){
elCode.value = getValue();
}
elMode[1].checked = true;
//elOpts.selectedIndex = 0;
redrawMode( false );
}
function getValue(){
var data = $( isSelectMode() ? elOpts : elCode ).serializeArray();
return data[0] && data[0].value || '';
}
function getLocale(){
var value = getValue();
return value ? loco.locale.parse(value) : loco.locale.clone( {lang:'zxx'} );
}
function onModeChange(){
redrawMode( isSelectMode() );
return true;
}
function redrawMode( selectMode ){
elCode.disabled = selectMode;
elOpts.disabled = ! selectMode;
fsCode.className = selectMode ? 'disabled' : 'active';
fsOpts.className = selectMode ? 'active' : 'disabled';
validate();
}
var elOpts = elForm['select-locale'],
elCode = elForm['custom-locale'],
elMode = elForm['use-selector'],
fsOpts = $(elOpts).focus( setSelectMode ).closest('fieldset').click( setSelectMode )[0],
fsCode = $(elCode).focus( setCustomMode ).closest('fieldset').click( setCustomMode )[0];
$(elMode).change( onModeChange );
onModeChange();
loco.watchtext( elCode, function(v){ $(elCode.form).triggerHandler('change'); } );
return {
val: getLocale
};
}( elForm );
/**
* Abstract selection of target directory
*/
var pathSelector = function(){
var elOpts = elForm['select-path'];
function getIndex(){
var pairs = $(elOpts).serializeArray(), pair = pairs[0];
return pair && pair.value || null;
}
function getSelected( name ){
var index = getIndex();
return index && elForm[name+'['+index+']'];
}
function getValue(){
var elField = getSelected('path');
return elField && elField.value;
}
function getLabel(){
var elField = getSelected('path');
return elField && $(elField.parentNode).find('code.path').text();
}
/*$(elForm['path[0]']).focus( function(){
elOpts[0].checked = true;
} );*/
return {
val: getValue,
txt: getLabel
};
}( elForm );
// enable disable form submission
function setFormDisabled( disabled ){
$(elForm).find('button.button-primary').each( function( i, button ){
button.disabled = disabled;
} );
}
// Recalculate form submission when any data changes
function validate(){
var locale = localeSelector && localeSelector.val(),
hasloc = locale && locale.isValid() && 'zxx' !== locale.lang,
hasdir = pathSelector && pathSelector.val(),
valid = hasloc && hasdir
;
redrawLocale( locale );
// disabled until back end validates file path
setFormDisabled( true );
// check calculated path against back end
if( valid ){
var newPath = pathSelector.txt();
if( newPath !== path ){
path = newPath;
fsHook.path.value = path;
fsConn.listen(onFsConnect).connect();
}
else {
setFormDisabled( false );
}
// connection back end won't warn about system files, so we'll toggle notice here
// actually no need as user will be warned when they go through to the edit screen
// $('#loco-fs-info')[ pathSelector.typ() ? 'removeClass' : 'addClass' ]('jshide');
}
}
// callback after file system connect has returned
function onFsConnect( valid ){
setFormDisabled( ! valid );
}
// show locale in all file paths (or place holder if empty)
function redrawLocale( locale ){
var $form = $(elForm),
loctag = locale && locale.toString('_') || '',
suffix = loctag ? ( 'zxx' === loctag ? '<locale>' : loctag ) : '<invalid>'
;
$form.find('code.path span').each( function( i, el ){
el.textContent = suffix;
} );
$form.find('span.lang').each( function( i, icon ){
setLocaleIcon( icon, locale );
} );
}
function setLocaleIcon( icon, locale ){
if( locale && 'zxx' !== locale.lang ){
icon.setAttribute('lang',locale.lang);
icon.setAttribute('class',locale.getIcon());
}
else {
icon.setAttribute('lang','');
icon.setAttribute('class','lang nolang');
}
}
// Submit form to Ajax end point when ..erm.. submitted
function onMsginitSuccess( data ){
var href = data && data.redirect;
if( href ){
// TODO show success panel and hide form instead of redirect?
// loco.notices.success('YES');
location.assign( href );
}
}
function process( event ){
event.preventDefault();
fsConn.applyCreds( elForm );
loco.ajax.submit( event.target, onMsginitSuccess );
// TODO some kind of loader?
return false;
}
$(elForm)
.change( validate )
.submit( process );
redrawLocale( localeSelector.val() );
}( window, document, jQuery );

View File

@@ -0,0 +1,47 @@
/**
* Script for POT file initializing page
*/
!function( window, document, $ ){
var loco = window.locoScope,
fsHook = document.getElementById('loco-fs'),
elForm = document.getElementById('loco-potinit')
;
// enable disable form submission
/*function setFormDisabled( disabled ){
$(elForm).find('button.button-primary').each( function( i, button ){
button.disabled = disabled;
} );
}*/
// Submit form to Ajax end point when ..erm.. submitted
function onXgettextSuccess( data ){
var href = data && data.redirect;
if( href ){
location.assign( href );
}
}
function process( event ){
event.preventDefault();
loco.ajax.submit( event.target, onXgettextSuccess );
return false;
}
$(elForm)
.submit( process );
//setFormDisabled( false );
if( fsHook ){
loco.fs.init(fsHook).setForm( elForm );
}
}( window, document, jQuery );

View File

@@ -0,0 +1,191 @@
/**
* Script for PO/POT source view screen
*/
!function( window, document, $ ){
var $modal,
loco = window.locoScope,
conf = window.locoConf,
view = document.getElementById('loco-po');
// index messages and enable text filter
! function( view, dict ){
var min, max,
texts = [],
blocks = [],
valid = true,
filtered = false,
items = $(view).find('li')
;
function flush(){
if( texts.length ){
blocks.push( [min,max] );
dict.push( texts );
texts = [];
}
min = null;
}
items.each( function( i, li ){
var text, $li = $(li);
// empty line indicates end of message
if( $li.find('span.po-none').length ) {
flush();
}
// context indexable if po-text present
else {
max = i;
if( null == min ){
min = i;
}
if( text = $li.find('.po-text').text() ){
texts = texts.concat( text.replace(/\\[ntvfab\\"]/g, ' ').split(' ') );
}
}
} );
flush();
// indexing done, enable filtering
// TODO for filtering to perform well, we must perform off-screen buffering of redundant <li> nodes
// TODO found text highlighting. (more complex than first thought)
function ol( start ){
return $('<ol class="msgcat"></ol>').attr('start',start).appendTo(view);
}
function filter(s){
var i, block, found = dict.find(s), f = -1, length = found.length, $ol;
$('ol',view).remove();
if( length ){
while( ++f < length ){
block = blocks[ found[f] ];
i = block[0];
$ol = ol( i+1 );
for( ; i <= block[1]; i++ ){
$ol.append( items[i]/*.cloneNode(true)*/ );
}
}
validate(true);
}
else {
validate(false);
// translators: When text filtering reduces to an empty view
ol(0).append( $('<li></li>').text( loco.l10n._('Nothing matches the text filter') ) );
}
filtered = true;
resize();
};
function unfilter(){
if( filtered ){
validate(true);
filtered = false;
$('ol',view).remove();
ol(1).append( items );
resize();
}
}
function validate( bool ){
if( valid !== bool ){
$('#loco-content')[ bool ? 'removeClass' : 'addClass' ]('loco-invalid');
valid = bool;
}
}
loco.watchtext( $(view.parentNode).find('form.loco-filter')[0].q, function(q){
q ? filter(q) : unfilter();
} );
}( view, loco.fulltext.init() );
// OK to show view now. may have taken a while to render and index
$(view).removeClass('loco-loading');
// resize function fits scrollable viewport, accounting for headroom and touching bottom of screen.
var resize = function(){
function top( el, ancestor ){
var y = el.offsetTop||0;
while( ( el = el.offsetParent ) && el !== ancestor ){
y += el.offsetTop||0;
}
return y;
}
var fixHeight,
maxHeight = view.clientHeight - 2
;
return function(){
var topBanner = top( view, document.body ),
winHeight = window.innerHeight,
setHeight = winHeight - topBanner - 20
;
if( fixHeight !== setHeight ){
if( setHeight < maxHeight ){
view.style.height = String(setHeight)+'px';
}
else {
view.style.height = '';
}
fixHeight = setHeight;
}
};
}();
resize();
$(window).resize( resize );
// enable file reference links to open modal to ajax service
$(view).click( function(event){
var link = event.target;
if( link.hasAttribute('href') ){
event.preventDefault();
getModal().html('<div class="loco-loading"></div>').dialog('option','title','Loading..').off('dialogopen').dialog('open').on('dialogopen',onModalOpen);
var postdata = $.extend( { ref:link.textContent, path:conf.popath }, conf.project||{} );
loco.ajax.post( 'fsReference', postdata, onRefSource, onRefError );
return false;
}
} );
// http://api.jqueryui.com/dialog/
function getModal(){
return $modal || ( $modal = $('<div id="loco-po-ref"></div>').dialog( {
dialogClass : 'loco-modal',
modal : true,
autoOpen : false,
closeOnEscape : true,
resizable : false,
height : 500
} ) );
}
// when reference pulling fails (e.g. file not found, or line nonexistant)
function onRefError( xhr, error, message ){
$error = $('<p></p>').text( message );
getModal().dialog('close').html('').dialog('option','title','Error').append($error).dialog('open');
}
// display reference source when received via ajax response
function onRefSource( result ){
var code = result && result.code;
if( ! code ){
return;
}
var i = -1,
length = code.length,
$ol = $('<ol></ol>').attr('class',result.type);
while( ++i < length ){
$('<li></li>').html( code[i] ).appendTo($ol);
}
// Highlight referenced line
$ol.find('li').eq( result.line - 1 ).attr('class','highlighted');
// TODO enable highlight toggling of other lines via click/drag etc..
getModal().dialog('close').html('').dialog('option','title', result.path+':'+result.line).append($ol).dialog('open');
}
// scroll to first highlighted line of code once modal open
function onModalOpen( event, ui ){
var div = event.target,
line = $(div).find('li.highlighted')[0],
yAbs = line && line.offsetTop || 0, // <- position of line relative to container
yVis = Math.floor( div.clientHeight / 2 ), // <- target position of line relative to view port
yAdj = Math.max( 0, yAbs - yVis ) // scroll required to move line to visual position
;
div.scrollTop = yAdj;
}
}( window, document, jQuery );

View File

@@ -0,0 +1,129 @@
/**
* Script for bundle setup page
* TODO translations
*/
!function( window, document, $ ){
/**
* Look up bundle configuration on remote server
*/
function find( vendor, slug, version ){
function onFailure(){
if( timer ){
destroy();
onTimeout();
}
};
function onResponse( data, status, obj ){
if( timer ){
destroy();
var match = data && data.exact,
status = data && data.status
;
if( match ){
setJson( match );
}
else if( 404 === status ){
unsetJson("Sorry, we don't know a bundle by this name");
}
else {
loco.notices.debug( data.error || 'Unknown server error' );
onTimeout();
}
}
};
function onTimeout(){
unsetJson('Failed to contact remote API');
timer = null;
}
function destroy(){
if( timer ){
clearTimeout( timer );
timer = null;
}
}
var timer = setTimeout( onTimeout, 3000 );
unsetJson('');
$.ajax( {
url: conf.apiUrl+'/'+vendor+'/'+slug+'.jsonp?version='+encodeURIComponent(version),
dataType: 'jsonp',
success: onResponse,
error: onFailure,
cache: true
} );
return {
abort: destroy
};
}
function setJson( json ){
elForm['json-content'].value = json;
$('#loco-remote-empty').hide();
//$('#loco-remote-query').hide();
$('#loco-remote-found').show();
}
function unsetJson( message ){
elForm['json-content'].value = '';
//$('#loco-remote-query').show();
$('#loco-remote-empty').show().find('span').text( message );
$('#loco-remote-found').hide().removeClass('jshide');
}
function onFindClick( event ){
event.preventDefault();
finder && finder.abort();
finder = find( elForm.vendor.value, elForm.slug.value, elForm.version.value );
return false;
}
function onCancelClick( event ){
event.preventDefault();
unsetJson('');
return false;
}
function setVendors( list ){
var i = -1,
value, label,
length = list.length,
$select = $(elForm.vendor).html('')
;
while( ++i < length ){
value = list[i][0];
label = list[i][1];
$select.append( $('<option></option>').attr('value',value).text(label) );
}
}
var loco = window.locoScope,
conf = window.locoConf,
finder,
elForm = document.getElementById('loco-remote'),
$findButt = $(elForm).find('button[type="button"]').click( onFindClick ),
$resetButt = $(elForm).find('input[type="reset"]').click( onCancelClick );
// pull vendor list
$.ajax( {
url: conf.apiUrl+'/vendors.jsonp',
dataType: 'jsonp',
success: setVendors,
cache: true
} );
}( window, document, jQuery );

View File

@@ -0,0 +1,337 @@
=== Plugin Name ===
Contributors: timwhitlock
Tags: translation, translators, localization, localisation, l10n, i18n, Gettext, PO, MO, productivity, multilingual, internationalization
Requires at least: 4.1
Requires PHP: 5.2.4
Tested up to: 5.2.4
Stable tag: 2.3.1
License: GPLv2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
Translate WordPress plugins and themes directly in your browser
== Description ==
Loco Translate provides in-browser editing of WordPress translation files.
It also provides localization tools for developers, such as extracting strings and generating templates.
Features include:
* Built-in translation editor within WordPress admin
* Create and update language files directly in your theme or plugin
* Extraction of translatable strings from your source code
* Native MO file compilation without the need for Gettext on your system
* Support for PO features including comments, references and plural forms
* PO source view with clickable source code references
* Protected language directory for saving custom translations
* Configurable PO file backups with diff and restore capability
* Built-in WordPress locale codes
Official [Loco](https://localise.biz/) WordPress plugin by Tim Whitlock.
For more information please visit our [plugin page](https://localise.biz/wordpress/plugin).
== Installation ==
= Basic usage: =
Translators: To translate a theme into your language, follow these steps:
1. Create the protected languages directory at `wp-content/languages/loco/themes`
2. Ensure this directory writeable by the web server
3. Find your theme in the list at *Loco Translate > Themes*
4. Click `+ New language` and follow the on-screen prompts.
Developers: To translate your own theme or plugin for distribution, follow these steps:
1. Create a `languages` subdirectory in your bundles root directory
2. Ensure this directory writeable by the web server
3. Find the bundle at either *Loco Translate > Themes* or *Loco Translate > Plugins*
4. Click `+ Create template` and follow the on-screen prompts to extract your strings.
5. Click `+ New language` and follow the on-screen prompts to add your own translations.
= Installing manually: =
1. Unzip all files to the `wp-content/plugins/loco-translate` directory
2. Log into WordPress admin and activate the 'Loco Translate' plugin through the 'Plugins' menu
3. Go to *Loco Translate > Home* in the left-hand menu to start translating
More information on using the plugin is [available here](https://localise.biz/wordpress/plugin).
== Frequently Asked Questions ==
= How do I use it? =
Try our [Guides and Tutorials](https://localise.biz/wordpress/plugin#guides).
= How do I get help? =
If you have a problem using Loco Translate, please try our [help pages](https://localise.biz/wordpress/plugin).
There's a lot of information there to help you understand how it works and the most common pitfalls to avoid.
To report a bug please start a new topic in the [support forum](https://wordpress.org/support/plugin/loco-translate),
but please check the [FAQs](https://localise.biz/wordpress/plugin/faqs) for similar issues first.
If you decide to submit a bug report please post enough [relevant detail](https://localise.biz/wordpress/plugin/faqs/debug-info) for us to reproduce your issue.
= Is my data protected? =
We don't collect your data or snoop on you. See the [plugin privacy notice](https://localise.biz/wordpress/plugin/privacy).
== Screenshots ==
1. Translating strings in the browser with the Loco PO Editor
2. Showing translation progress for theme language files
3. PO source view with text filter and clickable file references
4. Restore tab showing PO diff view with revert function
5. Showing access to translations by installed language
== Changelog ==
= 2.3.1 =
* Default POT getter now looks in "lang" directory
* Not calling deprecated magic quotes functions under PHP 7.4
* Fixed issue with conflicting page hooks
* Ajax file uploads now enabled by default
* Removed legacy option migrations from 1.x branch
* Bumped WP compatibility to 5.2.4
= 2.3.0 =
* Added experimental support for multipart uploads
* Added relocation tab for moving translation sets
* Creation of missing directories when writing new files
* Fixed duplicate file addition when iterating over symlink
* Bumped WP compatibility to 5.2.1
= 2.2.2 =
* Security fixes for reading sensitive files
* Fixed old PHP version error in data files
* Bumped WP compatibility to 5.1.1
= 2.2.1 =
* Fixed bug where plural tabs not displaying RTL
* Various improvements to PO parser incl. better charset handling
* Excluding node_modules and vendor directories by default
* Transients now have maximum lifespan of 10 days, refreshed after 24h
* Symlink fix for followed theme paths detected outside theme
* Deprecated config repository lookup
* Bumped WP compatibility to 5.1
= 2.2.0 =
* Fix for empty language code when getting plural rules
* Added X-Loco-Version header to generated Gettext files
* Added sanity check for mbstring.func_overload madness
* Added "Assign template" link on missing template page
* Added JavaScript string extraction (experimental)
* Editor supports sprintf-js when javascript-format tag present
* Fix for duplicate comments when end punctuation differs
* Marking msgctxt more clearly in editor views
* Added `loco_admin_shutdown` action hook
* Bumped WP compatibility to 5.0 (beta)
= 2.1.5 =
* Updated locale data
* Minor fix to file reference resolution
* Fixed windows paths with trailing backslash
* Fixed ssh-keys toggling issue
* Rejigged buffer handling during Ajax
* Bumped WP compatibility to 4.9.8
= 2.1.4 =
* Bumped WP compatibility to 4.9.6
* Hooked in privacy policy suggestion
= 2.1.3 =
* Added loco_locale_name filter and updated locale data
* Fixed editor column sorting to update as values change
* Supporting RTL text in editor preview rows
* Minor refactor of debug mode routing check
* Minor PO parser improvements
* Bumped WP compatibility to 4.9.5
= 2.1.2 =
* Fixed undeclared property in admin hook
* Fixed incompatibility with older WordPress
* Fixed incorrect millisecond reporting in footer
* Removed locale progress column for en_US locale
* Tweaks to debugging and error logging
= 2.1.1 =
* Setting `Project-Id-Version` on new POT files
* Added source view to quick links in file tables
* Supporting only WordPress style locale codes
* Editor screen tolerates missing PO headers
* Ajax debugging improvements for issue reporting
* Added loco_parse_locale action callback
= 2.1.0 =
* Add `fs_protect` setting to avoid overwriting system files
* Fixed bug in connect dialogue where errors not redisplayed
* Minor improvements to inline notices
* Removed downgrade notice under version tab
* Fixed extraction bug where file header confused with comment
* Resolved some inconsistencies between PHP and JS utilities
* Added Restore tab with diff display
* Added `loco_settings` hook
* Prevented editor from changing PO document order
* Added default string sorting to extracted strings
* Added "Languages" section for grouping files by locale
* Fixed bug where translations loaded before user profile language set
* Added loco_locale_plurals filter for customising plural rules
* Allowing PO files to enforce their own Plural-Forms rules
* Added `loco_allow_remote` filter for debugging remote problems
* Updated plural forms from Unicode CLDR
* PHP extractor avoids repeated comments
* Bumped WP compatibility to 4.9.4
= 2.0.17 =
* Unofficial languages showing in “Installed” dropdown
* Fixed extraction bug where comment confused with file header
* Fixed issue where src attributes requested from server during HTML strip
* Added loco_admin_init hook into ajax router for consistency
* Added warning on file info page when file is managed by WordPress
* Minor help link and layout tweaks
* Bumped WP compatibility to 4.9.1
= 2.0.16 =
* File writer observes wp_is_file_mod_allowed
* Fixed progress bug in editor for locales with nplurals=1
* Made plural form categories translatable for editor UI
* Sync-from-source raises warning when files are skipped
* Added hack for extracting from .twig as per .php
* Added warning when child themes declare parent text domain
* Added option to control PO line wrapping
* Bumped WP compatibility to 4.8.2
= 2.0.15 =
* Permanently removed legacy version 1.x
* Fixed bug where editor code view was not redrawn on resize
* Fixed bug where fuzzy flag caused format flag to be ignored
* Fixed bug where autoloader responded to very long class names
* Purging WP object cache when active plugin list changes
* Added experimental source word count into POT info tab
* Bumped WP compatibility to 4.8.1
= 2.0.14 =
* Editor improvements inc. column sorting
* Added warnings that legacy version will be removed
* Added PO source view text filtering
* Added _fs_nonce for 4.7.5 compatibility
* Migrated to canonical text domain
* Removed wp class autoloading
= 2.0.13 =
* CSS conflict fixes
* Added option for UTF-8 byte order mark
* Printf highlighting observes no-php-format flag
* Fixed issue with translator role losing “read” permission
= 2.0.12 =
* Minor fix for root path configs
* Added alternative PHP extensions setting
* Bumped WP version to 4.7.3
* LoadHelper fix for core files
* Allow revoking of permissions from translator role
* Allow network admins to deny access to site admins
= 2.0.11 =
* Extra debug logging and error diagnostics
* Forcefully clear output buffers before Ajax flush
* Bumped WordPress version to 4.7
* Experimental wildcard text domain support
= 2.0.10 =
* Allows missing domain argument in plugin_locale filter
* Reverted editor changes that disabled readonly text
* Added invisibles and coding editor switches
* Added table filtering via text query
* Added Last-Translator user preference
= 2.0.9 =
* Bumped minimum WordPress version to 4.1
* Some optimisation of transient caching
* Fixed hash table settings bug
= 2.0.8 =
* Source refs fix for files in unknown subsets
* Downgrades PO formatting exceptions to PHP warnings
* Renamed function prefixes to avoid PHP 7 warnings
* Better support for php-format and no-php-format flag
* PO source and editor UI tweaks
* Localised strings and implemented in js
= 2.0.7 =
* Fixed prototype.js conflict
* More Windows file path fixes
* Added loco_current_translator filter
* Fixed false positive in extra files test
= 2.0.6 =
* PO wrapping bugfix
* Downgraded source code bugfix
* Tolerating headerless POT files
* Core bundle metadata tweaks
= 2.0.5 =
* Deferred missing tokenizer warning
* Allows editing of files in unconfigured sets
* Added maximum PHP file size for string extraction
* Display of PHP fatal errors during Ajax
= 2.0.4 =
* Reduced session failures to debug notices
* Added wp_roles support for WP < 4.3
* Fixed domain listener bugs
= 2.0.3 =
* Added support for Windows servers
* Removed incomplete config warning on bundle overview
= 2.0.2 =
* Fixed bug when absolute path used to get plugins
* Added loco_plugins_data filter
* Added theme Template Name header extraction
* Minor copy amends
= 2.0.1 =
* Added help link in settings page
* Fixed opendir warnings in legacy code
* Catching session errors during init
* Removing meta row link when plugin not found
= 2.0.0 =
* First release of completely rebuilt version 2
== Upgrade Notice ==
= 2.3.1 =
* Various bug fixes and improvements
== Keyboard shortcuts ==
The PO file editor supports the following keyboard shortcuts for faster translating:
* Done and Next: `Ctrl ↵`
* Next string: `Ctrl ↓`
* Previous string: `Ctrl ↑`
* Next untranslated: `Shift Ctrl ↓`
* Previous untranslated: `Shift Ctrl ↑`
* Copy from source text: `Ctrl B`
* Clear translation: `Ctrl K`
* Toggle Fuzzy: `Ctrl U`
* Save PO / compile MO: `Ctrl S`
* Toggle invisibles: `Shift Ctrl I`
Mac users can use ⌘ Cmd instead of Ctrl.

View File

@@ -0,0 +1,469 @@
<?php
/**
* Represents a WordPress locale
*
* @property string $lang
* @property string $region
* @property string $variant
*/
class Loco_Locale implements JsonSerializable {
/**
* Language subtags
* @var array
*/
private $tag;
/**
* Cached composite tag
* @var string
*/
private $_tag;
/**
* Cached icon css class
* @var string
*/
private $icon;
/**
* Name in English
* @var string
*/
private $name;
/**
* Name in language of self
* @var string
*/
private $_name;
/**
* Cache of raw plural data
* @var array
*/
private $plurals;
/**
* Validity cache
* @var bool
*/
private $valid;
/**
* @return Loco_Locale
*/
public static function parse( $tag ){
$locale = new Loco_Locale('');
try {
$locale->setSubtags( loco_parse_wp_locale($tag) );
}
catch( Exception $e ){
// isValid should return false
}
do_action( 'loco_parse_locale', $locale, $tag );
return $locale;
}
/**
* Construct from subtags NOT from composite tag. See self::parse
* Note that this skips normalization and validation steps
*/
public function __construct( $lang = '', $region = '', $variant = '' ){
$this->tag = compact('lang','region','variant');
}
/**
* @internal
* Allow read access to subtags
*/
public function __get( $t ){
return isset($this->tag[$t]) ? $this->tag[$t] : '';
}
/**
* @internal
* Allow write access to subtags
*/
public function __set( $t, $s ){
if( isset($this->tag[$t]) ){
$this->tag[$t] = $s;
$this->setSubtags( $this->tag );
}
}
/**
* Set subtags as produced from loco_parse_wp_locale
* @return Loco_Locale
*/
public function setSubtags( array $tag ){
$this->valid = false;
$default = array( 'lang' => '', 'region' => '', 'variant' => '' );
// disallow setting of unsupported tags
if( $bad = array_diff_key($tag, $default) ){
throw new Loco_error_LocaleException('Unsupported subtags: '.implode(',',$bad) );
}
$tag += $default;
// language tag is minimum requirement
if( ! $tag['lang'] ){
throw new Loco_error_LocaleException('Locale must have a language');
}
// no UN codes in Wordpress
if( is_numeric($tag['region']) ){
throw new Loco_error_LocaleException('Numeric regions not supported');
}
// single, scalar variant. Only using for Formal german currently.
if( is_array($tag['variant']) ){
$tag['variant'] = implode('_',$tag['variant']);
}
// normalize case
$tag['lang'] = strtolower($tag['lang']);
$tag['region'] = strtoupper($tag['region']);
$tag['variant'] = strtolower($tag['variant']);
// set subtags and invalidate cache of language tag
$this->tag = $tag;
$this->_tag = null;
$this->icon = null;
$this->valid = true;
return $this;
}
/**
* @return Loco_Locale
*/
public function normalize(){
try {
$this->setSubtags( $this->tag );
}
catch( Loco_error_LocaleException $e ){
$this->_tag = '';
$this->icon = null;
$this->name = 'Invalid locale';
$this->_name = null;
}
return $this;
}
/**
* @return string
*/
public function __toString(){
$str = $this->_tag;
if( is_null($str) ){
$str = implode('_',array_filter($this->tag));
$this->_tag = $str;
}
return $str;
}
/**
* Get stored name in current display language.
* Note that no dynamic translation of English name is performed, but can be altered with loco_parse_locale filter
* @return string | null
*/
public function getName(){
if( $name = $this->name ){
// use canonical native name only when current language matches
// deliberately not matching whole tag such that fr_CA would show native name of fr_FR
if( $_name = $this->getNativeName() ){
$locale = self::parse( function_exists('get_user_locale') ? get_user_locale() : get_locale() );
if( $this->lang === $locale->lang ){
$name = $_name;
}
}
return $name;
}
}
/**
* Get canonical native name as defined by WordPress
* @return string | null
*/
public function getNativeName(){
if( $name = $this->_name ){
return $name;
}
}
/**
* @return string
*/
public function getIcon(){
$icon = $this->icon;
if( is_null($icon) ){
$tag = array();
if( ! $this->tag['lang'] ){
$tag[] = 'lang lang-zxx';
}
foreach( $this->tag as $class => $code ){
if( $code ){
$tag[] = $class.' '.$class.'-'.$code;
}
}
$icon = strtolower( implode(' ',$tag) );
$this->icon = $icon;
}
return $icon;
}
/**
* @return Loco_Locale
*/
public function setIcon( $css ){
if( $css ){
$this->icon = (string) $css;
}
else {
$this->icon = null;
}
return $this;
}
/**
* @return Loco_Locale
*/
public function setName( $english_name, $native_name = '' ){
$this->name = apply_filters('loco_locale_name', $english_name, $native_name );
$this->_name = (string) $native_name;
return $this;
}
/**
* Test whether locale is valid
*/
public function isValid(){
if( is_null($this->valid) ){
$this->normalize();
}
return $this->valid;
}
/**
* Resolve this locale's "official" name from WordPress's translation api
* @return string English name currently set
*/
public function fetchName( Loco_api_WordPressTranslations $api ){
$tag = (string) $this;
// pull from WordPress translations API if network allowed
if( $locale = $api->getLocale($tag) ){
$this->setName( $locale->getName(), $locale->getNativeName() );
}
return $this->getName();
}
/**
* Resolve this locale's name from compiled Loco data
* @return string English name currently set
*/
public function buildName(){
$names = array();
// should at least have a language or not valid
if( $this->isValid() ){
$code = $this->tag['lang'];
$db = Loco_data_CompiledData::get('languages');
if( $name = $db[$code] ){
// if variant is present add only that in brackets (no lookup required)
if( $code = $this->tag['variant'] ){
$name .= ' ('.ucfirst($code).')';
}
// else add region in brackets if present
else if( $code = $this->tag['region'] ){
$db = Loco_data_CompiledData::get('regions');
if( $extra = $db[$code] ){
$name .= ' ('.$extra.')';
}
else {
$name .= ' ('.$code.')';
}
}
$this->setName( $name );
}
}
else {
$this->setName( __('Invalid locale','loco-translate') );
}
return $this->getName();
}
/**
* Ensure locale has a label, even if it has to fall back to language code or error
* @return string
*/
public function ensureName( Loco_api_WordPressTranslations $api ){
$name = $this->getName();
if( ! $name ){
$name = $this->fetchName($api);
// failing that, build own own name from components
if( ! $name ){
$name = $this->buildName();
// last resort, use tag as name
if( ! $name ){
$name = (string) $this;
$this->setName( $name );
}
}
}
return $name;
}
/**
* @return array
*/
public function jsonSerialize(){
$a = $this->tag;
$a['label'] = $this->getName();
// plural data expected by editor
$p = $this->getPluralData();
$a['pluraleq'] = $p[0];
$a['plurals'] = $p[1];
$a['nplurals'] = count($p[1]);
return $a;
}
/**
* Get plural data with translated forms
* @internal
* @return array [ (string) equation, (array) forms ]
*/
public function getPluralData(){
$cache = $this->plurals;
if( ! $cache ){
$lc = $this->lang;
$db = Loco_data_CompiledData::get('plurals');
$id = $lc && isset($db[$lc]) ? $db[$lc] : 0;
$cache = $this->setPlurals( $db[''][$id] );
}
return $cache;
}
/**
* @return int
*/
public function getPluralCount(){
$raw = $this->getPluralData();
return count( $raw[1] );
}
/**
* @return array
*/
private function setPlurals( array $raw ){
$raw = apply_filters( 'loco_locale_plurals', $raw, $this );
// handle languages with no plural forms, where n is always 0
if( ! isset($raw[1][1]) ){
// Translators: Plural category for languages that have no plurals
$raw[1] = array( _x('All forms','Plural category','loco-translate') );
$raw[0] = '0';
}
// else translate all implemented plural forms
// for meaning of categories, see http://cldr.unicode.org/index/cldr-spec/plural-rules
else {
$forms = array(
// Translators: Plural category for zero quantity
'zero' => _x('Zero','Plural category','loco-translate'),
// Translators: Plural category for singular quantity
'one' => _x('One','Plural category','loco-translate'),
// Translators: Plural category used in some multi-plural languages
'two' => _x('Two','Plural category','loco-translate'),
// Translators: Plural category used in some multi-plural languages
'few' => _x('Few','Plural category','loco-translate'),
// Translators: Plural category used in some multi-plural languages
'many' => _x('Many','Plural category','loco-translate'),
// Translators: General plural category not covered by other forms
'other' => _x('Other','Plural category','loco-translate'),
);
foreach( $raw[1] as $k => $v ){
if( isset($forms[$v]) ){
$raw[1][$k] = $forms[$v];
}
}
}
$this->plurals = $raw;
return $raw;
}
/**
* Get PO style Plural-Forms header value comprising number of forms and integer equation for n
* @return string
*/
public function getPluralFormsHeader(){
list( $equation, $forms ) = $this->getPluralData();
return sprintf('nplurals=%u; plural=%s;', count($forms), $equation );
}
/**
* Apply PO style Plural-Forms header.
* @param string e.g. "nplurals=2; plural=n != 1;"
* @return Loco_Locale
*/
public function setPluralFormsHeader( $str ){
if( ! preg_match('/^nplurals=(\\d);\s*plural=([ +\\-\\/*%!=<>|&?:()n0-9]+);?$/', $str, $match ) ){
throw new InvalidArgumentException('Invalid Plural-Forms header, '.json_encode($str) );
}
$cache = $this->getPluralData();
$exprn = $match[2];
// always alter if equation differs
if( $cache[0] !== $exprn ){
$this->plurals[0] = $exprn;
// alter number of forms if changed
$nplurals = max( 1, (int) $match[1] );
if( $nplurals !== count($cache[1]) ){
// named forms must also change, but Plural-Forms cannot contain this information
// as a cheat, we'll assume first form always "one" and last always "other"
for( $i = 1; $i < $nplurals; $i++ ){
$name = 1 === $i ? 'one' : sprintf('Plural %u',$i);
$forms[] = $name;
}
$forms[] = 'other';
$this->setPlurals( array($exprn,$forms) );
}
}
return $this;
}
/**
* @return string
*/
public function exportJson(){
return json_encode( $this->jsonSerialize() );
}
}
// Depends on compiled library
if( ! function_exists('loco_parse_wp_locale') ){
loco_include('lib/compiled/locales.php');
}

View File

@@ -0,0 +1,78 @@
<?php
/**
* @codeCoverageIgnore
*/
class Loco_admin_DebugController extends Loco_mvc_AdminController {
/**
* {@inheritdoc}
*/
public function init(){
parent::init();
$this->set('title','DEBUG');
}
/**
* {@inheritdoc}
*/
public function render(){
// debug package listener
$themes = array();
/* @var $bundle Loco_package_Bundle */
foreach( Loco_package_Listener::singleton()->getThemes() as $bundle ){
$themes[] = array (
'id' => $bundle->getId(),
'name' => $bundle->getName(),
'default' => $bundle->getDefaultProject()->getSlug(),
'count' => count($bundle),
);
}
$this->set('themes', $themes );
$plugins = array();
/* @var $bundle Loco_package_Bundle */
foreach( Loco_package_Listener::singleton()->getPlugins() as $bundle ){
$plugins[] = array (
'id' => $bundle->getId(),
'name' => $bundle->getName(),
'default' => $bundle->getDefaultProject()->getSlug(),
'count' => count($bundle),
);
}
// $this->set( 'plugins', Loco_package_Plugin::get_plugins() );
// $this->set('installed', wp_get_installed_translations('plugins') );
// $this->set('active', get_option( 'active_plugins', array() ) );
// $this->set('langs',get_available_languages());
/*$plugins = get_plugins();
$plugin_info = get_site_transient( 'update_plugins' );
foreach( $plugins as $plugin_file => $plugin_data ){
if ( isset( $plugin_info->response[$plugin_file] ) ) {
$plugins[$plugin_file]['____'] = $plugin_info->response[$plugin_file];
}
}*/
/*/ inspect session and test flash messages
$session = Loco_data_Session::get();
$session->flash( 'success', microtime() );
$this->set('session', $session->getArrayCopy() );
Loco_data_Session::close();*/
// try some notices
Loco_error_AdminNotices::add( new Loco_error_Success('This is a sample success message') );
Loco_error_AdminNotices::add( new Loco_error_Warning('This is a sample warning') );
Loco_error_AdminNotices::add( new Loco_error_Exception('This is a sample error') );
Loco_error_AdminNotices::add( new Loco_error_Debug('This is a sample debug message') );
//*/
return $this->view('admin/debug');
}
}

View File

@@ -0,0 +1,42 @@
<?php
/**
*
*/
class Loco_admin_ErrorController extends Loco_mvc_AdminController {
public function renderError( Exception $e ){
$this->set('error', Loco_error_Exception::convert($e) );
return $this->render();
}
public function render(){
$e = $this->get('error');
if( $e ){
/* @var Loco_error_Exception $e */
$file = Loco_mvc_FileParams::create( new Loco_fs_File( $e->getRealFile() ) );
$file['line'] = $e->getRealLine();
$this->set('file', $file );
if( loco_debugging() ){
$trace = array();
foreach( $e->getRealTrace() as $raw ) {
$frame = new Loco_mvc_ViewParams($raw);
if( $frame->has('file') ){
$frame['file'] = Loco_mvc_FileParams::create( new Loco_fs_File($frame['file']) )->relpath;
}
$trace[] = $frame;
}
$this->set('trace',$trace);
}
}
else {
$e = new Loco_error_Exception('Unknown error');
$this->set('error', $e );
}
return $this->view( $e->getTemplate() );
}
}

View File

@@ -0,0 +1,72 @@
<?php
/**
* Generic navigation helper.
*/
class Loco_admin_Navigation extends ArrayIterator {
/**
* @return Loco_admin_Navigation
*/
public function add( $name, $href = null, $active = false ){
$this[] = new Loco_mvc_ViewParams( compact('name','href','active') );
return $this;
}
/* not currently used
* @return Loco_admin_Navigation
*
public function addRoute( $name, $action ){
$href = Loco_mvc_AdminRouter::generate( $action );
return $this->add( $name, $href );
}*/
/**
* Create a breadcrumb trail for a given view below a bundle
* @return Loco_admin_Navigation
*/
public static function createBreadcrumb( Loco_package_Bundle $bundle ){
$nav = new Loco_admin_Navigation;
// root link depends on bundle type
$type = strtolower( $bundle->getType() );
if( 'core' !== $type ){
$link = new Loco_mvc_ViewParams( array(
'href' => Loco_mvc_AdminRouter::generate($type),
) );
if( 'theme' === $type ){
$link['name'] = __('Themes','loco-translate');
}
else {
$link['name'] = __('Plugins','loco-translate');
}
$nav[] = $link;
}
// Add actual bundle page, href may be unset to show as current page if needed
$nav->add (
$bundle->getName(),
Loco_mvc_AdminRouter::generate( $type.'-view', array( 'bundle' => $bundle->getHandle() ) )
);
// client code will add current page
return $nav;
}
/**
* @return Loco_mvc_ViewParams
*
public function getSecondLast(){
$i = count($this);
if( $i > 1 ){
return $this[ $i-2 ];
}
}*/
}

View File

@@ -0,0 +1,35 @@
<?php
/**
*
*/
abstract class Loco_admin_RedirectController extends Loco_mvc_AdminController {
/**
* Get full URL for redirecting to.
* @var string
*/
abstract public function getLocation();
/**
* {@inheritdoc}
*/
public function init(){
$location = $this->getLocation();
if( $location && wp_redirect($location) ){
// @codeCoverageIgnoreStart
exit;
}
}
/**
* @internal
*/
public function render(){
return 'Failed to redirect';
}
}

View File

@@ -0,0 +1,90 @@
<?php
/**
* Highest level Loco admin screen.
*/
class Loco_admin_RootController extends Loco_admin_list_BaseController {
/**
* {@inheritdoc}
*/
public function getHelpTabs(){
return array (
__('Overview','default') => $this->viewSnippet('tab-home'),
);
}
/**
* Render main entry home screen
*/
public function render(){
// translators: home screen title where %s is the version number
$this->set('title', sprintf( __('Loco Translate %s','loco-translate'), loco_plugin_version() ) );
// Show currently active theme on home page
$theme = Loco_package_Theme::create(null);
$this->set('theme', $this->bundleParam($theme) );
// Show plugins that have currently loaded translations
$bundles = array();
foreach( Loco_package_Listener::singleton()->getPlugins() as $bundle ){
try {
$bundles[] = $this->bundleParam($bundle);
}
catch( Exception $e ){
// bundle should exist if we heard it. reduce to debug notice
Loco_error_AdminNotices::debug( $e->getMessage() );
}
}
$this->set('plugins', $bundles );
// Show recently "used' bundles
$bundles = array();
$recent = Loco_data_RecentItems::get();
// filter in lieu of plugin setting
$maxlen = apply_filters('loco_num_recent_bundles', 10 );
foreach( $recent->getBundles(0,$maxlen) as $id ){
try {
$bundle = Loco_package_Bundle::fromId($id);
$bundles[] = $this->bundleParam($bundle);
}
catch( Exception $e ){
// possible that bundle ID changed since being saved in recent items list
}
}
$this->set('recent', $bundles );
// current locale and related links
$locale = Loco_Locale::parse( get_locale() );
$api = new Loco_api_WordPressTranslations;
$tag = (string) $locale;
$this->set( 'siteLocale', new Loco_mvc_ViewParams( array(
'code' => $tag,
'name' => ( $name = $locale->ensureName($api) ),
'attr' => 'class="'.$locale->getIcon().'" lang="'.$locale->lang.'"',
'link' => '<a href="'.esc_url(Loco_mvc_AdminRouter::generate('lang-view', array('locale'=>$tag) )).'">'.esc_html($name).'</a>',
//'opts' => admin_url('options-general.php').'#WPLANG',
) ) );
// user's "admin" language may differ and is worth showing
if( function_exists('get_user_locale') ){
$locale = Loco_Locale::parse( get_user_locale() );
$alt = (string) $locale;
if( $tag !== $alt ){
$this->set( 'adminLocale', new Loco_mvc_ViewParams( array(
'name' => ( $name = $locale->ensureName($api) ),
'link' => '<a href="'.esc_url(Loco_mvc_AdminRouter::generate('lang-view', array('locale'=>$tag) )).'">'.esc_html($name).'</a>',
) ) );
}
}
$this->set('title', __('Welcome to Loco Translate','loco-translate') );
return $this->view('admin/root');
}
}

View File

@@ -0,0 +1,161 @@
<?php
/**
* Base controller for any admin screen related to a bundle
*/
abstract class Loco_admin_bundle_BaseController extends Loco_mvc_AdminController {
/**
* @var Loco_package_Bundle
*/
private $bundle;
/**
* @var Loco_package_Project
*/
private $project;
/**
* @return Loco_package_Bundle
*/
public function getBundle(){
if( ! $this->bundle ){
$type = $this->get('type');
$handle = $this->get('bundle');
$this->bundle = Loco_package_Bundle::createType( $type, $handle );
}
return $this->bundle;
}
/**
* Commit bundle config to database
* @return Loco_admin_bundle_BaseController
*/
protected function saveBundle(){
$custom = new Loco_config_CustomSaved;
if( $custom->setBundle($this->bundle)->persist() ){
Loco_error_AdminNotices::success( __('Configuration saved','loco-translate') );
}
// invalidate bundle in memory so next fetch is re-configured from DB
$this->bundle = null;
return $this;
}
/**
* Remove bundle config from database
* @return Loco_admin_bundle_BaseController
*/
protected function resetBundle(){
$option = $this->bundle->getCustomConfig();
if( $option && $option->remove() ){
Loco_error_AdminNotices::success( __('Configuration reset','loco-translate') );
// invalidate bundle in memory so next fetch falls back to auto-config
$this->bundle = null;
}
return $this;
}
/**
* @return Loco_package_Project
*/
public function getProject(){
if( ! $this->project ){
$bundle = $this->getBundle();
$domain = $this->get('domain');
if( ! $domain ){
throw new Loco_error_Exception( sprintf('Translation set not known in %s', $bundle ) );
}
$this->project = $bundle->getProjectById($domain);
if( ! $this->project ){
throw new Loco_error_Exception( sprintf('Unknown translation set: %s not in %s', json_encode($domain), $bundle ) );
}
}
return $this->project;
}
/**
* @return Loco_admin_Navigation
*/
protected function prepareNavigation(){
$bundle = $this->getBundle();
// navigate up to bundle listing page
$breadcrumb = Loco_admin_Navigation::createBreadcrumb( $bundle );
$this->set( 'breadcrumb', $breadcrumb );
// navigate between bundle view siblings
$tabs = new Loco_admin_Navigation;
$this->set( 'tabs', $tabs );
$actions = array (
'view' => __('Overview','loco-translate'),
'setup' => __('Setup','loco-translate'),
'conf' => __('Advanced','loco-translate'),
);
if( loco_debugging() ){
$actions['debug'] = __('Debug','loco-translate');
}
$suffix = $this->get('action');
$prefix = strtolower( $this->get('type') );
$getarg = array_intersect_key( $_GET, array('bundle'=>'') );
foreach( $actions as $action => $name ){
$href = Loco_mvc_AdminRouter::generate( $prefix.'-'.$action, $getarg );
$tabs->add( $name, $href, $action === $suffix );
}
return $breadcrumb;
}
/**
* Prepare file system connect
* @param string "create", "update", "delete"
* @param string path relative to wp-content
* @return Loco_mvc_HiddenFields
*/
protected function prepareFsConnect( $type, $relpath ){
$fields = new Loco_mvc_HiddenFields( array(
'auth' => $type,
'path' => $relpath,
'loco-nonce' => wp_create_nonce('fsConnect'),
'_fs_nonce' => wp_create_nonce('filesystem-credentials'), // <- WP 4.7.5 added security fix
) ) ;
$this->set('fsFields', $fields );
// may have fs credentials saved in session
try {
if( Loco_data_Settings::get()->fs_persist ){
$session = Loco_data_Session::get();
if( isset($session['loco-fs']) ){
$fields['connection_type'] = $session['loco-fs']['connection_type'];
}
}
}
catch( Exception $e ){
Loco_error_AdminNotices::debug( $e->getMessage() );
}
// Run pre-checks that may determine file should not be written
if( $relpath ){
$file = new Loco_fs_File( $relpath );
$file->normalize( loco_constant('WP_CONTENT_DIR') );
// total file system block makes connection type irrelevant
try {
$api = new Loco_api_WordPressFileSystem;
$api->preAuthorize($file);
}
catch( Loco_error_WriteException $e ){
$this->set('fsLocked', $e->getMessage() );
}
}
return $fields;
}
}

View File

@@ -0,0 +1,145 @@
<?php
/**
* Bundle configuration page
*/
class Loco_admin_bundle_ConfController extends Loco_admin_bundle_BaseController {
/**
* {@inheritdoc}
*/
public function init(){
parent::init();
$this->enqueueStyle('config');
$this->enqueueScript('config');
$bundle = $this->getBundle();
// translators: where %s is a plugin or theme
$this->set( 'title', sprintf( __('Configure %s','loco-translate'),$bundle->getName() ) );
$post = Loco_mvc_PostParams::get();
// always set a nonce for current bundle
$nonce = $this->setNonce( $this->get('_route').'-'.$this->get('bundle') );
$this->set('nonce', $nonce );
try {
// Save configuration if posted
if( $post->has('conf') ){
if( ! $post->name ){
$post->name = $bundle->getName();
}
$this->checkNonce( $nonce->action );
$model = new Loco_config_FormModel;
$model->loadForm( $post );
// configure bundle from model in full
$bundle->clear();
$reader = new Loco_config_BundleReader( $bundle );
$reader->loadModel( $model );
$this->saveBundle();
}
// Delete configuration if posted
else if( $post->has('unconf') ){
$this->resetBundle();
}
}
catch( Exception $e ){
Loco_error_AdminNotices::warn( $e->getMessage() );
}
}
/**
* {@inheritdoc}
*/
public function getHelpTabs(){
return array (
__('Advanced tab','loco-translate') => $this->viewSnippet('tab-bundle-conf'),
);
}
/**
* {@inheritdoc}
*/
public function render() {
$parent = null;
$bundle = $this->getBundle();
$default = $bundle->getDefaultProject();
$base = $bundle->getDirectoryPath();
// parent themes are inherited into bundle, we don't want them in the child theme config
if( $bundle->isTheme() && ( $parent = $bundle->getParent() ) ){
$this->set( 'parent', new Loco_mvc_ViewParams( array(
'name' => $parent->getName(),
'href' => Loco_mvc_AdminRouter::generate('theme-conf', array( 'bundle' => $parent->getSlug() ) + $_GET ),
) ) );
}
// render postdata straight back to form if sent
$data = Loco_mvc_PostParams::get();
// else build initial data from current bundle state
if( ! $data->has('conf') ){
// create single default set for totally unconfigured bundles
if( 0 === count($bundle) ){
$bundle->createDefault('');
}
$writer = new Loco_config_BundleWriter($bundle);
$data = $writer->toForm();
// removed parent bundle from config form, as they are inherited
/* @var Loco_package_Project $project */
foreach( $bundle as $i => $project ){
if( $parent && $parent->hasProject($project) ){
// warn if child theme uses parent theme's text domain (but allowing to render so we don't get an empty form.
if( $project === $default ){
Loco_error_AdminNotices::warn( __("Child theme declares the same Text Domain as the parent theme",'loco-translate') );
}
// else safe to remove parent theme configuration as it should be held in its own bundle
else {
$data['conf'][$i]['removed'] = true;
}
}
}
}
// build config blocks for form
$i = 0;
$conf = array();
foreach( $data['conf'] as $raw ){
if( empty($raw['removed']) ){
$slug = $raw['slug'];
$domain = $raw['domain'] or $domain = 'untitled';
$raw['prefix'] = sprintf('conf[%u]', $i++ );
$raw['short'] = ! $slug || ( $slug === $domain ) ? $domain : $domain.'→'.$slug;
$conf[] = new Loco_mvc_ViewParams( $raw );
}
}
// bundle level configs
$name = $bundle->getName();
$excl = $data['exclude'];
// access to type of configuration that's currently saved
$this->set('saved', $bundle->isConfigured() );
// link to author if there are config problems
$info = $bundle->getHeaderInfo();
$this->set('author', $info->getAuthorLink() );
// link for downloading current configuration XML file
$args = array (
'path' => 'loco.xml',
'action' => 'loco_download',
'bundle' => $bundle->getHandle(),
'type' => $bundle->getType()
);
$this->set( 'xmlUrl', Loco_mvc_AjaxRouter::generate( 'DownloadConf', $args ) );
$this->set( 'manUrl', apply_filters('loco_external','https://localise.biz/wordpress/plugin/manual/bundle-config') );
$this->prepareNavigation()->add( __('Advanced configuration','loco-translate') );
return $this->view('admin/bundle/conf', compact('conf','base','name','excl') );
}
}

View File

@@ -0,0 +1,52 @@
<?php
/**
* Bundle debugger.
* Shows bundle diagnostics and highlights problems
*/
class Loco_admin_bundle_DebugController extends Loco_admin_bundle_BaseController {
/**
* {@inheritdoc}
*/
public function init(){
parent::init();
$bundle = $this->getBundle();
$this->set('title', 'Debug: '.$bundle );
}
/**
* {@inheritdoc}
*/
public function render(){
$this->prepareNavigation()->add( __('Bundle diagnostics','loco-translate') );
$bundle = $this->getBundle();
$debugger = new Loco_package_Debugger($bundle);
$this->set('notices', $notices = new Loco_mvc_ViewParams );
/* @var $notice Loco_error_Exception */
foreach( $debugger as $notice ){
$notices[] = new Loco_mvc_ViewParams( array(
'style' => 'notice inline notice-'.$notice->getType(),
'title' => $notice->getTitle(),
'body' => $notice->getMessage(),
) );
}
$meta = $bundle->getHeaderInfo();
$this->set('meta', new Loco_mvc_ViewParams( array(
'vendor' => $meta->getVendorHost(),
'author' => $meta->getAuthorCredit(),
) ) );
if( count($bundle) ){
$writer = new Loco_config_BundleWriter( $bundle );
$this->set( 'xml', $writer->toXml() );
}
return $this->view('admin/bundle/debug');
}
}

View File

@@ -0,0 +1,162 @@
<?php
/**
* Pseudo-bundle view, lists all files available in a single locale
*/
class Loco_admin_bundle_LocaleController extends Loco_mvc_AdminController {
/**
* @var Loco_Locale
*/
private $locale;
/**
* {@inheritdoc}
*/
public function init(){
parent::init();
$tag = $this->get('locale');
$locale = Loco_Locale::parse($tag);
if( $locale->isValid() ){
$api = new Loco_api_WordPressTranslations;
$this->set('title', $locale->ensureName($api) );
$this->locale = $locale;
$this->enqueueStyle('locale')->enqueueStyle('fileinfo');
}
}
/**
* {@inheritdoc}
*/
public function getHelpTabs(){
return array (
__('Overview','default') => $this->viewSnippet('tab-locale-view'),
);
}
/**
* {@inheritdoc}
*/
public function render(){
// locale already parsed during init (for page title)
$locale = $this->locale;
if( ! $locale || ! $locale->isValid() ){
throw new Loco_error_Exception('Invalid locale argument');
}
// language may not be "installed" but we still want to inspect available files
$api = new Loco_api_WordPressTranslations;
$installed = $api->isInstalled($locale);
$tag = (string) $locale;
$package = new Loco_package_Locale( $locale );
// Get PO files for this locale
$files = $package->findLocaleFiles();
$translations = array();
$modified = 0;
$npofiles = 0;
$nfiles = 0;
// source locale means we want to see POT instead of translations
if( 'en_US' === $tag ){
$files = $package->findTemplateFiles()->augment($files);
}
/* @var Loco_fs_File */
foreach( $files as $file ){
$nfiles++;
if( 'pot' !== $file->extension() ){
$npofiles++;
}
$modified = max( $modified, $file->modified() );
$project = $package->getProject($file);
// do similarly to Loco_admin_bundle_ViewController::createFileParams
$meta = Loco_gettext_Metadata::load($file);
$dir = new Loco_fs_LocaleDirectory( $file->dirname() );
// arguments for deep link into project
$slug = $project->getSlug();
$domain = $project->getDomain()->getName();
$bundle = $project->getBundle();
$type = strtolower( $bundle->getType() );
$args = array(
// 'locale' => $tag,
'bundle' => $bundle->getHandle(),
'domain' => $project->getId(),
'path' => $meta->getPath(false),
);
// append data required for PO table row, except use bundle data instead of locale data
$translations[$type][] = new Loco_mvc_ViewParams( array (
// bundle info
'title' => $project->getName(),
'domain' => $domain,
'short' => ! $slug || $project->isDomainDefault() ? $domain : $domain.'→'.$slug,
// file info
'meta' => $meta,
'name' => $file->basename(),
'time' => $file->modified(),
'type' => strtoupper( $file->extension() ),
'todo' => $meta->countIncomplete(),
'total' => $meta->getTotal(),
// author / system / custom / other
'store' => $dir->getTypeLabel( $dir->getTypeId() ),
// links
'view' => Loco_mvc_AdminRouter::generate( $type.'-file-view', $args ),
'info' => Loco_mvc_AdminRouter::generate( $type.'-file-info', $args ),
'edit' => Loco_mvc_AdminRouter::generate( $type.'-file-edit', $args ),
'move' => Loco_mvc_AdminRouter::generate( $type.'-file-move', $args ),
'delete' => Loco_mvc_AdminRouter::generate( $type.'-file-delete', $args ),
'copy' => Loco_mvc_AdminRouter::generate( $type.'-msginit', $args ),
) );
}
$title = __( 'Installed languages', 'loco-translate' );
$breadcrumb = new Loco_admin_Navigation;
$breadcrumb->add( $title, Loco_mvc_AdminRouter::generate('lang') );
//$breadcrumb->add( $locale->getName() );
$breadcrumb->add( $tag );
// It's unlikely that an "installed" language would have no files, but could happen if only MO on disk
if( 0 === $nfiles ){
return $this->view('admin/errors/no-locale', compact('breadcrumb','locale') );
}
// files may be available for language even if not installed (i.e. no core files on disk)
if( ! $installed || ! isset($translations['core']) && 'en_US' !== $tag ){
Loco_error_AdminNotices::warn( __('No core translation files are installed for this language','loco-translate') )
->addLink('https://codex.wordpress.org/Installing_WordPress_in_Your_Language', __('Documentation','loco-translate') );
}
// Translated type labels and "See all <type>" links
$types = array(
'core' => new Loco_mvc_ViewParams( array(
'name' => __('WordPress Core','loco-translate'),
'text' => __('See all core translations','loco-translate'),
'href' => Loco_mvc_AdminRouter::generate('core')
) ),
'theme' => new Loco_mvc_ViewParams( array(
'name' => __('Themes','loco-translate'),
'text' => __('See all themes','loco-translate'),
'href' => Loco_mvc_AdminRouter::generate('theme')
) ),
'plugin' => new Loco_mvc_ViewParams( array(
'name' => __('Plugins','loco-translate'),
'text' => __('See all plugins','loco-translate'),
'href' => Loco_mvc_AdminRouter::generate('plugin')
) ),
);
$this->set( 'locale', new Loco_mvc_ViewParams( array(
'code' => $tag,
'name' => $locale->getName(),
'attr' => 'class="'.$locale->getIcon().'" lang="'.$locale->lang.'"',
) ) );
return $this->view( 'admin/bundle/locale', compact('breadcrumb','translations','types','npofiles','modified') );
}
}

View File

@@ -0,0 +1,193 @@
<?php
/**
*
*/
class Loco_admin_bundle_SetupController extends Loco_admin_bundle_BaseController {
/**
* {@inheritdoc}
*/
public function init(){
parent::init();
$bundle = $this->getBundle();
// translators: where %s is a plugin or theme
$this->set( 'title', sprintf( __('Set up %s','loco-translate'),$bundle->getName() ) );
}
/**
* {@inheritdoc}
*/
public function getHelpTabs(){
return array (
__('Setup tab','loco-translate') => $this->viewSnippet('tab-bundle-setup'),
);
}
/**
* {@inheritdoc}
*/
public function render(){
$this->prepareNavigation()->add( __('Bundle setup','loco-translate') );
$bundle = $this->getBundle();
$action = 'setup:'.$bundle->getId();
// execute auto-configure if posted
$post = Loco_mvc_PostParams::get();
if( $post->has('auto-setup') && $this->checkNonce( 'auto-'.$action) ){
if( 0 === count($bundle) ){
$bundle->createDefault();
}
foreach( $bundle as $project ){
if( ! $project->getPot() && ( $file = $project->guessPot() ) ){
$project->setPot( $file );
}
}
// forcefully add every additional project into bundle
foreach( $bundle->invert() as $project ){
if( ! $project->getPot() && ( $file = $project->guessPot() ) ){
$project->setPot( $file );
}
$bundle[] = $project;
}
$this->saveBundle();
$bundle = $this->getBundle();
$this->set('auto', null );
}
// execute XML-based config if posted
else if( $post->has('xml-setup') && $this->checkNonce( 'xml-'.$action) ){
$bundle->clear();
$model = new Loco_config_XMLModel;
$model->loadXml( trim( $post['xml-content'] ) );
$reader = new Loco_config_BundleReader($bundle);
$reader->loadModel( $model );
$this->saveBundle();
$bundle = $this->getBundle();
$this->set('xml', null );
}
// execute JSON-based config if posted
else if( $post->has('json-setup') && $this->checkNonce( 'json-'.$action) ){
$bundle->clear();
$model = new Loco_config_ArrayModel;
$model->loadJson( trim( $post['json-content'] ) );
$reader = new Loco_config_BundleReader($bundle);
$reader->loadModel( $model );
$this->saveBundle();
$bundle = $this->getBundle();
$this->set('json', null );
}
// execute reset if posted
else if( $post->has('reset-setup') && $this->checkNonce( 'reset-'.$action) ){
$this->resetBundle();
$bundle = $this->getBundle();
}
// bundle author links
$info = $bundle->getHeaderInfo();
$this->set( 'credit', $info->getAuthorCredit() );
// render according to current configuration method (save type)
$configured = $this->get('force') or $configured = $bundle->isConfigured();
$notices = new ArrayIterator;
$this->set('notices', $notices );
// collect configuration warnings
foreach( $bundle as $project ){
$potfile = $project->getPot();
if( ! $potfile ){
$notices[] = sprintf('No translation template for the "%s" text domain', $project->getSlug() );
}
}
// if extra files found consider incomplete
if( $bundle->isTheme() || ( $bundle->isPlugin() && ! $bundle->isSingleFile() ) ){
$unknown = Loco_package_Inverter::export($bundle);
$n = 0;
foreach( $unknown as $ext => $files ){
$n += count($files);
}
if( $n ){
$notices[] = sprintf( _n("One file can't be matched to a known set of strings","%s files can't be matched to a known set of strings",$n,'loco-translate'), number_format($n) );
}
}
// display setup options if at least one option specified
$doconf = false;
// enable form to invoke auto-configuration
if( $this->get('auto') ){
$fields = new Loco_mvc_HiddenFields();
$fields->setNonce( 'auto-'.$action );
$this->set('autoFields', $fields );
$doconf = true;
}
// enable form to paste XML config
if( $this->get('xml') ){
$fields = new Loco_mvc_HiddenFields();
$fields->setNonce( 'xml-'.$action );
$this->set('xmlFields', $fields );
$doconf = true;
}
// enable form to paste JSON config (via remote lookup)
if( $this->get('json') ){
$fields = new Loco_mvc_HiddenFields( array(
'json-content' => '',
'version' => $info->Version,
) );
$fields->setNonce( 'json-'.$action );
$this->set('jsonFields', $fields );
// other information for looking up bundle via api
$this->set('vendorSlug', $bundle->getSlug() );
// remote config is done via JavaScript
$this->enqueueScript('setup');
$apiBase = apply_filters( 'loco_api_url', 'https://localise.biz/api' );
$this->set('js', new Loco_mvc_ViewParams( array(
'apiUrl' => $apiBase.'/wp/'.strtolower( $bundle->getType() ),
) ) );
$doconf = true;
}
// display configurator if configurating
if( $doconf ){
return $this->view( 'admin/bundle/setup/conf' );
}
// else set configurator links back to self with required option
// ...
if( ! $configured || ! count($bundle) ){
return $this->view( 'admin/bundle/setup/none' );
}
if( 'db' === $configured ){
// form for resetting config
$fields = new Loco_mvc_HiddenFields();
$fields->setNonce( 'reset-'.$action );
$this->set( 'reset', $fields );
return $this->view('admin/bundle/setup/saved');
}
if( 'internal' === $configured ){
return $this->view('admin/bundle/setup/core');
}
if( 'file' === $configured ){
return $this->view('admin/bundle/setup/author');
}
if( count($notices) ){
return $this->view('admin/bundle/setup/partial');
}
return $this->view('admin/bundle/setup/meta');
}
}

View File

@@ -0,0 +1,322 @@
<?php
/**
* Bundle overview.
* First tier bundle view showing resources across all projects
*/
class Loco_admin_bundle_ViewController extends Loco_admin_bundle_BaseController {
/**
* {@inheritdoc}
*/
public function init(){
parent::init();
$bundle = $this->getBundle();
$this->set('title', $bundle->getName() );
$this->enqueueStyle('bundle');
}
/**
* {@inheritdoc}
*/
public function getHelpTabs(){
return array (
__('Overview','default') => $this->viewSnippet('tab-bundle-view'),
);
}
/**
* Generate a link for a specific file resource within a project
* @return string
*/
private function getResourceLink( $page, Loco_package_Project $project, Loco_gettext_Metadata $meta, array $args = array() ){
$args['path'] = $meta->getPath(false);
return $this->getProjectLink( $page, $project, $args );
}
/**
* Generate a link for a project, but without being for a specific file
* @return string
*/
private function getProjectLink( $page, Loco_package_Project $project, array $args = array() ){
$args['bundle'] = $this->get('bundle');
$args['domain'] = $project->getId();
$route = strtolower( $this->get('type') ).'-'.$page;
return Loco_mvc_AdminRouter::generate( $route, $args );
}
/**
* Initialize view parameters for a project
* @param Loco_package_Project
* @return Loco_mvc_ViewParams
*/
private function createProjectParams( Loco_package_Project $project ){
$name = $project->getName();
$domain = $project->getDomain()->getName();
$slug = $project->getSlug();
$p = new Loco_mvc_ViewParams( array (
'id' => $project->getId(),
'name' => $name,
'slug' => $slug,
'domain' => $domain,
'short' => ! $slug || $project->isDomainDefault() ? $domain : $domain.'→'.$slug,
) );
// POT template file
$file = $project->getPot();
if( $file && $file->exists() ){
$meta = Loco_gettext_Metadata::load($file);
$p['pot'] = new Loco_mvc_ViewParams( array(
// POT info
'name' => $file->basename(),
'time' => $file->modified(),
// POT links
'info' => $this->getResourceLink('file-info', $project, $meta ),
'edit' => $this->getResourceLink('file-edit', $project, $meta ),
) );
}
// PO/MO files
$po = $project->findLocaleFiles('po');
$mo = $project->findLocaleFiles('mo');
$p['po'] = $this->createProjectPairs( $project, $po, $mo );
// also pull invalid files so everything is available to the UI
$mo = $project->findNotLocaleFiles('mo');
$po = $project->findNotLocaleFiles('po')->augment( $project->findNotLocaleFiles('pot') );
$p['_po'] = $this->createProjectPairs( $project, $po, $mo );
// always offer msginit even if we find out later we can't extract any strings
$p['nav'][] = new Loco_mvc_ViewParams( array(
'href' => $this->getProjectLink('msginit', $project ),
'name' => __('New language','loco-translate'),
'icon' => 'add',
) );
$pot = $project->getPot();
// prevent editing of POT when config prohibits
if( $project->isPotLocked() ) {
if( $pot && $pot->exists() ){
$meta = Loco_gettext_Metadata::load($pot);
$p['nav'][] = new Loco_mvc_ViewParams( array(
'href' => $this->getResourceLink('file-view', $project, $meta ),
'name' => __('View template','loco-translate'),
'icon' => 'file',
) );
}
}
// offer template editing if permitted
else if( $pot && $pot->exists() ){
$p['pot'] = $pot;
$meta = Loco_gettext_Metadata::load($pot);
$p['nav'][] = new Loco_mvc_ViewParams( array(
'href' => $this->getResourceLink('file-edit', $project, $meta ),
'name' => __('Edit template','loco-translate'),
'icon' => 'pencil',
) );
}
// else offer creation of new Template
else {
$p['nav'][] = new Loco_mvc_ViewParams( array(
'href' => $this->getProjectLink('xgettext', $project ),
'name' => __('Create template','loco-translate'),
'icon' => 'add',
) );
}
return $p;
}
/**
* Collect PO/MO pairings, ignoring any PO that is in use as a template
*/
private function createPairs( Loco_fs_FileList $po, Loco_fs_FileList $mo, Loco_fs_File $pot = null ){
$pairs = array();
/* @var $pofile Loco_fs_LocaleFile */
foreach( $po as $pofile ){
if( $pot && $pofile->equal($pot) ){
continue;
}
$pair = array( $pofile, null );
$mofile = $pofile->cloneExtension('mo');
if( $mofile->exists() ){
$pair[1] = $mofile;
}
$pairs[] = $pair;
}
/* @var $mofile Loco_fs_LocaleFile */
foreach( $mo as $mofile ){
$pofile = $mofile->cloneExtension('po');
if( $pot && $pofile->equal($pot) ){
continue;
}
if( ! $pofile->exists() ){
$pairs[] = array( null, $mofile );
}
}
return $pairs;
}
/**
* Initialize view parameters for each row representing a localized resource pair
* @return array collection of entries corresponding to available PO/MO pair.
*/
private function createProjectPairs( Loco_package_Project $project, Loco_fs_LocaleFileList $po, Loco_fs_LocaleFileList $mo ){
// populate official locale names for all found, or default to our own
if( $locales = $po->getLocales() + $mo->getLocales() ){
$api = new Loco_api_WordPressTranslations;
/* @var $locale Loco_Locale */
foreach( $locales as $tag => $locale ){
$locale->ensureName($api);
}
}
// collate as unique [PO,MO] pairs ensuring canonical template excluded
$pairs = $this->createPairs( $po, $mo, $project->getPot() );
$rows = array();
foreach( $pairs as $pair ){
// favour PO file if it exists
list( $pofile, $mofile ) = $pair;
$file = $pofile or $file = $mofile;
// establish locale, or assume invalid
$locale = null;
/* @var Loco_fs_LocaleFile $file */
if( 'pot' !== $file->extension() ){
$tag = $file->getSuffix();
if( isset($locales[$tag]) ){
$locale = $locales[$tag];
}
}
$rows[] = $this->createFileParams( $project, $file, $locale );
}
return $rows;
}
/**
* @param Loco_package_Project
* @param Loco_fs_File
* @param Loco_Locale
* @return Loco_mvc_ViewParams
*/
private function createFileParams( Loco_package_Project $project, Loco_fs_File $file, Loco_Locale $locale = null ){
// Pull Gettext meta data from cache if possible
$meta = Loco_gettext_Metadata::load($file);
$dir = new Loco_fs_LocaleDirectory( $file->dirname() );
// routing arguments
$args = array (
'path' => $meta->getPath(false),
);
// Return data required for PO table row
return new Loco_mvc_ViewParams( array (
// locale info
'lcode' => $locale ? (string) $locale : '',
'lname' => $locale ? $locale->getName() : '',
'lattr' => $locale ? 'class="'.$locale->getIcon().'" lang="'.$locale->lang.'"' : '',
// file info
'meta' => $meta,
'name' => $file->basename(),
'time' => $file->modified(),
'type' => strtoupper( $file->extension() ),
'todo' => $meta->countIncomplete(),
'total' => $meta->getTotal(),
// author / system / custom / other
'store' => $dir->getTypeLabel( $dir->getTypeId() ),
// links
'view' => $this->getProjectLink('file-view', $project, $args ),
'info' => $this->getProjectLink('file-info', $project, $args ),
'edit' => $this->getProjectLink('file-edit', $project, $args ),
'move' => $this->getProjectLink('file-move', $project, $args ),
'delete' => $this->getProjectLink('file-delete', $project, $args ),
'copy' => $this->getProjectLink('msginit', $project, $args ),
) );
}
/**
* Prepare view parameters for all projects in a bundle
* @param Loco_package_Bundle
* @return array<Loco_mvc_ViewParams>
*/
private function createBundleListing( Loco_package_Bundle $bundle ){
$projects = array();
/* @var $project Loco_package_Project */
foreach( $bundle as $project ){
$projects[] = $this->createProjectParams($project);
}
return $projects;
}
/**
* {@inheritdoc}
*/
public function render(){
$this->prepareNavigation();
$bundle = $this->getBundle();
$this->set('name', $bundle->getName() );
// bundle may not be fully configured
$configured = $bundle->isConfigured();
// Hello Dolly is an exception. don't show unless configured deliberately
if( ! $configured && 'hello.php' === $bundle->getHandle() && 'Hello Dolly' === $bundle->getName() ){
$this->set( 'redirect', Loco_mvc_AdminRouter::generate('core-view') );
return $this->view('admin/bundle/alias');
}
// Collect all configured projects
$projects = $this->createBundleListing( $bundle );
$unknown = array();
// sniff additional unknown files if bundle is a theme or directory-based plugin that's been auto-detected
if( 'file' === $configured || 'internal' === $configured ){
// presumed complete
}
else if( $bundle->isTheme() || ( $bundle->isPlugin() && ! $bundle->isSingleFile() ) ){
// TODO This needs abstracting into the Loco_package_Inverter class
$prefixes = array();
$po = new Loco_fs_LocaleFileList;
$mo = new Loco_fs_LocaleFileList;
foreach( Loco_package_Inverter::export($bundle) as $ext => $files ){
$list = 'mo' === $ext ? $mo : $po;
foreach( $files as $file ){
$file = new Loco_fs_LocaleFile($file);
$list->addLocalized( $file );
// Only look in system locations if locale is valid and domain/prefix available
$locale = $file->getLocale();
if( $locale->isValid() && ( $domain = $file->getPrefix() ) ){
$prefixes[$domain] = true;
}
}
}
// pick up given files in system locations only
foreach( $prefixes as $domain => $_bool ){
$dummy = new Loco_package_Project( $bundle, new Loco_package_TextDomain($domain), '' );
$bundle->addProject( $dummy ); // <- required to configure locations
$dummy->excludeTargetPath( $bundle->getDirectoryPath() );
$po->augment( $dummy->findLocaleFiles('po') );
$mo->augment( $dummy->findLocaleFiles('mo') );
}
// a fake project is required to disable functions that require a configured project
$dummy = new Loco_package_Project( $bundle, new Loco_package_TextDomain(''), '' );
$unknown = $this->createProjectPairs( $dummy, $po, $mo );
}
$this->set('projects', $projects );
$this->set('unknown', $unknown );
return $this->view( 'admin/bundle/view' );
}
}

View File

@@ -0,0 +1,45 @@
<?php
/**
* Base controller for global plugin configuration screens
*/
abstract class Loco_admin_config_BaseController extends Loco_mvc_AdminController {
/**
* {@inheritdoc}
*/
public function init(){
parent::init();
// navigate between config view siblings, but only if privileged user
if( current_user_can('manage_options') ){
$tabs = new Loco_admin_Navigation;
$this->set( 'tabs', $tabs );
$actions = array (
'' => __('Site options','loco-translate'),
'user' => __('User options','loco-translate'),
'version' => __('Version','loco-translate'),
);
if( loco_debugging() ){
$actions['debug'] = __('Debug','loco-translate');
}
$suffix = (string) $this->get('action');
foreach( $actions as $action => $name ){
$href = Loco_mvc_AdminRouter::generate( 'config-'.$action, $_GET );
$tabs->add( $name, $href, $action === $suffix );
}
}
}
/**
* {@inheritdoc}
*/
public function getHelpTabs(){
return array (
__('Overview','default') => $this->viewSnippet('tab-settings'),
);
}
}

View File

@@ -0,0 +1,150 @@
<?php
/**
* Plugin config check (system diagnostics)
*/
class Loco_admin_config_DebugController extends Loco_admin_config_BaseController {
/**
* {@inheritdoc}
*/
public function init(){
parent::init();
$this->set( 'title', __('Debug','loco-translate') );
}
/**
* @param string
* @return int
*/
private function memory_size( $raw ){
$bytes = wp_convert_hr_to_bytes($raw);
return Loco_mvc_FileParams::renderBytes($bytes);
}
/**
* @param string
* @return string
*/
private function rel_path( $path ){
if( is_string($path) && $path && '/' === $path[0] ){
$file = new Loco_fs_File( $path );
$path = $file->getRelativePath(ABSPATH);
}
else if( ! $path ){
$path = '(none)';
}
return $path;
}
/**
* {@inheritdoc}
*/
public function render(){
$title = __('System diagnostics','loco-translate');
$breadcrumb = new Loco_admin_Navigation;
$breadcrumb->add( $title );
// extensions that are normally enabled in PHP by default
loco_check_extension('json');
loco_check_extension('ctype');
// product versions:
$versions = new Loco_mvc_ViewParams( array (
'Loco Translate' => loco_plugin_version(),
'WordPress' => $GLOBALS['wp_version'],
'PHP' => phpversion().' ('.PHP_SAPI.')',
'Server' => isset($_SERVER['SERVER_SOFTWARE']) ? $_SERVER['SERVER_SOFTWARE'] : ( function_exists('apache_get_version') ? apache_get_version() : '' ),
) );
// we want to know about modules in case there are security mods installed known to break functionality
if( function_exists('apache_get_modules') && ( $mods = preg_grep('/^mod_/',apache_get_modules() ) ) ){
$versions['Server'] .= ' + '.implode(', ',$mods);
}
// byte code cache (currently only checking for Zend OPcache)
if( function_exists('opcache_get_configuration') && ini_get('opcache.enable') ){
$info = opcache_get_configuration();
$vers = $info['version'];
$versions[ $vers['opcache_product_name'] ] = ' '.$vers['version'];
}
// utf8 / encoding:
$encoding = new Loco_mvc_ViewParams( array (
'OK' => "\xCE\x9F\xCE\x9A",
'tick' => "\xE2\x9C\x93",
'json' => json_decode('"\\u039f\\u039a \\u2713"'),
'mbstring' => loco_check_extension('mbstring') ? "\xCE\x9F\xCE\x9A \xE2\x9C\x93" : 'No',
) );
// Sanity check mbstring.func_overload
if( 2 !== strlen("\xC2\xA3") ){
$encoding->mbstring = 'Error, disable mbstring.func_overload';
}
// PHP / env memory settings:
$memory = new Loco_mvc_PostParams( array(
'WP_MEMORY_LIMIT' => $this->memory_size( loco_constant('WP_MEMORY_LIMIT') ),
'WP_MAX_MEMORY_LIMIT' => $this->memory_size( loco_constant('WP_MAX_MEMORY_LIMIT') ),
'PHP memory_limit' => $this->memory_size( ini_get('memory_limit') ),
'PHP post_max_size' => $this->memory_size( ini_get('post_max_size') ),
//'PHP upload_max_filesize' => $this->memory_size( ini_get('upload_max_filesize') ),
'PHP max_execution_time' => (string) ini_get('max_execution_time'),
) );
// Check if raising memory limit works (wp>=4.6)
if( function_exists('wp_is_ini_value_changeable') && wp_is_ini_value_changeable('memory_limit') ){
$memory['PHP memory_limit'] .= ' (changeable)';
}
// Ajaxing:
$this->enqueueScript('debug');
$this->set( 'js', new Loco_mvc_ViewParams( array (
'nonces' => array( 'ping' => wp_create_nonce('ping') ),
) ) );
// File system access
$dir = new Loco_fs_Directory( loco_constant('LOCO_LANG_DIR') ) ;
$ctx = new Loco_fs_FileWriter( $dir );
$fsp = Loco_data_Settings::get()->fs_protect;
$fs = new Loco_mvc_PostParams( array(
'langdir' => $this->rel_path( $dir->getPath() ),
'writable' => $ctx->writable(),
'disabled' => $ctx->disabled(),
'fs_protect' => 1 === $fsp ? 'Warn' : ( $fsp ? 'Block' : 'Off' ),
) );
// Debug and error log settings
$debug = new Loco_mvc_ViewParams( array(
'WP_DEBUG' => loco_constant('WP_DEBUG') ? 'On' : 'Off',
'WP_DEBUG_LOG' => loco_constant('WP_DEBUG_LOG') ? 'On' : 'Off',
'WP_DEBUG_DISPLAY' => loco_constant('WP_DEBUG_DISPLAY') ? 'On' : 'Off',
'PHP display_errors' => ini_get('display_errors') ? 'On' : 'Off',
'PHP log_errors' => ini_get('log_errors') ? 'On' : 'Off',
'PHP error_log' => $this->rel_path( ini_get('error_log') ),
) );
/* Output buffering settings
$this->set('ob', new Loco_mvc_ViewParams( array(
'output_handler' => ini_get('output_handler'),
'zlib.output_compression' => ini_get('zlib.output_compression'),
'zlib.output_compression_level' => ini_get('zlib.output_compression_level'),
'zlib.output_handler' => ini_get('zlib.output_handler'),
) ) );*/
// alert to known system setting problems:
if( version_compare(PHP_VERSION,'7.4','<') ){
if( get_magic_quotes_gpc() ){
Loco_error_AdminNotices::add( new Loco_error_Debug('You have "magic_quotes_gpc" enabled. We recommend you disable this in PHP') );
}
if( get_magic_quotes_runtime() ){
Loco_error_AdminNotices::add( new Loco_error_Debug('You have "magic_quotes_runtime" enabled. We recommend you disable this in PHP') );
}
}
return $this->view('admin/config/debug', compact('breadcrumb','versions','encoding','memory','fs','debug') );
}
}

View File

@@ -0,0 +1,55 @@
<?php
/**
* User-level plugin preferences
*/
class Loco_admin_config_PrefsController extends Loco_admin_config_BaseController {
/**
* {@inheritdoc}
*/
public function init(){
parent::init();
$this->set( 'title', __('User options','loco-translate') );
// user preference options
$opts = Loco_data_Preferences::get();
$this->set( 'opts', $opts );
// default value for Last-Translator credit
$user = wp_get_current_user();
$name = $user->get('display_name') or $name = 'nobody';
$email = $user->get('user_email') or $email = 'nobody@localhost';
$this->set('credit', sprintf('%s <%s>', $name, $email ) );
// handle save action
$nonce = $this->setNonce('save-prefs');
try {
if( $this->checkNonce($nonce->action) ){
$post = Loco_mvc_PostParams::get();
if( $post->has('opts') ){
$opts->populate( $post->opts )->persist();
Loco_error_AdminNotices::success( __('Settings saved','loco-translate') );
}
}
}
catch( Loco_error_Exception $e ){
Loco_error_AdminNotices::add($e);
}
}
/**
* {@inheritdoc}
*/
public function render(){
$title = __('Plugin settings','loco-translate');
$breadcrumb = new Loco_admin_Navigation;
$breadcrumb->add( $title );
return $this->view('admin/config/prefs', compact('breadcrumb') );
}
}

View File

@@ -0,0 +1,84 @@
<?php
/**
* Site-wide Loco options (plugin settings)
*/
class Loco_admin_config_SettingsController extends Loco_admin_config_BaseController {
/**
* {@inheritdoc}
*/
public function init(){
parent::init();
// set current plugin options and defaults for placeholders
$opts = Loco_data_Settings::get();
$this->set( 'opts', $opts );
$this->set( 'dflt', Loco_data_Settings::create() );
// roles and capabilities
$perms = new Loco_data_Permissions;
// handle save action
$nonce = $this->setNonce('save-config');
try {
if( $this->checkNonce($nonce->action) ){
$post = Loco_mvc_PostParams::get();
if( $post->has('opts') ){
$opts->populate( $post->opts )->persist();
$perms->populate( $post->has('caps') ? $post->caps : array() );
// done update
Loco_error_AdminNotices::success( __('Settings saved','loco-translate') );
// remove saved params from session if persistent options unset
if( ! $opts['fs_persist'] ){
$session = Loco_data_Session::get();
if( isset($session['loco-fs']) ){
unset( $session['loco-fs'] );
$session->persist();
}
}
}
}
}
catch( Loco_error_Exception $e ){
Loco_error_AdminNotices::add($e);
}
$this->set('caps', $caps = new Loco_mvc_ViewParams );
// there is no distinct role for network admin, so we'll fake it for UI
if( is_multisite() ){
$caps[''] = new Loco_mvc_ViewParams( array(
'label' => __('Super Admin','default'),
'name' => 'dummy-admin-cap',
'attrs' => 'checked disabled'
) );
}
/* @var $role WP_Role */
foreach( $perms->getRoles() as $id => $role ){
$caps[$id] = new Loco_mvc_ViewParams( array(
'value' => '1',
'label' => $perms->getRoleName($id),
'name' => 'caps['.$id.'][loco_admin]',
'attrs' => $perms->isProtectedRole($role) ? 'checked disabled ' : ( $role->has_cap('loco_admin') ? 'checked ' : '' ),
) );
}
}
/**
* {@inheritdoc}
*/
public function render(){
$title = __('Plugin settings','loco-translate');
$breadcrumb = new Loco_admin_Navigation;
$breadcrumb->add( $title );
return $this->view('admin/config/settings', compact('breadcrumb') );
}
}

View File

@@ -0,0 +1,65 @@
<?php
/**
* Plugin version / upgrade screen
*/
class Loco_admin_config_VersionController extends Loco_admin_config_BaseController {
/**
* {@inheritdoc}
*/
public function init(){
parent::init();
$this->set( 'title', __('Version','loco-translate') );
}
/**
* {@inheritdoc}
*/
public function render(){
$title = __('Plugin settings','loco-translate');
$breadcrumb = new Loco_admin_Navigation;
$breadcrumb->add( $title );
// current plugin version
$version = loco_plugin_version();
// check for auto-update availability
if( $updates = get_site_transient('update_plugins') ){
$key = loco_plugin_self();
if( isset($updates->response[$key]) ){
$latest = $updates->response[$key]->new_version;
// if current version is lower than latest, prompt update
if( version_compare($version,$latest,'<') ){
$this->setUpdate($latest);
}
}
}
// notify if running a development snapshot, but only if ahead of latest stable
if( '-dev' === substr($version,-4) ){
$this->set( 'devel', true );
}
// $this->setUpdate('2.0.1-debug');
return $this->view('admin/config/version', compact('breadcrumb','version') );
}
/**
* @param string version
* @return void
*/
private function setUpdate( $version ){
$action = 'upgrade-plugin_'.loco_plugin_self();
$link = admin_url( 'update.php?action=upgrade-plugin&plugin='.rawurlencode(loco_plugin_self()) );
$this->set('update', $version );
$this->set('update_href', wp_nonce_url( $link, $action ) );
}
}

View File

@@ -0,0 +1,167 @@
<?php
/**
* Base class for a file resource belonging to a bundle
* Root > List > Bundle > Resource
*/
abstract class Loco_admin_file_BaseController extends Loco_admin_bundle_BaseController {
/**
* @var Loco_Locale
*/
private $locale;
/**
* @return Loco_Locale
*/
protected function getLocale(){
return $this->locale;
}
/**
* Check file is valid or return error
* @param Loco_fs_File
* @return string rendered error
*/
protected function getFileError( Loco_fs_File $file = null ){
// file must exist for editing
if( is_null($file) || ! $file->exists() ){
return $this->view( 'admin/errors/file-missing', array() );
}
if( $file->isDirectory() ){
$this->set('info', Loco_mvc_FileParams::create($file) );
return $this->view( 'admin/errors/file-isdir', array() );
}
// security validations
try {
Loco_gettext_Data::ext( $file );
// TODO also need to block access to files outside content directory
// this is more difficult as can symlink into and out of the tree.
}
catch( Exception $e ){
return $this->view( 'admin/errors/file-sec', array( 'reason' => $e->getMessage() ) );
}
return '';
}
/**
* {@inheritdoc}
*/
public function init(){
parent::init();
// views at this level are always related to a file
// file is permitted to be missing during this execution.
$path = $this->get('path');
if( ! $path ){
throw new Loco_error_Exception('path argument required');
}
$file = new Loco_fs_LocaleFile( $path );
$file->normalize( loco_constant('WP_CONTENT_DIR') );
$ext = strtolower( $file->extension() );
// POT file has no locale
if( 'pot' === $ext ){
$locale = null;
$localised = false;
}
// else file may have a locale suffix (unless invalid, such as "default.po")
else {
$locale = $file->getLocale();
$localised = $locale->isValid();
}
if( $localised ){
$this->locale = $locale;
$code = (string) $locale;
$this->set( 'locale', new Loco_mvc_ViewParams( array(
'code' => $code,
'lang' => $locale->lang,
'icon' => $locale->getIcon(),
'name' => $locale->ensureName( new Loco_api_WordPressTranslations ),
'href' => Loco_mvc_AdminRouter::generate('lang-view', array('locale'=>$code) ),
) ) );
}
else {
$this->set( 'locale', null );
}
$this->set('file', $file );
$this->set('filetype', strtoupper($ext) );
$this->set('title', $file->basename() );
// navigate up to root from this bundle sub view
$bundle = $this->getBundle();
$breadcrumb = Loco_admin_Navigation::createBreadcrumb( $bundle );
$this->set( 'breadcrumb', $breadcrumb );
// navigate between sub view siblings for this resource
$tabs = new Loco_admin_Navigation;
$this->set( 'tabs', $tabs );
$actions = array (
'file-edit' => __('Editor','loco-translate'),
'file-view' => __('Source','loco-translate'),
'file-info' => __('File info','loco-translate'),
'file-diff' => __('Restore','loco-translate'),
'file-move' => $localised ? __('Relocate','loco-translate') : null,
'file-delete' => __('Delete','loco-translate'),
);
$suffix = $this->get('action');
$prefix = $this->get('type');
$args = array_intersect_key($_GET,array('path'=>1,'bundle'=>1,'domain'=>1));
foreach( $actions as $action => $name ){
if( is_string($name) ){
$href = Loco_mvc_AdminRouter::generate( $prefix.'-'.$action, $args );
$tabs->add( $name, $href, $action === $suffix );
}
}
// Provide common language creation link if project scope is is valid
try {
$project = $this->getProject();
$args = array( 'bundle' => $bundle->getHandle(), 'domain' => $project->getId() );
$this->set( 'msginit', new Loco_mvc_ViewParams( array (
'href' => Loco_mvc_AdminRouter::generate( $prefix.'-msginit', $args ),
'text' => __('New language','loco-translate'),
) ) );
}
catch( Exception $e ){
}
}
/**
* {@inheritdoc}
*/
public function view( $tpl, array $args = array() ){
if( $breadcrumb = $this->get('breadcrumb') ){
// Add project name into breadcrumb if not the same as bundle name
try {
$project = $this->getProject();
if( $project->getName() !== $this->getBundle()->getName() ){
$breadcrumb->add( $project->getName() );
}
}
catch( Loco_error_Exception $e ){
// ignore missing project in breadcrumb
}
// Always add page title as final breadcrumb element
$title = $this->get('title') or $title = 'Untitled';
$breadcrumb->add( $title );
}
return parent::view( $tpl, $args );
}
}

View File

@@ -0,0 +1,113 @@
<?php
/**
* File delete function
*/
class Loco_admin_file_DeleteController extends Loco_admin_file_BaseController {
/**
* Expand single path to all files that will be deleted
* @param Loco_fs_File primary file being deleted, probably the PO
* @return array
*/
private function expandFiles( Loco_fs_File $file ){
try {
$siblings = new Loco_fs_Siblings( $file );
}
catch( InvalidArgumentException $e ){
$ext = $file->extension();
throw new Loco_error_Exception( sprintf('Refusing to delete a %s file', strtoupper($ext) ) );
}
return $siblings->expand();
}
/**
* {@inheritdoc}
*/
public function init(){
parent::init();
$file = $this->get('file');
// set up form for delete confirmation
if( $file->exists() && ! $file->isDirectory() ){
// nonce action will be specific to file for extra security
// TODO could also add file MD5 to avoid deletion after changes made.
$path = $file->getPath();
$action = 'delete:'.$path;
// set up view now in case of late failure
$fields = new Loco_mvc_HiddenFields( array() );
$fields->setNonce( $action );
$this->set( 'hidden', $fields );
// attempt delete if valid nonce posted back
if( $this->checkNonce($action) ){
$api = new Loco_api_WordPressFileSystem;
// delete dependant files first, so master still exists if others fail
$files = array_reverse( $this->expandFiles($file) );
try {
/* @var $trash Loco_fs_File */
foreach( $files as $trash ){
$api->authorizeDelete($trash);
$trash->unlink();
}
// flash message for display after redirect
try {
$n = count( $files );
Loco_data_Session::get()->flash('success', sprintf( _n('File deleted','%u files deleted',$n,'loco-translate'),$n) );
Loco_data_Session::close();
}
catch( Exception $e ){
// tolerate session failure
}
// redirect to bundle overview
$href = Loco_mvc_AdminRouter::generate( $this->get('type').'-view', array( 'bundle' => $this->get('bundle') ) );
if( wp_redirect($href) ){
exit;
}
}
catch( Loco_error_Exception $e ){
Loco_error_AdminNotices::add( $e );
}
}
}
// set page title before render sets inline title
$bundle = $this->getBundle();
$this->set('title', sprintf( __('Delete %s','loco-translate'), $file->basename() ).' &lsaquo; '.$bundle->getName() );
}
/**
* {@inheritdoc}
*/
public function render(){
$file = $this->get('file');
if( $fail = $this->getFileError($file) ){
return $fail;
}
$files = $this->expandFiles( $file );
$info = Loco_mvc_FileParams::create($file);
$this->set( 'info', $info );
$this->set( 'title', sprintf( __('Delete %s','loco-translate'), $info->name ) );
// warn about additional files that will be deleted along with this
if( $deps = array_slice($files,1) ){
$count = count($deps);
$this->set('warn', sprintf( _n( 'One dependent file will also be deleted', '%u dependent files will also be deleted', $count, 'loco-translate' ), $count ) );
$infos = array();
foreach( $deps as $depfile ){
$infos[] = Loco_mvc_FileParams::create( $depfile );
}
$this->set('deps', $infos );
}
$this->prepareFsConnect( 'delete', $this->get('path') );
$this->enqueueScript('delete');
return $this->view('admin/file/delete');
}
}

View File

@@ -0,0 +1,156 @@
<?php
/**
* File revisions and rollback
*/
class Loco_admin_file_DiffController extends Loco_admin_file_BaseController {
/**
* {@inheritdoc}
*/
public function init(){
parent::init();
$this->enqueueStyle('podiff');
$pofile = $this->get('file');
if( $pofile->exists() && ! $pofile->isDirectory() ){
$path = $pofile->getPath();
$action = 'restore:'.$path;
// set up view now in case of late failure
$fields = new Loco_mvc_HiddenFields( array() );
$fields->setNonce( $action );
$this->set( 'hidden', $fields );
// attempt rollback if valid nonce posted back with backup path
if( $this->checkNonce($action) ){
try {
$post = Loco_mvc_PostParams::get();
$api = new Loco_api_WordPressFileSystem;
// Restore
if( $path = $post->backup ){
$target = new Loco_fs_File( $path );
$target->normalize( loco_constant('WP_CONTENT_DIR') );
// parse PO. we'll need it for MO compile anyway
$source = $target->getContents();
$data = Loco_gettext_Data::fromSource( $source );
// backup current master before restoring
$backups = new Loco_fs_Revisions($pofile);
if( $num_backups = Loco_data_Settings::get()->num_backups ){
$api->authorizeCopy($pofile);
$backups->create();
}
// authorize master for file modification
$api->authorizeUpdate($pofile);
// recompile binary if it exists
$mofile = $pofile->cloneExtension('mo');
if( $mofile->exists() ){
$mofile->putContents( $data->msgfmt() );
}
// replacing source file last in case of failures
$pofile->putContents( $source );
Loco_error_AdminNotices::success( __('File restored','loco-translate') );
// prune to configured level after success
$backups->prune( $num_backups );
$backups = null;
}
// Delete
else if( $path = $post->delete ){
$target = new Loco_fs_File( $path );
$target->normalize( loco_constant('WP_CONTENT_DIR') );
$api->authorizeDelete( $target );
$target->unlink();
Loco_error_AdminNotices::success( __('File deleted','loco-translate') );
}
else {
throw new Loco_error_Exception('Nothing selected');
}
}
catch( Loco_error_Exception $e ){
Loco_error_AdminNotices::add( $e );
}
}
}
$bundle = $this->getBundle();
$this->set('title', sprintf( __('Restore %s','loco-translate'), $pofile->basename() ).' &lsaquo; '.$bundle->getName() );
}
/**
* {@inheritdoc}
*/
public function render(){
$file = $this->get('file');
if( $fail = $this->getFileError($file) ){
return $fail;
}
$info = Loco_mvc_FileParams::create($file);
$info['mtime'] = $file->modified();
$this->set( 'master', $info );
$this->set( 'title', sprintf( __('Restore %s','loco-translate'), $info->name ) );
$enabled = Loco_data_Settings::get()->num_backups;
$this->set( 'enabled', $enabled );
$files = array();
$wp_content = loco_constant('WP_CONTENT_DIR');
$paths = array( $file->getRelativePath($wp_content) );
$podate = 'pot' === $file->extension() ? 'POT-Creation-Date' : 'PO-Revision-Date';
$backups = new Loco_fs_Revisions($file);
foreach( $backups->getPaths() as $path ){
$tmp = new Loco_fs_File( $path );
$info = Loco_mvc_FileParams::create($tmp);
// time file was snapshotted is actually the time the next version was updated
// $info['mtime'] = $backups->getTimestamp($path);
// pull "real" update time, meaning when the revision was last updated as current version
try {
$head = Loco_gettext_Data::head($tmp)->getHeaders();
if( $value = $head->trimmed($podate) ){
$info['potime'] = Loco_gettext_Data::parseDate($value);
}
else {
throw new Loco_error_Exception('Backup has no '.$podate.' field');
}
}
catch( Exception $e ){
Loco_error_AdminNotices::debug( $e->getMessage() );
continue;
}
$paths[] = $tmp->getRelativePath($wp_content);
$files[] = $info;
}
// no backups = no restore
if( ! $files ){
return $this->view('admin/errors/no-backups');
}
/*/ warn if current backup settings aren't enough to restore without losing older revisions
$min = count($files) + 1;
if( $enabled < $min ){
$notice = Loco_error_AdminNotices::info('We recommend enabling more backups before restoring');
$notice->addLink( apply_filters('loco_external','https://localise.biz/wordpress/plugin/manual/settings#po'), __('Documentation','loco-translate') )
->addLink( Loco_mvc_AdminRouter::generate('config').'#loco--num-backups', __('Settings') );
}*/
// restore permissions required are create and delete on current location
$this->prepareFsConnect( 'update', $this->get('path') );
// prepare revision arguments for JavaScript
$this->set( 'js', new Loco_mvc_ViewParams( array(
'paths' => $paths,
'nonces' => array (
'diff' => wp_create_nonce('diff'),
)
) ) );
$this->enqueueScript('podiff');
return $this->view('admin/file/diff', compact('files','backups') );
}
}

View File

@@ -0,0 +1,205 @@
<?php
/**
* PO editor view
*/
class Loco_admin_file_EditController extends Loco_admin_file_BaseController {
/**
* {@inheritdoc}
*/
public function init(){
parent::init();
$this->enqueueStyle('editor');
//
$file = $this->get('file');
$bundle = $this->getBundle();
// translators: %1$s is the file name, %2$s is the bundle name
$this->set('title', sprintf( __('Editing %1$s in %2$s','loco-translate'), $file->basename(), $bundle ) );
}
/**
* {@inheritdoc}
*/
public function getHelpTabs(){
return array (
__('Overview','default') => $this->viewSnippet('tab-file-edit'),
);
}
/**
* {@inheritdoc}
*/
public function render(){
// file must exist for editing
$file = $this->get('file');
if( $fail = $this->getFileError($file) ){
return $fail;
}
// editor will be rendered
$this->enqueueScript('editor');
// Parse file data into JavaScript for editor
try {
$this->set('modified', $file->modified() );
$data = Loco_gettext_Data::load( $file );
}
catch( Exception $e ){
Loco_error_AdminNotices::add( Loco_error_Exception::convert($e) );
$data = Loco_gettext_Data::dummy();
}
$head = $data->getHeaders();
// default is to permit editing of any file
$readonly = false;
// Establish if file belongs to a configured project
try {
$bundle = $this->getBundle();
$project = $this->getProject();
}
// Fine if not, this just means sync isn't possible.
catch( Loco_error_Exception $e ){
Loco_error_AdminNotices::add( $e );
Loco_error_AdminNotices::debug( sprintf("Sync is disabled because this file doesn't relate to a known set of translations", $bundle ) );
$project = null;
}
// Establish PO/POT edit mode
if( $locale = $this->getLocale() ){
// alternative POT file may be forced by PO headers
if( $value = $head['X-Loco-Template'] ){
$potfile = new Loco_fs_File($value);
$potfile->normalize( $bundle->getDirectoryPath() );
}
// no way to get configured POT if invalid project
else if( is_null($project) ){
$potfile = null;
}
// else use project-configured template, assuming there is one
else if( $potfile = $project->getPot() ){
// Handle situation where project defines a localised file as the official template
if( $potfile->equal($file) ){
$locale = null;
$potfile = null;
}
}
if( $potfile ){
// Validate template file as long as it exists
if( $potfile->exists() ){
try {
$potdata = Loco_gettext_Data::load( $potfile );
}
catch( Exception $e ){
// translators: Where %s is the name of the invalid POT file
Loco_error_AdminNotices::warn( sprintf( __('Translation template is invalid (%s)','loco-translate'), $potfile->basename() ) );
$potfile = null;
}
if( $potfile && ! $potdata->equalSource($data) ){
Loco_error_AdminNotices::debug( sprintf( __("Translations don't match template. Run sync to update from %s",'loco-translate'), $potfile->basename() ) );
}
}
// else template doesn't exist, so sync will be done to source code
else {
// Loco_error_AdminNotices::debug( sprintf( __('Template file not found (%s)','loco-translate'), $potfile->basename() ) );
$potfile = null;
}
}
if( $locale ){
// allow PO file to dictate its own Plural-Forms
try {
$locale->setPluralFormsHeader( $head['Plural-Forms'] );
}
catch( InvalidArgumentException $e ){
// ignore invalid Plural-Forms
}
// fill in missing PO headers now locale is fully resolved
$data->localize($locale);
// If MO file will be compiled, check for library/config problems
if ( 2 !== strlen( "\xC2\xA3" ) ) {
Loco_error_AdminNotices::warn('Your mbstring configuration will result in corrupt MO files. Please ensure mbstring.func_overload is disabled');
}
}
}
// notify if template is locked (save and sync will be disabled)
if( is_null($locale) && $project && $project->isPotLocked() ){
$this->set('fsDenied', true );
$readonly = true;
}
// back end expects paths relative to wp-content
$wp_content = loco_constant('WP_CONTENT_DIR');
$this->set( 'js', new Loco_mvc_ViewParams( array(
'podata' => $data->jsonSerialize(),
'powrap' => (int) Loco_data_Settings::get()->po_width,
'multipart' => (bool) Loco_data_Settings::get()->ajax_files,
'locale' => $locale ? $locale->jsonSerialize() : null,
'potpath' => $locale && $potfile ? $potfile->getRelativePath($wp_content) : null,
'popath' => $this->get('path'),
'readonly' => $readonly,
'project' => $project ? array (
'bundle' => $bundle->getId(),
'domain' => (string) $project->getId(),
) : null,
'nonces' => $readonly ? null : array (
'save' => wp_create_nonce('save'),
'sync' => wp_create_nonce('sync'),
),
) ) );
$this->set( 'ui', new Loco_mvc_ViewParams( array(
// Translators: button for adding a new string when manually editing a POT file
'add' => _x('Add','Editor','loco-translate'),
// Translators: button for removing a string when manually editing a POT file
'del' => _x('Remove','Editor','loco-translate'),
'help' => __('Help','loco-translate'),
// Translators: Button that saves translations to disk
'save' => _x('Save','Editor','loco-translate'),
// Translators: Button that runs in-editor sync/operation
'sync' => _x('Sync','Editor','loco-translate'),
// Translators: Button that reloads current screen
'revert' => _x('Revert','Editor','loco-translate'),
// Translators: Button that toggles a translation's Fuzzy flag
'fuzzy' => _x('Fuzzy','Editor','loco-translate'),
// Translators: Button for downloading a PO, MO or POT file
'download' => _x('Download','Editor','loco-translate'),
// Translators: Placeholder text for text filter above editor
'filter' => __('Filter translations','loco-translate'),
// Translators: Button that toggles invisible characters
'invs' => _x('Toggle invisibles','Editor','loco-translate'),
// Translators: Button that toggles between "code" and regular text editing modes
'code' => _x('Toggle code view','Editor','loco-translate'),
) ) );
// Download form params
$hidden = new Loco_mvc_HiddenFields( array(
'path' => '',
'source' => '',
'route' => 'download',
'action' => 'loco_download',
) );
$this->set( 'dlFields', $hidden->setNonce('download') );
$this->set( 'dlAction', admin_url('admin-ajax.php','relative') );
// Remote file system required if file is not directly writable
$this->prepareFsConnect( 'update', $this->get('path') );
// set simpler title for breadcrumb
$this->set('title', $file->basename() );
// ok to render editor as either po or pot
$tpl = $locale ? 'po' : 'pot';
return $this->view( 'admin/file/edit-'.$tpl, array() );
}
}

View File

@@ -0,0 +1,201 @@
<?php
/**
* File info / management view.
*/
class Loco_admin_file_InfoController extends Loco_admin_file_BaseController {
/**
* {@inheritdoc}
*/
public function init(){
parent::init();
$this->enqueueStyle('fileinfo');
//
$file = $this->get('file');
$bundle = $this->getBundle();
$this->set('title', $file->basename().' &lsaquo; '.$bundle->getName() );
}
/**
* {@inheritdoc}
*/
public function getHelpTabs(){
return array (
__('Overview','default') => $this->viewSnippet('tab-file-info'),
);
}
/**
* {@inheritdoc}
*/
public function render(){
$file = $this->get('file');
$name = $file->basename();
$this->set('title', $name );
if( $fail = $this->getFileError($file) ){
return $fail;
}
// file info
$ext = strtolower( $file->extension() );
$finfo = Loco_mvc_FileParams::create( $file );
$this->set('file', $finfo );
$finfo['type'] = strtoupper($ext);
if( $file->exists() ){
$finfo['existent'] = true;
$finfo['writable'] = $file->writable();
$finfo['deletable'] = $file->deletable();
$finfo['mtime'] = $file->modified();
// Notify if file is managed by WordPress
$api = new Loco_api_WordPressFileSystem;
if( $api->isAutoUpdatable($file) ){
$finfo['autoupdate'] = true;
}
}
// location info
$dir = new Loco_fs_LocaleDirectory( $file->dirname() );
$dinfo = Loco_mvc_FileParams::create( $dir );
$this->set('dir', $dinfo );
$dinfo['type'] = $dir->getTypeId();
if( $dir->exists() && $dir->isDirectory() ){
$dinfo['existent'] = true;
$dinfo['writable'] = $dir->writable();
}
// collect note worthy problems with file headers
$debugging = loco_debugging();
$debug = array();
// get the name of the webserver for information purposes
$this->set('httpd', Loco_compat_PosixExtension::getHttpdUser() );
// unknown file template if required
$locale = null;
$project = null;
$tpl = 'admin/file/info-other';
// we should know the project the file belongs to, but permitting orphans for debugging
try {
$project = $this->getProject();
$template = $project->getPot();
$isTemplate = $template && $file->equal($template);
$this->set('isTemplate', $isTemplate );
$this->set('project', $project );
}
catch( Loco_error_Exception $e ){
$debug[] = $e->getMessage();
$isTemplate = false;
$template = null;
}
// file will be Gettext most likely
if( 'pot' === $ext || 'po' === $ext || 'mo' === $ext ){
// treat as template until locale verified
$tpl = 'admin/file/info-pot';
// don't attempt to pull locale of template file
if( 'pot' !== $ext && ! $isTemplate ){
$locale = $file->getLocale();
$code = (string) $locale;
if( $locale->isValid() ){
// find PO/MO counter parts
if( 'po' === $ext ){
$tpl = 'admin/file/info-po';
$sibling = $file->cloneExtension('mo');
}
else {
$tpl = 'admin/file/info-mo';
$sibling = $file->cloneExtension('po');
}
$info = Loco_mvc_FileParams::create($sibling);
$this->set( 'sibling', $info );
if( $sibling->exists() ){
$info['existent'] = true;
$info['writable'] = $sibling->writable();
}
}
}
// Do full parse to get stats and headers
try {
$data = Loco_gettext_Data::load($file);
$head = $data->getHeaders();
$author = $head->trimmed('Last-Translator') or $author = __('Unknown author','loco-translate');
$this->set( 'author', $author );
// date headers may not be same as file modification time (files copied to server etc..)
$podate = $head->trimmed( $locale ? 'PO-Revision-Date' : 'POT-Creation-Date' );
$potime = Loco_gettext_Data::parseDate($podate) or $potime = $file->modified();
$this->set('potime', $potime );
// access to meta stats, normally cached on listing pages
$meta = Loco_gettext_Metadata::create($file,$data);
$this->set( 'meta', $meta );
// allow PO header to specify alternative template for sync
if( $head->has('X-Loco-Template') ){
$altpot = new Loco_fs_File($head['X-Loco-Template']);
$altpot->normalize( $this->getBundle()->getDirectoryPath() );
if( $altpot->exists() && ( ! $template || ! $template->equal($altpot) ) ){
$template = $altpot;
}
}
// establish whether PO is in sync with POT
if( $template && ! $isTemplate && 'po' === $ext && $template->exists() ){
try {
$this->set('potfile', new Loco_mvc_FileParams( array(
'synced' => Loco_gettext_Data::load($template)->equalSource($data),
), $template ) );
}
catch( Exception $e ){
// ignore invalid template in this context
}
}
if( $debugging ){
// missing or invalid headers are tollerated but developers should be notified
if( $debugging && ! count($head) ){
$debug[] = __('File does not have a valid header','loco-translate');
}
// Language header sanity checks, raising developer (debug) warnings
if( $locale ){
if( $value = $head['Language'] ){
$check = (string) Loco_Locale::parse($value);
if( $check !== $code ){
$debug[]= sprintf( __('Language header is "%s" but file name contains "%s"','loco-translate'), $value, $code );
}
}
if( $value = $head['Plural-Forms'] ){
try {
$locale->setPluralFormsHeader($value);
}
catch( InvalidArgumentException $e ){
$debug[] = sprintf('Plural-Forms header is invalid, "%s"',$value);
}
}
}
// Other sanity checks
if( $project && ( $value = $head['Project-Id-Version'] ) && $value !== $project->getName() ){
$debug[] = sprintf('Project-Id-Version header is "%s" but project is "%s"', $value, $project );
}
}
// Count source text for templates only (assumed English)
if( 'admin/file/info-pot' === $tpl ){
$counter = new Loco_gettext_WordCount($data);
$this->set('words', $counter->count() );
}
}
catch( Loco_error_Exception $e ){
$this->set('error', $e->getMessage() );
$tpl = 'admin/file/info-other';
}
}
if( $debugging && $debug ){
$this->set( 'debug', new Loco_mvc_ViewParams($debug) );
}
return $this->view( $tpl );
}
}

View File

@@ -0,0 +1,185 @@
<?php
/**
* Translation set relocation tool.
* Moves PO/MO pair and all related files to a new location
*/
class Loco_admin_file_MoveController extends Loco_admin_file_BaseController {
/**
* {@inheritdoc}
*/
public function init(){
parent::init();
$file = $this->get('file');
/* @var Loco_fs_File $file */
if( $file->exists() && ! $file->isDirectory() ){
$files = new Loco_fs_Siblings($file);
// nonce action will be specific to file for extra security
$path = $file->getPath();
$action = 'move:'.$path;
// set up view now in case of late failure
$fields = new Loco_mvc_HiddenFields( array() );
$fields->setNonce( $action );
$fields['auth'] = 'move';
$fields['path'] = $this->get('path');
$this->set('hidden',$fields );
// attempt move if valid nonce posted back
while( $this->checkNonce($action) ){
// Chosen location should be valid as a posted "dest" parameter
if( ! Loco_mvc_PostParams::get()->has('dest') ){
Loco_error_AdminNotices::err('No destination posted');
break;
}
$target = new Loco_fs_LocaleFile( Loco_mvc_PostParams::get()->dest );
$ext = $target->extension();
// primary file extension should only be permitted to change between po and pot
if( $ext !== $file->extension() && 'po' !== $ext && 'pot' !== $ext ){
Loco_error_AdminNotices::err('Invalid file extension, *.po or *.pot only');
break;
}
$target->normalize( loco_constant('WP_CONTENT_DIR') );
$target_dir = $target->getParent()->getPath();
// Primary file gives template remapping, so all files are renamed with same stub.
// this can only be one of three things: (en -> en) or (foo-en -> en) or (en -> foo-en)
// suffix will then consist of file extension, plus any other stuff like backup file date.
$target_base = $target->filename();
$source_snip = strlen( $file->filename() );
// buffer all files to move to preempt write failures
$movable = array();
$api = new Loco_api_WordPressFileSystem;
foreach( $files->expand() as $source ){
$suffix = substr( $source->basename(), $source_snip ); // <- e.g. "-backup.po~"
$target = new Loco_fs_File( $target_dir.'/'.$target_base.$suffix );
// permit valid change of file extension on primary source file (po/pot)
if( $source === $files->getSource() && $target->extension() !== $ext ){
$target = $target->cloneExtension($ext);
}
if( ! $api->authorizeMove($source,$target) ) {
Loco_error_AdminNotices::err('Failed to authorize relocation of '.$source->basename() );
break 2;
}
$movable[] = array($source,$target);
}
// commit moves. If any fail we'll have separated the files, which is bad
$count = 0;
$total = count($movable);
foreach( $movable as $pair ){
try {
$pair[0]->move( $pair[1] );
$count++;
}
catch( Loco_error_Exception $e ){
Loco_error_AdminNotices::add($e);
}
}
// flash messages for display after redirect
try {
if( $count ) {
Loco_data_Session::get()->flash( 'success', sprintf( _n( 'File moved', '%u files moved', $total, 'loco-translate' ), $total ) );
}
if( $total > $count ){
$diff = $total - $count;
Loco_data_Session::get()->flash( 'error', sprintf( _n( 'One file could not be moved', '%u files could not be moved', $diff, 'loco-translate' ), $diff ) );
}
Loco_data_Session::close();
}
catch( Exception $e ){
// tolerate session failure
}
// redirect to bundle overview
$href = Loco_mvc_AdminRouter::generate( $this->get('type').'-view', array( 'bundle' => $this->get('bundle') ) );
if( wp_redirect($href) ){
exit;
}
break;
}
}
// set page title before render sets inline title
$bundle = $this->getBundle();
$this->set('title', sprintf( __('Move %s','loco-translate'), $file->basename() ).' &lsaquo; '.$bundle->getName() );
}
/**
* {@inheritdoc}
*/
public function render(){
$file = $this->get('file');
if( $fail = $this->getFileError($file) ){
return $fail;
}
// relocation requires knowing text domain and locale
try {
$project = $this->getProject();
}
catch( Loco_error_Exception $e ){
Loco_error_AdminNotices::warn($e->getMessage());
$project = null;
}
$files = new Loco_fs_Siblings($file);
$file = new Loco_fs_LocaleFile( $files->getSource() );
$locale = $file->getLocale();
// switch between canonical move and custom file path mode
$custom = is_null($project) || $this->get('custom') || 'po' !== $file->extension() || ! $locale->isValid();
// common page elements:
$this->set('files',$files->expand() );
$this->set('title', sprintf( __('Move %s','loco-translate'), $file->filename() ) );
$this->enqueueScript('move');
// set info for existing file location
$content_dir = loco_constant('WP_CONTENT_DIR');
$current = $file->getRelativePath($content_dir);
$parent = new Loco_fs_LocaleDirectory( $file->dirname() );
$typeId = $parent->getTypeId();
$this->set('current', new Loco_mvc_ViewParams(array(
'path' => $parent->getRelativePath($content_dir),
'type' => $parent->getTypeLabel($typeId),
)) );
// moving files will require deletion permission on current file location
// plus write permission on target location, but we don't know what that is yet.
$fields = $this->prepareFsConnect('move',$current);
$fields['path'] = '';
$fields['dest'] = '';
// custom file move template (POT mode)
if( $custom ){
$this->get('hidden')->offsetSet('custom','1');
$this->set('file', Loco_mvc_FileParams::create($file) );
return $this->view('admin/file/move-pot');
}
// establish valid locations for translation set, which may include current:
$filechoice = $project->initLocaleFiles($locale);
// start with current location so always first in list
$locations = array();
$locations[$typeId] = new Loco_mvc_ViewParams( array(
'label' => $parent->getTypeLabel($typeId),
'paths' => array( new Loco_mvc_ViewParams( array(
'path' => $current,
'active' => true,
) ) )
) );
/* @var Loco_fs_File $pofile */
foreach( $filechoice as $pofile ){
$relpath = $pofile->getRelativePath($content_dir);
if( $current === $relpath ){
continue;
}
// initialize location type (system, etc..)
$parent = new Loco_fs_LocaleDirectory( $pofile->dirname() );
$typeId = $parent->getTypeId();
if( ! isset($locations[$typeId]) ){
$locations[$typeId] = new Loco_mvc_ViewParams( array(
'label' => $parent->getTypeLabel($typeId),
'paths' => array(),
) );
}
$choice = new Loco_mvc_ViewParams( array(
'path' => $relpath,
) );
$locations[$typeId]['paths'][] = $choice;
}
$this->set('locations', $locations );
$this->set('advanced', $_SERVER['REQUEST_URI'].'&custom=1' );
return $this->view('admin/file/move-po');
}
}

View File

@@ -0,0 +1,101 @@
<?php
/**
* File view / source formatted view.
*/
class Loco_admin_file_ViewController extends Loco_admin_file_BaseController {
/**
* {@inheritdoc}
*/
public function init(){
parent::init();
$this->enqueueStyle('poview');
//
$file = $this->get('file');
$bundle = $this->getBundle();
$this->set( 'title', 'Source of '.$file->basename().' &lsaquo; '.$bundle->getName() );
}
/**
* {@inheritdoc}
*/
public function getHelpTabs(){
return array (
__('Overview','default') => $this->viewSnippet('tab-file-view'),
);
}
/**
* {@inheritdoc}
*/
public function render(){
// file must exist for editing
/* @var Loco_fs_File $file */
$file = $this->get('file');
$name = $file->basename();
$type = strtolower( $file->extension() );
$this->set('title', $name );
if( $fail = $this->getFileError($file) ){
return $fail;
}
// Establish if file belongs to a configured project
try {
$bundle = $this->getBundle();
$project = $this->getProject();
}
catch( Exception $e ){
$project = null;
}
// Parse data before rendering, so we know it's a valid Gettext format
try {
$this->set('modified', $file->modified() );
$data = Loco_gettext_Data::load( $file );
}
catch( Loco_error_ParseException $e ){
Loco_error_AdminNotices::add( Loco_error_Exception::convert($e) );
$data = Loco_gettext_Data::dummy();
}
$this->set( 'meta', Loco_gettext_Metadata::create($file, $data) );
// binary MO will be hex-formatted in template
if( 'mo' === $type ){
$this->set('bin', $file->getContents() );
return $this->view('admin/file/view-mo' );
}
// else is a PO or POT file
$this->enqueueScript('poview');//->enqueueScript('min/highlight');
$lines = preg_split('/(?:\\n|\\r\\n?)/', Loco_gettext_Data::ensureUtf8( $file->getContents() ) );
$this->set( 'lines', $lines );
// ajax parameters required for pulling reference sources
$this->set('js', new Loco_mvc_ViewParams( array (
'popath' => $this->get('path'),
'nonces' => array(
'fsReference' => wp_create_nonce('fsReference'),
),
'project' => $bundle ? array (
'bundle' => $bundle->getId(),
'domain' => $project ? $project->getId() : '',
) : null,
) ) );
// treat as PO if file name has locale
if( $this->getLocale() ){
return $this->view('admin/file/view-po' );
}
// else view as POT
return $this->view('admin/file/view-pot' );
}
}

View File

@@ -0,0 +1,327 @@
<?php
/**
* pre-msginit function. Prepares arguments for creating a new PO language file
*/
class Loco_admin_init_InitPoController extends Loco_admin_bundle_BaseController {
/**
* {@inheritdoc}
*/
public function init(){
parent::init();
$this->enqueueStyle('poinit');
//
$bundle = $this->getBundle();
$this->set('title', __('New language','loco-translate').' &lsaquo; '.$bundle );
}
/**
* {@inheritdoc}
*/
public function getHelpTabs(){
return array (
__('Overview','default') => $this->viewSnippet('tab-init-po'),
);
}
/**
* Sort to the left the best option for saving new translation files
* @return Loco_mvc_ViewParams
*/
private function sortPreferred( array $choices ){
usort( $choices, array(__CLASS__,'_onSortPreferred') );
$best = current( $choices );
if( $best && ! $best['disabled'] ){
return $best;
}
}
/**
* @internal
*/
public static function _onSortPreferred( Loco_mvc_ViewParams $a, Loco_mvc_ViewParams $b ){
$x = self::scoreFileChoice($a);
$y = self::scoreFileChoice($b);
return $x === $y ? 0 : ( $x > $y ? -1 : 1 );
}
/**
* Score an individual file choice for sorting preferred
* @return int
*/
private static function scoreFileChoice( Loco_mvc_ViewParams $p ){
$score = 0;
if( $p['writable'] ){
$score++;
}
if( $p['disabled'] ){
$score -= 2;
}
if( $p['systype'] ){
$score--;
}
return $score;
}
/**
* @internal
*/
public static function _onSortLocationKeys( $a, $b ){
static $order = array('custom' => 4, 'wplang' => 3, 'theme' => 2, 'plugin' => 2, 'other' => 1 );
$x = $order[$a];
$y = $order[$b];
return $x === $y ? 0 : ( $x > $y ? -1 : 1 );
}
/**
* {@inheritdoc}
*/
public function render(){
$breadcrumb = $this->prepareNavigation();
// "new" tab is confusing when no project-scope navigation
// $this->get('tabs')->add( __('New PO','loco-translate'), '', true );
// bundle mandatory, but project optional
$bundle = $this->getBundle();
try {
$project = $this->getProject();
$slug = $project->getSlug();
$domain = (string) $project->getDomain();
$subhead = sprintf( __('Initializing new translations in "%s"','loco-translate'), $slug?$slug:$domain );
}
catch( Loco_error_Exception $e ){
$project = null;
$subhead = __('Initializing new translations in unknown set','loco-translate');
}
$title = __('New language','loco-translate');
$this->set('subhead', $subhead );
// navigate up to bundle listing page
$breadcrumb->add( $title );
$this->set( 'breadcrumb', $breadcrumb );
// default locale is a placeholder
$locale = new Loco_Locale('zxx');
$content_dir = untrailingslashit( loco_constant('WP_CONTENT_DIR') );
$copying = false;
// Permit using any provided file a template instead of POT
if( $potpath = $this->get('path') ){
$potfile = new Loco_fs_LocaleFile($potpath);
$potfile->normalize( $content_dir );
if( ! $potfile->exists() ){
throw new Loco_error_Exception('Forced template argument must exist');
}
$copying = true;
// forced source could be a POT (although UI would normally prevent it)
if( $potfile->getSuffix() ){
$locale = $potfile->getLocale();
$this->set('sourceLocale', $locale );
}
}
// else project not configured. UI should prevent this by not offering msginit
else if( ! $project ){
throw new Loco_error_Exception('Cannot add new language to unconfigured set');
}
// else POT file may or may not be known, and may or may not exist
else {
$potfile = $project->getPot();
}
$locales = array();
$installed = array();
$api = new Loco_api_WordPressTranslations;
// pull installed list first, this will include en_US and any non-standard languages installed
foreach( $api->getInstalledCore() as $tag ){
$locale = Loco_Locale::parse($tag);
if( $locale->isValid() ){
$tag = (string) $tag;
// We may not have names for these, so just the language tag will show
$installed[$tag] = new Loco_mvc_ViewParams( array(
'value' => $tag,
'icon' => $locale->getIcon(),
'label' => $locale->ensureName($api),
) );
}
}
// pull the same list of "available" languages as used in WordPress settings
/* @var $locale Loco_Locale */
foreach( $api->getAvailableCore() as $tag => $locale ){
if( ! array_key_exists($tag,$installed) ){
$locales[$tag] = new Loco_mvc_ViewParams( array(
'value' => $tag,
'icon' => $locale->getIcon(),
'label' => $locale->ensureName($api),
) );
}
}
// two locale lists built for "installed" and "available" dropdowns
$this->set( 'locales', $locales );
$this->set( 'installed', $installed );
// Critical that user selects the correct save location:
if( $project ){
$filechoice = $project->initLocaleFiles( $locale );
}
// without configured project we will only allow save to same location
else {
$filechoice = new Loco_fs_FileList;
}
// show information about POT file if we are initializing from template
if( $potfile && $potfile->exists() ){
$meta = Loco_gettext_Metadata::load($potfile);
$total = $meta->getTotal();
$summary = sprintf( _n('One string found in %2$s','%s strings found in %s',$total,'loco-translate'), number_format($total), $potfile->basename() );
$this->set( 'pot', new Loco_mvc_ViewParams( array(
'name' => $potfile->basename(),
'path' => $meta->getPath(false),
) ) );
// if copying an existing PO file, we can fairly safely establish the correct prefixing
if( $copying ){
$poname = ( $prefix = $potfile->getPrefix() ) ? sprintf('%s-%s.po',$prefix,$locale) : sprintf('%s.po',$locale);
$pofile = new Loco_fs_LocaleFile( $poname );
$pofile->normalize( $potfile->dirname() );
$filechoice->add( $pofile );
}
/// else if POT is in a folder we don't know about, we may as well add to the choices
// TODO this means another utility function in project for prefixing rules on individual location
}
// else no template exists, so we prompt to extract from source
else {
$this->set( 'ext', new Loco_mvc_ViewParams( array(
'link' => Loco_mvc_AdminRouter::generate( $this->get('type').'-xgettext', $_GET ),
'text' => __('Create template','loco-translate'),
) ) );
// if forcing source extraction show brief description of source files
if( $this->get('extract') ){
// Tokenizer required for string extraction
if( ! loco_check_extension('tokenizer') ){
return $this->view('admin/errors/no-tokenizer');
}
$nfiles = count( $project->findSourceFiles() );
$summary = sprintf( _n('1 source file will be scanned for translatable strings','%s source files will be scanned for translatable strings',$nfiles,'loco-translate'), number_format_i18n($nfiles) );
}
// else prompt for template creation before continuing
else {
$this->set( 'skip', new Loco_mvc_ViewParams( array(
'link' => Loco_mvc_AdminRouter::generate( $this->get('_route'), $_GET + array( 'extract' => '1' ) ),
'text' => __('Skip template','loco-translate'),
) ) );
// POT could still be defined, it might just not exist yet
if( $potfile ){
$this->set('pot', Loco_mvc_FileParams::create($potfile) );
}
// else offer assignment of a new file
else {
$this->set( 'conf', new Loco_mvc_ViewParams( array(
'link' => Loco_mvc_AdminRouter::generate( $this->get('type').'-conf', array_intersect_key($_GET,array('bundle'=>'')) ),
'text' => __('Assign template','loco-translate'),
) ) );
}
return $this->view('admin/init/init-prompt');
}
}
$this->set( 'summary', $summary );
// group established locations into types (official, etc..)
// there is no point checking whether any of these file exist, because we don't know what language will be chosen yet.
$sortable = array();
$locations = array();
$fs_protect = Loco_data_Settings::get()->fs_protect;
$fs_failure = null;
/* @var Loco_fs_File $pofile */
foreach( $filechoice as $pofile ){
$parent = new Loco_fs_LocaleDirectory( $pofile->dirname() );
$systype = $parent->getUpdateType();
$typeId = $parent->getTypeId();
if( ! isset($locations[$typeId]) ){
$locations[$typeId] = new Loco_mvc_ViewParams( array(
'label' => $parent->getTypeLabel( $typeId ),
'paths' => array(),
) );
}
// folder may be unwritable (requiring connect to create file) or may be denied under security settings
try {
$context = $parent->getWriteContext()->authorize();
$writable = $context->writable();
$disabled = false;
}
catch( Loco_error_WriteException $e ){
$fs_failure = $e->getMessage();
$writable = false;
$disabled = true;
}
$choice = new Loco_mvc_ViewParams( array (
'checked' => '',
'writable' => $writable,
'disabled' => $disabled,
'systype' => $systype,
'parent' => Loco_mvc_FileParams::create( $parent ),
'hidden' => $pofile->getRelativePath($content_dir),
'holder' => str_replace( (string) $locale, '<span>&lt;locale&gt;</span>', $pofile->basename() ),
) );
// may need to show system file warnings
if( $systype && $fs_protect ){
$choice['syswarn'] = true;
}
$sortable[] = $choice;
$locations[$typeId]['paths'][] = $choice;
}
// display locations in runtime preference order
uksort( $locations, array(__CLASS__,'_onSortLocationKeys') );
$this->set( 'locations', $locations );
// pre-select best (safest/writable) option
if( $preferred = $this->sortPreferred( $sortable ) ){
$preferred['checked'] = 'checked';
}
// else show total lock message. probably file mods disallowed
else if( $fs_failure ){
$this->set('fsLocked', $fs_failure );
}
// hidden fields to pass through to Ajax endpoint
$this->set('hidden', new Loco_mvc_HiddenFields( array(
'action' => 'loco_json',
'route' => 'msginit',
'loco-nonce' => $this->setNonce('msginit')->value,
'type' => $bundle->getType(),
'bundle' => $bundle->getHandle(),
'domain' => $project ? $project->getId() : '',
'source' => $potpath,
) ) );
$this->set('help', new Loco_mvc_ViewParams( array(
'href' => apply_filters('loco_external','https://localise.biz/wordpress/plugin/manual/msginit'),
'text' => __("What's this?",'loco-translate'),
) ) );
// file system prompts will be handled when paths are selected (i.e. we don't have one yet)
$this->prepareFsConnect( 'create', '' );
$this->enqueueScript('poinit');
return $this->view( 'admin/init/init-po', array() );
}
}

View File

@@ -0,0 +1,151 @@
<?php
/**
* pre-xgettext function. Initializes a new PO file for a given locale
*/
class Loco_admin_init_InitPotController extends Loco_admin_bundle_BaseController {
/**
* {@inheritdoc}
*/
public function init(){
parent::init();
$this->enqueueStyle('poinit');
//
$bundle = $this->getBundle();
$this->set('title', __('New template','loco-translate').' &lsaquo; '.$bundle );
}
/**
* {@inheritdoc}
*/
public function getHelpTabs(){
return array (
__('Overview','default') => $this->viewSnippet('tab-init-pot'),
);
}
/**
* {@inheritdoc}
*/
public function render(){
$breadcrumb = $this->prepareNavigation();
// "new" tab is confusing when no project-scope navigation
// $this->get('tabs')->add( __('New POT','loco-translate'), '', true );
$bundle = $this->getBundle();
$project = $this->getProject();
$slug = $project->getSlug();
$domain = (string) $project->getDomain();
$this->set('domain', $domain );
// Tokenizer required for string extraction
if( ! loco_check_extension('tokenizer') ){
return $this->view('admin/errors/no-tokenizer');
}
// Establish default POT path whether it exists or not
$pot = $project->getPot();
while( ! $pot ){
$name = ( $slug ? $slug : $domain ).'.pot';
/* @var $dir Loco_fs_Directory */
foreach( $project->getConfiguredTargets() as $dir ){
$pot = new Loco_fs_File( $dir->getPath().'/'.$name );
break 2;
}
// unlikely to have no configured targets, but possible ... so default to standard
$pot = new Loco_fs_File( $bundle->getDirectoryPath().'/languages/'.$name );
break;
}
// POT should actually not exist at this stage. It should be edited instead.
if( $pot->exists() ){
throw new Loco_error_Exception( __('Template file already exists','loco-translate') );
}
// Bundle may deliberately lock template to avoid end-user tampering
// it makes little sense to do so when template doesn't exist, but we will honour the setting anyway.
if( $project->isPotLocked() ){
throw new Loco_error_Exception('Template is protected from updates by the bundle configuration');
}
// Just warn if POT writing will fail when saved, but still show screen
$dir = $pot->getParent();
// Avoiding full source scan until actioned, but calculate size to manage expectations
$bytes = 0;
$nfiles = 0;
$nskip = 0;
$largest = 0;
$sources = $project->findSourceFiles();
// skip files larger than configured maximum
$opts = Loco_data_Settings::get();
$max = wp_convert_hr_to_bytes( $opts->max_php_size );
/* @var $sourceFile Loco_fs_File */
foreach( $sources as $sourceFile ){
$nfiles++;
$fsize = $sourceFile->size();
$largest = max( $largest, $fsize );
if( $fsize > $max ){
$nskip += 1;
// uncomment to log which files are too large to be scanned
// Loco_error_AdminNotices::debug( sprintf('%s is %s',$sourceFile,Loco_mvc_FileParams::renderBytes($fsize)) );
}
else {
$bytes += $fsize;
}
}
$this->set( 'scan', new Loco_mvc_ViewParams( array (
'bytes' => $bytes,
'count' => $nfiles,
'skip' => $nskip,
'size' => Loco_mvc_FileParams::renderBytes($bytes),
'large' => Loco_mvc_FileParams::renderBytes($max),
'largest' => Loco_mvc_FileParams::renderBytes($largest),
) ) );
// file metadata
$this->set('pot', Loco_mvc_FileParams::create( $pot ) );
$this->set('dir', Loco_mvc_FileParams::create( $dir ) );
$title = __('New template file','loco-translate');
$subhead = sprintf( __('New translations template for "%s"','loco-translate'), $project );
$this->set('subhead', $subhead );
// navigate up to bundle listing page
$breadcrumb->add( $title );
$this->set( 'breadcrumb', $breadcrumb );
// ajax service takes the target directory path
$content_dir = loco_constant('WP_CONTENT_DIR');
$target_path = $pot->getParent()->getRelativePath($content_dir);
// hidden fields to pass through to Ajax endpoint
$this->set( 'hidden', new Loco_mvc_ViewParams( array(
'action' => 'loco_json',
'route' => 'xgettext',
'loco-nonce' => $this->setNonce('xgettext')->value,
'type' => $bundle->getType(),
'bundle' => $bundle->getHandle(),
'domain' => $project->getId(),
'path' => $target_path,
'name' => $pot->basename(),
) ) );
// File system connect required if location not writable
$relpath = $pot->getRelativePath($content_dir);
$this->prepareFsConnect('create', $relpath );
$this->enqueueScript('potinit');
return $this->view( 'admin/init/init-pot' );
}
}

View File

@@ -0,0 +1,86 @@
<?php
/**
* Common controller for listing of all bundle types
*/
abstract class Loco_admin_list_BaseController extends Loco_mvc_AdminController {
private $bundles = array();
/**
* build renderable bundle variables
* @return Loco_mvc_ViewParams
*/
protected function bundleParam( Loco_package_Bundle $bundle ){
$handle = $bundle->getHandle();
// compatibility will be 'ok', 'warn' or 'error' depending on severity
if( $default = $bundle->getDefaultProject() ){
$compat = $default->getPot() instanceof Loco_fs_File;
}
else {
$compat = false;
}
//$info = $bundle->getHeaderInfo();
return new Loco_mvc_ViewParams( array (
'id' => $bundle->getId(),
'name' => $bundle->getName(),
'dflt' => $default ? $default->getDomain() : '--',
'size' => count( $bundle ),
'save' => $bundle->isConfigured(),
'type' => $type = strtolower( $bundle->getType() ),
'view' => Loco_mvc_AdminRouter::generate( $type.'-view', array( 'bundle' => $handle ) ),
'time' => $bundle->getLastUpdated(),
) );
}
/**
* Add bundle to enabled or disabled list, depending on whether it is configured
*/
protected function addBundle( Loco_package_Bundle $bundle ){
$this->bundles[] = $this->bundleParam($bundle);
}
/**
* {@inheritdoc}
*/
public function getHelpTabs(){
return array (
__('Overview','default') => $this->viewSnippet('tab-list-bundles'),
);
}
/**
* {@inheritdoc}
*/
public function render(){
// breadcrumb is just the root
$here = new Loco_admin_Navigation( array (
new Loco_mvc_ViewParams( array( 'name' => $this->get('title') ) ),
) );
/*/ tab between the types of bundles
$types = array (
'' => __('Home','loco-translate'),
'theme' => __('Themes','loco-translate'),
'plugin' => __('Plugins','loco-translate'),
);
$current = $this->get('_route');
$tabs = new Loco_admin_Navigation;
foreach( $types as $type => $name ){
$href = Loco_mvc_AdminRouter::generate($type);
$tabs->add( $name, $href, $type === $current );
}
*/
return $this->view( 'admin/list/bundles', array (
'bundles' => $this->bundles,
'breadcrumb' => $here,
) );
}
}

View File

@@ -0,0 +1,16 @@
<?php
/**
* Dummy controller skips "core" list view, rendering the core projects directly as a single bundle.
* Route: loco-core -> loco-core-view
*/
class Loco_admin_list_CoreController extends Loco_admin_RedirectController {
/**
* {@inheritdoc}
*/
public function getLocation(){
return Loco_mvc_AdminRouter::generate('core-view');
}
}

View File

@@ -0,0 +1,109 @@
<?php
/**
* Lists all installed locales.
* WordPress decides what is "installed" based on presence of core translation files
*/
class Loco_admin_list_LocalesController extends Loco_mvc_AdminController {
/**
* {@inheritdoc}
*/
public function init(){
parent::init();
$this->enqueueStyle('locale');
}
/**
* {@inheritdoc}
*/
public function getHelpTabs(){
return array (
__('Overview','default') => $this->viewSnippet('tab-list-locales'),
);
}
/**
* {@inheritdoc}
*/
public function render(){
$this->set( 'title', __( 'Installed languages', 'loco-translate' ) );
$used = array();
$locales = array();
$api = new Loco_api_WordPressTranslations;
$active = get_locale();
// list which sites have each language as their WPLANG setting
if( $multisite = is_multisite() ){
$this->set('multisite',true);
/* @var WP_Site $site */
foreach( get_sites() as $site ){
$id = (int) $site->blog_id;
$tag = get_blog_option( $id, 'WPLANG') or $tag = 'en_US';
$name = get_blog_option( $id, 'blogname' );
$used[$tag][] = $name;
}
}
// else single site shows tick instead of site name
else {
$used[$active][] = '✓';
}
// add installed languages to file crawler
$finder = new Loco_package_Locale;
// Pull "installed" languages (including en_US)
foreach( $api->getInstalledCore() as $tag ){
$locale = Loco_Locale::parse($tag);
if( $locale->isValid() ){
$tag = (string) $locale;
$finder->addLocale($locale);
$args = array( 'locale' => $tag );
$locales[$tag] = new Loco_mvc_ViewParams( array(
'nfiles' => 0,
'time' => 0,
'lcode' => $tag,
'lname' => $locale->ensureName($api),
'lattr' => 'class="'.$locale->getIcon().'" lang="'.$locale->lang.'"',
'href' => Loco_mvc_AdminRouter::generate('lang-view',$args),
'used' => isset($used[$tag]) ? implode( ', ', $used[$tag] ) : ( $multisite ? '--' : '' ),
'active' => $active === $tag,
) );
}
}
$this->set('locales', $locales );
// Count up unique PO files
foreach( $finder->findLocaleFiles() as $file ){
if( preg_match('/(?:^|-)([_a-zA-Z]+).po$/', $file->basename(), $r ) ){
$locale = Loco_Locale::parse($r[1]);
if( $locale->isValid() ){
$tag = (string) $locale;
$locales[$tag]['nfiles']++;
$locales[$tag]['time'] = max( $locales[$tag]['time'], $file->modified() );
}
}
}
// POT files are in en_US locale
$tag = 'en_US';
foreach( $finder->findTemplateFiles() as $file ){
$locales[$tag]['nfiles']++;
$locales[$tag]['time'] = max( $locales[$tag]['time'], $file->modified() );
}
return $this->view( 'admin/list/locales' );
}
}

View File

@@ -0,0 +1,31 @@
<?php
/**
* List all bundles of type "plugin"
* Route: loco-plugin
*/
class Loco_admin_list_PluginsController extends Loco_admin_list_BaseController {
public function render(){
$this->set( 'type', 'plugin' );
$this->set( 'title', __( 'Translate plugins', 'loco-translate' ) );
foreach( Loco_package_Plugin::get_plugins() as $handle => $data ){
try {
$bundle = Loco_package_Plugin::create( $handle );
$this->addBundle($bundle);
}
// @codeCoverageIgnoreStart
catch( Exception $e ){
$bundle = new Loco_package_Plugin( $handle, $handle );
$this->addBundle( $bundle );
}
// @codeCoverageIgnoreEnd
}
return parent::render();
}
}

View File

@@ -0,0 +1,26 @@
<?php
/**
* List all bundles of type "theme"
* Route: loco-theme
*/
class Loco_admin_list_ThemesController extends Loco_admin_list_BaseController {
public function render(){
$this->set('type', 'theme' );
$this->set('title', __( 'Translate themes', 'loco-translate' ) );
/* @var $theme WP_Theme */
foreach( wp_get_themes() as $theme ){
$bundle = Loco_package_Theme::create( $theme->get_stylesheet() );
$this->addBundle( $bundle );
}
return parent::render();
}
}

View File

@@ -0,0 +1,58 @@
<?php
/**
* Ajax "diff" route, for rendering PO/POT file diffs
*/
class Loco_ajax_DiffController extends Loco_mvc_AjaxController {
/**
* {@inheritdoc}
*/
public function render(){
$post = $this->validate();
// require x2 valid files for diffing
if( ! $post->lhs || ! $post->rhs ){
throw new InvalidArgumentException('Path parameters required');
}
$dir = loco_constant('WP_CONTENT_DIR');
$lhs = new Loco_fs_File( $post->lhs ); $lhs->normalize($dir);
$rhs = new Loco_fs_File( $post->rhs ); $rhs->normalize($dir);
// avoid diffing non Gettext source files
$exts = array_flip( array( 'pot', 'pot~', 'po', 'po~' ) );
/* @var $file Loco_fs_File */
foreach( array($lhs,$rhs) as $file ){
if( ! $file->exists() ){
throw new InvalidArgumentException('File paths must exist');
}
if( ! $file->underContentDirectory() ){
throw new InvalidArgumentException('Files must be under '.basename($dir) );
}
$ext = $file->extension();
if( ! isset($exts[$ext]) ){
throw new InvalidArgumentException('Disallowed file extension');
}
}
// OK to diff files as HTML table
$renderer = new Loco_output_DiffRenderer;
$emptysrc = $renderer->_startDiff().$renderer->_endDiff();
$tablesrc = $renderer->renderFiles( $rhs, $lhs );
if( $tablesrc === $emptysrc ){
// translators: Where %s is a file name
$message = __('Revisions are identical, you can delete %s','loco-translate');
$this->set( 'error', sprintf( $message, $rhs->basename() ) );
}
else {
$this->set( 'html', $tablesrc );
}
return parent::render();
}
}

View File

@@ -0,0 +1,35 @@
<?php
/**
* Downloads a bundle configuration as XML or Json
*/
class Loco_ajax_DownloadConfController extends Loco_ajax_common_BundleController {
/**
* {@inheritdoc}
*/
public function render(){
$this->validate();
$bundle = $this->getBundle();
$file = new Loco_fs_File( $this->get('path') );
// TODO should we download axtual loco.xml file if bundle is configured from it?
//$file->normalize( $bundle->getDirectoryPath() );
//if( $file->exists() ){}
$writer = new Loco_config_BundleWriter($bundle);
switch( $file->extension() ){
case 'xml':
return $writer->toXml();
case 'json':
return json_encode( $writer->jsonSerialize() );
}
// @codeCoverageIgnoreStart
throw new Loco_error_Exception('Specify either XML or JSON file path');
}
}

View File

@@ -0,0 +1,59 @@
<?php
/**
* Ajax "download" route, for outputting raw gettext file contents.
*/
class Loco_ajax_DownloadController extends Loco_mvc_AjaxController {
/**
* {@inheritdoc}
*/
public function render(){
$post = $this->validate();
// we need a path, but it may not need to exist
$file = new Loco_fs_File( $this->get('path') );
$file->normalize( loco_constant( 'WP_CONTENT_DIR') );
$is_binary = 'mo' === strtolower( $file->extension() );
// posted source must be clean and must parse as whatever the file extension claims to be
if( $raw = $post->source ){
// compile source if target is MO
if( $is_binary ) {
$raw = Loco_gettext_Data::fromSource($raw)->msgfmt();
}
}
// else file can be output directly if it exists.
// note that files on disk will not be parsed or manipulated. they will download strictly as-is
else if( $file->exists() ){
$raw = $file->getContents();
}
/*/ else if PO exists but MO doesn't, we can compile it on the fly
else if( ! $is_binary ){
}*/
else {
throw new Loco_error_Exception('File not found and no source posted');
}
// Observe UTF-8 BOM setting
if( ! $is_binary ){
$has_bom = "\xEF\xBB\xBF" === substr($raw,0,3);
$use_bom = (bool) Loco_data_Settings::get()->po_utf8_bom;
// only alter file if valid UTF-8. Deferring detection overhead until required
if( $has_bom !== $use_bom && 'UTF-8' === mb_detect_encoding( $raw, array('UTF-8','ISO-8859-1'), true ) ){
if( $use_bom ){
$raw = "\xEF\xBB\xBF".$raw; // prepend
}
else {
$raw = substr($raw,3); // strip bom
}
}
}
return $raw;
}
}

View File

@@ -0,0 +1,151 @@
<?php
/**
* Ajax service that provides remote server authentication for file system *write* operations
*/
class Loco_ajax_FsConnectController extends Loco_mvc_AjaxController {
/**
* @var Loco_api_WordPressFileSystem
*/
private $api;
/**
* @param Loco_fs_File existing file path (must exist)
* @return bool
*/
private function authorizeDelete( Loco_fs_File $file ){
$files = new Loco_fs_Siblings($file);
// require remote authentication if at least one dependant file is not deletable directly
foreach( $files->expand() as $file ){
if( ! $this->api->authorizeDelete($file) ){
return false;
}
}
// else no dependants failed deletable test
return true;
}
/**
* @param Loco_fs_File file being moved (must exist)
* @param Loco_fs_File target path (should not exist)
* @return bool
*/
private function authorizeMove( Loco_fs_File $source, Loco_fs_File $target = null ){
return $this->api->authorizeMove($source,$target);
}
/**
* @param Loco_fs_File new file path (should not exist)
* @return bool
*/
private function authorizeCreate( Loco_fs_File $file ){
return $this->api->authorizeCreate($file);
}
/**
* @return bool
*/
private function authorizeUpdate( Loco_fs_File $file ){
if( ! $this->api->authorizeUpdate($file) ){
return false;
}
// if backups are enabled, we need to be able to create new files too (i.e. update parent directory)
if( Loco_data_Settings::get()->num_backups && ! $this->api->authorizeCopy($file) ){
return false;
}
// updating file may also recompile binary, which may or may not exist
$files = new Loco_fs_Siblings( $file );
if( $file = $files->getBinary() ){
return $this->api->authorizeSave($file);
}
// else no dependants to update
return true;
}
/**
* {@inheritdoc}
*/
public function render(){
// establish operation being authorized (create,delete,etc..)
$post = $this->validate();
$type = $post->auth;
$func = 'authorize'.ucfirst($type);
$auth = array( $this, $func );
if( ! is_callable($auth) ){
throw new Loco_error_Exception('Unexpected file operation');
}
// all auth methods require at least one file argument
$file = new Loco_fs_File( $post->path );
$base = loco_constant('WP_CONTENT_DIR');
$file->normalize($base);
$args = array($file);
// some auth methods also require a destination/target (move,copy,etc..)
if( $dest = $post->dest ){
$file = new Loco_fs_File($dest);
$file->normalize($base);
$args[] = $file;
}
// call auth method and respond with status and prompt HTML if connect required
try {
$this->api = new Loco_api_WordPressFileSystem;
if( call_user_func_array($auth,$args) ){
$this->set( 'authed', true );
$this->set( 'valid', $this->api->getOutputCredentials() );
$this->set( 'creds', $this->api->getInputCredentials() );
$this->set( 'method', $this->api->getFileSystem()->method );
$this->set( 'success', __('Connected to remote file system','loco-translate') );
// warning when writing to this location is risky (overwrites during wp update)
if( Loco_data_Settings::get()->fs_protect && $file->getUpdateType() ){
if( 'create' === $type ){
$message = __('This file may be overwritten or deleted when you update WordPress','loco-translate');
}
else if( 'delete' === $type ){
$message = __('This directory is managed by WordPress, be careful what you delete','loco-translate');
}
else if( 'move' === $type ){
$message = __('This directory is managed by WordPress. Removed files may be restored during updates','loco-translate');
}
else {
$message = __('Changes to this file may be overwritten or deleted when you update WordPress','loco-translate');
}
$this->set('warning',$message);
}
}
else if( $html = $this->api->getForm() ){
$this->set( 'authed', false );
$this->set( 'prompt', $html );
// supporting text based on file operation type explains why auth is required
if( 'create' === $type ){
$message = __('Creating this file requires permission','loco-translate');
}
else if( 'delete' === $type ){
$message = __('Deleting this file requires permission','loco-translate');
}
else if( 'move' === $type ){
$message = __('This move operation requires permission','loco-translate');
}
else {
$message = __('Saving this file requires permission','loco-translate');
}
// message is printed before default text, so needs delimiting.
$this->set('message',$message.'.');
}
else {
throw new Loco_error_Exception('Failed to get credentials form');
}
}
catch( Loco_error_WriteException $e ){
$this->set('authed', false );
$this->set('reason', $e->getMessage() );
}
return parent::render();
}
}

View File

@@ -0,0 +1,198 @@
<?php
/**
* Ajax service that returns source code for a given file system reference
* Currently this is only PHP, but could theoretically be any file type.
*/
class Loco_ajax_FsReferenceController extends Loco_ajax_common_BundleController {
/**
* @param string
* @return Loco_fs_File
*/
private function findSourceFile( $refpath ){
/*/ absolute file path means no search paths required
if( Loco_fs_File::abs($refpath) ){
$srcfile = new Loco_fs_File( $refpath );
if( $srcfile->exists() ){
return $srcfile;
}
}*/
// reference may be resolvable via referencing PO file's location
$pofile = new Loco_fs_File( $this->get('path') );
$pofile->normalize( loco_constant('WP_CONTENT_DIR') );
if( ! $pofile->exists() ){
throw new InvalidArgumentException('PO/POT file required to resolve reference');
}
$search = new Loco_gettext_SearchPaths;
$search->init($pofile);
if( $srcfile = $search->match($refpath) ){
return $srcfile;
}
// check against PO file location when no search paths or search paths failed
$srcfile = new Loco_fs_File($refpath);
$srcfile->normalize( $pofile->dirname() );
if( $srcfile->exists() ){
return $srcfile;
}
// reference may be resolvable via known project roots
try {
$bundle = $this->getBundle();
// Loco extractions will always be relative to bundle root
$srcfile = new Loco_fs_File( $refpath );
$srcfile->normalize( $bundle->getDirectoryPath() );
if( $srcfile->exists() ){
return $srcfile;
}
// check relative to parent theme root
if( $bundle->isTheme() && ( $parent = $bundle->getParent() ) ){
$srcfile = new Loco_fs_File( $refpath );
$srcfile->normalize( $parent->getDirectoryPath() );
if( $srcfile->exists() ){
return $srcfile;
}
}
// final attempt - search all project source roots
// TODO is there too large a risk of false positives? especially with files like index.php
/* @var $root Loco_fs_Directory */
/*foreach( $this->getProject($bundle)->getConfiguredSources() as $root ){
if( $root->isDirectory() ){
$srcfile = new Loco_fs_File( $refpath );
$srcfile->normalize( $root->getPath() );
if( $srcfile->exists() ){
return $srcfile;
}
}
}*/
}
catch( Loco_error_Exception $e ){
// permitted for there to be no bundle or project when viewing orphaned file
}
throw new Loco_error_Exception( sprintf('Failed to find source file matching "%s"',$refpath) );
}
/**
* {@inheritdoc}
*/
public function render(){
$post = $this->validate();
// at the very least we need a reference to examine
if( ! $post->has('ref') ){
throw new InvalidArgumentException('ref parameter required');
}
// reference must parse as <path>:<line>
$ref = $post->ref;
if( ! preg_match('/^(.+):(\\d+)$/', $ref, $r ) ){
throw new InvalidArgumentException('Invalid file reference, '.$ref );
}
// find file or fail
list( , $refpath, $refline ) = $r;
$srcfile = $this->findSourceFile($refpath);
// deny access to sensitive files
if( 'wp-config.php' === $srcfile->basename() ){
throw new InvalidArgumentException('File access disallowed');
}
// validate allowed source file types
$conf = Loco_data_Settings::get();
$ext = strtolower( $srcfile->extension() );
$allow = array_merge( array('php','js'), $conf->php_alias, $conf->jsx_alias );
if( ! in_array($ext,$allow,true) ){
throw new InvalidArgumentException('File extension disallowed, '.$ext );
}
// get file type from registered file extensions:
$type = $conf->ext2type( $ext );
$this->set('type', $type );
$this->set('line', (int) $refline );
$this->set('path', $srcfile->getRelativePath( loco_constant('WP_CONTENT_DIR') ) );
// source code will be HTML-tokenized into multiple lines
$code = array();
// observe the same size limits for source highlighting as for string extraction as tokenizing will use the same amount of juice
$maxbytes = wp_convert_hr_to_bytes( $conf->max_php_size );
// tokenizers require gettext utilities, easiest just to ping the extraction library
if( ! class_exists('Loco_gettext_Extraction',true) ){
throw new RuntimeException('Failed to load tokenizers'); // @codeCoverageIgnore
}
// PHP is the most likely format.
if( 'php' === $type && ( $srcfile->size() <= $maxbytes ) && loco_check_extension('tokenizer') ) {
$tokens = new LocoPHPTokens( token_get_all( $srcfile->getContents() ) );
}
else if( 'js' === $type ){
$tokens = new LocoJsTokens( $srcfile->getContents() );
}
else {
$tokens = null;
}
// highlighting on back end because tokenizer provides more control than highlight.js
if( $tokens instanceof LocoTokensInterface ){
$thisline = 1;
while( $tok = $tokens->advance() ){
if( is_array($tok) ){
// line numbers added in PHP 5.2.2 - WordPress minimum is 5.2.4
list( $t, $str, $startline ) = $tok;
$clss = token_name($t);
// tokens can span multiple lines (whitespace/html/comments)
$lines = preg_split('/\\R/', $str );
}
else {
// scalar symbol will always start on the line that the previous token ended on
$clss = 'T_NONE';
$lines = array( $tok );
$startline = $thisline;
}
// token can span multiple lines, so include only bytes on required line[s]
foreach( $lines as $i => $line ){
$thisline = $startline + $i;
$html = '<code class="'.$clss.'">'.htmlentities($line,ENT_COMPAT,'UTF-8').'</code>';
// append highlighted token to current line
$j = $thisline - 1;
if( isset($code[$j]) ){
$code[$j] .= $html;
}
else {
$code[$j] = $html;
}
}
}
}
// permit limited other file types, but without back end highlighting
else if( 'js' === $type || 'twig' === $type || 'php' === $type ){
foreach( preg_split( '/\\R/u', $srcfile->getContents() ) as $line ){
$code[] = '<code>'.htmlentities($line,ENT_COMPAT,'UTF-8').'</code>';
}
}
else {
throw new Loco_error_Exception( sprintf('%s source view not supported', $type) ); // @codeCoverageIgnore
}
if( ! isset($code[$refline-1]) ){
throw new Loco_error_Exception( sprintf('Line %u not in source file', $refline) );
}
$this->set( 'code', $code );
return parent::render();
}
}

View File

@@ -0,0 +1,132 @@
<?php
/**
* Ajax "msginit" route, for initializing new translation files
*/
class Loco_ajax_MsginitController extends Loco_ajax_common_BundleController {
/**
* @return Loco_Locale
*/
private function getLocale(){
if( $this->get('use-selector') ){
$tag = $this->get('select-locale');
}
else {
$tag = $this->get('custom-locale');
}
$locale = Loco_Locale::parse($tag);
if( ! $locale->isValid() ){
throw new Loco_error_LocaleException('Invalid locale');
}
return $locale;
}
/**
* {@inheritdoc}
*/
public function render(){
$post = $this->validate();
$bundle = $this->getBundle();
$project = $this->getProject( $bundle );
$domain = (string) $project->getDomain();
$locale = $this->getLocale();
$suffix = (string) $locale;
// The front end posts a template path, so we must replace the actual locale code
$base = loco_constant('WP_CONTENT_DIR');
$path = $post->path[ $post['select-path'] ];
// The request_filesystem_credentials function will try to access the "path" field later
$_POST['path'] = $path;
$pofile = new Loco_fs_LocaleFile( $path );
if( $suffix !== $pofile->getSuffix() ){
$pofile = $pofile->cloneLocale( $locale );
if( $suffix !== $pofile->getSuffix() ){
throw new Loco_error_Exception('Failed to suffix file path with locale code');
}
}
// target PO should not exist yet
$pofile->normalize( $base );
$api = new Loco_api_WordPressFileSystem;
$api->authorizeCreate( $pofile );
// Target MO probably doesn't exist, but we don't want to overwrite it without asking
$mofile = $pofile->cloneExtension('mo');
if( $mofile->exists() ){
throw new Loco_error_Exception( __('MO file exists for this language already. Delete it first','loco-translate') );
}
/*/ Same for JSON file, but WordPress >= only 5
$jsfile = function_exists('wp_set_script_translations') ? $pofile->cloneExtension('json') : null;
if( $jsfile && $jsfile->exists() ){
throw new Loco_error_Exception( __('JSON file exists for this language already. Delete it first','loco-translate') );
}*/
// Permit forcing of any parsable file as strings template
if( $source = $post->source ){
$potfile = new Loco_fs_File( $source );
$potfile->normalize( $base );
$data = Loco_gettext_Data::load($potfile);
// Remove target strings when copying PO
if( $post->strip ){
$data->strip();
}
}
// else parse POT file if project defines one that exists
else if( ( $potfile = $project->getPot() ) && $potfile->exists() ){
$data = Loco_gettext_Data::load($potfile);
}
// else extract directly from source code, assuming domain passed though from front end
else {
$extr = new Loco_gettext_Extraction( $bundle );
$data = $extr->addProject($project)->includeMeta()->getTemplate($domain);
$potfile = null;
}
// Let template define Project-Id-Version, else set header to current project name
$headers = array();
$vers = $data->getHeaders()->{'Project-Id-Version'};
if( ! $vers || 'PACKAGE VERSION' === $vers ){
$headers['Project-Id-Version'] = $project->getName();
}
// relative path from bundle root to the template/source this file was created from
if( $potfile && $post->link ){
$headers['X-Loco-Template'] = $potfile->getRelativePath( $bundle->getDirectoryPath() );
}
$data->localize( $locale, $headers );
$posize = $pofile->putContents( $data->msgcat() );
$mosize = $mofile->putContents( $data->msgfmt() );
//$jssize = $jsfile && ( $sub = $data->splitJs() ) ? $jsfile->putContents($data->jedize($domain,$sub)) : 0;
// set debug response data
$this->set( 'debug', array (
'poname' => $pofile->basename(),
'posize' => $posize,
'mosize' => $mosize,
//'jssize' => $jssize,
'source' => $potfile ? $potfile->basename() : '',
) );
// push recent items on file creation
// TODO push project and locale file
Loco_data_RecentItems::get()->pushBundle( $bundle )->persist();
// front end will redirect to the editor
$type = strtolower( $this->get('type') );
$this->set( 'redirect', Loco_mvc_AdminRouter::generate( sprintf('%s-file-edit',$type), array (
'path' => $pofile->getRelativePath($base),
'bundle' => $bundle->getHandle(),
'domain' => $project->getId(),
) ) );
return parent::render();
}
}

View File

@@ -0,0 +1,28 @@
<?php
/**
* Ajax "ping" route, for testing Ajax responses are working.
*/
class Loco_ajax_PingController extends Loco_mvc_AjaxController {
/**
* {@inheritdoc}
*/
public function render(){
$post = $this->validate();
// echo back bytes posted
if( $post->has('echo') ){
$this->set( 'ping', $post['echo'] );
}
// else just send pong
else {
$this->set( 'ping', 'pong' );
}
// always send tick symbol to check json serializing of unicode
$this->set( 'utf8', "\xE2\x9C\x93" );
return parent::render();
}
}

View File

@@ -0,0 +1,165 @@
<?php
/**
* Ajax "save" route, for saving editor contents to disk
*/
class Loco_ajax_SaveController extends Loco_ajax_common_BundleController {
/**
* {@inheritdoc}
*/
public function render(){
$post = $this->validate();
// path parameter must not be empty
$path = $post->path;
if( ! $path ){
throw new InvalidArgumentException('Path parameter required');
}
// locale must be posted to indicate whether PO or POT
$locale = $post->locale;
if( is_null($locale) ){
throw new InvalidArgumentException('Locale parameter required');
}
$pofile = new Loco_fs_LocaleFile( $path );
$pofile->normalize( loco_constant('WP_CONTENT_DIR') );
$poexists = $pofile->exists();
// ensure we only deal with PO/POT source files.
// posting of MO file paths is permitted when PO is missing, but we're about to fix that
$ext = $pofile->extension();
if( 'mo' === $ext ){
$pofile = $pofile->cloneExtension('po');
}
else if( 'pot' === $ext ){
$locale = '';
}
else if( 'po' !== $ext ){
throw new Loco_error_Exception('Invalid file path');
}
// force the use of remote file system when configured from front end
$api = new Loco_api_WordPressFileSystem;
// data posted may be either 'multipart/form-data' (recommended for large files)
if( isset($_FILES['po']) ){
$data = Loco_gettext_Data::fromSource( Loco_data_Upload::src('po') );
}
// else 'application/x-www-form-urlencoded' by default
else {
$data = Loco_gettext_Data::fromSource( $post->data );
}
// WordPress-ize some headers that differ from JavaScript libs
if( $compile = (bool) $locale ){
$head = $data->getHeaders();
$head['Language'] = strtr( $locale, '-', '_' );
}
// backup existing file before overwriting, but still allow if backups fails
$num_backups = Loco_data_Settings::get()->num_backups;
if( $num_backups && $poexists ){
try {
$api->authorizeCopy( $pofile );
$backups = new Loco_fs_Revisions( $pofile );
$backups->create();
$backups->prune($num_backups);
}
catch( Exception $e ){
Loco_error_AdminNotices::debug( $e->getMessage() );
$message = __('Failed to create backup file in "%s". Check file permissions or disable backups','loco-translate');
Loco_error_AdminNotices::warn( sprintf( $message, $pofile->getParent()->basename() ) );
}
}
// commit file directly to disk
$api->authorizeSave( $pofile );
$bytes = $pofile->putContents( $data->msgcat() );
$mtime = $pofile->modified();
// add bundle to recent items on file creation
try {
$bundle = $this->getBundle();
Loco_data_RecentItems::get()->pushBundle( $bundle )->persist();
}
catch( Exception $e ){
// editor permitted to save files not in a bundle, so catching failures
$bundle = null;
}
// start success data with bytes written and timestamp
$this->set('locale', $locale );
$this->set('pobytes', $bytes );
$this->set('poname', $pofile->basename() );
$this->set('modified', $mtime);
$this->set('datetime', Loco_mvc_ViewParams::date_i18n($mtime) );
// Compile MO and JSON files unless saving template
if( $compile ){
try {
$mofile = $pofile->cloneExtension('mo');
$api->authorizeSave( $mofile );
$bytes = $mofile->putContents( $data->msgfmt() );
$this->set( 'mobytes', $bytes );
Loco_error_AdminNotices::success( __('PO file saved and MO file compiled','loco-translate') );
}
catch( Exception $e ){
Loco_error_AdminNotices::debug( $e->getMessage() );
Loco_error_AdminNotices::warn( __('PO file saved, but MO file compilation failed','loco-translate') );
$this->set( 'mobytes', 0 );
// prevent further compilation if MO failed
$compile = false;
}
}
else {
Loco_error_AdminNotices::success( __('POT file saved','loco-translate') );
}
/*/ Compile JSON translations for WordPress >= 5
if( $compile && $bundle && function_exists('wp_set_script_translations') ){
$bytes = 0;
try {
list($domain) = Loco_package_Project::splitId( $this->get('domain') );
// hash file reference according to WordPress logic (see load_script_textdomain)
$base = $pofile->dirname().'/'.$pofile->filename();
foreach( $data->exportRefs('\\.jsx?') as $ref => $messages ){
if( '.min.js' === substr($ref,-7) ) {
$ref = substr($ref,0,-7).'.js';
}
// filter similarly to WP's `load_script_textdomain_relative_path` which is called from `load_script_textdomain`
$ref = apply_filters( 'loco_script_relative_path', $ref, $domain );
// referenced file must exist in bundle, or will never be loaded and so not require a .json file
$file = new Loco_fs_File( $bundle->getDirectoryPath().'/'.$ref );
if( $file->exists() && ! $file->isDirectory() ){
$file = new Loco_fs_File( $base.'-'.md5($ref).'.json' );
$api->authorizeSave( $file );
$bytes += $file->putContents( $data->jedize($domain,$messages) );
}
else {
Loco_error_AdminNotices::warn( sprintf('%s not found in bundle',$ref) );
}
}
// single JSON file containing all .js ref from this file
if( $messages = $data->splitJs() ){
$file = $pofile->cloneExtension('json');
$api->authorizeSave( $file );
$bytes = $file->putContents( $data->jedize($domain,$messages) );
}
}
catch( Exception $e ){
Loco_error_AdminNotices::debug( $e->getMessage() );
Loco_error_AdminNotices::warn( __('JSON compilation failed','loco-translate') );
}
$this->set( 'jsbytes', $bytes );
}*/
return parent::render();
}
}

View File

@@ -0,0 +1,78 @@
<?php
/**
* Ajax "sync" route.
* Performs the basic in-editor sync function from the old 1.x version.
*/
class Loco_ajax_SyncController extends Loco_mvc_AjaxController {
/**
* {@inheritdoc}
*/
public function render(){
$post = $this->validate();
$bundle = Loco_package_Bundle::fromId( $post->bundle );
$project = $bundle->getProjectById( $post->domain );
if( ! $project instanceof Loco_package_Project ){
throw new Loco_error_Exception('No such project '.$post->domain);
}
$file = new Loco_fs_File( $post->path );
$base = loco_constant('WP_CONTENT_DIR');
$file->normalize( $base );
// POT file always synced with source code (even if a PO being used as POT)
if( 'pot' === $post->type ){
$potfile = null;
}
// allow post data to force a template file path
else if( $path = $post->sync ){
$potfile = new Loco_fs_File($path);
$potfile->normalize( $base );
}
// else use project-configured template if one is defined
else {
$potfile = $project->getPot();
}
// sync with POT if it exists
if( $potfile && $potfile->exists() ){
$this->set('pot', $potfile->basename() );
try {
$data = Loco_gettext_Data::load($potfile);
}
catch( Exception $e ){
// translators: Where %s is the name of the invalid POT file
throw new Loco_error_ParseException( sprintf( __('Translation template is invalid (%s)','loco-translate'), $potfile->basename() ) );
}
}
// else sync with source code
else {
$this->set('pot', '' );
$domain = (string) $project->getDomain();
$extr = new Loco_gettext_Extraction($bundle);
$extr->addProject($project);
// bail if any files were skipped
if( $list = $extr->getSkipped() ){
$n = count($list);
$maximum = Loco_mvc_FileParams::renderBytes( wp_convert_hr_to_bytes( Loco_data_Settings::get()->max_php_size ) );
$largest = Loco_mvc_FileParams::renderBytes( $extr->getMaxPhpSize() );
// Translators: Where %2$s is the maximum size of a file that will be included and %3$s is the largest encountered
$text = _n('One file has been skipped because it\'s %3$s. (Max is %2$s). Check all strings are present before saving.','%s files over %2$s have been skipped. (Largest is %3$s). Check all strings are present before saving.',$n,'loco-translate');
$text = sprintf( $text, number_format($n), $maximum, $largest );
// not failing, just warning. Nothing will be saved until user saves editor state
Loco_error_AdminNotices::warn( $text );
}
// OK to return available strings
$data = $extr->includeMeta()->getTemplate($domain);
}
$this->set( 'po', $data->jsonSerialize() );
return parent::render();
}
}

View File

@@ -0,0 +1,81 @@
<?php
/**
* Ajax "xgettext" route, for initializing new template file from source code
*/
class Loco_ajax_XgettextController extends Loco_ajax_common_BundleController {
/**
* {@inheritdoc}
*/
public function render(){
$this->validate();
$bundle = $this->getBundle();
$project = $this->getProject( $bundle );
// target location may not be next to POT file at all
$base = loco_constant('WP_CONTENT_DIR');
$target = new Loco_fs_Directory( $this->get('path') );
$target->normalize( $base );
if( $target->exists() && ! $target->isDirectory() ){
throw new Loco_error_Exception('Target is not a directory');
}
// basename should be posted from front end
$name = $this->get('name');
if( ! $name ){
throw new Loco_error_Exception('Front end did not post $name');
}
// POT file shouldn't exist currently
$potfile = new Loco_fs_File( $target.'/'.$name );
$api = new Loco_api_WordPressFileSystem;
$api->authorizeCreate($potfile);
// Do extraction and grab only given domain's strings
$ext = new Loco_gettext_Extraction( $bundle );
$domain = $project->getDomain()->getName();
$data = $ext->addProject($project)->includeMeta()->getTemplate( $domain );
// additional headers to set in new POT file
$head = $data->getHeaders();
$head['Project-Id-Version'] = $project->getName();
// write POT file to disk returning byte length
$potsize = $potfile->putContents( $data->msgcat(true) );
// set response data for debugging
if( loco_debugging() ){
$this->set( 'debug', array (
'potname' => $potfile->basename(),
'potsize' => $potsize,
'total' => $ext->getTotal(),
) );
}
// push recent items on file creation
// TODO push project and locale file
Loco_data_RecentItems::get()->pushBundle( $bundle )->persist();
// put flash message into session to be displayed on redirected page
try {
Loco_data_Session::get()->flash('success', __('Template file created','loco-translate') );
Loco_data_Session::close();
}
catch( Exception $e ){
Loco_error_AdminNotices::debug( $e->getMessage() );
}
// redirect front end to bundle view. Discourages manual editing of template
$type = strtolower( $bundle->getType() );
$href = Loco_mvc_AdminRouter::generate( sprintf('%s-view',$type), array(
'bundle' => $bundle->getHandle(),
) );
$hash = '#loco-'.$project->getId();
$this->set( 'redirect', $href.$hash );
return parent::render();
}
}

View File

@@ -0,0 +1,39 @@
<?php
/**
* Common functions for all Ajax actions that operate on a bundle
*/
abstract class Loco_ajax_common_BundleController extends Loco_mvc_AjaxController {
/**
* @return Loco_package_Bundle
*/
protected function getBundle(){
if( $id = $this->get('bundle') ){
// type may be passed as separate argument
if( $type = $this->get('type') ){
return Loco_package_Bundle::createType( $type, $id );
}
// else embedded in standalone bundle identifier
// TODO standardize this across all Ajax end points
return Loco_package_Bundle::fromId($id);
}
// else may have type embedded in bundle
throw new Loco_error_Exception('No bundle identifier posted');
}
/**
* @param Loco_package_Bundle
* @return Loco_package_Project
*/
protected function getProject( Loco_package_Bundle $bundle ){
$project = $bundle->getProjectById( $this->get('domain') );
if( ! $project ){
throw new Loco_error_Exception('Failed to find translation project');
}
return $project;
}
}

Some files were not shown because too many files have changed in this diff Show More