JFIF $ d d 7 * Z qn )ZP 0 !1 A "#a$2Qr D8 a Ri [f \m I ykI w0 c u F c Rı?lO7 к _f˓ [C $殷WF< _W ԣ s Kc ëI zyQy /_LK ℂ ;C",p F A : /] =H ~ , l s / 9 ć: [=/#f ; ) x { ٛE Q )~ = 𘙲r*2~ a _V = ' ku m F D } KYYC )( { * g& f `툪 r y ` = ^ c J. I ]( * `wq 1dđ #̩ ͑ 0 ; H ] u 搂 @:~ ו K L N s h } O I R* 8 :2 ! lD JV o ( 3 = M ( z Ȱ+i *N A r6K n S l ) ! J J Ӂ* % ݉?|D } d5: eP0 R; {$X' x F @ .ÊB {,WJ uQ ɲRI ;9 Q E琯 6 2fT .DU J ;*c P A\IL Nj! J ۱+ O\ ͔ ] ޒ S J ȧ c%A N ol ՎprU L Zԛe r E2= X D Xg VQeӓ k y P7 U* o m Q I s ,K `) 6\G3t?pgjr m ۛجw luGt f h 9uyP 0D;Uڽ"OXl i f $ ) &|ML 0 Z rm1 [ H XP l P R0 'G=i 2N +0 e 2 ]] 9 VT P O 7 h(F * 癈 ' = Q VZD F,d ߬ ~ T X G[ `le 69C R(!S2! P < 0 x < !1AQ "Raq 02Br #S CTb ? Ζ "]mH5WR 7 k.ۛ ! } Q~ + yԏ z |@ T20 S~ K e k *zF f^2X *( @ 8 r ? C I uI|֓>^Ex LgNU Y+{.R Ѫ τV YT D I 6 2 '8Y2 7'\T P.6 d &˦ @V q i |8 -O Ε ] ʔ U= T L 8= ;6c | !qfF3 a ů& ~$ l} 'NW U s$U k^S V : U#6w + +s &r+n ڐ { @ 29gL u " T Ù M=6 (^ "7 r}=6Yݾ lC u hquymp Ǧ G jhs ǜNl ɻ} o 7 #S6 a w 4 ! OS r D 5 7 % | ?x> L | / nD 6?/8 w # [ ) L 7 +6〼T A T g ! %5 Mm Z /c -{1_Je "| ^$ 'O & ޱմ < H vNqϡQ$ < 6 &Nݛ ֨tǻN (9 n ru~'όN sMG& ,uH Kw *Ī 5@ u6 BԒ i YN * f 0 _ GhKW u lT <nj- 0{ M L~ | /a v\ E = ?1 \ɟuq Ђ Y O a K ޡP q $ H8o Q˼EV ~ k 0 h' L Pp M -M" m^ !6l nմ 쮝C U T . ! :vMǨb h ډ Vc r @~p # !1QAa q ? AL0L x LU 4 d ; x " 16ad0 7.\ߋ 0 = P l v/ " c A Ay A|2pO8"d MV #1] = \ " ˡ ] k v I9 d ۓ & q ) ǐq E ꞅ v "P )q X &Q З non]я u ג./ ; gZ$ M QР 2M ]h ni 0 P $ێ 3CɏGjώy : m c # S_ l@ '_ \m|C 2`K _ 1 cIvGc $? PCQ S+D ? K aײ k Fr b 8 ύ NEدg ĝ U 3 6 #Ik+ v { v i ( ވ ? a e) X xy/*@Q Ǽ Ծ ) $ IWO l c G; !i k{ gOŀ L ('x T rb $ w) R $ & N1E td U3Uȉ1p M"N* (DNy d96.(jQ) X 5 c QɎMy W ? Q *!R > 6=7)Xj 5`J ] e 8%t ! + ' ! 1Q 5 !1 AQaq ё #2 "0BRb ? G t^ ## .l lQ T $ v , ,m 㵜5u bV =sY+ @d { N ! dn O < .- B; _w J t 6;Q Jd. Q c%p{ 1 ,sND dF H I0 Гo X ш e黅X ۢ F :)[ F G X ƹ / w _c MeD ,ʡc c . W D tA $ j@ : ) - #u c1< @ۗ9F ) KJ- hpP] _ x [ q B lb p ʖ w q " LFGd ƶ * s +ډ_Zc " ? % t[I P 6 J ]# =ɺ V v v CGs Gh1 > )6 |e y ? L ӣ m,4 G W Ui ` ] uJVo VDG< SB 6ϏQ @ TiU l y O U 0 kfV ~ ~}S Z @ *WU Ui ## ; s /[ = ! 7}" W N ] ' (L ! ~ y5 g9T̅J kbM' +s :S +B) v @ M j e C f j E 0 Y\Qn z G 1 д~ W o{T 9?` Rm yhsy3 !HAD ] m c1 ~ 2 L S u7x T ;j$ `} 4-> L# vzŏ I L S ֭ T { r jGK C ; b pU= - ` Bs K. S F w4 M q] Z dH S 0 ) tLg
<?php
loco_require_lib('compiled/gettext.php');
/**
* Wrapper for array forms of parsed PO data
*/
class Loco_gettext_Data extends LocoPoIterator implements JsonSerializable {
/**
* Normalize file extension to internal type.
* @return string Normalized file extension "po", "pot", "mo", "json" or "php"
* @throws Loco_error_Exception
*/
public static function ext( Loco_fs_File $file ):string {
$ext = rtrim( strtolower( $file->extension() ), '~' );
if( 'po' === $ext || 'pot' === $ext || 'mo' === $ext || 'json' === $ext ){
return $ext;
}
// only observing the full `.l10n.php` extension as a translation format.
if( 'php' === $ext && '.l10n.php' === substr($file->getPath(),-9) ){
return 'php';
}
// translators: Error thrown when attempting to parse a file that is not a supported translation file format
throw new Loco_error_Exception( sprintf( __('%s is not a Gettext file','loco-translate'), $file->basename() ) );
}
public static function load( Loco_fs_File $file, ?string $type = null ):self {
if( is_null($type) ) {
$type = self::ext($file);
}
$type = strtolower($type);
// catch parse errors, so we can inform user of which file is bad
try {
if( 'po' === $type || 'pot' === $type ){
return self::fromSource( $file->getContents() );
}
if( 'mo' === $type ){
return self::fromBinary( $file->getContents() );
}
if( 'json' === $type ){
return self::fromJson( $file->getContents() );
}
if( 'php' === $type ){
return self::fromPhp( $file->getPath() );
}
throw new InvalidArgumentException('No parser for '.$type.' files');
}
catch( Loco_error_ParseException $e ){
$path = $file->getRelativePath( loco_constant('WP_CONTENT_DIR') );
Loco_error_AdminNotices::debug( sprintf('Failed to parse %s as a %s file; %s',$path,strtoupper($type),$e->getMessage()) );
throw new Loco_error_ParseException( sprintf('Invalid %s file: %s',$type,basename($path)) );
}
}
/**
* Like load but just pulls header, saving a full parse
* @throws InvalidArgumentException
*/
public static function head( Loco_fs_File $file ):?LocoPoHeaders {
$p = new LocoPoParser( $file->getContents() );
$p->parse(0);
return $p->getHeader();
}
/**
* @param string $src PO source
*/
public static function fromSource( string $src ):self {
$p = new LocoPoParser($src);
return new Loco_gettext_Data( $p->parse() );
}
/**
* @param string $bin MO bytes
*/
public static function fromBinary( string $bin ):self {
$p = new LocoMoParser($bin);
return new Loco_gettext_Data( $p->parse() );
}
/**
* @param string $json Jed source
*/
public static function fromJson( string $json ):self {
$blob = json_decode( $json, true );
$p = new LocoJedParser( $blob['locale_data'] );
// note that headers outside of locale_data are won't be parsed out. we don't currently need them.
return new Loco_gettext_Data( $p->parse() );
}
/**
* @param string $path PHP file path
*/
public static function fromPhp( string $path ):self {
$blob = include $path;
if( ! is_array($blob) || ! array_key_exists('messages',$blob) ){
throw new Loco_error_ParseException('Invalid PHP translation file');
}
// refactor PHP structure into JED format
$p = new LocoMoPhpParser($blob);
return new Loco_gettext_Data( $p->parse() );
}
/**
* Create a dummy/empty instance with minimum content to be a valid PO file.
*/
public static function dummy():self {
return new Loco_gettext_Data( [ ['source'=>'','target'=>'Language:'] ] );
}
/**
* Ensure PO source is UTF-8.
* Required if we want PO code when we're not parsing it. e.g. source view
*/
public static function ensureUtf8( string $src ):string {
$src = loco_remove_bom($src,$cs);
if( ! $cs ){
// read PO header, requiring partial parse
try {
$cs = LocoPoHeaders::fromSource($src)->getCharset();
}
catch( Loco_error_ParseException $e ){
Loco_error_AdminNotices::debug( $e->getMessage() );
}
}
return loco_convert_utf8($src,$cs,false);
}
/**
* Compile messages to binary MO format
* @return string MO file source
* @throws Loco_error_Exception
*/
public function msgfmt():string {
if( 2 !== strlen("\xC2\xA3") ){
throw new Loco_error_Exception('Refusing to compile MO file. Please disable mbstring.func_overload'); // @codeCoverageIgnore
}
$mo = new LocoMo( $this, $this->getHeaders() );
$opts = Loco_data_Settings::get();
if( $opts->gen_hash ){
$mo->enableHash();
}
if( $opts->use_fuzzy ){
$mo->useFuzzy();
}
/*/ TODO optionally exclude .js strings
if( $opts->purge_js ){
$mo->filter....
}*/
return $mo->compile();
}
/**
* Get final UTF-8 string for writing to file
* @param bool $sort Whether to sort output, generally only for extracting strings
*/
public function msgcat( bool $sort = false ):string {
// set maximum line width, zero or >= 15
$this->wrap( Loco_data_Settings::get()->po_width );
// concat with default text sorting if specified
$po = $this->render( $sort ? [ 'LocoPoIterator', 'compare' ] : null );
// Prepend byte order mark only if configured
if( Loco_data_Settings::get()->po_utf8_bom ){
$po = "\xEF\xBB\xBF".$po;
}
return $po;
}
/**
* Compile JED flavour JSON
* @param string $domain text domain for JED metadata
* @param string $source reference to file that uses included strings
* @return string JSON source, or empty if JED file has no entries
*/
public function msgjed( string $domain = 'messages', string $source = '' ):string {
// note that JED is sparse, like MO. We won't write empty files.
$data = $this->exportJed();
if( 1 >= count($data) ){
return '';
}
$head = $this->getHeaders();
$head['domain'] = $domain;
// Pretty formatting for debugging. Doing as per WordPress and always escaping Unicode.
$json_options = 0;
if( Loco_data_Settings::get()->jed_pretty ){
$json_options |= loco_constant('JSON_PRETTY_PRINT') | loco_constant('JSON_UNESCAPED_SLASHES'); // | loco_constant('JSON_UNESCAPED_UNICODE');
}
// PO should have a date if localised properly
return json_encode( [
'translation-revision-date' => $head['PO-Revision-Date'],
'generator' => $head['X-Generator'],
'source' => $source,
'domain' => $domain,
'locale_data' => [
$domain => $data,
],
], $json_options );
}
/**
* @return array
*/
#[ReturnTypeWillChange]
public function jsonSerialize(){
$po = $this->getArrayCopy();
// exporting headers non-scalar so js doesn't have to parse them
try {
$headers = $this->getHeaders();
if( count($headers) && '' === $po[0]['source'] ){
$po[0]['target'] = $headers->getArrayCopy();
}
}
// suppress header errors when serializing
// @codeCoverageIgnoreStart
catch( Exception $e ){ }
// @codeCoverageIgnoreEnd
return $po;
}
/**
* Create a signature for use in comparing source strings between documents
*/
public function getSourceDigest():string {
$data = $this->getHashes();
return md5( implode("\1",$data) );
}
/**
* @param string[] $custom custom headers
*/
public function localize( Loco_Locale $locale, array $custom = [] ):self {
$date = gmdate('Y-m-d H:i').'+0000';
// headers that must always be set if absent
$defaults = [
'Project-Id-Version' => '',
'Report-Msgid-Bugs-To' => '',
'POT-Creation-Date' => $date,
];
// headers that must always override when localizing
$required = [
'PO-Revision-Date' => $date,
'Last-Translator' => '',
'Language-Team' => $locale->getName(),
'Language' => (string) $locale,
'Plural-Forms' => $locale->getPluralFormsHeader(),
'MIME-Version' => '1.0',
'Content-Type' => 'text/plain; charset=UTF-8',
'Content-Transfer-Encoding' => '8bit',
'X-Generator' => 'Loco https://localise.biz/',
'X-Loco-Version' => sprintf('%s; wp-%s; php-%s', loco_plugin_version(), $GLOBALS['wp_version'], PHP_VERSION ),
];
// Allow some existing headers to remain if PO was previously localized to the same language
$headers = $this->getHeaders();
$previous = Loco_Locale::parse( $headers->trimmed('Language') );
if( $previous->lang === $locale->lang ){
$header = $headers->trimmed('Plural-Forms');
if( preg_match('/^\\s*nplurals\\s*=\\s*\\d+\\s*;\\s*plural\\s*=/', $header) ) {
$required['Plural-Forms'] = $header;
}
if( $previous->region === $locale->region && $previous->variant === $locale->variant ){
unset( $required['Language-Team'] );
}
}
// set user's preferred Last-Translator credit if configured
if( function_exists('get_current_user_id') && get_current_user_id() ){
$prefs = Loco_data_Preferences::get();
$credit = (string) $prefs->credit;
if( '' === $credit ){
$credit = $prefs->default_credit();
}
// filter credit with current username and email
$user = wp_get_current_user();
$credit = apply_filters( 'loco_current_translator', $credit, $user->get('display_name'), $user->get('email') );
if( '' !== $credit ){
$required['Last-Translator'] = $credit;
}
}
$headers = $this->applyHeaders($required,$defaults,$custom);
// avoid non-empty POT placeholders that won't have been set from $defaults
if( 'PACKAGE VERSION' === $headers['Project-Id-Version'] ){
$headers['Project-Id-Version'] = '';
}
// finally allow headers to be modified via filter
$replaced = apply_filters( 'loco_po_headers', $headers );
if( $replaced instanceof LocoPoHeaders && $replaced !== $headers ){
$this->setHeaders($replaced);
}
return $this->initPo();
}
public function templatize( string $domain = '' ):self {
$date = gmdate('Y-m-d H:i').'+0000'; // <- forcing UCT
$defaults = [
'Project-Id-Version' => 'PACKAGE VERSION',
'Report-Msgid-Bugs-To' => '',
];
$required = [
'POT-Creation-Date' => $date,
'PO-Revision-Date' => 'YEAR-MO-DA HO:MI+ZONE',
'Last-Translator' => 'FULL NAME <EMAIL@ADDRESS>',
'Language-Team' => '',
'Language' => '',
'Plural-Forms' => 'nplurals=INTEGER; plural=EXPRESSION;',
'MIME-Version' => '1.0',
'Content-Type' => 'text/plain; charset=UTF-8',
'Content-Transfer-Encoding' => '8bit',
'X-Generator' => 'Loco https://localise.biz/',
'X-Loco-Version' => sprintf('%s; wp-%s; php-%s', loco_plugin_version(), $GLOBALS['wp_version'], PHP_VERSION ),
'X-Domain' => $domain,
];
$headers = $this->applyHeaders($required,$defaults);
// finally allow headers to be modified via filter
$replaced = apply_filters( 'loco_pot_headers', $headers );
if( $replaced instanceof LocoPoHeaders && $replaced !== $headers ){
$this->setHeaders($replaced);
}
return $this->initPot();
}
private function applyHeaders( array $required = [], array $defaults = [], array $custom = [] ):LocoPoHeaders {
$headers = $this->getHeaders();
// only set absent or empty headers from default list
foreach( $defaults as $key => $value ){
if( ! $headers[$key] ){
$headers[$key] = $value;
}
}
// add required headers with custom ones overriding
if( $custom ){
$required = array_merge( $required, $custom );
}
// TODO fix ordering weirdness here. required headers seem to get appended wrongly
foreach( $required as $key => $value ){
$headers[$key] = $value;
}
return $headers;
}
/**
* Remap proprietary base path when PO file is moving to another location.
*
* @param Loco_fs_File $origin the file that was originally extracted to (POT)
* @param Loco_fs_File $target the file that must now target references relative to itself
* @param string $vendor name used in header keys
* @return bool whether base header was altered
*/
public function rebaseHeader( Loco_fs_File $origin, Loco_fs_File $target, string $vendor ):bool {
$base = $target->getParent();
$head = $this->getHeaders();
$key = $head->normalize('X-'.$vendor.'-Basepath');
if( $key ){
$oldRelBase = $head[$key];
$oldAbsBase = new Loco_fs_Directory($oldRelBase);
$oldAbsBase->normalize( $origin->getParent() );
$newRelBase = $oldAbsBase->getRelativePath($base);
// new base path is relative to $target location
$head[$key] = $newRelBase;
return true;
}
return false;
}
/**
* Inherit meta values from header given, but leave standard headers intact.
*/
public function inheritHeader( LocoPoHeaders $source ):void {
$target = $this->getHeaders();
foreach( $source as $key => $value ){
if( 'X-' === substr($key,0,2) ) {
$target[$key] = $value;
}
}
}
/**
* @param string $podate Gettext data formatted "YEAR-MO-DA HO:MI+ZONE"
*/
public static function parseDate( string $podate ):int {
if( method_exists('DateTime','createFromFormat') ){
$objdate = DateTime::createFromFormat('Y-m-d H:iO', $podate);
if( $objdate instanceof DateTime ){
return $objdate->getTimestamp();
}
}
return strtotime($podate);
}
}