PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.8.7
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.8.7
1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 0.8.8 0.8.7 All 34 releases
desktop-mode / includes / users-window / window.php

window.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 0.8.7, at includes/users-window/window.php

621 lines 21.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Desktop Mode — Native Users Window: registration, template, REST fields.
4 *
5 * Mirrors the structure of the Posts/Pages windows
6 * (`includes/posts-window/`, `includes/pages-window/`) adapted for
7 * the user collection — `/wp/v2/users` rather than `/wp/v2/posts`,
8 * a Role/Email column set instead of taxonomies, and a much heavier
9 * permission story (see `permissions.php` + `rest.php`).
10 *
11 * @package WPDesktopMode
12 * @since 0.18.0
13 */
14
15 defined( 'ABSPATH' ) || exit;
16
17 /**
18 * Echoes the native Users window's template body.
19 *
20 * Same toolbar/table/pager structure as the Posts and Pages
21 * windows. Reuses the `data-desktop-mode-posts-*` selectors so the
22 * shared JS bundle binds to the same hooks regardless of window
23 * mode (the namespace is an internal contract — both windows use
24 * the same template machinery).
25 *
26 * @since 0.18.0
27 */
28 function desktop_mode_users_window_render_template() {
29 $can_create = current_user_can( 'create_users' );
30 ob_start();
31 ?>
32 <div class="desktop-mode-posts desktop-mode-users" data-desktop-mode-posts-root>
33 <wpd-tabs value="all" class="desktop-mode-users__tabs" data-desktop-mode-users-tabs>
34 <wpd-tab value="all"><?php esc_html_e( 'All users', 'desktop-mode' ); ?></wpd-tab>
35 <?php if ( $can_create ) : ?>
36 <wpd-tab value="add-new"><?php esc_html_e( 'Add new', 'desktop-mode' ); ?></wpd-tab>
37 <?php endif; ?>
38 <?php /* Profile tab — always visible, always shows the
39 CURRENT logged-in user. Other-user row clicks
40 open a separate `desktop-mode-user-edit`
41 window. */ ?>
42 <wpd-tab value="edit" data-desktop-mode-users-edit-tab>
43 <?php esc_html_e( 'Profile', 'desktop-mode' ); ?>
44 </wpd-tab>
45 </wpd-tabs>
46
47 <wpd-tabpanel for="all" class="desktop-mode-posts__panel">
48 <header class="desktop-mode-posts__toolbar" data-desktop-mode-posts-toolbar>
49 <div class="desktop-mode-posts__toolbar-left">
50 <wpd-segmented data-desktop-mode-posts-status value=""></wpd-segmented>
51 <wpd-text-field
52 data-desktop-mode-posts-search
53 placeholder="<?php esc_attr_e( 'Search name, username, email…', 'desktop-mode' ); ?>"
54 ></wpd-text-field>
55 </div>
56 <div class="desktop-mode-posts__toolbar-right" data-desktop-mode-posts-bulk hidden>
57 <span class="desktop-mode-posts__count" data-desktop-mode-posts-count></span>
58 <span class="desktop-mode-posts__bulk-actions" data-desktop-mode-posts-bulk-actions></span>
59 </div>
60 <div class="desktop-mode-posts__toolbar-trailing">
61 <span class="desktop-mode-posts__toolbar-extras" data-desktop-mode-posts-toolbar-extras></span>
62 <wpd-button variant="ghost" data-desktop-mode-posts-refresh title="<?php esc_attr_e( 'Refresh', 'desktop-mode' ); ?>">
63 <span class="dashicons dashicons-update" aria-hidden="true"></span>
64 </wpd-button>
65 <wpd-button variant="primary" data-desktop-mode-posts-new>
66 <span class="dashicons dashicons-plus" aria-hidden="true"></span>
67 <?php esc_html_e( 'Add new', 'desktop-mode' ); ?>
68 </wpd-button>
69 </div>
70 </header>
71 <div class="desktop-mode-posts__body" data-desktop-mode-posts-body>
72 <wpd-table
73 data-desktop-mode-posts-table
74 selectable="multi"
75 sticky-header
76 sticky-columns="1"
77 hover
78 striped
79 bordered
80 loading
81 >
82 <div slot="empty" class="desktop-mode-posts__empty">
83 <span class="dashicons dashicons-admin-users" aria-hidden="true"></span>
84 <p><?php esc_html_e( 'No users found.', 'desktop-mode' ); ?></p>
85 <p class="desktop-mode-posts__empty-hint">
86 <?php esc_html_e( 'Try a different search or change the role filter.', 'desktop-mode' ); ?>
87 </p>
88 </div>
89 </wpd-table>
90 </div>
91 <footer class="desktop-mode-posts__pager" data-desktop-mode-posts-pager>
92 <div class="desktop-mode-posts__pager-meta">
93 <span data-desktop-mode-posts-page-indicator></span>
94 </div>
95 <div class="desktop-mode-posts__pager-nav">
96 <wpd-button variant="ghost" data-desktop-mode-posts-prev disabled>
97 <span class="dashicons dashicons-arrow-left-alt2" aria-hidden="true"></span>
98 <?php esc_html_e( 'Previous', 'desktop-mode' ); ?>
99 </wpd-button>
100 <wpd-button variant="ghost" data-desktop-mode-posts-next disabled>
101 <?php esc_html_e( 'Next', 'desktop-mode' ); ?>
102 <span class="dashicons dashicons-arrow-right-alt2" aria-hidden="true"></span>
103 </wpd-button>
104 <label class="desktop-mode-posts__pager-perpage">
105 <?php esc_html_e( 'Per page', 'desktop-mode' ); ?>
106 <select data-desktop-mode-posts-per-page>
107 <option value="10">10</option>
108 <option value="20" selected>20</option>
109 <option value="50">50</option>
110 <option value="100">100</option>
111 </select>
112 </label>
113 </div>
114 </footer>
115 </wpd-tabpanel>
116
117 <?php if ( $can_create ) : ?>
118 <wpd-tabpanel for="add-new" class="desktop-mode-users__add-panel">
119 <wpd-form
120 data-desktop-mode-users-add-form
121 submit-label="<?php esc_attr_e( 'Add user', 'desktop-mode' ); ?>"
122 reset-label="<?php esc_attr_e( 'Reset', 'desktop-mode' ); ?>"
123 >
124 <div slot="header">
125 <h2><?php esc_html_e( 'Add a new user', 'desktop-mode' ); ?></h2>
126 <p class="desktop-mode-users__form-lede">
127 <?php esc_html_e( 'WordPress will create the account and (optionally) email the user a notification with a link to set their own password.', 'desktop-mode' ); ?>
128 </p>
129 </div>
130
131 <wpd-text-field
132 name="username"
133 label="<?php esc_attr_e( 'Username (required)', 'desktop-mode' ); ?>"
134 placeholder="<?php esc_attr_e( 'e.g. jane.doe', 'desktop-mode' ); ?>"
135 autocomplete="off"
136 required
137 ></wpd-text-field>
138 <wpd-text-field
139 name="email"
140 type="email"
141 label="<?php esc_attr_e( 'Email (required)', 'desktop-mode' ); ?>"
142 placeholder="<?php esc_attr_e( 'jane@example.com', 'desktop-mode' ); ?>"
143 autocomplete="off"
144 required
145 ></wpd-text-field>
146 <wpd-text-field
147 name="first_name"
148 label="<?php esc_attr_e( 'First name', 'desktop-mode' ); ?>"
149 autocomplete="off"
150 ></wpd-text-field>
151 <wpd-text-field
152 name="last_name"
153 label="<?php esc_attr_e( 'Last name', 'desktop-mode' ); ?>"
154 autocomplete="off"
155 ></wpd-text-field>
156 <wpd-text-field
157 name="url"
158 type="url"
159 label="<?php esc_attr_e( 'Website', 'desktop-mode' ); ?>"
160 placeholder="https://example.com"
161 autocomplete="off"
162 full-width
163 ></wpd-text-field>
164 <?php /* The `role` and `locale` selects are declared here
165 so the components upgrade with the rest of the
166 form; the option list (which depends on the
167 viewer's `editable_roles` map and the install's
168 available languages) is appended JS-side in
169 `mountAddUserForm`. */ ?>
170 <wpd-select
171 name="role"
172 label="<?php esc_attr_e( 'Role', 'desktop-mode' ); ?>"
173 ></wpd-select>
174 <wpd-select
175 name="locale"
176 label="<?php esc_attr_e( 'Language', 'desktop-mode' ); ?>"
177 ></wpd-select>
178 <wpd-text-field
179 name="password"
180 type="password"
181 reveal
182 label="<?php esc_attr_e( 'Password', 'desktop-mode' ); ?>"
183 placeholder="<?php esc_attr_e( 'Auto-generated; click Generate to set one.', 'desktop-mode' ); ?>"
184 autocomplete="new-password"
185 full-width
186 ></wpd-text-field>
187 <div class="desktop-mode-users__form-pwd-actions" full-width>
188 <wpd-button
189 variant="ghost"
190 type="button"
191 data-action="generate-password"
192 >
193 <span class="dashicons dashicons-randomize" aria-hidden="true"></span>
194 <?php esc_html_e( 'Generate strong password', 'desktop-mode' ); ?>
195 </wpd-button>
196 <p class="desktop-mode-users__form-hint">
197 <?php esc_html_e( 'Leave blank to let WordPress generate one and email it to the user.', 'desktop-mode' ); ?>
198 </p>
199 </div>
200 <wpd-checkbox-label
201 name="send_notification"
202 label="<?php esc_attr_e( 'Send the new user an email about their account', 'desktop-mode' ); ?>"
203 checked
204 full-width
205 ></wpd-checkbox-label>
206 </wpd-form>
207 </wpd-tabpanel>
208 <?php endif; ?>
209
210 <wpd-tabpanel for="edit" class="desktop-mode-users__edit-panel">
211 <?php
212 /* The Profile tab is hard-wired to the viewer's own user
213 * id — the JS render shell sets the `user-id` attribute
214 * once the window is mounted. The component does the
215 * rest (lazy-loads data, paints sidebar + form +
216 * activity). Other-user editing happens in the dedicated
217 * `desktop-mode-user-edit` window via row-click. */
218 ?>
219 <wpd-user-profile data-wpd-user-profile-self></wpd-user-profile>
220 </wpd-tabpanel>
221 </div>
222 <?php
223 $html = (string) ob_get_clean();
224
225 /**
226 * Filter the native Users window's template HTML.
227 *
228 * @since 0.18.0
229 *
230 * @param string $html Default template HTML.
231 */
232 $filtered = (string) apply_filters( 'desktop_mode_users_window_template_html', $html );
233
234 if ( function_exists( 'desktop_mode_kses_native_window_template' ) ) {
235 echo desktop_mode_kses_native_window_template( $filtered ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- helper kses-escapes.
236 } else {
237 // Backwards-compatible fallback for the rare case the helper
238 // isn't loaded yet (e.g. a plugin echoing the template
239 // outside the framework's lifecycle).
240 echo wp_kses( $filtered, wp_kses_allowed_html( 'post' ) );
241 }
242 }
243
244 /**
245 * Register the native Users window on `init` (priority 20).
246 *
247 * @since 0.18.0
248 */
249 function desktop_mode_users_window_register_window() {
250 if ( ! desktop_mode_users_window_user_can_register() ) {
251 return;
252 }
253
254 $viewer_id = (int) get_current_user_id();
255
256 $window_args = array(
257 'title' => __( 'Users', 'desktop-mode' ),
258 'icon' => 'dashicons-admin-users',
259 'template' => 'desktop_mode_users_window_render_template',
260 // Reuse the Posts bundle — same script + style handles. The
261 // shared module branches on `cfg.mode` to render the Users
262 // view.
263 'script' => 'desktop-mode-posts-window',
264 'style' => 'desktop-mode-posts-window',
265 'width' => 1100,
266 'height' => 720,
267 'min_width' => 720,
268 'min_height' => 480,
269 'placement' => 'none',
270 'config' => array(
271 'mode' => 'users',
272 'introSlug' => 'users',
273 'restRoot' => esc_url_raw( rest_url() ),
274 'restNonce' => wp_create_nonce( 'wp_rest' ),
275 'postsUrl' => esc_url_raw( rest_url( 'wp/v2/users' ) ),
276 'editPostUrlBase' => esc_url_raw( admin_url( 'user-edit.php' ) ),
277 'newPostUrl' => esc_url_raw( admin_url( 'user-new.php' ) ),
278 'usersUrl' => esc_url_raw( rest_url( 'wp/v2/users' ) ),
279 'currentUserId' => $viewer_id,
280 'defaultPerPage' => 20,
281 'queryArgs' => desktop_mode_users_window_default_query_args(),
282 'introSeen' => desktop_mode_has_seen_intro( $viewer_id, 'users' ),
283 'introUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/intros/seen' ) ),
284
285 // Capability flags surfaced to the JS — UI hides actions
286 // the viewer can't perform. Server still re-checks every
287 // mutation, so a tampered flag here changes nothing
288 // security-wise.
289 'canEdit' => current_user_can( 'edit_users' ),
290 'canPromote' => current_user_can( 'promote_users' ),
291 'canCreate' => current_user_can( 'create_users' ),
292 'canDelete' => is_multisite()
293 ? current_user_can( 'remove_users' )
294 : current_user_can( 'delete_users' ),
295 'isMultisite' => is_multisite(),
296
297 // Role list — `{ slug: name }` for every role the viewer
298 // can assign. Empty when the viewer lacks `promote_users`.
299 'assignableRoles' => desktop_mode_users_window_role_label_map( $viewer_id ),
300 // Full role catalog for the role-FILTER dropdown (which
301 // shows EVERY role on the site, even those the viewer
302 // can't assign — they can still filter by them).
303 'allRoles' => desktop_mode_users_window_all_roles_map(),
304
305 // Available site locales for the Add User form's
306 // language dropdown. `'site-default'` = empty string
307 // (the user inherits the site's locale).
308 'locales' => desktop_mode_users_window_locales_map(),
309 'siteLocale' => (string) get_locale(),
310 'defaultRole' => (string) get_option( 'default_role', 'subscriber' ),
311 'createUserUrl' => esc_url_raw(
312 rest_url( 'desktop-mode/v1/users' )
313 ),
314
315 // REST mutation routes — the JS bundle reads these so a
316 // rename or namespace move stays in one place.
317 'bulkRoleUrl' => esc_url_raw(
318 rest_url( 'desktop-mode/v1/users/bulk-role' )
319 ),
320 'bulkDeleteUrl' => esc_url_raw(
321 rest_url( 'desktop-mode/v1/users/bulk-delete' )
322 ),
323 // Profile sub-tab — uses the same config blob to read
324 // the user-edit field option lists, the insights
325 // endpoint base, and the locale/role maps.
326 'insightsUrlBase' => esc_url_raw(
327 rest_url( 'desktop-mode/v1/users/' )
328 ),
329 'contactMethods' => (array) apply_filters(
330 'user_contactmethods',
331 array(),
332 null
333 ),
334 'colorSchemes' => function_exists( 'desktop_mode_user_edit_window_color_schemes' )
335 ? desktop_mode_user_edit_window_color_schemes()
336 : array(),
337 'sendResetUrlBase' => esc_url_raw(
338 rest_url( 'desktop-mode/v1/users/' )
339 ),
340 ),
341 );
342
343 /**
344 * Filter the args used to register the native Users window.
345 *
346 * @since 0.18.0
347 *
348 * @param array $window_args Args passed to `desktop_mode_register_window()`.
349 */
350 $window_args = (array) apply_filters( 'desktop_mode_users_window_args', $window_args );
351
352 $registered = desktop_mode_register_window( 'desktop-mode-users', $window_args );
353 if ( is_wp_error( $registered ) ) {
354 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
355 error_log( '[desktop-mode] Native Users window registration failed: ' . $registered->get_error_message() );
356 }
357 }
358 add_action( 'init', 'desktop_mode_users_window_register_window', 20 );
359
360 /**
361 * Default REST query args for the Users window.
362 *
363 * @since 0.18.0
364 *
365 * @return array
366 */
367 function desktop_mode_users_window_default_query_args() {
368 $args = array(
369 // `_fields` whitelists the columns we render plus the four
370 // REST fields registered below. Skipping the whitelist would
371 // pull every meta + every embedded link the user controller
372 // emits — heavy on every page change.
373 '_fields' =>
374 'id,name,slug,email,url,description,roles,registered_date,avatar_urls,'
375 . 'desktop_mode_user_stats,desktop_mode_last_login,desktop_mode_presence,'
376 . 'desktop_mode_can_edit,desktop_mode_assignable_roles',
377 // `who=authors` would hide subscribers — we want the full
378 // list. `context=edit` is required because `email`, `roles`,
379 // and `registered_date` are edit-context-only on
380 // `/wp/v2/users`; in `view` they're omitted from the response
381 // entirely (independent of `_fields`), which paints the
382 // table as "No role" / empty email / empty registered date.
383 // The window is already gated on `list_users`, the cap
384 // `context=edit` requires, so this is safe.
385 'context' => 'edit',
386 'per_page' => 20,
387 );
388
389 /**
390 * Filter the default outbound REST query args for the Users window.
391 *
392 * @since 0.18.0
393 *
394 * @param array $args Default args.
395 */
396 return (array) apply_filters( 'desktop_mode_users_window_query_args', $args );
397 }
398
399 /**
400 * Build the `{ slug: label }` map for every role on the install.
401 *
402 * Used by the Users window's role FILTER (vs. role-CHANGE menu —
403 * see {@see desktop_mode_users_window_role_label_map()} for that).
404 *
405 * @since 0.18.0
406 *
407 * @return array<string,string>
408 */
409 function desktop_mode_users_window_all_roles_map() {
410 $roles = wp_roles();
411 $map = array();
412 foreach ( (array) $roles->roles as $slug => $info ) {
413 $map[ (string) $slug ] = isset( $info['name'] )
414 ? translate_user_role( (string) $info['name'] )
415 : (string) $slug;
416 }
417 return $map;
418 }
419
420 /**
421 * Build the `{ slug: label }` map for roles the viewer is allowed
422 * to assign. Empty when the viewer lacks `promote_users`.
423 *
424 * @since 0.18.0
425 *
426 * @param int $viewer_id Viewer's user id.
427 * @return array<string,string>
428 */
429 function desktop_mode_users_window_role_label_map( $viewer_id ) {
430 $slugs = desktop_mode_users_window_assignable_roles( (int) $viewer_id );
431 if ( empty( $slugs ) ) {
432 return array();
433 }
434 $all = desktop_mode_users_window_all_roles_map();
435 $out = array();
436 foreach ( $slugs as $slug ) {
437 if ( isset( $all[ $slug ] ) ) {
438 $out[ $slug ] = $all[ $slug ];
439 }
440 }
441 return $out;
442 }
443
444 /**
445 * Register the Users-window REST fields on the `user` resource.
446 *
447 * Fields:
448 *
449 * - desktop_mode_user_stats — `{ posts: int, pages: int, comments: int }`
450 * - desktop_mode_last_login — UTC unix timestamp, or null when never
451 * - desktop_mode_presence — 'online' | 'inactive' | 'offline'
452 * - desktop_mode_can_edit — viewer can edit / promote this row
453 * - desktop_mode_assignable_roles — role slugs the viewer can assign to this row
454 *
455 * Each field returns sensible empty defaults when the viewer lacks
456 * the cap to see the value, so the JS never has to defend against
457 * "field present but null".
458 *
459 * @since 0.18.0
460 */
461 function desktop_mode_users_window_register_rest_fields() {
462 register_rest_field(
463 'user',
464 'desktop_mode_user_stats',
465 array(
466 'get_callback' => static function ( $row ) {
467 $id = isset( $row['id'] ) ? (int) $row['id'] : 0;
468 if ( $id <= 0 ) {
469 return array(
470 'posts' => 0,
471 'pages' => 0,
472 'comments' => 0,
473 );
474 }
475 $posts = (int) count_user_posts( $id, 'post', true );
476 $pages = post_type_exists( 'page' )
477 ? (int) count_user_posts( $id, 'page', true )
478 : 0;
479 $comments = (int) get_comments(
480 array(
481 'user_id' => $id,
482 'count' => true,
483 'status' => 'approve',
484 )
485 );
486 return array(
487 'posts' => $posts,
488 'pages' => $pages,
489 'comments' => $comments,
490 );
491 },
492 'schema' => array(
493 'description' => __( 'Per-user content stats: published post / page / comment counts.', 'desktop-mode' ),
494 'type' => 'object',
495 'context' => array( 'view', 'edit', 'embed' ),
496 'readonly' => true,
497 ),
498 )
499 );
500
501 register_rest_field(
502 'user',
503 'desktop_mode_last_login',
504 array(
505 'get_callback' => static function ( $row ) {
506 $id = isset( $row['id'] ) ? (int) $row['id'] : 0;
507 if ( $id <= 0 ) {
508 return null;
509 }
510 $ts = (int) get_user_meta( $id, DESKTOP_MODE_LAST_LOGIN_META_KEY, true );
511 return $ts > 0 ? $ts : null;
512 },
513 'schema' => array(
514 'description' => __( 'UTC unix timestamp of this user’s last successful login, or null when never recorded.', 'desktop-mode' ),
515 'type' => array( 'integer', 'null' ),
516 'context' => array( 'view', 'edit', 'embed' ),
517 'readonly' => true,
518 ),
519 )
520 );
521
522 register_rest_field(
523 'user',
524 'desktop_mode_presence',
525 array(
526 'get_callback' => static function ( $row ) {
527 $id = isset( $row['id'] ) ? (int) $row['id'] : 0;
528 if ( $id <= 0 || ! function_exists( 'desktop_mode_presence_status_for_user' ) ) {
529 return 'offline';
530 }
531 return (string) desktop_mode_presence_status_for_user( $id );
532 },
533 'schema' => array(
534 'description' => __( 'Live presence status: online / inactive / offline.', 'desktop-mode' ),
535 'type' => 'string',
536 'enum' => array( 'online', 'inactive', 'offline' ),
537 'context' => array( 'view', 'edit', 'embed' ),
538 'readonly' => true,
539 ),
540 )
541 );
542
543 register_rest_field(
544 'user',
545 'desktop_mode_can_edit',
546 array(
547 'get_callback' => static function ( $row ) {
548 $id = isset( $row['id'] ) ? (int) $row['id'] : 0;
549 $viewer = (int) get_current_user_id();
550 if ( $id <= 0 || $viewer <= 0 ) {
551 return false;
552 }
553 return (bool) user_can( $viewer, 'edit_user', $id );
554 },
555 'schema' => array(
556 'description' => __( 'Whether the requester can edit this user.', 'desktop-mode' ),
557 'type' => 'boolean',
558 'context' => array( 'view', 'edit', 'embed' ),
559 'readonly' => true,
560 ),
561 )
562 );
563
564 register_rest_field(
565 'user',
566 'desktop_mode_assignable_roles',
567 array(
568 'get_callback' => static function ( $row ) {
569 $id = isset( $row['id'] ) ? (int) $row['id'] : 0;
570 $viewer = (int) get_current_user_id();
571 if ( $id <= 0 || $viewer <= 0 ) {
572 return array();
573 }
574 return array_values( desktop_mode_users_window_assignable_roles( $viewer, $id ) );
575 },
576 'schema' => array(
577 'description' => __( 'Role slugs the requester can assign to this user.', 'desktop-mode' ),
578 'type' => 'array',
579 'items' => array( 'type' => 'string' ),
580 'context' => array( 'view', 'edit', 'embed' ),
581 'readonly' => true,
582 ),
583 )
584 );
585 }
586 add_action( 'rest_api_init', 'desktop_mode_users_window_register_rest_fields' );
587
588 /**
589 * Build the `[ slug => label ]` map for the Add User locale picker.
590 *
591 * Site default is keyed under `''` (empty string) so the form can
592 * reflect "Site default — English (United States)" as the default
593 * choice without forcing the user to know which slug to send.
594 *
595 * @since 0.18.0
596 *
597 * @return array<string,string>
598 */
599 function desktop_mode_users_window_locales_map() {
600 $out = array(
601 '' => sprintf(
602 // translators: %s is the site's current locale (e.g. "en_US").
603 __( 'Site default — %s', 'desktop-mode' ),
604 get_locale()
605 ),
606 );
607 if ( ! function_exists( 'get_available_languages' ) ) {
608 require_once ABSPATH . 'wp-admin/includes/translation-install.php';
609 }
610 $languages = (array) get_available_languages();
611 foreach ( $languages as $slug ) {
612 $out[ (string) $slug ] = (string) $slug;
613 }
614 // Always offer en_US even if no .mo file is installed — core
615 // always treats it as available.
616 if ( ! isset( $out['en_US'] ) ) {
617 $out['en_US'] = 'en_US';
618 }
619 return $out;
620 }
621