emdash 0.9.0 → 0.11.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 (239) hide show
  1. package/dist/{adapters-DoNJiveC.d.mts → adapters-BktHA7EO.d.mts} +1 -1
  2. package/dist/{adapters-DoNJiveC.d.mts.map → adapters-BktHA7EO.d.mts.map} +1 -1
  3. package/dist/{apply-BzltprvY.mjs → apply-Ded_1vng.mjs} +167 -254
  4. package/dist/apply-Ded_1vng.mjs.map +1 -0
  5. package/dist/astro/index.d.mts +6 -6
  6. package/dist/astro/index.mjs +10 -2
  7. package/dist/astro/index.mjs.map +1 -1
  8. package/dist/astro/middleware/auth.d.mts +5 -5
  9. package/dist/astro/middleware/auth.mjs +5 -5
  10. package/dist/astro/middleware/redirect.mjs +5 -5
  11. package/dist/astro/middleware/request-context.mjs +4 -4
  12. package/dist/astro/middleware/setup.mjs +1 -1
  13. package/dist/astro/middleware.d.mts.map +1 -1
  14. package/dist/astro/middleware.mjs +94 -43
  15. package/dist/astro/middleware.mjs.map +1 -1
  16. package/dist/astro/types.d.mts +12 -11
  17. package/dist/astro/types.d.mts.map +1 -1
  18. package/dist/{base64-BRICGH2l.mjs → base64-MBPo9ozB.mjs} +1 -1
  19. package/dist/{base64-BRICGH2l.mjs.map → base64-MBPo9ozB.mjs.map} +1 -1
  20. package/dist/{byline-BSaNL1w7.mjs → byline-gFn1r0vA.mjs} +4 -4
  21. package/dist/{byline-BSaNL1w7.mjs.map → byline-gFn1r0vA.mjs.map} +1 -1
  22. package/dist/{bylines-CvJ3PYz2.mjs → bylines-DTFI8nDM.mjs} +5 -5
  23. package/dist/{bylines-CvJ3PYz2.mjs.map → bylines-DTFI8nDM.mjs.map} +1 -1
  24. package/dist/{cache-C6N_hhN7.mjs → cache-BAJbeoZ8.mjs} +3 -3
  25. package/dist/{cache-C6N_hhN7.mjs.map → cache-BAJbeoZ8.mjs.map} +1 -1
  26. package/dist/{chunks-NBQVDOci.mjs → chunks-BK1oZS-l.mjs} +2 -2
  27. package/dist/{chunks-NBQVDOci.mjs.map → chunks-BK1oZS-l.mjs.map} +1 -1
  28. package/dist/cli/index.mjs +342 -95
  29. package/dist/cli/index.mjs.map +1 -1
  30. package/dist/client/cf-access.d.mts +1 -1
  31. package/dist/client/index.d.mts +1 -1
  32. package/dist/client/index.mjs +1 -1
  33. package/dist/{config-BI0V3ICQ.mjs → config-CVssduLe.mjs} +1 -1
  34. package/dist/{config-BI0V3ICQ.mjs.map → config-CVssduLe.mjs.map} +1 -1
  35. package/dist/{content-8lOYF0pr.mjs → content-CERxPUN0.mjs} +14 -3
  36. package/dist/content-CERxPUN0.mjs.map +1 -0
  37. package/dist/database/instrumentation.d.mts +6 -4
  38. package/dist/database/instrumentation.d.mts.map +1 -1
  39. package/dist/database/instrumentation.mjs +19 -7
  40. package/dist/database/instrumentation.mjs.map +1 -1
  41. package/dist/db/index.d.mts +3 -3
  42. package/dist/db/index.mjs +1 -1
  43. package/dist/db/libsql.d.mts +1 -1
  44. package/dist/db/postgres.d.mts +1 -1
  45. package/dist/db/sqlite.d.mts +1 -1
  46. package/dist/{db-errors-WRezodiz.mjs → db-errors-B7P2pSCn.mjs} +1 -1
  47. package/dist/{db-errors-WRezodiz.mjs.map → db-errors-B7P2pSCn.mjs.map} +1 -1
  48. package/dist/{default-D8ksjWhO.mjs → default-pHuz9WF6.mjs} +1 -1
  49. package/dist/{default-D8ksjWhO.mjs.map → default-pHuz9WF6.mjs.map} +1 -1
  50. package/dist/{error-D_-tqP-I.mjs → error-DqnRMM5z.mjs} +1 -1
  51. package/dist/{error-D_-tqP-I.mjs.map → error-DqnRMM5z.mjs.map} +1 -1
  52. package/dist/{index-BFRaVcD6.d.mts → index-Cg-rC4Gj.d.mts} +110 -87
  53. package/dist/index-Cg-rC4Gj.d.mts.map +1 -0
  54. package/dist/index.d.mts +11 -11
  55. package/dist/index.mjs +29 -28
  56. package/dist/{load-DDqMMvZL.mjs → load-DR1VwFXR.mjs} +2 -2
  57. package/dist/{load-DDqMMvZL.mjs.map → load-DR1VwFXR.mjs.map} +1 -1
  58. package/dist/{loader-CKLbBnhK.mjs → loader-ou_PXAjg.mjs} +31 -6
  59. package/dist/loader-ou_PXAjg.mjs.map +1 -0
  60. package/dist/{manifest-schema-DqWNC3lM.mjs → manifest-schema-CXAbd1vH.mjs} +1 -1
  61. package/dist/{manifest-schema-DqWNC3lM.mjs.map → manifest-schema-CXAbd1vH.mjs.map} +1 -1
  62. package/dist/media/index.d.mts +1 -1
  63. package/dist/media/index.mjs +1 -1
  64. package/dist/media/local-runtime.d.mts +7 -7
  65. package/dist/media/local-runtime.mjs +3 -3
  66. package/dist/{media-BW32b4gi.mjs → media-1fFhub9c.mjs} +22 -10
  67. package/dist/media-1fFhub9c.mjs.map +1 -0
  68. package/dist/{mode-ier8jbBk.mjs → mode-YhqNVef_.mjs} +1 -1
  69. package/dist/{mode-ier8jbBk.mjs.map → mode-YhqNVef_.mjs.map} +1 -1
  70. package/dist/{options-BVp3UsTS.mjs → options-nPxWnrya.mjs} +1 -1
  71. package/dist/{options-BVp3UsTS.mjs.map → options-nPxWnrya.mjs.map} +1 -1
  72. package/dist/page/index.d.mts +2 -2
  73. package/dist/{patterns-CrCYkMBb.mjs → patterns-DsUZ4uxI.mjs} +1 -1
  74. package/dist/{patterns-CrCYkMBb.mjs.map → patterns-DsUZ4uxI.mjs.map} +1 -1
  75. package/dist/{placeholder-BE4o_2dc.d.mts → placeholder-CDPtkelt.d.mts} +1 -1
  76. package/dist/{placeholder-BE4o_2dc.d.mts.map → placeholder-CDPtkelt.d.mts.map} +1 -1
  77. package/dist/{placeholder-CIJejMlK.mjs → placeholder-Ci0RLeCk.mjs} +1 -1
  78. package/dist/{placeholder-CIJejMlK.mjs.map → placeholder-Ci0RLeCk.mjs.map} +1 -1
  79. package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
  80. package/dist/plugins/adapt-sandbox-entry.mjs +2 -2
  81. package/dist/{public-url-DByxYjUw.mjs → public-url-B1AxbbbQ.mjs} +1 -1
  82. package/dist/{public-url-DByxYjUw.mjs.map → public-url-B1AxbbbQ.mjs.map} +1 -1
  83. package/dist/{query-Cg9ZKRQ0.mjs → query-8c_meo_K.mjs} +13 -13
  84. package/dist/{query-Cg9ZKRQ0.mjs.map → query-8c_meo_K.mjs.map} +1 -1
  85. package/dist/{redirect-BhUBKRc1.mjs → redirect-C5H7VGIX.mjs} +3 -3
  86. package/dist/{redirect-BhUBKRc1.mjs.map → redirect-C5H7VGIX.mjs.map} +1 -1
  87. package/dist/{registry-Dw70ChxB.mjs → registry-Do34mz_P.mjs} +7 -6
  88. package/dist/registry-Do34mz_P.mjs.map +1 -0
  89. package/dist/{request-cache-B-bmkipQ.mjs → request-cache-D4I69LeL.mjs} +6 -2
  90. package/dist/request-cache-D4I69LeL.mjs.map +1 -0
  91. package/dist/request-context.d.mts +27 -1
  92. package/dist/request-context.d.mts.map +1 -1
  93. package/dist/request-context.mjs +16 -3
  94. package/dist/request-context.mjs.map +1 -1
  95. package/dist/{runner-C7ADox5q.mjs → runner-DIcU2UCC.mjs} +465 -148
  96. package/dist/runner-DIcU2UCC.mjs.map +1 -0
  97. package/dist/{runner-Bnoj7vjK.d.mts → runner-Iu3IZSDM.d.mts} +2 -2
  98. package/dist/{runner-Bnoj7vjK.d.mts.map → runner-Iu3IZSDM.d.mts.map} +1 -1
  99. package/dist/runtime.d.mts +6 -6
  100. package/dist/runtime.mjs +3 -3
  101. package/dist/{search-dOGEccMa.mjs → search-DuWhx4NG.mjs} +322 -108
  102. package/dist/search-DuWhx4NG.mjs.map +1 -0
  103. package/dist/{secrets-CW3reAnU.mjs → secrets-CZ8rxLX3.mjs} +3 -3
  104. package/dist/{secrets-CW3reAnU.mjs.map → secrets-CZ8rxLX3.mjs.map} +1 -1
  105. package/dist/seed/index.d.mts +2 -2
  106. package/dist/seed/index.mjs +15 -14
  107. package/dist/seo/index.d.mts +1 -1
  108. package/dist/storage/local.d.mts +1 -1
  109. package/dist/storage/local.mjs +1 -1
  110. package/dist/storage/s3.d.mts +1 -1
  111. package/dist/storage/s3.mjs +1 -1
  112. package/dist/taxonomies-Bw76xAxo.mjs +407 -0
  113. package/dist/taxonomies-Bw76xAxo.mjs.map +1 -0
  114. package/dist/taxonomy-D6NvlKo8.mjs +218 -0
  115. package/dist/taxonomy-D6NvlKo8.mjs.map +1 -0
  116. package/dist/{tokens-D7zMmWi2.mjs → tokens-CyRDPVW2.mjs} +2 -2
  117. package/dist/{tokens-D7zMmWi2.mjs.map → tokens-CyRDPVW2.mjs.map} +1 -1
  118. package/dist/{transaction-Cn2rjY78.mjs → transaction-D44LBXvU.mjs} +1 -1
  119. package/dist/{transaction-Cn2rjY78.mjs.map → transaction-D44LBXvU.mjs.map} +1 -1
  120. package/dist/{transport-DNEfeMaU.d.mts → transport-DX_5rpsq.d.mts} +1 -1
  121. package/dist/{transport-DNEfeMaU.d.mts.map → transport-DX_5rpsq.d.mts.map} +1 -1
  122. package/dist/{transport-BeMCmin1.mjs → transport-xpzIjCIB.mjs} +1 -1
  123. package/dist/{transport-BeMCmin1.mjs.map → transport-xpzIjCIB.mjs.map} +1 -1
  124. package/dist/{types-CIOg5AR8.mjs → types-56BKbld_.mjs} +1 -1
  125. package/dist/types-56BKbld_.mjs.map +1 -0
  126. package/dist/{types-CRxNbK-Z.mjs → types-BIgulNsW.mjs} +2 -2
  127. package/dist/{types-CRxNbK-Z.mjs.map → types-BIgulNsW.mjs.map} +1 -1
  128. package/dist/{types-CrtWgIvl.d.mts → types-BQx6ZXpR.d.mts} +10 -1
  129. package/dist/types-BQx6ZXpR.d.mts.map +1 -0
  130. package/dist/{types-CJsYGpco.d.mts → types-B_CXXnzh.d.mts} +1 -1
  131. package/dist/{types-CJsYGpco.d.mts.map → types-B_CXXnzh.d.mts.map} +1 -1
  132. package/dist/{types-M78DQ1lx.d.mts → types-C-aFbqmA.d.mts} +1 -1
  133. package/dist/{types-M78DQ1lx.d.mts.map → types-C-aFbqmA.d.mts.map} +1 -1
  134. package/dist/types-DiI8NOG_.mjs +16 -0
  135. package/dist/types-DiI8NOG_.mjs.map +1 -0
  136. package/dist/{types-BuBIptGk.d.mts → types-IN5z_S3P.d.mts} +158 -92
  137. package/dist/types-IN5z_S3P.d.mts.map +1 -0
  138. package/dist/{types-BSyXeCFW.d.mts → types-IZSZfEwv.d.mts} +4 -3
  139. package/dist/types-IZSZfEwv.d.mts.map +1 -0
  140. package/dist/{types-CDbKp7ND.mjs → types-K-EkEQCI.mjs} +1 -1
  141. package/dist/{types-CDbKp7ND.mjs.map → types-K-EkEQCI.mjs.map} +1 -1
  142. package/dist/{validate-BfQh_C_y.d.mts → validate-CO3JjFV5.d.mts} +22 -5
  143. package/dist/validate-CO3JjFV5.d.mts.map +1 -0
  144. package/dist/{validate-Baqf0slj.mjs → validate-UK4Ja1uo.mjs} +14 -10
  145. package/dist/validate-UK4Ja1uo.mjs.map +1 -0
  146. package/dist/{validation-BfEI7tNe.mjs → validation-Vc5DQkJa.mjs} +5 -5
  147. package/dist/{validation-BfEI7tNe.mjs.map → validation-Vc5DQkJa.mjs.map} +1 -1
  148. package/dist/version-Bg31I_Ff.mjs +7 -0
  149. package/dist/{version-DoxrVdYf.mjs.map → version-Bg31I_Ff.mjs.map} +1 -1
  150. package/dist/{zod-generator-CC0xNe_K.mjs → zod-generator-CHnJUP2l.mjs} +8 -3
  151. package/dist/zod-generator-CHnJUP2l.mjs.map +1 -0
  152. package/package.json +9 -8
  153. package/src/api/errors.ts +5 -0
  154. package/src/api/handlers/content.ts +20 -0
  155. package/src/api/handlers/dashboard.ts +29 -36
  156. package/src/api/handlers/media-allowlist.ts +40 -0
  157. package/src/api/handlers/media.ts +1 -1
  158. package/src/api/handlers/menus.ts +400 -89
  159. package/src/api/handlers/taxonomies.ts +273 -97
  160. package/src/api/handlers/validate-media-fields.ts +125 -0
  161. package/src/api/schemas/common.ts +7 -0
  162. package/src/api/schemas/media.ts +23 -3
  163. package/src/api/schemas/menus.ts +23 -0
  164. package/src/api/schemas/schema.ts +11 -2
  165. package/src/api/schemas/taxonomies.ts +39 -0
  166. package/src/astro/integration/routes.ts +10 -0
  167. package/src/astro/middleware.ts +46 -11
  168. package/src/astro/routes/api/content/[collection]/[id]/permanent.ts +1 -1
  169. package/src/astro/routes/api/import/wordpress/rewrite-url-helpers.ts +196 -0
  170. package/src/astro/routes/api/import/wordpress/rewrite-urls.ts +9 -177
  171. package/src/astro/routes/api/media/upload-url.ts +10 -4
  172. package/src/astro/routes/api/media.ts +12 -4
  173. package/src/astro/routes/api/menus/[name]/items.ts +16 -6
  174. package/src/astro/routes/api/menus/[name]/reorder.ts +8 -3
  175. package/src/astro/routes/api/menus/[name]/translations.ts +82 -0
  176. package/src/astro/routes/api/menus/[name].ts +19 -10
  177. package/src/astro/routes/api/menus/index.ts +9 -6
  178. package/src/astro/routes/api/taxonomies/[name]/terms/[slug]/translations.ts +89 -0
  179. package/src/astro/routes/api/taxonomies/[name]/terms/[slug].ts +22 -22
  180. package/src/astro/routes/api/taxonomies/[name]/terms/index.ts +11 -14
  181. package/src/astro/routes/api/taxonomies/index.ts +9 -6
  182. package/src/astro/types.ts +5 -1
  183. package/src/auth/rate-limit.ts +3 -3
  184. package/src/cli/commands/bundle-utils.ts +81 -6
  185. package/src/cli/commands/bundle.ts +18 -15
  186. package/src/cli/commands/export-seed.ts +139 -24
  187. package/src/cli/commands/plugin-init.ts +216 -90
  188. package/src/database/instrumentation.ts +22 -8
  189. package/src/database/migrations/016_api_tokens.ts +18 -3
  190. package/src/database/migrations/036_i18n_menus_and_taxonomies.ts +477 -0
  191. package/src/database/migrations/037_credential_algorithm.ts +18 -0
  192. package/src/database/migrations/runner.ts +4 -0
  193. package/src/database/repositories/content.ts +11 -0
  194. package/src/database/repositories/media.ts +40 -10
  195. package/src/database/repositories/taxonomy.ts +193 -89
  196. package/src/database/types.ts +12 -3
  197. package/src/emdash-runtime.ts +16 -3
  198. package/src/fields/file.ts +7 -6
  199. package/src/fields/image.ts +12 -11
  200. package/src/fields/types.ts +3 -0
  201. package/src/i18n/resolve.ts +37 -0
  202. package/src/index.ts +1 -1
  203. package/src/loader.ts +49 -2
  204. package/src/mcp/server.ts +114 -26
  205. package/src/media/mime.ts +75 -0
  206. package/src/menus/index.ts +143 -124
  207. package/src/menus/types.ts +15 -1
  208. package/src/plugins/types.ts +81 -191
  209. package/src/request-cache.ts +6 -2
  210. package/src/request-context.ts +42 -2
  211. package/src/schema/registry.ts +5 -5
  212. package/src/schema/types.ts +3 -2
  213. package/src/schema/zod-generator.ts +12 -2
  214. package/src/seed/apply.ts +157 -54
  215. package/src/seed/types.ts +18 -1
  216. package/src/seed/validate.ts +27 -13
  217. package/src/taxonomies/index.ts +230 -213
  218. package/src/taxonomies/types.ts +10 -0
  219. package/dist/apply-BzltprvY.mjs.map +0 -1
  220. package/dist/content-8lOYF0pr.mjs.map +0 -1
  221. package/dist/index-BFRaVcD6.d.mts.map +0 -1
  222. package/dist/loader-CKLbBnhK.mjs.map +0 -1
  223. package/dist/media-BW32b4gi.mjs.map +0 -1
  224. package/dist/registry-Dw70ChxB.mjs.map +0 -1
  225. package/dist/request-cache-B-bmkipQ.mjs.map +0 -1
  226. package/dist/runner-C7ADox5q.mjs.map +0 -1
  227. package/dist/search-dOGEccMa.mjs.map +0 -1
  228. package/dist/taxonomies-ZlRtD6AG.mjs +0 -315
  229. package/dist/taxonomies-ZlRtD6AG.mjs.map +0 -1
  230. package/dist/types-4fVtCIm0.mjs +0 -68
  231. package/dist/types-4fVtCIm0.mjs.map +0 -1
  232. package/dist/types-BSyXeCFW.d.mts.map +0 -1
  233. package/dist/types-BuBIptGk.d.mts.map +0 -1
  234. package/dist/types-CIOg5AR8.mjs.map +0 -1
  235. package/dist/types-CrtWgIvl.d.mts.map +0 -1
  236. package/dist/validate-Baqf0slj.mjs.map +0 -1
  237. package/dist/validate-BfQh_C_y.d.mts.map +0 -1
  238. package/dist/version-DoxrVdYf.mjs +0 -7
  239. package/dist/zod-generator-CC0xNe_K.mjs.map +0 -1
@@ -32,6 +32,7 @@ import type {
32
32
  SeedWidget,
33
33
  SeedContentEntry,
34
34
  } from "../../seed/types.js";
35
+ import { slugify } from "../../utils/slugify.js";
35
36
 
36
37
  const SETTINGS_PREFIX = "site:";
37
38
 
@@ -101,7 +102,7 @@ export const exportSeedCommand = defineCommand({
101
102
  /**
102
103
  * Export database to seed file format
103
104
  */
104
- async function exportSeed(db: Kysely<Database>, withContent?: string): Promise<SeedFile> {
105
+ export async function exportSeed(db: Kysely<Database>, withContent?: string): Promise<SeedFile> {
105
106
  const seed: SeedFile = {
106
107
  $schema: "https://emdashcms.com/seed.schema.json",
107
108
  version: "1",
@@ -212,41 +213,69 @@ async function exportCollections(db: Kysely<Database>): Promise<SeedCollection[]
212
213
  * Export taxonomy definitions and terms
213
214
  */
214
215
  async function exportTaxonomies(db: Kysely<Database>): Promise<SeedTaxonomy[]> {
215
- // Get taxonomy definitions
216
- const defs = await db.selectFrom("_emdash_taxonomy_defs").selectAll().execute();
216
+ const i18nEnabled = isI18nEnabled();
217
+
218
+ // Mirrors the content export pattern: one entry per (name, locale), stable
219
+ // seed-local id, translations linked via `translationOf` to the anchor's id.
220
+ const defs = await db
221
+ .selectFrom("_emdash_taxonomy_defs")
222
+ .selectAll()
223
+ .orderBy(["name", "locale"])
224
+ .execute();
217
225
 
218
226
  const result: SeedTaxonomy[] = [];
219
227
  const termRepo = new TaxonomyRepository(db);
220
228
 
229
+ // translation_group -> seed-local id of first def we emitted in that group.
230
+ const defGroupToSeedId = new Map<string, string>();
231
+
221
232
  for (const def of defs) {
222
- // Get terms for this taxonomy
223
- const terms = await termRepo.findByName(def.name);
233
+ const defSeedId =
234
+ i18nEnabled && def.locale ? `tax:${def.name}:${def.locale}` : `tax:${def.name}`;
224
235
 
225
- // Build term tree for hierarchical taxonomies
226
- const seedTerms: SeedTaxonomyTerm[] = [];
236
+ // Terms in this def's locale.
237
+ const terms = await termRepo.findByName(def.name, { locale: def.locale });
227
238
 
228
- // First, create a map of id -> slug for parent resolution
239
+ // id -> slug for parent resolution within this locale.
229
240
  const idToSlug = new Map<string, string>();
230
- for (const term of terms) {
231
- idToSlug.set(term.id, term.slug);
232
- }
241
+ for (const term of terms) idToSlug.set(term.id, term.slug);
242
+
243
+ // translation_group -> seed id of the anchor term.
244
+ const termGroupToSeedId = new Map<string, string>();
233
245
 
246
+ const seedTerms: SeedTaxonomyTerm[] = [];
234
247
  for (const term of terms) {
248
+ const termSeedId =
249
+ i18nEnabled && term.locale
250
+ ? `term:${def.name}:${term.slug}:${term.locale}`
251
+ : `term:${def.name}:${term.slug}`;
252
+
235
253
  const seedTerm: SeedTaxonomyTerm = {
254
+ id: termSeedId,
236
255
  slug: term.slug,
237
256
  label: term.label,
238
257
  description: typeof term.data?.description === "string" ? term.data.description : undefined,
239
258
  };
240
259
 
241
- // Resolve parent slug
242
- if (term.parentId) {
243
- seedTerm.parent = idToSlug.get(term.parentId);
260
+ if (term.parentId) seedTerm.parent = idToSlug.get(term.parentId);
261
+
262
+ if (i18nEnabled && term.locale) {
263
+ seedTerm.locale = term.locale;
264
+ if (term.translationGroup) {
265
+ const anchor = termGroupToSeedId.get(term.translationGroup);
266
+ if (anchor) seedTerm.translationOf = anchor;
267
+ else termGroupToSeedId.set(term.translationGroup, termSeedId);
268
+ }
244
269
  }
245
270
 
246
271
  seedTerms.push(seedTerm);
247
272
  }
248
273
 
274
+ // Anchors first so import can resolve `translationOf`.
275
+ seedTerms.sort((a, b) => Number(!!a.translationOf) - Number(!!b.translationOf));
276
+
249
277
  const taxonomy: SeedTaxonomy = {
278
+ id: defSeedId,
250
279
  name: def.name,
251
280
  label: def.label,
252
281
  labelSingular: def.label_singular || undefined,
@@ -254,13 +283,23 @@ async function exportTaxonomies(db: Kysely<Database>): Promise<SeedTaxonomy[]> {
254
283
  collections: def.collections ? JSON.parse(def.collections) : [],
255
284
  };
256
285
 
257
- if (seedTerms.length > 0) {
258
- taxonomy.terms = seedTerms;
286
+ if (i18nEnabled && def.locale) {
287
+ taxonomy.locale = def.locale;
288
+ if (def.translation_group) {
289
+ const anchor = defGroupToSeedId.get(def.translation_group);
290
+ if (anchor) taxonomy.translationOf = anchor;
291
+ else defGroupToSeedId.set(def.translation_group, defSeedId);
292
+ }
259
293
  }
260
294
 
295
+ if (seedTerms.length > 0) taxonomy.terms = seedTerms;
296
+
261
297
  result.push(taxonomy);
262
298
  }
263
299
 
300
+ // Anchors first at def level too.
301
+ result.sort((a, b) => Number(!!a.translationOf) - Number(!!b.translationOf));
302
+
264
303
  return result;
265
304
  }
266
305
 
@@ -268,13 +307,25 @@ async function exportTaxonomies(db: Kysely<Database>): Promise<SeedTaxonomy[]> {
268
307
  * Export menus with their items
269
308
  */
270
309
  async function exportMenus(db: Kysely<Database>): Promise<SeedMenu[]> {
271
- // Get all menus
272
- const menus = await db.selectFrom("_emdash_menus").selectAll().execute();
310
+ const i18nEnabled = isI18nEnabled();
311
+
312
+ const menus = await db
313
+ .selectFrom("_emdash_menus")
314
+ .selectAll()
315
+ .orderBy(["name", "locale"])
316
+ .execute();
273
317
 
274
318
  const result: SeedMenu[] = [];
319
+ // translation_group -> seed-local id of the anchor menu in that group.
320
+ const groupToSeedId = new Map<string, string>();
321
+ // Shared across menus: translated items reference anchor items in sibling menus.
322
+ const itemGroupToSeedId = new Map<string, string>();
323
+ const usedItemSeedIds = new Set<string>();
275
324
 
276
325
  for (const menu of menus) {
277
- // Get menu items
326
+ const seedId =
327
+ i18nEnabled && menu.locale ? `menu:${menu.name}:${menu.locale}` : `menu:${menu.name}`;
328
+
278
329
  const items = await db
279
330
  .selectFrom("_emdash_menu_items")
280
331
  .selectAll()
@@ -282,16 +333,36 @@ async function exportMenus(db: Kysely<Database>): Promise<SeedMenu[]> {
282
333
  .orderBy("sort_order", "asc")
283
334
  .execute();
284
335
 
285
- // Build item tree
286
- const seedItems = buildMenuItemTree(items);
336
+ const seedItems = buildMenuItemTree(items, {
337
+ i18nEnabled,
338
+ menuName: menu.name,
339
+ menuLocale: menu.locale ?? null,
340
+ itemGroupToSeedId,
341
+ usedItemSeedIds,
342
+ });
287
343
 
288
- result.push({
344
+ const seedMenu: SeedMenu = {
345
+ id: seedId,
289
346
  name: menu.name,
290
347
  label: menu.label,
291
348
  items: seedItems,
292
- });
349
+ };
350
+
351
+ if (i18nEnabled && menu.locale) {
352
+ seedMenu.locale = menu.locale;
353
+ if (menu.translation_group) {
354
+ const anchor = groupToSeedId.get(menu.translation_group);
355
+ if (anchor) seedMenu.translationOf = anchor;
356
+ else groupToSeedId.set(menu.translation_group, seedId);
357
+ }
358
+ }
359
+
360
+ result.push(seedMenu);
293
361
  }
294
362
 
363
+ // Anchors first so import can resolve `translationOf`.
364
+ result.sort((a, b) => Number(!!a.translationOf) - Number(!!b.translationOf));
365
+
295
366
  return result;
296
367
  }
297
368
 
@@ -315,7 +386,17 @@ function buildMenuItemTree(
315
386
  target: string | null;
316
387
  title_attr: string | null;
317
388
  css_classes: string | null;
389
+ locale?: string | null;
390
+ translation_group?: string | null;
318
391
  }>,
392
+ i18nCtx: {
393
+ i18nEnabled: boolean;
394
+ menuName: string;
395
+ menuLocale: string | null;
396
+ // translation_group -> seed-local id of the anchor item in that group.
397
+ itemGroupToSeedId: Map<string, string>;
398
+ usedItemSeedIds: Set<string>;
399
+ },
319
400
  ): SeedMenuItem[] {
320
401
  // Build parent -> children map
321
402
  const childMap = new Map<string | null, typeof items>();
@@ -328,10 +409,28 @@ function buildMenuItemTree(
328
409
  childMap.get(parentId)!.push(item);
329
410
  }
330
411
 
412
+ function makeSeedId(item: (typeof items)[number]): string {
413
+ const base = slugify(item.label || "") || item.id;
414
+ const locale = i18nCtx.i18nEnabled ? (item.locale ?? i18nCtx.menuLocale) : null;
415
+ const candidate = locale
416
+ ? `item:${i18nCtx.menuName}:${base}:${locale}`
417
+ : `item:${i18nCtx.menuName}:${base}`;
418
+ if (!i18nCtx.usedItemSeedIds.has(candidate)) {
419
+ i18nCtx.usedItemSeedIds.add(candidate);
420
+ return candidate;
421
+ }
422
+ // Collision fallback: append DB id to disambiguate duplicate labels.
423
+ const fallback = locale
424
+ ? `item:${i18nCtx.menuName}:${base}:${item.id}:${locale}`
425
+ : `item:${i18nCtx.menuName}:${base}:${item.id}`;
426
+ i18nCtx.usedItemSeedIds.add(fallback);
427
+ return fallback;
428
+ }
429
+
331
430
  // Recursively build tree
332
431
  function buildLevel(parentId: string | null): SeedMenuItem[] {
333
432
  const children = childMap.get(parentId) || [];
334
- return children.map((item) => {
433
+ const result = children.map((item) => {
335
434
  const seedItem: SeedMenuItem = {
336
435
  type: item.type,
337
436
  label: item.label || undefined,
@@ -354,6 +453,18 @@ function buildMenuItemTree(
354
453
  seedItem.cssClasses = item.css_classes;
355
454
  }
356
455
 
456
+ if (i18nCtx.i18nEnabled) {
457
+ const itemLocale = item.locale ?? i18nCtx.menuLocale;
458
+ const seedId = makeSeedId(item);
459
+ seedItem.id = seedId;
460
+ if (itemLocale) seedItem.locale = itemLocale;
461
+ if (item.translation_group) {
462
+ const anchor = i18nCtx.itemGroupToSeedId.get(item.translation_group);
463
+ if (anchor && anchor !== seedId) seedItem.translationOf = anchor;
464
+ else if (!anchor) i18nCtx.itemGroupToSeedId.set(item.translation_group, seedId);
465
+ }
466
+ }
467
+
357
468
  // Add children
358
469
  const itemChildren = buildLevel(item.id);
359
470
  if (itemChildren.length > 0) {
@@ -362,6 +473,10 @@ function buildMenuItemTree(
362
473
 
363
474
  return seedItem;
364
475
  });
476
+
477
+ // Sibling order is preserved (maps to sort_order on import). Cross-menu
478
+ // `translationOf` already resolves because exportMenus sorts anchors first.
479
+ return result;
365
480
  }
366
481
 
367
482
  return buildLevel(null);