@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.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,9 +504,29 @@ 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,
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(),
442
530
  /**
443
531
  * The topic the mailing is sent under. Null for a letter (a one-to-one
444
532
  * mailing with at most one recipient, which belongs to no topic and whose
@@ -469,6 +557,8 @@ var MailingContent = {
469
557
  name: z5.string().max(200).optional(),
470
558
  subject: z5.string().min(1).max(998),
471
559
  preheader: z5.string().max(500).optional(),
560
+ /** See `Mailing.send_locale`. */
561
+ send_locale: LocaleTag.nullable().optional(),
472
562
  /**
473
563
  * Omitted or null makes the mailing a letter: one recipient at most, sent to
474
564
  * anybody not blocked on every topic, with an unsubscribe link that offers
@@ -481,11 +571,15 @@ var MailingContent = {
481
571
  };
482
572
  var MailingCreate = z5.object({
483
573
  ...MailingContent,
574
+ subject: MailingContent.subject.optional(),
484
575
  document: TemplateDocument.optional(),
485
576
  template_id: Id.optional()
486
577
  }).refine((v) => v.document === void 0 !== (v.template_id === void 0), {
487
578
  message: "exactly one of document or template_id",
488
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"]
489
583
  });
490
584
  var MailingUpdate = z5.object({
491
585
  name: z5.string().max(200).nullable().optional(),
@@ -493,6 +587,7 @@ var MailingUpdate = z5.object({
493
587
  preheader: z5.string().max(500).nullable().optional(),
494
588
  /** Null turns a draft into a letter, which it can only be with at most one recipient. */
495
589
  topic: Slug.nullable().optional(),
590
+ send_locale: LocaleTag.nullable().optional(),
496
591
  provider_id: Id.optional(),
497
592
  metadata: MailingMetadata.optional(),
498
593
  document: TemplateDocument.optional()
@@ -515,7 +610,8 @@ var MailingRetryFailedRequest = z5.object({
515
610
  var MailingActionRequest = z5.object({}).strict();
516
611
  var MailingTestRequest = z5.object({
517
612
  to: Email,
518
- merge: Properties.optional()
613
+ merge: Properties.optional(),
614
+ locale: LocaleTag.optional()
519
615
  });
520
616
  var MailingTestResult = z5.object({
521
617
  message_id: Id,
@@ -582,9 +678,27 @@ var MessageSummary = z5.object({
582
678
  error: z5.string().nullable(),
583
679
  is_test: z5.boolean(),
584
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(),
585
683
  created_at: Timestamp
586
684
  });
587
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
+ });
588
702
  var MessageListQuery = PageQuery.extend({
589
703
  mailing_id: Id.optional(),
590
704
  outcome: MessageOutcome.optional()
@@ -1094,15 +1208,22 @@ var Unchosen = z7.literal("");
1094
1208
  var DraftSlug = z7.union([Slug, Unchosen]);
1095
1209
  var DraftPropertyKey = z7.union([ContactPropertyKey, Unchosen]);
1096
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();
1097
1216
  var EmailStep = z7.object({
1098
1217
  kind: z7.literal("email"),
1099
1218
  ...label,
1100
1219
  ...next,
1101
1220
  template_id: Id.nullish(),
1102
1221
  document: TemplateDocument.nullish(),
1103
- /** 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. */
1104
1223
  subject: z7.string().max(998),
1105
- preheader: z7.string().max(500).nullish()
1224
+ preheader: z7.string().max(500).nullish(),
1225
+ locale: LocaleTag.nullish(),
1226
+ languages: z7.record(LocaleTag, EmailStepLanguage).nullish()
1106
1227
  }).strict();
1107
1228
  var DelayStep = z7.object({
1108
1229
  kind: z7.literal("delay"),
@@ -1408,7 +1529,7 @@ function validateAutomation(flow, settings, ctx) {
1408
1529
  if (!step.template_id && !step.document) {
1409
1530
  add({ code: "empty_email", step_id: id, path: [...path, "template_id"], message: "This email has neither a template nor content." });
1410
1531
  }
1411
- if (step.subject.trim() === "") {
1532
+ if (step.subject.trim() === "" && !(step.template_id && known.templates_with_subject?.has(step.template_id))) {
1412
1533
  add({ code: "not_chosen", step_id: id, path: [...path, "subject"], message: "This email has no subject line." });
1413
1534
  }
1414
1535
  if (step.template_id) checkRef(known.templates, step.template_id, "template", [...path, "template_id"], add, id);
@@ -1893,7 +2014,7 @@ var AutomationReport = z7.object({
1893
2014
  var AutomationExitRunRequest = z7.object({ reason: z7.string().max(200).optional() }).strict();
1894
2015
  var AutomationRunParams = z7.object({ id: Id, run_id: Id });
1895
2016
  var AutomationStepParams = z7.object({ id: Id, step_id: StepId });
1896
- var AutomationTestEmailRequest = z7.object({ to: Email, merge: Properties.optional() }).strict();
2017
+ var AutomationTestEmailRequest = z7.object({ to: Email, merge: Properties.optional(), locale: LocaleTag.optional() }).strict();
1897
2018
  var AutomationValidateRequest = z7.object({ flow: AutomationFlow.optional(), settings: AutomationSettings.partial().optional() }).strict();
1898
2019
  var EventCreate = z7.object({
1899
2020
  name: EventName,
@@ -1962,10 +2083,19 @@ var MergeDefaults = z8.record(MergeFieldName, MergeDefaultValue).refine((m) => O
1962
2083
  message: `a reserved merge field (${Object.keys(RESERVED_MERGE_FIELDS).join(", ")}) cannot have a workspace default`
1963
2084
  });
1964
2085
  var WorkspaceSettings = z8.object({
1965
- /** Default language of the hosted unsubscribe page (BCP 47 tag, e.g. "de"). */
1966
- default_locale: z8.string().min(2).max(35),
1967
- /** Languages the hosted pages are offered in. Includes `default_locale`. */
1968
- 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"),
1969
2099
  /** Open and click tracking. Off by default, opt-in per workspace. */
1970
2100
  tracking_enabled: z8.boolean(),
1971
2101
  /** Where images, stylesheets and fonts in a mail may load from. `any` by default. */
@@ -2217,6 +2347,200 @@ var InviteAccepted = z9.object({
2217
2347
  });
2218
2348
  var INVITE_REFUSAL_REASONS = ["email_mismatch", "inviter_lacks_role", "expired", "revoked", "accepted"];
2219
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
+
2220
2544
  // src/providers.ts
2221
2545
  import { z as z10 } from "zod";
2222
2546
  var PROVIDER_KINDS = ["smtp", "resend"];
@@ -3811,6 +4135,15 @@ var platformRoutes = {
3811
4135
  access: "read",
3812
4136
  phase: "S4"
3813
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
+ },
3814
4147
  "signupForms.list": {
3815
4148
  method: "GET",
3816
4149
  path: "/v1/signup-forms",
@@ -4408,6 +4741,7 @@ export {
4408
4741
  EXPORT_WARNING_COUNT_HEADER,
4409
4742
  Email,
4410
4743
  EmailStep,
4744
+ EmailStepLanguage,
4411
4745
  ErrorBody,
4412
4746
  ErrorCodeSchema,
4413
4747
  Event,
@@ -4459,6 +4793,8 @@ export {
4459
4793
  JsonValue,
4460
4794
  LETTER_RECIPIENT_REFUSAL,
4461
4795
  LIST_UNSUBSCRIBE_POST_VALUE,
4796
+ LOCALE_TAG_PATTERN,
4797
+ LocaleTag,
4462
4798
  MAILING_ACTIONS,
4463
4799
  MAILING_KINDS,
4464
4800
  MAILING_STATUSES,
@@ -4483,12 +4819,15 @@ export {
4483
4819
  MAX_MJML_ELEMENTS,
4484
4820
  MAX_MJML_IMPORT_BYTES,
4485
4821
  MAX_PAGE_LIMIT,
4822
+ MAX_PREHEADER_LENGTH,
4486
4823
  MAX_RECIPIENTS_PER_BATCH,
4487
4824
  MAX_RE_ENTRY_COOLDOWN_SECONDS,
4488
4825
  MAX_SAVED_SECTIONS,
4489
4826
  MAX_SPLIT_BRANCHES,
4490
4827
  MAX_STEPS,
4491
4828
  MAX_STEP_VISITS,
4829
+ MAX_SUBJECT_LENGTH,
4830
+ MAX_TEMPLATE_TRANSLATIONS,
4492
4831
  MAX_TRIGGERS,
4493
4832
  MEMBER_ROLES,
4494
4833
  MERGE_FIELD_PATTERN,
@@ -4506,6 +4845,9 @@ export {
4506
4845
  MailingExportQuery,
4507
4846
  MailingFinishedData,
4508
4847
  MailingKind,
4848
+ MailingLanguage,
4849
+ MailingLanguageReport,
4850
+ MailingLanguageStatus,
4509
4851
  MailingListQuery,
4510
4852
  MailingMetadata,
4511
4853
  MailingRetryFailedRequest,
@@ -4606,6 +4948,7 @@ export {
4606
4948
  SuppressionCreate,
4607
4949
  SuppressionListQuery,
4608
4950
  SuppressionReason,
4951
+ TEMPLATE_LANGUAGE_STATUSES,
4609
4952
  TERMINAL_MAILING_STATUSES,
4610
4953
  Tag,
4611
4954
  TagAssignment,
@@ -4621,8 +4964,13 @@ export {
4621
4964
  TemplateImportPreview,
4622
4965
  TemplateImportPreviewRequest,
4623
4966
  TemplateImportResult,
4967
+ TemplateLanguageStatus,
4624
4968
  TemplateListQuery,
4969
+ TemplatePreheader,
4970
+ TemplateSubject,
4625
4971
  TemplateSummary,
4972
+ TemplateTranslation,
4973
+ TemplateTranslations,
4626
4974
  TemplateUpdate,
4627
4975
  TemplateUpdatedData,
4628
4976
  TemplateVersion,
@@ -4675,14 +5023,22 @@ export {
4675
5023
  WorkspaceSettings,
4676
5024
  WorkspaceUpdate,
4677
5025
  acceptsIdempotencyKey,
5026
+ addLanguageDraft,
4678
5027
  alwaysWaits,
4679
5028
  automationFilterFamily,
4680
5029
  automationRoutes,
4681
5030
  billingRoutes,
4682
5031
  buildPath,
5032
+ buildTemplatePatch,
4683
5033
  canTransition,
5034
+ canonicalLocale,
4684
5035
  cycles,
4685
5036
  defaultSettings,
5037
+ draftDocument,
5038
+ draftsDirty,
5039
+ draftsFromTemplate,
5040
+ draftsPatch,
5041
+ editLanguageDraft,
4686
5042
  emptyFlow,
4687
5043
  errorBody,
4688
5044
  exportFilename,
@@ -4698,6 +5054,10 @@ export {
4698
5054
  isDayEntirelyPast,
4699
5055
  isPublishable,
4700
5056
  isRetryableError,
5057
+ keepLanguageDocument,
5058
+ languageName,
5059
+ languageTabs,
5060
+ matchLocale,
4701
5061
  matchRoute,
4702
5062
  missingRequiredMergeFields,
4703
5063
  nowSeconds,
@@ -4706,12 +5066,19 @@ export {
4706
5066
  parseExportWarningsHeader,
4707
5067
  parseUsageWarningHeader,
4708
5068
  platformRoutes,
5069
+ primaryLanguage,
4709
5070
  reachable,
5071
+ readyWithoutSubject,
5072
+ rebaseDrafts,
5073
+ removeLanguageDraft,
5074
+ restorePatch,
4710
5075
  routes,
4711
5076
  sendingRoutes,
5077
+ setLanguageStatus,
4712
5078
  signWebhook,
4713
5079
  statusForError,
4714
5080
  stepTargets,
5081
+ storedTemplateLine,
4715
5082
  stronglyConnected,
4716
5083
  templateRoutes,
4717
5084
  testGroupSize,
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.4.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": {