@mrrocket/sdk 0.18.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +24 -1
  3. package/dist/api-client.d.ts +28 -4
  4. package/dist/api-client.js +30 -13
  5. package/dist/api-client.js.map +1 -1
  6. package/dist/browser/mrrocket-flows.js +8 -3
  7. package/dist/browser/mrrocket-messenger.js +38 -5
  8. package/dist/browser/mrrocket.js +1 -1
  9. package/dist/browser.d.ts +1 -1
  10. package/dist/browser.js.map +1 -1
  11. package/dist/client.d.ts +6 -1
  12. package/dist/client.js +22 -4
  13. package/dist/client.js.map +1 -1
  14. package/dist/core.d.ts +5 -1
  15. package/dist/core.js +1 -1
  16. package/dist/core.js.map +1 -1
  17. package/dist/feedback.js +161 -119
  18. package/dist/feedback.js.map +1 -1
  19. package/dist/flows-ui.d.ts +1 -1
  20. package/dist/flows-ui.js +100 -72
  21. package/dist/flows-ui.js.map +1 -1
  22. package/dist/flows-web.js +2 -1
  23. package/dist/flows-web.js.map +1 -1
  24. package/dist/flows.d.ts +38 -6
  25. package/dist/flows.js +51 -17
  26. package/dist/flows.js.map +1 -1
  27. package/dist/i18n.d.ts +22 -0
  28. package/dist/i18n.js +74 -0
  29. package/dist/i18n.js.map +1 -0
  30. package/dist/index.d.ts +3 -1
  31. package/dist/index.js +3 -0
  32. package/dist/index.js.map +1 -1
  33. package/dist/messenger-ui.d.ts +9 -3
  34. package/dist/messenger-ui.js +324 -152
  35. package/dist/messenger-ui.js.map +1 -1
  36. package/dist/messenger.d.ts +3 -1
  37. package/dist/messenger.js +3 -2
  38. package/dist/messenger.js.map +1 -1
  39. package/dist/paywall.js +2 -1
  40. package/dist/paywall.js.map +1 -1
  41. package/dist/react-native/index.d.cts +66 -8
  42. package/dist/react-native/index.d.ts +66 -8
  43. package/dist/react-native/index.js +615 -334
  44. package/dist/react-native/index.js.map +1 -1
  45. package/dist/server.d.ts +4 -2
  46. package/dist/server.js +2 -2
  47. package/dist/server.js.map +1 -1
  48. package/dist/styles-css.d.ts +1 -1
  49. package/dist/styles-css.js +35 -2
  50. package/dist/styles-css.js.map +1 -1
  51. package/dist/survey.js +18 -14
  52. package/dist/survey.js.map +1 -1
  53. package/dist/ui.d.ts +2 -0
  54. package/dist/ui.js +15 -12
  55. package/dist/ui.js.map +1 -1
  56. package/package.json +6 -3
@@ -31,7 +31,9 @@ __export(native_exports, {
31
31
  SDK_VERSION: () => SDK_VERSION,
32
32
  activeRouteName: () => activeRouteName,
33
33
  createMRRocket: () => createMRRocket,
34
+ currentLocale: () => currentLocale,
34
35
  getMRRocket: () => getMRRocket,
36
+ setLocale: () => setLocale,
35
37
  useCancelFlow: () => useCancelFlow,
36
38
  useExpoRouterTracking: () => useExpoRouterTracking,
37
39
  useFeedback: () => useFeedback,
@@ -50,8 +52,68 @@ __export(native_exports, {
50
52
  module.exports = __toCommonJS(native_exports);
51
53
  var import_react4 = require("react");
52
54
 
55
+ // src/i18n.ts
56
+ var g = globalThis;
57
+ var s = g.__mrrocketI18n ?? (g.__mrrocketI18n = { version: 0, onWords: /* @__PURE__ */ new Set(), onLocale: /* @__PURE__ */ new Set() });
58
+ var tell = (listeners) => {
59
+ for (const fn of [...listeners]) {
60
+ try {
61
+ fn();
62
+ } catch {
63
+ }
64
+ }
65
+ };
66
+ var t = (word) => s.words?.[word] || word;
67
+ var tf = (word, vars) => t(word).replace(/\{(\w+)\}/g, (all, k) => k in vars ? String(vars[k]) : all);
68
+ var setWords = (words, lang) => {
69
+ const same2 = s.lang === lang && JSON.stringify(s.words ?? {}) === JSON.stringify(words ?? {});
70
+ s.words = words;
71
+ s.lang = lang;
72
+ if (same2) return;
73
+ s.version++;
74
+ tell(s.onWords);
75
+ };
76
+ var tidy = (locale) => {
77
+ const tag = locale?.trim().replace(/_/g, "-");
78
+ if (!tag) return void 0;
79
+ try {
80
+ return Intl.getCanonicalLocales(tag)[0];
81
+ } catch (e) {
82
+ return e instanceof RangeError ? void 0 : tag;
83
+ }
84
+ };
85
+ var setLocale = (locale) => {
86
+ const next = tidy(locale);
87
+ if (next === s.locale) return;
88
+ s.locale = next;
89
+ s.version++;
90
+ tell(s.onWords);
91
+ tell(s.onLocale);
92
+ };
93
+ var setDeviceLocale = (locale) => {
94
+ s.device = tidy(locale);
95
+ };
96
+ var currentLocale = () => s.locale ?? s.device ?? (typeof navigator !== "undefined" && navigator.language || void 0);
97
+ var langParam = () => {
98
+ const l = currentLocale();
99
+ return l ? `&lang=${encodeURIComponent(l)}` : "";
100
+ };
101
+ var wordsVersion = () => s.version;
102
+ var onWords = (fn) => {
103
+ s.onWords.add(fn);
104
+ return () => {
105
+ s.onWords.delete(fn);
106
+ };
107
+ };
108
+ var onLocale = (fn) => {
109
+ s.onLocale.add(fn);
110
+ return () => {
111
+ s.onLocale.delete(fn);
112
+ };
113
+ };
114
+
53
115
  // src/core.ts
54
- var SDK_VERSION = "0.18.0";
116
+ var SDK_VERSION = "0.21.0";
55
117
  var propOf = (person, name) => {
56
118
  const want = name.trim().toLowerCase();
57
119
  for (const [k, v] of Object.entries(person)) if (k.toLowerCase() === want) return v;
@@ -77,7 +139,7 @@ var matchesCondition = (c, person) => {
77
139
  var evaluateFlag = (flag, distinctId, person = {}) => {
78
140
  if (!flag.on) return false;
79
141
  const subject = { id: distinctId, ...person };
80
- const set = flag.sets.length ? flag.sets.find((s) => s.conditions.every((c) => matchesCondition(c, subject))) : { conditions: [], rollout: 100 };
142
+ const set = flag.sets.length ? flag.sets.find((s2) => s2.conditions.every((c) => matchesCondition(c, subject))) : { conditions: [], rollout: 100 };
81
143
  if (!set) return false;
82
144
  if (flagUnit(`${flag.key}:rollout:${distinctId}`) * 100 >= set.rollout) return false;
83
145
  if (!flag.variants?.length) return true;
@@ -108,11 +170,11 @@ var parseQuery = (url) => {
108
170
  const q = url.split("#")[0];
109
171
  const at = q.indexOf("?");
110
172
  if (at < 0) return out;
111
- const dec = (s) => {
173
+ const dec = (s2) => {
112
174
  try {
113
- return decodeURIComponent(s.replace(/\+/g, " "));
175
+ return decodeURIComponent(s2.replace(/\+/g, " "));
114
176
  } catch {
115
- return s;
177
+ return s2;
116
178
  }
117
179
  };
118
180
  for (const pair of q.slice(at + 1).split("&")) {
@@ -184,9 +246,9 @@ var useOnboardingItem = (flows, id, start) => {
184
246
  return;
185
247
  }
186
248
  const up = () => {
187
- const s = flows.onboarding.item(id);
188
- setState(s);
189
- if (s && !s.done && !s.dismissed) flows.onboarding.seen(id);
249
+ const s2 = flows.onboarding.item(id);
250
+ setState(s2);
251
+ if (s2 && !s2.done && !s2.dismissed) flows.onboarding.seen(id);
190
252
  };
191
253
  up();
192
254
  return flows.subscribe(up);
@@ -216,9 +278,9 @@ var useOnboardingItem = (flows, id, start) => {
216
278
  // src/identity.ts
217
279
  var UNSIGNED_HELP = "401: sign the user id on your server with userHash(id) from @mrrocket/sdk/next/server and pass identify(id, props, { hash }).";
218
280
  var warnUnsigned = (status, log = (m) => console.warn(m)) => {
219
- const g = globalThis;
220
- if (status !== 401 || g.__mrrocketUnsigned) return;
221
- g.__mrrocketUnsigned = true;
281
+ const g2 = globalThis;
282
+ if (status !== 401 || g2.__mrrocketUnsigned) return;
283
+ g2.__mrrocketUnsigned = true;
222
284
  log(`[MRRocket] ${UNSIGNED_HELP}`);
223
285
  };
224
286
  var nextSigned = (signed, id, hash, log = (m) => console.warn(m)) => {
@@ -242,11 +304,11 @@ var BRAND_URL = "https://mrrocket.dev/?utm_source=messenger&utm_medium=powered-b
242
304
  var inkOn = (hex) => {
243
305
  const m = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(hex);
244
306
  if (!m) return "#ffffff";
245
- const [r, g, b] = [m[1], m[2], m[3]].map((x) => {
307
+ const [r, g2, b] = [m[1], m[2], m[3]].map((x) => {
246
308
  const c = parseInt(x, 16) / 255;
247
309
  return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
248
310
  });
249
- return 0.2126 * r + 0.7152 * g + 0.0722 * b > 0.45 ? "#111214" : "#ffffff";
311
+ return 0.2126 * r + 0.7152 * g2 + 0.0722 * b > 0.45 ? "#111214" : "#ffffff";
250
312
  };
251
313
  var ApiError = class extends Error {
252
314
  constructor(message, status) {
@@ -255,7 +317,11 @@ var ApiError = class extends Error {
255
317
  this.status = status;
256
318
  }
257
319
  };
258
- var qs = (params) => Object.entries(params).filter((e) => !!e[1]).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
320
+ var qs = (params, lang = currentLocale()) => Object.entries({ ...params, lang }).filter((e) => !!e[1]).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
321
+ var withLang = (url) => {
322
+ const q = qs({});
323
+ return q ? `${url}?${q}` : url;
324
+ };
259
325
  var call = async (url, init, signal) => {
260
326
  let res;
261
327
  try {
@@ -267,7 +333,7 @@ var call = async (url, init, signal) => {
267
333
  });
268
334
  } catch (e) {
269
335
  if (e.name === "AbortError") throw e;
270
- throw new ApiError("Could not reach the server. Check your connection and try again.", 0);
336
+ throw new ApiError(t("Could not reach the server. Check your connection and try again."), 0);
271
337
  }
272
338
  if (!res.ok) {
273
339
  warnUnsigned(res.status);
@@ -276,7 +342,7 @@ var call = async (url, init, signal) => {
276
342
  detail = (await res.json()).error ?? "";
277
343
  } catch {
278
344
  }
279
- throw new ApiError(detail || (res.status === 429 ? "Too many requests. Try again in a minute." : "Something went wrong. Please try again."), res.status);
345
+ throw new ApiError(detail || (res.status === 429 ? t("Too many requests. Try again in a minute.") : t("Something went wrong. Please try again.")), res.status);
280
346
  }
281
347
  return await res.json();
282
348
  };
@@ -284,7 +350,7 @@ var createApi = (c) => {
284
350
  const base = `${c.host}/api/sdk`;
285
351
  const who2 = { key: c.apiKey, distinctId: c.distinctId, user: c.user, userHash: c.userHash };
286
352
  const me = { key: c.apiKey, distinctId: c.distinctId, userHash: c.userHash };
287
- const post = (path, body, signal) => call(`${base}${path}`, { method: "POST", body: JSON.stringify({ ...who2, ...body }) }, signal);
353
+ const post = (path, body, signal) => call(withLang(`${base}${path}`), { method: "POST", body: JSON.stringify({ ...who2, ...body }) }, signal);
288
354
  return {
289
355
  list: (o, signal) => call(`${base}/feedback?${qs({ ...me, board: o.board, sort: o.sort, q: o.q })}`, void 0, signal),
290
356
  post: (id, signal) => call(`${base}/feedback/${encodeURIComponent(id)}?${qs(me)}`, void 0, signal),
@@ -297,8 +363,12 @@ var createApi = (c) => {
297
363
  survey: (url, signal) => call(`${base}/surveys?${qs({ ...me, url })}`, void 0, signal),
298
364
  seen: (id) => post(`/surveys/${encodeURIComponent(id)}/seen`, { user: void 0 }),
299
365
  respond: (id, r) => post(`/surveys/${encodeURIComponent(id)}/respond`, r),
300
- chat: (signal) => call(`${base}/messages?${qs(me)}`, void 0, signal),
301
- /** A message; `attachments` are upload ids from `uploadChatFile` (then the body may be empty). */
366
+ /** One chat by id, else the visitor's newest; `conversations` lists them all. */
367
+ chat: (signal, conversation) => call(`${base}/messages?${qs({ ...me, conversation })}`, void 0, signal),
368
+ /**
369
+ * A message; `attachments` are upload ids from `uploadChatFile` (then the body may be empty). `conversation` is
370
+ * the chat it answers, or "new" to start one; without it, the running chat or else a new one.
371
+ */
302
372
  sendChat: (m) => post("/messages", m),
303
373
  /** Uploads one picked file for the next message (pictures and PDFs, at most 10 MB); answers its upload id. */
304
374
  uploadChatFile: (file, signal) => {
@@ -308,10 +378,10 @@ var createApi = (c) => {
308
378
  if (c.userHash) form.append("userHash", c.userHash);
309
379
  if ("uri" in file) form.append("file", file);
310
380
  else form.append("file", file, file.name || "file");
311
- return call(`${base}/messages/attachments`, { method: "POST", body: form }, signal);
381
+ return call(withLang(`${base}/messages/attachments`), { method: "POST", body: form }, signal);
312
382
  },
313
383
  /** Where a sent file of this visitor's chat is read (an image's src, a PDF's link). */
314
- attachmentUrl: (id) => `${base}/messages/attachments/${encodeURIComponent(id)}?${qs(me)}`,
384
+ attachmentUrl: (id) => `${base}/messages/attachments/${encodeURIComponent(id)}?${qs(me, "")}`,
315
385
  articles: (q, signal) => call(`${base}/articles?${qs({ key: c.apiKey, q })}`, void 0, signal),
316
386
  article: (id, signal) => call(`${base}/articles/${encodeURIComponent(id)}?${qs(me)}`, void 0, signal),
317
387
  messengerLook: (signal) => call(`${base}/messenger?${qs({ key: c.apiKey })}`, void 0, signal),
@@ -322,13 +392,13 @@ var createApi = (c) => {
322
392
  // src/batch.ts
323
393
  var MAX_BATCH = 100;
324
394
  var MAX_BYTES = 6e4;
325
- var byteLength = (s) => {
395
+ var byteLength = (s2) => {
326
396
  let n = 0;
327
- for (let i = 0; i < s.length; i++) {
328
- const c = s.charCodeAt(i);
397
+ for (let i = 0; i < s2.length; i++) {
398
+ const c = s2.charCodeAt(i);
329
399
  if (c < 128) n += 1;
330
400
  else if (c < 2048) n += 2;
331
- else if (c >= 55296 && c <= 56319 && i + 1 < s.length && (s.charCodeAt(i + 1) & 64512) === 56320) {
401
+ else if (c >= 55296 && c <= 56319 && i + 1 < s2.length && (s2.charCodeAt(i + 1) & 64512) === 56320) {
332
402
  n += 4;
333
403
  i++;
334
404
  } else n += 3;
@@ -363,7 +433,7 @@ var batchRequests = (key, events, sdk, userHash) => {
363
433
 
364
434
  // src/errors.ts
365
435
  var MAX_STACK = 8 * 1024;
366
- var clip = (s, n) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
436
+ var clip = (s2, n) => s2.length > n ? `${s2.slice(0, n - 1)}\u2026` : s2;
367
437
  var describeError = (error) => {
368
438
  if (error instanceof Error) return { type: error.name || "Error", message: clip(error.message || String(error), 1e3), stack: error.stack ? clip(error.stack, MAX_STACK) : void 0 };
369
439
  if (typeof error === "string") return { type: "Error", message: clip(error, 1e3) };
@@ -384,18 +454,20 @@ var PLAIN_CANCEL = {
384
454
  question: "",
385
455
  reasons: [],
386
456
  offers: {},
387
- confirm: { title: "Cancel your subscription?", body: "You keep access until the end of the billing period.", button: "Cancel subscription" },
457
+ // Read when it is shown, so it follows the visitor's language.
458
+ get confirm() {
459
+ return { title: t("Cancel your subscription?"), body: t("You keep access until the end of the billing period."), button: t("Cancel subscription") };
460
+ },
388
461
  offersFor: "business"
389
462
  };
390
- var FAILED = "Something went wrong. Please try again.";
391
463
  var PROGRESS_KEY = "mrrocket:onboarding";
392
- var eventSlug = (s) => s.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
464
+ var eventSlug = (s2) => s2.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
393
465
  var SIGNUP = /* @__PURE__ */ new Set(["signed_up", "signup", "sign_up", "registered", "account_created", "user_signed_up"]);
394
466
  var matchesTrigger = (trigger, event) => {
395
- const t = eventSlug(trigger);
467
+ const t2 = eventSlug(trigger);
396
468
  const e = eventSlug(event);
397
- if (!t || !e) return false;
398
- return t === e || SIGNUP.has(t) && SIGNUP.has(e);
469
+ if (!t2 || !e) return false;
470
+ return t2 === e || SIGNUP.has(t2) && SIGNUP.has(e);
399
471
  };
400
472
  var startsOnLoad = (trigger) => /^(first (launch|visit|open|session|login|sign ?in|app open)|app (opened|launched)|on load|every visit|always)?$/i.test(trigger.trim());
401
473
  var isPageTrigger = (trigger) => trigger.trim().startsWith("/");
@@ -404,16 +476,30 @@ var momentOf = (f) => f.when ?? f.trigger;
404
476
  var manual = (trigger) => eventSlug(trigger) === "from_a_checklist" || eventSlug(trigger) === "from_your_code" || eventSlug(trigger) === eventSlug(NEEDS_NEWER);
405
477
  var AFTER_PREFIX = "after:";
406
478
  var afterOf = (trigger) => {
407
- const t = (trigger ?? "").trim();
408
- return t.toLowerCase().startsWith(AFTER_PREFIX) ? t.slice(AFTER_PREFIX.length).trim() || null : null;
479
+ const t2 = (trigger ?? "").trim();
480
+ return t2.toLowerCase().startsWith(AFTER_PREFIX) ? t2.slice(AFTER_PREFIX.length).trim() || null : null;
409
481
  };
410
482
  var opensOf = (step) => step.opens ?? (step.startsTour ? { kind: "tour", id: step.startsTour } : void 0);
483
+ var OTHER_PREFIX = "other:";
484
+ var scaleOf = (q) => {
485
+ const min = q?.min ?? 0;
486
+ const max = q?.max ?? 10;
487
+ return Array.from({ length: Math.max(0, Math.min(max - min, 20) + 1) }, (_, i) => min + i);
488
+ };
411
489
  var answerOk = (q, value) => {
412
490
  if (!q || value == null) return false;
413
- if (q.type === "choice") return (q.options ?? []).some((o) => o.id === value);
491
+ if (q.type === "choice") {
492
+ if (q.other && value.startsWith(OTHER_PREFIX)) return value.slice(OTHER_PREFIX.length).trim().length > 0;
493
+ return (q.options ?? []).some((o) => o.id === value);
494
+ }
495
+ if (q.type === "scale") return scaleOf(q).some((n) => String(n) === value);
414
496
  return value.trim().length > 0 && value.length <= 500;
415
497
  };
416
- var answerLabel = (q, value) => q?.type === "choice" ? q.options?.find((o) => o.id === value)?.label ?? value : value.trim();
498
+ var answerLabel = (q, value) => {
499
+ if (q?.type !== "choice") return value.trim();
500
+ if (q.other && value.startsWith(OTHER_PREFIX)) return value.slice(OTHER_PREFIX.length).trim();
501
+ return q.options?.find((o) => o.id === value)?.label ?? value;
502
+ };
417
503
  var videoEmbed = (url) => {
418
504
  const u = (url ?? "").trim();
419
505
  if (!/^https?:\/\//i.test(u)) return null;
@@ -435,12 +521,14 @@ var runId = () => `cf_${Math.random().toString(36).slice(2, 10)}${Date.now().toS
435
521
  var discounted = (amount, percent) => Math.round(amount * (1 - percent / 100) * 100) / 100;
436
522
  var money = (amount, currency) => {
437
523
  try {
438
- return new Intl.NumberFormat(void 0, { style: "currency", currency }).format(amount);
524
+ return new Intl.NumberFormat(currentLocale(), { style: "currency", currency }).format(amount);
439
525
  } catch {
440
526
  return `${amount.toFixed(2)} ${currency}`;
441
527
  }
442
528
  };
443
- var offerButton = (o) => o.kind === "contact" ? "Send to the team" : o.kind === "pause" ? "Pause instead" : o.kind === "downgrade" ? "Switch plan" : "Accept offer";
529
+ var offerButton = (o) => o.kind === "contact" ? t("Send to the team") : o.kind === "pause" ? t("Pause instead") : o.kind === "downgrade" ? t("Switch plan") : t("Accept offer");
530
+ var monthsWord = (n) => n === 1 ? t("1 month") : tf("{n} months", { n });
531
+ var forMonths = (n) => tf("for {months}", { months: monthsWord(n) });
444
532
  var safeUrl = (url) => {
445
533
  const u = (url ?? "").trim();
446
534
  return /^https?:\/\//i.test(u) || /^\/(?!\/)/.test(u) ? u : void 0;
@@ -587,13 +675,13 @@ var createFlows = (env) => {
587
675
  }
588
676
  };
589
677
  const merge = (state) => {
590
- for (const [id, s] of Object.entries(state ?? {})) {
678
+ for (const [id, s2] of Object.entries(state ?? {})) {
591
679
  const p = prog(id);
592
- for (const d of s.done ?? []) p.done.add(d);
593
- if (s.dismissed) p.dismissed = true;
594
- for (const [k, v] of Object.entries(s.answers ?? {})) if (!p.answers.has(k)) p.answers.set(k, v === "yes");
595
- for (const [k, v] of Object.entries(s.at ?? {})) if (typeof v === "number" && !p.at.has(k)) p.at.set(k, v);
596
- for (const [k, v] of Object.entries(s.reached ?? {})) if (typeof v === "number" && (!p.reached.has(k) || v < p.reached.get(k))) p.reached.set(k, v);
680
+ for (const d of s2.done ?? []) p.done.add(d);
681
+ if (s2.dismissed) p.dismissed = true;
682
+ for (const [k, v] of Object.entries(s2.answers ?? {})) if (!p.answers.has(k)) p.answers.set(k, v === "yes");
683
+ for (const [k, v] of Object.entries(s2.at ?? {})) if (typeof v === "number" && !p.at.has(k)) p.at.set(k, v);
684
+ for (const [k, v] of Object.entries(s2.reached ?? {})) if (typeof v === "number" && (!p.reached.has(k) || v < p.reached.get(k))) p.reached.set(k, v);
597
685
  }
598
686
  };
599
687
  const wakeAt = (until) => {
@@ -633,14 +721,14 @@ var createFlows = (env) => {
633
721
  return {
634
722
  type: "checklist",
635
723
  flow,
636
- items: items.map((s) => {
637
- const o = opensOf(s);
638
- const t = o && (o.kind === "tour" || o.kind === "modal") && o.id !== flow.id ? flowById(o.id) : void 0;
639
- const target = t && t.shownBy !== "code" && nodesOf(t).length ? t : void 0;
724
+ items: items.map((s2) => {
725
+ const o = opensOf(s2);
726
+ const t2 = o && (o.kind === "tour" || o.kind === "modal") && o.id !== flow.id ? flowById(o.id) : void 0;
727
+ const target = t2 && t2.shownBy !== "code" && nodesOf(t2).length ? t2 : void 0;
640
728
  const opens = o && (o.kind === "page" ? !!o.target.trim() : o.kind === "code" || !!target) ? o : void 0;
641
729
  return {
642
- step: s,
643
- done: p.done.has(s.id),
730
+ step: s2,
731
+ done: p.done.has(s2.id),
644
732
  ...target && o?.kind === "tour" ? { tour: target } : {},
645
733
  ...opens ? { opens } : {},
646
734
  ...target ? { target } : {}
@@ -696,7 +784,7 @@ var createFlows = (env) => {
696
784
  active = null;
697
785
  const list = back ? flowById(back.flow) : void 0;
698
786
  if (back && list) {
699
- const item = stepsIn(nodesOf(list)).find((s) => s.id === back.step);
787
+ const item = stepsIn(nodesOf(list)).find((s2) => s2.id === back.step);
700
788
  const p = prog(list.id);
701
789
  if (item && !item.event && !progress.get(ended2)?.dismissed && !p.done.has(item.id)) {
702
790
  markDone(p, item.id);
@@ -739,8 +827,8 @@ var createFlows = (env) => {
739
827
  };
740
828
  const allowed = (f) => f.audience === "answered" ? !!f.answered && answers.get(f.answered.question) === f.answered.option : f.audience !== "signed_in" || !!env.personId();
741
829
  const predecessorEnded = (id) => {
742
- const g = flowById(id);
743
- return !g || finished(g);
830
+ const g2 = flowById(id);
831
+ return !g2 || finished(g2);
744
832
  };
745
833
  const due = (f, trigger = momentOf(f)) => {
746
834
  const path = env.path?.();
@@ -775,7 +863,7 @@ var createFlows = (env) => {
775
863
  };
776
864
  const answerStep = (flow, step, raw) => {
777
865
  const q = step.question;
778
- const value = q.type === "text" ? raw.trim().slice(0, 500) : raw;
866
+ const value = q.type === "text" ? raw.trim().slice(0, 500) : raw.startsWith(OTHER_PREFIX) ? OTHER_PREFIX + raw.slice(OTHER_PREFIX.length).trim().slice(0, 490) : raw;
779
867
  answers.set(q.key, value);
780
868
  drafts.delete(step.id);
781
869
  markDone(prog(flow.id), step.id);
@@ -886,7 +974,7 @@ var createFlows = (env) => {
886
974
  () => finish(c, r),
887
975
  () => {
888
976
  c.busy = false;
889
- c.error = FAILED;
977
+ c.error = t("Something went wrong. Please try again.");
890
978
  emit();
891
979
  }
892
980
  );
@@ -923,11 +1011,11 @@ var createFlows = (env) => {
923
1011
  for (const l of lists) {
924
1012
  const p = prog(l.id);
925
1013
  let here = false;
926
- for (const s of stepsIn(nodesOf(l)))
927
- if (s.kind === "checklist" && s.event && !p.done.has(s.id) && matchesTrigger(s.event, name)) {
928
- markDone(p, s.id);
1014
+ for (const s2 of stepsIn(nodesOf(l)))
1015
+ if (s2.kind === "checklist" && s2.event && !p.done.has(s2.id) && matchesTrigger(s2.event, name)) {
1016
+ markDone(p, s2.id);
929
1017
  here = true;
930
- env.track(ONBOARDING_EVENT, { flow: l.id, step: s.id, kind: s.kind, action: "completed" });
1018
+ env.track(ONBOARDING_EVENT, { flow: l.id, step: s2.id, kind: s2.kind, action: "completed" });
931
1019
  }
932
1020
  if (here) {
933
1021
  ticked = true;
@@ -966,8 +1054,8 @@ var createFlows = (env) => {
966
1054
  answer: (text2) => {
967
1055
  const c = cancel;
968
1056
  if (!c || c.busy || c.screen !== "followup") return;
969
- const t = text2.slice(0, 500);
970
- c.answer = t.trim() ? t : void 0;
1057
+ const t2 = text2.slice(0, 500);
1058
+ c.answer = t2.trim() ? t2 : void 0;
971
1059
  emit();
972
1060
  },
973
1061
  next: () => {
@@ -1109,11 +1197,22 @@ var createFlows = (env) => {
1109
1197
  drafts.set(v.step.id, String(value).slice(0, 500));
1110
1198
  emit();
1111
1199
  },
1200
+ skip: () => {
1201
+ const v = onboardingView();
1202
+ const q = v?.type === "step" && v.step.kind === "question" ? v.step.question : void 0;
1203
+ if (v?.type !== "step" || !q?.optional) return;
1204
+ drafts.delete(v.step.id);
1205
+ markDone(prog(v.flow.id), v.step.id);
1206
+ env.track(ONBOARDING_EVENT, { flow: v.flow.id, step: v.step.id, kind: "question", action: "completed", question: q.key, skipped: true });
1207
+ celebrate(v.flow);
1208
+ save();
1209
+ emit();
1210
+ },
1112
1211
  back: () => {
1113
1212
  const v = onboardingView();
1114
1213
  if (v?.type !== "step" || v.step.kind !== "question" || !v.back) return;
1115
1214
  const line = stepsIn(nodesOf(v.flow));
1116
- const at = line.findIndex((s) => s.id === v.step.id);
1215
+ const at = line.findIndex((s2) => s2.id === v.step.id);
1117
1216
  const p = prog(v.flow.id);
1118
1217
  for (let i = at - 1; i >= 0; i--)
1119
1218
  if (p.done.has(line[i].id)) {
@@ -1128,11 +1227,11 @@ var createFlows = (env) => {
1128
1227
  const f = flowById(id);
1129
1228
  if (!f) return null;
1130
1229
  const p = progress.get(f.id);
1131
- const steps = stepsIn(nodesOf(f)).map((s) => {
1132
- const a = s.question ? answers.get(s.question.key) : void 0;
1133
- return { ...s, done: !!p?.done.has(s.id), ...a !== void 0 ? { answer: a } : {} };
1230
+ const steps = stepsIn(nodesOf(f)).map((s2) => {
1231
+ const a = s2.question ? answers.get(s2.question.key) : void 0;
1232
+ return { ...s2, done: !!p?.done.has(s2.id), ...a !== void 0 ? { answer: a } : {} };
1134
1233
  });
1135
- const done = steps.length > 0 && steps.every((s) => s.done);
1234
+ const done = steps.length > 0 && steps.every((s2) => s2.done);
1136
1235
  const dismissed = !!p?.dismissed;
1137
1236
  return { item: f, steps, done, dismissed, eligible: !done && !dismissed && allowed(f) && due(f) };
1138
1237
  },
@@ -1144,21 +1243,21 @@ var createFlows = (env) => {
1144
1243
  env.track(ONBOARDING_EVENT, { flow: f.id, action: "started" });
1145
1244
  }
1146
1245
  const p = progress.get(f.id);
1147
- for (const s of stepsIn(nodesOf(f)))
1148
- if (!p?.done.has(s.id) && !shown.has(`s:${s.id}`)) {
1149
- shown.add(`s:${s.id}`);
1150
- env.track(ONBOARDING_EVENT, { flow: f.id, step: s.id, kind: s.kind, action: "shown" });
1151
- if (s.kind !== "checklist") break;
1246
+ for (const s2 of stepsIn(nodesOf(f)))
1247
+ if (!p?.done.has(s2.id) && !shown.has(`s:${s2.id}`)) {
1248
+ shown.add(`s:${s2.id}`);
1249
+ env.track(ONBOARDING_EVENT, { flow: f.id, step: s2.id, kind: s2.kind, action: "shown" });
1250
+ if (s2.kind !== "checklist") break;
1152
1251
  }
1153
1252
  },
1154
1253
  complete: (id, stepId) => {
1155
1254
  const f = flowById(id);
1156
- const s = f && stepsIn(nodesOf(f)).find((x) => x.id === stepId);
1157
- if (!f || !s || s.kind === "question") return;
1255
+ const s2 = f && stepsIn(nodesOf(f)).find((x) => x.id === stepId);
1256
+ if (!f || !s2 || s2.kind === "question") return;
1158
1257
  const p = prog(f.id);
1159
- if (p.done.has(s.id)) return;
1160
- markDone(p, s.id);
1161
- env.track(ONBOARDING_EVENT, { flow: f.id, step: s.id, kind: s.kind, action: "completed" });
1258
+ if (p.done.has(s2.id)) return;
1259
+ markDone(p, s2.id);
1260
+ env.track(ONBOARDING_EVENT, { flow: f.id, step: s2.id, kind: s2.kind, action: "completed" });
1162
1261
  ended(f);
1163
1262
  },
1164
1263
  dismissItem: (id) => {
@@ -1173,9 +1272,9 @@ var createFlows = (env) => {
1173
1272
  },
1174
1273
  answerItem: (id, question, value) => {
1175
1274
  const f = flowById(id);
1176
- const s = f && stepsIn(nodesOf(f)).find((x) => x.kind === "question" && (x.id === question || x.question?.key === question));
1177
- if (!f || !s?.question || !answerOk(s.question, value)) return;
1178
- answerStep(f, s, value);
1275
+ const s2 = f && stepsIn(nodesOf(f)).find((x) => x.kind === "question" && (x.id === question || x.question?.key === question));
1276
+ if (!f || !s2?.question || !answerOk(s2.question, value)) return;
1277
+ answerStep(f, s2, value);
1179
1278
  ended(f);
1180
1279
  }
1181
1280
  },
@@ -1263,7 +1362,7 @@ var createPaywallHost = (env) => {
1263
1362
  },
1264
1363
  () => {
1265
1364
  c.busy = false;
1266
- c.error = "The purchase did not go through. Please try again.";
1365
+ c.error = t("The purchase did not go through. Please try again.");
1267
1366
  emit();
1268
1367
  }
1269
1368
  );
@@ -1338,10 +1437,10 @@ var RageDetector = class {
1338
1437
  __publicField(this, "clicks", []);
1339
1438
  __publicField(this, "fired", false);
1340
1439
  }
1341
- push(x, y, t) {
1342
- const near = this.clicks.filter((c) => t - c.t <= this.windowMs && Math.hypot(c.x - x, c.y - y) <= this.radius);
1440
+ push(x, y, t2) {
1441
+ const near = this.clicks.filter((c) => t2 - c.t <= this.windowMs && Math.hypot(c.x - x, c.y - y) <= this.radius);
1343
1442
  if (near.length !== this.clicks.length) this.fired = false;
1344
- this.clicks = [...near, { x, y, t }];
1443
+ this.clicks = [...near, { x, y, t: t2 }];
1345
1444
  if (this.clicks.length >= this.count && !this.fired) {
1346
1445
  this.fired = true;
1347
1446
  return true;
@@ -1357,9 +1456,9 @@ var BUFFER_MS = 6e4;
1357
1456
  var CHUNK_FRAMES = 12;
1358
1457
  var CHUNK_CHARS = 7e5;
1359
1458
  var MAX_MS = 30 * 6e4;
1360
- var shareOf = (s) => {
1459
+ var shareOf = (s2) => {
1361
1460
  let h3 = 2166136261;
1362
- for (let i = 0; i < s.length; i++) h3 = Math.imul(h3 ^ s.charCodeAt(i), 16777619);
1461
+ for (let i = 0; i < s2.length; i++) h3 = Math.imul(h3 ^ s2.charCodeAt(i), 16777619);
1363
1462
  return (h3 >>> 0) / 4294967296;
1364
1463
  };
1365
1464
  var createRecorder = (deps, st) => {
@@ -1484,14 +1583,14 @@ var measureMasks = () => Promise.all(
1484
1583
  (ref) => new Promise((resolve) => {
1485
1584
  const v = ref.current;
1486
1585
  if (!v) return resolve("none");
1487
- const t = setTimeout(() => resolve("failed"), 100);
1586
+ const t2 = setTimeout(() => resolve("failed"), 100);
1488
1587
  try {
1489
1588
  v.measureInWindow((x, y, w, h3) => {
1490
- clearTimeout(t);
1589
+ clearTimeout(t2);
1491
1590
  resolve(w > 0 && h3 > 0 ? [x, y, w, h3] : "none");
1492
1591
  });
1493
1592
  } catch {
1494
- clearTimeout(t);
1593
+ clearTimeout(t2);
1495
1594
  resolve("failed");
1496
1595
  }
1497
1596
  })
@@ -1528,7 +1627,7 @@ var MAX_CRUMBS = 20;
1528
1627
  var MAX_BACKOFF_MS = 5 * 60 * 1e3;
1529
1628
  var REQUEST_TIMEOUT_MS = 15e3;
1530
1629
  var EAGER_COUNT = 20;
1531
- var clip2 = (s, n) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
1630
+ var clip2 = (s2, n) => s2.length > n ? `${s2.slice(0, n - 1)}\u2026` : s2;
1532
1631
  var createClient = (options, deps) => {
1533
1632
  const apiKey = (options.apiKey ?? "").trim();
1534
1633
  const host = (options.host ?? DEFAULT_HOST).trim().replace(/\/+$/, "");
@@ -1556,6 +1655,8 @@ var createClient = (options, deps) => {
1556
1655
  context = deps.context();
1557
1656
  } catch {
1558
1657
  }
1658
+ setDeviceLocale(context.locale);
1659
+ if (options.locale) setLocale(options.locale);
1559
1660
  const appId = (deps.appId || "app").replace(/[^A-Za-z0-9._-]/g, "") || "app";
1560
1661
  const release = options.release ?? (context.appVersion ? `${context.appVersion}${context.build ? ` (${context.build})` : ""}` : void 0);
1561
1662
  const sdk = { v: SDK_VERSION, errors: errorsOn, platform: context.platform };
@@ -1601,7 +1702,7 @@ var createClient = (options, deps) => {
1601
1702
  const fetchInApp = async (id) => {
1602
1703
  if (!apiKey) return null;
1603
1704
  const hash = hashOf(id);
1604
- const r = await deps.fetch(`${host}/api/sdk/inapp?key=${encodeURIComponent(apiKey)}&distinctId=${encodeURIComponent(id)}${hash ? `&userHash=${hash}` : ""}`, { method: "GET" });
1705
+ const r = await deps.fetch(`${host}/api/sdk/inapp?key=${encodeURIComponent(apiKey)}&distinctId=${encodeURIComponent(id)}${hash ? `&userHash=${hash}` : ""}${langParam()}`, { method: "GET" });
1605
1706
  warnUnsigned(r.status, deps.log);
1606
1707
  return r.ok ? await r.json() : null;
1607
1708
  };
@@ -1687,9 +1788,9 @@ var createClient = (options, deps) => {
1687
1788
  const sendId = () => consent === "persistent" ? distinctId() : identified;
1688
1789
  const hashOf = (id) => hashFor(signed, id);
1689
1790
  const touchSession = () => {
1690
- const t = now();
1691
- if (!session || t - session.last >= SESSION_TIMEOUT_MS) session = { id: newDistinctId(), last: t };
1692
- else session.last = t;
1791
+ const t2 = now();
1792
+ if (!session || t2 - session.last >= SESSION_TIMEOUT_MS) session = { id: newDistinctId(), last: t2 };
1793
+ else session.last = t2;
1693
1794
  return session.id;
1694
1795
  };
1695
1796
  const urlOf = (path = currentScreen) => `app://${appId}${path ?? "/"}`;
@@ -1878,7 +1979,7 @@ var createClient = (options, deps) => {
1878
1979
  const fetchConfig = async () => {
1879
1980
  if (!apiKey) return;
1880
1981
  try {
1881
- const r = await deps.fetch(`${host}/api/sdk/config?key=${encodeURIComponent(apiKey)}`, { method: "GET" });
1982
+ const r = await deps.fetch(`${host}/api/sdk/config?key=${encodeURIComponent(apiKey)}${langParam()}`, { method: "GET" });
1882
1983
  if (!r.ok) {
1883
1984
  warn(`Config request answered ${r.status}. Check the public key and the host.`);
1884
1985
  return;
@@ -1886,6 +1987,7 @@ var createClient = (options, deps) => {
1886
1987
  const c = await r.json();
1887
1988
  if (c && Array.isArray(c.experiments)) {
1888
1989
  config = c;
1990
+ setWords(c.words, c.lang);
1889
1991
  if (consent !== "off") await set(KEYS.config, JSON.stringify(c));
1890
1992
  changed();
1891
1993
  }
@@ -2004,7 +2106,10 @@ var createClient = (options, deps) => {
2004
2106
  }
2005
2107
  try {
2006
2108
  const c = cached ? JSON.parse(cached) : null;
2007
- if (c && Array.isArray(c.experiments) && !config) config = c;
2109
+ if (c && Array.isArray(c.experiments) && !config) {
2110
+ config = c;
2111
+ setWords(c.words, c.lang);
2112
+ }
2008
2113
  } catch {
2009
2114
  await remove(KEYS.config);
2010
2115
  }
@@ -2060,6 +2165,9 @@ var createClient = (options, deps) => {
2060
2165
  await fetchConfig();
2061
2166
  markReady();
2062
2167
  refreshFlows();
2168
+ onLocale(() => {
2169
+ void fetchConfig().then(refreshFlows);
2170
+ });
2063
2171
  };
2064
2172
  void start().catch(() => {
2065
2173
  loaded = true;
@@ -2248,13 +2356,13 @@ var createClient = (options, deps) => {
2248
2356
  return client;
2249
2357
  };
2250
2358
  var activeRouteName = (state) => {
2251
- let s = state;
2359
+ let s2 = state;
2252
2360
  let name;
2253
- for (let depth = 0; s && Array.isArray(s.routes) && s.routes.length && depth < 20; depth++) {
2254
- const route = s.routes[typeof s.index === "number" ? s.index : s.routes.length - 1];
2361
+ for (let depth = 0; s2 && Array.isArray(s2.routes) && s2.routes.length && depth < 20; depth++) {
2362
+ const route = s2.routes[typeof s2.index === "number" ? s2.index : s2.routes.length - 1];
2255
2363
  if (!route) break;
2256
2364
  name = route.name ?? name;
2257
- s = route.state;
2365
+ s2 = route.state;
2258
2366
  }
2259
2367
  return name;
2260
2368
  };
@@ -2410,9 +2518,9 @@ var Finale = ({ kind, accent }) => {
2410
2518
  RN.View,
2411
2519
  { pointerEvents: "none", accessibilityElementsHidden: true, importantForAccessibility: "no-hide-descendants", style: { position: "absolute", left: 0, right: 0, top: 0, bottom: 0 } },
2412
2520
  ...bits.map((b, i) => {
2413
- const t = values[i];
2414
- const fade = t.interpolate({ inputRange: [0, 0.7, 1], outputRange: [1, 1, 0] });
2415
- const scale = b.shape === "star" ? t.interpolate({ inputRange: [0, 0.25, 1], outputRange: [0.2, 1.15, 0.6] }) : 1;
2521
+ const t2 = values[i];
2522
+ const fade = t2.interpolate({ inputRange: [0, 0.7, 1], outputRange: [1, 1, 0] });
2523
+ const scale = b.shape === "star" ? t2.interpolate({ inputRange: [0, 0.25, 1], outputRange: [0.2, 1.15, 0.6] }) : 1;
2416
2524
  return (0, import_react2.createElement)(
2417
2525
  A.View,
2418
2526
  {
@@ -2429,9 +2537,9 @@ var Finale = ({ kind, accent }) => {
2429
2537
  justifyContent: "center",
2430
2538
  opacity: fade,
2431
2539
  transform: [
2432
- { translateX: t.interpolate({ inputRange: [0, 1], outputRange: [0, b.dx] }) },
2433
- { translateY: t.interpolate({ inputRange: [0, 0.5, 1], outputRange: [0, b.mid, b.dy] }) },
2434
- { rotate: t.interpolate({ inputRange: [0, 1], outputRange: ["0deg", `${b.rot}deg`] }) },
2540
+ { translateX: t2.interpolate({ inputRange: [0, 1], outputRange: [0, b.dx] }) },
2541
+ { translateY: t2.interpolate({ inputRange: [0, 0.5, 1], outputRange: [0, b.mid, b.dy] }) },
2542
+ { rotate: t2.interpolate({ inputRange: [0, 1], outputRange: ["0deg", `${b.rot}deg`] }) },
2435
2543
  { scale }
2436
2544
  ]
2437
2545
  }
@@ -2443,10 +2551,9 @@ var Finale = ({ kind, accent }) => {
2443
2551
  };
2444
2552
  var LIGHT = { bg: "#ffffff", fg: "#1c1d20", muted: "#5d6068", soft: "#f4f5f7", line: "#e4e5e8" };
2445
2553
  var DARK = { bg: "#17181b", fg: "#f1f2f4", muted: "#a9acb3", soft: "#232428", line: "#2e2f35" };
2446
- var months = (n) => `${n} ${n === 1 ? "month" : "months"}`;
2447
2554
  var MRRocketFlows = (props) => {
2448
2555
  const [, setTick] = (0, import_react2.useState)(0);
2449
- (0, import_react2.useEffect)(() => props.flows.subscribe(() => setTick((t) => t + 1)), [props.flows]);
2556
+ (0, import_react2.useEffect)(() => props.flows.subscribe(() => setTick((t2) => t2 + 1)), [props.flows]);
2450
2557
  const fin = props.flows.finale?.() ?? null;
2451
2558
  const accent = props.appearance?.accent ?? props.flows.accent?.() ?? "#3f76ff";
2452
2559
  return (0, import_react2.createElement)(import_react2.Fragment, null, (0, import_react2.createElement)(FlowsBody, props), fin ? (0, import_react2.createElement)(Finale, { key: fin.key, kind: fin.kind, accent }) : null);
@@ -2456,8 +2563,8 @@ var FlowsBody = ({ flows, paywall, appearance = {} }) => {
2456
2563
  const dark = appearance.theme === "dark" || (appearance.theme ?? "auto") === "auto" && scheme === "dark";
2457
2564
  const p = { ...dark ? DARK : LIGHT, accent: appearance.accent ?? flows.accent?.() ?? "#3f76ff", danger: "#d92d20" };
2458
2565
  const [, setTick] = (0, import_react2.useState)(0);
2459
- (0, import_react2.useEffect)(() => flows.subscribe(() => setTick((t) => t + 1)), [flows]);
2460
- (0, import_react2.useEffect)(() => paywall?.subscribe(() => setTick((t) => t + 1)), [paywall]);
2566
+ (0, import_react2.useEffect)(() => flows.subscribe(() => setTick((t2) => t2 + 1)), [flows]);
2567
+ (0, import_react2.useEffect)(() => paywall?.subscribe(() => setTick((t2) => t2 + 1)), [paywall]);
2461
2568
  const pw = paywall?.view() ?? null;
2462
2569
  const v = flows.view();
2463
2570
  const [comment, setComment] = (0, import_react2.useState)("");
@@ -2546,7 +2653,7 @@ var FlowsBody = ({ flows, paywall, appearance = {} }) => {
2546
2653
  { length: w.total },
2547
2654
  (_, i) => (0, import_react2.createElement)(RN.View, { key: i, style: { width: 16, height: 4, borderRadius: 4, backgroundColor: i <= w.index ? p.accent : p.line } })
2548
2655
  ) : [],
2549
- close("Close", host.close)
2656
+ close(t("Close"), host.close)
2550
2657
  ),
2551
2658
  title(pg.title),
2552
2659
  pg.body ? muted(pg.body, { fontSize: 15 }) : null,
@@ -2591,8 +2698,8 @@ var FlowsBody = ({ flows, paywall, appearance = {} }) => {
2591
2698
  )
2592
2699
  ) : null,
2593
2700
  w.error ? text2(w.error, { color: p.danger, fontSize: 13 }) : null,
2594
- btn(w.busy ? "One moment\u2026" : pg.button || "Continue", "primary", host.next, w.busy || pg.kind === "plans" && !w.plan),
2595
- w.index ? btn("Back", "quiet", host.back, w.busy) : null
2701
+ btn(w.busy ? t("One moment\u2026") : pg.button || t("Continue"), "primary", host.next, w.busy || pg.kind === "plans" && !w.plan),
2702
+ w.index ? btn(t("Back"), "quiet", host.back, w.busy) : null
2596
2703
  ],
2597
2704
  host.close
2598
2705
  );
@@ -2607,7 +2714,7 @@ var FlowsBody = ({ flows, paywall, appearance = {} }) => {
2607
2714
  { length: v.steps },
2608
2715
  (_, i) => (0, import_react2.createElement)(RN.View, { key: i, style: { width: 16, height: 4, borderRadius: 4, backgroundColor: i < v.step ? p.accent : p.line } })
2609
2716
  ) : [],
2610
- close("Close", flows.cancel.close)
2717
+ close(t("Close"), flows.cancel.close)
2611
2718
  );
2612
2719
  const error = v.error ? text2(v.error, { color: p.danger, fontSize: 13 }) : null;
2613
2720
  let body = [];
@@ -2638,45 +2745,45 @@ var FlowsBody = ({ flows, paywall, appearance = {} }) => {
2638
2745
  );
2639
2746
  if (v.screen === "survey")
2640
2747
  body = [
2641
- title(v.flow.question || "Why are you leaving?"),
2748
+ title(v.flow.question || t("Why are you leaving?")),
2642
2749
  choices(v.flow.reasons.map((r) => ({ key: r.id, label: r.label, on: v.reason === r.id, press: () => flows.cancel.pick(r.id) }))),
2643
- btn("Continue", "primary", flows.cancel.next, !v.reason),
2644
- btn("Never mind, keep my plan", "quiet", flows.cancel.close)
2750
+ btn(t("Continue"), "primary", flows.cancel.next, !v.reason),
2751
+ btn(t("Never mind, keep my plan"), "quiet", flows.cancel.close)
2645
2752
  ];
2646
2753
  else if (v.screen === "followup" && v.followUp) {
2647
2754
  const f = v.followUp;
2648
2755
  const label = v.flow.reasons.find((r) => r.id === v.reason)?.label;
2649
2756
  body = [
2650
- label ? muted(`You said: ${label}`) : null,
2757
+ label ? muted(tf("You said: {label}", { label })) : null,
2651
2758
  title(f.question),
2652
- f.answers.length ? choices(f.answers.map((a, i) => ({ key: String(i), label: a, on: v.answer === a, press: () => flows.cancel.answer(a) }))) : input("Your answer (optional)", v.answer ?? "", flows.cancel.answer),
2653
- btn("Continue", "primary", flows.cancel.next),
2654
- btn("Never mind, keep my plan", "quiet", flows.cancel.close)
2759
+ f.answers.length ? choices(f.answers.map((a, i) => ({ key: String(i), label: f.answerLabels?.[i] || a, on: v.answer === a, press: () => flows.cancel.answer(a) }))) : input(t("Your answer (optional)"), v.answer ?? "", flows.cancel.answer),
2760
+ btn(t("Continue"), "primary", flows.cancel.next),
2761
+ btn(t("Never mind, keep my plan"), "quiet", flows.cancel.close)
2655
2762
  ];
2656
2763
  } else if (v.screen === "offer" && v.offer) {
2657
2764
  const o = v.offer;
2658
2765
  const label = v.flow.reasons.find((r) => r.id === v.reason)?.label;
2659
- const detail = o.kind === "discount" && o.percent ? v.price ? `${money(discounted(v.price.amount, o.percent), v.price.currency)} instead of ${money(v.price.amount, v.price.currency)}${o.months ? ` for ${months(o.months)}` : ""}` : `${o.percent}% off${o.months ? ` for ${months(o.months)}` : ""}` : o.kind === "pause" && o.months ? `Pause for up to ${months(o.months)}. Your data stays.` : o.kind === "downgrade" && o.planName ? o.planName : null;
2766
+ const detail = o.kind === "discount" && o.percent ? v.price ? `${tf("{price} instead of {old}", { price: money(discounted(v.price.amount, o.percent), v.price.currency), old: money(v.price.amount, v.price.currency) })}${o.months ? ` ${forMonths(o.months)}` : ""}` : `${tf("{percent}% off", { percent: o.percent })}${o.months ? ` ${forMonths(o.months)}` : ""}` : o.kind === "pause" && o.months ? tf("Pause for up to {months}. Your data stays.", { months: monthsWord(o.months) }) : o.kind === "downgrade" && o.planName ? o.planName : null;
2660
2767
  body = [
2661
- label ? muted(`You said: ${label}`) : null,
2768
+ label ? muted(tf("You said: {label}", { label })) : null,
2662
2769
  title(o.headline),
2663
2770
  detail ? (0, import_react2.createElement)(RN.View, { style: { backgroundColor: p.soft, borderRadius: 10, padding: 12 } }, text2(detail, { fontWeight: "600" })) : null,
2664
- o.kind === "contact" ? input("What would make you stay?") : null,
2665
- btn(v.busy ? "One moment\u2026" : offerButton(o), "primary", () => flows.cancel.accept(comment), v.busy),
2666
- btn("No thanks, continue cancelling", "quiet", flows.cancel.decline, v.busy)
2771
+ o.kind === "contact" ? input(t("What would make you stay?")) : null,
2772
+ btn(v.busy ? t("One moment\u2026") : offerButton(o), "primary", () => flows.cancel.accept(comment), v.busy),
2773
+ btn(t("No thanks, continue cancelling"), "quiet", flows.cancel.decline, v.busy)
2667
2774
  ];
2668
2775
  } else
2669
2776
  body = [
2670
2777
  title(v.flow.confirm.title),
2671
2778
  v.flow.confirm.body ? muted(v.flow.confirm.body, { fontSize: 15 }) : null,
2672
- v.steps > 1 ? input("Anything else we should know? (optional)") : null,
2673
- btn(v.busy ? "One moment\u2026" : v.flow.confirm.button, "danger", () => flows.cancel.confirm(comment), v.busy),
2674
- btn("Keep my plan", "secondary", flows.cancel.close, v.busy)
2779
+ v.steps > 1 ? input(t("Anything else we should know? (optional)")) : null,
2780
+ btn(v.busy ? t("One moment\u2026") : v.flow.confirm.button, "danger", () => flows.cancel.confirm(comment), v.busy),
2781
+ btn(t("Keep my plan"), "secondary", flows.cancel.close, v.busy)
2675
2782
  ];
2676
2783
  return sheet([top, ...body, error], flows.cancel.close);
2677
2784
  }
2678
- const media = (s) => {
2679
- const m2 = mediaOf(s);
2785
+ const media = (s2) => {
2786
+ const m2 = mediaOf(s2);
2680
2787
  if (!m2) return null;
2681
2788
  if (m2.kind === "image")
2682
2789
  return RN.Image ? (0, import_react2.createElement)(RN.Image, {
@@ -2688,12 +2795,12 @@ var FlowsBody = ({ flows, paywall, appearance = {} }) => {
2688
2795
  }) : null;
2689
2796
  const v2 = videoEmbed(m2.url);
2690
2797
  const open = v2?.kind === "file" ? v2.src : m2.url;
2691
- return RN.Linking ? btn("\u25B6 Watch the video", "secondary", () => void RN.Linking?.openURL(open).catch(() => {
2798
+ return RN.Linking ? btn(`\u25B6 ${t("Watch the video")}`, "secondary", () => void RN.Linking?.openURL(open).catch(() => {
2692
2799
  })) : null;
2693
2800
  };
2694
2801
  if (v.type === "step" && v.step.kind === "question") {
2695
- const s = v.step;
2696
- const q = s.question;
2802
+ const s2 = v.step;
2803
+ const q = s2.question;
2697
2804
  const ok = answerOk(q, v.answer);
2698
2805
  return sheet(
2699
2806
  [
@@ -2704,48 +2811,84 @@ var FlowsBody = ({ flows, paywall, appearance = {} }) => {
2704
2811
  { length: v.total },
2705
2812
  (_, i) => (0, import_react2.createElement)(RN.View, { key: i, style: { width: 16, height: 4, borderRadius: 4, backgroundColor: i <= v.index ? p.accent : p.line } })
2706
2813
  ) : [],
2707
- close("Close", flows.onboarding.dismiss)
2814
+ close(t("Close"), flows.onboarding.dismiss)
2708
2815
  ),
2709
2816
  v.index === 0 && v.flow.icon ? (0, import_react2.createElement)(RN.View, { key: "icon" }, iconNode(v.flow.icon, 36, p.accent)) : null,
2710
- title(s.title),
2711
- s.body ? muted(s.body, { fontSize: 15 }) : null,
2817
+ title(s2.title),
2818
+ s2.body ? muted(s2.body, { fontSize: 15 }) : null,
2712
2819
  q?.type === "choice" ? (0, import_react2.createElement)(
2713
2820
  RN.ScrollView,
2714
2821
  { key: "opts", style: { maxHeight: 320 }, contentContainerStyle: { gap: 6 }, accessibilityRole: "radiogroup" },
2715
- ...(q.options ?? []).map(
2716
- (o) => (0, import_react2.createElement)(
2822
+ ...[...q.options ?? [], ...q.other ? [{ id: OTHER_PREFIX, label: q.otherLabel || t("Other") }] : []].map((o) => {
2823
+ const isOther = o.id === OTHER_PREFIX;
2824
+ const on = isOther ? !!v.answer?.startsWith(OTHER_PREFIX) : v.answer === o.id;
2825
+ return (0, import_react2.createElement)(
2717
2826
  RN.Pressable,
2718
2827
  {
2719
2828
  key: o.id,
2720
- onPress: () => flows.onboarding.answer(o.id),
2829
+ onPress: () => isOther && on ? void 0 : flows.onboarding.answer(o.id),
2721
2830
  accessibilityRole: "radio",
2722
- accessibilityState: { checked: v.answer === o.id },
2831
+ accessibilityState: { checked: on },
2723
2832
  style: {
2724
2833
  minHeight: 44,
2725
2834
  paddingHorizontal: 12,
2726
2835
  borderRadius: 10,
2727
2836
  borderWidth: 1,
2728
- borderColor: v.answer === o.id ? p.accent : p.line,
2729
- backgroundColor: v.answer === o.id ? p.soft : p.bg,
2837
+ borderColor: on ? p.accent : p.line,
2838
+ backgroundColor: on ? p.soft : p.bg,
2730
2839
  justifyContent: "center"
2731
2840
  }
2732
2841
  },
2733
2842
  text2(o.label, { fontWeight: "500" })
2734
- )
2735
- )
2843
+ );
2844
+ }),
2845
+ q.other && v.answer?.startsWith(OTHER_PREFIX) ? (0, import_react2.createElement)(RN.TextInput, {
2846
+ key: "other",
2847
+ value: v.answer.slice(OTHER_PREFIX.length),
2848
+ onChangeText: (t2) => flows.onboarding.answer(OTHER_PREFIX + t2),
2849
+ onSubmitEditing: flows.onboarding.next,
2850
+ placeholder: t("Your answer"),
2851
+ placeholderTextColor: p.muted,
2852
+ maxLength: 490,
2853
+ accessibilityLabel: q.otherLabel || t("Other"),
2854
+ style: { height: 44, borderWidth: 1, borderColor: p.line, borderRadius: 10, paddingHorizontal: 12, color: p.fg }
2855
+ }) : null
2856
+ ) : q?.type === "scale" ? (0, import_react2.createElement)(
2857
+ RN.View,
2858
+ { key: "scale", style: { gap: 6 } },
2859
+ (0, import_react2.createElement)(
2860
+ RN.View,
2861
+ { style: { flexDirection: "row", gap: 4 }, accessibilityRole: "radiogroup" },
2862
+ ...scaleOf(q).map((n) => {
2863
+ const on = v.answer === String(n);
2864
+ return (0, import_react2.createElement)(
2865
+ RN.Pressable,
2866
+ {
2867
+ key: n,
2868
+ onPress: () => flows.onboarding.answer(String(n)),
2869
+ accessibilityRole: "radio",
2870
+ accessibilityState: { checked: on },
2871
+ style: { flex: 1, height: 44, borderRadius: 8, borderWidth: 1, borderColor: on ? p.accent : p.line, backgroundColor: on ? p.accent : p.bg, alignItems: "center", justifyContent: "center" }
2872
+ },
2873
+ text2(String(n), { fontWeight: "500", color: on ? "#fff" : p.fg })
2874
+ );
2875
+ })
2876
+ ),
2877
+ q.lowLabel || q.highLabel ? (0, import_react2.createElement)(RN.View, { style: { flexDirection: "row", justifyContent: "space-between" } }, muted(q.lowLabel || ""), muted(q.highLabel || "")) : null
2736
2878
  ) : q ? (0, import_react2.createElement)(RN.TextInput, {
2737
2879
  key: "answer",
2738
2880
  value: v.answer ?? "",
2739
- onChangeText: (t) => flows.onboarding.answer(t),
2881
+ onChangeText: (t2) => flows.onboarding.answer(t2),
2740
2882
  onSubmitEditing: flows.onboarding.next,
2741
- placeholder: q.placeholder || "Your answer",
2883
+ placeholder: q.placeholder || t("Your answer"),
2742
2884
  placeholderTextColor: p.muted,
2743
2885
  maxLength: 500,
2744
- accessibilityLabel: s.title,
2886
+ accessibilityLabel: s2.title,
2745
2887
  style: { height: 44, borderWidth: 1, borderColor: p.line, borderRadius: 10, paddingHorizontal: 12, color: p.fg }
2746
2888
  }) : null,
2747
- btn(s.button || (v.last ? "Done" : "Continue"), "primary", flows.onboarding.next, !ok),
2748
- v.back ? btn("Back", "quiet", flows.onboarding.back) : null
2889
+ btn(s2.button || (v.last ? t("Done") : t("Continue")), "primary", flows.onboarding.next, !ok),
2890
+ q?.optional ? btn(q.skipLabel || t("Skip"), "quiet", flows.onboarding.skip) : null,
2891
+ v.back ? btn(t("Back"), "quiet", flows.onboarding.back) : null
2749
2892
  ],
2750
2893
  flows.onboarding.dismiss
2751
2894
  );
@@ -2756,14 +2899,14 @@ var FlowsBody = ({ flows, paywall, appearance = {} }) => {
2756
2899
  (0, import_react2.createElement)(
2757
2900
  RN.View,
2758
2901
  { style: { flexDirection: "row", alignItems: "center" } },
2759
- v.total > 1 ? muted(`Step ${v.index + 1} of ${v.total}`) : null,
2760
- close("Close the tour", flows.onboarding.dismiss)
2902
+ v.total > 1 ? muted(tf("Step {n} of {total}", { n: v.index + 1, total: v.total })) : null,
2903
+ close(t("Close the tour"), flows.onboarding.dismiss)
2761
2904
  ),
2762
2905
  v.step.kind === "modal" && v.flow.icon ? (0, import_react2.createElement)(RN.View, { key: "icon" }, iconNode(v.flow.icon, 36, p.accent)) : null,
2763
2906
  media(v.step),
2764
2907
  title(v.step.title),
2765
2908
  v.step.body ? muted(v.step.body, { fontSize: 15 }) : null,
2766
- btn(v.step.button || (v.last ? "Done" : "Next"), "primary", flows.onboarding.next)
2909
+ btn(v.step.button || (v.last ? t("Done") : t("Next")), "primary", flows.onboarding.next)
2767
2910
  ],
2768
2911
  flows.onboarding.dismiss
2769
2912
  );
@@ -2777,9 +2920,9 @@ var FlowsBody = ({ flows, paywall, appearance = {} }) => {
2777
2920
  RN.View,
2778
2921
  { key: "t", style: { flexDirection: "row", alignItems: "center" } },
2779
2922
  title(v.flow.name),
2780
- close("Hide the checklist", flows.onboarding.dismiss)
2923
+ close(t("Hide the checklist"), flows.onboarding.dismiss)
2781
2924
  ),
2782
- muted(`${done} of ${v.items.length} done`),
2925
+ muted(tf("{done} of {total} done", { done, total: v.items.length })),
2783
2926
  ...v.items.map(
2784
2927
  ({ step, done: d, opens }) => (0, import_react2.createElement)(
2785
2928
  RN.Pressable,
@@ -2831,14 +2974,14 @@ var FlowsBody = ({ flows, paywall, appearance = {} }) => {
2831
2974
  RN.View,
2832
2975
  { key: "t", style: { flexDirection: "row", alignItems: "center", gap: 8 } },
2833
2976
  (0, import_react2.createElement)(RN.View, { style: { flex: 1 } }, text2(m.title, { fontWeight: "700" }), m.body ? muted(m.body) : null),
2834
- close("Dismiss", flows.message.dismiss)
2977
+ close(t("Dismiss"), flows.message.dismiss)
2835
2978
  ),
2836
2979
  m.button ? btn(m.button, "primary", flows.message.click) : null
2837
2980
  ])
2838
2981
  );
2839
2982
  return sheet(
2840
2983
  [
2841
- (0, import_react2.createElement)(RN.View, { key: "t", style: { flexDirection: "row" } }, close("Dismiss", flows.message.dismiss)),
2984
+ (0, import_react2.createElement)(RN.View, { key: "t", style: { flexDirection: "row" } }, close(t("Dismiss"), flows.message.dismiss)),
2842
2985
  title(m.title),
2843
2986
  m.body ? muted(m.body, { fontSize: 15 }) : null,
2844
2987
  m.button ? btn(m.button, "primary", flows.message.click) : null
@@ -2912,10 +3055,10 @@ var expoPush = () => {
2912
3055
  },
2913
3056
  token: async (projectId) => {
2914
3057
  if (RN2.Platform.OS === "android" && n.setNotificationChannelAsync)
2915
- await n.setNotificationChannelAsync("default", { name: "Default", importance: n.AndroidImportance?.HIGH ?? 4 });
3058
+ await n.setNotificationChannelAsync("default", { name: t("Default"), importance: n.AndroidImportance?.HIGH ?? 4 });
2916
3059
  const id = projectId ?? constants.expoConfig?.extra?.eas?.projectId ?? constants.easConfig?.projectId;
2917
- const t = await n.getExpoPushTokenAsync(id ? { projectId: id } : void 0);
2918
- return typeof t?.data === "string" && t.data ? t.data : null;
3060
+ const push = await n.getExpoPushTokenAsync(id ? { projectId: id } : void 0);
3061
+ return typeof push?.data === "string" && push.data ? push.data : null;
2919
3062
  },
2920
3063
  onOpen: (listener) => {
2921
3064
  const sub = n.addNotificationResponseReceivedListener((r) => listener(dataOf(r) ?? {}));
@@ -2926,8 +3069,8 @@ var expoPush = () => {
2926
3069
  };
2927
3070
  var asyncStorage = () => {
2928
3071
  const m = asyncStorageModule;
2929
- const s = m?.default ?? m;
2930
- return s && typeof s.getItem === "function" && typeof s.setItem === "function" && typeof s.removeItem === "function" ? s : null;
3072
+ const s2 = m?.default ?? m;
3073
+ return s2 && typeof s2.getItem === "function" && typeof s2.setItem === "function" && typeof s2.removeItem === "function" ? s2 : null;
2931
3074
  };
2932
3075
  var DEVICE_TYPES = { 1: "phone", 2: "tablet", 3: "desktop", 4: "tv" };
2933
3076
  var text = (v, max = 80) => typeof v === "string" && v.trim() ? v.trim().slice(0, max) : typeof v === "number" ? String(v) : void 0;
@@ -2971,10 +3114,10 @@ var recorderDeps = () => {
2971
3114
  }
2972
3115
  return {
2973
3116
  capture: async (width) => {
2974
- const s = RN2.Dimensions.get("screen");
2975
- if (!s?.width) return null;
3117
+ const s2 = RN2.Dimensions.get("screen");
3118
+ if (!s2?.width) return null;
2976
3119
  const w = Math.round(width);
2977
- const h3 = Math.round(s.height / s.width * width);
3120
+ const h3 = Math.round(s2.height / s2.width * width);
2978
3121
  const img = await capture({ format: "jpg", quality: 0.5, result: "base64", width: w, height: h3 });
2979
3122
  return img ? { img: img.replace(/^data:image\/\w+;base64,/, ""), w, h: h3 } : null;
2980
3123
  },
@@ -3001,8 +3144,8 @@ var nativeDeps = (overrides = {}) => ({
3001
3144
  context: readContext,
3002
3145
  appId: text(expoApplication?.applicationId, 150),
3003
3146
  screen: () => {
3004
- const s = RN2.Dimensions.get("screen");
3005
- return s ? { w: s.width, h: s.height } : void 0;
3147
+ const s2 = RN2.Dimensions.get("screen");
3148
+ return s2 ? { w: s2.width, h: s2.height } : void 0;
3006
3149
  },
3007
3150
  initialUrl: () => RN2.Linking.getInitialURL(),
3008
3151
  openUrl: (url) => void RN2.Linking.openURL(url).catch(() => void 0),
@@ -3100,7 +3243,7 @@ var kit = (p) => {
3100
3243
  RN3.View,
3101
3244
  { style: { flexDirection: "row", alignItems: "center", paddingHorizontal: 16, paddingTop: 16, paddingBottom: 12, borderBottomWidth: 1, borderBottomColor: p.line } },
3102
3245
  title(heading),
3103
- closeX("Close", onClose)
3246
+ closeX(t("Close"), onClose)
3104
3247
  ),
3105
3248
  ...children
3106
3249
  )
@@ -3108,13 +3251,13 @@ var kit = (p) => {
3108
3251
  return { text: text2, muted, title, btn, closeX, field, chip, page };
3109
3252
  };
3110
3253
  var timeAgo = (iso) => {
3111
- const t = new Date(iso).getTime();
3112
- if (Number.isNaN(t)) return "";
3113
- const d = Math.max(0, Date.now() - t) / 1e3;
3114
- if (d < 60) return "just now";
3115
- if (d < 3600) return `${Math.floor(d / 60)}m ago`;
3116
- if (d < 86400) return `${Math.floor(d / 3600)}h ago`;
3117
- return `${Math.floor(d / 86400)}d ago`;
3254
+ const at = new Date(iso).getTime();
3255
+ if (Number.isNaN(at)) return "";
3256
+ const d = Math.max(0, Date.now() - at) / 1e3;
3257
+ if (d < 60) return t("just now");
3258
+ if (d < 3600) return tf("{n}m ago", { n: Math.floor(d / 60) });
3259
+ if (d < 86400) return tf("{n}h ago", { n: Math.floor(d / 3600) });
3260
+ return tf("{n}d ago", { n: Math.floor(d / 86400) });
3118
3261
  };
3119
3262
  var opener = () => {
3120
3263
  let open = false;
@@ -3153,8 +3296,8 @@ var SurveySheet = ({ survey, api, p, onDone }) => {
3153
3296
  }, [api, survey.id]);
3154
3297
  (0, import_react3.useEffect)(() => {
3155
3298
  if (!done) return;
3156
- const t = setTimeout(onDone, 2500);
3157
- return () => clearTimeout(t);
3299
+ const t2 = setTimeout(onDone, 2500);
3300
+ return () => clearTimeout(t2);
3158
3301
  }, [done, onDone]);
3159
3302
  const finish = (completed, all = answers) => {
3160
3303
  if (finished.current) return;
@@ -3166,7 +3309,7 @@ var SurveySheet = ({ survey, api, p, onDone }) => {
3166
3309
  onDone();
3167
3310
  };
3168
3311
  const next = () => {
3169
- if (q.required && !isAnswered(value)) return setError("Please answer this question.");
3312
+ if (q.required && !isAnswered(value)) return setError(t("Please answer this question."));
3170
3313
  setError("");
3171
3314
  if (!last) return setStep(step + 1);
3172
3315
  finish(true);
@@ -3196,14 +3339,14 @@ var SurveySheet = ({ survey, api, p, onDone }) => {
3196
3339
  onPress: () => pick(n),
3197
3340
  accessibilityRole: "radio",
3198
3341
  accessibilityState: { checked: on },
3199
- accessibilityLabel: `${n} of ${scale - 1 + start}`,
3342
+ accessibilityLabel: tf("{n} of {total}", { n, total: scale - 1 + start }),
3200
3343
  style: { width: 40, height: 40, borderRadius: 8, alignItems: "center", justifyContent: "center", borderWidth: 1, borderColor: on ? p.accent : p.line, backgroundColor: on ? p.accent : p.bg }
3201
3344
  },
3202
3345
  k.text(String(n), { color: on ? "#ffffff" : p.fg, fontWeight: "600" })
3203
3346
  );
3204
3347
  })
3205
3348
  ),
3206
- q.low || q.high || q.kind === "nps" ? (0, import_react3.createElement)(RN3.View, { style: { flexDirection: "row", justifyContent: "space-between" } }, k.muted(q.low ?? (q.kind === "nps" ? "Not likely" : "")), k.muted(q.high ?? (q.kind === "nps" ? "Very likely" : ""))) : null
3349
+ q.low || q.high || q.kind === "nps" ? (0, import_react3.createElement)(RN3.View, { style: { flexDirection: "row", justifyContent: "space-between" } }, k.muted(q.low ?? (q.kind === "nps" ? t("Not likely") : "")), k.muted(q.high ?? (q.kind === "nps" ? t("Very likely") : ""))) : null
3207
3350
  );
3208
3351
  } else if (q.kind === "single" || q.kind === "multi") {
3209
3352
  const multi = q.kind === "multi";
@@ -3211,7 +3354,7 @@ var SurveySheet = ({ survey, api, p, onDone }) => {
3211
3354
  input = (0, import_react3.createElement)(
3212
3355
  RN3.View,
3213
3356
  { style: { gap: 6 } },
3214
- ...(q.options ?? []).map((o) => {
3357
+ ...(q.options ?? []).map((o, i) => {
3215
3358
  const on = multi ? picked.includes(o) : value === o;
3216
3359
  return (0, import_react3.createElement)(
3217
3360
  RN3.Pressable,
@@ -3222,24 +3365,25 @@ var SurveySheet = ({ survey, api, p, onDone }) => {
3222
3365
  accessibilityState: { checked: on },
3223
3366
  style: { minHeight: 44, paddingHorizontal: 12, borderRadius: 10, justifyContent: "center", borderWidth: 1, borderColor: on ? p.accent : p.line, backgroundColor: on ? p.soft : p.bg }
3224
3367
  },
3225
- k.text(o, { fontWeight: "500" })
3368
+ // The label in the person's language; what is recorded stays the option itself.
3369
+ k.text(q.labels?.[i] || o, { fontWeight: "500" })
3226
3370
  );
3227
3371
  })
3228
3372
  );
3229
- } else input = k.field({ value: value ?? "", onChange: pick, placeholder: q.placeholder ?? "Type your answer", multiline: true });
3230
- const body = done ? [k.text(survey.thanks || "Thanks for your feedback.", { fontWeight: "600", textAlign: "center", paddingVertical: 16 })] : [
3373
+ } else input = k.field({ value: value ?? "", onChange: pick, placeholder: q.placeholder ?? t("Type your answer"), multiline: true });
3374
+ const body = done ? [k.text(survey.thanks || t("Thanks for your feedback."), { fontWeight: "600", textAlign: "center", paddingVertical: 16 })] : [
3231
3375
  (0, import_react3.createElement)(
3232
3376
  RN3.View,
3233
3377
  { key: "top", style: { flexDirection: "row", alignItems: "center", gap: 4 } },
3234
3378
  ...survey.questions.map((x, i) => (0, import_react3.createElement)(RN3.View, { key: x.key, style: { width: 16, height: 4, borderRadius: 4, backgroundColor: i <= step ? p.accent : p.line } })),
3235
3379
  (0, import_react3.createElement)(RN3.View, { style: { flex: 1 } }),
3236
- k.closeX("Close survey", dismiss)
3380
+ k.closeX(t("Close survey"), dismiss)
3237
3381
  ),
3238
- (0, import_react3.createElement)(RN3.Text, { key: "q", accessibilityRole: "header", style: { color: p.fg, fontSize: 17, fontWeight: "700" } }, q.title, q.required ? "" : (0, import_react3.createElement)(RN3.Text, { style: { color: p.muted, fontSize: 13, fontWeight: "400" } }, " optional")),
3382
+ (0, import_react3.createElement)(RN3.Text, { key: "q", accessibilityRole: "header", style: { color: p.fg, fontSize: 17, fontWeight: "700" } }, q.title, q.required ? "" : (0, import_react3.createElement)(RN3.Text, { style: { color: p.muted, fontSize: 13, fontWeight: "400" } }, ` ${t("optional")}`)),
3239
3383
  q.description ? k.muted(q.description) : null,
3240
3384
  input,
3241
3385
  error ? k.text(error, { color: p.danger, fontSize: 13 }) : null,
3242
- k.btn(last ? "Send" : "Next", "primary", next)
3386
+ k.btn(last ? t("Send") : t("Next"), "primary", next)
3243
3387
  ];
3244
3388
  return (0, import_react3.createElement)(
3245
3389
  RN3.Modal,
@@ -3272,9 +3416,9 @@ var MRRocketSurveys = ({ client, appearance }) => {
3272
3416
  const api = client.api();
3273
3417
  if (stop || !api) return;
3274
3418
  api.survey("", ctrl.signal).then((r) => {
3275
- const s = r.survey;
3276
- if (stop || !s || !s.questions.length || client.isSurveyDone(s.id)) return;
3277
- setSurvey({ s, api });
3419
+ const s2 = r.survey;
3420
+ if (stop || !s2 || !s2.questions.length || client.isSurveyDone(s2.id)) return;
3421
+ setSurvey({ s: s2, api });
3278
3422
  }).catch(() => void 0);
3279
3423
  });
3280
3424
  }, 3e3);
@@ -3312,7 +3456,7 @@ var FeedbackSheet = ({ client, p, title }) => {
3312
3456
  const user = client.getUser();
3313
3457
  (0, import_react3.useEffect)(() => {
3314
3458
  const api = client.api();
3315
- if (!api) return setError("Feedback is off while tracking is off.");
3459
+ if (!api) return setError(t("Feedback is off while tracking is off."));
3316
3460
  const ctrl = new AbortController();
3317
3461
  api.list({ sort: "top" }, ctrl.signal).then((l) => {
3318
3462
  setPosts(l.posts);
@@ -3345,8 +3489,8 @@ var FeedbackSheet = ({ client, p, title }) => {
3345
3489
  const tabs = (0, import_react3.createElement)(
3346
3490
  RN3.View,
3347
3491
  { style: { flexDirection: "row", gap: 8, paddingHorizontal: 16, paddingTop: 12 } },
3348
- k.chip("Top requests", tab === "top", () => setTab("top")),
3349
- k.chip("Suggest", tab === "new", () => {
3492
+ k.chip(t("Top requests"), tab === "top", () => setTab("top")),
3493
+ k.chip(t("Suggest"), tab === "new", () => {
3350
3494
  setTab("new");
3351
3495
  setSent(false);
3352
3496
  })
@@ -3355,8 +3499,8 @@ var FeedbackSheet = ({ client, p, title }) => {
3355
3499
  RN3.ScrollView,
3356
3500
  { style: { flex: 1 }, contentContainerStyle: { padding: 16, gap: 10 } },
3357
3501
  error ? k.text(error, { color: p.danger, fontSize: 13 }) : null,
3358
- posts === null && !error ? k.muted("Loading\u2026") : null,
3359
- posts?.length === 0 ? k.muted("No requests yet. Be the first to suggest one.") : null,
3502
+ posts === null && !error ? k.muted(t("Loading\u2026")) : null,
3503
+ posts?.length === 0 ? k.muted(t("No requests yet. Be the first to suggest one.")) : null,
3360
3504
  ...(posts ?? []).map(
3361
3505
  (post) => (0, import_react3.createElement)(
3362
3506
  RN3.View,
@@ -3367,7 +3511,7 @@ var FeedbackSheet = ({ client, p, title }) => {
3367
3511
  onPress: () => vote(post),
3368
3512
  accessibilityRole: "button",
3369
3513
  accessibilityState: { selected: post.voted },
3370
- accessibilityLabel: `${post.voted ? "Remove your vote from" : "Upvote"} ${post.title}. ${post.votes} votes`,
3514
+ accessibilityLabel: post.voted ? tf("Remove your vote from {title}. {votes}", { title: post.title, votes: post.votes === 1 ? t("1 vote") : tf("{n} votes", { n: post.votes }) }) : tf("Upvote {title}. {votes}", { title: post.title, votes: post.votes === 1 ? t("1 vote") : tf("{n} votes", { n: post.votes }) }),
3371
3515
  style: { width: 44, minHeight: 48, borderRadius: 10, alignItems: "center", justifyContent: "center", borderWidth: 1, borderColor: post.voted ? p.accent : p.line }
3372
3516
  },
3373
3517
  k.text("\u25B2", { fontSize: 11, color: post.voted ? p.accent : p.muted }),
@@ -3380,36 +3524,39 @@ var FeedbackSheet = ({ client, p, title }) => {
3380
3524
  const form = (0, import_react3.createElement)(
3381
3525
  RN3.ScrollView,
3382
3526
  { style: { flex: 1 }, contentContainerStyle: { padding: 16, gap: 10 }, keyboardShouldPersistTaps: "handled" },
3383
- sent ? k.text("Thanks! Your request is on the board.", { fontWeight: "600" }) : null,
3527
+ sent ? k.text(t("Thanks! Your request is on the board."), { fontWeight: "600" }) : null,
3384
3528
  boards.length > 1 ? (0, import_react3.createElement)(RN3.View, { style: { flexDirection: "row", flexWrap: "wrap", gap: 6 } }, ...boards.map((b) => k.chip(b.label, board === b.id, () => setBoard(b.id), b.id))) : null,
3385
- k.field({ value: subject, onChange: setSubject, placeholder: "Short title", max: 120 }),
3386
- k.field({ value: details, onChange: setDetails, placeholder: "Details (optional)", multiline: true }),
3529
+ k.field({ value: subject, onChange: setSubject, placeholder: t("Short title"), max: 120 }),
3530
+ k.field({ value: details, onChange: setDetails, placeholder: t("Details (optional)"), multiline: true }),
3387
3531
  ...user?.email ? [] : [
3388
- k.muted("Your name and email are optional; they let the team follow up."),
3389
- k.field({ value: name, onChange: setName, placeholder: "Your name (optional)", max: 100 }),
3390
- k.field({ value: email, onChange: setEmail, placeholder: "Your email (optional)", max: 200, email: true })
3532
+ k.muted(t("Your name and email are optional; they let the team follow up.")),
3533
+ k.field({ value: name, onChange: setName, placeholder: t("Your name (optional)"), max: 100 }),
3534
+ k.field({ value: email, onChange: setEmail, placeholder: t("Your email (optional)"), max: 200, email: true })
3391
3535
  ],
3392
3536
  error ? k.text(error, { color: p.danger, fontSize: 13 }) : null,
3393
- k.btn(sending ? "Sending\u2026" : "Send request", "primary", submit, subject.trim().length < 3 || sending)
3537
+ k.btn(sending ? t("Sending\u2026") : t("Send request"), "primary", submit, subject.trim().length < 3 || sending)
3394
3538
  );
3395
3539
  return k.page(title, close, [tabs, tab === "top" ? list : form]);
3396
3540
  };
3397
3541
  var MRRocketFeedback = ({ client, appearance, options = {} }) => {
3398
3542
  const p = usePalette(appearance);
3399
3543
  const open = feedbackOpen.useOpen();
3400
- return open ? (0, import_react3.createElement)(FeedbackSheet, { client, p, title: options.title ?? "Feedback" }) : null;
3544
+ return open ? (0, import_react3.createElement)(FeedbackSheet, { client, p, title: options.title ?? t("Feedback") }) : null;
3401
3545
  };
3402
3546
  var POLL_OPEN_MS = 4e3;
3403
3547
  var POLL_WAITING_MS = 8e3;
3404
3548
  var POLL_IDLE_MS = 3e4;
3549
+ var POLL_QUIET_MS = 6e4;
3405
3550
  var WAITING_FOR_MS = 10 * 6e4;
3406
3551
  var EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
3407
- var whoLabel = { visitor: "You", team: "Team", ai: "AI agent", auto: "Team" };
3408
- var who = (from, agentName) => from === "ai" && agentName?.trim() || whoLabel[from];
3552
+ var whoLabel = (from) => from === "visitor" ? t("You") : from === "ai" ? t("AI agent") : t("Team");
3553
+ var who = (from, agentName) => from === "ai" && agentName?.trim() || whoLabel(from);
3409
3554
  var msgView = "home";
3555
+ var msgChat = null;
3410
3556
  var messengerOpen = opener();
3411
- var openAt = (v = "home") => {
3557
+ var openAt = (v = "home", chatId = null) => {
3412
3558
  msgView = v;
3559
+ msgChat = chatId;
3413
3560
  messengerOpen.set(true);
3414
3561
  };
3415
3562
  var useMessenger = () => ({
@@ -3436,7 +3583,10 @@ try {
3436
3583
  imagePicker = null;
3437
3584
  }
3438
3585
  var PHOTO_TYPES = { jpg: "image/jpeg", jpeg: "image/jpeg", png: "image/png", webp: "image/webp", gif: "image/gif" };
3439
- var teamOf = (t) => (t?.messages ?? []).filter((m) => m.from !== "visitor");
3586
+ var samePlace = (a, b) => a.chatId === b.chatId && a.fresh === b.fresh;
3587
+ var shownChat = (at, chats) => at.fresh ? null : at.chatId ?? (chats.length === 1 ? chats[0].id : null);
3588
+ var isAnswer = (m) => m.from === "team" || m.from === "ai";
3589
+ var lineOf = (m) => m.body || (m.attachments?.length === 1 ? tf("Sent {name}", { name: m.attachments[0].name }) : m.attachments?.length ? tf("Sent {n} files", { n: m.attachments.length }) : "");
3440
3590
  var svg2 = null;
3441
3591
  try {
3442
3592
  svg2 = require("react-native-svg");
@@ -3473,42 +3623,80 @@ var HU = {
3473
3623
  var hairline = { shadowColor: "#000", shadowOpacity: 0.07, shadowRadius: 3, shadowOffset: { width: 0, height: 1 }, elevation: 1 };
3474
3624
  var useChat = (client, o, open, view) => {
3475
3625
  const [thread, setThread] = (0, import_react3.useState)(null);
3626
+ const [chats, setChats] = (0, import_react3.useState)([]);
3627
+ const [hasEmail, setHasEmail] = (0, import_react3.useState)(false);
3628
+ const [place, setPlace] = (0, import_react3.useState)({ chatId: null, fresh: false });
3476
3629
  const [loaded, setLoaded] = (0, import_react3.useState)(false);
3477
3630
  const [error, setError] = (0, import_react3.useState)("");
3478
3631
  const [unseen, setUnseen] = (0, import_react3.useState)([]);
3479
- const known = (0, import_react3.useRef)(null);
3632
+ const known = (0, import_react3.useRef)(/* @__PURE__ */ new Map());
3633
+ const seeded = (0, import_react3.useRef)(false);
3480
3634
  const watching = open && view === "messages";
3481
3635
  const watch = (0, import_react3.useRef)(watching);
3482
3636
  watch.current = watching;
3637
+ const at = (0, import_react3.useRef)(place);
3638
+ const list = (0, import_react3.useRef)(chats);
3639
+ const readFailed = (0, import_react3.useRef)(false);
3483
3640
  const [active, setActive] = (0, import_react3.useState)(RN3.AppState.currentState === "active");
3484
3641
  (0, import_react3.useEffect)(() => {
3485
3642
  const sub = RN3.AppState.addEventListener("change", (st) => setActive(st === "active"));
3486
3643
  return () => sub.remove();
3487
3644
  }, []);
3645
+ const enter = (0, import_react3.useCallback)((chatId, fresh = false) => {
3646
+ const next = { chatId, fresh };
3647
+ if (samePlace(at.current, next)) return;
3648
+ at.current = next;
3649
+ setPlace(next);
3650
+ setThread(null);
3651
+ setError("");
3652
+ }, []);
3488
3653
  const take = (0, import_react3.useCallback)(
3489
- (t) => {
3490
- const team = teamOf(t);
3491
- if (known.current !== null) {
3492
- const i = team.findIndex((m) => m.id === known.current);
3493
- const fresh = team.slice(i + 1).filter((m) => m.from !== "auto");
3494
- if (fresh.length) {
3495
- if (o.haptics !== false) buzz();
3496
- if (!watch.current) setUnseen((u) => [...u, ...fresh]);
3497
- }
3498
- }
3499
- known.current = team.at(-1)?.id ?? "";
3500
- setThread(t);
3654
+ (t2) => {
3655
+ const read = t2.conversation;
3656
+ const last = t2.messages.at(-1);
3657
+ const all = t2.conversations ?? (read && last ? [{ id: read.id, status: read.status, subject: lineOf(t2.messages[0]), last }] : []);
3658
+ const fresh = [];
3659
+ const learn = (chat, answers, newest) => {
3660
+ const was = known.current.get(chat);
3661
+ const since = was ?? (seeded.current ? "" : void 0);
3662
+ if (since !== void 0) fresh.push(...answers.filter((m) => m.at > since).map((m) => ({ ...m, chat })));
3663
+ known.current.set(chat, newest ?? was ?? "");
3664
+ };
3665
+ for (const c of all) if (c.id !== read?.id) learn(c.id, c.last && isAnswer(c.last) ? [c.last] : [], c.last?.at);
3666
+ if (read) learn(read.id, t2.messages.filter(isAnswer), last?.at);
3667
+ seeded.current = true;
3668
+ if (fresh.length && o.haptics !== false) buzz();
3669
+ const shown2 = shownChat(at.current, all);
3670
+ const counted = fresh.filter((m) => !watch.current || m.chat !== shown2);
3671
+ setUnseen((u) => [...u.filter((m) => all.some((c) => c.id === m.chat)), ...counted]);
3672
+ list.current = all;
3673
+ setChats(all);
3674
+ setHasEmail(t2.hasEmail);
3675
+ if (at.current.chatId === null || read?.id === at.current.chatId) setThread(t2);
3501
3676
  },
3502
3677
  [o.haptics]
3503
3678
  );
3679
+ const arrived = (0, import_react3.useCallback)(
3680
+ (from, t2) => {
3681
+ const went = t2.conversation?.id;
3682
+ if (went && samePlace(at.current, from)) {
3683
+ at.current = { chatId: went, fresh: false };
3684
+ setPlace(at.current);
3685
+ }
3686
+ take(t2);
3687
+ },
3688
+ [take]
3689
+ );
3690
+ const shown = shownChat(place, chats);
3504
3691
  (0, import_react3.useEffect)(() => {
3505
- if (watching) setUnseen([]);
3506
- }, [watching]);
3692
+ if (watching && shown) setUnseen((u) => u.filter((m) => m.chat !== shown));
3693
+ }, [watching, shown]);
3507
3694
  (0, import_react3.useEffect)(() => {
3508
3695
  if (!active) return;
3509
3696
  let stop = false;
3510
3697
  let timer;
3511
3698
  let ctrl = null;
3699
+ let refused = false;
3512
3700
  const tick = async () => {
3513
3701
  const api = client.api();
3514
3702
  if (!api) {
@@ -3516,23 +3704,32 @@ var useChat = (client, o, open, view) => {
3516
3704
  return;
3517
3705
  }
3518
3706
  ctrl = new AbortController();
3519
- let t = null;
3520
3707
  try {
3521
3708
  await client.ready;
3522
- t = await api.chat(ctrl.signal);
3709
+ const t2 = await api.chat(ctrl.signal, place.chatId ?? void 0);
3523
3710
  if (stop) return;
3524
- take(t);
3525
- setError("");
3711
+ take(t2);
3712
+ if (readFailed.current) setError("");
3713
+ readFailed.current = false;
3526
3714
  } catch (e) {
3527
3715
  if (e.name === "AbortError" || stop) return;
3528
- if (watch.current) setError(e.message);
3716
+ refused = [401, 403].includes(e.status ?? 0);
3717
+ if (e.status === 404 && place.chatId) enter(null);
3718
+ else if (watch.current) {
3719
+ readFailed.current = true;
3720
+ setError(e.message);
3721
+ }
3529
3722
  }
3530
3723
  setLoaded(true);
3531
- const waiting = !!t?.messages.some((m) => m.from === "visitor") && t?.conversation?.status !== "solved";
3532
- const wrote = (t?.messages ?? []).filter((m) => m.from === "visitor").at(-1)?.at;
3533
- const recent = !!wrote && Date.now() - new Date(wrote).getTime() < WAITING_FOR_MS;
3534
- const ms = watch.current ? POLL_OPEN_MS : waiting && recent ? POLL_WAITING_MS : waiting || open ? POLL_IDLE_MS : 0;
3724
+ const running = list.current.filter((c) => c.status !== "solved");
3725
+ const recent = running.some((c) => c.last && Date.now() - new Date(c.last.at).getTime() < WAITING_FOR_MS);
3726
+ const ms = watch.current ? POLL_OPEN_MS : running.length && recent ? POLL_WAITING_MS : running.length || open ? POLL_IDLE_MS : 0;
3535
3727
  if (!stop && ms) timer = setTimeout(tick, ms);
3728
+ else if (!stop) timer = setTimeout(quiet, POLL_QUIET_MS);
3729
+ };
3730
+ const quiet = () => {
3731
+ if (client.getUser() && !refused) void tick();
3732
+ else timer = setTimeout(quiet, POLL_QUIET_MS);
3536
3733
  };
3537
3734
  void tick();
3538
3735
  return () => {
@@ -3540,8 +3737,22 @@ var useChat = (client, o, open, view) => {
3540
3737
  ctrl?.abort();
3541
3738
  clearTimeout(timer);
3542
3739
  };
3543
- }, [client, take, active, open, watching]);
3544
- return { thread, loaded, error, setError, unseen, take };
3740
+ }, [client, take, enter, active, open, watching, place]);
3741
+ return {
3742
+ // A new conversation has nothing to show yet; polls keep reading the newest for the list.
3743
+ thread: place.fresh ? null : thread,
3744
+ chats,
3745
+ hasEmail,
3746
+ place,
3747
+ /** Messages shows the list: several conversations and none chosen. */
3748
+ listing: place.chatId === null && !place.fresh && chats.length > 1,
3749
+ enter,
3750
+ loaded,
3751
+ error,
3752
+ setError,
3753
+ unseen,
3754
+ arrived
3755
+ };
3545
3756
  };
3546
3757
  var useArticles = (client, on, query) => {
3547
3758
  const [list, setList] = (0, import_react3.useState)(null);
@@ -3549,7 +3760,7 @@ var useArticles = (client, on, query) => {
3549
3760
  (0, import_react3.useEffect)(() => {
3550
3761
  if (!on) return;
3551
3762
  let stop = false;
3552
- const t = setTimeout(
3763
+ const t2 = setTimeout(
3553
3764
  () => void client.ready.then(
3554
3765
  () => client.api()?.articles(query.trim() || void 0).then((r) => {
3555
3766
  if (stop) return;
@@ -3561,27 +3772,27 @@ var useArticles = (client, on, query) => {
3561
3772
  );
3562
3773
  return () => {
3563
3774
  stop = true;
3564
- clearTimeout(t);
3775
+ clearTimeout(t2);
3565
3776
  };
3566
3777
  }, [client, on, query]);
3567
3778
  return { list, any };
3568
3779
  };
3569
3780
  var spansOf = (p, spans, base) => spans.map(
3570
- (s, i) => (0, import_react3.createElement)(
3781
+ (s2, i) => (0, import_react3.createElement)(
3571
3782
  RN3.Text,
3572
3783
  {
3573
3784
  key: i,
3574
- onPress: s.href ? () => void RN3.Linking.openURL(s.href).catch(() => void 0) : void 0,
3575
- accessibilityRole: s.href ? "link" : void 0,
3785
+ onPress: s2.href ? () => void RN3.Linking.openURL(s2.href).catch(() => void 0) : void 0,
3786
+ accessibilityRole: s2.href ? "link" : void 0,
3576
3787
  style: {
3577
3788
  ...base,
3578
- ...s.bold ? { fontWeight: "700" } : {},
3579
- ...s.italic ? { fontStyle: "italic" } : {},
3580
- ...s.code ? { fontFamily: RN3.Platform.OS === "ios" ? "Menlo" : "monospace", backgroundColor: p.soft } : {},
3581
- ...s.href ? { color: p.accent, textDecorationLine: "underline" } : {}
3789
+ ...s2.bold ? { fontWeight: "700" } : {},
3790
+ ...s2.italic ? { fontStyle: "italic" } : {},
3791
+ ...s2.code ? { fontFamily: RN3.Platform.OS === "ios" ? "Menlo" : "monospace", backgroundColor: p.soft } : {},
3792
+ ...s2.href ? { color: p.accent, textDecorationLine: "underline" } : {}
3582
3793
  }
3583
3794
  },
3584
- s.text
3795
+ s2.text
3585
3796
  )
3586
3797
  );
3587
3798
  var ArticleView = ({ client, p, id, onAsk }) => {
@@ -3595,7 +3806,7 @@ var ArticleView = ({ client, p, id, onAsk }) => {
3595
3806
  return () => ctrl.abort();
3596
3807
  }, [client, id]);
3597
3808
  if (error) return k.text(error, { color: p.danger });
3598
- if (!a) return k.muted("Loading\u2026");
3809
+ if (!a) return k.muted(t("Loading\u2026"));
3599
3810
  const base = { color: p.fg, fontSize: 15, lineHeight: 22 };
3600
3811
  const blocks = a.blocks.map((b, i) => {
3601
3812
  if (b.type === "h2" || b.type === "h3") return (0, import_react3.createElement)(RN3.Text, { key: i, accessibilityRole: "header", style: { ...base, fontSize: b.type === "h2" ? 17 : 16, fontWeight: "700", marginTop: 8 } }, ...spansOf(p, b.spans, {}));
@@ -3624,11 +3835,11 @@ var ArticleView = ({ client, p, id, onAsk }) => {
3624
3835
  (0, import_react3.createElement)(
3625
3836
  RN3.View,
3626
3837
  { style: { flexDirection: "row", alignItems: "center", gap: 8, padding: 12, borderRadius: 10, backgroundColor: p.soft, marginTop: 8 } },
3627
- rated ? k.muted("Thanks for letting us know.") : k.muted("Did this answer your question?", { flex: 1 }),
3628
- rated ? null : k.btn("Yes", "secondary", () => rate(true), false, { height: 34 }),
3629
- rated ? null : k.btn("No", "secondary", () => rate(false), false, { height: 34 })
3838
+ rated ? k.muted(t("Thanks for letting us know.")) : k.muted(t("Did this answer your question?"), { flex: 1 }),
3839
+ rated ? null : k.btn(t("Yes"), "secondary", () => rate(true), false, { height: 34 }),
3840
+ rated ? null : k.btn(t("No"), "secondary", () => rate(false), false, { height: 34 })
3630
3841
  ),
3631
- k.btn("Still need help? Send us a message", "secondary", onAsk)
3842
+ k.btn(t("Still need help? Send us a message"), "secondary", onAsk)
3632
3843
  );
3633
3844
  };
3634
3845
  var MessengerSheet = ({
@@ -3642,14 +3853,15 @@ var MessengerSheet = ({
3642
3853
  }) => {
3643
3854
  const k = kit(p);
3644
3855
  const [body, setBody] = (0, import_react3.useState)("");
3856
+ const [asked, setAsked] = (0, import_react3.useState)("");
3645
3857
  const [email, setEmail] = (0, import_react3.useState)("");
3646
3858
  const [pending, setPending] = (0, import_react3.useState)(null);
3647
3859
  const [slow, setSlow] = (0, import_react3.useState)(false);
3648
3860
  (0, import_react3.useEffect)(() => {
3649
3861
  setSlow(false);
3650
3862
  if (!pending) return;
3651
- const t = setTimeout(() => setSlow(true), 1500);
3652
- return () => clearTimeout(t);
3863
+ const t2 = setTimeout(() => setSlow(true), 1500);
3864
+ return () => clearTimeout(t2);
3653
3865
  }, [pending]);
3654
3866
  const [photos, setPhotos] = (0, import_react3.useState)([]);
3655
3867
  const [query, setQuery] = (0, import_react3.useState)("");
@@ -3657,24 +3869,25 @@ var MessengerSheet = ({
3657
3869
  const arts = useArticles(client, helpOn, query);
3658
3870
  const scroll = (0, import_react3.useRef)(null);
3659
3871
  const close = () => messengerOpen.set(false);
3660
- const greeting = o.greeting ?? "Hi! How can we help?";
3661
- const { thread } = chat;
3662
- const count = (thread?.messages.length ?? 0) + (pending ? 1 : 0);
3872
+ const greeting = o.greeting ?? t("Hi! How can we help?");
3873
+ const { thread, place } = chat;
3874
+ const pendingHere = pending && samePlace(pending.to, place) ? pending : null;
3875
+ const count = (thread?.messages.length ?? 0) + (pendingHere ? 1 : 0);
3663
3876
  (0, import_react3.useEffect)(() => {
3664
3877
  if (view === "messages") setTimeout(() => scroll.current?.scrollToEnd?.({ animated: true }), 50);
3665
- }, [count, view]);
3666
- const needEmail = (o.askEmail ?? true) && !thread?.hasEmail && !client.getUser()?.email;
3878
+ }, [count, view, place]);
3879
+ const needEmail = (o.askEmail ?? true) && !chat.hasEmail && !client.getUser()?.email;
3667
3880
  const emailOk = !email.trim() || EMAIL.test(email.trim());
3668
3881
  const pickPhotos = async () => {
3669
3882
  const api = client.api();
3670
3883
  if (!api || !imagePicker?.launchImageLibraryAsync || pending) return;
3671
3884
  const room = CHAT_FILES_PER_MESSAGE - photos.length;
3672
- if (room <= 0) return chat.setError(`Up to ${CHAT_FILES_PER_MESSAGE} files per message.`);
3885
+ if (room <= 0) return chat.setError(tf("Up to {n} files per message.", { n: CHAT_FILES_PER_MESSAGE }));
3673
3886
  let res;
3674
3887
  try {
3675
3888
  res = await imagePicker.launchImageLibraryAsync({ allowsMultipleSelection: room > 1, selectionLimit: room, quality: 0.8, exif: false });
3676
3889
  } catch (e) {
3677
- return chat.setError(e.message || "The photos could not be opened.");
3890
+ return chat.setError(e.message || t("The photos could not be opened."));
3678
3891
  }
3679
3892
  if (res.canceled || res.cancelled || !res.assets?.length) return;
3680
3893
  chat.setError("");
@@ -3683,11 +3896,11 @@ var MessengerSheet = ({
3683
3896
  const name = a.fileName || a.uri.split("/").pop() || "photo.jpg";
3684
3897
  const type = a.mimeType && Object.values(PHOTO_TYPES).includes(a.mimeType) ? a.mimeType : PHOTO_TYPES[name.split(".").pop()?.toLowerCase() ?? ""];
3685
3898
  if (!type) {
3686
- chat.setError("Only pictures (PNG, JPEG, WebP, GIF) can be sent.");
3899
+ chat.setError(t("Only pictures (PNG, JPEG, WebP, GIF) can be sent."));
3687
3900
  continue;
3688
3901
  }
3689
3902
  if (a.fileSize && a.fileSize > CHAT_FILE_MAX_BYTES) {
3690
- chat.setError(`${name} is larger than 10 MB.`);
3903
+ chat.setError(tf("{name} is larger than 10 MB.", { name }));
3691
3904
  continue;
3692
3905
  }
3693
3906
  const ph = { key: `local-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 7)}`, uri: a.uri, name, type, size: a.fileSize ?? 0, up: Promise.resolve() };
@@ -3706,29 +3919,29 @@ var MessengerSheet = ({
3706
3919
  setPhotos((list) => [...list, ...added]);
3707
3920
  };
3708
3921
  const canSend = (!!body.trim() || photos.length > 0) && !pending && emailOk;
3709
- const send = async () => {
3710
- const text2 = body.trim();
3922
+ const deliver = async (text2, files, to) => {
3711
3923
  const api = client.api();
3712
- const files = photos;
3713
- if (!text2 && !files.length || pending || !emailOk || !api) return;
3924
+ if (!api) return;
3714
3925
  setPending({
3715
3926
  id: "pending",
3716
3927
  from: "visitor",
3717
3928
  body: text2,
3718
3929
  at: (/* @__PURE__ */ new Date()).toISOString(),
3930
+ to,
3719
3931
  ...files.length ? { attachments: files.map((f) => ({ id: f.key, name: f.name, type: f.type, size: f.size })) } : {}
3720
3932
  });
3721
- setBody("");
3722
- setPhotos([]);
3723
3933
  chat.setError("");
3724
3934
  try {
3725
3935
  await Promise.all(files.map((f) => f.up));
3726
3936
  const ids = files.filter((f) => f.id).map((f) => f.id);
3727
- if (!text2 && !ids.length) throw new Error("The photo could not be sent.");
3937
+ if (!text2 && !ids.length) throw new Error(t("The photo could not be sent."));
3728
3938
  const win = RN3.Dimensions.get("screen");
3729
- chat.take(
3939
+ const conversation = to.fresh ? "new" : to.chatId ?? thread?.conversation?.id;
3940
+ chat.arrived(
3941
+ to,
3730
3942
  await api.sendChat({
3731
3943
  body: text2,
3944
+ ...conversation ? { conversation } : {},
3732
3945
  email: needEmail && email.trim() ? email.trim() : void 0,
3733
3946
  ...ids.length ? { attachments: ids } : {},
3734
3947
  // A little context for a bug report: the system and the screen size.
@@ -3736,13 +3949,44 @@ var MessengerSheet = ({
3736
3949
  })
3737
3950
  );
3738
3951
  } catch (e) {
3739
- setBody(text2);
3740
- setPhotos(files.filter((f) => f.id));
3741
- chat.setError(e.message);
3952
+ const gone = e.status === 404;
3953
+ chat.enter(gone ? null : to.chatId, gone || to.fresh);
3954
+ setBody((typed) => [text2, typed.trim()].filter(Boolean).join("\n"));
3955
+ setPhotos((list) => [...files.filter((f) => f.id), ...list]);
3956
+ chat.setError(gone ? t("This conversation is no longer available. Your message is still here to send as a new one.") : e.message);
3742
3957
  } finally {
3743
3958
  setPending(null);
3744
3959
  }
3745
3960
  };
3961
+ const send = () => {
3962
+ const text2 = body.trim();
3963
+ const files = photos;
3964
+ if (!text2 && !files.length || pending || !emailOk || !client.api()) return;
3965
+ setBody("");
3966
+ setPhotos([]);
3967
+ void deliver(text2, files, place);
3968
+ };
3969
+ const openMessages = () => {
3970
+ chat.enter(null);
3971
+ setView("messages");
3972
+ };
3973
+ const openChat = (id) => {
3974
+ chat.enter(id);
3975
+ setView("messages");
3976
+ };
3977
+ const startChat = () => {
3978
+ chat.enter(null, true);
3979
+ setView("messages");
3980
+ };
3981
+ const ask = () => {
3982
+ const text2 = asked.trim();
3983
+ if (!text2) return openMessages();
3984
+ if (pending || !client.api()) return;
3985
+ setAsked("");
3986
+ startChat();
3987
+ if (emailOk) void deliver(text2, [], { chatId: null, fresh: true });
3988
+ else setBody((typed) => [text2, typed.trim()].filter(Boolean).join("\n"));
3989
+ };
3746
3990
  const localUris = (0, import_react3.useRef)(/* @__PURE__ */ new Map());
3747
3991
  for (const f of photos) localUris.current.set(f.key, f.uri);
3748
3992
  const fileUrl = (id) => localUris.current.get(id) ?? client.api()?.attachmentUrl(id) ?? "";
@@ -3797,7 +4041,7 @@ var MessengerSheet = ({
3797
4041
  k.text(`\u{1F4C4} ${a.title}`, { fontSize: 13 })
3798
4042
  )
3799
4043
  ),
3800
- m.at ? k.muted(`${mine ? "" : `${who(m.from, o.agentName)} \xB7 `}${isPending && slow ? "Sending\u2026" : timeAgo(m.at)}`, { fontSize: 11, textAlign: mine ? "right" : "left", paddingHorizontal: 6 }) : null
4044
+ m.at ? k.muted(`${mine ? "" : `${who(m.from, o.agentName)} \xB7 `}${isPending && slow ? t("Sending\u2026") : timeAgo(m.at)}`, { fontSize: 11, textAlign: mine ? "right" : "left", paddingHorizontal: 6 }) : null
3801
4045
  );
3802
4046
  };
3803
4047
  const row = (a) => (0, import_react3.createElement)(
@@ -3810,55 +4054,76 @@ var MessengerSheet = ({
3810
4054
  const searchRow = (onPress) => (0, import_react3.createElement)(
3811
4055
  RN3.Pressable,
3812
4056
  { key: "s", onPress, accessibilityRole: "search", style: { flexDirection: "row", alignItems: "center", height: 44, borderRadius: r.field, paddingHorizontal: 14, backgroundColor: hu.def } },
3813
- k.muted("Search for help", { flex: 1, fontSize: 15 }),
4057
+ k.muted(t("Search for help"), { flex: 1, fontSize: 15 }),
3814
4058
  glyph("search", p.accent, 16)
3815
4059
  );
3816
- const search = k.field({ value: query, onChange: setQuery, placeholder: "Search for help", max: 200, style: { borderWidth: 0, borderRadius: r.field, backgroundColor: raised, ...surf } });
4060
+ const search = k.field({ value: query, onChange: setQuery, placeholder: t("Search for help"), max: 200, style: { borderWidth: 0, borderRadius: r.field, backgroundColor: raised, ...surf } });
3817
4061
  const first = client.getUser()?.name?.trim().split(/\s+/)[0];
3818
- const rest = greeting.replace(/^(hi|hello|hey|hallo)( there)?\s*[!,.]*\s*/i, "").trim() || "How can we help you today?";
4062
+ const rest = (o.greeting !== void 0 ? greeting.replace(/^(hi|hello|hey|hallo|moin|servus|bonjour|salut|hola|ciao|salve|hoi|ol[áa]|cze[śs][ćc]|hej|こんにちは)( there)?\s*[!,.!、。]*\s*/i, "").trim() : t("How can we help?")) || t("How can we help you today?");
3819
4063
  const hero = (fg, sub) => (0, import_react3.createElement)(
3820
4064
  RN3.Text,
3821
4065
  { key: "hero", accessibilityRole: "header", style: { color: fg, fontSize: 25, fontWeight: "600", letterSpacing: -0.6, lineHeight: 30 } },
3822
- first ? `Hi, ${first} \u{1F44B}
3823
- ` : "Hi there \u{1F44B}\n",
4066
+ `${first ? tf("Hi, {name}", { name: first }) : t("Hi there")} \u{1F44B}
4067
+ `,
3824
4068
  (0, import_react3.createElement)(RN3.Text, { style: { color: sub, fontWeight: "500" } }, rest)
3825
4069
  );
3826
- const last = thread?.messages.at(-1);
4070
+ const chatRow = (c) => {
4071
+ const m = c.last;
4072
+ const n = chat.unseen.filter((u) => u.chat === c.id).length;
4073
+ const closed = c.status === "solved" ? ` \xB7 ${t("Closed")}` : "";
4074
+ const subject = c.subject || (m ? lineOf(m) : "");
4075
+ const line = m ? lineOf(m) : "";
4076
+ return (0, import_react3.createElement)(
4077
+ RN3.Pressable,
4078
+ { key: c.id, onPress: () => openChat(c.id), accessibilityRole: "button", accessibilityLabel: `${tf("Conversation: {subject}", { subject })}${closed}${n ? `, ${tf("{n} new", { n })}` : ""}` },
4079
+ card(
4080
+ [
4081
+ (0, import_react3.createElement)(
4082
+ RN3.View,
4083
+ { key: "r", style: { flexDirection: "row", alignItems: "center", gap: 10 } },
4084
+ title ? teamMark(title, alpha(p.accent, 0.16), p.accent, 26) : null,
4085
+ (0, import_react3.createElement)(
4086
+ RN3.View,
4087
+ { style: { flex: 1, gap: 1 } },
4088
+ (0, import_react3.createElement)(RN3.Text, { numberOfLines: 1, style: { color: hu.fg, fontSize: 14, fontWeight: "500" } }, subject),
4089
+ m && line !== subject ? (0, import_react3.createElement)(RN3.Text, { numberOfLines: 1, style: { color: hu.muted, fontSize: 13 } }, line) : null,
4090
+ m ? k.muted(`${who(m.from, o.agentName)} \xB7 ${timeAgo(m.at)}${closed}`, { fontSize: 11.5 }) : null
4091
+ ),
4092
+ n ? (0, import_react3.createElement)(RN3.View, { style: { minWidth: 20, height: 20, paddingHorizontal: 5, borderRadius: 10, backgroundColor: p.danger, alignItems: "center", justifyContent: "center" } }, k.text(String(Math.min(n, 9)), { color: "#fff", fontSize: 11, fontWeight: "700" })) : glyph("next", p.muted, 14)
4093
+ )
4094
+ ],
4095
+ { paddingVertical: 10, paddingHorizontal: 12 }
4096
+ )
4097
+ );
4098
+ };
3827
4099
  const showHelp = helpOn && arts.any !== false;
3828
4100
  let content;
3829
4101
  if (view === "home") {
4102
+ const running = chat.chats.filter((c) => c.status !== "solved");
4103
+ const rows = (running.length ? running : chat.chats).slice(0, 2);
3830
4104
  const cards = [
3831
- // "Ask a question": the words go into the chat's field and are sent as the chat opens.
4105
+ // "Ask a question": its own field; the words start a new conversation.
3832
4106
  (0, import_react3.createElement)(
3833
4107
  RN3.View,
3834
4108
  { key: "ask", style: { flexDirection: "row", alignItems: "center", gap: 8, height: 48, borderRadius: 24, paddingLeft: 18, paddingRight: 6, backgroundColor: hu.surface, ...surf } },
3835
- k.field({ value: body, onChange: setBody, placeholder: "Ask a question", max: 4e3, style: { flex: 1, borderWidth: 0, paddingHorizontal: 0, backgroundColor: "transparent" } }),
4109
+ k.field({ value: asked, onChange: setAsked, placeholder: t("Ask a question"), max: 4e3, onSubmit: ask, style: { flex: 1, borderWidth: 0, paddingHorizontal: 0, backgroundColor: "transparent" } }),
3836
4110
  (0, import_react3.createElement)(
3837
4111
  RN3.Pressable,
3838
4112
  {
3839
- onPress: () => {
3840
- setView("messages");
3841
- if (body.trim()) void send();
3842
- },
4113
+ onPress: ask,
3843
4114
  accessibilityRole: "button",
3844
- accessibilityLabel: "Send",
3845
- style: { width: 36, height: 36, borderRadius: 18, alignItems: "center", justifyContent: "center", backgroundColor: body.trim() ? p.accent : hu.def }
4115
+ accessibilityLabel: t("Send"),
4116
+ style: { width: 36, height: 36, borderRadius: 18, alignItems: "center", justifyContent: "center", backgroundColor: asked.trim() ? p.accent : hu.def }
3846
4117
  },
3847
- glyph("up", body.trim() ? ink : hu.muted, 18)
4118
+ glyph("up", asked.trim() ? ink : hu.muted, 18)
3848
4119
  )
3849
4120
  ),
3850
- last ? (0, import_react3.createElement)(
4121
+ ...rows.map(chatRow),
4122
+ chat.chats.length > rows.length ? (0, import_react3.createElement)(
3851
4123
  RN3.Pressable,
3852
- { key: "recent", onPress: () => setView("messages"), accessibilityRole: "button" },
3853
- card([
3854
- (0, import_react3.createElement)(
3855
- RN3.View,
3856
- { key: "r", style: { flexDirection: "row", alignItems: "center", gap: 10 } },
3857
- title ? teamMark(title, alpha(p.accent, 0.16), p.accent, 26) : null,
3858
- (0, import_react3.createElement)(RN3.View, { style: { flex: 1, gap: 1 } }, (0, import_react3.createElement)(RN3.Text, { numberOfLines: 1, style: { color: hu.fg, fontSize: 14, fontWeight: "500" } }, last.body || (last.attachments?.length ? `Sent ${last.attachments.length === 1 ? last.attachments[0].name : `${last.attachments.length} files`}` : "")), k.muted(`${last.from === "visitor" ? "You" : who(last.from, o.agentName)} \xB7 ${timeAgo(last.at)}`, { fontSize: 11.5 })),
3859
- chat.unseen.length ? (0, import_react3.createElement)(RN3.View, { style: { width: 8, height: 8, borderRadius: 4, backgroundColor: p.danger } }) : glyph("next", p.muted, 14)
3860
- )
3861
- ], { paddingVertical: 10, paddingHorizontal: 12 })
4124
+ { key: "all", onPress: openMessages, accessibilityRole: "button", accessibilityLabel: tf("All conversations ({n})", { n: chat.chats.length }), style: { flexDirection: "row", alignItems: "center", justifyContent: "center", gap: 4, paddingVertical: 6 } },
4125
+ k.muted(tf("All conversations ({n})", { n: chat.chats.length }), { fontSize: 13, fontWeight: "500" }),
4126
+ glyph("next", p.muted, 12)
3862
4127
  ) : null,
3863
4128
  showHelp ? card([searchRow(() => setView("help")), ...(arts.list ?? []).slice(0, 4).map(row)], { padding: 8, gap: 2 }) : null
3864
4129
  ];
@@ -3873,7 +4138,14 @@ var MessengerSheet = ({
3873
4138
  (0, import_react3.createElement)(RN3.View, { style: { backgroundColor: p.accent, paddingHorizontal: 22, paddingTop: 4, paddingBottom: 64 } }, hero(ink, alpha(ink, 0.8))),
3874
4139
  (0, import_react3.createElement)(RN3.View, { style: { marginTop: -48, paddingHorizontal: 16, gap: 12 } }, ...cards)
3875
4140
  );
3876
- } else if (view === "messages")
4141
+ } else if (view === "messages" && chat.listing)
4142
+ content = (0, import_react3.createElement)(
4143
+ RN3.ScrollView,
4144
+ { key: "chats", style: { flex: 1, backgroundColor: canvas }, contentContainerStyle: { paddingHorizontal: 16, paddingTop: 4, paddingBottom: 16, gap: 12 } },
4145
+ ...chat.chats.map(chatRow),
4146
+ k.btn(t("New conversation"), "primary", startChat)
4147
+ );
4148
+ else if (view === "messages")
3877
4149
  content = (0, import_react3.createElement)(
3878
4150
  RN3.View,
3879
4151
  { key: "messages", style: { flex: 1, backgroundColor: hu.bg } },
@@ -3881,11 +4153,11 @@ var MessengerSheet = ({
3881
4153
  RN3.ScrollView,
3882
4154
  { ref: scroll, style: { flex: 1 }, contentContainerStyle: { paddingHorizontal: 16, paddingTop: 4, paddingBottom: 16, gap: 12 } },
3883
4155
  // The greeting opens an empty chat only, never above the visitor's first words (2026-10-30).
3884
- !thread?.messages.length && !pending ? bubble({ id: "greeting", from: "team", body: greeting, at: "" }) : null,
3885
- !chat.loaded ? k.muted("Loading\u2026") : null,
4156
+ !thread?.messages.length && !pendingHere && !place.chatId ? bubble({ id: "greeting", from: "team", body: greeting, at: "" }) : null,
4157
+ !chat.loaded && !place.fresh && !pendingHere || place.chatId && !thread ? k.muted(t("Loading\u2026")) : null,
3886
4158
  ...(thread?.messages ?? []).map((m) => bubble(m)),
3887
- pending ? bubble(pending, true) : null,
3888
- thread?.conversation?.status === "solved" && !pending ? k.muted("This conversation is closed. Write to start a new one.", { textAlign: "center" }) : null
4159
+ pendingHere ? bubble(pendingHere, true) : null,
4160
+ thread?.conversation?.status === "solved" && !pendingHere ? k.muted(t("This conversation is closed. Write to start a new one."), { textAlign: "center" }) : null
3889
4161
  ),
3890
4162
  (0, import_react3.createElement)(
3891
4163
  RN3.View,
@@ -3899,13 +4171,13 @@ var MessengerSheet = ({
3899
4171
  (f) => (0, import_react3.createElement)(
3900
4172
  RN3.View,
3901
4173
  { key: f.key, style: { width: 56, height: 56 } },
3902
- (0, import_react3.createElement)(RN3.Image, { source: { uri: f.uri }, resizeMode: "cover", accessibilityLabel: f.id ? f.name : `${f.name}, uploading`, style: { width: 56, height: 56, borderRadius: Math.max(4, r.card - 4), backgroundColor: p.soft, opacity: f.id ? 1 : 0.5 } }),
4174
+ (0, import_react3.createElement)(RN3.Image, { source: { uri: f.uri }, resizeMode: "cover", accessibilityLabel: f.id ? f.name : tf("{name}, uploading", { name: f.name }), style: { width: 56, height: 56, borderRadius: Math.max(4, r.card - 4), backgroundColor: p.soft, opacity: f.id ? 1 : 0.5 } }),
3903
4175
  (0, import_react3.createElement)(
3904
4176
  RN3.Pressable,
3905
4177
  {
3906
4178
  onPress: () => setPhotos((list) => list.filter((x) => x.key !== f.key)),
3907
4179
  accessibilityRole: "button",
3908
- accessibilityLabel: `Remove ${f.name}`,
4180
+ accessibilityLabel: tf("Remove {name}", { name: f.name }),
3909
4181
  hitSlop: 8,
3910
4182
  style: { position: "absolute", top: -7, right: -7, width: 22, height: 22, borderRadius: 11, borderWidth: 2, borderColor: p.bg, backgroundColor: p.fg, alignItems: "center", justifyContent: "center" }
3911
4183
  },
@@ -3922,28 +4194,28 @@ var MessengerSheet = ({
3922
4194
  needEmail ? k.field({
3923
4195
  value: email,
3924
4196
  onChange: setEmail,
3925
- placeholder: "Email for our reply (optional)",
4197
+ placeholder: t("Email for our reply (optional)"),
3926
4198
  email: true,
3927
4199
  max: 200,
3928
4200
  style: { minHeight: 0, marginRight: 10, marginBottom: 4, paddingHorizontal: 0, paddingTop: 0, paddingBottom: 8, borderWidth: 0, borderRadius: 0, borderBottomWidth: 1, borderBottomColor: emailOk ? p.line : p.danger, backgroundColor: "transparent", fontSize: 13.5 }
3929
4201
  }) : null,
3930
- k.field({ value: body, onChange: setBody, placeholder: "Write a message\u2026", multiline: true, max: 4e3, style: { minHeight: 28, maxHeight: 120, borderWidth: 0, paddingHorizontal: 0, paddingVertical: 4, backgroundColor: "transparent" } }),
4202
+ k.field({ value: body, onChange: setBody, placeholder: t("Write a message\u2026"), multiline: true, max: 4e3, style: { minHeight: 28, maxHeight: 120, borderWidth: 0, paddingHorizontal: 0, paddingVertical: 4, backgroundColor: "transparent" } }),
3931
4203
  (0, import_react3.createElement)(
3932
4204
  RN3.View,
3933
4205
  { style: { flexDirection: "row", alignItems: "center", marginLeft: -8 } },
3934
4206
  imagePicker?.launchImageLibraryAsync ? (0, import_react3.createElement)(
3935
4207
  RN3.Pressable,
3936
- { onPress: () => void pickPhotos(), disabled: !!pending, accessibilityRole: "button", accessibilityLabel: "Attach a photo or screenshot", hitSlop: 6, style: { width: 34, height: 34, borderRadius: 17, alignItems: "center", justifyContent: "center" } },
4208
+ { onPress: () => void pickPhotos(), disabled: !!pending, accessibilityRole: "button", accessibilityLabel: t("Attach a photo or screenshot"), hitSlop: 6, style: { width: 34, height: 34, borderRadius: 17, alignItems: "center", justifyContent: "center" } },
3937
4209
  glyph("clip", hu.muted, 19)
3938
4210
  ) : null,
3939
4211
  (0, import_react3.createElement)(RN3.View, { style: { flex: 1 } }),
3940
4212
  (0, import_react3.createElement)(
3941
4213
  RN3.Pressable,
3942
4214
  {
3943
- onPress: () => void send(),
4215
+ onPress: send,
3944
4216
  disabled: !canSend,
3945
4217
  accessibilityRole: "button",
3946
- accessibilityLabel: "Send",
4218
+ accessibilityLabel: t("Send"),
3947
4219
  style: { width: 34, height: 34, borderRadius: 17, alignItems: "center", justifyContent: "center", backgroundColor: canSend ? p.accent : hu.def }
3948
4220
  },
3949
4221
  glyph("up", canSend ? ink : hu.muted, 18)
@@ -3959,15 +4231,15 @@ var MessengerSheet = ({
3959
4231
  RN3.ScrollView,
3960
4232
  { key: "help", style: { flex: 1, backgroundColor: canvas }, contentContainerStyle: { paddingHorizontal: 16, paddingTop: 4, paddingBottom: 16, gap: 12 }, keyboardShouldPersistTaps: "handled" },
3961
4233
  search,
3962
- arts.list === null ? k.muted("Loading\u2026") : null,
3963
- arts.list?.length === 0 ? (0, import_react3.createElement)(RN3.View, { style: { gap: 10, alignItems: "center", paddingVertical: 24 } }, k.muted(query.trim() ? `Nothing found for \u201C${query.trim()}\u201D.` : "No help articles yet."), k.btn("Send us a message", "primary", () => setView("messages"))) : null,
4234
+ arts.list === null ? k.muted(t("Loading\u2026")) : null,
4235
+ arts.list?.length === 0 ? (0, import_react3.createElement)(RN3.View, { style: { gap: 10, alignItems: "center", paddingVertical: 24 } }, k.muted(query.trim() ? tf("Nothing found for \u201C{q}\u201D.", { q: query.trim() }) : t("No help articles yet.")), k.btn(t("Send us a message"), "primary", startChat)) : null,
3964
4236
  ...query.trim() ? arts.list?.length ? [card((arts.list ?? []).map(row), { padding: 8 })] : [] : [...groups].map(([name, items]) => (0, import_react3.createElement)(RN3.View, { key: name, style: { gap: 6 } }, k.muted(`${name} \xB7 ${items.length}`, { fontSize: 12, fontWeight: "600", paddingHorizontal: 2 }), card(items.map(row), { padding: 8 })))
3965
4237
  );
3966
4238
  } else
3967
4239
  content = (0, import_react3.createElement)(
3968
4240
  RN3.ScrollView,
3969
4241
  { key: `a-${view.article}`, style: { flex: 1 }, contentContainerStyle: { paddingHorizontal: 16, paddingTop: 4, paddingBottom: 32 } },
3970
- (0, import_react3.createElement)(ArticleView, { client, p, id: view.article, onAsk: () => setView("messages") })
4242
+ (0, import_react3.createElement)(ArticleView, { client, p, id: view.article, onAsk: startChat })
3971
4243
  );
3972
4244
  const current = typeof view === "string" ? view : "help";
3973
4245
  const tab = (v, name, icon, badge = 0) => {
@@ -3976,7 +4248,7 @@ var MessengerSheet = ({
3976
4248
  RN3.Pressable,
3977
4249
  {
3978
4250
  key: v,
3979
- onPress: () => setView(v),
4251
+ onPress: () => v === "messages" ? openMessages() : setView(v),
3980
4252
  accessibilityRole: "tab",
3981
4253
  accessibilityLabel: name,
3982
4254
  accessibilityState: { selected: on },
@@ -3992,21 +4264,21 @@ var MessengerSheet = ({
3992
4264
  const ground = onAccent ? p.accent : hu.bg;
3993
4265
  const back = (to, label) => (0, import_react3.createElement)(
3994
4266
  RN3.Pressable,
3995
- { key: "back", onPress: () => setView(to), accessibilityRole: "button", accessibilityLabel: label, hitSlop: 10, style: { width: 32, height: 32, alignItems: "center", justifyContent: "center", transform: [{ rotate: "180deg" }] } },
4267
+ { key: "back", onPress: to, accessibilityRole: "button", accessibilityLabel: label, hitSlop: 10, style: { width: 32, height: 32, alignItems: "center", justifyContent: "center", transform: [{ rotate: "180deg" }] } },
3996
4268
  glyph("next", hu.fg, 18)
3997
4269
  );
3998
4270
  const head = (0, import_react3.createElement)(
3999
4271
  RN3.View,
4000
4272
  { key: "head", style: { flexDirection: "row", alignItems: "center", gap: 8, paddingLeft: 12, paddingRight: 12, paddingTop: 12, paddingBottom: 6, backgroundColor: ground } },
4001
- typeof view !== "string" ? back("help", "All articles") : view === "messages" ? back("home", "Back to home") : view === "home" && (o.logo || title) ? o.logo ? (0, import_react3.createElement)(RN3.Image, { key: "logo", source: { uri: o.logo }, accessibilityIgnoresInvertColors: true, resizeMode: "contain", style: { height: 28, aspectRatio: 1, maxWidth: 132, borderRadius: 8, marginLeft: 4 } }) : teamMark(title, alpha(p.accent, 0.16), p.accent, 28) : null,
4002
- // In the chat: who answers, in the middle.
4003
- view === "messages" && title ? (0, import_react3.createElement)(
4273
+ typeof view !== "string" ? back(() => setView("help"), t("All articles")) : view === "messages" && !chat.listing && chat.chats.length > 1 ? back(openMessages, t("All conversations")) : view === "messages" ? back(() => setView("home"), t("Back to home")) : view === "home" && (o.logo || title) ? o.logo ? (0, import_react3.createElement)(RN3.Image, { key: "logo", source: { uri: o.logo }, accessibilityIgnoresInvertColors: true, resizeMode: "contain", style: { height: 28, aspectRatio: 1, maxWidth: 132, borderRadius: 8, marginLeft: 4 } }) : teamMark(title, alpha(p.accent, 0.16), p.accent, 28) : null,
4274
+ // In the chat: who answers, in the middle; over the list its name.
4275
+ view === "messages" && chat.listing ? (0, import_react3.createElement)(RN3.View, { style: { flex: 1, alignItems: "center" } }, k.text(t("Messages"), { color: hu.fg, fontSize: 15, fontWeight: "600" })) : view === "messages" && title ? (0, import_react3.createElement)(
4004
4276
  RN3.View,
4005
4277
  { style: { flex: 1, flexDirection: "row", alignItems: "center", justifyContent: "center", gap: 8 } },
4006
4278
  teamMark(title, alpha(p.accent, 0.16), p.accent, 28),
4007
4279
  (0, import_react3.createElement)(RN3.Text, { numberOfLines: 1, style: { flexShrink: 1, color: hu.fg, fontSize: 15, fontWeight: "600" } }, title)
4008
4280
  ) : (0, import_react3.createElement)(RN3.View, { style: { flex: 1 } }),
4009
- (0, import_react3.createElement)(RN3.Pressable, { onPress: close, accessibilityRole: "button", accessibilityLabel: "Close", hitSlop: 12, style: { width: 32, height: 32, borderRadius: 10, alignItems: "center", justifyContent: "center" } }, glyph("cross", onAccent ? ink : p.muted, 18))
4281
+ (0, import_react3.createElement)(RN3.Pressable, { onPress: close, accessibilityRole: "button", accessibilityLabel: t("Close"), hitSlop: 12, style: { width: 32, height: 32, borderRadius: 10, alignItems: "center", justifyContent: "center" } }, glyph("cross", onAccent ? ink : p.muted, 18))
4010
4282
  );
4011
4283
  const powered = brand ? (0, import_react3.createElement)(
4012
4284
  RN3.View,
@@ -4014,8 +4286,9 @@ var MessengerSheet = ({
4014
4286
  (0, import_react3.createElement)(
4015
4287
  RN3.Text,
4016
4288
  { onPress: () => void RN3.Linking.openURL(BRAND_URL).catch(() => void 0), accessibilityRole: "link", hitSlop: 8, style: { color: p.muted, fontSize: 11.5 } },
4017
- "Powered by ",
4018
- (0, import_react3.createElement)(RN3.Text, { style: { fontWeight: "600" } }, "MRRocket")
4289
+ t("Powered by {brand}").split("{brand}")[0],
4290
+ (0, import_react3.createElement)(RN3.Text, { style: { fontWeight: "600" } }, "MRRocket"),
4291
+ t("Powered by {brand}").split("{brand}")[1]
4019
4292
  )
4020
4293
  ) : null;
4021
4294
  return (0, import_react3.createElement)(
@@ -4026,12 +4299,12 @@ var MessengerSheet = ({
4026
4299
  { behavior: RN3.Platform.OS === "ios" ? "padding" : void 0, style: { flex: 1, backgroundColor: hu.bg } },
4027
4300
  head,
4028
4301
  content,
4029
- view === "messages" ? null : (0, import_react3.createElement)(
4302
+ view === "messages" && !chat.listing ? null : (0, import_react3.createElement)(
4030
4303
  RN3.View,
4031
4304
  { key: "tabs", accessibilityRole: "tablist", style: { flexDirection: "row", marginHorizontal: 12, marginTop: 6, marginBottom: 6, padding: 4, borderRadius: 22, backgroundColor: hu.surface } },
4032
- tab("home", "Home", "home"),
4033
- tab("messages", "Messages", "chat", chat.unseen.length),
4034
- showHelp ? tab("help", "Help", "help") : null
4305
+ tab("home", t("Home"), "home"),
4306
+ tab("messages", t("Messages"), "chat", chat.unseen.length),
4307
+ showHelp ? tab("help", t("Help"), "help") : null
4035
4308
  ),
4036
4309
  powered,
4037
4310
  (0, import_react3.createElement)(RN3.View, { style: { height: 16 } })
@@ -4046,7 +4319,7 @@ var MRRocketMessenger = ({ client, appearance, options: given = {} }) => {
4046
4319
  () => client.api()?.messengerLook(ctrl.signal).then(setLook).catch(() => void 0)
4047
4320
  );
4048
4321
  return () => ctrl.abort();
4049
- }, [client]);
4322
+ }, [client, wordsVersion()]);
4050
4323
  const defined = Object.fromEntries(Object.entries(given).filter(([, v]) => v !== void 0));
4051
4324
  const options = look ? {
4052
4325
  title: look.title,
@@ -4070,25 +4343,29 @@ var MRRocketMessenger = ({ client, appearance, options: given = {} }) => {
4070
4343
  const open = messengerOpen.useOpen();
4071
4344
  const [view, setViewState] = (0, import_react3.useState)(msgView);
4072
4345
  const [banner, setBanner] = (0, import_react3.useState)(null);
4073
- (0, import_react3.useEffect)(() => {
4074
- if (open) setViewState(msgView);
4075
- }, [open]);
4076
4346
  const setView = (v) => {
4077
4347
  msgView = v;
4078
4348
  setViewState(v);
4079
4349
  };
4080
4350
  const chat = useChat(client, options, open, view);
4351
+ const { enter } = chat;
4352
+ (0, import_react3.useEffect)(() => {
4353
+ if (!open) return;
4354
+ setViewState(msgView);
4355
+ if (msgView === "messages") enter(msgChat);
4356
+ }, [open, enter]);
4081
4357
  const latest = chat.unseen.at(-1);
4082
4358
  (0, import_react3.useEffect)(() => {
4083
4359
  if (!latest || open || options.preview === false) return;
4084
4360
  setBanner(latest);
4085
- const t = setTimeout(() => setBanner(null), 6e3);
4086
- return () => clearTimeout(t);
4361
+ const t2 = setTimeout(() => setBanner(null), 6e3);
4362
+ return () => clearTimeout(t2);
4087
4363
  }, [latest, open, options.preview]);
4088
4364
  (0, import_react3.useEffect)(() => {
4089
4365
  if (open) setBanner(null);
4090
4366
  }, [open]);
4091
4367
  const unseen = chat.unseen.length;
4368
+ const news = latest?.chat ?? null;
4092
4369
  const ink = inkOn(p.accent);
4093
4370
  const label = options.label?.trim() ?? "";
4094
4371
  const r = CORNERS[options.corners ?? "round"];
@@ -4103,9 +4380,9 @@ var MRRocketMessenger = ({ client, appearance, options: given = {} }) => {
4103
4380
  (0, import_react3.createElement)(
4104
4381
  RN3.Pressable,
4105
4382
  {
4106
- onPress: () => openAt(unseen ? "messages" : "home"),
4383
+ onPress: () => openAt(news ? "messages" : "home", news),
4107
4384
  accessibilityRole: "button",
4108
- accessibilityLabel: unseen ? `Open chat, ${unseen} new` : "Open chat",
4385
+ accessibilityLabel: unseen === 1 ? t("Open chat, 1 new reply") : unseen ? tf("Open chat, {n} new replies", { n: unseen }) : t("Open chat"),
4109
4386
  style: {
4110
4387
  height: label ? 50 : 56,
4111
4388
  minWidth: label ? 0 : 56,
@@ -4132,7 +4409,7 @@ var MRRocketMessenger = ({ client, appearance, options: given = {} }) => {
4132
4409
  RN3.Pressable,
4133
4410
  {
4134
4411
  key: "banner",
4135
- onPress: () => openAt("messages"),
4412
+ onPress: () => openAt("messages", banner.chat),
4136
4413
  accessibilityRole: "button",
4137
4414
  accessibilityLiveRegion: "polite",
4138
4415
  style: {
@@ -4152,7 +4429,7 @@ var MRRocketMessenger = ({ client, appearance, options: given = {} }) => {
4152
4429
  elevation: 6
4153
4430
  }
4154
4431
  },
4155
- k.muted(`${who(banner.from, options.agentName)}${unseen > 1 ? ` \xB7 ${unseen} new` : ""}`, { fontWeight: "600", fontSize: 12 }),
4432
+ k.muted(`${who(banner.from, options.agentName)}${unseen > 1 ? ` \xB7 ${tf("{n} new", { n: unseen })}` : ""}`, { fontWeight: "600", fontSize: 12 }),
4156
4433
  (0, import_react3.createElement)(RN3.Text, { numberOfLines: 3, style: { color: p.fg, fontSize: 15, lineHeight: 20 } }, banner.body)
4157
4434
  ) : null;
4158
4435
  if (!launcher && !toast && !open) return null;
@@ -4195,6 +4472,10 @@ var getMRRocket = () => shared;
4195
4472
  var Ctx = (0, import_react4.createContext)(null);
4196
4473
  var MRRocketProvider = ({ client: given, children, consent, flows = true, appearance, surveys = true, messenger = {}, feedback = {}, heatmaps = true, ...options }) => {
4197
4474
  const [client] = (0, import_react4.useState)(() => given ?? createMRRocket({ ...options, consent }));
4475
+ (0, import_react4.useSyncExternalStore)(onWords, wordsVersion, wordsVersion);
4476
+ (0, import_react4.useEffect)(() => {
4477
+ if (options.locale) setLocale(options.locale);
4478
+ }, [options.locale]);
4198
4479
  (0, import_react4.useEffect)(() => {
4199
4480
  if (consent) client.setConsent(consent);
4200
4481
  }, [client, consent]);