@agent-native/core 0.76.11 → 0.76.13

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 (58) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +19 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/production-agent.ts +7 -4
  5. package/corpus/core/src/client/FeedbackButton.tsx +162 -18
  6. package/corpus/core/src/client/blocks/library/AnnotatedCodeBlock.tsx +11 -2
  7. package/corpus/core/src/client/blocks/library/ApiEndpointBlock.tsx +46 -19
  8. package/corpus/core/src/client/blocks/library/MermaidBlock.tsx +7 -5
  9. package/corpus/core/src/client/blocks/library/block-copy.ts +367 -0
  10. package/corpus/core/src/client/blocks/library/diagram.tsx +8 -6
  11. package/corpus/core/src/client/blocks/library/wireframe.tsx +7 -2
  12. package/corpus/core/src/client/i18n.tsx +4 -0
  13. package/corpus/templates/clips/app/components/recorder/camera-visualizer.tsx +88 -10
  14. package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +64 -0
  15. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +94 -9
  16. package/corpus/templates/clips/app/lib/camera-blur.ts +341 -0
  17. package/corpus/templates/clips/app/lib/camera-composite.ts +6 -1
  18. package/corpus/templates/clips/app/lib/recorder-preferences.ts +15 -0
  19. package/corpus/templates/clips/app/routes/record.tsx +13 -0
  20. package/corpus/templates/clips/desktop/src/app.tsx +101 -69
  21. package/corpus/templates/clips/desktop/src/lib/voice-dictation.ts +109 -11
  22. package/corpus/templates/clips/desktop/src/styles.css +19 -4
  23. package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +1 -1
  24. package/corpus/templates/clips/desktop/src-tauri/src/native_speech.rs +143 -86
  25. package/corpus/templates/clips/package.json +1 -0
  26. package/corpus/templates/clips/vite.config.ts +38 -1
  27. package/corpus/templates/mail/app/root.tsx +90 -6
  28. package/dist/agent/production-agent.d.ts +7 -4
  29. package/dist/agent/production-agent.d.ts.map +1 -1
  30. package/dist/agent/production-agent.js +7 -4
  31. package/dist/agent/production-agent.js.map +1 -1
  32. package/dist/client/FeedbackButton.d.ts.map +1 -1
  33. package/dist/client/FeedbackButton.js +142 -14
  34. package/dist/client/FeedbackButton.js.map +1 -1
  35. package/dist/client/blocks/library/AnnotatedCodeBlock.d.ts.map +1 -1
  36. package/dist/client/blocks/library/AnnotatedCodeBlock.js +6 -2
  37. package/dist/client/blocks/library/AnnotatedCodeBlock.js.map +1 -1
  38. package/dist/client/blocks/library/ApiEndpointBlock.d.ts.map +1 -1
  39. package/dist/client/blocks/library/ApiEndpointBlock.js +14 -6
  40. package/dist/client/blocks/library/ApiEndpointBlock.js.map +1 -1
  41. package/dist/client/blocks/library/MermaidBlock.d.ts.map +1 -1
  42. package/dist/client/blocks/library/MermaidBlock.js +6 -4
  43. package/dist/client/blocks/library/MermaidBlock.js.map +1 -1
  44. package/dist/client/blocks/library/block-copy.d.ts +33 -0
  45. package/dist/client/blocks/library/block-copy.d.ts.map +1 -0
  46. package/dist/client/blocks/library/block-copy.js +329 -0
  47. package/dist/client/blocks/library/block-copy.js.map +1 -0
  48. package/dist/client/blocks/library/diagram.d.ts.map +1 -1
  49. package/dist/client/blocks/library/diagram.js +7 -5
  50. package/dist/client/blocks/library/diagram.js.map +1 -1
  51. package/dist/client/blocks/library/wireframe.d.ts.map +1 -1
  52. package/dist/client/blocks/library/wireframe.js +4 -2
  53. package/dist/client/blocks/library/wireframe.js.map +1 -1
  54. package/dist/client/i18n.d.ts +1 -0
  55. package/dist/client/i18n.d.ts.map +1 -1
  56. package/dist/client/i18n.js +3 -0
  57. package/dist/client/i18n.js.map +1 -1
  58. package/package.json +1 -1
package/corpus/README.md CHANGED
@@ -27,5 +27,5 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
27
27
 
28
28
  ## Generated Counts
29
29
 
30
- - core files: 1928
31
- - template files: 4250
30
+ - core files: 1929
31
+ - template files: 4251
@@ -1,5 +1,24 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.76.13
4
+
5
+ ### Patch Changes
6
+
7
+ - d12772c: Durable background agent-chat: raise the foreground circuit-breaker's claim grace
8
+ from 8s to 15s (`BACKGROUND_CLAIM_GRACE_MS`). Heavy apps (observed on analytics
9
+ in prod) take longer than 8s to cold-start the background function and reach
10
+ `claimBackgroundRun`, so the foreground recovered inline every time — adding ~8s
11
+ latency per turn and never using the 15-minute background budget. 15s lets the
12
+ slow-but-alive workers win the claim while staying well within the foreground's
13
+ ~40s soft-timeout; a genuinely dead worker still falls back to inline.
14
+
15
+ ## 0.76.12
16
+
17
+ ### Patch Changes
18
+
19
+ - 93292e4: Improve localization coverage by localizing framework error screens and docs UI surfaces, and add a baseline-aware i18n guard that fails on new raw visible UI strings.
20
+ - 93292e4: Localize shared feedback and docs block chrome while tightening the i18n raw-literal guard.
21
+
3
22
  ## 0.76.11
4
23
 
5
24
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.76.11",
3
+ "version": "0.76.13",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22"
@@ -153,11 +153,14 @@ export { PROVIDER_TO_ENV };
153
153
  /**
154
154
  * Grace window + poll interval for the foreground circuit-breaker that confirms
155
155
  * a background worker actually CLAIMED a 202-dispatched run before recovering
156
- * inline. The grace is long enough for a cold-start worker to win the claim
157
- * (~1-2s typical) and short enough to recover quickly within the foreground's
158
- * ~40s soft-timeout.
156
+ * inline. The grace must cover the worker's cold-start + per-request init before
157
+ * it reaches `claimBackgroundRun`: light apps win the claim in ~1-2s, but heavy
158
+ * apps (e.g. analytics) were observed in prod taking >8s, so an 8s grace made
159
+ * their worker lose the race every time and always fall back to inline (adding
160
+ * ~8s latency with no background budget). 15s covers the slow apps while staying
161
+ * well within the foreground's ~40s soft-timeout.
159
162
  */
160
- export const BACKGROUND_CLAIM_GRACE_MS = 8_000;
163
+ export const BACKGROUND_CLAIM_GRACE_MS = 15_000;
161
164
  export const BACKGROUND_CLAIM_POLL_MS = 400;
162
165
 
163
166
  export type BackgroundDispatchOutcome =
@@ -13,6 +13,8 @@ import { IconMessage2, IconCheck } from "@tabler/icons-react";
13
13
  import { cn } from "./utils.js";
14
14
  import { useSession } from "./use-session.js";
15
15
  import { getFeedbackClientContext } from "./feedback-context.js";
16
+ import { useLocale } from "./i18n.js";
17
+ import { DEFAULT_LOCALE, type LocaleCode } from "../localization/shared.js";
16
18
 
17
19
  const DEFAULT_FEEDBACK_URL =
18
20
  "https://forms.agent-native.com/f/agent-native-feedback/_16ewV";
@@ -27,10 +29,142 @@ interface FormSchema {
27
29
  fieldId: string;
28
30
  }
29
31
 
30
- const DEFAULT_PLACEHOLDER =
31
- "What's working, what's broken, or what would you change?";
32
- const DEFAULT_SUBMIT_TEXT = "Send feedback";
33
- const DEFAULT_SUCCESS_MESSAGE = "Thanks for the feedback!";
32
+ const FEEDBACK_COPY: Record<
33
+ LocaleCode,
34
+ {
35
+ label: string;
36
+ placeholder: string;
37
+ submit: string;
38
+ submitting: string;
39
+ success: string;
40
+ loadError: string;
41
+ invalidUrl: string;
42
+ emptyError: string;
43
+ sendError: string;
44
+ keyboardHint: string;
45
+ }
46
+ > = {
47
+ "en-US": {
48
+ label: "Feedback",
49
+ placeholder: "What's working, what's broken, or what would you change?",
50
+ submit: "Send feedback",
51
+ submitting: "Sending...",
52
+ success: "Thanks for the feedback!",
53
+ loadError: "Couldn't load feedback form",
54
+ invalidUrl: "Invalid feedback URL",
55
+ emptyError: "Please write something first",
56
+ sendError: "Couldn't send feedback",
57
+ keyboardHint: "{{shortcut}}+Enter to send",
58
+ },
59
+ "zh-CN": {
60
+ label: "反馈",
61
+ placeholder: "哪些地方好用、哪里坏了,或你想改什么?",
62
+ submit: "发送反馈",
63
+ submitting: "正在发送...",
64
+ success: "感谢你的反馈!",
65
+ loadError: "无法加载反馈表单",
66
+ invalidUrl: "反馈 URL 无效",
67
+ emptyError: "请先写点内容",
68
+ sendError: "无法发送反馈",
69
+ keyboardHint: "{{shortcut}}+Enter 发送",
70
+ },
71
+ "es-ES": {
72
+ label: "Comentarios",
73
+ placeholder: "¿Qué funciona, qué falla o qué cambiarías?",
74
+ submit: "Enviar comentarios",
75
+ submitting: "Enviando...",
76
+ success: "Gracias por tus comentarios.",
77
+ loadError: "No se pudo cargar el formulario",
78
+ invalidUrl: "URL de comentarios no válida",
79
+ emptyError: "Escribe algo primero",
80
+ sendError: "No se pudieron enviar los comentarios",
81
+ keyboardHint: "{{shortcut}}+Enter para enviar",
82
+ },
83
+ "fr-FR": {
84
+ label: "Retour",
85
+ placeholder: "Qu'est-ce qui marche, casse ou devrait changer ?",
86
+ submit: "Envoyer",
87
+ submitting: "Envoi...",
88
+ success: "Merci pour votre retour.",
89
+ loadError: "Impossible de charger le formulaire",
90
+ invalidUrl: "URL de retour invalide",
91
+ emptyError: "Écrivez quelque chose d'abord",
92
+ sendError: "Impossible d'envoyer le retour",
93
+ keyboardHint: "{{shortcut}}+Entrée pour envoyer",
94
+ },
95
+ "de-DE": {
96
+ label: "Feedback",
97
+ placeholder: "Was funktioniert, was ist kaputt, was würdest du ändern?",
98
+ submit: "Feedback senden",
99
+ submitting: "Wird gesendet...",
100
+ success: "Danke für dein Feedback!",
101
+ loadError: "Feedback-Formular konnte nicht geladen werden",
102
+ invalidUrl: "Ungültige Feedback-URL",
103
+ emptyError: "Bitte zuerst etwas schreiben",
104
+ sendError: "Feedback konnte nicht gesendet werden",
105
+ keyboardHint: "{{shortcut}}+Enter zum Senden",
106
+ },
107
+ "ja-JP": {
108
+ label: "フィードバック",
109
+ placeholder: "良い点、問題点、変更したい点を教えてください。",
110
+ submit: "送信",
111
+ submitting: "送信中...",
112
+ success: "フィードバックありがとうございます。",
113
+ loadError: "フォームを読み込めませんでした",
114
+ invalidUrl: "フィードバック URL が無効です",
115
+ emptyError: "先に内容を入力してください",
116
+ sendError: "送信できませんでした",
117
+ keyboardHint: "{{shortcut}}+Enter で送信",
118
+ },
119
+ "ko-KR": {
120
+ label: "피드백",
121
+ placeholder: "잘 되는 점, 깨진 점, 바꾸고 싶은 점을 알려주세요.",
122
+ submit: "피드백 보내기",
123
+ submitting: "보내는 중...",
124
+ success: "피드백 감사합니다.",
125
+ loadError: "피드백 양식을 불러올 수 없습니다",
126
+ invalidUrl: "피드백 URL이 올바르지 않습니다",
127
+ emptyError: "먼저 내용을 입력해 주세요",
128
+ sendError: "피드백을 보낼 수 없습니다",
129
+ keyboardHint: "{{shortcut}}+Enter로 보내기",
130
+ },
131
+ "pt-BR": {
132
+ label: "Feedback",
133
+ placeholder: "O que funciona, quebrou ou você mudaria?",
134
+ submit: "Enviar feedback",
135
+ submitting: "Enviando...",
136
+ success: "Obrigado pelo feedback!",
137
+ loadError: "Não foi possível carregar o formulário",
138
+ invalidUrl: "URL de feedback inválida",
139
+ emptyError: "Escreva algo primeiro",
140
+ sendError: "Não foi possível enviar o feedback",
141
+ keyboardHint: "{{shortcut}}+Enter para enviar",
142
+ },
143
+ "hi-IN": {
144
+ label: "फ़ीडबैक",
145
+ placeholder: "क्या काम कर रहा है, क्या टूटा है, या आप क्या बदलना चाहेंगे?",
146
+ submit: "फ़ीडबैक भेजें",
147
+ submitting: "भेजा जा रहा है...",
148
+ success: "फ़ीडबैक के लिए धन्यवाद!",
149
+ loadError: "फ़ीडबैक फ़ॉर्म लोड नहीं हुआ",
150
+ invalidUrl: "फ़ीडबैक URL अमान्य है",
151
+ emptyError: "पहले कुछ लिखें",
152
+ sendError: "फ़ीडबैक भेजा नहीं जा सका",
153
+ keyboardHint: "भेजने के लिए {{shortcut}}+Enter",
154
+ },
155
+ "ar-SA": {
156
+ label: "ملاحظات",
157
+ placeholder: "ما الذي يعمل، وما المعطل، وما الذي تريد تغييره؟",
158
+ submit: "إرسال الملاحظات",
159
+ submitting: "جار الإرسال...",
160
+ success: "شكرا لملاحظاتك!",
161
+ loadError: "تعذر تحميل نموذج الملاحظات",
162
+ invalidUrl: "رابط الملاحظات غير صالح",
163
+ emptyError: "اكتب شيئا أولا",
164
+ sendError: "تعذر إرسال الملاحظات",
165
+ keyboardHint: "{{shortcut}}+Enter للإرسال",
166
+ },
167
+ };
34
168
 
35
169
  function parseTarget(url: string): ParsedTarget | null {
36
170
  try {
@@ -114,7 +248,7 @@ const honeypotStyle: CSSProperties = {
114
248
 
115
249
  export function FeedbackButton({
116
250
  variant = "sidebar",
117
- label = "Feedback",
251
+ label,
118
252
  url = DEFAULT_FEEDBACK_URL,
119
253
  className,
120
254
  side,
@@ -128,6 +262,9 @@ export function FeedbackButton({
128
262
  }: FeedbackButtonProps) {
129
263
  const target = parseTarget(url);
130
264
  const { session } = useSession();
265
+ const { locale } = useLocale();
266
+ const copy = FEEDBACK_COPY[locale] ?? FEEDBACK_COPY[DEFAULT_LOCALE];
267
+ const resolvedLabel = label ?? copy.label;
131
268
 
132
269
  const [uncontrolledOpen, setUncontrolledOpen] = useState(false);
133
270
  const open = controlledOpen ?? uncontrolledOpen;
@@ -163,10 +300,10 @@ export function FeedbackButton({
163
300
  .then((s) => setSchema(s))
164
301
  .catch((err) => {
165
302
  console.error("[FeedbackButton] schema load failed", err);
166
- setError("Couldn't load feedback form");
303
+ setError(copy.loadError);
167
304
  });
168
305
  } else {
169
- setError("Invalid feedback URL");
306
+ setError(copy.invalidUrl);
170
307
  }
171
308
  const t = setTimeout(() => textareaRef.current?.focus(), 30);
172
309
  return () => {
@@ -184,7 +321,7 @@ export function FeedbackButton({
184
321
  if (!target || submitting) return;
185
322
  const trimmed = value.trim();
186
323
  if (!trimmed) {
187
- setError("Please write something first");
324
+ setError(copy.emptyError);
188
325
  return;
189
326
  }
190
327
  setSubmitting(true);
@@ -223,7 +360,7 @@ export function FeedbackButton({
223
360
  closeTimerRef.current = setTimeout(() => setOpen(false), 1400);
224
361
  } catch (err) {
225
362
  setSubmitting(false);
226
- setError(err instanceof Error ? err.message : "Couldn't send feedback");
363
+ setError(err instanceof Error ? err.message : copy.sendError);
227
364
  }
228
365
  },
229
366
  [
@@ -236,6 +373,8 @@ export function FeedbackButton({
236
373
  chatSessionId,
237
374
  chatStorageKey,
238
375
  setOpen,
376
+ copy.emptyError,
377
+ copy.sendError,
239
378
  ],
240
379
  );
241
380
 
@@ -254,7 +393,7 @@ export function FeedbackButton({
254
393
  <PopoverPrimitive.Trigger asChild>
255
394
  <button
256
395
  type="button"
257
- aria-label={label}
396
+ aria-label={resolvedLabel}
258
397
  className={cn(
259
398
  "flex h-6 w-6 items-center justify-center rounded text-muted-foreground hover:text-foreground hover:bg-accent/50",
260
399
  className,
@@ -269,7 +408,7 @@ export function FeedbackButton({
269
408
  sideOffset={6}
270
409
  className="z-[300] overflow-hidden rounded-md border border-border bg-popover px-2 py-1 text-[11px] text-foreground shadow-md animate-in fade-in-0 zoom-in-95"
271
410
  >
272
- {label}
411
+ {resolvedLabel}
273
412
  </TooltipPrimitive.Content>
274
413
  </TooltipPrimitive.Portal>
275
414
  </TooltipPrimitive.Root>
@@ -280,14 +419,14 @@ export function FeedbackButton({
280
419
  <PopoverPrimitive.Trigger asChild>
281
420
  <button
282
421
  type="button"
283
- aria-label={label}
422
+ aria-label={resolvedLabel}
284
423
  className={cn(
285
424
  "flex h-8 items-center gap-2 rounded-md border border-border bg-transparent px-3 text-sm text-muted-foreground transition hover:border-foreground/40 hover:text-foreground",
286
425
  className,
287
426
  )}
288
427
  >
289
428
  <IconMessage2 size={14} stroke={1.5} />
290
- <span>{label}</span>
429
+ <span>{resolvedLabel}</span>
291
430
  </button>
292
431
  </PopoverPrimitive.Trigger>
293
432
  );
@@ -302,14 +441,14 @@ export function FeedbackButton({
302
441
  )}
303
442
  >
304
443
  <IconMessage2 className="h-4 w-4" />
305
- <span>{label}</span>
444
+ <span>{resolvedLabel}</span>
306
445
  </button>
307
446
  </PopoverPrimitive.Trigger>
308
447
  );
309
448
  }
310
449
 
311
450
  const resolvedSide = side ?? (variant === "sidebar" ? "top" : "bottom");
312
- const resolvedPlaceholder = placeholder ?? DEFAULT_PLACEHOLDER;
451
+ const resolvedPlaceholder = placeholder ?? copy.placeholder;
313
452
 
314
453
  return (
315
454
  <PopoverPrimitive.Root open={open} onOpenChange={setOpen}>
@@ -329,7 +468,7 @@ export function FeedbackButton({
329
468
  <IconCheck size={20} stroke={2.5} />
330
469
  </div>
331
470
  <div className="text-sm font-medium text-foreground">
332
- {DEFAULT_SUCCESS_MESSAGE}
471
+ {copy.success}
333
472
  </div>
334
473
  </div>
335
474
  ) : (
@@ -363,14 +502,19 @@ export function FeedbackButton({
363
502
  )}
364
503
  >
365
504
  {error ??
366
- `${/Mac|iPhone|iPad/.test(navigator.userAgent) ? "⌘" : "Ctrl"}+Enter to send`}
505
+ copy.keyboardHint.replace(
506
+ "{{shortcut}}",
507
+ /Mac|iPhone|iPad/.test(navigator.userAgent)
508
+ ? "⌘"
509
+ : "Ctrl",
510
+ )}
367
511
  </div>
368
512
  <button
369
513
  type="submit"
370
514
  disabled={submitting || !value.trim()}
371
515
  className="inline-flex h-8 items-center justify-center rounded-md bg-primary px-3 text-xs font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
372
516
  >
373
- {submitting ? "Sending…" : DEFAULT_SUBMIT_TEXT}
517
+ {submitting ? copy.submitting : copy.submit}
374
518
  </button>
375
519
  </div>
376
520
  </form>
@@ -27,6 +27,7 @@ import {
27
27
  } from "./annotation-rail.js";
28
28
  import { CodeFilenameLabel } from "./code-filename-label.js";
29
29
  import { DevInput, DevLabel, DevTextarea } from "./dev-doc-ui.js";
30
+ import { useBlockCopy } from "./block-copy.js";
30
31
 
31
32
  /**
32
33
  * "Explain this code" walkthrough block: a standard syntax-highlighted code
@@ -158,6 +159,7 @@ function AnnotatedCodeRead({
158
159
  summary,
159
160
  ctx,
160
161
  }: BlockReadProps<AnnotatedCodeData>) {
162
+ const copy = useBlockCopy();
161
163
  // On-hover popover (anchored to the right of the code) replaces the old
162
164
  // persistent rail: nothing is visible when idle. `codeRef` measures the code
163
165
  // block's right edge; `hover` carries the active index + captured geometry.
@@ -312,7 +314,11 @@ function AnnotatedCodeRead({
312
314
  tabIndex={isAnnotated ? 0 : undefined}
313
315
  role={isAnnotated ? "button" : undefined}
314
316
  aria-expanded={isAnnotated ? isActive : undefined}
315
- aria-label={isAnnotated ? `Line ${lineNo} annotation` : undefined}
317
+ aria-label={
318
+ isAnnotated
319
+ ? copy.lineAnnotation.replace("{{line}}", String(lineNo))
320
+ : undefined
321
+ }
316
322
  className={cn(
317
323
  "relative flex w-full",
318
324
  isAnnotated && "cursor-pointer",
@@ -494,7 +500,10 @@ function AnnotatedCodeRead({
494
500
  ···
495
501
  </span>
496
502
  <span className="flex-1 text-[11px] text-plan-muted/70">
497
- {hiddenCount} lines — click to expand
503
+ {copy.hiddenLinesExpand.replace(
504
+ "{{count}}",
505
+ String(hiddenCount),
506
+ )}
498
507
  </span>
499
508
  </button>
500
509
  );
@@ -22,6 +22,7 @@ import {
22
22
  API_ENDPOINT_METHODS,
23
23
  API_PARAM_LOCATIONS,
24
24
  } from "./api-endpoint.config.js";
25
+ import { useBlockCopy } from "./block-copy.js";
25
26
  import { JsonExplorerSurface } from "./JsonExplorerBlock.js";
26
27
  import {
27
28
  DevBadge,
@@ -127,31 +128,33 @@ const CHANGE_INK: Record<ApiEndpointChange, string> = {
127
128
  */
128
129
  function ChangeChip({
129
130
  change,
131
+ label = CHANGE_LABEL[change],
130
132
  variant = "glyph",
131
133
  className,
132
134
  }: {
133
135
  change: ApiEndpointChange;
136
+ label?: string;
134
137
  variant?: "glyph" | "label";
135
138
  className?: string;
136
139
  }) {
137
140
  if (variant === "label") {
138
141
  return (
139
142
  <span
140
- title={CHANGE_LABEL[change]}
143
+ title={label}
141
144
  className={cn(
142
145
  "shrink-0 rounded px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wide",
143
146
  CHANGE_BADGE[change],
144
147
  className,
145
148
  )}
146
149
  >
147
- {CHANGE_LABEL[change]}
150
+ {label}
148
151
  </span>
149
152
  );
150
153
  }
151
154
  return (
152
155
  <span
153
- title={CHANGE_LABEL[change]}
154
- aria-label={CHANGE_LABEL[change]}
156
+ title={label}
157
+ aria-label={label}
155
158
  className={cn(
156
159
  "flex size-4 shrink-0 items-center justify-center rounded text-[10px] font-bold leading-none",
157
160
  CHANGE_BADGE[change],
@@ -402,6 +405,13 @@ export function ApiEndpointRead({
402
405
  ctx,
403
406
  }: BlockReadProps<ApiEndpointData>) {
404
407
  const [open, setOpen] = useState(false);
408
+ const copy = useBlockCopy();
409
+ const changeLabel: Record<ApiEndpointChange, string> = {
410
+ added: copy.added,
411
+ modified: copy.modified,
412
+ removed: copy.removed,
413
+ renamed: copy.renamed,
414
+ };
405
415
 
406
416
  const params = data.params ?? [];
407
417
  const responses = data.responses ?? [];
@@ -467,10 +477,16 @@ export function ApiEndpointRead({
467
477
  >
468
478
  {data.path}
469
479
  </span>
470
- {data.change && <ChangeChip change={data.change} variant="label" />}
480
+ {data.change && (
481
+ <ChangeChip
482
+ change={data.change}
483
+ label={changeLabel[data.change]}
484
+ variant="label"
485
+ />
486
+ )}
471
487
  {data.deprecated && (
472
488
  <DevBadge className="shrink-0 border-amber-500/40 text-amber-600 dark:text-amber-300">
473
- Deprecated
489
+ {copy.deprecated}
474
490
  </DevBadge>
475
491
  )}
476
492
  {(data.summary || summary) && (
@@ -481,7 +497,7 @@ export function ApiEndpointRead({
481
497
  {data.auth && (
482
498
  <IconLock
483
499
  className="size-3.5 shrink-0 text-plan-muted"
484
- aria-label="Requires authentication"
500
+ aria-label={copy.requiresAuthentication}
485
501
  />
486
502
  )}
487
503
  </button>
@@ -495,7 +511,9 @@ export function ApiEndpointRead({
495
511
  <div className="mb-4 flex items-center gap-2 text-xs text-plan-muted">
496
512
  <IconLock className="size-3.5 shrink-0" />
497
513
  <span>
498
- <span className="font-medium text-plan-text">Auth:</span>{" "}
514
+ <span className="font-medium text-plan-text">
515
+ {copy.auth}:
516
+ </span>{" "}
499
517
  {data.auth}
500
518
  </span>
501
519
  </div>
@@ -510,17 +528,21 @@ export function ApiEndpointRead({
510
528
  {params.length > 0 && (
511
529
  <div className="mt-5">
512
530
  <div className="text-xs font-semibold uppercase tracking-wide text-plan-muted">
513
- Parameters
531
+ {copy.parameters}
514
532
  </div>
515
533
  <div className="mt-2 overflow-hidden rounded-lg border border-plan-line">
516
534
  <table className="w-full border-collapse text-sm">
517
535
  <thead>
518
536
  <tr className="bg-accent/30 text-left text-xs uppercase tracking-wide text-plan-muted">
519
- <th className="px-3 py-2 font-medium">Name</th>
520
- <th className="px-3 py-2 font-medium">In</th>
521
- <th className="px-3 py-2 font-medium">Type</th>
522
- <th className="px-3 py-2 font-medium">Required</th>
523
- <th className="px-3 py-2 font-medium">Description</th>
537
+ <th className="px-3 py-2 font-medium">{copy.name}</th>
538
+ <th className="px-3 py-2 font-medium">{copy.in}</th>
539
+ <th className="px-3 py-2 font-medium">{copy.type}</th>
540
+ <th className="px-3 py-2 font-medium">
541
+ {copy.requiredColumn}
542
+ </th>
543
+ <th className="px-3 py-2 font-medium">
544
+ {copy.description}
545
+ </th>
524
546
  </tr>
525
547
  </thead>
526
548
  <tbody>
@@ -556,7 +578,12 @@ export function ApiEndpointRead({
556
578
  >
557
579
  {param.name}
558
580
  </span>
559
- {change && <ChangeChip change={change} />}
581
+ {change && (
582
+ <ChangeChip
583
+ change={change}
584
+ label={changeLabel[change]}
585
+ />
586
+ )}
560
587
  </span>
561
588
  </td>
562
589
  <td className="px-3 py-2">
@@ -589,11 +616,11 @@ export function ApiEndpointRead({
589
616
  current={
590
617
  param.required ? (
591
618
  <span className="font-medium text-red-600 dark:text-red-300">
592
- required
619
+ {copy.required}
593
620
  </span>
594
621
  ) : (
595
622
  <span className="text-plan-muted">
596
- optional
623
+ {copy.optional}
597
624
  </span>
598
625
  )
599
626
  }
@@ -615,7 +642,7 @@ export function ApiEndpointRead({
615
642
  <div className="mt-5">
616
643
  <div className="flex items-center gap-2">
617
644
  <span className="text-xs font-semibold uppercase tracking-wide text-plan-muted">
618
- Request body
645
+ {copy.requestBody}
619
646
  </span>
620
647
  {data.request?.contentType && (
621
648
  <span className="rounded bg-accent/40 px-1.5 py-0.5 font-mono text-[11px] text-plan-muted">
@@ -636,7 +663,7 @@ export function ApiEndpointRead({
636
663
  {responses.length > 0 && (
637
664
  <div className="mt-5">
638
665
  <div className="text-xs font-semibold uppercase tracking-wide text-plan-muted">
639
- Responses
666
+ {copy.responses}
640
667
  </div>
641
668
  <div className="mt-2 flex flex-col gap-3">
642
669
  {responses.map((response, index) => (
@@ -5,6 +5,7 @@ import type { BlockEditProps, BlockReadProps } from "../types.js";
5
5
  import type { MermaidData } from "./mermaid.config.js";
6
6
  import { DevInput, DevLabel } from "./dev-doc-ui.js";
7
7
  import { DiagramLightbox } from "./diagram.js";
8
+ import { useBlockCopy } from "./block-copy.js";
8
9
 
9
10
  /**
10
11
  * Read + Edit renderers for a `mermaid` block — a Mermaid diagram definition
@@ -163,6 +164,7 @@ function MermaidDiagram({
163
164
  idSeed: string;
164
165
  }) {
165
166
  const isDark = useIsDark();
167
+ const copy = useBlockCopy();
166
168
  // Only render the diagram after mount: `mermaid` is client-only and SSR has no
167
169
  // DOM for it to measure against.
168
170
  const [mounted, setMounted] = useState(false);
@@ -225,7 +227,7 @@ function MermaidDiagram({
225
227
  if (!mounted) {
226
228
  return (
227
229
  <div className="mt-2 flex min-h-24 items-center justify-center rounded-lg border border-plan-line bg-plan-code text-sm text-plan-muted">
228
- Loading diagram…
230
+ {copy.loadingDiagram}
229
231
  </div>
230
232
  );
231
233
  }
@@ -237,7 +239,7 @@ function MermaidDiagram({
237
239
  {source}
238
240
  </pre>
239
241
  <p className="text-sm text-plan-muted">
240
- Could not render diagram: {state.error}
242
+ {copy.couldNotRenderDiagram}: {state.error}
241
243
  </p>
242
244
  </div>
243
245
  );
@@ -246,7 +248,7 @@ function MermaidDiagram({
246
248
  if (!state.svg) {
247
249
  return (
248
250
  <div className="mt-2 flex min-h-24 items-center justify-center rounded-lg border border-plan-line bg-plan-code text-sm text-plan-muted">
249
- Add a diagram definition to render it.
251
+ {copy.addDiagramDefinition}
250
252
  </div>
251
253
  );
252
254
  }
@@ -263,8 +265,8 @@ function MermaidDiagram({
263
265
  type="button"
264
266
  data-plan-interactive
265
267
  onClick={() => setExpanded(true)}
266
- aria-label="Expand diagram"
267
- title="Expand diagram"
268
+ aria-label={copy.expandDiagram}
269
+ title={copy.expandDiagram}
268
270
  className="an-diagram-expand-trigger absolute right-2 top-2 z-10 flex size-7 items-center justify-center rounded-md border border-border/60 bg-background/90 text-muted-foreground opacity-0 shadow-sm backdrop-blur transition-[color,opacity] hover:text-foreground focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring group-hover/mermaid:opacity-100"
269
271
  >
270
272
  <IconArrowsMaximize className="size-4" />