@bquery/bquery 1.5.0 → 1.7.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 (380) hide show
  1. package/README.md +193 -23
  2. package/dist/a11y/announce.d.ts +43 -0
  3. package/dist/a11y/announce.d.ts.map +1 -0
  4. package/dist/a11y/audit.d.ts +42 -0
  5. package/dist/a11y/audit.d.ts.map +1 -0
  6. package/dist/a11y/index.d.ts +53 -0
  7. package/dist/a11y/index.d.ts.map +1 -0
  8. package/dist/a11y/media-preferences.d.ts +77 -0
  9. package/dist/a11y/media-preferences.d.ts.map +1 -0
  10. package/dist/a11y/roving-tab-index.d.ts +38 -0
  11. package/dist/a11y/roving-tab-index.d.ts.map +1 -0
  12. package/dist/a11y/skip-link.d.ts +37 -0
  13. package/dist/a11y/skip-link.d.ts.map +1 -0
  14. package/dist/a11y/trap-focus.d.ts +49 -0
  15. package/dist/a11y/trap-focus.d.ts.map +1 -0
  16. package/dist/a11y/types.d.ts +152 -0
  17. package/dist/a11y/types.d.ts.map +1 -0
  18. package/dist/a11y-C5QOVvRn.js +421 -0
  19. package/dist/a11y-C5QOVvRn.js.map +1 -0
  20. package/dist/a11y.es.mjs +14 -0
  21. package/dist/component/component.d.ts +13 -5
  22. package/dist/component/component.d.ts.map +1 -1
  23. package/dist/component/html.d.ts +40 -3
  24. package/dist/component/html.d.ts.map +1 -1
  25. package/dist/component/index.d.ts +3 -2
  26. package/dist/component/index.d.ts.map +1 -1
  27. package/dist/component/library.d.ts.map +1 -1
  28. package/dist/component/scope.d.ts +138 -0
  29. package/dist/component/scope.d.ts.map +1 -0
  30. package/dist/component/types.d.ts +184 -17
  31. package/dist/component/types.d.ts.map +1 -1
  32. package/dist/component-CuuTijA6.js +684 -0
  33. package/dist/component-CuuTijA6.js.map +1 -0
  34. package/dist/component.es.mjs +10 -6
  35. package/dist/{config-DRmZZno3.js → config-BW35FKuA.js} +4 -4
  36. package/dist/config-BW35FKuA.js.map +1 -0
  37. package/dist/constraints-3lV9yyBw.js +100 -0
  38. package/dist/constraints-3lV9yyBw.js.map +1 -0
  39. package/dist/core/collection.d.ts +48 -0
  40. package/dist/core/collection.d.ts.map +1 -1
  41. package/dist/core/element.d.ts +92 -0
  42. package/dist/core/element.d.ts.map +1 -1
  43. package/dist/core/env.d.ts +18 -0
  44. package/dist/core/env.d.ts.map +1 -0
  45. package/dist/core/index.d.ts +1 -0
  46. package/dist/core/index.d.ts.map +1 -1
  47. package/dist/core/shared.d.ts +8 -0
  48. package/dist/core/shared.d.ts.map +1 -1
  49. package/dist/core/utils/index.d.ts +52 -41
  50. package/dist/core/utils/index.d.ts.map +1 -1
  51. package/dist/core-Cjl7GUu8.js +717 -0
  52. package/dist/core-Cjl7GUu8.js.map +1 -0
  53. package/dist/{core-DPdbItcq.js → core-DnlyjbF2.js} +1 -1
  54. package/dist/{core-DPdbItcq.js.map → core-DnlyjbF2.js.map} +1 -1
  55. package/dist/core.es.mjs +45 -44
  56. package/dist/custom-directives-7wAShnnd.js +9 -0
  57. package/dist/custom-directives-7wAShnnd.js.map +1 -0
  58. package/dist/devtools/devtools.d.ts +212 -0
  59. package/dist/devtools/devtools.d.ts.map +1 -0
  60. package/dist/devtools/index.d.ts +20 -0
  61. package/dist/devtools/index.d.ts.map +1 -0
  62. package/dist/devtools/types.d.ts +69 -0
  63. package/dist/devtools/types.d.ts.map +1 -0
  64. package/dist/devtools-D2fQLhDN.js +122 -0
  65. package/dist/devtools-D2fQLhDN.js.map +1 -0
  66. package/dist/devtools.es.mjs +19 -0
  67. package/dist/dnd/draggable.d.ts +51 -0
  68. package/dist/dnd/draggable.d.ts.map +1 -0
  69. package/dist/dnd/droppable.d.ts +38 -0
  70. package/dist/dnd/droppable.d.ts.map +1 -0
  71. package/dist/dnd/index.d.ts +47 -0
  72. package/dist/dnd/index.d.ts.map +1 -0
  73. package/dist/dnd/sortable.d.ts +43 -0
  74. package/dist/dnd/sortable.d.ts.map +1 -0
  75. package/dist/dnd/types.d.ts +250 -0
  76. package/dist/dnd/types.d.ts.map +1 -0
  77. package/dist/dnd-B8EgyzaI.js +244 -0
  78. package/dist/dnd-B8EgyzaI.js.map +1 -0
  79. package/dist/dnd.es.mjs +6 -0
  80. package/dist/env-NeVmr4Gf.js +19 -0
  81. package/dist/env-NeVmr4Gf.js.map +1 -0
  82. package/dist/forms/create-form.d.ts +49 -0
  83. package/dist/forms/create-form.d.ts.map +1 -0
  84. package/dist/forms/index.d.ts +39 -0
  85. package/dist/forms/index.d.ts.map +1 -0
  86. package/dist/forms/types.d.ts +139 -0
  87. package/dist/forms/types.d.ts.map +1 -0
  88. package/dist/forms/validators.d.ts +179 -0
  89. package/dist/forms/validators.d.ts.map +1 -0
  90. package/dist/forms-C3yovgH9.js +141 -0
  91. package/dist/forms-C3yovgH9.js.map +1 -0
  92. package/dist/forms.es.mjs +14 -0
  93. package/dist/full.d.ts +37 -9
  94. package/dist/full.d.ts.map +1 -1
  95. package/dist/full.es.mjs +186 -91
  96. package/dist/full.iife.js +47 -31
  97. package/dist/full.iife.js.map +1 -1
  98. package/dist/full.umd.js +47 -31
  99. package/dist/full.umd.js.map +1 -1
  100. package/dist/i18n/formatting.d.ts +40 -0
  101. package/dist/i18n/formatting.d.ts.map +1 -0
  102. package/dist/i18n/i18n.d.ts +48 -0
  103. package/dist/i18n/i18n.d.ts.map +1 -0
  104. package/dist/i18n/index.d.ts +57 -0
  105. package/dist/i18n/index.d.ts.map +1 -0
  106. package/dist/i18n/translate.d.ts +83 -0
  107. package/dist/i18n/translate.d.ts.map +1 -0
  108. package/dist/i18n/types.d.ts +156 -0
  109. package/dist/i18n/types.d.ts.map +1 -0
  110. package/dist/i18n-BnnhTFOS.js +89 -0
  111. package/dist/i18n-BnnhTFOS.js.map +1 -0
  112. package/dist/i18n.es.mjs +6 -0
  113. package/dist/index.d.ts +11 -0
  114. package/dist/index.d.ts.map +1 -1
  115. package/dist/index.es.mjs +233 -138
  116. package/dist/media/battery.d.ts +35 -0
  117. package/dist/media/battery.d.ts.map +1 -0
  118. package/dist/media/breakpoints.d.ts +51 -0
  119. package/dist/media/breakpoints.d.ts.map +1 -0
  120. package/dist/media/clipboard.d.ts +30 -0
  121. package/dist/media/clipboard.d.ts.map +1 -0
  122. package/dist/media/device-sensors.d.ts +54 -0
  123. package/dist/media/device-sensors.d.ts.map +1 -0
  124. package/dist/media/geolocation.d.ts +38 -0
  125. package/dist/media/geolocation.d.ts.map +1 -0
  126. package/dist/media/index.d.ts +42 -0
  127. package/dist/media/index.d.ts.map +1 -0
  128. package/dist/media/media-query.d.ts +36 -0
  129. package/dist/media/media-query.d.ts.map +1 -0
  130. package/dist/media/network.d.ts +35 -0
  131. package/dist/media/network.d.ts.map +1 -0
  132. package/dist/media/types.d.ts +173 -0
  133. package/dist/media/types.d.ts.map +1 -0
  134. package/dist/media/viewport.d.ts +32 -0
  135. package/dist/media/viewport.d.ts.map +1 -0
  136. package/dist/media-Di2Ta22s.js +340 -0
  137. package/dist/media-Di2Ta22s.js.map +1 -0
  138. package/dist/media.es.mjs +12 -0
  139. package/dist/motion/index.d.ts +7 -3
  140. package/dist/motion/index.d.ts.map +1 -1
  141. package/dist/motion/morph.d.ts +27 -0
  142. package/dist/motion/morph.d.ts.map +1 -0
  143. package/dist/motion/parallax.d.ts +30 -0
  144. package/dist/motion/parallax.d.ts.map +1 -0
  145. package/dist/motion/reduced-motion.d.ts +36 -3
  146. package/dist/motion/reduced-motion.d.ts.map +1 -1
  147. package/dist/motion/types.d.ts +58 -0
  148. package/dist/motion/types.d.ts.map +1 -1
  149. package/dist/motion/typewriter.d.ts +31 -0
  150. package/dist/motion/typewriter.d.ts.map +1 -0
  151. package/dist/motion-qPj_TYGv.js +530 -0
  152. package/dist/motion-qPj_TYGv.js.map +1 -0
  153. package/dist/motion.es.mjs +27 -23
  154. package/dist/mount-SM07RUa6.js +403 -0
  155. package/dist/mount-SM07RUa6.js.map +1 -0
  156. package/dist/{object-qGpWr6-J.js → object-BCk-1c8T.js} +5 -4
  157. package/dist/{object-qGpWr6-J.js.map → object-BCk-1c8T.js.map} +1 -1
  158. package/dist/{platform-B7JhGBc7.js → platform-CPbCprb6.js} +3 -3
  159. package/dist/platform-CPbCprb6.js.map +1 -0
  160. package/dist/platform.es.mjs +2 -2
  161. package/dist/plugin/index.d.ts +22 -0
  162. package/dist/plugin/index.d.ts.map +1 -0
  163. package/dist/plugin/registry.d.ts +108 -0
  164. package/dist/plugin/registry.d.ts.map +1 -0
  165. package/dist/plugin/types.d.ts +110 -0
  166. package/dist/plugin/types.d.ts.map +1 -0
  167. package/dist/plugin-cPoOHFLY.js +64 -0
  168. package/dist/plugin-cPoOHFLY.js.map +1 -0
  169. package/dist/plugin.es.mjs +9 -0
  170. package/dist/reactive/computed.d.ts +7 -0
  171. package/dist/reactive/computed.d.ts.map +1 -1
  172. package/dist/reactive-Cfv0RK6x.js +233 -0
  173. package/dist/reactive-Cfv0RK6x.js.map +1 -0
  174. package/dist/reactive.es.mjs +18 -17
  175. package/dist/registry-CWf368tT.js +26 -0
  176. package/dist/registry-CWf368tT.js.map +1 -0
  177. package/dist/router/bq-link.d.ts +112 -0
  178. package/dist/router/bq-link.d.ts.map +1 -0
  179. package/dist/router/constraints.d.ts +9 -0
  180. package/dist/router/constraints.d.ts.map +1 -0
  181. package/dist/router/index.d.ts +14 -6
  182. package/dist/router/index.d.ts.map +1 -1
  183. package/dist/router/match.d.ts +0 -1
  184. package/dist/router/match.d.ts.map +1 -1
  185. package/dist/router/path-pattern.d.ts +14 -0
  186. package/dist/router/path-pattern.d.ts.map +1 -0
  187. package/dist/router/query.d.ts.map +1 -1
  188. package/dist/router/router.d.ts +3 -1
  189. package/dist/router/router.d.ts.map +1 -1
  190. package/dist/router/types.d.ts +48 -4
  191. package/dist/router/types.d.ts.map +1 -1
  192. package/dist/router/use-route.d.ts +50 -0
  193. package/dist/router/use-route.d.ts.map +1 -0
  194. package/dist/router/utils.d.ts +3 -0
  195. package/dist/router/utils.d.ts.map +1 -1
  196. package/dist/router-BrthaP_z.js +473 -0
  197. package/dist/router-BrthaP_z.js.map +1 -0
  198. package/dist/router.es.mjs +13 -10
  199. package/dist/{sanitize-jyJ2ryE2.js → sanitize-B1V4JswB.js} +95 -83
  200. package/dist/sanitize-B1V4JswB.js.map +1 -0
  201. package/dist/security/index.d.ts +2 -0
  202. package/dist/security/index.d.ts.map +1 -1
  203. package/dist/security/sanitize.d.ts +4 -1
  204. package/dist/security/sanitize.d.ts.map +1 -1
  205. package/dist/security/trusted-html.d.ts +53 -0
  206. package/dist/security/trusted-html.d.ts.map +1 -0
  207. package/dist/security.es.mjs +10 -9
  208. package/dist/ssr/hydrate.d.ts +65 -0
  209. package/dist/ssr/hydrate.d.ts.map +1 -0
  210. package/dist/ssr/index.d.ts +59 -0
  211. package/dist/ssr/index.d.ts.map +1 -0
  212. package/dist/ssr/render.d.ts +62 -0
  213. package/dist/ssr/render.d.ts.map +1 -0
  214. package/dist/ssr/serialize.d.ts +118 -0
  215. package/dist/ssr/serialize.d.ts.map +1 -0
  216. package/dist/ssr/types.d.ts +70 -0
  217. package/dist/ssr/types.d.ts.map +1 -0
  218. package/dist/ssr-B2qd_WBB.js +248 -0
  219. package/dist/ssr-B2qd_WBB.js.map +1 -0
  220. package/dist/ssr.es.mjs +9 -0
  221. package/dist/store/create-store.d.ts.map +1 -1
  222. package/dist/store/define-store.d.ts +1 -1
  223. package/dist/store/define-store.d.ts.map +1 -1
  224. package/dist/store/index.d.ts +1 -1
  225. package/dist/store/index.d.ts.map +1 -1
  226. package/dist/store/mapping.d.ts +1 -1
  227. package/dist/store/mapping.d.ts.map +1 -1
  228. package/dist/store/persisted.d.ts +38 -4
  229. package/dist/store/persisted.d.ts.map +1 -1
  230. package/dist/store/types.d.ts +140 -3
  231. package/dist/store/types.d.ts.map +1 -1
  232. package/dist/store/utils.d.ts +2 -2
  233. package/dist/store/utils.d.ts.map +1 -1
  234. package/dist/store/watch.d.ts +1 -1
  235. package/dist/store/watch.d.ts.map +1 -1
  236. package/dist/store-DWpyH6p5.js +338 -0
  237. package/dist/store-DWpyH6p5.js.map +1 -0
  238. package/dist/store.es.mjs +11 -10
  239. package/dist/storybook/index.d.ts +37 -0
  240. package/dist/storybook/index.d.ts.map +1 -0
  241. package/dist/storybook.es.mjs +151 -0
  242. package/dist/storybook.es.mjs.map +1 -0
  243. package/dist/testing/index.d.ts +23 -0
  244. package/dist/testing/index.d.ts.map +1 -0
  245. package/dist/testing/testing.d.ts +156 -0
  246. package/dist/testing/testing.d.ts.map +1 -0
  247. package/dist/testing/types.d.ts +134 -0
  248. package/dist/testing/types.d.ts.map +1 -0
  249. package/dist/testing-CsqjNUyy.js +224 -0
  250. package/dist/testing-CsqjNUyy.js.map +1 -0
  251. package/dist/testing.es.mjs +9 -0
  252. package/dist/type-guards-Do9DWgNp.js +44 -0
  253. package/dist/type-guards-Do9DWgNp.js.map +1 -0
  254. package/dist/untrack-DJVQQ2WM.js +33 -0
  255. package/dist/untrack-DJVQQ2WM.js.map +1 -0
  256. package/dist/view/custom-directives.d.ts +20 -0
  257. package/dist/view/custom-directives.d.ts.map +1 -0
  258. package/dist/view/evaluate.d.ts.map +1 -1
  259. package/dist/view/process.d.ts.map +1 -1
  260. package/dist/view.es.mjs +11 -10
  261. package/package.json +52 -11
  262. package/src/a11y/announce.ts +131 -0
  263. package/src/a11y/audit.ts +314 -0
  264. package/src/a11y/index.ts +68 -0
  265. package/src/a11y/media-preferences.ts +255 -0
  266. package/src/a11y/roving-tab-index.ts +164 -0
  267. package/src/a11y/skip-link.ts +255 -0
  268. package/src/a11y/trap-focus.ts +184 -0
  269. package/src/a11y/types.ts +183 -0
  270. package/src/component/component.ts +345 -65
  271. package/src/component/html.ts +153 -53
  272. package/src/component/index.ts +12 -2
  273. package/src/component/library.ts +66 -28
  274. package/src/component/scope.ts +212 -0
  275. package/src/component/types.ts +238 -19
  276. package/src/core/collection.ts +707 -628
  277. package/src/core/element.ts +981 -774
  278. package/src/core/env.ts +60 -0
  279. package/src/core/index.ts +49 -48
  280. package/src/core/shared.ts +62 -13
  281. package/src/core/utils/index.ts +148 -83
  282. package/src/devtools/devtools.ts +410 -0
  283. package/src/devtools/index.ts +48 -0
  284. package/src/devtools/types.ts +104 -0
  285. package/src/dnd/draggable.ts +296 -0
  286. package/src/dnd/droppable.ts +228 -0
  287. package/src/dnd/index.ts +62 -0
  288. package/src/dnd/sortable.ts +307 -0
  289. package/src/dnd/types.ts +293 -0
  290. package/src/forms/create-form.ts +278 -0
  291. package/src/forms/index.ts +65 -0
  292. package/src/forms/types.ts +154 -0
  293. package/src/forms/validators.ts +265 -0
  294. package/src/full.ts +260 -3
  295. package/src/i18n/formatting.ts +67 -0
  296. package/src/i18n/i18n.ts +200 -0
  297. package/src/i18n/index.ts +67 -0
  298. package/src/i18n/translate.ts +182 -0
  299. package/src/i18n/types.ts +171 -0
  300. package/src/index.ts +108 -36
  301. package/src/media/battery.ts +116 -0
  302. package/src/media/breakpoints.ts +131 -0
  303. package/src/media/clipboard.ts +80 -0
  304. package/src/media/device-sensors.ts +158 -0
  305. package/src/media/geolocation.ts +119 -0
  306. package/src/media/index.ts +76 -0
  307. package/src/media/media-query.ts +92 -0
  308. package/src/media/network.ts +115 -0
  309. package/src/media/types.ts +177 -0
  310. package/src/media/viewport.ts +84 -0
  311. package/src/motion/index.ts +57 -48
  312. package/src/motion/morph.ts +151 -0
  313. package/src/motion/parallax.ts +120 -0
  314. package/src/motion/reduced-motion.ts +66 -17
  315. package/src/motion/transition.ts +97 -97
  316. package/src/motion/types.ts +63 -0
  317. package/src/motion/typewriter.ts +164 -0
  318. package/src/platform/announcer.ts +208 -208
  319. package/src/platform/config.ts +163 -163
  320. package/src/platform/cookies.ts +165 -165
  321. package/src/platform/index.ts +39 -39
  322. package/src/platform/meta.ts +168 -168
  323. package/src/plugin/index.ts +37 -0
  324. package/src/plugin/registry.ts +269 -0
  325. package/src/plugin/types.ts +137 -0
  326. package/src/reactive/async-data.ts +486 -486
  327. package/src/reactive/computed.ts +130 -92
  328. package/src/reactive/index.ts +37 -37
  329. package/src/reactive/signal.ts +29 -29
  330. package/src/router/bq-link.ts +279 -0
  331. package/src/router/constraints.ts +201 -0
  332. package/src/router/index.ts +49 -41
  333. package/src/router/match.ts +312 -106
  334. package/src/router/path-pattern.ts +52 -0
  335. package/src/router/query.ts +38 -35
  336. package/src/router/router.ts +402 -211
  337. package/src/router/types.ts +139 -93
  338. package/src/router/use-route.ts +68 -0
  339. package/src/router/utils.ts +157 -116
  340. package/src/security/constants.ts +211 -211
  341. package/src/security/index.ts +12 -10
  342. package/src/security/sanitize.ts +6 -2
  343. package/src/security/trusted-html.ts +71 -0
  344. package/src/ssr/hydrate.ts +82 -0
  345. package/src/ssr/index.ts +70 -0
  346. package/src/ssr/render.ts +508 -0
  347. package/src/ssr/serialize.ts +296 -0
  348. package/src/ssr/types.ts +81 -0
  349. package/src/store/create-store.ts +467 -329
  350. package/src/store/define-store.ts +2 -1
  351. package/src/store/index.ts +27 -22
  352. package/src/store/mapping.ts +2 -1
  353. package/src/store/persisted.ts +249 -61
  354. package/src/store/types.ts +247 -94
  355. package/src/store/utils.ts +135 -141
  356. package/src/store/watch.ts +2 -1
  357. package/src/storybook/index.ts +480 -0
  358. package/src/testing/index.ts +42 -0
  359. package/src/testing/testing.ts +593 -0
  360. package/src/testing/types.ts +170 -0
  361. package/src/view/custom-directives.ts +30 -0
  362. package/src/view/evaluate.ts +292 -290
  363. package/src/view/process.ts +108 -92
  364. package/dist/component-CY5MVoYN.js +0 -531
  365. package/dist/component-CY5MVoYN.js.map +0 -1
  366. package/dist/config-DRmZZno3.js.map +0 -1
  367. package/dist/core-CK2Mfpf4.js +0 -648
  368. package/dist/core-CK2Mfpf4.js.map +0 -1
  369. package/dist/motion-C5DRdPnO.js +0 -415
  370. package/dist/motion-C5DRdPnO.js.map +0 -1
  371. package/dist/platform-B7JhGBc7.js.map +0 -1
  372. package/dist/reactive-BDya-ia8.js +0 -253
  373. package/dist/reactive-BDya-ia8.js.map +0 -1
  374. package/dist/router-CijiICxt.js +0 -188
  375. package/dist/router-CijiICxt.js.map +0 -1
  376. package/dist/sanitize-jyJ2ryE2.js.map +0 -1
  377. package/dist/store-CPK9E62U.js +0 -262
  378. package/dist/store-CPK9E62U.js.map +0 -1
  379. package/dist/view-Cdi0g-qo.js +0 -396
  380. package/dist/view-Cdi0g-qo.js.map +0 -1
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Core i18n factory function.
3
+ * @module bquery/i18n
4
+ */
5
+
6
+ import { computed, signal } from '../reactive/index';
7
+ import { isPrototypePollutionKey } from '../core/utils/object';
8
+ import { formatDate, formatNumber } from './formatting';
9
+ import { deepMerge, translate } from './translate';
10
+ import type {
11
+ DateFormatOptions,
12
+ I18nConfig,
13
+ I18nInstance,
14
+ LocaleLoader,
15
+ LocaleMessages,
16
+ Messages,
17
+ NumberFormatOptions,
18
+ TranslateParams,
19
+ } from './types';
20
+
21
+ /**
22
+ * Creates a reactive internationalization instance.
23
+ *
24
+ * The returned object provides:
25
+ * - `$locale` — a reactive signal for the current locale
26
+ * - `t()` — translation with interpolation and pluralization
27
+ * - `tc()` — reactive translation that auto-updates on locale change
28
+ * - `loadLocale()` — register lazy-loaded locale files
29
+ * - `n()` — locale-aware number formatting
30
+ * - `d()` — locale-aware date formatting
31
+ *
32
+ * @param config - Initial configuration
33
+ * @returns An i18n instance
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * import { createI18n } from '@bquery/bquery/i18n';
38
+ *
39
+ * const i18n = createI18n({
40
+ * locale: 'en',
41
+ * fallbackLocale: 'en',
42
+ * messages: {
43
+ * en: {
44
+ * greeting: 'Hello, {name}!',
45
+ * items: '{count} item | {count} items',
46
+ * },
47
+ * de: {
48
+ * greeting: 'Hallo, {name}!',
49
+ * items: '{count} Gegenstand | {count} Gegenstände',
50
+ * },
51
+ * },
52
+ * });
53
+ *
54
+ * i18n.t('greeting', { name: 'Ada' }); // 'Hello, Ada!'
55
+ * i18n.t('items', { count: 3 }); // '3 items'
56
+ *
57
+ * // Switch locale reactively
58
+ * i18n.$locale.value = 'de';
59
+ * i18n.t('greeting', { name: 'Ada' }); // 'Hallo, Ada!'
60
+ * ```
61
+ */
62
+ export const createI18n = (config: I18nConfig): I18nInstance => {
63
+ const { locale: initialLocale, messages: initialMessages, fallbackLocale } = config;
64
+
65
+ const sanitizeLocaleMessages = (localeMessages: LocaleMessages): LocaleMessages =>
66
+ deepMerge(Object.create(null) as LocaleMessages, localeMessages);
67
+
68
+ // Deep-clone initial messages to prevent external mutation
69
+ const messages = Object.create(null) as Messages;
70
+ for (const [loc, msgs] of Object.entries(initialMessages)) {
71
+ if (isPrototypePollutionKey(loc)) {
72
+ continue;
73
+ }
74
+ messages[loc] = sanitizeLocaleMessages(msgs);
75
+ }
76
+
77
+ // Reactive locale signal
78
+ const $locale = signal(initialLocale);
79
+
80
+ // Lazy-loader registry
81
+ const loaders = new Map<string, LocaleLoader>();
82
+
83
+ // Track which loaders have been invoked to avoid duplicate loads
84
+ const loadedLocales = new Set<string>(Object.keys(messages));
85
+
86
+ /**
87
+ * Get messages for a locale, or undefined if not loaded.
88
+ */
89
+ const getMessages = (loc: string): LocaleMessages | undefined => {
90
+ if (isPrototypePollutionKey(loc)) {
91
+ return undefined;
92
+ }
93
+ return messages[loc];
94
+ };
95
+
96
+ /**
97
+ * Register a lazy-loader for a locale.
98
+ */
99
+ const loadLocale = (loc: string, loader: LocaleLoader): void => {
100
+ if (isPrototypePollutionKey(loc)) {
101
+ return;
102
+ }
103
+ loaders.set(loc, loader);
104
+ };
105
+
106
+ /**
107
+ * Ensure a locale's messages are loaded.
108
+ */
109
+ const ensureLocale = async (loc: string): Promise<void> => {
110
+ if (isPrototypePollutionKey(loc)) return;
111
+ if (loadedLocales.has(loc)) return;
112
+
113
+ const loader = loaders.get(loc);
114
+ if (!loader) {
115
+ throw new Error(`bQuery i18n: No messages or loader registered for locale "${loc}".`);
116
+ }
117
+
118
+ const loaded = await loader();
119
+ // Handle both default exports and direct objects
120
+ const msgs = (loaded as { default?: LocaleMessages }).default ?? (loaded as LocaleMessages);
121
+ messages[loc] = sanitizeLocaleMessages(msgs);
122
+ loadedLocales.add(loc);
123
+ };
124
+
125
+ /**
126
+ * Translate a key path.
127
+ */
128
+ const t = (key: string, params: TranslateParams = {}): string => {
129
+ const currentLocale = $locale.value;
130
+ const currentMessages = messages[currentLocale];
131
+ const fallbackMessages = fallbackLocale ? messages[fallbackLocale] : undefined;
132
+
133
+ return translate(currentMessages, key, params, fallbackMessages);
134
+ };
135
+
136
+ /**
137
+ * Reactive translation — returns a computed signal.
138
+ */
139
+ const tc = (key: string, params: TranslateParams = {}) => {
140
+ return computed(() => {
141
+ // Reading $locale.value creates a reactive dependency
142
+ const currentLocale = $locale.value;
143
+ const currentMessages = messages[currentLocale];
144
+ const fallbackMessages = fallbackLocale ? messages[fallbackLocale] : undefined;
145
+
146
+ return translate(currentMessages, key, params, fallbackMessages);
147
+ });
148
+ };
149
+
150
+ /**
151
+ * Format a number with the current (or overridden) locale.
152
+ */
153
+ const n = (value: number, options?: NumberFormatOptions): string => {
154
+ const loc = options?.locale ?? $locale.value;
155
+ return formatNumber(value, loc, options);
156
+ };
157
+
158
+ /**
159
+ * Format a date with the current (or overridden) locale.
160
+ */
161
+ const d = (value: Date | number, options?: DateFormatOptions): string => {
162
+ const loc = options?.locale ?? $locale.value;
163
+ return formatDate(value, loc, options);
164
+ };
165
+
166
+ /**
167
+ * Merge additional messages into a locale.
168
+ */
169
+ const mergeMessages = (loc: string, newMessages: LocaleMessages): void => {
170
+ if (isPrototypePollutionKey(loc)) {
171
+ return;
172
+ }
173
+ if (!messages[loc]) {
174
+ messages[loc] = Object.create(null) as LocaleMessages;
175
+ loadedLocales.add(loc);
176
+ }
177
+ messages[loc] = deepMerge(messages[loc], sanitizeLocaleMessages(newMessages));
178
+ };
179
+
180
+ /**
181
+ * List all available locales (loaded + registered loaders).
182
+ */
183
+ const availableLocales = (): string[] => {
184
+ const locales = new Set<string>([...loadedLocales, ...loaders.keys()]);
185
+ return Array.from(locales).sort();
186
+ };
187
+
188
+ return {
189
+ $locale,
190
+ t,
191
+ tc,
192
+ loadLocale,
193
+ ensureLocale,
194
+ n,
195
+ d,
196
+ getMessages,
197
+ mergeMessages,
198
+ availableLocales,
199
+ };
200
+ };
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Internationalization (i18n) module for bQuery.js.
3
+ *
4
+ * Provides a reactive, TypeScript-first internationalization API
5
+ * with interpolation, pluralization, lazy-loading, and locale-aware
6
+ * formatting — all backed by bQuery's signal-based reactivity system.
7
+ *
8
+ * @module bquery/i18n
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * import { createI18n } from '@bquery/bquery/i18n';
13
+ *
14
+ * const i18n = createI18n({
15
+ * locale: 'en',
16
+ * fallbackLocale: 'en',
17
+ * messages: {
18
+ * en: {
19
+ * greeting: 'Hello, {name}!',
20
+ * items: '{count} item | {count} items',
21
+ * nested: { deep: { key: 'Found it!' } },
22
+ * },
23
+ * de: {
24
+ * greeting: 'Hallo, {name}!',
25
+ * items: '{count} Gegenstand | {count} Gegenstände',
26
+ * },
27
+ * },
28
+ * });
29
+ *
30
+ * // Basic translation
31
+ * i18n.t('greeting', { name: 'Ada' }); // 'Hello, Ada!'
32
+ *
33
+ * // Pluralization
34
+ * i18n.t('items', { count: 1 }); // '1 item'
35
+ * i18n.t('items', { count: 5 }); // '5 items'
36
+ *
37
+ * // Reactive translation (auto-updates on locale change)
38
+ * const label = i18n.tc('greeting', { name: 'Ada' });
39
+ * console.log(label.value); // 'Hello, Ada!'
40
+ *
41
+ * i18n.$locale.value = 'de';
42
+ * console.log(label.value); // 'Hallo, Ada!'
43
+ *
44
+ * // Number & date formatting
45
+ * i18n.n(1234.56); // '1,234.56' (en)
46
+ * i18n.d(new Date(), { dateStyle: 'long' }); // 'March 26, 2026'
47
+ *
48
+ * // Lazy-load a locale
49
+ * i18n.loadLocale('fr', () => import('./locales/fr.json'));
50
+ * await i18n.ensureLocale('fr');
51
+ * i18n.$locale.value = 'fr';
52
+ * ```
53
+ */
54
+
55
+ export { createI18n } from './i18n';
56
+ export { formatDate, formatNumber } from './formatting';
57
+
58
+ export type {
59
+ DateFormatOptions,
60
+ I18nConfig,
61
+ I18nInstance,
62
+ LocaleLoader,
63
+ LocaleMessages,
64
+ Messages,
65
+ NumberFormatOptions,
66
+ TranslateParams,
67
+ } from './types';
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Translation resolution helpers.
3
+ * @module bquery/i18n
4
+ * @internal
5
+ */
6
+
7
+ import { isPlainObject, isPrototypePollutionKey, merge } from '../core/utils/object';
8
+ import type { LocaleMessages, TranslateParams } from './types';
9
+
10
+ /**
11
+ * Resolves a dot-delimited key path against a messages object.
12
+ *
13
+ * @param messages - The locale messages
14
+ * @param key - Dot-delimited key (e.g. 'user.welcome')
15
+ * @returns The resolved string, or `undefined` if not found
16
+ *
17
+ * @internal
18
+ */
19
+ export const resolveKey = (messages: LocaleMessages, key: string): string | undefined => {
20
+ const parts = key.split('.');
21
+ let current: LocaleMessages | string = messages;
22
+
23
+ for (const part of parts) {
24
+ if (typeof current === 'string') return undefined;
25
+ if (current[part] === undefined) return undefined;
26
+ current = current[part];
27
+ }
28
+
29
+ return typeof current === 'string' ? current : undefined;
30
+ };
31
+
32
+ /**
33
+ * Interpolates `{param}` placeholders in a string.
34
+ *
35
+ * @param template - The template string with `{key}` placeholders
36
+ * @param params - Key-value pairs for replacement
37
+ * @returns The interpolated string
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * interpolate('Hello, {name}!', { name: 'Ada' });
42
+ * // → 'Hello, Ada!'
43
+ * ```
44
+ *
45
+ * @internal
46
+ */
47
+ export const interpolate = (template: string, params: TranslateParams): string => {
48
+ return template.replace(/\{(\w+)\}/g, (match, key: string) => {
49
+ if (key in params) {
50
+ return String(params[key]);
51
+ }
52
+ return match; // Leave unmatched placeholders as-is
53
+ });
54
+ };
55
+
56
+ /**
57
+ * Selects the correct plural form from a pipe-delimited string.
58
+ *
59
+ * Supports two formats:
60
+ * - **Two forms:** `"singular | plural"` — singular when count === 1
61
+ * - **Three forms:** `"zero | one | many"` — zero when count === 0,
62
+ * one when count === 1, many otherwise
63
+ *
64
+ * The `count` parameter must be present in `params` for pluralization.
65
+ * If no `count` param exists or the string has no pipes, the string is
66
+ * returned as-is.
67
+ *
68
+ * @param template - Pipe-delimited plural forms
69
+ * @param params - Must include a `count` key for plural selection
70
+ * @returns The selected form
71
+ *
72
+ * @example
73
+ * ```ts
74
+ * pluralize('{count} item | {count} items', { count: 1 });
75
+ * // → '{count} item'
76
+ *
77
+ * pluralize('no items | {count} item | {count} items', { count: 0 });
78
+ * // → 'no items'
79
+ * ```
80
+ *
81
+ * @internal
82
+ */
83
+ export const pluralize = (template: string, params: TranslateParams): string => {
84
+ if (!template.includes('|')) return template;
85
+ if (!('count' in params)) return template;
86
+
87
+ const count = Number(params.count);
88
+ const forms = template.split('|').map((s) => s.trim());
89
+
90
+ if (forms.length === 3) {
91
+ // zero | one | many
92
+ if (count === 0) return forms[0];
93
+ if (count === 1) return forms[1];
94
+ return forms[2];
95
+ }
96
+
97
+ if (forms.length === 2) {
98
+ // singular | plural
99
+ return count === 1 ? forms[0] : forms[1];
100
+ }
101
+
102
+ // More than 3 forms: use last form for "many"
103
+ if (count === 0 && forms.length > 0) return forms[0];
104
+ if (count === 1 && forms.length > 1) return forms[1];
105
+ return forms[forms.length - 1];
106
+ };
107
+
108
+ /**
109
+ * Full translation pipeline: resolve → pluralize → interpolate.
110
+ *
111
+ * @param messages - Locale messages
112
+ * @param key - Dot-delimited key path
113
+ * @param params - Interpolation + pluralization params
114
+ * @param fallbackMessages - Optional fallback locale messages
115
+ * @returns The translated string, or the key if not found
116
+ *
117
+ * @internal
118
+ */
119
+ export const translate = (
120
+ messages: LocaleMessages | undefined,
121
+ key: string,
122
+ params: TranslateParams,
123
+ fallbackMessages?: LocaleMessages
124
+ ): string => {
125
+ let template: string | undefined;
126
+
127
+ // Try current locale
128
+ if (messages) {
129
+ template = resolveKey(messages, key);
130
+ }
131
+
132
+ // Fallback locale
133
+ if (template === undefined && fallbackMessages) {
134
+ template = resolveKey(fallbackMessages, key);
135
+ }
136
+
137
+ // Key not found — return key as-is
138
+ if (template === undefined) {
139
+ return key;
140
+ }
141
+
142
+ // Pluralize first, then interpolate
143
+ const pluralized = pluralize(template, params);
144
+ return interpolate(pluralized, params);
145
+ };
146
+
147
+ /**
148
+ * Deep merges source into target and returns a sanitized, prototype-safe copy.
149
+ *
150
+ * @param target - Target messages object
151
+ * @param source - Source messages to merge
152
+ * @returns A new merged, sanitized messages object
153
+ *
154
+ * @internal
155
+ */
156
+ export const deepMerge = (target: LocaleMessages, source: LocaleMessages): LocaleMessages => {
157
+ const merged = merge(
158
+ target as Record<string, unknown>,
159
+ source as Record<string, unknown>
160
+ ) as LocaleMessages;
161
+
162
+ const cloneSafeMessages = (value: unknown): unknown => {
163
+ if (Array.isArray(value)) {
164
+ return value.map((entry) => cloneSafeMessages(entry));
165
+ }
166
+
167
+ if (!isPlainObject(value)) {
168
+ return value;
169
+ }
170
+
171
+ const safeObject = Object.create(null) as Record<string, unknown>;
172
+ for (const [key, entry] of Object.entries(value)) {
173
+ if (isPrototypePollutionKey(key)) {
174
+ continue;
175
+ }
176
+ safeObject[key] = cloneSafeMessages(entry);
177
+ }
178
+ return safeObject;
179
+ };
180
+
181
+ return cloneSafeMessages(merged) as LocaleMessages;
182
+ };
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Type definitions for the i18n module.
3
+ * @module bquery/i18n
4
+ */
5
+
6
+ import type { ReadonlySignal, Signal } from '../reactive/index';
7
+
8
+ /**
9
+ * A nested record of translation messages.
10
+ * Values can be strings or nested objects for namespacing.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * const messages: LocaleMessages = {
15
+ * greeting: 'Hello',
16
+ * user: {
17
+ * name: 'Name',
18
+ * welcome: 'Welcome, {name}!',
19
+ * },
20
+ * items: '{count} item | {count} items',
21
+ * };
22
+ * ```
23
+ */
24
+ export type LocaleMessages = {
25
+ [key: string]: string | LocaleMessages;
26
+ };
27
+
28
+ /**
29
+ * Record of locale codes to their messages.
30
+ */
31
+ export type Messages = {
32
+ [locale: string]: LocaleMessages;
33
+ };
34
+
35
+ /**
36
+ * Interpolation parameters for translation strings.
37
+ * Values are converted to strings during interpolation.
38
+ */
39
+ export type TranslateParams = Record<string, string | number>;
40
+
41
+ /**
42
+ * Configuration for creating an i18n instance.
43
+ */
44
+ export type I18nConfig = {
45
+ /** The initial locale code (e.g. 'en', 'de', 'fr') */
46
+ locale: string;
47
+ /** Pre-loaded message dictionaries keyed by locale */
48
+ messages: Messages;
49
+ /** Fallback locale when a key is missing in the current locale */
50
+ fallbackLocale?: string;
51
+ };
52
+
53
+ /**
54
+ * A lazy-loader function that returns a promise resolving to locale messages.
55
+ */
56
+ export type LocaleLoader = () => Promise<LocaleMessages | { default: LocaleMessages }>;
57
+
58
+ /**
59
+ * Options for number formatting via `Intl.NumberFormat`.
60
+ */
61
+ export type NumberFormatOptions = Intl.NumberFormatOptions & {
62
+ /** Override the locale for this specific formatting call */
63
+ locale?: string;
64
+ };
65
+
66
+ /**
67
+ * Options for date formatting via `Intl.DateTimeFormat`.
68
+ */
69
+ export type DateFormatOptions = Intl.DateTimeFormatOptions & {
70
+ /** Override the locale for this specific formatting call */
71
+ locale?: string;
72
+ };
73
+
74
+ /**
75
+ * The public i18n instance returned by `createI18n()`.
76
+ */
77
+ export type I18nInstance = {
78
+ /**
79
+ * Reactive signal holding the current locale code.
80
+ * Setting `.value` switches the locale and reactively updates
81
+ * all computed translations.
82
+ */
83
+ $locale: Signal<string>;
84
+
85
+ /**
86
+ * Translate a key path with optional interpolation and pluralization.
87
+ *
88
+ * @param key - Dot-delimited key path (e.g. 'user.welcome')
89
+ * @param params - Interpolation values (e.g. `{ name: 'Ada' }`)
90
+ * @returns The translated string, or the key itself if not found
91
+ *
92
+ * @example
93
+ * ```ts
94
+ * i18n.t('greeting'); // 'Hello'
95
+ * i18n.t('user.welcome', { name: 'Ada' }); // 'Welcome, Ada!'
96
+ * i18n.t('items', { count: 3 }); // '3 items'
97
+ * ```
98
+ */
99
+ t: (key: string, params?: TranslateParams) => string;
100
+
101
+ /**
102
+ * Reactive translation — returns a ReadonlySignal that updates
103
+ * when the locale changes.
104
+ *
105
+ * @param key - Dot-delimited key path
106
+ * @param params - Interpolation values
107
+ * @returns A reactive signal containing the translated string
108
+ */
109
+ tc: (key: string, params?: TranslateParams) => ReadonlySignal<string>;
110
+
111
+ /**
112
+ * Register a lazy-loader for a locale's messages.
113
+ * The loader is invoked when the locale is first needed.
114
+ *
115
+ * @param locale - Locale code
116
+ * @param loader - Async function returning messages
117
+ *
118
+ * @example
119
+ * ```ts
120
+ * i18n.loadLocale('de', () => import('./locales/de.json'));
121
+ * ```
122
+ */
123
+ loadLocale: (locale: string, loader: LocaleLoader) => void;
124
+
125
+ /**
126
+ * Ensure a locale's messages are loaded (triggers lazy-loader if registered).
127
+ *
128
+ * @param locale - Locale code to load
129
+ * @returns A promise that resolves when the locale is ready
130
+ */
131
+ ensureLocale: (locale: string) => Promise<void>;
132
+
133
+ /**
134
+ * Format a number according to the current locale using `Intl.NumberFormat`.
135
+ *
136
+ * @param value - Number to format
137
+ * @param options - Intl.NumberFormat options
138
+ * @returns The formatted number string
139
+ */
140
+ n: (value: number, options?: NumberFormatOptions) => string;
141
+
142
+ /**
143
+ * Format a date according to the current locale using `Intl.DateTimeFormat`.
144
+ *
145
+ * @param value - Date to format
146
+ * @param options - Intl.DateTimeFormat options
147
+ * @returns The formatted date string
148
+ */
149
+ d: (value: Date | number, options?: DateFormatOptions) => string;
150
+
151
+ /**
152
+ * Get all currently loaded messages for a locale.
153
+ *
154
+ * @param locale - Locale code
155
+ * @returns The messages object, or undefined if not loaded
156
+ */
157
+ getMessages: (locale: string) => LocaleMessages | undefined;
158
+
159
+ /**
160
+ * Merge additional messages into an existing locale.
161
+ *
162
+ * @param locale - Locale code
163
+ * @param messages - Messages to merge (deep merge)
164
+ */
165
+ mergeMessages: (locale: string, messages: LocaleMessages) => void;
166
+
167
+ /**
168
+ * List all available locale codes (loaded or registered for lazy-loading).
169
+ */
170
+ availableLocales: () => string[];
171
+ };