@marlinjai/mail-contract 0.3.0 → 0.5.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.
package/dist/index.js CHANGED
@@ -150,6 +150,7 @@ __export(index_exports, {
150
150
  EXPORT_WARNING_COUNT_HEADER: () => EXPORT_WARNING_COUNT_HEADER,
151
151
  Email: () => Email,
152
152
  EmailStep: () => EmailStep,
153
+ EmailStepLanguage: () => EmailStepLanguage,
153
154
  ErrorBody: () => ErrorBody,
154
155
  ErrorCodeSchema: () => ErrorCodeSchema,
155
156
  Event: () => Event,
@@ -199,7 +200,10 @@ __export(index_exports, {
199
200
  InviteListQuery: () => InviteListQuery,
200
201
  InviteStatus: () => InviteStatus,
201
202
  JsonValue: () => JsonValue,
203
+ LETTER_RECIPIENT_REFUSAL: () => LETTER_RECIPIENT_REFUSAL,
202
204
  LIST_UNSUBSCRIBE_POST_VALUE: () => LIST_UNSUBSCRIBE_POST_VALUE,
205
+ LOCALE_TAG_PATTERN: () => LOCALE_TAG_PATTERN,
206
+ LocaleTag: () => LocaleTag,
203
207
  MAILING_ACTIONS: () => MAILING_ACTIONS,
204
208
  MAILING_KINDS: () => MAILING_KINDS,
205
209
  MAILING_STATUSES: () => MAILING_STATUSES,
@@ -217,18 +221,22 @@ __export(index_exports, {
217
221
  MAX_IMPORT_URL_LENGTH: () => MAX_IMPORT_URL_LENGTH,
218
222
  MAX_INVITE_TTL_DAYS: () => MAX_INVITE_TTL_DAYS,
219
223
  MAX_IN_LIST: () => MAX_IN_LIST,
224
+ MAX_LETTER_RECIPIENTS: () => MAX_LETTER_RECIPIENTS,
220
225
  MAX_MERGE_DEFAULTS: () => MAX_MERGE_DEFAULTS,
221
226
  MAX_MERGE_DEFAULT_LENGTH: () => MAX_MERGE_DEFAULT_LENGTH,
222
227
  MAX_MJML_DEPTH: () => MAX_MJML_DEPTH,
223
228
  MAX_MJML_ELEMENTS: () => MAX_MJML_ELEMENTS,
224
229
  MAX_MJML_IMPORT_BYTES: () => MAX_MJML_IMPORT_BYTES,
225
230
  MAX_PAGE_LIMIT: () => MAX_PAGE_LIMIT,
231
+ MAX_PREHEADER_LENGTH: () => MAX_PREHEADER_LENGTH,
226
232
  MAX_RECIPIENTS_PER_BATCH: () => MAX_RECIPIENTS_PER_BATCH,
227
233
  MAX_RE_ENTRY_COOLDOWN_SECONDS: () => MAX_RE_ENTRY_COOLDOWN_SECONDS,
228
234
  MAX_SAVED_SECTIONS: () => MAX_SAVED_SECTIONS,
229
235
  MAX_SPLIT_BRANCHES: () => MAX_SPLIT_BRANCHES,
230
236
  MAX_STEPS: () => MAX_STEPS,
231
237
  MAX_STEP_VISITS: () => MAX_STEP_VISITS,
238
+ MAX_SUBJECT_LENGTH: () => MAX_SUBJECT_LENGTH,
239
+ MAX_TEMPLATE_TRANSLATIONS: () => MAX_TEMPLATE_TRANSLATIONS,
232
240
  MAX_TRIGGERS: () => MAX_TRIGGERS,
233
241
  MEMBER_ROLES: () => MEMBER_ROLES,
234
242
  MERGE_FIELD_PATTERN: () => MERGE_FIELD_PATTERN,
@@ -246,6 +254,9 @@ __export(index_exports, {
246
254
  MailingExportQuery: () => MailingExportQuery,
247
255
  MailingFinishedData: () => MailingFinishedData,
248
256
  MailingKind: () => MailingKind,
257
+ MailingLanguage: () => MailingLanguage,
258
+ MailingLanguageReport: () => MailingLanguageReport,
259
+ MailingLanguageStatus: () => MailingLanguageStatus,
249
260
  MailingListQuery: () => MailingListQuery,
250
261
  MailingMetadata: () => MailingMetadata,
251
262
  MailingRetryFailedRequest: () => MailingRetryFailedRequest,
@@ -346,6 +357,7 @@ __export(index_exports, {
346
357
  SuppressionCreate: () => SuppressionCreate,
347
358
  SuppressionListQuery: () => SuppressionListQuery,
348
359
  SuppressionReason: () => SuppressionReason,
360
+ TEMPLATE_LANGUAGE_STATUSES: () => TEMPLATE_LANGUAGE_STATUSES,
349
361
  TERMINAL_MAILING_STATUSES: () => TERMINAL_MAILING_STATUSES,
350
362
  Tag: () => Tag,
351
363
  TagAssignment: () => TagAssignment,
@@ -361,8 +373,13 @@ __export(index_exports, {
361
373
  TemplateImportPreview: () => TemplateImportPreview,
362
374
  TemplateImportPreviewRequest: () => TemplateImportPreviewRequest,
363
375
  TemplateImportResult: () => TemplateImportResult,
376
+ TemplateLanguageStatus: () => TemplateLanguageStatus,
364
377
  TemplateListQuery: () => TemplateListQuery,
378
+ TemplatePreheader: () => TemplatePreheader,
379
+ TemplateSubject: () => TemplateSubject,
365
380
  TemplateSummary: () => TemplateSummary,
381
+ TemplateTranslation: () => TemplateTranslation,
382
+ TemplateTranslations: () => TemplateTranslations,
366
383
  TemplateUpdate: () => TemplateUpdate,
367
384
  TemplateUpdatedData: () => TemplateUpdatedData,
368
385
  TemplateVersion: () => TemplateVersion,
@@ -415,14 +432,22 @@ __export(index_exports, {
415
432
  WorkspaceSettings: () => WorkspaceSettings,
416
433
  WorkspaceUpdate: () => WorkspaceUpdate,
417
434
  acceptsIdempotencyKey: () => acceptsIdempotencyKey,
435
+ addLanguageDraft: () => addLanguageDraft,
418
436
  alwaysWaits: () => alwaysWaits,
419
437
  automationFilterFamily: () => automationFilterFamily,
420
438
  automationRoutes: () => automationRoutes,
421
439
  billingRoutes: () => billingRoutes,
422
440
  buildPath: () => buildPath,
441
+ buildTemplatePatch: () => buildTemplatePatch,
423
442
  canTransition: () => canTransition,
443
+ canonicalLocale: () => canonicalLocale,
424
444
  cycles: () => cycles,
425
445
  defaultSettings: () => defaultSettings,
446
+ draftDocument: () => draftDocument,
447
+ draftsDirty: () => draftsDirty,
448
+ draftsFromTemplate: () => draftsFromTemplate,
449
+ draftsPatch: () => draftsPatch,
450
+ editLanguageDraft: () => editLanguageDraft,
426
451
  emptyFlow: () => emptyFlow,
427
452
  errorBody: () => errorBody,
428
453
  exportFilename: () => exportFilename,
@@ -438,6 +463,10 @@ __export(index_exports, {
438
463
  isDayEntirelyPast: () => isDayEntirelyPast,
439
464
  isPublishable: () => isPublishable,
440
465
  isRetryableError: () => isRetryableError,
466
+ keepLanguageDocument: () => keepLanguageDocument,
467
+ languageName: () => languageName,
468
+ languageTabs: () => languageTabs,
469
+ matchLocale: () => matchLocale,
441
470
  matchRoute: () => matchRoute,
442
471
  missingRequiredMergeFields: () => missingRequiredMergeFields,
443
472
  nowSeconds: () => nowSeconds,
@@ -446,12 +475,19 @@ __export(index_exports, {
446
475
  parseExportWarningsHeader: () => parseExportWarningsHeader,
447
476
  parseUsageWarningHeader: () => parseUsageWarningHeader,
448
477
  platformRoutes: () => platformRoutes,
478
+ primaryLanguage: () => primaryLanguage,
449
479
  reachable: () => reachable,
480
+ readyWithoutSubject: () => readyWithoutSubject,
481
+ rebaseDrafts: () => rebaseDrafts,
482
+ removeLanguageDraft: () => removeLanguageDraft,
483
+ restorePatch: () => restorePatch,
450
484
  routes: () => routes,
451
485
  sendingRoutes: () => sendingRoutes,
486
+ setLanguageStatus: () => setLanguageStatus,
452
487
  signWebhook: () => signWebhook,
453
488
  statusForError: () => statusForError,
454
489
  stepTargets: () => stepTargets,
490
+ storedTemplateLine: () => storedTemplateLine,
455
491
  stronglyConnected: () => stronglyConnected,
456
492
  templateRoutes: () => templateRoutes,
457
493
  testGroupSize: () => testGroupSize,
@@ -487,6 +523,29 @@ function page(item) {
487
523
  }
488
524
  var IdParams = import_zod.z.object({ id: Id });
489
525
  var Ok = import_zod.z.object({ ok: import_zod.z.literal(true) });
526
+ var LOCALE_TAG_PATTERN = /^[A-Za-z]{2,3}(?:-[A-Za-z]{4})?(?:-(?:[A-Za-z]{2}|[0-9]{3}))?$/;
527
+ var LocaleTag = import_zod.z.string().min(2).max(35).regex(LOCALE_TAG_PATTERN, "a language tag such as en, fr or pt-BR");
528
+ function canonicalLocale(tag) {
529
+ if (typeof tag !== "string") return null;
530
+ const trimmed = tag.trim().replace(/_/g, "-");
531
+ if (!LOCALE_TAG_PATTERN.test(trimmed)) return null;
532
+ return trimmed.split("-").map((part, i) => {
533
+ if (i === 0) return part.toLowerCase();
534
+ if (part.length === 4) return part[0].toUpperCase() + part.slice(1).toLowerCase();
535
+ return part.toUpperCase();
536
+ }).join("-");
537
+ }
538
+ function primaryLanguage(tag) {
539
+ return canonicalLocale(tag)?.split("-")[0] ?? null;
540
+ }
541
+ function matchLocale(wanted, available) {
542
+ const canonical = canonicalLocale(wanted);
543
+ if (!canonical) return null;
544
+ const exact = available.find((a) => canonicalLocale(a) === canonical);
545
+ if (exact !== void 0) return exact;
546
+ const primary = canonical.split("-")[0];
547
+ return available.find((a) => primaryLanguage(a) === primary) ?? null;
548
+ }
490
549
 
491
550
  // src/errors.ts
492
551
  var import_zod2 = require("zod");
@@ -602,32 +661,71 @@ var TemplateDocument = import_zod3.z.object({
602
661
  sections: import_zod3.z.array(import_zod3.z.record(import_zod3.z.unknown()))
603
662
  }).passthrough();
604
663
  var MAX_DOCUMENT_BYTES = 1e6;
664
+ var MAX_SUBJECT_LENGTH = 998;
665
+ var MAX_PREHEADER_LENGTH = 500;
666
+ var TemplateSubject = import_zod3.z.string().max(MAX_SUBJECT_LENGTH).regex(/^[^\x00-\x1f\x7f]*$/, "one line, without control characters");
667
+ var TemplatePreheader = import_zod3.z.string().max(MAX_PREHEADER_LENGTH).regex(/^[^\x00-\x1f\x7f]*$/, "one line, without control characters");
668
+ var TEMPLATE_LANGUAGE_STATUSES = ["draft", "ready"];
669
+ var TemplateLanguageStatus = import_zod3.z.enum(TEMPLATE_LANGUAGE_STATUSES);
670
+ var TemplateTranslation = import_zod3.z.object({
671
+ subject: TemplateSubject.nullable(),
672
+ preheader: TemplatePreheader.nullable(),
673
+ document: TemplateDocument,
674
+ status: TemplateLanguageStatus
675
+ });
676
+ var MAX_TEMPLATE_TRANSLATIONS = 19;
677
+ var TemplateTranslations = import_zod3.z.record(LocaleTag, TemplateTranslation).refine((t) => Object.keys(t).length <= MAX_TEMPLATE_TRANSLATIONS, `at most ${MAX_TEMPLATE_TRANSLATIONS} language versions`);
605
678
  var Template = import_zod3.z.object({
606
679
  id: Id,
607
680
  name: import_zod3.z.string().min(1).max(200),
608
681
  description: import_zod3.z.string().max(2e3).nullable(),
682
+ /**
683
+ * The main language: what `subject`, `preheader` and `document` are written
684
+ * in, and what every recipient without a ready version of their own gets.
685
+ * Set from the workspace's `default_locale` when the template is created
686
+ * unless given, and never changed by a later change of that setting.
687
+ */
688
+ locale: LocaleTag,
689
+ /** The main language's subject line; null when the mailing that uses the template gives one. */
690
+ subject: TemplateSubject.nullable(),
691
+ preheader: TemplatePreheader.nullable(),
609
692
  document: TemplateDocument,
610
- /** Increments on every saved change. */
693
+ /**
694
+ * The other languages. A version whose language the workspace no longer
695
+ * lists is kept, but no send uses it until the language is listed again.
696
+ */
697
+ translations: TemplateTranslations,
698
+ /** Increments on every saved change, of any language. */
611
699
  version: import_zod3.z.number().int().min(1),
612
700
  thumbnail_url: import_zod3.z.string().url().nullable(),
613
701
  archived_at: Timestamp.nullable(),
614
702
  created_at: Timestamp,
615
703
  updated_at: Timestamp
616
704
  });
617
- var TemplateSummary = Template.omit({ document: true });
705
+ var TemplateSummary = Template.omit({ document: true, translations: true }).extend({
706
+ languages: import_zod3.z.record(LocaleTag, TemplateLanguageStatus)
707
+ });
618
708
  var TemplateCreate = import_zod3.z.object({
619
709
  name: import_zod3.z.string().min(1).max(200),
620
710
  description: import_zod3.z.string().max(2e3).optional(),
621
- document: TemplateDocument
711
+ locale: LocaleTag.optional(),
712
+ subject: TemplateSubject.nullable().optional(),
713
+ preheader: TemplatePreheader.nullable().optional(),
714
+ document: TemplateDocument,
715
+ translations: TemplateTranslations.optional()
622
716
  });
623
717
  var TemplateUpdate = import_zod3.z.object({
624
718
  base_version: import_zod3.z.number().int().min(1),
625
719
  name: import_zod3.z.string().min(1).max(200).optional(),
626
720
  description: import_zod3.z.string().max(2e3).nullable().optional(),
721
+ locale: LocaleTag.optional(),
722
+ subject: TemplateSubject.nullable().optional(),
723
+ preheader: TemplatePreheader.nullable().optional(),
627
724
  document: TemplateDocument.optional(),
725
+ translations: import_zod3.z.record(LocaleTag, TemplateTranslation.nullable()).optional(),
628
726
  archived: import_zod3.z.boolean().optional()
629
727
  }).refine(
630
- (v) => v.name !== void 0 || v.description !== void 0 || v.document !== void 0 || v.archived !== void 0,
728
+ (v) => v.name !== void 0 || v.description !== void 0 || v.locale !== void 0 || v.subject !== void 0 || v.preheader !== void 0 || v.document !== void 0 || v.translations !== void 0 || v.archived !== void 0,
631
729
  "at least one field besides base_version"
632
730
  );
633
731
  var SavedSectionBody = import_zod3.z.record(import_zod3.z.unknown());
@@ -657,7 +755,11 @@ var TemplateListQuery = PageQuery.extend({
657
755
  var TemplateVersion = import_zod3.z.object({
658
756
  template_id: Id,
659
757
  version: import_zod3.z.number().int().min(1),
758
+ locale: LocaleTag,
759
+ subject: TemplateSubject.nullable(),
760
+ preheader: TemplatePreheader.nullable(),
660
761
  document: TemplateDocument,
762
+ translations: TemplateTranslations,
661
763
  created_by: import_zod3.z.string().nullable(),
662
764
  created_at: Timestamp
663
765
  });
@@ -678,7 +780,8 @@ var CompileResult = import_zod3.z.object({
678
780
  errors: import_zod3.z.array(CompileMessage)
679
781
  });
680
782
  var TemplateCompileRequest = import_zod3.z.object({
681
- version: import_zod3.z.number().int().min(1).optional()
783
+ version: import_zod3.z.number().int().min(1).optional(),
784
+ locale: LocaleTag.optional()
682
785
  });
683
786
  var CompileRequest = import_zod3.z.object({
684
787
  document: TemplateDocument
@@ -770,7 +873,8 @@ var EXPORT_FORMATS = ["mjml", "html"];
770
873
  var ExportFormat = import_zod3.z.enum(EXPORT_FORMATS);
771
874
  var TemplateExportQuery = import_zod3.z.object({
772
875
  format: ExportFormat,
773
- version: import_zod3.z.coerce.number().int().min(1).optional()
876
+ version: import_zod3.z.coerce.number().int().min(1).optional(),
877
+ locale: LocaleTag.optional()
774
878
  });
775
879
  var MailingExportQuery = import_zod3.z.object({ format: ExportFormat });
776
880
  var EXPORT_CONTENT_TYPES = {
@@ -901,10 +1005,35 @@ var Mailing = import_zod5.z.object({
901
1005
  name: import_zod5.z.string().max(200).nullable(),
902
1006
  subject: import_zod5.z.string().min(1).max(998),
903
1007
  preheader: import_zod5.z.string().max(500).nullable(),
904
- /** The template the document was taken from, if any. The snapshot is what is sent. */
1008
+ /**
1009
+ * The template the document was taken from, if any. The snapshot is what is
1010
+ * sent in the main language; the template's other language versions are
1011
+ * taken, the ready ones, when the mailing starts (see "Languages" in the
1012
+ * contract's documentation).
1013
+ */
905
1014
  template_id: Id.nullable(),
906
1015
  document: TemplateDocument,
907
- topic: Slug,
1016
+ /**
1017
+ * The main language: what `subject`, `preheader` and `document` are written
1018
+ * in, and what every recipient without a ready version in their own language
1019
+ * gets. The template's main language, or the workspace's default for a
1020
+ * mailing made from a document.
1021
+ */
1022
+ locale: LocaleTag,
1023
+ /**
1024
+ * The language every recipient is treated as reading, or null to read each
1025
+ * recipient's own (their `merge.locale`, else their contact's language). A
1026
+ * letter sets it to the language its one reader wrote in; a broadcast can use
1027
+ * it to send one language version to everyone. It is matched like anyone's
1028
+ * language: without a ready version in it, the main language goes out.
1029
+ */
1030
+ send_locale: LocaleTag.nullable(),
1031
+ /**
1032
+ * The topic the mailing is sent under. Null for a letter (a one-to-one
1033
+ * mailing with at most one recipient, which belongs to no topic and whose
1034
+ * unsubscribe link offers every topic) and for an automation's notification.
1035
+ */
1036
+ topic: Slug.nullable(),
908
1037
  provider_id: Id,
909
1038
  status: MailingStatus,
910
1039
  counts: MailingCounts,
@@ -929,23 +1058,37 @@ var MailingContent = {
929
1058
  name: import_zod5.z.string().max(200).optional(),
930
1059
  subject: import_zod5.z.string().min(1).max(998),
931
1060
  preheader: import_zod5.z.string().max(500).optional(),
932
- topic: Slug,
1061
+ /** See `Mailing.send_locale`. */
1062
+ send_locale: LocaleTag.nullable().optional(),
1063
+ /**
1064
+ * Omitted or null makes the mailing a letter: one recipient at most, sent to
1065
+ * anybody not blocked on every topic, with an unsubscribe link that offers
1066
+ * every topic and no List-Unsubscribe headers. See "Letters" in the
1067
+ * contract's documentation.
1068
+ */
1069
+ topic: Slug.nullable().optional(),
933
1070
  provider_id: Id,
934
1071
  metadata: MailingMetadata.optional()
935
1072
  };
936
1073
  var MailingCreate = import_zod5.z.object({
937
1074
  ...MailingContent,
1075
+ subject: MailingContent.subject.optional(),
938
1076
  document: TemplateDocument.optional(),
939
1077
  template_id: Id.optional()
940
1078
  }).refine((v) => v.document === void 0 !== (v.template_id === void 0), {
941
1079
  message: "exactly one of document or template_id",
942
1080
  path: ["document"]
1081
+ }).refine((v) => v.template_id !== void 0 || v.subject !== void 0, {
1082
+ message: "a mailing made from a document needs a subject",
1083
+ path: ["subject"]
943
1084
  });
944
1085
  var MailingUpdate = import_zod5.z.object({
945
1086
  name: import_zod5.z.string().max(200).nullable().optional(),
946
1087
  subject: MailingContent.subject.optional(),
947
1088
  preheader: import_zod5.z.string().max(500).nullable().optional(),
948
- topic: Slug.optional(),
1089
+ /** Null turns a draft into a letter, which it can only be with at most one recipient. */
1090
+ topic: Slug.nullable().optional(),
1091
+ send_locale: LocaleTag.nullable().optional(),
949
1092
  provider_id: Id.optional(),
950
1093
  metadata: MailingMetadata.optional(),
951
1094
  document: TemplateDocument.optional()
@@ -968,7 +1111,8 @@ var MailingRetryFailedRequest = import_zod5.z.object({
968
1111
  var MailingActionRequest = import_zod5.z.object({}).strict();
969
1112
  var MailingTestRequest = import_zod5.z.object({
970
1113
  to: Email,
971
- merge: Properties.optional()
1114
+ merge: Properties.optional(),
1115
+ locale: LocaleTag.optional()
972
1116
  });
973
1117
  var MailingTestResult = import_zod5.z.object({
974
1118
  message_id: Id,
@@ -993,6 +1137,8 @@ var Recipient = import_zod5.z.object({
993
1137
  updated_at: Timestamp
994
1138
  });
995
1139
  var MAX_RECIPIENTS_PER_BATCH = 1e3;
1140
+ var MAX_LETTER_RECIPIENTS = 1;
1141
+ var LETTER_RECIPIENT_REFUSAL = "letter_has_one_recipient";
996
1142
  var RecipientInput = import_zod5.z.object({
997
1143
  contact_id: Id.optional(),
998
1144
  external_id: import_zod5.z.string().min(1).max(255).optional(),
@@ -1033,9 +1179,27 @@ var MessageSummary = import_zod5.z.object({
1033
1179
  error: import_zod5.z.string().nullable(),
1034
1180
  is_test: import_zod5.z.boolean(),
1035
1181
  recipient_count: import_zod5.z.number().int().min(1),
1182
+ /** The language version the message was sent in; null for messages archived before languages existed. */
1183
+ locale: LocaleTag.nullable(),
1036
1184
  created_at: Timestamp
1037
1185
  });
1038
1186
  var Message = MessageSummary.extend({ html: import_zod5.z.string() });
1187
+ var MailingLanguageStatus = import_zod5.z.enum(["main", "ready", "draft", "not_a_workspace_language"]);
1188
+ var MailingLanguage = import_zod5.z.object({
1189
+ locale: LocaleTag,
1190
+ status: MailingLanguageStatus,
1191
+ /** Queued recipients who will get this version. */
1192
+ planned: import_zod5.z.number().int().min(0),
1193
+ /** Messages already sent in this language (tests excluded). */
1194
+ sent: import_zod5.z.number().int().min(0)
1195
+ });
1196
+ var MailingLanguageReport = import_zod5.z.object({
1197
+ main_locale: LocaleTag,
1198
+ send_locale: LocaleTag.nullable(),
1199
+ languages: import_zod5.z.array(MailingLanguage),
1200
+ fallback: import_zod5.z.array(import_zod5.z.object({ wanted: import_zod5.z.string(), planned: import_zod5.z.number().int().min(1) })),
1201
+ no_language: import_zod5.z.number().int().min(0)
1202
+ });
1039
1203
  var MessageListQuery = PageQuery.extend({
1040
1204
  mailing_id: Id.optional(),
1041
1205
  outcome: MessageOutcome.optional()
@@ -1545,15 +1709,22 @@ var Unchosen = import_zod7.z.literal("");
1545
1709
  var DraftSlug = import_zod7.z.union([Slug, Unchosen]);
1546
1710
  var DraftPropertyKey = import_zod7.z.union([ContactPropertyKey, Unchosen]);
1547
1711
  var label = { label: import_zod7.z.string().max(120).nullish() };
1712
+ var EmailStepLanguage = import_zod7.z.object({
1713
+ subject: import_zod7.z.string().min(1).max(998),
1714
+ preheader: import_zod7.z.string().max(500).nullable(),
1715
+ document: TemplateDocument
1716
+ }).strict();
1548
1717
  var EmailStep = import_zod7.z.object({
1549
1718
  kind: import_zod7.z.literal("email"),
1550
1719
  ...label,
1551
1720
  ...next,
1552
1721
  template_id: Id.nullish(),
1553
1722
  document: TemplateDocument.nullish(),
1554
- /** Empty while the step is being written; `not_chosen` refuses to publish it. */
1723
+ /** Empty while the step is being written, or when its template has a subject; else `not_chosen` refuses to publish it. */
1555
1724
  subject: import_zod7.z.string().max(998),
1556
- preheader: import_zod7.z.string().max(500).nullish()
1725
+ preheader: import_zod7.z.string().max(500).nullish(),
1726
+ locale: LocaleTag.nullish(),
1727
+ languages: import_zod7.z.record(LocaleTag, EmailStepLanguage).nullish()
1557
1728
  }).strict();
1558
1729
  var DelayStep = import_zod7.z.object({
1559
1730
  kind: import_zod7.z.literal("delay"),
@@ -1859,7 +2030,7 @@ function validateAutomation(flow, settings, ctx) {
1859
2030
  if (!step.template_id && !step.document) {
1860
2031
  add({ code: "empty_email", step_id: id, path: [...path, "template_id"], message: "This email has neither a template nor content." });
1861
2032
  }
1862
- if (step.subject.trim() === "") {
2033
+ if (step.subject.trim() === "" && !(step.template_id && known.templates_with_subject?.has(step.template_id))) {
1863
2034
  add({ code: "not_chosen", step_id: id, path: [...path, "subject"], message: "This email has no subject line." });
1864
2035
  }
1865
2036
  if (step.template_id) checkRef(known.templates, step.template_id, "template", [...path, "template_id"], add, id);
@@ -2344,7 +2515,7 @@ var AutomationReport = import_zod7.z.object({
2344
2515
  var AutomationExitRunRequest = import_zod7.z.object({ reason: import_zod7.z.string().max(200).optional() }).strict();
2345
2516
  var AutomationRunParams = import_zod7.z.object({ id: Id, run_id: Id });
2346
2517
  var AutomationStepParams = import_zod7.z.object({ id: Id, step_id: StepId });
2347
- var AutomationTestEmailRequest = import_zod7.z.object({ to: Email, merge: Properties.optional() }).strict();
2518
+ var AutomationTestEmailRequest = import_zod7.z.object({ to: Email, merge: Properties.optional(), locale: LocaleTag.optional() }).strict();
2348
2519
  var AutomationValidateRequest = import_zod7.z.object({ flow: AutomationFlow.optional(), settings: AutomationSettings.partial().optional() }).strict();
2349
2520
  var EventCreate = import_zod7.z.object({
2350
2521
  name: EventName,
@@ -2413,10 +2584,19 @@ var MergeDefaults = import_zod8.z.record(MergeFieldName, MergeDefaultValue).refi
2413
2584
  message: `a reserved merge field (${Object.keys(RESERVED_MERGE_FIELDS).join(", ")}) cannot have a workspace default`
2414
2585
  });
2415
2586
  var WorkspaceSettings = import_zod8.z.object({
2416
- /** Default language of the hosted unsubscribe page (BCP 47 tag, e.g. "de"). */
2417
- default_locale: import_zod8.z.string().min(2).max(35),
2418
- /** Languages the hosted pages are offered in. Includes `default_locale`. */
2419
- locales: import_zod8.z.array(import_zod8.z.string().min(2).max(35)).min(1).max(20),
2587
+ /**
2588
+ * The workspace's main language (a {@link LocaleTag}, e.g. "de"): the main
2589
+ * language of a new template, and of the hosted pages when nothing else
2590
+ * decides.
2591
+ */
2592
+ default_locale: LocaleTag,
2593
+ /**
2594
+ * The languages the workspace writes in, including `default_locale`, none
2595
+ * twice (compared without regard to casing). A template can have a version in
2596
+ * each; the hosted pages are offered in those of them they support (English,
2597
+ * German, French, Italian, Spanish). The service stores canonical casing.
2598
+ */
2599
+ locales: import_zod8.z.array(LocaleTag).min(1).max(20).refine((l) => new Set(l.map((t) => t.toLowerCase())).size === l.length, "a language appears twice"),
2420
2600
  /** Open and click tracking. Off by default, opt-in per workspace. */
2421
2601
  tracking_enabled: import_zod8.z.boolean(),
2422
2602
  /** Where images, stylesheets and fonts in a mail may load from. `any` by default. */
@@ -2530,6 +2710,7 @@ var AUDIT_ACTIONS = [
2530
2710
  "provider.anomaly_cleared",
2531
2711
  "topic.created",
2532
2712
  "topic.updated",
2713
+ "topic.deleted",
2533
2714
  "template.created",
2534
2715
  "template.updated",
2535
2716
  "template.deleted",
@@ -2667,6 +2848,200 @@ var InviteAccepted = import_zod9.z.object({
2667
2848
  });
2668
2849
  var INVITE_REFUSAL_REASONS = ["email_mismatch", "inviter_lacks_role", "expired", "revoked", "accepted"];
2669
2850
 
2851
+ // src/template-languages.ts
2852
+ function languageTabs(mainLocale, versions, workspaceLocales) {
2853
+ const main = canonicalLocale(mainLocale) ?? mainLocale;
2854
+ const statuses = /* @__PURE__ */ new Map();
2855
+ for (const [tag, status] of Object.entries(versions)) {
2856
+ const canonical = canonicalLocale(tag) ?? tag;
2857
+ if (canonical !== main) statuses.set(canonical, status);
2858
+ }
2859
+ const tabs = [{ locale: main, kind: "main", status: "ready" }];
2860
+ const seen = /* @__PURE__ */ new Set([main]);
2861
+ for (const tag of workspaceLocales) {
2862
+ const canonical = canonicalLocale(tag) ?? tag;
2863
+ if (seen.has(canonical)) continue;
2864
+ seen.add(canonical);
2865
+ tabs.push({ locale: canonical, kind: "translation", status: statuses.get(canonical) ?? "missing" });
2866
+ }
2867
+ const kept = [...statuses.keys()].filter((tag) => !seen.has(tag)).sort();
2868
+ for (const tag of kept) tabs.push({ locale: tag, kind: "kept", status: statuses.get(tag) });
2869
+ return tabs;
2870
+ }
2871
+ function languageName(tag) {
2872
+ const canonical = canonicalLocale(tag) ?? tag;
2873
+ try {
2874
+ const names = new Intl.DisplayNames(["en"], { type: "language", fallback: "none" });
2875
+ const name = names.of(canonical);
2876
+ return name && name !== canonical ? name : canonical;
2877
+ } catch {
2878
+ return canonical;
2879
+ }
2880
+ }
2881
+ function storedTemplateLine(value) {
2882
+ if (value === null || value === void 0) return null;
2883
+ const trimmed = value.trim();
2884
+ return trimmed === "" ? null : trimmed;
2885
+ }
2886
+ function buildTemplatePatch(args) {
2887
+ const { loaded, main, documentChanged, translations, changed, removed } = args;
2888
+ const out = {};
2889
+ const subject = storedTemplateLine(main.subject);
2890
+ if (subject !== storedTemplateLine(loaded.subject)) out.subject = subject;
2891
+ const preheader = storedTemplateLine(main.preheader);
2892
+ if (preheader !== storedTemplateLine(loaded.preheader)) out.preheader = preheader;
2893
+ if (documentChanged) out.document = main.document;
2894
+ const patch = {};
2895
+ for (const tag of changed) {
2896
+ const version = translations[tag];
2897
+ if (version && !removed.has(tag)) {
2898
+ patch[tag] = { subject: storedTemplateLine(version.subject), preheader: storedTemplateLine(version.preheader), document: version.document, status: version.status };
2899
+ }
2900
+ }
2901
+ for (const tag of removed) {
2902
+ if (Object.prototype.hasOwnProperty.call(loaded.translations, tag)) patch[tag] = null;
2903
+ }
2904
+ if (Object.keys(patch).length > 0) out.translations = patch;
2905
+ return out;
2906
+ }
2907
+ function restorePatch(old, current, workspaceLocales) {
2908
+ const oldMain = canonicalLocale(old.locale) ?? old.locale;
2909
+ const listed = workspaceLocales ? new Set(workspaceLocales.map((t) => canonicalLocale(t) ?? t)) : null;
2910
+ const patch = {};
2911
+ const skipped = [];
2912
+ const untouched = /* @__PURE__ */ new Set();
2913
+ for (const [tag, version] of Object.entries(old.translations)) {
2914
+ const canonical = canonicalLocale(tag) ?? tag;
2915
+ if (canonical === oldMain) continue;
2916
+ if (listed && !listed.has(canonical)) {
2917
+ skipped.push(canonical);
2918
+ untouched.add(canonical);
2919
+ continue;
2920
+ }
2921
+ patch[canonical] = version;
2922
+ }
2923
+ for (const tag of Object.keys(current.translations)) {
2924
+ const canonical = canonicalLocale(tag) ?? tag;
2925
+ if (!(canonical in patch) && !untouched.has(canonical)) patch[canonical] = null;
2926
+ if (canonical === oldMain) patch[canonical] = null;
2927
+ }
2928
+ const update = {
2929
+ ...oldMain !== (canonicalLocale(current.locale) ?? current.locale) ? { locale: oldMain } : {},
2930
+ subject: old.subject,
2931
+ preheader: old.preheader,
2932
+ document: old.document,
2933
+ ...Object.keys(patch).length > 0 ? { translations: patch } : {}
2934
+ };
2935
+ return { update, skipped: skipped.sort() };
2936
+ }
2937
+ function draftsFromTemplate(template) {
2938
+ const translations = {};
2939
+ for (const [tag, version] of Object.entries(template.translations)) translations[canonicalLocale(tag) ?? tag] = version;
2940
+ return {
2941
+ main: { subject: template.subject ?? "", preheader: template.preheader ?? "", document: template.document },
2942
+ translations,
2943
+ mainDocumentChanged: false,
2944
+ changed: [],
2945
+ removed: []
2946
+ };
2947
+ }
2948
+ var withChanged = (list2, tag) => list2.includes(tag) ? [...list2] : [...list2, tag];
2949
+ var without = (list2, tag) => list2.filter((t) => t !== tag);
2950
+ function draftDocument(drafts, locale) {
2951
+ if (locale === null) return drafts.main.document;
2952
+ return drafts.translations[locale]?.document;
2953
+ }
2954
+ function addLanguageDraft(drafts, locale) {
2955
+ const tag = canonicalLocale(locale) ?? locale;
2956
+ if (drafts.translations[tag]) return drafts;
2957
+ return {
2958
+ ...drafts,
2959
+ translations: {
2960
+ ...drafts.translations,
2961
+ [tag]: {
2962
+ subject: drafts.main.subject,
2963
+ preheader: drafts.main.preheader,
2964
+ document: structuredClone(drafts.main.document),
2965
+ status: "draft"
2966
+ }
2967
+ },
2968
+ changed: withChanged(drafts.changed, tag),
2969
+ removed: without(drafts.removed, tag)
2970
+ };
2971
+ }
2972
+ function removeLanguageDraft(drafts, locale) {
2973
+ const tag = canonicalLocale(locale) ?? locale;
2974
+ const { [tag]: _gone, ...rest } = drafts.translations;
2975
+ return { ...drafts, translations: rest, changed: without(drafts.changed, tag), removed: withChanged(drafts.removed, tag) };
2976
+ }
2977
+ function editLanguageDraft(drafts, locale, edit) {
2978
+ if (locale === null) {
2979
+ return {
2980
+ ...drafts,
2981
+ main: { ...drafts.main, ...edit },
2982
+ mainDocumentChanged: drafts.mainDocumentChanged || edit.document !== void 0
2983
+ };
2984
+ }
2985
+ const version = drafts.translations[locale];
2986
+ if (!version) return drafts;
2987
+ const next2 = { ...version, ...edit };
2988
+ return { ...drafts, translations: { ...drafts.translations, [locale]: next2 }, changed: withChanged(drafts.changed, locale) };
2989
+ }
2990
+ function keepLanguageDocument(drafts, locale, document) {
2991
+ if (locale === null) return { ...drafts, main: { ...drafts.main, document } };
2992
+ const version = drafts.translations[locale];
2993
+ if (!version) return drafts;
2994
+ return { ...drafts, translations: { ...drafts.translations, [locale]: { ...version, document } } };
2995
+ }
2996
+ function setLanguageStatus(drafts, locale, status) {
2997
+ const version = drafts.translations[locale];
2998
+ if (!version || version.status === status) return drafts;
2999
+ return { ...drafts, translations: { ...drafts.translations, [locale]: { ...version, status } }, changed: withChanged(drafts.changed, locale) };
3000
+ }
3001
+ function draftsPatch(drafts, loaded) {
3002
+ return buildTemplatePatch({
3003
+ loaded,
3004
+ main: drafts.main,
3005
+ documentChanged: drafts.mainDocumentChanged,
3006
+ translations: drafts.translations,
3007
+ changed: new Set(drafts.changed),
3008
+ removed: new Set(drafts.removed)
3009
+ });
3010
+ }
3011
+ function draftsDirty(drafts, loaded) {
3012
+ return Object.keys(draftsPatch(drafts, loaded)).length > 0;
3013
+ }
3014
+ function readyWithoutSubject(drafts) {
3015
+ return Object.entries(drafts.translations).filter(([tag, v]) => drafts.changed.includes(tag) && v.status === "ready" && storedTemplateLine(v.subject) === null).map(([tag]) => tag);
3016
+ }
3017
+ function rebaseDrafts(sent, now, saved) {
3018
+ const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
3019
+ let out = draftsFromTemplate(saved);
3020
+ if (now.main.subject !== sent.main.subject) out = editLanguageDraft(out, null, { subject: now.main.subject });
3021
+ if (now.main.preheader !== sent.main.preheader) out = editLanguageDraft(out, null, { preheader: now.main.preheader });
3022
+ out = same(now.main.document, sent.main.document) ? keepLanguageDocument(out, null, now.main.document) : editLanguageDraft(out, null, { document: now.main.document });
3023
+ const tags = /* @__PURE__ */ new Set([...Object.keys(sent.translations), ...Object.keys(now.translations)]);
3024
+ for (const tag of tags) {
3025
+ const before = sent.translations[tag];
3026
+ const after = now.translations[tag];
3027
+ if (same(before, after)) {
3028
+ if (after) out = keepLanguageDocument(out, tag, after.document);
3029
+ continue;
3030
+ }
3031
+ if (!after) {
3032
+ out = removeLanguageDraft(out, tag);
3033
+ continue;
3034
+ }
3035
+ out = {
3036
+ ...out,
3037
+ translations: { ...out.translations, [tag]: after },
3038
+ changed: withChanged(out.changed, tag),
3039
+ removed: without(out.removed, tag)
3040
+ };
3041
+ }
3042
+ return out;
3043
+ }
3044
+
2670
3045
  // src/providers.ts
2671
3046
  var import_zod10 = require("zod");
2672
3047
  var PROVIDER_KINDS = ["smtp", "resend"];
@@ -3777,6 +4152,16 @@ var sendingRoutes = {
3777
4152
  access: "admin",
3778
4153
  phase: "S2"
3779
4154
  },
4155
+ /** Refused with `conflict` while a mailing or an automation step still uses the topic; subscriptions and suppressions on it go with it. */
4156
+ "topics.delete": {
4157
+ method: "DELETE",
4158
+ path: "/v1/topics/:id",
4159
+ params: IdParams,
4160
+ response: Ok,
4161
+ status: 200,
4162
+ access: "admin",
4163
+ phase: "S2"
4164
+ },
3780
4165
  "contacts.upsert": {
3781
4166
  method: "POST",
3782
4167
  path: "/v1/contacts",
@@ -4251,6 +4636,15 @@ var platformRoutes = {
4251
4636
  access: "read",
4252
4637
  phase: "S4"
4253
4638
  },
4639
+ "mailings.languages": {
4640
+ method: "GET",
4641
+ path: "/v1/mailings/:id/languages",
4642
+ params: IdParams,
4643
+ response: MailingLanguageReport,
4644
+ status: 200,
4645
+ access: "read",
4646
+ phase: "S4"
4647
+ },
4254
4648
  "signupForms.list": {
4255
4649
  method: "GET",
4256
4650
  path: "/v1/signup-forms",
@@ -4849,6 +5243,7 @@ function matchRoute(method, pathname) {
4849
5243
  EXPORT_WARNING_COUNT_HEADER,
4850
5244
  Email,
4851
5245
  EmailStep,
5246
+ EmailStepLanguage,
4852
5247
  ErrorBody,
4853
5248
  ErrorCodeSchema,
4854
5249
  Event,
@@ -4898,7 +5293,10 @@ function matchRoute(method, pathname) {
4898
5293
  InviteListQuery,
4899
5294
  InviteStatus,
4900
5295
  JsonValue,
5296
+ LETTER_RECIPIENT_REFUSAL,
4901
5297
  LIST_UNSUBSCRIBE_POST_VALUE,
5298
+ LOCALE_TAG_PATTERN,
5299
+ LocaleTag,
4902
5300
  MAILING_ACTIONS,
4903
5301
  MAILING_KINDS,
4904
5302
  MAILING_STATUSES,
@@ -4916,18 +5314,22 @@ function matchRoute(method, pathname) {
4916
5314
  MAX_IMPORT_URL_LENGTH,
4917
5315
  MAX_INVITE_TTL_DAYS,
4918
5316
  MAX_IN_LIST,
5317
+ MAX_LETTER_RECIPIENTS,
4919
5318
  MAX_MERGE_DEFAULTS,
4920
5319
  MAX_MERGE_DEFAULT_LENGTH,
4921
5320
  MAX_MJML_DEPTH,
4922
5321
  MAX_MJML_ELEMENTS,
4923
5322
  MAX_MJML_IMPORT_BYTES,
4924
5323
  MAX_PAGE_LIMIT,
5324
+ MAX_PREHEADER_LENGTH,
4925
5325
  MAX_RECIPIENTS_PER_BATCH,
4926
5326
  MAX_RE_ENTRY_COOLDOWN_SECONDS,
4927
5327
  MAX_SAVED_SECTIONS,
4928
5328
  MAX_SPLIT_BRANCHES,
4929
5329
  MAX_STEPS,
4930
5330
  MAX_STEP_VISITS,
5331
+ MAX_SUBJECT_LENGTH,
5332
+ MAX_TEMPLATE_TRANSLATIONS,
4931
5333
  MAX_TRIGGERS,
4932
5334
  MEMBER_ROLES,
4933
5335
  MERGE_FIELD_PATTERN,
@@ -4945,6 +5347,9 @@ function matchRoute(method, pathname) {
4945
5347
  MailingExportQuery,
4946
5348
  MailingFinishedData,
4947
5349
  MailingKind,
5350
+ MailingLanguage,
5351
+ MailingLanguageReport,
5352
+ MailingLanguageStatus,
4948
5353
  MailingListQuery,
4949
5354
  MailingMetadata,
4950
5355
  MailingRetryFailedRequest,
@@ -5045,6 +5450,7 @@ function matchRoute(method, pathname) {
5045
5450
  SuppressionCreate,
5046
5451
  SuppressionListQuery,
5047
5452
  SuppressionReason,
5453
+ TEMPLATE_LANGUAGE_STATUSES,
5048
5454
  TERMINAL_MAILING_STATUSES,
5049
5455
  Tag,
5050
5456
  TagAssignment,
@@ -5060,8 +5466,13 @@ function matchRoute(method, pathname) {
5060
5466
  TemplateImportPreview,
5061
5467
  TemplateImportPreviewRequest,
5062
5468
  TemplateImportResult,
5469
+ TemplateLanguageStatus,
5063
5470
  TemplateListQuery,
5471
+ TemplatePreheader,
5472
+ TemplateSubject,
5064
5473
  TemplateSummary,
5474
+ TemplateTranslation,
5475
+ TemplateTranslations,
5065
5476
  TemplateUpdate,
5066
5477
  TemplateUpdatedData,
5067
5478
  TemplateVersion,
@@ -5114,14 +5525,22 @@ function matchRoute(method, pathname) {
5114
5525
  WorkspaceSettings,
5115
5526
  WorkspaceUpdate,
5116
5527
  acceptsIdempotencyKey,
5528
+ addLanguageDraft,
5117
5529
  alwaysWaits,
5118
5530
  automationFilterFamily,
5119
5531
  automationRoutes,
5120
5532
  billingRoutes,
5121
5533
  buildPath,
5534
+ buildTemplatePatch,
5122
5535
  canTransition,
5536
+ canonicalLocale,
5123
5537
  cycles,
5124
5538
  defaultSettings,
5539
+ draftDocument,
5540
+ draftsDirty,
5541
+ draftsFromTemplate,
5542
+ draftsPatch,
5543
+ editLanguageDraft,
5125
5544
  emptyFlow,
5126
5545
  errorBody,
5127
5546
  exportFilename,
@@ -5137,6 +5556,10 @@ function matchRoute(method, pathname) {
5137
5556
  isDayEntirelyPast,
5138
5557
  isPublishable,
5139
5558
  isRetryableError,
5559
+ keepLanguageDocument,
5560
+ languageName,
5561
+ languageTabs,
5562
+ matchLocale,
5140
5563
  matchRoute,
5141
5564
  missingRequiredMergeFields,
5142
5565
  nowSeconds,
@@ -5145,12 +5568,19 @@ function matchRoute(method, pathname) {
5145
5568
  parseExportWarningsHeader,
5146
5569
  parseUsageWarningHeader,
5147
5570
  platformRoutes,
5571
+ primaryLanguage,
5148
5572
  reachable,
5573
+ readyWithoutSubject,
5574
+ rebaseDrafts,
5575
+ removeLanguageDraft,
5576
+ restorePatch,
5149
5577
  routes,
5150
5578
  sendingRoutes,
5579
+ setLanguageStatus,
5151
5580
  signWebhook,
5152
5581
  statusForError,
5153
5582
  stepTargets,
5583
+ storedTemplateLine,
5154
5584
  stronglyConnected,
5155
5585
  templateRoutes,
5156
5586
  testGroupSize,