first commit

This commit is contained in:
Roman Pyrih
2025-08-26 10:50:15 +02:00
commit 97352dcdc9
6905 changed files with 2717203 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

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

View 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,'%">&nbsp;</div>'; } if( '0' === $complete ){ echo '&nbsp;'; } 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.'">&nbsp;</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'); }

View File

@@ -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; }

View 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); } }

View 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']];

View 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'];

File diff suppressed because one or more lines are too long

View 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']]]];

File diff suppressed because one or more lines are too long

View 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() ) );
}

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
#loco-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}

View 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}

View 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}

View File

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

View File

@@ -0,0 +1 @@
.js #loco-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}

View File

@@ -0,0 +1 @@
.wrap #loco-editor .is-table .wg-tr:nth-child(even){background-color:rgba(9,100,132,.05)}.wrap #loco-editor .wg-split-x>nav.wg-tabs>a.active,.wrap #loco-editor .is-table .wg-cols>div>div.selected{background-color:#096484}.wrap #loco-editor .is-editable>.wg-content>textarea:focus,.wrap #loco-editor .is-editable>.wg-content.has-focus .ace_scroller,.wrap #loco-editor .is-editable>.wg-content.has-focus .mce-content-body{border-color:#5b9dd9;-webkit-box-shadow:inset 0 0 .6em rgba(30,140,190,.8);-moz-box-shadow:inset 0 0 .6em rgba(30,140,190,.8);box-shadow:inset 0 0 .6em rgba(30,140,190,.8)}.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}

View File

@@ -0,0 +1 @@
.wrap #loco-editor .is-table .wg-tr:nth-child(even){background-color:rgba(199,165,137,.05)}.wrap #loco-editor .wg-split-x>nav.wg-tabs>a.active,.wrap #loco-editor .is-table .wg-cols>div>div.selected{background-color:#c7a589}.wrap #loco-editor .is-editable>.wg-content>textarea:focus,.wrap #loco-editor .is-editable>.wg-content.has-focus .ace_scroller,.wrap #loco-editor .is-editable>.wg-content.has-focus .mce-content-body{border-color:#5b9dd9;-webkit-box-shadow:inset 0 0 .6em rgba(30,140,190,.8);-moz-box-shadow:inset 0 0 .6em rgba(30,140,190,.8);box-shadow:inset 0 0 .6em rgba(30,140,190,.8)}.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}

View File

@@ -0,0 +1 @@
.wrap #loco-editor .is-table .wg-tr:nth-child(even){background-color:rgba(163,183,69,.05)}.wrap #loco-editor .wg-split-x>nav.wg-tabs>a.active,.wrap #loco-editor .is-table .wg-cols>div>div.selected{background-color:#a3b745}.wrap #loco-editor .is-editable>.wg-content>textarea:focus,.wrap #loco-editor .is-editable>.wg-content.has-focus .ace_scroller,.wrap #loco-editor .is-editable>.wg-content.has-focus .mce-content-body{border-color:#5b9dd9;-webkit-box-shadow:inset 0 0 .6em rgba(30,140,190,.8);-moz-box-shadow:inset 0 0 .6em rgba(30,140,190,.8);box-shadow:inset 0 0 .6em rgba(30,140,190,.8)}.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}

View File

@@ -0,0 +1 @@
.wrap #loco-editor .is-table .wg-tr:nth-child(even){background-color:rgba(136,136,136,.05)}.wrap #loco-editor .wg-split-x>nav.wg-tabs>a.active,.wrap #loco-editor .is-table .wg-cols>div>div.selected{background-color:#888}.wrap #loco-editor .is-editable>.wg-content>textarea:focus,.wrap #loco-editor .is-editable>.wg-content.has-focus .ace_scroller,.wrap #loco-editor .is-editable>.wg-content.has-focus .mce-content-body{border-color:#5b9dd9;-webkit-box-shadow:inset 0 0 .6em rgba(30,140,190,.8);-moz-box-shadow:inset 0 0 .6em rgba(30,140,190,.8);box-shadow:inset 0 0 .6em rgba(30,140,190,.8)}.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}

View File

@@ -0,0 +1 @@
.wrap #loco-editor .is-table .wg-tr:nth-child(even){background-color:rgba(225,77,67,.05)}.wrap #loco-editor .wg-split-x>nav.wg-tabs>a.active,.wrap #loco-editor .is-table .wg-cols>div>div.selected{background-color:#e14d43}.wrap #loco-editor .is-editable>.wg-content>textarea:focus,.wrap #loco-editor .is-editable>.wg-content.has-focus .ace_scroller,.wrap #loco-editor .is-editable>.wg-content.has-focus .mce-content-body{border-color:#5b9dd9;-webkit-box-shadow:inset 0 0 .6em rgba(30,140,190,.8);-moz-box-shadow:inset 0 0 .6em rgba(30,140,190,.8);box-shadow:inset 0 0 .6em rgba(30,140,190,.8)}.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}

View File

@@ -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}

View File

@@ -0,0 +1 @@
.wrap #loco-editor .is-table .wg-tr:nth-child(even){background-color:rgba(158,186,160,.05)}.wrap #loco-editor .wg-split-x>nav.wg-tabs>a.active,.wrap #loco-editor .is-table .wg-cols>div>div.selected{background-color:#9ebaa0}.wrap #loco-editor .is-editable>.wg-content>textarea:focus,.wrap #loco-editor .is-editable>.wg-content.has-focus .ace_scroller,.wrap #loco-editor .is-editable>.wg-content.has-focus .mce-content-body{border-color:#5b9dd9;-webkit-box-shadow:inset 0 0 .6em rgba(30,140,190,.8);-moz-box-shadow:inset 0 0 .6em rgba(30,140,190,.8);box-shadow:inset 0 0 .6em rgba(30,140,190,.8)}.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}

View File

@@ -0,0 +1 @@
.wrap #loco-editor .is-table .wg-tr:nth-child(even){background-color:rgba(221,130,59,.05)}.wrap #loco-editor .wg-split-x>nav.wg-tabs>a.active,.wrap #loco-editor .is-table .wg-cols>div>div.selected{background-color:#dd823b}.wrap #loco-editor .is-editable>.wg-content>textarea:focus,.wrap #loco-editor .is-editable>.wg-content.has-focus .ace_scroller,.wrap #loco-editor .is-editable>.wg-content.has-focus .mce-content-body{border-color:#5b9dd9;-webkit-box-shadow:inset 0 0 .6em rgba(30,140,190,.8);-moz-box-shadow:inset 0 0 .6em rgba(30,140,190,.8);box-shadow:inset 0 0 .6em rgba(30,140,190,.8)}.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}

Binary file not shown.

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 668 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 580 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1001 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 742 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 B

File diff suppressed because it is too large Load Diff

View 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);

View File

@@ -0,0 +1 @@
"use strict";

View 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);

View 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);

View 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);

View 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);

View 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);

View 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);

View 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);

View 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);

View 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);

View 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);

View 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);

View 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 bundles root directory
2. Ensure this directory writeable by the web server
3. Find the bundle at either *Loco Translate > Themes* or *Loco Translate > Plugins*
4. Click `+ Create template` and follow the on-screen prompts to extract your strings.
5. Click `+ New language` and follow the on-screen prompts to add your own translations.
= Installing manually: =
1. Unzip all files to the `wp-content/plugins/loco-translate` directory
2. Log into WordPress admin and activate the 'Loco Translate' plugin through the 'Plugins' menu
3. Go to *Loco Translate > Home* in the left-hand menu to start translating
More information on using the plugin is [available here](https://localise.biz/wordpress/plugin).
== Frequently Asked Questions ==
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.

View 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');
}

View 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');
}
}

View File

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

View File

@@ -0,0 +1,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;
}
}

View File

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

View File

@@ -0,0 +1,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');
}
}

View File

@@ -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;
}
}

View File

@@ -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') );
}
}

View File

@@ -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') );
}
}

View File

@@ -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;
}
}

View File

@@ -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' );
}
}

View File

@@ -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') );
}
}

View File

@@ -0,0 +1,45 @@
<?php
/**
* Base controller for global plugin configuration screens
*/
abstract class Loco_admin_config_BaseController extends Loco_mvc_AdminController {
/**
* {@inheritdoc}
*/
public function init(){
parent::init();
// navigate between config view siblings, but only if privileged user
if( current_user_can('manage_options') ){
$tabs = new Loco_admin_Navigation;
$this->set( 'tabs', $tabs );
$actions = [
'' => __('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'),
];
}
}

View File

@@ -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') );
}
}

View File

@@ -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') );
}
}

View File

@@ -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') );
}
}

View File

@@ -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') );
}*/
}

View File

@@ -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 );
}
}

View File

@@ -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() ).' &lsaquo; '.$bundle->getName() );
}
/**
* {@inheritdoc}
*/
public function render(){
$file = $this->get('file');
if( $fail = $this->getFileError($file) ){
return $fail;
}
$files = $this->expandFiles( $file );
$info = Loco_mvc_FileParams::create($file);
$this->set( 'info', $info );
// 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');
}
}

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