first commit
2341
wp-content/plugins/loco-translate/languages/loco-translate.pot
Normal file
5
wp-content/plugins/loco-translate/lib/compiled/README.md
Normal 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 7.2.24 compatibility in WordPress.
|
||||
370
wp-content/plugins/loco-translate/lib/compiled/gettext.php
Normal file
@@ -0,0 +1,370 @@
|
||||
<?php
|
||||
/**
|
||||
* Downgraded for PHP 7.2 compatibility. Do not edit.
|
||||
* @noinspection ALL
|
||||
*/
|
||||
interface LocoArrayInterface extends ArrayAccess, Iterator, Countable, JsonSerializable { }
|
||||
class LocoHeaders extends ArrayIterator implements LocoArrayInterface {
|
||||
private /*array*/ $map = [];
|
||||
public function __construct(array $raw = [] ){ if( $raw ){ $keys = array_keys( $raw ); $this->map = array_combine( array_map( 'strtolower', $keys ), $keys ); parent::__construct($raw); } }
|
||||
public function normalize( string $k ):?string { $k = strtolower($k); return array_key_exists($k,$this->map) ? $this->map[$k] : null; }
|
||||
public function add($key, $val ):self { $this->offsetSet( $key, $val ); return $this; }
|
||||
public function __toString():string { $pairs = []; foreach( $this as $key => $val ){ $pairs[] = $key.': '.$val; } return implode("\n", $pairs ); }
|
||||
public function trimmed( string $prop ):string { return trim( $this->__get($prop) ); }
|
||||
public function has( string $key):bool { return array_key_exists( strtolower($key), $this->map ); }
|
||||
public function __get( string $key ){ return $this->offsetGet( $key ); }
|
||||
public function __set( string $key, /*mixed*/ $val ):void { $this->offsetSet( $key, $val ); }
|
||||
#[ReturnTypeWillChange]
|
||||
public function offsetExists( /*mixed*/ $key ):bool { return $this->has($key); }
|
||||
#[ReturnTypeWillChange]
|
||||
public function offsetGet( /*mixed*/ $key ) { $k = $this->normalize($key); if( is_null($k) ){ return ''; } return parent::offsetGet($k); }
|
||||
#[ReturnTypeWillChange]
|
||||
public function offsetSet( /*mixed*/ $key, /*mixed*/ $value ):void { $k = strtolower($key); if( isset($this->map[$k]) && $key !== $this->map[$k] ){ parent::offsetUnset( $this->map[$k] ); } $this->map[$k] = $key; parent::offsetSet( $key, $value ); }
|
||||
#[ReturnTypeWillChange]
|
||||
public function offsetUnset( /*mixed*/ $key ):void { $k = strtolower($key); if( isset($this->map[$k]) ){ parent::offsetUnset( $this->map[$k] ); unset( $this->map[$k] ); } }
|
||||
#[ReturnTypeWillChange]
|
||||
public function jsonSerialize():array { return $this->getArrayCopy(); } }
|
||||
function loco_normalize_charset( string $cs ):string { if( preg_match('/^UTF-?(8|16-?(LE|BE)?)$/i',$cs,$r,PREG_UNMATCHED_AS_NULL) ){ return '8' === $r[1] ? 'UTF-8' : 'UTF-16'.$r[2]; } try { return mb_preferred_mime_name($cs); } catch( ValueError $e ){ try { if( preg_match('/^csISO(\\w+)/i',$cs,$r) || preg_match('/^(\\w+)8$/',$cs,$r) ){ return mb_preferred_mime_name($r[1]); } throw $e; } catch( ValueError $e ){ throw new InvalidArgumentException('Unsupported character encoding: '.$cs ); } } }
|
||||
class LocoPoHeaders extends LocoHeaders {
|
||||
private /*string*/ $cs = null;
|
||||
public function getCharset():string { $cs = $this->cs; if( is_null($cs) ){ $cs = ''; $raw = $this->offsetGet('content-type'); if( $raw && preg_match('!\\bcharset[= ]+([-\\w]+)!',$raw,$r) ){ try { $cs = loco_normalize_charset($r[1]); } catch( InvalidArgumentException $e ){ } catch( Throwable $e ){ trigger_error( $e->getMessage(), E_USER_NOTICE ); } } $this->cs = $cs; } return $cs; }
|
||||
public function setCharset( string $to ):string { $to = loco_normalize_charset($to); $from = $this->getCharset(); $this->cs = $to; $this['Content-Type'] = 'text/plain; charset='.$to; if( '' !== $from && $from !== $to ){ foreach( $this as $key => $val ){ $this[$key] = mb_convert_encoding($val,$to,$from); } } return $to; }
|
||||
public static function fromMsgstr( string $str ):LocoPoHeaders { $headers = new LocoPoHeaders; $key = ''; foreach( preg_split('/[\\r\\n]+/',$str) as $line ){ $i = strpos($line,':'); if( is_int($i) ){ $key = trim( substr($line,0,$i), " \t" ); $headers->offsetSet( $key, ltrim( substr($line,++$i)," \t" ) ); } else if( '' !== $key ){ $headers->offsetSet( $key, $headers->offsetGet($key)."\n".$line ); } } $cs = $headers->getCharset(); if( '' !== $cs && 'UTF-8' !== $cs && 'UTF-8' !== mb_detect_encoding($str,['UTF-8',$cs],true) ){ foreach( $headers as $key => $val ){ $headers[$key] = mb_convert_encoding($val,'UTF-8',[$cs]); } } return $headers; }
|
||||
public static function fromSource( string $raw ):LocoPoHeaders { $po = new LocoPoParser($raw); $po->parse(0); return $po->getHeader(); } }
|
||||
function loco_convert_utf8( string $str, string $enc, bool $strict ):string { if( '' === $enc || 'UTF-8' === $enc || 'US-ASCII' === $enc ){ if( false === preg_match('//u',$str) ){ if( $strict ){ $e = new Loco_error_ParseException( $enc ? 'Invalid '.$enc.' encoding' : 'Unknown character encoding' ); if( preg_match('/^(?:[\\x00-\\x7F]|[\\xC0-\\xDF][\\x80-\\xBF]|[\\xE0-\\xEF][\\x80-\\xBF]{2}|[\\xF0-\\xFF][\\x80-\\xBF]{3})*/',$str,$r) && $str !== $r[0] ){ $e->setOffsetContext( strlen($r[0]), $str ); } throw $e; } $str = loco_fix_utf8($str); } } else if( 'ISO-8859-1' === $enc ) { $str = mb_convert_encoding( $str, 'UTF-8', 'Windows-1252' ); } else { $str = mb_convert_encoding( $str, 'UTF-8', $enc ); } return $str; }
|
||||
function loco_fix_utf8( string $str ):string { $fix = ''; while( is_string($str) && '' !== $str ){ if( preg_match('/^(?:[\\x00-\\x7F]|[\\xC0-\\xDF][\\x80-\\xBF]|[\\xE0-\\xEF][\\x80-\\xBF]{2}|[\\xF0-\\xFF][\\x80-\\xBF]{3})+/',$str,$r) ){ $fix .= $r[0]; $str = substr($str, strlen($r[0]) ); } else { $fix.= mb_convert_encoding( $str[0], 'UTF-8', 'Windows-1252' ); $str = substr($str,1); } } return loco_convert_utf8($fix,'',true); }
|
||||
abstract class LocoGettextParser {
|
||||
private /*LocoPoHeaders*/ $head = null;
|
||||
private /*string*/ $cs = '';
|
||||
abstract public function parse( int $limit = -1 ):array;
|
||||
protected function setHeader( LocoPoHeaders $head ):LocoPoHeaders { $this->head = $head; $cs = $head->getCharset(); if( '' !== $cs ){ if( '' === $this->cs ){ $this->setCharset($cs); } } return $head; }
|
||||
public function getHeader():?LocoPoHeaders { return $this->head; }
|
||||
protected function setCharset( string $cs ):void { $this->cs = $cs; }
|
||||
protected function getCharset():?string { return $this->cs; }
|
||||
protected function str( string $str ):string { if( '' !== $str ){ $str = loco_convert_utf8($str,$this->cs,false); } return $str; }
|
||||
protected function initMsgKey( string $key ):array { $r = explode("\4",$key); $value = [ 'source' => array_pop($r), 'target' => '', ]; if( isset($r[0]) ){ $value['context'] = $r[0]; } return $value; } }
|
||||
function loco_remove_bom( string $s, &$c ):string { $bom = substr($s,0,2); if( "\xFF\xFE" === $bom ){ $c = 'UTF-16LE'; return substr($s,2); } if( "\xFE\xFF" === $bom ){ $c = 'UTF-16BE'; return substr($s,2); } if( "\xEF\xBB" === $bom && "\xBF" === $s[2] ){ $c = 'UTF-8'; return substr($s,3); } $c = ''; return $s; }
|
||||
function loco_parse_reference_id( string $refs, &$_id ):string { if( false === ( $n = strpos($refs,'loco:') ) ){ $_id = ''; return $refs; } $_id = substr($refs, $n+5, 24 ); $refs = substr_replace( $refs, '', $n, 29 ); return trim( $refs ); }
|
||||
class LocoPoParser extends LocoGettextParser implements Iterator {
|
||||
private /*array*/ $lines = [];
|
||||
private /*int*/ $i;
|
||||
private /*int*/ $k;
|
||||
private /*array*/ $m;
|
||||
public function __construct( string $src ){ if( '' !== $src ){ $src = loco_remove_bom($src,$cs); if( $cs && 'UTF-8' !== $cs ){ $src = mb_convert_encoding( $src, 'UTF-8', $cs ); $cs = 'UTF-8'; } if( 'UTF-8' === $cs ){ $this->setCharset('UTF-8'); } $this->lines = preg_split('/(\\r\\n?|\\n)/', $src ); } }
|
||||
#[ReturnTypeWillChange]
|
||||
public function rewind():void { $this->i = -1; $this->k = -1; $this->next(); }
|
||||
#[ReturnTypeWillChange]
|
||||
public function valid():bool { return is_int($this->i); }
|
||||
#[ReturnTypeWillChange]
|
||||
public function key():?int { return $this->k; }
|
||||
#[ReturnTypeWillChange]
|
||||
public function current():?array { return $this->m; }
|
||||
#[ReturnTypeWillChange]
|
||||
public function next():void { $valid = false; $entry = [ '#' => [], 'id' => [null], 'str' => [null] ]; $i = $this->i; while( array_key_exists(++$i,$this->lines) ){ $line = $this->lines[$i]; try { if( '' === $line ){ if( $valid ){ break; } continue; } $c = $line[0]; if( '#' === $c ){ if( $valid ){ $i--; break; } if( '#' === $line ){ continue; } $f = $line[1]; $entry['#'][$f][] = trim( substr( $line, 1+strlen($f) ), " \n\r\t"); } else if( preg_match('/^msg(id(?:_plural)?|ctxt|str(?:\\[(\\d+)])?)[ \\t]*/', $line, $r ) ){ if( isset($r[2]) ){ $key = 'str'; $idx = (int) $r[2]; } else { $key = $r[1]; $idx = 0; } if( $valid && 'str' !== $key && null !== $entry['str'][0] ){ $i--; break; } $snip = strlen($r[0]); if( '"' !== substr($line,$snip,1) ){ throw new Exception('Expected " to follow msg'.$key); } $val = ''; $line = substr($line,$snip); while( true ){ if( '"' === $line || ! substr($line,-1) === '"' ){ throw new Exception('Unterminated msg'.$key ); } $val .= substr( $line, 1, -1 ); $j = $i + 1; if( array_key_exists($j,$this->lines) && ( $line = $this->lines[$j] ) && '"' === $line[0] ){ $i = $j; } else { break; } } if( ! $valid ){ $valid = true; } if( 'id_plural' === $key ){ $key = 'id'; $idx = 1; } $entry[$key][$idx] = stripcslashes($val); } else if( preg_match('/^[ \\t]+$/',$line) ){ if( $valid ) { break; } } else if( '"' === $c ){ throw new Exception('String encountered without keyword'); } else { throw new Exception('Junk'); } } catch( Exception $e ){ } } if( $valid ){ ++$this->k; $this->i = $i; $this->m = $entry; } else { $this->i = null; $this->k = null; $this->m = null; } }
|
||||
public function parse( int $limit = -1 ):array { $this->rewind(); if( ! $this->valid() ){ throw new Loco_error_ParseException('Invalid PO file'); } $entry = $this->current(); if( '' !== $entry['id'][0] || isset($entry['ctxt']) || is_null($entry['str'][0]) ){ $head = $this->setHeader( new LocoPoHeaders ); } else { $head = $this->setHeader( LocoPoHeaders::fromMsgstr($entry['str'][0]) ); } if( 0 === $limit ){ return []; } $i = -1; $assets = []; $lk = $head['X-Loco-Lookup']; while( $this->valid() ){ $entry = $this->current(); $msgid = $entry['id'][0]; if( is_null($msgid) ){ $this->next(); continue; } if( ++$i === $limit ){ return $assets; } $asset = [ 'source' => $this->str( $msgid ), 'target' => $this->str( (string) $entry['str'][0] ), 'context' => null, ]; $prev_entry = null; if( isset($entry['ctxt']) ){ $asset['context'] = $this->str( $entry['ctxt'][0] ); } $cmt = $entry['#']; if( isset($cmt[' ']) ){ $asset['comment'] = $this->str( implode("\n", $cmt[' '] ) ); } if( isset($cmt['.']) ){ $asset['notes'] = $this->str( implode("\n", $cmt['.'] ) ); } if( isset($cmt[':']) ){ if( $refs = implode( ' ', $cmt[':'] ) ) { $refs = $this->str($refs); if( $refs = loco_parse_reference_id( $refs, $_id ) ){ $asset['refs'] = $refs; } if( $_id ){ $asset['_id'] = $_id; } } } if( isset($cmt[',']) ){ foreach( $cmt[','] as $flags ){ foreach( explode(',',$flags) as $flag ){ if( $flag = trim($flag," \t") ){ if( preg_match('/^((?:no-)?\w+)-format/', $flag, $r ) ){ $asset['format'] = $r[1]; } else if( 'fuzzy' === $flag ){ $asset['flag'] = 4; } } } } } if( isset($cmt['|']) ){ $p = new LocoPoParser(''); $p->lines = $cmt['|']; $p->setCharset( $this->getCharset() ); try { $prev_entry = $p->parse(); } catch( Loco_error_ParseException $e ){ } if( $prev_entry ){ $msgid = $prev_entry[0]['source']; if( $lk && 'text' !== $lk ){ $asset[$lk] = $asset['source']; $asset['source'] = $msgid; } else if( substr($msgid,0,5) === 'loco:' ){ $asset['_id'] = substr($msgid,5); } else { $asset['prev'] = $prev_entry; $prev_entry = null; } } } $assets[] = $asset; if( isset($entry['id'][1]) ){ $idx = 0; $pidx = count($assets) - 1; $num = max( 2, count($entry['str']) ); while( ++$idx < $num ){ $plural = [ 'source' => '', 'target' => isset($entry['str'][$idx]) ? $this->str($entry['str'][$idx]) : '', 'plural' => $idx, 'parent' => $pidx, ]; if( 1 === $idx ){ $plural['source'] = $this->str($entry['id'][1]); if( is_array($prev_entry) && isset($prev_entry[1]) ){ if( $lk && 'text' !== $lk ){ $plural[$lk] = $plural['source']; $plural['source'] = $prev_entry[1]['source']; } } } if( isset($asset['flag']) ){ $plural['flag'] = $asset['flag']; } $assets[] = $plural; } } $this->next(); } if( -1 === $i ){ throw new Loco_error_ParseException('Invalid PO file'); } else if( 0 === $i && '' === $assets[0]['source'] && '' === $assets[0]['target'] ){ throw new Loco_error_ParseException('Invalid PO file' ); } return $assets; } }
|
||||
class LocoMoParser extends LocoGettextParser {
|
||||
private /*string*/ $bin;
|
||||
private /*bool*/ $be = null;
|
||||
private /*int*/ $n = null;
|
||||
private /*int*/ $o = null;
|
||||
private /*int*/ $t = null;
|
||||
private /*int*/ $v = null;
|
||||
public function __construct( string $bin ){ $this->bin = $bin; }
|
||||
public function getAt( int $idx ) { $offset = $this->targetOffset(); $offset += ( $idx * 8 ); $len = $this->integerAt( $offset ); $idx = $this->integerAt( $offset + 4 ); $txt = $this->bytes( $idx, $len ); if( false !== strpos($txt,"\0") ){ return explode( "\0", $txt ); } return $txt; }
|
||||
public function parse( int $limit = -1 ):array { $i = -1; $r = []; $sourceOffset = $this->sourceOffset(); $targetOffset = $this->targetOffset(); $soffset = $sourceOffset; $toffset = $targetOffset; while( $soffset < $targetOffset ){ $len = $this->integerAt( $soffset ); $idx = $this->integerAt( $soffset + 4 ); $src = $this->bytes( $idx, $len ); $eot = strpos( $src, "\x04" ); if( false === $eot ){ $context = null; } else { $context = $this->str( substr($src, 0, $eot ) ); $src = substr( $src, $eot+1 ); } $sources = explode( "\0", $src, 2 ); $len = $this->integerAt( $toffset ); $idx = $this->integerAt( $toffset + 4 ); $targets = explode( "\0", $this->bytes( $idx, $len ) ); if( -1 === $i && '' === $sources[0] && is_null($context) ){ $this->setHeader( LocoPoHeaders::fromMsgstr($targets[0]) ); } if( ++$i > $limit && -1 !== $limit ){ break; } $r[$i] = [ 'source' => $this->str( $sources[0] ), 'target' => $this->str( $targets[0] ), 'context' => $context, ]; if( isset($sources[1]) ){ $p = count($r) - 1; $nforms = max( 2, count($targets) ); for( $n = 1; $n < $nforms; $n++ ){ $r[++$i] = [ 'source' => 1 === $n && isset($sources[1]) ? $this->str($sources[1]) : '', 'target' => isset($targets[$n]) ? $this->str( $targets[$n] ) : '', 'parent' => $p, 'plural' => $n, ]; } } $soffset += 8; $toffset += 8; } return $r; }
|
||||
public function isBigendian():bool { if( is_null($this->be) ){ $str = $this->words( 0, 1 ); if( "\xDE\x12\x04\x95" === $str ){ $this->be = false; } else if( "\x95\x04\x12\xDE" === $str ){ $this->be = true; } else { throw new Loco_error_ParseException('Invalid MO format'); } } return $this->be; }
|
||||
public function version():int { if( is_null($this->v) ){ $this->v = $this->integerWord(1); } return $this->v; }
|
||||
#[ReturnTypeWillChange]
|
||||
public function count():int { if( is_null($this->n) ){ $this->n = $this->integerWord(2); } return $this->n; }
|
||||
public function sourceOffset():int { if( is_null($this->o) ){ $this->o = $this->integerWord(3); } return $this->o; }
|
||||
public function targetOffset():int { if( is_null($this->t) ){ $this->t = $this->integerWord(4); } return $this->t; }
|
||||
public function getHashTable():string { $s = $this->integerWord(5); $h = $this->integerWord(6); return $this->bytes( $h, $s * 4 ); }
|
||||
private function bytes( int $offset, int $length ):string { $s = substr( $this->bin, $offset, $length ); if( strlen($s) !== $length ){ throw new Loco_error_ParseException('Failed to read '.$length.' bytes at ['.$offset.']' ); } return $s; }
|
||||
private function words( int $offset, int $length ):string { return $this->bytes( $offset * 4, $length * 4 ); }
|
||||
private function integerWord( int $offset ):int { return $this->integerAt( $offset * 4 ); }
|
||||
private function integerAt( int $offset ):int { $str = $this->bytes( $offset, 4 ); $fmt = $this->isBigendian() ? 'N' : 'V'; $arr = unpack( $fmt, $str ); if( ! isset($arr[1]) || ! is_int($arr[1]) ){ throw new Loco_error_ParseException('Failed to read integer at byte '.$offset); } return $arr[1]; } }
|
||||
class LocoJedParser extends LocoGettextParser {
|
||||
private /*array*/ $ld;
|
||||
public function __construct( array $struct ){ $this->ld = $struct; }
|
||||
public function parse( int $limit = -1 ): array { $values = []; foreach( $this->ld as $messages ){ if( ! is_array($messages) ){ throw new Loco_error_ParseException('Array expected'); } $msgid = key($messages); if( '' === $msgid ){ $this->setHeader( new LocoJedHeaders($messages['']) ); unset($messages['']); } else { $this->setHeader( new LocoJedHeaders ); } $values[] = [ 'source' => '', 'target' => $this->getHeader(), ]; $i = -1; foreach( $messages as $key => $list ){ if( ++$i === $limit ){ break; } $value = $this->initMsgKey($key); $index = count($values); foreach( $list as $j => $msgstr ){ if( ! is_string($msgstr) ){ throw new Loco_error_ParseException('msgstr must be scalar'); } $value['target'] = $msgstr; if( 0 < $j ){ $value['plural'] = $j; $value['parent'] = $index; $value['source'] = ''; } $values[] = $value; } } } return $values; } }
|
||||
class LocoJedHeaders extends LocoPoHeaders {
|
||||
public function __construct( array $raw = [] ) { foreach( ['Language'=>'lang','plural_forms'=>'Plural-Forms'] as $canonical => $alias ){ if( array_key_exists($alias,$raw) && ! array_key_exists($canonical,$raw) ){ $raw[$canonical] = $raw[$alias]; } } parent::__construct($raw); } }
|
||||
class LocoMoPhpParser extends LocoGettextParser {
|
||||
private /*array*/ $msgs;
|
||||
public function __construct( array $struct ){ $this->msgs = $struct['messages']; unset($struct['messages']); $this->setHeader( new LocoPoHeaders($struct) ); }
|
||||
public function parse( int $limit = -1 ): array { $values = [ [ 'source' => '', 'target' => $this->getHeader(), ] ]; $i = -1; foreach( $this->msgs as $key => $bin ){ if( ++$i === $limit ){ break; } $value = $this->initMsgKey($key); $index = count($values); foreach( explode("\0",$bin) as $i => $msgstr ){ $value['target'] = $msgstr; if( 0 < $i ){ $value['plural'] = $i; $value['parent'] = $index; $value['source'] = ''; } $values[] = $value; } } return $values; } }
|
||||
abstract class LocoPo {
|
||||
public static function pair( string $key, string $text, int $width = 79, string $eol = "\n", string $esc = '\\n' ):string { if( '' === $text ){ return $key.' ""'; } $text = addcslashes( $text, "\t\x0B\x0C\x07\x08\\\"" ); if( $esc ) { $text = preg_replace('/\\r\\n?|\\n/', $esc.$eol, $text, -1, $nbr ); } else { $eol = "\n"; $text = preg_replace_callback('/\\r\\n?|\\n/',[__CLASS__,'replace_br'], $text, -1, $nbr ); } if( $nbr ){ } else if( $width && $width < mb_strlen($text,'UTF-8') + strlen($key) + 3 ){ } else { return $key.' "'.$text.'"'; } $lines = [ $key.' "' ]; if( $width ){ $width -= 2; $a = '/^.{0,'.($width-1).'}[-– .,:;?!)\\]}>]/u'; $b = '/^[^-– .,:;?!)\\]}>]+/u'; foreach( explode($eol,$text) as $unwrapped ){ $length = mb_strlen( $unwrapped, 'UTF-8' ); while( $length > $width ){ if( preg_match( $a, $unwrapped, $r ) ){ $line = $r[0]; } else if( preg_match( $b, $unwrapped, $r ) ){ $line = $r[0]; } else { throw new Exception('Wrapping error'); } $lines[] = $line; $trunc = mb_strlen($line,'UTF-8'); $length -= $trunc; $unwrapped = (string) substr( $unwrapped, strlen($line) ); if( ( '' === $unwrapped && 0 !== $length ) || ( 0 === $length && '' !== $unwrapped ) ){ throw new Exception('Truncation error'); } } if( 0 !== $length ){ $lines[] = $unwrapped; } } } else { foreach( explode($eol,$text) as $unwrapped ){ $lines[] = $unwrapped; } } return implode('"'.$eol.'"',$lines).'"'; }
|
||||
private static function replace_br( array $r ):string { return addcslashes($r[0],"\r\n")."\n"; }
|
||||
public static function refs( string $text, int $width = 76, string $eol = "\n" ):string { $text = preg_replace('/\\s+/u', ' ', $text ); if( $width ){ $text = wordwrap( $text, $width, $eol.'#: ' ); } return '#: '.$text; }
|
||||
public static function prefix( string $text, string $prefix, string $eol = "\n" ):string { return $prefix . implode($eol.$prefix, self::split($text) ); }
|
||||
public static function split( string $text ):array { $lines = preg_split('/\\R/u', $text ); if( false === $lines ){ if( false === preg_match('//u',$text) ){ $text = mb_convert_encoding( $text, 'UTF-8', 'Windows-1252' ); } $lines = preg_split('/\\r?\\n+/', $text ); } return $lines; }
|
||||
public static function trim( string $text ):string { $lines = []; $deferred = null; foreach( explode("\n",$text) as $line ){ if( '' === $line ){ continue; } if( preg_match('/^msg[a-z]+(?:\\[\\d+])? ""/',$line) ){ $deferred = $line; continue; } if( $deferred && '"' === $line[0] ){ $lines[] = $deferred; $deferred = null; } $lines[] = $line; } return implode("\n",$lines); } }
|
||||
class LocoPoIndex extends ArrayIterator {
|
||||
public function compare( LocoPoMessage $a, LocoPoMessage $b ):int { $h = $a->getHash(); if( ! isset($this[$h]) ){ return 1; } $j = $b->getHash(); if( ! isset($this[$j]) ){ return -1; } return $this[$h] > $this[$j] ? 1 : -1; } }
|
||||
class LocoPoMessage extends ArrayObject {
|
||||
public function __construct( array $r ){ $r['key'] = $r['source']; parent::__construct($r); }
|
||||
public function __get( string $prop ) { return $this->offsetExists($prop) ? $this->offsetGet($prop) : null; }
|
||||
public function isFuzzy():bool { return 4 === $this->__get('flag'); }
|
||||
public function getFormat():string { $f = $this->__get('format'); if( is_string($f) && '' !== $f ){ return $f; } return ''; }
|
||||
private function getPoFlags():array { $flags = []; foreach( array_merge( [$this], $this->__get('plurals')?:[] ) as $form ){ if( $form->isFuzzy() ){ $flags[0] = 'fuzzy'; } $f = $form->getFormat(); if( '' !== $f ){ $flags[1] = $f.'-format'; } } return array_values($flags); }
|
||||
public function getHash():string { $hash = $this->getKey(); if( $this->offsetExists('plurals') ){ foreach( $this->offsetGet('plurals') as $p ){ $hash .= "\0".$p->getKey(); break; } } return $hash; }
|
||||
public function getKey():string { $msgid = (string) $this['source']; $msgctxt = (string) $this->__get('context'); if( '' !== $msgctxt ){ if( '' === $msgid ){ $msgid = '('.$msgctxt.')'; } $msgid = $msgctxt."\4".$msgid; } return $msgid; }
|
||||
public function exportSerial( string $f = 'target' ):array { $a = [ $this[$f] ]; if( $this->offsetExists('plurals') ){ $plurals = $this->offsetGet('plurals'); if( is_array($plurals) ){ foreach( $plurals as $p ){ $a[] = $p[$f]; } } } return $a; }
|
||||
public function __toString(){ return $this->render( 79, 76 ); }
|
||||
public function render( int $width, int $ref_width, int $max_forms = 0 ):string { $s = []; try { if( $text = $this->__get('comment') ) { $s[] = LocoPo::prefix( $text, '# '); } if( $text = $this->__get('notes') ) { $s[] = LocoPo::prefix( $text, '#. '); } if( $text = $this->__get('refs') ){ $s[] = LocoPo::refs( $text, $ref_width ); } if( $texts = $this->getPoFlags() ){ $s[] = '#, '.implode(', ',$texts); } $prev = $this->__get('prev'); if( is_array($prev) && $prev ){ foreach( new LocoPoIterator($prev) as $p ){ $text = $p->render( max(0,$width-3), 0 ); $s[] = LocoPo::prefix( LocoPo::trim($text),'#| '); break; } } $text = $this->__get('context'); if( is_string($text) && '' !== $text ){ $s[] = LocoPo::pair('msgctxt', $text, $width ); } $s[] = LocoPo::pair( 'msgid', $this['source'], $width ); $target = $this['target']; $plurals = $this->__get('plurals'); if( is_array($plurals) ){ if( array_key_exists(0,$plurals) ){ $p = $plurals[0]; $s[] = LocoPo::pair('msgid_plural', $p['source'], $width ); $s[] = LocoPo::pair('msgstr[0]', $target, $width ); $i = 0; while( array_key_exists($i,$plurals) ){ $p = $plurals[$i]; if( ++$i === $max_forms ){ break; } $s[] = LocoPo::pair('msgstr['.$i.']', $p['target'], $width ); } } else if( isset($this['plural_key']) ){ $s[] = LocoPo::pair('msgid_plural', $this['plural_key'], $width ); $s[] = LocoPo::pair('msgstr[0]', $target, $width ); } else { trigger_error('Missing plural_key in zero plural export'); $s[] = LocoPo::pair('msgstr', $target, $width ); } } else { $s[] = LocoPo::pair('msgstr', $target, $width ); } } catch( Exception $e ){ trigger_error( $e->getMessage(), E_USER_WARNING ); } return implode("\n",$s)."\n"; }
|
||||
public function merge( LocoPoMessage $def, bool $translate = false ):void { if( $def->getHash() !== $this->getHash() ){ $prev = [ 'source' => '', 'target' => '' ]; $prev = $this->diff('source',$def,$prev); $prev = $this->diff('context',$def,$prev); $this['flag'] = 4; $this['prev'] = [ $prev ]; $defPlural = $def->getPlural(0); $ourPlural = $this->getPlural(0); if( $defPlural && $ourPlural ) { $ourPlural->merge($defPlural); if( $ourPlural->offsetExists('prev') ) { $this['prev'][] = $ourPlural->prev[0]+['parent'=>0,'plural'=>1]; $ourPlural->offsetUnset('prev'); } } else if( $defPlural ){ $this['plurals'] = [ clone $defPlural ]; } else if( $ourPlural ){ $this['prev'][] = $ourPlural->exportBasic() + ['parent'=>0,'plural'=>1]; $this->offsetUnset('plurals'); } } foreach( ['notes','refs','format'] as $f ){ if( $def->offsetExists($f) ){ $this->offsetSet($f,$def->offsetGet($f)); } else if( $this->offsetExists($f) ){ $this->offsetUnset($f); } } if( $translate && '' === $this['target'] && '' !== $def['target'] ){ $this['target'] = $def['target']; if( $def->offsetExists('comment') ) { $this['comment'] = $def['comment']; } if( $this->offsetExists('plurals') ){ foreach( $this['plurals'] as $i => $ourPlural ){ if( '' === $ourPlural['target'] ){ $defPlural = $def->getPlural($i); if( $defPlural ){ $ourPlural['target'] = $defPlural['target']; } } } } } }
|
||||
private function diff( string $key, LocoPoMessage $def, array $prev ):array { $old = $this->__get($key); $new = $def->__get($key); if( $new !== $old ){ $this->offsetSet($key,$new); if( is_string($old) && '' !== $old ){ $prev[$key] = $old; } } return $prev; }
|
||||
private function getPlural( int $i ):?self { if( $this->offsetExists('plurals') ){ $plurals = $this->offsetGet('plurals'); if( is_array($plurals) && array_key_exists($i,$plurals) ){ return $plurals[$i]; } } return null; }
|
||||
private function exportBasic():array { return [ 'source' => $this['source'], 'context' => $this->context, 'target' => '', ]; }
|
||||
public function export():array { $a = $this->getArrayCopy(); unset($a['key']); if( array_key_exists('plurals',$a) ){ foreach( $a['plurals'] as $i => $p ){ if( $p instanceof ArrayObject ){ $a['plurals'][$i] = $p->getArrayCopy(); } } } return $a; }
|
||||
public function strip():self { $this['target'] = ''; $plurals = $this->plurals; if( is_array($plurals) ){ foreach( $plurals as $p ){ $p->strip(); } } return $this; }
|
||||
public function translated():int { $n = 0; if( '' !== (string) $this['target'] ){ $n++; } if( $this->offsetExists('plurals') ){ foreach( $this->offsetGet('plurals') as $plural ) { if( '' !== (string) $plural['target']) { $n++; } } } return $n; } }
|
||||
class LocoPoIterator implements Iterator, Countable {
|
||||
private /*array*/ $po;
|
||||
private /*LocoPoHeaders*/ $headers = null;
|
||||
private /*int*/ $i;
|
||||
private /*int*/ $t;
|
||||
private /*int*/ $j;
|
||||
private /*int*/ $z = 0;
|
||||
private /*int*/ $w = 79;
|
||||
public function __construct( iterable $po ){ if( is_array($po) ){ $this->po = $po; } else if( $po instanceof Traversable ){ $this->po = iterator_to_array($po,false); } else { throw new InvalidArgumentException('PO data must be array or iterator'); } $this->t = count( $this->po ); if( 0 === $this->t ){ throw new InvalidArgumentException('Empty PO data'); } $h = $po[0]; if( '' !== $h['source'] || ( isset($h['context']) && '' !== $h['context'] ) || ( isset($po[1]['parent']) && 0 === $po[1]['parent'] ) ){ $this->z = -1; } }
|
||||
public function push( LocoPoMessage $p ):void { $raw = $p->export(); $plurals = $p->plurals; unset($raw['plurals']); $i = count($this->po); $this->po[$i] = $raw; $this->t++; if( is_array($plurals) ) { $j = 0; foreach( $plurals as $p ) { $raw = $p->export(); $raw['parent'] = $i; $raw['plural'] = ++$j; $this->po[] = $raw; $this->t++; } } }
|
||||
public function concat( iterable $more ):self { foreach( $more as $message ){ $this->push($message); } return $this; }
|
||||
public function __clone() { if( $this->headers ){ $this->headers = new LocoPoHeaders( $this->headers->getArrayCopy() ); } }
|
||||
#[ReturnTypeWillChange]
|
||||
public function count():int { return $this->t - ( $this->z + 1 ); }
|
||||
public function wrap( int $width ):self { if( $width > 0 ){ $this->w = max( 15, $width ); } else { $this->w = 0; } return $this; }
|
||||
#[ReturnTypeWillChange]
|
||||
public function rewind():void { $this->i = $this->z; $this->j = -1; $this->next(); }
|
||||
#[ReturnTypeWillChange]
|
||||
public function key():?int { return $this->j; }
|
||||
#[ReturnTypeWillChange]
|
||||
public function valid():bool { return is_int($this->i); }
|
||||
#[ReturnTypeWillChange]
|
||||
public function next():void { $i = $this->i; while( ++$i < $this->t ){ if( array_key_exists('parent',$this->po[$i]) ){ continue; } $this->j++; $this->i = $i; return; } $this->i = null; $this->j = null; }
|
||||
#[ReturnTypeWillChange]
|
||||
public function current():LocoPoMessage { return $this->item( $this->i ); }
|
||||
private function item( int $i ):LocoPoMessage { $po = $this->po; $parent = new LocoPoMessage( $po[$i] ); $plurals = []; $nonseq = $parent->offsetExists('child'); $j = $nonseq ? $parent['child'] : $i+1; while( isset($po[$j]['parent']) && $i === $po[$j]['parent'] ){ $plurals[] = new LocoPoMessage($po[$j++]); } if( $plurals ){ $parent['plurals'] = $plurals; } return $parent; }
|
||||
public function exportEntry( int $i ):LocoPoMessage { return $this->item( $i + ( 1-$this->z) ); }
|
||||
public function getArrayCopy():array { $po = $this->po; if( 0 === $this->z ){ $po[0]['target'] = (string) $this->getHeaders(); } return $po; }
|
||||
public function clear():void { if( 0 === $this->z ){ $this->po = [ $this->po[0] ]; $this->t = 1; } else { $this->po = []; $this->t = 0; } }
|
||||
public function getHeaders():LocoPoHeaders { if( is_null($this->headers) ){ $header = $this->po[0]; if( 0 === $this->z ){ $value = $header['target']; if( is_string($value) ){ $this->headers = LocoPoHeaders::fromMsgstr($value); } else if( $value instanceof LocoPoHeaders ){ $this->headers = $value; } else if( is_array($value) ){ $this->headers = new LocoPoHeaders($value); } } else { $this->headers = new LocoPoHeaders; } } return $this->headers; }
|
||||
public function setHeaders( LocoPoHeaders $head ):self { $this->headers = $head; if( 0 === $this->z ){ $this->po[0]['target'] = null; } return $this; }
|
||||
public function initPo():self { if( 0 === $this->z ){ unset( $this->po[0]['flag'] ); } return $this; }
|
||||
public function initPot():self { if( 0 === $this->z ){ $this->po[0]['flag'] = 4; } return $this; }
|
||||
public function strip():self { $po = $this->po; $i = count($po); $z = $this->z; while( --$i > $z ){ $po[$i]['target'] = ''; } $this->po = $po; return $this; }
|
||||
public function __toString():string { try { return $this->render(); } catch( Exception $e ){ trigger_error( $e->getMessage(), E_USER_WARNING ); return ''; } }
|
||||
public function render( ?callable $sorter = null ):string { $width = $this->w; $ref_width = max( 0, $width - 3 ); $h = $this->exportHeader(); $msg = new LocoPoMessage( $h ); $s = $msg->render( $width, $ref_width ); if( $sorter ){ $msgs = []; foreach( $this as $msg ){ $msgs[] = $msg; } usort( $msgs, $sorter ); } else { $msgs = $this; } $h = $this->getHeaders()->offsetGet('Plural-Forms'); if( is_string($h) && preg_match('/nplurals\\s*=\\s*(\\d)/',$h,$r) ){ $max_pl = (int) $r[1]; } else { $max_pl = 0; } foreach( $msgs as $msg ){ $s .= "\n".$msg->render( $width, $ref_width, $max_pl ); } return $s; }
|
||||
public function exportJed():array { $head = $this->getHeaders(); $a = [ '' => [ 'domain' => $head['domain'], 'lang' => $head['language'], 'plural-forms' => $head['plural-forms'], ] ]; foreach( $this as $message ){ if( $message->translated() ){ $a[ $message->getKey() ] = $message->exportSerial(); } } return $a; }
|
||||
private function exportHeader():array { if( 0 === $this->z ){ $h = $this->po[0]; } else { $h = [ 'source' => '', 'target' => '' ]; } if( $this->headers ){ $h['target'] = (string) $this->headers; } return $h; }
|
||||
public function exportRefs( string $grep = '' ):array { $a = []; if( '' === $grep ) { $grep = '/(\\S+):\\d+/'; } else { $grep = '/(\\S*'.$grep.'):\\d+/'; } $self = get_class($this); $base = [ $this->exportHeader() ]; foreach( $this as $message ){ if( preg_match_all( $grep, (string) $message->refs, $r ) ){ foreach( $r[1] as $ref ) { if( array_key_exists($ref,$a) ){ $po = $a[$ref]; } else { $po = new $self($base); $a[$ref] = $po; } $po->push($message); } } } return $a; }
|
||||
public function splitRefs( ?array $map = null ):array { $a = []; $self = get_class($this); $base = [ $this->exportHeader() ]; if( is_array($map) ){ $grep = implode('|',array_keys($map)); } else { $grep = '[a-z]+'; } foreach( $this as $message ){ $refs = ltrim( (string) $message->refs ); if( '' !== $refs ){ if( preg_match_all('/\\S+\\.('.$grep.'):\\d+/', $refs, $r, PREG_SET_ORDER ) ){ $tmp = []; foreach( $r as $rr ) { list( $ref, $ext ) = $rr; $tmp[$ext][$ref] = true; } foreach( $tmp as $ext => $refs ){ if( is_array($map) ){ $ext = $map[$ext]; } if( array_key_exists($ext,$a) ){ $po = $a[$ext]; } else { $po = new $self($base); $a[$ext] = $po; } $message = clone $message; $message['refs'] = implode(' ',array_keys($refs) ); $po->push($message); } } } } return $a; }
|
||||
public function getHashes():array { $a = []; foreach( $this as $msg ){ $a[] = $msg->getKey(); } sort( $a, SORT_STRING ); return $a; }
|
||||
public function equalSource( LocoPoIterator $that ):bool { return $this->getHashes() === $that->getHashes(); }
|
||||
public function equal( LocoPoIterator $that ):bool { if( $this->t !== $that->t ){ return false; } $i = $this->z; $fields = [ 'source', 'context', 'notes', 'refs', 'target', 'comment', 'flag', 'parent', 'plural' ]; while( ++$i < $this->t ){ $a = $this->po[$i]; $b = $that->po[$i]; foreach( $fields as $f ){ $af = $a[$f] ?? ''; $bf = $b[$f] ?? ''; if( $af !== $bf ){ return false; } } } return true; }
|
||||
public function sort( ?callable $func = null ):self { $order = []; foreach( $this as $msg ){ $order[] = $msg; } usort( $order, $func ?: [__CLASS__,'compare'] ); $this->clear(); foreach( $order as $p ){ $this->push($p); } return $this; }
|
||||
public static function compare( LocoPoMessage $a, LocoPoMessage $b ):int { $h = $a->getHash(); $j = $b->getHash(); $n = strcasecmp( $h, $j ); if( 0 === $n ){ $n = strcmp( $h, $j ); if( 0 === $n ){ return 0; } } return $n > 0 ? 1 : -1; }
|
||||
public function createSorter():array { $index = []; foreach( $this as $i => $msg ){ $index[ $msg->getHash() ] = $i; } $obj = new LocoPoIndex( $index ); return [ $obj, 'compare' ]; } }
|
||||
class LocoMoTable {
|
||||
private /*int*/ $size = 0;
|
||||
private /*string*/ $bin = '';
|
||||
private /*array*/ $map = null;
|
||||
public function __construct( /*mixed*/ $data = '' ){ if( is_array($data) ){ $this->compile( $data ); } else if( '' !== $data ){ $this->parse( $data ); } }
|
||||
#[ReturnTypeWillChange]
|
||||
public function count():int { if( is_null($this->size) ){ if( $this->bin ){ $this->size = (int) ( strlen( $this->bin ) / 4 ); } else if( is_array($this->map) ){ $this->size = count($this->map); } else { return 0; } if( ! self::is_prime($this->size) || $this->size < 3 ){ throw new Exception('Size expected to be prime number above 2, got '.$this->size); } } return $this->size; }
|
||||
public function bytes():int { return $this->count() * 4; }
|
||||
public function __toString():string { return $this->bin; }
|
||||
public function export():array { if( is_null($this->map) ){ $this->parse($this->bin); } return $this->map; }
|
||||
private function reset( int $length ):int { $this->size = max( 3, self::next_prime( $length * 4 / 3 ) ); $this->bin = ''; $this->map = []; return $this->size; }
|
||||
public function compile( array $msgids ):void { $hash_tab_size = $this->reset( count($msgids) ); $packed = array_fill( 0, $hash_tab_size, "\0\0\0\0" ); $j = 0; foreach( $msgids as $msgid ){ $hash_val = self::hashpjw( $msgid ); $idx = $hash_val % $hash_tab_size; if( array_key_exists($idx, $this->map) ){ $incr = 1 + ( $hash_val % ( $hash_tab_size - 2 ) ); do { $idx += $incr; if( $hash_val === $idx ){ throw new Exception('Unable to find empty slot in hash table'); } $idx %= $hash_tab_size; } while( array_key_exists($idx, $this->map ) ); } $this->map[$idx] = $j; $packed[$idx] = pack('V', ++$j ); } $this->bin = implode('',$packed); }
|
||||
public function lookup( string $msgid, array $msgids ):int { $hash_val = self::hashpjw( $msgid ); $idx = $hash_val % $this->size; $incr = 1 + ( $hash_val % ( $this->size - 2 ) ); while( true ){ if( ! array_key_exists($idx, $this->map) ){ break; } $j = $this->map[$idx]; if( isset($msgids[$j]) && $msgid === $msgids[$j] ){ return $j; } $idx += $incr; if( $idx === $hash_val ){ break; } $idx %= $this->size; } return -1; }
|
||||
private function parse( string $bin ):void { $this->bin = $bin; $this->size = null; $hash_tab_size = $this->count(); $this->map = []; $idx = -1; $byte = 0; while( ++$idx < $hash_tab_size ){ $word = substr( $this->bin, $byte, 4 ); if( "\0\0\0\0" !== $word ){ list(,$j) = unpack('V', $word ); $this->map[$idx] = $j - 1; } $byte += 4; } }
|
||||
public static function hashpjw( string $str ):int { $i = -1; $hval = 0; $len = strlen($str); while( ++$i < $len ){ $ord = ord( substr($str,$i,1) ); $hval = ( $hval << 4 ) + $ord; $g = $hval & 0xf0000000; if( $g !== 0 ){ $hval ^= $g >> 24; $hval ^= $g; } } return $hval; }
|
||||
private static function next_prime( float $seed ):int { $seed = (int) floor($seed); $seed |= 1; while ( ! self::is_prime($seed) ){ $seed += 2; } return $seed; }
|
||||
private static function is_prime( int $num ):bool { if( 1 === $num ){ return false; } if( 2 === $num ){ return true; } if( $num % 2 == 0 ) { return false; } for( $i = 3; $i <= ceil(sqrt($num)); $i = $i + 2) { if($num % $i == 0 ){ return false; } } return true; } }
|
||||
class LocoMo {
|
||||
private /*string*/ $bin;
|
||||
private /*Iterator*/ $msgs;
|
||||
private /*LocoPoHeaders*/ $head;
|
||||
private /*LocoMoTable*/ $hash = null;
|
||||
private /*bool*/ $use_fuzzy = false;
|
||||
private /*string*/ $cs = null;
|
||||
public function __construct( Iterator $export, ?LocoPoHeaders $head = null ){ if( $head ){ $this->head = $head; } else { $this->head = new LocoPoHeaders; $this->setHeader('Project-Id-Version','Loco'); } $this->msgs = $export; $this->bin = ''; }
|
||||
public function setCharset( string $cs ):void { $cs = $this->head->setCharset($cs); $this->cs = 'UTF-8' === $cs ? null : $cs; }
|
||||
public function enableHash():void { $this->hash = new LocoMoTable; }
|
||||
public function useFuzzy():void { $this->use_fuzzy = true; }
|
||||
public function setHeader( string $key, string $val ):self { $this->head->add($key,$val); return $this; }
|
||||
private function str( string $s ):string { if( $cs = $this->cs ){ $s = mb_convert_encoding($s,$cs,['UTF-8']); } return $s; }
|
||||
public function compile():string { $table = ['']; $sources = ['']; $targets = [ (string) $this->head ]; $fuzzy_flag = 4; $skip_fuzzy = ! $this->use_fuzzy; if( $this->head->has('Plural-Forms') && preg_match('/^nplurals=(\\d)/',$this->head->trimmed('Plural-Forms'), $r) ){ $nplurals = (int) $r[1]; $maxplural = max( 0, $nplurals-1 ); } else { $maxplural = 1; } $unique = []; foreach( $this->msgs as $r ){ if( $skip_fuzzy && isset($r['flag']) && $fuzzy_flag === $r['flag'] ){ continue; } $msgid = $this->str( $r['key'] ); if( isset($r['context']) ){ $msgctxt = $this->str( $r['context'] ); if( '' !== $msgctxt ){ if( '' === $msgid ){ $msgid = '('.$msgctxt.')'; } $msgid = $msgctxt."\x04".$msgid; } } if( '' === $msgid ){ continue; } if( array_key_exists($msgid,$unique) ){ continue; } $unique[$msgid] = true; $msgstr = $this->str( $r['target'] ); if( '' === $msgstr ){ continue; } $table[] = $msgid; if( isset($r['plurals']) ){ if( $r['plurals'] ){ $i = 0; foreach( $r['plurals'] as $i => $p ){ if( $i === 0 ){ $msgid .= "\0".$this->str($p['key']); } $msgstr .= "\0".$this->str($p['target']); } while( $maxplural > ++$i ){ $msgstr .= "\0"; } } else if( isset($r['plural_key']) ){ $msgid .= "\0".$this->str($r['plural_key']); } } $sources[] = $msgid; $targets[] = $msgstr; } asort( $sources, SORT_STRING ); $this->bin = "\xDE\x12\x04\x95\x00\x00\x00\x00"; $n = count($sources); $this->writeInteger( $n ); $offset = 28; $this->writeInteger( $offset ); $offset += $n * 8; $this->writeInteger( $offset ); if( $this->hash ){ sort( $table, SORT_STRING ); $this->hash->compile( $table ); $s = $this->hash->count(); } else { $s = 0; } $this->writeInteger( $s ); $offset += $n * 8; $this->writeInteger( $offset ); if( $s ){ $offset += $s * 4; } $source = ''; foreach( $sources as $str ){ $source .= $str."\0"; $this->writeInteger( $strlen = strlen($str) ); $this->writeInteger( $offset ); $offset += $strlen + 1; } $target = ''; foreach( array_keys($sources) as $i ){ $str = $targets[$i]; $target .= $str."\0"; $this->writeInteger( $strlen = strlen($str) ); $this->writeInteger( $offset ); $offset += $strlen + 1; } if( $this->hash ){ $this->bin .= $this->hash->__toString(); } $this->bin .= $source; $this->bin .= $target; return $this->bin; }
|
||||
private function writeInteger( int $num ):void { $this->bin .= pack( 'V', $num ); } }
|
||||
interface LocoTokensInterface extends Iterator {
|
||||
public function advance();
|
||||
public function ignore( ...$symbols ):self; }
|
||||
class LocoTokenizer implements LocoTokensInterface { const /*int*/T_LITERAL = 0; const /*int*/T_UNKNOWN = -1;
|
||||
private /*string*/ $src;
|
||||
private /*int*/ $pos;
|
||||
private /*int*/ $line;
|
||||
private /*int*/ $col;
|
||||
private /*int*/ $max;
|
||||
private /*array*/ $rules = [];
|
||||
private /*array*/ $skip = [];
|
||||
private /*mixed*/ $tok;
|
||||
private /*int*/ $len;
|
||||
public function __construct( string $src = '' ){ $this->init($src); }
|
||||
public function parse( string $src ):array { return iterator_to_array( $this->generate($src) ); }
|
||||
public function generate( string $src ):Generator { $this->init($src); while( $this->valid() ){ yield $this->current(); $this->next(); } }
|
||||
public function init( string $src ):self { $this->src = $src; $this->rewind(); return $this; }
|
||||
public function define( string $grep, /*mixed*/ $t = 0 ):self { if('^' !== $grep[1] ){ throw new InvalidArgumentException('Expression '.$grep.' isn\'t anchored'); } if( ! is_int($t) && ! is_callable($t) ){ throw new InvalidArgumentException('Non-integer token must be valid callback'); } $sniff = $grep[2]; if( $sniff === preg_quote($sniff,$grep[0]) ){ $this->rules[$sniff][] = [ $grep, $t ]; } else { $this->rules[''][] = [ $grep, $t ]; } return $this; }
|
||||
public function ignore( ...$symbols ):LocoTokensInterface { $this->skip += array_fill_keys( $symbols, true ); return $this; }
|
||||
public function allow( ...$symbols ):self { $this->skip = array_diff_key( $this->skip, array_fill_keys($symbols,true) ); return $this; }
|
||||
#[ReturnTypeWillChange]
|
||||
public function current() { return $this->tok; }
|
||||
public function advance() { $tok = $this->current(); $this->next(); return $tok; }
|
||||
#[ReturnTypeWillChange]
|
||||
public function next():void { $tok = null; $offset = $this->pos; $column = $this->col; $line = $this->line; while( $offset <= $this->max ){ $t = null; $s = ''; $text = substr($this->src,$offset); foreach( [$text[0],''] as $k ){ if( isset($this->rules[$k]) ) { foreach( $this->rules[$k] as $rule) { if( preg_match($rule[0], $text, $match ) ) { $s = $match[0]; $t = $rule[1]; if( ! is_int($t) ) { $t = call_user_func( $t, $s, $match ); } break 2; } } } } if( is_null($t) ){ $n = preg_match('/^./u',$text,$match); if( false === $n ){ $s = $text[0]; $match = [ mb_convert_encoding($s,'UTF-8','Windows-1252') ]; } $s = (string) $match[0]; $t = self::T_UNKNOWN; } $length = strlen($s); if( 0 === $length ){ throw new Loco_error_ParseException('Failed to match anything'); } $offset += $length; $lines = preg_split('/\\r?\\n/',$s); $nlines = count($lines); if( $nlines > 1 ){ $next_line = $line + ( $nlines - 1 ); $next_column = strlen( end($lines) ); } else { $next_line = $line; $next_column = $column + $length; } if( array_key_exists($t,$this->skip) ){ $line = $next_line; $column = $next_column; continue; } $tok = self::T_LITERAL === $t ? $s : [ $t, $s, $line, $column ]; $line = $next_line; $column = $next_column; $this->len++; break; } $this->tok = $tok; $this->pos = $offset; $this->col = $column; $this->line = $line; }
|
||||
#[ReturnTypeWillChange]
|
||||
public function key():?int { return $this->len ? $this->len-1 : null; }
|
||||
#[ReturnTypeWillChange]
|
||||
public function valid():bool { return null !== $this->tok; }
|
||||
#[ReturnTypeWillChange]
|
||||
public function rewind():void { $this->len = 0; $this->pos = 0; $this->col = 0; $this->line = 1; $this->max = strlen($this->src) - 1; $this->next(); } }
|
||||
function loco_utf8_chr( int $u ){ if( $u < 0x80 ){ if( $u < 0 ){ throw new RangeException( sprintf('%d is out of Unicode range', $u ) ); } return chr($u); } if( $u < 0x800 ) { return chr( ($u>>6) & 0x1F | 0xC0 ).chr( $u & 0x3F | 0x80 ); } if( $u < 0x10000 ) { return chr( $u>>12 & 15 | 0xE0 ).chr( $u>>6 & 0x3F | 0x80 ).chr( $u & 0x3F | 0x80 ); } if( $u < 0x110000 ) { return chr( $u>>18 & 7 | 0xF0 ).chr( $u>>12 & 0x3F | 0x80 ).chr( $u>>6 & 0x3F | 0x80 ).chr( $u & 0x3F | 0x80 ); } throw new RangeException( sprintf('\\x%X is out of Unicode range', $u ) ); }
|
||||
function loco_resolve_surrogates( string $s ){ return preg_replace_callback('/\\xED([\\xA0-\\xAF])([\\x80-\\xBF])\\xED([\\xB0-\\xBF])([\\x80-\\xBF])/', '_loco_resolve_surrogates', $s ); }
|
||||
function _loco_resolve_surrogates( array $r ){ return loco_utf8_chr ( ( ( ( ( 832 | ( ord($r[1]) & 0x3F ) ) << 6 ) | ( ord($r[2]) & 0x3F ) ) - 0xD800 ) * 0x400 + ( ( ( ( 832 | ( ord($r[3]) & 0x3F ) ) << 6 ) | ( ord($r[4]) & 0x3F ) ) - 0xDC00 ) + 0x10000 ); }
|
||||
class LocoEscapeParser {
|
||||
private /*array*/ $map;
|
||||
private /*string*/ $grep;
|
||||
public function __construct( array $map = [] ){ $this->map = $map; $rules = ['\\\\']; if( $map ){ $rules[] = '['.implode(array_keys($map)).']'; } if( ! isset($map['U']) ) { $rules[] = 'U[0-9A-Fa-f]{5,8}'; } if( ! isset($map['u']) ) { $rules[] = 'u(?:\\{[0-9A-Fa-f]+\\}|[0-9A-Fa-f]{1,4})(?:\\\\u(?:\\{[0-9A-Fa-f]+\\}|[0-9A-Fa-f]{1,4}))*'; } $this->grep = '/\\\\('.implode('|',$rules).')/'; }
|
||||
final public function unescape( string $s ):string { if( '' !== $s ) { return $this->stripSlashes( preg_replace_callback($this->grep, [$this, 'unescapeMatch'], $s) ); } return ''; }
|
||||
final public function unescapeMatch( array $r ):string { $s = $r[0]; $c = $s[1]; if( isset($this->map[$c]) ){ return $this->map[$c]; } if( 'u' === $c ){ $str = ''; $surrogates = false; foreach( explode('\\u',$s) as $i => $h ){ if( '' !== $h ){ $h = ltrim( trim($h,'{}'),'0'); $u = intval($h,16); $str.= loco_utf8_chr($u); if( ! $surrogates ){ $surrogates = $u >= 0xD800 && $u <= 0xDBFF; } } } if( $surrogates ){ $str = loco_resolve_surrogates($str); } return $str; } if( 'U' === $c ){ return loco_utf8_chr( intval(substr($s,2),16) ); } if( 'x' === $c ){ return chr( intval(substr($s,2),16) ); } if( ctype_digit($c) ){ return chr( intval(substr($s,1),8) ); } return $s; }
|
||||
protected function stripSlashes( string $s ):string { return stripcslashes($s); } }
|
||||
class LocoJsTokens extends LocoTokenizer {
|
||||
private static /*LocoEscapeParser*/ $lex = null;
|
||||
protected static /*array*/ $words = [ 'true' => 1, 'false' => 1, 'null' => 1, 'break' => T_BREAK, 'else' => T_ELSE, 'new' => T_NEW, 'var' => 1, 'case' => T_CASE, 'finally' => T_FINALLY, 'return' => T_RETURN, 'void' => 1, 'catch' => T_CATCH, 'for' => T_FOR, 'switch' => T_SWITCH, 'while' => T_WHILE, 'continue' => T_CONTINUE, 'function' => T_FUNCTION, 'this' => T_STRING, 'with' => 1, 'default' => T_DEFAULT, 'if' => T_IF, 'throw' => T_THROW, 'delete' => 1, 'in' => 1, 'try' => T_TRY, 'do' => T_DO, 'instanceof' => 1, 'typeof' => 1, ];
|
||||
public static function decapse( string $encapsed ):string { $s = substr($encapsed,1,-1); $l = self::$lex; if( is_null($l) ){ $l = new LocoEscapeParser( [ 'U' => 'U', 'a' => 'a', ] ); self::$lex = $l; } return $l->unescape($s); }
|
||||
public function __construct( string $src = '' ){ $this->ignore(T_WHITESPACE); $this->define('/^(?:\\\\u[0-9A-F]{4,4}|[$_\\pL\\p{Nl}])(?:\\\\u[0-9A-F]{4}|[$_\\pL\\pN\\p{Mn}\\p{Mc}\\p{Pc}])*/ui', [$this,'matchWord'] ); $this->define('/^\\s+/u', T_WHITESPACE ); $this->define('!^//.*!', T_COMMENT ); $this->define('!^/\\*.*\\*/!Us', [$this,'matchComment'] ); $this->define('/^"(?:\\\\.|[^\\r\\n\\p{Zl}\\p{Zp}"\\\\])*"/u', T_CONSTANT_ENCAPSED_STRING ); $this->define('/^\'(?:\\\\.|[^\\r\\n\\p{Zl}\\p{Zp}\'\\\\])*\'/u', T_CONSTANT_ENCAPSED_STRING ); $this->define('/^[-+;,<>.=:|&^!?*%~(){}[\\]]/'); parent::__construct($src); }
|
||||
public function matchWord( string $s ):int { if( array_key_exists($s,self::$words) ){ return self::$words[$s]; } return T_STRING; }
|
||||
public function matchComment( string $s ):int { if( substr($s,0,3) === '/**' ){ return T_DOC_COMMENT; } return T_COMMENT; } }
|
||||
interface LocoExtractorInterface {
|
||||
public function setDomain( string $default ):void;
|
||||
public function tokenize( string $src ):LocoTokensInterface;
|
||||
public function extract( LocoExtracted $strings, LocoTokensInterface $tokens, string $fileref = '' ):void;
|
||||
public function extractSource( string $src, string $fileref ):LocoExtracted; }
|
||||
class LocoExtracted implements Countable {
|
||||
private /*array*/ $exp = [];
|
||||
private /*array*/ $reg = [];
|
||||
private /*array*/ $dom = [];
|
||||
private /*string*/ $dflt = '';
|
||||
public function extractSource( LocoExtractorInterface $ext, string $src, string $fileref = '' ):self { $ext->extract( $this, $ext->tokenize($src), $fileref ); return $this; }
|
||||
public function export():array { return $this->exp; }
|
||||
#[ReturnTypeWillChange]
|
||||
public function count():int { return count( $this->exp ); }
|
||||
public function getDomainCounts():array { return $this->dom; }
|
||||
public function setDomain( string $default ):self { $this->dflt = $default; return $this; }
|
||||
public function getDomain():string { return $this->dflt; }
|
||||
private function key( array $entry ):string { $key = (string) $entry['source']; foreach( ['context','domain'] as $i => $prop ){ if( array_key_exists($prop,$entry) ) { $add = (string) $entry[$prop]; if( '' !== $add ){ $key .= ord($i).$add; } } } return $key; }
|
||||
public function pushEntry( array $entry, string $domain ):int { if( '' === $domain || '*' === $domain ){ $domain = $this->dflt; } $entry['id'] = ''; $entry['target'] = ''; $entry['domain'] = $domain; $key = $this->key($entry); if( isset($this->reg[$key]) ){ $index = $this->reg[$key]; $clash = $this->exp[$index]; if( $value = $this->mergeField( $clash, $entry, 'refs', ' ') ){ $this->exp[$index]['refs'] = $value; } if( $value = $this->mergeField( $clash, $entry, 'notes', "\n") ){ $this->exp[$index]['notes'] = $value; } } else { $index = count($this->exp); $this->reg[$key] = $index; $this->exp[$index] = $entry; if( isset($this->dom[$domain]) ){ $this->dom[$domain]++; } else { $this->dom[$domain] = 1; } } return $index; }
|
||||
public function pushPlural( array $entry, int $sindex ):void { $parent = $this->exp[$sindex]; $domain = $parent['domain']; $pkey = $this->key($parent)."\2"; if( ! array_key_exists($pkey,$this->reg) ){ $pindex = count($this->exp); $this->reg[$pkey] = $pindex; $entry += [ 'id' => '', 'target' => '', 'plural' => 1, 'parent' => $sindex, 'domain' => $domain, ]; $this->exp[$pindex] = $entry; if( isset($entry['format']) && ! isset( $parent['format']) ) { $this->exp[$sindex]['format'] = $entry['format']; } if( $pindex !== $sindex + $entry['plural']) { $this->exp[$sindex]['child'] = $pindex; } } }
|
||||
public function mergeField( array $old, array $new, string $field, string $glue ):string { $prev = isset($old[$field]) ? $old[$field] : ''; if( isset($new[$field]) ){ $text = $new[$field]; if( '' !== $prev && $prev !== $text ){ if( 'notes' === $field && preg_match( '/^'.preg_quote( rtrim($text,'. '),'/').'[. ]*$/mu', $prev ) ) { $text = $prev; } else { $text = $prev.$glue.$text; } } return $text; } return $prev; }
|
||||
public function filter( string $domain ):array { if( '' === $domain ){ $domain = $this->dflt; } $map = []; $newOffset = 1; $matchAll = '*' === $domain; $raw = [ [ 'id' => '', 'source' => '', 'target' => '', 'domain' => $matchAll ? '' : $domain, ] ]; foreach( $this->exp as $oldOffset => $r ){ if( isset($r['parent']) ){ if( isset($map[$r['parent']]) ){ $r['parent'] = $map[ $r['parent'] ]; $raw[ $newOffset++ ] = $r; } } else { if( $matchAll ){ $match = true; } else if( isset($r['domain']) ){ $match = $domain === $r['domain']; } else { $match = $domain === ''; } if( $match ){ $map[ $oldOffset ] = $newOffset; $raw[ $newOffset++ ] = $r; } } } return $raw; } }
|
||||
abstract class LocoExtractor implements LocoExtractorInterface {
|
||||
private /*array*/ $rules;
|
||||
private /*array*/ $wp = [];
|
||||
private /*string*/ $domain = '';
|
||||
abstract protected function fsniff( string $str ):string;
|
||||
abstract protected function decapse( string $raw ):string;
|
||||
abstract protected function comment( string $comment ):string;
|
||||
public function __construct( array $rules ){ $this->rules = $rules; }
|
||||
public function setDomain( string $default ):void { $this->domain = $default; }
|
||||
public function headerize( array $tags, string $domain = '' ):self { if( isset($this->wp[$domain]) ){ $this->wp[$domain] += $tags; } else { $this->wp[$domain] = $tags; } return $this; }
|
||||
protected function getHeaders():array { return $this->wp; }
|
||||
final public function extractSource( string $src, string $fileref ):LocoExtracted { $strings = new LocoExtracted; $this->extract( $strings, $this->tokenize($src), $fileref ); return $strings; }
|
||||
public function rule( string $keyword ):string { return isset($this->rules[$keyword]) ? $this->rules[$keyword] : ''; }
|
||||
protected function push( LocoExtracted $strings, string $rule, array $args, string $comment = '', string $ref = '' ):?int { $s = strpos( $rule, 's'); $p = strpos( $rule, 'p'); $c = strpos( $rule, 'c'); $d = strpos( $rule, 'd'); if( false === $s || ! isset($args[$s]) ){ return null; } $msgid = $args[$s]; if( ! is_string($msgid) ){ return null; } $entry = [ 'source' => $msgid, ]; if( is_int($c) && isset($args[$c]) ){ $entry['context'] = $args[$c]; } else if( '' === $msgid ){ return null; } if( $ref ){ $entry['refs'] = $ref; } if( is_int($d) && array_key_exists($d,$args) ){ $domain = $args[$d]; if( is_null($domain) ){ $domain = ''; } } else if( '' === $this->domain ) { $domain = $strings->getDomain(); } else { $domain = $this->domain; } $format = ''; $comment = $this->comment($comment); if( '' !== $comment ){ if( preg_match('/^xgettext:\\s*([-a-z]+)-format\\s*/mi', $comment, $r, PREG_OFFSET_CAPTURE ) ){ $format = $r[1][0]; $entry['format'] = $format; $comment = trim( substr_replace( $comment,'', $r[0][1], strlen($r[0][0]) ) ); } if( preg_match('/^references?:( *.+:\\d+)*\\s*/mi', $comment, $r, PREG_OFFSET_CAPTURE ) ){ $entry['refs'] = trim($r[1][0],' '); $comment = trim( substr_replace( $comment, '', $r[0][1], strlen($r[0][0]) ) ); } $entry['notes'] = $comment; } $msgid_plural = is_int($p) && isset($args[$p]) ? $args[$p] : ''; if( '' === $format ){ $format = $this->fsniff($msgid); if( '' !== $format ){ $entry['format'] = $format; } else if( '' !== $msgid_plural ){ $format = $this->fsniff($msgid_plural); if( '' !== $format ){ $entry['format'] = $format; } } } $index = $strings->pushEntry($entry,$domain); if( '' !== $msgid_plural ){ $entry = [ 'source' => $msgid_plural, ]; if( '' !== $format ) { $entry['format'] = $format; } $strings->pushPlural($entry,$index); } return $index; }
|
||||
protected function utf8( string $str ):string { if( false === preg_match('//u',$str) ){ $str = mb_convert_encoding( $str, 'UTF-8', 'Windows-1252' ); } return $str; } }
|
||||
class LocoPHPTokens implements LocoTokensInterface, Countable {
|
||||
private /*int*/ $i = null;
|
||||
private /*array*/ $tokens;
|
||||
private /*array*/ $skip_tokens;
|
||||
private /*array*/ $literal_tokens;
|
||||
public function __construct( array $tokens ){ $this->tokens = $tokens; $this->reset(); }
|
||||
public function reset():void { $this->rewind(); $this->literal_tokens = []; $this->skip_tokens = []; }
|
||||
public function literal( ...$symbols ):self { $this->literal_tokens += array_fill_keys($symbols,true); return $this; }
|
||||
public function ignore( ...$symbols ):LocoTokensInterface { $this->skip_tokens += array_fill_keys($symbols,true); return $this; }
|
||||
public function export():array { return array_values( iterator_to_array($this) ); }
|
||||
public function advance() { if( $this->valid() ){ $tok = $this->current(); $this->next(); return $tok; } return null; }
|
||||
#[ReturnTypeWillChange]
|
||||
public function rewind():void { $this->i = ( false === reset($this->tokens) ? null : key($this->tokens) ); }
|
||||
#[ReturnTypeWillChange]
|
||||
public function valid():bool { while( is_int($this->i) ){ $tok = $this->tokens[$this->i]; if( array_key_exists( is_array($tok)?$tok[0]:$tok, $this->skip_tokens ) ){ $this->next(); } else { return true; } } return false; }
|
||||
#[ReturnTypeWillChange]
|
||||
public function key():?int { return $this->i; }
|
||||
#[ReturnTypeWillChange]
|
||||
public function next():void { $this->i = ( false === next($this->tokens) ? null : key($this->tokens) ); }
|
||||
#[ReturnTypeWillChange]
|
||||
public function current() { $tok = $this->tokens[$this->i]; if( is_array($tok) && isset($this->literal_tokens[$tok[0]]) ){ return $tok[1]; } return $tok; }
|
||||
public function __toString():string { $s = []; foreach( $this as $token ){ $s[] = is_array($token) ? $token[1] : $token; } return implode('',$s); }
|
||||
#[ReturnTypeWillChange]
|
||||
public function count():int { return count($this->tokens); } }
|
||||
class LocoPHPEscapeParser extends LocoEscapeParser {
|
||||
public function __construct(){ parent::__construct( [ 'n' => "\n", 'r' => "\r", 't' => "\t", 'v' => "\x0B", 'f' => "\x0C", 'e' => "\x1B", '$' => '$', '\\' => '\\', '"' => '"', ] ); }
|
||||
protected function stripSlashes( string $s ):string { return preg_replace_callback('/\\\\(x[0-9A-Fa-f]{1,2}|[0-3]?[0-7]{1,2})/', [$this,'unescapeMatch'], $s, -1, $n ); } }
|
||||
function loco_unescape_php_string( string $s ):string { static $l; if( is_null($l) ) { $l = new LocoPHPEscapeParser; } return $l->unescape($s); }
|
||||
function loco_decapse_php_string( string $s ):string { if( '' === $s ){ return ''; } $q = $s[0]; if( "'" === $q ){ return str_replace( ['\\'.$q, '\\\\'], [$q, '\\'], substr( $s, 1, -1 ) ); } if( '"' !== $q ){ return $s; } return loco_unescape_php_string( substr($s,1,-1) ); }
|
||||
function loco_parse_php_comment( string $comment ):string { $comment = trim( $comment,"/ \n\r\t" ); if( '' !== $comment && '*' === $comment[0] ){ $lines = []; $junk = "\r\t/ *"; foreach( explode("\n",$comment) as $line ){ $line = trim($line,$junk); if( '' !== $line ){ $lines[] = $line; } } $comment = implode("\n", $lines); } return $comment; }
|
||||
function loco_parse_wp_comment( string $block ):array { $header = []; if( '/*' === substr($block,0,2) ){ $junk = "\r\t/ *"; foreach( explode("\n", $block) as $line ){ if( false !== ( $i = strpos($line,':') ) ){ $key = substr($line,0,$i); $val = substr($line,++$i); $header[ trim($key,$junk) ] = trim($val,$junk); } } } return $header; }
|
||||
class LocoPHPExtractor extends LocoExtractor {
|
||||
private /*array*/ $defs = [];
|
||||
public function tokenize( string $src ):LocoTokensInterface { return new LocoPHPTokens( token_get_all($src) ); }
|
||||
public function decapse( string $raw ):string { return loco_decapse_php_string( $raw ); }
|
||||
public function fsniff( string $str ):string { $format = ''; $offset = 0; while( preg_match('/%(?:[1-9]\\d*\\$)?(?:\'.|[-+0 ])*\\d*(?:\\.\\d+)?(.|$)/',$str,$r,PREG_OFFSET_CAPTURE,$offset) ){ $type = $r[1][0]; list($match,$offset) = $r[0]; if( '%' === $type && '%%' !== $match ){ return ''; } if( '' === $type || ! preg_match('/^[bcdeEfFgGosuxX%]/',$type) ){ return ''; } $offset += strlen($match); if( preg_match('/^% +[a-z]/i',$match) || preg_match('/^%[b-ou-x]/i',$match) ){ continue; } $format = 'php'; } return $format; }
|
||||
protected function comment( string $comment ):string { return preg_replace('/^translators:\\s+/mi', '', loco_parse_php_comment($comment) ); }
|
||||
public function define( string $name, string $value ):self { $this->defs[$name] = $value; return $this; }
|
||||
public function extract( LocoExtracted $strings, LocoTokensInterface $tokens, string $fileref = '' ):void { $tokens->ignore(T_WHITESPACE); $n = 0; $depth = 0; $comment = ''; $narg = 0; $args = []; $ref = ''; $rule = ''; $wp = $this->getHeaders(); $tokens->rewind(); while( $tok = $tokens->advance() ){ if( is_string($tok) ){ $s = $tok; $t = null; } else { $t = $tok[0]; $s = $tok[1]; } if( $depth ){ if( ')' === $s || ']' === $s ){ if( 0 === --$depth ){ if( $this->push( $strings, $rule, $args, $comment, $ref ) ){ $n++; } $comment = ''; } } else if( '(' === $s || '[' === $s ){ $depth++; $args[$narg] = null; } else if( 1 === $depth ){ if( ',' === $s ){ $narg++; } else if( T_CONSTANT_ENCAPSED_STRING === $t ){ $s = self::utf8($s); $args[$narg] = $this->decapse($s); } else if( T_STRING === $t && array_key_exists($s,$this->defs) ){ $args[$narg] = $this->defs[$s]; } else { $args[$narg] = null; } } } else if( T_COMMENT === $t || T_DOC_COMMENT === $t ){ $was_header = false; $s = self::utf8($s); if( 0 === $n ){ if( false !== strpos($s,'* @package') ){ $was_header = true; } if( $wp && ( $header = loco_parse_wp_comment($s) ) ){ foreach( $wp as $domain => $tags ){ foreach( array_intersect_key($header,$tags) as $tag => $text ){ $ref = $fileref ? $fileref.':'.$tok[2]: ''; $meta = $tags[$tag]; if( is_string($meta) ){ $meta = ['notes'=>$meta]; trigger_error( $tag.' header defaulted to "notes"',E_USER_DEPRECATED); } $strings->pushEntry( ['source'=>$text,'refs'=>$ref] + $meta, (string) $domain ); $was_header = true; } } } } if( ! $was_header ) { $comment = $s; } } else if( T_STRING === $t && '(' === $tokens->advance() && ( $rule = $this->rule($s) ) ){ $ref = $fileref ? $fileref.':'.$tok[2]: ''; $depth = 1; $args = []; $narg = 0; } else if( '' !== $comment && ! preg_match('!^[/* ]+(translators|xgettext):!im',$comment) ){ $comment = ''; } } } }
|
||||
class LocoJsExtractor extends LocoPHPExtractor {
|
||||
public function tokenize( string $src ):LocoTokensInterface { return new LocoJsTokens($src); }
|
||||
public function fsniff( string $str ):string { return parent::fsniff($str) ? 'javascript' : ''; }
|
||||
public function decapse( string $raw ):string { return LocoJsTokens::decapse($raw); } }
|
||||
class LocoTwigExtractor extends LocoPHPExtractor {
|
||||
public function tokenize( string $src ):LocoTokensInterface { return parent::tokenize( '<?php '.preg_replace('/{#([^#]+)#}/su','/*\\1*/',$src) ); } }
|
||||
class LocoBladeExtractor extends LocoPHPExtractor {
|
||||
public function tokenize( string $src ):LocoTokensInterface { return parent::tokenize( '<?php '.preg_replace('/{{--(.+)--}}/su','/*\\1*/',$src) ); } }
|
||||
class LocoWpJsonExtractor implements LocoExtractorInterface {
|
||||
private static /*array*/ $types = [];
|
||||
private /*string*/ $base = '.';
|
||||
private /*string*/ $domain = '';
|
||||
public function __construct() { if( defined('ABSPATH') ){ $this->setBase( rtrim(ABSPATH,'/').'/wp-includes' ); } }
|
||||
public function setBase( string $path ):void { $this->base = $path; }
|
||||
private function getType( string $type ):stdClass { if( array_key_exists($type,self::$types) ){ return self::$types[$type]; } $path = $this->base.'/'.$type.'-i18n.json'; if ( ! file_exists($path) ) { throw new Exception( basename($path).' not found in '.$this->base ); } return json_decode( file_get_contents($path) ); }
|
||||
public function tokenize( string $src ): LocoTokensInterface { $raw = json_decode($src,true); if( ! is_array($raw) || ! array_key_exists('$schema',$raw) ){ throw new InvalidArgumentException('Invalid JSON'); } if( ! preg_match('!^https?://schemas.wp.org/trunk/(block|theme)\\.json!', $raw['$schema'], $r ) ){ throw new InvalidArgumentException('Unsupported schema'); } if( '' === $this->domain && array_key_exists('textdomain',$raw) ){ $this->domain = $raw['textdomain']; } return new LocoWpJsonStrings( $raw, $this->getType($r[1]) ); }
|
||||
public function setDomain( string $default ):void { $this->domain = $default; }
|
||||
public function extract( LocoExtracted $strings, LocoTokensInterface $tokens, string $fileref = '' ):void { if( ! preg_match('/:\\d+$/',$fileref) ){ $fileref.=':1'; } $tokens->rewind(); while( $tok = $tokens->advance() ){ $tok['refs'] = $fileref; $strings->pushEntry( $tok, $this->domain ); } }
|
||||
final public function extractSource( string $src, string $fileref ):LocoExtracted { $strings = new LocoExtracted; $this->extract( $strings, $this->tokenize($src), $fileref ); return $strings; } }
|
||||
class LocoWpJsonStrings extends ArrayIterator implements LocoTokensInterface {
|
||||
public function __construct( array $raw, stdClass $tpl ){ parent::__construct(); $this->walk( $tpl, $raw ); }
|
||||
public function advance() { $tok = $this->current(); $this->next(); return $tok; }
|
||||
public function ignore( ...$symbols ):LocoTokensInterface { return $this; }
|
||||
private function walk( /*mixed*/ $tpl, /*mixed*/ $raw ):void { if( is_string($tpl) && is_string($raw) ) { $this->offsetSet( null, [ 'context' => $tpl, 'source' => $raw, ] ); return; } if( is_array($tpl) && is_array($raw) ) { foreach ( $raw as $value ) { self::walk( $tpl[0], $value ); } } else if( is_object($tpl) && is_array($raw) ) { $group_key = '*'; foreach ( $raw as $key => $value ) { if ( isset($tpl->$key) ) { $this->walk( $tpl->$key, $value ); } else if ( isset($tpl->$group_key) ) { $this->walk( $tpl->$group_key, $value ); } } } } }
|
||||
function loco_wp_extractor( string $type = 'php', string $ext = '' ):LocoExtractorInterface { if( 'json' === $type ){ return new LocoWpJsonExtractor; } static $rules = [ '__' => 'sd', '_e' => 'sd', '_c' => 'sd', '_n' => 'sp_d', '_n_noop' => 'spd', '_nc' => 'sp_d', '__ngettext' => 'spd', '__ngettext_noop' => 'spd', '_x' => 'scd', '_ex' => 'scd', '_nx' => 'sp_cd', '_nx_noop' => 'spcd', 'esc_attr__' => 'sd', 'esc_html__' => 'sd', 'esc_attr_e' => 'sd', 'esc_html_e' => 'sd', 'esc_attr_x' => 'scd', 'esc_html_x' => 'scd', ]; if( 'php' === $type ){ return substr($ext,-9) === 'blade.php' ? new LocoBladeExtractor($rules) : new LocoPHPExtractor($rules); } if( 'js' === $type ){ return new LocoJsExtractor($rules); } if( 'twig' === $type ){ return new LocoTwigExtractor($rules); } throw new InvalidArgumentException('No extractor for '.$type); }
|
||||
function loco_string_percent( int $n, int $t ):string { if( ! $t || ! $n ){ return '0'; } if( $t === $n ){ return '100'; } $dp = 0; $n = 100 * $n / $t; if( $n > 99 ){ return rtrim( number_format( min( $n, 99.9 ), ++$dp ), '.0' ); } if( $n < 0.5 ){ $n = max( $n, 0.0001 ); do { $s = number_format( $n, ++$dp ); } while( preg_match('/^0\\.0+$/',$s) && $dp < 4 ); return substr($s,1); } return number_format( $n, $dp ); }
|
||||
function loco_print_progress( int $translated, int $untranslated, int $flagged ):void { $total = $translated + $untranslated; $complete = loco_string_percent( $translated - $flagged, $total ); $class = 'progress'; if( ! $translated && ! $flagged ){ $class .= ' empty'; } else if( '100' === $complete ){ $class .= ' done'; } echo '<div class="',$class,'"><div class="t">'; if( $flagged ){ $s = loco_string_percent( $flagged, $total ); echo '<div class="bar f" style="width:',$s,'%"> </div>'; } if( '0' === $complete ){ echo ' '; } else { $class = 'bar p'; $p = (int) $complete; $class .= sprintf(' p-%u', 10*floor($p/10) ); $style = 'width:'.$complete.'%'; if( $flagged ){ $remain = 100.0 - (float) $s; $style .= '; max-width: '.sprintf('%s',$remain).'%'; } echo '<div class="',$class,'" style="'.$style.'"> </div>'; } echo '</div><div class="l">',$complete,'%</div></div>'; }
|
||||
class LocoFuzzyMatcher implements Countable {
|
||||
private /*array*/ $pot = [];
|
||||
private /*array*/ $po = [];
|
||||
private /*array*/ $diff = [];
|
||||
private /*float*/ $dmax = .20;
|
||||
#[ReturnTypeWillChange]
|
||||
public function count():int { return count($this->pot); }
|
||||
public function unmatched():array { return array_values($this->pot); }
|
||||
public function redundant():array { return array_values($this->po); }
|
||||
public function setFuzziness( $s ):void { if( $this->po ){ throw new LogicException('Cannot setFuzziness() after calling match()'); } $this->dmax = (float) max( 0, min( (int) $s, 100 ) ) / 100; }
|
||||
public function add( iterable $a ):void { $source = isset($a['source']) ? (string) $a['source'] : ''; $context = isset($a['context']) ? (string) $a['context'] : ''; $key = $source."\4".$context; $this->pot[$key] = $a; }
|
||||
private function key( iterable $a ):string { $source = isset($a['source']) ? (string) $a['source'] : ''; $context = isset($a['context']) ? (string) $a['context'] : ''; return $source."\4".$context; }
|
||||
protected function getRef( iterable $a ):?iterable { $key = $this->key($a); return array_key_exists($key,$this->pot) ? $this->pot[$key] : null; }
|
||||
public function match( iterable $a ):?iterable { $old = $this->key($a); if( isset($this->pot[$old]) ){ $new = $this->pot[$old]; unset($this->pot[$old]); return $new; } $this->po[$old] = $a; $target = isset($a['target']) ? (string) $a['target'] : ''; $comment = isset($a['comment']) ? (string) $a['comment'] : ''; if( '' === $target && '' === $comment ){ return null; } if( 0 < $this->dmax ){ foreach( $this->pot as $new => $_ ){ $dist = $this->distance($old,$new); if( -1 !== $dist ){ $this->diff[] = [ $old, $new, $dist ]; } } } return null; }
|
||||
private function distance( string $a, string $b ):int { $a = strtolower($a); $b = strtolower($b); if( $a === $b ){ return 0; } $lenA = strlen($a); $lenB = strlen($b); $lenDiff = abs($lenA-$lenB); $max = min($lenA,$lenB) + $lenDiff; $max = (int) ceil( $this->dmax * $max ); if( $max < $lenDiff ) { return -1; } $len = max($lenA,$lenB); if( $len < 256 ){ $d = levenshtein($a,$b); return $d > $max ? -1 : $d; } $d = 0; for( $i = 0; $i < $len; $i+=$max ){ $aa = substr($a,$i,$max); $bb = substr($b,$i,$max); $d += levenshtein($aa,$bb); if( $d > $max ){ return -1; } } return $d; }
|
||||
public function getFuzzyMatches():array { $pairs = []; usort( $this->diff, [__CLASS__,'compareDistance'] ); foreach( $this->diff as $pair ){ list($old,$new) = $pair; if( ! array_key_exists($new,$this->pot) || ! array_key_exists($old,$this->po) ){ continue; } $pairs[] = [ $this->po[$old], $this->pot[$new], ]; unset($this->po[$old]); unset($this->pot[$new]); if( ! $this->po || ! $this->pot ){ break; } } $this->diff = []; return $pairs; }
|
||||
public function exportPo():LocoPoIterator { $p = new LocoPoIterator([ ['source' => ''], ]); $p->concat($this->pot); return $p; }
|
||||
private static function compareDistance( array $a, array $b ):int { return $a[2] - $b[2]; } }
|
||||
if( function_exists('loco_check_extension') ) { loco_check_extension('mbstring'); }
|
||||
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
/**
|
||||
* Downgraded for PHP 7.2 compatibility. Do not edit.
|
||||
* @noinspection ALL
|
||||
*/
|
||||
function loco_parse_wp_locale( string $tag ):array { if( ! preg_match( '/^([a-z]{2,3})(?:[-_]([a-z]{2}))?(?:[-_]([a-z\\d]{3,8}))?$/i', $tag, $tags ) ){ throw new InvalidArgumentException('Invalid WordPress locale: '.json_encode($tag) ); } $data = [ 'lang' => strtolower( $tags[1] ), ]; if( array_key_exists(2,$tags) && $tags[2] ){ $data['region'] = strtoupper($tags[2]); } if( array_key_exists(3,$tags) && $tags[3] ){ $data['variant'] = strtolower($tags[3]); } return $data; }
|
||||
23
wp-content/plugins/loco-translate/lib/compiled/phpunit.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
/**
|
||||
* Downgraded for PHP 7.2 compatibility. Do not edit.
|
||||
* @noinspection ALL
|
||||
*/
|
||||
class LocoDomQueryFilter {
|
||||
private /*string*/ $tag = '';
|
||||
private /*array*/ $attr = [];
|
||||
public function __construct( string $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 ):iterable { 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, bool $recursive ):ArrayIterator { 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; } }
|
||||
class LocoDomQuery extends ArrayIterator {
|
||||
public static function parse( string $source ):DOMDocument { $dom = new DOMDocument('1.0', 'UTF-8' ); $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( $value instanceof DOMDocument ){ $value = [ $value->documentElement ]; } else if( $value instanceof DOMNode ){ $value = [ $value ]; } if( is_iterable($value) ){ $nodes = []; foreach( $value as $node ){ $nodes[] = $node; } } else if( is_string($value) || method_exists($value,'__toString') ){ $value = self::parse( $value ); $nodes = [ $value->documentElement ]; } else { $type = is_object($value) ? get_class($value) : gettype($value); throw new InvalidArgumentException('Cannot construct DOM from '.$type ); } parent::__construct( $nodes ); }
|
||||
public function eq( $index ):self { $q = new LocoDomQuery([]); if( $el = $this[$index] ){ $q[] = $el; } return $q; }
|
||||
public function find( $value ):self { $q = new LocoDomQuery( [] ); $f = new LocoDomQueryFilter($value); foreach( $this as $el ){ foreach( $f->filter($el) as $match ){ $q[] = $match; } } return $q; }
|
||||
public function children():self { $q = new LocoDomQuery([]); foreach( $this as $el ){ if( $el instanceof DOMNode ){ foreach( $el->childNodes as $child ) { $q[] = $child; } } } return $q; }
|
||||
public function text():string{ $s = ''; foreach( $this as $el ){ $s .= $el->textContent; } return $s; }
|
||||
public function html():string { $s = ''; foreach( $this as $outer ){ foreach( $outer->childNodes as $inner ){ $s .= $inner->ownerDocument->saveXML($inner); } break; } return $s; }
|
||||
public function attr( string $name ):?string { foreach( $this as $el ){ return $el->getAttribute($name); } return null; }
|
||||
public function hasClass( string $class ):bool { foreach( $this as $el ){ $classes = $el->getAttribute('class'); if( is_string($classes) && false !== strpos($classes,$class) ){ return true; } } return false; }
|
||||
public function getFormData():array { parse_str( $this->serializeForm(), $data ); return $data; }
|
||||
public function serializeForm():string { $pairs = []; foreach( ['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); } }
|
||||
5
wp-content/plugins/loco-translate/lib/data/gp.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
/**
|
||||
* Compiled data. Do not edit.
|
||||
*/
|
||||
return ['version'=>'4.0.0','aliases'=>['arg'=>'an','bg-bg'=>'bg','bn-bd'=>'bn','bre'=>'br','bs-ba'=>'bs','ca-valencia'=>'ca-val','cs-cz'=>'cs','da-dk'=>'da','de-de'=>'de','ewe'=>'ee','en-us'=>'en','es-es'=>'es','fa-ir'=>'fa','fr-fr'=>'fr','gl-es'=>'gl','haw-us'=>'haw','he-il'=>'he','hi-in'=>'hi','hu-hu'=>'hu','id-id'=>'id','is-is'=>'is','it-it'=>'it','jv-id'=>'jv','ka-ge'=>'ka','ko-kr'=>'ko','lb-lu'=>'lb','lt-lt'=>'lt','me-me'=>'me','mg-mg'=>'mg','mk-mk'=>'mk','ml-in'=>'ml','ms-my'=>'ms','my-mm'=>'mya','ne-np'=>'ne','nb-no'=>'nb','nl-nl'=>'nl','nn-no'=>'nn','pa-in'=>'pa','art-xpirate'=>'pirate','pl-pl'=>'pl','pt-pt'=>'pt','pt-pt-ao90'=>'pt-ao90','ro-ro'=>'ro','ru-ru'=>'ru','si-lk'=>'si','sk-sk'=>'sk','sl-si'=>'sl','so-so'=>'so','sr-rs'=>'sr','su-id'=>'su','sv-se'=>'sv','ta-in'=>'ta','tr-tr'=>'tr','tt-ru'=>'tt','ug-cn'=>'ug','uz-uz'=>'uz']];
|
||||
5
wp-content/plugins/loco-translate/lib/data/languages.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
/**
|
||||
* Compiled data. Do not edit.
|
||||
*/
|
||||
return ['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','bgn'=>'Western Balochi','bho'=>'Bhojpuri','bi'=>'Bislama','bm'=>'Bambara','bn'=>'Bengali','bo'=>'Tibetan','br'=>'Breton','brx'=>'Bodo (India)','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','dsb'=>'Lower Sorbian','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','fon'=>'Fon','fr'=>'French','frp'=>'Arpitan','fuc'=>'Pulaar','fur'=>'Friulian','fy'=>'Western Frisian','ga'=>'Irish','gax'=>'Borana-Arsi-Guji Oromo','gd'=>'Scottish Gaelic','gl'=>'Galician','gn'=>'Guarani','gu'=>'Gujarati','gv'=>'Manx','ha'=>'Hausa','haw'=>'Hawaiian','haz'=>'Hazaragi','he'=>'Hebrew','hi'=>'Hindi','ho'=>'Hiri Motu','hr'=>'Croatian','hsb'=>'Upper Sorbian','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','kaa'=>'Kara-Kalpak','kab'=>'Kabyle','kg'=>'Kongo','ki'=>'Kikuyu','kj'=>'Kuanyama','kk'=>'Kazakh','kl'=>'Kalaallisut','km'=>'Central Khmer','kmr'=>'Northern Kurdish','kn'=>'Kannada','ko'=>'Korean','kr'=>'Kanuri','ks'=>'Kashmiri','ku'=>'Kurdish','kv'=>'Komi','kw'=>'Cornish','ky'=>'Kirghiz','la'=>'Latin','lb'=>'Luxembourgish','lg'=>'Ganda','li'=>'Limburgan','lij'=>'Ligurian','lmo'=>'Lombard','ln'=>'Lingala','lo'=>'Lao','lt'=>'Lithuanian','lu'=>'Luba-Katanga','lv'=>'Latvian','mai'=>'Maithili','mfe'=>'Morisyen','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','nqo'=>'N\'Ko','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','pap'=>'Papiamento','pcd'=>'Picard','pcm'=>'Nigerian Pidgin','pi'=>'Pali','pl'=>'Polish','ps'=>'Pushto','pt'=>'Portuguese','qu'=>'Quechua','rhg'=>'Rohingya','rm'=>'Romansh','rn'=>'Rundi','ro'=>'Romanian','ru'=>'Russian','rw'=>'Kinyarwanda','sa'=>'Sanskrit','sah'=>'Yakut','sc'=>'Sardinian','scn'=>'Sicilian','sd'=>'Sindhi','se'=>'Northern Sami','sg'=>'Sango','sh'=>'Serbo-Croatian','si'=>'Sinhala','sk'=>'Slovak','skr'=>'Saraiki','sl'=>'Slovenian','sm'=>'Samoan','sn'=>'Shona','so'=>'Somali','sq'=>'Albanian','sr'=>'Serbian','ss'=>'Swati','st'=>'Southern Sotho','su'=>'Sundanese','sv'=>'Swedish','sw'=>'Swahili','syr'=>'Syriac','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','vec'=>'Venetian','vi'=>'Vietnamese','vo'=>'Volapük','wa'=>'Walloon','wo'=>'Wolof','xh'=>'Xhosa','yi'=>'Yiddish','yo'=>'Yoruba','za'=>'Zhuang','zgh'=>'Standard Moroccan Tamazight','zh'=>'Chinese','zu'=>'Zulu','tlh'=>'Klingon'];
|
||||
5
wp-content/plugins/loco-translate/lib/data/locales.php
Normal file
5
wp-content/plugins/loco-translate/lib/data/plurals.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
/**
|
||||
* Compiled data. Do not edit.
|
||||
*/
|
||||
return ['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,''=>[0=>[0=>'n != 1',1=>[1=>'one','0,2,3…'=>'other']],1=>[0=>'n > 1',1=>['0,1'=>'one','2,3,4…'=>'other']],2=>[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=>[0=>'zero',1=>'one',2=>'two','3,4,5…'=>'few','11,12,13…'=>'many','100,101,102…'=>'other']],3=>[0=>'(n%10==1 && n%100!=11 ? 0 : n%10 >= 2 && n%10<=4 &&(n%100<10||n%100 >= 20)? 1 : 2)',1=>['1,21,31…'=>'one','2,3,4…'=>'few','0,5,6…'=>'other']],4=>[0=>'0',1=>['0,1,2…'=>'other']],5=>[0=>'( n == 1 ) ? 0 : ( n >= 2 && n <= 4 ) ? 1 : 2',1=>[1=>'one','2,3,4'=>'few','0,5,6…'=>'other']],6=>[0=>'n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n==3 ? 3 : n==6 ? 4 : 5',1=>[0=>'zero',1=>'one',2=>'two',3=>'few',6=>'many','4,5,7…'=>'other']],7=>[0=>'n==1 ? 0 : n==2 ? 1 : n<7 ? 2 : n<11 ? 3 : 4',1=>[1=>'one',2=>'two','0,3,4…'=>'few','7,8,9…'=>'many','11,12,13…'=>'other']],8=>[0=>'n==1||n==11 ? 0 : n==2||n==12 ? 1 :(n >= 3 && n<=10)||(n >= 13 && n<=19)? 2 : 3',1=>['1,11'=>'one','2,12'=>'two','3,4,5…'=>'few','0,20,21…'=>'other']],9=>[0=>'n%10==1 ? 0 : n%10==2 ? 1 : n%20==0 ? 2 : 3',1=>['1,11,21…'=>'one','2,12,22…'=>'two','0,20,100…'=>'few','3,4,5…'=>'other']],10=>[0=>'n%10==1 && n%100!=11 ? 0 : n%10 >= 2 && n%10<=4 &&(n%100<10||n%100 >= 20)? 1 : 2',1=>['1,21,31…'=>'one','2,3,4…'=>'few','0,5,6…'=>'other']],11=>[0=>'n == 1 ? 0 : n == 2 ? 1 : 2',1=>[1=>'one',2=>'two','0,3,4…'=>'other']],12=>[0=>'(n%10==1 && n%100!=11 ? 0 : n%10 >= 2 &&(n%100<10||n%100 >= 20)? 1 : 2)',1=>['1,21,31…'=>'one','2,3,4…'=>'few','0,10,11…'=>'other']],13=>[0=>'n%10==0||( n%100 >= 11 && n%100<=19)? 0 :(n%10==1 && n%100!=11 ? 1 : 2)',1=>['0,10,11…'=>'zero','1,21,31…'=>'one','2,3,4…'=>'other']],14=>[0=>'( n % 10 == 1 && n % 100 != 11 ) ? 0 : 1',1=>['1,21,31…'=>'one','0,2,3…'=>'other']],15=>[0=>'(n==1 ? 0 : n==0||( n%100>1 && n%100<11)? 1 :(n%100>10 && n%100<20)? 2 : 3)',1=>[1=>'one','0,2,3…'=>'few','11,12,13…'=>'many','20,21,22…'=>'other']],16=>[0=>'(n==1 ? 0 : n%10 >= 2 && n%10<=4 &&(n%100<10||n%100 >= 20)? 1 : 2)',1=>[1=>'one','2,3,4…'=>'few','0,5,6…'=>'other']],17=>[0=>'(n==1 ? 0 :(((n%100>19)||(( n%100==0)&&(n!=0)))? 2 : 1))',1=>[1=>'one','0,2,3…'=>'few','20,21,22…'=>'other']],18=>[0=>'n%100==1 ? 0 : n%100==2 ? 1 : n%100==3||n%100==4 ? 2 : 3',1=>['1,101,201…'=>'one','2,102,202…'=>'two','3,4,103…'=>'few','0,5,6…'=>'other']]]];
|
||||
5
wp-content/plugins/loco-translate/lib/data/regions.php
Normal file
193
wp-content/plugins/loco-translate/loco.php
Normal file
@@ -0,0 +1,193 @@
|
||||
<?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.8.0
|
||||
Requires at least: 6.6
|
||||
Requires PHP: 7.4
|
||||
Tested up to: 6.8.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;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get absolute path to Loco primary plugin file
|
||||
*/
|
||||
function loco_plugin_file(): string {
|
||||
return __FILE__;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get version of this plugin
|
||||
*/
|
||||
function loco_plugin_version(): string {
|
||||
return '2.8.0';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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(): string {
|
||||
static $handle;
|
||||
isset($handle) or $handle = plugin_basename(__FILE__);
|
||||
return $handle;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get absolute path to plugin root directory
|
||||
*/
|
||||
function loco_plugin_root(): string {
|
||||
return __DIR__;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check whether currently running in debug mode
|
||||
*/
|
||||
function loco_debugging(): bool {
|
||||
return apply_filters('loco_debug', WP_DEBUG );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Whether currently processing an Ajax request
|
||||
*/
|
||||
function loco_doing_ajax(): bool {
|
||||
return defined('DOING_AJAX') && DOING_AJAX;
|
||||
}
|
||||
|
||||
|
||||
if( ! function_exists('loco_constant') ) {
|
||||
/**
|
||||
* Evaluate a constant by name
|
||||
* @return mixed
|
||||
*/
|
||||
function loco_constant( string $name ) {
|
||||
return defined($name) ? constant($name) : null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Runtime inclusion of any file under plugin root
|
||||
*
|
||||
* @param string $relpath PHP file path relative to __DIR__
|
||||
* @return mixed return value from included file
|
||||
*/
|
||||
function loco_include( string $relpath ){
|
||||
$path = loco_plugin_root().'/'.$relpath;
|
||||
if( ! file_exists($path) ){
|
||||
$message = 'File not found: '.$path;
|
||||
// debug specifics to error log in case full call stack not visible
|
||||
if( 'cli' !== PHP_SAPI ) {
|
||||
error_log( sprintf( '[Loco.debug] Failed on loco_include(%s). !file_exists(%s)', var_export($relpath,true), var_export($path,true) ) );
|
||||
}
|
||||
// handle circular file inclusion error if error class not found
|
||||
if( loco_class_exists('Loco_error_Exception') ){
|
||||
throw new Loco_error_Exception($message);
|
||||
}
|
||||
else {
|
||||
throw new Exception($message.'; additionally src/error/Exception.php not loadable');
|
||||
}
|
||||
}
|
||||
return include $path;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Require dependant library once only
|
||||
|
||||
* @param string $path PHP file path relative to ./lib
|
||||
*/
|
||||
function loco_require_lib( string $path ):void {
|
||||
require_once loco_plugin_root().'/lib/'.$path;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check PHP extension required by Loco and load polyfill if needed
|
||||
*/
|
||||
function loco_check_extension( string $name ): bool {
|
||||
static $cache = [];
|
||||
if( ! array_key_exists($name,$cache) ) {
|
||||
if( extension_loaded($name) ){
|
||||
$cache[$name] = true;
|
||||
}
|
||||
else {
|
||||
// translators: %s refers to the name of a missing PHP extension, for example "mbstring".
|
||||
Loco_error_AdminNotices::warn( sprintf( __('Loco Translate requires the "%s" PHP extension. Ask your hosting provider to install it','loco-translate'), $name ) );
|
||||
class_exists( ucfirst($name).'Extension' ); // <- pings Loco_hooks_AdminHooks::autoload_compat
|
||||
$cache[$name] = false;
|
||||
}
|
||||
}
|
||||
return $cache[$name];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Class autoloader for Loco classes under src directory.
|
||||
* e.g. class "Loco_foo_Bar" will be found in "src/foo/Bar.php"
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
function loco_autoload( string $name ):void {
|
||||
if( 'Loco_' === substr($name,0,5) ){
|
||||
loco_include( 'src/'.strtr( substr($name,5), '_', '/' ).'.php' );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* class_exists wrapper that fails silently.
|
||||
*/
|
||||
function loco_class_exists( string $class ): bool {
|
||||
try {
|
||||
return class_exists($class);
|
||||
}
|
||||
catch( Throwable $e ){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Startup errors will raise notices. Check your error logs if error reporting is quiet
|
||||
try {
|
||||
spl_autoload_register('loco_autoload');
|
||||
|
||||
// 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. Set constant empty to disable
|
||||
if( LOCO_LANG_DIR ){
|
||||
new Loco_hooks_LoadHelper;
|
||||
}
|
||||
|
||||
// initialize hooks for admin screens
|
||||
if ( is_admin() ) {
|
||||
new Loco_hooks_AdminHooks;
|
||||
}
|
||||
|
||||
// enable wp cli commands
|
||||
if( class_exists('WP_CLI',false) ) {
|
||||
WP_CLI::add_command('loco','Loco_cli_Commands');
|
||||
}
|
||||
|
||||
}
|
||||
catch( Throwable $e ){
|
||||
trigger_error(sprintf('[Loco.fatal] %s in %s:%u',$e->getMessage(), $e->getFile(), $e->getLine() ) );
|
||||
}
|
||||
24
wp-content/plugins/loco-translate/loco.xml
Normal 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>
|
||||
1
wp-content/plugins/loco-translate/pub/css/admin.css
Normal file
1
wp-content/plugins/loco-translate/pub/css/bundle.css
Normal 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}}
|
||||
1
wp-content/plugins/loco-translate/pub/css/config.css
Normal 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}
|
||||
1
wp-content/plugins/loco-translate/pub/css/editor.css
Normal file
1
wp-content/plugins/loco-translate/pub/css/fileinfo.css
Normal file
@@ -0,0 +1 @@
|
||||
#loco-admin.wrap .panel-info nav,#loco-admin.wrap .notice-info nav{display:block;position:absolute;right:0;top:0;font-size:1.3em;padding:1em}#loco-admin.wrap .panel-info nav a,#loco-admin.wrap .notice-info nav a{color:#666;margin-left:10px}#loco-admin.wrap .panel-info nav a:hover,#loco-admin.wrap .notice-info nav a:hover{color:#000;text-decoration:none}#loco-admin.wrap .panel-info dl,#loco-admin.wrap .notice-info dl{margin-top:0;display:inline-block}#loco-admin.wrap .panel-info dl dt,#loco-admin.wrap .panel-info dl dd,#loco-admin.wrap .notice-info dl dt,#loco-admin.wrap .notice-info dl dd{line-height:1.4em}#loco-admin.wrap .panel-info dl dt,#loco-admin.wrap .notice-info dl dt{font-weight:bold;color:#555}#loco-admin.wrap .panel-info dl dd,#loco-admin.wrap .notice-info dl dd{margin-left:0;margin-bottom:.8em}#loco-admin.wrap .panel-info dl div.progress .l,#loco-admin.wrap .notice-info dl div.progress .l{display:none}
|
||||
1
wp-content/plugins/loco-translate/pub/css/locale.css
Normal file
@@ -0,0 +1 @@
|
||||
#loco-admin.wrap td.loco-not-active{color:#aaa}#loco-admin.wrap div.loco-projects>h3{float:left}#loco-admin.wrap div.loco-projects form.loco-filter{float:right;margin:1em 0}
|
||||
1
wp-content/plugins/loco-translate/pub/css/podiff.css
Normal file
@@ -0,0 +1 @@
|
||||
#loco-admin.wrap .revisions-diff{padding:10px;min-height:20px}#loco-admin.wrap table.diff{border-collapse:collapse;table-layout:auto}#loco-admin.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-admin.wrap table.diff td>span{color:#aaa}#loco-admin.wrap table.diff td>span:after{content:". "}#loco-admin.wrap table.diff tbody{border-top:1px dashed #ccc}#loco-admin.wrap table.diff tbody:first-child{border-top:none}#loco-admin.wrap table.diff td>.dashicons{display:none}#loco-admin.wrap .revisions.loading .diff-meta{color:#eee}#loco-admin.wrap .revisions.loading .loading-indicator span.spinner{visibility:visible;background:#fff url(../img/spin-modal.gif?v=2.8.0) center center no-repeat}@media only screen and (-webkit-min-device-pixel-ratio: 2),only screen and (min--moz-device-pixel-ratio: 2),only screen and (-o-min-device-pixel-ratio: 2/1),only screen and (min-device-pixel-ratio: 2),only screen and (min-resolution: 192dpi),only screen and (min-resolution: 2dppx){#loco-admin.wrap .revisions.loading .loading-indicator span.spinner{background-size:16px;background-image:url(../img/spin-modal@2x.gif?v=2.8.0)}}#loco-admin.wrap .revisions-meta{clear:both;padding:10px 12px;margin:0;position:relative;top:10px}#loco-admin.wrap .revisions-meta .diff-meta{clear:none;float:left;width:50%;padding:0;min-height:auto}#loco-admin.wrap .revisions-meta .diff-meta button{margin-top:5px}#loco-admin.wrap .revisions-meta .diff-meta-current{float:right;text-align:right}#loco-admin.wrap .revisions-meta time{color:#72777c}#loco-admin.wrap .revisions-control-frame{margin:10px 0}#loco-admin.wrap .revisions-diff-frame{margin-top:20px}
|
||||
1
wp-content/plugins/loco-translate/pub/css/poinit.css
Normal 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}
|
||||
1
wp-content/plugins/loco-translate/pub/css/poview.css
Normal file
@@ -0,0 +1 @@
|
||||
.js #loco-admin.wrap .loco-loading{min-height:100px;background:#fff url(../img/spin-modal.gif?v=2.8.0) center center no-repeat}@media only screen and (-webkit-min-device-pixel-ratio: 2),only screen and (min--moz-device-pixel-ratio: 2),only screen and (-o-min-device-pixel-ratio: 2/1),only screen and (min-device-pixel-ratio: 2),only screen and (min-resolution: 192dpi),only screen and (min-resolution: 2dppx){.js #loco-admin.wrap .loco-loading{background-size:16px;background-image:url(../img/spin-modal@2x.gif?v=2.8.0)}}.js #loco-admin.wrap .loco-loading ol.msgcat{display:none}#loco-admin.wrap #loco-po{padding-right:0;overflow:auto}#loco-admin.wrap ol.msgcat{margin-left:3em;padding-top:1em;border-top:1px dashed #ccc}#loco-admin.wrap ol.msgcat:first-child{padding-top:0;border-top:none}#loco-admin.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-admin.wrap ol.msgcat li>*{color:#333;white-space:pre}#loco-admin.wrap ol.msgcat li>.po-comment{color:#3cc200}#loco-admin.wrap ol.msgcat li>.po-refs{color:#0073aa}#loco-admin.wrap ol.msgcat li>.po-refs a{color:inherit;text-decoration:none}#loco-admin.wrap ol.msgcat li>.po-refs a:hover{text-decoration:underline}#loco-admin.wrap ol.msgcat li>.po-flags{color:#77904a}#loco-admin.wrap ol.msgcat li>.po-flags em{font-style:normal}#loco-admin.wrap ol.msgcat li>.po-word{color:#000}#loco-admin.wrap ol.msgcat li>.po-junk{font-style:italic;color:#ccc}#loco-admin.wrap ol.msgcat li>.po-string>span{color:#c931c7}#loco-admin.wrap form.loco-filter{top:0;right:0;position:absolute}#loco-admin.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-admin.wrap .loco-invalid ol.msgcat{list-style-type:none}#loco-admin.wrap .loco-invalid ol.msgcat li{color:#000}
|
||||
1
wp-content/plugins/loco-translate/pub/css/skins/blue.css
Normal 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)}.wp-core-ui .button-primary.loco-loading[disabled]{color:hsl(38.0392156863,10%,80%) !important;background:rgb(219.2535211268,152.5267605634,36.9464788732) !important;border-color:rgb(219.2535211268,152.5267605634,36.9464788732) !important}.wp-core-ui .button-primary.loco-loading[disabled]:before{background:rgba(0,0,0,0) url(../../img/skins/blue/spin-primary-button.gif?v=2.8.0) 0 0 no-repeat !important}@media only screen and (-webkit-min-device-pixel-ratio: 2),only screen and (min--moz-device-pixel-ratio: 2),only screen and (-o-min-device-pixel-ratio: 2/1),only screen and (min-device-pixel-ratio: 2),only screen and (min-resolution: 192dpi),only screen and (min-resolution: 2dppx){.wp-core-ui .button-primary.loco-loading[disabled]:before{background-size:16px !important;background-image:url(../../img/skins/blue/spin-primary-button@2x.gif?v=2.8.0) !important}}.debug{color:#e1a948}
|
||||
@@ -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)}.wp-core-ui .button-primary.loco-loading[disabled]{color:hsl(27.0967741935,10%,80%) !important;background:hsl(27.0967741935,35.632183908%,57.8823529412%) !important;border-color:hsl(27.0967741935,35.632183908%,57.8823529412%) !important}.wp-core-ui .button-primary.loco-loading[disabled]:before{background:rgba(0,0,0,0) url(../../img/skins/coffee/spin-primary-button.gif?v=2.8.0) 0 0 no-repeat !important}@media only screen and (-webkit-min-device-pixel-ratio: 2),only screen and (min--moz-device-pixel-ratio: 2),only screen and (-o-min-device-pixel-ratio: 2/1),only screen and (min-device-pixel-ratio: 2),only screen and (min-resolution: 192dpi),only screen and (min-resolution: 2dppx){.wp-core-ui .button-primary.loco-loading[disabled]:before{background-size:16px !important;background-image:url(../../img/skins/coffee/spin-primary-button@2x.gif?v=2.8.0) !important}}.debug{color:#c7a589}
|
||||
@@ -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)}.wp-core-ui .button-primary.loco-loading[disabled]{color:hsl(70.5263157895,10%,80%) !important;background:rgb(136.6095238095,153.3714285714,57.8285714286) !important;border-color:rgb(136.6095238095,153.3714285714,57.8285714286) !important}.wp-core-ui .button-primary.loco-loading[disabled]:before{background:rgba(0,0,0,0) url(../../img/skins/ectoplasm/spin-primary-button.gif?v=2.8.0) 0 0 no-repeat !important}@media only screen and (-webkit-min-device-pixel-ratio: 2),only screen and (min--moz-device-pixel-ratio: 2),only screen and (-o-min-device-pixel-ratio: 2/1),only screen and (min-device-pixel-ratio: 2),only screen and (min-resolution: 192dpi),only screen and (min-resolution: 2dppx){.wp-core-ui .button-primary.loco-loading[disabled]:before{background-size:16px !important;background-image:url(../../img/skins/ectoplasm/spin-primary-button@2x.gif?v=2.8.0) !important}}.debug{color:#a3b745}
|
||||
@@ -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)}.wp-core-ui .button-primary.loco-loading[disabled]{color:hsl(192,10%,80%) !important;background:hsl(192,96.1538461538%,32.7843137255%) !important;border-color:hsl(192,96.1538461538%,32.7843137255%) !important}.wp-core-ui .button-primary.loco-loading[disabled]:before{background:rgba(0,0,0,0) url(../../img/skins/light/spin-primary-button.gif?v=2.8.0) 0 0 no-repeat !important}@media only screen and (-webkit-min-device-pixel-ratio: 2),only screen and (min--moz-device-pixel-ratio: 2),only screen and (-o-min-device-pixel-ratio: 2/1),only screen and (min-device-pixel-ratio: 2),only screen and (min-resolution: 192dpi),only screen and (min-resolution: 2dppx){.wp-core-ui .button-primary.loco-loading[disabled]:before{background-size:16px !important;background-image:url(../../img/skins/light/spin-primary-button@2x.gif?v=2.8.0) !important}}.debug{color:#04a4cc}
|
||||
@@ -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)}.wp-core-ui .button-primary.loco-loading[disabled]{color:hsl(3.7974683544,10%,80%) !important;background:rgb(216.6311926606,46.0917431193,34.5688073394) !important;border-color:rgb(216.6311926606,46.0917431193,34.5688073394) !important}.wp-core-ui .button-primary.loco-loading[disabled]:before{background:rgba(0,0,0,0) url(../../img/skins/midnight/spin-primary-button.gif?v=2.8.0) 0 0 no-repeat !important}@media only screen and (-webkit-min-device-pixel-ratio: 2),only screen and (min--moz-device-pixel-ratio: 2),only screen and (-o-min-device-pixel-ratio: 2/1),only screen and (min-device-pixel-ratio: 2),only screen and (min-resolution: 192dpi),only screen and (min-resolution: 2dppx){.wp-core-ui .button-primary.loco-loading[disabled]:before{background-size:16px !important;background-image:url(../../img/skins/midnight/spin-primary-button@2x.gif?v=2.8.0) !important}}.debug{color:#e14d43}
|
||||
@@ -0,0 +1 @@
|
||||
.wrap #loco-editor .is-table .wg-tr:nth-child(even){background-color:rgba(56,88,233,.05)}.wrap #loco-editor .wg-split-x>nav.wg-tabs>a.active,.wrap #loco-editor .is-table .wg-cols>div>div.selected{background-color:#3858e9}.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)}.wp-core-ui .button-primary.loco-loading[disabled]{color:hsl(229.1525423729,10%,80%) !important;background:rgb(24.7076923077,60.6461538462,223.4923076923) !important;border-color:rgb(24.7076923077,60.6461538462,223.4923076923) !important}.wp-core-ui .button-primary.loco-loading[disabled]:before{background:rgba(0,0,0,0) url(../../img/skins/modern/spin-primary-button.gif?v=2.8.0) 0 0 no-repeat !important}@media only screen and (-webkit-min-device-pixel-ratio: 2),only screen and (min--moz-device-pixel-ratio: 2),only screen and (-o-min-device-pixel-ratio: 2/1),only screen and (min-device-pixel-ratio: 2),only screen and (min-resolution: 192dpi),only screen and (min-resolution: 2dppx){.wp-core-ui .button-primary.loco-loading[disabled]:before{background-size:16px !important;background-image:url(../../img/skins/modern/spin-primary-button@2x.gif?v=2.8.0) !important}}.debug{color:#3858e9}
|
||||
@@ -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)}.wp-core-ui .button-primary.loco-loading[disabled]{color:hsl(124.2857142857,10%,80%) !important;background:rgb(134.1590361446,169.0409638554,136.6506024096) !important;border-color:rgb(134.1590361446,169.0409638554,136.6506024096) !important}.wp-core-ui .button-primary.loco-loading[disabled]:before{background:rgba(0,0,0,0) url(../../img/skins/ocean/spin-primary-button.gif?v=2.8.0) 0 0 no-repeat !important}@media only screen and (-webkit-min-device-pixel-ratio: 2),only screen and (min--moz-device-pixel-ratio: 2),only screen and (-o-min-device-pixel-ratio: 2/1),only screen and (min-device-pixel-ratio: 2),only screen and (min-resolution: 192dpi),only screen and (min-resolution: 2dppx){.wp-core-ui .button-primary.loco-loading[disabled]:before{background-size:16px !important;background-image:url(../../img/skins/ocean/spin-primary-button@2x.gif?v=2.8.0) !important}}.debug{color:#9ebaa0}
|
||||
@@ -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)}.wp-core-ui .button-primary.loco-loading[disabled]{color:hsl(26.2962962963,10%,80%) !important;background:rgb(203.84,109.2,35.36) !important;border-color:rgb(203.84,109.2,35.36) !important}.wp-core-ui .button-primary.loco-loading[disabled]:before{background:rgba(0,0,0,0) url(../../img/skins/sunrise/spin-primary-button.gif?v=2.8.0) 0 0 no-repeat !important}@media only screen and (-webkit-min-device-pixel-ratio: 2),only screen and (min--moz-device-pixel-ratio: 2),only screen and (-o-min-device-pixel-ratio: 2/1),only screen and (min-device-pixel-ratio: 2),only screen and (min-resolution: 192dpi),only screen and (min-resolution: 2dppx){.wp-core-ui .button-primary.loco-loading[disabled]:before{background-size:16px !important;background-image:url(../../img/skins/sunrise/spin-primary-button@2x.gif?v=2.8.0) !important}}.debug{color:#dd823b}
|
||||
BIN
wp-content/plugins/loco-translate/pub/font/loco.eot
Normal file
124
wp-content/plugins/loco-translate/pub/font/loco.svg
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
wp-content/plugins/loco-translate/pub/font/loco.ttf
Normal file
BIN
wp-content/plugins/loco-translate/pub/font/loco.woff
Normal file
BIN
wp-content/plugins/loco-translate/pub/img/api/deepl.png
Normal file
|
After Width: | Height: | Size: 668 B |
BIN
wp-content/plugins/loco-translate/pub/img/api/deepl@2x.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
wp-content/plugins/loco-translate/pub/img/api/google.png
Normal file
|
After Width: | Height: | Size: 580 B |
BIN
wp-content/plugins/loco-translate/pub/img/api/google@2x.png
Normal file
|
After Width: | Height: | Size: 1001 B |
BIN
wp-content/plugins/loco-translate/pub/img/api/microsoft.png
Normal file
|
After Width: | Height: | Size: 742 B |
BIN
wp-content/plugins/loco-translate/pub/img/api/microsoft@2x.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
wp-content/plugins/loco-translate/pub/img/flags-16.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
wp-content/plugins/loco-translate/pub/img/flags-32.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
wp-content/plugins/loco-translate/pub/img/logo-foot.gif
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
BIN
wp-content/plugins/loco-translate/pub/img/spin-editor-button.gif
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
BIN
wp-content/plugins/loco-translate/pub/img/spin-modal.gif
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
wp-content/plugins/loco-translate/pub/img/spin-modal@2x.gif
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
wp-content/plugins/loco-translate/pub/img/wg/splitx.png
Normal file
|
After Width: | Height: | Size: 87 B |
BIN
wp-content/plugins/loco-translate/pub/img/wg/splity.png
Normal file
|
After Width: | Height: | Size: 90 B |
5478
wp-content/plugins/loco-translate/pub/js/min/admin.js
Normal file
51
wp-content/plugins/loco-translate/pub/js/min/config.js
Normal file
@@ -0,0 +1,51 @@
|
||||
"use strict";
|
||||
|
||||
!function(n, p, d) {
|
||||
function h(a, b) {
|
||||
let c = a.offsetTop;
|
||||
for (;(a = a.offsetParent) && a !== b; ) c += a.offsetTop;
|
||||
return c;
|
||||
}
|
||||
function k() {
|
||||
function a(e, f) {
|
||||
e = f.name.replace("[0]", l);
|
||||
d(f).attr("name", e).val("");
|
||||
}
|
||||
var b = d("#loco-conf > div");
|
||||
let c = b.eq(0).clone();
|
||||
b = b.length;
|
||||
let l = "[" + b + "]";
|
||||
c.attr("id", "loco-conf-" + b);
|
||||
c.find("input").each(a);
|
||||
c.find("textarea").each(a);
|
||||
c.find("h2").eq(0).html("New set <span>(untitled)</span>");
|
||||
c.insertBefore("#loco-form-foot");
|
||||
g(c.find("a.icon-del"), b);
|
||||
c.hide().slideDown(500);
|
||||
d("html, body").animate({
|
||||
scrollTop: h(c[0])
|
||||
}, 500);
|
||||
}
|
||||
function g(a, b) {
|
||||
return a.on("click", function(c) {
|
||||
c.preventDefault();
|
||||
m(b);
|
||||
return !1;
|
||||
});
|
||||
}
|
||||
function m(a) {
|
||||
var b = d("#loco-conf-" + a);
|
||||
b.find('input[name="conf[' + a + '][removed]"]').val("1");
|
||||
b.slideUp(500, function() {
|
||||
d(this).hide().find("table").remove();
|
||||
});
|
||||
}
|
||||
d("#loco-conf > div").each(function(a, b) {
|
||||
g(d(b).find("a.icon-del"), a);
|
||||
});
|
||||
d("#loco-add-butt").attr("disabled", !1).on("click", function(a) {
|
||||
a.preventDefault();
|
||||
k();
|
||||
return !1;
|
||||
});
|
||||
}(window, document, window.jQuery);
|
||||
1
wp-content/plugins/loco-translate/pub/js/min/debug.js
Normal file
@@ -0,0 +1 @@
|
||||
"use strict";
|
||||
7
wp-content/plugins/loco-translate/pub/js/min/delete.js
Normal file
@@ -0,0 +1,7 @@
|
||||
"use strict";
|
||||
|
||||
!function(c, a) {
|
||||
let b = a.getElementById("loco-fs");
|
||||
a = a.getElementById("loco-del");
|
||||
b && a && c.loco.fs.init(b).setForm(a);
|
||||
}(window, document);
|
||||
582
wp-content/plugins/loco-translate/pub/js/min/editor.js
Normal file
@@ -0,0 +1,582 @@
|
||||
"use strict";
|
||||
|
||||
!function(C, d) {
|
||||
function u(a) {
|
||||
return k.l10n._(a);
|
||||
}
|
||||
function G(a, b, c) {
|
||||
return k.l10n.n(a, b, c);
|
||||
}
|
||||
function z(a) {
|
||||
return a.format(0, ".", Ea);
|
||||
}
|
||||
function Fa(a) {
|
||||
k.ajax.post("sync", ma, function(b) {
|
||||
const c = [];
|
||||
var e = b.pot, h = b.po;
|
||||
const t = b.done || {
|
||||
add: [],
|
||||
del: [],
|
||||
fuz: []
|
||||
};
|
||||
var n = t.add.length;
|
||||
const r = t.del.length, A = t.fuz.length, D = t.trn || 0;
|
||||
B.clear().load(h);
|
||||
f.load(B);
|
||||
da(f);
|
||||
if (n || r || A || D) {
|
||||
if (e ? c.push(v(u("Merged from %s"), e)) : c.push(u("Merged from source code")),
|
||||
n && c.push(v(G("%s new string added", "%s new strings added", n), z(n))), r && c.push(v(G("%s obsolete string removed", "%s obsolete strings removed", r), z(r))),
|
||||
A && c.push(v(G("%s string marked Fuzzy", "%s strings marked Fuzzy", A), z(A))),
|
||||
D && c.push(v(G("%s translation copied", "%s translations copied", D), z(D))), d(H).trigger("poUnsaved", []),
|
||||
T(), Ga && C.console) {
|
||||
e = console;
|
||||
h = -1;
|
||||
for (n = t.add.length; ++h < n; ) e.log(" + " + String(t.add[h]));
|
||||
n = t.del.length;
|
||||
for (h = 0; h < n; h++) e.log(" - " + String(t.del[h]));
|
||||
n = t.fuz.length;
|
||||
for (h = 0; h < n; h++) e.log(" ~ " + String(t.fuz[h]));
|
||||
}
|
||||
} else e ? c.push(v(u("Strings up to date with %s"), e)) : c.push(u("Strings up to date with source code"));
|
||||
k.notices.success(c.join(". "));
|
||||
d(H).trigger("poMerge", [ b ]);
|
||||
a && a();
|
||||
}, a);
|
||||
}
|
||||
function Ha(a) {
|
||||
const b = a.currentTarget;
|
||||
a.stopImmediatePropagation();
|
||||
b.disabled = !0;
|
||||
na();
|
||||
b.disabled = !1;
|
||||
}
|
||||
function na() {
|
||||
const a = [];
|
||||
B.each(function(b, c) {
|
||||
f.validate(c) && a.push(c);
|
||||
});
|
||||
k.notices.clear();
|
||||
oa(a);
|
||||
}
|
||||
function da(a) {
|
||||
a.invalid && (oa(a.invalid), a.invalid = null);
|
||||
}
|
||||
function oa(a) {
|
||||
const b = a.length;
|
||||
if (0 === b) k.notices.success(u("No formatting errors detected")); else {
|
||||
const c = [ v(G("%s possible error detected", "%s possible errors detected", b), b), u("Check the translations marked with a warning sign") ];
|
||||
k.notices.warn(c.join(". ")).slow();
|
||||
}
|
||||
0 < b && f.current(a[0]);
|
||||
}
|
||||
function Ia(a) {
|
||||
const b = a.id, c = k.apis, e = c.providers();
|
||||
return c.create(a, e[b] || e._);
|
||||
}
|
||||
function pa() {
|
||||
for (var a = -1, b, c = [], e = L, h = e.length, t = String(Ja); ++a < h; ) try {
|
||||
b = e[a], null == b.src && (b.src = t), c.push(Ia(b));
|
||||
} catch (n) {
|
||||
k.notices.error(String(n));
|
||||
}
|
||||
return c;
|
||||
}
|
||||
function qa(a) {
|
||||
function b(e) {
|
||||
ea = new Date().getTime();
|
||||
L = e && e.apis || [];
|
||||
0 === L.length ? P = fa("loco-apis-empty", e.html) : U = fa("loco-apis-batch", e.html);
|
||||
c.remove();
|
||||
a(L);
|
||||
}
|
||||
if (V || ra) k.notices.error("APIs not available in current mode"); else if (null == L || 0 === L.length || 10 < Math.round((new Date().getTime() - ea) / 1e3)) {
|
||||
P && P.remove();
|
||||
P = null;
|
||||
U && U.remove();
|
||||
U = null;
|
||||
W && W.remove();
|
||||
L = W = null;
|
||||
var c = d('<div><div class="loco-loading"></div></div>').dialog({
|
||||
dialogClass: "loco-modal loco-modal-no-close",
|
||||
appendTo: "#loco-admin.wrap",
|
||||
title: "Loading..",
|
||||
modal: !0,
|
||||
autoOpen: !0,
|
||||
closeOnEscape: !1,
|
||||
resizable: !1,
|
||||
draggable: !1,
|
||||
position: sa,
|
||||
height: 200
|
||||
});
|
||||
k.ajax.get("apis", {
|
||||
locale: String(E)
|
||||
}, b);
|
||||
} else ea = new Date().getTime(), a(L);
|
||||
}
|
||||
function fa(a, b) {
|
||||
b = d(b);
|
||||
b.attr("id", a);
|
||||
b.dialog({
|
||||
dialogClass: "loco-modal",
|
||||
appendTo: "#loco-admin.wrap",
|
||||
title: b.attr("title"),
|
||||
modal: !0,
|
||||
autoOpen: !1,
|
||||
closeOnEscape: !0,
|
||||
resizable: !1,
|
||||
draggable: !1,
|
||||
position: sa
|
||||
});
|
||||
return b;
|
||||
}
|
||||
function ta() {
|
||||
qa(function(a) {
|
||||
a.length ? Ka() : ua();
|
||||
});
|
||||
}
|
||||
function La(a) {
|
||||
a.preventDefault();
|
||||
qa(function(b) {
|
||||
b.length ? Ma() : ua();
|
||||
});
|
||||
return !1;
|
||||
}
|
||||
function ua() {
|
||||
P ? P.dialog("open") : k.notices.error("Logic error. Unconfigured API modal missing");
|
||||
}
|
||||
function Ma() {
|
||||
function a(g) {
|
||||
a: {
|
||||
var q = d(g.api).val();
|
||||
for (var Q, M = Z || (Z = pa()), R = M.length, S = -1; ++S < R; ) if (Q = M[S],
|
||||
Q.getId() === q) {
|
||||
q = Q;
|
||||
break a;
|
||||
}
|
||||
k.notices.error("No " + q + " client");
|
||||
q = void 0;
|
||||
}
|
||||
g = g.existing.checked;
|
||||
N.text("Calculating....");
|
||||
h = k.apis.createJob(q);
|
||||
h.init(B, g);
|
||||
t = q.toString();
|
||||
N.text(v(u("%s unique source strings."), z(h.length)) + " " + v(u("%s characters will be sent for translation."), z(h.chars)));
|
||||
F[0].disabled = h.length ? !1 : !0;
|
||||
n = null;
|
||||
}
|
||||
function b(g) {
|
||||
h && (D && g.fuzzy(0, !0), f.pasteMessage(g), g === f.active && f.setStatus(g),
|
||||
f.unsave(g, 0), r++, A && !g.valid() && (A = !1));
|
||||
}
|
||||
function c(g, q) {
|
||||
g = q ? 100 * g / q : 0;
|
||||
N.text(v(u("Translation progress %s%%"), z(g)));
|
||||
}
|
||||
function e() {
|
||||
F.removeClass("loco-loading");
|
||||
if (h && n) {
|
||||
var g = n.todo();
|
||||
g && k.notices.warn(v(G("Translation job aborted with %s string remaining", "Translation job aborted with %s strings remaining", g), z(g))).slow();
|
||||
g = [];
|
||||
const q = n.did();
|
||||
q && g.push(v(G("%1$s string translated via %2$s", "%1$s strings translated via %2$s", q), z(q), t));
|
||||
r ? g.push(v(G("%s string updated", "%s strings updated", r), z(r))) : q && g.push(u("Nothing needed updating"));
|
||||
g.length && k.notices.success(g.join(". ")).slow();
|
||||
n = h = null;
|
||||
}
|
||||
r && (T(), f.rebuildSearch());
|
||||
I && (I.off("dialogclose").dialog("close"), I = null);
|
||||
f.fire("poAuto");
|
||||
A || na();
|
||||
}
|
||||
let h, t, n, r = 0, A = !0, D = !1, I = U.dialog("open");
|
||||
const y = I.find("form"), F = y.find("button.button-primary"), N = d("#loco-job-progress");
|
||||
F.removeClass("loco-loading");
|
||||
F[0].disabled = !0;
|
||||
k.notices.clear();
|
||||
y.off("submit change");
|
||||
a(y[0]);
|
||||
y.on("change", function(g) {
|
||||
g = g.target;
|
||||
const q = g.name;
|
||||
"api" !== q && "existing" !== q || a(g.form);
|
||||
return !0;
|
||||
}).on("submit", function(g) {
|
||||
g.preventDefault();
|
||||
F.addClass("loco-loading");
|
||||
F[0].disabled = !0;
|
||||
r = 0;
|
||||
c(0);
|
||||
D = g.target.fuzzy.checked;
|
||||
n = h.dispatch(B).done(e).each(b).prog(c).stat();
|
||||
});
|
||||
I.off("dialogclose").on("dialogclose", function() {
|
||||
h.abort();
|
||||
I = null;
|
||||
e();
|
||||
});
|
||||
}
|
||||
function Ka() {
|
||||
function a(l) {
|
||||
if (l.isDefaultPrevented()) return !1;
|
||||
var p = l.which;
|
||||
let m = -1;
|
||||
49 <= p && 57 >= p ? m = p - 49 : 97 <= p && 105 >= p && (m = p - 97);
|
||||
return 0 <= m && 9 > m && (p = g && g.find("button.button-primary").eq(m)) && 1 === p.length ? (p.click(),
|
||||
l.preventDefault(), l.stopPropagation(), !1) : !0;
|
||||
}
|
||||
function b(l, p) {
|
||||
return function(m) {
|
||||
m.preventDefault();
|
||||
m.stopPropagation();
|
||||
h();
|
||||
m = f.current();
|
||||
const w = f.getTargetOffset();
|
||||
m.translate(p, w);
|
||||
f.focus().reloadMessage(m);
|
||||
};
|
||||
}
|
||||
function c(l, p, m, w) {
|
||||
let J = w.getId(), O = R[J], va = String(O + 1), Na = w.getUrl(), wa = u("Use this translation");
|
||||
w = String(w);
|
||||
let xa = X && X[J];
|
||||
l = d('<button class="button button-primary"></button>').attr("tabindex", String(1 + N + O)).on("click", b(l, p));
|
||||
l.attr("accesskey", va);
|
||||
1 < q.length && (wa += " (" + va + ")");
|
||||
l.text(wa);
|
||||
xa && xa.replaceWith(d('<div class="loco-api loco-api-result loco-api-' + J + '"></div>').append(d('<div class="loco-api-credit">Translated by </div>').append(d('<a target="_blank" tabindex="-1"></a>').attr("href", Na).text(w))).append(d("<blockquote " + I + "></blockquote>").text(p || "FAILED")).append(l));
|
||||
++S === Q && (g && g.dialog("option", "title", u("Suggested translations") + " — " + m.label),
|
||||
N += S, y.attr("disabled") && y.attr("disabled", !1));
|
||||
0 === O && l.focus();
|
||||
}
|
||||
function e(l) {
|
||||
const p = d('<div class="loco-api loco-api-loading"></div>').text("Calling " + l + " ...");
|
||||
return X[l.getId()] = p;
|
||||
}
|
||||
function h(l) {
|
||||
g && null == l && g.dialog("close");
|
||||
X = R = g = null;
|
||||
d(C).off("keydown", a);
|
||||
}
|
||||
function t(l) {
|
||||
return function(p, m, w) {
|
||||
M[l.getId()] = m;
|
||||
c(p, m, w, l);
|
||||
};
|
||||
}
|
||||
function n(l) {
|
||||
const p = r.notes(), m = r.context();
|
||||
var w = m + "" + l;
|
||||
M = ya[w] || (ya[w] = {});
|
||||
for (w = -1; ++w < Q; ) {
|
||||
const J = q[w], O = J.getId();
|
||||
g.append(e(J));
|
||||
R[O] = w;
|
||||
M[O] ? c(l, M[O], E, J) : J.translate({
|
||||
source: l,
|
||||
context: m,
|
||||
notes: p
|
||||
}, E, t(J));
|
||||
}
|
||||
}
|
||||
const r = f.current();
|
||||
if (!r) return !1;
|
||||
var A = r.pluralized();
|
||||
const D = A ? Math.min(f.getTargetOffset(), 1) : 0, I = 'lang="' + String(E) + '" dir="' + (E.isRTL() ? "RTL" : "LTR") + '"';
|
||||
let y, F = r.source(null, D);
|
||||
A ? (y = d('<select lang="en" name="s" disabled></select>'), r.eachSrc(function(l, p) {
|
||||
var m = f.t();
|
||||
m = l ? m._x("Plural", "Editor") : m._x("Single", "Editor");
|
||||
m = d("<optgroup></optgroup>").attr("label", m);
|
||||
y.append(m.append(d("<option></option>").attr("value", String(l)).text(p)));
|
||||
}), y.val(String(D)), y.on("change", function(l) {
|
||||
g.find("div.loco-api-result").remove();
|
||||
X = {};
|
||||
R = {};
|
||||
S = 0;
|
||||
F = r.source(null, l.target.selectedIndex);
|
||||
y.attr("disabled", "true");
|
||||
n(F);
|
||||
})) : y = d('<blockquote lang="en"></blockquote>').text(F);
|
||||
let N = 99, g = (W || (W = fa("loco-apis-hint", "<div></div>"))).html("").append(d('<div class="loco-api"><p>Source text:</p></div>').append(y)).dialog("option", "title", u("Loading suggestions") + "...").off("dialogclose").on("dialogclose", h).dialog("open");
|
||||
(A = r.translation(D)) && d('<div class="loco-api"><p>Current translation:</p></div>').append(d("<blockquote " + I + "></blockquote>").text(A)).append(d('<button class="button"></button>').attr("tabindex", String(++N)).text(u("Keep this translation")).on("click", function(l) {
|
||||
l.preventDefault();
|
||||
h();
|
||||
})).appendTo(g);
|
||||
const q = Z || (Z = pa()), Q = q.length;
|
||||
let M, R = {}, S = 0, X = {};
|
||||
n(F);
|
||||
d(C).on("keydown", a);
|
||||
return !0;
|
||||
}
|
||||
function Oa(a) {
|
||||
const b = new FormData();
|
||||
for (const c in a) a.hasOwnProperty(c) && b.append(c, a[c]);
|
||||
return b;
|
||||
}
|
||||
function za(a) {
|
||||
let b = d.extend({
|
||||
locale: String(B.locale() || "")
|
||||
}, Aa || {});
|
||||
Ba && Ba.applyCreds(b);
|
||||
ha ? (b = Oa(b), b.append("po", new Blob([ String(B) ], {
|
||||
type: "application/x-gettext"
|
||||
}), String(b.path).split("/").pop() || "untitled.po")) : b.data = String(B);
|
||||
k.ajax.post("save", b, function(c) {
|
||||
a && a();
|
||||
f.save(!0);
|
||||
d("#loco-po-modified").text(c.datetime || "[datetime error]");
|
||||
da(f);
|
||||
}, a);
|
||||
}
|
||||
function Pa() {
|
||||
f.dirty && za();
|
||||
}
|
||||
function Qa() {
|
||||
return u("Your changes will be lost if you continue without saving");
|
||||
}
|
||||
function Ra(a) {
|
||||
function b() {
|
||||
a.disabled = !1;
|
||||
d(a).removeClass("loco-loading");
|
||||
}
|
||||
f.on("poUnsaved", function() {
|
||||
a.disabled = !1;
|
||||
d(a).addClass("button-primary");
|
||||
}).on("poSave", function() {
|
||||
a.disabled = !0;
|
||||
d(a).removeClass("button-primary");
|
||||
});
|
||||
Aa = d.extend({
|
||||
path: ia
|
||||
}, x.project || {});
|
||||
d(a).on("click", function(c) {
|
||||
c.preventDefault();
|
||||
a.disabled = !0;
|
||||
d(a).addClass("loco-loading");
|
||||
za(b);
|
||||
return !1;
|
||||
});
|
||||
return !0;
|
||||
}
|
||||
function Sa(a) {
|
||||
const b = x.project;
|
||||
if (b) {
|
||||
var c = function() {
|
||||
a.disabled = !1;
|
||||
d(a).removeClass("loco-loading");
|
||||
};
|
||||
f.on("poUnsaved", function() {
|
||||
a.disabled = !0;
|
||||
}).on("poSave", function() {
|
||||
a.disabled = !1;
|
||||
});
|
||||
ma = {
|
||||
bundle: b.bundle,
|
||||
domain: b.domain,
|
||||
type: V ? "pot" : "po",
|
||||
path: ia || "",
|
||||
sync: Ta || "",
|
||||
mode: Ua || ""
|
||||
};
|
||||
d(a).on("click", function(e) {
|
||||
e.preventDefault();
|
||||
a.disabled = !0;
|
||||
d(a).addClass("loco-loading");
|
||||
Fa(c);
|
||||
return !1;
|
||||
});
|
||||
a.disabled = !1;
|
||||
}
|
||||
return !0;
|
||||
}
|
||||
function Va(a) {
|
||||
f.on("poUnsaved", function() {
|
||||
a.disabled = !0;
|
||||
}).on("poSave poAuto", function() {
|
||||
a.disabled = !1;
|
||||
});
|
||||
d(a).on("click", La);
|
||||
a.disabled = !1;
|
||||
return !0;
|
||||
}
|
||||
function Wa(a) {
|
||||
d(a).on("click", Ha);
|
||||
a.disabled = !1;
|
||||
}
|
||||
function Xa(a) {
|
||||
a.disabled = !1;
|
||||
d(a).on("click", function(b) {
|
||||
b.preventDefault();
|
||||
b = 1;
|
||||
var c, e = /(\d+)$/;
|
||||
for (c = "New message"; B.get(c); ) b = e.exec(c) ? Math.max(b, Number(RegExp.$1)) : b,
|
||||
c = "New message " + ++b;
|
||||
f.add(c);
|
||||
return !1;
|
||||
});
|
||||
return !0;
|
||||
}
|
||||
function Ya(a) {
|
||||
a.disabled = !1;
|
||||
d(a).on("click", function(b) {
|
||||
b.preventDefault();
|
||||
f.del();
|
||||
return !1;
|
||||
});
|
||||
return !0;
|
||||
}
|
||||
function ja(a, b) {
|
||||
a.disabled = !1;
|
||||
d(a).on("click", function() {
|
||||
let c = ia;
|
||||
"archive" === b ? c = c.replace(/\.po$/, ".zip") : "binary" === b && (c = c.replace(/\.po$/, ".mo"));
|
||||
const e = a.form;
|
||||
e.path.value = c;
|
||||
e.source.value = B.toString();
|
||||
return !0;
|
||||
});
|
||||
return !0;
|
||||
}
|
||||
function ka(a) {
|
||||
a.preventDefault();
|
||||
return !1;
|
||||
}
|
||||
function T() {
|
||||
var a = f.stats(), b = a.t, c = a.f, e = a.u;
|
||||
b = v(G("%s string", "%s strings", b), z(b));
|
||||
var h = [];
|
||||
E && (b = v(u("%s%% translated"), a.p.replace("%", "")) + ", " + b, c && h.push(v(u("%s fuzzy"), z(c))),
|
||||
e && h.push(v(u("%s untranslated"), z(e))), h.length && (b += " (" + h.join(", ") + ")"));
|
||||
d("#loco-po-status").text(b);
|
||||
}
|
||||
function Ca(a, b) {
|
||||
a = b.getAttribute("data-loco");
|
||||
const c = Y[a];
|
||||
c && c(b, a) || d(b).addClass("loco-noop");
|
||||
}
|
||||
const k = C.loco, x = k && k.conf, H = document.getElementById("loco-editor-inner");
|
||||
if (k && x && H) {
|
||||
var Ga = !!x.WP_DEBUG, la = k.po.ref && k.po.ref.init(k, x), ma = null, Aa = null, ha = x.multipart, Za = k.l10n, v = k.string.sprintf, Ea = x.wpnum && x.wpnum.thousands_sep || ",", E = x.locale, B = k.po.init(E).wrap(x.powrap), V = !E, Ja = k.locale.clone(x.source || {
|
||||
lang: "en"
|
||||
}), $a = document.getElementById("loco-actions"), ia = x.popath, Ta = x.potpath, Ua = x.syncmode, K = document.getElementById("loco-fs"), Ba = K && k.fs.init(K), ra = x.readonly;
|
||||
K = !ra;
|
||||
var aa = C.sessionStorage || {
|
||||
setItem: function() {},
|
||||
getItem: function() {
|
||||
return "";
|
||||
}
|
||||
}, ba = !!aa.getItem("loco-ed-invs"), ca = !!aa.getItem("loco-ed-code"), L, Z, ya = {}, W, U, P, ea = 0, sa = {
|
||||
my: "top",
|
||||
at: "top",
|
||||
of: "#loco-content"
|
||||
};
|
||||
!ha || C.FormData && C.Blob || (ha = !1, k.notices.warn("Your browser doesn't support Ajax file uploads. Falling back to standard postdata"));
|
||||
la || k.notices.warn("admin.js is out of date. Please empty your browser cache and reload the page.");
|
||||
var Da = function() {
|
||||
var a, b = parseInt(d(H).css("min-height") || 0);
|
||||
return function() {
|
||||
for (var c = H, e = c.offsetTop || 0; (c = c.offsetParent) && c !== document.body; ) e += c.offsetTop || 0;
|
||||
c = Math.max(b, C.innerHeight - e - 20);
|
||||
a !== c && (H.style.height = String(c) + "px", a = c);
|
||||
};
|
||||
}();
|
||||
Da();
|
||||
d(C).resize(Da);
|
||||
H.innerHTML = "";
|
||||
var f = k.po.ed.init(H).localise(Za);
|
||||
k.po.kbd.init(f).add("save", K ? Pa : ka).add("hint", E && K && ta || ka).enable("copy", "clear", "enter", "next", "prev", "fuzzy", "save", "invis", "hint");
|
||||
var Y = {
|
||||
save: K && Ra,
|
||||
sync: K && Sa,
|
||||
revert: function(a) {
|
||||
f.on("poUnsaved", function() {
|
||||
a.disabled = !1;
|
||||
}).on("poSave", function() {
|
||||
a.disabled = !0;
|
||||
});
|
||||
d(a).on("click", function(b) {
|
||||
b.preventDefault();
|
||||
location.reload();
|
||||
return !1;
|
||||
});
|
||||
return !0;
|
||||
},
|
||||
invs: function(a) {
|
||||
const b = d(a);
|
||||
a.disabled = !1;
|
||||
f.on("poInvs", function(c, e) {
|
||||
b[e ? "addClass" : "removeClass"]("inverted");
|
||||
ba !== e && (ba = e, aa.setItem("loco-ed-invs", e ? "1" : ""));
|
||||
});
|
||||
b.on("click", function(c) {
|
||||
c.preventDefault();
|
||||
f.setInvs(!f.getInvs());
|
||||
return !1;
|
||||
});
|
||||
k.tooltip.init(b);
|
||||
return !0;
|
||||
},
|
||||
code: function(a) {
|
||||
const b = d(a);
|
||||
a.disabled = !1;
|
||||
f.on("poMode", function() {
|
||||
const c = f.getMono();
|
||||
b[c ? "addClass" : "removeClass"]("inverted");
|
||||
ca !== c && (ca = c, aa.setItem("loco-ed-code", c ? "1" : ""));
|
||||
});
|
||||
b.on("click", function(c) {
|
||||
c.preventDefault();
|
||||
f.setMono(!f.getMono());
|
||||
return !1;
|
||||
});
|
||||
k.tooltip.init(b);
|
||||
return !0;
|
||||
},
|
||||
source: ja,
|
||||
binary: V ? null : ja,
|
||||
archive: V ? null : ja
|
||||
};
|
||||
V ? (Y.add = K && Xa, Y.del = K && Ya) : (Y.auto = Va, Y.lint = Wa);
|
||||
d("#loco-editor > nav .button").each(Ca);
|
||||
d("#loco-content > form .button").each(Ca);
|
||||
d($a).on("submit", ka);
|
||||
(function(a) {
|
||||
function b(h) {
|
||||
d(a.parentNode)[h || null == h ? "removeClass" : "addClass"]("invalid");
|
||||
}
|
||||
f.searchable(k.fulltext.init());
|
||||
a.disabled = !1;
|
||||
var c = a.value = "", e = k.watchtext(a, function(h) {
|
||||
h = f.filter(h, !0);
|
||||
b(h);
|
||||
});
|
||||
f.on("poFilter", function(h, t, n) {
|
||||
c = e.val();
|
||||
e.val(t || "");
|
||||
b(n);
|
||||
}).on("poMerge", function() {
|
||||
c && f.filter(c);
|
||||
});
|
||||
})(document.getElementById("loco-search"));
|
||||
ba && f.setInvs(ba);
|
||||
ca && f.setMono(ca);
|
||||
f.on("poUnsaved", function() {
|
||||
C.onbeforeunload = Qa;
|
||||
}).on("poSave", function() {
|
||||
T();
|
||||
C.onbeforeunload = null;
|
||||
}).on("poHint", ta).on("poUpdate", T).on("poMeta", function(a, b) {
|
||||
b = "CODE" === b.tagName ? b : b.getElementsByTagName("CODE")[0];
|
||||
return b && la ? (la.load(b.textContent), a.preventDefault(), !1) : !0;
|
||||
});
|
||||
B.load(x.podata);
|
||||
f.load(B);
|
||||
(E = f.targetLocale) ? E.isRTL() && d(H).addClass("trg-rtl") : f.unlock();
|
||||
T();
|
||||
da(f);
|
||||
delete k.conf;
|
||||
}
|
||||
}(window, window.jQuery);
|
||||
7
wp-content/plugins/loco-translate/pub/js/min/head.js
Normal file
@@ -0,0 +1,7 @@
|
||||
"use strict";
|
||||
|
||||
!function(c, a) {
|
||||
var b = a.getElementById("loco-fs");
|
||||
a = a.getElementById("loco-main");
|
||||
b && a && c.loco.fs.init(b).setForm(a);
|
||||
}(window, document);
|
||||
24
wp-content/plugins/loco-translate/pub/js/min/move.js
Normal file
@@ -0,0 +1,24 @@
|
||||
"use strict";
|
||||
|
||||
!function(k, e, f) {
|
||||
function g(a) {
|
||||
f(b).find("button.button-primary").each(function(r, l) {
|
||||
l.disabled = a;
|
||||
});
|
||||
}
|
||||
function m(a) {
|
||||
g(!(a && c));
|
||||
}
|
||||
function n(a) {
|
||||
a = a.target || {};
|
||||
"dest" !== a.name || !a.checked && "text" !== a.type || (a = a.value) && a !== c && (c = a,
|
||||
g(!0), p !== a && (d.dest.value = a, h.connect()));
|
||||
}
|
||||
function q(a) {
|
||||
if (c) return !0;
|
||||
a.preventDefault();
|
||||
return !1;
|
||||
}
|
||||
let h, c, d = e.getElementById("loco-fs"), b = e.getElementById("loco-main"), p = b.path.value;
|
||||
d && b && (h = k.loco.fs.init(d).setForm(b).listen(m), f(b).change(n).submit(q));
|
||||
}(window, document, window.jQuery);
|
||||
72
wp-content/plugins/loco-translate/pub/js/min/podiff.js
Normal file
@@ -0,0 +1,72 @@
|
||||
"use strict";
|
||||
|
||||
!function(h, g, d) {
|
||||
function p() {
|
||||
return d(e).removeClass("loading");
|
||||
}
|
||||
function k(a) {
|
||||
return d(e).find("div.diff").html(a);
|
||||
}
|
||||
function x(a) {
|
||||
p();
|
||||
return d('<p class="error"></p>').text(a).appendTo(k(""));
|
||||
}
|
||||
function E(a, b) {
|
||||
let c = b.getElementsByTagName("tr"), y = c.length;
|
||||
a = b.getAttribute("data-diff").split(/\D+/);
|
||||
b = a[0];
|
||||
let q = a[1], F = a[2], G = a[3];
|
||||
for (a = 0; a < y; a++) {
|
||||
var l = c[a].getElementsByTagName("td");
|
||||
var m = l[0], z = b++;
|
||||
z <= q && d("<span></span>").text(String(z)).prependTo(m);
|
||||
l = l[2];
|
||||
m = F++;
|
||||
m <= G && d("<span></span>").text(String(m)).prependTo(l);
|
||||
}
|
||||
}
|
||||
function H(a) {
|
||||
f && f.abort();
|
||||
let b = A[a];
|
||||
null != b ? (k(b), p()) : (k(""), d(e).addClass("loading"), f = r.ajax.post("diff", {
|
||||
lhs: t.paths[a],
|
||||
rhs: t.paths[a + 1]
|
||||
}, function(c, y, q) {
|
||||
q === f && (c && c.html ? (b = c.html, A[a] = b, k(b).find("tbody").each(E), p()) : x(c && c.error || "Unknown error"));
|
||||
}, function(c) {
|
||||
c === f && (f = null, x("Failed to generate diff"));
|
||||
}));
|
||||
}
|
||||
function I(a) {
|
||||
a.preventDefault();
|
||||
u(n - 1);
|
||||
return !1;
|
||||
}
|
||||
function J(a) {
|
||||
a.preventDefault();
|
||||
u(n + 1);
|
||||
return !1;
|
||||
}
|
||||
function u(a) {
|
||||
if (0 <= a && a <= v) {
|
||||
n = a;
|
||||
H(a);
|
||||
{
|
||||
a = n;
|
||||
let b = a + 1;
|
||||
B.disabled = a >= v;
|
||||
C.disabled = 0 >= a;
|
||||
w.addClass("jshide").removeClass("diff-meta-current");
|
||||
w.eq(a).removeClass("jshide").addClass("diff-meta-current");
|
||||
w.eq(b).removeClass("jshide");
|
||||
}
|
||||
}
|
||||
}
|
||||
let f, A = [], r = h.loco, t = r.conf, n = 0, v = t.paths.length - 2, e = g.getElementById("loco-ui");
|
||||
h = g.getElementById("loco-fs");
|
||||
g = e.getElementsByTagName("form").item(0);
|
||||
let D = e.getElementsByTagName("button"), w = d(e).find("div.diff-meta"), B = D.item(0), C = D.item(1);
|
||||
h && g && r.fs.init(h).setForm(g);
|
||||
v && (d(B).on("click", J).parent().removeClass("jshide"), d(C).on("click", I).parent().removeClass("jshide"));
|
||||
u(0);
|
||||
}(window, document, window.jQuery);
|
||||
99
wp-content/plugins/loco-translate/pub/js/min/poinit.js
Normal file
@@ -0,0 +1,99 @@
|
||||
"use strict";
|
||||
|
||||
!function(z, w, d) {
|
||||
function p(a) {
|
||||
d(h).find("button.button-primary").each(function(c, b) {
|
||||
b.disabled = a;
|
||||
});
|
||||
}
|
||||
function x() {
|
||||
var a = q && q.val(), c = a && a.isValid() && "zxx" !== a.lang;
|
||||
const b = r && r.val();
|
||||
c = c && b;
|
||||
A(a);
|
||||
p(!0);
|
||||
c && (a = r.txt(), a !== t ? (t = a, u.path.value = t, y.listen(B).connect()) : p(!1));
|
||||
}
|
||||
function B(a) {
|
||||
p(!a);
|
||||
}
|
||||
function A(a) {
|
||||
const c = d(h), b = a && a.toString("_") || "", g = b ? "zxx" === b ? "{locale}" : b : "{invalid}";
|
||||
c.find("code.path span").each(function(n, e) {
|
||||
e.textContent = g;
|
||||
});
|
||||
c.find("span.lang").each(function(n, e) {
|
||||
a && "zxx" !== a.lang ? (e.setAttribute("lang", a.lang), e.setAttribute("class", a.getIcon())) : (e.setAttribute("lang", ""),
|
||||
e.setAttribute("class", "lang nolang"));
|
||||
});
|
||||
}
|
||||
function C(a) {
|
||||
(a = a && a.redirect) && location.assign(a);
|
||||
}
|
||||
let t = "";
|
||||
const l = z.loco, u = w.getElementById("loco-fs"), h = w.getElementById("loco-poinit"), y = u && l.fs.init(u), q = function(a) {
|
||||
function c() {
|
||||
m[0].checked = !0;
|
||||
e(!0);
|
||||
}
|
||||
function b() {
|
||||
k.value || (k.value = g());
|
||||
m[1].checked = !0;
|
||||
e(!1);
|
||||
}
|
||||
function g() {
|
||||
const f = d(m[0].checked ? v : k).serializeArray();
|
||||
return f[0] && f[0].value || "";
|
||||
}
|
||||
function n() {
|
||||
e(m[0].checked);
|
||||
return !0;
|
||||
}
|
||||
function e(f) {
|
||||
k.disabled = f;
|
||||
v.disabled = !f;
|
||||
D.className = f ? "disabled" : "active";
|
||||
E.className = f ? "active" : "disabled";
|
||||
x();
|
||||
}
|
||||
const v = a["select-locale"], k = a["custom-locale"], m = a["use-selector"], E = d(v).on("focus", c).closest("fieldset").on("click", c)[0], D = d(k).on("focus", b).closest("fieldset").on("click", b)[0];
|
||||
return {
|
||||
val: function() {
|
||||
var f = g();
|
||||
return f ? l.locale.parse(f) : l.locale.clone({
|
||||
lang: "zxx"
|
||||
});
|
||||
},
|
||||
init: function() {
|
||||
d(m).change(n);
|
||||
n();
|
||||
l.watchtext(k, function() {
|
||||
d(k.form).triggerHandler("change");
|
||||
});
|
||||
}
|
||||
};
|
||||
}(h), r = function() {
|
||||
function a(b) {
|
||||
var g;
|
||||
return (g = (g = d(c).serializeArray()[0]) && g.value || null) && h[b + "[" + g + "]"];
|
||||
}
|
||||
const c = h["select-path"];
|
||||
return {
|
||||
val: function() {
|
||||
const b = a("path");
|
||||
return b && b.value;
|
||||
},
|
||||
txt: function() {
|
||||
const b = a("path");
|
||||
return b && d(b.parentNode).find("code.path").text();
|
||||
}
|
||||
};
|
||||
}(h);
|
||||
q.init();
|
||||
d(h).on("change", x).on("submit", function(a) {
|
||||
a.preventDefault();
|
||||
y.applyCreds(h);
|
||||
l.ajax.submit(a.target, C);
|
||||
return !1;
|
||||
});
|
||||
}(window, document, window.jQuery);
|
||||
16
wp-content/plugins/loco-translate/pub/js/min/potinit.js
Normal file
@@ -0,0 +1,16 @@
|
||||
"use strict";
|
||||
|
||||
!function(c, b, e) {
|
||||
function f(a) {
|
||||
(a = a && a.redirect) && location.assign(a);
|
||||
}
|
||||
const d = c.loco;
|
||||
c = b.getElementById("loco-fs");
|
||||
b = b.getElementById("loco-potinit");
|
||||
e(b).on("submit", function(a) {
|
||||
a.preventDefault();
|
||||
d.ajax.submit(a.target, f);
|
||||
return !1;
|
||||
});
|
||||
c && d.fs.init(c).setForm(b);
|
||||
}(window, document, window.jQuery);
|
||||
56
wp-content/plugins/loco-translate/pub/js/min/poview.js
Normal file
@@ -0,0 +1,56 @@
|
||||
"use strict";
|
||||
|
||||
!function(p, u, c) {
|
||||
const g = p.loco, C = g.po.ref.init(g, g.conf), h = u.getElementById("loco-po");
|
||||
!function(d, e) {
|
||||
function b() {
|
||||
l.length && (v.push([ m, w ]), e.push(l), l = []);
|
||||
m = null;
|
||||
}
|
||||
function k(a) {
|
||||
return c('<ol class="msgcat"></ol>').attr("start", a).appendTo(d);
|
||||
}
|
||||
function q(a) {
|
||||
x !== a && (c("#loco-content")[a ? "removeClass" : "addClass"]("loco-invalid"),
|
||||
x = a);
|
||||
}
|
||||
let m, w, l = [], v = [], x = !0, r = !1, t = c(d).find("li");
|
||||
t.each(function(a, f) {
|
||||
f = c(f);
|
||||
f.find("span.po-none").length ? b() : (w = a, null == m && (m = a), a = f.find(".po-text").text(),
|
||||
"" !== a && (l = l.concat(a.replace(/\\[ntvfab\\"]/g, " ").split(" "))));
|
||||
});
|
||||
b();
|
||||
g.watchtext(c(d.parentNode).find("form.loco-filter")[0].q, function(a) {
|
||||
if (a) {
|
||||
var f = e.find(a), y = -1, z = f.length, A;
|
||||
c("ol", d).remove();
|
||||
if (z) {
|
||||
for (;++y < z; ) {
|
||||
var B = v[f[y]];
|
||||
a = B[0];
|
||||
for (A = k(a + 1); a <= B[1]; a++) A.append(t[a]);
|
||||
}
|
||||
q(!0);
|
||||
} else q(!1), k(0).append(c("<li></li>").text(g.l10n._("Nothing matches the text filter")));
|
||||
r = !0;
|
||||
n();
|
||||
} else r && (q(!0), r = !1, c("ol", d).remove(), k(1).append(t), n());
|
||||
});
|
||||
}(h, g.fulltext.init());
|
||||
c(h).removeClass("loco-loading");
|
||||
var n = function() {
|
||||
let d, e = h.clientHeight - 2;
|
||||
return function() {
|
||||
for (var b = h, k = b.offsetTop || 0; (b = b.offsetParent) && b !== u.body; ) k += b.offsetTop || 0;
|
||||
b = p.innerHeight - k - 20;
|
||||
d !== b && (h.style.height = b < e ? String(b) + "px" : "", d = b);
|
||||
};
|
||||
}();
|
||||
n();
|
||||
c(p).resize(n);
|
||||
c(h).on("click", function(d) {
|
||||
const e = d.target;
|
||||
if (e.hasAttribute("href")) return d.preventDefault(), C.load(e.textContent), !1;
|
||||
});
|
||||
}(window, document, window.jQuery);
|
||||
60
wp-content/plugins/loco-translate/pub/js/min/setup.js
Normal file
@@ -0,0 +1,60 @@
|
||||
"use strict";
|
||||
|
||||
!function(r, t, b) {
|
||||
function u(a, f, k) {
|
||||
function e() {
|
||||
l("Failed to contact remote API");
|
||||
c = null;
|
||||
}
|
||||
function g() {
|
||||
c && (clearTimeout(c), c = null);
|
||||
}
|
||||
var c = setTimeout(e, 3e3);
|
||||
l("");
|
||||
b.ajax({
|
||||
url: n.apiUrl + "/" + a + "/" + f + ".jsonp?version=" + encodeURIComponent(k),
|
||||
dataType: "jsonp",
|
||||
success: function(h) {
|
||||
if (c) {
|
||||
g();
|
||||
const p = h && h.exact, v = h && h.status;
|
||||
p ? (d["json-content"].value = p, b("#loco-remote-empty").hide(), b("#loco-remote-found").show()) : 404 === v ? l("Sorry, we don't know a bundle by this name") : (q.notices.error(h.error || "Unknown server error"),
|
||||
e());
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
c && (g(), e());
|
||||
},
|
||||
cache: !0
|
||||
});
|
||||
return {
|
||||
abort: g
|
||||
};
|
||||
}
|
||||
function l(a) {
|
||||
d["json-content"].value = "";
|
||||
b("#loco-remote-empty").show().find("span").text(a);
|
||||
b("#loco-remote-found").hide().removeClass("jshide");
|
||||
}
|
||||
var q = r.loco, n = q.conf || {}, m, d = t.getElementById("loco-remote");
|
||||
b(d).find('button[type="button"]').on("click", function(a) {
|
||||
a.preventDefault();
|
||||
m && m.abort();
|
||||
m = u(d.vendor.value, d.slug.value, d.version.value);
|
||||
return !1;
|
||||
});
|
||||
b(d).find('input[type="reset"]').on("click", function(a) {
|
||||
a.preventDefault();
|
||||
l("");
|
||||
return !1;
|
||||
});
|
||||
b.ajax({
|
||||
url: n.apiUrl + "/vendors.jsonp",
|
||||
dataType: "jsonp",
|
||||
success: function(a) {
|
||||
for (var f = -1, k, e, g = a.length, c = b(d.vendor).html(""); ++f < g; ) k = a[f][0],
|
||||
e = a[f][1], c.append(b("<option></option>").attr("value", k).text(e));
|
||||
},
|
||||
cache: !0
|
||||
});
|
||||
}(window, document, window.jQuery);
|
||||
40
wp-content/plugins/loco-translate/pub/js/min/system.js
Normal file
@@ -0,0 +1,40 @@
|
||||
"use strict";
|
||||
|
||||
!function(d, c) {
|
||||
function k(a, b, e) {
|
||||
"success" !== b && (e = d.ajax.parse(d.ajax.strip(a.responseText)));
|
||||
c("#loco-ajax-check").text("FAILED: " + e).addClass("loco-danger");
|
||||
}
|
||||
function g(a, b) {
|
||||
return c("#loco-api-" + a).text(b);
|
||||
}
|
||||
function m(a) {
|
||||
var b = a.getId();
|
||||
a.key() ? a.verify(function(e) {
|
||||
e ? g(b, "OK ✓") : g(b, "FAILED").addClass("loco-danger");
|
||||
}) : g(b, d.l10n._("No API key"));
|
||||
}
|
||||
var f = c("#loco-utf8-check")[0].textContent, h = d.conf;
|
||||
1 === f.length && 10003 === f.charCodeAt(0) || d.notices.warn("This page has a problem rendering UTF-8").stick();
|
||||
window.ajaxurl && c("#loco-ajax-url").text(window.ajaxurl);
|
||||
c("#loco-vers-jquery").text([ c.fn && c.fn.jquery || "unknown", "ui/" + (c.ui && c.ui.version || "none"), "migrate/" + (c.migrateVersion || "none") ].join("; "));
|
||||
d.ajax.post("ping", {
|
||||
echo: "ΟΚ ✓"
|
||||
}, function(a, b, e) {
|
||||
a && a.ping ? c("#loco-ajax-check").text(a.ping) : k(e, b, a && a.error && a.error.message);
|
||||
}, k);
|
||||
f = h.apis;
|
||||
h = f.length;
|
||||
const l = d.apis.providers();
|
||||
if (d.apis) {
|
||||
let a = -1;
|
||||
for (;++a < h; ) {
|
||||
const b = f[a], e = b.id;
|
||||
try {
|
||||
m(d.apis.create(b, l[e] || l._));
|
||||
} catch (n) {
|
||||
g(e, String(n));
|
||||
}
|
||||
}
|
||||
} else d.notices.error("admin.js is out of date. Please empty your browser cache.");
|
||||
}(window.loco, window.jQuery);
|
||||
54
wp-content/plugins/loco-translate/pub/js/min/upload.js
Normal file
@@ -0,0 +1,54 @@
|
||||
"use strict";
|
||||
|
||||
!function(e, l, m) {
|
||||
function h(a) {
|
||||
const n = m(b).find("button.button-primary");
|
||||
n.each(function(A, t) {
|
||||
t.disabled = a;
|
||||
});
|
||||
return n;
|
||||
}
|
||||
function p() {
|
||||
h(!0).addClass("loco-loading");
|
||||
}
|
||||
function f(a) {
|
||||
h(a).removeClass("loco-loading");
|
||||
}
|
||||
function u(a) {
|
||||
f(!(a && c && d));
|
||||
}
|
||||
function q() {
|
||||
b.path.value = c + "/" + d;
|
||||
p();
|
||||
g.connect();
|
||||
}
|
||||
function v(a) {
|
||||
d = String(b.f.value).split(/[\\\/]/).pop();
|
||||
a = a.target || {};
|
||||
if ("dir" === a.name && a.checked) {
|
||||
if ((a = a.value) && a !== c && (c = a, d)) {
|
||||
q();
|
||||
return;
|
||||
}
|
||||
} else if ("f" === a.name && c) {
|
||||
q();
|
||||
return;
|
||||
}
|
||||
h(!(c && d && g.authed()));
|
||||
}
|
||||
function w(a) {
|
||||
a.redirect ? (f(!0), e.location.assign(a.redirect)) : f(!1);
|
||||
}
|
||||
function x() {
|
||||
f(!1);
|
||||
}
|
||||
function y(a) {
|
||||
if (c && d && g.authed()) return z ? (a.preventDefault(), a = new FormData(b), p(),
|
||||
k.ajax.post("upload", a, w, x), !1) : !0;
|
||||
a.preventDefault();
|
||||
return !1;
|
||||
}
|
||||
let g, c, d;
|
||||
const k = e.loco, z = (k && k.conf || {}).multipart && e.FormData && e.Blob, r = l.getElementById("loco-fs"), b = l.getElementById("loco-main");
|
||||
r && b && (g = e.loco.fs.init(r).setForm(b).listen(u), m(b).change(v).submit(y));
|
||||
}(window, document, window.jQuery);
|
||||
601
wp-content/plugins/loco-translate/readme.txt
Normal file
@@ -0,0 +1,601 @@
|
||||
=== Loco Translate ===
|
||||
Contributors: timwhitlock
|
||||
Tags: translation, language, multilingual, l10n, i18n
|
||||
Requires at least: 6.6
|
||||
Requires PHP: 7.4
|
||||
Tested up to: 6.8.1
|
||||
Stable tag: 2.8.0
|
||||
License: GPLv2 or later
|
||||
License URI: http://www.gnu.org/licenses/gpl-2.0.html
|
||||
|
||||
Translate WordPress plugins and themes directly in your browser. Versatile PO file editor with integrated AI translation providers.
|
||||
|
||||
|
||||
== Description ==
|
||||
|
||||
Loco Translate provides in-browser editing of WordPress translation files and integration with automatic translation services.
|
||||
|
||||
It also provides Gettext/localization tools for developers, such as extracting strings and generating templates.
|
||||
|
||||
Features include:
|
||||
|
||||
* Built-in translation editor within WordPress admin
|
||||
* Integration with translation APIs including DeepL, Google, Lecto, Microsoft and OpenAI.
|
||||
* 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
|
||||
* JSON (Jed) file compilation compatible with WordPress script localization
|
||||
* Support for standard 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 bundle’s 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 ==
|
||||
|
||||
Please visit the [FAQs page](https://localise.biz/wordpress/plugin/faqs) on our website for the most common issues.
|
||||
|
||||
= How do I use Loco Translate? =
|
||||
|
||||
Try our [Guides and Tutorials](https://localise.biz/wordpress/plugin#guides).
|
||||
|
||||
= How do I get more 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 track 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
|
||||
6. Suggestion feature showing results from several providers
|
||||
|
||||
|
||||
== Changelog ==
|
||||
|
||||
= 2.8.0 =
|
||||
* Bugfix for PHP 8.0 compatibility
|
||||
* Dropped support for PHP < 7.4
|
||||
|
||||
= 2.7.3 =
|
||||
* PHP 8.4 compatibility
|
||||
* Bumped WordPress compatibility to 6.8.1
|
||||
|
||||
= 2.7.2 =
|
||||
* DeepL client moved to back end, because CORS
|
||||
* Rolled in support for OpenAI / ChatGPT translation
|
||||
* Workaround for JSON file references with no line number
|
||||
* Bumped WordPress compatibility to 6.7.2
|
||||
|
||||
= 2.7.1 =
|
||||
* Debug logging of unloaded domains reduced to a summary
|
||||
|
||||
= 2.7.0 =
|
||||
* Raised minimum requirements to WordPress 6.6
|
||||
* Minimum PHP version becomes 7.2.24 as per WordPress 6.6
|
||||
* Locale-filtered bundle list now searches for base language
|
||||
* Loading helper forcefully removes prematurely loaded text domains
|
||||
* Machine translation hooks now have access to message context
|
||||
* Persistent UI state for code view and invisible character modes
|
||||
|
||||
= 2.6.14 =
|
||||
* Critical fix: A relative path passed to `load_textdomain` no longer throws exception.
|
||||
|
||||
= 2.6.13 =
|
||||
* Fix for direct calls to load_textdomain with custom paths
|
||||
* This resolves a regression in 2.6.12
|
||||
|
||||
= 2.6.12 =
|
||||
* Major fix to custom load_textdomain loader. Works when original file is absent
|
||||
* Fixed bug in template comparison when JSON files need to be merged
|
||||
* CSS fixes including reinstating of unsaved "star" icon
|
||||
* Domain listener fixed for JIT loading
|
||||
* Bumped WordPress compatibility to 6.7
|
||||
|
||||
= 2.6.11 =
|
||||
* Removed accidental console trace
|
||||
* Bumped WordPress compatibility to 6.6.0
|
||||
* Added lang_dir_for_domain fix to handle system file absence
|
||||
|
||||
= 2.6.10 =
|
||||
* Added loco_api_provider_{id} filter
|
||||
* JSON compiler observes configured .js aliases
|
||||
* Fixed a missing security check - thanks Nosa Shandy
|
||||
* Added .blade.php tokenizer hack
|
||||
* Bumped WordPress compatibility to 6.5.4
|
||||
|
||||
= 2.6.9 =
|
||||
* Rolled back load helper changes
|
||||
* Moved debug messages to action hooks
|
||||
* String debugger improvements
|
||||
|
||||
= 2.6.8 =
|
||||
* Added string debugger
|
||||
* Added Zip download button instead of MO
|
||||
* Added debug messages about premature domain loading
|
||||
* Added warning when system translations not installed
|
||||
* Compiler avoids writing empty JSON translation files
|
||||
* UI promotes PO copy over msginit/xgettext routes
|
||||
* Populating msginit fields when copying a PO
|
||||
* Bumped WordPress compatibility to 6.5.3
|
||||
|
||||
= 2.6.7 =
|
||||
* WordPress 6.5.0 compatible
|
||||
* Support for performant translation files in PHP format
|
||||
* Added block.json and theme.json extraction
|
||||
* Added theme pattern files to php string extractor
|
||||
* Fixed a bug where unused plural forms were counted as untranslated
|
||||
* Replaced CSS .notice with .panel to mitigate nag-blocker problems
|
||||
* Removed bundle debug screen (deprecated since 2.6.5)
|
||||
* Workaround for absent "source" references in JED files
|
||||
* Extension polyfills now restricted to Loco admin screens.
|
||||
|
||||
= 2.6.6 =
|
||||
* Replaced open_basedir check with error capturing
|
||||
|
||||
= 2.6.5 =
|
||||
* Added syntax checking function
|
||||
* Removed deepl_api_url config. Free API detected from :fx key suffix.
|
||||
* Fixed bug in relative path calculations
|
||||
* Fixed API suggestions for plural forms
|
||||
* Fixed bug clearing unsaved state icons
|
||||
* Added total strings count to PO file tables
|
||||
* Sharper flags and spinners (@x2 pixel support)
|
||||
* Handling upload_tmp_dir values outside of open_basedir
|
||||
* Suppressing E_WARNING when testing file is_readable
|
||||
* Bundle debug screen is deprecated (moving into Setup)
|
||||
* Showing System Diagnostics when debug is off
|
||||
* Bumped WordPress compatibility to 6.3.1
|
||||
|
||||
= 2.6.4 =
|
||||
* Bumped WordPress version to 6.1.1
|
||||
* Dropped support for Internet Explorer
|
||||
* Updated JavaScript to ECMAScript 6
|
||||
* Added `loco_bundle_configured` hook
|
||||
* Fixed error icon not clearing after correction
|
||||
|
||||
= 2.6.3 =
|
||||
* Fixed bug in plural forms comparison
|
||||
* Fixed bug generating author theme jsons
|
||||
* Fixed errors in bundle debugger
|
||||
* Extended cli type argument to filter specific bundle
|
||||
* Bumped WordPress version to 6.0.3
|
||||
|
||||
= 2.6.2 =
|
||||
* Bumped WordPress version to 6.0.0
|
||||
* Better labelling of reverse-engineered plural forms
|
||||
* Removed undocumented loco_locale_plurals filter; use loco_po_headers
|
||||
* Added PO folder location indicator in breadcrumb
|
||||
* Added syntax validation for formatted strings
|
||||
|
||||
= 2.6.1 =
|
||||
* Bumped WordPress version to 5.9.2
|
||||
* Fix for CVE-2022-0765 reported by Taurus Omar via wpscan
|
||||
|
||||
= 2.6.0 =
|
||||
* Dropped support for WordPress < 5.2
|
||||
* Code upgrades for >= PHP 5.6.20
|
||||
* Bumped WordPress version to 5.9.1
|
||||
* Removed Yandex API integration
|
||||
* Added loco_compile_script_reference filter
|
||||
* Plural-Forms retained when copying PO to same language
|
||||
|
||||
= 2.5.8 =
|
||||
* Compatible with PHP 8.1
|
||||
* Bumped WordPress version to 5.9
|
||||
* Added deprecation warning prior to v2.6
|
||||
|
||||
= 2.5.7 =
|
||||
* Fixed bug in 2.5.6 where remote APIs could not be used in batch mode
|
||||
* Enforcing 10k character limit per request for Microsoft and Yandex Translators
|
||||
* Style fix for revision/diff table under restore tab
|
||||
|
||||
= 2.5.6 =
|
||||
* Added loco_api_provider_source filter
|
||||
* Fixed bug loading user preferences saved in older version
|
||||
* Refactored file finder to avoid recursive function calls
|
||||
* Fixed bug displaying two forms for zero plural languages
|
||||
* Added Lecto AI to translation API providers
|
||||
* Bumped WordPress version to 5.8.3
|
||||
|
||||
= 2.5.5 =
|
||||
* Fixed double file extension vulnerability reported by WordFence
|
||||
* Better performance when scanning directories for file types
|
||||
|
||||
= 2.5.4 =
|
||||
* Fixed vulnerability reported by Tomi Ashari via wpscan
|
||||
* Added filters loco_po_headers and loco_pot_headers
|
||||
* Bumped WordPress version to 5.8.1
|
||||
|
||||
= 2.5.3 =
|
||||
* Adds option to merge JSON translations when syncing from PO
|
||||
* Adds screen for editing file headers and sync options
|
||||
* Fix for missing responseText in failed Ajax responses
|
||||
* Fix for HTML entities returned from `number_format_i18n`
|
||||
* Localized number formatting in JavaScript
|
||||
* Replaced usage of date_i18n with wp_date
|
||||
* Added configurable API endpoint for DeepL
|
||||
* Bumped WordPress version to 5.7.2
|
||||
|
||||
= 2.5.2 =
|
||||
* Added implied formality and loco_locale_formality filter
|
||||
* Added cli fetch command (experimental)
|
||||
* Bumped WordPress version to 5.7
|
||||
|
||||
= 2.5.1 =
|
||||
* Support for new Yandex translate API
|
||||
* Support for DeepL formality parameter
|
||||
* Removed literal "1" and "one" instances from singular strings
|
||||
* Buffering compiled JSON to support strings from multiple sources
|
||||
* Added `loco_compile_single_json` filter for specifying custom JSON
|
||||
* Added `loco_extracted_template` hook for adding custom strings
|
||||
* Sync no longer removes the editor's current text filter
|
||||
* Bumped WordPress version to 5.6.2
|
||||
|
||||
= 2.5.0 =
|
||||
* PHP 8.0.0 compatibility
|
||||
* Bumped WordPress version to 5.6.0
|
||||
* Added JSON translation file generation
|
||||
* Added custom JSON loading to LoadHelper
|
||||
* Disabled emoji image replacement on our admin screens
|
||||
|
||||
= 2.4.6 =
|
||||
* Fixed critical bug syncing PO directly to source code
|
||||
* Added plugin setting for allowing/disallowing missing POT
|
||||
* Fixed WP5.5 issue with multiple ID attributes on script tags
|
||||
|
||||
= 2.4.5 =
|
||||
* Added WP-CLI sync and extract commands
|
||||
* Fixed {locale} placeholder bug introduced in 2.4.4
|
||||
* Improved handling of invalid character encodings
|
||||
* Sync (msgmerge) moved to back end
|
||||
* New fuzzy matching with fuzziness setting
|
||||
* Bumped WordPress version to 5.5.3
|
||||
|
||||
= 2.4.4 =
|
||||
* Added PO file upload feature
|
||||
* Added download button to file info page
|
||||
* Fix for extracting plurals also used as singulars
|
||||
* Updating API keys no longer require editor page reload
|
||||
* Catching fatal startup errors in loco.php
|
||||
* Supporting max_php_size=0 to mean no size restriction
|
||||
* Auto-update detection now checks new site options
|
||||
* Bumped WordPress version to 5.5.1
|
||||
|
||||
= 2.4.3 =
|
||||
* Improved fix for default syncing of msgstr fields
|
||||
* Reverted accidental removal of js debug flag
|
||||
* Minor fixes to API error messages
|
||||
* Removed use of jQuery.browser
|
||||
* Bugfix for new preferences in usermeta
|
||||
|
||||
= 2.4.2 =
|
||||
* Added loco_file_written hook
|
||||
* Improved script tampering warning
|
||||
* Added keypress for selecting auto-suggestion
|
||||
* Sync no longer copies msgstr fields by default
|
||||
* Style tweaks for WordPress 5.5
|
||||
|
||||
= 2.4.1 =
|
||||
* Fixed mapping of some API languages
|
||||
* Added locale filter to user preferences
|
||||
* Added debugging for credential form failures
|
||||
* Fixed deprecated use of array_key_exists
|
||||
* Added DeepL API service provider
|
||||
* Improved script tampering detection
|
||||
* Bumped WordPress version to 5.5
|
||||
* Added "modern" skin styles
|
||||
|
||||
= 2.4.0 =
|
||||
* Added support for third party translation APIs
|
||||
* Added file references to editor source pane in code view
|
||||
* Added fuzzy matching during editor Sync operation
|
||||
* Style changes including rearrangement of editor buttons
|
||||
* Elevated warnings when scripts are tampered with
|
||||
* Removed remnants of legacy version 1.x
|
||||
|
||||
= 2.3.4 =
|
||||
* Updated translatable strings
|
||||
* Added missing template recommendation
|
||||
* Alerting in debug mode when scripts are tampered with
|
||||
* Fix for Hello Dolly being installed into a folder
|
||||
* Removed translation column in POT edit mode
|
||||
* Added setting to prevent 'translating' of POT files
|
||||
* Enabled some linkable translations using wp_kses
|
||||
* Bumped WordPress version to 5.4.1
|
||||
|
||||
= 2.3.3 =
|
||||
* Fixed fatal error when class not found
|
||||
|
||||
= 2.3.2 =
|
||||
* Removed login/email from default Last-Translator credit
|
||||
* Bumped WP compatibility to 5.4
|
||||
* Fixed PHP 7.4 deprecations
|
||||
|
||||
= 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 as per [exploit-db 46619](https://www.exploit-db.com/exploits/46619)
|
||||
* 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.8.0 =
|
||||
* Various improvements and bug fixes
|
||||
|
||||
|
||||
|
||||
== 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`
|
||||
* Suggest translation: `Ctrl J`
|
||||
|
||||
Mac users can use ⌘ Cmd instead of Ctrl.
|
||||
555
wp-content/plugins/loco-translate/src/Locale.php
Normal file
@@ -0,0 +1,555 @@
|
||||
<?php
|
||||
/**
|
||||
* Represents a WordPress locale
|
||||
*
|
||||
* @property string $lang
|
||||
* @property string $region
|
||||
* @property string $variant
|
||||
*/
|
||||
class Loco_Locale implements JsonSerializable {
|
||||
|
||||
/**
|
||||
* Language subtags
|
||||
* @var string[]
|
||||
*/
|
||||
private array $tag;
|
||||
|
||||
/**
|
||||
* Cached composite tag
|
||||
*/
|
||||
private ?string $_tag = null;
|
||||
|
||||
/**
|
||||
* Cached icon css class
|
||||
*/
|
||||
private ?string $icon = null;
|
||||
|
||||
/**
|
||||
* Name in English
|
||||
*/
|
||||
private string $name = '';
|
||||
|
||||
/**
|
||||
* Name in language of self
|
||||
*/
|
||||
private ?string $_name = null;
|
||||
|
||||
/**
|
||||
* Plural equation expressed in terms of "n"
|
||||
*/
|
||||
private string $pluraleq;
|
||||
|
||||
/**
|
||||
* Cache of plural forms mapped optionally to CLDR mnemonic tags
|
||||
*/
|
||||
private ?array $plurals = null;
|
||||
|
||||
/**
|
||||
* Validity cache
|
||||
*/
|
||||
private ?bool $valid = null;
|
||||
|
||||
/**
|
||||
* @param string $tag Full language tag
|
||||
*/
|
||||
public static function parse( string $tag ):self {
|
||||
$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( string $lang = '', string $region = '', string $variant = '' ){
|
||||
if( 1 == func_num_args() && isset($lang[3]) ){
|
||||
throw new BadMethodCallException('Did you mean Loco_Locale::parse('.var_export($lang,1).') ?');
|
||||
}
|
||||
$this->tag = compact('lang','region','variant');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Allow read access to subtags
|
||||
* @internal
|
||||
* @param string $t subtag
|
||||
* @return string
|
||||
*/
|
||||
public function __get( $t ){
|
||||
return $this->tag[ $t ] ?? '';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Allow write access to subtags
|
||||
* @internal
|
||||
* @param string $t subtag, e.g. "lang"
|
||||
* @param string $s subtag value, e.g. "en"
|
||||
* @return void
|
||||
*/
|
||||
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
|
||||
* @param string[] $tag
|
||||
*/
|
||||
public function setSubtags( array $tag ):self {
|
||||
$this->valid = false;
|
||||
$default = [ '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( preg_match('/^\\d+$/',$tag['region']) ){
|
||||
throw new Loco_error_LocaleException('Numeric regions not supported');
|
||||
}
|
||||
// non-standard variant code. e.g. formal/informal
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Ensure correct casing of subtags
|
||||
*/
|
||||
public function normalize():self {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param bool $translate whether to get name in current display language
|
||||
*/
|
||||
public function getName( bool $translate = true ):?string{
|
||||
$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( $translate ){
|
||||
$locale = self::parse( function_exists('get_user_locale') ? get_user_locale() : get_locale() );
|
||||
if( $this->lang === $locale->lang && $this->_name ){
|
||||
$name = $this->_name;
|
||||
}
|
||||
/*/ Note that no dynamic translation of English name is performed, but can be filtered with loco_parse_locale
|
||||
else {
|
||||
$name = __($name,'loco-translate-languages');
|
||||
}*/
|
||||
}
|
||||
if( is_string($name) && '' !== $name ){
|
||||
return $name;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get canonical native name as defined by WordPress
|
||||
*/
|
||||
public function getNativeName():?string {
|
||||
$name = $this->_name;
|
||||
if( is_string($name) && '' !== $name ){
|
||||
return $name;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get CSS class for locale icon
|
||||
*/
|
||||
public function getIcon(): ?string {
|
||||
$icon = $this->icon;
|
||||
if( is_null($icon) ){
|
||||
$tag = [];
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Force custom icon, or reset. Used in tests.
|
||||
*/
|
||||
public function setIcon( string $css ):self {
|
||||
$this->icon = $css ?: null;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set custom locale name, and optional translation
|
||||
*/
|
||||
public function setName( string $english_name, string $native_name = '' ):self {
|
||||
$this->name = apply_filters('loco_locale_name', $english_name, $native_name );
|
||||
$this->_name = $native_name ?: null;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Test whether locale is valid
|
||||
*/
|
||||
public function isValid():bool {
|
||||
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 ): ?string {
|
||||
$tag = (string) $this;
|
||||
// pull from WordPress translations API if network allowed
|
||||
$locale = $api->getLocale($tag);
|
||||
if( $locale ){
|
||||
$this->setName( $locale->getName(false), $locale->getNativeName() );
|
||||
}
|
||||
return $this->getName(false);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Resolve this locale's name from compiled Loco data
|
||||
* @return string English name currently set
|
||||
*/
|
||||
public function buildName(): ?string {
|
||||
// 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
|
||||
*/
|
||||
public function ensureName( Loco_api_WordPressTranslations $api ):string {
|
||||
$name = $this->getName();
|
||||
if( ! $name ){
|
||||
$name = $this->fetchName($api);
|
||||
// failing that, build 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;
|
||||
}
|
||||
|
||||
|
||||
#[ReturnTypeWillChange]
|
||||
public function jsonSerialize():array{
|
||||
$a = $this->tag;
|
||||
$a['label'] = $this->getName();
|
||||
// plural data expected by editor
|
||||
$p = $this->getPluralData();
|
||||
$a['pluraleq'] = $p[0];
|
||||
$a['nplurals'] = count($p[1]);
|
||||
$a['plurals'] = $this->getPluralForms();
|
||||
// tone setting may be used by some external translation providers
|
||||
$a['tone'] = $this->getFormality();
|
||||
return $a;
|
||||
}
|
||||
|
||||
|
||||
private function getPluralData():array {
|
||||
if( is_null($this->plurals) ){
|
||||
$lc = strtolower($this->lang);
|
||||
$db = Loco_data_CompiledData::get('plurals');
|
||||
$id = $lc && isset($db[$lc]) ? $db[$lc] : 0;
|
||||
list( $this->pluraleq, $this->plurals ) = $db[''][$id];
|
||||
}
|
||||
return [ $this->pluraleq, $this->plurals ];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get translated plural form labels
|
||||
* @return string[]
|
||||
*/
|
||||
public function getPluralForms(): array {
|
||||
list( , $plurals ) = $this->getPluralData();
|
||||
$nplurals = count($plurals);
|
||||
// Languages with no plural forms, where n always yields 0. The UI doesn't show a label for this.
|
||||
if( 1 === $nplurals ){
|
||||
return [ 'All' ];
|
||||
}
|
||||
// Germanic plurals can show singular/plural as per source string text boxes
|
||||
// Note that french style plurals include n=0 under the "Single", but we will show "Single (0,1)"
|
||||
if( 2 === $nplurals ){
|
||||
$l10n = [
|
||||
'one' => _x('Single','Editor','loco-translate'),
|
||||
'other' => _x('Plural',"Editor",'loco-translate'),
|
||||
];
|
||||
}
|
||||
// else translate all implemented plural forms and show sample numbers if useful:
|
||||
// for meaning of categories, see http://cldr.unicode.org/index/cldr-spec/plural-rules
|
||||
else {
|
||||
$l10n = [
|
||||
// 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'),
|
||||
];
|
||||
}
|
||||
// process labels to be shown in editor tab, appending sample values of `n` if useful
|
||||
$labels = [];
|
||||
foreach( $plurals as $sample => $tag ){
|
||||
if( is_int($sample) ){
|
||||
$sample = sprintf('%u',$sample);
|
||||
}
|
||||
// if CLDR tag is to be used we'll need to translate it
|
||||
if( array_key_exists($tag,$l10n) ){
|
||||
$name = $l10n[$tag];
|
||||
}
|
||||
else {
|
||||
$name = $tag;
|
||||
}
|
||||
// show just samples if no name
|
||||
if( '' === $name ){
|
||||
$labels[] = $sample;
|
||||
}
|
||||
// show just name if label is numeric, or samples are redundant
|
||||
else if(
|
||||
preg_match('/\\d/',$name) ||
|
||||
( 'one' === $tag && '1' === $sample ) ||
|
||||
( 'two' === $tag && '2' === $sample ) ||
|
||||
( 'zero' === $tag && '0' === $sample ) ||
|
||||
( 'other' === $tag && 2 === $nplurals )
|
||||
){
|
||||
$labels[] = $name;
|
||||
}
|
||||
// else both - most common for standard CLDR forms
|
||||
else {
|
||||
$labels[] = sprintf('%s (%s)', $name, $sample );
|
||||
}
|
||||
}
|
||||
return $labels;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get PO style Plural-Forms header value comprising number of forms and integer equation for n
|
||||
*/
|
||||
public function getPluralFormsHeader(): string {
|
||||
list( $equation, $forms ) = $this->getPluralData();
|
||||
return sprintf('nplurals=%u; plural=%s;', count($forms), $equation );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Apply PO style Plural-Forms header.
|
||||
* @param string $str header value e.g. "nplurals=2; plural=n != 1;"
|
||||
* @return void
|
||||
*/
|
||||
public function setPluralFormsHeader( string $str ){
|
||||
if( ! preg_match('#^nplurals=(\\d);\\s*plural=([-+/*%!=<>|&?:()n\\d ]+);?$#', $str, $match ) ){
|
||||
throw new InvalidArgumentException('Invalid Plural-Forms header, '.json_encode($str) );
|
||||
}
|
||||
$nplurals = (int) $match[1];
|
||||
$pluraleq = trim( $match[2],' ');
|
||||
// single form requires no further inspection
|
||||
if( 2 > $nplurals ){
|
||||
$this->pluraleq = '0';
|
||||
$this->plurals = ['other'];
|
||||
return;
|
||||
}
|
||||
// Override new equation in all cases
|
||||
$previous = $this->getPluralData()[0];
|
||||
$this->pluraleq = $pluraleq;
|
||||
// quit asap if plural forms being set aren't changing anything
|
||||
if( $nplurals === count($this->plurals) && self::hashPlural($previous) === self::hashPlural($pluraleq) ){
|
||||
return;
|
||||
}
|
||||
// compile sample keys as per built-in CLDR rule for this language
|
||||
$keys = [];
|
||||
$formula = new Plural_Forms($pluraleq);
|
||||
$ns = [0,1,2,3,4,5,6,7,8,9,10,11,12,13,20,21,22,30,31,32,100,101,102,103,104,111,200,201,202,301,302];
|
||||
for( $i = 0; $i < $nplurals; $i++ ){
|
||||
$sample = [];
|
||||
$suffix = '';
|
||||
foreach( $ns as $j => $n ){
|
||||
if( is_null($n) || $formula->execute($n) !== $i ){
|
||||
continue;
|
||||
}
|
||||
$ns[$j] = null;
|
||||
if( array_key_exists(2,$sample) ){
|
||||
$suffix = "\xE2\x80\xA6";
|
||||
break;
|
||||
}
|
||||
else {
|
||||
$sample[] = $n;
|
||||
}
|
||||
}
|
||||
$keys[] = implode(',',$sample).$suffix;
|
||||
}
|
||||
// cast to string for comparison due to PHP forcing integer keys in this->plurals
|
||||
$expect = implode('|',$keys);
|
||||
$actual = implode('|',array_keys($this->plurals));
|
||||
// use mnemonic tags only if they match the default (CLDR) tags for the current language
|
||||
if( $expect !== $actual ){
|
||||
// exception when two forms only and the first accepts n=1 and second n=2
|
||||
if( 2 === $nplurals && 0 === $formula->execute(1) && 1 === $formula->execute(2) ){
|
||||
$tags = ['one','other'];
|
||||
}
|
||||
// blanking CLDR tags means only samples will be used as labels
|
||||
else {
|
||||
$tags = array_fill(0,$nplurals,'');
|
||||
// Translators: Shown when a PO file's Plural-Forms header has a different formula from the Unicode CLDR rules
|
||||
Loco_error_AdminNotices::info( __('Plural forms differ from Loco Translate\'s built in rules for this language','loco-translate') );
|
||||
}
|
||||
// set new plural forms
|
||||
$this->plurals = array_combine($keys,$tags);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Crude normalizer for a plural equation such that similar formulae can be compared.
|
||||
* @param string $str original plural equation
|
||||
* @return string signature for comparison
|
||||
*/
|
||||
private static function hashPlural( string $str ):string {
|
||||
return trim( str_replace([' ','<>'],['','!='],$str), '()' );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get formality setting, whether implied or explicit.
|
||||
* @return string either "", "formal" or "informal"
|
||||
*/
|
||||
public function getFormality():string {
|
||||
$value = '';
|
||||
$tag = $this->__toString();
|
||||
$variant = $this->variant;
|
||||
if( '' === $variant ){
|
||||
// if a formal variant exists, tone may be implied informal
|
||||
$d = Loco_data_CompiledData::get('locales');
|
||||
if( $d->offsetExists($tag.'_formal') ){
|
||||
if( ! $d->offsetExists($tag.'_informal') ) {
|
||||
$value = 'informal';
|
||||
}
|
||||
}
|
||||
// if an informal variant exists, tone may be implied formal
|
||||
else if( $d->offsetExists($tag.'_informal') ){
|
||||
if( ! $d->offsetExists($tag.'_formal') ) {
|
||||
$value = 'formal';
|
||||
}
|
||||
}
|
||||
}
|
||||
else if( 'formal' === $variant || 'informal' === $variant ){
|
||||
$value = $variant;
|
||||
}
|
||||
return apply_filters('loco_locale_formality',$value,$tag);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Depends on compiled library
|
||||
if( ! function_exists('loco_parse_wp_locale') ){
|
||||
loco_require_lib('compiled/locales.php');
|
||||
}
|
||||
|
||||
953
wp-content/plugins/loco-translate/src/admin/DebugController.php
Normal file
@@ -0,0 +1,953 @@
|
||||
<?php
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class Loco_admin_DebugController extends Loco_mvc_AdminController {
|
||||
|
||||
/**
|
||||
* Text domain of debugger, limits when gets logged
|
||||
* @var string|null $domain
|
||||
*/
|
||||
private $domain;
|
||||
|
||||
/**
|
||||
* Temporarily forced locale
|
||||
* @var string|null $locale
|
||||
*/
|
||||
private $locale;
|
||||
|
||||
/**
|
||||
* Log lines for final result
|
||||
* @var null|ArrayIterator
|
||||
*/
|
||||
private $output;
|
||||
|
||||
/**
|
||||
* Current indent for recursive logging calls
|
||||
* @var string
|
||||
*/
|
||||
private $indent = '';
|
||||
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function init(){
|
||||
parent::init();
|
||||
// get a better default locale than en_US
|
||||
$locale = get_locale();
|
||||
if( 'en_US' === $locale ){
|
||||
foreach( get_available_languages() as $locale ){
|
||||
if( 'en_US' !== $locale ){
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
$params = [
|
||||
'domain' => '',
|
||||
'locale' => '',
|
||||
'msgid' => '',
|
||||
'msgctxt' => '',
|
||||
'msgid_plural' => '',
|
||||
'n' => '',
|
||||
'unhook' => '',
|
||||
'loader' => '',
|
||||
'loadpath' => '',
|
||||
'jspath' => '',
|
||||
];
|
||||
$defaults = [
|
||||
'n' => '1',
|
||||
'domain' => 'default',
|
||||
'locale' => $locale,
|
||||
];
|
||||
foreach( array_intersect_key(stripslashes_deep($_GET),$params) as $k => $value ){
|
||||
if( '' !== $value ){
|
||||
$params[$k] = $value;
|
||||
}
|
||||
}
|
||||
$this->set('form', new Loco_mvc_ViewParams($params) );
|
||||
$this->set('default', new Loco_mvc_ViewParams($defaults+$params) );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
private function log( ...$args ){
|
||||
$message = array_shift($args);
|
||||
if( $args ){
|
||||
$message = vsprintf($message,$args);
|
||||
}
|
||||
if( is_null($this->output) ){
|
||||
$this->output = new ArrayIterator;
|
||||
$this->set('log', $this->output );
|
||||
}
|
||||
// redact any path information outside of WordPress root, and shorten any common locations
|
||||
$message = str_replace( [LOCO_LANG_DIR,WP_LANG_DIR,WP_CONTENT_DIR,ABSPATH], ['{loco_lang_dir}','{wp_lang_dir}','{wp_content_dir}','{abspath}'], $message );
|
||||
$this->output[] = $this->indent.$message;
|
||||
}
|
||||
|
||||
|
||||
private function logDomainState( $domain ) {
|
||||
$indent = $this->indent;
|
||||
$this->indent = $indent.' . ';
|
||||
// filter callback should log determined locale, but may not be set up yet
|
||||
$locale = determine_locale();
|
||||
$this->log('determine_locale() == %s', $locale );
|
||||
// Show the state just prior to potentially triggering JIT. There are no hooks between __() and load_textdomain().
|
||||
global $l10n, $l10n_unloaded, $wp_textdomain_registry;
|
||||
$this->log('$l10[%s] == %s', $domain, self::debugMember($l10n,$domain) );
|
||||
$this->log('$l10n_unloaded[%s] == %s', $domain, self::debugMember($l10n_unloaded,$domain) );
|
||||
$this->log('$wp_textdomain_registry->has(%s) == %b', $domain, $wp_textdomain_registry->has($domain) );
|
||||
$this->log('is_textdomain_loaded(%s) == %b', $domain, is_textdomain_loaded($domain) );
|
||||
// the following will fire more hooks, making mess of logs. We should already see this value above directly from $l10n[$domain]
|
||||
// $this->log(' ? get_translations_for_domain(%s) == %s', $domain, self::debugType( get_translations_for_domain($domain) ) );
|
||||
$this->indent = $indent;
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static function debugMember( array $data, $key ){
|
||||
return self::debugType( array_key_exists($key,$data) ? $data[$key] : null );
|
||||
}
|
||||
|
||||
|
||||
private static function debugType( $value ){
|
||||
return is_object($value) ? get_class($value) : json_encode($value,JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* `loco_unload_early_textdomain` filter callback.
|
||||
*/
|
||||
public function filter_loco_unload_early_textdomain( $bool, $domain ){
|
||||
if( $this->domain === $domain ){
|
||||
$value = $GLOBALS['l10n'][$domain]??null;
|
||||
$type = is_object($value) ? get_class($value) : gettype($value);
|
||||
$this->log('~ filter:loco_unload_early_textdomain: $l10n[%s] => %s; returning %s', $domain, $type, json_encode($bool) );
|
||||
}
|
||||
return $bool;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* `loco_unloaded_textdomain` action callback from the loading helper
|
||||
*/
|
||||
public function on_loco_unloaded_textdomain( $domain ){
|
||||
if( $domain === $this->domain ){
|
||||
$this->log('~ action:loco_unloaded_textdomain: Text domain loaded prematurely, unloaded "%s"',$domain);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* `loco_unseen_textdomain` action callback from the loading helper
|
||||
* TODO This has been scrapped in rewritten helper. Move the logic somewhere else.
|
||||
*/
|
||||
public function on_loco_unseen_textdomain( $domain ){
|
||||
if( $domain !== $this->domain ){
|
||||
return;
|
||||
}
|
||||
$locale = determine_locale();
|
||||
if( 'en_US' === $locale ){
|
||||
return;
|
||||
}
|
||||
if( is_textdomain_loaded($domain) ){
|
||||
$this->log('~ action:loco_unseen_textdomain: "%s" was loaded before helper started',$domain);
|
||||
}
|
||||
else {
|
||||
$this->log('~ action:loco_unseen_textdomain: "%s" isn\'t loaded for "%s"',$domain,$locale);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* `pre_determine_locale` filter callback
|
||||
*/
|
||||
public function filter_pre_determine_locale( ?string $locale = null ):?string {
|
||||
if( is_string($this->locale) ) {
|
||||
$this->log( '~ filter:pre_determine_locale: %s => %s', $locale ?: 'none', $this->locale );
|
||||
$locale = $this->locale;
|
||||
}
|
||||
return $locale;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* `load_textdomain` callback
|
||||
*/
|
||||
public function on_load_textdomain( $domain, $mopath ){
|
||||
if( $domain === $this->domain ){
|
||||
$this->log('~ action:load_textdomain: %s', $mopath );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* `load_textdomain_mofile` callback
|
||||
*/
|
||||
public function filter_load_textdomain_mofile( $mofile, $domain ){
|
||||
if( $domain === $this->domain ){
|
||||
$this->log('~ filter:load_textdomain_mofile: %s (exists=%b)', $mofile, file_exists($mofile) );
|
||||
}
|
||||
return $mofile;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* `load_translation_file` filter callback
|
||||
*/
|
||||
public function filter_load_translation_file( $file, $domain/*, $locale = ''*/ ){
|
||||
if( $domain === $this->domain ){
|
||||
$this->log('~ filter:load_translation_file: %s (exists=%b)', $file, file_exists($file) );
|
||||
}
|
||||
return $file;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* `translation_file_format` filter callback
|
||||
* TODO let form option override 'php' as preferred format
|
||||
*/
|
||||
public function filter_translation_file_format( $preferred_format, $domain ){
|
||||
if( $domain === $this->domain ){
|
||||
$this->log('~ filter:translation_file_format: %s', $preferred_format );
|
||||
}
|
||||
return $preferred_format;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* `lang_dir_for_domain` filter callback, requires WP>=6.6
|
||||
*/
|
||||
public function filter_lang_dir_for_domain( $path, $domain, $locale ){
|
||||
if( $domain === $this->domain && $locale === $this->locale ){
|
||||
if( $path ) {
|
||||
$this->log( '~ filter:lang_dir_for_domain %s', $path );
|
||||
}
|
||||
else {
|
||||
$this->log( '! filter:lang_dir_for_domain has no path. JIT likely to fail');
|
||||
}
|
||||
}
|
||||
return $path;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* `load_script_textdomain_relative_path` filter callback
|
||||
*/
|
||||
public function filter_load_script_textdomain_relative_path( $relative/*, $src*/ ){
|
||||
if( preg_match('!pub/js/(?:min|src)/dummy.js!', $relative )){
|
||||
$form = $this->get('form');
|
||||
$path = $form['jspath'];
|
||||
//error_log( json_encode(func_get_args(),JSON_UNESCAPED_SLASHES).' -> '.$path );
|
||||
$this->log( '~ filter:load_script_textdomain_relative_path: %s => %s', $relative, $path );
|
||||
return $path;
|
||||
}
|
||||
return $relative;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* `pre_load_script_translations` filter callback
|
||||
* @noinspection PhpUnusedParameterInspection
|
||||
*/
|
||||
public function filter_pre_load_script_translations( $translations, $file, $handle /*, $domain*/ ){
|
||||
if( 'loco-translate-dummy' === $handle && ! is_null($translations) ){
|
||||
$this->log('~ filter:pre_load_script_translations: Short-circuited with %s value', gettype($translations) );
|
||||
}
|
||||
return $translations;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* `load_script_translation_file` filter callback.
|
||||
*/
|
||||
public function filter_load_script_translation_file( $file, $handle/* ,$domain*/ ){
|
||||
if( 'loco-translate-dummy' === $handle ){
|
||||
// error_log( json_encode(func_get_args(),JSON_UNESCAPED_SLASHES) );
|
||||
// if file is not found, this will fire again with file=false
|
||||
$this->log('~ filter:load_script_translation_file: %s', var_export($file,true) );
|
||||
}
|
||||
return $file;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* `load_script_translations` filter callback
|
||||
* @noinspection PhpUnusedParameterInspection
|
||||
*/
|
||||
public function filter_load_script_translations( $translations, $file, $handle, $domain ){
|
||||
if( 'loco-translate-dummy' === $handle ){
|
||||
// just log it if the value isn't JSON.
|
||||
if( ! is_string($translations) || '' === $translations || '{' !== $translations[0] ) {
|
||||
$this->log( '~ filter:load_script_translations: %s', var_export($translations,true) );
|
||||
}
|
||||
}
|
||||
return $translations;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* `[n]gettext[_with_context]` filter callback
|
||||
*/
|
||||
public function temp_filter_gettext(){
|
||||
$i = func_num_args() - 1;
|
||||
$args = func_get_args();
|
||||
$translation = $args[0];
|
||||
if( $args[$i] === $this->domain ){
|
||||
$args = array_slice($args,1,--$i);
|
||||
$opts = JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE;
|
||||
$this->log('~ filter:gettext: %s => %s', json_encode($args,$opts), json_encode($translation,$opts) );
|
||||
}
|
||||
return $translation;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @return null|Loco_package_Bundle
|
||||
*/
|
||||
private function getBundleByDomain( $domain, $type ){
|
||||
if( 'default' === $domain ){
|
||||
$this->log('Have WordPress core bundle');
|
||||
return Loco_package_Core::create();
|
||||
}
|
||||
if( 'plugin' === $type ){
|
||||
$search = Loco_package_Plugin::getAll();
|
||||
}
|
||||
else if( 'theme' === $type || 'child' === $type ){
|
||||
$type = 'theme';
|
||||
$search = Loco_package_Theme::getAll();
|
||||
}
|
||||
else {
|
||||
$type = 'bundle';
|
||||
$search = array_merge( Loco_package_Plugin::getAll(), Loco_package_Theme::getAll() );
|
||||
}
|
||||
/* @var Loco_package_Bundle $bundle */
|
||||
foreach( $search as $bundle ){
|
||||
/* @var Loco_package_Project $project */
|
||||
foreach( $bundle as $project ){
|
||||
if( $project->getDomain()->getName() === $domain ){
|
||||
$this->log('Have %s bundle => %s', strtolower($bundle->getType()), $bundle->getName() );
|
||||
return $bundle;
|
||||
}
|
||||
}
|
||||
}
|
||||
$message = 'No '.$type.' known with text domain '.$domain;
|
||||
Loco_error_AdminNotices::warn($message);
|
||||
$this->log('! '.$message);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return LocoPoMessage|null
|
||||
*/
|
||||
private function findMessage( $findKey, Loco_gettext_Data $messages ){
|
||||
/* @var LocoPoMessage $m */
|
||||
foreach( $messages as $m ){
|
||||
if( $m->getKey() === $findKey ){
|
||||
return $m;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get translation from a message falling back to source, as per __, _n etc..
|
||||
*/
|
||||
private function getMsgstr( LocoPoMessage $m, $pluralIndex = 0 ){
|
||||
$values = $m->exportSerial();
|
||||
if( array_key_exists($pluralIndex,$values) && '' !== $values[$pluralIndex] ){
|
||||
return $values[$pluralIndex];
|
||||
}
|
||||
$values = $m->exportSerial('source');
|
||||
if( $pluralIndex ){
|
||||
if( array_key_exists(1,$values) && '' !== $values[1] ){
|
||||
return $values[1];
|
||||
}
|
||||
$this->log('! message is singular, defaulting to msgid');
|
||||
}
|
||||
return $values[0];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Look up a source key in given messages, returning source if untranslated, and null if not found.
|
||||
* @return string|null
|
||||
*/
|
||||
private function findMsgstr( $findKey, $pluralIndex, Loco_gettext_Data $messages ){
|
||||
$m = $this->findMessage( $findKey, $messages );
|
||||
return $m ? $this->getMsgstr( $m, $pluralIndex ) : null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return Plural_Forms|null
|
||||
*/
|
||||
private function parsePluralForms( $raw ){
|
||||
try {
|
||||
$this->log('Parsing header: %s', $raw );
|
||||
if( ! preg_match( '#^nplurals=\\d+;\\s*plural=([-+/*%!=<>|&?:()n\\d ]+);?$#', $raw, $match ) ) {
|
||||
throw new InvalidArgumentException( 'Invalid Plural-Forms header, ' . json_encode($raw) );
|
||||
}
|
||||
return new Plural_Forms( trim( $match[1],'() ') );
|
||||
}
|
||||
catch( Exception $e ){
|
||||
$this->log('! %s', $e->getMessage() );
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private function selectPluralForm( $quantity, $pluralIndex, ?Plural_Forms $eq = null ){
|
||||
try {
|
||||
if( $eq instanceof Plural_Forms ) {
|
||||
$pluralIndex = $eq->execute( $quantity );
|
||||
$this->log( '> Selected plural form [%u]', $pluralIndex );
|
||||
}
|
||||
}
|
||||
catch ( Exception $e ){
|
||||
$this->log('! Keeping plural form [%u]; %s', $pluralIndex, $e->getMessage() );
|
||||
}
|
||||
return $pluralIndex;
|
||||
}
|
||||
|
||||
|
||||
/*private function logTextDomainsLoaded(){
|
||||
foreach(['l10n','l10n_unloaded'] as $k ){
|
||||
foreach( $GLOBALS[$k] as $d => $t ){
|
||||
$type = is_object($t) ? get_class($t) : gettype($t);
|
||||
$this->log('? $%s[%s] => %s', $k, var_export($d,true), $type );
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
|
||||
/*public function on_unload_textdomain( $domain, $reloadable ){
|
||||
$this->log('~ action:unload_textdomain: %s, reloadable = %b', $domain, $reloadable);
|
||||
}*/
|
||||
|
||||
|
||||
/**
|
||||
* Forcefully remove the no reload flag which prevents JIT loading.
|
||||
* Note that since WP 6.7 load_(theme|plugin)_textdomain invokes JIT loader
|
||||
*/
|
||||
private function unlockDomain( $domain ) {
|
||||
global $l10n_unloaded;
|
||||
if( is_array($l10n_unloaded) && isset($l10n_unloaded[$domain]) ){
|
||||
$this->log('Removing JIT lock');
|
||||
unset( $l10n_unloaded[$domain] );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Prepare text domain for MO file lookup
|
||||
* @return void
|
||||
*/
|
||||
private function preloadDomain( $domain, $type, $path ){
|
||||
// plugin and theme loaders allow missing path argument, custom loader does not
|
||||
if( '' === $path ){
|
||||
$file = null;
|
||||
$path = false;
|
||||
}
|
||||
// Just-in-time loader takes no path argument
|
||||
else if( 'none' === $type || '' === $type ){
|
||||
$file = null;
|
||||
Loco_error_AdminNotices::debug('Path argument ignored. Not required for this loading option.');
|
||||
}
|
||||
else {
|
||||
$this->log('Have path argument => %s', $path );
|
||||
$file = new Loco_fs_File($path);
|
||||
}
|
||||
|
||||
// Without a loader the current state of the text domain will be used for our translation.
|
||||
// If the text domain was loaded before we set our locale, it may be in the wrong language.
|
||||
if( 'none' === $type ){
|
||||
$this->log('No loader, current state is:');
|
||||
$this->logDomainState($domain);
|
||||
// Note that is_textdomain_loaded() returns false even if NOOP_Translations is set,
|
||||
// and NOOP_Translations being set prevents JIT loading, so will never translate our forced locale!
|
||||
if( isset($GLOBALS['l10n'][$domain]) ){
|
||||
// WordPress >= 6.5
|
||||
if( class_exists('WP_Translation_Controller',false) ) {
|
||||
$locale = WP_Translation_Controller::get_instance()->get_locale();
|
||||
if( $locale && $locale !== $this->locale ){
|
||||
Loco_error_AdminNotices::warn( sprintf('Translations already loaded for "%s". A loader is recommended to select "%s"',$locale,$this->locale) );
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Unload text domain for any forced loading method
|
||||
$this->log('Unloading text domain for %s loader', $type?:'auto' );
|
||||
$returned = unload_textdomain($domain);
|
||||
$callee = 'unload_textdomain';
|
||||
// Bootstrap text domain if a loading function was selected
|
||||
if( 'plugin' === $type ){
|
||||
if( $file ){
|
||||
if( $file->isAbsolute() ){
|
||||
$path = $file->getRelativePath(WP_PLUGIN_DIR);
|
||||
}
|
||||
else {
|
||||
$file->normalize(WP_PLUGIN_DIR);
|
||||
}
|
||||
if( ! $file->exists() || ! $file->isDirectory() ){
|
||||
throw new InvalidArgumentException('Loader argument must be a directory relative to WP_PLUGIN_DIR');
|
||||
}
|
||||
}
|
||||
$this->log('Calling load_plugin_textdomain with $plugin_rel_path=%s',$path);
|
||||
$returned = load_plugin_textdomain( $domain, false, $path );
|
||||
$callee = 'load_plugin_textdomain';
|
||||
$this->unlockDomain($domain);
|
||||
}
|
||||
else if( 'theme' === $type || 'child' === $type ){
|
||||
// Note that absent path argument will use current theme, and not necessarily whatever $domain is
|
||||
if( $file && ( ! $file->isAbsolute() || ! $file->isDirectory() ) ){
|
||||
throw new InvalidArgumentException('Path argument must reference the theme directory');
|
||||
}
|
||||
$this->log('Calling load_theme_textdomain with $path=%s',$path);
|
||||
$returned = load_theme_textdomain( $domain, $path );
|
||||
$callee = 'load_theme_textdomain';
|
||||
$this->unlockDomain($domain);
|
||||
}
|
||||
else if( 'custom' === $type ){
|
||||
if( $file && ! $file->isAbsolute() ){
|
||||
$path = $file->normalize(WP_CONTENT_DIR);
|
||||
$this->log('Resolving relative path argument to %s',$path);
|
||||
}
|
||||
if( is_null($file) || ! $file->exists() || $file->isDirectory() ){
|
||||
throw new InvalidArgumentException('Path argument must reference an existent file');
|
||||
}
|
||||
$expected = [ $this->locale.'.mo', $this->locale.'.l10n.php' ];
|
||||
$bits = explode('-',$file->basename() );
|
||||
if( ! in_array( end($bits), $expected) ){
|
||||
throw new InvalidArgumentException('Path argument must end in '.$this->locale.'.mo');
|
||||
}
|
||||
$this->log('Calling load_textdomain with $mofile=%s',$path);
|
||||
$returned = load_textdomain($domain,$path,$this->locale);
|
||||
$callee = 'load_textdomain';
|
||||
}
|
||||
// JIT doesn't work for WordPress core
|
||||
else if( 'default' === $domain ){
|
||||
$this->log('Reloading default text domain');
|
||||
$callee = 'load_default_textdomain';
|
||||
$returned = load_default_textdomain($this->locale);
|
||||
}
|
||||
// Defaulting to JIT (auto):
|
||||
// When we called unload_textdomain we passed $reloadable=false on purpose to force memory removal
|
||||
// So if we want to allow _load_textdomain_just_in_time, we'll have to hack the reloadable lock.
|
||||
else {
|
||||
$this->unlockDomain($domain);
|
||||
}
|
||||
$this->log('> %s returned %s', $callee, var_export($returned,true) );
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Preload domain for a script, then forcing retrieval of JSON.
|
||||
*/
|
||||
private function preloadScript( $path, string $domain, ?Loco_package_Bundle $bundle = null ):Loco_gettext_Data {
|
||||
$this->log('Have script argument => %s', $path );
|
||||
if( preg_match('/^[0-9a-f]{32}$/',$path) ){
|
||||
throw new Loco_error_Exception('Enter the script path, not the hash');
|
||||
}
|
||||
// normalize file reference if bundle is known. Warning already raised if not.
|
||||
// simulator will allow non-existent js. We can still find translations even if it's absent.
|
||||
$jsfile = new Loco_fs_File($path);
|
||||
if( $bundle ){
|
||||
$basepath = $bundle->getDirectoryPath();
|
||||
if( $jsfile->isAbsolute() ) {
|
||||
$path = $jsfile->getRelativePath($basepath);
|
||||
$this->get('form')['jspath'] = $path;
|
||||
}
|
||||
else {
|
||||
$jsfile->normalize($basepath);
|
||||
}
|
||||
if( ! $jsfile->exists() ){
|
||||
$this->log( '! Script not found. load_script_textdomain may fail');
|
||||
}
|
||||
}
|
||||
// log hashable path for comparison with what WordPress computes:
|
||||
if( '.min.js' === substr($path,-7) ) {
|
||||
$path = substr($path,0,-7).'.js';
|
||||
}
|
||||
else {
|
||||
$valid = array_flip( Loco_data_Settings::get()->jsx_alias ?: ['js'] );
|
||||
if( ! array_key_exists($jsfile->extension(),$valid) ) {
|
||||
Loco_error_AdminNotices::debug("Script path didn't end with .".implode('|',array_keys($valid) ) );
|
||||
}
|
||||
}
|
||||
$hash = md5($path);
|
||||
$this->log('> md5(%s) => %s', var_export($path,true), $hash );
|
||||
// filters will point our debug script to the actual script we're simulating
|
||||
$handle = $this->enqueueScript('dummy');
|
||||
if( ! wp_set_script_translations($handle,$domain) ){
|
||||
throw new Loco_error_Exception('wp_set_script_translations returned false');
|
||||
}
|
||||
// load_script_textdomain won't fire until footer, so grab JSON directly
|
||||
$this->log('Calling load_script_textdomain( %s )', trim(json_encode([$handle,$domain],JSON_UNESCAPED_SLASHES),'[]') );
|
||||
$json = load_script_textdomain($handle,$domain);
|
||||
$this->dequeueScript('dummy');
|
||||
if( is_string($json) && '' !== $json ){
|
||||
$this->log('> Parsing %u bytes of JSON...', strlen($json) );
|
||||
return Loco_gettext_Data::fromJson($json);
|
||||
}
|
||||
throw new Loco_error_Exception('load_script_textdomain returned '.var_export($json,true) );
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Run the string lookup and render result screen, unless an error is thrown.
|
||||
* @return string
|
||||
*/
|
||||
private function renderResult( Loco_mvc_ViewParams $form ){
|
||||
$msgid = $form['msgid'];
|
||||
$msgctxt = $form['msgctxt'];
|
||||
// singular form by default
|
||||
$msgid_plural = $form['msgid_plural'];
|
||||
$quantity = ctype_digit($form['n']) ? (int) $form['n'] : 1;
|
||||
$pluralIndex = 0;
|
||||
//
|
||||
$domain = $form['domain']?:'default';
|
||||
$this->log('Running test for domain => %s', $domain );
|
||||
//$this->logDomainState($domain);
|
||||
$default = $this->get('default');
|
||||
$tag = $form['locale'] ?: $default['locale'];
|
||||
$locale = Loco_Locale::parse($tag);
|
||||
if( ! $locale->isValid() ){
|
||||
throw new InvalidArgumentException('Invalid locale code ('.$tag.')');
|
||||
}
|
||||
// unhook all existing filters, including our own
|
||||
if( $form['unhook'] ){
|
||||
$this->log('Unhooking l10n filters');
|
||||
array_map( 'remove_all_filters', [
|
||||
// these filters are all used by Loco_hooks_LoadHelper, and will need re-hooking afterwards:
|
||||
'theme_locale','plugin_locale','unload_textdomain','load_textdomain','load_script_translation_file','load_script_translations',
|
||||
// these filters also affect text domain loading / file reading:
|
||||
'pre_load_textdomain','override_load_textdomain','load_textdomain_mofile','translation_file_format','load_translation_file','override_unload_textdomain','lang_dir_for_domain',
|
||||
// script translation hooks:
|
||||
'load_script_textdomain_relative_path','pre_load_script_translations','load_script_translation_file','load_script_translations',
|
||||
// these filters affect translation fetching via __, _n, _x and _nx:
|
||||
'gettext','ngettext','gettext_with_context','ngettext_with_context'
|
||||
] );
|
||||
// helper isn't a singleton, and will be garbage-collected now. Restart it.
|
||||
new Loco_hooks_LoadHelper;
|
||||
}
|
||||
// Ensuring our forced locale requires no other filters be allowed to run.
|
||||
// We're doing this whether "unhook" is set or not, otherwise determine_locale won't work.
|
||||
remove_all_filters('pre_determine_locale');
|
||||
$this->reHook();
|
||||
$this->locale = (string) $locale;
|
||||
$this->log('Have locale: %s', $this->locale );
|
||||
$actual = determine_locale();
|
||||
if( $actual !== $this->locale ){
|
||||
$this->log('determine_locale() => %s', $actual );
|
||||
Loco_error_AdminNotices::warn( sprintf('Locale %s is overriding %s', $actual, $this->locale) );
|
||||
}
|
||||
// Deferred setting of text domain to avoid hooks firing before we're ready
|
||||
$this->domain = $domain;
|
||||
//new Loco_hooks_LoadDebugger($domain);
|
||||
|
||||
// Perform preloading according to user choice, and optional path argument.
|
||||
$type = $form['loader'];
|
||||
$bundle = $this->getBundleByDomain($domain,$type);
|
||||
$this->preloadDomain( $domain, $type, $form['loadpath'] );
|
||||
|
||||
// Create source message for string query
|
||||
class_exists('Loco_gettext_Data');
|
||||
$message = new LocoPoMessage(['source'=>$msgid,'context'=>$msgctxt,'target'=>'']);
|
||||
$this->log('Query: %s', LocoPo::pair('msgid',$msgid) );
|
||||
if( '' !== $msgid_plural ){
|
||||
$this->log(' | %s (n=%u)', LocoPo::pair('msgid_plural',$msgid_plural), $quantity );
|
||||
$message->offsetSet('plurals', [new LocoPoMessage(['source'=>$msgid_plural,'target'=>''])] );
|
||||
}
|
||||
$findKey = $message->getKey();
|
||||
|
||||
// Perform runtime translation request via WordPress
|
||||
if( '' === $msgctxt ){
|
||||
if( '' === $msgid_plural ) {
|
||||
$callee = '__';
|
||||
$params = [ $msgid, $domain ];
|
||||
$this->addHook('gettext', 'temp_filter_gettext', 3, 99 );
|
||||
}
|
||||
else {
|
||||
$callee = '_n';
|
||||
$params = [ $msgid, $msgid_plural, $quantity, $domain ];
|
||||
$this->addHook('ngettext', 'temp_filter_gettext', 5, 99 );
|
||||
}
|
||||
}
|
||||
else {
|
||||
$this->log(' | %s', LocoPo::pair('msgctxt',$msgctxt) );
|
||||
if( '' === $msgid_plural ){
|
||||
$callee = '_x';
|
||||
$params = [ $msgid, $msgctxt, $domain ];
|
||||
$this->addHook('gettext_with_context', 'temp_filter_gettext', 4, 99 );
|
||||
}
|
||||
else {
|
||||
$callee = '_nx';
|
||||
$params = [ $msgid, $msgid_plural, $quantity, $msgctxt, $domain ];
|
||||
$this->addHook('ngettext_with_context', 'temp_filter_gettext', 6, 99 );
|
||||
}
|
||||
}
|
||||
$this->log('Calling %s( %s )', $callee, trim( json_encode($params,JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE), '[]') );
|
||||
$msgstr = call_user_func_array($callee,$params);
|
||||
$this->log("====>| %s", LocoPo::pair('msgstr',$msgstr,0) );
|
||||
|
||||
// Post check for text domain auto-load failure
|
||||
$loaded = get_translations_for_domain($domain);
|
||||
if( ! is_textdomain_loaded($domain) ){
|
||||
$this->log('! Text domain not loaded after %s() call completed', $callee );
|
||||
$this->log(' get_translations_for_domain => %s', self::debugType($loaded) );
|
||||
}
|
||||
|
||||
// Establish retrospectively if a non-zero plural index was used.
|
||||
if( '' !== $msgid_plural ){
|
||||
$header = null;
|
||||
if( class_exists('WP_Translation_Controller',false) ){
|
||||
$h = WP_Translation_Controller::get_instance()->get_headers($domain);
|
||||
if( array_key_exists('Plural-Forms',$h) ) {
|
||||
$header = $h['Plural-Forms'];
|
||||
}
|
||||
}
|
||||
if( is_null($header) ){
|
||||
$header = $locale->getPluralFormsHeader();
|
||||
$this->log('! Can\'t get Plural-Forms; Using built-in rules');
|
||||
}
|
||||
$pluralIndex = $this->selectPluralForm( $quantity, $pluralIndex, $this->parsePluralForms($header) );
|
||||
}
|
||||
|
||||
// Simulate JavaScript translation if script path is set. This will be used as a secondary result.
|
||||
$path = $form['jspath'];
|
||||
if( is_string($path) && '' !== $path ) {
|
||||
try {
|
||||
$data = $this->preloadScript( $path, $domain, $bundle );
|
||||
// Let JED-defined plural forms override plural index
|
||||
if( '' !== $msgid_plural ){
|
||||
$header = $data->getHeaders()->offsetGet('Plural-Forms');
|
||||
if( $header ){
|
||||
$pluralIndex = $this->selectPluralForm( $quantity, $pluralIndex, $this->parsePluralForms($header) );
|
||||
}
|
||||
}
|
||||
$msgstr = $this->findMsgstr( $findKey, $pluralIndex, $data );
|
||||
if( is_null($msgstr) ){
|
||||
$this->log('! No match in JSON');
|
||||
}
|
||||
else {
|
||||
$this->log("====>| %s", LocoPo::pair('msgstr',$msgstr,0) );
|
||||
}
|
||||
// Override primary translation result for script translation
|
||||
$callee = 'load_script_textdomain';
|
||||
}
|
||||
catch( Exception $e ){
|
||||
$this->log('! %s (falling back to PHP)', $e->getMessage() );
|
||||
Loco_error_AdminNotices::warn('Script translation failed. Falling back to PHP translation');
|
||||
}
|
||||
}
|
||||
|
||||
// Establish translation success, assuming that source being returned is equivalent to an absent translation
|
||||
$fallback = $pluralIndex ? $msgid_plural : $msgid;
|
||||
$translated = is_string($msgstr) && '' !== $msgstr && $msgstr !== $fallback;
|
||||
$this->log('Translated result state => %s', $translated?'true':'false');
|
||||
|
||||
// We're done with our temporary hooks now.
|
||||
$this->domain = null;
|
||||
$this->locale = null;
|
||||
|
||||
// Obtain all possible translations from all known targets (requires bundle)
|
||||
$pofiles = new Loco_fs_FileList;
|
||||
if( $bundle ){
|
||||
foreach( $bundle as $project ) {
|
||||
if( $project instanceof Loco_package_Project && $project->getDomain()->getName() === $domain ){
|
||||
$pofiles->augment( $project->initLocaleFiles($locale) );
|
||||
}
|
||||
}
|
||||
}
|
||||
// Without a configured bundle, we'll have to search all possible locations, but this won't include Author files.
|
||||
// We may as well add these anyway, in case bundle is misconfigured. Small risk of plugin/theme domain conflicts.
|
||||
if( 'default' !== $domain ){
|
||||
/* @var Loco_package_Bundle $tmp */
|
||||
foreach( [ new Loco_package_Plugin('',''), new Loco_package_Theme('','') ] as $tmp ) {
|
||||
foreach( $tmp->getSystemTargets() as $root ){
|
||||
$pofiles->add( new Loco_fs_LocaleFile( sprintf('%s/%s-%s.po',$root,$domain,$locale) ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
$grouped = [];
|
||||
$matches = [];
|
||||
$searched = 0;
|
||||
$matched = 0;
|
||||
$this->log('Searching %u possible locations for string versions', $pofiles->count() );
|
||||
/* @var Loco_fs_LocaleFile $pofile */
|
||||
foreach( $pofiles as $pofile ){
|
||||
// initialize translation set for this PO and its siblings
|
||||
$dir = new Loco_fs_LocaleDirectory( $pofile->dirname() );
|
||||
$type = $dir->getTypeId();
|
||||
$args = [ 'type' => $dir->getTypeLabel($type) ];
|
||||
// as long as we know the bundle and the PO file exists, we can link to the editor.
|
||||
// bear in mind that domain may not be unique to one set of translations (core) so ...
|
||||
if( $bundle && $pofile->exists() ){
|
||||
$route = strtolower($bundle->getType()).'-file-edit';
|
||||
// find exact project in bundle. Required for core, or any multi-domain bundle
|
||||
$project = $bundle->getDefaultProject();
|
||||
if( is_null($project) || 1 < $bundle->count() ){
|
||||
$slug = $pofile->getPrefix();
|
||||
foreach( $bundle as $candidate ){
|
||||
if( $candidate->getSlug() === $slug ){
|
||||
$project = $candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
$args['href'] = Loco_mvc_AdminRouter::generate( $route, [
|
||||
'bundle' => $bundle->getHandle(),
|
||||
'domain' => $project ? $project->getId() : $domain,
|
||||
'path' => $pofile->getRelativePath(WP_CONTENT_DIR),
|
||||
] );
|
||||
}
|
||||
$groupIdx = count($grouped);
|
||||
$grouped[] = new Loco_mvc_FileParams( $args, $pofile );
|
||||
// even if PO file is missing, we can search the MO, JSON etc..
|
||||
$siblings = new Loco_fs_Siblings($pofile);
|
||||
$siblings->setDomain($domain);
|
||||
$exts = [];
|
||||
foreach( $siblings->expand() as $file ){
|
||||
try {
|
||||
$ext = strtolower( $file->fullExtension() );
|
||||
if( ! preg_match('!^(?:pot?|mo|json|l10n\\.php)$!',$ext) || ! $file->exists() ){
|
||||
continue;
|
||||
}
|
||||
$searched++;
|
||||
$message = $this->findMessage($findKey,Loco_gettext_Data::load($file));
|
||||
if( $message ){
|
||||
$matched++;
|
||||
$value = $this->getMsgstr($message,$pluralIndex);
|
||||
$args = [ 'msgstr' => $value ];
|
||||
$matches[$groupIdx][] = new Loco_mvc_FileParams($args,$file);
|
||||
$this->log('> found in %s => %s', $file, var_export($value,true) );
|
||||
$exts[$ext] = $message->translated();
|
||||
}
|
||||
}
|
||||
catch( Exception $e ){
|
||||
Loco_error_Debug::trace( '%s in %s', $e->getMessage(), $file );
|
||||
}
|
||||
}
|
||||
// warn if found in PO, but not MO.
|
||||
if( isset($exts['po']) && $exts['po'] && ! isset($exts['mo']) ){
|
||||
Loco_error_AdminNotices::debug('Found in PO, but not MO. Is it fuzzy? Does it need recompiling?');
|
||||
}
|
||||
}
|
||||
|
||||
// display result if translation occurred, or if we found the string in at least one file, even if empty
|
||||
$this->log('> %u matches in %u locations; %u files searched', $matched, count($grouped), $searched );
|
||||
if( $matches || $translated ){
|
||||
$result = new Loco_mvc_ViewParams( $form->getArrayCopy() );
|
||||
$result['translated'] = $translated;
|
||||
$result['msgstr'] = $msgstr;
|
||||
$result['callee'] = $callee;
|
||||
$result['grouped'] = $grouped;
|
||||
$result['matches'] = $matches;
|
||||
$result['searched'] = $searched;
|
||||
$result['calleeDoc'] = 'https://developer.wordpress.org/reference/functions/'.$callee.'/';
|
||||
return $this->view( 'admin/debug/debug-result', ['result'=>$result]);
|
||||
}
|
||||
// Source string not found in any translation files
|
||||
$name = $bundle ? $bundle->getName() : $domain;
|
||||
throw new Loco_error_Warning('No `'.$locale.'` translations found for this string in '.$name );
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
private function surpriseMe(){
|
||||
$project = null;
|
||||
/* @var Loco_package_Bundle[] $bundles */
|
||||
$bundles = array_merge( Loco_package_Plugin::getAll(), Loco_package_Theme::getAll(), [ Loco_package_Core::create() ] );
|
||||
while( $bundles && is_null($project) ){
|
||||
$key = array_rand($bundles);
|
||||
$project = $bundles[$key]->getDefaultProject();
|
||||
unset($bundles[$key]);
|
||||
}
|
||||
// It should be impossible for project to be null, due to WordPress core always being non-empty
|
||||
if( ! $project instanceof Loco_package_Project ){
|
||||
throw new LogicException('No translation projects');
|
||||
}
|
||||
$domain = $project->getDomain()->getName();
|
||||
// Pluck a random locale from existing PO translations
|
||||
$files = $project->findLocaleFiles('po')->getArrayCopy();
|
||||
$pofile = $files ? $files[ array_rand($files) ] : null;
|
||||
$locale = $pofile instanceof Loco_fs_LocaleFile ? (string) $pofile->getLocale() : '';
|
||||
// Get a random source string from the code... avoiding full extraction.. pluck a PHP file...
|
||||
class_exists('Loco_gettext_Data');
|
||||
$message = new LocoPoMessage(['source'=>'']);
|
||||
$extractor = loco_wp_extractor();
|
||||
$extractor->setDomain($domain);
|
||||
$files = $project->getSourceFinder()->group('php')->export()->getArrayCopy();
|
||||
while( $files ){
|
||||
$key = array_rand($files);
|
||||
$file = $files[$key];
|
||||
$strings = ( new LocoExtracted )->extractSource( $extractor, $file->getContents() )->export();
|
||||
if( $strings ){
|
||||
$message = new LocoPoMessage( $strings[ array_rand($strings) ] );
|
||||
break;
|
||||
}
|
||||
// try next source file...
|
||||
unset($files[$key]);
|
||||
}
|
||||
// apply random choice
|
||||
$form = $this->get('form');
|
||||
$form['domain'] = $domain;
|
||||
$form['locale'] = $locale;
|
||||
$form['msgid'] = $message->source;
|
||||
$form['msgctxt'] = $message->context;
|
||||
// random message could be a plural form
|
||||
$plurals = $message->plurals;
|
||||
if( is_array($plurals) && array_key_exists(0,$plurals) && $plurals[0] instanceof LocoPoMessage ){
|
||||
$form['msgid_plural'] = $plurals[0]['source'];
|
||||
}
|
||||
Loco_error_AdminNotices::info( sprintf('Randomly selected "%s". Click Submit to check a string.', $project->getName() ) );
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function render(){
|
||||
$title = __('String debugger','loco-translate');
|
||||
$this->set('breadcrumb', Loco_admin_Navigation::createSimple($title) );
|
||||
|
||||
try {
|
||||
// Process form if (at least) "msgid" is set
|
||||
$form = $this->get('form');
|
||||
if( '' !== $form['msgid'] ){
|
||||
return $this->renderResult($form);
|
||||
}
|
||||
// Pluck a random string for testing
|
||||
else if( array_key_exists('randomize',$_GET) ){
|
||||
$this->surpriseMe();
|
||||
}
|
||||
}
|
||||
catch( Loco_error_Exception $e ){
|
||||
Loco_error_AdminNotices::add($e);
|
||||
}
|
||||
catch( Exception $e ){
|
||||
Loco_error_AdminNotices::add( Loco_error_Exception::convert($e) );
|
||||
}
|
||||
|
||||
return $this->view('admin/debug/debug-form');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 = [];
|
||||
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() );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
56
wp-content/plugins/loco-translate/src/admin/Navigation.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
/**
|
||||
* Generic navigation helper.
|
||||
*/
|
||||
class Loco_admin_Navigation extends ArrayIterator {
|
||||
|
||||
|
||||
public function add( string $name, ?string $href = null, ?bool $active = false ):self {
|
||||
$this[] = new Loco_mvc_ViewParams( compact('name','href','active') );
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a breadcrumb trail for a given view below a bundle
|
||||
*/
|
||||
public static function createBreadcrumb( Loco_package_Bundle $bundle ):self {
|
||||
$nav = new Loco_admin_Navigation;
|
||||
|
||||
// root link depends on bundle type
|
||||
$type = strtolower( $bundle->getType() );
|
||||
if( 'core' !== $type ){
|
||||
$link = new Loco_mvc_ViewParams( [
|
||||
'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', [ 'bundle' => $bundle->getHandle() ] )
|
||||
);
|
||||
|
||||
// client code will add current page
|
||||
return $nav;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a basic breadcrumb comprising title only
|
||||
*/
|
||||
public static function createSimple( string $name ):self {
|
||||
$nav = new Loco_admin_Navigation;
|
||||
$nav->add($name);
|
||||
return $nav;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
/**
|
||||
* Highest level Loco admin screen.
|
||||
*/
|
||||
class Loco_admin_RootController extends Loco_admin_list_BaseController {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getHelpTabs(){
|
||||
return [
|
||||
__('Overview','loco-translate') => $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( get_stylesheet() );
|
||||
$this->set('theme', $this->bundleParam($theme) );
|
||||
|
||||
// Show plugins that have currently loaded translations
|
||||
$bundles = [];
|
||||
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 = [];
|
||||
$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( [
|
||||
'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', ['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( [
|
||||
'name' => ( $name = $locale->ensureName($api) ),
|
||||
'link' => '<a href="'.esc_url(Loco_mvc_AdminRouter::generate('lang-view', ['locale'=>$tag] )).'">'.esc_html($name).'</a>',
|
||||
] ) );
|
||||
}
|
||||
}
|
||||
|
||||
$this->set('title', __('Welcome to Loco Translate','loco-translate') );
|
||||
|
||||
// Deprecation warnings:
|
||||
// At time of writing, WordPress below 5.2 accounts for about a quarter, whereas PHP below is 5.6 half of that.
|
||||
/* if( version_compare(PHP_VERSION,'5.6.20','<') || version_compare($GLOBALS['wp_version'],'5.2','<') ){
|
||||
Loco_error_AdminNotices::warn('The next release of Loco Translate will require at least WordPress 5.2 and PHP 5.6.20'); // @codeCoverageIgnore
|
||||
}*/
|
||||
|
||||
return $this->view('admin/root');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
/**
|
||||
* Base controller for any admin screen related to a bundle
|
||||
*/
|
||||
abstract class Loco_admin_bundle_BaseController extends Loco_mvc_AdminController {
|
||||
|
||||
private ?Loco_package_Bundle $bundle = null;
|
||||
|
||||
private ?Loco_package_Project $project = null;
|
||||
|
||||
|
||||
public function getBundle():Loco_package_Bundle {
|
||||
if( ! $this->bundle ){
|
||||
$type = $this->get('type');
|
||||
$handle = $this->get('bundle')??'';
|
||||
$this->bundle = Loco_package_Bundle::createType( $type, $handle );
|
||||
}
|
||||
return $this->bundle;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get current project's text domain if available
|
||||
*/
|
||||
public function getDomain():string {
|
||||
$project = $this->getOptionalProject();
|
||||
if( $project instanceof Loco_package_Project ){
|
||||
return $project->getDomain()->getName();
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Commit bundle config to database
|
||||
*/
|
||||
protected function saveBundle():self {
|
||||
$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
|
||||
*/
|
||||
protected function resetBundle():self {
|
||||
$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;
|
||||
}
|
||||
|
||||
|
||||
protected function getProject():Loco_package_Project{
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
protected function getOptionalProject():?Loco_package_Project {
|
||||
try {
|
||||
return $this->getProject();
|
||||
}
|
||||
catch( Throwable $e ){
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected function prepareNavigation():Loco_admin_Navigation {
|
||||
$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 = [
|
||||
'view' => __('Overview','loco-translate'),
|
||||
'setup' => __('Setup','loco-translate'),
|
||||
'conf' => __('Advanced','loco-translate'),
|
||||
];
|
||||
// Debugger is deprecated. It remains accessible but will be removed or replaced in future versions
|
||||
// If you want to see the Debug button, hook in the following filter with "__return_true"
|
||||
if( apply_filters('loco_deprecated',false) ){
|
||||
$actions['debug'] = __('Debug','loco-translate');
|
||||
}
|
||||
$suffix = $this->get('action');
|
||||
$prefix = strtolower( $this->get('type') );
|
||||
$getarg = array_intersect_key( $_GET, ['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 $type "create", "update", "delete"
|
||||
* @param string $relpath Path relative to wp-content
|
||||
*/
|
||||
protected function prepareFsConnect( string $type, string $relpath ):Loco_mvc_HiddenFields {
|
||||
|
||||
$fields = new Loco_mvc_HiddenFields( [
|
||||
'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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
<?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, and security check passes
|
||||
if( $post->has('conf') && $this->checkNonce($nonce->action) ){
|
||||
if( ! $post->name ){
|
||||
$post->name = $bundle->getName();
|
||||
}
|
||||
$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->checkNonce($nonce->action) ){
|
||||
$this->resetBundle();
|
||||
}
|
||||
}
|
||||
catch( Exception $e ){
|
||||
Loco_error_AdminNotices::warn( $e->getMessage() );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getHelpTabs(){
|
||||
return [
|
||||
__('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( [
|
||||
'name' => $parent->getName(),
|
||||
'href' => Loco_mvc_AdminRouter::generate('theme-conf', [ '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 = [];
|
||||
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 = [
|
||||
'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') );
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
<?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 [
|
||||
__('Overview','loco-translate') => $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 );
|
||||
|
||||
// search for base language, unless it's a separate, installed language
|
||||
if( $locale->lang !== (string) $locale ){
|
||||
$fallback = new Loco_Locale($locale->lang);
|
||||
if( ! $api->isInstalled($fallback) ){
|
||||
$package->addLocale($fallback);
|
||||
}
|
||||
}
|
||||
|
||||
// Get PO files for this locale
|
||||
$files = $package->findLocaleFiles();
|
||||
$translations = [];
|
||||
$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 $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 = [
|
||||
// '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( [
|
||||
// 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 = [
|
||||
'core' => new Loco_mvc_ViewParams( [
|
||||
'name' => __('WordPress Core','loco-translate'),
|
||||
'text' => __('See all core translations','loco-translate'),
|
||||
'href' => Loco_mvc_AdminRouter::generate('core')
|
||||
] ),
|
||||
'theme' => new Loco_mvc_ViewParams( [
|
||||
'name' => __('Themes','loco-translate'),
|
||||
'text' => __('See all themes','loco-translate'),
|
||||
'href' => Loco_mvc_AdminRouter::generate('theme')
|
||||
] ),
|
||||
'plugin' => new Loco_mvc_ViewParams( [
|
||||
'name' => __('Plugins','loco-translate'),
|
||||
'text' => __('See all plugins','loco-translate'),
|
||||
'href' => Loco_mvc_AdminRouter::generate('plugin')
|
||||
] ),
|
||||
];
|
||||
|
||||
$this->set( 'locale', new Loco_mvc_ViewParams( [
|
||||
'code' => $tag,
|
||||
'name' => $locale->getName(),
|
||||
'attr' => 'class="'.$locale->getIcon().'" lang="'.$locale->lang.'"',
|
||||
] ) );
|
||||
|
||||
// Sort each translation set alphabetically by bundle name...
|
||||
foreach( array_keys($translations) as $type ){
|
||||
usort( $translations[$type], function( ArrayAccess $a, ArrayAccess $b ):int {
|
||||
return strcasecmp($a['title'],$b['title']);
|
||||
} );
|
||||
}
|
||||
|
||||
return $this->view( 'admin/bundle/locale', compact('breadcrumb','translations','types','npofiles','modified') );
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
<?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 [
|
||||
__('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 ){
|
||||
// translators: %s is a quantity of files which were found, but whose context is unknown
|
||||
$notices[] = sprintf( _n("%s 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_i18n($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;
|
||||
}
|
||||
|
||||
/*/ JSON config via remote lookup has been scrapped
|
||||
if( $this->get('json') ){
|
||||
$fields = new Loco_mvc_HiddenFields( [
|
||||
'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( [
|
||||
'apiUrl' => $apiBase.'/wp/'.strtolower( $bundle->getType() ),
|
||||
] ) );
|
||||
$doconf = true;
|
||||
}*/
|
||||
|
||||
|
||||
// display configurator if configuring
|
||||
if( $doconf ){
|
||||
return $this->view( 'admin/bundle/setup/conf' );
|
||||
}
|
||||
|
||||
// Add some debugging information on all screens except config
|
||||
// this used to be accessed via the Debug tab, which is removed
|
||||
if( loco_debugging() && count($bundle) ){
|
||||
$this->set('debug', $this->getDebug($bundle) );
|
||||
}
|
||||
|
||||
// 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');
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @return Loco_mvc_ViewParams
|
||||
*/
|
||||
private function getDebug( Loco_package_Bundle $bundle ){
|
||||
$debug = new Loco_mvc_ViewParams;
|
||||
|
||||
// XML config
|
||||
$writer = new Loco_config_BundleWriter($bundle);
|
||||
$debug['xml'] = $writer->toXml();
|
||||
|
||||
// general notes, followed by related warnings
|
||||
$notes = [];
|
||||
$warns = [];
|
||||
|
||||
// show auto-detected settings, either assumed (by wp) or declared (by author)
|
||||
if( 'meta' === $bundle->isConfigured() ){
|
||||
// Text Domain:
|
||||
$native = $bundle->getHeaderInfo();
|
||||
$domain = $native->TextDomain;
|
||||
if( $domain ){
|
||||
// Translators: %s will be replaced with a text domain, e.g. "loco-translate"
|
||||
$notes[] = sprintf( __('WordPress says the primary text domain is "%s"','loco-translate'), $domain );
|
||||
// WordPress 4.6 changes mean this header could be a fallback and not actually declared by the author
|
||||
if( $bundle->isPlugin() ) {
|
||||
$map = [ 'TextDomain' => 'Text Domain' ];
|
||||
$raw = get_file_data( $bundle->getBootstrapPath(), $map, 'plugin' );
|
||||
if( ! isset($raw['TextDomain']) || '' === $raw['TextDomain'] ) {
|
||||
// Translators: This warning is shown when a text domain has defaulted to same as the folder name (or slug)
|
||||
$warns[] = __("This plugin doesn't declare a text domain. It's assumed to be the same as the slug, but this could be wrong",'loco-translate');
|
||||
}
|
||||
}
|
||||
// Warn if WordPress-assumed text domain is not configured. plugin/theme headers won't be translated
|
||||
$domains = $bundle->getDomains();
|
||||
if ( ! isset($domains[$domain ]) && ! isset($domains['*']) ) {
|
||||
$warns[] = __("This text domain is not in Loco Translate's bundle configuration",'loco-translate');
|
||||
}
|
||||
}
|
||||
else {
|
||||
$warns[] = __("This bundle does't declare a text domain; try configuring it in the Advanced tab",'loco-translate');
|
||||
}
|
||||
// Domain Path:
|
||||
$path = $native->DomainPath;
|
||||
if( $path ){
|
||||
// Translators: %s will be replaced with a relative path like "/languages"
|
||||
$notes[] = sprintf( __('The domain path is declared by the author as "%s"','loco-translate'), $path );
|
||||
}
|
||||
else {
|
||||
$guess = new Loco_fs_Directory( $bundle->getDirectoryPath().'/languages' );
|
||||
if( $guess->readable() ){
|
||||
$notes[] = __('The default "languages" domain path has been detected','loco-translate');
|
||||
}
|
||||
else {
|
||||
$warns[] = __("This bundle doesn't declare a domain path. Add one via the Advanced tab if needed",'loco-translate');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$debug['notices'] = [ 'info' => $notes, 'warning' => $warns ];
|
||||
return $debug;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
<?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 [
|
||||
__('Overview','loco-translate') => $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 ){
|
||||
return $this->getProjectLink( $page, $project, [
|
||||
'path' => $meta->getPath(false),
|
||||
] );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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 = [] ){
|
||||
$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
|
||||
* @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( [
|
||||
'id' => $project->getId(),
|
||||
'name' => $name,
|
||||
'slug' => $slug,
|
||||
'domain' => $domain,
|
||||
'short' => ! $slug || $project->isDomainDefault() ? $domain : $domain.'→'.$slug,
|
||||
'warnings' => [],
|
||||
] );
|
||||
|
||||
// POT template file
|
||||
$pot = null;
|
||||
$file = $project->getPot();
|
||||
if( $file && $file->exists() ){
|
||||
$pot = Loco_gettext_Metadata::load($file);
|
||||
$p['pot'] = new Loco_mvc_ViewParams( [
|
||||
// POT info
|
||||
'name' => $file->basename(),
|
||||
'time' => $file->modified(),
|
||||
// POT links
|
||||
'info' => $this->getResourceLink('file-info', $project, $pot ),
|
||||
'edit' => $this->getResourceLink('file-edit', $project, $pot ),
|
||||
] );
|
||||
}
|
||||
|
||||
// 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 );
|
||||
|
||||
// offer msginit unless plugin settings disallows optional POT
|
||||
if( $pot || 2 > Loco_data_Settings::get()->pot_expected ){
|
||||
$p['nav'][] = new Loco_mvc_ViewParams( [
|
||||
'href' => $this->getProjectLink('msginit', $project ),
|
||||
'name' => __('New language','loco-translate'),
|
||||
'icon' => 'add',
|
||||
] );
|
||||
}
|
||||
|
||||
// Always offer PO file upload
|
||||
$p['nav'][] = new Loco_mvc_ViewParams( [
|
||||
'href' => $this->getProjectLink('upload', $project ),
|
||||
'name' => __('Upload PO','loco-translate'),
|
||||
'icon' => 'upload',
|
||||
] );
|
||||
|
||||
// prevent editing of POT when config prohibits
|
||||
if( $pot ){
|
||||
if( $project->isPotLocked() || 1 < Loco_data_Settings::get()->pot_protect ) {
|
||||
$p['nav'][] = new Loco_mvc_ViewParams( [
|
||||
'href' => $this->getResourceLink('file-view', $project, $pot ),
|
||||
'name' => __('View template','loco-translate'),
|
||||
'icon' => 'file',
|
||||
] );
|
||||
}
|
||||
// offer template editing if permitted
|
||||
else {
|
||||
$p['nav'][] = new Loco_mvc_ViewParams( [
|
||||
'href' => $this->getResourceLink('file-edit', $project, $pot ),
|
||||
'name' => __('Edit template','loco-translate'),
|
||||
'icon' => 'pencil',
|
||||
] );
|
||||
}
|
||||
}
|
||||
// else offer creation of new Template
|
||||
else {
|
||||
$p['nav'][] = new Loco_mvc_ViewParams( [
|
||||
'href' => $this->getProjectLink('xgettext', $project ),
|
||||
'name' => __('Create template','loco-translate'),
|
||||
'icon' => 'add',
|
||||
] );
|
||||
}
|
||||
|
||||
// foreach locale, establish if text domain is installed in system location, flag if not.
|
||||
$installed = [];
|
||||
foreach( $p['po'] as $pair ){
|
||||
$lc = $pair['lcode'];
|
||||
if( $pair['installed'] ){
|
||||
$installed[$lc] = true;
|
||||
}
|
||||
else if( ! array_key_exists($lc,$installed) ){
|
||||
$installed[$lc] = false;
|
||||
}
|
||||
}
|
||||
$p['installed'] = $installed;
|
||||
// warning only necessary for WP<6.6 due to `lang_dir_for_domain` fix
|
||||
if( ! function_exists('wp_get_l10n_php_file_data') && in_array(false,$installed,true) ){
|
||||
$p['warnings'][] = __('Custom translations may not work without System translations installed','loco-translate');
|
||||
}
|
||||
return $p;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Collect PO/MO pairings, ignoring any PO that is in use as a template
|
||||
* @return array[]
|
||||
*/
|
||||
private function createPairs( Loco_fs_FileList $po, Loco_fs_FileList $mo, ?Loco_fs_File $pot = null ):array {
|
||||
$pairs = [];
|
||||
/* @var $pofile Loco_fs_LocaleFile */
|
||||
foreach( $po as $pofile ){
|
||||
if( $pot && $pofile->equal($pot) ){
|
||||
continue;
|
||||
}
|
||||
$pair = [ $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[] = [ 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 ):array {
|
||||
// populate official locale names for all found, or default to our own
|
||||
if( $locales = $po->getLocales() + $mo->getLocales() ){
|
||||
$api = new Loco_api_WordPressTranslations;
|
||||
foreach( $locales as $locale ){
|
||||
$locale->ensureName($api);
|
||||
}
|
||||
}
|
||||
// collate as unique [PO,MO] pairs ensuring canonical template excluded
|
||||
$pairs = $this->createPairs( $po, $mo, $project->getPot() );
|
||||
$rows = [];
|
||||
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 );
|
||||
}
|
||||
// Sort PO pairs in alphabetical order, with custom before system, before author
|
||||
usort( $rows, function( ArrayAccess $a, ArrayAccess $b ):int {
|
||||
return strcasecmp( $a['lname'], $b['lname'] );
|
||||
} );
|
||||
return $rows;
|
||||
}
|
||||
|
||||
|
||||
private function createFileParams( Loco_package_Project $project, Loco_fs_File $file, ?Loco_Locale $locale = null ):Loco_mvc_ViewParams {
|
||||
// Pull Gettext meta data from cache if possible
|
||||
$meta = Loco_gettext_Metadata::load($file);
|
||||
$dir = new Loco_fs_LocaleDirectory( $file->dirname() );
|
||||
$dType = $dir->getTypeId();
|
||||
// routing arguments
|
||||
$args = [
|
||||
'path' => $meta->getPath(false),
|
||||
];
|
||||
// Return data required for PO table row
|
||||
return new Loco_mvc_ViewParams( [
|
||||
// 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
|
||||
'installed' => 'wplang' === $dType,
|
||||
'store' => $dir->getTypeLabel($dType),
|
||||
// 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
|
||||
* @return Loco_mvc_ViewParams[]
|
||||
*/
|
||||
private function createBundleListing( Loco_package_Bundle $bundle ){
|
||||
$projects = [];
|
||||
/* @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( 'Hello Dolly' === $bundle->getName() && 'hello.php' === basename($bundle->getHandle()) ){
|
||||
if( ! $configured || 'meta' === $configured ){
|
||||
$this->set( 'redirect', Loco_mvc_AdminRouter::generate('core-view') );
|
||||
return $this->view('admin/bundle/alias');
|
||||
}
|
||||
}
|
||||
|
||||
// Collect all configured projects
|
||||
$projects = $this->createBundleListing( $bundle );
|
||||
$unknown = [];
|
||||
|
||||
// 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 = [];
|
||||
$po = new Loco_fs_LocaleFileList;
|
||||
$mo = new Loco_fs_LocaleFileList;
|
||||
$prefs = Loco_data_Preferences::get();
|
||||
foreach( Loco_package_Inverter::export($bundle) as $ext => $files ){
|
||||
$list = 'mo' === $ext ? $mo : $po;
|
||||
foreach( $files as $file ){
|
||||
$file = new Loco_fs_LocaleFile($file);
|
||||
$locale = $file->getLocale();
|
||||
if( $prefs && ! $prefs->has_locale($locale) ){
|
||||
continue;
|
||||
}
|
||||
$list->addLocalized( $file );
|
||||
// Only look in system locations if locale is valid and domain/prefix available
|
||||
if( $locale->isValid() ){
|
||||
$domain = $file->getPrefix();
|
||||
if( $domain ) {
|
||||
$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' );
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
/**
|
||||
* API keys/settings screen
|
||||
*/
|
||||
class Loco_admin_config_ApisController extends Loco_admin_config_BaseController {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function init(){
|
||||
parent::init();
|
||||
$this->set( 'title', __('API keys','loco-translate') );
|
||||
|
||||
// Collect configurable API keys bundled with plugin
|
||||
$apis = [];
|
||||
foreach( Loco_api_Providers::builtin() as $api ){
|
||||
$apis[ $api['id'] ] = new Loco_mvc_ViewParams($api);
|
||||
}
|
||||
// Add any additional API hooks for information only
|
||||
$hooked = [];
|
||||
foreach( Loco_api_Providers::export() as $api ){
|
||||
$id = $api['id'];
|
||||
if( ! array_key_exists($id,$apis) ){
|
||||
$hooked[ $id ] = new Loco_mvc_ViewParams($api);
|
||||
}
|
||||
}
|
||||
|
||||
$this->set('apis',$apis);
|
||||
$this->set('hooked',$hooked);
|
||||
|
||||
// handle save action
|
||||
$nonce = $this->setNonce('save-apis');
|
||||
try {
|
||||
if( $this->checkNonce($nonce->action) ){
|
||||
$post = Loco_mvc_PostParams::get();
|
||||
if( $post->has('api') ){
|
||||
// Save only options in post. Avoids overwrite of missing site options
|
||||
$data = [];
|
||||
$filter = [];
|
||||
foreach( $apis as $id => $api ){
|
||||
$fields = $post->api[$id]??null;
|
||||
if( is_array($fields) ){
|
||||
foreach( $fields as $prop => $value ){
|
||||
$apis[$id][$prop] = $value;
|
||||
$prop = $id.'_api_'.$prop;
|
||||
$data[$prop] = $value;
|
||||
$filter[] = $prop;
|
||||
}
|
||||
}
|
||||
}
|
||||
if( $filter ){
|
||||
Loco_data_Settings::get()->populate($data,$filter)->persistIfDirty();
|
||||
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 );
|
||||
|
||||
// common ui elements / labels
|
||||
$this->set( 'ui', new Loco_mvc_ViewParams( [
|
||||
'api_key' => __('API key','loco-translate'),
|
||||
'api_url' => __('API URL','loco-translate'),
|
||||
'api_region' => __('API region','loco-translate'),
|
||||
] ) );
|
||||
|
||||
return $this->view('admin/config/apis', compact('breadcrumb') );
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 = [
|
||||
'' => __('Site options','loco-translate'),
|
||||
'user' => __('User options','loco-translate'),
|
||||
'apis' => __('API keys','loco-translate'),
|
||||
'version' => __('Version','loco-translate'),
|
||||
'debug' => __('System','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 [
|
||||
__('Overview','loco-translate') => $this->viewSnippet('tab-config'),
|
||||
__('API keys','loco-translate') => $this->viewSnippet('tab-config-apis'),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
<?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 $raw
|
||||
* @return int
|
||||
*/
|
||||
private function memory_size( $raw ){
|
||||
$bytes = wp_convert_hr_to_bytes($raw);
|
||||
return Loco_mvc_FileParams::renderBytes($bytes);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get path relative to WordPress ABSPATH
|
||||
* @param string $path
|
||||
* @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;
|
||||
}
|
||||
|
||||
|
||||
private function file_params( Loco_fs_File $file ){
|
||||
$ctx = new Loco_fs_FileWriter($file);
|
||||
return new Loco_mvc_ViewParams(['path'=>$this->rel_path($file->getPath()), 'writable'=>$ctx->writable()]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* {@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( [
|
||||
'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() : '' ),
|
||||
'jQuery' => '...',
|
||||
] );
|
||||
// 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);
|
||||
}
|
||||
// Add Xdebug version if installed
|
||||
if( extension_loaded('xdebug') ){
|
||||
$versions['PHP'] .= ' + Xdebug '. phpversion('xdebug');
|
||||
}
|
||||
|
||||
// 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:
|
||||
$cs = get_option('blog_charset');
|
||||
$encoding = new Loco_mvc_ViewParams( [
|
||||
'OK' => "\xCE\x9F\xCE\x9A",
|
||||
'tick' => "\xE2\x9C\x93",
|
||||
'json' => json_decode('"\\u039f\\u039a \\u2713"'),
|
||||
'charset' => $cs.' '.( preg_match('/^utf-?8$/i',$cs) ? "\xE2\x9C\x93" : '(not recommended)' ),
|
||||
'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( [
|
||||
'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('system');
|
||||
$this->set( 'js', new Loco_mvc_ViewParams( [
|
||||
'nonces' => [ 'ping' => wp_create_nonce('ping'), 'apis' => wp_create_nonce('apis') ],
|
||||
] ) );
|
||||
|
||||
// Third party API integrations:
|
||||
$apis = [];
|
||||
$jsapis = [];
|
||||
foreach( Loco_api_Providers::sort( Loco_api_Providers::export() ) as $api ){
|
||||
$apis[] = new Loco_mvc_ViewParams($api);
|
||||
$jsapis[] = $api;
|
||||
}
|
||||
if( $apis ){
|
||||
$this->set('apis',$apis);
|
||||
$jsconf = $this->get('js');
|
||||
$jsconf['apis'] = $jsapis;
|
||||
}
|
||||
|
||||
// File system access
|
||||
$ctx = new Loco_fs_FileWriter( new Loco_fs_Directory(WP_LANG_DIR) );
|
||||
$fsp = Loco_data_Settings::get()->fs_protect;
|
||||
$fs = new Loco_mvc_PostParams( [
|
||||
'disabled' => $ctx->disabled(),
|
||||
'fs_protect' => 1 === $fsp ? 'Warn' : ( $fsp ? 'Block' : 'Off' ),
|
||||
] );
|
||||
// important locations, starting with LOCO_LANG_DIR
|
||||
$locations = [
|
||||
'WP_LANG_DIR' => $this->file_params( new Loco_fs_Directory( loco_constant('WP_LANG_DIR') ) ),
|
||||
'LOCO_LANG_DIR' => $this->file_params( new Loco_fs_Directory( loco_constant('LOCO_LANG_DIR') ) ),
|
||||
];
|
||||
// WP_TEMP_DIR takes precedence over sys_get_temp_dir in WordPress get_temp_dir();
|
||||
if( defined('WP_TEMP_DIR') ){
|
||||
$locations['WP_TEMP_DIR'] = $this->file_params( new Loco_fs_Directory(WP_TEMP_DIR) );
|
||||
}
|
||||
$locations['PHP sys_temp_dir'] = $this->file_params( new Loco_fs_Directory( sys_get_temp_dir() ) );
|
||||
$locations['PHP upload_tmp_dir'] = $this->file_params( new Loco_fs_Directory( ini_get('upload_tmp_dir') ) );
|
||||
$locations['PHP error_log'] = $this->file_params( new Loco_fs_Directory( ini_get('error_log') ) );
|
||||
|
||||
// Debug and error log settings
|
||||
$debug = new Loco_mvc_ViewParams( [
|
||||
'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',
|
||||
] );
|
||||
|
||||
/* 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','<') ){
|
||||
// phpcs:disable -- PHP version is checked prior to deprecated function call.
|
||||
if( get_magic_quotes_gpc() ){
|
||||
Loco_error_AdminNotices::info('You have "magic_quotes_gpc" enabled. We recommend you disable this in PHP');
|
||||
}
|
||||
if( get_magic_quotes_runtime() ){
|
||||
Loco_error_AdminNotices::info('You have "magic_quotes_runtime" enabled. We recommend you disable this in PHP');
|
||||
}
|
||||
if( version_compare(PHP_VERSION,'5.6.20','<') ){
|
||||
Loco_error_AdminNotices::info('Your PHP version is very old. We recommend you upgrade');
|
||||
}
|
||||
// phpcs:enable
|
||||
}
|
||||
|
||||
return $this->view('admin/config/debug', compact('breadcrumb','versions','encoding','memory','fs','locations','debug') );
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?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 );
|
||||
|
||||
// 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') );
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?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 : [] );
|
||||
// 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( [
|
||||
'label' => __('Super Admin','loco-translate'),
|
||||
'name' => 'dummy-admin-cap',
|
||||
'attrs' => 'checked disabled'
|
||||
] );
|
||||
}
|
||||
foreach( $perms->getRoles() as $id => $role ){
|
||||
$caps[$id] = new Loco_mvc_ViewParams( [
|
||||
'value' => '1',
|
||||
'label' => $perms->getRoleName($id),
|
||||
'name' => 'caps['.$id.'][loco_admin]',
|
||||
'attrs' => $perms->isProtectedRole($role) ? 'checked disabled ' : ( $role->has_cap('loco_admin') ? 'checked ' : '' ),
|
||||
] );
|
||||
}
|
||||
// allow/deny warning levels
|
||||
$this->set('verbose', new Loco_mvc_ViewParams( [
|
||||
0 => __('Allow','loco-translate'),
|
||||
1 => __('Allow (with warning)','loco-translate'),
|
||||
2 => __('Disallow','loco-translate'),
|
||||
] ) );
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* {@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') );
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?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();
|
||||
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->setLocoUpdate($latest);
|
||||
}
|
||||
}
|
||||
}
|
||||
// notify if running a development snapshot, but only if ahead of latest stable
|
||||
if( '-dev' === substr($version,-4) ){
|
||||
$this->set( 'devel', true );
|
||||
}
|
||||
|
||||
|
||||
// check PHP version, noting that we want to move to minimum version 5.6 as per latest WordPress
|
||||
$phpversion = PHP_VERSION;
|
||||
if( version_compare($phpversion,'7.4.0','<') ){
|
||||
$this->set('phpupdate','7.4');
|
||||
}
|
||||
|
||||
|
||||
// check WordPress version, No plans to increase this until WP bumps their min PHP requirement.
|
||||
$wpversion = $GLOBALS['wp_version'];
|
||||
/*if( version_compare($wpversion,'5.2','<') ){
|
||||
$this->setWpUpdate('5.2');
|
||||
}*/
|
||||
|
||||
return $this->view('admin/config/version', compact('breadcrumb','version','phpversion','wpversion') );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string version
|
||||
*/
|
||||
private function setLocoUpdate( $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 ) );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string minimum recommended version
|
||||
*
|
||||
private function setWpUpdate( $version ){
|
||||
$this->set('wpupdate',$version);
|
||||
$this->set('wpupdate_href', admin_url('update-core.php') );
|
||||
}*/
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
<?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
|
||||
* @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', [] );
|
||||
}
|
||||
if( $file->isDirectory() ){
|
||||
$this->set('info', Loco_mvc_FileParams::create($file) );
|
||||
return $this->view( 'admin/errors/file-isdir', [] );
|
||||
}
|
||||
// security validations
|
||||
try {
|
||||
Loco_gettext_Data::ext( $file );
|
||||
}
|
||||
catch( Exception $e ){
|
||||
return $this->view( 'admin/errors/file-sec', [ 'reason' => $e->getMessage() ] );
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set template title argument for a file
|
||||
* @return void
|
||||
*/
|
||||
protected function setFileTitle( Loco_fs_File $file, $format = '%s' ){
|
||||
$name = Loco_mvc_ViewParams::format($format,[$file->basename()]);
|
||||
// append folder location for translation files
|
||||
if( in_array( $file->extension(), ['po','mo'] ) ){
|
||||
$dir = new Loco_fs_LocaleDirectory( $file->dirname() );
|
||||
$type = $dir->getTypeLabel( $dir->getTypeId() );
|
||||
$name .= ' ('.mb_strtolower($type,'UTF-8').')';
|
||||
}
|
||||
$this->set('title', $name );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* {@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( [
|
||||
'code' => $code,
|
||||
'lang' => $locale->lang,
|
||||
'icon' => $locale->getIcon(),
|
||||
'name' => $locale->ensureName( new Loco_api_WordPressTranslations ),
|
||||
'href' => Loco_mvc_AdminRouter::generate('lang-view', ['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 = [
|
||||
'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,['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 valid
|
||||
$project = $this->getOptionalProject();
|
||||
if( $project ){
|
||||
$args = [ 'bundle' => $bundle->getHandle(), 'domain' => $project->getId() ];
|
||||
$this->set( 'msginit', new Loco_mvc_ViewParams( [
|
||||
'href' => Loco_mvc_AdminRouter::generate( $prefix.'-msginit', $args ),
|
||||
'text' => __('New language','loco-translate'),
|
||||
] ) );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function view( $tpl, array $args = [] ){
|
||||
|
||||
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
|
||||
$breadcrumb->add( $this->get('title')?:'Untitled' );
|
||||
}
|
||||
|
||||
return parent::view( $tpl, $args );
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<?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 $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) ) );
|
||||
}
|
||||
$siblings->setDomain( $this->getDomain() );
|
||||
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( [] );
|
||||
$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 );
|
||||
// translators: %u is a number of files which were successfully deleted
|
||||
Loco_data_Session::get()->flash('success', sprintf( _n('%u 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', [ '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();
|
||||
// translators: Page title where %s is the name of a file to be deleted
|
||||
$this->set('title', sprintf( __('Delete %s','loco-translate'), $file->basename() ).' ‹ '.$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 );
|
||||
// phpcs:ignore -- duplicate string
|
||||
$this->setFileTitle( $file, __('Delete %s','loco-translate') );
|
||||
|
||||
// warn about additional files that will be deleted along with this
|
||||
if( $deps = array_slice($files,1) ){
|
||||
$count = count($deps);
|
||||
// translators: Warning that deleting a file will also delete others. %s indicates that quantity.
|
||||
$this->set('warn', sprintf( _n( '%s dependent file will also be deleted', '%s dependent files will also be deleted', $count, 'loco-translate' ), $count ) );
|
||||
$infos = [];
|
||||
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');
|
||||
}
|
||||
|
||||
}
|
||||