| 1 | <?php |
|---|
| 2 | /** |
|---|
| 3 | * @codeCoverageIgnore |
|---|
| 4 | * @noinspection PhpUnused |
|---|
| 5 | */ |
|---|
| 6 | class Loco_admin_DebugController extends Loco_mvc_AdminController { |
|---|
| 7 | |
|---|
| 8 | /** |
|---|
| 9 | * Text domain of debugger, limits when gets logged |
|---|
| 10 | * @var string|null $domain |
|---|
| 11 | */ |
|---|
| 12 | private $domain; |
|---|
| 13 | |
|---|
| 14 | /** |
|---|
| 15 | * Temporarily forced locale |
|---|
| 16 | * @var string|null $locale |
|---|
| 17 | */ |
|---|
| 18 | private $locale; |
|---|
| 19 | |
|---|
| 20 | /** |
|---|
| 21 | * Log lines for final result |
|---|
| 22 | * @var null|ArrayIterator |
|---|
| 23 | */ |
|---|
| 24 | private $output; |
|---|
| 25 | |
|---|
| 26 | /** |
|---|
| 27 | * Current indent for recursive logging calls |
|---|
| 28 | * @var string |
|---|
| 29 | */ |
|---|
| 30 | private $indent = ''; |
|---|
| 31 | |
|---|
| 32 | |
|---|
| 33 | /** |
|---|
| 34 | * {@inheritdoc} |
|---|
| 35 | */ |
|---|
| 36 | public function init(){ |
|---|
| 37 | parent::init(); |
|---|
| 38 | // get a better default locale than en_US |
|---|
| 39 | $locale = get_locale(); |
|---|
| 40 | if( 'en_US' === $locale ){ |
|---|
| 41 | foreach( get_available_languages() as $locale ){ |
|---|
| 42 | if( 'en_US' !== $locale ){ |
|---|
| 43 | break; |
|---|
| 44 | } |
|---|
| 45 | } |
|---|
| 46 | } |
|---|
| 47 | $params = [ |
|---|
| 48 | 'domain' => '', |
|---|
| 49 | 'locale' => '', |
|---|
| 50 | 'msgid' => '', |
|---|
| 51 | 'msgctxt' => '', |
|---|
| 52 | 'msgid_plural' => '', |
|---|
| 53 | 'n' => '', |
|---|
| 54 | 'unhook' => '', |
|---|
| 55 | 'loader' => '', |
|---|
| 56 | 'loadpath' => '', |
|---|
| 57 | 'jspath' => '', |
|---|
| 58 | ]; |
|---|
| 59 | $defaults = [ |
|---|
| 60 | 'n' => '1', |
|---|
| 61 | 'domain' => 'default', |
|---|
| 62 | 'locale' => $locale, |
|---|
| 63 | ]; |
|---|
| 64 | foreach( array_intersect_key(stripslashes_deep($_GET),$params) as $k => $value ){ |
|---|
| 65 | if( '' !== $value ){ |
|---|
| 66 | $params[$k] = $value; |
|---|
| 67 | } |
|---|
| 68 | } |
|---|
| 69 | $this->set('form', new Loco_mvc_ViewParams($params) ); |
|---|
| 70 | $this->set('default', new Loco_mvc_ViewParams($defaults+$params) ); |
|---|
| 71 | } |
|---|
| 72 | |
|---|
| 73 | |
|---|
| 74 | /** |
|---|
| 75 | * @return void |
|---|
| 76 | */ |
|---|
| 77 | private function log( ...$args ){ |
|---|
| 78 | $message = array_shift($args); |
|---|
| 79 | if( $args ){ |
|---|
| 80 | $message = vsprintf($message,$args); |
|---|
| 81 | } |
|---|
| 82 | if( is_null($this->output) ){ |
|---|
| 83 | $this->output = new ArrayIterator; |
|---|
| 84 | $this->set('log', $this->output ); |
|---|
| 85 | } |
|---|
| 86 | // redact any path information outside of WordPress root, and shorten any common locations |
|---|
| 87 | $message = str_replace( [LOCO_LANG_DIR,WP_LANG_DIR,WP_CONTENT_DIR,ABSPATH], ['{loco_lang_dir}','{wp_lang_dir}','{wp_content_dir}','{abspath}'], $message ); |
|---|
| 88 | $this->output[] = $this->indent.$message; |
|---|
| 89 | } |
|---|
| 90 | |
|---|
| 91 | |
|---|
| 92 | private function logDomainState( $domain ) { |
|---|
| 93 | $indent = $this->indent; |
|---|
| 94 | $this->indent = $indent.' . '; |
|---|
| 95 | // filter callback should log determined locale, but may not be set up yet |
|---|
| 96 | $locale = determine_locale(); |
|---|
| 97 | $this->log('determine_locale() == %s', $locale ); |
|---|
| 98 | // Show the state just prior to potentially triggering JIT. There are no hooks between __() and load_textdomain(). |
|---|
| 99 | global $l10n, $l10n_unloaded, $wp_textdomain_registry; |
|---|
| 100 | $this->log('$l10[%s] == %s', $domain, self::debugMember($l10n,$domain) ); |
|---|
| 101 | $this->log('$l10n_unloaded[%s] == %s', $domain, self::debugMember($l10n_unloaded,$domain) ); |
|---|
| 102 | $this->log('$wp_textdomain_registry->has(%s) == %b', $domain, $wp_textdomain_registry->has($domain) ); |
|---|
| 103 | $this->log('is_textdomain_loaded(%s) == %b', $domain, is_textdomain_loaded($domain) ); |
|---|
| 104 | // the following will fire more hooks, making mess of logs. We should already see this value above directly from $l10n[$domain] |
|---|
| 105 | // $this->log(' ? get_translations_for_domain(%s) == %s', $domain, self::debugType( get_translations_for_domain($domain) ) ); |
|---|
| 106 | $this->indent = $indent; |
|---|
| 107 | } |
|---|
| 108 | |
|---|
| 109 | |
|---|
| 110 | |
|---|
| 111 | private static function debugMember( array $data, $key ){ |
|---|
| 112 | return self::debugType( array_key_exists($key,$data) ? $data[$key] : null ); |
|---|
| 113 | } |
|---|
| 114 | |
|---|
| 115 | |
|---|
| 116 | private static function debugType( $value ){ |
|---|
| 117 | return is_object($value) ? get_class($value) : json_encode($value,JSON_UNESCAPED_SLASHES); |
|---|
| 118 | } |
|---|
| 119 | |
|---|
| 120 | |
|---|
| 121 | /** |
|---|
| 122 | * `loco_unload_early_textdomain` filter callback. |
|---|
| 123 | */ |
|---|
| 124 | public function filter_loco_unload_early_textdomain( $bool, $domain ){ |
|---|
| 125 | if( $this->domain === $domain ){ |
|---|
| 126 | $value = $GLOBALS['l10n'][$domain]??null; |
|---|
| 127 | $type = is_object($value) ? get_class($value) : gettype($value); |
|---|
| 128 | $this->log('~ filter:loco_unload_early_textdomain: $l10n[%s] => %s; returning %s', $domain, $type, json_encode($bool) ); |
|---|
| 129 | } |
|---|
| 130 | return $bool; |
|---|
| 131 | } |
|---|
| 132 | |
|---|
| 133 | |
|---|
| 134 | /** |
|---|
| 135 | * `loco_unloaded_textdomain` action callback from the loading helper |
|---|
| 136 | */ |
|---|
| 137 | public function on_loco_unloaded_textdomain( $domain ){ |
|---|
| 138 | if( $domain === $this->domain ){ |
|---|
| 139 | $this->log('~ action:loco_unloaded_textdomain: Text domain loaded prematurely, unloaded "%s"',$domain); |
|---|
| 140 | } |
|---|
| 141 | } |
|---|
| 142 | |
|---|
| 143 | |
|---|
| 144 | /** |
|---|
| 145 | * @deprecated |
|---|
| 146 | * `loco_unseen_textdomain` action callback from the loading helper |
|---|
| 147 | * TODO This has been scrapped in rewritten helper. Move the logic somewhere else. |
|---|
| 148 | */ |
|---|
| 149 | public function on_loco_unseen_textdomain( $domain ){ |
|---|
| 150 | if( $domain !== $this->domain ){ |
|---|
| 151 | return; |
|---|
| 152 | } |
|---|
| 153 | $locale = determine_locale(); |
|---|
| 154 | if( 'en_US' === $locale ){ |
|---|
| 155 | return; |
|---|
| 156 | } |
|---|
| 157 | if( is_textdomain_loaded($domain) ){ |
|---|
| 158 | $this->log('~ action:loco_unseen_textdomain: "%s" was loaded before helper started',$domain); |
|---|
| 159 | } |
|---|
| 160 | else { |
|---|
| 161 | $this->log('~ action:loco_unseen_textdomain: "%s" isn\'t loaded for "%s"',$domain,$locale); |
|---|
| 162 | } |
|---|
| 163 | } |
|---|
| 164 | |
|---|
| 165 | |
|---|
| 166 | /** |
|---|
| 167 | * `pre_determine_locale` filter callback |
|---|
| 168 | */ |
|---|
| 169 | public function filter_pre_determine_locale( ?string $locale = null ):?string { |
|---|
| 170 | if( is_string($this->locale) ) { |
|---|
| 171 | $this->log( '~ filter:pre_determine_locale: %s => %s', $locale ?: 'none', $this->locale ); |
|---|
| 172 | $locale = $this->locale; |
|---|
| 173 | } |
|---|
| 174 | return $locale; |
|---|
| 175 | } |
|---|
| 176 | |
|---|
| 177 | |
|---|
| 178 | /** |
|---|
| 179 | * `load_textdomain` callback |
|---|
| 180 | */ |
|---|
| 181 | public function on_load_textdomain( $domain, $mopath ){ |
|---|
| 182 | if( $domain === $this->domain ){ |
|---|
| 183 | $this->log('~ action:load_textdomain: %s', $mopath ); |
|---|
| 184 | } |
|---|
| 185 | } |
|---|
| 186 | |
|---|
| 187 | |
|---|
| 188 | /** |
|---|
| 189 | * `load_textdomain_mofile` callback |
|---|
| 190 | */ |
|---|
| 191 | public function filter_load_textdomain_mofile( $mofile, $domain ){ |
|---|
| 192 | if( $domain === $this->domain ){ |
|---|
| 193 | $this->log('~ filter:load_textdomain_mofile: %s (exists=%b)', $mofile, file_exists($mofile) ); |
|---|
| 194 | } |
|---|
| 195 | return $mofile; |
|---|
| 196 | } |
|---|
| 197 | |
|---|
| 198 | |
|---|
| 199 | /** |
|---|
| 200 | * `load_translation_file` filter callback |
|---|
| 201 | */ |
|---|
| 202 | public function filter_load_translation_file( $file, $domain/*, $locale = ''*/ ){ |
|---|
| 203 | if( $domain === $this->domain ){ |
|---|
| 204 | $this->log('~ filter:load_translation_file: %s (exists=%b)', $file, file_exists($file) ); |
|---|
| 205 | } |
|---|
| 206 | return $file; |
|---|
| 207 | } |
|---|
| 208 | |
|---|
| 209 | |
|---|
| 210 | /** |
|---|
| 211 | * `translation_file_format` filter callback |
|---|
| 212 | * TODO let form option override 'php' as preferred format |
|---|
| 213 | */ |
|---|
| 214 | public function filter_translation_file_format( $preferred_format, $domain ){ |
|---|
| 215 | if( $domain === $this->domain ){ |
|---|
| 216 | $this->log('~ filter:translation_file_format: %s', $preferred_format ); |
|---|
| 217 | } |
|---|
| 218 | return $preferred_format; |
|---|
| 219 | } |
|---|
| 220 | |
|---|
| 221 | |
|---|
| 222 | |
|---|
| 223 | /** |
|---|
| 224 | * `lang_dir_for_domain` filter callback, requires WP>=6.6 |
|---|
| 225 | */ |
|---|
| 226 | public function filter_lang_dir_for_domain( $path, $domain, $locale ){ |
|---|
| 227 | if( $domain === $this->domain && $locale === $this->locale ){ |
|---|
| 228 | if( $path ) { |
|---|
| 229 | $this->log( '~ filter:lang_dir_for_domain %s', $path ); |
|---|
| 230 | } |
|---|
| 231 | else { |
|---|
| 232 | $this->log( '! filter:lang_dir_for_domain has no path. JIT likely to fail'); |
|---|
| 233 | } |
|---|
| 234 | } |
|---|
| 235 | return $path; |
|---|
| 236 | } |
|---|
| 237 | |
|---|
| 238 | |
|---|
| 239 | /** |
|---|
| 240 | * `load_script_textdomain_relative_path` filter callback |
|---|
| 241 | */ |
|---|
| 242 | public function filter_load_script_textdomain_relative_path( $relative/*, $src*/ ){ |
|---|
| 243 | if( preg_match('!pub/js/(?:min|src)/dummy.js!', $relative )){ |
|---|
| 244 | $form = $this->get('form'); |
|---|
| 245 | $path = $form['jspath']; |
|---|
| 246 | //error_log( json_encode(func_get_args(),JSON_UNESCAPED_SLASHES).' -> '.$path ); |
|---|
| 247 | $this->log( '~ filter:load_script_textdomain_relative_path: %s => %s', $relative, $path ); |
|---|
| 248 | return $path; |
|---|
| 249 | } |
|---|
| 250 | return $relative; |
|---|
| 251 | } |
|---|
| 252 | |
|---|
| 253 | |
|---|
| 254 | /** |
|---|
| 255 | * `pre_load_script_translations` filter callback |
|---|
| 256 | * @noinspection PhpUnusedParameterInspection |
|---|
| 257 | */ |
|---|
| 258 | public function filter_pre_load_script_translations( $translations, $file, $handle /*, $domain*/ ){ |
|---|
| 259 | if( 'loco-translate-dummy' === $handle && ! is_null($translations) ){ |
|---|
| 260 | $this->log('~ filter:pre_load_script_translations: Short-circuited with %s value', gettype($translations) ); |
|---|
| 261 | } |
|---|
| 262 | return $translations; |
|---|
| 263 | } |
|---|
| 264 | |
|---|
| 265 | |
|---|
| 266 | /** |
|---|
| 267 | * `load_script_translation_file` filter callback. |
|---|
| 268 | */ |
|---|
| 269 | public function filter_load_script_translation_file( $file, $handle/* ,$domain*/ ){ |
|---|
| 270 | if( 'loco-translate-dummy' === $handle ){ |
|---|
| 271 | // error_log( json_encode(func_get_args(),JSON_UNESCAPED_SLASHES) ); |
|---|
| 272 | // if file is not found, this will fire again with file=false |
|---|
| 273 | $this->log('~ filter:load_script_translation_file: %s', var_export($file,true) ); |
|---|
| 274 | } |
|---|
| 275 | return $file; |
|---|
| 276 | } |
|---|
| 277 | |
|---|
| 278 | |
|---|
| 279 | /** |
|---|
| 280 | * `load_script_translations` filter callback |
|---|
| 281 | * @noinspection PhpUnusedParameterInspection |
|---|
| 282 | */ |
|---|
| 283 | public function filter_load_script_translations( $translations, $file, $handle, $domain ){ |
|---|
| 284 | if( 'loco-translate-dummy' === $handle ){ |
|---|
| 285 | // just log it if the value isn't JSON. |
|---|
| 286 | if( ! is_string($translations) || '' === $translations || '{' !== $translations[0] ) { |
|---|
| 287 | $this->log( '~ filter:load_script_translations: %s', var_export($translations,true) ); |
|---|
| 288 | } |
|---|
| 289 | } |
|---|
| 290 | return $translations; |
|---|
| 291 | } |
|---|
| 292 | |
|---|
| 293 | |
|---|
| 294 | /** |
|---|
| 295 | * `[n]gettext[_with_context]` filter callback |
|---|
| 296 | */ |
|---|
| 297 | public function temp_filter_gettext(){ |
|---|
| 298 | $i = func_num_args() - 1; |
|---|
| 299 | $args = func_get_args(); |
|---|
| 300 | $translation = $args[0]; |
|---|
| 301 | if( $args[$i] === $this->domain ){ |
|---|
| 302 | $args = array_slice($args,1,--$i); |
|---|
| 303 | $opts = JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE; |
|---|
| 304 | $this->log('~ filter:gettext: %s => %s', json_encode($args,$opts), json_encode($translation,$opts) ); |
|---|
| 305 | } |
|---|
| 306 | return $translation; |
|---|
| 307 | } |
|---|
| 308 | |
|---|
| 309 | |
|---|
| 310 | |
|---|
| 311 | /** |
|---|
| 312 | * @return null|Loco_package_Bundle |
|---|
| 313 | */ |
|---|
| 314 | private function getBundleByDomain( $domain, $type ){ |
|---|
| 315 | if( 'default' === $domain ){ |
|---|
| 316 | $this->log('Have WordPress core bundle'); |
|---|
| 317 | return Loco_package_Core::create(); |
|---|
| 318 | } |
|---|
| 319 | if( 'plugin' === $type ){ |
|---|
| 320 | $search = Loco_package_Plugin::getAll(); |
|---|
| 321 | } |
|---|
| 322 | else if( 'theme' === $type || 'child' === $type ){ |
|---|
| 323 | $type = 'theme'; |
|---|
| 324 | $search = Loco_package_Theme::getAll(); |
|---|
| 325 | } |
|---|
| 326 | else { |
|---|
| 327 | $type = 'bundle'; |
|---|
| 328 | $search = array_merge( Loco_package_Plugin::getAll(), Loco_package_Theme::getAll() ); |
|---|
| 329 | } |
|---|
| 330 | /* @var Loco_package_Bundle $bundle */ |
|---|
| 331 | foreach( $search as $bundle ){ |
|---|
| 332 | /* @var Loco_package_Project $project */ |
|---|
| 333 | foreach( $bundle as $project ){ |
|---|
| 334 | if( $project->getDomain()->getName() === $domain ){ |
|---|
| 335 | $this->log('Have %s bundle => %s', strtolower($bundle->getType()), $bundle->getName() ); |
|---|
| 336 | return $bundle; |
|---|
| 337 | } |
|---|
| 338 | } |
|---|
| 339 | } |
|---|
| 340 | $message = 'No '.$type.' known with text domain '.$domain; |
|---|
| 341 | Loco_error_AdminNotices::warn($message); |
|---|
| 342 | $this->log('! '.$message); |
|---|
| 343 | return null; |
|---|
| 344 | } |
|---|
| 345 | |
|---|
| 346 | |
|---|
| 347 | /** |
|---|
| 348 | * @return LocoPoMessage|null |
|---|
| 349 | */ |
|---|
| 350 | private function findMessage( $findKey, Loco_gettext_Data $messages ){ |
|---|
| 351 | /* @var LocoPoMessage $m */ |
|---|
| 352 | foreach( $messages as $m ){ |
|---|
| 353 | if( $m->getKey() === $findKey ){ |
|---|
| 354 | return $m; |
|---|
| 355 | } |
|---|
| 356 | } |
|---|
| 357 | return null; |
|---|
| 358 | } |
|---|
| 359 | |
|---|
| 360 | |
|---|
| 361 | /** |
|---|
| 362 | * Get translation from a message falling back to source, as per __, _n etc.. |
|---|
| 363 | */ |
|---|
| 364 | private function getMsgstr( LocoPoMessage $m, $pluralIndex = 0 ){ |
|---|
| 365 | $values = $m->exportSerial(); |
|---|
| 366 | if( array_key_exists($pluralIndex,$values) && '' !== $values[$pluralIndex] ){ |
|---|
| 367 | return $values[$pluralIndex]; |
|---|
| 368 | } |
|---|
| 369 | $values = $m->exportSerial('source'); |
|---|
| 370 | if( $pluralIndex ){ |
|---|
| 371 | if( array_key_exists(1,$values) && '' !== $values[1] ){ |
|---|
| 372 | return $values[1]; |
|---|
| 373 | } |
|---|
| 374 | $this->log('! message is singular, defaulting to msgid'); |
|---|
| 375 | } |
|---|
| 376 | return $values[0]; |
|---|
| 377 | } |
|---|
| 378 | |
|---|
| 379 | |
|---|
| 380 | /** |
|---|
| 381 | * Look up a source key in given messages, returning source if untranslated, and null if not found. |
|---|
| 382 | * @return string|null |
|---|
| 383 | */ |
|---|
| 384 | private function findMsgstr( $findKey, $pluralIndex, Loco_gettext_Data $messages ){ |
|---|
| 385 | $m = $this->findMessage( $findKey, $messages ); |
|---|
| 386 | return $m ? $this->getMsgstr( $m, $pluralIndex ) : null; |
|---|
| 387 | } |
|---|
| 388 | |
|---|
| 389 | |
|---|
| 390 | /** |
|---|
| 391 | * @return Plural_Forms|null |
|---|
| 392 | */ |
|---|
| 393 | private function parsePluralForms( $raw ){ |
|---|
| 394 | try { |
|---|
| 395 | $this->log('Parsing header: %s', $raw ); |
|---|
| 396 | if( ! preg_match( '#^nplurals=\\d+;\\s*plural=([-+/*%!=<>|&?:()n\\d ]+);?$#', $raw, $match ) ) { |
|---|
| 397 | throw new InvalidArgumentException( 'Invalid Plural-Forms header, ' . json_encode($raw) ); |
|---|
| 398 | } |
|---|
| 399 | return new Plural_Forms( trim( $match[1],'() ') ); |
|---|
| 400 | } |
|---|
| 401 | catch( Exception $e ){ |
|---|
| 402 | $this->log('! %s', $e->getMessage() ); |
|---|
| 403 | return null; |
|---|
| 404 | } |
|---|
| 405 | } |
|---|
| 406 | |
|---|
| 407 | |
|---|
| 408 | |
|---|
| 409 | private function selectPluralForm( $quantity, $pluralIndex, ?Plural_Forms $eq = null ){ |
|---|
| 410 | try { |
|---|
| 411 | if( $eq instanceof Plural_Forms ) { |
|---|
| 412 | $pluralIndex = $eq->execute( $quantity ); |
|---|
| 413 | $this->log( '> Selected plural form [%u]', $pluralIndex ); |
|---|
| 414 | } |
|---|
| 415 | } |
|---|
| 416 | catch ( Exception $e ){ |
|---|
| 417 | $this->log('! Keeping plural form [%u]; %s', $pluralIndex, $e->getMessage() ); |
|---|
| 418 | } |
|---|
| 419 | return $pluralIndex; |
|---|
| 420 | } |
|---|
| 421 | |
|---|
| 422 | |
|---|
| 423 | /*private function logTextDomainsLoaded(){ |
|---|
| 424 | foreach(['l10n','l10n_unloaded'] as $k ){ |
|---|
| 425 | foreach( $GLOBALS[$k] as $d => $t ){ |
|---|
| 426 | $type = is_object($t) ? get_class($t) : gettype($t); |
|---|
| 427 | $this->log('? $%s[%s] => %s', $k, var_export($d,true), $type ); |
|---|
| 428 | } |
|---|
| 429 | } |
|---|
| 430 | }*/ |
|---|
| 431 | |
|---|
| 432 | |
|---|
| 433 | /*public function on_unload_textdomain( $domain, $reloadable ){ |
|---|
| 434 | $this->log('~ action:unload_textdomain: %s, reloadable = %b', $domain, $reloadable); |
|---|
| 435 | }*/ |
|---|
| 436 | |
|---|
| 437 | |
|---|
| 438 | /** |
|---|
| 439 | * Forcefully remove the no reload flag which prevents JIT loading. |
|---|
| 440 | * Note that since WP 6.7 load_(theme|plugin)_textdomain invokes JIT loader |
|---|
| 441 | */ |
|---|
| 442 | private function unlockDomain( $domain ) { |
|---|
| 443 | global $l10n_unloaded; |
|---|
| 444 | if( is_array($l10n_unloaded) && isset($l10n_unloaded[$domain]) ){ |
|---|
| 445 | $this->log('Removing JIT lock'); |
|---|
| 446 | unset( $l10n_unloaded[$domain] ); |
|---|
| 447 | } |
|---|
| 448 | } |
|---|
| 449 | |
|---|
| 450 | |
|---|
| 451 | /** |
|---|
| 452 | * Prepare text domain for MO file lookup |
|---|
| 453 | * @return void |
|---|
| 454 | */ |
|---|
| 455 | private function preloadDomain( $domain, $type, $path ){ |
|---|
| 456 | // plugin and theme loaders allow missing path argument, custom loader does not |
|---|
| 457 | if( '' === $path ){ |
|---|
| 458 | $file = null; |
|---|
| 459 | $path = false; |
|---|
| 460 | } |
|---|
| 461 | // Just-in-time loader takes no path argument |
|---|
| 462 | else if( 'none' === $type || '' === $type ){ |
|---|
| 463 | $file = null; |
|---|
| 464 | Loco_error_AdminNotices::debug('Path argument ignored. Not required for this loading option.'); |
|---|
| 465 | } |
|---|
| 466 | else { |
|---|
| 467 | $this->log('Have path argument => %s', $path ); |
|---|
| 468 | $file = new Loco_fs_File($path); |
|---|
| 469 | } |
|---|
| 470 | |
|---|
| 471 | // Without a loader the current state of the text domain will be used for our translation. |
|---|
| 472 | // If the text domain was loaded before we set our locale, it may be in the wrong language. |
|---|
| 473 | if( 'none' === $type ){ |
|---|
| 474 | $this->log('No loader, current state is:'); |
|---|
| 475 | $this->logDomainState($domain); |
|---|
| 476 | // Note that is_textdomain_loaded() returns false even if NOOP_Translations is set, |
|---|
| 477 | // and NOOP_Translations being set prevents JIT loading, so will never translate our forced locale! |
|---|
| 478 | if( isset($GLOBALS['l10n'][$domain]) ){ |
|---|
| 479 | // WordPress >= 6.5 |
|---|
| 480 | if( class_exists('WP_Translation_Controller',false) ) { |
|---|
| 481 | $locale = WP_Translation_Controller::get_instance()->get_locale(); |
|---|
| 482 | if( $locale && $locale !== $this->locale ){ |
|---|
| 483 | Loco_error_AdminNotices::warn( sprintf('Translations already loaded for "%s". A loader is recommended to select "%s"',$locale,$this->locale) ); |
|---|
| 484 | } |
|---|
| 485 | } |
|---|
| 486 | } |
|---|
| 487 | return; |
|---|
| 488 | } |
|---|
| 489 | |
|---|
| 490 | // Unload text domain for any forced loading method |
|---|
| 491 | $this->log('Unloading text domain for %s loader', $type?:'auto' ); |
|---|
| 492 | $returned = unload_textdomain($domain); |
|---|
| 493 | $callee = 'unload_textdomain'; |
|---|
| 494 | // Bootstrap text domain if a loading function was selected |
|---|
| 495 | if( 'plugin' === $type ){ |
|---|
| 496 | if( $file ){ |
|---|
| 497 | if( $file->isAbsolute() ){ |
|---|
| 498 | $path = $file->getRelativePath(WP_PLUGIN_DIR); |
|---|
| 499 | } |
|---|
| 500 | else { |
|---|
| 501 | $file->normalize(WP_PLUGIN_DIR); |
|---|
| 502 | } |
|---|
| 503 | if( ! $file->exists() || ! $file->isDirectory() ){ |
|---|
| 504 | throw new InvalidArgumentException('Loader argument must be a directory relative to WP_PLUGIN_DIR'); |
|---|
| 505 | } |
|---|
| 506 | } |
|---|
| 507 | $this->log('Calling load_plugin_textdomain with $plugin_rel_path=%s',$path); |
|---|
| 508 | $returned = load_plugin_textdomain( $domain, false, $path ); |
|---|
| 509 | $callee = 'load_plugin_textdomain'; |
|---|
| 510 | $this->unlockDomain($domain); |
|---|
| 511 | } |
|---|
| 512 | else if( 'theme' === $type || 'child' === $type ){ |
|---|
| 513 | // Note that absent path argument will use current theme, and not necessarily whatever $domain is |
|---|
| 514 | if( $file && ( ! $file->isAbsolute() || ! $file->isDirectory() ) ){ |
|---|
| 515 | throw new InvalidArgumentException('Path argument must reference the theme directory'); |
|---|
| 516 | } |
|---|
| 517 | $this->log('Calling load_theme_textdomain with $path=%s',$path); |
|---|
| 518 | $returned = load_theme_textdomain( $domain, $path ); |
|---|
| 519 | $callee = 'load_theme_textdomain'; |
|---|
| 520 | $this->unlockDomain($domain); |
|---|
| 521 | } |
|---|
| 522 | else if( 'custom' === $type ){ |
|---|
| 523 | if( $file && ! $file->isAbsolute() ){ |
|---|
| 524 | $path = $file->normalize(WP_CONTENT_DIR); |
|---|
| 525 | $this->log('Resolving relative path argument to %s',$path); |
|---|
| 526 | } |
|---|
| 527 | if( is_null($file) || ! $file->exists() || $file->isDirectory() ){ |
|---|
| 528 | throw new InvalidArgumentException('Path argument must reference an existent file'); |
|---|
| 529 | } |
|---|
| 530 | $expected = [ $this->locale.'.mo', $this->locale.'.l10n.php' ]; |
|---|
| 531 | $bits = explode('-',$file->basename() ); |
|---|
| 532 | if( ! in_array( end($bits), $expected) ){ |
|---|
| 533 | throw new InvalidArgumentException('Path argument must end in '.$this->locale.'.mo'); |
|---|
| 534 | } |
|---|
| 535 | $this->log('Calling load_textdomain with $mofile=%s',$path); |
|---|
| 536 | $returned = load_textdomain($domain,$path,$this->locale); |
|---|
| 537 | $callee = 'load_textdomain'; |
|---|
| 538 | } |
|---|
| 539 | // JIT doesn't work for WordPress core |
|---|
| 540 | else if( 'default' === $domain ){ |
|---|
| 541 | $this->log('Reloading default text domain'); |
|---|
| 542 | $callee = 'load_default_textdomain'; |
|---|
| 543 | $returned = load_default_textdomain($this->locale); |
|---|
| 544 | } |
|---|
| 545 | // Defaulting to JIT (auto): |
|---|
| 546 | // When we called unload_textdomain we passed $reloadable=false on purpose to force memory removal |
|---|
| 547 | // So if we want to allow _load_textdomain_just_in_time, we'll have to hack the reloadable lock. |
|---|
| 548 | else { |
|---|
| 549 | $this->unlockDomain($domain); |
|---|
| 550 | } |
|---|
| 551 | $this->log('> %s returned %s', $callee, var_export($returned,true) ); |
|---|
| 552 | } |
|---|
| 553 | |
|---|
| 554 | |
|---|
| 555 | |
|---|
| 556 | /** |
|---|
| 557 | * Preload domain for a script, then forcing retrieval of JSON. |
|---|
| 558 | */ |
|---|
| 559 | private function preloadScript( $path, string $domain, ?Loco_package_Bundle $bundle = null ):Loco_gettext_Data { |
|---|
| 560 | $this->log('Have script argument => %s', $path ); |
|---|
| 561 | if( preg_match('/^[0-9a-f]{32}$/',$path) ){ |
|---|
| 562 | throw new Loco_error_Exception('Enter the script path, not the hash'); |
|---|
| 563 | } |
|---|
| 564 | // normalize file reference if bundle is known. Warning already raised if not. |
|---|
| 565 | // simulator will allow non-existent js. We can still find translations even if it's absent. |
|---|
| 566 | $jsfile = new Loco_fs_File($path); |
|---|
| 567 | if( $bundle ){ |
|---|
| 568 | $basepath = $bundle->getDirectoryPath(); |
|---|
| 569 | if( $jsfile->isAbsolute() ) { |
|---|
| 570 | $path = $jsfile->getRelativePath($basepath); |
|---|
| 571 | $this->get('form')['jspath'] = $path; |
|---|
| 572 | } |
|---|
| 573 | else { |
|---|
| 574 | $jsfile->normalize($basepath); |
|---|
| 575 | } |
|---|
| 576 | if( ! $jsfile->exists() ){ |
|---|
| 577 | $this->log( '! Script not found. load_script_textdomain may fail'); |
|---|
| 578 | } |
|---|
| 579 | } |
|---|
| 580 | // log hashable path for comparison with what WordPress computes: |
|---|
| 581 | if( '.min.js' === substr($path,-7) ) { |
|---|
| 582 | $path = substr($path,0,-7).'.js'; |
|---|
| 583 | } |
|---|
| 584 | else { |
|---|
| 585 | $valid = array_flip( Loco_data_Settings::get()->jsx_alias ?: ['js'] ); |
|---|
| 586 | if( ! array_key_exists($jsfile->extension(),$valid) ) { |
|---|
| 587 | Loco_error_AdminNotices::debug("Script path didn't end with .".implode('|',array_keys($valid) ) ); |
|---|
| 588 | } |
|---|
| 589 | } |
|---|
| 590 | $hash = md5($path); |
|---|
| 591 | $this->log('> md5(%s) => %s', var_export($path,true), $hash ); |
|---|
| 592 | // filters will point our debug script to the actual script we're simulating |
|---|
| 593 | $handle = $this->enqueueScript('dummy'); |
|---|
| 594 | if( ! wp_set_script_translations($handle,$domain) ){ |
|---|
| 595 | throw new Loco_error_Exception('wp_set_script_translations returned false'); |
|---|
| 596 | } |
|---|
| 597 | // load_script_textdomain won't fire until footer, so grab JSON directly |
|---|
| 598 | $this->log('Calling load_script_textdomain( %s )', trim(json_encode([$handle,$domain],JSON_UNESCAPED_SLASHES),'[]') ); |
|---|
| 599 | $json = load_script_textdomain($handle,$domain); |
|---|
| 600 | $this->dequeueScript('dummy'); |
|---|
| 601 | if( is_string($json) && '' !== $json ){ |
|---|
| 602 | $this->log('> Parsing %u bytes of JSON...', strlen($json) ); |
|---|
| 603 | return Loco_gettext_Data::fromJson($json); |
|---|
| 604 | } |
|---|
| 605 | throw new Loco_error_Exception('load_script_textdomain returned '.var_export($json,true) ); |
|---|
| 606 | } |
|---|
| 607 | |
|---|
| 608 | |
|---|
| 609 | |
|---|
| 610 | /** |
|---|
| 611 | * Run the string lookup and render result screen, unless an error is thrown. |
|---|
| 612 | * @return string |
|---|
| 613 | */ |
|---|
| 614 | private function renderResult( Loco_mvc_ViewParams $form ){ |
|---|
| 615 | $msgid = $form['msgid']; |
|---|
| 616 | $msgctxt = $form['msgctxt']; |
|---|
| 617 | // singular form by default |
|---|
| 618 | $msgid_plural = $form['msgid_plural']; |
|---|
| 619 | $quantity = ctype_digit($form['n']) ? (int) $form['n'] : 1; |
|---|
| 620 | $pluralIndex = 0; |
|---|
| 621 | // |
|---|
| 622 | $domain = $form['domain']?:'default'; |
|---|
| 623 | $this->log('Running test for domain => %s', $domain ); |
|---|
| 624 | //$this->logDomainState($domain); |
|---|
| 625 | $default = $this->get('default'); |
|---|
| 626 | $tag = $form['locale'] ?: $default['locale']; |
|---|
| 627 | $locale = Loco_Locale::parse($tag); |
|---|
| 628 | if( ! $locale->isValid() ){ |
|---|
| 629 | throw new InvalidArgumentException('Invalid locale code ('.$tag.')'); |
|---|
| 630 | } |
|---|
| 631 | // unhook all existing filters, including our own |
|---|
| 632 | if( $form['unhook'] ){ |
|---|
| 633 | $this->log('Unhooking l10n filters'); |
|---|
| 634 | array_map( 'remove_all_filters', [ |
|---|
| 635 | // these filters are all used by Loco_hooks_LoadHelper, and will need re-hooking afterwards: |
|---|
| 636 | 'theme_locale','plugin_locale','unload_textdomain','load_textdomain','load_script_translation_file','load_script_translations', |
|---|
| 637 | // these filters also affect text domain loading / file reading: |
|---|
| 638 | 'pre_load_textdomain','override_load_textdomain','load_textdomain_mofile','translation_file_format','load_translation_file','override_unload_textdomain','lang_dir_for_domain', |
|---|
| 639 | // script translation hooks: |
|---|
| 640 | 'load_script_textdomain_relative_path','pre_load_script_translations','load_script_translation_file','load_script_translations', |
|---|
| 641 | // these filters affect translation fetching via __, _n, _x and _nx: |
|---|
| 642 | 'gettext','ngettext','gettext_with_context','ngettext_with_context' |
|---|
| 643 | ] ); |
|---|
| 644 | // helper isn't a singleton, and will be garbage-collected now. Restart it. |
|---|
| 645 | new Loco_hooks_LoadHelper; |
|---|
| 646 | } |
|---|
| 647 | // Ensuring our forced locale requires no other filters be allowed to run. |
|---|
| 648 | // We're doing this whether "unhook" is set or not, otherwise determine_locale won't work. |
|---|
| 649 | remove_all_filters('pre_determine_locale'); |
|---|
| 650 | $this->reHook(); |
|---|
| 651 | $this->locale = (string) $locale; |
|---|
| 652 | $this->log('Have locale: %s', $this->locale ); |
|---|
| 653 | $actual = determine_locale(); |
|---|
| 654 | if( $actual !== $this->locale ){ |
|---|
| 655 | $this->log('determine_locale() => %s', $actual ); |
|---|
| 656 | Loco_error_AdminNotices::warn( sprintf('Locale %s is overriding %s', $actual, $this->locale) ); |
|---|
| 657 | } |
|---|
| 658 | // Deferred setting of text domain to avoid hooks firing before we're ready |
|---|
| 659 | $this->domain = $domain; |
|---|
| 660 | //new Loco_hooks_LoadDebugger($domain); |
|---|
| 661 | |
|---|
| 662 | // Perform preloading according to user choice, and optional path argument. |
|---|
| 663 | $type = $form['loader']; |
|---|
| 664 | $bundle = $this->getBundleByDomain($domain,$type); |
|---|
| 665 | $this->preloadDomain( $domain, $type, $form['loadpath'] ); |
|---|
| 666 | |
|---|
| 667 | // Create source message for string query |
|---|
| 668 | class_exists('Loco_gettext_Data'); |
|---|
| 669 | $message = new LocoPoMessage(['source'=>$msgid,'context'=>$msgctxt,'target'=>'']); |
|---|
| 670 | $this->log('Query: %s', LocoPo::pair('msgid',$msgid) ); |
|---|
| 671 | if( '' !== $msgid_plural ){ |
|---|
| 672 | $this->log(' | %s (n=%u)', LocoPo::pair('msgid_plural',$msgid_plural), $quantity ); |
|---|
| 673 | $message->offsetSet('plurals', [new LocoPoMessage(['source'=>$msgid_plural,'target'=>''])] ); |
|---|
| 674 | } |
|---|
| 675 | $findKey = $message->getKey(); |
|---|
| 676 | |
|---|
| 677 | // Perform runtime translation request via WordPress |
|---|
| 678 | if( '' === $msgctxt ){ |
|---|
| 679 | if( '' === $msgid_plural ) { |
|---|
| 680 | $callee = '__'; |
|---|
| 681 | $params = [ $msgid, $domain ]; |
|---|
| 682 | $this->addHook('gettext', 'temp_filter_gettext', 3, 99 ); |
|---|
| 683 | } |
|---|
| 684 | else { |
|---|
| 685 | $callee = '_n'; |
|---|
| 686 | $params = [ $msgid, $msgid_plural, $quantity, $domain ]; |
|---|
| 687 | $this->addHook('ngettext', 'temp_filter_gettext', 5, 99 ); |
|---|
| 688 | } |
|---|
| 689 | } |
|---|
| 690 | else { |
|---|
| 691 | $this->log(' | %s', LocoPo::pair('msgctxt',$msgctxt) ); |
|---|
| 692 | if( '' === $msgid_plural ){ |
|---|
| 693 | $callee = '_x'; |
|---|
| 694 | $params = [ $msgid, $msgctxt, $domain ]; |
|---|
| 695 | $this->addHook('gettext_with_context', 'temp_filter_gettext', 4, 99 ); |
|---|
| 696 | } |
|---|
| 697 | else { |
|---|
| 698 | $callee = '_nx'; |
|---|
| 699 | $params = [ $msgid, $msgid_plural, $quantity, $msgctxt, $domain ]; |
|---|
| 700 | $this->addHook('ngettext_with_context', 'temp_filter_gettext', 6, 99 ); |
|---|
| 701 | } |
|---|
| 702 | } |
|---|
| 703 | $this->log('Calling %s( %s )', $callee, trim( json_encode($params,JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE), '[]') ); |
|---|
| 704 | $msgstr = call_user_func_array($callee,$params); |
|---|
| 705 | $this->log("====>| %s", LocoPo::pair('msgstr',$msgstr,0) ); |
|---|
| 706 | |
|---|
| 707 | // Post check for text domain auto-load failure |
|---|
| 708 | $loaded = get_translations_for_domain($domain); |
|---|
| 709 | if( ! is_textdomain_loaded($domain) ){ |
|---|
| 710 | $this->log('! Text domain not loaded after %s() call completed', $callee ); |
|---|
| 711 | $this->log(' get_translations_for_domain => %s', self::debugType($loaded) ); |
|---|
| 712 | } |
|---|
| 713 | |
|---|
| 714 | // Establish retrospectively if a non-zero plural index was used. |
|---|
| 715 | if( '' !== $msgid_plural ){ |
|---|
| 716 | $header = null; |
|---|
| 717 | if( class_exists('WP_Translation_Controller',false) ){ |
|---|
| 718 | $h = WP_Translation_Controller::get_instance()->get_headers($domain); |
|---|
| 719 | if( array_key_exists('Plural-Forms',$h) ) { |
|---|
| 720 | $header = $h['Plural-Forms']; |
|---|
| 721 | } |
|---|
| 722 | } |
|---|
| 723 | if( is_null($header) ){ |
|---|
| 724 | $header = $locale->getPluralFormsHeader(); |
|---|
| 725 | $this->log('! Can\'t get Plural-Forms; Using built-in rules'); |
|---|
| 726 | } |
|---|
| 727 | $pluralIndex = $this->selectPluralForm( $quantity, $pluralIndex, $this->parsePluralForms($header) ); |
|---|
| 728 | } |
|---|
| 729 | |
|---|
| 730 | // Simulate JavaScript translation if script path is set. This will be used as a secondary result. |
|---|
| 731 | $path = $form['jspath']; |
|---|
| 732 | if( is_string($path) && '' !== $path ) { |
|---|
| 733 | try { |
|---|
| 734 | $data = $this->preloadScript( $path, $domain, $bundle ); |
|---|
| 735 | // Let JED-defined plural forms override plural index |
|---|
| 736 | if( '' !== $msgid_plural ){ |
|---|
| 737 | $header = $data->getHeaders()->offsetGet('Plural-Forms'); |
|---|
| 738 | if( $header ){ |
|---|
| 739 | $pluralIndex = $this->selectPluralForm( $quantity, $pluralIndex, $this->parsePluralForms($header) ); |
|---|
| 740 | } |
|---|
| 741 | } |
|---|
| 742 | $msgstr = $this->findMsgstr( $findKey, $pluralIndex, $data ); |
|---|
| 743 | if( is_null($msgstr) ){ |
|---|
| 744 | $this->log('! No match in JSON'); |
|---|
| 745 | } |
|---|
| 746 | else { |
|---|
| 747 | $this->log("====>| %s", LocoPo::pair('msgstr',$msgstr,0) ); |
|---|
| 748 | } |
|---|
| 749 | // Override primary translation result for script translation |
|---|
| 750 | $callee = 'load_script_textdomain'; |
|---|
| 751 | } |
|---|
| 752 | catch( Exception $e ){ |
|---|
| 753 | $this->log('! %s (falling back to PHP)', $e->getMessage() ); |
|---|
| 754 | Loco_error_AdminNotices::warn('Script translation failed. Falling back to PHP translation'); |
|---|
| 755 | } |
|---|
| 756 | } |
|---|
| 757 | |
|---|
| 758 | // Establish translation success, assuming that source being returned is equivalent to an absent translation |
|---|
| 759 | $fallback = $pluralIndex ? $msgid_plural : $msgid; |
|---|
| 760 | $translated = is_string($msgstr) && '' !== $msgstr && $msgstr !== $fallback; |
|---|
| 761 | $this->log('Translated result state => %s', $translated?'true':'false'); |
|---|
| 762 | |
|---|
| 763 | // We're done with our temporary hooks now. |
|---|
| 764 | $this->domain = null; |
|---|
| 765 | $this->locale = null; |
|---|
| 766 | |
|---|
| 767 | // Obtain all possible translations from all known targets (requires bundle) |
|---|
| 768 | $pofiles = new Loco_fs_FileList; |
|---|
| 769 | if( $bundle ){ |
|---|
| 770 | foreach( $bundle as $project ) { |
|---|
| 771 | if( $project instanceof Loco_package_Project && $project->getDomain()->getName() === $domain ){ |
|---|
| 772 | $pofiles->augment( $project->initLocaleFiles($locale) ); |
|---|
| 773 | } |
|---|
| 774 | } |
|---|
| 775 | } |
|---|
| 776 | // Without a configured bundle, we'll have to search all possible locations, but this won't include Author files. |
|---|
| 777 | // We may as well add these anyway, in case bundle is misconfigured. Small risk of plugin/theme domain conflicts. |
|---|
| 778 | if( 'default' !== $domain ){ |
|---|
| 779 | /* @var Loco_package_Bundle $tmp */ |
|---|
| 780 | foreach( [ new Loco_package_Plugin('',''), new Loco_package_Theme('','') ] as $tmp ) { |
|---|
| 781 | foreach( $tmp->getSystemTargets() as $root ){ |
|---|
| 782 | $pofiles->add( new Loco_fs_LocaleFile( sprintf('%s/%s-%s.po',$root,$domain,$locale) ) ); |
|---|
| 783 | } |
|---|
| 784 | } |
|---|
| 785 | } |
|---|
| 786 | $grouped = []; |
|---|
| 787 | $matches = []; |
|---|
| 788 | $searched = 0; |
|---|
| 789 | $matched = 0; |
|---|
| 790 | $this->log('Searching %u possible locations for string versions', $pofiles->count() ); |
|---|
| 791 | /* @var Loco_fs_LocaleFile $pofile */ |
|---|
| 792 | foreach( $pofiles as $pofile ){ |
|---|
| 793 | // initialize translation set for this PO and its siblings |
|---|
| 794 | $dir = new Loco_fs_LocaleDirectory( $pofile->dirname() ); |
|---|
| 795 | $type = $dir->getTypeId(); |
|---|
| 796 | $args = [ 'type' => $dir->getTypeLabel($type) ]; |
|---|
| 797 | // as long as we know the bundle and the PO file exists, we can link to the editor. |
|---|
| 798 | // bear in mind that domain may not be unique to one set of translations (core) so ... |
|---|
| 799 | if( $bundle && $pofile->exists() ){ |
|---|
| 800 | $route = strtolower($bundle->getType()).'-file-edit'; |
|---|
| 801 | // find exact project in bundle. Required for core, or any multi-domain bundle |
|---|
| 802 | $project = $bundle->getDefaultProject(); |
|---|
| 803 | if( is_null($project) || 1 < $bundle->count() ){ |
|---|
| 804 | $slug = $pofile->getPrefix(); |
|---|
| 805 | foreach( $bundle as $candidate ){ |
|---|
| 806 | if( $candidate->getSlug() === $slug ){ |
|---|
| 807 | $project = $candidate; |
|---|
| 808 | break; |
|---|
| 809 | } |
|---|
| 810 | } |
|---|
| 811 | } |
|---|
| 812 | $args['href'] = Loco_mvc_AdminRouter::generate( $route, [ |
|---|
| 813 | 'bundle' => $bundle->getHandle(), |
|---|
| 814 | 'domain' => $project ? $project->getId() : $domain, |
|---|
| 815 | 'path' => $pofile->getRelativePath(WP_CONTENT_DIR), |
|---|
| 816 | ] ); |
|---|
| 817 | } |
|---|
| 818 | $groupIdx = count($grouped); |
|---|
| 819 | $grouped[] = new Loco_mvc_FileParams( $args, $pofile ); |
|---|
| 820 | // even if PO file is missing, we can search the MO, JSON etc.. |
|---|
| 821 | $siblings = new Loco_fs_Siblings($pofile); |
|---|
| 822 | $siblings->setDomain($domain); |
|---|
| 823 | $exts = []; |
|---|
| 824 | foreach( $siblings->expand() as $file ){ |
|---|
| 825 | try { |
|---|
| 826 | $ext = strtolower( $file->fullExtension() ); |
|---|
| 827 | if( ! preg_match('!^(?:pot?|mo|json|l10n\\.php)$!',$ext) || ! $file->exists() ){ |
|---|
| 828 | continue; |
|---|
| 829 | } |
|---|
| 830 | $searched++; |
|---|
| 831 | $message = $this->findMessage($findKey,Loco_gettext_Data::load($file)); |
|---|
| 832 | if( $message ){ |
|---|
| 833 | $matched++; |
|---|
| 834 | $value = $this->getMsgstr($message,$pluralIndex); |
|---|
| 835 | $args = [ 'msgstr' => $value ]; |
|---|
| 836 | $matches[$groupIdx][] = new Loco_mvc_FileParams($args,$file); |
|---|
| 837 | $this->log('> found in %s => %s', $file, var_export($value,true) ); |
|---|
| 838 | $exts[$ext] = $message->translated(); |
|---|
| 839 | } |
|---|
| 840 | } |
|---|
| 841 | catch( Exception $e ){ |
|---|
| 842 | Loco_error_Debug::trace( '%s in %s', $e->getMessage(), $file ); |
|---|
| 843 | } |
|---|
| 844 | } |
|---|
| 845 | // warn if found in PO, but not MO. |
|---|
| 846 | if( isset($exts['po']) && $exts['po'] && ! isset($exts['mo']) ){ |
|---|
| 847 | Loco_error_AdminNotices::debug('Found in PO, but not MO. Is it fuzzy? Does it need recompiling?'); |
|---|
| 848 | } |
|---|
| 849 | } |
|---|
| 850 | |
|---|
| 851 | // display result if translation occurred, or if we found the string in at least one file, even if empty |
|---|
| 852 | $this->log('> %u matches in %u locations; %u files searched', $matched, count($grouped), $searched ); |
|---|
| 853 | if( $matches || $translated ){ |
|---|
| 854 | $result = new Loco_mvc_ViewParams( $form->getArrayCopy() ); |
|---|
| 855 | $result['translated'] = $translated; |
|---|
| 856 | $result['msgstr'] = $msgstr; |
|---|
| 857 | $result['callee'] = $callee; |
|---|
| 858 | $result['grouped'] = $grouped; |
|---|
| 859 | $result['matches'] = $matches; |
|---|
| 860 | $result['searched'] = $searched; |
|---|
| 861 | $result['calleeDoc'] = 'https://developer.wordpress.org/reference/functions/'.$callee.'/'; |
|---|
| 862 | return $this->view( 'admin/debug/debug-result', ['result'=>$result]); |
|---|
| 863 | } |
|---|
| 864 | // Source string not found in any translation files |
|---|
| 865 | $name = $bundle ? $bundle->getName() : $domain; |
|---|
| 866 | throw new Loco_error_Warning('No `'.$locale.'` translations found for this string in '.$name ); |
|---|
| 867 | } |
|---|
| 868 | |
|---|
| 869 | |
|---|
| 870 | |
|---|
| 871 | /** |
|---|
| 872 | * @return void |
|---|
| 873 | */ |
|---|
| 874 | private function surpriseMe(){ |
|---|
| 875 | $project = null; |
|---|
| 876 | /* @var Loco_package_Bundle[] $bundles */ |
|---|
| 877 | $bundles = array_merge( Loco_package_Plugin::getAll(), Loco_package_Theme::getAll(), [ Loco_package_Core::create() ] ); |
|---|
| 878 | while( $bundles && is_null($project) ){ |
|---|
| 879 | $key = array_rand($bundles); |
|---|
| 880 | $project = $bundles[$key]->getDefaultProject(); |
|---|
| 881 | unset($bundles[$key]); |
|---|
| 882 | } |
|---|
| 883 | // It should be impossible for project to be null, due to WordPress core always being non-empty |
|---|
| 884 | if( ! $project instanceof Loco_package_Project ){ |
|---|
| 885 | throw new LogicException('No translation projects'); |
|---|
| 886 | } |
|---|
| 887 | $domain = $project->getDomain()->getName(); |
|---|
| 888 | // Pluck a random locale from existing PO translations |
|---|
| 889 | $files = $project->findLocaleFiles('po')->getArrayCopy(); |
|---|
| 890 | $pofile = $files ? $files[ array_rand($files) ] : null; |
|---|
| 891 | $locale = $pofile instanceof Loco_fs_LocaleFile ? (string) $pofile->getLocale() : ''; |
|---|
| 892 | // Get a random source string from the code... avoiding full extraction.. pluck a PHP file... |
|---|
| 893 | class_exists('Loco_gettext_Data'); |
|---|
| 894 | $message = new LocoPoMessage(['source'=>'']); |
|---|
| 895 | $extractor = loco_wp_extractor(); |
|---|
| 896 | $extractor->setDomain($domain); |
|---|
| 897 | $files = $project->getSourceFinder()->group('php')->export()->getArrayCopy(); |
|---|
| 898 | while( $files ){ |
|---|
| 899 | $key = array_rand($files); |
|---|
| 900 | $file = $files[$key]; |
|---|
| 901 | $strings = ( new LocoExtracted )->extractSource( $extractor, $file->getContents() )->export(); |
|---|
| 902 | if( $strings ){ |
|---|
| 903 | $message = new LocoPoMessage( $strings[ array_rand($strings) ] ); |
|---|
| 904 | break; |
|---|
| 905 | } |
|---|
| 906 | // try next source file... |
|---|
| 907 | unset($files[$key]); |
|---|
| 908 | } |
|---|
| 909 | // apply random choice |
|---|
| 910 | $form = $this->get('form'); |
|---|
| 911 | $form['domain'] = $domain; |
|---|
| 912 | $form['locale'] = $locale; |
|---|
| 913 | $form['msgid'] = $message->source; |
|---|
| 914 | $form['msgctxt'] = $message->context; |
|---|
| 915 | // random message could be a plural form |
|---|
| 916 | $plurals = $message->plurals; |
|---|
| 917 | if( is_array($plurals) && array_key_exists(0,$plurals) && $plurals[0] instanceof LocoPoMessage ){ |
|---|
| 918 | $form['msgid_plural'] = $plurals[0]['source']; |
|---|
| 919 | } |
|---|
| 920 | Loco_error_AdminNotices::info( sprintf('Randomly selected "%s". Click Submit to check a string.', $project->getName() ) ); |
|---|
| 921 | } |
|---|
| 922 | |
|---|
| 923 | |
|---|
| 924 | |
|---|
| 925 | /** |
|---|
| 926 | * {@inheritdoc} |
|---|
| 927 | */ |
|---|
| 928 | public function render(){ |
|---|
| 929 | $title = __('String debugger','loco-translate'); |
|---|
| 930 | $this->set('breadcrumb', Loco_admin_Navigation::createSimple($title) ); |
|---|
| 931 | |
|---|
| 932 | try { |
|---|
| 933 | // Process form if (at least) "msgid" is set |
|---|
| 934 | $form = $this->get('form'); |
|---|
| 935 | if( '' !== $form['msgid'] ){ |
|---|
| 936 | return $this->renderResult($form); |
|---|
| 937 | } |
|---|
| 938 | // Pluck a random string for testing |
|---|
| 939 | else if( array_key_exists('randomize',$_GET) ){ |
|---|
| 940 | $this->surpriseMe(); |
|---|
| 941 | } |
|---|
| 942 | } |
|---|
| 943 | catch( Loco_error_Exception $e ){ |
|---|
| 944 | Loco_error_AdminNotices::add($e); |
|---|
| 945 | } |
|---|
| 946 | catch( Exception $e ){ |
|---|
| 947 | Loco_error_AdminNotices::add( Loco_error_Exception::convert($e) ); |
|---|
| 948 | } |
|---|
| 949 | |
|---|
| 950 | return $this->view('admin/debug/debug-form'); |
|---|
| 951 | } |
|---|
| 952 | |
|---|
| 953 | } |
|---|