@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
@@ -11,8 +11,9 @@ async function loadGeneralContent() {
11
11
 
12
12
  function renderGeneralContent(
13
13
  props: Parameters<Awaited<ReturnType<typeof loadGeneralContent>>>[0],
14
+ locale: Parameters<typeof createI18n>[0] = "en",
14
15
  ) {
15
- const i18n = createI18n("en");
16
+ const i18n = createI18n(locale);
16
17
  const c = {
17
18
  get(key: string) {
18
19
  if (key === "i18n") return i18n;
@@ -36,9 +37,6 @@ function createProps(
36
37
  return {
37
38
  siteName: "My Blog",
38
39
  siteDescription: "A test blog",
39
- siteLanguage: "en",
40
- dashboardLanguage: "en",
41
- cjkSerifFont: "off",
42
40
  siteNameFallback: "Fallback Name",
43
41
  siteDescriptionFallback: "Fallback Description",
44
42
  mainRssFeed: "featured" as const,
@@ -50,6 +48,30 @@ function createProps(
50
48
  siteFooter: "Footer text",
51
49
  showJantBrandingOnHome: false,
52
50
  noindex: false,
51
+ discover: "",
52
+ // The deployment has no answer of its own; a self-hosted site opts in.
53
+ discoverDefault: "" as const,
54
+ discoverPages: {
55
+ home: "https://jant.me/discover",
56
+ links: "https://jant.me/links",
57
+ quotes: "https://jant.me/quotes",
58
+ rules: "https://jant.me/discover/about",
59
+ },
60
+ discoverStatus: {
61
+ announced: true,
62
+ announceError: null,
63
+ announceAt: STALE_ANNOUNCE_AT,
64
+ hasDirectory: true,
65
+ managedByHost: false,
66
+ submitUrl: "https://jant.me/discover/submit",
67
+ declaredMode: "latest" as const,
68
+ publicPostCount: 5,
69
+ featuredPostCount: 2,
70
+ established: true,
71
+ minPublicPosts: 1,
72
+ firstReadMaxHours: 6,
73
+ },
74
+ rssFeedsEnabled: true,
53
75
  demoMode,
54
76
  aboutPage: {
55
77
  state: "missing" as const,
@@ -69,6 +91,12 @@ function createProps(
69
91
  };
70
92
  }
71
93
 
94
+ // The first-read line is the one status sentence that expires, so the fixtures
95
+ // place the announcement relative to the clock rather than on a fixed date that
96
+ // drifts in and out of the window as time passes.
97
+ const STALE_ANNOUNCE_AT = Math.floor(Date.now() / 1000) - 7 * 24 * 3600;
98
+ const FRESH_ANNOUNCE_AT = Math.floor(Date.now() / 1000) - 60;
99
+
72
100
  describe("GeneralContent", () => {
73
101
  it("omits the demo-mode attribute when demo mode is disabled", async () => {
74
102
  const html = await renderGeneralContent(createProps(false));
@@ -81,4 +109,296 @@ describe("GeneralContent", () => {
81
109
 
82
110
  expect(html).toMatch(/<jant-settings-general[^>]*demo-mode(?:=|\s|>)/);
83
111
  });
112
+
113
+ // The client turns the name and the rules into links by finding them in the
114
+ // help line, so the labels have to agree with the sentence character for
115
+ // character. A translation that reworded one and not the other would only
116
+ // lose that link, but this catches it here rather than in a screenshot.
117
+ it("carries every linked term verbatim inside the help line", async () => {
118
+ const html = await renderGeneralContent(createProps(false));
119
+
120
+ expect(html).toContain(
121
+ "Jant Discover is a directory of Jant blogs, curated by hand by the Jant community",
122
+ );
123
+ expect(html).toContain(
124
+ "appear on the Links and Quotes lists 24 hours after they are published",
125
+ );
126
+ expect(html).toContain("See the Discover community rules.");
127
+ expect(html).toContain(
128
+ "&quot;discoverName&quot;:&quot;Jant Discover&quot;",
129
+ );
130
+ expect(html).toContain(
131
+ "&quot;discoverRules&quot;:&quot;Discover community rules&quot;",
132
+ );
133
+ });
134
+
135
+ // Translations are written by hand or by a model, and one that rewords a
136
+ // placeholder's value, or drops the placeholder, loses that link without a
137
+ // word. Every catalog has to keep all four runs verbatim.
138
+ it.each(["en", "zh-Hans", "zh-Hant"] as const)(
139
+ "keeps every linked term inside the %s help line",
140
+ async (locale) => {
141
+ const html = await renderGeneralContent(createProps(false), locale);
142
+ const attribute = /<jant-settings-general[^>]*\slabels="([^"]*)"/.exec(
143
+ html,
144
+ )?.[1];
145
+ const labels = JSON.parse(
146
+ (attribute ?? "{}").replaceAll("&quot;", '"').replaceAll("&amp;", "&"),
147
+ ) as Record<string, string>;
148
+ const intro = labels["discoverIntro"] ?? "";
149
+
150
+ for (const term of [
151
+ labels["discoverName"],
152
+ "Links",
153
+ "Quotes",
154
+ labels["discoverRules"],
155
+ ]) {
156
+ expect(term).toBeTruthy();
157
+ expect(intro).toContain(term);
158
+ }
159
+ // And the delay, so an author knows how long they have to edit.
160
+ expect(intro).toContain("24");
161
+ },
162
+ );
163
+
164
+ it("hands the browser every page the help line links to", async () => {
165
+ const html = await renderGeneralContent(createProps(false));
166
+
167
+ expect(html).toContain(
168
+ 'discover-pages="{&quot;home&quot;:&quot;https://jant.me/discover&quot;,&quot;links&quot;:&quot;https://jant.me/links&quot;,&quot;quotes&quot;:&quot;https://jant.me/quotes&quot;,&quot;rules&quot;:&quot;https://jant.me/discover/about&quot;}"',
169
+ );
170
+ });
171
+
172
+ // No directory, no addresses: the help line then renders as plain text.
173
+ it("omits the directory's pages when none is configured", async () => {
174
+ const html = await renderGeneralContent(
175
+ createProps(false, { discoverPages: null }),
176
+ );
177
+
178
+ expect(html).not.toContain("discover-pages");
179
+ });
180
+
181
+ // The browser decides what the site declares, because two of the inputs are
182
+ // controls on this page. Resolving `noindex` into the default here would put
183
+ // that decision back on the server and freeze it at page load — the checkbox
184
+ // would then go on claiming a listing the feed had already dropped.
185
+ it("hands the browser the deployment default with noindex unresolved", async () => {
186
+ const html = await renderGeneralContent(
187
+ createProps(false, { discoverDefault: "latest", noindex: true }),
188
+ );
189
+
190
+ expect(html).toContain('discover-default="latest"');
191
+ });
192
+
193
+ // The controls above the block already say that the site is listed and what
194
+ // its feed declares, and a site past the directory's threshold has nothing
195
+ // to do about being past it. A site in that state is told nothing at all.
196
+ it("says nothing when the setting is already doing what it says", async () => {
197
+ const html = await renderGeneralContent(createProps(false));
198
+
199
+ expect(html).toContain('discover-status="{&quot;lines&quot;:[]');
200
+ // Nothing failed, so neither the retry nor the manual form is offered.
201
+ expect(html).toContain("&quot;showAnnounce&quot;:false");
202
+ expect(html).toContain("&quot;submitUrl&quot;:null");
203
+ });
204
+
205
+ // The status sentences carry runtime numbers, so they are translated on the
206
+ // server rather than in the browser. What reaches the component is finished
207
+ // text — and this one only for as long as the answer is outstanding.
208
+ it("confirms an announcement while its first read is still due", async () => {
209
+ const html = await renderGeneralContent(
210
+ createProps(false, {
211
+ discoverStatus: {
212
+ ...createProps(false).discoverStatus,
213
+ announceAt: FRESH_ANNOUNCE_AT,
214
+ },
215
+ }),
216
+ );
217
+
218
+ expect(html).toContain(
219
+ "Feed address sent. A directory reads a newly announced feed within 6 hours.",
220
+ );
221
+ });
222
+
223
+ // An opt-in that cannot take effect is worth saying, because the ticked box
224
+ // above claims the opposite.
225
+ it("says the opt-in is not yet effective without a public post", async () => {
226
+ const html = await renderGeneralContent(
227
+ createProps(false, {
228
+ discoverStatus: {
229
+ ...createProps(false).discoverStatus,
230
+ publicPostCount: 0,
231
+ featuredPostCount: 0,
232
+ established: false,
233
+ },
234
+ }),
235
+ );
236
+
237
+ expect(html).toContain(
238
+ "Nothing published yet. jant.me lists a blog once it has one public post.",
239
+ );
240
+ });
241
+
242
+ it("says a featured-only feed has nothing to carry", async () => {
243
+ const html = await renderGeneralContent(
244
+ createProps(false, {
245
+ discoverStatus: {
246
+ ...createProps(false).discoverStatus,
247
+ declaredMode: "featured" as const,
248
+ featuredPostCount: 0,
249
+ },
250
+ }),
251
+ );
252
+
253
+ expect(html).toContain("no post is marked Featured");
254
+ });
255
+
256
+ it("offers the manual form only when the announcement failed", async () => {
257
+ const html = await renderGeneralContent(
258
+ createProps(false, {
259
+ discoverStatus: {
260
+ announced: false,
261
+ announceError: "The directory answered 503.",
262
+ announceAt: STALE_ANNOUNCE_AT,
263
+ hasDirectory: true,
264
+ managedByHost: false,
265
+ submitUrl: "https://jant.me/discover/submit",
266
+ declaredMode: "latest",
267
+ publicPostCount: 5,
268
+ featuredPostCount: 2,
269
+ established: true,
270
+ minPublicPosts: 1,
271
+ firstReadMaxHours: 6,
272
+ },
273
+ }),
274
+ );
275
+
276
+ expect(html).toContain(
277
+ "The directory could not be reached: The directory answered 503.",
278
+ );
279
+ expect(html).toContain("&quot;showAnnounce&quot;:true");
280
+ expect(html).toContain("https://jant.me/discover/submit");
281
+ });
282
+
283
+ // A site whose mode came from the deployment's own default declares itself
284
+ // without ever having announced. Nothing else on the page sends the address
285
+ // now that the section saves on change, so the button has to.
286
+ it("offers the announcement to a site that has never made one", async () => {
287
+ const html = await renderGeneralContent(
288
+ createProps(false, {
289
+ discoverStatus: {
290
+ announced: null,
291
+ announceError: null,
292
+ announceAt: null,
293
+ hasDirectory: true,
294
+ managedByHost: false,
295
+ submitUrl: "https://jant.me/discover/submit",
296
+ declaredMode: "latest",
297
+ publicPostCount: 5,
298
+ featuredPostCount: 2,
299
+ established: true,
300
+ minPublicPosts: 1,
301
+ firstReadMaxHours: 6,
302
+ },
303
+ }),
304
+ );
305
+
306
+ expect(html).toContain(
307
+ "Not announced yet. No directory has been told this site exists.",
308
+ );
309
+ expect(html).toContain("&quot;showAnnounce&quot;:true");
310
+ // The manual form stays with the failure it belongs to.
311
+ expect(html).toContain("&quot;submitUrl&quot;:null");
312
+ });
313
+
314
+ it("reports nothing at all when the feed declares none", async () => {
315
+ const html = await renderGeneralContent(
316
+ createProps(false, {
317
+ discoverStatus: {
318
+ announced: null,
319
+ announceError: null,
320
+ announceAt: null,
321
+ hasDirectory: true,
322
+ managedByHost: false,
323
+ submitUrl: "https://jant.me/discover/submit",
324
+ declaredMode: "none",
325
+ publicPostCount: 5,
326
+ featuredPostCount: 2,
327
+ established: true,
328
+ minPublicPosts: 1,
329
+ firstReadMaxHours: 6,
330
+ },
331
+ }),
332
+ );
333
+
334
+ // A site that is not listed is told nothing: not that it is not listed —
335
+ // the unticked checkbox says that — not how close it is to a threshold it
336
+ // has opted out of, and not whether an announcement it never made got
337
+ // through. With no lines the component drops the whole block.
338
+ expect(html).toContain('discover-status="{&quot;lines&quot;:[]');
339
+ expect(html).not.toContain("Not announced yet");
340
+ expect(html).not.toContain("jant.me lists a blog once");
341
+ expect(html).toContain("&quot;showAnnounce&quot;:false");
342
+ });
343
+
344
+ // The announcement answers "does the directory know my address". A blog on
345
+ // a hosted platform never has to ask: the control plane enrols its whole
346
+ // fleet, so an owner who has never touched this control is already listed,
347
+ // and telling them otherwise sends them after a task that does not exist.
348
+ it("says nothing about announcing on a hosted blog", async () => {
349
+ const html = await renderGeneralContent(
350
+ createProps(false, {
351
+ discoverStatus: {
352
+ announced: null,
353
+ announceError: null,
354
+ announceAt: null,
355
+ hasDirectory: true,
356
+ managedByHost: true,
357
+ submitUrl: "https://jant.me/discover/submit",
358
+ declaredMode: "latest",
359
+ publicPostCount: 5,
360
+ featuredPostCount: 2,
361
+ established: true,
362
+ minPublicPosts: 1,
363
+ firstReadMaxHours: 6,
364
+ },
365
+ }),
366
+ );
367
+
368
+ expect(html).not.toContain("Not announced yet");
369
+ expect(html).not.toContain("https://jant.me/discover/submit");
370
+ expect(html).toContain("&quot;showAnnounce&quot;:false");
371
+ expect(html).toContain("&quot;submitUrl&quot;:null");
372
+ // Nothing is left: the announcement was the only part of this block a
373
+ // hosted owner could ever have acted on.
374
+ expect(html).toContain('discover-status="{&quot;lines&quot;:[]');
375
+ });
376
+
377
+ // A failed announcement is the one case that used to leave a hosted owner
378
+ // reading an error about their own host's plumbing, with a Retry and a
379
+ // manual submission form for a directory that already has them.
380
+ it("hides the retry and the manual form on a hosted blog", async () => {
381
+ const html = await renderGeneralContent(
382
+ createProps(false, {
383
+ discoverStatus: {
384
+ announced: false,
385
+ announceError: "The directory answered 503.",
386
+ announceAt: STALE_ANNOUNCE_AT,
387
+ hasDirectory: true,
388
+ managedByHost: true,
389
+ submitUrl: "https://jant.me/discover/submit",
390
+ declaredMode: "latest",
391
+ publicPostCount: 5,
392
+ featuredPostCount: 2,
393
+ established: true,
394
+ minPublicPosts: 1,
395
+ firstReadMaxHours: 6,
396
+ },
397
+ }),
398
+ );
399
+
400
+ expect(html).not.toContain("The directory could not be reached");
401
+ expect(html).toContain("&quot;showAnnounce&quot;:false");
402
+ expect(html).toContain("&quot;submitUrl&quot;:null");
403
+ });
84
404
  });
@@ -0,0 +1,144 @@
1
+ import type { Context } from "hono";
2
+ import { renderToString } from "hono/jsx/dom/server";
3
+ import { describe, expect, it } from "vitest";
4
+ import { I18nProvider } from "../../../../i18n/context.js";
5
+ import { createI18n } from "../../../../i18n/i18n.js";
6
+
7
+ async function loadLanguageContent() {
8
+ const { LanguageContent } = await import("../LanguageContent.js");
9
+ return LanguageContent;
10
+ }
11
+
12
+ type LanguageContentProps = Parameters<
13
+ Awaited<ReturnType<typeof loadLanguageContent>>
14
+ >[0];
15
+
16
+ function renderLanguageContent(overrides: Partial<LanguageContentProps> = {}) {
17
+ const i18n = createI18n("en");
18
+ const c = {
19
+ get(key: string) {
20
+ if (key === "i18n") return i18n;
21
+ return undefined;
22
+ },
23
+ } as unknown as Context;
24
+
25
+ I18nProvider({ c, children: "" });
26
+
27
+ return loadLanguageContent().then((LanguageContent) =>
28
+ renderToString(
29
+ LanguageContent({
30
+ contentLanguage: "zh-Hans",
31
+ dashboardLanguage: "en",
32
+ multilingualEnabled: false,
33
+ additionalLanguages: [],
34
+ unmarkedPostCount: 0,
35
+ sitePathPrefix: "",
36
+ ...overrides,
37
+ }),
38
+ ),
39
+ );
40
+ }
41
+
42
+ /** Pull the JSON blob the component reads its translated copy from. */
43
+ function readLabels(html: string): Record<string, string> {
44
+ const match = /labels="([^"]*)"/.exec(html);
45
+ if (!match?.[1]) throw new Error("expected a labels attribute");
46
+ const decoded = match[1]
47
+ .replace(/&quot;/g, '"')
48
+ .replace(/&amp;/g, "&")
49
+ .replace(/&lt;/g, "<")
50
+ .replace(/&gt;/g, ">")
51
+ .replace(/&#39;/g, "'");
52
+ return JSON.parse(decoded) as Record<string, string>;
53
+ }
54
+
55
+ describe("LanguageContent", () => {
56
+ it("mounts the Lit element with an initial-state payload", async () => {
57
+ const html = await renderLanguageContent({
58
+ multilingualEnabled: true,
59
+ additionalLanguages: ["en", "ja"],
60
+ unmarkedPostCount: 3,
61
+ sitePathPrefix: "/blog",
62
+ });
63
+
64
+ expect(html).toContain("<jant-settings-language");
65
+ const state = /id="language-settings-initial-data">(.*?)<\/script>/s.exec(
66
+ html,
67
+ );
68
+ expect(state?.[1]).toBeDefined();
69
+ expect(JSON.parse(state?.[1] ?? "{}")).toEqual({
70
+ contentLanguage: "zh-Hans",
71
+ dashboardLanguage: "en",
72
+ multilingualEnabled: true,
73
+ additionalLanguages: ["en", "ja"],
74
+ unmarkedPostCount: 3,
75
+ sitePathPrefix: "/blog",
76
+ // The browser bundle cannot resolve the docs host, so the server sends it.
77
+ multilingualDocsUrl: "https://jant.me/docs/multilingual",
78
+ });
79
+ });
80
+
81
+ // Lingui resolves an ICU message completely at `i18n._()` time, so any
82
+ // plural has to be formatted here — a template shipped to the browser would
83
+ // be formatted against an undefined count and render "NaN".
84
+ describe("copy that carries runtime values", () => {
85
+ it("resolves the plural against the real post count", async () => {
86
+ const labels = readLabels(
87
+ await renderLanguageContent({
88
+ unmarkedPostCount: 347,
89
+ }),
90
+ );
91
+
92
+ expect(labels.enableMarkWarning).toContain("347 existing posts");
93
+ expect(labels.enableMarkWarning).not.toContain("NaN");
94
+ expect(labels.enableMarkWarning).not.toContain("plural");
95
+ });
96
+
97
+ it("uses the singular form for a single post", async () => {
98
+ const labels = readLabels(
99
+ await renderLanguageContent({
100
+ unmarkedPostCount: 1,
101
+ }),
102
+ );
103
+
104
+ expect(labels.enableMarkWarning).toContain("1 existing post with");
105
+ expect(labels.enableMarkWarning).not.toContain("posts");
106
+ });
107
+
108
+ it("leaves the language slot for the component to fill", async () => {
109
+ const labels = readLabels(
110
+ await renderLanguageContent({
111
+ unmarkedPostCount: 5,
112
+ }),
113
+ );
114
+
115
+ // Chosen inside the dialog, so it cannot be resolved here.
116
+ expect(labels.enableMarkWarning).toContain("{language}");
117
+ expect(labels.removeLanguage).toContain("{language}");
118
+ });
119
+
120
+ it("leaves every slot of the change-primary and disable copy", async () => {
121
+ const labels = readLabels(await renderLanguageContent());
122
+
123
+ expect(labels.changePrimaryBody).toContain("{next}");
124
+ expect(labels.changePrimaryBody).toContain("{previous}");
125
+ expect(labels.changePrimaryBody).toContain("{prefix}");
126
+ expect(labels.disableBody).toContain("{prefix}");
127
+ expect(labels.removeLastBody).toContain("{language}");
128
+ expect(labels.removeLastBody).toContain("{prefix}");
129
+ });
130
+
131
+ it("never leaks an unresolved ICU construct into a label", async () => {
132
+ const labels = readLabels(
133
+ await renderLanguageContent({
134
+ unmarkedPostCount: 12,
135
+ }),
136
+ );
137
+
138
+ for (const [key, value] of Object.entries(labels)) {
139
+ expect(value, key).not.toContain("plural,");
140
+ expect(value, key).not.toContain("NaN");
141
+ }
142
+ });
143
+ });
144
+ });
@@ -0,0 +1,66 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { describe, expect, it } from "vitest";
4
+ import { createI18n } from "../../../../i18n/i18n.js";
5
+ import { getDiscoverCopy } from "../discover-copy.js";
6
+
7
+ const CORE_ROOT = resolve(import.meta.dirname, "../../../../..");
8
+
9
+ /**
10
+ * Every msgid in a .po file, mapped to its msgstr. Joins the continuation
11
+ * lines a long entry may be wrapped onto.
12
+ */
13
+ function readCatalog(locale: string): Map<string, string> {
14
+ const po = readFileSync(
15
+ resolve(CORE_ROOT, `src/i18n/locales/settings/${locale}.po`),
16
+ "utf8",
17
+ );
18
+ const entries = new Map<string, string>();
19
+ for (const block of po.split(/\n{2,}/)) {
20
+ const field = (keyword: string) => {
21
+ const match = new RegExp(
22
+ `^${keyword} ("(?:[^"\\\\]|\\\\.)*"(?:\\n"(?:[^"\\\\]|\\\\.)*")*)`,
23
+ "m",
24
+ ).exec(block);
25
+ if (!match?.[1]) return null;
26
+ return match[1]
27
+ .split("\n")
28
+ .map((line) => JSON.parse(line) as string)
29
+ .join("");
30
+ };
31
+ const id = field("msgid");
32
+ const str = field("msgstr");
33
+ if (id !== null && str !== null) entries.set(id, str);
34
+ }
35
+ return entries;
36
+ }
37
+
38
+ describe("getDiscoverCopy", () => {
39
+ // Setup shows `about` alone; Settings shows it and goes on. The opening is
40
+ // one message, so the two can only differ in length, never in wording.
41
+ it("opens the settings line with the sentence setup shows", () => {
42
+ const copy = getDiscoverCopy(createI18n("en"));
43
+
44
+ expect(copy.about).toBe(
45
+ "Jant Discover is a directory of Jant blogs, curated by hand by the Jant community to help people find new Jant blogs and posts.",
46
+ );
47
+ expect(copy.intro.startsWith(`${copy.about} `)).toBe(true);
48
+ });
49
+
50
+ // Tests render through a macro stub that never reaches a translation, so the
51
+ // catalogs are read directly. A translation that drops `{about}` would leave
52
+ // the settings line without its first sentence in that language only.
53
+ it.each(["zh-Hans", "zh-Hant"])(
54
+ "keeps the opening sentence in the %s settings line",
55
+ (locale) => {
56
+ const catalog = readCatalog(locale);
57
+ const intro = [...catalog].find(([id]) => id.startsWith("{about} "));
58
+ const about = catalog.get(
59
+ "{name} is a directory of Jant blogs, curated by hand by the Jant community to help people find new Jant blogs and posts.",
60
+ );
61
+
62
+ expect(intro?.[1]).toContain("{about}");
63
+ expect(about).toContain("{name}");
64
+ },
65
+ );
66
+ });
@@ -0,0 +1,116 @@
1
+ /**
2
+ * What the Jant Discover checkbox says, wherever it appears.
3
+ *
4
+ * The checkbox is on the settings page, and it is asked once more on the
5
+ * first-run setup screen. One control appearing twice should not describe
6
+ * itself two ways, so both surfaces take their words from here rather than
7
+ * each keeping a wording of its own that drifts when the other is edited.
8
+ *
9
+ * The two help lines differ in length, not in wording. Setup shows only the
10
+ * sentence saying what the directory is: the delay, the lists and the rules
11
+ * matter to an author with posts to show, and setup comes before the first
12
+ * one — anyone curious follows the name to the directory itself. The settings
13
+ * line carries that same sentence as its `{about}` placeholder and goes on
14
+ * from there, so the opening can only ever be edited in one place, in every
15
+ * locale.
16
+ *
17
+ * Lives under `ui/dash/` because that is what puts it in the translated
18
+ * catalog: `lingui.config` splits by path, and only `routes/dash/**`,
19
+ * `ui/dash/**` and the setup page are translated.
20
+ */
21
+
22
+ import { msg } from "@lingui/core/macro";
23
+ import type { I18n } from "../../../i18n/i18n.js";
24
+ import {
25
+ DISCOVER_LIST_NAMES,
26
+ DISCOVER_PUBLIC_DELAY_HOURS,
27
+ } from "../../../lib/discover.js";
28
+
29
+ type Translator = Pick<I18n, "_">;
30
+
31
+ /** The Discover checkbox's words, translated. */
32
+ export interface DiscoverCopy {
33
+ /** The directory's name, as `label`, `about` and `intro` carry it. */
34
+ name: string;
35
+ /** The rules page's name, as the end of `intro` carries it. */
36
+ rules: string;
37
+ /** The checkbox label, on both surfaces. */
38
+ label: string;
39
+ /** What the directory is, in one sentence: setup's whole help line. */
40
+ about: string;
41
+ /** The settings page's help line, which opens with `about`. */
42
+ intro: string;
43
+ }
44
+
45
+ /**
46
+ * Translate the Discover checkbox's label and help lines.
47
+ *
48
+ * The directory's name and the rules page's name are split out so the label
49
+ * and the help lines can carry them as placeholders, leaving word order free
50
+ * per locale; they are returned too, because `discoverIntroRuns` finds each
51
+ * one in the translated line to make it a link.
52
+ *
53
+ * @param i18n - The request's i18n instance
54
+ * @returns The five strings, in the i18n instance's locale
55
+ * @example
56
+ * ```ts
57
+ * const copy = getDiscoverCopy(i18n);
58
+ * copy.label; // "Allow Jant Discover to list my site"
59
+ * copy.intro.startsWith(copy.about); // true
60
+ * ```
61
+ */
62
+ export function getDiscoverCopy(i18n: Translator): DiscoverCopy {
63
+ const name = i18n._(
64
+ msg({
65
+ message: "Jant Discover",
66
+ comment:
67
+ "@context: Name of the Jant blog directory. Appears inside the Discover checkbox label, and in the help line under it as the link to the directory.",
68
+ }),
69
+ );
70
+
71
+ // The run of text the rules link sits on, at the end of the help line.
72
+ const rules = i18n._(
73
+ msg({
74
+ message: "Discover community rules",
75
+ comment:
76
+ "@context: Link text at the end of the help line under the Discover checkbox, leading to the directory's page on how blogs are listed: joining, review, and what takes a blog off. This run of text is rendered as the link, so translate it as it should read inside that sentence.",
77
+ }),
78
+ );
79
+
80
+ const label = i18n._(
81
+ msg({
82
+ message: "Allow {name} to list my site",
83
+ comment:
84
+ "@context: Checkbox for joining the Jant Discover directory, on the settings page and once on the first-run setup screen. {name} is the directory's name, kept as one run of text.",
85
+ }),
86
+ { name },
87
+ );
88
+
89
+ const about = i18n._(
90
+ msg({
91
+ message:
92
+ "{name} is a directory of Jant blogs, curated by hand by the Jant community to help people find new Jant blogs and posts.",
93
+ comment:
94
+ "@context: What the Jant Discover directory is, in one sentence. The whole help line under the Discover checkbox on the first-run setup screen, and the opening of the settings page's longer one. {name} is the directory's name and is rendered as the link to it, so keep it as one run of text.",
95
+ }),
96
+ { name },
97
+ );
98
+
99
+ const intro = i18n._(
100
+ msg({
101
+ message:
102
+ "{about} A post you mark Featured appears on the Discover home page {hours} hours later, and link and quote posts appear on the {links} and {quotes} lists {hours} hours after they are published. You can keep editing them in the meantime. See the {rules}.",
103
+ comment:
104
+ "@context: Help text under the Jant Discover checkbox on the settings page. {about} is the sentence saying what the directory is, translated on its own and shown alone on the setup screen; keep it, and join it to what follows as your language joins two sentences. {links} and {quotes} are the directory's two list names, which stay in English, and {rules} is the name of its rules page; each is rendered as a link, so keep every placeholder as one run of text. {hours} is how long the directory waits before showing a post, which is the time left to edit it. 'Featured' is the mark on a post, as this site's own UI spells it; the lowercase 'link and quote' are the post formats, not the lists.",
105
+ }),
106
+ {
107
+ about,
108
+ hours: DISCOVER_PUBLIC_DELAY_HOURS,
109
+ links: DISCOVER_LIST_NAMES.links,
110
+ quotes: DISCOVER_LIST_NAMES.quotes,
111
+ rules,
112
+ },
113
+ );
114
+
115
+ return { name, rules, label, about, intro };
116
+ }