@seed-ship/mcp-ui-solid 6.19.0 → 6.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (262) hide show
  1. package/CHANGELOG.md +532 -0
  2. package/README.md +798 -1
  3. package/dist/adapters/connector.cjs +15 -4
  4. package/dist/adapters/connector.cjs.map +1 -1
  5. package/dist/adapters/connector.d.ts +38 -0
  6. package/dist/adapters/connector.d.ts.map +1 -1
  7. package/dist/adapters/connector.js +15 -4
  8. package/dist/adapters/connector.js.map +1 -1
  9. package/dist/adapters/index.d.ts +4 -3
  10. package/dist/adapters/index.d.ts.map +1 -1
  11. package/dist/adapters/macro-run.cjs +25 -13
  12. package/dist/adapters/macro-run.cjs.map +1 -1
  13. package/dist/adapters/macro-run.d.ts +37 -2
  14. package/dist/adapters/macro-run.d.ts.map +1 -1
  15. package/dist/adapters/macro-run.js +25 -13
  16. package/dist/adapters/macro-run.js.map +1 -1
  17. package/dist/adapters.cjs +2 -0
  18. package/dist/adapters.cjs.map +1 -1
  19. package/dist/adapters.js +4 -2
  20. package/dist/components/ActionGroupRenderer.cjs +3 -1
  21. package/dist/components/ActionGroupRenderer.cjs.map +1 -1
  22. package/dist/components/ActionGroupRenderer.d.ts.map +1 -1
  23. package/dist/components/ActionGroupRenderer.js +3 -1
  24. package/dist/components/ActionGroupRenderer.js.map +1 -1
  25. package/dist/components/AgentCard.cjs +11 -8
  26. package/dist/components/AgentCard.cjs.map +1 -1
  27. package/dist/components/AgentCard.d.ts.map +1 -1
  28. package/dist/components/AgentCard.js +11 -8
  29. package/dist/components/AgentCard.js.map +1 -1
  30. package/dist/components/AgentHandoff.cjs +6 -1
  31. package/dist/components/AgentHandoff.cjs.map +1 -1
  32. package/dist/components/AgentHandoff.d.ts.map +1 -1
  33. package/dist/components/AgentHandoff.js +6 -1
  34. package/dist/components/AgentHandoff.js.map +1 -1
  35. package/dist/components/ArtifactRenderer.cjs +25 -6
  36. package/dist/components/ArtifactRenderer.cjs.map +1 -1
  37. package/dist/components/ArtifactRenderer.d.ts.map +1 -1
  38. package/dist/components/ArtifactRenderer.js +25 -6
  39. package/dist/components/ArtifactRenderer.js.map +1 -1
  40. package/dist/components/AutocompleteDropdown.cjs +30 -24
  41. package/dist/components/AutocompleteDropdown.cjs.map +1 -1
  42. package/dist/components/AutocompleteDropdown.d.ts.map +1 -1
  43. package/dist/components/AutocompleteDropdown.js +30 -24
  44. package/dist/components/AutocompleteDropdown.js.map +1 -1
  45. package/dist/components/AutocompleteFormField.cjs +3 -1
  46. package/dist/components/AutocompleteFormField.cjs.map +1 -1
  47. package/dist/components/AutocompleteFormField.d.ts.map +1 -1
  48. package/dist/components/AutocompleteFormField.js +4 -2
  49. package/dist/components/AutocompleteFormField.js.map +1 -1
  50. package/dist/components/BriefingDiff.cjs +45 -39
  51. package/dist/components/BriefingDiff.cjs.map +1 -1
  52. package/dist/components/BriefingDiff.d.ts.map +1 -1
  53. package/dist/components/BriefingDiff.js +45 -39
  54. package/dist/components/BriefingDiff.js.map +1 -1
  55. package/dist/components/CarouselRenderer.cjs +8 -2
  56. package/dist/components/CarouselRenderer.cjs.map +1 -1
  57. package/dist/components/CarouselRenderer.d.ts.map +1 -1
  58. package/dist/components/CarouselRenderer.js +8 -2
  59. package/dist/components/CarouselRenderer.js.map +1 -1
  60. package/dist/components/ChartJSRenderer.cjs +60 -36
  61. package/dist/components/ChartJSRenderer.cjs.map +1 -1
  62. package/dist/components/ChartJSRenderer.d.ts.map +1 -1
  63. package/dist/components/ChartJSRenderer.js +61 -37
  64. package/dist/components/ChartJSRenderer.js.map +1 -1
  65. package/dist/components/ChatPrompt.cjs +15 -9
  66. package/dist/components/ChatPrompt.cjs.map +1 -1
  67. package/dist/components/ChatPrompt.d.ts.map +1 -1
  68. package/dist/components/ChatPrompt.js +15 -9
  69. package/dist/components/ChatPrompt.js.map +1 -1
  70. package/dist/components/CodeBlockRenderer.cjs +34 -16
  71. package/dist/components/CodeBlockRenderer.cjs.map +1 -1
  72. package/dist/components/CodeBlockRenderer.d.ts.map +1 -1
  73. package/dist/components/CodeBlockRenderer.js +34 -16
  74. package/dist/components/CodeBlockRenderer.js.map +1 -1
  75. package/dist/components/DataPreviewSection.cjs +91 -44
  76. package/dist/components/DataPreviewSection.cjs.map +1 -1
  77. package/dist/components/DataPreviewSection.d.ts.map +1 -1
  78. package/dist/components/DataPreviewSection.js +92 -45
  79. package/dist/components/DataPreviewSection.js.map +1 -1
  80. package/dist/components/DegradedFallback.cjs +20 -16
  81. package/dist/components/DegradedFallback.cjs.map +1 -1
  82. package/dist/components/DegradedFallback.d.ts.map +1 -1
  83. package/dist/components/DegradedFallback.js +20 -16
  84. package/dist/components/DegradedFallback.js.map +1 -1
  85. package/dist/components/FeedbackInline.cjs +19 -3
  86. package/dist/components/FeedbackInline.cjs.map +1 -1
  87. package/dist/components/FeedbackInline.js +19 -3
  88. package/dist/components/FeedbackInline.js.map +1 -1
  89. package/dist/components/FooterRenderer.cjs +21 -17
  90. package/dist/components/FooterRenderer.cjs.map +1 -1
  91. package/dist/components/FooterRenderer.d.ts.map +1 -1
  92. package/dist/components/FooterRenderer.js +21 -17
  93. package/dist/components/FooterRenderer.js.map +1 -1
  94. package/dist/components/FormFieldRenderer.cjs +116 -93
  95. package/dist/components/FormFieldRenderer.cjs.map +1 -1
  96. package/dist/components/FormFieldRenderer.d.ts.map +1 -1
  97. package/dist/components/FormFieldRenderer.js +116 -93
  98. package/dist/components/FormFieldRenderer.js.map +1 -1
  99. package/dist/components/FormRenderer.cjs +55 -31
  100. package/dist/components/FormRenderer.cjs.map +1 -1
  101. package/dist/components/FormRenderer.d.ts.map +1 -1
  102. package/dist/components/FormRenderer.js +55 -31
  103. package/dist/components/FormRenderer.js.map +1 -1
  104. package/dist/components/GenerativeUIErrorBoundary.cjs +20 -15
  105. package/dist/components/GenerativeUIErrorBoundary.cjs.map +1 -1
  106. package/dist/components/GenerativeUIErrorBoundary.d.ts.map +1 -1
  107. package/dist/components/GenerativeUIErrorBoundary.js +20 -15
  108. package/dist/components/GenerativeUIErrorBoundary.js.map +1 -1
  109. package/dist/components/GraphRenderer.cjs +56 -33
  110. package/dist/components/GraphRenderer.cjs.map +1 -1
  111. package/dist/components/GraphRenderer.d.ts.map +1 -1
  112. package/dist/components/GraphRenderer.js +56 -33
  113. package/dist/components/GraphRenderer.js.map +1 -1
  114. package/dist/components/ImageGalleryRenderer.cjs +13 -4
  115. package/dist/components/ImageGalleryRenderer.cjs.map +1 -1
  116. package/dist/components/ImageGalleryRenderer.d.ts.map +1 -1
  117. package/dist/components/ImageGalleryRenderer.js +13 -4
  118. package/dist/components/ImageGalleryRenderer.js.map +1 -1
  119. package/dist/components/LightboxOverlay.cjs +16 -8
  120. package/dist/components/LightboxOverlay.cjs.map +1 -1
  121. package/dist/components/LightboxOverlay.d.ts.map +1 -1
  122. package/dist/components/LightboxOverlay.js +17 -9
  123. package/dist/components/LightboxOverlay.js.map +1 -1
  124. package/dist/components/MapRenderer.cjs +59 -29
  125. package/dist/components/MapRenderer.cjs.map +1 -1
  126. package/dist/components/MapRenderer.d.ts +10 -2
  127. package/dist/components/MapRenderer.d.ts.map +1 -1
  128. package/dist/components/MapRenderer.js +60 -30
  129. package/dist/components/MapRenderer.js.map +1 -1
  130. package/dist/components/ModalRenderer.cjs +4 -1
  131. package/dist/components/ModalRenderer.cjs.map +1 -1
  132. package/dist/components/ModalRenderer.d.ts.map +1 -1
  133. package/dist/components/ModalRenderer.js +5 -2
  134. package/dist/components/ModalRenderer.js.map +1 -1
  135. package/dist/components/ScratchpadPanel.cjs +578 -535
  136. package/dist/components/ScratchpadPanel.cjs.map +1 -1
  137. package/dist/components/ScratchpadPanel.d.ts.map +1 -1
  138. package/dist/components/ScratchpadPanel.js +578 -535
  139. package/dist/components/ScratchpadPanel.js.map +1 -1
  140. package/dist/components/StreamingUIRenderer.cjs +92 -28
  141. package/dist/components/StreamingUIRenderer.cjs.map +1 -1
  142. package/dist/components/StreamingUIRenderer.d.ts.map +1 -1
  143. package/dist/components/StreamingUIRenderer.js +92 -28
  144. package/dist/components/StreamingUIRenderer.js.map +1 -1
  145. package/dist/components/UIResourceRenderer.cjs +456 -363
  146. package/dist/components/UIResourceRenderer.cjs.map +1 -1
  147. package/dist/components/UIResourceRenderer.d.ts +21 -0
  148. package/dist/components/UIResourceRenderer.d.ts.map +1 -1
  149. package/dist/components/UIResourceRenderer.js +458 -365
  150. package/dist/components/UIResourceRenderer.js.map +1 -1
  151. package/dist/components/VerifiedText.cjs +39 -20
  152. package/dist/components/VerifiedText.cjs.map +1 -1
  153. package/dist/components/VerifiedText.d.ts +2 -1
  154. package/dist/components/VerifiedText.d.ts.map +1 -1
  155. package/dist/components/VerifiedText.js +39 -20
  156. package/dist/components/VerifiedText.js.map +1 -1
  157. package/dist/components/VideoRenderer.cjs +10 -5
  158. package/dist/components/VideoRenderer.cjs.map +1 -1
  159. package/dist/components/VideoRenderer.d.ts.map +1 -1
  160. package/dist/components/VideoRenderer.js +10 -5
  161. package/dist/components/VideoRenderer.js.map +1 -1
  162. package/dist/components/chart-data-table.cjs +38 -17
  163. package/dist/components/chart-data-table.cjs.map +1 -1
  164. package/dist/components/chart-data-table.d.ts +30 -1
  165. package/dist/components/chart-data-table.d.ts.map +1 -1
  166. package/dist/components/chart-data-table.js +38 -17
  167. package/dist/components/chart-data-table.js.map +1 -1
  168. package/dist/context/MCPActionContext.cjs +3 -1
  169. package/dist/context/MCPActionContext.cjs.map +1 -1
  170. package/dist/context/MCPActionContext.d.ts.map +1 -1
  171. package/dist/context/MCPActionContext.js +3 -1
  172. package/dist/context/MCPActionContext.js.map +1 -1
  173. package/dist/context/MCPUIStringsContext.cjs +285 -2
  174. package/dist/context/MCPUIStringsContext.cjs.map +1 -1
  175. package/dist/context/MCPUIStringsContext.d.ts +600 -3
  176. package/dist/context/MCPUIStringsContext.d.ts.map +1 -1
  177. package/dist/context/MCPUIStringsContext.js +285 -2
  178. package/dist/context/MCPUIStringsContext.js.map +1 -1
  179. package/dist/hooks/index.d.ts +2 -2
  180. package/dist/hooks/index.d.ts.map +1 -1
  181. package/dist/hooks/useStreamingUI.cjs +28 -10
  182. package/dist/hooks/useStreamingUI.cjs.map +1 -1
  183. package/dist/hooks/useStreamingUI.d.ts +37 -0
  184. package/dist/hooks/useStreamingUI.d.ts.map +1 -1
  185. package/dist/hooks/useStreamingUI.js +28 -10
  186. package/dist/hooks/useStreamingUI.js.map +1 -1
  187. package/dist/hooks.cjs +1 -0
  188. package/dist/hooks.cjs.map +1 -1
  189. package/dist/hooks.js +2 -1
  190. package/dist/index.cjs +23 -8
  191. package/dist/index.cjs.map +1 -1
  192. package/dist/index.d.cts +9 -3
  193. package/dist/index.d.ts +9 -3
  194. package/dist/index.d.ts.map +1 -1
  195. package/dist/index.js +18 -3
  196. package/dist/index.js.map +1 -1
  197. package/dist/services/validation.cjs +41 -16
  198. package/dist/services/validation.cjs.map +1 -1
  199. package/dist/services/validation.d.ts +47 -2
  200. package/dist/services/validation.d.ts.map +1 -1
  201. package/dist/services/validation.js +41 -16
  202. package/dist/services/validation.js.map +1 -1
  203. package/dist/utils/degraded-projections.cjs +37 -12
  204. package/dist/utils/degraded-projections.cjs.map +1 -1
  205. package/dist/utils/degraded-projections.d.ts +39 -3
  206. package/dist/utils/degraded-projections.d.ts.map +1 -1
  207. package/dist/utils/degraded-projections.js +37 -12
  208. package/dist/utils/degraded-projections.js.map +1 -1
  209. package/dist/utils/format-string.cjs +10 -0
  210. package/dist/utils/format-string.cjs.map +1 -0
  211. package/dist/utils/format-string.d.ts +26 -0
  212. package/dist/utils/format-string.d.ts.map +1 -0
  213. package/dist/utils/format-string.js +10 -0
  214. package/dist/utils/format-string.js.map +1 -0
  215. package/dist/validation.cjs +3 -0
  216. package/dist/validation.cjs.map +1 -1
  217. package/dist/validation.d.cts +2 -1
  218. package/dist/validation.d.ts +2 -1
  219. package/dist/validation.d.ts.map +1 -1
  220. package/dist/validation.js +4 -1
  221. package/package.json +1 -1
  222. package/src/adapters/connector.ts +58 -4
  223. package/src/adapters/index.ts +12 -2
  224. package/src/adapters/macro-run.ts +71 -11
  225. package/src/components/ActionGroupRenderer.tsx +3 -1
  226. package/src/components/AgentCard.tsx +27 -9
  227. package/src/components/AgentHandoff.tsx +4 -1
  228. package/src/components/ArtifactRenderer.tsx +17 -5
  229. package/src/components/AutocompleteDropdown.tsx +8 -6
  230. package/src/components/AutocompleteFormField.tsx +5 -1
  231. package/src/components/BriefingDiff.tsx +11 -3
  232. package/src/components/CarouselRenderer.tsx +4 -2
  233. package/src/components/ChartJSRenderer.tsx +18 -9
  234. package/src/components/ChatPrompt.tsx +13 -8
  235. package/src/components/CodeBlockRenderer.tsx +12 -10
  236. package/src/components/DataPreviewSection.tsx +92 -26
  237. package/src/components/DegradedFallback.tsx +4 -2
  238. package/src/components/FeedbackInline.tsx +2 -2
  239. package/src/components/FooterRenderer.tsx +3 -1
  240. package/src/components/FormFieldRenderer.tsx +34 -19
  241. package/src/components/FormRenderer.tsx +38 -10
  242. package/src/components/GenerativeUIErrorBoundary.tsx +8 -3
  243. package/src/components/GraphRenderer.tsx +31 -16
  244. package/src/components/ImageGalleryRenderer.tsx +8 -4
  245. package/src/components/LightboxOverlay.tsx +6 -4
  246. package/src/components/MapRenderer.tsx +57 -20
  247. package/src/components/ModalRenderer.tsx +3 -1
  248. package/src/components/ScratchpadPanel.tsx +95 -44
  249. package/src/components/StreamingUIRenderer.tsx +41 -11
  250. package/src/components/UIResourceRenderer.tsx +145 -63
  251. package/src/components/VerifiedText.tsx +19 -9
  252. package/src/components/VideoRenderer.tsx +6 -4
  253. package/src/components/chart-data-table.ts +68 -14
  254. package/src/context/MCPActionContext.tsx +4 -1
  255. package/src/context/MCPUIStringsContext.tsx +1004 -1
  256. package/src/hooks/index.ts +2 -1
  257. package/src/hooks/useStreamingUI.ts +64 -10
  258. package/src/index.ts +23 -0
  259. package/src/services/validation.ts +93 -16
  260. package/src/utils/degraded-projections.ts +75 -12
  261. package/src/utils/format-string.ts +33 -0
  262. package/src/validation.ts +5 -0
@@ -22,7 +22,11 @@ import {
22
22
  type DuplicateMountInfo,
23
23
  } from '../utils/duplicate-mount-registry'
24
24
  import { useTelemetry } from '../context/MCPUITelemetryContext'
25
- import { useMCPUIStrings } from '../context/MCPUIStringsContext'
25
+ import {
26
+ DEFAULT_MCPUI_STRINGS,
27
+ formatMCPUIString,
28
+ useMCPUIStrings,
29
+ } from '../context/MCPUIStringsContext'
26
30
 
27
31
  /**
28
32
  * How `<UIResourceRenderer>` reacts when `validateComponent()` rejects a
@@ -65,6 +69,7 @@ import { marked } from 'marked'
65
69
  * Copy button component with visual feedback
66
70
  */
67
71
  function CopyButton(props: { getText: () => string; title?: string; position?: 'top-right' | 'bottom-right' }) {
72
+ const strings = useMCPUIStrings()
68
73
  const [copied, setCopied] = createSignal(false)
69
74
 
70
75
  const handleCopy = async () => {
@@ -87,8 +92,8 @@ function CopyButton(props: { getText: () => string; title?: string; position?: '
87
92
  <button
88
93
  onClick={handleCopy}
89
94
  class={`${positionClasses()} opacity-60 hover:opacity-100 px-2 py-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 transition-all shadow-sm z-10`}
90
- title={props.title || 'Copy'}
91
- aria-label={props.title || 'Copy'}
95
+ title={props.title || strings.copy}
96
+ aria-label={props.title || strings.copy}
92
97
  data-mcp-ui-action="copy"
93
98
  type="button"
94
99
  >
@@ -210,6 +215,7 @@ function ChartRenderer(props: {
210
215
  /** Host opt-in for the external quickchart.io fallback (audit P1.7). */
211
216
  allowQuickchartFallback?: boolean
212
217
  }) {
218
+ const strings = useMCPUIStrings()
213
219
  const [useNative, setUseNative] = createSignal(false)
214
220
  const [iframeUrl, setIframeUrl] = createSignal<string>()
215
221
  const [isLoading, setIsLoading] = createSignal(true)
@@ -223,6 +229,11 @@ function ChartRenderer(props: {
223
229
  const params = () => props.component.params as any
224
230
  const rendererPref = () => params()?.renderer || 'auto'
225
231
  const allowQuickchart = () => props.allowQuickchartFallback === true
232
+ /** `alt` / `aria-label` of the quickchart image — localized chrome. */
233
+ const chartImageAlt = () =>
234
+ params()?.title
235
+ ? formatMCPUIString(strings.chartWithTitleAlt, { title: String(params().title) })
236
+ : strings.chartVisualizationAlt
226
237
 
227
238
  // Emit a clear, observable signal whenever we decline the external fallback.
228
239
  const signalBlockedFallback = (reason: string) => {
@@ -242,7 +253,7 @@ function ChartRenderer(props: {
242
253
  if (!params()?.data?.datasets) {
243
254
  return (
244
255
  <div class="p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
245
- <p class="text-red-500 dark:text-red-400 text-sm">Invalid chart data: missing data.datasets</p>
256
+ <p class="text-red-500 dark:text-red-400 text-sm">{strings.chartInvalidData}</p>
246
257
  </div>
247
258
  )
248
259
  }
@@ -270,7 +281,7 @@ function ChartRenderer(props: {
270
281
  setUseNative(true)
271
282
  setIsLoading(false)
272
283
  } else {
273
- setError('Chart.js is not available. Install chart.js peer dependency.')
284
+ setError(strings.chartJsUnavailable)
274
285
  setIsLoading(false)
275
286
  }
276
287
  } else {
@@ -327,9 +338,11 @@ function ChartRenderer(props: {
327
338
  <Show when={degraded()}>
328
339
  <div class="p-3">
329
340
  <DegradedFallback
330
- message="Interactive chart unavailable — install the chart.js peer dependency, or set allowQuickchartFallback to use the external quickchart.io renderer."
331
- caption="Showing the chart data as a table."
332
- {...chartToDegradedTable(params() ?? {})}
341
+ message={strings.chartIframeUnavailable}
342
+ caption={strings.chartQuickchartCaption}
343
+ {...chartToDegradedTable(params() ?? {}, {
344
+ series: strings.degradedSeries,
345
+ })}
333
346
  />
334
347
  </div>
335
348
  </Show>
@@ -343,7 +356,7 @@ function ChartRenderer(props: {
343
356
  <Show when={error()}>
344
357
  <div class="absolute inset-0 flex items-center justify-center p-4">
345
358
  <div class="text-center">
346
- <p class="text-red-600 dark:text-red-400 text-sm font-medium">Chart Error</p>
359
+ <p class="text-red-600 dark:text-red-400 text-sm font-medium">{strings.chartError}</p>
347
360
  <p class="text-gray-600 dark:text-gray-400 text-xs mt-1">{error()}</p>
348
361
  </div>
349
362
  </div>
@@ -356,13 +369,13 @@ function ChartRenderer(props: {
356
369
  {params()?.title}
357
370
  </h3>
358
371
  </Show>
359
- <div class="w-full h-full" role="img" aria-label={params()?.title ? `Chart: ${params()?.title}` : 'Chart visualization'}>
372
+ <div class="w-full h-full" role="img" aria-label={chartImageAlt()}>
360
373
  <img
361
374
  src={iframeUrl()}
362
- alt={params()?.title ? `Chart: ${params()?.title}` : 'Chart visualization'}
375
+ alt={chartImageAlt()}
363
376
  class="w-full h-auto max-h-[300px] object-contain"
364
377
  onError={() => {
365
- setError('Failed to load chart')
378
+ setError(strings.chartLoadFailed)
366
379
  props.onError?.({
367
380
  type: 'render',
368
381
  message: 'Chart rendering failed',
@@ -491,6 +504,27 @@ export interface CitationCtx {
491
504
  id: number,
492
505
  mapping: { page: number | string; file?: string; file_id?: number | string } | undefined
493
506
  ) => string
507
+ /**
508
+ * Template for the visible placeholder kept when `map` is EMPTY and a
509
+ * marker therefore resolves to nothing — `{id}` is the marker number.
510
+ *
511
+ * Optional: `transformCellCitations` falls back to
512
+ * `DEFAULT_MCPUI_STRINGS.citationUnresolved` (`'[ref. {id}]'`). The
513
+ * renderers populate it from `strings.citationUnresolved`, so a host with a
514
+ * `<MCPUIStringsProvider>` gets its own wording; a direct
515
+ * `renderCellValue(value, ctx)` call may pass it explicitly.
516
+ *
517
+ * @since v6.20.0 — the hardcoded placeholder used to be French.
518
+ */
519
+ unresolvedLabel?: string
520
+ /**
521
+ * Tooltip template of the default citation chip button — `{label}` is the
522
+ * file name and page. Falls back to `DEFAULT_MCPUI_STRINGS.citationViewSource`;
523
+ * the renderers populate it from `strings.citationViewSource`.
524
+ *
525
+ * @since v6.20.0
526
+ */
527
+ viewSourceLabel?: string
494
528
  }
495
529
 
496
530
  /**
@@ -502,7 +536,8 @@ export interface CitationCtx {
502
536
  function defaultCitationChip(
503
537
  pageNum: number | string,
504
538
  fileName: string,
505
- verified = true
539
+ verified = true,
540
+ viewSourceLabel: string = DEFAULT_MCPUI_STRINGS.citationViewSource
506
541
  ): string {
507
542
  const safeDocName = encodeURIComponent(fileName || '')
508
543
  const label = fileName ? `${fileName} - ${pageNum}` : `${pageNum}`
@@ -516,7 +551,7 @@ function defaultCitationChip(
516
551
  ` data-citation-page="${pageNum}"`,
517
552
  ` data-citation-doc="${safeDocName}"`,
518
553
  ' data-citation-verified="true"',
519
- ` title="View source - ${label}">`,
554
+ ` title="${escapeHtml(formatMCPUIString(viewSourceLabel, { label }))}">`,
520
555
  '<svg class="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>',
521
556
  '</button>',
522
557
  '</span>',
@@ -543,12 +578,16 @@ function transformCellCitations(text: string, ctx: CitationCtx): string {
543
578
  const id = parseInt(idStr, 10)
544
579
  const mapping = ctx.map[id] ?? ctx.map[String(id)]
545
580
  if (ctx.render) return ctx.render(id, mapping)
546
- if (mapping) return defaultCitationChip(mapping.page, mapping.file ?? '', true)
581
+ if (mapping) return defaultCitationChip(mapping.page, mapping.file ?? '', true, ctx.viewSourceLabel)
547
582
  // Unresolved id: when the map is non-empty (consumer claims to know
548
583
  // the citations), drop silently — it's likely an LLM hallucination.
549
584
  // When the map is empty (consumer didn't supply one), preserve a
550
585
  // human-visible placeholder so the marker isn't lost.
551
- return Object.keys(ctx.map).length > 0 ? '' : `[réf. ${id}]`
586
+ return Object.keys(ctx.map).length > 0
587
+ ? ''
588
+ : formatMCPUIString(ctx.unresolvedLabel ?? DEFAULT_MCPUI_STRINGS.citationUnresolved, {
589
+ id,
590
+ })
552
591
  }
553
592
  )
554
593
  }
@@ -670,7 +709,17 @@ function TableRenderer(props: {
670
709
  // every `renderCellValue` call below. Absent → undefined → cells render
671
710
  // as before (regression-safe).
672
711
  const citationCtx: CitationCtx | undefined = tableParams.citationMap
673
- ? { map: tableParams.citationMap, render: tableParams.citationRender }
712
+ ? {
713
+ map: tableParams.citationMap,
714
+ render: tableParams.citationRender,
715
+ // Getters, not snapshots: cells read the provider's current wording.
716
+ get unresolvedLabel() {
717
+ return strings.citationUnresolved
718
+ },
719
+ get viewSourceLabel() {
720
+ return strings.citationViewSource
721
+ },
722
+ }
674
723
  : undefined
675
724
 
676
725
  // ─── Client-side sorting (v4.0.5) ────────────────────────
@@ -706,7 +755,7 @@ function TableRenderer(props: {
706
755
  if (isNum) {
707
756
  cmp = (Number(va) || 0) - (Number(vb) || 0)
708
757
  } else {
709
- cmp = String(va).localeCompare(String(vb), 'fr', { sensitivity: 'base' })
758
+ cmp = String(va).localeCompare(String(vb), strings.locale, { sensitivity: 'base' })
710
759
  }
711
760
  return dir === 'desc' ? -cmp : cmp
712
761
  })
@@ -729,7 +778,7 @@ function TableRenderer(props: {
729
778
  // context where users scan many tables across messages. Backward-compat
730
779
  // for anyone who explicitly disabled.
731
780
  const isSearchable = () => tableParams.searchable !== false
732
- const searchPlaceholder = () => tableParams.searchPlaceholder || 'Rechercher dans le tableau...'
781
+ const searchPlaceholder = () => tableParams.searchPlaceholder || strings.tableSearchPlaceholder
733
782
 
734
783
  const handleSearch = (value: string) => {
735
784
  setSearchQuery(value)
@@ -800,7 +849,7 @@ function TableRenderer(props: {
800
849
  for (const n of [10, 30, 60, 100]) {
801
850
  if (n < total) opts.push({ value: n, label: String(n) })
802
851
  }
803
- opts.push({ value: 0, label: 'All' })
852
+ opts.push({ value: 0, label: strings.paginationAllRows })
804
853
  return opts
805
854
  }
806
855
 
@@ -1006,18 +1055,18 @@ function TableRenderer(props: {
1006
1055
  }
1007
1056
 
1008
1057
  return (
1009
- <ExpandableWrapper title={tableParams.title || 'Table'} copyData={getTableCSV()} copyLabel="Copy table (CSV)" toolbarVariant={props.toolbarVariant} onExpandedChange={setSelfExpanded}>
1058
+ <ExpandableWrapper title={tableParams.title || strings.tableTitle} copyData={getTableCSV()} copyLabel={strings.tableCopyCsv} toolbarVariant={props.toolbarVariant} onExpandedChange={setSelfExpanded}>
1010
1059
  <div class={`relative w-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden group ${
1011
1060
  isExpanded() ? 'flex-1 min-h-0 flex flex-col' : 'h-full'
1012
1061
  } ${tableParams.className || ''}`}>
1013
- <Show when={exportable} fallback={<CopyButton getText={getTableCSV} title="Copy table (CSV)" position="top-right" />}>
1062
+ <Show when={exportable} fallback={<CopyButton getText={getTableCSV} title={strings.tableCopyCsv} position="top-right" />}>
1014
1063
  <div class="absolute right-10 top-2 z-10">
1015
1064
  <button
1016
1065
  ref={exportTriggerRef}
1017
1066
  onClick={() => setShowExportMenu(!showExportMenu())}
1018
1067
  class="opacity-60 hover:opacity-100 px-2 py-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 transition-all shadow-sm"
1019
- title="Export table"
1020
- aria-label="Export table"
1068
+ title={strings.tableExport}
1069
+ aria-label={strings.tableExport}
1021
1070
  aria-haspopup="menu"
1022
1071
  aria-expanded={showExportMenu()}
1023
1072
  >
@@ -1032,13 +1081,13 @@ function TableRenderer(props: {
1032
1081
  width={144}
1033
1082
  >
1034
1083
  <Show when={(exportFormats as string[]).includes('tsv')}>
1035
- <button onClick={() => handleExport('tsv')} class="w-full text-left px-3 py-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300">Copy TSV</button>
1084
+ <button onClick={() => handleExport('tsv')} class="w-full text-left px-3 py-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300">{strings.tableCopyTsv}</button>
1036
1085
  </Show>
1037
1086
  <Show when={(exportFormats as string[]).includes('csv')}>
1038
- <button onClick={() => handleExport('csv')} class="w-full text-left px-3 py-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300">Download CSV</button>
1087
+ <button onClick={() => handleExport('csv')} class="w-full text-left px-3 py-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300">{strings.tableDownloadCsv}</button>
1039
1088
  </Show>
1040
1089
  <Show when={(exportFormats as string[]).includes('json')}>
1041
- <button onClick={() => handleExport('json')} class="w-full text-left px-3 py-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300">Download JSON</button>
1090
+ <button onClick={() => handleExport('json')} class="w-full text-left px-3 py-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300">{strings.tableDownloadJson}</button>
1042
1091
  </Show>
1043
1092
  </PortalDropdownMenu>
1044
1093
  </div>
@@ -1048,7 +1097,9 @@ function TableRenderer(props: {
1048
1097
  <h3 id={`${tableId}-title`} class="text-sm font-semibold text-gray-900 dark:text-white mb-3 flex-shrink-0">
1049
1098
  {tableParams.title}
1050
1099
  <Show when={isVirtualizing()}>
1051
- <span class="ml-2 text-xs font-normal text-gray-400">(virtualized: {tableParams.rows?.length} rows)</span>
1100
+ <span class="ml-2 text-xs font-normal text-gray-400">
1101
+ {formatMCPUIString(strings.tableVirtualizedRows, { count: tableParams.rows?.length ?? 0 })}
1102
+ </span>
1052
1103
  </Show>
1053
1104
  </h3>
1054
1105
  </Show>
@@ -1069,13 +1120,16 @@ function TableRenderer(props: {
1069
1120
  type="button"
1070
1121
  onClick={() => { handleSearch(''); setSearchQuery(''); setDebouncedQuery('') }}
1071
1122
  class="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 text-sm"
1072
- aria-label="Clear search"
1123
+ aria-label={strings.tableClearSearch}
1073
1124
  >&times;</button>
1074
1125
  </Show>
1075
1126
  </div>
1076
1127
  <Show when={debouncedQuery() && filteredRows().length !== sortedRows().length}>
1077
1128
  <p class="text-xs text-gray-500 dark:text-gray-400 mb-2">
1078
- {filteredRows().length} result{filteredRows().length !== 1 ? 's' : ''} on {sortedRows().length}
1129
+ {formatMCPUIString(
1130
+ filteredRows().length === 1 ? strings.tableSearchResultsOne : strings.tableSearchResultsMany,
1131
+ { count: filteredRows().length, total: sortedRows().length }
1132
+ )}
1079
1133
  </p>
1080
1134
  </Show>
1081
1135
  </Show>
@@ -1106,7 +1160,7 @@ function TableRenderer(props: {
1106
1160
  })()
1107
1161
  }
1108
1162
  role="region"
1109
- aria-label={tableParams.title || 'Data table'}
1163
+ aria-label={tableParams.title || strings.tableAriaLabel}
1110
1164
  tabindex="0"
1111
1165
  >
1112
1166
  <table
@@ -1122,7 +1176,7 @@ function TableRenderer(props: {
1122
1176
  class="px-6 py-3 text-left text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider border-b border-gray-200 dark:border-gray-700 first:pl-6 last:pr-6 bg-gray-100 dark:bg-gray-900 cursor-pointer select-none hover:bg-gray-200 dark:hover:bg-gray-800 transition-colors"
1123
1177
  style={column.width ? { width: column.width } : {}}
1124
1178
  on:click={() => handleSort(column.key)}
1125
- title={`Sort by ${column.label}`}
1179
+ title={formatMCPUIString(strings.sortBy, { column: column.label })}
1126
1180
  >
1127
1181
  <span class="inline-flex items-center gap-1">
1128
1182
  {column.label}
@@ -1151,12 +1205,14 @@ function TableRenderer(props: {
1151
1205
  <Show when={tableParams.pagination}>
1152
1206
  <div class="mt-3 flex items-center justify-between text-xs text-gray-500 dark:text-gray-400">
1153
1207
  <span>
1154
- Showing {tableParams.pagination.currentPage * tableParams.pagination.pageSize + 1} -{' '}
1155
- {Math.min(
1156
- (tableParams.pagination.currentPage + 1) * tableParams.pagination.pageSize,
1157
- tableParams.pagination.totalRows
1158
- )}{' '}
1159
- of {tableParams.pagination.totalRows}
1208
+ {formatMCPUIString(strings.tableServerPageRange, {
1209
+ start: tableParams.pagination.currentPage * tableParams.pagination.pageSize + 1,
1210
+ end: Math.min(
1211
+ (tableParams.pagination.currentPage + 1) * tableParams.pagination.pageSize,
1212
+ tableParams.pagination.totalRows
1213
+ ),
1214
+ total: tableParams.pagination.totalRows,
1215
+ })}
1160
1216
  </span>
1161
1217
  </div>
1162
1218
  </Show>
@@ -1165,7 +1221,7 @@ function TableRenderer(props: {
1165
1221
  <Show when={needsClientPagination()}>
1166
1222
  <div class="mt-3 flex items-center justify-between text-xs text-gray-500 dark:text-gray-400">
1167
1223
  <span>
1168
- {clientRangeStart()}&ndash;{clientRangeEnd()} / {filteredRows().length.toLocaleString('fr-FR')}
1224
+ {clientRangeStart()}&ndash;{clientRangeEnd()} / {filteredRows().length.toLocaleString(strings.locale)}
1169
1225
  </span>
1170
1226
  <div class="flex items-center gap-2">
1171
1227
  <button
@@ -1202,7 +1258,7 @@ function TableRenderer(props: {
1202
1258
  {(opt) => <option value={opt.value}>{opt.label}</option>}
1203
1259
  </For>
1204
1260
  </select>
1205
- <span class="text-gray-400">/ page</span>
1261
+ <span class="text-gray-400">{strings.perPageSuffix}</span>
1206
1262
  </Show>
1207
1263
  </div>
1208
1264
  </div>
@@ -1217,6 +1273,7 @@ function TableRenderer(props: {
1217
1273
  * Render a metric card component
1218
1274
  */
1219
1275
  function MetricRenderer(props: { component: UIComponent }) {
1276
+ const strings = useMCPUIStrings()
1220
1277
  const metricParams = props.component.params as any
1221
1278
 
1222
1279
  // Generate copyable text for metric
@@ -1229,7 +1286,7 @@ function MetricRenderer(props: { component: UIComponent }) {
1229
1286
 
1230
1287
  return (
1231
1288
  <div class="relative w-full h-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-4 group">
1232
- <CopyButton getText={getMetricText} title="Copy metric" position="top-right" />
1289
+ <CopyButton getText={getMetricText} title={strings.copyMetric} position="top-right" />
1233
1290
  <div class="flex flex-col h-full justify-between">
1234
1291
  <div>
1235
1292
  <p class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">
@@ -1277,14 +1334,17 @@ function MetricRenderer(props: { component: UIComponent }) {
1277
1334
  * Extract image data from markdown image link format
1278
1335
  * Pattern: [![alt](image-url)](link-url)\n*Photo by Author*
1279
1336
  */
1280
- function extractImageFromMarkdown(content: string): { alt: string; imageUrl: string; linkUrl: string; credit: string } | null {
1337
+ function extractImageFromMarkdown(
1338
+ content: string,
1339
+ fallbackAlt: string
1340
+ ): { alt: string; imageUrl: string; linkUrl: string; credit: string } | null {
1281
1341
  // Pattern: [![alt text](image-url)](link-url) followed by optional credit line
1282
1342
  const imagePattern = /\[!\[([^\]]*)\]\(([^)]+)\)\]\(([^)]+)\)\s*\*([^*]+)\*/
1283
1343
  const match = content.match(imagePattern)
1284
1344
 
1285
1345
  if (match) {
1286
1346
  return {
1287
- alt: match[1] || 'Image',
1347
+ alt: match[1] || fallbackAlt,
1288
1348
  imageUrl: match[2],
1289
1349
  linkUrl: match[3],
1290
1350
  credit: match[4].trim()
@@ -1298,6 +1358,7 @@ function extractImageFromMarkdown(content: string): { alt: string; imageUrl: str
1298
1358
  * Render a text component (with optional markdown)
1299
1359
  */
1300
1360
  function TextRenderer(props: { component: UIComponent }) {
1361
+ const strings = useMCPUIStrings()
1301
1362
  const textParams = props.component.params as any
1302
1363
 
1303
1364
  // Check if this is an image markdown that should be rendered as image
@@ -1308,7 +1369,7 @@ function TextRenderer(props: { component: UIComponent }) {
1308
1369
  // markdown path; an unusable link URL only drops the anchor.
1309
1370
  const imageData = createMemo(() => {
1310
1371
  if (textParams.markdown && textParams.content) {
1311
- const extracted = extractImageFromMarkdown(textParams.content)
1372
+ const extracted = extractImageFromMarkdown(textParams.content, strings.imageAlt)
1312
1373
  if (!extracted) return null
1313
1374
  const imageUrl = safeUrl(extracted.imageUrl, { allowDataImage: true })
1314
1375
  if (!imageUrl) return null
@@ -1351,7 +1412,7 @@ function TextRenderer(props: { component: UIComponent }) {
1351
1412
  when={imageData()}
1352
1413
  fallback={
1353
1414
  <div class="relative w-full h-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-4 group">
1354
- <CopyButton getText={getTextContent} title="Copy text" position="top-right" />
1415
+ <CopyButton getText={getTextContent} title={strings.copyText} position="top-right" />
1355
1416
  <SafeHtml
1356
1417
  class={`prose prose-sm dark:prose-invert max-w-none ${textParams.className || ''}`}
1357
1418
  html={htmlContent}
@@ -1396,6 +1457,7 @@ function TextRenderer(props: { component: UIComponent }) {
1396
1457
  * Render an iframe component
1397
1458
  */
1398
1459
  function IframeRenderer(props: { component: UIComponent }) {
1460
+ const strings = useMCPUIStrings()
1399
1461
  const params = props.component.params as any
1400
1462
  return (
1401
1463
  <div class="w-full h-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden flex flex-col">
@@ -1406,7 +1468,7 @@ function IframeRenderer(props: { component: UIComponent }) {
1406
1468
  </Show>
1407
1469
  <iframe
1408
1470
  src={params.url}
1409
- title={params.title || 'Embedded content'}
1471
+ title={params.title || strings.iframeTitle}
1410
1472
  class="w-full border-0 flex-1"
1411
1473
  style={`height: ${params.height || '400px'}; min-height: 300px;`}
1412
1474
  sandbox={getIframeSandbox(params.url)}
@@ -1420,6 +1482,7 @@ function IframeRenderer(props: { component: UIComponent }) {
1420
1482
  * Render an image component
1421
1483
  */
1422
1484
  function ImageRenderer(props: { component: UIComponent }) {
1485
+ const strings = useMCPUIStrings()
1423
1486
  const params = props.component.params as any
1424
1487
 
1425
1488
  return (
@@ -1430,11 +1493,13 @@ function ImageRenderer(props: { component: UIComponent }) {
1430
1493
  target="_blank"
1431
1494
  rel="noopener noreferrer"
1432
1495
  class="cursor-zoom-in focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 rounded"
1433
- aria-label={`View full size: ${params.alt || 'image'}`}
1496
+ aria-label={formatMCPUIString(strings.imageViewFullSize, {
1497
+ alt: params.alt || strings.imageAltFallback,
1498
+ })}
1434
1499
  >
1435
1500
  <img
1436
1501
  src={safeUrl(params.url, { allowDataImage: true })}
1437
- alt={params.alt || 'Image'}
1502
+ alt={params.alt || strings.imageAlt}
1438
1503
  class="max-w-full max-h-[500px] object-contain rounded shadow-sm hover:opacity-95 transition-opacity"
1439
1504
  loading="lazy"
1440
1505
  />
@@ -1453,6 +1518,7 @@ function ImageRenderer(props: { component: UIComponent }) {
1453
1518
  * Render a link component
1454
1519
  */
1455
1520
  function LinkRenderer(props: { component: UIComponent }) {
1521
+ const strings = useMCPUIStrings()
1456
1522
  const params = props.component.params as any
1457
1523
 
1458
1524
  return (
@@ -1460,7 +1526,10 @@ function LinkRenderer(props: { component: UIComponent }) {
1460
1526
  href={safeUrl(params.url)}
1461
1527
  target="_blank"
1462
1528
  rel="noopener noreferrer"
1463
- aria-label={`${params.label || 'Link'}: ${params.description || params.url} (opens in new tab)`}
1529
+ aria-label={formatMCPUIString(strings.linkOpensInNewTab, {
1530
+ label: params.label || strings.linkLabel,
1531
+ description: params.description || params.url,
1532
+ })}
1464
1533
  class={`flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors group h-full focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 ${params.className || ''}`}
1465
1534
  onClick={(e) => e.stopPropagation()}
1466
1535
  >
@@ -1525,6 +1594,7 @@ function ComponentRenderer(props: {
1525
1594
  // mounted above; null-checked at every dispatch site so apps that don't
1526
1595
  // opt in see zero behavior change.
1527
1596
  const telemetry = useTelemetry()
1597
+ const strings = useMCPUIStrings()
1528
1598
 
1529
1599
  onMount(() => {
1530
1600
  markRenderEnd(props.component.id)
@@ -1590,7 +1660,7 @@ function ComponentRenderer(props: {
1590
1660
  }
1591
1661
 
1592
1662
  const mode: ValidationErrorMode = props.errorMode ?? 'block'
1593
- const firstError = validation.errors?.[0]?.message || 'Unknown validation error'
1663
+ const firstError = validation.errors?.[0]?.message || strings.validationUnknownError
1594
1664
 
1595
1665
  // P1.6 — an UNKNOWN component type must never produce a silent blank,
1596
1666
  // whatever the errorMode. The renderer has no branch for it, so even
@@ -1609,7 +1679,7 @@ function ComponentRenderer(props: {
1609
1679
  <div
1610
1680
  class="inline-flex items-center gap-1.5 px-2 py-1 rounded-md bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 text-xs text-yellow-800 dark:text-yellow-200"
1611
1681
  role="alert"
1612
- aria-label="Component validation warning"
1682
+ aria-label={strings.validationWarning}
1613
1683
  title={firstError}
1614
1684
  >
1615
1685
  <svg
@@ -1627,7 +1697,7 @@ function ComponentRenderer(props: {
1627
1697
  <line x1="12" y1="9" x2="12" y2="13" />
1628
1698
  <line x1="12" y1="17" x2="12.01" y2="17" />
1629
1699
  </svg>
1630
- <span>Invalid {props.component.type}</span>
1700
+ <span>{formatMCPUIString(strings.invalidComponent, { type: props.component.type })}</span>
1631
1701
  </div>
1632
1702
  )
1633
1703
  }
@@ -1635,7 +1705,7 @@ function ComponentRenderer(props: {
1635
1705
  // mode === 'block' (default, pre-v5.4.0 behavior)
1636
1706
  return (
1637
1707
  <div class="w-full h-full bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
1638
- <p class="text-sm font-medium text-red-900 dark:text-red-100">Validation Error</p>
1708
+ <p class="text-sm font-medium text-red-900 dark:text-red-100">{strings.validationError}</p>
1639
1709
  <p class="text-xs text-red-700 dark:text-red-300 mt-1">
1640
1710
  {firstError}
1641
1711
  </p>
@@ -1737,12 +1807,13 @@ function ComponentRenderer(props: {
1737
1807
  * 'UNKNOWN_COMPONENT_TYPE'`), so this component stays purely presentational.
1738
1808
  */
1739
1809
  function UnsupportedComponentFallback(props: { component: UIComponent }) {
1810
+ const strings = useMCPUIStrings()
1740
1811
  return (
1741
1812
  <div
1742
1813
  role="alert"
1743
1814
  class="w-full rounded-lg border border-amber-300 bg-amber-50 px-3 py-2 text-sm text-amber-800 dark:border-amber-700/50 dark:bg-amber-900/20 dark:text-amber-200"
1744
1815
  >
1745
- Unsupported component type: <code class="font-mono">{props.component.type}</code>
1816
+ {strings.unsupportedComponentType} <code class="font-mono">{props.component.type}</code>
1746
1817
  </div>
1747
1818
  )
1748
1819
  }
@@ -1852,13 +1923,21 @@ function ActionRenderer(props: { component: UIComponent }) {
1852
1923
  * Handles {error: true, message: "...", tool: "...", suggestions: [...]} format
1853
1924
  */
1854
1925
  function ErrorCardRenderer(props: { error: any }) {
1926
+ const strings = useMCPUIStrings()
1927
+ // The timestamp depends on the viewer's time zone, which the server cannot
1928
+ // know. Render it only after mount so SSR and hydration produce the same markup.
1929
+ const [mounted, setMounted] = createSignal(false)
1930
+ onMount(() => setMounted(true))
1855
1931
  const getErrorText = () => {
1856
- return `Error in ${props.error.tool || 'unknown tool'}: ${props.error.message || 'Unknown error'}`
1932
+ return formatMCPUIString(strings.errorCopyText, {
1933
+ tool: props.error.tool || strings.errorUnknownTool,
1934
+ message: props.error.message || strings.errorUnknown,
1935
+ })
1857
1936
  }
1858
1937
 
1859
1938
  return (
1860
1939
  <div class="relative w-full bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 group">
1861
- <CopyButton getText={getErrorText} title="Copy error details" position="top-right" />
1940
+ <CopyButton getText={getErrorText} title={strings.copyErrorDetails} position="top-right" />
1862
1941
  <div class="flex items-start gap-3">
1863
1942
  <div class="p-2 bg-red-100 dark:bg-red-900/40 rounded-full shrink-0">
1864
1943
  <svg class="w-5 h-5 text-red-600 dark:text-red-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
@@ -1867,19 +1946,21 @@ function ErrorCardRenderer(props: { error: any }) {
1867
1946
  </div>
1868
1947
  <div class="flex-1 min-w-0">
1869
1948
  <h4 class="text-sm font-semibold text-red-800 dark:text-red-200">
1870
- Tool Error: {props.error.tool || 'Unknown'}
1949
+ {formatMCPUIString(strings.toolErrorTitle, {
1950
+ tool: props.error.tool || strings.errorUnknownToolName,
1951
+ })}
1871
1952
  </h4>
1872
1953
  <p class="text-sm text-red-700 dark:text-red-300 mt-1">
1873
- {props.error.message || 'An error occurred during tool execution'}
1954
+ {props.error.message || strings.errorToolExecution}
1874
1955
  </p>
1875
1956
  <Show when={props.error.type}>
1876
1957
  <p class="text-xs text-red-600 dark:text-red-400 mt-1">
1877
- Type: {props.error.type}
1958
+ {formatMCPUIString(strings.errorTypeLabel, { type: props.error.type })}
1878
1959
  </p>
1879
1960
  </Show>
1880
1961
  <Show when={props.error.suggestions?.length}>
1881
1962
  <div class="mt-3">
1882
- <p class="text-xs font-medium text-red-700 dark:text-red-300">Suggestions:</p>
1963
+ <p class="text-xs font-medium text-red-700 dark:text-red-300">{strings.errorSuggestions}</p>
1883
1964
  <ul class="mt-1 text-xs text-red-600 dark:text-red-400 list-disc list-inside">
1884
1965
  <For each={props.error.suggestions}>
1885
1966
  {(suggestion: string) => <li>{suggestion}</li>}
@@ -1887,9 +1968,9 @@ function ErrorCardRenderer(props: { error: any }) {
1887
1968
  </ul>
1888
1969
  </div>
1889
1970
  </Show>
1890
- <Show when={props.error.timestamp}>
1971
+ <Show when={mounted() && props.error.timestamp}>
1891
1972
  <p class="text-xs text-red-500 dark:text-red-500 mt-2">
1892
- {new Date(props.error.timestamp).toLocaleString()}
1973
+ {new Date(props.error.timestamp).toLocaleString(strings.locale)}
1893
1974
  </p>
1894
1975
  </Show>
1895
1976
  </div>
@@ -1922,6 +2003,7 @@ function isUIResource(content: any): boolean {
1922
2003
  * Handles HTML resources returned by tools like ui_show_dashboard, ui_show_health
1923
2004
  */
1924
2005
  function UIResourceHtmlRenderer(props: { resource: any }) {
2006
+ const strings = useMCPUIStrings()
1925
2007
  const htmlContent = () => {
1926
2008
  if (props.resource.content?.htmlString) {
1927
2009
  return sanitizeHtml(String(props.resource.content.htmlString), SANITIZE_PROFILES.resource)
@@ -1930,7 +2012,7 @@ function UIResourceHtmlRenderer(props: { resource: any }) {
1930
2012
  }
1931
2013
 
1932
2014
  const resourceTitle = () => {
1933
- return props.resource.metadata?.title || props.resource.uri?.replace('ui://deposium/', '') || 'Resource'
2015
+ return props.resource.metadata?.title || props.resource.uri?.replace('ui://deposium/', '') || strings.resourceTitle
1934
2016
  }
1935
2017
 
1936
2018
  return (