@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.mjs CHANGED
@@ -22,6 +22,29 @@ function page(item) {
22
22
  }
23
23
  var IdParams = z.object({ id: Id });
24
24
  var Ok = z.object({ ok: z.literal(true) });
25
+ var LOCALE_TAG_PATTERN = /^[A-Za-z]{2,3}(?:-[A-Za-z]{4})?(?:-(?:[A-Za-z]{2}|[0-9]{3}))?$/;
26
+ var LocaleTag = z.string().min(2).max(35).regex(LOCALE_TAG_PATTERN, "a language tag such as en, fr or pt-BR");
27
+ function canonicalLocale(tag) {
28
+ if (typeof tag !== "string") return null;
29
+ const trimmed = tag.trim().replace(/_/g, "-");
30
+ if (!LOCALE_TAG_PATTERN.test(trimmed)) return null;
31
+ return trimmed.split("-").map((part, i) => {
32
+ if (i === 0) return part.toLowerCase();
33
+ if (part.length === 4) return part[0].toUpperCase() + part.slice(1).toLowerCase();
34
+ return part.toUpperCase();
35
+ }).join("-");
36
+ }
37
+ function primaryLanguage(tag) {
38
+ return canonicalLocale(tag)?.split("-")[0] ?? null;
39
+ }
40
+ function matchLocale(wanted, available) {
41
+ const canonical = canonicalLocale(wanted);
42
+ if (!canonical) return null;
43
+ const exact = available.find((a) => canonicalLocale(a) === canonical);
44
+ if (exact !== void 0) return exact;
45
+ const primary = canonical.split("-")[0];
46
+ return available.find((a) => primaryLanguage(a) === primary) ?? null;
47
+ }
25
48
 
26
49
  // src/errors.ts
27
50
  import { z as z2 } from "zod";
@@ -137,32 +160,71 @@ var TemplateDocument = z3.object({
137
160
  sections: z3.array(z3.record(z3.unknown()))
138
161
  }).passthrough();
139
162
  var MAX_DOCUMENT_BYTES = 1e6;
163
+ var MAX_SUBJECT_LENGTH = 998;
164
+ var MAX_PREHEADER_LENGTH = 500;
165
+ var TemplateSubject = z3.string().max(MAX_SUBJECT_LENGTH).regex(/^[^\x00-\x1f\x7f]*$/, "one line, without control characters");
166
+ var TemplatePreheader = z3.string().max(MAX_PREHEADER_LENGTH).regex(/^[^\x00-\x1f\x7f]*$/, "one line, without control characters");
167
+ var TEMPLATE_LANGUAGE_STATUSES = ["draft", "ready"];
168
+ var TemplateLanguageStatus = z3.enum(TEMPLATE_LANGUAGE_STATUSES);
169
+ var TemplateTranslation = z3.object({
170
+ subject: TemplateSubject.nullable(),
171
+ preheader: TemplatePreheader.nullable(),
172
+ document: TemplateDocument,
173
+ status: TemplateLanguageStatus
174
+ });
175
+ var MAX_TEMPLATE_TRANSLATIONS = 19;
176
+ var TemplateTranslations = z3.record(LocaleTag, TemplateTranslation).refine((t) => Object.keys(t).length <= MAX_TEMPLATE_TRANSLATIONS, `at most ${MAX_TEMPLATE_TRANSLATIONS} language versions`);
140
177
  var Template = z3.object({
141
178
  id: Id,
142
179
  name: z3.string().min(1).max(200),
143
180
  description: z3.string().max(2e3).nullable(),
181
+ /**
182
+ * The main language: what `subject`, `preheader` and `document` are written
183
+ * in, and what every recipient without a ready version of their own gets.
184
+ * Set from the workspace's `default_locale` when the template is created
185
+ * unless given, and never changed by a later change of that setting.
186
+ */
187
+ locale: LocaleTag,
188
+ /** The main language's subject line; null when the mailing that uses the template gives one. */
189
+ subject: TemplateSubject.nullable(),
190
+ preheader: TemplatePreheader.nullable(),
144
191
  document: TemplateDocument,
145
- /** Increments on every saved change. */
192
+ /**
193
+ * The other languages. A version whose language the workspace no longer
194
+ * lists is kept, but no send uses it until the language is listed again.
195
+ */
196
+ translations: TemplateTranslations,
197
+ /** Increments on every saved change, of any language. */
146
198
  version: z3.number().int().min(1),
147
199
  thumbnail_url: z3.string().url().nullable(),
148
200
  archived_at: Timestamp.nullable(),
149
201
  created_at: Timestamp,
150
202
  updated_at: Timestamp
151
203
  });
152
- var TemplateSummary = Template.omit({ document: true });
204
+ var TemplateSummary = Template.omit({ document: true, translations: true }).extend({
205
+ languages: z3.record(LocaleTag, TemplateLanguageStatus)
206
+ });
153
207
  var TemplateCreate = z3.object({
154
208
  name: z3.string().min(1).max(200),
155
209
  description: z3.string().max(2e3).optional(),
156
- document: TemplateDocument
210
+ locale: LocaleTag.optional(),
211
+ subject: TemplateSubject.nullable().optional(),
212
+ preheader: TemplatePreheader.nullable().optional(),
213
+ document: TemplateDocument,
214
+ translations: TemplateTranslations.optional()
157
215
  });
158
216
  var TemplateUpdate = z3.object({
159
217
  base_version: z3.number().int().min(1),
160
218
  name: z3.string().min(1).max(200).optional(),
161
219
  description: z3.string().max(2e3).nullable().optional(),
220
+ locale: LocaleTag.optional(),
221
+ subject: TemplateSubject.nullable().optional(),
222
+ preheader: TemplatePreheader.nullable().optional(),
162
223
  document: TemplateDocument.optional(),
224
+ translations: z3.record(LocaleTag, TemplateTranslation.nullable()).optional(),
163
225
  archived: z3.boolean().optional()
164
226
  }).refine(
165
- (v) => v.name !== void 0 || v.description !== void 0 || v.document !== void 0 || v.archived !== void 0,
227
+ (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,
166
228
  "at least one field besides base_version"
167
229
  );
168
230
  var SavedSectionBody = z3.record(z3.unknown());
@@ -192,7 +254,11 @@ var TemplateListQuery = PageQuery.extend({
192
254
  var TemplateVersion = z3.object({
193
255
  template_id: Id,
194
256
  version: z3.number().int().min(1),
257
+ locale: LocaleTag,
258
+ subject: TemplateSubject.nullable(),
259
+ preheader: TemplatePreheader.nullable(),
195
260
  document: TemplateDocument,
261
+ translations: TemplateTranslations,
196
262
  created_by: z3.string().nullable(),
197
263
  created_at: Timestamp
198
264
  });
@@ -213,7 +279,8 @@ var CompileResult = z3.object({
213
279
  errors: z3.array(CompileMessage)
214
280
  });
215
281
  var TemplateCompileRequest = z3.object({
216
- version: z3.number().int().min(1).optional()
282
+ version: z3.number().int().min(1).optional(),
283
+ locale: LocaleTag.optional()
217
284
  });
218
285
  var CompileRequest = z3.object({
219
286
  document: TemplateDocument
@@ -305,7 +372,8 @@ var EXPORT_FORMATS = ["mjml", "html"];
305
372
  var ExportFormat = z3.enum(EXPORT_FORMATS);
306
373
  var TemplateExportQuery = z3.object({
307
374
  format: ExportFormat,
308
- version: z3.coerce.number().int().min(1).optional()
375
+ version: z3.coerce.number().int().min(1).optional(),
376
+ locale: LocaleTag.optional()
309
377
  });
310
378
  var MailingExportQuery = z3.object({ format: ExportFormat });
311
379
  var EXPORT_CONTENT_TYPES = {
@@ -436,10 +504,35 @@ var Mailing = z5.object({
436
504
  name: z5.string().max(200).nullable(),
437
505
  subject: z5.string().min(1).max(998),
438
506
  preheader: z5.string().max(500).nullable(),
439
- /** The template the document was taken from, if any. The snapshot is what is sent. */
507
+ /**
508
+ * The template the document was taken from, if any. The snapshot is what is
509
+ * sent in the main language; the template's other language versions are
510
+ * taken, the ready ones, when the mailing starts (see "Languages" in the
511
+ * contract's documentation).
512
+ */
440
513
  template_id: Id.nullable(),
441
514
  document: TemplateDocument,
442
- topic: Slug,
515
+ /**
516
+ * The main language: what `subject`, `preheader` and `document` are written
517
+ * in, and what every recipient without a ready version in their own language
518
+ * gets. The template's main language, or the workspace's default for a
519
+ * mailing made from a document.
520
+ */
521
+ locale: LocaleTag,
522
+ /**
523
+ * The language every recipient is treated as reading, or null to read each
524
+ * recipient's own (their `merge.locale`, else their contact's language). A
525
+ * letter sets it to the language its one reader wrote in; a broadcast can use
526
+ * it to send one language version to everyone. It is matched like anyone's
527
+ * language: without a ready version in it, the main language goes out.
528
+ */
529
+ send_locale: LocaleTag.nullable(),
530
+ /**
531
+ * The topic the mailing is sent under. Null for a letter (a one-to-one
532
+ * mailing with at most one recipient, which belongs to no topic and whose
533
+ * unsubscribe link offers every topic) and for an automation's notification.
534
+ */
535
+ topic: Slug.nullable(),
443
536
  provider_id: Id,
444
537
  status: MailingStatus,
445
538
  counts: MailingCounts,
@@ -464,23 +557,37 @@ var MailingContent = {
464
557
  name: z5.string().max(200).optional(),
465
558
  subject: z5.string().min(1).max(998),
466
559
  preheader: z5.string().max(500).optional(),
467
- topic: Slug,
560
+ /** See `Mailing.send_locale`. */
561
+ send_locale: LocaleTag.nullable().optional(),
562
+ /**
563
+ * Omitted or null makes the mailing a letter: one recipient at most, sent to
564
+ * anybody not blocked on every topic, with an unsubscribe link that offers
565
+ * every topic and no List-Unsubscribe headers. See "Letters" in the
566
+ * contract's documentation.
567
+ */
568
+ topic: Slug.nullable().optional(),
468
569
  provider_id: Id,
469
570
  metadata: MailingMetadata.optional()
470
571
  };
471
572
  var MailingCreate = z5.object({
472
573
  ...MailingContent,
574
+ subject: MailingContent.subject.optional(),
473
575
  document: TemplateDocument.optional(),
474
576
  template_id: Id.optional()
475
577
  }).refine((v) => v.document === void 0 !== (v.template_id === void 0), {
476
578
  message: "exactly one of document or template_id",
477
579
  path: ["document"]
580
+ }).refine((v) => v.template_id !== void 0 || v.subject !== void 0, {
581
+ message: "a mailing made from a document needs a subject",
582
+ path: ["subject"]
478
583
  });
479
584
  var MailingUpdate = z5.object({
480
585
  name: z5.string().max(200).nullable().optional(),
481
586
  subject: MailingContent.subject.optional(),
482
587
  preheader: z5.string().max(500).nullable().optional(),
483
- topic: Slug.optional(),
588
+ /** Null turns a draft into a letter, which it can only be with at most one recipient. */
589
+ topic: Slug.nullable().optional(),
590
+ send_locale: LocaleTag.nullable().optional(),
484
591
  provider_id: Id.optional(),
485
592
  metadata: MailingMetadata.optional(),
486
593
  document: TemplateDocument.optional()
@@ -503,7 +610,8 @@ var MailingRetryFailedRequest = z5.object({
503
610
  var MailingActionRequest = z5.object({}).strict();
504
611
  var MailingTestRequest = z5.object({
505
612
  to: Email,
506
- merge: Properties.optional()
613
+ merge: Properties.optional(),
614
+ locale: LocaleTag.optional()
507
615
  });
508
616
  var MailingTestResult = z5.object({
509
617
  message_id: Id,
@@ -528,6 +636,8 @@ var Recipient = z5.object({
528
636
  updated_at: Timestamp
529
637
  });
530
638
  var MAX_RECIPIENTS_PER_BATCH = 1e3;
639
+ var MAX_LETTER_RECIPIENTS = 1;
640
+ var LETTER_RECIPIENT_REFUSAL = "letter_has_one_recipient";
531
641
  var RecipientInput = z5.object({
532
642
  contact_id: Id.optional(),
533
643
  external_id: z5.string().min(1).max(255).optional(),
@@ -568,9 +678,27 @@ var MessageSummary = z5.object({
568
678
  error: z5.string().nullable(),
569
679
  is_test: z5.boolean(),
570
680
  recipient_count: z5.number().int().min(1),
681
+ /** The language version the message was sent in; null for messages archived before languages existed. */
682
+ locale: LocaleTag.nullable(),
571
683
  created_at: Timestamp
572
684
  });
573
685
  var Message = MessageSummary.extend({ html: z5.string() });
686
+ var MailingLanguageStatus = z5.enum(["main", "ready", "draft", "not_a_workspace_language"]);
687
+ var MailingLanguage = z5.object({
688
+ locale: LocaleTag,
689
+ status: MailingLanguageStatus,
690
+ /** Queued recipients who will get this version. */
691
+ planned: z5.number().int().min(0),
692
+ /** Messages already sent in this language (tests excluded). */
693
+ sent: z5.number().int().min(0)
694
+ });
695
+ var MailingLanguageReport = z5.object({
696
+ main_locale: LocaleTag,
697
+ send_locale: LocaleTag.nullable(),
698
+ languages: z5.array(MailingLanguage),
699
+ fallback: z5.array(z5.object({ wanted: z5.string(), planned: z5.number().int().min(1) })),
700
+ no_language: z5.number().int().min(0)
701
+ });
574
702
  var MessageListQuery = PageQuery.extend({
575
703
  mailing_id: Id.optional(),
576
704
  outcome: MessageOutcome.optional()
@@ -1080,15 +1208,22 @@ var Unchosen = z7.literal("");
1080
1208
  var DraftSlug = z7.union([Slug, Unchosen]);
1081
1209
  var DraftPropertyKey = z7.union([ContactPropertyKey, Unchosen]);
1082
1210
  var label = { label: z7.string().max(120).nullish() };
1211
+ var EmailStepLanguage = z7.object({
1212
+ subject: z7.string().min(1).max(998),
1213
+ preheader: z7.string().max(500).nullable(),
1214
+ document: TemplateDocument
1215
+ }).strict();
1083
1216
  var EmailStep = z7.object({
1084
1217
  kind: z7.literal("email"),
1085
1218
  ...label,
1086
1219
  ...next,
1087
1220
  template_id: Id.nullish(),
1088
1221
  document: TemplateDocument.nullish(),
1089
- /** Empty while the step is being written; `not_chosen` refuses to publish it. */
1222
+ /** Empty while the step is being written, or when its template has a subject; else `not_chosen` refuses to publish it. */
1090
1223
  subject: z7.string().max(998),
1091
- preheader: z7.string().max(500).nullish()
1224
+ preheader: z7.string().max(500).nullish(),
1225
+ locale: LocaleTag.nullish(),
1226
+ languages: z7.record(LocaleTag, EmailStepLanguage).nullish()
1092
1227
  }).strict();
1093
1228
  var DelayStep = z7.object({
1094
1229
  kind: z7.literal("delay"),
@@ -1394,7 +1529,7 @@ function validateAutomation(flow, settings, ctx) {
1394
1529
  if (!step.template_id && !step.document) {
1395
1530
  add({ code: "empty_email", step_id: id, path: [...path, "template_id"], message: "This email has neither a template nor content." });
1396
1531
  }
1397
- if (step.subject.trim() === "") {
1532
+ if (step.subject.trim() === "" && !(step.template_id && known.templates_with_subject?.has(step.template_id))) {
1398
1533
  add({ code: "not_chosen", step_id: id, path: [...path, "subject"], message: "This email has no subject line." });
1399
1534
  }
1400
1535
  if (step.template_id) checkRef(known.templates, step.template_id, "template", [...path, "template_id"], add, id);
@@ -1879,7 +2014,7 @@ var AutomationReport = z7.object({
1879
2014
  var AutomationExitRunRequest = z7.object({ reason: z7.string().max(200).optional() }).strict();
1880
2015
  var AutomationRunParams = z7.object({ id: Id, run_id: Id });
1881
2016
  var AutomationStepParams = z7.object({ id: Id, step_id: StepId });
1882
- var AutomationTestEmailRequest = z7.object({ to: Email, merge: Properties.optional() }).strict();
2017
+ var AutomationTestEmailRequest = z7.object({ to: Email, merge: Properties.optional(), locale: LocaleTag.optional() }).strict();
1883
2018
  var AutomationValidateRequest = z7.object({ flow: AutomationFlow.optional(), settings: AutomationSettings.partial().optional() }).strict();
1884
2019
  var EventCreate = z7.object({
1885
2020
  name: EventName,
@@ -1948,10 +2083,19 @@ var MergeDefaults = z8.record(MergeFieldName, MergeDefaultValue).refine((m) => O
1948
2083
  message: `a reserved merge field (${Object.keys(RESERVED_MERGE_FIELDS).join(", ")}) cannot have a workspace default`
1949
2084
  });
1950
2085
  var WorkspaceSettings = z8.object({
1951
- /** Default language of the hosted unsubscribe page (BCP 47 tag, e.g. "de"). */
1952
- default_locale: z8.string().min(2).max(35),
1953
- /** Languages the hosted pages are offered in. Includes `default_locale`. */
1954
- locales: z8.array(z8.string().min(2).max(35)).min(1).max(20),
2086
+ /**
2087
+ * The workspace's main language (a {@link LocaleTag}, e.g. "de"): the main
2088
+ * language of a new template, and of the hosted pages when nothing else
2089
+ * decides.
2090
+ */
2091
+ default_locale: LocaleTag,
2092
+ /**
2093
+ * The languages the workspace writes in, including `default_locale`, none
2094
+ * twice (compared without regard to casing). A template can have a version in
2095
+ * each; the hosted pages are offered in those of them they support (English,
2096
+ * German, French, Italian, Spanish). The service stores canonical casing.
2097
+ */
2098
+ locales: z8.array(LocaleTag).min(1).max(20).refine((l) => new Set(l.map((t) => t.toLowerCase())).size === l.length, "a language appears twice"),
1955
2099
  /** Open and click tracking. Off by default, opt-in per workspace. */
1956
2100
  tracking_enabled: z8.boolean(),
1957
2101
  /** Where images, stylesheets and fonts in a mail may load from. `any` by default. */
@@ -2065,6 +2209,7 @@ var AUDIT_ACTIONS = [
2065
2209
  "provider.anomaly_cleared",
2066
2210
  "topic.created",
2067
2211
  "topic.updated",
2212
+ "topic.deleted",
2068
2213
  "template.created",
2069
2214
  "template.updated",
2070
2215
  "template.deleted",
@@ -2202,6 +2347,200 @@ var InviteAccepted = z9.object({
2202
2347
  });
2203
2348
  var INVITE_REFUSAL_REASONS = ["email_mismatch", "inviter_lacks_role", "expired", "revoked", "accepted"];
2204
2349
 
2350
+ // src/template-languages.ts
2351
+ function languageTabs(mainLocale, versions, workspaceLocales) {
2352
+ const main = canonicalLocale(mainLocale) ?? mainLocale;
2353
+ const statuses = /* @__PURE__ */ new Map();
2354
+ for (const [tag, status] of Object.entries(versions)) {
2355
+ const canonical = canonicalLocale(tag) ?? tag;
2356
+ if (canonical !== main) statuses.set(canonical, status);
2357
+ }
2358
+ const tabs = [{ locale: main, kind: "main", status: "ready" }];
2359
+ const seen = /* @__PURE__ */ new Set([main]);
2360
+ for (const tag of workspaceLocales) {
2361
+ const canonical = canonicalLocale(tag) ?? tag;
2362
+ if (seen.has(canonical)) continue;
2363
+ seen.add(canonical);
2364
+ tabs.push({ locale: canonical, kind: "translation", status: statuses.get(canonical) ?? "missing" });
2365
+ }
2366
+ const kept = [...statuses.keys()].filter((tag) => !seen.has(tag)).sort();
2367
+ for (const tag of kept) tabs.push({ locale: tag, kind: "kept", status: statuses.get(tag) });
2368
+ return tabs;
2369
+ }
2370
+ function languageName(tag) {
2371
+ const canonical = canonicalLocale(tag) ?? tag;
2372
+ try {
2373
+ const names = new Intl.DisplayNames(["en"], { type: "language", fallback: "none" });
2374
+ const name = names.of(canonical);
2375
+ return name && name !== canonical ? name : canonical;
2376
+ } catch {
2377
+ return canonical;
2378
+ }
2379
+ }
2380
+ function storedTemplateLine(value) {
2381
+ if (value === null || value === void 0) return null;
2382
+ const trimmed = value.trim();
2383
+ return trimmed === "" ? null : trimmed;
2384
+ }
2385
+ function buildTemplatePatch(args) {
2386
+ const { loaded, main, documentChanged, translations, changed, removed } = args;
2387
+ const out = {};
2388
+ const subject = storedTemplateLine(main.subject);
2389
+ if (subject !== storedTemplateLine(loaded.subject)) out.subject = subject;
2390
+ const preheader = storedTemplateLine(main.preheader);
2391
+ if (preheader !== storedTemplateLine(loaded.preheader)) out.preheader = preheader;
2392
+ if (documentChanged) out.document = main.document;
2393
+ const patch = {};
2394
+ for (const tag of changed) {
2395
+ const version = translations[tag];
2396
+ if (version && !removed.has(tag)) {
2397
+ patch[tag] = { subject: storedTemplateLine(version.subject), preheader: storedTemplateLine(version.preheader), document: version.document, status: version.status };
2398
+ }
2399
+ }
2400
+ for (const tag of removed) {
2401
+ if (Object.prototype.hasOwnProperty.call(loaded.translations, tag)) patch[tag] = null;
2402
+ }
2403
+ if (Object.keys(patch).length > 0) out.translations = patch;
2404
+ return out;
2405
+ }
2406
+ function restorePatch(old, current, workspaceLocales) {
2407
+ const oldMain = canonicalLocale(old.locale) ?? old.locale;
2408
+ const listed = workspaceLocales ? new Set(workspaceLocales.map((t) => canonicalLocale(t) ?? t)) : null;
2409
+ const patch = {};
2410
+ const skipped = [];
2411
+ const untouched = /* @__PURE__ */ new Set();
2412
+ for (const [tag, version] of Object.entries(old.translations)) {
2413
+ const canonical = canonicalLocale(tag) ?? tag;
2414
+ if (canonical === oldMain) continue;
2415
+ if (listed && !listed.has(canonical)) {
2416
+ skipped.push(canonical);
2417
+ untouched.add(canonical);
2418
+ continue;
2419
+ }
2420
+ patch[canonical] = version;
2421
+ }
2422
+ for (const tag of Object.keys(current.translations)) {
2423
+ const canonical = canonicalLocale(tag) ?? tag;
2424
+ if (!(canonical in patch) && !untouched.has(canonical)) patch[canonical] = null;
2425
+ if (canonical === oldMain) patch[canonical] = null;
2426
+ }
2427
+ const update = {
2428
+ ...oldMain !== (canonicalLocale(current.locale) ?? current.locale) ? { locale: oldMain } : {},
2429
+ subject: old.subject,
2430
+ preheader: old.preheader,
2431
+ document: old.document,
2432
+ ...Object.keys(patch).length > 0 ? { translations: patch } : {}
2433
+ };
2434
+ return { update, skipped: skipped.sort() };
2435
+ }
2436
+ function draftsFromTemplate(template) {
2437
+ const translations = {};
2438
+ for (const [tag, version] of Object.entries(template.translations)) translations[canonicalLocale(tag) ?? tag] = version;
2439
+ return {
2440
+ main: { subject: template.subject ?? "", preheader: template.preheader ?? "", document: template.document },
2441
+ translations,
2442
+ mainDocumentChanged: false,
2443
+ changed: [],
2444
+ removed: []
2445
+ };
2446
+ }
2447
+ var withChanged = (list2, tag) => list2.includes(tag) ? [...list2] : [...list2, tag];
2448
+ var without = (list2, tag) => list2.filter((t) => t !== tag);
2449
+ function draftDocument(drafts, locale) {
2450
+ if (locale === null) return drafts.main.document;
2451
+ return drafts.translations[locale]?.document;
2452
+ }
2453
+ function addLanguageDraft(drafts, locale) {
2454
+ const tag = canonicalLocale(locale) ?? locale;
2455
+ if (drafts.translations[tag]) return drafts;
2456
+ return {
2457
+ ...drafts,
2458
+ translations: {
2459
+ ...drafts.translations,
2460
+ [tag]: {
2461
+ subject: drafts.main.subject,
2462
+ preheader: drafts.main.preheader,
2463
+ document: structuredClone(drafts.main.document),
2464
+ status: "draft"
2465
+ }
2466
+ },
2467
+ changed: withChanged(drafts.changed, tag),
2468
+ removed: without(drafts.removed, tag)
2469
+ };
2470
+ }
2471
+ function removeLanguageDraft(drafts, locale) {
2472
+ const tag = canonicalLocale(locale) ?? locale;
2473
+ const { [tag]: _gone, ...rest } = drafts.translations;
2474
+ return { ...drafts, translations: rest, changed: without(drafts.changed, tag), removed: withChanged(drafts.removed, tag) };
2475
+ }
2476
+ function editLanguageDraft(drafts, locale, edit) {
2477
+ if (locale === null) {
2478
+ return {
2479
+ ...drafts,
2480
+ main: { ...drafts.main, ...edit },
2481
+ mainDocumentChanged: drafts.mainDocumentChanged || edit.document !== void 0
2482
+ };
2483
+ }
2484
+ const version = drafts.translations[locale];
2485
+ if (!version) return drafts;
2486
+ const next2 = { ...version, ...edit };
2487
+ return { ...drafts, translations: { ...drafts.translations, [locale]: next2 }, changed: withChanged(drafts.changed, locale) };
2488
+ }
2489
+ function keepLanguageDocument(drafts, locale, document) {
2490
+ if (locale === null) return { ...drafts, main: { ...drafts.main, document } };
2491
+ const version = drafts.translations[locale];
2492
+ if (!version) return drafts;
2493
+ return { ...drafts, translations: { ...drafts.translations, [locale]: { ...version, document } } };
2494
+ }
2495
+ function setLanguageStatus(drafts, locale, status) {
2496
+ const version = drafts.translations[locale];
2497
+ if (!version || version.status === status) return drafts;
2498
+ return { ...drafts, translations: { ...drafts.translations, [locale]: { ...version, status } }, changed: withChanged(drafts.changed, locale) };
2499
+ }
2500
+ function draftsPatch(drafts, loaded) {
2501
+ return buildTemplatePatch({
2502
+ loaded,
2503
+ main: drafts.main,
2504
+ documentChanged: drafts.mainDocumentChanged,
2505
+ translations: drafts.translations,
2506
+ changed: new Set(drafts.changed),
2507
+ removed: new Set(drafts.removed)
2508
+ });
2509
+ }
2510
+ function draftsDirty(drafts, loaded) {
2511
+ return Object.keys(draftsPatch(drafts, loaded)).length > 0;
2512
+ }
2513
+ function readyWithoutSubject(drafts) {
2514
+ return Object.entries(drafts.translations).filter(([tag, v]) => drafts.changed.includes(tag) && v.status === "ready" && storedTemplateLine(v.subject) === null).map(([tag]) => tag);
2515
+ }
2516
+ function rebaseDrafts(sent, now, saved) {
2517
+ const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
2518
+ let out = draftsFromTemplate(saved);
2519
+ if (now.main.subject !== sent.main.subject) out = editLanguageDraft(out, null, { subject: now.main.subject });
2520
+ if (now.main.preheader !== sent.main.preheader) out = editLanguageDraft(out, null, { preheader: now.main.preheader });
2521
+ out = same(now.main.document, sent.main.document) ? keepLanguageDocument(out, null, now.main.document) : editLanguageDraft(out, null, { document: now.main.document });
2522
+ const tags = /* @__PURE__ */ new Set([...Object.keys(sent.translations), ...Object.keys(now.translations)]);
2523
+ for (const tag of tags) {
2524
+ const before = sent.translations[tag];
2525
+ const after = now.translations[tag];
2526
+ if (same(before, after)) {
2527
+ if (after) out = keepLanguageDocument(out, tag, after.document);
2528
+ continue;
2529
+ }
2530
+ if (!after) {
2531
+ out = removeLanguageDraft(out, tag);
2532
+ continue;
2533
+ }
2534
+ out = {
2535
+ ...out,
2536
+ translations: { ...out.translations, [tag]: after },
2537
+ changed: withChanged(out.changed, tag),
2538
+ removed: without(out.removed, tag)
2539
+ };
2540
+ }
2541
+ return out;
2542
+ }
2543
+
2205
2544
  // src/providers.ts
2206
2545
  import { z as z10 } from "zod";
2207
2546
  var PROVIDER_KINDS = ["smtp", "resend"];
@@ -3312,6 +3651,16 @@ var sendingRoutes = {
3312
3651
  access: "admin",
3313
3652
  phase: "S2"
3314
3653
  },
3654
+ /** Refused with `conflict` while a mailing or an automation step still uses the topic; subscriptions and suppressions on it go with it. */
3655
+ "topics.delete": {
3656
+ method: "DELETE",
3657
+ path: "/v1/topics/:id",
3658
+ params: IdParams,
3659
+ response: Ok,
3660
+ status: 200,
3661
+ access: "admin",
3662
+ phase: "S2"
3663
+ },
3315
3664
  "contacts.upsert": {
3316
3665
  method: "POST",
3317
3666
  path: "/v1/contacts",
@@ -3786,6 +4135,15 @@ var platformRoutes = {
3786
4135
  access: "read",
3787
4136
  phase: "S4"
3788
4137
  },
4138
+ "mailings.languages": {
4139
+ method: "GET",
4140
+ path: "/v1/mailings/:id/languages",
4141
+ params: IdParams,
4142
+ response: MailingLanguageReport,
4143
+ status: 200,
4144
+ access: "read",
4145
+ phase: "S4"
4146
+ },
3789
4147
  "signupForms.list": {
3790
4148
  method: "GET",
3791
4149
  path: "/v1/signup-forms",
@@ -4383,6 +4741,7 @@ export {
4383
4741
  EXPORT_WARNING_COUNT_HEADER,
4384
4742
  Email,
4385
4743
  EmailStep,
4744
+ EmailStepLanguage,
4386
4745
  ErrorBody,
4387
4746
  ErrorCodeSchema,
4388
4747
  Event,
@@ -4432,7 +4791,10 @@ export {
4432
4791
  InviteListQuery,
4433
4792
  InviteStatus,
4434
4793
  JsonValue,
4794
+ LETTER_RECIPIENT_REFUSAL,
4435
4795
  LIST_UNSUBSCRIBE_POST_VALUE,
4796
+ LOCALE_TAG_PATTERN,
4797
+ LocaleTag,
4436
4798
  MAILING_ACTIONS,
4437
4799
  MAILING_KINDS,
4438
4800
  MAILING_STATUSES,
@@ -4450,18 +4812,22 @@ export {
4450
4812
  MAX_IMPORT_URL_LENGTH,
4451
4813
  MAX_INVITE_TTL_DAYS,
4452
4814
  MAX_IN_LIST,
4815
+ MAX_LETTER_RECIPIENTS,
4453
4816
  MAX_MERGE_DEFAULTS,
4454
4817
  MAX_MERGE_DEFAULT_LENGTH,
4455
4818
  MAX_MJML_DEPTH,
4456
4819
  MAX_MJML_ELEMENTS,
4457
4820
  MAX_MJML_IMPORT_BYTES,
4458
4821
  MAX_PAGE_LIMIT,
4822
+ MAX_PREHEADER_LENGTH,
4459
4823
  MAX_RECIPIENTS_PER_BATCH,
4460
4824
  MAX_RE_ENTRY_COOLDOWN_SECONDS,
4461
4825
  MAX_SAVED_SECTIONS,
4462
4826
  MAX_SPLIT_BRANCHES,
4463
4827
  MAX_STEPS,
4464
4828
  MAX_STEP_VISITS,
4829
+ MAX_SUBJECT_LENGTH,
4830
+ MAX_TEMPLATE_TRANSLATIONS,
4465
4831
  MAX_TRIGGERS,
4466
4832
  MEMBER_ROLES,
4467
4833
  MERGE_FIELD_PATTERN,
@@ -4479,6 +4845,9 @@ export {
4479
4845
  MailingExportQuery,
4480
4846
  MailingFinishedData,
4481
4847
  MailingKind,
4848
+ MailingLanguage,
4849
+ MailingLanguageReport,
4850
+ MailingLanguageStatus,
4482
4851
  MailingListQuery,
4483
4852
  MailingMetadata,
4484
4853
  MailingRetryFailedRequest,
@@ -4579,6 +4948,7 @@ export {
4579
4948
  SuppressionCreate,
4580
4949
  SuppressionListQuery,
4581
4950
  SuppressionReason,
4951
+ TEMPLATE_LANGUAGE_STATUSES,
4582
4952
  TERMINAL_MAILING_STATUSES,
4583
4953
  Tag,
4584
4954
  TagAssignment,
@@ -4594,8 +4964,13 @@ export {
4594
4964
  TemplateImportPreview,
4595
4965
  TemplateImportPreviewRequest,
4596
4966
  TemplateImportResult,
4967
+ TemplateLanguageStatus,
4597
4968
  TemplateListQuery,
4969
+ TemplatePreheader,
4970
+ TemplateSubject,
4598
4971
  TemplateSummary,
4972
+ TemplateTranslation,
4973
+ TemplateTranslations,
4599
4974
  TemplateUpdate,
4600
4975
  TemplateUpdatedData,
4601
4976
  TemplateVersion,
@@ -4648,14 +5023,22 @@ export {
4648
5023
  WorkspaceSettings,
4649
5024
  WorkspaceUpdate,
4650
5025
  acceptsIdempotencyKey,
5026
+ addLanguageDraft,
4651
5027
  alwaysWaits,
4652
5028
  automationFilterFamily,
4653
5029
  automationRoutes,
4654
5030
  billingRoutes,
4655
5031
  buildPath,
5032
+ buildTemplatePatch,
4656
5033
  canTransition,
5034
+ canonicalLocale,
4657
5035
  cycles,
4658
5036
  defaultSettings,
5037
+ draftDocument,
5038
+ draftsDirty,
5039
+ draftsFromTemplate,
5040
+ draftsPatch,
5041
+ editLanguageDraft,
4659
5042
  emptyFlow,
4660
5043
  errorBody,
4661
5044
  exportFilename,
@@ -4671,6 +5054,10 @@ export {
4671
5054
  isDayEntirelyPast,
4672
5055
  isPublishable,
4673
5056
  isRetryableError,
5057
+ keepLanguageDocument,
5058
+ languageName,
5059
+ languageTabs,
5060
+ matchLocale,
4674
5061
  matchRoute,
4675
5062
  missingRequiredMergeFields,
4676
5063
  nowSeconds,
@@ -4679,12 +5066,19 @@ export {
4679
5066
  parseExportWarningsHeader,
4680
5067
  parseUsageWarningHeader,
4681
5068
  platformRoutes,
5069
+ primaryLanguage,
4682
5070
  reachable,
5071
+ readyWithoutSubject,
5072
+ rebaseDrafts,
5073
+ removeLanguageDraft,
5074
+ restorePatch,
4683
5075
  routes,
4684
5076
  sendingRoutes,
5077
+ setLanguageStatus,
4685
5078
  signWebhook,
4686
5079
  statusForError,
4687
5080
  stepTargets,
5081
+ storedTemplateLine,
4688
5082
  stronglyConnected,
4689
5083
  templateRoutes,
4690
5084
  testGroupSize,
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.3.0",
6
+ "version": "0.5.0",
7
7
  "description": "The v1 API contract of the mail service: zod schemas and types for every request, response, error and webhook, the route table, and webhook signing helpers",
8
8
  "license": "MIT",
9
9
  "repository": {