'zh_CN', 'zh-TW' => 'zh_TW', 'fr' => 'fr_CA', ); /** * Active component language. * * @var string|null */ private static $language = null; /** * Site language while a component language is active (null: unsupported or no component language). * * @var string|null */ private static $site = null; /** * Bundled translations by English source text (plurals: singular and plural joined by NUL). * * @var array|null */ private static $index = null; /** * Set a component language and return its previous value. * * @param mixed $language Language. * * @return mixed Result or rendered output as documented by the caller. */ public static function language( $language ) { $previous = self::$language; self::$language = $language; self::$site = null === $language ? null : self::site_language(); return $previous; } /** * Map a WordPress locale to a component language. * * @param string $locale WordPress locale such as zh_HK or fr_CA. * @return string|null en, zh, zh-TW or fr; null for other languages. */ public static function locale_language( $locale ) { $locale = (string) $locale; if ( in_array( $locale, array( 'zh_CN', 'zh_SG' ), true ) ) { return 'zh'; } if ( in_array( $locale, array( 'zh_TW', 'zh_HK' ), true ) ) { return 'zh-TW'; } if ( 'fr' === $locale || str_starts_with( $locale, 'fr_' ) ) { return 'fr'; } if ( 'en' === $locale || str_starts_with( $locale, 'en_' ) ) { return 'en'; } return null; } /** * Language WordPress is currently translating into. * * @return string|null Component language of determine_locale(), or null when unsupported. */ public static function site_language() { return self::locale_language( determine_locale() ); } /** * Whether a component language is the one WordPress already translates into. * * @param string $language Component language. * @return bool True when site translations (and $wp_locale names) are already in that language. */ public static function matches_site( $language ) { $site = null !== self::$language && $language === self::$language ? self::$site : self::site_language(); return $language === $site; } /** * Component language that must override WordPress translations, if any. * * @return string|null Language, or null when WordPress translations already match. */ private static function override_language() { return null !== self::$language && self::$language !== self::$site ? self::$language : null; } /** * Translate a module string when its language differs from the site language. * * @param mixed $translation Translation WordPress loaded (community pack), or the English text. * @param mixed $text English source text. * * @return mixed Translation in the component language. */ public static function translate( $translation, $text ) { $language = self::override_language(); if ( null === $language ) { // Same language as the site: a community translation wins; only untranslated text uses the bundled catalog. if ( $translation !== $text ) { return $translation; } $language = self::site_language(); if ( null === $language || 'en' === $language ) { return $translation; } } if ( 'en' === $language ) { return $text; } $value = self::index()[ $text ][ $language ] ?? ''; // Untranslated strings fall back to English rather than to the (different) site language. return is_string( $value ) && '' !== $value ? $value : $text; } /** * Translate a counted module string when its language differs from the site language. * * @param mixed $translation Translation WordPress selected. * @param mixed $single English singular. * @param mixed $plural English plural. * @param mixed $number Count. * @return mixed Translation in the component language. */ public static function translate_plural( $translation, $single, $plural, $number ) { $language = self::override_language(); if ( null === $language ) { // Same language as the site: a community translation wins; only untranslated text uses the bundled catalog. if ( $translation !== $single && $translation !== $plural ) { return $translation; } $language = self::site_language(); if ( null === $language || 'en' === $language ) { return $translation; } } $many = 1 !== (int) $number; $value = 'en' === $language ? '' : ( self::index()[ $single . "\0" . $plural ][ $language ] ?? '' ); if ( is_array( $value ) && 2 === count( $value ) && '' !== $value[0] && '' !== $value[1] ) { // French (the only two-form bundled language): singular for 0 and 1. return (int) $number > 1 ? $value[1] : $value[0]; } if ( is_string( $value ) && '' !== $value ) { return $value; // Chinese has one form. } return $many ? $plural : $single; } /** * Bundled translations indexed by source text, built once per request. * * @return array Map of English text (plural: singular NUL plural) to array( zh, zh-TW, fr ). */ private static function index() { if ( null === self::$index ) { $rows = wp_json_file_decode( AKLA_DIR . 'languages/translations.json', array( 'associative' => true ) ); self::$index = array(); foreach ( is_array( $rows ) ? $rows : array() as $row ) { if ( ! is_array( $row ) || ! isset( $row[0] ) ) { continue; } $key = is_array( $row[0] ) ? implode( "\0", $row[0] ) : (string) $row[0]; self::$index[ $key ] = array( 'zh' => $row[1] ?? '', 'zh-TW' => $row[2] ?? '', 'fr' => $row[3] ?? '', ); } } return self::$index; } /** * Bundled catalog locale for a site locale without its own file (zh_HK → zh_TW, zh_SG → zh_CN, fr_FR → fr_CA). * * @param string $locale WordPress locale. * @return string|null Bundled locale, or null. */ private static function bundled_locale( $locale ) { $language = self::locale_language( $locale ); return null === $language || 'en' === $language ? null : self::CATALOGS[ $language ]; } /** * Use the closest bundled catalog when the site locale has none (filter: load_textdomain_mofile). * * @param mixed $mofile Catalog path WordPress is about to load. * @param mixed $domain Text domain. * @return mixed Catalog path. */ public static function mofile( $mofile, $domain ) { if ( self::DOMAIN !== $domain || ! is_string( $mofile ) || is_readable( $mofile ) ) { return $mofile; } $locale = determine_locale(); $bundled = self::bundled_locale( $locale ); if ( null === $bundled || $bundled === $locale ) { return $mofile; } $candidate = AKLA_DIR . 'languages/' . self::DOMAIN . '-' . $bundled . '.mo'; return is_readable( $candidate ) ? $candidate : $mofile; } /** * Use the closest bundled script catalog when the site locale has none (filter: load_script_translation_file). * * @param mixed $file JSON catalog path. * @param mixed $handle Script handle. * @param mixed $domain Text domain. * @return mixed JSON catalog path. */ public static function script_file( $file, $handle, $domain ) { if ( self::DOMAIN !== $domain || ! is_string( $file ) || is_readable( $file ) ) { return $file; } $locale = determine_locale(); $bundled = self::bundled_locale( $locale ); $prefix = self::DOMAIN . '-' . $locale . '-'; $name = basename( $file ); if ( null === $bundled || $bundled === $locale || ! str_starts_with( $name, $prefix ) ) { return $file; } $candidate = AKLA_DIR . 'languages/' . self::DOMAIN . '-' . $bundled . '-' . substr( $name, strlen( $prefix ) ); return is_readable( $candidate ) ? $candidate : $file; } /** * Translate an existing catalog code; preserve unknown values without guessing. * * @param string $code Source code, retained separately as the submitted value. * @return string Plain label; callers must escape for their output context. */ public static function price_unit( $code ) { $labels = array( 'DROP_IN' => __( 'Single class', 'actikidz-local-activities' ), 'PACKAGE' => __( 'Class package', 'actikidz-local-activities' ), 'MONTHLY' => __( 'Monthly membership', 'actikidz-local-activities' ), 'SEMESTER' => __( 'Semester program', 'actikidz-local-activities' ), 'ANNUAL' => __( 'Annual membership', 'actikidz-local-activities' ), 'VARIES' => __( 'Varies by schedule', 'actikidz-local-activities' ), ); return $labels[ $code ] ?? $code; } }