@helpai/elements 0.30.1 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/schema.json CHANGED
@@ -628,7 +628,7 @@
628
628
  "properties": {
629
629
  "text": {
630
630
  "default": "👋 Hi! Need any help?",
631
- "description": "The message to show in the bubble. Defaults to a friendly greeting; replace with your own copy (e.g. 'Click here →' or 'Try our new feature!').",
631
+ "description": "The message to show in the bubble. Defaults to a friendly greeting; replace with your own copy (e.g. 'Click here →'). May also be an i18n key (e.g. `launcherCallout`) resolved against `i18n.strings` for per-locale copy; a non-key literal renders as-is.",
632
632
  "type": "string",
633
633
  "minLength": 1,
634
634
  "maxLength": 500
@@ -1590,6 +1590,104 @@
1590
1590
  "default": true,
1591
1591
  "description": "Submitted marker expands in place to a read-only list of the visitor's answers. Set `false` for sensitive answers that shouldn't linger on screen.",
1592
1592
  "type": "boolean"
1593
+ },
1594
+ "defaultLocale": {
1595
+ "description": "BCP-47 locale the top-level strings (title / fields copy) are authored in.",
1596
+ "type": "string",
1597
+ "minLength": 1,
1598
+ "maxLength": 35
1599
+ },
1600
+ "translations": {
1601
+ "description": "Per-locale string overrides — the data module trims to the bootstrap `?locale=` (≤1); the widget overlays it onto the default copy.",
1602
+ "maxItems": 50,
1603
+ "type": "array",
1604
+ "items": {
1605
+ "type": "object",
1606
+ "properties": {
1607
+ "locale": {
1608
+ "type": "string",
1609
+ "minLength": 1,
1610
+ "maxLength": 35,
1611
+ "description": "BCP-47 locale tag (e.g. `en`, `fr-CA`, `zh-Hant-HK`)."
1612
+ },
1613
+ "title": {
1614
+ "type": "string",
1615
+ "maxLength": 120
1616
+ },
1617
+ "description": {
1618
+ "type": "string",
1619
+ "maxLength": 500
1620
+ },
1621
+ "submitLabel": {
1622
+ "type": "string",
1623
+ "maxLength": 60
1624
+ },
1625
+ "fields": {
1626
+ "maxItems": 50,
1627
+ "type": "array",
1628
+ "items": {
1629
+ "type": "object",
1630
+ "properties": {
1631
+ "name": {
1632
+ "type": "string",
1633
+ "minLength": 1,
1634
+ "maxLength": 80,
1635
+ "description": "The canonical field `name` this localizes."
1636
+ },
1637
+ "label": {
1638
+ "type": "string",
1639
+ "minLength": 1,
1640
+ "maxLength": 200
1641
+ },
1642
+ "placeholder": {
1643
+ "type": "string",
1644
+ "maxLength": 200
1645
+ },
1646
+ "validationHint": {
1647
+ "type": "string",
1648
+ "maxLength": 280
1649
+ },
1650
+ "options": {
1651
+ "maxItems": 50,
1652
+ "type": "array",
1653
+ "items": {
1654
+ "type": "object",
1655
+ "properties": {
1656
+ "value": {
1657
+ "type": "string",
1658
+ "minLength": 1,
1659
+ "maxLength": 200,
1660
+ "description": "The canonical option `value` this localizes."
1661
+ },
1662
+ "label": {
1663
+ "type": "string",
1664
+ "minLength": 1,
1665
+ "maxLength": 200
1666
+ },
1667
+ "description": {
1668
+ "type": "string",
1669
+ "maxLength": 280
1670
+ }
1671
+ },
1672
+ "required": [
1673
+ "value"
1674
+ ],
1675
+ "additionalProperties": {}
1676
+ }
1677
+ }
1678
+ },
1679
+ "required": [
1680
+ "name"
1681
+ ],
1682
+ "additionalProperties": {}
1683
+ }
1684
+ }
1685
+ },
1686
+ "required": [
1687
+ "locale"
1688
+ ],
1689
+ "additionalProperties": {}
1690
+ }
1593
1691
  }
1594
1692
  },
1595
1693
  "required": [
@@ -2395,7 +2493,7 @@
2395
2493
  "properties": {
2396
2494
  "text": {
2397
2495
  "default": "👋 Hi! Need any help?",
2398
- "description": "The message to show in the bubble. Defaults to a friendly greeting; replace with your own copy (e.g. 'Click here →' or 'Try our new feature!').",
2496
+ "description": "The message to show in the bubble. Defaults to a friendly greeting; replace with your own copy (e.g. 'Click here →'). May also be an i18n key (e.g. `launcherCallout`) resolved against `i18n.strings` for per-locale copy; a non-key literal renders as-is.",
2399
2497
  "type": "string",
2400
2498
  "minLength": 1,
2401
2499
  "maxLength": 500
package/schema.mjs CHANGED
@@ -171,7 +171,7 @@ var calloutShapeSchema = z6.enum(["pill", "bubble", "callout"]);
171
171
  var calloutPositionSchema = z6.enum(["auto", "horizontal", "vertical"]);
172
172
  var launcherCalloutSchema = z6.object({
173
173
  text: z6.string().min(1).max(500).default("\u{1F44B} Hi! Need any help?").describe(
174
- "The message to show in the bubble. Defaults to a friendly greeting; replace with your own copy (e.g. 'Click here \u2192' or 'Try our new feature!')."
174
+ "The message to show in the bubble. Defaults to a friendly greeting; replace with your own copy (e.g. 'Click here \u2192'). May also be an i18n key (e.g. `launcherCallout`) resolved against `i18n.strings` for per-locale copy; a non-key literal renders as-is."
175
175
  ),
176
176
  shape: calloutShapeSchema.default("pill").describe("Visual shape \u2014 `pill` (slim label + arrow), `bubble` (speech-bubble tail), `callout` (loud variant)."),
177
177
  position: calloutPositionSchema.default("auto").describe(
@@ -417,6 +417,25 @@ var formConditionSchema = z12.object({
417
417
  minMessages: z12.number().int().min(0).max(1e3).optional(),
418
418
  maxMessages: z12.number().int().min(0).max(1e3).optional()
419
419
  }).loose();
420
+ var fieldOptionTranslationSchema = z12.object({
421
+ value: z12.string().min(1).max(200).describe("The canonical option `value` this localizes."),
422
+ label: z12.string().min(1).max(200).optional(),
423
+ description: z12.string().max(280).optional()
424
+ }).loose();
425
+ var formFieldTranslationSchema = z12.object({
426
+ name: z12.string().min(1).max(80).describe("The canonical field `name` this localizes."),
427
+ label: z12.string().min(1).max(200).optional(),
428
+ placeholder: z12.string().max(200).optional(),
429
+ validationHint: z12.string().max(280).optional(),
430
+ options: z12.array(fieldOptionTranslationSchema).max(50).optional()
431
+ }).loose();
432
+ var formTranslationSchema = z12.object({
433
+ locale: localeSchema,
434
+ title: z12.string().max(120).optional(),
435
+ description: z12.string().max(500).optional(),
436
+ submitLabel: z12.string().max(60).optional(),
437
+ fields: z12.array(formFieldTranslationSchema).max(50).optional()
438
+ }).loose();
420
439
  var formDefSchema = z12.object({
421
440
  id: z12.string().min(1).max(80).describe("Stable id \u2014 persistence dedupe + `manual` openForm(id)."),
422
441
  on: z12.union([formTriggerSchema, z12.array(formTriggerSchema).min(1).max(8)]).describe("Trigger(s) that surface this form."),
@@ -435,6 +454,10 @@ var formDefSchema = z12.object({
435
454
  ),
436
455
  reviewable: z12.boolean().default(true).describe(
437
456
  "Submitted marker expands in place to a read-only list of the visitor's answers. Set `false` for sensitive answers that shouldn't linger on screen."
457
+ ),
458
+ defaultLocale: localeSchema.optional().describe("BCP-47 locale the top-level strings (title / fields copy) are authored in."),
459
+ translations: z12.array(formTranslationSchema).max(50).optional().describe(
460
+ "Per-locale string overrides \u2014 the data module trims to the bootstrap `?locale=` (\u22641); the widget overlays it onto the default copy."
438
461
  )
439
462
  }).loose();
440
463
  var formsSchema = z12.array(formDefSchema).max(20).describe(
@@ -871,12 +894,15 @@ export {
871
894
  feedbackEventSchema,
872
895
  feedbackSchema,
873
896
  fieldOptionSchema,
897
+ fieldOptionTranslationSchema,
874
898
  fieldTypeSchema,
875
899
  fieldValidationSchema,
876
900
  footerSchema,
877
901
  formConditionSchema,
878
902
  formDefSchema,
879
903
  formFieldSchema,
904
+ formFieldTranslationSchema,
905
+ formTranslationSchema,
880
906
  formTriggerSchema,
881
907
  formsSchema,
882
908
  handshakeResponseSchema,
package/web-component.mjs CHANGED
@@ -275,8 +275,8 @@ function resolveStrings(locale, override) {
275
275
  function primaryTag(locale) {
276
276
  return (locale.split("-")[0] ?? "en").toLowerCase();
277
277
  }
278
- function moduleLabel(strings, label) {
279
- return strings[label] ?? label;
278
+ function localizeText(strings, value) {
279
+ return strings[value] ?? value;
280
280
  }
281
281
  function isNestedShape(override) {
282
282
  if (!override) return false;
@@ -286,6 +286,51 @@ function isNestedShape(override) {
286
286
  return false;
287
287
  }
288
288
 
289
+ // src/i18n/overlay.ts
290
+ function pickTranslation(translations, locale) {
291
+ if (!translations?.length || !locale) return void 0;
292
+ return translations.find((t) => t.locale === locale);
293
+ }
294
+ function overlayContent(item, locale) {
295
+ const { translations, defaultLocale: _defaultLocale, ...base } = item;
296
+ const tr = pickTranslation(translations, locale);
297
+ if (!tr) return base;
298
+ return {
299
+ ...base,
300
+ title: tr.title ?? base.title,
301
+ description: tr.description ?? base.description,
302
+ content: tr.content ?? base.content
303
+ };
304
+ }
305
+ function overlayFormDef(def, locale) {
306
+ const { translations, defaultLocale: _defaultLocale, ...base } = def;
307
+ const tr = pickTranslation(translations, locale);
308
+ if (!tr) return base;
309
+ const fieldTrByName = new Map((tr.fields ?? []).map((f) => [f.name, f]));
310
+ const fields = base.fields.map((field) => {
311
+ const ftr = fieldTrByName.get(field.name);
312
+ if (!ftr) return field;
313
+ const optionTrByValue = new Map((ftr.options ?? []).map((o) => [o.value, o]));
314
+ return {
315
+ ...field,
316
+ label: ftr.label ?? field.label,
317
+ placeholder: ftr.placeholder ?? field.placeholder,
318
+ validationHint: ftr.validationHint ?? field.validationHint,
319
+ options: field.options?.map((option) => {
320
+ const otr = optionTrByValue.get(option.value);
321
+ return otr ? { ...option, label: otr.label ?? option.label, description: otr.description ?? option.description } : option;
322
+ })
323
+ };
324
+ });
325
+ return {
326
+ ...base,
327
+ title: tr.title ?? base.title,
328
+ description: tr.description ?? base.description,
329
+ submitLabel: tr.submitLabel ?? base.submitLabel,
330
+ fields
331
+ };
332
+ }
333
+
289
334
  // src/core/storage.ts
290
335
  var LocalStorageAdapter = class {
291
336
  get(key) {
@@ -537,7 +582,7 @@ function resolveOptions(rawOpts) {
537
582
  humanInLoop: opts.features?.humanInLoop ?? DEFAULT_FEATURES.humanInLoop,
538
583
  tools: opts.features?.tools
539
584
  },
540
- forms: resolveForms(opts.forms),
585
+ forms: resolveForms(opts.forms, locale),
541
586
  endpoints: {
542
587
  upload: opts.endpoints?.upload ?? DEFAULT_ENDPOINTS.upload,
543
588
  transcribe: opts.endpoints?.transcribe ?? DEFAULT_ENDPOINTS.transcribe,
@@ -660,13 +705,14 @@ function resolveModules(overrides) {
660
705
  const byId = Object.fromEntries(list.map((m) => [m.id, m]));
661
706
  return { list, byId };
662
707
  }
663
- function resolveForms(overrides) {
708
+ function resolveForms(overrides, locale) {
664
709
  var _a;
665
710
  if (!overrides?.length) return DEFAULT_FORMS;
666
711
  const list = [];
667
712
  const seen = /* @__PURE__ */ new Set();
668
- for (const def of overrides) {
669
- if (!def?.id || seen.has(def.id)) continue;
713
+ for (const raw of overrides) {
714
+ if (!raw?.id || seen.has(raw.id)) continue;
715
+ const def = overlayFormDef(raw, locale);
670
716
  const fields = (def.fields ?? []).filter((f) => Boolean(f && f.name && f.label && f.type));
671
717
  const triggers = (Array.isArray(def.on) ? def.on : [def.on]).map(parseTrigger).filter((t) => t !== null);
672
718
  if (fields.length === 0 || triggers.length === 0) continue;
@@ -1803,7 +1849,7 @@ var AgentTransport = class {
1803
1849
  }
1804
1850
  /** Fetch content rows by filter. `GET /pai/content` (data-API), TTL-cached. */
1805
1851
  listContent(query = {}) {
1806
- const key = JSON.stringify(query, Object.keys(query).toSorted());
1852
+ const key = `${this.locale ?? ""}|${JSON.stringify(query, Object.keys(query).toSorted())}`;
1807
1853
  const cached = this.contentCache.get(key);
1808
1854
  if (cached && Date.now() - cached.at < CONTENT_CACHE_TTL_MS) {
1809
1855
  log5.debug("listContent \u2192 cache", query);
@@ -1824,7 +1870,7 @@ var AgentTransport = class {
1824
1870
  }
1825
1871
  log5.debug("listContent \u2192", query);
1826
1872
  const res = await this.getJson(url.toString(), "listContent");
1827
- const items = res.items ?? [];
1873
+ const items = (res.items ?? []).map((item) => overlayContent(item, this.locale));
1828
1874
  log5.debug("listContent \u2190", { count: items.length, total: res.pagination?.total });
1829
1875
  return { ...res, items };
1830
1876
  }
@@ -3084,7 +3130,7 @@ function mountRawSvg(host, source) {
3084
3130
  } catch {
3085
3131
  }
3086
3132
  }
3087
- function LauncherCallout({ callout, launcherPosition, closeLabel, onDismiss }) {
3133
+ function LauncherCallout({ callout, launcherPosition, strings, closeLabel, onDismiss }) {
3088
3134
  const [visible, setVisible] = useState(false);
3089
3135
  useEffect(() => {
3090
3136
  const id = setTimeout(() => setVisible(true), callout.delayMs);
@@ -3106,7 +3152,7 @@ function LauncherCallout({ callout, launcherPosition, closeLabel, onDismiss }) {
3106
3152
  "data-animated": callout.animated ? "true" : void 0,
3107
3153
  "data-testid": TID.callout,
3108
3154
  children: [
3109
- /* @__PURE__ */ jsx2("span", { children: callout.text }),
3155
+ /* @__PURE__ */ jsx2("span", { children: localizeText(strings, callout.text) }),
3110
3156
  /* @__PURE__ */ jsx2(
3111
3157
  "button",
3112
3158
  {
@@ -5666,9 +5712,16 @@ function createController(depsRef) {
5666
5712
  else if (form.frequency !== "always") depsRef.current.persistence.saveFormDone(form.id);
5667
5713
  depsRef.current.onComplete(form, trigger, values, skipped);
5668
5714
  };
5715
+ const refresh = () => {
5716
+ const active = activeForm.value;
5717
+ if (!active) return;
5718
+ const next = depsRef.current.forms.list.find((f) => f.id === active.form.id);
5719
+ if (next && next !== active.form) activeForm.value = { form: next, trigger: active.trigger };
5720
+ };
5669
5721
  return {
5670
5722
  activeForm,
5671
5723
  fire,
5724
+ refresh,
5672
5725
  complete: (values) => finish(values, false),
5673
5726
  skip: () => finish({}, true)
5674
5727
  };
@@ -6080,7 +6133,7 @@ function HomeRoot(props2) {
6080
6133
  const greeting = resolveGreeting(props2);
6081
6134
  const avatars = config.userAvatars ?? [];
6082
6135
  const status = config.status;
6083
- const contentTitle = config.contentBlockTitle ? moduleLabel(strings, config.contentBlockTitle) : strings.homeContentTitle;
6136
+ const contentTitle = config.contentBlockTitle ? localizeText(strings, config.contentBlockTitle) : strings.homeContentTitle;
6084
6137
  return /* @__PURE__ */ jsx29("div", { class: `${p25}-module ${p25}-home`, "data-testid": TID.homeView, children: /* @__PURE__ */ jsxs24("div", { class: `${p25}-home-scroll`, children: [
6085
6138
  /* @__PURE__ */ jsxs24("div", { class: `${p25}-home-hero`, children: [
6086
6139
  /* @__PURE__ */ jsxs24("div", { class: `${p25}-home-hero-top`, children: [
@@ -6235,7 +6288,7 @@ function HomeTabBar({ modules, activeTab, strings, unreadCount, onSelect }) {
6235
6288
  /* @__PURE__ */ jsx31(Icon, {}),
6236
6289
  badge ? /* @__PURE__ */ jsx31("span", { class: `${p27}-tab-badge`, "data-testid": TID.tabBadge, children: badge }) : null
6237
6290
  ] }),
6238
- /* @__PURE__ */ jsx31("span", { class: `${p27}-tab-label`, children: moduleLabel(strings, m.label) })
6291
+ /* @__PURE__ */ jsx31("span", { class: `${p27}-tab-label`, children: localizeText(strings, m.label) })
6239
6292
  ]
6240
6293
  },
6241
6294
  m.id
@@ -6906,7 +6959,7 @@ function App({ options, hostElement, bus }) {
6906
6959
  void (async () => {
6907
6960
  try {
6908
6961
  const res = await dataBootRef.current;
6909
- setRemoteForms(resolveForms((res ?? { forms: [] }).forms));
6962
+ setRemoteForms(resolveForms((res ?? { forms: [] }).forms, effectiveLocale));
6910
6963
  } catch (err) {
6911
6964
  log16.debug("bootstrap failed \u2014 treating as no forms", { err });
6912
6965
  } finally {
@@ -6924,6 +6977,23 @@ function App({ options, hostElement, bus }) {
6924
6977
  void runResume(handle);
6925
6978
  }
6926
6979
  }, [activated]);
6980
+ const formsLocaleRef = useRef9(null);
6981
+ useEffect17(() => {
6982
+ if (!activated) return;
6983
+ if (formsLocaleRef.current === null) {
6984
+ formsLocaleRef.current = effectiveLocale;
6985
+ return;
6986
+ }
6987
+ if (formsLocaleRef.current === effectiveLocale) return;
6988
+ formsLocaleRef.current = effectiveLocale;
6989
+ let cancelled = false;
6990
+ void transport.bootstrap().then((res) => {
6991
+ if (!cancelled) setRemoteForms(resolveForms((res ?? { forms: [] }).forms, effectiveLocale));
6992
+ }).catch((err) => log16.debug("forms re-localize failed \u2014 keeping current copy", { err }));
6993
+ return () => {
6994
+ cancelled = true;
6995
+ };
6996
+ }, [activated, effectiveLocale, transport]);
6927
6997
  const lastUserSig = useRef9(userSig);
6928
6998
  useEffect17(() => {
6929
6999
  if (!conversationReady || lastUserSig.current === userSig) return;
@@ -7080,6 +7150,9 @@ function App({ options, hostElement, bus }) {
7080
7150
  }
7081
7151
  });
7082
7152
  const activeForm = useComputed7(() => forms.activeForm.value);
7153
+ useEffect17(() => {
7154
+ forms.refresh();
7155
+ }, [effectiveForms, forms]);
7083
7156
  const pageArea = options.pageContext?.area ? String(options.pageContext.area) : void 0;
7084
7157
  const msgCount = useComputed7(() => messagesSig.value.length);
7085
7158
  useEffect17(() => {
@@ -7466,6 +7539,7 @@ function App({ options, hostElement, bus }) {
7466
7539
  {
7467
7540
  callout: calloutToRender,
7468
7541
  launcherPosition: effectiveOptions.position,
7542
+ strings: effectiveOptions.strings,
7469
7543
  closeLabel: effectiveOptions.strings.close,
7470
7544
  onDismiss: dismissCallout
7471
7545
  }