@jant/core 0.6.16 → 0.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 (508) hide show
  1. package/README.md +11 -11
  2. package/bin/commands/assets/upload.js +192 -15
  3. package/bin/commands/import-site.js +60 -4
  4. package/bin/commands/reset-password.js +21 -8
  5. package/bin/commands/site/snapshot/export.js +1 -1
  6. package/bin/commands/site/snapshot/import.js +1 -1
  7. package/bin/lib/d1-query.js +5 -16
  8. package/bin/lib/node-env.js +40 -4
  9. package/bin/lib/site-selection.js +85 -29
  10. package/bin/lib/wrangler-cli.js +28 -2
  11. package/bin/lib/wrangler-json.js +35 -0
  12. package/dist/{app-C7_4oab9.js → app-K_Aa1MMn.js} +52476 -13330
  13. package/dist/client/.vite/manifest.json +255 -90
  14. package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
  15. package/dist/client/_assets/chunks/create-editor-CD3FhrOB.js +257 -0
  16. package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
  17. package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
  18. package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
  19. package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
  20. package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
  21. package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
  22. package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
  23. package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
  24. package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
  25. package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
  26. package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
  27. package/dist/client/_assets/chunks/sortable-list-CgaL2jCs.js +2 -0
  28. package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
  29. package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
  30. package/dist/client/_assets/chunks/unsafe-svg-0QCkP0vZ.js +2 -0
  31. package/dist/client/_assets/client-GiYENVw8.css +2 -0
  32. package/dist/client/_assets/client-VnFxJN7G.js +295 -0
  33. package/dist/client/_assets/client-auth-k8gJ6QJj.js +828 -0
  34. package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
  35. package/dist/client/_assets/client-compose-B_kDtWkW.js +2004 -0
  36. package/dist/client/_assets/client-manage-M90aSTOk.js +2170 -0
  37. package/dist/client/_assets/client-settings-DfYs9n1F.js +1112 -0
  38. package/dist/{export-Dl5KCiEs.js → github-sync-BPAvT999.js} +2254 -107
  39. package/dist/index.js +2 -5
  40. package/dist/node.js +5 -7
  41. package/package.json +3 -3
  42. package/src/__tests__/auth-session-freshness.test.ts +57 -0
  43. package/src/__tests__/basecoat-variants.test.ts +121 -0
  44. package/src/__tests__/bin/assets-upload.test.ts +68 -0
  45. package/src/__tests__/export-hugo-build.test.ts +260 -0
  46. package/src/__tests__/export-service.test.ts +231 -0
  47. package/src/__tests__/helpers/component-css.ts +91 -0
  48. package/src/__tests__/helpers/db.ts +33 -0
  49. package/src/__tests__/helpers/export-fixtures.ts +3 -0
  50. package/src/__tests__/stylesheet-audience.test.ts +159 -0
  51. package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
  52. package/src/app.tsx +71 -16
  53. package/src/auth.ts +18 -1
  54. package/src/client/__tests__/collection-page-actions.test.ts +51 -2
  55. package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
  56. package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
  57. package/src/client/__tests__/compose-bridge.test.ts +272 -0
  58. package/src/client/__tests__/compose-launch.test.ts +62 -1
  59. package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
  60. package/src/client/__tests__/compose-triggers.test.ts +90 -0
  61. package/src/client/__tests__/copy-field.test.ts +162 -0
  62. package/src/client/__tests__/custom-url-menu.test.ts +127 -6
  63. package/src/client/__tests__/image-processor.test.ts +324 -2
  64. package/src/client/__tests__/lazy-entries.test.ts +92 -0
  65. package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
  66. package/src/client/__tests__/runtime-paths.test.ts +84 -0
  67. package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
  68. package/src/client/__tests__/toast.test.ts +23 -0
  69. package/src/client/__tests__/video-processor-probe.test.ts +175 -0
  70. package/src/client/__tests__/video-processor.test.ts +181 -0
  71. package/src/client/audio-processor.ts +24 -15
  72. package/src/client/audio-waveform.ts +49 -0
  73. package/src/client/collection-dialog-host.ts +72 -0
  74. package/src/client/collection-navigation.ts +51 -16
  75. package/src/client/collection-page-actions.ts +26 -0
  76. package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
  77. package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
  78. package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
  79. package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
  80. package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
  81. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
  82. package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
  83. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
  84. package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
  85. package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
  86. package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
  87. package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
  88. package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
  89. package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
  90. package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
  91. package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
  92. package/src/client/components/collection-dialog-types.ts +49 -0
  93. package/src/client/components/collection-manager-types.ts +32 -1
  94. package/src/client/components/collection-types.ts +1 -15
  95. package/src/client/components/compose-types.ts +75 -1
  96. package/src/client/components/jant-collection-dialog.ts +722 -0
  97. package/src/client/components/jant-collection-directory.ts +567 -182
  98. package/src/client/components/jant-collection-form.ts +53 -232
  99. package/src/client/components/jant-command-palette.ts +100 -49
  100. package/src/client/components/jant-compose-dialog.ts +1486 -707
  101. package/src/client/components/jant-compose-editor.ts +673 -336
  102. package/src/client/components/jant-compose-fullscreen.ts +51 -43
  103. package/src/client/components/jant-config-editor.ts +65 -53
  104. package/src/client/components/jant-confirm-dialog.ts +13 -11
  105. package/src/client/components/jant-locale-picker.ts +418 -0
  106. package/src/client/components/jant-media-lightbox.ts +146 -136
  107. package/src/client/components/jant-nav-manager.ts +690 -366
  108. package/src/client/components/jant-post-menu.ts +1086 -282
  109. package/src/client/components/jant-post-picker.ts +324 -0
  110. package/src/client/components/jant-repo-picker.ts +138 -106
  111. package/src/client/components/jant-settings-avatar.ts +31 -27
  112. package/src/client/components/jant-settings-general.ts +617 -543
  113. package/src/client/components/jant-settings-language.ts +1087 -0
  114. package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
  115. package/src/client/components/jant-text-preview.ts +94 -86
  116. package/src/client/components/nav-manager-types.ts +62 -1
  117. package/src/client/components/settings-types.ts +14 -29
  118. package/src/client/components/smart-collection-conditions.ts +285 -0
  119. package/src/client/components/smart-collection-dialog-types.ts +68 -0
  120. package/src/client/compose-bridge.ts +214 -141
  121. package/src/client/compose-launch.ts +76 -2
  122. package/src/client/compose-shortcuts.ts +6 -8
  123. package/src/client/compose-triggers.ts +50 -0
  124. package/src/client/copy-field.ts +108 -0
  125. package/src/client/custom-url-menu.ts +60 -4
  126. package/src/client/icons.ts +14 -0
  127. package/src/client/image-plan.ts +165 -0
  128. package/src/client/image-processor.ts +195 -117
  129. package/src/client/image-worker-client.ts +45 -0
  130. package/src/client/image-worker.ts +135 -0
  131. package/src/client/lazy-entries.ts +95 -0
  132. package/src/client/lazy-slugify.ts +2 -2
  133. package/src/client/media-scroll-hint.ts +43 -2
  134. package/src/client/mediabunny.ts +29 -0
  135. package/src/client/post-picker.ts +41 -0
  136. package/src/client/runtime-paths.ts +74 -1
  137. package/src/client/settings-bridge.ts +4 -10
  138. package/src/client/site-header-nav.js +18 -6
  139. package/src/client/smart-collection-dialog-host.ts +116 -0
  140. package/src/client/smart-collection-page-actions.ts +208 -0
  141. package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
  142. package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
  143. package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
  144. package/src/client/tiptap/bubble-menu.ts +47 -16
  145. package/src/client/tiptap/floating-position.ts +216 -2
  146. package/src/client/tiptap/inline-image-upload.ts +7 -3
  147. package/src/client/tiptap/link-toolbar.ts +22 -9
  148. package/src/client/toast.ts +13 -0
  149. package/src/client/types/vite-worker.d.ts +12 -0
  150. package/src/client/upload-with-metadata.ts +3 -13
  151. package/src/client/video-processor.ts +351 -107
  152. package/src/client-auth.ts +14 -31
  153. package/src/client-compose.ts +19 -0
  154. package/src/client-manage.ts +13 -0
  155. package/src/client-settings.ts +14 -0
  156. package/src/client-site.ts +10 -3
  157. package/src/client.ts +4 -0
  158. package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
  159. package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
  160. package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
  161. package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
  162. package/src/db/migrations/0032_married_tomas.sql +3 -0
  163. package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
  164. package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
  165. package/src/db/migrations/meta/0032_snapshot.json +2582 -0
  166. package/src/db/migrations/meta/0033_snapshot.json +2816 -0
  167. package/src/db/migrations/meta/0034_snapshot.json +2816 -0
  168. package/src/db/migrations/meta/_journal.json +21 -0
  169. package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
  170. package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
  171. package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
  172. package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
  173. package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
  174. package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
  175. package/src/db/migrations/pg/meta/_journal.json +21 -0
  176. package/src/db/pg/schema.ts +195 -27
  177. package/src/db/post-visibility.ts +117 -0
  178. package/src/db/schema.ts +222 -27
  179. package/src/i18n/__tests__/detect.test.ts +26 -81
  180. package/src/i18n/__tests__/supported-locales.test.ts +83 -0
  181. package/src/i18n/context.tsx +12 -2
  182. package/src/i18n/detect.ts +7 -66
  183. package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
  184. package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
  185. package/src/i18n/locales/public/en.po +490 -158
  186. package/src/i18n/locales/public/en.ts +1 -1
  187. package/src/i18n/locales/public/zh-Hans.po +487 -155
  188. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  189. package/src/i18n/locales/public/zh-Hant.po +487 -155
  190. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  191. package/src/i18n/locales/settings/en.po +551 -69
  192. package/src/i18n/locales/settings/en.ts +1 -1
  193. package/src/i18n/locales/settings/zh-Hans.po +597 -115
  194. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  195. package/src/i18n/locales/settings/zh-Hant.po +610 -128
  196. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  197. package/src/i18n/locales.ts +94 -0
  198. package/src/i18n/middleware.ts +12 -2
  199. package/src/i18n/supported-locales.ts +80 -0
  200. package/src/lib/__tests__/api-settings.test.ts +11 -2
  201. package/src/lib/__tests__/asset-path.test.ts +92 -0
  202. package/src/lib/__tests__/deferred.test.ts +90 -0
  203. package/src/lib/__tests__/discover-ping.test.ts +89 -0
  204. package/src/lib/__tests__/discover.test.ts +410 -0
  205. package/src/lib/__tests__/feed-policy.test.ts +26 -0
  206. package/src/lib/__tests__/feed.test.ts +1526 -15
  207. package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
  208. package/src/lib/__tests__/hosted-signin.test.ts +1 -1
  209. package/src/lib/__tests__/http-cache.test.ts +162 -0
  210. package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
  211. package/src/lib/__tests__/jant-docs.test.ts +23 -0
  212. package/src/lib/__tests__/lang-detect.test.ts +231 -0
  213. package/src/lib/__tests__/media-helpers.test.ts +81 -0
  214. package/src/lib/__tests__/navigation.test.ts +135 -1
  215. package/src/lib/__tests__/resolve-config.test.ts +64 -3
  216. package/src/lib/__tests__/schemas.test.ts +42 -4
  217. package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
  218. package/src/lib/__tests__/slug.test.ts +70 -0
  219. package/src/lib/__tests__/slugify.test.ts +85 -0
  220. package/src/lib/__tests__/storage.test.ts +32 -0
  221. package/src/lib/__tests__/thread-fold.test.ts +113 -0
  222. package/src/lib/__tests__/upload-sideload.test.ts +14 -0
  223. package/src/lib/__tests__/url-fetch.test.ts +1 -2
  224. package/src/lib/__tests__/url.test.ts +78 -72
  225. package/src/lib/__tests__/view-language.test.ts +413 -0
  226. package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
  227. package/src/lib/address.ts +57 -0
  228. package/src/lib/api-posts.ts +12 -0
  229. package/src/lib/asset-path.ts +98 -1
  230. package/src/lib/build-env.ts +16 -0
  231. package/src/lib/collection-groups.ts +5 -1
  232. package/src/lib/collection-paths.ts +3 -10
  233. package/src/lib/constants.ts +28 -3
  234. package/src/lib/copy-field.ts +36 -0
  235. package/src/lib/crypto.ts +40 -0
  236. package/src/lib/csp-builder.ts +2 -1
  237. package/src/lib/deferred.ts +79 -0
  238. package/src/lib/discover-ping.ts +141 -0
  239. package/src/lib/discover.ts +437 -0
  240. package/src/lib/env.ts +134 -0
  241. package/src/lib/errors.ts +33 -0
  242. package/src/lib/feed-policy.ts +236 -2
  243. package/src/lib/feed.ts +926 -73
  244. package/src/lib/filter-dimensions.ts +1349 -0
  245. package/src/lib/github-sync-site-config.ts +3 -0
  246. package/src/lib/hosted-signin.ts +1 -1
  247. package/src/lib/http-cache.ts +148 -0
  248. package/src/lib/hugo-markdown.ts +16 -0
  249. package/src/lib/icons.ts +9 -2
  250. package/src/lib/ids.ts +3 -0
  251. package/src/lib/jant-docs.ts +42 -0
  252. package/src/lib/lang-detect.ts +308 -0
  253. package/src/lib/media-helpers.ts +13 -1
  254. package/src/lib/navigation.ts +78 -11
  255. package/src/lib/pagination.ts +28 -24
  256. package/src/lib/per-language-surfaces.ts +67 -0
  257. package/src/lib/post-meta.ts +19 -0
  258. package/src/lib/render.tsx +55 -7
  259. package/src/lib/resolve-config.ts +53 -5
  260. package/src/lib/schemas.ts +201 -10
  261. package/src/lib/site-header-fragment.tsx +2 -1
  262. package/src/lib/site-unavailable-page.ts +47 -0
  263. package/src/lib/slug.ts +51 -7
  264. package/src/lib/slugify.ts +113 -0
  265. package/src/lib/startup-config.ts +4 -6
  266. package/src/lib/storage.ts +7 -0
  267. package/src/lib/summary.ts +50 -0
  268. package/src/lib/thread-fold.ts +161 -0
  269. package/src/lib/time.ts +32 -0
  270. package/src/lib/timeline.ts +42 -1
  271. package/src/lib/tiptap-render.ts +1 -2
  272. package/src/lib/upload.ts +16 -13
  273. package/src/lib/url.ts +139 -86
  274. package/src/lib/version.ts +10 -8
  275. package/src/lib/view-language.ts +436 -0
  276. package/src/lib/view.ts +51 -23
  277. package/src/lib/viewer-context.ts +62 -0
  278. package/src/middleware/__tests__/auth.test.ts +70 -2
  279. package/src/middleware/__tests__/cache-control.test.ts +71 -3
  280. package/src/middleware/__tests__/onboarding.test.ts +76 -5
  281. package/src/middleware/__tests__/secure-headers.test.ts +1 -1
  282. package/src/middleware/__tests__/session.test.ts +181 -33
  283. package/src/middleware/auth.ts +50 -31
  284. package/src/middleware/cache-control.ts +75 -10
  285. package/src/middleware/config.ts +6 -6
  286. package/src/middleware/error-handler.ts +12 -0
  287. package/src/middleware/onboarding.ts +32 -10
  288. package/src/middleware/secure-headers.ts +1 -1
  289. package/src/middleware/session.ts +96 -8
  290. package/src/node/__tests__/cli-migrate.test.ts +21 -19
  291. package/src/node/__tests__/cli-reset-password.test.ts +81 -8
  292. package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
  293. package/src/node/__tests__/cli-site-selection.test.ts +128 -38
  294. package/src/node/__tests__/runtime.test.ts +37 -0
  295. package/src/node/index.ts +1 -0
  296. package/src/preset.css +76 -22
  297. package/src/routes/__tests__/feed-validator.test.ts +156 -0
  298. package/src/routes/api/__tests__/collections.test.ts +114 -0
  299. package/src/routes/api/__tests__/discover.test.ts +222 -0
  300. package/src/routes/api/__tests__/nav-items.test.ts +212 -2
  301. package/src/routes/api/__tests__/post-translations.test.ts +829 -0
  302. package/src/routes/api/__tests__/settings.test.ts +24 -3
  303. package/src/routes/api/__tests__/telegram.test.ts +6 -5
  304. package/src/routes/api/collections.ts +49 -6
  305. package/src/routes/api/custom-urls.ts +0 -1
  306. package/src/routes/api/discover.ts +95 -0
  307. package/src/routes/api/export.ts +3 -0
  308. package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
  309. package/src/routes/api/nav-items.ts +55 -8
  310. package/src/routes/api/posts.ts +159 -0
  311. package/src/routes/api/public/__tests__/archive.test.ts +111 -0
  312. package/src/routes/api/public/archive.ts +77 -81
  313. package/src/routes/api/public/posts.ts +23 -6
  314. package/src/routes/api/smart-collections.ts +132 -0
  315. package/src/routes/api/telegram.ts +2 -40
  316. package/src/routes/api/uploads.ts +1 -2
  317. package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
  318. package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
  319. package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
  320. package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
  321. package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
  322. package/src/routes/auth/__tests__/setup.test.ts +278 -26
  323. package/src/routes/auth/__tests__/signin.test.ts +135 -0
  324. package/src/routes/auth/setup.tsx +934 -186
  325. package/src/routes/auth/signin.tsx +10 -8
  326. package/src/routes/compose.tsx +5 -0
  327. package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
  328. package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
  329. package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
  330. package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
  331. package/src/routes/dash/custom-urls.tsx +1 -37
  332. package/src/routes/dash/settings.tsx +502 -41
  333. package/src/routes/discover-announce.ts +96 -0
  334. package/src/routes/feed/__tests__/feed.test.ts +165 -2
  335. package/src/routes/feed/feed.ts +69 -40
  336. package/src/routes/feed/sitemap.ts +95 -11
  337. package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
  338. package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
  339. package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
  340. package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
  341. package/src/routes/pages/__tests__/collections.test.ts +9 -3
  342. package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
  343. package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
  344. package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
  345. package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
  346. package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
  347. package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
  348. package/src/routes/pages/archive.tsx +471 -369
  349. package/src/routes/pages/collection.tsx +94 -96
  350. package/src/routes/pages/collections.tsx +38 -50
  351. package/src/routes/pages/featured.tsx +32 -13
  352. package/src/routes/pages/home.tsx +25 -6
  353. package/src/routes/pages/language.tsx +113 -0
  354. package/src/routes/pages/latest.tsx +26 -16
  355. package/src/routes/pages/new.tsx +3 -0
  356. package/src/routes/pages/page.tsx +178 -17
  357. package/src/routes/pages/partials.tsx +41 -0
  358. package/src/routes/pages/search.tsx +18 -3
  359. package/src/routes/pages/smart-collection.tsx +340 -0
  360. package/src/routes/pages/subscribe.tsx +182 -0
  361. package/src/runtime/__tests__/index.test.ts +19 -0
  362. package/src/runtime/__tests__/node.test.ts +129 -4
  363. package/src/runtime/cloudflare.ts +21 -2
  364. package/src/runtime/index.ts +27 -1
  365. package/src/runtime/node.ts +18 -1
  366. package/src/runtime/site.ts +95 -24
  367. package/src/services/__tests__/auth.test.ts +116 -7
  368. package/src/services/__tests__/collection.test.ts +108 -4
  369. package/src/services/__tests__/language.test.ts +495 -0
  370. package/src/services/__tests__/navigation.test.ts +213 -10
  371. package/src/services/__tests__/path.test.ts +180 -0
  372. package/src/services/__tests__/post-language.test.ts +830 -0
  373. package/src/services/__tests__/post.test.ts +46 -9
  374. package/src/services/__tests__/settings.test.ts +228 -17
  375. package/src/services/__tests__/site-admin.test.ts +5 -5
  376. package/src/services/__tests__/smart-collection.test.ts +651 -0
  377. package/src/services/about-page.ts +22 -1
  378. package/src/services/auth.ts +51 -49
  379. package/src/services/bootstrap.ts +188 -38
  380. package/src/services/collection-directory-position.ts +76 -0
  381. package/src/services/collection.ts +331 -46
  382. package/src/services/custom-url.ts +27 -22
  383. package/src/services/export-theme/assets/client-site.css +1 -1
  384. package/src/services/export-theme/assets/client-site.js +123 -123
  385. package/src/services/export-theme/layouts/_default/rss.xml +77 -1
  386. package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
  387. package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
  388. package/src/services/export-theme/layouts/partials/pagination.html +14 -13
  389. package/src/services/export-theme/layouts/partials/post-card.html +8 -4
  390. package/src/services/export-theme/layouts/partials/reply.html +2 -1
  391. package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
  392. package/src/services/export-theme/styles/main.css +13 -1
  393. package/src/services/export.ts +278 -19
  394. package/src/services/index.ts +34 -3
  395. package/src/services/language.ts +412 -0
  396. package/src/services/mcp.ts +3 -1
  397. package/src/services/media.ts +14 -6
  398. package/src/services/navigation.ts +429 -67
  399. package/src/services/path.ts +221 -6
  400. package/src/services/post.ts +1021 -152
  401. package/src/services/search.ts +39 -2
  402. package/src/services/settings.ts +238 -43
  403. package/src/services/site-admin.ts +24 -7
  404. package/src/services/site.ts +77 -1
  405. package/src/services/smart-collection.ts +636 -0
  406. package/src/services/telegram.ts +20 -22
  407. package/src/services/upload-session.ts +1 -3
  408. package/src/style-author.css +27 -0
  409. package/src/styles/components-author.css +1622 -0
  410. package/src/styles/components.css +24 -1571
  411. package/src/styles/site-media.css +16 -2
  412. package/src/styles/tokens.css +82 -12
  413. package/src/styles/ui-author.css +7602 -0
  414. package/src/styles/ui.css +1087 -7219
  415. package/src/types/app-context.ts +7 -0
  416. package/src/types/bindings.ts +3 -0
  417. package/src/types/config.ts +94 -15
  418. package/src/types/constants.ts +188 -16
  419. package/src/types/entities.ts +87 -1
  420. package/src/types/operations.ts +64 -2
  421. package/src/types/props.ts +157 -31
  422. package/src/types/views.ts +55 -1
  423. package/src/ui/__tests__/font-themes.test.ts +52 -12
  424. package/src/ui/compose/ComposeDialog.tsx +201 -7
  425. package/src/ui/compose/ComposePrompt.tsx +1 -5
  426. package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
  427. package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
  428. package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
  429. package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
  430. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
  431. package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
  432. package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
  433. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  434. package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
  435. package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
  436. package/src/ui/dash/settings/GeneralContent.tsx +275 -113
  437. package/src/ui/dash/settings/LanguageContent.tsx +409 -0
  438. package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
  439. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
  440. package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
  441. package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
  442. package/src/ui/dash/settings/discover-copy.ts +116 -0
  443. package/src/ui/feed/LinkCard.tsx +55 -45
  444. package/src/ui/feed/NoteCard.tsx +9 -4
  445. package/src/ui/feed/PostStatusBadges.tsx +90 -43
  446. package/src/ui/feed/QuoteCard.tsx +55 -30
  447. package/src/ui/feed/ThreadPreview.tsx +6 -2
  448. package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
  449. package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
  450. package/src/ui/feed/thread-preview-state.ts +20 -6
  451. package/src/ui/font-themes.ts +39 -29
  452. package/src/ui/layouts/BaseLayout.tsx +155 -10
  453. package/src/ui/layouts/SiteLayout.tsx +192 -34
  454. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
  455. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
  456. package/src/ui/pages/ArchivePage.tsx +282 -289
  457. package/src/ui/pages/CollectionPage.tsx +86 -76
  458. package/src/ui/pages/CollectionsPage.tsx +5 -0
  459. package/src/ui/pages/ComposePage.tsx +8 -0
  460. package/src/ui/pages/PostPage.tsx +47 -0
  461. package/src/ui/pages/SearchPage.tsx +3 -3
  462. package/src/ui/pages/SmartCollectionPage.tsx +434 -0
  463. package/src/ui/pages/SubscribePage.tsx +236 -0
  464. package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
  465. package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
  466. package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
  467. package/src/ui/shared/CollectionDirectory.tsx +178 -3
  468. package/src/ui/shared/CollectionsManager.tsx +70 -17
  469. package/src/ui/shared/CopyField.tsx +127 -0
  470. package/src/ui/shared/EmptyPostContent.tsx +23 -0
  471. package/src/ui/shared/MediaGallery.tsx +125 -46
  472. package/src/ui/shared/Pagination.tsx +20 -5
  473. package/src/ui/shared/PostFooter.tsx +20 -11
  474. package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
  475. package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
  476. package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
  477. package/src/ui/shared/__tests__/pagination.test.ts +38 -24
  478. package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
  479. package/src/ui/shared/archive-labels.ts +98 -0
  480. package/src/ui/shared/collection-management-labels.ts +94 -16
  481. package/src/ui/shared/navigation-labels.ts +45 -71
  482. package/src/ui/shared/post-article-attributes.ts +2 -0
  483. package/src/ui/shared/smart-collection-labels.ts +343 -0
  484. package/dist/app-C75Zc8dD.js +0 -6
  485. package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
  486. package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
  487. package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
  488. package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
  489. package/dist/client/_assets/client-D3VZyG4L.css +0 -2
  490. package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
  491. package/dist/env-BPYViDJJ.js +0 -281
  492. package/dist/github-api-BNF35Lkx.js +0 -176
  493. package/dist/github-app-DdiD-bC4.js +0 -275
  494. package/dist/github-sync-Bqg62IvV.js +0 -4
  495. package/dist/github-sync-HdK2EgvJ.js +0 -431
  496. package/dist/url-CbLAtlZe.js +0 -900
  497. package/src/client/__tests__/collection-created-notice.test.ts +0 -36
  498. package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
  499. package/src/client/__tests__/post-form-bridge.test.ts +0 -82
  500. package/src/client/collection-created-notice.ts +0 -66
  501. package/src/client/collection-form-bridge.ts +0 -191
  502. package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
  503. package/src/client/components/jant-post-form.ts +0 -422
  504. package/src/client/components/post-form-template.ts +0 -352
  505. package/src/client/components/post-form-types.ts +0 -96
  506. package/src/client/media-metadata.ts +0 -247
  507. package/src/client/post-form-bridge.ts +0 -193
  508. package/src/ui/pages/CollectionEditorPage.tsx +0 -175
@@ -0,0 +1,495 @@
1
+ /**
2
+ * Multilingual lifecycle: enabling, disabling, and the invariants that keep the
3
+ * language set and the `post.language` column agreeing with each other.
4
+ */
5
+
6
+ import { describe, it, expect, beforeEach } from "vitest";
7
+ import {
8
+ createTestDatabase,
9
+ DEFAULT_TEST_SITE_ID,
10
+ } from "../../__tests__/helpers/db.js";
11
+ import { createCollectionService } from "../collection.js";
12
+ import { createCustomUrlService } from "../custom-url.js";
13
+ import { createLanguageService, type LanguageService } from "../language.js";
14
+ import { createPathService } from "../path.js";
15
+ import { createPostService } from "../post.js";
16
+ import { createSettingsService } from "../settings.js";
17
+ import type { Database } from "../../db/index.js";
18
+
19
+ function noteBody(text: string): string {
20
+ return JSON.stringify({
21
+ type: "doc",
22
+ content: [{ type: "paragraph", content: [{ type: "text", text }] }],
23
+ });
24
+ }
25
+
26
+ describe("LanguageService", () => {
27
+ let db: Database;
28
+ let language: LanguageService;
29
+ let posts: ReturnType<typeof createPostService>;
30
+ let settings: ReturnType<typeof createSettingsService>;
31
+ let paths: ReturnType<typeof createPathService>;
32
+ let collections: ReturnType<typeof createCollectionService>;
33
+
34
+ beforeEach(async () => {
35
+ const testDb = createTestDatabase();
36
+ db = testDb.db as unknown as Database;
37
+ paths = createPathService(db, DEFAULT_TEST_SITE_ID);
38
+ settings = createSettingsService(db, DEFAULT_TEST_SITE_ID);
39
+ posts = createPostService(
40
+ db,
41
+ { slugIdLength: 5 },
42
+ DEFAULT_TEST_SITE_ID,
43
+ paths,
44
+ );
45
+ collections = createCollectionService(db, DEFAULT_TEST_SITE_ID, paths);
46
+ language = createLanguageService({ settings, posts, paths });
47
+ await settings.set("SITE_LANGUAGE", "zh-Hans");
48
+ });
49
+
50
+ describe("state", () => {
51
+ it("reports a single-language site as off", async () => {
52
+ const state = await language.getState();
53
+
54
+ expect(state).toEqual({
55
+ enabled: false,
56
+ primary: "zh-Hans",
57
+ additional: [],
58
+ all: ["zh-Hans"],
59
+ });
60
+ });
61
+
62
+ it("stays off when the flag is set but no other language is configured", async () => {
63
+ await settings.set("MULTILINGUAL_ENABLED", "true");
64
+
65
+ expect((await language.getState()).enabled).toBe(false);
66
+ });
67
+
68
+ it("drops the primary language from the additional list", async () => {
69
+ await settings.set("MULTILINGUAL_ENABLED", "true");
70
+ await settings.set("ADDITIONAL_LANGUAGES", "zh-Hans,en");
71
+
72
+ const state = await language.getState();
73
+ expect(state.additional).toEqual(["en"]);
74
+ expect(state.all).toEqual(["zh-Hans", "en"]);
75
+ });
76
+ });
77
+
78
+ describe("enable", () => {
79
+ it("stamps existing posts and turns the feature on", async () => {
80
+ const post = await posts.create({
81
+ format: "note",
82
+ body: noteBody("旧文"),
83
+ });
84
+
85
+ const { markedCount } = await language.enable({
86
+ primary: "zh-Hans",
87
+ additional: ["en"],
88
+ });
89
+
90
+ expect(markedCount).toBe(1);
91
+ expect((await posts.getById(post.id))?.language).toBe("zh-Hans");
92
+ expect(await language.getState()).toMatchObject({
93
+ enabled: true,
94
+ primary: "zh-Hans",
95
+ additional: ["en"],
96
+ });
97
+ });
98
+
99
+ it("marks with the language chosen in the dialog, not the stored one", async () => {
100
+ // The stored site language may never have been set deliberately, so the
101
+ // dialog's value wins and is written back.
102
+ const post = await posts.create({
103
+ format: "note",
104
+ body: noteBody("English post"),
105
+ });
106
+
107
+ await language.enable({ primary: "en", additional: ["zh-Hans"] });
108
+
109
+ expect((await posts.getById(post.id))?.language).toBe("en");
110
+ expect(await settings.get("SITE_LANGUAGE")).toBe("en");
111
+ });
112
+
113
+ it("refuses to turn on with no second language", async () => {
114
+ await expect(
115
+ language.enable({ primary: "zh-Hans", additional: [] }),
116
+ ).rejects.toThrow(/at least one more language/);
117
+ expect((await language.getState()).enabled).toBe(false);
118
+ });
119
+
120
+ it("ignores the primary language repeated in the additional list", async () => {
121
+ await expect(
122
+ language.enable({ primary: "zh-Hans", additional: ["zh-Hans"] }),
123
+ ).rejects.toThrow(/at least one more language/);
124
+ });
125
+
126
+ it("normalizes the tags it stores", async () => {
127
+ await language.enable({ primary: "ZH-hans", additional: ["EN", "JA"] });
128
+
129
+ expect(await settings.get("ADDITIONAL_LANGUAGES")).toBe("en,ja");
130
+ expect(await settings.get("SITE_LANGUAGE")).toBe("zh-Hans");
131
+ });
132
+
133
+ // Turning multilingual off keeps both the languages and the stamps, and
134
+ // the re-enable dialog lets the author edit the list freely — so enable
135
+ // itself must hold the invariant the remove guard holds, or dropping a
136
+ // language there would strand its posts outside every view.
137
+ it("refuses a list that leaves stamped posts without a language", async () => {
138
+ await language.enable({ primary: "zh-Hans", additional: ["ja"] });
139
+ await posts.create({
140
+ format: "note",
141
+ body: noteBody("日本語の投稿"),
142
+ language: "ja",
143
+ });
144
+ await language.disable();
145
+
146
+ await expect(
147
+ language.enable({ primary: "zh-Hans", additional: ["en"] }),
148
+ ).rejects.toThrow(/still written in a language missing from this list/);
149
+ // Nothing was written: the site stays off with its old lists intact.
150
+ expect((await language.getState()).enabled).toBe(false);
151
+ expect(await settings.get("ADDITIONAL_LANGUAGES")).toBe("ja");
152
+ });
153
+
154
+ it("accepts any list that seats every stamped language", async () => {
155
+ await language.enable({ primary: "zh-Hans", additional: ["ja"] });
156
+ await posts.create({
157
+ format: "note",
158
+ body: noteBody("日本語の投稿"),
159
+ language: "ja",
160
+ });
161
+ await language.disable();
162
+
163
+ // ja changes seats — it becomes the primary — and en joins fresh.
164
+ await language.enable({ primary: "ja", additional: ["zh-Hans", "en"] });
165
+
166
+ expect(await language.getState()).toMatchObject({
167
+ enabled: true,
168
+ primary: "ja",
169
+ additional: ["zh-Hans", "en"],
170
+ });
171
+ });
172
+ });
173
+
174
+ describe("URL prefix conflicts", () => {
175
+ it("refuses a language whose prefix is taken by a post", async () => {
176
+ await posts.create({
177
+ format: "note",
178
+ slug: "ja",
179
+ title: "Just a post",
180
+ body: noteBody("hello"),
181
+ });
182
+
183
+ await expect(
184
+ language.enable({ primary: "zh-Hans", additional: ["ja"] }),
185
+ ).rejects.toThrow(/\/ja is already taken/);
186
+ });
187
+
188
+ it("refuses a language whose prefix shadows a nested path", async () => {
189
+ await posts.create({
190
+ format: "note",
191
+ path: "ja/notes",
192
+ title: "Nested",
193
+ body: noteBody("hello"),
194
+ });
195
+
196
+ await expect(language.addLanguage("ja")).rejects.toThrow(/already taken/);
197
+ });
198
+
199
+ it("refuses a language whose prefix is a Jant application route", async () => {
200
+ // `is` is a real ISO 639 code (Icelandic) — but so are `it`, `no`, and
201
+ // `go`, which is why prefixes are checked here rather than reserved
202
+ // globally.
203
+ await settings.set("SITE_LANGUAGE", "en");
204
+ await expect(
205
+ language.enable({ primary: "en", additional: ["archive"] }),
206
+ ).rejects.toThrow(/Jant already uses/);
207
+ });
208
+
209
+ it("leaves the site untouched when one language of several conflicts", async () => {
210
+ await posts.create({
211
+ format: "note",
212
+ slug: "ja",
213
+ title: "Just a post",
214
+ body: noteBody("hello"),
215
+ });
216
+
217
+ await expect(
218
+ language.enable({ primary: "zh-Hans", additional: ["en", "ja"] }),
219
+ ).rejects.toThrow();
220
+
221
+ expect((await language.getState()).enabled).toBe(false);
222
+ expect(await settings.get("ADDITIONAL_LANGUAGES")).toBeNull();
223
+ });
224
+
225
+ it("does not mind a collection whose slug merely starts with the letters", async () => {
226
+ await collections.create({ slug: "japanese", title: "Japanese" });
227
+
228
+ await expect(language.addLanguage("ja")).resolves.toBeUndefined();
229
+ });
230
+
231
+ it("blocks a new custom URL from claiming a live language prefix", async () => {
232
+ await language.enable({ primary: "zh-Hans", additional: ["en"] });
233
+ const customUrls = createCustomUrlService(
234
+ db,
235
+ DEFAULT_TEST_SITE_ID,
236
+ paths,
237
+ );
238
+
239
+ await expect(
240
+ customUrls.create({
241
+ path: "en",
242
+ targetType: "redirect",
243
+ toPath: "somewhere",
244
+ }),
245
+ ).rejects.toThrow(/reserved/);
246
+ expect(await customUrls.isPathAvailable("en")).toBe(false);
247
+ });
248
+
249
+ it("blocks a new post slug from claiming a live language prefix", async () => {
250
+ await language.enable({ primary: "zh-Hans", additional: ["en"] });
251
+
252
+ await expect(
253
+ posts.create({ format: "note", slug: "en", body: noteBody("hi") }),
254
+ ).rejects.toThrow(/reserved/);
255
+ });
256
+
257
+ it("leaves those slugs available while multilingual is off", async () => {
258
+ const post = await posts.create({
259
+ format: "note",
260
+ slug: "en",
261
+ body: noteBody("hi"),
262
+ });
263
+
264
+ expect(post.slug).toBe("en");
265
+ });
266
+ });
267
+
268
+ describe("changing the primary language", () => {
269
+ beforeEach(async () => {
270
+ await language.enable({ primary: "zh-Hans", additional: ["en", "ja"] });
271
+ });
272
+
273
+ it("swaps both lists in one step", async () => {
274
+ await language.setPrimary("en");
275
+
276
+ const state = await language.getState();
277
+ expect(state.primary).toBe("en");
278
+ // Both halves: `en` leaves the list, `zh-Hans` joins it. Doing only one
279
+ // would leave a language's posts with no view at all.
280
+ expect(state.additional).toEqual(["ja", "zh-Hans"]);
281
+ });
282
+
283
+ it("does not touch any post", async () => {
284
+ const post = await posts.create({
285
+ format: "note",
286
+ body: noteBody("中文"),
287
+ language: "zh-Hans",
288
+ });
289
+
290
+ await language.setPrimary("en");
291
+
292
+ expect((await posts.getById(post.id))?.language).toBe("zh-Hans");
293
+ });
294
+
295
+ it("refuses a language the site does not serve", async () => {
296
+ await expect(language.setPrimary("ko")).rejects.toThrow(
297
+ /Add that language/,
298
+ );
299
+ });
300
+
301
+ it("is a no-op when it is already primary", async () => {
302
+ await language.setPrimary("zh-Hans");
303
+ expect((await language.getState()).additional).toEqual(["en", "ja"]);
304
+ });
305
+ });
306
+
307
+ describe("removing a language", () => {
308
+ beforeEach(async () => {
309
+ await language.enable({ primary: "zh-Hans", additional: ["en", "ja"] });
310
+ });
311
+
312
+ it("removes a language nobody has written in", async () => {
313
+ await language.removeLanguage("ja");
314
+
315
+ expect((await language.getState()).additional).toEqual(["en"]);
316
+ });
317
+
318
+ it("refuses while posts still use it, and says how many", async () => {
319
+ await posts.create({
320
+ format: "note",
321
+ body: noteBody("one"),
322
+ language: "en",
323
+ });
324
+ await posts.create({
325
+ format: "note",
326
+ body: noteBody("two"),
327
+ language: "en",
328
+ });
329
+
330
+ await expect(language.removeLanguage("en")).rejects.toThrow(
331
+ /2 posts are still written/,
332
+ );
333
+ expect((await language.getState()).additional).toEqual(["en", "ja"]);
334
+ });
335
+
336
+ it("counts drafts too", async () => {
337
+ await posts.create({
338
+ format: "note",
339
+ body: noteBody("draft"),
340
+ language: "en",
341
+ status: "draft",
342
+ });
343
+
344
+ await expect(language.removeLanguage("en")).rejects.toThrow(
345
+ /One post is still written/,
346
+ );
347
+ });
348
+
349
+ it("still counts posts after multilingual is switched off", async () => {
350
+ await posts.create({
351
+ format: "note",
352
+ body: noteBody("english"),
353
+ language: "en",
354
+ });
355
+ await language.disable();
356
+
357
+ await expect(language.removeLanguage("en")).rejects.toThrow(
358
+ /still written/,
359
+ );
360
+ });
361
+
362
+ it("refuses to remove the primary language", async () => {
363
+ await expect(language.removeLanguage("zh-Hans")).rejects.toThrow(
364
+ /primary language/,
365
+ );
366
+ });
367
+
368
+ it("leaves the feature on while another language remains", async () => {
369
+ expect(await language.removeLanguage("ja")).toEqual({
370
+ multilingualDisabled: false,
371
+ });
372
+
373
+ expect(await settings.get("MULTILINGUAL_ENABLED")).toBe("true");
374
+ expect((await language.getState()).enabled).toBe(true);
375
+ });
376
+
377
+ it("turns the feature off with the last language it serves", async () => {
378
+ await language.removeLanguage("ja");
379
+
380
+ expect(await language.removeLanguage("en")).toEqual({
381
+ multilingualDisabled: true,
382
+ });
383
+
384
+ // Not merely masked by the empty list: the flag itself is gone, so every
385
+ // reader of the setting agrees with what the site actually serves.
386
+ expect(await settings.get("MULTILINGUAL_ENABLED")).toBeFalsy();
387
+ expect(await language.getState()).toEqual({
388
+ enabled: false,
389
+ primary: "zh-Hans",
390
+ additional: [],
391
+ all: ["zh-Hans"],
392
+ });
393
+ });
394
+
395
+ it("still refuses the last language while posts use it", async () => {
396
+ await language.removeLanguage("ja");
397
+ await posts.create({
398
+ format: "note",
399
+ body: noteBody("english"),
400
+ language: "en",
401
+ });
402
+
403
+ await expect(language.removeLanguage("en")).rejects.toThrow(
404
+ /One post is still written/,
405
+ );
406
+ expect((await language.getState()).enabled).toBe(true);
407
+ expect(await settings.get("MULTILINGUAL_ENABLED")).toBe("true");
408
+ });
409
+
410
+ it("has nothing to turn off when multilingual is already off", async () => {
411
+ await language.removeLanguage("ja");
412
+ await language.disable();
413
+
414
+ expect(await language.removeLanguage("en")).toEqual({
415
+ multilingualDisabled: false,
416
+ });
417
+ expect((await language.getState()).additional).toEqual([]);
418
+ });
419
+ });
420
+
421
+ describe("turning it off and back on", () => {
422
+ it("keeps the configuration and every post language", async () => {
423
+ await language.enable({ primary: "zh-Hans", additional: ["en"] });
424
+ const post = await posts.create({
425
+ format: "note",
426
+ body: noteBody("English"),
427
+ language: "en",
428
+ });
429
+
430
+ await language.disable();
431
+
432
+ const off = await language.getState();
433
+ expect(off.enabled).toBe(false);
434
+ expect(off.additional).toEqual(["en"]);
435
+ expect((await posts.getById(post.id))?.language).toBe("en");
436
+ });
437
+
438
+ it("only marks posts written while it was off", async () => {
439
+ await language.enable({ primary: "zh-Hans", additional: ["en"] });
440
+ const marked = await posts.create({
441
+ format: "note",
442
+ body: noteBody("English"),
443
+ language: "en",
444
+ });
445
+
446
+ await language.disable();
447
+ // Composing has no language control while multilingual is off.
448
+ const unmarked = await posts.create({
449
+ format: "note",
450
+ body: noteBody("written while off"),
451
+ });
452
+
453
+ expect((await language.getEnablePreview()).pendingCount).toBe(1);
454
+
455
+ const { markedCount } = await language.enable({
456
+ primary: "zh-Hans",
457
+ additional: ["en"],
458
+ });
459
+
460
+ expect(markedCount).toBe(1);
461
+ expect((await posts.getById(marked.id))?.language).toBe("en");
462
+ expect((await posts.getById(unmarked.id))?.language).toBe("zh-Hans");
463
+ });
464
+ });
465
+
466
+ describe("the language-set invariant", () => {
467
+ it("holds after enable, add, primary change, and remove", async () => {
468
+ await language.enable({ primary: "zh-Hans", additional: ["en"] });
469
+ await posts.create({
470
+ format: "note",
471
+ body: noteBody("中文"),
472
+ language: "zh-Hans",
473
+ });
474
+ await posts.create({
475
+ format: "note",
476
+ body: noteBody("English"),
477
+ language: "en",
478
+ });
479
+
480
+ await language.addLanguage("ja");
481
+ await language.setPrimary("en");
482
+ await language.removeLanguage("ja");
483
+
484
+ const state = await language.getState();
485
+ const configured = new Set(state.all);
486
+ const used = (await posts.list()).map((post) => post.language);
487
+
488
+ expect(used.length).toBeGreaterThan(0);
489
+ for (const tag of used) {
490
+ expect(tag).not.toBeNull();
491
+ expect(configured.has(tag as string)).toBe(true);
492
+ }
493
+ });
494
+ });
495
+ });