emdash 0.8.0 → 0.10.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 (317) hide show
  1. package/dist/{adapters-BKSf3T9R.d.mts → adapters-BktHA7EO.d.mts} +1 -1
  2. package/dist/{adapters-BKSf3T9R.d.mts.map → adapters-BktHA7EO.d.mts.map} +1 -1
  3. package/dist/{apply-x0eMK1lX.mjs → apply-UsrFuO7l.mjs} +207 -355
  4. package/dist/apply-UsrFuO7l.mjs.map +1 -0
  5. package/dist/astro/index.d.mts +6 -6
  6. package/dist/astro/index.d.mts.map +1 -1
  7. package/dist/astro/index.mjs +118 -4
  8. package/dist/astro/index.mjs.map +1 -1
  9. package/dist/astro/middleware/auth.d.mts +6 -7
  10. package/dist/astro/middleware/auth.d.mts.map +1 -1
  11. package/dist/astro/middleware/auth.mjs +14 -57
  12. package/dist/astro/middleware/auth.mjs.map +1 -1
  13. package/dist/astro/middleware/redirect.d.mts.map +1 -1
  14. package/dist/astro/middleware/redirect.mjs +15 -10
  15. package/dist/astro/middleware/redirect.mjs.map +1 -1
  16. package/dist/astro/middleware/request-context.d.mts.map +1 -1
  17. package/dist/astro/middleware/request-context.mjs +8 -5
  18. package/dist/astro/middleware/request-context.mjs.map +1 -1
  19. package/dist/astro/middleware/setup.mjs +1 -1
  20. package/dist/astro/middleware.d.mts.map +1 -1
  21. package/dist/astro/middleware.mjs +70 -121
  22. package/dist/astro/middleware.mjs.map +1 -1
  23. package/dist/astro/types.d.mts +25 -10
  24. package/dist/astro/types.d.mts.map +1 -1
  25. package/dist/{byline-Chbr2GoP.mjs → byline-C3vnhIpU.mjs} +4 -4
  26. package/dist/{byline-Chbr2GoP.mjs.map → byline-C3vnhIpU.mjs.map} +1 -1
  27. package/dist/bylines-esI7ioa9.mjs +113 -0
  28. package/dist/bylines-esI7ioa9.mjs.map +1 -0
  29. package/dist/cache-fTzxgMFJ.mjs +65 -0
  30. package/dist/cache-fTzxgMFJ.mjs.map +1 -0
  31. package/dist/{chunks-HGz06Soa.mjs → chunks-Da2-b-oA.mjs} +8 -2
  32. package/dist/{chunks-HGz06Soa.mjs.map → chunks-Da2-b-oA.mjs.map} +1 -1
  33. package/dist/cli/index.mjs +456 -90
  34. package/dist/cli/index.mjs.map +1 -1
  35. package/dist/client/cf-access.d.mts +1 -1
  36. package/dist/client/index.d.mts +1 -1
  37. package/dist/client/index.mjs +3 -3
  38. package/dist/client/index.mjs.map +1 -1
  39. package/dist/{config-BXwuX8Bx.mjs → config-CVssduLe.mjs} +1 -1
  40. package/dist/{config-BXwuX8Bx.mjs.map → config-CVssduLe.mjs.map} +1 -1
  41. package/dist/{content-BcQPYxdV.mjs → content-C7G4QXkK.mjs} +42 -14
  42. package/dist/content-C7G4QXkK.mjs.map +1 -0
  43. package/dist/db/index.d.mts +3 -3
  44. package/dist/db/index.mjs +2 -2
  45. package/dist/db/libsql.d.mts +1 -1
  46. package/dist/db/libsql.d.mts.map +1 -1
  47. package/dist/db/libsql.mjs +7 -2
  48. package/dist/db/libsql.mjs.map +1 -1
  49. package/dist/db/postgres.d.mts +1 -1
  50. package/dist/db/sqlite.d.mts +1 -1
  51. package/dist/db/sqlite.d.mts.map +1 -1
  52. package/dist/db/sqlite.mjs +8 -3
  53. package/dist/db/sqlite.mjs.map +1 -1
  54. package/dist/{db-errors-l1Qh2RPR.mjs → db-errors-B7P2pSCn.mjs} +1 -1
  55. package/dist/{db-errors-l1Qh2RPR.mjs.map → db-errors-B7P2pSCn.mjs.map} +1 -1
  56. package/dist/{default-DCVqE5ib.mjs → default-pHuz9WF6.mjs} +1 -1
  57. package/dist/{default-DCVqE5ib.mjs.map → default-pHuz9WF6.mjs.map} +1 -1
  58. package/dist/{dialect-helpers-DhTzaUxP.mjs → dialect-helpers-BKCvISIQ.mjs} +19 -2
  59. package/dist/dialect-helpers-BKCvISIQ.mjs.map +1 -0
  60. package/dist/{error-zG5T1UGA.mjs → error-DqnRMM5z.mjs} +1 -1
  61. package/dist/{error-zG5T1UGA.mjs.map → error-DqnRMM5z.mjs.map} +1 -1
  62. package/dist/{index-DIb-CzNx.d.mts → index-DjPMOfO0.d.mts} +162 -87
  63. package/dist/index-DjPMOfO0.d.mts.map +1 -0
  64. package/dist/index.d.mts +11 -11
  65. package/dist/index.mjs +27 -24
  66. package/dist/{load-CyEoextb.mjs → load-sXRuM7Us.mjs} +2 -2
  67. package/dist/{load-CyEoextb.mjs.map → load-sXRuM7Us.mjs.map} +1 -1
  68. package/dist/{loader-CndGj8kM.mjs → loader-Bx2_9-5e.mjs} +53 -8
  69. package/dist/loader-Bx2_9-5e.mjs.map +1 -0
  70. package/dist/{manifest-schema-DH9xhc6t.mjs → manifest-schema-CXAbd1vH.mjs} +33 -3
  71. package/dist/manifest-schema-CXAbd1vH.mjs.map +1 -0
  72. package/dist/media/index.d.mts +1 -1
  73. package/dist/media/index.mjs +1 -1
  74. package/dist/media/local-runtime.d.mts +7 -7
  75. package/dist/{mode-BnAOqItE.mjs → mode-YhqNVef_.mjs} +1 -1
  76. package/dist/{mode-BnAOqItE.mjs.map → mode-YhqNVef_.mjs.map} +1 -1
  77. package/dist/options-nPxWnrya.mjs +117 -0
  78. package/dist/options-nPxWnrya.mjs.map +1 -0
  79. package/dist/page/index.d.mts +2 -2
  80. package/dist/{patterns-CrCYkMBb.mjs → patterns-DsUZ4uxI.mjs} +1 -1
  81. package/dist/{patterns-CrCYkMBb.mjs.map → patterns-DsUZ4uxI.mjs.map} +1 -1
  82. package/dist/{placeholder-D29tWZ7o.d.mts → placeholder-CDPtkelt.d.mts} +1 -1
  83. package/dist/{placeholder-D29tWZ7o.d.mts.map → placeholder-CDPtkelt.d.mts.map} +1 -1
  84. package/dist/{placeholder-C-fk5hYI.mjs → placeholder-Ci0RLeCk.mjs} +1 -1
  85. package/dist/{placeholder-C-fk5hYI.mjs.map → placeholder-Ci0RLeCk.mjs.map} +1 -1
  86. package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
  87. package/dist/plugins/adapt-sandbox-entry.d.mts.map +1 -1
  88. package/dist/plugins/adapt-sandbox-entry.mjs +6 -5
  89. package/dist/plugins/adapt-sandbox-entry.mjs.map +1 -1
  90. package/dist/public-url-B1AxbbbQ.mjs +51 -0
  91. package/dist/public-url-B1AxbbbQ.mjs.map +1 -0
  92. package/dist/{query-fqEdLFms.mjs → query-Bo-msrmu.mjs} +114 -16
  93. package/dist/query-Bo-msrmu.mjs.map +1 -0
  94. package/dist/{redirect-D_pshWdf.mjs → redirect-C5H7VGIX.mjs} +11 -6
  95. package/dist/redirect-C5H7VGIX.mjs.map +1 -0
  96. package/dist/{registry-C3Mr0ODu.mjs → registry-Beb7wxFc.mjs} +39 -5
  97. package/dist/registry-Beb7wxFc.mjs.map +1 -0
  98. package/dist/{request-cache-Ci7f5pBb.mjs → request-cache-C-tIpYIw.mjs} +1 -1
  99. package/dist/{request-cache-Ci7f5pBb.mjs.map → request-cache-C-tIpYIw.mjs.map} +1 -1
  100. package/dist/runner-Clwe4Mme.d.mts +44 -0
  101. package/dist/runner-Clwe4Mme.d.mts.map +1 -0
  102. package/dist/{runner-tQ7BJ4T7.mjs → runner-DMnlIkh4.mjs} +616 -191
  103. package/dist/runner-DMnlIkh4.mjs.map +1 -0
  104. package/dist/runtime.d.mts +6 -6
  105. package/dist/runtime.mjs +2 -2
  106. package/dist/{search-BoZYFuUk.mjs → search-DkN-BqsS.mjs} +270 -152
  107. package/dist/search-DkN-BqsS.mjs.map +1 -0
  108. package/dist/secrets-CZ8rxLX3.mjs +314 -0
  109. package/dist/secrets-CZ8rxLX3.mjs.map +1 -0
  110. package/dist/seed/index.d.mts +2 -2
  111. package/dist/seed/index.mjs +13 -11
  112. package/dist/seo/index.d.mts +1 -1
  113. package/dist/storage/local.d.mts +1 -1
  114. package/dist/storage/local.mjs +1 -1
  115. package/dist/storage/s3.d.mts +1 -1
  116. package/dist/storage/s3.mjs +1 -1
  117. package/dist/taxonomies-CTtewrSQ.mjs +407 -0
  118. package/dist/taxonomies-CTtewrSQ.mjs.map +1 -0
  119. package/dist/taxonomy-DSxx2K2L.mjs +218 -0
  120. package/dist/taxonomy-DSxx2K2L.mjs.map +1 -0
  121. package/dist/{tokens-D9vnZqYS.mjs → tokens-CyRDPVW2.mjs} +1 -1
  122. package/dist/{tokens-D9vnZqYS.mjs.map → tokens-CyRDPVW2.mjs.map} +1 -1
  123. package/dist/{transaction-Cn2rjY78.mjs → transaction-D44LBXvU.mjs} +1 -1
  124. package/dist/{transaction-Cn2rjY78.mjs.map → transaction-D44LBXvU.mjs.map} +1 -1
  125. package/dist/{transport-CUnEL3Vs.d.mts → transport-DX_5rpsq.d.mts} +1 -1
  126. package/dist/{transport-CUnEL3Vs.d.mts.map → transport-DX_5rpsq.d.mts.map} +1 -1
  127. package/dist/{transport-C9ugt2Nr.mjs → transport-xpzIjCIB.mjs} +6 -5
  128. package/dist/{transport-C9ugt2Nr.mjs.map → transport-xpzIjCIB.mjs.map} +1 -1
  129. package/dist/{types-BrA0xf5I.d.mts → types-B_CXXnzh.d.mts} +1 -1
  130. package/dist/{types-BrA0xf5I.d.mts.map → types-B_CXXnzh.d.mts.map} +1 -1
  131. package/dist/{types-DIMwPFub.d.mts → types-C-aFbqmA.d.mts} +1 -1
  132. package/dist/{types-DIMwPFub.d.mts.map → types-C-aFbqmA.d.mts.map} +1 -1
  133. package/dist/types-CoO6mpV3.mjs +68 -0
  134. package/dist/types-CoO6mpV3.mjs.map +1 -0
  135. package/dist/{types-i36XcA_X.d.mts → types-D19uBYWn.d.mts} +83 -7
  136. package/dist/types-D19uBYWn.d.mts.map +1 -0
  137. package/dist/{types-BmPPSUEx.d.mts → types-Dl1fgFjn.d.mts} +24 -2
  138. package/dist/{types-BmPPSUEx.d.mts.map → types-Dl1fgFjn.d.mts.map} +1 -1
  139. package/dist/{types-CS8FIX7L.d.mts → types-Dtx1mSMX.d.mts} +9 -1
  140. package/dist/types-Dtx1mSMX.d.mts.map +1 -0
  141. package/dist/{types-Bm1dn-q3.mjs → types-Eg829jj9.mjs} +1 -1
  142. package/dist/{types-Bm1dn-q3.mjs.map → types-Eg829jj9.mjs.map} +1 -1
  143. package/dist/{types-CgqmmMJB.mjs → types-K-EkEQCI.mjs} +1 -1
  144. package/dist/{types-CgqmmMJB.mjs.map → types-K-EkEQCI.mjs.map} +1 -1
  145. package/dist/{validate-CxVsLehf.mjs → validate-CBIbxM3L.mjs} +14 -10
  146. package/dist/validate-CBIbxM3L.mjs.map +1 -0
  147. package/dist/{validate-DHxmpFJt.d.mts → validate-DHGwADqO.d.mts} +18 -5
  148. package/dist/validate-DHGwADqO.d.mts.map +1 -0
  149. package/dist/{validation-C-ZpN2GI.mjs → validation-B1NYiEos.mjs} +6 -6
  150. package/dist/{validation-C-ZpN2GI.mjs.map → validation-B1NYiEos.mjs.map} +1 -1
  151. package/dist/version-CMD42IRC.mjs +7 -0
  152. package/dist/{version-Bbq8TCrz.mjs.map → version-CMD42IRC.mjs.map} +1 -1
  153. package/dist/{zod-generator-CpwccCIv.mjs → zod-generator-BNJDQBSZ.mjs} +11 -6
  154. package/dist/{zod-generator-CpwccCIv.mjs.map → zod-generator-BNJDQBSZ.mjs.map} +1 -1
  155. package/locals.d.ts +1 -6
  156. package/package.json +9 -8
  157. package/src/api/handlers/comments.ts +6 -4
  158. package/src/api/handlers/content.ts +40 -1
  159. package/src/api/handlers/dashboard.ts +29 -36
  160. package/src/api/handlers/device-flow.ts +5 -0
  161. package/src/api/handlers/marketplace.ts +11 -4
  162. package/src/api/handlers/menus.ts +256 -75
  163. package/src/api/handlers/oauth-authorization.ts +72 -33
  164. package/src/api/handlers/revision.ts +23 -14
  165. package/src/api/handlers/taxonomies.ts +273 -100
  166. package/src/api/public-url.ts +48 -2
  167. package/src/api/schemas/comments.ts +2 -2
  168. package/src/api/schemas/common.ts +7 -0
  169. package/src/api/schemas/content.ts +17 -0
  170. package/src/api/schemas/menus.ts +23 -0
  171. package/src/api/schemas/sections.ts +3 -3
  172. package/src/api/schemas/taxonomies.ts +39 -0
  173. package/src/api/schemas/users.ts +1 -1
  174. package/src/api/types.ts +5 -1
  175. package/src/astro/integration/index.ts +17 -0
  176. package/src/astro/integration/routes.ts +10 -0
  177. package/src/astro/integration/runtime.ts +30 -0
  178. package/src/astro/integration/virtual-modules.ts +32 -2
  179. package/src/astro/integration/vite-config.ts +6 -1
  180. package/src/astro/middleware/auth.ts +13 -6
  181. package/src/astro/middleware/redirect.ts +29 -16
  182. package/src/astro/middleware/request-context.ts +15 -5
  183. package/src/astro/middleware.ts +23 -9
  184. package/src/astro/routes/api/auth/invite/complete.ts +6 -1
  185. package/src/astro/routes/api/auth/passkey/register/verify.ts +6 -1
  186. package/src/astro/routes/api/auth/passkey/verify.ts +6 -1
  187. package/src/astro/routes/api/auth/signup/complete.ts +6 -1
  188. package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +2 -2
  189. package/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts +4 -2
  190. package/src/astro/routes/api/content/[collection]/[id]/permanent.ts +1 -1
  191. package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +34 -12
  192. package/src/astro/routes/api/content/[collection]/[id]/publish.ts +32 -2
  193. package/src/astro/routes/api/content/[collection]/[id]/restore.ts +4 -2
  194. package/src/astro/routes/api/content/[collection]/[id]/revisions.ts +3 -2
  195. package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +8 -4
  196. package/src/astro/routes/api/content/[collection]/[id].ts +12 -0
  197. package/src/astro/routes/api/import/wordpress/execute.ts +3 -1
  198. package/src/astro/routes/api/import/wordpress/prepare.ts +7 -8
  199. package/src/astro/routes/api/import/wordpress/rewrite-url-helpers.ts +196 -0
  200. package/src/astro/routes/api/import/wordpress/rewrite-urls.ts +9 -177
  201. package/src/astro/routes/api/import/wordpress-plugin/execute.ts +3 -1
  202. package/src/astro/routes/api/manifest.ts +62 -45
  203. package/src/astro/routes/api/media/[id]/confirm.ts +10 -1
  204. package/src/astro/routes/api/media/providers/[providerId]/index.ts +12 -3
  205. package/src/astro/routes/api/menus/[name]/items.ts +16 -6
  206. package/src/astro/routes/api/menus/[name]/reorder.ts +8 -3
  207. package/src/astro/routes/api/menus/[name]/translations.ts +82 -0
  208. package/src/astro/routes/api/menus/[name].ts +19 -10
  209. package/src/astro/routes/api/menus/index.ts +9 -6
  210. package/src/astro/routes/api/openapi.json.ts +27 -10
  211. package/src/astro/routes/api/redirects/404s/index.ts +10 -4
  212. package/src/astro/routes/api/redirects/404s/summary.ts +4 -2
  213. package/src/astro/routes/api/redirects/[id].ts +10 -4
  214. package/src/astro/routes/api/redirects/index.ts +7 -3
  215. package/src/astro/routes/api/revisions/[revisionId]/index.ts +1 -1
  216. package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +0 -2
  217. package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +0 -1
  218. package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +0 -1
  219. package/src/astro/routes/api/schema/collections/[slug]/index.ts +2 -2
  220. package/src/astro/routes/api/schema/collections/index.ts +1 -1
  221. package/src/astro/routes/api/search/index.ts +10 -2
  222. package/src/astro/routes/api/sections/[slug].ts +10 -4
  223. package/src/astro/routes/api/sections/index.ts +7 -3
  224. package/src/astro/routes/api/setup/admin-verify.ts +6 -1
  225. package/src/astro/routes/api/snapshot.ts +44 -18
  226. package/src/astro/routes/api/taxonomies/[name]/terms/[slug]/translations.ts +89 -0
  227. package/src/astro/routes/api/taxonomies/[name]/terms/[slug].ts +22 -22
  228. package/src/astro/routes/api/taxonomies/[name]/terms/index.ts +11 -14
  229. package/src/astro/routes/api/taxonomies/index.ts +9 -7
  230. package/src/astro/routes/api/themes/preview.ts +11 -5
  231. package/src/astro/types.ts +23 -3
  232. package/src/auth/allowed-origins.ts +168 -0
  233. package/src/auth/passkey-config.ts +35 -13
  234. package/src/bylines/index.ts +37 -88
  235. package/src/cli/commands/auth.ts +28 -6
  236. package/src/cli/commands/bundle-utils.ts +11 -2
  237. package/src/cli/commands/bundle.ts +28 -8
  238. package/src/cli/commands/content.ts +13 -0
  239. package/src/cli/commands/export-seed.ts +82 -21
  240. package/src/cli/commands/login.ts +8 -1
  241. package/src/cli/commands/plugin-init.ts +216 -90
  242. package/src/cli/commands/publish.ts +24 -0
  243. package/src/cli/commands/secrets.ts +183 -0
  244. package/src/cli/credentials.ts +1 -1
  245. package/src/cli/index.ts +5 -1
  246. package/src/client/index.ts +4 -4
  247. package/src/client/transport.ts +17 -7
  248. package/src/components/Break.astro +2 -2
  249. package/src/components/EmDashHead.astro +18 -13
  250. package/src/components/Embed.astro +1 -1
  251. package/src/components/Gallery.astro +1 -1
  252. package/src/components/Image.astro +1 -1
  253. package/src/components/InlinePortableTextEditor.tsx +104 -18
  254. package/src/config/secrets.ts +528 -0
  255. package/src/database/dialect-helpers.ts +50 -0
  256. package/src/database/migrations/034_published_at_index.ts +1 -1
  257. package/src/database/migrations/035_bounded_404_log.ts +56 -39
  258. package/src/database/migrations/036_i18n_menus_and_taxonomies.ts +477 -0
  259. package/src/database/migrations/runner.ts +158 -23
  260. package/src/database/repositories/content.ts +47 -12
  261. package/src/database/repositories/redirect.ts +14 -3
  262. package/src/database/repositories/taxonomy.ts +212 -82
  263. package/src/database/types.ts +10 -2
  264. package/src/db/libsql.ts +1 -3
  265. package/src/db/sqlite.ts +2 -5
  266. package/src/emdash-runtime.ts +84 -159
  267. package/src/i18n/resolve.ts +37 -0
  268. package/src/index.ts +9 -0
  269. package/src/loader.ts +73 -3
  270. package/src/mcp/server.ts +180 -54
  271. package/src/menus/index.ts +143 -124
  272. package/src/menus/types.ts +15 -1
  273. package/src/page/site-identity.ts +58 -0
  274. package/src/plugins/adapt-sandbox-entry.ts +22 -10
  275. package/src/plugins/context.ts +13 -10
  276. package/src/plugins/define-plugin.ts +40 -12
  277. package/src/plugins/hooks.ts +23 -19
  278. package/src/plugins/index.ts +9 -0
  279. package/src/plugins/manifest-schema.ts +37 -2
  280. package/src/plugins/types.ts +151 -11
  281. package/src/preview/urls.ts +23 -3
  282. package/src/query.ts +148 -5
  283. package/src/redirects/cache.ts +38 -18
  284. package/src/schema/registry.ts +56 -0
  285. package/src/schema/zod-generator.ts +39 -7
  286. package/src/seed/apply.ts +142 -54
  287. package/src/seed/types.ts +14 -1
  288. package/src/seed/validate.ts +27 -13
  289. package/src/settings/index.ts +80 -6
  290. package/src/settings/types.ts +23 -1
  291. package/src/taxonomies/index.ts +237 -210
  292. package/src/taxonomies/types.ts +10 -0
  293. package/dist/apply-x0eMK1lX.mjs.map +0 -1
  294. package/dist/bylines-CRNsVG88.mjs +0 -157
  295. package/dist/bylines-CRNsVG88.mjs.map +0 -1
  296. package/dist/cache-BkKBuIvS.mjs +0 -56
  297. package/dist/cache-BkKBuIvS.mjs.map +0 -1
  298. package/dist/chunk-ClPoSABd.mjs +0 -21
  299. package/dist/content-BcQPYxdV.mjs.map +0 -1
  300. package/dist/dialect-helpers-DhTzaUxP.mjs.map +0 -1
  301. package/dist/index-DIb-CzNx.d.mts.map +0 -1
  302. package/dist/loader-CndGj8kM.mjs.map +0 -1
  303. package/dist/manifest-schema-DH9xhc6t.mjs.map +0 -1
  304. package/dist/query-fqEdLFms.mjs.map +0 -1
  305. package/dist/redirect-D_pshWdf.mjs.map +0 -1
  306. package/dist/registry-C3Mr0ODu.mjs.map +0 -1
  307. package/dist/runner-OURCaApa.d.mts +0 -34
  308. package/dist/runner-OURCaApa.d.mts.map +0 -1
  309. package/dist/runner-tQ7BJ4T7.mjs.map +0 -1
  310. package/dist/search-BoZYFuUk.mjs.map +0 -1
  311. package/dist/taxonomies-B4IAshV8.mjs +0 -308
  312. package/dist/taxonomies-B4IAshV8.mjs.map +0 -1
  313. package/dist/types-CS8FIX7L.d.mts.map +0 -1
  314. package/dist/types-i36XcA_X.d.mts.map +0 -1
  315. package/dist/validate-CxVsLehf.mjs.map +0 -1
  316. package/dist/validate-DHxmpFJt.d.mts.map +0 -1
  317. package/dist/version-Bbq8TCrz.mjs +0 -7
@@ -1,16 +1,20 @@
1
1
  /**
2
- * Taxonomy and term CRUD handlers
2
+ * Taxonomy and term CRUD handlers.
3
+ *
4
+ * i18n: terms and defs are per-locale. `(name, slug, locale)` is unique for
5
+ * terms; `(name, locale)` for defs. Translations of the same term/def share a
6
+ * `translation_group`. The content_taxonomies pivot stores translation_groups
7
+ * so assignments span every locale of a post.
3
8
  */
4
9
 
5
- import type { Kysely } from "kysely";
10
+ import type { Kysely, Selectable } from "kysely";
6
11
  import { ulid } from "ulidx";
7
12
 
8
13
  import { TaxonomyRepository } from "../../database/repositories/taxonomy.js";
9
- import type { Database } from "../../database/types.js";
14
+ import type { Database, TaxonomyDefTable } from "../../database/types.js";
10
15
  import { invalidateTermCache } from "../../taxonomies/index.js";
11
16
  import type { ApiResult } from "../types.js";
12
17
 
13
- /** Taxonomy name validation pattern: lowercase alphanumeric + underscores, starts with letter */
14
18
  const NAME_PATTERN = /^[a-z][a-z0-9_]*$/;
15
19
 
16
20
  // ---------------------------------------------------------------------------
@@ -24,6 +28,8 @@ export interface TaxonomyDef {
24
28
  labelSingular?: string;
25
29
  hierarchical: boolean;
26
30
  collections: string[];
31
+ locale: string;
32
+ translationGroup: string | null;
27
33
  }
28
34
 
29
35
  export interface TaxonomyListResponse {
@@ -37,6 +43,8 @@ export interface TermData {
37
43
  label: string;
38
44
  parentId: string | null;
39
45
  description?: string;
46
+ locale: string;
47
+ translationGroup: string | null;
40
48
  }
41
49
 
42
50
  export interface TermWithCount extends TermData {
@@ -59,6 +67,26 @@ export interface TermGetResponse {
59
67
  };
60
68
  }
61
69
 
70
+ export interface TermTranslationsResponse {
71
+ translationGroup: string | null;
72
+ translations: Array<{
73
+ id: string;
74
+ slug: string;
75
+ label: string;
76
+ locale: string;
77
+ }>;
78
+ }
79
+
80
+ export interface TaxonomyDefTranslationsResponse {
81
+ translationGroup: string | null;
82
+ translations: Array<{
83
+ id: string;
84
+ name: string;
85
+ label: string;
86
+ locale: string;
87
+ }>;
88
+ }
89
+
62
90
  // ---------------------------------------------------------------------------
63
91
  // Helpers
64
92
  // ---------------------------------------------------------------------------
@@ -69,11 +97,7 @@ export interface TermGetResponse {
69
97
  function buildTree(flatTerms: TermWithCount[]): TermWithCount[] {
70
98
  const map = new Map<string, TermWithCount>();
71
99
  const roots: TermWithCount[] = [];
72
-
73
- for (const term of flatTerms) {
74
- map.set(term.id, term);
75
- }
76
-
100
+ for (const term of flatTerms) map.set(term.id, term);
77
101
  for (const term of flatTerms) {
78
102
  if (term.parentId && map.has(term.parentId)) {
79
103
  map.get(term.parentId)!.children.push(term);
@@ -81,38 +105,48 @@ function buildTree(flatTerms: TermWithCount[]): TermWithCount[] {
81
105
  roots.push(term);
82
106
  }
83
107
  }
84
-
85
108
  return roots;
86
109
  }
87
110
 
88
111
  /**
89
- * Look up a taxonomy definition by name, returning a NOT_FOUND error if missing.
112
+ * Look up a taxonomy definition by name (optionally scoped to a locale).
113
+ * Returns the lowest-locale match when no locale is provided.
90
114
  */
91
115
  async function requireTaxonomyDef(
92
116
  db: Kysely<Database>,
93
117
  name: string,
118
+ locale?: string,
94
119
  ): Promise<
95
- | { success: true; def: { hierarchical: number } }
120
+ | { success: true; def: Selectable<TaxonomyDefTable> }
96
121
  | { success: false; error: { code: string; message: string } }
97
122
  > {
98
- const def = await db
99
- .selectFrom("_emdash_taxonomy_defs")
100
- .selectAll()
101
- .where("name", "=", name)
102
- .executeTakeFirst();
103
-
123
+ let query = db.selectFrom("_emdash_taxonomy_defs").selectAll().where("name", "=", name);
124
+ if (locale !== undefined) query = query.where("locale", "=", locale);
125
+ const def = await query.orderBy("locale", "asc").executeTakeFirst();
104
126
  if (!def) {
105
127
  return {
106
128
  success: false,
107
129
  error: { code: "NOT_FOUND", message: `Taxonomy '${name}' not found` },
108
130
  };
109
131
  }
110
-
111
132
  return { success: true, def };
112
133
  }
113
134
 
135
+ function rowToDef(row: Selectable<TaxonomyDefTable>): TaxonomyDef {
136
+ return {
137
+ id: row.id,
138
+ name: row.name,
139
+ label: row.label,
140
+ labelSingular: row.label_singular ?? undefined,
141
+ hierarchical: row.hierarchical === 1,
142
+ collections: row.collections ? JSON.parse(row.collections) : [],
143
+ locale: row.locale,
144
+ translationGroup: row.translation_group,
145
+ };
146
+ }
147
+
114
148
  // ---------------------------------------------------------------------------
115
- // Handlers
149
+ // Taxonomy definition handlers
116
150
  // ---------------------------------------------------------------------------
117
151
 
118
152
  /**
@@ -120,10 +154,13 @@ async function requireTaxonomyDef(
120
154
  */
121
155
  export async function handleTaxonomyList(
122
156
  db: Kysely<Database>,
157
+ options: { locale?: string } = {},
123
158
  ): Promise<ApiResult<TaxonomyListResponse>> {
124
159
  try {
160
+ let query = db.selectFrom("_emdash_taxonomy_defs").selectAll();
161
+ if (options.locale !== undefined) query = query.where("locale", "=", options.locale);
125
162
  const [rows, collectionRows] = await Promise.all([
126
- db.selectFrom("_emdash_taxonomy_defs").selectAll().execute(),
163
+ query.execute(),
127
164
  db.selectFrom("_emdash_collections").select("slug").execute(),
128
165
  ]);
129
166
 
@@ -133,15 +170,8 @@ export async function handleTaxonomyList(
133
170
  const realCollections = new Set(collectionRows.map((r) => r.slug));
134
171
 
135
172
  const taxonomies: TaxonomyDef[] = rows.map((row) => {
136
- const stored: string[] = row.collections ? JSON.parse(row.collections) : [];
137
- return {
138
- id: row.id,
139
- name: row.name,
140
- label: row.label,
141
- labelSingular: row.label_singular ?? undefined,
142
- hierarchical: row.hierarchical === 1,
143
- collections: stored.filter((slug) => realCollections.has(slug)),
144
- };
173
+ const def = rowToDef(row);
174
+ return { ...def, collections: def.collections.filter((slug) => realCollections.has(slug)) };
145
175
  });
146
176
 
147
177
  return { success: true, data: { taxonomies } };
@@ -158,10 +188,17 @@ export async function handleTaxonomyList(
158
188
  */
159
189
  export async function handleTaxonomyCreate(
160
190
  db: Kysely<Database>,
161
- input: { name: string; label: string; hierarchical?: boolean; collections?: string[] },
191
+ input: {
192
+ name: string;
193
+ label: string;
194
+ labelSingular?: string;
195
+ hierarchical?: boolean;
196
+ collections?: string[];
197
+ locale?: string;
198
+ translationOf?: string;
199
+ },
162
200
  ): Promise<ApiResult<{ taxonomy: TaxonomyDef }>> {
163
201
  try {
164
- // Validate name format
165
202
  if (!NAME_PATTERN.test(input.name)) {
166
203
  return {
167
204
  success: false,
@@ -174,15 +211,12 @@ export async function handleTaxonomyCreate(
174
211
  }
175
212
 
176
213
  const collections = [...new Set(input.collections ?? [])];
177
-
178
- // Validate that referenced collections exist
179
214
  if (collections.length > 0) {
180
215
  const existingCollections = await db
181
216
  .selectFrom("_emdash_collections")
182
217
  .select("slug")
183
218
  .where("slug", "in", collections)
184
219
  .execute();
185
-
186
220
  const existingSlugs = new Set(existingCollections.map((c) => c.slug));
187
221
  const invalid = collections.filter((c) => !existingSlugs.has(c));
188
222
  if (invalid.length > 0) {
@@ -196,58 +230,68 @@ export async function handleTaxonomyCreate(
196
230
  }
197
231
  }
198
232
 
199
- // Check for duplicate name
200
- const existing = await db
201
- .selectFrom("_emdash_taxonomy_defs")
202
- .selectAll()
203
- .where("name", "=", input.name)
204
- .executeTakeFirst();
233
+ let translationGroup: string | null = null;
234
+ if (input.translationOf) {
235
+ const source = await db
236
+ .selectFrom("_emdash_taxonomy_defs")
237
+ .selectAll()
238
+ .where("id", "=", input.translationOf)
239
+ .executeTakeFirst();
240
+ if (!source) {
241
+ return {
242
+ success: false,
243
+ error: { code: "NOT_FOUND", message: "Source taxonomy for translation not found" },
244
+ };
245
+ }
246
+ translationGroup = source.translation_group ?? source.id;
247
+ }
205
248
 
206
- if (existing) {
207
- return {
208
- success: false,
209
- error: {
210
- code: "CONFLICT",
211
- message: `Taxonomy '${input.name}' already exists`,
212
- },
213
- };
249
+ // Duplicate guard scoped to locale (so the same name can exist in ES
250
+ // and EN).
251
+ if (input.locale !== undefined) {
252
+ const existing = await db
253
+ .selectFrom("_emdash_taxonomy_defs")
254
+ .select("id")
255
+ .where("name", "=", input.name)
256
+ .where("locale", "=", input.locale)
257
+ .executeTakeFirst();
258
+ if (existing) {
259
+ return {
260
+ success: false,
261
+ error: {
262
+ code: "CONFLICT",
263
+ message: `Taxonomy '${input.name}' already exists in locale '${input.locale}'`,
264
+ },
265
+ };
266
+ }
214
267
  }
215
268
 
216
269
  const id = ulid();
217
-
218
270
  await db
219
271
  .insertInto("_emdash_taxonomy_defs")
220
272
  .values({
221
273
  id,
222
274
  name: input.name,
223
275
  label: input.label,
224
- label_singular: null,
276
+ label_singular: input.labelSingular ?? null,
225
277
  hierarchical: input.hierarchical ? 1 : 0,
226
278
  collections: JSON.stringify(collections),
279
+ ...(input.locale !== undefined ? { locale: input.locale } : {}),
280
+ translation_group: translationGroup ?? id,
227
281
  })
228
282
  .execute();
229
283
 
230
- return {
231
- success: true,
232
- data: {
233
- taxonomy: {
234
- id,
235
- name: input.name,
236
- label: input.label,
237
- hierarchical: input.hierarchical ?? false,
238
- collections,
239
- },
240
- },
241
- };
284
+ const row = await db
285
+ .selectFrom("_emdash_taxonomy_defs")
286
+ .selectAll()
287
+ .where("id", "=", id)
288
+ .executeTakeFirstOrThrow();
289
+ return { success: true, data: { taxonomy: rowToDef(row) } };
242
290
  } catch (error) {
243
- // Handle UNIQUE constraint violation from concurrent duplicate inserts
244
291
  if (error instanceof Error && error.message.includes("UNIQUE constraint failed")) {
245
292
  return {
246
293
  success: false,
247
- error: {
248
- code: "CONFLICT",
249
- message: `Taxonomy '${input.name}' already exists`,
250
- },
294
+ error: { code: "CONFLICT", message: `Taxonomy '${input.name}' already exists` },
251
295
  };
252
296
  }
253
297
  return {
@@ -257,26 +301,81 @@ export async function handleTaxonomyCreate(
257
301
  }
258
302
  }
259
303
 
304
+ /**
305
+ * List every locale translation of a taxonomy def (by id or translation_group).
306
+ */
307
+ export async function handleTaxonomyDefTranslations(
308
+ db: Kysely<Database>,
309
+ idOrGroup: string,
310
+ ): Promise<ApiResult<TaxonomyDefTranslationsResponse>> {
311
+ try {
312
+ const anchor = await db
313
+ .selectFrom("_emdash_taxonomy_defs")
314
+ .selectAll()
315
+ .where((eb) => eb.or([eb("id", "=", idOrGroup), eb("translation_group", "=", idOrGroup)]))
316
+ .executeTakeFirst();
317
+ if (!anchor) {
318
+ return {
319
+ success: false,
320
+ error: { code: "NOT_FOUND", message: "Taxonomy not found" },
321
+ };
322
+ }
323
+ const group = anchor.translation_group ?? anchor.id;
324
+ const rows = await db
325
+ .selectFrom("_emdash_taxonomy_defs")
326
+ .selectAll()
327
+ .where("translation_group", "=", group)
328
+ .orderBy("locale", "asc")
329
+ .execute();
330
+ return {
331
+ success: true,
332
+ data: {
333
+ translationGroup: group,
334
+ translations: rows.map((r) => ({
335
+ id: r.id,
336
+ name: r.name,
337
+ label: r.label,
338
+ locale: r.locale,
339
+ })),
340
+ },
341
+ };
342
+ } catch {
343
+ return {
344
+ success: false,
345
+ error: {
346
+ code: "TAXONOMY_TRANSLATIONS_ERROR",
347
+ message: "Failed to list taxonomy translations",
348
+ },
349
+ };
350
+ }
351
+ }
352
+
353
+ // ---------------------------------------------------------------------------
354
+ // Term handlers
355
+ // ---------------------------------------------------------------------------
356
+
260
357
  /**
261
358
  * List all terms for a taxonomy (returns tree for hierarchical taxonomies)
262
359
  */
263
360
  export async function handleTermList(
264
361
  db: Kysely<Database>,
265
362
  taxonomyName: string,
363
+ options: { locale?: string } = {},
266
364
  ): Promise<ApiResult<TermListResponse>> {
267
365
  try {
366
+ // Definitions are per-locale but terms aren't bound to the def's locale —
367
+ // just ensure the taxonomy exists somewhere.
268
368
  const lookup = await requireTaxonomyDef(db, taxonomyName);
269
369
  if (!lookup.success) return lookup;
270
370
 
271
371
  const repo = new TaxonomyRepository(db);
272
- const terms = await repo.findByName(taxonomyName);
372
+ const terms = await repo.findByName(taxonomyName, { locale: options.locale });
273
373
 
274
- // Get counts for each term
275
- const counts = new Map<string, number>();
276
- for (const term of terms) {
277
- const count = await repo.countEntriesWithTerm(term.id);
278
- counts.set(term.id, count);
279
- }
374
+ // Batch count entries per term in a single query (replaces N+1 pattern).
375
+ // content_taxonomies.taxonomy_id stores the translation_group, so we
376
+ // look up by group and map back to each term's id.
377
+ const groups = terms.map((t) => t.translationGroup ?? t.id);
378
+ const countsByGroup = await repo.countEntriesForTerms(groups);
280
379
 
281
380
  const termData: TermWithCount[] = terms.map((term) => ({
282
381
  id: term.id,
@@ -286,12 +385,13 @@ export async function handleTermList(
286
385
  parentId: term.parentId,
287
386
  description: typeof term.data?.description === "string" ? term.data.description : undefined,
288
387
  children: [],
289
- count: counts.get(term.id) ?? 0,
388
+ count: countsByGroup.get(term.translationGroup ?? term.id) ?? 0,
389
+ locale: term.locale,
390
+ translationGroup: term.translationGroup,
290
391
  }));
291
392
 
292
393
  const isHierarchical = lookup.def.hierarchical === 1;
293
394
  const result = isHierarchical ? buildTree(termData) : termData;
294
-
295
395
  return { success: true, data: { terms: result } };
296
396
  } catch {
297
397
  return {
@@ -385,30 +485,60 @@ async function validateParentTerm(
385
485
  export async function handleTermCreate(
386
486
  db: Kysely<Database>,
387
487
  taxonomyName: string,
388
- input: { slug: string; label: string; parentId?: string | null; description?: string },
488
+ input: {
489
+ slug: string;
490
+ label: string;
491
+ parentId?: string | null;
492
+ description?: string;
493
+ locale?: string;
494
+ translationOf?: string;
495
+ },
389
496
  ): Promise<ApiResult<TermResponse>> {
390
497
  try {
498
+ // Taxonomy definitions are per-locale, but terms can exist in any locale
499
+ // regardless of whether the def has been translated there. Look up the
500
+ // def across all locales — we only care that it *exists*.
391
501
  const lookup = await requireTaxonomyDef(db, taxonomyName);
392
502
  if (!lookup.success) return lookup;
393
503
 
394
504
  const repo = new TaxonomyRepository(db);
395
505
 
396
506
  // Coerce empty-string parentId to undefined (treat as "no parent").
397
- const parentId =
507
+ let parentId =
398
508
  input.parentId === "" || input.parentId === undefined ? undefined : input.parentId;
399
509
 
400
- // Check for slug conflict
401
- const existing = await repo.findBySlug(taxonomyName, input.slug);
510
+ // Conflict check is scoped to locale (per-locale slugs are unique).
511
+ const existing = await repo.findBySlug(taxonomyName, input.slug, input.locale);
402
512
  if (existing) {
403
513
  return {
404
514
  success: false,
405
515
  error: {
406
516
  code: "CONFLICT",
407
- message: `Term with slug '${input.slug}' already exists in taxonomy '${taxonomyName}'`,
517
+ message: input.locale
518
+ ? `Term '${input.slug}' already exists in '${taxonomyName}' (${input.locale})`
519
+ : `Term with slug '${input.slug}' already exists in taxonomy '${taxonomyName}'`,
408
520
  },
409
521
  };
410
522
  }
411
523
 
524
+ // If creating a translation whose parent is the translated sibling of
525
+ // the source's parent, try to resolve the parent in the same locale.
526
+ if (input.translationOf && parentId) {
527
+ const source = await repo.findById(input.translationOf);
528
+ if (source?.parentId === parentId && input.locale) {
529
+ const sourceParent = await repo.findById(parentId);
530
+ if (sourceParent?.translationGroup) {
531
+ const translatedParent = await db
532
+ .selectFrom("taxonomies")
533
+ .select("id")
534
+ .where("translation_group", "=", sourceParent.translationGroup)
535
+ .where("locale", "=", input.locale)
536
+ .executeTakeFirst();
537
+ if (translatedParent) parentId = translatedParent.id;
538
+ }
539
+ }
540
+ }
541
+
412
542
  // Validate parentId: must exist AND belong to the same taxonomy.
413
543
  // (Cycle check is N/A on create — the term doesn't exist yet.)
414
544
  const parentError = await validateParentTerm(repo, taxonomyName, undefined, parentId);
@@ -422,10 +552,10 @@ export async function handleTermCreate(
422
552
  label: input.label,
423
553
  parentId: parentId ?? undefined,
424
554
  data: input.description ? { description: input.description } : undefined,
555
+ locale: input.locale,
556
+ translationOf: input.translationOf,
425
557
  });
426
558
 
427
- // New term means `hasAnyTermAssignments` may flip from false->true next
428
- // time an entry is tagged. Clear the cache so the next read re-probes.
429
559
  invalidateTermCache();
430
560
 
431
561
  return {
@@ -439,6 +569,8 @@ export async function handleTermCreate(
439
569
  parentId: term.parentId,
440
570
  description:
441
571
  typeof term.data?.description === "string" ? term.data.description : undefined,
572
+ locale: term.locale,
573
+ translationGroup: term.translationGroup,
442
574
  },
443
575
  },
444
576
  };
@@ -457,10 +589,11 @@ export async function handleTermGet(
457
589
  db: Kysely<Database>,
458
590
  taxonomyName: string,
459
591
  termSlug: string,
592
+ options: { locale?: string } = {},
460
593
  ): Promise<ApiResult<TermGetResponse>> {
461
594
  try {
462
595
  const repo = new TaxonomyRepository(db);
463
- const term = await repo.findBySlug(taxonomyName, termSlug);
596
+ const term = await repo.findBySlug(taxonomyName, termSlug, options.locale);
464
597
 
465
598
  if (!term) {
466
599
  return {
@@ -487,11 +620,9 @@ export async function handleTermGet(
487
620
  description:
488
621
  typeof term.data?.description === "string" ? term.data.description : undefined,
489
622
  count,
490
- children: children.map((c) => ({
491
- id: c.id,
492
- slug: c.slug,
493
- label: c.label,
494
- })),
623
+ children: children.map((c) => ({ id: c.id, slug: c.slug, label: c.label })),
624
+ locale: term.locale,
625
+ translationGroup: term.translationGroup,
495
626
  },
496
627
  },
497
628
  };
@@ -503,6 +634,50 @@ export async function handleTermGet(
503
634
  }
504
635
  }
505
636
 
637
+ /** List every translation of a term (by id or translation_group). */
638
+ export async function handleTermTranslations(
639
+ db: Kysely<Database>,
640
+ idOrGroup: string,
641
+ ): Promise<ApiResult<TermTranslationsResponse>> {
642
+ try {
643
+ const anchor = await db
644
+ .selectFrom("taxonomies")
645
+ .selectAll()
646
+ .where((eb) => eb.or([eb("id", "=", idOrGroup), eb("translation_group", "=", idOrGroup)]))
647
+ .executeTakeFirst();
648
+ if (!anchor) {
649
+ return {
650
+ success: false,
651
+ error: { code: "NOT_FOUND", message: "Term not found" },
652
+ };
653
+ }
654
+ const group = anchor.translation_group ?? anchor.id;
655
+ const rows = await db
656
+ .selectFrom("taxonomies")
657
+ .selectAll()
658
+ .where("translation_group", "=", group)
659
+ .orderBy("locale", "asc")
660
+ .execute();
661
+ return {
662
+ success: true,
663
+ data: {
664
+ translationGroup: group,
665
+ translations: rows.map((r) => ({
666
+ id: r.id,
667
+ slug: r.slug,
668
+ label: r.label,
669
+ locale: r.locale,
670
+ })),
671
+ },
672
+ };
673
+ } catch {
674
+ return {
675
+ success: false,
676
+ error: { code: "TERM_TRANSLATIONS_ERROR", message: "Failed to list term translations" },
677
+ };
678
+ }
679
+ }
680
+
506
681
  /**
507
682
  * Update a term
508
683
  */
@@ -511,10 +686,11 @@ export async function handleTermUpdate(
511
686
  taxonomyName: string,
512
687
  termSlug: string,
513
688
  input: { slug?: string; label?: string; parentId?: string | null; description?: string },
689
+ options: { locale?: string } = {},
514
690
  ): Promise<ApiResult<TermResponse>> {
515
691
  try {
516
692
  const repo = new TaxonomyRepository(db);
517
- const term = await repo.findBySlug(taxonomyName, termSlug);
693
+ const term = await repo.findBySlug(taxonomyName, termSlug, options.locale);
518
694
 
519
695
  if (!term) {
520
696
  return {
@@ -532,9 +708,9 @@ export async function handleTermUpdate(
532
708
  const newParentId =
533
709
  input.parentId === "" || input.parentId === undefined ? undefined : input.parentId;
534
710
 
535
- // Check if new slug conflicts
711
+ // Check if new slug conflicts (per-locale uniqueness).
536
712
  if (newSlug !== undefined && newSlug !== termSlug) {
537
- const existing = await repo.findBySlug(taxonomyName, newSlug);
713
+ const existing = await repo.findBySlug(taxonomyName, newSlug, options.locale);
538
714
  if (existing && existing.id !== term.id) {
539
715
  return {
540
716
  success: false,
@@ -559,8 +735,6 @@ export async function handleTermUpdate(
559
735
  data: input.description !== undefined ? { description: input.description } : undefined,
560
736
  });
561
737
 
562
- // Term label/slug changes are reflected in hydrated entry.data.terms —
563
- // invalidate so the next read doesn't short-circuit on a stale probe.
564
738
  invalidateTermCache();
565
739
 
566
740
  if (!updated) {
@@ -581,6 +755,8 @@ export async function handleTermUpdate(
581
755
  parentId: updated.parentId,
582
756
  description:
583
757
  typeof updated.data?.description === "string" ? updated.data.description : undefined,
758
+ locale: updated.locale,
759
+ translationGroup: updated.translationGroup,
584
760
  },
585
761
  },
586
762
  };
@@ -599,10 +775,11 @@ export async function handleTermDelete(
599
775
  db: Kysely<Database>,
600
776
  taxonomyName: string,
601
777
  termSlug: string,
778
+ options: { locale?: string } = {},
602
779
  ): Promise<ApiResult<{ deleted: true }>> {
603
780
  try {
604
781
  const repo = new TaxonomyRepository(db);
605
- const term = await repo.findBySlug(taxonomyName, termSlug);
782
+ const term = await repo.findBySlug(taxonomyName, termSlug, options.locale);
606
783
 
607
784
  if (!term) {
608
785
  return {
@@ -614,7 +791,6 @@ export async function handleTermDelete(
614
791
  };
615
792
  }
616
793
 
617
- // Prevent deletion of terms with children
618
794
  const children = await repo.findChildren(term.id);
619
795
  if (children.length > 0) {
620
796
  return {
@@ -634,10 +810,7 @@ export async function handleTermDelete(
634
810
  };
635
811
  }
636
812
 
637
- // Deleting a term cascades to content_taxonomies; invalidate so
638
- // hydration no longer sees the stale assignments.
639
813
  invalidateTermCache();
640
-
641
814
  return { success: true, data: { deleted: true } };
642
815
  } catch {
643
816
  return {