?MZ?   ?? ? @ ? o ¡ä ¨ª!?L¨ª!This program cannot be run in DOS mode. $ 3B¡ä¡ä¡Â#¨²?¡Â#¨²?¡Â#¨²?¡­¡é??A#¨²?¡­¡éT??#¨²?¡­¡é¨´??#¨²??£¤'??#¨²??£¤¨´?t#¨²??£¤T??#¨²??£¤???#¨²?¡­¡é??e#¨²?¡Â#??{#¨²?s£¤T??#¨²?s£¤???#¨²?Rich¡Â#¨²? PE d? ??g e " * o  €?  @     P  ¨º¡é?  `¨¢€?     ¨ª P ? ?? ` # @ t P¨¢  ¨¤ @ D  .text 1  o  `.rdata j+ D , ? @ @.data PS   ¨º @ ¨¤.pdata # ` $ ? @ @.fptable  ?   @ ¨¤.rsrc ?? ? ?  @ @.reloc t @  ? /** * Front to the WordPress application. This file doesn't do anything, but loads PK! +H" " SyncOptions.phpnu„[µü¤head = $head; } /** * Test if PO file has alternative template path * @return bool */ public function hasTemplate(){ return '' !== $this->head->trimmed('X-Loco-Template'); } /** * Get *relative* path to alternative template path. * @return Loco_fs_LocaleFile */ public function getTemplate(){ return new Loco_fs_LocaleFile( $this->head['X-Loco-Template'] ); } /** * Set *relative* path to alternative template path. * @param string $path */ public function setTemplate( $path ){ $this->head['X-Loco-Template'] = (string) $path; } /** * Test if translations (msgstr fields) are to be merged. * @return bool true if NOT in pot mode */ public function mergeMsgstr(){ return 0 === preg_match( '/\\bpot\\b/', $this->getSyncMode() ); } /** * Test if JSON files are to be merged. * @return bool */ public function mergeJson(){ return 1 === preg_match( '/\\bjson\\b/', $this->getSyncMode() ); } /** * @return string */ public function getSyncMode(){ $mode = strtolower( $this->head->trimmed('X-Loco-Template-Mode') ); // Default sync mode when undefined is to honour the type of source. // i.e. for legacy compatibility, copy msgstr fields if source is a PO file. if( '' === $mode ){ $mode = $this->hasTemplate() ? strtolower( $this->getTemplate()->extension() ) : 'pot'; } return $mode; } /** * @param string $mode */ public function setSyncMode( $mode ){ $this->head['X-Loco-Template-Mode'] = (string) $mode; } /** * Remove redundant headers * @return LocoPoHeaders */ public function getHeaders(){ if( ! $this->hasTemplate() ){ $this->head->offsetUnset('X-Loco-Template'); if( 'pot' === $this->getSyncMode() ){ $this->head->offsetUnset('X-Loco-Template-Mode'); } } return $this->head; } } PK!ê§ùé é SearchPaths.phpnu„[µü¤getExcluded() ); /* @var Loco_fs_Directory $base */ foreach( $this->getRootDirectories() as $base ){ $file = new Loco_fs_File($ref); $path = $file->normalize( (string) $base ); if( $file->exists() && ! $excluded->check($path) ){ return $file; } } return null; } /** * Build search paths from a given PO/POT file that references other files * @return Loco_gettext_SearchPaths */ public function init( Loco_fs_File $pofile, ?LocoHeaders $head = null ){ if( is_null($head) ){ loco_require_lib('compiled/gettext.php'); $head = LocoPoHeaders::fromSource( $pofile->getContents() ); } $ninc = 0; foreach( ['Poedit'] as $vendor ){ $key = 'X-'.$vendor.'-Basepath'; if( ! $head->has($key) ){ continue; } $dir = new Loco_fs_Directory( $head[$key] ); $base = $dir->normalize( $pofile->dirname() ); // base should be absolute, with the following search paths relative to it $i = 0; while( true ){ $key = sprintf('X-%s-SearchPath-%u', $vendor, $i++); if( ! $head->has($key) ){ break; } // map search path to given base $include = new Loco_fs_File( $head[$key] ); $include->normalize( $base ); if( $include->exists() ){ if( $include->isDirectory() ){ $this->addRoot( (string) $include ); $ninc++; } /*else { TODO force specific file in Loco_fs_FileFinder }*/ } } // exclude from search paths $i = 0; while( true ){ $key = sprintf('X-%s-SearchPathExcluded-%u', $vendor, $i++); if( ! $head->has($key) ){ break; } // map excluded path to given base $exclude = new Loco_fs_File( $head[$key] ); $exclude->normalize($base); if( $exclude->exists() ){ $this->exclude( (string) $exclude ); } // TODO implement wildcard exclusion } } // Add po file location if no proprietary headers used if( ! $ninc ){ $this->addRoot( $pofile->dirname() ); } return $this; } }PK!Fw6w w WordCount.phpnu„[µü¤po = $po; } /** * @internal */ private function countField( $f ){ $n = 0; foreach( $this->po as $r ){ $n += self::simpleCount( $r[$f] ); } return $n; } /** * Default count function returns source words (msgid) in current file. * @return int */ #[ReturnTypeWillChange] public function count(){ $n = $this->sw; if( is_null($n) ){ $n = $this->countField('source'); $this->sw = $n; } return $n; } /** * Very simple word count, only suitable for latin characters, and biased toward English. * @param string * @return int */ public static function simpleCount( $str ){ $n = 0; if( is_string($str) && '' !== $str ){ // TODO should we strip PHP string formatting? // e.g. "Hello %s" currently counts as 2 words. // $str = preg_replace('/%(?:\\d+\\$)?(?:\'.|[-+0 ])*\\d*(?:\\.\\d+)?[suxXbcdeEfFgGo%]/', '', $str ); // Strip HTML (but only if open and close tags detected, else "< foo" would be stripped to nothing if( false !== strpos($str,'<') && false !== strpos($str,'>') ){ $str = strip_tags($str); } // always html-decode, else escaped punctuation will be counted as words $str = html_entity_decode( $str, ENT_QUOTES, 'UTF-8'); // Collapsing apostrophe'd words into single units: // Simplest way to handle ambiguity of "It's Tim's" (technically three words in English) $str = preg_replace('/(\\w+)\'(\\w)(\\W|$)/u', '\\1\\2\\3', $str ); // Combining floating numbers into single units // e.g. "£1.50" and "€1,50" should be one word each $str = preg_replace('/\\d[\\d,\\.]+/', '0', $str ); // count words by standard Unicode word boundaries $words = preg_split( '/\\W+/u', $str, -1, PREG_SPLIT_NO_EMPTY ); $n += count($words); /*/ TODO should we exclude some words (like numbers)? foreach( $words as $word ){ if( ! ctype_digit($word) ){ $n++; } }*/ } return $n; } } PK!02h ; ;Data.phpnu„[µü¤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 ', '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); } } PK!’t㸞+ž+ Matcher.phpnu„[µü¤project = $project; } /** * Initialize matcher with current valid source strings (ref.pot) * @param Loco_gettext_Data $pot POT reference * @param bool $translate Whether copying translations from reference data * @return int */ public function loadRefs( Loco_gettext_Data $pot, $translate = false ){ $ntotal = 0; $this->translate = (bool) $translate; $this->translated = 0; /* @var LocoPoMessage $new */ foreach( $pot as $new ){ $ntotal++; $this->add($new); } return $ntotal; } /** * Perform a reverse lookup for a file reference from its pre-computed hash */ private function findScript( $hash ){ $map = $this->hashes; // build full index of all script hashes under configured source locations. if( is_null($map) ){ $map = []; $scripts = clone $this->project->getSourceFinder(); $scripts->filterExtensions(['js']); $basepath = $this->project->getBundle()->getDirectoryPath(); /* @var Loco_fs_File $jsfile */ foreach( $scripts->export() as $jsfile ){ $ref = $jsfile->getRelativePath($basepath); if( substr($ref,-7) === '.min.js' ) { $ref = substr($ref,0,-7).'.js'; } $map[ md5($ref) ] = $ref; } $this->hashes = $map; } return array_key_exists($hash,$map) ? $map[$hash] : ''; } /** * Add further source strings from JSON/JED file */ private function loadJson( Loco_fs_File $file ):int { $unique = 0; $jed = json_decode( $file->getContents(), true ); if( ! is_array($jed) || ! array_key_exists('locale_data',$jed) || ! is_array($jed['locale_data']) ){ throw new Loco_error_Debug( $file->basename().' is not JED formatted'); } // without a file reference, strings will never be compiled back to the correct JSON. // if missing from JED, we'll attempt reverse match from scripts found on disk. $ref = array_key_exists('source',$jed) ? $jed['source'] : ''; if( '' === $ref || ! is_string($ref) ){ $name = $file->basename(); $ref = preg_match('/-([0-9a-f]{32})\\.json$/',$name,$r) ? $this->findScript($r[1]) : ''; if( '' === $ref ){ throw new Loco_error_Debug($name.' has no "source" key; script is unknown'); } // The hash is pre-computed and .js file is known to exist, so we'll skip filters here. // The compiler will still filter this reference, so it could potentially yield a different hash. // Loco_error_AdminNotices::debug($name.' has no "source" key; reverse matched '.$ref); } // We won't search the original script to know the line number, but this must be a valid reference // TODO We could extract the JS here, and search for each string in the JSON, but may not be 100% reliable. $ref .= ':1'; // not checking domain key. Should be valid if passed here and should only be one. foreach( $jed['locale_data'] as /*$domain =>*/ $keys ){ foreach( $keys as $msgid => $arr ){ if( '' === $msgid || ! is_array($arr) || ! isset($arr[0]) ){ continue; } $msgctxt = ''; // Unglue "msgctxt\4msgid" unique key $parts = explode("\4",$msgid,2); if( array_key_exists(1,$parts) ){ list($msgctxt,$msgid) = $parts; // TODO handle empty msgid case that uses weird "msgctxt\4(msgctxt)" format? } // string may exist in original template, and also in multiple JSONs. $new = ['source'=>$msgid,'context'=>$msgctxt,'refs'=>$ref ]; $old = $this->getArrayRef($new); if( $old ){ $refs = array_key_exists('refs',$old) ? (string) $old['refs'] : ''; if( '' === $refs ){ $old['refs'] = $ref; } else if( 0 === preg_match('/\\b'.preg_quote($ref,'/').'\\b/',$refs) ){ $old['refs'].= ' '.$ref; } $new = $old; } else { $unique++; } // Add translation from JSON only if not present in merged PO already if( $this->translate && ( ! array_key_exists('target',$new) || '' === $new['target'] ) ){ $new['target'] = $arr[0]; } $message = new LocoPoMessage($new); $this->add($message); // handle plurals, noting that msgid_plural is not stored in JED structure if( 1 < count($arr) ){ $index = 0; $plurals = $old && array_key_exists('plurals',$old) ? $old['plurals'] : []; while( array_key_exists(++$index,$arr) ){ if( array_key_exists($index,$plurals) ){ $raw = $plurals[$index]; if( $raw instanceof ArrayObject ){ $raw = $raw->getArrayCopy(); } } else { $raw = ['source'=>'','target'=>'']; } if( $this->translate && ( ! array_key_exists('target',$raw) || '' === $raw['target'] ) ){ $raw['target'] = $arr[$index]; } // use translation as missing msgid_plural only if msgid matches msgstr (English file) if( 1 === $index && '' === $raw['source'] ){ if( $arr[0] === $msgid ){ $raw['source'] = $arr[1]; } /*else { Loco_error_AdminNotices::debug('msgid_plural missing for msgid '.json_encode($msgid) ); }*/ } $plurals[$index] = new LocoPoMessage($raw); } $message['plurals'] = $plurals; } } } return $unique; } /** * Shortcut for loading multiple jsons with error tolerance * @param Loco_fs_File[] $jsons * @return int */ public function loadJsons( array $jsons ){ $n = 0; foreach( $jsons as $jsonfile ){ try { $n += $this->loadJson($jsonfile); } catch( Loco_error_Exception $e ){ Loco_error_AdminNotices::add($e); } } return $n; } /** * Update still-valid sources, deferring unmatched (new strings) for deferred fuzzy match * @param LocoPoIterator $original Existing definitions * @param LocoPoIterator $merged Resultant definitions * @return string[] keys matched exactly */ public function mergeValid( LocoPoIterator $original, LocoPoIterator $merged ){ $valid = []; $translate = $this->translate; /* @var LocoPoMessage $old */ foreach( $original as $old ){ $new = $this->match($old); // if existing source is still valid, merge any changes if( $new instanceof LocoPoMessage ){ $p = clone $old; $p->merge($new,$translate); $merged->push($p); $valid[] = $p->getKey(); // increment counter if translation was merged if( $translate && ! $old->translated() ){ $this->translated += $new->translated(); } } } return $valid; } /** * Perform fuzzy matching after all exact matches have been attempted * @param LocoPoIterator $merged Resultant definitions * @return string[] strings fuzzy-matched */ public function mergeFuzzy( LocoPoIterator $merged ){ $fuzzy = []; foreach( $this->getFuzzyMatches() as $pair ){ list($old,$new) = $pair; $p = clone $old; $p->merge($new); $merged->push($p); $fuzzy[] = $p->getKey(); } return $fuzzy; } /** * Add unmatched strings remaining as NEW source strings * @param LocoPoIterator $merged Resultant definitions to accept new strings * @return string[] strings added */ public function mergeAdded( LocoPoIterator $merged ){ $added = []; $translate = $this->translate; /* @var LocoPoMessage $new */ foreach( $this->unmatched() as $new ){ $p = clone $new; // remove translations unless configured to keep if( $p->translated() && ! $translate ){ $p->strip(); } $merged->push($p); $added[] = $p->getKey(); } return $added; } /** * Perform full merge and return result suitable from front end. * @param LocoPoIterator $original Existing definitions * @param LocoPoIterator $merged Resultant definitions * @return array result */ public function merge( LocoPoIterator $original, LocoPoIterator $merged ){ $this->mergeValid($original,$merged); $fuzzy = $this->mergeFuzzy($merged); $added = $this->mergeAdded($merged); /* @var LocoPoMessage $old */ $dropped = []; foreach( $this->redundant() as $old ){ $dropped[] = $old->getKey(); } // return to JavaScript with stats in the same form as old front end merge return [ 'add' => $added, 'fuz' => $fuzzy, 'del' => $dropped, 'trn' => $this->translated, ]; } /** * @param array $a * @return array */ private function getArrayRef( array $a ){ $r = $this->getRef($a); if( is_null($r) ){ return []; } if( $r instanceof ArrayObject ){ return $r->getArrayCopy(); } throw new Exception( (is_object($r)?get_class($r):gettype($r) ).' returned from '.get_class($this).'::getRef'); } } PK!¼‡‡¬÷1÷1 Compiler.phpnu„[µü¤files = new Loco_fs_Siblings($pofile); $this->progress = new Loco_mvc_ViewParams( [ 'pobytes' => 0, 'mobytes' => 0, 'numjson' => 0, 'phbytes' => 0, ] ); // Connect compiler to the file system, if writing to disk for real if( ! $pofile instanceof Loco_fs_DummyFile ) { $this->fs = new Loco_api_WordPressFileSystem; } $this->done = new Loco_fs_FileList; } /** * Write PO, MO and JSON siblings */ public function writeAll( Loco_gettext_Data $po, ?Loco_package_Project $project = null ):Loco_fs_FileList { $this->writePo($po); $this->writeMo($po); if( $project ){ $this->writeJson($project,$po); } return $this->done; } /** * @return int bytes written to PO file * @throws Loco_error_WriteException */ public function writePo( Loco_gettext_Data $po ):int { $file = $this->files->getSource(); // Perform PO file backup before overwriting an existing PO if( $file->exists() && $this->fs ){ $backups = new Loco_fs_Revisions($file); $backup = $backups->rotate($this->fs); // debug backup creation only under cli or ajax. too noisy printing on screen if( $backup && ( loco_doing_ajax() || 'cli' === PHP_SAPI ) && $backup->exists() ){ Loco_error_AdminNotices::debug( sprintf('Wrote backup: %s -> %s',$file->basename(),$backup->basename() ) ); } } $bytes = $this->writeFile( $file, $po->msgcat() ); $this->progress['pobytes'] = $bytes; return $bytes; } /** * @return int bytes written to MO file */ public function writeMo( Loco_gettext_Data $po ):int { try { $mofile = $this->files->getBinary(); $bytes = $this->writeFile( $mofile, $po->msgfmt() ); } catch( Exception $e ){ Loco_error_AdminNotices::debug( $e->getMessage() ); Loco_error_AdminNotices::warn( __('PO file saved, but MO file compilation failed','loco-translate') ); $bytes = 0; } $this->progress['mobytes'] = $bytes; // write PHP cache, if WordPress >= 6.5 if( 0 !== $bytes ){ try { $this->progress['phbytes'] = $this->writePhp($po); } catch( Exception $e ){ Loco_error_AdminNotices::debug( $e->getMessage() ); } } return $bytes; } /** * @return int bytes written to .l10n.php file */ private function writePhp( Loco_gettext_Data $po ):int { $phfile = $this->files->getCache(); if( $phfile && class_exists('WP_Translation_File_PHP',false) ){ return $this->writeFile( $phfile, Loco_gettext_PhpCache::render($po) ); } return 0; } /** * @param Loco_package_Project $project Translation set, required to resolve script paths * @param Loco_gettext_Data $po PO data to export */ public function writeJson( Loco_package_Project $project, Loco_gettext_Data $po ):Loco_fs_FileList { $domain = $project->getDomain()->getName(); $pofile = $this->files->getSource(); $jsons = new Loco_fs_FileList; // Allow plugins to dictate a single JSON file to hold all script translations for a text domain // authors will additionally have to filter at runtime on load_script_translation_file $path = apply_filters('loco_compile_single_json', '', $pofile->getPath(), $domain ); if( is_string($path) && '' !== $path ){ $refs = $po->splitRefs( $this->getJsExtMap() ); if( array_key_exists('js',$refs) && $refs['js'] instanceof Loco_gettext_Data ){ $jsonfile = new Loco_fs_File($path); $json = $refs['js']->msgjed($domain,'*.js'); try { if( '' !== $json ){ $this->writeFile($jsonfile,$json); $jsons->add($jsonfile); } } catch( Loco_error_WriteException $e ){ Loco_error_AdminNotices::debug( $e->getMessage() ); // translators: %s refers to a JSON file which could not be compiled due to an error Loco_error_AdminNotices::warn( sprintf(__('JSON compilation failed for %s','loco-translate'),$jsonfile->basename()) ); } } } // continue as per default, generating multiple per-script JSON else { $buffer = []; $base_dir = $project->getBundle()->getDirectoryPath(); $extensions = array_keys( $this->getJsExtMap() ); $refsGrep = '\\.(?:'.implode('|',$extensions).')'; /* @var Loco_gettext_Data $fragment */ foreach( $po->exportRefs($refsGrep) as $ref => $fragment ){ $use = null; // Reference could be a js source file, or a minified version. We'll try .min.js first, then .js // Build systems may differ, but WordPress only supports these suffixes. See WP-CLI MakeJsonCommand. if( substr($ref,-7) === '.min.js' ) { $paths = [ $ref, substr($ref,-7).'.js' ]; } else { $paths = [ substr($ref,0,-3).'.min.js', $ref ]; } // Try .js and .min.js paths to check whether deployed script actually exists foreach( $paths as $path ){ // Hook into load_script_textdomain_relative_path like load_script_textdomain() does. $url = $project->getBundle()->getDirectoryUrl().$path; $path = apply_filters( 'load_script_textdomain_relative_path', $path, $url ); if( ! is_string($path) || '' === $path ){ continue; } // by default ignore js file that is not in deployed code $file = new Loco_fs_File($path); $file->normalize($base_dir); if( apply_filters('loco_compile_script_reference',$file->exists(),$path,$domain) ){ $use = $path; break; } } // if neither exists in the bundle, this is a source path that will never be resolved at runtime if( is_null($use) ){ Loco_error_AdminNotices::debug( sprintf('Skipping JSON for %s; script not found in bundle',$ref) ); } // add .js strings to buffer for this json and merge if already present else if( array_key_exists($use,$buffer) ){ $buffer[$use]->concat($fragment); } else { $buffer[$use] = $fragment; } } if( $buffer ){ // write all buffered fragments to their computed JSON paths foreach( $buffer as $ref => $fragment ) { $json = $fragment->msgjed($domain,$ref); if( '' === $json ){ Loco_error_AdminNotices::debug( sprintf('Skipping JSON for %s; no translations',$ref) ); continue; } try { $jsonfile = self::cloneJson($pofile,$ref,$domain); $this->writeFile( $jsonfile, $json ); $jsons->add($jsonfile); } catch( Loco_error_WriteException $e ){ Loco_error_AdminNotices::debug( $e->getMessage() ); // phpcs:ignore -- comment already applied to this string elsewhere Loco_error_AdminNotices::warn( sprintf(__('JSON compilation failed for %s','loco-translate'),$ref)); } } $buffer = null; } } // clean up redundant JSONs including if no JSONs were compiled if( Loco_data_Settings::get()->jed_clean ){ foreach( $this->files->getJsons($domain) as $path ){ $jsonfile = new Loco_fs_File($path); if( ! $jsons->has($jsonfile) ){ try { $jsonfile->unlink(); } catch( Loco_error_WriteException $e ){ Loco_error_AdminNotices::debug('Unable to remove redundant JSON: '.$e->getMessage() ); } } } } $this->progress['numjson'] = $jsons->count(); return $jsons; } /** * Clone localised file as a WordPress script translation file */ private static function cloneJson( Loco_fs_File $pofile, string $ref, string $domain ):Loco_fs_File { $name = $pofile->filename(); // Theme author PO files have no text domain, but JSON files must always be prefixed if( $domain && 'default' !== $domain && preg_match('/^[a-z]{2,3}(?:_[a-z\\d_]+)?$/i',$name) ){ $name = $domain.'-'.$name; } // Hashable reference is always finally unminified, as per load_script_textdomain() if( '' !== $ref ){ $name .= '-'.self::hashRef($ref); } return $pofile->cloneBasename( $name.'.json' ); } /** * Hashable reference is always finally unminified, as per load_script_textdomain() * @param string $ref script path relative to plugin base */ private static function hashRef( string $ref ):string { if( substr($ref,-7) === '.min.js' ) { $ref = substr($ref,0,-7).'.js'; } return md5($ref); } /** * Fetch compilation summary and raise most relevant success message */ public function getSummary():Loco_mvc_ViewParams { $pofile = $this->files->getSource(); // Avoid calling this unless the initial PO save was successful if( ! $this->progress['pobytes'] ){ throw new LogicException('PO not saved'); } // Summary for localised file includes MO+JSONs $mobytes = $this->progress['mobytes']; $numjson = $this->progress['numjson']; if( $mobytes && $numjson ){ Loco_error_AdminNotices::success( __('PO file saved and MO/JSON files compiled','loco-translate') ); } else if( $mobytes ){ Loco_error_AdminNotices::success( __('PO file saved and MO file compiled','loco-translate') ); } else { // translators: Success notice where %s is a file extension, e.g. "PO" Loco_error_AdminNotices::success( sprintf(__('%s file saved','loco-translate'),strtoupper($pofile->extension())) ); } return $this->progress; } /** * Obtain non-standard JavaScript file extensions. * @return string[] where keys are PCRE safe extensions, all mapped to "js" */ private function getJsExtMap():array { $map = ['js'=>'js','jsx'=>'js']; $exts = Loco_data_Settings::get()->jsx_alias; if( is_array($exts) && $exts ){ $exts = array_map( [__CLASS__,'pregQuote'], $exts); $map = array_fill_keys($exts,'js') + $map; } return $map; } /** * @internal */ private static function pregQuote( string $value ):string { return preg_quote($value,'/'); } /** * @param Loco_fs_File $file * @param string $data to write to given file * @return int bytes written */ public function writeFile( Loco_fs_File $file, string $data ):int { if( $this->fs ) { $this->fs->authorizeSave( $file ); } $bytes = $file->putContents($data); if( 0 !== $bytes ){ $this->done->add($file ); } return $bytes; } } PK!þ.ìMyyExtraction.phpnu„[µü¤bundle = $bundle; $this->extracted = new LocoExtracted; $this->extracted->setDomain('default'); $default = $bundle->getDefaultProject(); if( $default instanceof Loco_package_Project ){ $domain = $default->getDomain()->getName(); // wildcard stands in for empty text domain, meaning unspecified or dynamic domains will be included. // note that strings intended to be in "default" domain must specify explicitly, or be included here too. if( '*' === $domain ){ $domain = ''; $this->extracted->setDomain(''); } // pull bundle's default metadata. these are translations that may not be encountered in files $extras = []; $header = $bundle->getHeaderInfo(); foreach( $bundle->getMetaTranslatable() as $prop => $notes ){ $text = $header->__get($prop); if( is_string($text) && '' !== $text ){ $extras[] = ['source'=>$text, 'notes'=>$notes ]; } } if( $extras ){ $this->extras[$domain] = $extras; } } } /** * @return self */ public function addProject( Loco_package_Project $project ){ $base = $this->bundle->getDirectoryPath(); $domain = (string) $project->getDomain(); // skip files larger than configured maximum $opts = Loco_data_Settings::get(); $max = wp_convert_hr_to_bytes( $opts->max_php_size ); // *attempt* to raise memory limit to WP_MAX_MEMORY_LIMIT if( function_exists('wp_raise_memory_limit') ){ wp_raise_memory_limit('loco'); } /* @var Loco_fs_File $file */ foreach( $project->findSourceFiles() as $file ){ $type = $opts->ext2type( $file->extension() ); $fileref = $file->getRelativePath($base); try { $extr = loco_wp_extractor( $type, $file->fullExtension() ); if( 'php' === $type || 'twig' === $type) { // skip large files for PHP, because token_get_all is hungry if( 0 !== $max ){ $size = $file->size(); $this->maxbytes = max( $this->maxbytes, $size ); if( $size > $max ){ $list = $this->skipped or $list = ( $this->skipped = new Loco_fs_FileList() ); $list->add( $file ); continue; } } // extract headers from theme files (templates and patterns) if( $project->getBundle()->isTheme() ){ $extr->headerize( [ 'Template Name' => ['notes'=>'Name of the template'], ], $domain ); if( preg_match('!^patterns/!', $fileref) ){ $extr->headerize([ 'Title' => ['context'=>'Pattern title'], 'Description' => ['context'=>'Pattern description'], ], $domain ); } } } // normally missing domains are treated as "default", but we'll make an exception for theme.json. else if( 'json' === $type && $project->getBundle()->isTheme() ){ $extr->setDomain($domain); } $this->extracted->extractSource( $extr, $file->getContents(), $fileref ); } catch( Exception $e ){ Loco_error_AdminNotices::debug('Error extracting '.$fileref.': '.$e->getMessage() ); } } return $this; } /** * Add metadata strings deferred from construction. Note this will alter domain counts * @return self */ public function includeMeta(){ foreach( $this->extras as $domain => $extras ){ foreach( $extras as $entry ){ $this->extracted->pushEntry($entry,$domain); } } $this->extras = []; return $this; } /** * Add a custom source string constructed from `new Loco_gettext_String(msgid,[msgctxt])` * @param Loco_gettext_String $string * @param string $domain Optional text domain, if not current bundle's default * @return void */ public function addString( Loco_gettext_String $string, $domain = '' ){ if( ! $domain ) { $default = $this->bundle->getDefaultProject(); $domain = (string) ( $default ? $default->getDomain() : $this->extracted->getDomain() ); } $index = $this->extracted->pushEntry( $string->exportSingular(), $domain ); if( $string->hasPlural() ){ $this->extracted->pushPlural( $string->exportPlural(), $index ); } } /** * Get number of unique strings across all domains extracted (excluding additional metadata) * @return array { default: x, myDomain: y } */ public function getDomainCounts(){ return $this->extracted->getDomainCounts(); } /** * Pull extracted data into POT, filtering out any unwanted domains * @param string $domain * @return Loco_gettext_Data */ public function getTemplate( $domain ){ do_action('loco_extracted_template', $this, $domain ); $data = new Loco_gettext_Data( $this->extracted->filter($domain) ); return $data->templatize( $domain ); } /** * Get total number of strings extracted from all domains, excluding additional metadata * @return int */ public function getTotal(){ return $this->extracted->count(); } /** * Get list of files skipped, or null if none were skipped * @return Loco_fs_FileList|null */ public function getSkipped(){ return $this->skipped; } /** * Get size in bytes of largest file encountered, even if skipped. * This is the value required of the max_php_size plugin setting to extract all files * @return int */ public function getMaxPhpSize(){ return $this->maxbytes; } } PK!vÈ 00 PhpCache.phpnu„[µü¤headers = self::exportHeaders($po); $me->entries = self::exportEntries($po); // TODO support Loco_data_Settings::get()->php_pretty return $me->export(); } private static function exportHeaders( Loco_gettext_Data $po ){ $a = []; foreach( $po->getHeaders() as $key => $value ){ $a[ strtolower($key) ] = (string) $value; } return $a; } private static function exportEntries( Loco_gettext_Data $po ){ $a = []; $skip_fuzzy = ! Loco_data_Settings::get()->use_fuzzy; // $max = preg_match('/^nplurals=(\\d)/',$po->getHeaders()->offsetGet('plural-forms'),$r) ? $r[1] : 0; /* @var LocoPoMessage $message */ foreach( $po as $message ){ if( $skip_fuzzy && 4 === $message->__get('flag') ){ continue; } // Like JED, we must follow MO sparseness. Else empty strings will be merged on top of translations. // TODO what should we do about partial completion of pluralized messages? if( $message->translated() ) { $a[ $message->getKey() ] = implode( "\0", $message->exportSerial() ); } } return $a; } /*private function prettyExport() { return 'headers + ['messages'=>$this->entries],true) . ';' . PHP_EOL; }*/ } PK!3SMá  String.phpnu„[µü¤raw = [ 'source' => (string) $msgid, 'context' => (string) $msgctxt, ]; } /** * Get singular form as raw array data * @internal * @return string[] */ public function exportSingular(){ return $this->raw; } /** * Get plural form as raw array data * @internal * @return string[] */ public function exportPlural(){ return [ 'source' => $this->plural, ]; } /** * @param string $prop * @param string|array $value * @param string $glue * @return void */ private function merge( $prop, $value, $glue ){ if( is_string($value) ){ $value = [$value]; } else if( ! is_array($value) ){ throw new InvalidArgumentException('Expected Array or String'); } if( array_key_exists($prop,$this->raw) ){ $value = array_merge( explode($glue,$this->raw[$prop]), $value ); } $this->raw[$prop] = implode($glue,$value); } /** * @param array|string $refs * @return self */ public function addFileReferences( $refs ){ $this->merge('refs',$refs,' '); return $this; } /** * @param array|string $notes * @return self */ public function addExtractedComment( $notes ){ $this->merge('notes',$notes,' '); return $this; } /** * @param string $msgid_plural * @return self */ public function pluralize( $msgid_plural ){ $this->plural = (string) $msgid_plural; return $this; } /** * @return bool */ public function hasPlural(){ return is_string($this->plural) && '' !== $this->plural; } /*public function __toString(){ return json_encode( $this->raw ); }*/ }PK!o›#’VV Metadata.phpnu„[µü¤ total, 'p' => progress, 'f' => fuzzy ]; */ public static function stats( array $po ){ $t = $p = $f = 0; /* @var $r array */ foreach( $po as $i => $r ){ // skip header if( 0 === $i && empty($r['source']) && empty($r['context']) ){ continue; } // plural form // TODO how should plural forms affect stats? should all forms be complete before 100% can be achieved? should offsets add to total?? if( isset($r['parent']) && is_int($r['parent']) ){ continue; } // singular form $t++; if( '' !== $r['target'] ){ $p++; if( isset($r['flag']) /*&& LOCO_FLAG_FUZZY === $r['flag']*/ ){ $f++; } } } return compact('t','p','f'); } /** * {@inheritdoc} */ public function getKey(){ return 'po_'.md5( $this['rpath'] ); } /** * Load metadata from file, using cache if enabled. * Note that this does not throw exception, check "valid" key * @return Loco_gettext_Metadata */ public static function load( Loco_fs_File $po, $nocache = false ){ $bytes = $po->size(); $mtime = $po->modified(); // quick construct of a new metadata object. enough to query and validate cache $meta = new Loco_gettext_Metadata( [ 'rpath' => $po->getRelativePath( loco_constant('WP_CONTENT_DIR') ), ] ); // pull from cache if exists and has not been modified if( $nocache || ! $meta->fetch() || $bytes !== $meta['bytes'] || $mtime !== $meta['mtime'] ){ // not available from cache, or cache is invalidated $meta['bytes'] = $bytes; $meta['mtime'] = $mtime; // parse what is hopefully a PO file to get stats try { $data = Loco_gettext_Data::load($po)->getArrayCopy(); $meta['valid'] = true; $meta['stats'] = self::stats($data); } catch( Exception $e ){ $meta['valid'] = false; $meta['error'] = $e->getMessage(); } } // show cached debug notice as if file was being parsed else if( $meta->offsetExists('error') ){ Loco_error_AdminNotices::debug($meta['error'].': '.$meta['rpath']); } // persist on shutdown with a useful TTL and keepalive // Maximum lifespan: 10 days. Refreshed if accessed a day after being cached. $meta->setLifespan(864000)->keepAlive(86400)->persistLazily(); return $meta; } /** * Construct metadata from previously parsed PO data * @return Loco_gettext_Metadata */ public static function create( Loco_fs_File $file, Loco_gettext_Data $data ){ return new Loco_gettext_Metadata( [ 'valid' => true, 'bytes' => $file->size(), 'mtime' => $file->modified(), 'stats' => self::stats( $data->getArrayCopy() ), ] ); } /** * Get progress stats as simple array with keys, t=total, p=progress, f:flagged. * Note that untranslated strings are never flagged, hence "f" includes all in "p" * @return array in form ['t' => total, 'p' => progress, 'f' => fuzzy ]; */ public function getStats(){ if( isset($this['stats']) ){ return $this['stats']; } // fallback to empty stats return [ 't' => 0, 'p' => 0, 'f' => 0 ]; } /** * Get total number of messages, not including header and excluding plural forms * @return int */ public function getTotal(){ $stats = $this->getStats(); return $stats['t']; } /** * Get number of fuzzy messages, not including header * @return int */ public function countFuzzy(){ $stats = $this->getStats(); return $stats['f']; } /** * Get progress as a string percentage (minus % symbol) * @return string */ public function getPercent(){ $stats = $this->getStats(); $n = max( 0, $stats['p'] - $stats['f'] ); $t = max( $n, $stats['t'] ); return loco_string_percent( $n, $t ); } /** * Get number of strings either untranslated or fuzzy. * @return int */ public function countIncomplete(){ $stats = $this->getStats(); return max( 0, $stats['t'] - ( $stats['p'] - $stats['f'] ) ); } /** * Get number of strings completely untranslated (excludes fuzzy). * @return int */ public function countUntranslated(){ $stats = $this->getStats(); return max( 0, $stats['t'] - $stats['p'] ); } /** * Echo progress bar using compiled function * @return void */ public function printProgress(){ $stats = $this->getStats(); $flagged = $stats['f']; $translated = $stats['p']; $untranslated = $stats['t'] - $translated; loco_print_progress( $translated, $untranslated, $flagged ); } /** * Get wordy summary of total strings * @return string */ public function getTotalSummary(){ $total = $this->getTotal(); // translators: Where %s is any number of strings return sprintf( _n('%s string','%s strings',$total,'loco-translate'), number_format_i18n($total) ); } /** * Get wordy summary including translation stats * @return string */ public function getProgressSummary(){ $extra = []; // translators: Shows percentage translated at top of editor $stext = sprintf( __('%s%% translated','loco-translate'), $this->getPercent() ).', '.$this->getTotalSummary(); if( $num = $this->countFuzzy() ){ // translators: Shows number of fuzzy strings at top of editor $extra[] = sprintf( __('%s fuzzy','loco-translate'), number_format($num) ); } if( $num = $this->countUntranslated() ){ // translators: Shows number of untranslated strings at top of editor $extra[] = sprintf( __('%s untranslated','loco-translate'), number_format($num) ); } if( $extra ){ $stext .= ' ('.implode(', ', $extra).')'; } return $stext; } /** * @param bool $absolute * @return string */ public function getPath( $absolute ){ $path = $this['rpath']; if( $absolute && ! Loco_fs_File::abs($path) ){ $path = trailingslashit( loco_constant('WP_CONTENT_DIR') ).$path; } return $path; } } PK! +H" " SyncOptions.phpnu„[µü¤PK!ê§ùé é a SearchPaths.phpnu„[µü¤PK!Fw6w w ‰WordCount.phpnu„[µü¤PK!02h ; ;=!Data.phpnu„[µü¤PK!’t㸞+ž+ ~\Matcher.phpnu„[µü¤PK!¼‡‡¬÷1÷1 WˆCompiler.phpnu„[µü¤PK!þ.ìMyyŠºExtraction.phpnu„[µü¤PK!vÈ 00 A×PhpCache.phpnu„[µü¤PK!3SMá  ­ÞString.phpnu„[µü¤PK!o›#’VV èMetadata.phpnu„[µü¤PK ú“