( $start + $offset ) % 7, range( 0, 6 ) ); } public static function week_start_date( \DateTimeImmutable $date ): \DateTimeImmutable { return $date->modify( '-' . ( ( (int) $date->format( 'w' ) - self::week_start() + 7 ) % 7 ) . ' days' ); } /** Preserve WordPress date formats, including escapes, and localize override names. */ public static function date( string $format, ?int $timestamp = null, ?\DateTimeZone $zone = null ): string { $zone = $zone ?: wp_timezone(); $timestamp = $timestamp ?? time(); // An explicit override must use its own names even when the ambient locale // matches: WordPress may still have an English WP_Locale without a core pack. if ( '' === Settings::configured_locale() ) { return (string) wp_date( $format, $timestamp, $zone ); } $date = ( new \DateTimeImmutable( '@' . $timestamp ) )->setTimezone( $zone ); if ( self::intl( 'IntlDateFormatter' ) ) { try { $icu_zone = preg_match( '/^[+-]\d{2}:\d{2}$/D', $zone->getName() ) ? 'GMT' . $zone->getName() : $zone->getName(); // Standalone month labels differ from date phrases in Polish, Czech and Finnish. $has_day = preg_match( '/[dj]/', preg_replace( '/\\\\./', '', $format ) ); $tokens = array( 'Y' => 'yyyy', 'y' => 'yy', 'm' => 'MM', 'n' => 'M', 'd' => 'dd', 'j' => 'd', 'D' => 'EEE', 'l' => 'EEEE', 'F' => $has_day ? 'MMMM' : 'LLLL', 'M' => $has_day ? 'MMM' : 'LLL', 'H' => 'HH', 'G' => 'H', 'h' => 'hh', 'g' => 'h', 'i' => 'mm', 's' => 'ss', 'a' => 'a', 'A' => 'a' ); $pattern = ''; $literal_buffer = ''; for ( $i = 0, $length = strlen( $format ); $i < $length; ++$i ) { $c = $format[ $i ]; if ( '\\' === $c && $i + 1 < $length ) { $literal = $format[ ++$i ]; } elseif ( 'a' === $c || 'A' === $c ) { $period = new \IntlDateFormatter( self::locale() . '@numbers=latn', \IntlDateFormatter::NONE, \IntlDateFormatter::NONE, $icu_zone, \IntlDateFormatter::GREGORIAN, 'a' ); $text = (string) $period->format( $timestamp ); $literal = 'a' === $c ? ( function_exists( 'mb_strtolower' ) ? mb_strtolower( $text, 'UTF-8' ) : strtolower( $text ) ) : ( function_exists( 'mb_strtoupper' ) ? mb_strtoupper( $text, 'UTF-8' ) : strtoupper( $text ) ); } elseif ( isset( $tokens[ $c ] ) ) { if ( '' !== $literal_buffer ) { $pattern .= "'" . str_replace( "'", "''", $literal_buffer ) . "'"; $literal_buffer = ''; } $pattern .= $tokens[ $c ]; continue; } else { $literal = ctype_alpha( $c ) ? $date->format( $c ) : $c; } $literal_buffer .= $literal; } if ( '' !== $literal_buffer ) { $pattern .= "'" . str_replace( "'", "''", $literal_buffer ) . "'"; } $formatter = new \IntlDateFormatter( self::locale() . '@numbers=latn', \IntlDateFormatter::NONE, \IntlDateFormatter::NONE, $icu_zone, \IntlDateFormatter::GREGORIAN, $pattern ); $value = $formatter->format( $timestamp ); if ( false !== $value ) { return $value; } } catch ( \Throwable $error ) { /* Local gettext fallback needs no PHP extension. */ } } $months = self::months(); $weekdays = self::weekdays(); $short_months = self::short_months(); $short_days = self::short_weekdays(); $value = ''; if ( preg_match( '/[dj]/', preg_replace( '/\\\\./', '', $format ) ) ) { $months = self::day_months(); } for ( $i = 0, $length = strlen( $format ); $i < $length; ++$i ) { $c = $format[ $i ]; if ( '\\' === $c && $i + 1 < $length ) { $value .= $format[ ++$i ]; continue; } if ( 'F' === $c ) { $value .= $months[ (int) $date->format( 'n' ) - 1 ]; } elseif ( 'l' === $c ) { $value .= $weekdays[ (int) $date->format( 'w' ) ]; } elseif ( 'M' === $c ) { $value .= $short_months[ (int) $date->format( 'n' ) - 1 ]; } elseif ( 'D' === $c ) { $value .= $short_days[ (int) $date->format( 'w' ) ]; } elseif ( 'a' === $c ) { $value .= (int) $date->format( 'G' ) < 12 ? _x( 'am', 'meridiem', 'abientot' ) : _x( 'pm', 'meridiem', 'abientot' ); } elseif ( 'A' === $c ) { $value .= (int) $date->format( 'G' ) < 12 ? _x( 'AM', 'meridiem', 'abientot' ) : _x( 'PM', 'meridiem', 'abientot' ); } else { $value .= $date->format( $c ); } } return $value; } private static function months(): array { return array( __( 'January', 'abientot' ), __( 'February', 'abientot' ), __( 'March', 'abientot' ), __( 'April', 'abientot' ), __( 'May', 'abientot' ), __( 'June', 'abientot' ), __( 'July', 'abientot' ), __( 'August', 'abientot' ), __( 'September', 'abientot' ), __( 'October', 'abientot' ), __( 'November', 'abientot' ), __( 'December', 'abientot' ) ); } private static function weekdays(): array { return array( __( 'Sunday', 'abientot' ), __( 'Monday', 'abientot' ), __( 'Tuesday', 'abientot' ), __( 'Wednesday', 'abientot' ), __( 'Thursday', 'abientot' ), __( 'Friday', 'abientot' ), __( 'Saturday', 'abientot' ) ); } private static function day_months(): array { return array( _x( 'January', 'genitive month', 'abientot' ), _x( 'February', 'genitive month', 'abientot' ), _x( 'March', 'genitive month', 'abientot' ), _x( 'April', 'genitive month', 'abientot' ), _x( 'May', 'genitive month', 'abientot' ), _x( 'June', 'genitive month', 'abientot' ), _x( 'July', 'genitive month', 'abientot' ), _x( 'August', 'genitive month', 'abientot' ), _x( 'September', 'genitive month', 'abientot' ), _x( 'October', 'genitive month', 'abientot' ), _x( 'November', 'genitive month', 'abientot' ), _x( 'December', 'genitive month', 'abientot' ) ); } private static function short_months(): array { return array( _x( 'Jan', 'abbreviated month', 'abientot' ), _x( 'Feb', 'abbreviated month', 'abientot' ), _x( 'Mar', 'abbreviated month', 'abientot' ), _x( 'Apr', 'abbreviated month', 'abientot' ), _x( 'May', 'abbreviated month', 'abientot' ), _x( 'Jun', 'abbreviated month', 'abientot' ), _x( 'Jul', 'abbreviated month', 'abientot' ), _x( 'Aug', 'abbreviated month', 'abientot' ), _x( 'Sep', 'abbreviated month', 'abientot' ), _x( 'Oct', 'abbreviated month', 'abientot' ), _x( 'Nov', 'abbreviated month', 'abientot' ), _x( 'Dec', 'abbreviated month', 'abientot' ) ); } private static function short_weekdays(): array { return array( _x( 'Sun', 'abbreviated weekday', 'abientot' ), _x( 'Mon', 'abbreviated weekday', 'abientot' ), _x( 'Tue', 'abbreviated weekday', 'abientot' ), _x( 'Wed', 'abbreviated weekday', 'abientot' ), _x( 'Thu', 'abbreviated weekday', 'abientot' ), _x( 'Fri', 'abbreviated weekday', 'abientot' ), _x( 'Sat', 'abbreviated weekday', 'abientot' ) ); } public static function date_names(): array { $zone = new \DateTimeZone( 'UTC' ); $months = array(); $short = array(); $days = array(); $short_days = array(); $day_months = array(); $day_short = array(); for ( $m = 1; $m <= 12; ++$m ) { $stamp = gmmktime( 12, 0, 0, $m, 1, 2024 ); $months[] = self::date( 'F', $stamp, $zone ); $short[] = self::date( 'M', $stamp, $zone ); $day_months[] = preg_replace( '/^\d+\s+/u', '', self::date( 'j F', $stamp, $zone ) ); $day_short[] = preg_replace( '/^\d+\s+/u', '', self::date( 'j M', $stamp, $zone ) ); } for ( $d = 0; $d < 7; ++$d ) { $stamp = gmmktime( 12, 0, 0, 1, 7 + $d, 2024 ); $days[] = self::date( 'l', $stamp, $zone ); $short_days[] = self::date( 'D', $stamp, $zone ); } return array( 'months' => $months, 'shortMonths' => $short, 'dayMonths' => $day_months, 'dayShortMonths' => $day_short, 'weekdays' => $days, 'shortWeekdays' => $short_days ); } public static function separators(): array { if ( self::intl( 'NumberFormatter' ) ) { try { $f = new \NumberFormatter( self::locale(), \NumberFormatter::DECIMAL ); return array( 'decimal' => $f->getSymbol( \NumberFormatter::DECIMAL_SEPARATOR_SYMBOL ), 'group' => $f->getSymbol( \NumberFormatter::GROUPING_SEPARATOR_SYMBOL ), 'primary' => max( 1, (int) $f->getAttribute( \NumberFormatter::GROUPING_SIZE ) ), 'secondary' => (int) $f->getAttribute( \NumberFormatter::SECONDARY_GROUPING_SIZE ) ); } catch ( \Throwable $error ) { } } if ( '' === Settings::configured_locale() ) { global $wp_locale; return array( 'decimal' => $wp_locale->number_format['decimal_point'], 'group' => $wp_locale->number_format['thousands_sep'] ); } return array( 'decimal' => _x( '.', 'decimal separator', 'abientot' ), 'group' => _x( ',', 'thousands separator', 'abientot' ) ); } public static function number( $value, int $decimals = 2 ): string { if ( self::intl( 'NumberFormatter' ) ) { try { $f = new \NumberFormatter( self::locale(), \NumberFormatter::DECIMAL ); $f->setAttribute( \NumberFormatter::MIN_FRACTION_DIGITS, $decimals ); $f->setAttribute( \NumberFormatter::MAX_FRACTION_DIGITS, $decimals ); $s = $f->format( $value ); if (false !== $s)return $s; } catch ( \Throwable $error ) { } } $symbols = self::separators(); return number_format( (float) $value, $decimals, $symbols['decimal'], $symbols['group'] ); } /** Reject ambiguous grouping; normalize Unicode numerals without float parsing. */ public static function currency( $value, string $currency, int $digits ): string { if ( self::intl( 'NumberFormatter' ) ) { try { $f = new \NumberFormatter( self::locale(), \NumberFormatter::CURRENCY ); $f->setTextAttribute( \NumberFormatter::CURRENCY_CODE, $currency ); $f->setPattern( preg_replace( '/¤+/u', '¤¤', $f->getPattern() ) ); $f->setAttribute( \NumberFormatter::MIN_FRACTION_DIGITS, $digits ); $f->setAttribute( \NumberFormatter::MAX_FRACTION_DIGITS, $digits ); $s = $f->format( $value ); if (false !== $s)return $s; } catch ( \Throwable $error ) { } } return $currency . ' ' . self::number( $value, $digits ); } public static function normalize_number_input( string $value ): string { $valid_utf8 = preg_match( '//u', $value ); if ( 1 !== $valid_utf8 ) { throw new \InvalidArgumentException( 'money_input' ); } $value = preg_replace( '/^\s+|\s+$/u', '', $value ); if ( class_exists( '\\IntlChar' ) ) { $value = preg_replace_callback( '/\p{Nd}/u', static function ( $match ) { $digit = \IntlChar::digit( $match[0] ); return is_int( $digit ) && $digit >= 0 && $digit <= 9 ? (string) $digit : $match[0]; }, $value ); } $sets = array( '٠١٢٣٤٥٦٧٨٩', '۰۱۲۳۴۵۶۷۸۹', '0123456789', '०१२३४५६७८९' ); foreach ( $sets as$set ) { $characters = preg_split( '//u', $set, -1, PREG_SPLIT_NO_EMPTY ); $value = strtr( $value, array_combine( $characters, range( 0, 9 ) ) ); } if ( ! is_string( $value ) || preg_match( '/[\x00-\x1f\x7f\x{202a}-\x{202e}\x{2066}-\x{2069}]/u', $value ) ) { throw new \InvalidArgumentException( 'money_input' ); } $symbols = self::separators(); $decimal = $symbols['decimal']; $group = $symbols['group']; if ( in_array( $group, array( '.', ',' ), true ) && preg_match( '/^\d{1,3}' . preg_quote( $group, '/' ) . '\d{3}$/D', $value ) ) { throw new \InvalidArgumentException( 'money_input' ); } // Both ASCII decimal conventions remain accepted for simple ungrouped inputs. if ( preg_match( '/^\d+(?:[.,]\d+)?$/D', $value ) ) { return str_replace( ',', '.', $value ); } if ( in_array( $group, array( ' ', "\xc2\xa0", "\xe2\x80\xaf" ), true ) ) { $value = str_replace( array( ' ', "\xc2\xa0", "\xe2\x80\xaf" ), $group, $value ); } if ( '' !== $group && $decimal !== $group ) { $primary = max( 1, (int) ( $symbols['primary'] ?? 3 ) ); $secondary = (int) ( $symbols['secondary'] ?? $primary ); if ( $secondary < 1 ) { $secondary = $primary; } $pattern = '/^\d{1,' . $secondary . '}(?:' . preg_quote( $group, '/' ) . '\d{' . $secondary . '})*' . preg_quote( $group, '/' ) . '\d{' . $primary . '}(?:' . preg_quote( $decimal, '/' ) . '\d+)?$/uD'; if ( preg_match( $pattern, $value ) ) { return str_replace( $decimal, '.', str_replace( $group, '', $value ) ); } } if ( '.' !== $decimal ) { $value = str_replace( $decimal, '.', $value ); } if ( ! preg_match( '/^\d+(?:\.\d+)?$/D', $value ) ) { throw new \InvalidArgumentException( 'money_input' ); } return $value; } public static function register_script(): void { PhoneField::register_assets(); wp_register_script( 'abientot-localization', ABIENTOT_PLUGIN_URL . 'assets/localization.js', array( 'abientot-phone-field' ), (string) filemtime( ABIENTOT_PLUGIN_DIR . 'assets/localization.js' ), true ); wp_localize_script( 'abientot-localization', 'abientotLocaleConfig', self::config() ); } public static function config(): array { return array( 'locale' => self::language(), 'direction' => self::direction(), 'weekStart' => self::week_start(), 'separators' => self::separators(), 'numberingSystem' => self::intl( 'NumberFormatter' ) ? '' : 'latn', 'intlCurrency' => self::intl( 'NumberFormatter' ), 'currencyPrecision' => Currency::precisions(), 'dateNames' => self::date_names(), 'meridiem' => array( 'am' => self::date( 'a', gmmktime( 9, 0, 0, 1, 1, 2024 ), new \DateTimeZone( 'UTC' ) ), 'pm' => self::date( 'a', gmmktime( 15, 0, 0, 1, 1, 2024 ), new \DateTimeZone( 'UTC' ) ) ) ); } }