| @@ -408,9 +408,17 @@ | ||
| 408 | 408 | // Use built assets in production |
| 409 | 409 | $appJs = YATRA_PLUGIN_PATH . 'assets/admin/dist/js/app.js'; |
| 410 | 410 | |
| 411 | 411 | if (file_exists($appJs)) { |
| 412 | - $jsVersion = YATRA_VERSION . '.' . filemtime($appJs) . '.view-icon-fix.' . time() . '.' . microtime(true); | |
| 412 | + // Version on the plugin version + the bundle's own mtime. That | |
| 413 | + // already changes on every update or rebuild, which is exactly | |
| 414 | + // when the cache must be busted. | |
| 415 | + // | |
| 416 | + // This previously appended time() . microtime(true), making the | |
| 417 | + // URL unique on every single request — so the ~3 MB admin bundle | |
| 418 | + // was re-downloaded on every admin page view and could never be | |
| 419 | + // cached by the browser. | |
| 420 | + $jsVersion = YATRA_VERSION . '.' . filemtime($appJs); | |
| 413 | 421 | |
| 414 | 422 | $localized_data = $this->buildAdminLocalizedData(); |
| 415 | 423 | |
| 416 | 424 | // Enqueue our script with media library as dependency |
| @@ -431,8 +439,21 @@ | ||
| 431 | 439 | $jsVersion, |
| 432 | 440 | true |
| 433 | 441 | ); |
| 434 | 442 | |
| 443 | + // The bundle calls the global wp.i18n.__() (it never ships its | |
| 444 | + // own copy), and scripts/extract-js-pot.mjs writes every admin | |
| 445 | + // string's `#:` reference as this bundle's path precisely so | |
| 446 | + // WordPress's md5(handle src) JSON lookup matches. This call is | |
| 447 | + // the missing last link: it tells WordPress to load | |
| 448 | + // i18n/languages/yatra-{locale}-{md5}.json (or the copy under | |
| 449 | + // WP_LANG_DIR/plugins) for the admin UI. Without it, translated | |
| 450 | + // admin strings never reach the SPA. Mirrors FrontendAssetsProvider. | |
| 451 | + if (function_exists('wp_set_script_translations')) { | |
| 452 | + wp_set_script_translations('yatra-admin', 'yatra', YATRA_PLUGIN_PATH . 'i18n/languages'); | |
| 453 | + $this->backfillScriptTranslations('yatra-admin'); | |
| 454 | + } | |
| 455 | + | |
| 435 | 456 | // Localize script data |
| 436 | 457 | wp_localize_script('yatra-admin', 'yatraAdmin', $localized_data); |
| 437 | 458 | |
| 438 | 459 | // Phone dataset for admin displays (flag + dial-code detection of |
| @@ -527,8 +548,119 @@ | ||
| 527 | 548 | * Enqueue setup wizard assets |
| 528 | 549 | * |
| 529 | 550 | * @return void |
| 530 | 551 | */ |
| 552 | + /** | |
| 553 | + * Feed the admin bundle its translations when no JSON file exists. | |
| 554 | + * | |
| 555 | + * wp_set_script_translations() can only read a `yatra-{locale}-{md5}.json` | |
| 556 | + * file. WordPress.org language packs ship one, so translations from | |
| 557 | + * translate.wordpress.org simply work — but a site translated by hand, with | |
| 558 | + * Loco Translate or a .po/.mo dropped into wp-content/languages, has only | |
| 559 | + * the PHP catalogue. For those sites every string in the React admin stayed | |
| 560 | + * in English no matter how complete the translation was, which looked like | |
| 561 | + * the plugin ignoring the translation altogether. | |
| 562 | + * | |
| 563 | + * The strings are the same ones the PHP catalogue already holds, so they are | |
| 564 | + * handed to wp.i18n directly. Core is asked first and left in charge | |
| 565 | + * whenever it can find a JSON file: that path is cached by the browser as a | |
| 566 | + * separate request, and this one is not. | |
| 567 | + * | |
| 568 | + * Nothing is emitted on an untranslated site — the map comes back empty and | |
| 569 | + * English sites carry no extra weight. | |
| 570 | + */ | |
| 571 | + private function backfillScriptTranslations(string $handle): void | |
| 572 | + { | |
| 573 | + if (!function_exists('load_script_textdomain')) { | |
| 574 | + return; | |
| 575 | + } | |
| 576 | + | |
| 577 | + // A real JSON file beats this: let WordPress load it as it normally would. | |
| 578 | + if (load_script_textdomain($handle, 'yatra', YATRA_PLUGIN_PATH . 'i18n/languages')) { | |
| 579 | + return; | |
| 580 | + } | |
| 581 | + | |
| 582 | + $localeData = $this->localeDataFromTextdomain('yatra'); | |
| 583 | + if ($localeData === []) { | |
| 584 | + return; | |
| 585 | + } | |
| 586 | + | |
| 587 | + wp_add_inline_script( | |
| 588 | + $handle, | |
| 589 | + 'wp.i18n.setLocaleData(' . wp_json_encode($localeData) . ', "yatra");', | |
| 590 | + 'before' | |
| 591 | + ); | |
| 592 | + } | |
| 593 | + | |
| 594 | + /** | |
| 595 | + * The loaded PHP catalogue, in the shape wp.i18n.setLocaleData() expects. | |
| 596 | + * | |
| 597 | + * Entries that were never translated are left out: they would only restate | |
| 598 | + * the English the bundle already carries. Contexts use the same NUL | |
| 599 | + * separator Jed and gettext use, so _x() resolves too. | |
| 600 | + * | |
| 601 | + * @return array<string, mixed> | |
| 602 | + */ | |
| 603 | + private function localeDataFromTextdomain(string $domain): array | |
| 604 | + { | |
| 605 | + $translations = get_translations_for_domain($domain); | |
| 606 | + if (!is_object($translations)) { | |
| 607 | + return []; | |
| 608 | + } | |
| 609 | + // Deliberately duck-typed, and deliberately not isset()/??. WordPress | |
| 610 | + // returns Translations, NOOP_Translations or — since the performant | |
| 611 | + // translations work — WP_Translations, which neither extends | |
| 612 | + // Translations nor declares `entries`: it serves that property through | |
| 613 | + // __get() and defines no __isset(), so both instanceof and isset() | |
| 614 | + // report nothing is there and quietly disable this fallback. Only a | |
| 615 | + // direct read reaches the magic getter. | |
| 616 | + if (!property_exists($translations, 'entries') && !method_exists($translations, '__get')) { | |
| 617 | + return []; | |
| 618 | + } | |
| 619 | + | |
| 620 | + $entries = $translations->entries; | |
| 621 | + if (!is_array($entries) || $entries === []) { | |
| 622 | + return []; | |
| 623 | + } | |
| 624 | + | |
| 625 | + $data = []; | |
| 626 | + foreach ($entries as $entry) { | |
| 627 | + if (!is_object($entry) || (string) $entry->singular === '') { | |
| 628 | + continue; | |
| 629 | + } | |
| 630 | + | |
| 631 | + $forms = array_values(array_filter( | |
| 632 | + (array) $entry->translations, | |
| 633 | + static fn($t) => is_string($t) && $t !== '' | |
| 634 | + )); | |
| 635 | + if ($forms === []) { | |
| 636 | + continue; | |
| 637 | + } | |
| 638 | + // Untranslated entries come back as the original string. | |
| 639 | + if (count($forms) === 1 && $forms[0] === $entry->singular) { | |
| 640 | + continue; | |
| 641 | + } | |
| 642 | + | |
| 643 | + $key = ($entry->context !== null && $entry->context !== '') | |
| 644 | + ? $entry->context . "\u{0004}" . $entry->singular | |
| 645 | + : $entry->singular; | |
| 646 | + | |
| 647 | + $data[$key] = $forms; | |
| 648 | + } | |
| 649 | + | |
| 650 | + if ($data === []) { | |
| 651 | + return []; | |
| 652 | + } | |
| 653 | + | |
| 654 | + $data[''] = [ | |
| 655 | + 'domain' => $domain, | |
| 656 | + 'lang' => determine_locale(), | |
| 657 | + 'plural-forms' => 'nplurals=2; plural=(n != 1);', | |
| 658 | + ]; | |
| 659 | + | |
| 660 | + return $data; | |
| 661 | + } | |
| 662 | + | |
| 531 | 663 | public function enqueueSetupWizardAssets(): void |
| 532 | 664 | { |
| 533 | 665 | // Enqueue setup wizard CSS |
| 534 | 666 | $cssPath = YATRA_PLUGIN_PATH . 'assets/admin/css/setup-wizard.css'; |