PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.20
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.20
1.10.20 1.10.19 1.10.18 1.10.17 1.10.16 1.10.15 1.10.13 1.10.14 1.10.12 1.10.11 1.10.10 1.10.9 1.10.8 untagged-3d9b7ccddc54df87c672 1.10.7 1.10.6 1.10.5 1.10.3 1.10.4 1.10.2 1.10.1 1.10.0 1.9.17 1.9.15 1.9.16 All 164 releases
← All changes | includes/Services/Opening_Hours_Formatter.php +88 -39 1.9.16 → 1.10.20 View file →
@@ -5,13 +5,21 @@
5 5 * Converts the structured opening hours array into human-readable strings
6 6 * in three formats: compact (grouped), vertical (one day per line), and
7 7 * inline (single comma-separated line).
8 8 *
9 + * Times and day names are rendered through Receipt_Date_Formatter so a receipt
10 + * uses a single convention source: the opening hours and the order timestamps
11 + * share the same clock convention, day-period style, and locale.
12 + *
9 13 * @package WCPOS\WooCommercePOS\Services
10 14 */
11 15
12 16 namespace WCPOS\WooCommercePOS\Services;
13 17
18 +use DateTimeImmutable;
19 +use DateTimeZone;
20 +use WCPOS\WooCommercePOS\i18n;
21 +
14 22 /**
15 23 * Opening_Hours_Formatter class.
16 24 */
17 25 class Opening_Hours_Formatter {
@@ -23,17 +31,19 @@
23 31
24 32 /**
25 33 * Format as vertical list — one day per line, newline-separated.
26 34 *
27 - * @param array $hours Structured hours array (keys 0–6).
35 + * @param array $hours Structured hours array (keys 0–6).
36 + * @param string $locale Optional receipt locale; defaults to the site locale.
28 37 * @return string Newline-separated string.
29 38 */
30 - public static function format_vertical( array $hours ): string {
31 - $lines = array();
39 + public static function format_vertical( array $hours, string $locale = '' ): string {
40 + $closed = self::get_closed_label( $locale );
41 + $lines = array();
32 42 foreach ( self::DAY_KEYS as $day ) {
33 - $day_name = self::get_day_name( $day );
43 + $day_name = self::get_day_name( $day, $locale );
34 44 $slots = isset( $hours[ (string) $day ] ) ? $hours[ (string) $day ] : array();
35 - $formatted = self::format_slots( $slots );
45 + $formatted = self::format_slots( $slots, $locale, $closed );
36 46 $lines[] = $day_name . ' ' . $formatted;
37 47 }
38 48
39 49 return implode( "\n", $lines );
@@ -41,17 +51,18 @@
41 51
42 52 /**
43 53 * Format as compact grouped — consecutive days with identical hours are ranged.
44 54 *
45 - * @param array $hours Structured hours array (keys 0–6).
55 + * @param array $hours Structured hours array (keys 0–6).
56 + * @param string $locale Optional receipt locale; defaults to the site locale.
46 57 * @return string Newline-separated string.
47 58 */
48 - public static function format_compact( array $hours ): string {
49 - $groups = self::group_consecutive_days( $hours );
59 + public static function format_compact( array $hours, string $locale = '' ): string {
60 + $groups = self::group_consecutive_days( $hours, $locale, self::get_closed_label( $locale ) );
50 61 $lines = array();
51 62
52 63 foreach ( $groups as $group ) {
53 - $day_label = self::format_day_range( $group['start'], $group['end'] );
64 + $day_label = self::format_day_range( $group['start'], $group['end'], $locale );
54 65 $lines[] = $day_label . ' ' . $group['formatted'];
55 66 }
56 67
57 68 return implode( "\n", $lines );
@@ -59,17 +70,18 @@
59 70
60 71 /**
61 72 * Format as inline — single comma-separated line using compact grouping.
62 73 *
63 - * @param array $hours Structured hours array (keys 0–6).
74 + * @param array $hours Structured hours array (keys 0–6).
75 + * @param string $locale Optional receipt locale; defaults to the site locale.
64 76 * @return string Single line string.
65 77 */
66 - public static function format_inline( array $hours ): string {
67 - $groups = self::group_consecutive_days( $hours );
78 + public static function format_inline( array $hours, string $locale = '' ): string {
79 + $groups = self::group_consecutive_days( $hours, $locale, self::get_closed_label( $locale ) );
68 80 $parts = array();
69 81
70 82 foreach ( $groups as $group ) {
71 - $day_label = self::format_day_range( $group['start'], $group['end'] );
83 + $day_label = self::format_day_range( $group['start'], $group['end'], $locale );
72 84 $parts[] = $day_label . ' ' . $group['formatted'];
73 85 }
74 86
75 87 return implode( ', ', $parts );
@@ -77,18 +89,20 @@
77 89
78 90 /**
79 91 * Group consecutive days that share identical time slots.
80 92 *
81 - * @param array $hours Structured hours array.
93 + * @param array $hours Structured hours array.
94 + * @param string $locale Receipt locale.
95 + * @param string $closed Closed-day label, already resolved for the receipt locale.
82 96 * @return array Array of groups, each with 'start', 'end', 'formatted'.
83 97 */
84 - private static function group_consecutive_days( array $hours ): array {
98 + private static function group_consecutive_days( array $hours, string $locale, string $closed ): array {
85 99 $groups = array();
86 100 $current = null;
87 101
88 102 foreach ( self::DAY_KEYS as $day ) {
89 103 $slots = isset( $hours[ (string) $day ] ) ? $hours[ (string) $day ] : array();
90 - $formatted = self::format_slots( $slots );
104 + $formatted = self::format_slots( $slots, $locale, $closed );
91 105
92 106 if ( null === $current || $current['formatted'] !== $formatted ) {
93 107 if ( null !== $current ) {
94 108 $groups[] = $current;
@@ -102,8 +116,9 @@
102 116 $current['end'] = $day;
103 117 }
104 118 }
105 119
120 + // @phpstan-ignore notIdentical.alwaysTrue (belt-and-braces for empty input)
106 121 if ( null !== $current ) {
107 122 $groups[] = $current;
108 123 }
109 124
@@ -112,29 +127,32 @@
112 127
113 128 /**
114 129 * Format a day range label.
115 130 *
116 - * @param int $start Start day index (0–6).
117 - * @param int $end End day index (0–6).
131 + * @param int $start Start day index (0–6).
132 + * @param int $end End day index (0–6).
133 + * @param string $locale Receipt locale.
118 134 * @return string
119 135 */
120 - private static function format_day_range( int $start, int $end ): string {
136 + private static function format_day_range( int $start, int $end, string $locale ): string {
121 137 if ( $start === $end ) {
122 - return self::get_day_name( $start );
138 + return self::get_day_name( $start, $locale );
123 139 }
124 140
125 - return self::get_day_name( $start ) . "\u{2013}" . self::get_day_name( $end );
141 + return self::get_day_name( $start, $locale ) . "\u{2013}" . self::get_day_name( $end, $locale );
126 142 }
127 143
128 144 /**
129 145 * Format time slots for a single day.
130 146 *
131 - * @param array $slots Flat array of time pairs.
147 + * @param array $slots Flat array of time pairs.
148 + * @param string $locale Receipt locale.
149 + * @param string $closed Closed-day label, already resolved for the receipt locale.
132 150 * @return string
133 151 */
134 - private static function format_slots( array $slots ): string {
152 + private static function format_slots( array $slots, string $locale, string $closed ): string {
135 153 if ( empty( $slots ) ) {
136 - return /* translators: Short WCPOS UI label; keep concise. */ __( 'Closed', 'woocommerce-pos' );
154 + return $closed;
137 155 }
138 156
139 157 // Drop trailing unpaired element to ensure open/close pairs.
140 158 if ( count( $slots ) % 2 !== 0 ) {
@@ -141,16 +159,16 @@
141 159 array_pop( $slots );
142 160 }
143 161
144 162 if ( empty( $slots ) ) {
145 - return /* translators: Short WCPOS UI label; keep concise. */ __( 'Closed', 'woocommerce-pos' );
163 + return $closed;
146 164 }
147 165
148 166 $ranges = array();
149 167 $slot_count = count( $slots );
150 168 for ( $i = 0; $i < $slot_count - 1; $i += 2 ) {
151 - $open = self::format_time( $slots[ $i ] );
152 - $close = self::format_time( $slots[ $i + 1 ] );
169 + $open = self::format_time( $slots[ $i ], $locale );
170 + $close = self::format_time( $slots[ $i + 1 ], $locale );
153 171 $ranges[] = $open . " \u{2013} " . $close;
154 172 }
155 173
156 174 return implode( ', ', $ranges );
@@ -156,34 +174,65 @@
156 174 return implode( ', ', $ranges );
157 175 }
158 176
159 177 /**
160 - * Format a time string according to WP time_format option.
178 + * Resolve the closed-day label in the receipt locale.
161 179 *
162 - * @param string $time Time in H:i format (e.g. "09:00").
180 + * The day names and times follow the receipt locale, so this label has to
181 + * as well — otherwise a store whose locale differs from the site's renders
182 + * a mixed-language line ("zo Closed"). Mirrors the locale guard in
183 + * Receipt_I18n_Labels::get_labels(); resolve once per public call, because
184 + * switch_to_locale() reloads the text domain.
185 + *
186 + * @param string $locale Receipt locale, or empty string for the site locale.
187 + * @return string
188 + */
189 + private static function get_closed_label( string $locale ): string {
190 + if ( '' !== $locale && get_locale() !== $locale && function_exists( 'switch_to_locale' ) && switch_to_locale( $locale ) ) {
191 + try {
192 + new i18n();
193 +
194 + return self::get_closed_label( '' );
195 + } finally {
196 + restore_previous_locale();
197 + }
198 + }
199 +
200 + return /* translators: Short WCPOS UI label; keep concise. */ __( 'Closed', 'woocommerce-pos' );
201 + }
202 +
203 + /**
204 + * Format a wall-clock time through the shared receipt time renderer.
205 + *
206 + * The stored value is a bare wall clock with no date or zone, so it is
207 + * anchored to a fixed UTC instant and rendered in UTC — the reference day
208 + * exists only to give the formatter a timestamp.
209 + *
210 + * @param string $time Time in H:i format (e.g. "09:00").
211 + * @param string $locale Receipt locale.
163 212 * @return string Formatted time (e.g. "9:00 AM" or "09:00").
164 213 */
165 - private static function format_time( string $time ): string {
166 - $timestamp = strtotime( '2000-01-01 ' . $time );
214 + private static function format_time( string $time, string $locale ): string {
215 + $timestamp = strtotime( '2000-01-01 ' . $time . ' UTC' );
167 216
168 217 if ( false === $timestamp ) {
169 218 return $time;
170 219 }
171 220
172 - $time_format = get_option( 'time_format', 'g:i A' );
173 -
174 - return date_i18n( $time_format, $timestamp );
221 + return Receipt_Date_Formatter::time( $timestamp, new DateTimeZone( 'UTC' ), '' !== $locale ? $locale : null );
175 222 }
176 223
177 224 /**
178 - * Get the localized short day name.
225 + * Get the localized short day name through the shared receipt renderer.
179 226 *
180 - * @param int $day Day index (0=Monday, 6=Sunday).
227 + * @param int $day Day index (0=Monday, 6=Sunday).
228 + * @param string $locale Receipt locale.
181 229 * @return string Short day name (e.g. "Mon", "Tue").
182 230 */
183 - private static function get_day_name( int $day ): string {
231 + private static function get_day_name( int $day, string $locale ): string {
184 232 // 2024-01-01 is a Monday. Offset by $day to get the right weekday.
185 - $timestamp = strtotime( '2024-01-01 +' . $day . ' days' );
233 + $utc = new DateTimeZone( 'UTC' );
234 + $timestamp = ( new DateTimeImmutable( '2024-01-01 00:00:00', $utc ) )->modify( '+' . $day . ' days' )->getTimestamp();
186 235
187 - return date_i18n( 'D', $timestamp );
236 + return Receipt_Date_Formatter::weekday_short( $timestamp, $utc, '' !== $locale ? $locale : null );
188 237 }
189 238 }