| 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 | |