| | 1 | <?php |
| | 2 | namespace WordPressdotorg\Plugin_Directory; |
| | 3 | |
| | 4 | /** |
| | 5 | * Translation for plugin content. |
| | 6 | * |
| | 7 | * @package WordPressdotorg\Plugin_Directory |
| | 8 | */ |
| | 9 | class Plugin_I18n { |
| | 10 | |
| | 11 | /** |
| | 12 | * @var string Global cache group for related caching |
| | 13 | */ |
| | 14 | var $i18n_cache_group = 'plugins-i18n'; |
| | 15 | |
| | 16 | public static $use_cache = true; |
| | 17 | public static $set_cache = true; |
| | 18 | |
| | 19 | /** |
| | 20 | * Fetch the instance of the Plugin_I18n class. |
| | 21 | */ |
| | 22 | public static function instance() { |
| | 23 | static $instance = null; |
| | 24 | |
| | 25 | global $wpdb; |
| | 26 | return ! is_null( $instance ) ? $instance : $instance = new Plugin_I18n( $wpdb ); |
| | 27 | } |
| | 28 | |
| | 29 | function __construct( $db, $tracker = null ) { |
| | 30 | if ( !empty( $db ) && is_object( $db ) ) { |
| | 31 | $this->db = $db; |
| | 32 | } |
| | 33 | |
| | 34 | wp_cache_add_global_groups( $this->i18n_cache_group ); |
| | 35 | } |
| | 36 | |
| | 37 | function wp_get_locale() { |
| | 38 | // Similar to bb_get_locale() |
| | 39 | if ( defined( WP_LANG ) ) |
| | 40 | return WP_LANG; |
| | 41 | global $locale; |
| | 42 | return $locale; |
| | 43 | } |
| | 44 | |
| | 45 | /** |
| | 46 | * Generates and returns a standard cache key format, for consistency |
| | 47 | * |
| | 48 | * @param string $slug Plugin slug |
| | 49 | * @param string $branch dev|stable |
| | 50 | * @param string $suffix Arbitrary cache key suffix, if needed for uniqueness |
| | 51 | * |
| | 52 | * @return string Cache key |
| | 53 | */ |
| | 54 | function cache_key( $slug, $branch, $suffix = null ) { |
| | 55 | // EG keys |
| | 56 | // plugin:press-this:stable-readme:originals |
| | 57 | // plugin:press-this:stable-readme:original:title |
| | 58 | // plugin:press-this:stable-readme:fr:title |
| | 59 | $key = "{$this->master_project}:{$slug}:{$branch}"; |
| | 60 | if ( !empty( $suffix ) ) { |
| | 61 | $key .= ":{$suffix}"; |
| | 62 | } |
| | 63 | return $key; |
| | 64 | } |
| | 65 | |
| | 66 | /** |
| | 67 | * Cache getting, with proper global cache group |
| | 68 | * |
| | 69 | * @param string $slug Plugin slug |
| | 70 | * @param string $branch dev|stable |
| | 71 | * @param string $suffix Arbitrary cache key suffix, if needed for uniqueness |
| | 72 | * |
| | 73 | * @return bool|mixed As returned by wp_cache_set() |
| | 74 | */ |
| | 75 | function cache_get( $slug, $branch, $suffix = null ) { |
| | 76 | if ( ! self::$use_cache ) { |
| | 77 | return false; |
| | 78 | } |
| | 79 | |
| | 80 | $key = $this->cache_key( $slug, $branch, $suffix ); |
| | 81 | return wp_cache_get( $key, $this->i18n_cache_group ); |
| | 82 | } |
| | 83 | |
| | 84 | /** |
| | 85 | * Cache setting, with proper global cache group |
| | 86 | * |
| | 87 | * @param string $slug Plugin slug |
| | 88 | * @param string $branch dev|stable |
| | 89 | * @param string $suffix Arbitrary cache key suffix, if needed for uniqueness |
| | 90 | * |
| | 91 | * @return bool As returned by wp_cache_set() |
| | 92 | */ |
| | 93 | function cache_set( $slug, $branch, $content, $suffix = null ) { |
| | 94 | if ( ! self::$set_cache ) { |
| | 95 | return false; |
| | 96 | } |
| | 97 | |
| | 98 | $key = $this->cache_key( $slug, $branch, $suffix ); |
| | 99 | return wp_cache_set( $key, $content, $this->i18n_cache_group ); |
| | 100 | } |
| | 101 | |
| | 102 | /** |
| | 103 | * Gets a GlotPress branch ID |
| | 104 | * |
| | 105 | * @param string $slug Plugin slug |
| | 106 | * @param string $branch dev|stable |
| | 107 | * |
| | 108 | * @return bool|int|mixed |
| | 109 | */ |
| | 110 | function get_gp_branch_id( $slug, $branch ) { |
| | 111 | $cache_suffix = "branch_id"; |
| | 112 | |
| | 113 | if ( false !== ( $branch_id = $this->cache_get( $slug, $branch, $cache_suffix ) ) ) { |
| | 114 | return $branch_id; |
| | 115 | } |
| | 116 | |
| | 117 | $branch_id = $this->db->get_var( $this->db->prepare( |
| | 118 | 'SELECT id FROM translate_projects WHERE path = %s', |
| | 119 | "wp-plugins/{$slug}/{$branch}" |
| | 120 | ) ); |
| | 121 | |
| | 122 | if ( empty( $branch_id ) ) { |
| | 123 | $branch_id = 0; |
| | 124 | } |
| | 125 | |
| | 126 | $this->cache_set( $slug, $branch, $branch_id, $cache_suffix ); |
| | 127 | |
| | 128 | return $branch_id; |
| | 129 | } |
| | 130 | |
| | 131 | /** |
| | 132 | * Gets GlotPress "originals" based on passed parameters |
| | 133 | * |
| | 134 | * @param string $slug Plugin slug |
| | 135 | * @param string $branch dev|stable |
| | 136 | * @param string $key Unique key |
| | 137 | * @param string $str String to match in GP |
| | 138 | * |
| | 139 | * @return array|bool|mixed|null |
| | 140 | */ |
| | 141 | function get_gp_originals( $slug, $branch, $key, $str ) { |
| | 142 | // Try to get a single original with the whole content first (title, etc), if passed, or get them all otherwise. |
| | 143 | if ( !empty( $key ) && !empty( $str ) ) { |
| | 144 | $originals = $this->search_gp_original( $slug, $branch, $key, $str ); |
| | 145 | if ( !empty( $originals ) ) { |
| | 146 | return array( $originals ); |
| | 147 | } |
| | 148 | // Do not cache this as originals, search_gp_original() does its own caching |
| | 149 | } |
| | 150 | |
| | 151 | $cache_suffix = 'originals'; |
| | 152 | |
| | 153 | if ( false !== ( $originals = $this->cache_get( $slug, $branch, $cache_suffix ) ) ) { |
| | 154 | return $originals; |
| | 155 | } |
| | 156 | |
| | 157 | $branch_id = $this->get_gp_branch_id( $slug, $branch ); |
| | 158 | |
| | 159 | if ( empty( $branch_id ) ) { |
| | 160 | return array(); |
| | 161 | } |
| | 162 | |
| | 163 | $originals = $this->db->get_results( $this->db->prepare( |
| | 164 | 'SELECT id, singular, comment FROM translate_originals WHERE project_id = %d AND status = %s ORDER BY CHAR_LENGTH(singular) DESC', |
| | 165 | $branch_id, '+active' |
| | 166 | ) ); |
| | 167 | |
| | 168 | if ( empty( $originals ) ) { |
| | 169 | $originals = array(); // still cache if empty, but as array, never false |
| | 170 | } |
| | 171 | |
| | 172 | $this->cache_set( $slug, $branch, $originals, $cache_suffix ); |
| | 173 | |
| | 174 | return $originals; |
| | 175 | } |
| | 176 | |
| | 177 | /** |
| | 178 | * Get GlotPress translation set ID based on passed params |
| | 179 | * |
| | 180 | * @param string $slug Plugin slug |
| | 181 | * @param string $branch dev|stable |
| | 182 | * @param string $locale EG: fr |
| | 183 | * |
| | 184 | * @return bool|int|mixed |
| | 185 | */ |
| | 186 | function get_gp_translation_set_id( $slug, $branch, $locale ) { |
| | 187 | $cache_suffix = "{$locale}:translation_set_id"; |
| | 188 | |
| | 189 | if ( false !== ( $translation_set_id = $this->cache_get( $slug, $branch, $cache_suffix ) ) ) { |
| | 190 | return $translation_set_id; |
| | 191 | } |
| | 192 | |
| | 193 | $branch_id = $this->get_gp_branch_id( $slug, $branch ); |
| | 194 | |
| | 195 | if ( empty( $branch_id ) ) { |
| | 196 | return 0; |
| | 197 | } |
| | 198 | |
| | 199 | $translation_set_id = $this->db->get_var( $this->db->prepare( |
| | 200 | 'SELECT id FROM translate_translation_sets WHERE project_id = %d AND locale = %s', |
| | 201 | $branch_id, $locale ) ); |
| | 202 | |
| | 203 | if ( empty( $translation_set_id ) ) { |
| | 204 | // Don't give up yet. Might be given fr_FR, which actually exists as locale=fr in GP. |
| | 205 | $translation_set_id = $this->db->get_var( $this->db->prepare( |
| | 206 | 'SELECT id FROM translate_translation_sets WHERE project_id = %d AND locale = %s', |
| | 207 | $branch_id, preg_replace( '/^([^-]+)(-.+)?$/', '\1', $locale ) ) ); |
| | 208 | } |
| | 209 | |
| | 210 | if ( empty( $translation_set_id ) ) { |
| | 211 | $translation_set_id = 0; |
| | 212 | } |
| | 213 | |
| | 214 | $this->cache_set( $slug, $branch, $translation_set_id, $cache_suffix ); |
| | 215 | |
| | 216 | return $translation_set_id; |
| | 217 | } |
| | 218 | |
| | 219 | /** |
| | 220 | * Searches GlotPress "originals" for the passed string |
| | 221 | * |
| | 222 | * @param string $slug Plugin slug |
| | 223 | * @param string $branch dev|stable |
| | 224 | * @param string $key Unique key |
| | 225 | * @param string $str String to be searched for |
| | 226 | * |
| | 227 | * @return bool|mixed|null |
| | 228 | */ |
| | 229 | function search_gp_original( $slug, $branch, $key, $str ) { |
| | 230 | $cache_suffix = "original:{$key}"; |
| | 231 | |
| | 232 | if ( false !== ( $original = $this->cache_get( $slug, $branch, $cache_suffix ) ) ) { |
| | 233 | return $original; |
| | 234 | } |
| | 235 | |
| | 236 | $branch_id = $this->get_gp_branch_id( $slug, $branch ); |
| | 237 | |
| | 238 | if ( empty( $branch_id ) ) { |
| | 239 | return false; |
| | 240 | } |
| | 241 | |
| | 242 | $original = $this->db->get_row( $this->db->prepare( |
| | 243 | 'SELECT id, singular, comment FROM translate_originals WHERE project_id = %d AND status = %s AND singular = %s', |
| | 244 | $branch_id, '+active', $str |
| | 245 | ) ); |
| | 246 | |
| | 247 | if ( empty( $original ) ) { |
| | 248 | $original = null; |
| | 249 | } |
| | 250 | |
| | 251 | $this->cache_set( $slug, $branch, $original, $cache_suffix ); |
| | 252 | |
| | 253 | return $original; |
| | 254 | } |
| | 255 | |
| | 256 | /** |
| | 257 | * Somewhat emulated equivalent of __() for content translation drawn directly from the GlotPress DB |
| | 258 | * |
| | 259 | * @param string $key Unique key, used for caching |
| | 260 | * @param string $content Content to be translated |
| | 261 | * @param array $args Misc arguments, such as BBPress topic id (otherwise acquired from global $topic_id) |
| | 262 | * |
| | 263 | * @return mixed |
| | 264 | */ |
| | 265 | function translate( $key, $content, $args = array() ) { |
| | 266 | if ( empty( $key ) || empty( $content ) ) { |
| | 267 | return $content; |
| | 268 | } |
| | 269 | |
| | 270 | if ( !empty( $args['post_id'] ) && is_numeric( $args['post_id'] ) ) { |
| | 271 | $topic = get_post( $args['post_id'] ); |
| | 272 | } else { |
| | 273 | global $post; |
| | 274 | } |
| | 275 | |
| | 276 | if ( empty( $post ) ) { |
| | 277 | return $content; |
| | 278 | } |
| | 279 | |
| | 280 | if ( !empty( $args['locale'] ) ) { |
| | 281 | $wp_locale = $args['locale']; |
| | 282 | } else { |
| | 283 | $wp_locale = $this->wp_get_locale(); |
| | 284 | } |
| | 285 | |
| | 286 | $server_name = strtolower( $_SERVER['SERVER_NAME'] ); |
| | 287 | if ( 'api.wordpress.org' == $server_name ) { |
| | 288 | // Support formats like fr, haz, and en_GB |
| | 289 | if ( ! empty( $_REQUEST['locale'] ) ) { |
| | 290 | $wp_locale = preg_replace( '/[^a-zA-Z_]/', '', $_REQUEST['locale'] ); |
| | 291 | } else if ( ! empty( $_REQUEST['request'] ) ) { |
| | 292 | $request = maybe_unserialize( $_REQUEST['request'] ); |
| | 293 | if ( ! empty( $request ) && ! empty( $request->locale ) ) { |
| | 294 | $wp_locale = preg_replace( '/[^a-zA-Z_]/', '', $request->locale ); |
| | 295 | } |
| | 296 | } |
| | 297 | } |
| | 298 | |
| | 299 | if ( ! $wp_locale ) { |
| | 300 | return $content; |
| | 301 | } |
| | 302 | |
| | 303 | require_once GLOTPRESS_LOCALES_PATH; |
| | 304 | $gp_locale = \GP_Locales::by_field( 'wp_locale', $wp_locale ); |
| | 305 | |
| | 306 | if ( ! $gp_locale || 'en' === $gp_locale->slug ) { |
| | 307 | return $content; |
| | 308 | } |
| | 309 | |
| | 310 | $locale = $gp_locale->slug; // The slug is the locale of a translation set. |
| | 311 | $slug = $post->post_name; |
| | 312 | |
| | 313 | $post->stable_tag = get_post_meta( $post->ID, 'stable_tag', true ); |
| | 314 | |
| | 315 | if ( empty( $slug ) ) { |
| | 316 | return $content; |
| | 317 | } |
| | 318 | |
| | 319 | $branch = ( empty( $post->stable_tag ) || 'trunk' === $post->stable_tag ) ? 'dev' : 'stable'; |
| | 320 | |
| | 321 | if ( empty( $args['code_i18n'] ) || true !== $args['code_i18n'] ) { |
| | 322 | $branch .= '-readme'; |
| | 323 | } |
| | 324 | |
| | 325 | $cache_suffix = "{$locale}:{$key}"; |
| | 326 | |
| | 327 | // Try the cache |
| | 328 | if ( false !== ( $cache = $this->cache_get( $slug, $branch, $cache_suffix ) ) ) { |
| | 329 | // DEBUG |
| | 330 | // var_dump( array( $slug, $branch, $cache_suffix, $cache ) ); |
| | 331 | return $cache; |
| | 332 | } |
| | 333 | |
| | 334 | $originals = $this->get_gp_originals( $slug, $branch, $key, $content ); |
| | 335 | |
| | 336 | if ( empty( $originals ) ) { |
| | 337 | return $content; |
| | 338 | } |
| | 339 | |
| | 340 | $translation_set_id = $this->get_gp_translation_set_id( $slug, $branch, $locale ); |
| | 341 | |
| | 342 | if ( empty( $translation_set_id ) ) { |
| | 343 | return $content; |
| | 344 | } |
| | 345 | |
| | 346 | foreach ( $originals as $original ) { |
| | 347 | if ( empty( $original->id ) ) { |
| | 348 | continue; |
| | 349 | } |
| | 350 | |
| | 351 | $translation = $this->db->get_var( $this->db->prepare( |
| | 352 | 'SELECT translation_0 FROM translate_translations WHERE original_id = %d AND translation_set_id = %d AND status = %s', |
| | 353 | $original->id, $translation_set_id, 'current' |
| | 354 | ) ); |
| | 355 | |
| | 356 | if ( empty( $translation ) ) { |
| | 357 | continue; |
| | 358 | } |
| | 359 | |
| | 360 | $content = $this->translate_gp_original( $original->singular, $translation, $content ); |
| | 361 | } |
| | 362 | |
| | 363 | $this->cache_set( $slug, $branch, $content, $cache_suffix ); |
| | 364 | |
| | 365 | return $content; |
| | 366 | } |
| | 367 | |
| | 368 | /** |
| | 369 | * Takes content, searches for $original, and replaces it by $translation |
| | 370 | * |
| | 371 | * @param string $original English string |
| | 372 | * @param string $translation Translation |
| | 373 | * @param string $content Content to be searched |
| | 374 | * |
| | 375 | * @return mixed |
| | 376 | */ |
| | 377 | function translate_gp_original( $original, $translation, $content ) { |
| | 378 | if ( false === strpos( $content, '<' ) ) { |
| | 379 | $content = str_replace( $original, $translation, $content ); |
| | 380 | } else { |
| | 381 | $original = preg_quote( $original, '/' ); |
| | 382 | $content = preg_replace( "/(<([a-z0-9]*)\b[^>]*>){$original}(<\/\\2>)/m", "\\1{$translation}\\3", $content ); |
| | 383 | } |
| | 384 | |
| | 385 | return $content; |
| | 386 | } |
| | 387 | |
| | 388 | } |
| | 389 | |