@hanzo/build 0.2.80 → 0.2.81

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 (165) hide show
  1. package/README.md +13 -0
  2. package/lib/activity.d.ts +3 -7
  3. package/lib/activity.js +18 -5
  4. package/lib/api/agents.d.ts +1 -20
  5. package/lib/api/agents.js +16 -8
  6. package/lib/api/auto.d.ts +3 -23
  7. package/lib/api/auto.js +39 -22
  8. package/lib/api/billing.d.ts +0 -35
  9. package/lib/api/billing.js +6 -5
  10. package/lib/api/call.d.ts +16 -8
  11. package/lib/api/call.js +32 -1
  12. package/lib/api/capabilities.d.ts +0 -11
  13. package/lib/api/capabilities.js +2 -1
  14. package/lib/api/codebases.d.ts +0 -10
  15. package/lib/api/codebases.js +3 -2
  16. package/lib/api/coding.d.ts +3 -32
  17. package/lib/api/coding.js +13 -7
  18. package/lib/api/connectors.d.ts +1 -17
  19. package/lib/api/connectors.js +5 -4
  20. package/lib/api/environment.d.ts +6 -20
  21. package/lib/api/environment.js +22 -19
  22. package/lib/api/github.d.ts +0 -22
  23. package/lib/api/github.js +3 -2
  24. package/lib/api/keys.d.ts +0 -14
  25. package/lib/api/keys.js +4 -3
  26. package/lib/api/library.d.ts +3 -17
  27. package/lib/api/library.js +23 -22
  28. package/lib/api/machines.d.ts +0 -15
  29. package/lib/api/machines.js +6 -5
  30. package/lib/api/mcp.d.ts +0 -9
  31. package/lib/api/mcp.js +3 -2
  32. package/lib/api/members.d.ts +0 -15
  33. package/lib/api/members.js +4 -3
  34. package/lib/api/memory.d.ts +0 -11
  35. package/lib/api/memory.js +2 -1
  36. package/lib/api/places.d.ts +1 -9
  37. package/lib/api/places.js +12 -1
  38. package/lib/api/plugins.d.ts +1 -16
  39. package/lib/api/plugins.js +4 -3
  40. package/lib/api/profile.d.ts +0 -10
  41. package/lib/api/profile.js +5 -4
  42. package/lib/api/projects.d.ts +0 -14
  43. package/lib/api/projects.js +2 -1
  44. package/lib/api/provider.d.ts +2 -14
  45. package/lib/api/provider.js +12 -10
  46. package/lib/api/sandbox.d.ts +0 -17
  47. package/lib/api/sandbox.js +5 -4
  48. package/lib/api/sessions.d.ts +0 -17
  49. package/lib/api/sessions.js +5 -4
  50. package/lib/api/skills.d.ts +1 -15
  51. package/lib/api/skills.js +5 -4
  52. package/lib/api/studio.d.ts +0 -14
  53. package/lib/api/studio.js +6 -5
  54. package/lib/api/turn.d.ts +6 -28
  55. package/lib/api/turn.js +105 -49
  56. package/lib/api/verdict.d.ts +0 -9
  57. package/lib/api/verdict.js +2 -1
  58. package/lib/api/webhooks.d.ts +1 -14
  59. package/lib/api/webhooks.js +5 -4
  60. package/lib/ask.js +7 -4
  61. package/lib/automations.d.ts +5 -4
  62. package/lib/automations.js +95 -74
  63. package/lib/back.js +10 -4
  64. package/lib/builder.js +7 -5
  65. package/lib/carry.js +20 -16
  66. package/lib/customize/agents.js +78 -31
  67. package/lib/customize/connectors.js +62 -48
  68. package/lib/customize/index.js +43 -10
  69. package/lib/customize/plugins.js +34 -22
  70. package/lib/customize/say.d.ts +13 -1
  71. package/lib/customize/say.js +15 -21
  72. package/lib/customize/skills.js +31 -22
  73. package/lib/customize/ui.d.ts +5 -9
  74. package/lib/customize/ui.js +26 -16
  75. package/lib/data.d.ts +1 -1
  76. package/lib/data.js +3 -2
  77. package/lib/desk.js +45 -33
  78. package/lib/door.js +35 -10
  79. package/lib/environment.js +25 -19
  80. package/lib/files.js +43 -34
  81. package/lib/find.js +12 -9
  82. package/lib/foot.js +7 -4
  83. package/lib/forge.js +61 -40
  84. package/lib/gear.d.ts +2 -1
  85. package/lib/gear.js +6 -4
  86. package/lib/git.js +29 -19
  87. package/lib/landing.d.ts +5 -5
  88. package/lib/landing.js +52 -44
  89. package/lib/library.js +73 -51
  90. package/lib/meter.d.ts +1 -1
  91. package/lib/meter.js +14 -5
  92. package/lib/mic.js +12 -5
  93. package/lib/mind.d.ts +1 -0
  94. package/lib/mind.js +5 -3
  95. package/lib/nav.d.ts +6 -5
  96. package/lib/nav.js +37 -30
  97. package/lib/panel.d.ts +2 -1
  98. package/lib/panel.js +52 -36
  99. package/lib/plan.d.ts +4 -20
  100. package/lib/plan.js +26 -10
  101. package/lib/plans.js +49 -30
  102. package/lib/plugins/design.js +3 -2
  103. package/lib/plugins/index.d.ts +23 -4
  104. package/lib/plugins/index.js +11 -26
  105. package/lib/plugins/pdf.js +2 -1
  106. package/lib/plugins/project.js +6 -3
  107. package/lib/plugins/research.js +3 -2
  108. package/lib/plugins/sketch.js +6 -3
  109. package/lib/plugins/slides.js +3 -2
  110. package/lib/plugins/template.js +6 -3
  111. package/lib/plus.js +25 -13
  112. package/lib/prefs.d.ts +0 -14
  113. package/lib/prefs.js +2 -1
  114. package/lib/project.js +69 -62
  115. package/lib/prompt.d.ts +12 -11
  116. package/lib/prompt.js +38 -20
  117. package/lib/publish.js +17 -12
  118. package/lib/reply.js +6 -3
  119. package/lib/run.js +72 -54
  120. package/lib/say.d.ts +2 -0
  121. package/lib/say.js +7 -0
  122. package/lib/scheduled.js +54 -43
  123. package/lib/section.d.ts +1 -0
  124. package/lib/section.js +8 -2
  125. package/lib/settings/account.d.ts +1 -0
  126. package/lib/settings/account.js +31 -21
  127. package/lib/settings/billing.js +55 -28
  128. package/lib/settings/capabilities.js +26 -15
  129. package/lib/settings/card.js +19 -12
  130. package/lib/settings/code.js +22 -11
  131. package/lib/settings/credits.d.ts +4 -4
  132. package/lib/settings/credits.js +34 -21
  133. package/lib/settings/environments.js +21 -14
  134. package/lib/settings/general.js +26 -14
  135. package/lib/settings/index.js +6 -3
  136. package/lib/settings/integrations.js +42 -28
  137. package/lib/settings/keys.js +37 -19
  138. package/lib/settings/list.d.ts +0 -5
  139. package/lib/settings/list.js +24 -15
  140. package/lib/settings/machines.d.ts +3 -0
  141. package/lib/settings/machines.js +63 -38
  142. package/lib/settings/members.js +23 -14
  143. package/lib/settings/memory.js +12 -6
  144. package/lib/settings/notifications.js +32 -19
  145. package/lib/settings/plan.d.ts +1 -1
  146. package/lib/settings/plan.js +40 -17
  147. package/lib/settings/privacy.js +26 -13
  148. package/lib/settings/ui.d.ts +3 -2
  149. package/lib/settings/ui.js +9 -3
  150. package/lib/settings/usage.js +7 -3
  151. package/lib/shelf.js +18 -15
  152. package/lib/side.d.ts +1 -26
  153. package/lib/side.js +11 -5
  154. package/lib/standing.d.ts +2 -10
  155. package/lib/standing.js +5 -3
  156. package/lib/suggest.d.ts +8 -28
  157. package/lib/suggest.js +39 -4
  158. package/lib/suggestions.js +14 -5
  159. package/lib/summary.d.ts +1 -1
  160. package/lib/summary.js +4 -1
  161. package/lib/sync.js +22 -12
  162. package/lib/tools.d.ts +4 -3
  163. package/lib/tools.js +10 -7
  164. package/lib/transcript.js +10 -8
  165. package/package.json +4 -2
package/lib/section.js CHANGED
@@ -11,6 +11,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
11
11
  * host's own `go`.
12
12
  */
13
13
  import { SizableText } from '@hanzo/gui';
14
+ import { useI18n } from '@hanzo/i18n/react';
14
15
  import { SidebarItem, SidebarSection, StatusDot } from '@hanzo/ui/chat';
15
16
  import { useMemo } from 'react';
16
17
  import { useRecents } from './data.js';
@@ -36,7 +37,8 @@ const Note = ({ children }) => (_jsx(SizableText, { size: "$1", color: "$soft",
36
37
  * own. `onPick` fires after a move, so a host that draws its rail as a drawer
37
38
  * on a phone can close it.
38
39
  */
39
- export function DevSection({ host, onPick, label = 'Runs', children, }) {
40
+ export function DevSection({ host, onPick, label, children, }) {
41
+ const { t } = useI18n();
40
42
  const sessions = useSessions(host);
41
43
  const r = route(host.path);
42
44
  // Every row, a place or a run, closes a drawer behind it.
@@ -44,5 +46,9 @@ export function DevSection({ host, onPick, label = 'Runs', children, }) {
44
46
  press?.();
45
47
  onPick?.();
46
48
  };
47
- return (_jsxs(_Fragment, { children: [nav(host, host.go).map((g) => (_jsx(SidebarSection, { label: g.label, children: g.links.map((l) => (_jsx(SidebarItem, { icon: l.icon, active: l.active, onPress: then(l.onPress), children: l.label }, l.id))) }, g.id))), children, _jsx(SidebarSection, { label: label, children: !host.person ? (_jsx(Note, { children: "Sign in to see your runs." })) : sessions.value.length ? (sessions.value.map((s) => (_jsx(SidebarItem, { icon: _jsx(StatusDot, { status: DOTS[s.status] ?? 'idle' }), active: r.kind === 'run' && r.id === s.id, onPress: then(() => host.go(s.id)), children: s.title || 'Untitled run' }, s.id)))) : (_jsx(Note, { children: sessions.error ? sessions.error.message : sessions.loading ? 'Reading…' : 'No runs yet. Say what to build and it lands here.' })) })] }));
49
+ return (_jsxs(_Fragment, { children: [nav(host, host.go, t).map((g) => (_jsx(SidebarSection, { label: g.label, children: g.links.map((l) => (_jsx(SidebarItem, { icon: l.icon, active: l.active, onPress: then(l.onPress), children: l.label }, l.id))) }, g.id))), children, _jsx(SidebarSection, { label: label ?? t('Runs'), children: !host.person ? (_jsx(Note, { children: t('Sign in to see your runs.') })) : sessions.value.length ? (sessions.value.map((s) => (_jsx(SidebarItem, { icon: _jsx(StatusDot, { status: DOTS[s.status] ?? 'idle' }), active: r.kind === 'run' && r.id === s.id, onPress: then(() => host.go(s.id)), children: s.title || t('Untitled run') }, s.id)))) : (_jsx(Note, { children: sessions.error
50
+ ? t(sessions.error.message)
51
+ : sessions.loading
52
+ ? t('Reading…')
53
+ : `${t('No runs yet.')} ${t('Say what to build and it lands here.')}` })) })] }));
48
54
  }
@@ -1,3 +1,4 @@
1
+ /** What a person may say they work on, each label English and marked (`phrase`), drawn through `t`. */
1
2
  export declare const WORK: {
2
3
  id: string;
3
4
  label: string;
@@ -6,33 +6,41 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
6
6
  * to, the one this page acts in, a way to act in another, and logging out.
7
7
  *
8
8
  * The email is how the person signs in, so it is shown and not edited here.
9
+ *
10
+ * Its words are the reader's language (@hanzo/i18n `t`); a name, an email and
11
+ * an organization's id are drawn as given.
9
12
  */
10
13
  import { SizableText, XStack, YStack } from '@hanzo/gui';
14
+ import { phrase } from '@hanzo/i18n';
15
+ import { useI18n } from '@hanzo/i18n/react';
11
16
  import { Check, Copy, LogOut } from '@hanzogui/lucide-icons-2';
12
17
  import { Button, Input, Textarea } from '@hanzo/ui';
13
18
  import { ChipSelect } from '@hanzo/ui/product';
14
19
  import { useEffect, useRef, useState } from 'react';
20
+ import { told } from '../api/call.js';
15
21
  import { PHOTO, photo, rename } from '../api/profile.js';
16
22
  import { useHost, useTarget } from '../host.js';
17
23
  import { usePrefs } from '../prefs.js';
18
24
  import { Card, Field, Group, Heading, Note, Row, Soft } from './ui.js';
25
+ /** What a person may say they work on, each label English and marked (`phrase`), drawn through `t`. */
19
26
  export const WORK = [
20
- { id: 'engineering', label: 'Software engineering' },
21
- { id: 'data', label: 'Data science' },
22
- { id: 'design', label: 'Design' },
23
- { id: 'product', label: 'Product management' },
24
- { id: 'research', label: 'Research' },
25
- { id: 'writing', label: 'Writing' },
26
- { id: 'education', label: 'Education' },
27
- { id: 'operations', label: 'Operations' },
28
- { id: 'marketing', label: 'Marketing and sales' },
29
- { id: 'other', label: 'Something else' },
27
+ { id: 'engineering', label: phrase('Software engineering') },
28
+ { id: 'data', label: phrase('Data science') },
29
+ { id: 'design', label: phrase('Design') },
30
+ { id: 'product', label: phrase('Product management') },
31
+ { id: 'research', label: phrase('Research') },
32
+ { id: 'writing', label: phrase('Writing') },
33
+ { id: 'education', label: phrase('Education') },
34
+ { id: 'operations', label: phrase('Operations') },
35
+ { id: 'marketing', label: phrase('Marketing and sales') },
36
+ { id: 'other', label: phrase('Something else') },
30
37
  ];
31
38
  /** Long enough for real guidance, short enough to sit in the settings document beside everything else. */
32
39
  const INSTRUCTIONS = 3000;
33
40
  export function Account() {
41
+ const { t } = useI18n();
34
42
  const host = useHost();
35
- const t = useTarget();
43
+ const target = useTarget();
36
44
  const { prefs, save } = usePrefs();
37
45
  const who = host.person;
38
46
  const [name, setName] = useState(who?.name ?? '');
@@ -57,7 +65,7 @@ export function Account() {
57
65
  setInstructions(prefs.instructions ?? '');
58
66
  }, [prefs.callName, prefs.work, prefs.instructions]);
59
67
  if (!who) {
60
- return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title: "Account" }), _jsx(Soft, { children: "Sign in to see your account." }), host.signIn ? (_jsx(XStack, { justify: "center", children: _jsx(Button, { size: "sm", onPress: () => host.signIn?.(), children: "Sign in" }) })) : null] }));
68
+ return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title: t('Account') }), _jsx(Soft, { children: t('Sign in to see your account.') }), host.signIn ? (_jsx(XStack, { justify: "center", children: _jsx(Button, { size: "sm", onPress: () => host.signIn?.(), children: t('Sign in') }) })) : null] }));
61
69
  }
62
70
  const patch = {};
63
71
  if (callName.trim() !== (prefs.callName ?? ''))
@@ -74,13 +82,13 @@ export function Account() {
74
82
  setNote('');
75
83
  try {
76
84
  if (renamed)
77
- setShownName((await rename(t, name)).displayName || name.trim());
85
+ setShownName((await rename(target, name)).displayName || name.trim());
78
86
  if (Object.keys(patch).length)
79
87
  await save(patch);
80
- setNote('Saved');
88
+ setNote(t('Saved'));
81
89
  }
82
90
  catch (e) {
83
- setNote(e.message);
91
+ setNote(told(e, t));
84
92
  }
85
93
  finally {
86
94
  setBusy(false);
@@ -90,11 +98,11 @@ export function Account() {
90
98
  setBusy(true);
91
99
  setNote('');
92
100
  try {
93
- setFace(await photo(t, file));
94
- setNote('Your photo is saved');
101
+ setFace(await photo(target, file));
102
+ setNote(t('Your photo is saved'));
95
103
  }
96
104
  catch (e) {
97
- setNote(e.message);
105
+ setNote(told(e, t));
98
106
  }
99
107
  finally {
100
108
  setBusy(false);
@@ -102,12 +110,14 @@ export function Account() {
102
110
  };
103
111
  const org = host.org;
104
112
  const orgs = host.memberships ?? (org ? [org] : []);
105
- const chosenWork = WORK.find((w) => w.id === work) ?? (work ? { id: work, label: work } : null);
106
- return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title: "Account", detail: "Your Hanzo identity, and what Hanzo should know about you.", action: host.signOut ? (_jsxs(Button, { size: "sm", variant: "outline", onPress: () => host.signOut?.(), children: [_jsx(LogOut, { size: 14 }), " Log out"] })) : undefined }), _jsx(Group, { title: "Profile", children: _jsxs(YStack, { gap: "$4", children: [_jsxs(XStack, { items: "center", gap: "$3", children: [face ? (_jsx("img", { src: face, alt: "", width: 48, height: 48, style: { borderRadius: 999, objectFit: 'cover' } })) : (_jsx(YStack, { width: 48, height: 48, rounded: 999, bg: "$raised", items: "center", justify: "center", children: _jsx(SizableText, { size: "$5", color: "$ink", children: (shownName || who.email || '?').charAt(0).toUpperCase() }) })), _jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$4", color: "$ink", numberOfLines: 1, children: shownName || who.email }), _jsx(SizableText, { size: "$2", color: "$soft", numberOfLines: 1, children: who.email })] }), _jsx(Button, { size: "sm", variant: "outline", disabled: busy, onPress: () => picker.current?.click(), children: "Change photo" }), _jsx("input", { ref: picker, type: "file", accept: PHOTO, hidden: true, style: { display: 'none' }, tabIndex: -1, "aria-label": "Photo", onChange: (e) => {
113
+ // The list's words in the reader's language; a work the list does not hold is the person's own, as given.
114
+ const works = WORK.map((w) => ({ ...w, label: t(w.label) }));
115
+ const chosenWork = works.find((w) => w.id === work) ?? (work ? { id: work, label: work } : null);
116
+ return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title: t('Account'), detail: t('Your Hanzo identity, and what Hanzo should know about you.'), action: host.signOut ? (_jsxs(Button, { size: "sm", variant: "outline", onPress: () => host.signOut?.(), children: [_jsx(LogOut, { size: 14 }), " ", t('Log out')] })) : undefined }), _jsx(Group, { title: t('Profile'), children: _jsxs(YStack, { gap: "$4", children: [_jsxs(XStack, { items: "center", gap: "$3", children: [face ? (_jsx("img", { src: face, alt: "", width: 48, height: 48, style: { borderRadius: 999, objectFit: 'cover' } })) : (_jsx(YStack, { width: 48, height: 48, rounded: 999, bg: "$raised", items: "center", justify: "center", children: _jsx(SizableText, { size: "$5", color: "$ink", children: (shownName || who.email || '?').charAt(0).toUpperCase() }) })), _jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$4", color: "$ink", numberOfLines: 1, children: shownName || who.email }), _jsx(SizableText, { size: "$2", color: "$soft", numberOfLines: 1, children: who.email })] }), _jsx(Button, { size: "sm", variant: "outline", disabled: busy, onPress: () => picker.current?.click(), children: t('Change photo') }), _jsx("input", { ref: picker, type: "file", accept: PHOTO, hidden: true, style: { display: 'none' }, tabIndex: -1, "aria-label": t('Photo'), onChange: (e) => {
107
117
  const input = e.currentTarget;
108
118
  const file = input.files?.[0];
109
119
  input.value = '';
110
120
  if (file)
111
121
  void upload(file);
112
- } })] }), _jsx(Field, { label: "Full name", children: _jsx(Input, { value: name, onChangeText: setName, "aria-label": "Full name", placeholder: "Your name" }) }), _jsx(Field, { label: "Email", hint: "You sign in with this address, so it is not changed here.", children: _jsx(SizableText, { size: "$2", color: "$soft", numberOfLines: 1, children: who.email || 'No email on this account' }) }), _jsx(Field, { label: "What should Hanzo call you?", children: _jsx(Input, { value: callName, onChangeText: setCallName, "aria-label": "What should Hanzo call you?", placeholder: shownName.split(' ')[0] || 'A first name' }) }), _jsx(Field, { label: "What best describes your work?", children: _jsx(XStack, { children: _jsx(ChipSelect, { name: "Work", label: chosenWork?.label ?? 'Choose one', chosen: chosenWork, items: WORK, onChange: (w) => setWork(w.id), placement: "bottom-start", width: 260 }) }) }), _jsx(Field, { label: "Instructions for Hanzo", hint: `Every coding run reads these before it starts. ${instructions.length}/${INSTRUCTIONS}`, children: _jsx(Textarea, { value: instructions, onChangeText: (v) => setInstructions(v.slice(0, INSTRUCTIONS)), "aria-label": "Instructions for Hanzo", placeholder: "How you like to work: the languages and tools you prefer, the style to follow, what to avoid.", rows: 5 }) }), _jsxs(XStack, { items: "center", gap: "$3", children: [_jsx(Button, { size: "sm", disabled: busy || !changed, onPress: () => void submit(), children: "Save" }), _jsx(Note, { children: note })] })] }) }), org ? (_jsx(Group, { title: "Organization", detail: "The organization this page acts in. Runs, codebases and settings belong to it.", children: _jsx(Card, { children: _jsx(Row, { first: true, mono: true, title: org, detail: `Organization ID${host.admin ? ' · you are an admin' : ''}`, trailing: _jsx(Button, { size: "icon-sm", variant: "ghost", "aria-label": copied ? 'Organization ID copied' : 'Copy organization ID', onPress: () => void navigator.clipboard?.writeText(org).then(() => setCopied(true)), children: copied ? _jsx(Check, { size: 14 }) : _jsx(Copy, { size: 14 }) }) }) }) })) : null, _jsx(Group, { title: "Organizations", detail: "Every organization you belong to. Switch to act in another.", children: orgs.length ? (_jsx(Card, { children: orgs.map((o, i) => (_jsx(Row, { first: i === 0, title: o, detail: o === host.org ? `Acting in ${o}${host.admin ? ' as an admin' : ''}` : undefined, trailing: o === host.org ? (_jsx(Check, { size: 16 })) : host.chooseOrg ? (_jsx(Button, { size: "sm", variant: "ghost", onPress: () => host.chooseOrg?.(o), children: "Switch" })) : null }, o))) })) : (_jsx(Soft, { children: "You belong to no organization yet." })) })] }));
122
+ } })] }), _jsx(Field, { label: t('Full name'), children: _jsx(Input, { value: name, onChangeText: setName, "aria-label": t('Full name'), placeholder: t('Your name') }) }), _jsx(Field, { label: t('Email'), hint: t('You sign in with this address, so it is not changed here.'), children: _jsx(SizableText, { size: "$2", color: "$soft", numberOfLines: 1, children: who.email || t('No email on this account') }) }), _jsx(Field, { label: t('What should Hanzo call you?'), children: _jsx(Input, { value: callName, onChangeText: setCallName, "aria-label": t('What should Hanzo call you?'), placeholder: shownName.split(' ')[0] || t('A first name') }) }), _jsx(Field, { label: t('What best describes your work?'), children: _jsx(XStack, { children: _jsx(ChipSelect, { name: t('Work'), label: chosenWork?.label ?? t('Choose one'), chosen: chosenWork, items: works, onChange: (w) => setWork(w.id), placement: "bottom-start", width: 260 }) }) }), _jsx(Field, { label: t('Instructions for Hanzo'), hint: t('Every coding run reads these before it starts. {used}/{most}', { used: instructions.length, most: INSTRUCTIONS }), children: _jsx(Textarea, { value: instructions, onChangeText: (v) => setInstructions(v.slice(0, INSTRUCTIONS)), "aria-label": t('Instructions for Hanzo'), placeholder: t('How you like to work: the languages and tools you prefer, the style to follow, what to avoid.'), rows: 5 }) }), _jsxs(XStack, { items: "center", gap: "$3", children: [_jsx(Button, { size: "sm", disabled: busy || !changed, onPress: () => void submit(), children: t('Save') }), _jsx(Note, { children: note })] })] }) }), org ? (_jsx(Group, { title: t('Organization'), detail: t('The organization this page acts in. Runs, codebases and settings belong to it.'), children: _jsx(Card, { children: _jsx(Row, { first: true, mono: true, title: org, detail: host.admin ? t('Organization ID · you are an admin') : t('Organization ID'), trailing: _jsx(Button, { size: "icon-sm", variant: "ghost", "aria-label": copied ? t('Organization ID copied') : t('Copy organization ID'), onPress: () => void navigator.clipboard?.writeText(org).then(() => setCopied(true)), children: copied ? _jsx(Check, { size: 14 }) : _jsx(Copy, { size: 14 }) }) }) }) })) : null, _jsx(Group, { title: t('Organizations'), detail: t('Every organization you belong to. Switch to act in another.'), children: orgs.length ? (_jsx(Card, { children: orgs.map((o, i) => (_jsx(Row, { first: i === 0, title: o, detail: o === host.org ? (host.admin ? t('Acting in {org} as an admin', { org: o }) : t('Acting in {org}', { org: o })) : undefined, trailing: o === host.org ? (_jsx(Check, { size: 16 })) : host.chooseOrg ? (_jsx(Button, { size: "sm", variant: "ghost", onPress: () => host.chooseOrg?.(o), children: t('Switch') })) : null }, o))) })) : (_jsx(Soft, { children: t('You belong to no organization yet.') })) })] }));
113
123
  }
@@ -4,12 +4,18 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
4
4
  * credits (`Credits`: the balance, grants, top-ups, auto-reload, this month's
5
5
  * spend and its limit), the cards it pays with, its invoices, and ending the
6
6
  * plan. Changing plan is the Plans screen; this page says what is true now.
7
+ *
8
+ * Its words are the reader's language (@hanzo/i18n `t`); a plan's name, a
9
+ * card's, an invoice's lines and its status are billing's, drawn as given.
7
10
  */
8
11
  import { SizableText, XStack, YStack } from '@hanzo/gui';
12
+ import { phrase } from '@hanzo/i18n';
13
+ import { useI18n } from '@hanzo/i18n/react';
9
14
  import { CreditCard, Download, Plus, X } from '@hanzogui/lucide-icons-2';
10
15
  import { Button, Dialog, DialogContent, DialogTitle, Input, toast } from '@hanzo/ui';
11
16
  import { useEffect, useState } from 'react';
12
17
  import { cancel, chosen, current, detach, formatAddress, invoices, label, makeDefault, methods, money, pdf, reactivate, subscriptions, tier, updateMethod, } from '../api/billing.js';
18
+ import { told } from '../api/call.js';
13
19
  import { useRead } from '../data.js';
14
20
  import { useHost, useTarget } from '../host.js';
15
21
  import { Title } from '../meter.js';
@@ -19,13 +25,14 @@ import { AddCard } from './card.js';
19
25
  import { Credits } from './credits.js';
20
26
  import { Card, day, Field, Group, Heading, Note, Row, Soft } from './ui.js';
21
27
  export function Billing() {
28
+ const { t } = useI18n();
22
29
  const host = useHost();
23
- const t = useTarget();
30
+ const target = useTarget();
24
31
  const signed = Boolean(host.person);
25
- const subs = useRead(signed ? () => subscriptions(t) : null, [], [t, signed]);
26
- const cards = useRead(signed ? () => methods(t) : null, [], [t, signed]);
27
- const bills = useRead(signed ? () => invoices(t) : null, [], [t, signed]);
28
- const billed = useRead(signed ? () => tier(t) : null, null, [t, signed]);
32
+ const subs = useRead(signed ? () => subscriptions(target) : null, [], [target, signed]);
33
+ const cards = useRead(signed ? () => methods(target) : null, [], [target, signed]);
34
+ const bills = useRead(signed ? () => invoices(target) : null, [], [target, signed]);
35
+ const billed = useRead(signed ? () => tier(target) : null, null, [target, signed]);
29
36
  const [adding, setAdding] = useState(false);
30
37
  const [ending, setEnding] = useState(false);
31
38
  const [editingBilling, setEditingBilling] = useState(null);
@@ -33,7 +40,7 @@ export function Billing() {
33
40
  const [working, setWorking] = useState(false);
34
41
  const [note, setNote] = useState('');
35
42
  if (!signed) {
36
- return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title: "Billing" }), _jsx(Soft, { children: "Sign in to see this organization\u2019s billing." })] }));
43
+ return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title: t('Billing') }), _jsx(Soft, { children: t('Sign in to see this organization’s billing.') })] }));
37
44
  }
38
45
  const plan = current(subs.value);
39
46
  // The plan by its family, `Max` with `20x` beside it; the subscription names
@@ -54,32 +61,33 @@ export function Billing() {
54
61
  toast.success(done);
55
62
  }
56
63
  catch (e) {
57
- setNote(e.message);
64
+ setNote(told(e, t));
58
65
  }
59
66
  finally {
60
67
  setWorking(false);
61
68
  }
62
69
  };
63
- return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title: "Billing", detail: `The plan ${host.org ?? 'this organization'} is on, the cards it pays with, and its invoices.` }), _jsxs(XStack, { items: "center", gap: "$3", px: "$4", py: "$4", borderWidth: 1, borderColor: "$borderColor", rounded: "$3", flexWrap: "wrap", children: [_jsxs(YStack, { flex: 1, minW: 200, gap: "$1", "data-slot": "billing-plan", children: [reading ? (_jsx(SizableText, { size: "$5", color: "$ink", children: "Reading your plan\u2026" })) : (_jsx(Title, { label: title ?? { name: 'Free', tag: '' }, size: "$5" })), _jsx(SizableText, { size: "$2", color: "$soft", children: subs.error ? subs.error.message : plan ? terms(plan) : title ? 'Your plan.' : 'No paid plan. Usage is paid from credits.' })] }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(path({ kind: 'screen', screen: 'plans' })), children: "Adjust plan" })] }), _jsx(Credits, { target: t, admin: host.admin, onCard: () => setAdding(true) }), _jsxs(Group, { title: "Payment", detail: "The card a plan renews on and a top-up charges.", action: host.admin && (cards.error || cards.value.length > 0) ? (_jsxs(Button, { size: "sm", variant: "outline", onPress: () => setAdding(true), children: [_jsx(Plus, { size: 14 }), " Add card"] })) : undefined, children: [cards.error ? (_jsx(Soft, { children: cards.error.message })) : cards.loading && !cards.value.length ? (_jsx(Soft, { children: "Reading cards\u2026" })) : empty ? (_jsxs(XStack, { items: "center", gap: "$3", px: "$4", py: "$4", borderWidth: 1, borderColor: "$borderColor", rounded: "$3", flexWrap: "wrap", children: [_jsx(CreditCard, { size: 20 }), _jsxs(YStack, { flex: 1, minW: 180, gap: "$1", children: [_jsx(SizableText, { size: "$3", color: "$ink", children: "No card on file." }), _jsx(SizableText, { size: "$2", color: "$soft", children: host.admin ? 'Add one to pay for a plan or a top-up. Adding it charges nothing.' : 'An org admin adds cards.' })] }), host.admin ? (_jsxs(Button, { variant: "primary", onPress: () => setAdding(true), children: [_jsx(Plus, { size: 16 }), " Add card"] })) : null] })) : (_jsx(Card, { children: cards.value.map((m, i) => (_jsx(Row, { first: i === 0, leading: _jsx(CreditCard, { size: 16 }), title: label(m), detail: [
64
- m.expires ? `Expires ${m.expires}` : '',
65
- m.default ? 'Default' : '',
70
+ return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title: t('Billing'), detail: host.org ? t('The plan {org} is on, the cards it pays with, and its invoices.', { org: host.org }) : t('The plan this organization is on, the cards it pays with, and its invoices.') }), _jsxs(XStack, { items: "center", gap: "$3", px: "$4", py: "$4", borderWidth: 1, borderColor: "$borderColor", rounded: "$3", flexWrap: "wrap", children: [_jsxs(YStack, { flex: 1, minW: 200, gap: "$1", "data-slot": "billing-plan", children: [reading ? (_jsx(SizableText, { size: "$5", color: "$ink", children: t('Reading your plan…') })) : (_jsx(Title, { label: title ?? { name: phrase('Free'), tag: '' }, size: "$5" })), _jsx(SizableText, { size: "$2", color: "$soft", children: subs.error ? told(subs.error, t) : plan ? terms(plan, t) : title ? t('Your plan.') : t('No paid plan. Usage is paid from credits.') })] }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(path({ kind: 'screen', screen: 'plans' })), children: t('Adjust plan') })] }), _jsx(Credits, { target: target, admin: host.admin, onCard: () => setAdding(true) }), _jsxs(Group, { title: t('Payment'), detail: t('The card a plan renews on and a top-up charges.'), action: host.admin && (cards.error || cards.value.length > 0) ? (_jsxs(Button, { size: "sm", variant: "outline", onPress: () => setAdding(true), children: [_jsx(Plus, { size: 14 }), " ", t('Add card')] })) : undefined, children: [cards.error ? (_jsx(Soft, { children: told(cards.error, t) })) : cards.loading && !cards.value.length ? (_jsx(Soft, { children: t('Reading cards…') })) : empty ? (_jsxs(XStack, { items: "center", gap: "$3", px: "$4", py: "$4", borderWidth: 1, borderColor: "$borderColor", rounded: "$3", flexWrap: "wrap", children: [_jsx(CreditCard, { size: 20 }), _jsxs(YStack, { flex: 1, minW: 180, gap: "$1", children: [_jsx(SizableText, { size: "$3", color: "$ink", children: t('No card on file.') }), _jsx(SizableText, { size: "$2", color: "$soft", children: host.admin ? t('Add one to pay for a plan or a top-up. Adding it charges nothing.') : t('An org admin adds cards.') })] }), host.admin ? (_jsxs(Button, { variant: "primary", onPress: () => setAdding(true), children: [_jsx(Plus, { size: 16 }), " ", t('Add card')] })) : null] })) : (_jsx(Card, { children: cards.value.map((m, i) => (_jsx(Row, { first: i === 0, leading: _jsx(CreditCard, { size: 16 }), title: method(m, t), detail: [
71
+ m.expires ? t('Expires {date}', { date: m.expires }) : '',
72
+ m.default ? t('Default') : '',
66
73
  m.billingAddress ? formatAddress(m.billingAddress) : '',
67
74
  ]
68
75
  .filter(Boolean)
69
- .join(' · ') || undefined, trailing: host.admin ? (_jsxs(XStack, { gap: "$2", items: "center", children: [!m.default ? (_jsx(Button, { size: "sm", variant: "ghost", "aria-label": `Make ${label(m)} default`, disabled: working, onPress: () => void act(() => makeDefault(t, m.id), `${label(m)} is now the default`, cards.reload), children: "Make default" })) : null, _jsx(Button, { size: "sm", variant: "ghost", "aria-label": `Edit billing info for ${label(m)}`, disabled: working, onPress: () => setEditingBilling(m), children: "Edit info" }), _jsx(Button, { size: "sm", variant: "ghost", "aria-label": `Remove ${label(m)}`, disabled: working, onPress: () => void act(() => detach(t, m.id), `${label(m)} is removed`, cards.reload), children: "Remove" })] })) : undefined }, m.id))) })), !host.admin && cards.value.length ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "An org admin adds and removes cards." })) : null] }), _jsx(Group, { title: "Billing details", detail: "Information printed on invoices and payment receipts.", action: host.admin && defaultCard ? (_jsx(Button, { size: "sm", variant: "outline", onPress: () => setEditingBilling(defaultCard), children: defaultCard.billingAddress ? 'Edit details' : 'Add billing info' })) : undefined, children: defaultCard?.billingAddress ? (_jsx(Card, { children: _jsxs(YStack, { p: "$3", gap: "$1.5", children: [defaultCard.billingAddress.name ? (_jsx(SizableText, { size: "$3", color: "$ink", style: { fontWeight: 600 }, children: defaultCard.billingAddress.name })) : null, defaultCard.billingAddress.line1 ? (_jsxs(SizableText, { size: "$2", color: "$soft", children: [defaultCard.billingAddress.line1, defaultCard.billingAddress.line2 ? `, ${defaultCard.billingAddress.line2}` : ''] })) : null, defaultCard.billingAddress.city || defaultCard.billingAddress.state || defaultCard.billingAddress.postalCode ? (_jsx(SizableText, { size: "$2", color: "$soft", children: [defaultCard.billingAddress.city, defaultCard.billingAddress.state, defaultCard.billingAddress.postalCode]
76
+ .join(' · ') || undefined, trailing: host.admin ? (_jsxs(XStack, { gap: "$2", items: "center", children: [!m.default ? (_jsx(Button, { size: "sm", variant: "ghost", "aria-label": t('Make {card} default', { card: method(m, t) }), disabled: working, onPress: () => void act(() => makeDefault(target, m.id), t('{card} is now the default', { card: method(m, t) }), cards.reload), children: t('Make default') })) : null, _jsx(Button, { size: "sm", variant: "ghost", "aria-label": t('Edit billing info for {card}', { card: method(m, t) }), disabled: working, onPress: () => setEditingBilling(m), children: t('Edit info') }), _jsx(Button, { size: "sm", variant: "ghost", "aria-label": t('Remove {card}', { card: method(m, t) }), disabled: working, onPress: () => void act(() => detach(target, m.id), t('{card} is removed', { card: method(m, t) }), cards.reload), children: t('Remove') })] })) : undefined }, m.id))) })), !host.admin && cards.value.length ? (_jsx(SizableText, { size: "$1", color: "$soft", children: t('An org admin adds and removes cards.') })) : null] }), _jsx(Group, { title: t('Billing details'), detail: t('Information printed on invoices and payment receipts.'), action: host.admin && defaultCard ? (_jsx(Button, { size: "sm", variant: "outline", onPress: () => setEditingBilling(defaultCard), children: defaultCard.billingAddress ? t('Edit details') : t('Add billing info') })) : undefined, children: defaultCard?.billingAddress ? (_jsx(Card, { children: _jsxs(YStack, { p: "$3", gap: "$1.5", children: [defaultCard.billingAddress.name ? (_jsx(SizableText, { size: "$3", color: "$ink", style: { fontWeight: 600 }, children: defaultCard.billingAddress.name })) : null, defaultCard.billingAddress.line1 ? (_jsxs(SizableText, { size: "$2", color: "$soft", children: [defaultCard.billingAddress.line1, defaultCard.billingAddress.line2 ? `, ${defaultCard.billingAddress.line2}` : ''] })) : null, defaultCard.billingAddress.city || defaultCard.billingAddress.state || defaultCard.billingAddress.postalCode ? (_jsx(SizableText, { size: "$2", color: "$soft", children: [defaultCard.billingAddress.city, defaultCard.billingAddress.state, defaultCard.billingAddress.postalCode]
70
77
  .filter(Boolean)
71
- .join(' ') })) : null, defaultCard.billingAddress.country ? (_jsx(SizableText, { size: "$2", color: "$soft", children: defaultCard.billingAddress.country })) : null] }) })) : (_jsxs(XStack, { items: "center", justify: "space-between", gap: "$3", px: "$4", py: "$3", borderWidth: 1, borderColor: "$borderColor", rounded: "$3", flexWrap: "wrap", children: [_jsxs(YStack, { flex: 1, minW: 180, gap: "$1", children: [_jsx(SizableText, { size: "$2", color: "$ink", children: "No billing address on file." }), _jsx(SizableText, { size: "$1", color: "$soft", children: "Add your company name and address so they appear on monthly invoices and receipts." })] }), host.admin ? (_jsxs(Button, { size: "sm", variant: "outline", onPress: () => (defaultCard ? setEditingBilling(defaultCard) : setAdding(true)), children: [_jsx(Plus, { size: 14 }), " ", defaultCard ? 'Add billing info' : 'Set billing info'] })) : null] })) }), _jsx(Group, { title: "Invoices", children: bills.error ? (_jsx(Soft, { children: bills.error.message })) : bills.loading && !bills.value.length ? (_jsx(Soft, { children: "Reading invoices\u2026" })) : bills.value.length === 0 ? (_jsx(Card, { children: _jsx(Soft, { children: "No invoices yet." }) })) : (_jsxs(Card, { children: [_jsxs(XStack, { px: "$3", py: "$2", gap: "$3", children: [_jsx(SizableText, { flex: 1, size: "$1", color: "$soft", children: "Date" }), _jsx(SizableText, { width: 90, size: "$1", color: "$soft", style: { textAlign: 'right' }, children: "Total" }), _jsx(SizableText, { width: 70, size: "$1", color: "$soft", "$max-sm": { display: 'none' }, children: "Status" }), _jsx(YStack, { width: 64 })] }), bills.value.map((b) => (_jsxs(XStack, { items: "center", gap: "$3", px: "$3", py: "$2.5", borderTopWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { flex: 1, size: "$2", color: "$ink", numberOfLines: 1, children: day(b.date) || b.number || b.id }), _jsx(SizableText, { width: 90, size: "$2", color: "$ink", style: { textAlign: 'right' }, children: money(b.total, b.currency) }), _jsx(SizableText, { width: 70, size: "$1", color: "$soft", "$max-sm": { display: 'none' }, children: word(b.status) }), _jsx(XStack, { width: 64, justify: "flex-end", children: _jsx(Button, { size: "sm", variant: "ghost", "aria-label": `View invoice ${b.number || day(b.date)}`, onPress: () => setOpen(b), children: "View" }) })] }, b.id)))] })) }), plan ? (_jsx(Group, { title: "Cancellation", children: _jsx(Card, { children: _jsx(Row, { first: true, title: plan.ending ? `Your plan ends on ${day(plan.ends)}` : 'Cancel plan', detail: plan.ending ? 'Keep it, and it renews as before.' : 'It runs to the end of the period you paid for, then ends.', trailing: !host.admin ? undefined : plan.ending ? (_jsx(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void act(() => reactivate(t, plan.id), 'Your plan renews as before', subs.reload), children: "Keep plan" })) : (_jsx(Button, { size: "sm", variant: "destructive", disabled: working, onPress: () => setEnding(true), children: "Cancel" })) }) }) })) : null, _jsx(Note, { children: note }), _jsx(AddCard, { open: adding, onOpenChange: setAdding, onSaved: (m) => {
78
+ .join(' ') })) : null, defaultCard.billingAddress.country ? (_jsx(SizableText, { size: "$2", color: "$soft", children: defaultCard.billingAddress.country })) : null] }) })) : (_jsxs(XStack, { items: "center", justify: "space-between", gap: "$3", px: "$4", py: "$3", borderWidth: 1, borderColor: "$borderColor", rounded: "$3", flexWrap: "wrap", children: [_jsxs(YStack, { flex: 1, minW: 180, gap: "$1", children: [_jsx(SizableText, { size: "$2", color: "$ink", children: t('No billing address on file.') }), _jsx(SizableText, { size: "$1", color: "$soft", children: t('Add your company name and address so they appear on monthly invoices and receipts.') })] }), host.admin ? (_jsxs(Button, { size: "sm", variant: "outline", onPress: () => (defaultCard ? setEditingBilling(defaultCard) : setAdding(true)), children: [_jsx(Plus, { size: 14 }), " ", defaultCard ? t('Add billing info') : t('Set billing info')] })) : null] })) }), _jsx(Group, { title: t('Invoices'), children: bills.error ? (_jsx(Soft, { children: told(bills.error, t) })) : bills.loading && !bills.value.length ? (_jsx(Soft, { children: t('Reading invoices…') })) : bills.value.length === 0 ? (_jsx(Card, { children: _jsx(Soft, { children: t('No invoices yet.') }) })) : (_jsxs(Card, { children: [_jsxs(XStack, { px: "$3", py: "$2", gap: "$3", children: [_jsx(SizableText, { flex: 1, size: "$1", color: "$soft", children: t('Date') }), _jsx(SizableText, { width: 90, size: "$1", color: "$soft", text: "end", children: t('Total') }), _jsx(SizableText, { width: 70, size: "$1", color: "$soft", "$max-sm": { display: 'none' }, children: t('Status') }), _jsx(YStack, { width: 64 })] }), bills.value.map((b) => (_jsxs(XStack, { items: "center", gap: "$3", px: "$3", py: "$2.5", borderTopWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { flex: 1, size: "$2", color: "$ink", numberOfLines: 1, children: day(b.date) || b.number || b.id }), _jsx(SizableText, { width: 90, size: "$2", color: "$ink", text: "end", children: money(b.total, b.currency) }), _jsx(SizableText, { width: 70, size: "$1", color: "$soft", "$max-sm": { display: 'none' }, children: word(b.status) }), _jsx(XStack, { width: 64, justify: "flex-end", children: _jsx(Button, { size: "sm", variant: "ghost", "aria-label": t('View invoice {invoice}', { invoice: b.number || day(b.date) }), onPress: () => setOpen(b), children: t('View') }) })] }, b.id)))] })) }), plan ? (_jsx(Group, { title: t('Cancellation'), children: _jsx(Card, { children: _jsx(Row, { first: true, title: plan.ending ? t('Your plan ends on {date}', { date: day(plan.ends) }) : t('Cancel plan'), detail: plan.ending ? t('Keep it, and it renews as before.') : t('It runs to the end of the period you paid for, then ends.'), trailing: !host.admin ? undefined : plan.ending ? (_jsx(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void act(() => reactivate(target, plan.id), t('Your plan renews as before'), subs.reload), children: t('Keep plan') })) : (_jsx(Button, { size: "sm", variant: "destructive", disabled: working, onPress: () => setEnding(true), children: t('Cancel') })) }) }) })) : null, _jsx(Note, { children: note }), _jsx(AddCard, { open: adding, onOpenChange: setAdding, onSaved: (m) => {
72
79
  cards.reload();
73
- toast.success(`${label(m)} is saved`);
80
+ toast.success(t('{card} is saved', { card: method(m, t) }));
74
81
  } }), _jsx(EditBilling, { open: editingBilling !== null, onOpenChange: (o) => !o && setEditingBilling(null), card: editingBilling, onSaved: () => {
75
82
  cards.reload();
76
- } }), plan ? (_jsx(Ending, { plan: plan, open: ending, working: working, onOpenChange: setEnding, onConfirm: () => void act(() => cancel(t, plan.id), `Your plan ends on ${day(plan.ends)}`, () => {
83
+ } }), plan ? (_jsx(Ending, { plan: plan, open: ending, working: working, onOpenChange: setEnding, onConfirm: () => void act(() => cancel(target, plan.id), t('Your plan ends on {date}', { date: day(plan.ends) }), () => {
77
84
  setEnding(false);
78
85
  subs.reload();
79
86
  }) })) : null, open ? _jsx(Bill, { invoice: open, onClose: () => setOpen(null) }) : null] }));
80
87
  }
81
88
  function EditBilling({ open, onOpenChange, card, onSaved, }) {
82
- const t = useTarget();
89
+ const { t } = useI18n();
90
+ const target = useTarget();
83
91
  const [name, setName] = useState('');
84
92
  const [line1, setLine1] = useState('');
85
93
  const [line2, setLine2] = useState('');
@@ -125,39 +133,58 @@ function EditBilling({ open, onOpenChange, card, onSaved, }) {
125
133
  postalCode: postalCode.trim() || undefined,
126
134
  country: country.trim() || undefined,
127
135
  };
128
- await updateMethod(t, card.id, { billingAddress: address });
136
+ await updateMethod(target, card.id, { billingAddress: address });
129
137
  onSaved();
130
138
  onOpenChange(false);
131
- toast.success('Billing details updated');
139
+ toast.success(t('Billing details updated'));
132
140
  }
133
141
  catch (e) {
134
- setNote(e.message);
142
+ setNote(told(e, t));
135
143
  }
136
144
  finally {
137
145
  setWorking(false);
138
146
  }
139
147
  };
140
- return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: 460, showCloseButton: false, children: [_jsxs(XStack, { items: "center", justify: "space-between", gap: "$2", children: [_jsx(DialogTitle, { children: "Billing details" }), _jsx(XStack, { render: "button", "aria-label": "Close", p: "$1", onPress: () => onOpenChange(false), children: _jsx(X, { size: 16 }) })] }), _jsxs(YStack, { gap: "$3", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: "These details appear on your invoices and receipts." }), _jsx(Field, { label: "Business or personal name", children: _jsx(Input, { value: name, onChangeText: setName, placeholder: "e.g. Webby AI, Inc. or Joshua Lynch" }) }), _jsx(Field, { label: "Address line 1", children: _jsx(Input, { value: line1, onChangeText: setLine1, placeholder: "Street address or P.O. Box" }) }), _jsx(Field, { label: "Address line 2", hint: "Optional", children: _jsx(Input, { value: line2, onChangeText: setLine2, placeholder: "Suite, unit, building, floor" }) }), _jsxs(XStack, { gap: "$2", children: [_jsx(YStack, { flex: 1, children: _jsx(Field, { label: "City", children: _jsx(Input, { value: city, onChangeText: setCity, placeholder: "City" }) }) }), _jsx(YStack, { width: 100, children: _jsx(Field, { label: "State", children: _jsx(Input, { value: stateVal, onChangeText: setStateVal, placeholder: "State / Prov" }) }) })] }), _jsxs(XStack, { gap: "$2", children: [_jsx(YStack, { flex: 1, children: _jsx(Field, { label: "Postal / ZIP code", children: _jsx(Input, { value: postalCode, onChangeText: setPostalCode, placeholder: "ZIP or Postal Code" }) }) }), _jsx(YStack, { width: 110, children: _jsx(Field, { label: "Country", children: _jsx(Input, { value: country, onChangeText: setCountry, placeholder: "Country (US)" }) }) })] }), note ? (_jsx(SizableText, { size: "$2", color: "$red10", role: "alert", children: note })) : null, _jsxs(XStack, { gap: "$2", justify: "flex-end", pt: "$2", children: [_jsx(Button, { size: "sm", variant: "ghost", disabled: working, onPress: () => onOpenChange(false), children: "Cancel" }), _jsx(Button, { size: "sm", disabled: working, onPress: () => void save(), children: "Save details" })] })] })] }) }));
148
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: 460, showCloseButton: false, children: [_jsxs(XStack, { items: "center", justify: "space-between", gap: "$2", children: [_jsx(DialogTitle, { children: t('Billing details') }), _jsx(XStack, { render: "button", "aria-label": t('Close'), p: "$1", onPress: () => onOpenChange(false), children: _jsx(X, { size: 16 }) })] }), _jsxs(YStack, { gap: "$3", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: t('These details appear on your invoices and receipts.') }), _jsx(Field, { label: t('Business or personal name'), children: _jsx(Input, { value: name, onChangeText: setName, placeholder: t('e.g. Webby AI, Inc. or Joshua Lynch') }) }), _jsx(Field, { label: t('Address line 1'), children: _jsx(Input, { value: line1, onChangeText: setLine1, placeholder: t('Street address or P.O. Box') }) }), _jsx(Field, { label: t('Address line 2'), hint: t('Optional'), children: _jsx(Input, { value: line2, onChangeText: setLine2, placeholder: t('Suite, unit, building, floor') }) }), _jsxs(XStack, { gap: "$2", children: [_jsx(YStack, { flex: 1, children: _jsx(Field, { label: t('City'), children: _jsx(Input, { value: city, onChangeText: setCity, placeholder: t('City') }) }) }), _jsx(YStack, { width: 100, children: _jsx(Field, { label: t('State'), children: _jsx(Input, { value: stateVal, onChangeText: setStateVal, placeholder: t('State / Prov') }) }) })] }), _jsxs(XStack, { gap: "$2", children: [_jsx(YStack, { flex: 1, children: _jsx(Field, { label: t('Postal / ZIP code'), children: _jsx(Input, { value: postalCode, onChangeText: setPostalCode, placeholder: t('ZIP or Postal Code') }) }) }), _jsx(YStack, { width: 110, children: _jsx(Field, { label: t('Country'), children: _jsx(Input, { value: country, onChangeText: setCountry, placeholder: t('Country (US)') }) }) })] }), note ? (_jsx(SizableText, { size: "$2", color: "$red10", role: "alert", children: note })) : null, _jsxs(XStack, { gap: "$2", justify: "flex-end", pt: "$2", children: [_jsx(Button, { size: "sm", variant: "ghost", disabled: working, onPress: () => onOpenChange(false), children: t('Cancel') }), _jsx(Button, { size: "sm", disabled: working, onPress: () => void save(), children: t('Save details') })] })] })] }) }));
141
149
  }
142
- /** What the plan costs, and when it renews or ends. */
143
- function terms(p) {
144
- const cost = `${money(p.price * p.seats, p.currency)} a ${p.interval}${p.seats > 1 ? ` for ${p.seats} seats` : ''}.`;
145
- return p.ends ? `${cost} ${p.ending ? 'Ends' : 'Renews'} on ${day(p.ends)}.` : cost;
150
+ /** What a plan costs by its interval, alone and for its seats: one sentence each, so a language says the interval its own way. */
151
+ const COST = {
152
+ month: { one: phrase('{amount} a month.'), seats: phrase('{amount} a month for {seats} seats.') },
153
+ year: { one: phrase('{amount} a year.'), seats: phrase('{amount} a year for {seats} seats.') },
154
+ };
155
+ /** What the plan costs, and when it renews or ends, in the words of `t`. */
156
+ function terms(p, t) {
157
+ const amount = money(p.price * p.seats, p.currency);
158
+ const by = COST[p.interval];
159
+ const cost = by
160
+ ? t(p.seats > 1 ? by.seats : by.one, { amount, seats: p.seats })
161
+ : p.seats > 1
162
+ ? t('{amount} a {interval} for {seats} seats.', { amount, interval: p.interval, seats: p.seats })
163
+ : t('{amount} a {interval}.', { amount, interval: p.interval });
164
+ if (!p.ends)
165
+ return cost;
166
+ return `${cost} ${p.ending ? t('Ends on {date}.', { date: day(p.ends) }) : t('Renews on {date}.', { date: day(p.ends) })}`;
146
167
  }
168
+ /** A saved method as a row names it (api/billing.ts `label`); a card with no brand is said in the words of `t`. */
169
+ const method = (m, t) => (m.last4 && !m.brand ? t('Card •••• {last4}', { last4: m.last4 }) : label(m));
147
170
  const word = (s) => (s ? s.charAt(0).toUpperCase() + s.slice(1).replace(/_/g, ' ') : '');
148
171
  function Ending({ plan, open, working, onOpenChange, onConfirm, }) {
149
- return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: 420, showCloseButton: false, children: [_jsx(DialogTitle, { children: "Cancel your plan?" }), _jsxs(YStack, { gap: "$3", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: `${plan.name} stays on until ${day(plan.ends) || 'the end of this period'}, and nothing is charged after that. You can keep it any time before then.` }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => onOpenChange(false), children: "Keep plan" }), _jsx(Button, { size: "sm", variant: "destructive", disabled: working, onPress: onConfirm, children: "Cancel plan" })] })] })] }) }));
172
+ const { t } = useI18n();
173
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: 420, showCloseButton: false, children: [_jsx(DialogTitle, { children: t('Cancel your plan?') }), _jsxs(YStack, { gap: "$3", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: day(plan.ends)
174
+ ? t('{plan} stays on until {date}, and nothing is charged after that. You can keep it any time before then.', { plan: plan.name, date: day(plan.ends) })
175
+ : t('{plan} stays on until the end of this period, and nothing is charged after that. You can keep it any time before then.', { plan: plan.name }) }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => onOpenChange(false), children: t('Keep plan') }), _jsx(Button, { size: "sm", variant: "destructive", disabled: working, onPress: onConfirm, children: t('Cancel plan') })] })] })] }) }));
150
176
  }
151
177
  /** One invoice, read in the page, with its PDF to keep. Drawn while it is open, so each opens anew. */
152
178
  function Bill({ invoice: b, onClose }) {
153
- const t = useTarget();
179
+ const { t } = useI18n();
180
+ const target = useTarget();
154
181
  const [note, setNote] = useState('');
155
182
  const [working, setWorking] = useState(false);
156
183
  const keep = async () => {
157
184
  setWorking(true);
158
185
  setNote('');
159
186
  try {
160
- const blob = await pdf(t, b.id);
187
+ const blob = await pdf(target, b.id);
161
188
  const url = URL.createObjectURL(blob);
162
189
  const a = document.createElement('a');
163
190
  a.href = url;
@@ -166,12 +193,12 @@ function Bill({ invoice: b, onClose }) {
166
193
  window.setTimeout(() => URL.revokeObjectURL(url), 1000);
167
194
  }
168
195
  catch (e) {
169
- setNote(e.message);
196
+ setNote(told(e, t));
170
197
  }
171
198
  finally {
172
199
  setWorking(false);
173
200
  }
174
201
  };
175
202
  const line = (what, cents, strong = false) => (_jsxs(XStack, { justify: "space-between", gap: "$3", children: [_jsx(SizableText, { size: "$2", color: strong ? '$ink' : '$soft', children: what }), _jsx(SizableText, { size: "$2", color: strong ? '$ink' : '$soft', children: money(cents, b.currency) })] }, what));
176
- return (_jsx(Dialog, { open: true, onOpenChange: (o) => !o && onClose(), children: _jsxs(DialogContent, { maxW: 480, showCloseButton: false, children: [_jsxs(XStack, { items: "center", justify: "space-between", gap: "$2", children: [_jsx(DialogTitle, { children: b.number ? `Invoice ${b.number}` : 'Invoice' }), _jsx(XStack, { render: "button", "aria-label": "Close", p: "$1", onPress: onClose, children: _jsx(X, { size: 16 }) })] }), _jsxs(YStack, { gap: "$3", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: [day(b.date), word(b.status), b.start && b.end ? `for ${day(b.start)} – ${day(b.end)}` : ''].filter(Boolean).join(' · ') }), _jsx(YStack, { gap: "$1.5", borderTopWidth: 1, borderColor: "$borderColor", pt: "$3", children: b.lines.length ? b.lines.map((l, i) => line(l.description || `Line ${i + 1}`, l.amount)) : line('Subtotal', b.subtotal) }), _jsxs(YStack, { gap: "$1.5", borderTopWidth: 1, borderColor: "$borderColor", pt: "$3", children: [b.discount ? line('Discount', -b.discount) : null, b.tax ? line('Tax', b.tax) : null, line('Total', b.total, true), b.credit ? line('Credit applied', -b.credit) : null, line('Paid', b.paid), b.due ? line('Due', b.due, true) : null] }), _jsx(Note, { children: note }), _jsx(XStack, { justify: "flex-end", children: _jsxs(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void keep(), children: [_jsx(Download, { size: 14 }), " Download PDF"] }) })] })] }) }));
203
+ return (_jsx(Dialog, { open: true, onOpenChange: (o) => !o && onClose(), children: _jsxs(DialogContent, { maxW: 480, showCloseButton: false, children: [_jsxs(XStack, { items: "center", justify: "space-between", gap: "$2", children: [_jsx(DialogTitle, { children: b.number ? t('Invoice {number}', { number: b.number }) : t('Invoice') }), _jsx(XStack, { render: "button", "aria-label": t('Close'), p: "$1", onPress: onClose, children: _jsx(X, { size: 16 }) })] }), _jsxs(YStack, { gap: "$3", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: [day(b.date), word(b.status), b.start && b.end ? t('for {start} – {end}', { start: day(b.start), end: day(b.end) }) : ''].filter(Boolean).join(' · ') }), _jsx(YStack, { gap: "$1.5", borderTopWidth: 1, borderColor: "$borderColor", pt: "$3", children: b.lines.length ? b.lines.map((l, i) => line(l.description || t('Line {n}', { n: i + 1 }), l.amount)) : line(t('Subtotal'), b.subtotal) }), _jsxs(YStack, { gap: "$1.5", borderTopWidth: 1, borderColor: "$borderColor", pt: "$3", children: [b.discount ? line(t('Discount'), -b.discount) : null, b.tax ? line(t('Tax'), b.tax) : null, line(t('Total'), b.total, true), b.credit ? line(t('Credit applied'), -b.credit) : null, line(t('Paid'), b.paid), b.due ? line(t('Due'), b.due, true) : null] }), _jsx(Note, { children: note }), _jsx(XStack, { justify: "flex-end", children: _jsxs(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void keep(), children: [_jsx(Download, { size: 14 }), " ", t('Download PDF')] }) })] })] }) }));
177
204
  }
@@ -5,51 +5,62 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
5
5
  * servers. A tool that is off is still listed to the agent's discovery but is
6
6
  * refused when called. Switching a kind on turns on every tool of that kind;
7
7
  * off turns them all off. It is the organization's setting, not only yours.
8
+ *
9
+ * Its words are the reader's language (@hanzo/i18n `t`).
8
10
  */
9
11
  import { SizableText, YStack } from '@hanzo/gui';
12
+ import { phrase } from '@hanzo/i18n';
13
+ import { useI18n } from '@hanzo/i18n/react';
10
14
  import { Switch } from '@hanzo/ui';
11
15
  import { useState } from 'react';
16
+ import { told } from '../api/call.js';
12
17
  import { activate, SOURCES, tools } from '../api/capabilities.js';
13
18
  import { useRead } from '../data.js';
14
19
  import { useHost, useTarget } from '../host.js';
15
20
  import { Card, Group, Heading, Note, Row, Soft } from './ui.js';
21
+ /** Each kind's name and the sentence that says what it is, English and marked (`phrase`), drawn through `t`. */
16
22
  const KINDS = {
17
- connector: { label: 'Connector actions', about: 'Actions in the apps your organization connected' },
18
- function: { label: 'Functions', about: 'Functions your organization wrote' },
19
- 'zap-service': { label: 'Services', about: 'Routes of the services your organization runs' },
20
- agent: { label: 'Agents', about: 'Your organization’s agents, called as tools' },
21
- skill: { label: 'Skills', about: 'Skills attached to agents' },
22
- mcp: { label: 'MCP servers', about: 'Tools on the MCP servers your organization added' },
23
+ connector: { label: phrase('Connector actions'), about: phrase('Actions in the apps your organization connected.') },
24
+ function: { label: phrase('Functions'), about: phrase('Functions your organization wrote.') },
25
+ 'zap-service': { label: phrase('Services'), about: phrase('Routes of the services your organization runs.') },
26
+ agent: { label: phrase('Agents'), about: phrase('Your organization’s agents, called as tools.') },
27
+ skill: { label: phrase('Skills'), about: phrase('Skills attached to agents.') },
28
+ mcp: { label: phrase('MCP servers'), about: phrase('Tools on the MCP servers your organization added.') },
23
29
  };
24
30
  export function Capabilities() {
31
+ const { t } = useI18n();
25
32
  const host = useHost();
26
- const t = useTarget();
33
+ const target = useTarget();
27
34
  const signed = Boolean(host.person);
28
- const list = useRead(signed ? () => tools(t) : null, [], [t, signed]);
35
+ const list = useRead(signed ? () => tools(target) : null, [], [target, signed]);
29
36
  const [busy, setBusy] = useState('');
30
37
  const [note, setNote] = useState('');
31
38
  if (!signed) {
32
- return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title: "Capabilities" }), _jsx(Soft, { children: "Sign in to see what the agent may use." })] }));
39
+ return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title: t('Capabilities') }), _jsx(Soft, { children: t('Sign in to see what the agent may use.') })] }));
33
40
  }
34
41
  const kinds = SOURCES.map((s) => ({ source: s, all: list.value.filter((x) => x.source === s) })).filter((k) => k.all.length);
35
42
  const flip = async (source, all, on) => {
36
43
  setBusy(source);
37
44
  setNote('');
38
45
  try {
39
- await activate(t, all.map((x) => x.name), on);
46
+ await activate(target, all.map((x) => x.name), on);
40
47
  list.reload();
41
48
  }
42
49
  catch (e) {
43
- setNote(e.message);
50
+ setNote(told(e, t));
44
51
  }
45
52
  finally {
46
53
  setBusy('');
47
54
  }
48
55
  };
49
- return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title: "Capabilities", detail: `What the agent may use in ${host.org ?? 'this organization'}. A tool that is off is refused when the agent calls it.` }), _jsx(Group, { title: "Tools", detail: host.admin
50
- ? 'Switching a kind on or off applies to every tool of that kind, for everyone in the organization.'
51
- : 'An org admin switches these, for everyone in the organization.', children: list.error ? (_jsx(Soft, { children: list.error.message })) : list.loading && !list.value.length ? (_jsx(Soft, { children: "Reading tools\u2026" })) : !kinds.length ? (_jsx(Soft, { children: "No tools are available to this organization yet." })) : (_jsx(Card, { children: kinds.map(({ source, all }, i) => {
56
+ return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title: t('Capabilities'), detail: host.org
57
+ ? t('What the agent may use in {org}. A tool that is off is refused when the agent calls it.', { org: host.org })
58
+ : t('What the agent may use in this organization. A tool that is off is refused when the agent calls it.') }), _jsx(Group, { title: t('Tools'), detail: host.admin
59
+ ? t('Switching a kind on or off applies to every tool of that kind, for everyone in the organization.')
60
+ : t('An org admin switches these, for everyone in the organization.'), children: list.error ? (_jsx(Soft, { children: told(list.error, t) })) : list.loading && !list.value.length ? (_jsx(Soft, { children: t('Reading tools…') })) : !kinds.length ? (_jsx(Soft, { children: t('No tools are available to this organization yet.') })) : (_jsx(Card, { children: kinds.map(({ source, all }, i) => {
52
61
  const on = all.filter((x) => x.activated).length;
53
- return (_jsx(Row, { first: i === 0, title: KINDS[source].label, detail: `${KINDS[source].about}. ${on === all.length ? `All ${all.length} on` : on ? `${on} of ${all.length} on` : `${all.length} available, all off`}.`, trailing: host.admin ? (_jsx(Switch, { "aria-label": KINDS[source].label, checked: on === all.length, disabled: busy !== '', onCheckedChange: (v) => void flip(source, all, v) })) : (_jsx(SizableText, { size: "$1", color: "$soft", children: on === all.length ? 'On' : on ? 'Partly on' : 'Off' })) }, source));
62
+ // What the kind is, then how much of it is on: two sentences.
63
+ const count = on === all.length ? t('All {n} on.', { n: all.length }) : on ? t('{on} of {n} on.', { on, n: all.length }) : t('{n} available, all off.', { n: all.length });
64
+ return (_jsx(Row, { first: i === 0, title: t(KINDS[source].label), detail: `${t(KINDS[source].about)} ${count}`, trailing: host.admin ? (_jsx(Switch, { "aria-label": t(KINDS[source].label), checked: on === all.length, disabled: busy !== '', onCheckedChange: (v) => void flip(source, all, v) })) : (_jsx(SizableText, { size: "$1", color: "$soft", children: on === all.length ? t('On') : on ? t('Partly on') : t('Off') })) }, source));
54
65
  }) })) }), _jsx(Note, { children: note })] }));
55
66
  }
@@ -6,12 +6,18 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
6
6
  * the card from that. Which processor, and its sandbox or live account, is the
7
7
  * platform's answer at /v1/billing/settings, so the card is saved to the account
8
8
  * it will be charged on.
9
+ *
10
+ * Its words are the reader's language (@hanzo/i18n `t`); what the processor's
11
+ * field says of a card is the processor's, drawn as given.
9
12
  */
10
13
  import { SizableText, XStack, YStack } from '@hanzo/gui';
14
+ import { phrase } from '@hanzo/i18n';
15
+ import { useI18n } from '@hanzo/i18n/react';
11
16
  import { ChevronDown, ChevronUp, X } from '@hanzogui/lucide-icons-2';
12
17
  import { Button, Dialog, DialogContent, DialogTitle, Input } from '@hanzo/ui';
13
18
  import { useEffect, useRef, useState } from 'react';
14
19
  import { processor, save } from '../api/billing.js';
20
+ import { told } from '../api/call.js';
15
21
  import { useRead } from '../data.js';
16
22
  import { useTarget } from '../host.js';
17
23
  import { Field as FormField } from './ui.js';
@@ -29,10 +35,10 @@ function load(environment) {
29
35
  const s = document.createElement('script');
30
36
  s.src = src;
31
37
  s.async = true;
32
- s.onload = () => (window.Square ? resolve(window.Square) : reject(new Error('The card form did not load')));
38
+ s.onload = () => (window.Square ? resolve(window.Square) : reject(new Error(phrase('The card form did not load'))));
33
39
  s.onerror = () => {
34
40
  loads.delete(src);
35
- reject(new Error('The card form did not load'));
41
+ reject(new Error(phrase('The card form did not load')));
36
42
  };
37
43
  document.head.appendChild(s);
38
44
  });
@@ -41,8 +47,9 @@ function load(environment) {
41
47
  }
42
48
  const ready = (p) => Boolean(p && p.provider === 'square' && p.application && p.location);
43
49
  export function AddCard({ open, onOpenChange, onSaved }) {
44
- const t = useTarget();
45
- const cfg = useRead(open ? () => processor(t) : null, null, [t, open]);
50
+ const { t } = useI18n();
51
+ const target = useTarget();
52
+ const cfg = useRead(open ? () => processor(target) : null, null, [target, open]);
46
53
  const [box, setBox] = useState(null);
47
54
  const field = useRef(null);
48
55
  const [mounted, setMounted] = useState(false);
@@ -75,7 +82,7 @@ export function AddCard({ open, onOpenChange, onSaved }) {
75
82
  })
76
83
  .catch((e) => {
77
84
  if (!gone)
78
- setNote(e instanceof Error ? e.message : 'The card form did not load');
85
+ setNote(e instanceof Error ? told(e, t) : t('The card form did not load'));
79
86
  });
80
87
  return () => {
81
88
  gone = true;
@@ -94,7 +101,7 @@ export function AddCard({ open, onOpenChange, onSaved }) {
94
101
  try {
95
102
  const r = await f.tokenize();
96
103
  if (r.status !== 'OK' || !r.token)
97
- throw new Error(r.errors?.map((e) => e.message).filter(Boolean).join('; ') || 'The card details are incomplete');
104
+ throw new Error(r.errors?.map((e) => e.message).filter(Boolean).join('; ') || phrase('The card details are incomplete'));
98
105
  const billingAddress = name.trim() || postalCode.trim() || line1.trim() || city.trim() || stateVal.trim()
99
106
  ? {
100
107
  name: name.trim() || undefined,
@@ -105,19 +112,19 @@ export function AddCard({ open, onOpenChange, onSaved }) {
105
112
  country: country.trim() || 'US',
106
113
  }
107
114
  : undefined;
108
- const m = await save(t, r.token, billingAddress);
115
+ const m = await save(target, r.token, billingAddress);
109
116
  onSaved(m);
110
117
  onOpenChange(false);
111
118
  }
112
119
  catch (e) {
113
- setNote(e instanceof Error ? e.message : 'The card was not saved');
120
+ setNote(e instanceof Error ? told(e, t) : t('The card was not saved'));
114
121
  }
115
122
  finally {
116
123
  setWorking(false);
117
124
  }
118
125
  };
119
- const why = cfg.error ? cfg.error.message : !cfg.loading && !ready(p) ? 'This deployment takes no cards yet.' : '';
120
- return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: 460, showCloseButton: false, children: [_jsxs(XStack, { items: "center", justify: "space-between", gap: "$2", children: [_jsx(DialogTitle, { children: "Add a card" }), _jsx(XStack, { render: "button", "aria-label": "Close", p: "$1", onPress: () => onOpenChange(false), children: _jsx(X, { size: 16 }) })] }), _jsxs(YStack, { gap: "$3", children: [why ? (_jsx(SizableText, { size: "$2", color: "$soft", children: why })) : (_jsxs(_Fragment, { children: [_jsxs(YStack, { minH: 96, "aria-label": "Card details", children: [_jsx("div", { ref: setBox, style: { minHeight: 96 } }), !mounted && !note ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "Loading the card form\u2026" })) : null] }), _jsxs(XStack, { gap: "$2", children: [_jsx(YStack, { flex: 1, children: _jsx(FormField, { label: "Name on card", hint: "Optional", children: _jsx(Input, { value: name, onChangeText: setName, placeholder: "Name or business" }) }) }), _jsx(YStack, { width: 120, children: _jsx(FormField, { label: "Postal code", hint: "Optional", children: _jsx(Input, { value: postalCode, onChangeText: setPostalCode, placeholder: "ZIP / Postal" }) }) })] }), _jsx(XStack, { children: _jsxs(Button, { size: "sm", variant: "ghost", onPress: () => setMoreAddress(!moreAddress), children: [moreAddress ? _jsx(ChevronUp, { size: 14 }) : _jsx(ChevronDown, { size: 14 }), moreAddress ? 'Hide address' : 'Add billing address'] }) }), moreAddress ? (_jsxs(YStack, { gap: "$2", pt: "$1", children: [_jsx(FormField, { label: "Street address", children: _jsx(Input, { value: line1, onChangeText: setLine1, placeholder: "Address line 1" }) }), _jsxs(XStack, { gap: "$2", children: [_jsx(YStack, { flex: 1, children: _jsx(FormField, { label: "City", children: _jsx(Input, { value: city, onChangeText: setCity, placeholder: "City" }) }) }), _jsx(YStack, { width: 90, children: _jsx(FormField, { label: "State", children: _jsx(Input, { value: stateVal, onChangeText: setStateVal, placeholder: "State" }) }) }), _jsx(YStack, { width: 90, children: _jsx(FormField, { label: "Country", children: _jsx(Input, { value: country, onChangeText: setCountry, placeholder: "Country (US)" }) }) })] })] })) : null, _jsx(SizableText, { size: "$1", color: "$soft", children: p && p.environment !== 'production'
121
- ? 'Sandbox: use a test card. Nothing is charged.'
122
- : 'The card number goes to the payment processor, never to Hanzo. Saving it charges nothing.' })] })), note ? (_jsx(SizableText, { size: "$2", color: "$red10", role: "alert", children: note })) : null, _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => onOpenChange(false), children: "Cancel" }), _jsx(Button, { size: "sm", disabled: !mounted || working, onPress: () => void add(), children: "Save card" })] })] })] }) }));
126
+ const why = cfg.error ? told(cfg.error, t) : !cfg.loading && !ready(p) ? t('This deployment takes no cards yet.') : '';
127
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: 460, showCloseButton: false, children: [_jsxs(XStack, { items: "center", justify: "space-between", gap: "$2", children: [_jsx(DialogTitle, { children: t('Add a card') }), _jsx(XStack, { render: "button", "aria-label": t('Close'), p: "$1", onPress: () => onOpenChange(false), children: _jsx(X, { size: 16 }) })] }), _jsxs(YStack, { gap: "$3", children: [why ? (_jsx(SizableText, { size: "$2", color: "$soft", children: why })) : (_jsxs(_Fragment, { children: [_jsxs(YStack, { minH: 96, "aria-label": t('Card details'), children: [_jsx("div", { ref: setBox, style: { minHeight: 96 } }), !mounted && !note ? (_jsx(SizableText, { size: "$1", color: "$soft", children: t('Loading the card form…') })) : null] }), _jsxs(XStack, { gap: "$2", children: [_jsx(YStack, { flex: 1, children: _jsx(FormField, { label: t('Name on card'), hint: t('Optional'), children: _jsx(Input, { value: name, onChangeText: setName, placeholder: t('Name or business') }) }) }), _jsx(YStack, { width: 120, children: _jsx(FormField, { label: t('Postal code'), hint: t('Optional'), children: _jsx(Input, { value: postalCode, onChangeText: setPostalCode, placeholder: t('ZIP / Postal') }) }) })] }), _jsx(XStack, { children: _jsxs(Button, { size: "sm", variant: "ghost", onPress: () => setMoreAddress(!moreAddress), children: [moreAddress ? _jsx(ChevronUp, { size: 14 }) : _jsx(ChevronDown, { size: 14 }), moreAddress ? t('Hide address') : t('Add billing address')] }) }), moreAddress ? (_jsxs(YStack, { gap: "$2", pt: "$1", children: [_jsx(FormField, { label: t('Street address'), children: _jsx(Input, { value: line1, onChangeText: setLine1, placeholder: t('Address line 1') }) }), _jsxs(XStack, { gap: "$2", children: [_jsx(YStack, { flex: 1, children: _jsx(FormField, { label: t('City'), children: _jsx(Input, { value: city, onChangeText: setCity, placeholder: t('City') }) }) }), _jsx(YStack, { width: 90, children: _jsx(FormField, { label: t('State'), children: _jsx(Input, { value: stateVal, onChangeText: setStateVal, placeholder: t('State') }) }) }), _jsx(YStack, { width: 90, children: _jsx(FormField, { label: t('Country'), children: _jsx(Input, { value: country, onChangeText: setCountry, placeholder: t('Country (US)') }) }) })] })] })) : null, _jsx(SizableText, { size: "$1", color: "$soft", children: p && p.environment !== 'production'
128
+ ? t('Sandbox: use a test card. Nothing is charged.')
129
+ : t('The card number goes to the payment processor, never to Hanzo. Saving it charges nothing.') })] })), note ? (_jsx(SizableText, { size: "$2", color: "$red10", role: "alert", children: note })) : null, _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => onOpenChange(false), children: t('Cancel') }), _jsx(Button, { size: "sm", disabled: !mounted || working, onPress: () => void add(), children: t('Save card') })] })] })] }) }));
123
130
  }