@the-inclusionist/engine 9.0.0 → 11.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (532) hide show
  1. package/README.md +120 -78
  2. package/app/css/style.css +761 -377
  3. package/app/public/vendor/fonts/playwrite-cu.woff2 +0 -0
  4. package/app/public/vendor/fonts/playwrite-es-deco.woff2 +0 -0
  5. package/app/public/vendor/fonts/playwrite-es.woff2 +0 -0
  6. package/app/public/vendor/fonts/playwrite-gb-j.woff2 +0 -0
  7. package/app/public/vendor/fonts/playwrite-gb-s.woff2 +0 -0
  8. package/app/public/vendor/fonts/playwrite-pe.woff2 +0 -0
  9. package/app/public/vendor/fonts/playwrite-pt.woff2 +0 -0
  10. package/app/public/vendor/fonts-licences/NOTICE.txt +166 -0
  11. package/app/public/vendor/fonts-licences/OFL-1.1.txt +86 -0
  12. package/app/public/vendor/fonts.css +36 -84
  13. package/dist-pkg/boot/create-game.d.ts +499 -219
  14. package/dist-pkg/boot/create-game.js +3944 -562
  15. package/dist-pkg/core/a11y-sr.d.ts +25 -3
  16. package/dist-pkg/core/a11y-sr.js +38 -19
  17. package/dist-pkg/core/accessible-label.d.ts +17 -0
  18. package/dist-pkg/core/accessible-label.js +37 -0
  19. package/dist-pkg/core/accommodation-subjects.d.ts +26 -0
  20. package/dist-pkg/core/accommodation-subjects.js +32 -0
  21. package/dist-pkg/core/accommodations.d.ts +125 -0
  22. package/dist-pkg/core/accommodations.js +195 -0
  23. package/dist-pkg/core/actions.d.ts +108 -55
  24. package/dist-pkg/core/actions.js +122 -71
  25. package/dist-pkg/core/calm-mode.d.ts +31 -0
  26. package/dist-pkg/core/calm-mode.js +47 -0
  27. package/dist-pkg/core/camera-cycle.d.ts +15 -0
  28. package/dist-pkg/core/camera-cycle.js +7 -0
  29. package/dist-pkg/core/caption-duration.d.ts +15 -0
  30. package/dist-pkg/core/caption-duration.js +19 -0
  31. package/dist-pkg/core/cartridge-problems.d.ts +34 -0
  32. package/dist-pkg/core/cartridge-problems.js +53 -0
  33. package/dist-pkg/core/constants.d.ts +0 -54
  34. package/dist-pkg/core/constants.js +9 -76
  35. package/dist-pkg/core/contract.d.ts +142 -164
  36. package/dist-pkg/core/contract.js +222 -208
  37. package/dist-pkg/core/dom-query.d.ts +4 -4
  38. package/dist-pkg/core/dom-query.js +12 -20
  39. package/dist-pkg/core/entity.d.ts +62 -77
  40. package/dist-pkg/core/entity.js +18 -32
  41. package/dist-pkg/core/escape-html.d.ts +9 -10
  42. package/dist-pkg/core/escape-html.js +16 -20
  43. package/dist-pkg/core/flash-threshold.d.ts +17 -0
  44. package/dist-pkg/core/flash-threshold.js +113 -0
  45. package/dist-pkg/core/game-speed.d.ts +11 -0
  46. package/dist-pkg/core/game-speed.js +16 -0
  47. package/dist-pkg/core/genres.d.ts +20 -0
  48. package/dist-pkg/core/genres.js +44 -0
  49. package/dist-pkg/core/i18n.d.ts +71 -60
  50. package/dist-pkg/core/i18n.js +186 -200
  51. package/dist-pkg/core/loop.d.ts +17 -6
  52. package/dist-pkg/core/loop.js +26 -26
  53. package/dist-pkg/core/pause-icon-catalogue.d.ts +15 -0
  54. package/dist-pkg/core/pause-icon-catalogue.js +54 -0
  55. package/dist-pkg/core/ring.d.ts +17 -0
  56. package/dist-pkg/core/ring.js +29 -0
  57. package/dist-pkg/core/rng.d.ts +7 -12
  58. package/dist-pkg/core/rng.js +23 -53
  59. package/dist-pkg/core/route.d.ts +26 -26
  60. package/dist-pkg/core/route.js +126 -121
  61. package/dist-pkg/core/scenes.d.ts +37 -40
  62. package/dist-pkg/core/scenes.js +41 -43
  63. package/dist-pkg/core/screens.d.ts +4 -4
  64. package/dist-pkg/core/screens.js +11 -16
  65. package/dist-pkg/core/session-clock.d.ts +23 -0
  66. package/dist-pkg/core/session-clock.js +37 -0
  67. package/dist-pkg/core/setting-defaults.d.ts +61 -0
  68. package/dist-pkg/core/setting-defaults.js +66 -0
  69. package/dist-pkg/core/speech-rate.d.ts +29 -0
  70. package/dist-pkg/core/speech-rate.js +47 -0
  71. package/dist-pkg/core/state.d.ts +118 -118
  72. package/dist-pkg/core/state.js +322 -297
  73. package/dist-pkg/core/visual-cycles.d.ts +30 -0
  74. package/dist-pkg/core/visual-cycles.js +52 -0
  75. package/dist-pkg/core/visual-state.d.ts +13 -0
  76. package/dist-pkg/core/visual-state.js +6 -0
  77. package/dist-pkg/educational/activities-registry.d.ts +1 -1
  78. package/dist-pkg/educational/activities-registry.js +9 -8
  79. package/dist-pkg/educational/adaptive-engine.d.ts +11 -11
  80. package/dist-pkg/educational/adaptive-engine.js +17 -17
  81. package/dist-pkg/educational/segment-bar.d.ts +12 -12
  82. package/dist-pkg/educational/segment-bar.js +9 -9
  83. package/dist-pkg/i18n/en.js +324 -223
  84. package/dist-pkg/i18n/es.js +323 -223
  85. package/dist-pkg/i18n/pt.js +348 -226
  86. package/dist-pkg/input/default-bindings.d.ts +37 -38
  87. package/dist-pkg/input/default-bindings.js +127 -144
  88. package/dist-pkg/input/devices.d.ts +7 -7
  89. package/dist-pkg/input/devices.js +17 -16
  90. package/dist-pkg/input/edges.d.ts +23 -32
  91. package/dist-pkg/input/edges.js +23 -28
  92. package/dist-pkg/input/empathy-filter.d.ts +22 -0
  93. package/dist-pkg/input/empathy-filter.js +34 -0
  94. package/dist-pkg/input/face-map.d.ts +96 -0
  95. package/dist-pkg/input/face-map.js +255 -0
  96. package/dist-pkg/input/face-signals.d.ts +40 -0
  97. package/dist-pkg/input/face-signals.js +48 -0
  98. package/dist-pkg/input/gamepad.d.ts +195 -145
  99. package/dist-pkg/input/gamepad.js +383 -556
  100. package/dist-pkg/input/gaze-cycle.d.ts +64 -0
  101. package/dist-pkg/input/gaze-cycle.js +133 -0
  102. package/dist-pkg/input/gaze-relative.d.ts +68 -0
  103. package/dist-pkg/input/gaze-relative.js +142 -0
  104. package/dist-pkg/input/hand-map.d.ts +28 -0
  105. package/dist-pkg/input/hand-map.js +142 -0
  106. package/dist-pkg/input/input-cooldown.d.ts +17 -0
  107. package/dist-pkg/input/input-cooldown.js +39 -0
  108. package/dist-pkg/input/key-default.d.ts +30 -0
  109. package/dist-pkg/input/key-default.js +60 -0
  110. package/dist-pkg/input/keyboard-runtime.d.ts +15 -21
  111. package/dist-pkg/input/keyboard-runtime.js +23 -27
  112. package/dist-pkg/input/keyboard.d.ts +68 -63
  113. package/dist-pkg/input/keyboard.js +83 -105
  114. package/dist-pkg/input/keydown.d.ts +123 -137
  115. package/dist-pkg/input/keydown.js +284 -289
  116. package/dist-pkg/input/latch-edge.d.ts +33 -21
  117. package/dist-pkg/input/latch-edge.js +34 -35
  118. package/dist-pkg/input/latch-scope.d.ts +51 -50
  119. package/dist-pkg/input/latch-scope.js +61 -82
  120. package/dist-pkg/input/latch-store.d.ts +26 -30
  121. package/dist-pkg/input/latch-store.js +42 -50
  122. package/dist-pkg/input/latch-sync.d.ts +29 -29
  123. package/dist-pkg/input/latch-sync.js +25 -25
  124. package/dist-pkg/input/latch.d.ts +14 -17
  125. package/dist-pkg/input/latch.js +29 -37
  126. package/dist-pkg/input/pad-defaults.d.ts +13 -16
  127. package/dist-pkg/input/pad-defaults.js +21 -28
  128. package/dist-pkg/input/pad-reading.d.ts +76 -0
  129. package/dist-pkg/input/pad-reading.js +169 -0
  130. package/dist-pkg/input/pad-wizard.d.ts +99 -0
  131. package/dist-pkg/input/pad-wizard.js +256 -0
  132. package/dist-pkg/input/pointer-space.d.ts +19 -20
  133. package/dist-pkg/input/pointer-space.js +27 -32
  134. package/dist-pkg/input/pointer.d.ts +39 -40
  135. package/dist-pkg/input/pointer.js +42 -45
  136. package/dist-pkg/input/state.d.ts +101 -89
  137. package/dist-pkg/input/state.js +54 -134
  138. package/dist-pkg/input/switch-scan.d.ts +59 -0
  139. package/dist-pkg/input/switch-scan.js +67 -0
  140. package/dist-pkg/input/synthetic-source.d.ts +42 -0
  141. package/dist-pkg/input/synthetic-source.js +57 -0
  142. package/dist-pkg/input/touch-bindings.d.ts +127 -116
  143. package/dist-pkg/input/touch-bindings.js +158 -173
  144. package/dist-pkg/input/touch.d.ts +147 -72
  145. package/dist-pkg/input/touch.js +362 -126
  146. package/dist-pkg/input/transport-in-use.d.ts +107 -0
  147. package/dist-pkg/input/transport-in-use.js +130 -0
  148. package/dist-pkg/input/transports.d.ts +90 -117
  149. package/dist-pkg/input/transports.js +104 -106
  150. package/dist-pkg/input/virtual-controller.d.ts +82 -0
  151. package/dist-pkg/input/virtual-controller.js +69 -0
  152. package/dist-pkg/input/vocabulary-migration.d.ts +50 -49
  153. package/dist-pkg/input/vocabulary-migration.js +80 -79
  154. package/dist-pkg/input/voice-map.d.ts +59 -0
  155. package/dist-pkg/input/voice-map.js +123 -0
  156. package/dist-pkg/platform/audio-ambient.d.ts +13 -2
  157. package/dist-pkg/platform/audio-ambient.js +14 -14
  158. package/dist-pkg/platform/audio-earcons.d.ts +20 -19
  159. package/dist-pkg/platform/audio-earcons.js +51 -42
  160. package/dist-pkg/platform/audio-jingles.js +9 -10
  161. package/dist-pkg/platform/audio-mixer.d.ts +14 -2
  162. package/dist-pkg/platform/audio-mixer.js +31 -39
  163. package/dist-pkg/platform/audio-sonar.d.ts +49 -152
  164. package/dist-pkg/platform/audio-sonar.js +124 -302
  165. package/dist-pkg/platform/audio.d.ts +62 -19
  166. package/dist-pkg/platform/audio.js +193 -168
  167. package/dist-pkg/platform/choose-by-voice.d.ts +70 -0
  168. package/dist-pkg/platform/choose-by-voice.js +157 -0
  169. package/dist-pkg/platform/flash-sampler.d.ts +19 -0
  170. package/dist-pkg/platform/flash-sampler.js +79 -0
  171. package/dist-pkg/platform/font-library.d.ts +77 -0
  172. package/dist-pkg/platform/font-library.js +98 -0
  173. package/dist-pkg/platform/font-library.json +721 -0
  174. package/dist-pkg/platform/heavy-catalogue.d.ts +77 -0
  175. package/dist-pkg/platform/heavy-catalogue.js +256 -0
  176. package/dist-pkg/platform/heavy-mirror.d.ts +20 -0
  177. package/dist-pkg/platform/heavy-mirror.js +73 -0
  178. package/dist-pkg/platform/heavy.d.ts +136 -0
  179. package/dist-pkg/platform/heavy.js +324 -0
  180. package/dist-pkg/platform/interruptible-speech.d.ts +23 -16
  181. package/dist-pkg/platform/interruptible-speech.js +77 -54
  182. package/dist-pkg/platform/kokoro-port.d.ts +27 -0
  183. package/dist-pkg/platform/kokoro-port.js +50 -0
  184. package/dist-pkg/platform/kokoro-runtime.d.ts +20 -0
  185. package/dist-pkg/platform/kokoro-runtime.js +52 -0
  186. package/dist-pkg/platform/kokoro.d.ts +70 -0
  187. package/dist-pkg/platform/kokoro.js +133 -0
  188. package/dist-pkg/platform/listener-scope.d.ts +12 -0
  189. package/dist-pkg/platform/listener-scope.js +90 -0
  190. package/dist-pkg/platform/locale-host.d.ts +14 -0
  191. package/dist-pkg/platform/locale-host.js +45 -0
  192. package/dist-pkg/platform/microphone.d.ts +51 -0
  193. package/dist-pkg/platform/microphone.js +118 -0
  194. package/dist-pkg/platform/onnx-runtime.d.ts +34 -0
  195. package/dist-pkg/platform/onnx-runtime.js +42 -0
  196. package/dist-pkg/platform/reading-in-worker.d.ts +64 -0
  197. package/dist-pkg/platform/reading-in-worker.js +125 -0
  198. package/dist-pkg/platform/reading-model.d.ts +94 -0
  199. package/dist-pkg/platform/reading-model.js +272 -0
  200. package/dist-pkg/platform/reading-runtime.d.ts +24 -0
  201. package/dist-pkg/platform/reading-runtime.js +164 -0
  202. package/dist-pkg/platform/reading-worker.d.ts +41 -0
  203. package/dist-pkg/platform/reading-worker.js +58 -0
  204. package/dist-pkg/platform/reading.d.ts +80 -0
  205. package/dist-pkg/platform/reading.js +136 -0
  206. package/dist-pkg/platform/speech-recognition.d.ts +60 -0
  207. package/dist-pkg/platform/speech-recognition.js +104 -0
  208. package/dist-pkg/platform/speech.d.ts +23 -1
  209. package/dist-pkg/platform/speech.js +29 -18
  210. package/dist-pkg/platform/storage-keys.d.ts +96 -0
  211. package/dist-pkg/platform/storage-keys.js +106 -0
  212. package/dist-pkg/platform/storage.d.ts +46 -111
  213. package/dist-pkg/platform/storage.js +104 -169
  214. package/dist-pkg/platform/tts.d.ts +85 -39
  215. package/dist-pkg/platform/tts.js +447 -127
  216. package/dist-pkg/platform/vision-loop.d.ts +23 -0
  217. package/dist-pkg/platform/vision-loop.js +56 -0
  218. package/dist-pkg/platform/vision.d.ts +132 -0
  219. package/dist-pkg/platform/vision.js +117 -0
  220. package/dist-pkg/platform/voice-listener.d.ts +40 -0
  221. package/dist-pkg/platform/voice-listener.js +88 -0
  222. package/dist-pkg/platform/voice-plan.d.ts +15 -98
  223. package/dist-pkg/platform/voice-plan.js +56 -143
  224. package/dist-pkg/platform/vosk-runtime.d.ts +96 -0
  225. package/dist-pkg/platform/vosk-runtime.js +151 -0
  226. package/dist-pkg/platform/vosk-vocabulary.d.ts +6 -0
  227. package/dist-pkg/platform/vosk-vocabulary.js +178 -0
  228. package/dist-pkg/render/canvas.d.ts +13 -7
  229. package/dist-pkg/render/canvas.js +13 -13
  230. package/dist-pkg/render/crt.d.ts +48 -12
  231. package/dist-pkg/render/crt.js +78 -80
  232. package/dist-pkg/render/cvd-matrices.d.ts +22 -18
  233. package/dist-pkg/render/cvd-matrices.js +37 -38
  234. package/dist-pkg/render/hc-role-data.d.ts +7 -7
  235. package/dist-pkg/render/hc-role-data.js +14 -14
  236. package/dist-pkg/render/high-contrast.d.ts +67 -56
  237. package/dist-pkg/render/high-contrast.js +226 -217
  238. package/dist-pkg/render/low-vision-drawing.d.ts +5 -0
  239. package/dist-pkg/render/low-vision-drawing.js +46 -0
  240. package/dist-pkg/render/lq-filter.d.ts +32 -20
  241. package/dist-pkg/render/lq-filter.js +59 -65
  242. package/dist-pkg/render/port.d.ts +86 -88
  243. package/dist-pkg/render/port.js +17 -23
  244. package/dist-pkg/render/screen-pipeline.d.ts +42 -42
  245. package/dist-pkg/render/screen-pipeline.js +80 -87
  246. package/dist-pkg/render/sprite-fx.d.ts +2 -1
  247. package/dist-pkg/render/sprite-fx.js +16 -14
  248. package/dist-pkg/render/viewports.d.ts +20 -15
  249. package/dist-pkg/render/viewports.js +83 -114
  250. package/dist-pkg/render/viz-axes-labels.d.ts +47 -0
  251. package/dist-pkg/render/viz-axes-labels.js +88 -0
  252. package/dist-pkg/render/viz-axes.d.ts +89 -86
  253. package/dist-pkg/render/viz-axes.js +128 -116
  254. package/dist-pkg/render/viz-modes.d.ts +28 -29
  255. package/dist-pkg/render/viz-modes.js +40 -45
  256. package/dist-pkg/render/viz-refusal.d.ts +32 -0
  257. package/dist-pkg/render/viz-refusal.js +57 -0
  258. package/dist-pkg/render/viz-setters.d.ts +104 -66
  259. package/dist-pkg/render/viz-setters.js +182 -191
  260. package/dist-pkg/ui/aac-sets.d.ts +45 -0
  261. package/dist-pkg/ui/aac-sets.js +41 -0
  262. package/dist-pkg/ui/audio-choices.d.ts +68 -0
  263. package/dist-pkg/ui/audio-choices.js +98 -0
  264. package/dist-pkg/ui/audio-rows-that-apply.d.ts +12 -0
  265. package/dist-pkg/ui/audio-rows-that-apply.js +43 -0
  266. package/dist-pkg/ui/camera-control.d.ts +53 -0
  267. package/dist-pkg/ui/camera-control.js +48 -0
  268. package/dist-pkg/ui/changed-mark.d.ts +9 -8
  269. package/dist-pkg/ui/changed-mark.js +22 -53
  270. package/dist-pkg/ui/control-choices.d.ts +37 -0
  271. package/dist-pkg/ui/control-choices.js +84 -0
  272. package/dist-pkg/ui/debug-panel.d.ts +59 -43
  273. package/dist-pkg/ui/debug-panel.js +68 -69
  274. package/dist-pkg/ui/declared-words.d.ts +21 -0
  275. package/dist-pkg/ui/declared-words.js +66 -0
  276. package/dist-pkg/ui/dom.d.ts +10 -14
  277. package/dist-pkg/ui/dom.js +10 -47
  278. package/dist-pkg/ui/drawing-problems.d.ts +25 -0
  279. package/dist-pkg/ui/drawing-problems.js +94 -0
  280. package/dist-pkg/ui/eye-control.d.ts +36 -0
  281. package/dist-pkg/ui/eye-control.js +178 -0
  282. package/dist-pkg/ui/face-control.d.ts +25 -0
  283. package/dist-pkg/ui/face-control.js +183 -0
  284. package/dist-pkg/ui/focus-trap.d.ts +39 -49
  285. package/dist-pkg/ui/focus-trap.js +56 -65
  286. package/dist-pkg/ui/fonts.d.ts +157 -68
  287. package/dist-pkg/ui/fonts.js +305 -115
  288. package/dist-pkg/ui/footer-scroll-driver.d.ts +14 -0
  289. package/dist-pkg/ui/footer-scroll-driver.js +201 -0
  290. package/dist-pkg/ui/footer-scroll.d.ts +38 -0
  291. package/dist-pkg/ui/footer-scroll.js +55 -0
  292. package/dist-pkg/ui/game-options.d.ts +47 -0
  293. package/dist-pkg/ui/game-options.js +158 -0
  294. package/dist-pkg/ui/gaze-overlay.d.ts +64 -0
  295. package/dist-pkg/ui/gaze-overlay.js +165 -0
  296. package/dist-pkg/ui/hand-control.d.ts +25 -0
  297. package/dist-pkg/ui/hand-control.js +143 -0
  298. package/dist-pkg/ui/help-panel.d.ts +87 -0
  299. package/dist-pkg/ui/help-panel.js +242 -0
  300. package/dist-pkg/ui/hud-bands.d.ts +62 -0
  301. package/dist-pkg/ui/hud-bands.js +146 -0
  302. package/dist-pkg/ui/hud-row.d.ts +34 -0
  303. package/dist-pkg/ui/hud-row.js +80 -0
  304. package/dist-pkg/ui/hud.d.ts +73 -73
  305. package/dist-pkg/ui/hud.js +94 -98
  306. package/dist-pkg/ui/item-announcement.d.ts +13 -12
  307. package/dist-pkg/ui/item-announcement.js +25 -26
  308. package/dist-pkg/ui/layout.d.ts +103 -28
  309. package/dist-pkg/ui/layout.js +117 -99
  310. package/dist-pkg/ui/libras-avatar-clip.d.ts +30 -0
  311. package/dist-pkg/ui/libras-avatar-clip.js +86 -0
  312. package/dist-pkg/ui/libras-avatar-load.d.ts +52 -0
  313. package/dist-pkg/ui/libras-avatar-load.js +134 -0
  314. package/dist-pkg/ui/libras-avatar-plan.d.ts +102 -0
  315. package/dist-pkg/ui/libras-avatar-plan.js +211 -0
  316. package/dist-pkg/ui/libras-avatar-player.d.ts +28 -0
  317. package/dist-pkg/ui/libras-avatar-player.js +226 -0
  318. package/dist-pkg/ui/libras-avatar-stage.d.ts +37 -0
  319. package/dist-pkg/ui/libras-avatar-stage.js +208 -0
  320. package/dist-pkg/ui/libras-glosses.d.ts +28 -0
  321. package/dist-pkg/ui/libras-glosses.js +161 -0
  322. package/dist-pkg/ui/locale-flags.d.ts +16 -0
  323. package/dist-pkg/ui/locale-flags.js +30 -0
  324. package/dist-pkg/ui/loop-crash.d.ts +17 -17
  325. package/dist-pkg/ui/loop-crash.js +33 -62
  326. package/dist-pkg/ui/menu-intent.d.ts +61 -0
  327. package/dist-pkg/ui/menu-intent.js +88 -0
  328. package/dist-pkg/ui/menu-items.d.ts +20 -0
  329. package/dist-pkg/ui/menu-items.js +51 -0
  330. package/dist-pkg/ui/menu-nav.d.ts +98 -92
  331. package/dist-pkg/ui/menu-nav.js +398 -286
  332. package/dist-pkg/ui/mobility-choices.d.ts +26 -0
  333. package/dist-pkg/ui/mobility-choices.js +41 -0
  334. package/dist-pkg/ui/motion-choices.d.ts +31 -0
  335. package/dist-pkg/ui/motion-choices.js +68 -0
  336. package/dist-pkg/ui/motion-scene.d.ts +24 -19
  337. package/dist-pkg/ui/motion-scene.js +29 -61
  338. package/dist-pkg/ui/mount-panel.d.ts +81 -0
  339. package/dist-pkg/ui/mount-panel.js +61 -0
  340. package/dist-pkg/ui/panel-shell.d.ts +65 -31
  341. package/dist-pkg/ui/panel-shell.js +92 -66
  342. package/dist-pkg/ui/panel-widgets.d.ts +150 -0
  343. package/dist-pkg/ui/panel-widgets.js +294 -0
  344. package/dist-pkg/ui/pause-buttons.d.ts +35 -0
  345. package/dist-pkg/ui/pause-buttons.js +66 -0
  346. package/dist-pkg/ui/pause-icons.d.ts +261 -347
  347. package/dist-pkg/ui/pause-icons.js +671 -601
  348. package/dist-pkg/ui/pause-markup.d.ts +86 -0
  349. package/dist-pkg/ui/pause-markup.js +125 -0
  350. package/dist-pkg/ui/reach-notice.d.ts +22 -23
  351. package/dist-pkg/ui/reach-notice.js +55 -58
  352. package/dist-pkg/ui/scan-overlay.d.ts +23 -0
  353. package/dist-pkg/ui/scan-overlay.js +50 -0
  354. package/dist-pkg/ui/screen-text.d.ts +25 -0
  355. package/dist-pkg/ui/screen-text.js +152 -0
  356. package/dist-pkg/ui/session-clock.d.ts +32 -0
  357. package/dist-pkg/ui/session-clock.js +67 -0
  358. package/dist-pkg/ui/settings-aac.d.ts +70 -0
  359. package/dist-pkg/ui/settings-aac.js +208 -0
  360. package/dist-pkg/ui/settings-audio.d.ts +97 -114
  361. package/dist-pkg/ui/settings-audio.js +493 -425
  362. package/dist-pkg/ui/settings-controls.d.ts +53 -96
  363. package/dist-pkg/ui/settings-controls.js +143 -215
  364. package/dist-pkg/ui/settings-empathy.d.ts +39 -30
  365. package/dist-pkg/ui/settings-empathy.js +53 -60
  366. package/dist-pkg/ui/settings-mobility.d.ts +192 -0
  367. package/dist-pkg/ui/settings-mobility.js +332 -0
  368. package/dist-pkg/ui/settings-motion.d.ts +91 -70
  369. package/dist-pkg/ui/settings-motion.js +287 -198
  370. package/dist-pkg/ui/settings-panel.d.ts +58 -45
  371. package/dist-pkg/ui/settings-panel.js +189 -94
  372. package/dist-pkg/ui/settings-typo.d.ts +29 -78
  373. package/dist-pkg/ui/settings-typo.js +158 -129
  374. package/dist-pkg/ui/settings-visual.d.ts +29 -87
  375. package/dist-pkg/ui/settings-visual.js +240 -193
  376. package/dist-pkg/ui/shell.d.ts +112 -135
  377. package/dist-pkg/ui/shell.js +148 -217
  378. package/dist-pkg/ui/simulation-list.d.ts +27 -0
  379. package/dist-pkg/ui/simulation-list.js +76 -0
  380. package/dist-pkg/ui/simulation-over-the-world.d.ts +23 -0
  381. package/dist-pkg/ui/simulation-over-the-world.js +131 -0
  382. package/dist-pkg/ui/simulation-refusal.d.ts +5 -31
  383. package/dist-pkg/ui/simulation-refusal.js +8 -56
  384. package/dist-pkg/ui/switchable-control.d.ts +11 -0
  385. package/dist-pkg/ui/switchable-control.js +19 -0
  386. package/dist-pkg/ui/title.d.ts +3 -12
  387. package/dist-pkg/ui/title.js +49 -18
  388. package/dist-pkg/ui/top-band.d.ts +26 -0
  389. package/dist-pkg/ui/top-band.js +96 -0
  390. package/dist-pkg/ui/typo-choices.d.ts +71 -0
  391. package/dist-pkg/ui/typo-choices.js +85 -0
  392. package/dist-pkg/ui/visual-axes-panel.d.ts +11 -47
  393. package/dist-pkg/ui/visual-axes-panel.js +12 -94
  394. package/dist-pkg/ui/visual-choices.d.ts +77 -0
  395. package/dist-pkg/ui/visual-choices.js +107 -0
  396. package/dist-pkg/ui/vlibras.d.ts +77 -10
  397. package/dist-pkg/ui/vlibras.js +130 -81
  398. package/dist-pkg/ui/voice-control.d.ts +68 -0
  399. package/dist-pkg/ui/voice-control.js +276 -0
  400. package/dist-pkg/ui/voice-settings.d.ts +95 -0
  401. package/dist-pkg/ui/voice-settings.js +352 -0
  402. package/dist-pkg/ui/where-the-child-is.d.ts +31 -0
  403. package/dist-pkg/ui/where-the-child-is.js +70 -0
  404. package/docs/CREDITS.md +300 -44
  405. package/docs/LICENSES.md +189 -145
  406. package/package.json +51 -27
  407. package/scripts/check-cartridge.mjs +78 -0
  408. package/scripts/game-build.d.mts +25 -0
  409. package/scripts/game-build.mjs +144 -0
  410. package/scripts/heavy-into-the-delivery.mjs +388 -0
  411. package/scripts/libras-avatar.json +680 -0
  412. package/scripts/libras-avatar.mjs +246 -0
  413. package/scripts/libras-glosses/gloss.py +95 -0
  414. package/scripts/libras-glosses/pyproject.toml +32 -0
  415. package/scripts/libras-glosses/uv.lock +781 -0
  416. package/scripts/libras-glosses.mjs +351 -0
  417. package/scripts/licences/Apache-2.0.txt +176 -0
  418. package/scripts/licences/GPL-3.0.txt +674 -0
  419. package/scripts/licences/MIT.txt +17 -0
  420. package/scripts/licences/OFL-1.1.txt +86 -0
  421. package/scripts/licences/UFL-1.0.txt +96 -0
  422. package/scripts/licences/fonts.mjs +389 -0
  423. package/scripts/licences/third-party.mjs +301 -0
  424. package/scripts/licences/vosk-browser.NOTICE.txt +268 -0
  425. package/app/public/vendor/fonts/comicneue-400.woff2 +0 -0
  426. package/app/public/vendor/fonts/comicneue-700.woff2 +0 -0
  427. package/app/public/vendor/fonts/fondamento-400-ext.woff2 +0 -0
  428. package/app/public/vendor/fonts/fondamento-400.woff2 +0 -0
  429. package/app/public/vendor/fonts/inter-400.woff2 +0 -0
  430. package/app/public/vendor/fonts/inter-700.woff2 +0 -0
  431. package/app/public/vendor/fonts/lato-400.woff2 +0 -0
  432. package/app/public/vendor/fonts/lato-700.woff2 +0 -0
  433. package/app/public/vendor/fonts/literata-400.woff2 +0 -0
  434. package/app/public/vendor/fonts/literata-700.woff2 +0 -0
  435. package/app/public/vendor/fonts/newsreader-400.woff2 +0 -0
  436. package/app/public/vendor/fonts/newsreader-700.woff2 +0 -0
  437. package/app/public/vendor/fonts/opendyslexic-400.woff2 +0 -0
  438. package/app/public/vendor/fonts/opensans-400.woff2 +0 -0
  439. package/app/public/vendor/fonts/opensans-700.woff2 +0 -0
  440. package/app/public/vendor/fonts/pinyon-400.woff2 +0 -0
  441. package/app/public/vendor/fonts/pressstart-400.woff2 +0 -0
  442. package/app/public/vendor/fonts/quattro-400.woff2 +0 -0
  443. package/app/public/vendor/fonts/quattro-700.woff2 +0 -0
  444. package/app/public/vendor/fonts/sourcesans-400.woff2 +0 -0
  445. package/app/public/vendor/fonts/sourcesans-700.woff2 +0 -0
  446. package/app/public/vendor/fonts/sourceserif-400.woff2 +0 -0
  447. package/app/public/vendor/fonts/sourceserif-700.woff2 +0 -0
  448. package/app/public/vendor/fonts/ufmag-400.woff2 +0 -0
  449. package/dist-pkg/core/anel.d.ts +0 -17
  450. package/dist-pkg/core/anel.js +0 -31
  451. package/dist-pkg/core/collision.d.ts +0 -21
  452. package/dist-pkg/core/collision.js +0 -62
  453. package/dist-pkg/core/layers.d.ts +0 -63
  454. package/dist-pkg/core/layers.js +0 -95
  455. package/dist-pkg/core/letter-grid.d.ts +0 -55
  456. package/dist-pkg/core/letter-grid.js +0 -116
  457. package/dist-pkg/core/password.d.ts +0 -34
  458. package/dist-pkg/core/password.js +0 -205
  459. package/dist-pkg/core/rotulo-acessivel.d.ts +0 -17
  460. package/dist-pkg/core/rotulo-acessivel.js +0 -41
  461. package/dist-pkg/core/run-state.d.ts +0 -115
  462. package/dist-pkg/core/run-state.js +0 -40
  463. package/dist-pkg/core/tiles.d.ts +0 -12
  464. package/dist-pkg/core/tiles.js +0 -61
  465. package/dist-pkg/core/world.d.ts +0 -4
  466. package/dist-pkg/core/world.js +0 -58
  467. package/dist-pkg/input/origem-sintetica.d.ts +0 -44
  468. package/dist-pkg/input/origem-sintetica.js +0 -60
  469. package/dist-pkg/input/transporte-em-uso.d.ts +0 -101
  470. package/dist-pkg/input/transporte-em-uso.js +0 -130
  471. package/dist-pkg/platform/audio-nav.d.ts +0 -51
  472. package/dist-pkg/platform/audio-nav.js +0 -99
  473. package/dist-pkg/platform/guide-intensity.d.ts +0 -40
  474. package/dist-pkg/platform/guide-intensity.js +0 -73
  475. package/dist-pkg/platform/pesados-catalogo.d.ts +0 -14
  476. package/dist-pkg/platform/pesados-catalogo.js +0 -177
  477. package/dist-pkg/platform/pesados.d.ts +0 -31
  478. package/dist-pkg/platform/pesados.js +0 -75
  479. package/dist-pkg/platform/vozes-prontas.d.ts +0 -28
  480. package/dist-pkg/platform/vozes-prontas.js +0 -61
  481. package/dist-pkg/render/camera.d.ts +0 -58
  482. package/dist-pkg/render/camera.js +0 -105
  483. package/dist-pkg/render/cenario-data.d.ts +0 -127
  484. package/dist-pkg/render/cenario-data.js +0 -145
  485. package/dist-pkg/render/city-tex.d.ts +0 -63
  486. package/dist-pkg/render/city-tex.js +0 -228
  487. package/dist-pkg/render/city-tiles.d.ts +0 -11
  488. package/dist-pkg/render/city-tiles.js +0 -140
  489. package/dist-pkg/render/draw.d.ts +0 -149
  490. package/dist-pkg/render/draw.js +0 -231
  491. package/dist-pkg/render/fx.d.ts +0 -63
  492. package/dist-pkg/render/fx.js +0 -123
  493. package/dist-pkg/render/minimap.d.ts +0 -11
  494. package/dist-pkg/render/minimap.js +0 -94
  495. package/dist-pkg/render/parallax.d.ts +0 -85
  496. package/dist-pkg/render/parallax.js +0 -158
  497. package/dist-pkg/render/player-anim.d.ts +0 -71
  498. package/dist-pkg/render/player-anim.js +0 -152
  499. package/dist-pkg/render/recycling-tex.d.ts +0 -48
  500. package/dist-pkg/render/recycling-tex.js +0 -164
  501. package/dist-pkg/render/scene-city.d.ts +0 -77
  502. package/dist-pkg/render/scene-city.js +0 -181
  503. package/dist-pkg/render/scene-parallax.d.ts +0 -72
  504. package/dist-pkg/render/scene-parallax.js +0 -426
  505. package/dist-pkg/render/scene-sky.d.ts +0 -126
  506. package/dist-pkg/render/scene-sky.js +0 -295
  507. package/dist-pkg/render/set-cenario.d.ts +0 -54
  508. package/dist-pkg/render/set-cenario.js +0 -103
  509. package/dist-pkg/render/textures.d.ts +0 -75
  510. package/dist-pkg/render/textures.js +0 -240
  511. package/dist-pkg/render/title-scene.d.ts +0 -46
  512. package/dist-pkg/render/title-scene.js +0 -75
  513. package/dist-pkg/render/weather.d.ts +0 -96
  514. package/dist-pkg/render/weather.js +0 -171
  515. package/dist-pkg/render/wheelchair-sprites.d.ts +0 -32
  516. package/dist-pkg/render/wheelchair-sprites.js +0 -69
  517. package/dist-pkg/render/world-tex.d.ts +0 -18
  518. package/dist-pkg/render/world-tex.js +0 -87
  519. package/dist-pkg/ui/activities-menu.d.ts +0 -258
  520. package/dist-pkg/ui/activities-menu.js +0 -648
  521. package/dist-pkg/ui/caa-sets.d.ts +0 -39
  522. package/dist-pkg/ui/caa-sets.js +0 -65
  523. package/dist-pkg/ui/latch-refusal.d.ts +0 -32
  524. package/dist-pkg/ui/latch-refusal.js +0 -60
  525. package/dist-pkg/ui/map-hub.d.ts +0 -56
  526. package/dist-pkg/ui/map-hub.js +0 -138
  527. package/dist-pkg/ui/settings-caa.d.ts +0 -45
  528. package/dist-pkg/ui/settings-caa.js +0 -137
  529. package/dist-pkg/ui/settings-motor.d.ts +0 -175
  530. package/dist-pkg/ui/settings-motor.js +0 -342
  531. package/dist-pkg/ui/webcam.d.ts +0 -7
  532. package/dist-pkg/ui/webcam.js +0 -93
@@ -0,0 +1,44 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ const F = (family, names) => names.map((name) => ({ name, family }));
3
+ export const GENRES = Object.freeze([
4
+ ...F('Action', ['Platform games', 'Shooter games', 'First-person shooters', 'Hero shooters', 'Light gun shooters',
5
+ "Shoot 'em ups", 'Fighting games', "Beat 'em up games", 'Stealth games', 'Survival games', 'Rhythm games',
6
+ 'Battle royale games']),
7
+ ...F('Action-adventure', ['Survival horror', 'Metroidvania']),
8
+ ...F('Adventure', ['Text adventures', 'Graphic adventures', 'Visual novels', 'Interactive movie']),
9
+ ...F('Puzzle', ['Breakout clone game', 'Logical game', 'Physics game', 'Programming game', 'Puzzle-platform game',
10
+ 'Hidden object game', 'Reveal the picture game', 'Tile-matching game', 'Traditional puzzle game']),
11
+ ...F('Role-playing', ['Action RPG', 'CRPG', 'MMORPG', 'Roguelikes', 'Tactical RPG', 'Sandbox RPG',
12
+ 'First-person party-based RPG', 'Monster-taming']),
13
+ ...F('Simulation', ['Construction and management simulation', 'Life simulation', 'Vehicle simulation']),
14
+ ...F('Strategy', ['4X game', 'Artillery game', 'Auto battler', 'Auto chess', 'MOBA', 'RTS', 'RTT', 'Tower defense', 'TBS',
15
+ 'TBT', 'Wargame', 'Grand strategy wargame']),
16
+ ...F('Sports', ['Racing', 'Sports game']),
17
+ ...F('MMO', ['MMO']),
18
+ ...F('Other notable genres', ['Board game or card game']),
19
+ { name: 'Casino game', family: 'Other notable genres', mark: 'refused' },
20
+ ...F('Other notable genres', ['Digital collectible card game', 'Digital therapeutic video game', 'Gacha game']),
21
+ { name: 'Horror game', family: 'Other notable genres', mark: 'avoid' },
22
+ ...F('Other notable genres', ['Idle game', 'Party game', 'Photography game', 'Social deduction game', 'Trivia game',
23
+ 'Typing game']),
24
+ ].map((g) => Object.freeze(g)));
25
+ /** Is a declared genre acceptable? EMPTY means conformant; any line refuses the boot (a malformed declaration). */
26
+ export function genreProblems(genre) {
27
+ if (genre === undefined)
28
+ return [];
29
+ if (typeof genre !== 'string')
30
+ return ['genre: must be a genre name from the engine\'s list (ADR-0156), or absent'];
31
+ const g = GENRES.find((x) => x.name === genre);
32
+ if (!g)
33
+ return [`genre: «${genre}» is not in the engine's genre list (ADR-0156) - declare what the game plays like, or no genre`];
34
+ if (g.mark === 'refused')
35
+ return [`genre: Casino game is prohibited in this engine (ADR-0156 §2) - a cartridge that declares it does not boot`];
36
+ return [];
37
+ }
38
+ /** The line `problems` carries for an accepted genre with a mark: Horror game plays, and the author reads why to avoid it. */
39
+ export function genreWarning(genre) {
40
+ const g = typeof genre === 'string' ? GENRES.find((x) => x.name === genre) : undefined;
41
+ return g?.mark === 'avoid'
42
+ ? `genre: Horror game is marked avoid - it is hard to make work for children (ADR-0156 §3); consider what else the game plays like`
43
+ : null;
44
+ }
@@ -1,71 +1,82 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ /** One language's dictionary: a key and its text. */
2
3
  type LocaleDict = Record<string, string>;
4
+ /** The languages a page can switch to — a constant, so it stays a function of the module (ADR-0232 D3). */
5
+ export declare function availableLocales(): string[];
3
6
  /**
4
- * Registra as chaves DESTE jogo para um idioma. Chamável antes de o idioma existir — quem registra `en` antes
5
- * de qualquer `setLocale('en')` é atendido quando a troca acontecer.
6
- *
7
- * ⚠️ NÃO REAPLICA O DOM, de propósito. `applyDom` precisa de uma RAIZ, e alcançar o `document` global por
8
- * baixo de quem chama é o achado 15, que já custou uma correção. Um consumidor que registre depois de o
9
- * markup estático ter sido traduzido chama `applyDom(raiz)` ele mesmo — e o caso normal é registrar no boot,
10
- * antes de existir texto na tela.
11
- */
12
- export declare function registerDict(code: string, entries: LocaleDict): string[];
13
- export declare function t(key: string, params?: Record<string, string | number>): string;
14
- export declare function getLocale(): string;
15
- /**
16
- * A etiqueta BCP-47 do idioma corrente — o que se escreve em `<html lang>` e o que se entrega a APIs do
17
- * navegador que falam (Web Speech) ou comparam idioma.
7
+ * The port the chosen language is kept through (ADR-0178): `platform/storage` has this shape.
18
8
  *
19
- * Só o português precisa de região: 'pt' sozinho deixaria o navegador escolher entre pt-PT e pt-BR, e a
20
- * diferença de prosódia é audível para uma criança brasileira. Inglês e espanhol ficam sem região de
21
- * propósito — o navegador escolhe a variante local, que é o certo, e fixar 'en-US' imporia sotaque americano
22
- * a quem estivesse na Índia ou na Nigéria.
9
+ * 🔴 AND IT CARRIES THE BROWSER TOO (ADR-0221 step 7g): the page effects arrive as optional hooks that
10
+ * `platform/locale-host` builds and the composition root passes in, so `core` reaches no `document` or `window`.
23
11
  *
24
- * `setLocale` calculava isto em linha, ao escrever `<html lang>`. Agora os dois leem daqui: uma regra, dois
25
- * consumidores — que é exatamente a forma que este projeto já viu divergir quatro vezes.
12
+ * ⚠️ The hooks are OPTIONAL, and absent they mean «no page to tell»: a node test, a worker, a second engine in the same
13
+ * process.
26
14
  */
27
- export declare function bcp47(code?: string): string;
28
- export declare function availableLocales(): string[];
29
- export declare function applyDom(root?: ParentNode): void;
30
- export declare function setLocale(code: string): Promise<void>;
15
+ export interface LocalePort {
16
+ get(key: string, fallback: string | null): string | null;
17
+ set(key: string, value: string): unknown;
18
+ readonly KEYS: {
19
+ readonly lang: string;
20
+ };
21
+ /** What the HOST does once a language is kept: `<html lang>`, re-translating the markup, telling the page. */
22
+ readonly applied?: (locale: string, tag: string) => void;
23
+ /** The language the host prefers when nothing is stored (the browser's `navigator.language`). */
24
+ readonly preferred?: () => string | null;
25
+ /**
26
+ * Hears a language ANOTHER root on the same page switched to — the page's signal to its host (`i18n:change`).
27
+ * The LANGUAGE is the page's (`<html lang>` is one attribute), so every root follows it (ADR-0232 D3 erratum point 3).
28
+ */
29
+ readonly follow?: (react: (locale: string) => void) => void;
30
+ }
31
+ /** What a module that only TRANSLATES receives (ADR-0232 D3): a key and its `{param}`s in, the text of the page's language out. */
32
+ export type Translate = (key: string, params?: Record<string, string | number>) => string;
33
+ export declare function bcp47(code: string): string;
31
34
  /**
32
- * Boot: aplica pt (síncrono, para a página nunca ficar em branco) e, se o idioma preferido for outro, PEDE a
33
- * troca — que é assíncrona, porque os outros locales são chunks sob demanda.
35
+ * THE TRANSLATOR A ROOT BUILDS AND HANDS DOWN (ADR-0232 D3, and its errata).
34
36
  *
35
- * Continua devolvendo o locale de forma síncrona e NÃO bloqueia por si: quem precisar esperar chama
36
- * `idiomaPronto()`. Foi essa separação que faltava — ver o comentário lá embaixo.
37
- *
38
- * `root` ENTRA em vez de ser lido do global, e foi o `boot/createGame()` que cobrou (item 13): a raiz de
39
- * composição recebe o documento do hospedeiro por injeção e não tinha como repassá-lo — esta linha alcançava
40
- * o `document` global por baixo dela. Num navegador dá no mesmo; no project `node` é a diferença entre
41
- * bootar contra um DOM de mentira e não bootar. O padrão continua sendo o global, então nenhum chamador
42
- * muda: é a mesma regra do `applyDom` logo acima.
37
+ * It holds everything that changes: the page's language as this root knows it, the dictionaries loaded, and the GAME's own
38
+ * dictionary, which no other root on the page reads. A module that only translates receives the bare `t`; one that reads or
39
+ * sets the language receives this object.
43
40
  */
44
- export declare function initI18n(root?: ParentNode): string;
41
+ export interface Translator {
42
+ readonly t: Translate;
43
+ /** The page's language code (`pt`, `en`, `es`). */
44
+ readonly locale: () => string;
45
+ /** The BCP-47 tag of a language, the page's by default — what speech and recognition are given. */
46
+ readonly bcp47: (code?: string) => string;
47
+ /** Switches the PAGE's language; every root on the page follows (through the port's `follow`). */
48
+ readonly setLocale: (code: string) => Promise<void>;
49
+ /** Hears a language change; returns the release (the root passes this through its disposing door, ADR-0220). */
50
+ readonly onChange: (react: (locale: string) => void) => () => void;
51
+ /** Translates the declarative markup (`data-i18n`, `data-i18n-aria`) under `root`. */
52
+ readonly applyDom: (root: ParentNode) => void;
53
+ /**
54
+ * Boot: translates `root` in pt (synchronously, so the page is never blank) and, if the stored or preferred language is
55
+ * another, ASKS for the switch — asynchronous, because the other locales are on-demand chunks. Returns the locale now;
56
+ * whoever needs to wait calls `ready()`. `root` comes in, never the global `document` (finding 15 of `boot/create-game`).
57
+ */
58
+ readonly init: (root: ParentNode) => string;
59
+ /** Resolves when the language chosen at boot has loaded. */
60
+ readonly ready: () => Promise<void>;
61
+ /**
62
+ * Registers a game's keys for a language IN THIS TRANSLATOR — its root's dictionary, which no other root on the page
63
+ * reads (ADR-0232 D3 erratum); returns the refused ones (markup).
64
+ */
65
+ readonly registerDict: (code: string, entries: LocaleDict) => string[];
66
+ /** The keys this translator's game registered in one language and not another, for `problems` (study item E4). */
67
+ readonly dictionaryGaps: () => string[];
68
+ /**
69
+ * A WORD THE GAME DECLARED, by its key (ADR-0232 D3 erratum of 2026-09-25): resolved in the page's language from THIS
70
+ * game's dictionary, falling back to its pt — and `null` when the game's dictionary has it in neither. Never the key: a
71
+ * key on screen is an identifier in front of a child. Whoever draws decides what an absent word means (a row not drawn).
72
+ */
73
+ readonly word: (key: string) => string | null;
74
+ /** Does this game's dictionary have `key` in any language? What `problems` asks of every declared key. */
75
+ readonly declares: (key: string) => boolean;
76
+ }
45
77
  /**
46
- * Resolve quando o idioma escolhido no boot terminou de carregar (na hora, se for pt).
47
- *
48
- * ========================= POR QUE ISTO PRECISOU EXISTIR =========================
49
- * O `initI18n()` do main.js era a ÚLTIMA linha do boot, depois de o HUD, os menus de título e as telas de
50
- * pausa já estarem montados. Para pt isso não custava nada — já estava tudo em português. Para en/es, custava
51
- * metade da interface: o `applyDom` conserta o markup ESTÁTICO (`data-i18n`), mas o que o JavaScript monta
52
- * (os botões de cenário, os de atividade) tinha capturado o texto de pt e ninguém reconstruía.
53
- *
54
- * O sintoma era desconcertante: `t('cen.cidade')` devolvia "City" e o botão na tela dizia "Cidade".
55
- *
56
- * Isto NÃO responde à pergunta maior — QUANDO a interface se reconstrói ao trocar de idioma EM EXECUÇÃO —,
57
- * que segue com o Dev. Responde à menor, que não tem duas respostas: a interface não se constrói antes de o
58
- * idioma ser conhecido.
78
+ * Builds a translator. `port` keeps the chosen language and tells the page (ADR-0178); without one there is no page to tell
79
+ * and nothing to keep, and `setLocale` refuses — a test that only translates needs none.
59
80
  */
60
- export declare function idiomaPronto(): Promise<void>;
61
- declare const i18n: {
62
- t: typeof t;
63
- getLocale: typeof getLocale;
64
- availableLocales: typeof availableLocales;
65
- applyDom: typeof applyDom;
66
- setLocale: typeof setLocale;
67
- initI18n: typeof initI18n;
68
- idiomaPronto: typeof idiomaPronto;
69
- registerDict: typeof registerDict;
70
- };
71
- export default i18n;
81
+ export declare function createTranslator(port?: LocalePort): Translator;
82
+ export {};
@@ -1,236 +1,222 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-or-later
2
- // i18n — internacionalização (ver docs/plano-i18n.md).
3
- // O idioma padrão (pt) é import ESTÁTICO → dicionário pronto antes do game.js rodar (boot síncrono, sem
4
- // refatorar o init para async). Os demais entram sob demanda ao trocar de idioma, via import.meta.glob (o
5
- // Vite gera um chunk por locale e o SW cacheia). import.meta.glob (em vez de import(`…${code}.ts`) cru) é o
6
- // jeito nativo do Vite: casa arquivos .ts no build de forma explícita, sem depender do glob "adivinhado".
2
+ // core/i18n — internationalisation.
3
+ // The default language (pt) is a STATIC import, so its dictionary is ready before anything renders (a synchronous boot).
4
+ // The others load on demand when the language changes, as their own chunks (see the loaders below).
5
+ //
6
+ // 🔴 THIS MODULE HOLDS NO STATE (ADR-0232 D3, erratum of 2026-09-25). Everything that changes — the language in use, the
7
+ // dictionaries loaded, the game's own dictionary, who listens to a change, the boot's pending load, the port the choice is
8
+ // kept through — lives in the TRANSLATOR a composition root builds (`createTranslator`). What stays at module level is
9
+ // constant: the base dictionary, the list of languages, their loaders and their regions. Two roots on one page, or two
10
+ // test files, share nothing through this module.
7
11
  import pt from '../i18n/pt.js';
8
- import * as store from '../platform/storage.js';
9
12
  const AVAILABLE = ['pt', 'en', 'es'];
10
- const base = pt; // dicionário-base (fallback), tipado
11
- const DICTS = { pt: base }; // dicionários já carregados (pt embutido)
12
- const STORE_KEY = store.KEYS.lang;
13
- /* ===================== O DICIONÁRIO DE QUEM CONSOME A ENGINE =====================
14
- *
15
- * O achado 2 do `consumer-quiz` dizia que os dicionários são do jogo de plataforma e que um segundo jogo
16
- * herda 253 chaves para usar um punhado — "peso morto no pacote". Estava certo para um consumidor que mora
17
- * DENTRO deste repositório, porque as chaves dele cabem em `../i18n/pt.ts`.
18
- *
19
- * ⚠️ DE FORA, O MESMO ACHADO DEIXA DE SER PESO E VIRA PAREDE. Os locales entram pelo `import.meta.glob` logo
20
- * abaixo, e esse glob é resolvido NO BUILD DESTA ENGINE, contra ESTA pasta. Um jogo que instala
21
- * `@the-inclusionist/engine` não tem como pôr arquivo lá dentro, e `DICTS` é privado. Sem esta camada ele
22
- * fica sem NENHUM caminho para as próprias chaves — e o pilar 3 não abre exceção para consumidor.
23
- *
24
- * CAMADA SEPARADA, e não `DICTS[code] = {...DICTS[code], ...extra}`: `DICTS.pt` É o objeto importado de
25
- * `../i18n/pt.ts`. Mesclar ali mutaria o dicionário da própria engine, e dois jogos na mesma página herdariam
26
- * as strings um do outro. `tests/i18n-consumer-dict.node.test.js` prende exatamente isso.
27
- */
28
- const EXTRA = {};
29
- /**
30
- * Registra as chaves DESTE jogo para um idioma. Chamável antes de o idioma existir — quem registra `en` antes
31
- * de qualquer `setLocale('en')` é atendido quando a troca acontecer.
32
- *
33
- * ⚠️ NÃO REAPLICA O DOM, de propósito. `applyDom` precisa de uma RAIZ, e alcançar o `document` global por
34
- * baixo de quem chama é o achado 15, que já custou uma correção. Um consumidor que registre depois de o
35
- * markup estático ter sido traduzido chama `applyDom(raiz)` ele mesmo — e o caso normal é registrar no boot,
36
- * antes de existir texto na tela.
37
- */
38
- export function registerDict(code, entries) {
39
- const recusadas = [];
40
- const aceites = {};
41
- for (const chave in entries) {
42
- if (temMarcacao(entries[chave]))
43
- recusadas.push(chave);
13
+ const base = pt; // the base dictionary (fallback), typed
14
+ /** The languages a page can switch to — a constant, so it stays a function of the module (ADR-0232 D3). */
15
+ export function availableLocales() { return AVAILABLE.slice(); }
16
+ /** Puts `entries` into `layer` for `code`, refusing what carries markup (see `hasMarkup`); returns the refused keys. */
17
+ function addEntries(layer, code, entries) {
18
+ const refused = [];
19
+ const accepted = {};
20
+ for (const key in entries) {
21
+ if (hasMarkup(entries[key]))
22
+ refused.push(key);
44
23
  else
45
- aceites[chave] = entries[chave];
24
+ accepted[key] = entries[key];
46
25
  }
47
- if (recusadas.length) {
48
- // Alto, e não em silêncio: quem escreveu a string tem de saber que ela não entrou. Descartar calado
49
- // faria a chave crua aparecer na tela sem nada explicando, e isso lê-se como defeito da engine.
26
+ if (refused.length) {
27
+ // Loud, not silent: whoever wrote the string has to know it did not go in. Dropping it quietly would put the raw
28
+ // key on screen with nothing explaining it, and that reads as an engine defect.
50
29
  try {
51
- console.error('[inclusionist] i18n: chaves recusadas por conterem marcação — ' + recusadas.join(', '));
30
+ console.error('[inclusionist] i18n: keys refused because they contain markup — ' + refused.join(', '));
52
31
  }
53
32
  catch { /* noop */ }
54
33
  }
55
- EXTRA[code] = { ...EXTRA[code], ...aceites };
56
- return recusadas;
34
+ layer[code] = { ...layer[code], ...accepted };
35
+ return refused;
57
36
  }
58
37
  /**
59
- * A string traz marcação?
60
- *
61
- * ⚠️ POR QUE ISTO EXISTE AQUI, E NÃO NOS ~15 SINKS QUE CONSOMEM i18n. O gate
62
- * `tests/i18n-sem-markup.node.test.js` varre os dicionários DESTA árvore e prova que nenhuma entrada tem
63
- * tag. Ele não alcança — e não tem como alcançar — o `EXTRA`: são strings que um JOGO regista em tempo de
64
- * execução, de outro repositório (ADR-0083), e um teste desta árvore não as vê.
65
- *
66
- * ⚠️ E ELAS GANHAM DO DICIONÁRIO DA ENGINE. `resolver` consulta `EXTRA` primeiro, então um jogo pode sobrepor
67
- * QUALQUER chave — inclusive as que a engine cola em markup. Sem este cheque, «i18n» tinha deixado de
68
- * significar «texto que alguém desta árvore reviu», e nada registava a mudança.
69
- *
70
- * A verificação vai na FRONTEIRA e não nos sinks porque a fronteira é UMA: toda string de um jogo passa por
71
- * aqui. Quinze sinks seriam quinze lugares para esquecer, e o esquecimento não deixa rasto.
72
- *
73
- * O crivo é deliberadamente grosseiro — `<` seguido de letra ou de barra, e `&` de entidade. Ele recusa
74
- * `a < b` escrito com espaço? Não: `< ` não casa. Recusa «5<10»? Não, o dígito não casa. O que ele recusa é
75
- * o que se parece com uma tag, e uma frase de interface que precise disso precisa de outra frase.
38
+ * THE KEYS A CARTRIDGE REGISTERED IN ONE OF THE THREE LANGUAGES AND NOT IN ANOTHER (study item E4; ADR-0010 pillar 3),
39
+ * one line per missing language, for `problems`. 📏 Measured: the games that register a dictionary do it in pt, en and es,
40
+ * and nothing checked it — a key forgotten in one language shows Portuguese there, and nobody is told.
41
+ * A cartridge that registered nothing is not accused: strings it never gave the engine, the engine cannot see.
76
42
  */
77
- function temMarcacao(valor) {
78
- return typeof valor === 'string' && (/<[a-zA-Z/!?]/.test(valor) || /&[a-zA-Z#][a-zA-Z0-9]*;/.test(valor));
43
+ function gapsIn(layer) {
44
+ const registered = new Set();
45
+ for (const code of AVAILABLE)
46
+ for (const key in layer[code] ?? {})
47
+ registered.add(key);
48
+ if (!registered.size)
49
+ return [];
50
+ const SHOW = 5;
51
+ const gapLines = [];
52
+ for (const code of AVAILABLE) {
53
+ const missing = [...registered].filter((key) => !(key in (layer[code] ?? {})));
54
+ if (!missing.length)
55
+ continue;
56
+ const rest = missing.length > SHOW ? ` (and ${missing.length - SHOW} more)` : '';
57
+ gapLines.push(`the cartridge's dictionary lacks ${code} for ${missing.slice(0, SHOW).join(', ')}${rest}: `
58
+ + `a child playing in ${code} reads the fallback there (CreateGameOptions.dictionaries)`);
59
+ }
60
+ return gapLines;
79
61
  }
80
62
  /**
81
- * A cadeia de resolução, em cinco degraus. Ela ESPELHA a que já existia (`locale → pt → a própria chave`),
82
- * com o consumidor colado a cada degrau em vez de empilhado por cima:
63
+ * Does a string carry markup?
64
+ *
65
+ * ⚠️ WHY THIS LIVES HERE, AND NOT IN THE ~15 SINKS THAT CONSUME i18n. The gate `tests/i18n-without-markup.node.test.js`
66
+ * sweeps THIS tree's dictionaries and proves no entry has a tag. It does not reach — and cannot reach — a game's own
67
+ * dictionary: those are strings a GAME registers at run time, from another repository (ADR-0083), and a test in this tree
68
+ * never sees them.
83
69
  *
84
- * 1. consumidor no idioma corrente · 2. engine no idioma corrente ·
85
- * 3. consumidor em pt · 4. engine em pt · 5. a própria chave
70
+ * ⚠️ AND THEY WIN OVER THE ENGINE'S DICTIONARY. The resolver looks at the game's dictionary first, so a game can override
71
+ * ANY key — including the ones the engine pastes into markup. Without this check "i18n" would have stopped meaning "text
72
+ * someone in this tree reviewed", and nothing would record the change.
86
73
  *
87
- * O degrau 3 é o que faz um jogo que só escreveu pt seguir LEGÍVEL quando a criança troca para inglês: ela lê
88
- * português, exatamente como já lê hoje quando falta chave na engine. Degradar é melhor que calar, e é a
89
- * mesma escolha que o `.catch` mudo do `initI18n` já faz para o chunk que não carrega.
74
+ * The check sits at the BOUNDARY and not in the sinks because the boundary is ONE: every string from a game passes here.
90
75
  *
91
- * O degrau 2 vir ANTES do 3 é a única ordem defensável: idioma certo da engine vale mais que idioma errado do
92
- * consumidor. A inversão daria "Potência de 2" numa interface em espanhol que tinha a tradução na mão.
76
+ * The sieve is deliberately coarse — `<` followed by a letter or a slash, and `&` of an entity. `a < b` with a space and
77
+ * "5<10" pass; what it refuses is what looks like a tag, and an interface sentence that needs one needs another sentence.
93
78
  */
94
- function resolver(key) {
95
- const doJogo = EXTRA[locale];
96
- if (doJogo && key in doJogo)
97
- return doJogo[key];
98
- if (key in dict)
99
- return dict[key];
100
- const doJogoEmPt = EXTRA.pt;
101
- if (doJogoEmPt && key in doJogoEmPt)
102
- return doJogoEmPt[key];
103
- return key in base ? base[key] : key;
79
+ function hasMarkup(value) {
80
+ return typeof value === 'string' && (/<[a-zA-Z/!?]/.test(value) || /&[a-zA-Z#][a-zA-Z0-9]*;/.test(value));
104
81
  }
105
- /* ===================== OS CARREGADORES, E POR QUE DEIXARAM DE SER UM GLOB =====================
106
- *
107
- * Isto era `import.meta.glob<{default: LocaleDict}>('../i18n/*.ts')`, e a razão escrita no topo do arquivo
108
- * continua verdadeira para este repositório: o Vite gera um chunk por locale e o service worker o cacheia.
82
+ /* ===================== THE LOADERS, AND WHY THEY ARE NOT A GLOB =====================
109
83
  *
110
- * ⚠️ O QUE MUDOU FOI O DESTINO DO ARQUIVO, NÃO O ARGUMENTO. Medido em 2026-09-05 no primeiro build de
111
- * pacote (`tsc -p tsconfig.pkg.json`): a linha SOBREVIVE ao emit, intacta, em `dist-pkg/core/i18n.js` —
112
- * e ali ela é falsa em dois níveis ao mesmo tempo.
113
- * · O `tsc` não é o Vite: ele copia `import.meta.glob(...)` como chamada comum. Num consumidor que não
114
- * transforme o módulo, `import.meta.glob` é `undefined` e a chamada estoura no carregamento.
115
- * · E mesmo transformado, o padrão diz `*.ts` — ao lado do arquivo EMITIDO só existem `.js`. O glob casaria
116
- * zero arquivos, `ensure()` cairia no `return base`, e todo idioma que não fosse pt viraria português
117
- * SEM ERRO NENHUM. É o modo de falhar que este projeto já pagou uma vez, quando uma regex morreu em
118
- * silêncio com a checagem verde.
84
+ * This used to be `import.meta.glob('../i18n/*.ts')`. ⚠️ In the PACKAGE build (`tsc -p tsconfig.pkg.json`) that line
85
+ * survives the emit intact, and there it is false twice over: `tsc` copies `import.meta.glob(...)` as an ordinary call
86
+ * (undefined in a consumer that does not transform the module), and the pattern says `*.ts` next to emitted `.js` files —
87
+ * every language but pt would silently become Portuguese.
119
88
  *
120
- * ENUMERADO, ENTÃO — e o preço é MENOR do que eu escrevi antes de medir. O glob varria três arquivos que a
121
- * constante `AVAILABLE` logo acima JÁ ENUMERA, então não havia descoberta nenhuma a preservar.
122
- *
123
- * ⚠️ E O CODE-SPLITTING NÃO SE PERDE, o que eu tinha suposto que se perderia. O que o Vite divide é o
124
- * `import()` DINÂMICO, e ele continua aqui — a primeira versão deste comentário dizia que o custo eram
125
- * "~63 KB de parse a mais", e o build de 2026-09-05 mostrou o contrário na saída: `dist/assets/en-*.js`
126
- * (24,8 KB) e `dist/assets/es-*.js` (26,7 KB) seguem como chunks próprios, exatamente como com o glob. Quem
127
- * fica em pt nunca os avalia. O custo real é UM: três linhas a manter à mão no dia em que entrar um quarto
128
- * idioma — e `AVAILABLE` já era essa lista, então é o mesmo dia e o mesmo arquivo. */
89
+ * ENUMERATED, THEN — and code splitting is not lost, because what Vite splits is the dynamic `import()`: `en` and `es`
90
+ * still ship as their own chunks, and a child who stays in pt never evaluates them. The one cost is three lines to keep
91
+ * by hand the day a fourth language arrives — the same day and the same file as `AVAILABLE`. */
129
92
  const loaders = {
130
93
  en: () => import('../i18n/en.js'),
131
94
  es: () => import('../i18n/es.js'),
132
95
  };
133
- let locale = 'pt';
134
- let dict = base;
135
- // Traduz uma chave; a cadeia de fallback está em `resolver()` logo acima. Interpola {param}.
136
- export function t(key, params) {
137
- let s = resolver(key);
96
+ /** Interpolates `{param}` into a resolved string. */
97
+ function interpolate(s, params) {
138
98
  if (params)
139
99
  for (const k in params)
140
100
  s = s.replaceAll('{' + k + '}', String(params[k]));
141
101
  return s;
142
102
  }
143
- export function getLocale() { return locale; }
144
103
  /**
145
- * A etiqueta BCP-47 do idioma corrente — o que se escreve em `<html lang>` e o que se entrega a APIs do
146
- * navegador que falam (Web Speech) ou comparam idioma.
147
- *
148
- * Só o português precisa de região: 'pt' sozinho deixaria o navegador escolher entre pt-PT e pt-BR, e a
149
- * diferença de prosódia é audível para uma criança brasileira. Inglês e espanhol ficam sem região de
150
- * propósito — o navegador escolhe a variante local, que é o certo, e fixar 'en-US' imporia sotaque americano
151
- * a quem estivesse na Índia ou na Nigéria.
104
+ * THE BCP-47 TAG OF A LANGUAGE — what `<html lang>` says and what the browser's speech and recognition are given.
152
105
  *
153
- * `setLocale` calculava isto em linha, ao escrever `<html lang>`. Agora os dois leem daqui: uma regra, dois
154
- * consumidores — que é exatamente a forma que este projeto já viu divergir quatro vezes.
106
+ * Each language carries the REGION its flag stands for (the Dev, 2026-09-16: «Brasil, Estados Unidos e México. Cada bandeira indica a
107
+ * localização para qual o app está configurado»): pt-BR, en-US, es-MX. Voices are still matched on the language alone
108
+ * (`voicesForLocale`), so a region narrows nothing a child can hear. A translator answers it for the page's language.
155
109
  */
156
- export function bcp47(code = locale) { return code === 'pt' ? 'pt-BR' : code; }
157
- export function availableLocales() { return AVAILABLE.slice(); }
158
- // Aplica as traduções declarativas do HTML: [data-i18n] → textContent; [data-i18n-aria] → aria-label.
159
- export function applyDom(root = document) {
160
- root.querySelectorAll('[data-i18n]').forEach((el) => { const k = el.getAttribute('data-i18n'); if (k)
161
- el.textContent = t(k); });
162
- root.querySelectorAll('[data-i18n-aria]').forEach((el) => { const k = el.getAttribute('data-i18n-aria'); if (k)
163
- el.setAttribute('aria-label', t(k)); });
164
- }
165
- async function ensure(code) {
166
- if (DICTS[code])
167
- return DICTS[code];
168
- const load = loaders[code];
169
- if (!load)
170
- return base; // idioma sem arquivo → cai no pt
171
- const mod = await load();
172
- DICTS[code] = mod.default;
173
- return DICTS[code];
174
- }
175
- // Troca o idioma (carrega sob demanda), persiste, atualiza <html lang>, reaplica o DOM e avisa a UI.
176
- export async function setLocale(code) {
177
- if (!AVAILABLE.includes(code))
178
- code = 'pt';
179
- dict = await ensure(code);
180
- locale = code;
181
- store.set(STORE_KEY, code);
182
- document.documentElement.lang = bcp47(code);
183
- applyDom(document);
184
- window.dispatchEvent(new CustomEvent('i18n:change', { detail: { locale } }));
185
- }
186
- function pickDefault() {
187
- const saved = store.get(STORE_KEY, null);
188
- if (saved && AVAILABLE.includes(saved))
189
- return saved;
190
- const nav = ((navigator.language || 'pt').slice(0, 2)).toLowerCase();
191
- return AVAILABLE.includes(nav) ? nav : 'pt';
192
- }
193
- /** Promessa do carregamento pedido no boot. Resolve na hora quando o idioma é pt (dicionário estático). */
194
- let pendente = Promise.resolve();
110
+ const REGION_OF = { pt: 'pt-BR', en: 'en-US', es: 'es-MX' };
111
+ export function bcp47(code) { return REGION_OF[code] ?? code; }
195
112
  /**
196
- * Boot: aplica pt (síncrono, para a página nunca ficar em branco) e, se o idioma preferido for outro, PEDE a
197
- * troca — que é assíncrona, porque os outros locales são chunks sob demanda.
198
- *
199
- * Continua devolvendo o locale de forma síncrona e NÃO bloqueia por si: quem precisar esperar chama
200
- * `idiomaPronto()`. Foi essa separação que faltava — ver o comentário lá embaixo.
201
- *
202
- * `root` ENTRA em vez de ser lido do global, e foi o `boot/createGame()` que cobrou (item 13): a raiz de
203
- * composição recebe o documento do hospedeiro por injeção e não tinha como repassá-lo — esta linha alcançava
204
- * o `document` global por baixo dela. Num navegador dá no mesmo; no project `node` é a diferença entre
205
- * bootar contra um DOM de mentira e não bootar. O padrão continua sendo o global, então nenhum chamador
206
- * muda: é a mesma regra do `applyDom` logo acima.
113
+ * Builds a translator. `port` keeps the chosen language and tells the page (ADR-0178); without one there is no page to tell
114
+ * and nothing to keep, and `setLocale` refuses — a test that only translates needs none.
207
115
  */
208
- export function initI18n(root = document) {
209
- applyDom(root);
210
- const def = pickDefault();
211
- // `.catch` mudo de propósito: um chunk de locale que não carrega degrada para pt, e degradar é MUITO melhor
212
- // que travar o boot. Sem ele, um `await idiomaPronto()` lá fora derrubaria o jogo inteiro por causa do idioma.
213
- if (def !== 'pt')
214
- pendente = setLocale(def).catch(() => { });
215
- return locale;
116
+ export function createTranslator(port) {
117
+ let locale = 'pt';
118
+ let dict = base;
119
+ /** Dictionaries already loaded (pt built in). */
120
+ const loaded = { pt: base };
121
+ /** THIS game's dictionary. A separate layer and not a merge into `loaded`: `loaded.pt` IS the imported engine object. */
122
+ const own = {};
123
+ const listeners = new Set();
124
+ let pending = Promise.resolve();
125
+ /*
126
+ * The resolution chain, in five steps: 1. game in the current language · 2. engine in the current language · 3. game in pt ·
127
+ * 4. engine in pt · 5. the key itself. Step 3 keeps a game that only wrote pt READABLE in English (degrading beats going
128
+ * quiet); step 2 before step 3 because the engine's right language is worth more than the game's wrong one.
129
+ */
130
+ const resolveKey = (key) => {
131
+ const fromGame = own[locale];
132
+ if (fromGame && key in fromGame)
133
+ return fromGame[key];
134
+ if (key in dict)
135
+ return dict[key];
136
+ const fromGameInPt = own.pt;
137
+ if (fromGameInPt && key in fromGameInPt)
138
+ return fromGameInPt[key];
139
+ return key in base ? base[key] : key;
140
+ };
141
+ const t = (key, params) => interpolate(resolveKey(key), params);
142
+ const applyDom = (root) => {
143
+ root.querySelectorAll('[data-i18n]').forEach((el) => { const k = el.getAttribute('data-i18n'); if (k)
144
+ el.textContent = t(k); });
145
+ root.querySelectorAll('[data-i18n-aria]').forEach((el) => { const k = el.getAttribute('data-i18n-aria'); if (k)
146
+ el.setAttribute('aria-label', t(k)); });
147
+ };
148
+ const ensure = async (code) => {
149
+ if (loaded[code])
150
+ return loaded[code];
151
+ const load = loaders[code];
152
+ if (!load)
153
+ return base; // a language with no file → falls back to pt
154
+ const mod = await load();
155
+ loaded[code] = mod.default;
156
+ return loaded[code];
157
+ };
158
+ const tellListeners = () => {
159
+ for (const listener of [...listeners]) {
160
+ try {
161
+ listener(locale);
162
+ }
163
+ catch { /* a listener failed; the language changed */ }
164
+ }
165
+ };
166
+ const setLocale = async (requested) => {
167
+ // first, before anything changes: a language switched and then refused would leave the page half moved (ADR-0178)
168
+ if (!port)
169
+ throw new Error('core/i18n: setLocale on a translator without a port — the composition root builds it with the stored settings (ADR-0178)');
170
+ const code = AVAILABLE.includes(requested) ? requested : 'pt';
171
+ dict = await ensure(code);
172
+ locale = code;
173
+ port.set(port.KEYS.lang, code);
174
+ // 📌 The three page effects — `<html lang>`, re-translating the markup, telling the page — in ONE call to the host, which
175
+ // is what keeps this module free of `document` and `window` (ADR-0173, ADR-0221 step 7g).
176
+ port.applied?.(locale, bcp47(code));
177
+ tellListeners();
178
+ };
179
+ // 📌 ANOTHER ROOT SWITCHED THE PAGE: this one follows without keeping or telling again — the root that switched did both.
180
+ port?.follow?.((code) => {
181
+ if (code === locale || !AVAILABLE.includes(code))
182
+ return;
183
+ void ensure(code).then((d) => { dict = d; locale = code; tellListeners(); });
184
+ });
185
+ const pickDefault = () => {
186
+ const saved = port ? port.get(port.KEYS.lang, null) : null;
187
+ if (saved && AVAILABLE.includes(saved))
188
+ return saved;
189
+ const nav = ((port?.preferred?.() || 'pt').slice(0, 2)).toLowerCase();
190
+ return AVAILABLE.includes(nav) ? nav : 'pt';
191
+ };
192
+ const init = (root) => {
193
+ applyDom(root);
194
+ const def = pickDefault();
195
+ // A silent `.catch` on purpose: a locale chunk that fails to load degrades to pt, and degrading is FAR better than
196
+ // freezing the boot. Without it, an `await ready()` outside would bring the whole game down over the language.
197
+ if (def !== 'pt')
198
+ pending = setLocale(def).catch(() => { });
199
+ return locale;
200
+ };
201
+ const word = (key) => {
202
+ const inLocale = own[locale];
203
+ if (inLocale && key in inLocale)
204
+ return inLocale[key];
205
+ const inPt = own.pt;
206
+ return inPt && key in inPt ? inPt[key] : null;
207
+ };
208
+ return {
209
+ t,
210
+ locale: () => locale,
211
+ bcp47: (code = locale) => bcp47(code),
212
+ setLocale,
213
+ onChange: (react) => { listeners.add(react); return () => { listeners.delete(react); }; },
214
+ applyDom,
215
+ init,
216
+ ready: () => pending,
217
+ registerDict: (code, entries) => addEntries(own, code, entries),
218
+ dictionaryGaps: () => gapsIn(own),
219
+ word,
220
+ declares: (key) => Object.values(own).some((entries) => key in entries),
221
+ };
216
222
  }
217
- /**
218
- * Resolve quando o idioma escolhido no boot terminou de carregar (na hora, se for pt).
219
- *
220
- * ========================= POR QUE ISTO PRECISOU EXISTIR =========================
221
- * O `initI18n()` do main.js era a ÚLTIMA linha do boot, depois de o HUD, os menus de título e as telas de
222
- * pausa já estarem montados. Para pt isso não custava nada — já estava tudo em português. Para en/es, custava
223
- * metade da interface: o `applyDom` conserta o markup ESTÁTICO (`data-i18n`), mas o que o JavaScript monta
224
- * (os botões de cenário, os de atividade) tinha capturado o texto de pt e ninguém reconstruía.
225
- *
226
- * O sintoma era desconcertante: `t('cen.cidade')` devolvia "City" e o botão na tela dizia "Cidade".
227
- *
228
- * Isto NÃO responde à pergunta maior — QUANDO a interface se reconstrói ao trocar de idioma EM EXECUÇÃO —,
229
- * que segue com o Dev. Responde à menor, que não tem duas respostas: a interface não se constrói antes de o
230
- * idioma ser conhecido.
231
- */
232
- export function idiomaPronto() { return pendente; }
233
- const i18n = { t, getLocale, availableLocales, applyDom, setLocale, initI18n, idiomaPronto, registerDict };
234
- export default i18n;
235
- if (typeof window !== 'undefined')
236
- window.__i18n = i18n; // exposto p/ teste/preview