PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.9.6
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.9.6
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 / assets.php

assets.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 0.9.6, at includes/assets.php

512 lines 20.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Desktop Mode asset registration.
4 *
5 * Registers all desktop-mode CSS and JS handles with WordPress so they can
6 * be enqueued from anywhere in the plugin (or by third parties).
7 *
8 * @package WPDesktopMode
9 */
10
11 defined( 'ABSPATH' ) || exit;
12
13 /**
14 * Cache-buster for a stylesheet that `@import`s sub-sheets: the max
15 * `filemtime` across the file AND everything it (transitively) imports.
16 * Sub-sheet URLs carry no `?ver=` of their own, so the parent's stamp is
17 * the only cache key the browser ever sees for the subtree — it must
18 * move when any member changes.
19 *
20 * @since 0.9.4
21 *
22 * @param string $relative Stylesheet path relative to the plugin dir.
23 * @param string $fallback Version to use when the file is missing.
24 * @return string Version string.
25 */
26 function desktop_mode_css_subtree_version( $relative, $fallback ) {
27 $root = DESKTOP_MODE_DIR . $relative;
28 if ( ! file_exists( $root ) ) {
29 return (string) $fallback;
30 }
31 $max = (int) filemtime( $root );
32 $queue = array( $root );
33 $seen = array( $root => true );
34 while ( ! empty( $queue ) ) {
35 $file = array_pop( $queue );
36 $css = (string) file_get_contents( $file );
37 if ( ! preg_match_all( '/@import\s+url\(\s*["\']?([^"\')\s]+)["\']?\s*\)/i', $css, $matches ) ) {
38 continue;
39 }
40 foreach ( $matches[1] as $import ) {
41 $path = dirname( $file ) . '/' . $import;
42 if ( ! file_exists( $path ) || isset( $seen[ $path ] ) ) {
43 continue;
44 }
45 $seen[ $path ] = true;
46 $max = max( $max, (int) filemtime( $path ) );
47 $queue[] = $path;
48 }
49 }
50 return (string) $max;
51 }
52
53 /**
54 * Registers the desktop mode CSS and JS handles.
55 *
56 * @since 0.1.0
57 */
58 function desktop_mode_register_assets() {
59 $version = DESKTOP_MODE_VERSION;
60 $suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
61
62 // `filemtime`-stamped version for built bundles. The plugin-wide
63 // `DESKTOP_MODE_VERSION` is bumped per release, but the bundles
64 // iterate faster — without a per-file mtime stamp, two clients
65 // loading the same `?ver=…` URL can be served different bytes
66 // (whichever build was on disk at upload time). Stamping with the
67 // file's modification time guarantees the URL changes whenever the
68 // file does, so "is my fix deployed?" is answerable from the
69 // network tab. Falls back to `$version` when the file is missing
70 // (test envs that import this file before the build runs).
71 $built_version = static function ( $relative ) use ( $version ) {
72 $path = DESKTOP_MODE_DIR . $relative;
73 return file_exists( $path ) ? (string) filemtime( $path ) : $version;
74 };
75
76
77 // Styles.
78 wp_register_style(
79 'desktop-mode-variables',
80 DESKTOP_MODE_URL . 'assets/css/variables.css',
81 array(),
82 $version
83 );
84 // `filemtime`-stamped so the `<link rel="stylesheet">` URL matches the
85 // `<link rel="preload" as="style">` hint emitted by
86 // `desktop_mode_print_preload_hints()` (which stamps with filemtime).
87 // Registering this with the plain `$version` instead produced two
88 // different `?ver=` query strings for the same file, so the browser
89 // never matched the preload to the stylesheet and logged "preloaded
90 // but not used within a few seconds from the window's load event".
91 wp_register_style(
92 'desktop-mode',
93 DESKTOP_MODE_URL . 'assets/css/desktop.css',
94 array( 'desktop-mode-variables' ),
95 $built_version( 'assets/css/desktop.css' )
96 );
97 // `filemtime`-stamped — window-chrome iteration (drop overlays,
98 // new drag affordances, third-party-plugin compat) lands faster
99 // than plugin version bumps. Without an mtime stamp the browser
100 // keeps `?ver=<plugin-version>` valid for the whole release cycle
101 // and the user keeps seeing yesterday's CSS even after a hard
102 // reload.
103 //
104 // The stamp covers the whole `@import` SUBTREE, not just the parent
105 // file: `windows.css` imports `window-chrome.css`,
106 // `os-settings.css`, … and those sub-sheet URLs carry no `?ver=` of
107 // their own. Stamping only the parent meant an edit to a sub-sheet
108 // never changed any URL the browser knew about, so users kept
109 // yesterday's rules until an unrelated parent edit came along
110 // (exactly how the wallpaper-description card shipped with stale
111 // layout). The subtree max-mtime busts the parent whenever ANY
112 // sub-sheet changes; the re-parsed parent then re-requests the
113 // sub-sheets, whose fresh mtimes fail heuristic-cache reuse.
114 wp_register_style(
115 'desktop-mode-windows',
116 DESKTOP_MODE_URL . 'assets/css/windows.css',
117 array( 'desktop-mode-variables', 'dashicons' ),
118 desktop_mode_css_subtree_version( 'assets/css/windows.css', $version )
119 );
120 wp_register_style(
121 'desktop-mode-dock',
122 DESKTOP_MODE_URL . 'assets/css/dock.css',
123 array( 'desktop-mode-variables', 'dashicons' ),
124 $built_version( 'assets/css/dock.css' )
125 );
126 wp_register_style(
127 'desktop-mode-dock-peek',
128 DESKTOP_MODE_URL . 'assets/css/dock-peek.css',
129 array( 'desktop-mode-dock' ),
130 $version
131 );
132 // `filemtime`-stamped — the chromeless overrides iterate faster
133 // than the plugin-wide version bumps (per-page compat shims and
134 // page-title-action exceptions land in patches), and a stale
135 // cached copy means the user sees yesterday's rules. Without the
136 // stamp, the browser keeps `?ver=0.8.1` valid for the whole
137 // release cycle.
138 wp_register_style(
139 'desktop-mode-chromeless',
140 DESKTOP_MODE_URL . 'assets/css/chromeless.css',
141 array( 'desktop-mode' ),
142 $built_version( 'assets/css/chromeless.css' )
143 );
144
145 // `filemtime`-stamped — the AI assistant surface (error states,
146 // inline affordances) iterates faster than plugin version bumps.
147 // Without an mtime stamp the browser keeps `?ver=<plugin-version>`
148 // valid for the whole release cycle and the user keeps seeing
149 // yesterday's CSS even after a hard reload.
150 wp_register_style(
151 'desktop-mode-ai-assistant',
152 DESKTOP_MODE_URL . 'assets/css/ai-assistant.css',
153 array( 'desktop-mode-variables' ),
154 $built_version( 'assets/css/ai-assistant.css' )
155 );
156
157 wp_register_style(
158 'desktop-mode-bug-report',
159 DESKTOP_MODE_URL . 'assets/css/bug-report.css',
160 array( 'desktop-mode-variables' ),
161 $version
162 );
163
164 // `filemtime` instead of the plugin-wide `$version` for the
165 // recycle-bin CSS — this file iterates faster than the bundle
166 // and we never want a stale CSS cache to mask a real fix.
167 $recycle_bin_css = DESKTOP_MODE_DIR . 'assets/css/recycle-bin.css';
168 wp_register_style(
169 'desktop-mode-recycle-bin',
170 DESKTOP_MODE_URL . 'assets/css/recycle-bin.css',
171 array( 'desktop-mode-variables', 'dashicons' ),
172 file_exists( $recycle_bin_css ) ? (string) filemtime( $recycle_bin_css ) : $version
173 );
174
175 // `filemtime` for the native Posts window CSS — same rationale as
176 // the recycle-bin CSS: bundle iterates faster than the plugin
177 // version and stale caches are worse than the cost of a 304.
178 $posts_window_css = DESKTOP_MODE_DIR . 'assets/css/posts-window.css';
179 wp_register_style(
180 'desktop-mode-posts-window',
181 DESKTOP_MODE_URL . 'assets/css/posts-window.css',
182 array( 'desktop-mode-variables', 'dashicons' ),
183 file_exists( $posts_window_css ) ? (string) filemtime( $posts_window_css ) : $version
184 );
185
186 // Native Plugins window CSS — same `filemtime`-cache-bust posture
187 // as the Posts/Recycle Bin styles.
188 $plugins_window_css = DESKTOP_MODE_DIR . 'assets/css/plugins-window.css';
189 wp_register_style(
190 'desktop-mode-plugins-window',
191 DESKTOP_MODE_URL . 'assets/css/plugins-window.css',
192 array( 'desktop-mode-variables', 'dashicons' ),
193 file_exists( $plugins_window_css ) ? (string) filemtime( $plugins_window_css ) : $version
194 );
195
196 // Native Comments window CSS — same `filemtime` posture.
197 $comments_window_css = DESKTOP_MODE_DIR . 'assets/css/comments-window.css';
198 wp_register_style(
199 'desktop-mode-comments-window',
200 DESKTOP_MODE_URL . 'assets/css/comments-window.css',
201 array( 'desktop-mode-variables', 'dashicons' ),
202 file_exists( $comments_window_css ) ? (string) filemtime( $comments_window_css ) : $version
203 );
204
205 // Files-on-the-Desktop tile + layer styles. `filemtime` for the
206 // same reason as the recycle-bin / posts-window CSS: this file
207 // iterates faster than the plugin version, and a stale cache
208 // would mask a real fix.
209 $desktop_files_css = DESKTOP_MODE_DIR . 'assets/css/desktop-files.css';
210 wp_register_style(
211 'desktop-mode-files',
212 DESKTOP_MODE_URL . 'assets/css/desktop-files.css',
213 array( 'desktop-mode-variables', 'dashicons' ),
214 file_exists( $desktop_files_css ) ? (string) filemtime( $desktop_files_css ) : $version
215 );
216
217 // Games hub window (launcher grid, scoreboard, challenges) +
218 // per-game styles. Same `filemtime` cache-bust posture as the
219 // other fast-iterating feature stylesheets.
220 $games_css = DESKTOP_MODE_DIR . 'assets/css/games.css';
221 wp_register_style(
222 'desktop-mode-games',
223 DESKTOP_MODE_URL . 'assets/css/games.css',
224 array( 'desktop-mode-variables', 'dashicons' ),
225 file_exists( $games_css ) ? (string) filemtime( $games_css ) : $version
226 );
227 $game_inkfall_css = DESKTOP_MODE_DIR . 'assets/css/game-inkfall.css';
228 wp_register_style(
229 'desktop-mode-game-inkfall',
230 DESKTOP_MODE_URL . 'assets/css/game-inkfall.css',
231 array( 'desktop-mode-variables' ),
232 file_exists( $game_inkfall_css ) ? (string) filemtime( $game_inkfall_css ) : $version
233 );
234 $game_alphabet_soup_css = DESKTOP_MODE_DIR . 'assets/css/game-alphabet-soup.css';
235 wp_register_style(
236 'desktop-mode-game-alphabet-soup',
237 DESKTOP_MODE_URL . 'assets/css/game-alphabet-soup.css',
238 array( 'desktop-mode-variables' ),
239 file_exists( $game_alphabet_soup_css ) ? (string) filemtime( $game_alphabet_soup_css ) : $version
240 );
241
242 // Pinned-notes layer styles (paper, pushpin, pastel tokens, pin
243 // animations). Same `filemtime` cache-bust posture as the other
244 // fast-iterating feature stylesheets above.
245 $notes_css = DESKTOP_MODE_DIR . 'assets/css/notes.css';
246 wp_register_style(
247 'desktop-mode-notes',
248 DESKTOP_MODE_URL . 'assets/css/notes.css',
249 array( 'desktop-mode-variables', 'dashicons' ),
250 file_exists( $notes_css ) ? (string) filemtime( $notes_css ) : $version
251 );
252
253 // Scripts.
254 //
255 // `wp-hooks` — the shell exposes a WordPress-style filter/action
256 // API (`window.wp.hooks`) to third-party plugins.
257 // `wp-i18n` — the TS `__()` / `_x()` / `sprintf()` wrappers in
258 // `src/i18n.ts` delegate to `window.wp.i18n` for translation
259 // lookups. Both handles are core-shipped but only pre-enqueued
260 // when Gutenberg-adjacent deps pull them in, so we list them
261 // explicitly to guarantee load order.
262 wp_register_script(
263 'desktop-mode',
264 DESKTOP_MODE_URL . 'assets/js/desktop' . $suffix . '.js',
265 // `heartbeat` + `jquery` — the recycle-bin badge module
266 // (loaded as part of this bundle) opts into the WordPress
267 // Heartbeat API so the count tile / desktop-icon badge
268 // stays in sync even when the bin window is closed.
269 array( 'wp-hooks', 'wp-i18n', 'heartbeat', 'jquery' ),
270 $built_version( 'assets/js/desktop' . $suffix . '.js' ),
271 true
272 );
273
274 // `desktop-mode-iframe-bridge` — opt-in iframe-side bridge that
275 // provides `wp.desktop.iframe.publish/subscribe/onConnection/
276 // requestConnection` to any same-origin iframe that enqueues it.
277 // Same code is also injected inline by the chromeless bridge
278 // (so chromeless wp-admin pages don't need a separate enqueue)
279 // and auto-injected when a native window opts in via
280 // `iframeContent: { bridge: true }`. Plugins targeting their
281 // own iframe pages just enqueue this handle.
282 wp_register_script(
283 'desktop-mode-iframe-bridge',
284 DESKTOP_MODE_URL . 'assets/js/iframe-bridge' . $suffix . '.js',
285 array(),
286 $built_version( 'assets/js/iframe-bridge' . $suffix . '.js' ),
287 true
288 );
289
290 // `desktop-mode-gutenberg-drop-receiver` — iframe-side bundle
291 // enqueued only on the Block Editor screens (`post.php` /
292 // `post-new.php`) for desktop-mode users. Listens for
293 // `desktop-mode-drop` postMessages from the parent shell (see
294 // `src/drag/iframe-drop-targets.ts`) and inserts the matching
295 // Gutenberg block via `wp.data.dispatch('core/block-editor')`.
296 // Depends on `wp-blocks` + `wp-data` so the editor stores are
297 // guaranteed enqueued before the receiver runs its first message
298 // handler.
299 wp_register_script(
300 'desktop-mode-gutenberg-drop-receiver',
301 DESKTOP_MODE_URL . 'assets/js/gutenberg-drop-receiver' . $suffix . '.js',
302 array( 'wp-blocks', 'wp-data' ),
303 $built_version( 'assets/js/gutenberg-drop-receiver' . $suffix . '.js' ),
304 true
305 );
306
307 // `desktop-mode-recycle-bin` — small bundle for the Recycle Bin
308 // native window. Lazy-loaded by the native-window sync the first
309 // time the bin opens; registers a render callback on
310 // `window.desktopModeNativeWindows['desktop-mode-recycle-bin']`.
311 $recycle_bin_js = DESKTOP_MODE_DIR . 'assets/js/recycle-bin' . $suffix . '.js';
312 wp_register_script(
313 'desktop-mode-recycle-bin',
314 DESKTOP_MODE_URL . 'assets/js/recycle-bin' . $suffix . '.js',
315 // `heartbeat` + `jquery` — the bin opts in to the WordPress
316 // Heartbeat API while its window is open as the catch-all
317 // real-time channel for deletes that don't render an admin
318 // footer (REST/AJAX/other tabs/WP-CLI). See
319 // `src/recycle-bin/realtime.ts` for the subscriber.
320 array( 'wp-i18n', 'heartbeat', 'jquery' ),
321 file_exists( $recycle_bin_js ) ? (string) filemtime( $recycle_bin_js ) : $version,
322 true
323 );
324 wp_set_script_translations(
325 'desktop-mode-recycle-bin',
326 'desktop-mode',
327 DESKTOP_MODE_DIR . 'languages'
328 );
329
330 // `desktop-mode-games` — bundle for the Games hub native window
331 // (launcher grid, scoreboard, challenges client). Lazy-loaded by
332 // the native-window sync the first time the hub opens; registers a
333 // render callback on
334 // `window.desktopModeNativeWindows['desktop-mode-games']`.
335 // `heartbeat` + `jquery` — the challenges client rides the
336 // WordPress Heartbeat bus for live delivery.
337 $games_js = DESKTOP_MODE_DIR . 'assets/js/games' . $suffix . '.js';
338 wp_register_script(
339 'desktop-mode-games',
340 DESKTOP_MODE_URL . 'assets/js/games' . $suffix . '.js',
341 array( 'wp-i18n', 'heartbeat', 'jquery' ),
342 file_exists( $games_js ) ? (string) filemtime( $games_js ) : $version,
343 true
344 );
345 wp_set_script_translations(
346 'desktop-mode-games',
347 'desktop-mode',
348 DESKTOP_MODE_DIR . 'languages'
349 );
350
351 // `desktop-mode-game-inkfall` — the Inkfall game bundle. Loaded
352 // lazily by the games framework on first launch; publishes the
353 // game def on `window.desktopModeGames.inkfall`.
354 $game_inkfall_js = DESKTOP_MODE_DIR . 'assets/js/game-inkfall' . $suffix . '.js';
355 wp_register_script(
356 'desktop-mode-game-inkfall',
357 DESKTOP_MODE_URL . 'assets/js/game-inkfall' . $suffix . '.js',
358 array( 'wp-i18n' ),
359 file_exists( $game_inkfall_js ) ? (string) filemtime( $game_inkfall_js ) : $version,
360 true
361 );
362 wp_set_script_translations(
363 'desktop-mode-game-inkfall',
364 'desktop-mode',
365 DESKTOP_MODE_DIR . 'languages'
366 );
367
368 // `desktop-mode-game-alphabet-soup` — the Alphabet Soup game
369 // bundle. Loaded lazily by the games framework on first launch;
370 // publishes the game def on
371 // `window.desktopModeGames['alphabet-soup']`.
372 $game_alphabet_soup_js = DESKTOP_MODE_DIR . 'assets/js/game-alphabet-soup' . $suffix . '.js';
373 wp_register_script(
374 'desktop-mode-game-alphabet-soup',
375 DESKTOP_MODE_URL . 'assets/js/game-alphabet-soup' . $suffix . '.js',
376 array( 'wp-i18n' ),
377 file_exists( $game_alphabet_soup_js ) ? (string) filemtime( $game_alphabet_soup_js ) : $version,
378 true
379 );
380 wp_set_script_translations(
381 'desktop-mode-game-alphabet-soup',
382 'desktop-mode',
383 DESKTOP_MODE_DIR . 'languages'
384 );
385
386 // `desktop-mode-posts-window` — small bundle for the native Posts
387 // window. Lazy-loaded by the native-window sync the first time the
388 // window opens (via the dock-click swap when the user opts in);
389 // registers a render callback on
390 // `window.desktopModeNativeWindows['desktop-mode-posts']`.
391 $posts_window_js = DESKTOP_MODE_DIR . 'assets/js/posts-window' . $suffix . '.js';
392 wp_register_script(
393 'desktop-mode-posts-window',
394 DESKTOP_MODE_URL . 'assets/js/posts-window' . $suffix . '.js',
395 array( 'wp-i18n' ),
396 file_exists( $posts_window_js ) ? (string) filemtime( $posts_window_js ) : $version,
397 true
398 );
399 wp_set_script_translations(
400 'desktop-mode-posts-window',
401 'desktop-mode',
402 DESKTOP_MODE_DIR . 'languages'
403 );
404
405 // `desktop-mode-plugins-window` — small bundle for the native
406 // Plugins window. Lazy-loaded by the native-window sync the first
407 // time the window opens (via the dock-click swap when the user
408 // opts in); registers a render callback on
409 // `window.desktopModeNativeWindows['desktop-mode-plugins']`.
410 $plugins_window_js = DESKTOP_MODE_DIR . 'assets/js/plugins-window' . $suffix . '.js';
411 wp_register_script(
412 'desktop-mode-plugins-window',
413 DESKTOP_MODE_URL . 'assets/js/plugins-window' . $suffix . '.js',
414 array( 'wp-i18n' ),
415 file_exists( $plugins_window_js ) ? (string) filemtime( $plugins_window_js ) : $version,
416 true
417 );
418 wp_set_script_translations(
419 'desktop-mode-plugins-window',
420 'desktop-mode',
421 DESKTOP_MODE_DIR . 'languages'
422 );
423
424 // `desktop-mode-comments-window` — small bundle for the native
425 // Comments window. Lazy-loaded by the native-window sync the first
426 // time the window opens (via the dock-click swap when the user
427 // opts in); registers a render callback on
428 // `window.desktopModeNativeWindows['desktop-mode-comments']`.
429 $comments_window_js = DESKTOP_MODE_DIR . 'assets/js/comments-window' . $suffix . '.js';
430 wp_register_script(
431 'desktop-mode-comments-window',
432 DESKTOP_MODE_URL . 'assets/js/comments-window' . $suffix . '.js',
433 array( 'wp-i18n' ),
434 file_exists( $comments_window_js ) ? (string) filemtime( $comments_window_js ) : $version,
435 true
436 );
437 wp_set_script_translations(
438 'desktop-mode-comments-window',
439 'desktop-mode',
440 DESKTOP_MODE_DIR . 'languages'
441 );
442
443 // `desktop-mode-animated-logo-wallpaper` — built-in PixiJS canvas
444 // wallpaper, moved out of `desktop.min.js` in 0.8.4. The wallpaper
445 // `server-sync` loads this handle when the user selects the
446 // `wp-animated-logo` wallpaper (or opens OS Settings → Wallpaper
447 // and the picker pulls every registered canvas def in). The
448 // bundle's only side effect is publishing the `WallpaperDef` on
449 // `window.desktopModeWallpapers['wp-animated-logo']`.
450 $animated_logo_js = DESKTOP_MODE_DIR . 'assets/js/animated-logo-wallpaper' . $suffix . '.js';
451 wp_register_script(
452 'desktop-mode-animated-logo-wallpaper',
453 DESKTOP_MODE_URL . 'assets/js/animated-logo-wallpaper' . $suffix . '.js',
454 array( 'wp-hooks' ),
455 file_exists( $animated_logo_js ) ? (string) filemtime( $animated_logo_js ) : $version,
456 true
457 );
458
459 // `desktop-mode-snow-wallpaper` — built-in PixiJS canvas
460 // wallpaper: snowfall that accumulates on window tops and melts
461 // away. Same lazy-load path as the animated logo: the wallpaper
462 // `server-sync` injects this handle when the user selects the
463 // `wp-snow` wallpaper (or opens OS Settings → Wallpaper and the
464 // picker pulls the def in). The bundle's only side effect is
465 // publishing the `WallpaperDef` on
466 // `window.desktopModeWallpapers['wp-snow']`.
467 $snow_js = DESKTOP_MODE_DIR . 'assets/js/snow-wallpaper' . $suffix . '.js';
468 wp_register_script(
469 'desktop-mode-snow-wallpaper',
470 DESKTOP_MODE_URL . 'assets/js/snow-wallpaper' . $suffix . '.js',
471 array( 'wp-hooks', 'wp-i18n' ),
472 file_exists( $snow_js ) ? (string) filemtime( $snow_js ) : $version,
473 true
474 );
475 wp_set_script_translations(
476 'desktop-mode-snow-wallpaper',
477 'desktop-mode',
478 DESKTOP_MODE_DIR . 'languages'
479 );
480
481 // `desktop-mode-ai-assistant` — AI Copilot spotlight overlay,
482 // moved out of `desktop.min.js` in 0.8.4. The main bundle ships a
483 // stub matching the public `wp.desktop.ai` contract; the stub
484 // `<script>`-injects this handle the first time the user opens
485 // the assistant (Cmd+K or admin-bar button).
486 $ai_assistant_js = DESKTOP_MODE_DIR . 'assets/js/ai-assistant' . $suffix . '.js';
487 wp_register_script(
488 'desktop-mode-ai-assistant',
489 DESKTOP_MODE_URL . 'assets/js/ai-assistant' . $suffix . '.js',
490 array( 'wp-hooks', 'wp-i18n' ),
491 file_exists( $ai_assistant_js ) ? (string) filemtime( $ai_assistant_js ) : $version,
492 true
493 );
494 wp_set_script_translations(
495 'desktop-mode-ai-assistant',
496 'desktop-mode',
497 DESKTOP_MODE_DIR . 'languages'
498 );
499
500 // Wire the translation bundle to this script handle. WP looks
501 // for `languages/desktop-mode-{locale}-desktop-mode.json` and
502 // injects its `locale_data` into `wp.i18n` just before the
503 // script runs — so every `__()` call resolves to the right
504 // language without any runtime fetch.
505 wp_set_script_translations(
506 'desktop-mode',
507 'desktop-mode',
508 DESKTOP_MODE_DIR . 'languages'
509 );
510 }
511 add_action( 'init', 'desktop_mode_register_assets' );
512