| @@ -79,11 +79,22 @@ | ||
| 79 | 79 | * @param string|null $version Optional version override. |
| 80 | 80 | * @param string|null $languages_path Optional languages path override. |
| 81 | 81 | */ |
| 82 | 82 | public function __construct( ?string $text_domain = null, ?string $version = null, ?string $languages_path = null ) { |
| 83 | - $this->text_domain = $text_domain ?? 'woocommerce-pos'; | |
| 84 | - $this->version = $version ?? TRANSLATION_VERSION; | |
| 85 | - $this->transient_key = 'wcpos_i18n_' . $this->text_domain; | |
| 83 | + $this->text_domain = $text_domain ?? 'woocommerce-pos'; | |
| 84 | + $this->version = $version ?? TRANSLATION_VERSION; | |
| 85 | + $this->transient_key = 'wcpos_i18n_' . $this->text_domain; | |
| 86 | + | |
| 87 | + if ( ! $this->maintain() ) { | |
| 88 | + // A shopper's page only needs the file already on disk. Resolving the | |
| 89 | + // active path, checking versions, downloading and repairing are | |
| 90 | + // maintenance work (see is_maintenance_request()) — and each of them | |
| 91 | + // cost a query on EVERY storefront request without an object cache. | |
| 92 | + $this->languages_path = $languages_path ?? WP_LANG_DIR . '/plugins/'; | |
| 93 | + $this->load_existing_translation( null === $languages_path ); | |
| 94 | + return; | |
| 95 | + } | |
| 96 | + | |
| 86 | 97 | $this->languages_path = $languages_path ?? $this->resolve_languages_path(); |
| 87 | 98 | |
| 88 | 99 | $this->load_translations(); |
| 89 | 100 | } |
| @@ -88,8 +99,95 @@ | ||
| 88 | 99 | $this->load_translations(); |
| 89 | 100 | } |
| 90 | 101 | |
| 91 | 102 | /** |
| 103 | + * Whether this request keeps the translation files fresh, or only reads them. | |
| 104 | + * | |
| 105 | + * Admin, POS, REST (the Store API included), cron and WP-CLI requests | |
| 106 | + * maintain: they check the version markers and download. A plain storefront | |
| 107 | + * request loads whatever file exists and touches nothing else. | |
| 108 | + * | |
| 109 | + * Detection runs on `init`, before `REST_REQUEST` is defined and before the | |
| 110 | + * rewrite rules populate the POS query vars, so REST and the browser-loaded | |
| 111 | + * POS routes (`/<slug>/`, `/wcpos-auth/`, `/wcpos-checkout/…`) are matched | |
| 112 | + * from the request path. `wc-ajax` (add to cart, cart fragments, the classic | |
| 113 | + * checkout) is shopper traffic: WooCommerce marks it DOING_AJAX, which is | |
| 114 | + * why `wp_doing_ajax()` is deliberately not consulted — admin-ajax.php is | |
| 115 | + * already covered by `is_admin()`. | |
| 116 | + * | |
| 117 | + * @return bool | |
| 118 | + */ | |
| 119 | + public static function is_maintenance_request(): bool { | |
| 120 | + // The lane classifier owns the detection (it moved there when the same | |
| 121 | + // question started deciding which services a request constructs). | |
| 122 | + return ! Services\Request_Lane::is_storefront(); | |
| 123 | + } | |
| 124 | + | |
| 125 | + /** | |
| 126 | + * The maintenance decision, filterable so tests and hosts can force either path. | |
| 127 | + * | |
| 128 | + * @return bool | |
| 129 | + */ | |
| 130 | + protected function maintain(): bool { | |
| 131 | + /** | |
| 132 | + * Filters whether this request maintains the translation files (version | |
| 133 | + * checks and downloads) or only loads an existing one. | |
| 134 | + * | |
| 135 | + * @param bool $maintain Default from {@see i18n::is_maintenance_request()}. | |
| 136 | + */ | |
| 137 | + return (bool) apply_filters( 'woocommerce_pos_i18n_maintain', self::is_maintenance_request() ); | |
| 138 | + } | |
| 139 | + | |
| 140 | + /** | |
| 141 | + * Load the first existing translation file for the locale candidates, reading nothing else. | |
| 142 | + * | |
| 143 | + * @param bool $also_uploads Whether to also look in the uploads fallback directory | |
| 144 | + * (the default when no explicit path was given). | |
| 145 | + */ | |
| 146 | + protected function load_existing_translation( bool $also_uploads ): void { | |
| 147 | + $locale = determine_locale(); | |
| 148 | + if ( 'en_US' === $locale || empty( $locale ) ) { | |
| 149 | + return; | |
| 150 | + } | |
| 151 | + $primary = $this->languages_path; | |
| 152 | + $fallback = null; // Resolved lazily: wp_upload_dir() is not free. | |
| 153 | + foreach ( $this->get_locale_candidates( $locale ) as $candidate_locale ) { | |
| 154 | + $name = $this->text_domain . '-' . $candidate_locale . '.l10n.php'; | |
| 155 | + $primary_file = $primary . $name; | |
| 156 | + if ( $also_uploads && file_exists( $primary_file ) ) { | |
| 157 | + $fallback = $fallback ?? $this->get_fallback_languages_path(); | |
| 158 | + $fallback_file = $fallback . $name; | |
| 159 | + if ( file_exists( $fallback_file ) && 'uploads' === get_transient( $this->transient_key . '_active_path' ) ) { | |
| 160 | + $this->languages_path = $fallback; | |
| 161 | + if ( $this->load_translation_file( $candidate_locale, $fallback_file, false ) ) { | |
| 162 | + return; | |
| 163 | + } | |
| 164 | + } | |
| 165 | + } | |
| 166 | + if ( file_exists( $primary_file ) ) { | |
| 167 | + $this->languages_path = $primary; | |
| 168 | + if ( $this->load_translation_file( $candidate_locale, $primary_file, false ) ) { | |
| 169 | + return; | |
| 170 | + } | |
| 171 | + } | |
| 172 | + if ( ! $also_uploads ) { | |
| 173 | + continue; | |
| 174 | + } | |
| 175 | + if ( null === $fallback ) { | |
| 176 | + $fallback = $this->get_fallback_languages_path(); | |
| 177 | + } | |
| 178 | + if ( file_exists( $fallback . $name ) ) { | |
| 179 | + // load_translation_file() derives the .mo path WordPress expects from | |
| 180 | + // languages_path, so it must point at the directory the file is in. | |
| 181 | + $this->languages_path = $fallback; | |
| 182 | + if ( $this->load_translation_file( $candidate_locale, $fallback . $name, false ) ) { | |
| 183 | + return; | |
| 184 | + } | |
| 185 | + } | |
| 186 | + } | |
| 187 | + } | |
| 188 | + | |
| 189 | + /** | |
| 92 | 190 | * Load translations directly from plugin's languages folder. |
| 93 | 191 | * Downloads from jsDelivr if not cached or version changed. |
| 94 | 192 | */ |
| 95 | 193 | protected function load_translations(): void { |
| @@ -222,10 +320,30 @@ | ||
| 222 | 320 | * Load an existing translation file. |
| 223 | 321 | * |
| 224 | 322 | * @param string $locale Locale code for the file. |
| 225 | 323 | * @param string $file Path to the l10n PHP file. |
| 324 | + * @param bool $repair Whether a flat-format file may be converted and a corrupt one | |
| 325 | + * deleted (maintenance requests). The storefront path passes | |
| 326 | + * false and only loads a file that is already valid. | |
| 327 | + * | |
| 328 | + * @return bool Whether the translation file was loaded. | |
| 226 | 329 | */ |
| 227 | - protected function load_translation_file( string $locale, string $file ): void { | |
| 330 | + protected function load_translation_file( string $locale, string $file, bool $repair = true ): bool { | |
| 331 | + if ( ! $repair ) { | |
| 332 | + // Storefront path: read-only. A file in the old flat format or one | |
| 333 | + // that does not parse is simply not loaded; the next maintenance | |
| 334 | + // request converts or replaces it. | |
| 335 | + try { | |
| 336 | + $data = include $file; | |
| 337 | + } catch ( \ParseError $e ) { | |
| 338 | + return false; | |
| 339 | + } | |
| 340 | + if ( ! \is_array( $data ) || ! isset( $data['messages'] ) ) { | |
| 341 | + return false; | |
| 342 | + } | |
| 343 | + return load_textdomain( $this->text_domain, $this->languages_path . $this->text_domain . '-' . $locale . '.mo' ); | |
| 344 | + } | |
| 345 | + | |
| 228 | 346 | try { |
| 229 | 347 | $this->maybe_convert_file_format( $file ); |
| 230 | 348 | } catch ( \ParseError $e ) { |
| 231 | 349 | // File is corrupt — delete it and clear the version transient so it re-downloads. |
| @@ -232,14 +350,14 @@ | ||
| 232 | 350 | Logger::log( sprintf( 'i18n: Corrupt translation file deleted (%s): %s', $file, $e->getMessage() ) ); |
| 233 | 351 | wp_delete_file( $file ); |
| 234 | 352 | delete_transient( $this->transient_key . '_' . $locale ); |
| 235 | 353 | |
| 236 | - return; | |
| 354 | + return false; | |
| 237 | 355 | } |
| 238 | 356 | |
| 239 | 357 | // Pass the .mo path — WordPress internally looks for .l10n.php first. |
| 240 | 358 | $mofile = $this->languages_path . $this->text_domain . '-' . $locale . '.mo'; |
| 241 | - load_textdomain( $this->text_domain, $mofile ); | |
| 359 | + return load_textdomain( $this->text_domain, $mofile ); | |
| 242 | 360 | } |
| 243 | 361 | |
| 244 | 362 | /** |
| 245 | 363 | * Build the transient key used for missing-locale caching. |