$registry Optional code-to-name registry. * @return array */ public static function resolve( $locale, $registry = array() ) { $normalized = self::normalize( $locale ); $base = self::base( $normalized ); $variants = self::regional_variants(); $names = array_change_key_case( (array) $registry, CASE_LOWER ); $defaults = self::default_locales(); $english = ''; $native = ''; $known = false; if ( isset( $variants[ $normalized ] ) ) { $english = $variants[ $normalized ]['english_name']; $native = $variants[ $normalized ]['native_name']; $known = true; } elseif ( isset( $names[ strtolower( $normalized ) ] ) ) { $english = (string) $names[ strtolower( $normalized ) ]; $known = true; } elseif ( isset( $names[ $base ] ) ) { $english = (string) $names[ $base ]; $known = true; } if ( '' === $english ) { $english = '' !== $normalized ? strtoupper( $normalized ) : strtoupper( (string) $locale ); } $provider_locale = isset( $defaults[ $base ] ) && false === strpos( $normalized, '-' ) ? $defaults[ $base ] : $normalized; return array( 'input' => (string) $locale, 'locale' => $normalized, 'code' => $base, 'base' => $base, 'bcp47' => $provider_locale, 'english_name' => $english, 'native_name' => '' !== $native ? $native : $english, 'api_locale' => $provider_locale, 'default' => $provider_locale, 'known' => $known ? '1' : '0', ); } public static function translation_instruction( $source_locale, $target_locale, $registry = array() ) { $source = self::resolve( $source_locale, $registry ); $target = self::resolve( $target_locale, $registry ); if ( '1' !== $target['known'] || '' === $target['code'] ) { return ''; } return sprintf( 'Translate each segment from %1$s (%2$s) to %3$s (%4$s). Use natural modern %3$s. Do not transliterate. Do not use another language. Do not leave source-language text untranslated unless it is a brand name, URL, email, placeholder, technical identifier, or code.', $source['english_name'], $source['api_locale'], $target['english_name'], $target['api_locale'] ); } public static function provider_code( $locale, $provider, $source = false ) { $info = self::resolve( $locale ); $provider = sanitize_key( (string) $provider ); $base = $info['base']; if ( in_array( $provider, array( 'google_translate', 'yandex_translate' ), true ) ) { return $base; } if ( 'deepl' === $provider ) { $code = strtoupper( str_replace( '_', '-', $info['api_locale'] ) ); if ( $source ) { return $base ? strtoupper( $base ) : ''; } if ( in_array( $code, array( 'EN-US', 'EN-GB', 'PT-BR', 'PT-PT', 'ZH-HANS', 'ZH-HANT' ), true ) ) { return $code; } return $base ? strtoupper( $base ) : ''; } return $info['api_locale']; } /** * Check whether a provider response uses the expected script. * * Short labels and brand names are allowed to remain in Latin script. The * guard targets longer responses where a wrong provider locale would create * visibly mixed Translation Memory entries. * * @param string $text Provider response. * @param string $target_locale Requested target locale. * @return bool */ public static function matches_target_script( $text, $target_locale ) { $text = trim( wp_strip_all_tags( (string) $text ) ); $base = self::base( $target_locale ); $patterns = array( 'ka' => '/\p{Georgian}/u', 'ru' => '/\p{Cyrillic}/u', 'uk' => '/\p{Cyrillic}/u', 'ar' => '/\p{Arabic}/u', 'he' => '/\p{Hebrew}/u', 'zh' => '/\p{Han}/u', 'ja' => '/[\p{Hiragana}\p{Katakana}\p{Han}]/u', 'ko' => '/\p{Hangul}/u', ); if ( ! isset( $patterns[ $base ] ) ) { return true; } if ( 1 === preg_match( $patterns[ $base ], $text ) ) { return true; } $foreign_scripts = array( 'ka' => '/[\p{Cyrillic}\p{Thai}\p{Arabic}\p{Hebrew}\p{Han}\p{Hiragana}\p{Katakana}\p{Hangul}]/u', 'ru' => '/[\p{Georgian}\p{Thai}\p{Arabic}\p{Hebrew}\p{Han}\p{Hiragana}\p{Katakana}\p{Hangul}]/u', 'uk' => '/[\p{Georgian}\p{Thai}\p{Arabic}\p{Hebrew}\p{Han}\p{Hiragana}\p{Katakana}\p{Hangul}]/u', 'ar' => '/[\p{Georgian}\p{Cyrillic}\p{Thai}\p{Hebrew}\p{Han}\p{Hiragana}\p{Katakana}\p{Hangul}]/u', 'he' => '/[\p{Georgian}\p{Cyrillic}\p{Thai}\p{Arabic}\p{Han}\p{Hiragana}\p{Katakana}\p{Hangul}]/u', 'zh' => '/[\p{Georgian}\p{Cyrillic}\p{Thai}\p{Arabic}\p{Hebrew}\p{Hangul}]/u', 'ja' => '/[\p{Georgian}\p{Cyrillic}\p{Thai}\p{Arabic}\p{Hebrew}\p{Hangul}]/u', 'ko' => '/[\p{Georgian}\p{Cyrillic}\p{Thai}\p{Arabic}\p{Hebrew}\p{Han}\p{Hiragana}\p{Katakana}]/u', ); if ( isset( $foreign_scripts[ $base ] ) && 1 === preg_match( $foreign_scripts[ $base ], $text ) ) { return false; } // Short Latin brand names and technical labels may remain unchanged. return self::text_length( $text ) < 12; } /** * UTF-8-safe length helper without a hard mbstring dependency. * * @param string $text Text. * @return int */ private static function text_length( $text ) { return function_exists( 'mb_strlen' ) ? mb_strlen( (string) $text, 'UTF-8' ) : strlen( (string) $text ); } /** * Default provider locales for languages whose region matters. * * @return array */ public static function default_locales() { return array( 'ar' => 'ar-SA', 'de' => 'de-DE', 'en' => 'en-US', 'es' => 'es-ES', 'fr' => 'fr-FR', 'it' => 'it-IT', 'ja' => 'ja-JP', 'ka' => 'ka-GE', 'ko' => 'ko-KR', 'pt' => 'pt-PT', 'ru' => 'ru-RU', 'uk' => 'uk-UA', 'zh' => 'zh-CN', 'cs' => 'cs-CZ', 'et' => 'et-EE', 'he' => 'he-IL', 'lt' => 'lt-LT', 'lv' => 'lv-LV', 'pl' => 'pl-PL', 'sk' => 'sk-SK', 'tr' => 'tr-TR', ); } /** * Normalize a locale without discarding script or region subtags. * * @param string $locale Locale or language code. * @return string */ public static function normalize( $locale ) { $locale = str_replace( '_', '-', trim( (string) $locale ) ); $parts = array_values( array_filter( explode( '-', $locale ), 'strlen' ) ); if ( empty( $parts ) || ! preg_match( '/^[A-Za-z]{2,3}$/', $parts[0] ) ) { return ''; } $normalized = array( strtolower( $parts[0] ) ); foreach ( array_slice( $parts, 1 ) as $part ) { if ( preg_match( '/^[A-Za-z]{4}$/', $part ) ) { $normalized[] = ucfirst( strtolower( $part ) ); } elseif ( preg_match( '/^(?:[A-Za-z]{2}|[0-9]{3})$/', $part ) ) { $normalized[] = strtoupper( $part ); } else { return ''; } } return implode( '-', $normalized ); } /** * Get the base language. * * @param string $locale Locale. * @return string */ public static function base( $locale ) { $locale = self::normalize( $locale ); $parts = explode( '-', $locale ); return isset( $parts[0] ) ? $parts[0] : ''; } /** * Get explicitly supported regional variants. * * @return array> */ public static function regional_variants() { return array( 'de-DE' => self::item( 'de-DE', 'German (Germany)', 'Deutsch (Deutschland)', 'de', 'DE' ), 'de-AT' => self::item( 'de-AT', 'German (Austria)', 'Deutsch (Österreich)', 'de', 'AT' ), 'de-CH' => self::item( 'de-CH', 'German (Switzerland)', 'Deutsch (Schweiz)', 'de', 'CH' ), 'fr-FR' => self::item( 'fr-FR', 'French (France)', 'Français (France)', 'fr', 'FR' ), 'fr-BE' => self::item( 'fr-BE', 'French (Belgium)', 'Français (Belgique)', 'fr', 'BE' ), 'fr-CA' => self::item( 'fr-CA', 'French (Canada)', 'Français (Canada)', 'fr', 'CA' ), 'fr-CH' => self::item( 'fr-CH', 'French (Switzerland)', 'Français (Suisse)', 'fr', 'CH' ), 'it-IT' => self::item( 'it-IT', 'Italian (Italy)', 'Italiano (Italia)', 'it', 'IT' ), 'it-CH' => self::item( 'it-CH', 'Italian (Switzerland)', 'Italiano (Svizzera)', 'it', 'CH' ), 'en-US' => self::item( 'en-US', 'English (United States)', 'English (United States)', 'en', 'US' ), 'en-GB' => self::item( 'en-GB', 'English (United Kingdom)', 'English (United Kingdom)', 'en', 'GB' ), 'en-CA' => self::item( 'en-CA', 'English (Canada)', 'English (Canada)', 'en', 'CA' ), 'en-AU' => self::item( 'en-AU', 'English (Australia)', 'English (Australia)', 'en', 'AU' ), ); } /** * Return exact-to-base fallback candidates. * * @param string $locale Locale. * @return array */ public static function fallbacks( $locale ) { $locale = self::normalize( $locale ); $base = self::base( $locale ); return array_values( array_unique( array_filter( array( $locale, $base ) ) ) ); } /** * Build metadata item. * * @param string $locale Locale. * @param string $english English name. * @param string $native Native name. * @param string $base Base language. * @param string $region Region. * @return array */ private static function item( $locale, $english, $native, $base, $region ) { return array( 'locale' => $locale, 'base' => $base, 'region' => $region, 'english_name' => $english, 'native_name' => $native, 'slug' => strtolower( $locale ), 'hreflang' => $locale, 'fallback' => $base, 'enabled' => '1', ); } }