@marlinjai/mail-contract 0.4.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,
@@ -201,6 +202,8 @@ __export(index_exports, {
201
202
  JsonValue: () => JsonValue,
202
203
  LETTER_RECIPIENT_REFUSAL: () => LETTER_RECIPIENT_REFUSAL,
203
204
  LIST_UNSUBSCRIBE_POST_VALUE: () => LIST_UNSUBSCRIBE_POST_VALUE,
205
+ LOCALE_TAG_PATTERN: () => LOCALE_TAG_PATTERN,
206
+ LocaleTag: () => LocaleTag,
204
207
  MAILING_ACTIONS: () => MAILING_ACTIONS,
205
208
  MAILING_KINDS: () => MAILING_KINDS,
206
209
  MAILING_STATUSES: () => MAILING_STATUSES,
@@ -225,12 +228,15 @@ __export(index_exports, {
225
228
  MAX_MJML_ELEMENTS: () => MAX_MJML_ELEMENTS,
226
229
  MAX_MJML_IMPORT_BYTES: () => MAX_MJML_IMPORT_BYTES,
227
230
  MAX_PAGE_LIMIT: () => MAX_PAGE_LIMIT,
231
+ MAX_PREHEADER_LENGTH: () => MAX_PREHEADER_LENGTH,
228
232
  MAX_RECIPIENTS_PER_BATCH: () => MAX_RECIPIENTS_PER_BATCH,
229
233
  MAX_RE_ENTRY_COOLDOWN_SECONDS: () => MAX_RE_ENTRY_COOLDOWN_SECONDS,
230
234
  MAX_SAVED_SECTIONS: () => MAX_SAVED_SECTIONS,
231
235
  MAX_SPLIT_BRANCHES: () => MAX_SPLIT_BRANCHES,
232
236
  MAX_STEPS: () => MAX_STEPS,
233
237
  MAX_STEP_VISITS: () => MAX_STEP_VISITS,
238
+ MAX_SUBJECT_LENGTH: () => MAX_SUBJECT_LENGTH,
239
+ MAX_TEMPLATE_TRANSLATIONS: () => MAX_TEMPLATE_TRANSLATIONS,
234
240
  MAX_TRIGGERS: () => MAX_TRIGGERS,
235
241
  MEMBER_ROLES: () => MEMBER_ROLES,
236
242
  MERGE_FIELD_PATTERN: () => MERGE_FIELD_PATTERN,
@@ -248,6 +254,9 @@ __export(index_exports, {
248
254
  MailingExportQuery: () => MailingExportQuery,
249
255
  MailingFinishedData: () => MailingFinishedData,
250
256
  MailingKind: () => MailingKind,
257
+ MailingLanguage: () => MailingLanguage,
258
+ MailingLanguageReport: () => MailingLanguageReport,
259
+ MailingLanguageStatus: () => MailingLanguageStatus,
251
260
  MailingListQuery: () => MailingListQuery,
252
261
  MailingMetadata: () => MailingMetadata,
253
262
  MailingRetryFailedRequest: () => MailingRetryFailedRequest,
@@ -348,6 +357,7 @@ __export(index_exports, {
348
357
  SuppressionCreate: () => SuppressionCreate,
349
358
  SuppressionListQuery: () => SuppressionListQuery,
350
359
  SuppressionReason: () => SuppressionReason,
360
+ TEMPLATE_LANGUAGE_STATUSES: () => TEMPLATE_LANGUAGE_STATUSES,
351
361
  TERMINAL_MAILING_STATUSES: () => TERMINAL_MAILING_STATUSES,
352
362
  Tag: () => Tag,
353
363
  TagAssignment: () => TagAssignment,
@@ -363,8 +373,13 @@ __export(index_exports, {
363
373
  TemplateImportPreview: () => TemplateImportPreview,
364
374
  TemplateImportPreviewRequest: () => TemplateImportPreviewRequest,
365
375
  TemplateImportResult: () => TemplateImportResult,
376
+ TemplateLanguageStatus: () => TemplateLanguageStatus,
366
377
  TemplateListQuery: () => TemplateListQuery,
378
+ TemplatePreheader: () => TemplatePreheader,
379
+ TemplateSubject: () => TemplateSubject,
367
380
  TemplateSummary: () => TemplateSummary,
381
+ TemplateTranslation: () => TemplateTranslation,
382
+ TemplateTranslations: () => TemplateTranslations,
368
383
  TemplateUpdate: () => TemplateUpdate,
369
384
  TemplateUpdatedData: () => TemplateUpdatedData,
370
385
  TemplateVersion: () => TemplateVersion,
@@ -417,14 +432,22 @@ __export(index_exports, {
417
432
  WorkspaceSettings: () => WorkspaceSettings,
418
433
  WorkspaceUpdate: () => WorkspaceUpdate,
419
434
  acceptsIdempotencyKey: () => acceptsIdempotencyKey,
435
+ addLanguageDraft: () => addLanguageDraft,
420
436
  alwaysWaits: () => alwaysWaits,
421
437
  automationFilterFamily: () => automationFilterFamily,
422
438
  automationRoutes: () => automationRoutes,
423
439
  billingRoutes: () => billingRoutes,
424
440
  buildPath: () => buildPath,
441
+ buildTemplatePatch: () => buildTemplatePatch,
425
442
  canTransition: () => canTransition,
443
+ canonicalLocale: () => canonicalLocale,
426
444
  cycles: () => cycles,
427
445
  defaultSettings: () => defaultSettings,
446
+ draftDocument: () => draftDocument,
447
+ draftsDirty: () => draftsDirty,
448
+ draftsFromTemplate: () => draftsFromTemplate,
449
+ draftsPatch: () => draftsPatch,
450
+ editLanguageDraft: () => editLanguageDraft,
428
451
  emptyFlow: () => emptyFlow,
429
452
  errorBody: () => errorBody,
430
453
  exportFilename: () => exportFilename,
@@ -440,6 +463,10 @@ __export(index_exports, {
440
463
  isDayEntirelyPast: () => isDayEntirelyPast,
441
464
  isPublishable: () => isPublishable,
442
465
  isRetryableError: () => isRetryableError,
466
+ keepLanguageDocument: () => keepLanguageDocument,
467
+ languageName: () => languageName,
468
+ languageTabs: () => languageTabs,
469
+ matchLocale: () => matchLocale,
443
470
  matchRoute: () => matchRoute,
444
471
  missingRequiredMergeFields: () => missingRequiredMergeFields,
445
472
  nowSeconds: () => nowSeconds,
@@ -448,12 +475,19 @@ __export(index_exports, {
448
475
  parseExportWarningsHeader: () => parseExportWarningsHeader,
449
476
  parseUsageWarningHeader: () => parseUsageWarningHeader,
450
477
  platformRoutes: () => platformRoutes,
478
+ primaryLanguage: () => primaryLanguage,
451
479
  reachable: () => reachable,
480
+ readyWithoutSubject: () => readyWithoutSubject,
481
+ rebaseDrafts: () => rebaseDrafts,
482
+ removeLanguageDraft: () => removeLanguageDraft,
483
+ restorePatch: () => restorePatch,
452
484
  routes: () => routes,
453
485
  sendingRoutes: () => sendingRoutes,
486
+ setLanguageStatus: () => setLanguageStatus,
454
487
  signWebhook: () => signWebhook,
455
488
  statusForError: () => statusForError,
456
489
  stepTargets: () => stepTargets,
490
+ storedTemplateLine: () => storedTemplateLine,
457
491
  stronglyConnected: () => stronglyConnected,
458
492
  templateRoutes: () => templateRoutes,
459
493
  testGroupSize: () => testGroupSize,
@@ -489,6 +523,29 @@ function page(item) {
489
523
  }
490
524
  var IdParams = import_zod.z.object({ id: Id });
491
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
+ }
492
549
 
493
550
  // src/errors.ts
494
551
  var import_zod2 = require("zod");
@@ -604,32 +661,71 @@ var TemplateDocument = import_zod3.z.object({
604
661
  sections: import_zod3.z.array(import_zod3.z.record(import_zod3.z.unknown()))
605
662
  }).passthrough();
606
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`);
607
678
  var Template = import_zod3.z.object({
608
679
  id: Id,
609
680
  name: import_zod3.z.string().min(1).max(200),
610
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(),
611
692
  document: TemplateDocument,
612
- /** 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. */
613
699
  version: import_zod3.z.number().int().min(1),
614
700
  thumbnail_url: import_zod3.z.string().url().nullable(),
615
701
  archived_at: Timestamp.nullable(),
616
702
  created_at: Timestamp,
617
703
  updated_at: Timestamp
618
704
  });
619
- 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
+ });
620
708
  var TemplateCreate = import_zod3.z.object({
621
709
  name: import_zod3.z.string().min(1).max(200),
622
710
  description: import_zod3.z.string().max(2e3).optional(),
623
- document: TemplateDocument
711
+ locale: LocaleTag.optional(),
712
+ subject: TemplateSubject.nullable().optional(),
713
+ preheader: TemplatePreheader.nullable().optional(),
714
+ document: TemplateDocument,
715
+ translations: TemplateTranslations.optional()
624
716
  });
625
717
  var TemplateUpdate = import_zod3.z.object({
626
718
  base_version: import_zod3.z.number().int().min(1),
627
719
  name: import_zod3.z.string().min(1).max(200).optional(),
628
720
  description: import_zod3.z.string().max(2e3).nullable().optional(),
721
+ locale: LocaleTag.optional(),
722
+ subject: TemplateSubject.nullable().optional(),
723
+ preheader: TemplatePreheader.nullable().optional(),
629
724
  document: TemplateDocument.optional(),
725
+ translations: import_zod3.z.record(LocaleTag, TemplateTranslation.nullable()).optional(),
630
726
  archived: import_zod3.z.boolean().optional()
631
727
  }).refine(
632
- (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,
633
729
  "at least one field besides base_version"
634
730
  );
635
731
  var SavedSectionBody = import_zod3.z.record(import_zod3.z.unknown());
@@ -659,7 +755,11 @@ var TemplateListQuery = PageQuery.extend({
659
755
  var TemplateVersion = import_zod3.z.object({
660
756
  template_id: Id,
661
757
  version: import_zod3.z.number().int().min(1),
758
+ locale: LocaleTag,
759
+ subject: TemplateSubject.nullable(),
760
+ preheader: TemplatePreheader.nullable(),
662
761
  document: TemplateDocument,
762
+ translations: TemplateTranslations,
663
763
  created_by: import_zod3.z.string().nullable(),
664
764
  created_at: Timestamp
665
765
  });
@@ -680,7 +780,8 @@ var CompileResult = import_zod3.z.object({
680
780
  errors: import_zod3.z.array(CompileMessage)
681
781
  });
682
782
  var TemplateCompileRequest = import_zod3.z.object({
683
- version: import_zod3.z.number().int().min(1).optional()
783
+ version: import_zod3.z.number().int().min(1).optional(),
784
+ locale: LocaleTag.optional()
684
785
  });
685
786
  var CompileRequest = import_zod3.z.object({
686
787
  document: TemplateDocument
@@ -772,7 +873,8 @@ var EXPORT_FORMATS = ["mjml", "html"];
772
873
  var ExportFormat = import_zod3.z.enum(EXPORT_FORMATS);
773
874
  var TemplateExportQuery = import_zod3.z.object({
774
875
  format: ExportFormat,
775
- 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()
776
878
  });
777
879
  var MailingExportQuery = import_zod3.z.object({ format: ExportFormat });
778
880
  var EXPORT_CONTENT_TYPES = {
@@ -903,9 +1005,29 @@ var Mailing = import_zod5.z.object({
903
1005
  name: import_zod5.z.string().max(200).nullable(),
904
1006
  subject: import_zod5.z.string().min(1).max(998),
905
1007
  preheader: import_zod5.z.string().max(500).nullable(),
906
- /** 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
+ */
907
1014
  template_id: Id.nullable(),
908
1015
  document: TemplateDocument,
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(),
909
1031
  /**
910
1032
  * The topic the mailing is sent under. Null for a letter (a one-to-one
911
1033
  * mailing with at most one recipient, which belongs to no topic and whose
@@ -936,6 +1058,8 @@ var MailingContent = {
936
1058
  name: import_zod5.z.string().max(200).optional(),
937
1059
  subject: import_zod5.z.string().min(1).max(998),
938
1060
  preheader: import_zod5.z.string().max(500).optional(),
1061
+ /** See `Mailing.send_locale`. */
1062
+ send_locale: LocaleTag.nullable().optional(),
939
1063
  /**
940
1064
  * Omitted or null makes the mailing a letter: one recipient at most, sent to
941
1065
  * anybody not blocked on every topic, with an unsubscribe link that offers
@@ -948,11 +1072,15 @@ var MailingContent = {
948
1072
  };
949
1073
  var MailingCreate = import_zod5.z.object({
950
1074
  ...MailingContent,
1075
+ subject: MailingContent.subject.optional(),
951
1076
  document: TemplateDocument.optional(),
952
1077
  template_id: Id.optional()
953
1078
  }).refine((v) => v.document === void 0 !== (v.template_id === void 0), {
954
1079
  message: "exactly one of document or template_id",
955
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"]
956
1084
  });
957
1085
  var MailingUpdate = import_zod5.z.object({
958
1086
  name: import_zod5.z.string().max(200).nullable().optional(),
@@ -960,6 +1088,7 @@ var MailingUpdate = import_zod5.z.object({
960
1088
  preheader: import_zod5.z.string().max(500).nullable().optional(),
961
1089
  /** Null turns a draft into a letter, which it can only be with at most one recipient. */
962
1090
  topic: Slug.nullable().optional(),
1091
+ send_locale: LocaleTag.nullable().optional(),
963
1092
  provider_id: Id.optional(),
964
1093
  metadata: MailingMetadata.optional(),
965
1094
  document: TemplateDocument.optional()
@@ -982,7 +1111,8 @@ var MailingRetryFailedRequest = import_zod5.z.object({
982
1111
  var MailingActionRequest = import_zod5.z.object({}).strict();
983
1112
  var MailingTestRequest = import_zod5.z.object({
984
1113
  to: Email,
985
- merge: Properties.optional()
1114
+ merge: Properties.optional(),
1115
+ locale: LocaleTag.optional()
986
1116
  });
987
1117
  var MailingTestResult = import_zod5.z.object({
988
1118
  message_id: Id,
@@ -1049,9 +1179,27 @@ var MessageSummary = import_zod5.z.object({
1049
1179
  error: import_zod5.z.string().nullable(),
1050
1180
  is_test: import_zod5.z.boolean(),
1051
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(),
1052
1184
  created_at: Timestamp
1053
1185
  });
1054
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
+ });
1055
1203
  var MessageListQuery = PageQuery.extend({
1056
1204
  mailing_id: Id.optional(),
1057
1205
  outcome: MessageOutcome.optional()
@@ -1561,15 +1709,22 @@ var Unchosen = import_zod7.z.literal("");
1561
1709
  var DraftSlug = import_zod7.z.union([Slug, Unchosen]);
1562
1710
  var DraftPropertyKey = import_zod7.z.union([ContactPropertyKey, Unchosen]);
1563
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();
1564
1717
  var EmailStep = import_zod7.z.object({
1565
1718
  kind: import_zod7.z.literal("email"),
1566
1719
  ...label,
1567
1720
  ...next,
1568
1721
  template_id: Id.nullish(),
1569
1722
  document: TemplateDocument.nullish(),
1570
- /** 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. */
1571
1724
  subject: import_zod7.z.string().max(998),
1572
- 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()
1573
1728
  }).strict();
1574
1729
  var DelayStep = import_zod7.z.object({
1575
1730
  kind: import_zod7.z.literal("delay"),
@@ -1875,7 +2030,7 @@ function validateAutomation(flow, settings, ctx) {
1875
2030
  if (!step.template_id && !step.document) {
1876
2031
  add({ code: "empty_email", step_id: id, path: [...path, "template_id"], message: "This email has neither a template nor content." });
1877
2032
  }
1878
- if (step.subject.trim() === "") {
2033
+ if (step.subject.trim() === "" && !(step.template_id && known.templates_with_subject?.has(step.template_id))) {
1879
2034
  add({ code: "not_chosen", step_id: id, path: [...path, "subject"], message: "This email has no subject line." });
1880
2035
  }
1881
2036
  if (step.template_id) checkRef(known.templates, step.template_id, "template", [...path, "template_id"], add, id);
@@ -2360,7 +2515,7 @@ var AutomationReport = import_zod7.z.object({
2360
2515
  var AutomationExitRunRequest = import_zod7.z.object({ reason: import_zod7.z.string().max(200).optional() }).strict();
2361
2516
  var AutomationRunParams = import_zod7.z.object({ id: Id, run_id: Id });
2362
2517
  var AutomationStepParams = import_zod7.z.object({ id: Id, step_id: StepId });
2363
- 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();
2364
2519
  var AutomationValidateRequest = import_zod7.z.object({ flow: AutomationFlow.optional(), settings: AutomationSettings.partial().optional() }).strict();
2365
2520
  var EventCreate = import_zod7.z.object({
2366
2521
  name: EventName,
@@ -2429,10 +2584,19 @@ var MergeDefaults = import_zod8.z.record(MergeFieldName, MergeDefaultValue).refi
2429
2584
  message: `a reserved merge field (${Object.keys(RESERVED_MERGE_FIELDS).join(", ")}) cannot have a workspace default`
2430
2585
  });
2431
2586
  var WorkspaceSettings = import_zod8.z.object({
2432
- /** Default language of the hosted unsubscribe page (BCP 47 tag, e.g. "de"). */
2433
- default_locale: import_zod8.z.string().min(2).max(35),
2434
- /** Languages the hosted pages are offered in. Includes `default_locale`. */
2435
- 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"),
2436
2600
  /** Open and click tracking. Off by default, opt-in per workspace. */
2437
2601
  tracking_enabled: import_zod8.z.boolean(),
2438
2602
  /** Where images, stylesheets and fonts in a mail may load from. `any` by default. */
@@ -2684,6 +2848,200 @@ var InviteAccepted = import_zod9.z.object({
2684
2848
  });
2685
2849
  var INVITE_REFUSAL_REASONS = ["email_mismatch", "inviter_lacks_role", "expired", "revoked", "accepted"];
2686
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
+
2687
3045
  // src/providers.ts
2688
3046
  var import_zod10 = require("zod");
2689
3047
  var PROVIDER_KINDS = ["smtp", "resend"];
@@ -4278,6 +4636,15 @@ var platformRoutes = {
4278
4636
  access: "read",
4279
4637
  phase: "S4"
4280
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
+ },
4281
4648
  "signupForms.list": {
4282
4649
  method: "GET",
4283
4650
  path: "/v1/signup-forms",
@@ -4876,6 +5243,7 @@ function matchRoute(method, pathname) {
4876
5243
  EXPORT_WARNING_COUNT_HEADER,
4877
5244
  Email,
4878
5245
  EmailStep,
5246
+ EmailStepLanguage,
4879
5247
  ErrorBody,
4880
5248
  ErrorCodeSchema,
4881
5249
  Event,
@@ -4927,6 +5295,8 @@ function matchRoute(method, pathname) {
4927
5295
  JsonValue,
4928
5296
  LETTER_RECIPIENT_REFUSAL,
4929
5297
  LIST_UNSUBSCRIBE_POST_VALUE,
5298
+ LOCALE_TAG_PATTERN,
5299
+ LocaleTag,
4930
5300
  MAILING_ACTIONS,
4931
5301
  MAILING_KINDS,
4932
5302
  MAILING_STATUSES,
@@ -4951,12 +5321,15 @@ function matchRoute(method, pathname) {
4951
5321
  MAX_MJML_ELEMENTS,
4952
5322
  MAX_MJML_IMPORT_BYTES,
4953
5323
  MAX_PAGE_LIMIT,
5324
+ MAX_PREHEADER_LENGTH,
4954
5325
  MAX_RECIPIENTS_PER_BATCH,
4955
5326
  MAX_RE_ENTRY_COOLDOWN_SECONDS,
4956
5327
  MAX_SAVED_SECTIONS,
4957
5328
  MAX_SPLIT_BRANCHES,
4958
5329
  MAX_STEPS,
4959
5330
  MAX_STEP_VISITS,
5331
+ MAX_SUBJECT_LENGTH,
5332
+ MAX_TEMPLATE_TRANSLATIONS,
4960
5333
  MAX_TRIGGERS,
4961
5334
  MEMBER_ROLES,
4962
5335
  MERGE_FIELD_PATTERN,
@@ -4974,6 +5347,9 @@ function matchRoute(method, pathname) {
4974
5347
  MailingExportQuery,
4975
5348
  MailingFinishedData,
4976
5349
  MailingKind,
5350
+ MailingLanguage,
5351
+ MailingLanguageReport,
5352
+ MailingLanguageStatus,
4977
5353
  MailingListQuery,
4978
5354
  MailingMetadata,
4979
5355
  MailingRetryFailedRequest,
@@ -5074,6 +5450,7 @@ function matchRoute(method, pathname) {
5074
5450
  SuppressionCreate,
5075
5451
  SuppressionListQuery,
5076
5452
  SuppressionReason,
5453
+ TEMPLATE_LANGUAGE_STATUSES,
5077
5454
  TERMINAL_MAILING_STATUSES,
5078
5455
  Tag,
5079
5456
  TagAssignment,
@@ -5089,8 +5466,13 @@ function matchRoute(method, pathname) {
5089
5466
  TemplateImportPreview,
5090
5467
  TemplateImportPreviewRequest,
5091
5468
  TemplateImportResult,
5469
+ TemplateLanguageStatus,
5092
5470
  TemplateListQuery,
5471
+ TemplatePreheader,
5472
+ TemplateSubject,
5093
5473
  TemplateSummary,
5474
+ TemplateTranslation,
5475
+ TemplateTranslations,
5094
5476
  TemplateUpdate,
5095
5477
  TemplateUpdatedData,
5096
5478
  TemplateVersion,
@@ -5143,14 +5525,22 @@ function matchRoute(method, pathname) {
5143
5525
  WorkspaceSettings,
5144
5526
  WorkspaceUpdate,
5145
5527
  acceptsIdempotencyKey,
5528
+ addLanguageDraft,
5146
5529
  alwaysWaits,
5147
5530
  automationFilterFamily,
5148
5531
  automationRoutes,
5149
5532
  billingRoutes,
5150
5533
  buildPath,
5534
+ buildTemplatePatch,
5151
5535
  canTransition,
5536
+ canonicalLocale,
5152
5537
  cycles,
5153
5538
  defaultSettings,
5539
+ draftDocument,
5540
+ draftsDirty,
5541
+ draftsFromTemplate,
5542
+ draftsPatch,
5543
+ editLanguageDraft,
5154
5544
  emptyFlow,
5155
5545
  errorBody,
5156
5546
  exportFilename,
@@ -5166,6 +5556,10 @@ function matchRoute(method, pathname) {
5166
5556
  isDayEntirelyPast,
5167
5557
  isPublishable,
5168
5558
  isRetryableError,
5559
+ keepLanguageDocument,
5560
+ languageName,
5561
+ languageTabs,
5562
+ matchLocale,
5169
5563
  matchRoute,
5170
5564
  missingRequiredMergeFields,
5171
5565
  nowSeconds,
@@ -5174,12 +5568,19 @@ function matchRoute(method, pathname) {
5174
5568
  parseExportWarningsHeader,
5175
5569
  parseUsageWarningHeader,
5176
5570
  platformRoutes,
5571
+ primaryLanguage,
5177
5572
  reachable,
5573
+ readyWithoutSubject,
5574
+ rebaseDrafts,
5575
+ removeLanguageDraft,
5576
+ restorePatch,
5178
5577
  routes,
5179
5578
  sendingRoutes,
5579
+ setLanguageStatus,
5180
5580
  signWebhook,
5181
5581
  statusForError,
5182
5582
  stepTargets,
5583
+ storedTemplateLine,
5183
5584
  stronglyConnected,
5184
5585
  templateRoutes,
5185
5586
  testGroupSize,