@arqel-dev/ai 0.15.0 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { useArqelTranslations, useArqelLocale } from '@arqel-dev/react/utils';
1
2
  import { Label, Button, Card, CardContent, Badge, Alert, AlertDescription, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, Textarea } from '@arqel-dev/ui';
2
3
  import { useId, useState, useCallback, useMemo } from 'react';
3
4
  import { jsxs, jsx } from 'react/jsx-runtime';
@@ -82,9 +83,11 @@ function AiExtractInput(props) {
82
83
  csrfToken,
83
84
  onPopulateField
84
85
  } = props;
86
+ const t = useArqelTranslations();
85
87
  const sourceField = fieldProps?.sourceField ?? "";
86
88
  const targetFields = fieldProps?.targetFields ?? [];
87
- const buttonLabel = fieldProps?.buttonLabel ?? "Extract with AI";
89
+ const serverLabel = fieldProps?.buttonLabel;
90
+ const buttonLabel = serverLabel !== void 0 && serverLabel !== "" ? serverLabel : t("arqel.ai.extract", "Extract with AI");
88
91
  const reactId = useId();
89
92
  const previewId = `arqel-ai-extract-${reactId}`;
90
93
  const [extracted, setExtracted] = useState(value);
@@ -115,7 +118,12 @@ function AiExtractInput(props) {
115
118
  const handleExtract = useCallback(async () => {
116
119
  const url = buildExtractUrl(extractUrl, resource, field);
117
120
  if (url === null) {
118
- setError("Missing extract URL: provide `extractUrl` or both `resource` and `field`.");
121
+ setError(
122
+ t(
123
+ "arqel.ai.missing_extract_url",
124
+ "Missing extract URL: provide `extractUrl` or both `resource` and `field`."
125
+ )
126
+ );
119
127
  return;
120
128
  }
121
129
  const sourceText = coerceSourceText(
@@ -144,27 +152,28 @@ function AiExtractInput(props) {
144
152
  } catch {
145
153
  message = null;
146
154
  }
147
- setError(message ?? `Extraction failed (HTTP ${String(response.status)}).`);
155
+ setError(
156
+ message ?? t("arqel.ai.extract_error_http", "Extraction failed (HTTP :status).", {
157
+ status: String(response.status)
158
+ })
159
+ );
148
160
  return;
149
161
  }
150
162
  const body = await response.json();
151
163
  if (!isPlainRecord(body.extracted)) {
152
- setError("Extraction failed: invalid response body.");
164
+ setError(t("arqel.ai.extract_error_invalid", "Extraction failed: invalid response body."));
153
165
  return;
154
166
  }
155
167
  setExtracted(body.extracted);
156
168
  } catch {
157
- setError("Extraction failed: network error.");
169
+ setError(t("arqel.ai.extract_error_network", "Extraction failed: network error."));
158
170
  } finally {
159
171
  setIsLoading(false);
160
172
  }
161
- }, [csrfToken, extractUrl, field, formData, resource, sourceField]);
173
+ }, [csrfToken, extractUrl, field, formData, resource, sourceField, t]);
162
174
  const hasExtraction = extracted !== null && Object.keys(extracted).length > 0;
163
175
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", "data-arqel-field": "aiExtract", "data-field-name": name, children: [
164
- /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsxs(Label, { className: "text-muted-foreground", children: [
165
- "Source: ",
166
- sourceField
167
- ] }) }),
176
+ /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(Label, { className: "text-muted-foreground", children: t("arqel.ai.source", "Source: :field", { field: sourceField }) }) }),
168
177
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
169
178
  /* @__PURE__ */ jsxs(
170
179
  Button,
@@ -177,7 +186,7 @@ function AiExtractInput(props) {
177
186
  disabled: isLoading,
178
187
  "aria-label": buttonLabel,
179
188
  children: [
180
- isLoading ? /* @__PURE__ */ jsx("span", { role: "status", "aria-label": "Extracting", children: /* @__PURE__ */ jsx(Spinner, {}) }) : null,
189
+ isLoading ? /* @__PURE__ */ jsx("span", { role: "status", "aria-label": t("arqel.ai.status_extracting", "Extracting"), children: /* @__PURE__ */ jsx(Spinner, {}) }) : null,
181
190
  /* @__PURE__ */ jsx("span", { children: buttonLabel })
182
191
  ]
183
192
  }
@@ -190,7 +199,7 @@ function AiExtractInput(props) {
190
199
  onClick: () => {
191
200
  applyAll(extracted);
192
201
  },
193
- children: "Apply all"
202
+ children: t("arqel.ai.apply_all", "Apply all")
194
203
  }
195
204
  ) : null
196
205
  ] }),
@@ -219,8 +228,8 @@ function AiExtractInput(props) {
219
228
  onClick: () => {
220
229
  applyOne(target, val);
221
230
  },
222
- "aria-label": `Apply ${target}`,
223
- children: "Apply"
231
+ "aria-label": t("arqel.ai.apply_field", "Apply :field", { field: target }),
232
+ children: t("arqel.ai.apply", "Apply")
224
233
  }
225
234
  )
226
235
  ] })
@@ -228,7 +237,7 @@ function AiExtractInput(props) {
228
237
  },
229
238
  target
230
239
  );
231
- }) }) }) }) : /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground italic", children: "No extraction yet \u2014 click button to start." }),
240
+ }) }) }) }) : /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground italic", children: t("arqel.ai.extract_empty", "No extraction yet \u2014 click button to start.") }),
232
241
  error !== null ? /* @__PURE__ */ jsx(Alert, { variant: "destructive", children: /* @__PURE__ */ jsx(AlertDescription, { children: error }) }) : null
233
242
  ] });
234
243
  }
@@ -269,14 +278,15 @@ function readFileAsDataUrl(file) {
269
278
  reader.readAsDataURL(file);
270
279
  });
271
280
  }
272
- function formatBytes(bytes) {
281
+ function formatBytes(bytes, locale) {
282
+ const round1 = (value) => new Intl.NumberFormat(locale, { maximumFractionDigits: 1 }).format(value);
273
283
  if (bytes >= 1048576) {
274
- return `${(bytes / 1048576).toFixed(1)} MB`;
284
+ return `${round1(bytes / 1048576)} MB`;
275
285
  }
276
286
  if (bytes >= 1024) {
277
- return `${(bytes / 1024).toFixed(1)} KB`;
287
+ return `${round1(bytes / 1024)} KB`;
278
288
  }
279
- return `${String(bytes)} B`;
289
+ return `${round1(bytes)} B`;
280
290
  }
281
291
  function Spinner2() {
282
292
  return /* @__PURE__ */ jsxs(
@@ -320,7 +330,10 @@ function AiImageInput(props) {
320
330
  const populateFields = fieldProps?.populateFields ?? {};
321
331
  const acceptedMimes = fieldProps?.acceptedMimes ?? [];
322
332
  const maxFileSize = fieldProps?.maxFileSize ?? 0;
323
- const buttonLabel = fieldProps?.buttonLabel ?? "Analyze with AI";
333
+ const t = useArqelTranslations();
334
+ const locale = useArqelLocale();
335
+ const serverLabel = fieldProps?.buttonLabel;
336
+ const buttonLabel = serverLabel !== void 0 && serverLabel !== "" ? serverLabel : t("arqel.ai.analyze", "Analyze with AI");
324
337
  const reactId = useId();
325
338
  const inputId = `arqel-ai-image-${reactId}`;
326
339
  const [file, setFile] = useState(null);
@@ -340,7 +353,12 @@ function AiImageInput(props) {
340
353
  return;
341
354
  }
342
355
  if (maxFileSize > 0 && next.size > maxFileSize) {
343
- setError(`File too large: ${formatBytes(next.size)} (max ${formatBytes(maxFileSize)}).`);
356
+ setError(
357
+ t("arqel.ai.file_too_large", "File too large: :size (max :max).", {
358
+ size: formatBytes(next.size, locale),
359
+ max: formatBytes(maxFileSize, locale)
360
+ })
361
+ );
344
362
  setFile(null);
345
363
  setPreviewUrl(null);
346
364
  return;
@@ -352,7 +370,7 @@ function AiImageInput(props) {
352
370
  setPreviewUrl(null);
353
371
  }
354
372
  },
355
- [maxFileSize]
373
+ [locale, maxFileSize, t]
356
374
  );
357
375
  const handleAnalyze = useCallback(async () => {
358
376
  if (file === null) {
@@ -360,7 +378,12 @@ function AiImageInput(props) {
360
378
  }
361
379
  const url = buildAnalyzeUrl(analyzeUrl, resource, field);
362
380
  if (url === null) {
363
- setError("Missing analyze URL: provide `analyzeUrl` or both `resource` and `field`.");
381
+ setError(
382
+ t(
383
+ "arqel.ai.missing_analyze_url",
384
+ "Missing analyze URL: provide `analyzeUrl` or both `resource` and `field`."
385
+ )
386
+ );
364
387
  return;
365
388
  }
366
389
  setIsLoading(true);
@@ -387,12 +410,16 @@ function AiImageInput(props) {
387
410
  } catch {
388
411
  message = null;
389
412
  }
390
- setError(message ?? `Analysis failed (HTTP ${String(response.status)}).`);
413
+ setError(
414
+ message ?? t("arqel.ai.analyze_error_http", "Analysis failed (HTTP :status).", {
415
+ status: String(response.status)
416
+ })
417
+ );
391
418
  return;
392
419
  }
393
420
  const body = await response.json();
394
421
  if (!isStringRecord(body.analyses)) {
395
- setError("Analysis failed: invalid response body.");
422
+ setError(t("arqel.ai.analyze_error_invalid", "Analysis failed: invalid response body."));
396
423
  return;
397
424
  }
398
425
  setResults(body.analyses);
@@ -403,11 +430,11 @@ function AiImageInput(props) {
403
430
  onChange(imageBase64);
404
431
  }
405
432
  } catch {
406
- setError("Analysis failed: network error.");
433
+ setError(t("arqel.ai.analyze_error_network", "Analysis failed: network error."));
407
434
  } finally {
408
435
  setIsLoading(false);
409
436
  }
410
- }, [analyzeUrl, csrfToken, field, file, onChange, resource]);
437
+ }, [analyzeUrl, csrfToken, field, file, onChange, resource, t]);
411
438
  const applyOne = useCallback(
412
439
  (analysisKey, val) => {
413
440
  const target = populateMapping[analysisKey];
@@ -448,7 +475,9 @@ function AiImageInput(props) {
448
475
  htmlFor: inputId,
449
476
  className: "flex items-center justify-center w-full min-h-[6rem] rounded-sm border-2 border-dashed border-border bg-muted/40 px-4 py-6 text-sm text-muted-foreground hover:bg-muted cursor-pointer transition-colors",
450
477
  children: [
451
- /* @__PURE__ */ jsx("span", { children: file !== null ? file.name : `Click or drop image (${accept !== "" ? accept : "any"})` }),
478
+ /* @__PURE__ */ jsx("span", { children: file !== null ? file.name : t("arqel.ai.image_dropzone", "Click or drop image (:accept)", {
479
+ accept: accept !== "" ? accept : t("arqel.ai.image_accept_any", "any")
480
+ }) }),
452
481
  /* @__PURE__ */ jsx(
453
482
  "input",
454
483
  {
@@ -457,7 +486,7 @@ function AiImageInput(props) {
457
486
  accept,
458
487
  onChange: handleFileChange,
459
488
  className: "sr-only",
460
- "aria-label": "Image file"
489
+ "aria-label": t("arqel.ai.image_file", "Image file")
461
490
  }
462
491
  )
463
492
  ]
@@ -467,7 +496,7 @@ function AiImageInput(props) {
467
496
  "img",
468
497
  {
469
498
  src: previewUrl,
470
- alt: "Selected preview",
499
+ alt: t("arqel.ai.selected_preview_alt", "Selected preview"),
471
500
  "data-testid": "image-preview",
472
501
  className: "max-h-64 max-w-full rounded-sm"
473
502
  }
@@ -484,7 +513,7 @@ function AiImageInput(props) {
484
513
  disabled: !canAnalyze,
485
514
  "aria-label": buttonLabel,
486
515
  children: [
487
- isLoading ? /* @__PURE__ */ jsx("span", { role: "status", "aria-label": "Analyzing", children: /* @__PURE__ */ jsx(Spinner2, {}) }) : null,
516
+ isLoading ? /* @__PURE__ */ jsx("span", { role: "status", "aria-label": t("arqel.ai.status_analyzing", "Analyzing"), children: /* @__PURE__ */ jsx(Spinner2, {}) }) : null,
488
517
  /* @__PURE__ */ jsx("span", { children: buttonLabel })
489
518
  ]
490
519
  }
@@ -497,7 +526,7 @@ function AiImageInput(props) {
497
526
  onClick: () => {
498
527
  applyAll(results);
499
528
  },
500
- children: "Apply all"
529
+ children: t("arqel.ai.apply_all", "Apply all")
501
530
  }
502
531
  ) : null
503
532
  ] }),
@@ -531,8 +560,8 @@ function AiImageInput(props) {
531
560
  onClick: () => {
532
561
  applyOne(key, val);
533
562
  },
534
- "aria-label": `Apply ${key}`,
535
- children: "Apply"
563
+ "aria-label": t("arqel.ai.apply_field", "Apply :field", { field: key }),
564
+ children: t("arqel.ai.apply", "Apply")
536
565
  }
537
566
  ) : null
538
567
  ] })
@@ -547,7 +576,7 @@ function AiImageInput(props) {
547
576
  );
548
577
  }
549
578
  var DEFAULT_BUTTON_LABEL = "Classify with AI";
550
- var NO_CONTEXT_TOOLTIP = "No context fields configured. Add `classifyFromFields` to enable AI classification.";
579
+ var NO_CONTEXT_TOOLTIP_FALLBACK = "No context fields configured. Add `classifyFromFields` to enable AI classification.";
551
580
  function buildClassifyUrl(override, resource, field) {
552
581
  if (override !== void 0 && override !== "") {
553
582
  return override;
@@ -595,9 +624,11 @@ function AiSelectInput(props) {
595
624
  classifyUrl,
596
625
  csrfToken
597
626
  } = props;
627
+ const t = useArqelTranslations();
598
628
  const options = fieldProps?.options ?? {};
599
629
  const fallbackOption = fieldProps?.fallbackOption ?? null;
600
630
  const hasContextFields = fieldProps?.hasContextFields ?? false;
631
+ const buttonLabel = t("arqel.ai.classify", DEFAULT_BUTTON_LABEL);
601
632
  const reactId = useId();
602
633
  const selectId = `arqel-ai-select-${reactId}`;
603
634
  const [internalValue, setInternalValue] = useState(value);
@@ -626,7 +657,12 @@ function AiSelectInput(props) {
626
657
  const handleClassify = useCallback(async () => {
627
658
  const url = buildClassifyUrl(classifyUrl, resource, field);
628
659
  if (url === null) {
629
- setError("Missing classify URL: provide `classifyUrl` or both `resource` and `field`.");
660
+ setError(
661
+ t(
662
+ "arqel.ai.missing_classify_url",
663
+ "Missing classify URL: provide `classifyUrl` or both `resource` and `field`."
664
+ )
665
+ );
630
666
  return;
631
667
  }
632
668
  setIsLoading(true);
@@ -644,7 +680,11 @@ function AiSelectInput(props) {
644
680
  body: JSON.stringify({ formData: formData ?? {} })
645
681
  });
646
682
  if (!response.ok) {
647
- setError(`Classification failed (HTTP ${String(response.status)}).`);
683
+ setError(
684
+ t("arqel.ai.classify_error_http", "Classification failed (HTTP :status).", {
685
+ status: String(response.status)
686
+ })
687
+ );
648
688
  return;
649
689
  }
650
690
  const body = await response.json();
@@ -659,19 +699,19 @@ function AiSelectInput(props) {
659
699
  setSuggestion("fallback");
660
700
  return;
661
701
  }
662
- setError("Could not classify.");
702
+ setError(t("arqel.ai.classify_error_none", "Could not classify."));
663
703
  } catch {
664
- setError("Classification failed: network error.");
704
+ setError(t("arqel.ai.classify_error_network", "Classification failed: network error."));
665
705
  } finally {
666
706
  setIsLoading(false);
667
707
  }
668
- }, [applyValue, classifyUrl, csrfToken, fallbackOption, field, formData, resource]);
708
+ }, [applyValue, classifyUrl, csrfToken, fallbackOption, field, formData, resource, t]);
669
709
  const buttonDisabled = isLoading || !hasContextFields;
670
- const buttonTitle = !hasContextFields ? NO_CONTEXT_TOOLTIP : void 0;
710
+ const buttonTitle = !hasContextFields ? t("arqel.ai.classify_no_context_tooltip", NO_CONTEXT_TOOLTIP_FALLBACK) : void 0;
671
711
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", "data-arqel-field": "aiSelect", "data-field-name": name, children: [
672
712
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
673
713
  /* @__PURE__ */ jsxs(Select, { value: currentValue ?? "", onValueChange: handleSelectChange, name, children: [
674
- /* @__PURE__ */ jsx(SelectTrigger, { id: selectId, className: "flex-1", children: /* @__PURE__ */ jsx(SelectValue, { placeholder: "Select..." }) }),
714
+ /* @__PURE__ */ jsx(SelectTrigger, { id: selectId, className: "flex-1", children: /* @__PURE__ */ jsx(SelectValue, { placeholder: t("arqel.ai.select_placeholder", "Select...") }) }),
675
715
  /* @__PURE__ */ jsx(SelectContent, { children: Object.entries(options).map(([key, label]) => /* @__PURE__ */ jsx(SelectItem, { value: key, children: label }, key)) })
676
716
  ] }),
677
717
  /* @__PURE__ */ jsxs(
@@ -683,11 +723,11 @@ function AiSelectInput(props) {
683
723
  void handleClassify();
684
724
  },
685
725
  disabled: buttonDisabled,
686
- "aria-label": DEFAULT_BUTTON_LABEL,
726
+ "aria-label": buttonLabel,
687
727
  ...buttonTitle !== void 0 ? { title: buttonTitle } : {},
688
728
  children: [
689
- isLoading ? /* @__PURE__ */ jsx("span", { role: "status", "aria-label": "Classifying", children: /* @__PURE__ */ jsx(Spinner3, {}) }) : null,
690
- /* @__PURE__ */ jsx("span", { children: DEFAULT_BUTTON_LABEL })
729
+ isLoading ? /* @__PURE__ */ jsx("span", { role: "status", "aria-label": t("arqel.ai.status_classifying", "Classifying"), children: /* @__PURE__ */ jsx(Spinner3, {}) }) : null,
730
+ /* @__PURE__ */ jsx("span", { children: buttonLabel })
691
731
  ]
692
732
  }
693
733
  )
@@ -698,7 +738,7 @@ function AiSelectInput(props) {
698
738
  {
699
739
  variant: "secondary",
700
740
  className: suggestion === "ai" ? "border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300" : "border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300",
701
- children: suggestion === "ai" ? "Suggested by AI" : "Used fallback"
741
+ children: suggestion === "ai" ? t("arqel.ai.suggestion_ai", "Suggested by AI") : t("arqel.ai.suggestion_fallback", "Used fallback")
702
742
  }
703
743
  ),
704
744
  /* @__PURE__ */ jsx(
@@ -709,7 +749,7 @@ function AiSelectInput(props) {
709
749
  onClick: () => {
710
750
  setSuggestion(null);
711
751
  },
712
- children: "Accept"
752
+ children: t("arqel.ai.suggestion_accept", "Accept")
713
753
  }
714
754
  ),
715
755
  /* @__PURE__ */ jsx(
@@ -720,7 +760,7 @@ function AiSelectInput(props) {
720
760
  onClick: () => {
721
761
  setSuggestion(null);
722
762
  },
723
- children: "Pick another"
763
+ children: t("arqel.ai.suggestion_pick_another", "Pick another")
724
764
  }
725
765
  )
726
766
  ] }) : null,
@@ -776,7 +816,11 @@ function AiTextInput(props) {
776
816
  generateUrl,
777
817
  csrfToken
778
818
  } = props;
779
- const buttonLabel = fieldProps?.buttonLabel ?? DEFAULT_BUTTON_LABEL2;
819
+ const t = useArqelTranslations();
820
+ const locale = useArqelLocale();
821
+ const numberFormatter = useMemo(() => new Intl.NumberFormat(locale), [locale]);
822
+ const serverLabel = fieldProps?.buttonLabel;
823
+ const buttonLabel = serverLabel !== void 0 && serverLabel !== "" ? serverLabel : t("arqel.ai.generate", DEFAULT_BUTTON_LABEL2);
780
824
  const maxLength = fieldProps?.maxLength ?? null;
781
825
  const reactId = useId();
782
826
  const textareaId = `arqel-ai-text-${reactId}`;
@@ -810,7 +854,12 @@ function AiTextInput(props) {
810
854
  const handleGenerate = useCallback(async () => {
811
855
  const url = buildGenerateUrl(generateUrl, resource, field);
812
856
  if (url === null) {
813
- setError("Missing generate URL: provide `generateUrl` or both `resource` and `field`.");
857
+ setError(
858
+ t(
859
+ "arqel.ai.missing_generate_url",
860
+ "Missing generate URL: provide `generateUrl` or both `resource` and `field`."
861
+ )
862
+ );
814
863
  return;
815
864
  }
816
865
  setIsLoading(true);
@@ -827,7 +876,11 @@ function AiTextInput(props) {
827
876
  body: JSON.stringify({ formData: formData ?? {} })
828
877
  });
829
878
  if (!response.ok) {
830
- setError(`Generation failed (HTTP ${String(response.status)}).`);
879
+ setError(
880
+ t("arqel.ai.error_http", "Generation failed (HTTP :status).", {
881
+ status: String(response.status)
882
+ })
883
+ );
831
884
  return;
832
885
  }
833
886
  const body = await response.json();
@@ -835,12 +888,12 @@ function AiTextInput(props) {
835
888
  applyGeneratedText(text);
836
889
  setHasGenerated(true);
837
890
  } catch {
838
- setError("Generation failed: network error.");
891
+ setError(t("arqel.ai.error_network", "Generation failed: network error."));
839
892
  } finally {
840
893
  setIsLoading(false);
841
894
  }
842
- }, [applyGeneratedText, csrfToken, field, formData, generateUrl, resource]);
843
- const triggerLabel = hasGenerated ? REGENERATE_LABEL : buttonLabel;
895
+ }, [applyGeneratedText, csrfToken, field, formData, generateUrl, resource, t]);
896
+ const triggerLabel = hasGenerated ? t("arqel.ai.regenerate", REGENERATE_LABEL) : buttonLabel;
844
897
  const charCount = currentValue.length;
845
898
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", "data-arqel-field": "aiText", "data-field-name": name, children: [
846
899
  /* @__PURE__ */ jsx(
@@ -855,9 +908,9 @@ function AiTextInput(props) {
855
908
  }
856
909
  ),
857
910
  maxLength !== null ? /* @__PURE__ */ jsxs("div", { className: "text-xs text-muted-foreground self-end", "aria-live": "polite", children: [
858
- charCount,
911
+ numberFormatter.format(charCount),
859
912
  " / ",
860
- maxLength
913
+ numberFormatter.format(maxLength)
861
914
  ] }) : null,
862
915
  /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2", children: /* @__PURE__ */ jsxs(
863
916
  Button,
@@ -870,7 +923,7 @@ function AiTextInput(props) {
870
923
  disabled: isLoading,
871
924
  "aria-label": triggerLabel,
872
925
  children: [
873
- isLoading ? /* @__PURE__ */ jsx("span", { role: "status", "aria-label": "Generating", children: /* @__PURE__ */ jsx(Spinner4, {}) }) : null,
926
+ isLoading ? /* @__PURE__ */ jsx("span", { role: "status", "aria-label": t("arqel.ai.status_generating", "Generating"), children: /* @__PURE__ */ jsx(Spinner4, {}) }) : null,
874
927
  /* @__PURE__ */ jsx("span", { children: triggerLabel })
875
928
  ]
876
929
  }
@@ -940,6 +993,7 @@ function AiTranslateInput(props) {
940
993
  translateUrl,
941
994
  csrfToken
942
995
  } = props;
996
+ const t = useArqelTranslations();
943
997
  const languages = useMemo(() => fieldProps?.languages ?? [], [fieldProps?.languages]);
944
998
  const defaultLanguage = fieldProps?.defaultLanguage ?? languages[0] ?? "";
945
999
  const reactId = useId();
@@ -988,7 +1042,12 @@ function AiTranslateInput(props) {
988
1042
  }
989
1043
  const url = buildTranslateUrl(translateUrl, resource, field);
990
1044
  if (url === null) {
991
- setError("Missing translate URL: provide `translateUrl` or both `resource` and `field`.");
1045
+ setError(
1046
+ t(
1047
+ "arqel.ai.missing_translate_url",
1048
+ "Missing translate URL: provide `translateUrl` or both `resource` and `field`."
1049
+ )
1050
+ );
992
1051
  return;
993
1052
  }
994
1053
  setError(null);
@@ -1009,19 +1068,25 @@ function AiTranslateInput(props) {
1009
1068
  })
1010
1069
  });
1011
1070
  if (!response.ok) {
1012
- setError(`Translation failed (HTTP ${String(response.status)}).`);
1071
+ setError(
1072
+ t("arqel.ai.translate_error_http", "Translation failed (HTTP :status).", {
1073
+ status: String(response.status)
1074
+ })
1075
+ );
1013
1076
  return;
1014
1077
  }
1015
1078
  const body = await response.json();
1016
1079
  const incoming = body.translations;
1017
1080
  if (!isStringRecord2(incoming)) {
1018
- setError("Translation failed: invalid response body.");
1081
+ setError(
1082
+ t("arqel.ai.translate_error_invalid", "Translation failed: invalid response body.")
1083
+ );
1019
1084
  return;
1020
1085
  }
1021
1086
  const merged = { ...currentValue, ...incoming };
1022
1087
  applyValue(merged);
1023
1088
  } catch {
1024
- setError("Translation failed: network error.");
1089
+ setError(t("arqel.ai.translate_error_network", "Translation failed: network error."));
1025
1090
  } finally {
1026
1091
  setLoadingFor(targetLanguages, false);
1027
1092
  }
@@ -1034,6 +1099,7 @@ function AiTranslateInput(props) {
1034
1099
  field,
1035
1100
  resource,
1036
1101
  setLoadingFor,
1102
+ t,
1037
1103
  translateUrl
1038
1104
  ]
1039
1105
  );
@@ -1062,7 +1128,7 @@ function AiTranslateInput(props) {
1062
1128
  size: "sm",
1063
1129
  onClick: handleTranslateAllMissing,
1064
1130
  disabled: missingCount === 0 || loadingLangs.size > 0,
1065
- children: "Translate all missing"
1131
+ children: t("arqel.ai.translate_all_missing", "Translate all missing")
1066
1132
  }
1067
1133
  ) }),
1068
1134
  /* @__PURE__ */ jsx("div", { role: "tablist", id: tablistId, className: "flex items-center gap-1 border-b border-border", children: languages.map((lang) => {
@@ -1085,7 +1151,7 @@ function AiTranslateInput(props) {
1085
1151
  {
1086
1152
  variant: "secondary",
1087
1153
  role: "img",
1088
- "aria-label": "Missing translation",
1154
+ "aria-label": t("arqel.ai.missing_translation", "Missing translation"),
1089
1155
  "data-testid": `missing-dot-${lang}`,
1090
1156
  className: "px-1 py-0",
1091
1157
  children: "\u2022"
@@ -1104,7 +1170,9 @@ function AiTranslateInput(props) {
1104
1170
  value: activeText,
1105
1171
  onChange: handleTextareaChange,
1106
1172
  rows: 6,
1107
- "aria-label": `Translation in ${activeLang}`
1173
+ "aria-label": t("arqel.ai.translate_textarea_aria", "Translation in :language", {
1174
+ language: activeLang
1175
+ })
1108
1176
  }
1109
1177
  ),
1110
1178
  activeLang !== defaultLanguage ? /* @__PURE__ */ jsx("div", { className: "flex items-center", children: /* @__PURE__ */ jsxs(
@@ -1115,11 +1183,10 @@ function AiTranslateInput(props) {
1115
1183
  onClick: () => handleTranslateOne(activeLang),
1116
1184
  disabled: activeIsLoading,
1117
1185
  children: [
1118
- activeIsLoading ? /* @__PURE__ */ jsx("span", { role: "status", "aria-label": "Translating", children: /* @__PURE__ */ jsx(Spinner5, {}) }) : null,
1119
- /* @__PURE__ */ jsxs("span", { children: [
1120
- "Translate from ",
1121
- defaultLanguage
1122
- ] })
1186
+ activeIsLoading ? /* @__PURE__ */ jsx("span", { role: "status", "aria-label": t("arqel.ai.status_translating", "Translating"), children: /* @__PURE__ */ jsx(Spinner5, {}) }) : null,
1187
+ /* @__PURE__ */ jsx("span", { children: t("arqel.ai.translate_from", "Translate from :language", {
1188
+ language: defaultLanguage
1189
+ }) })
1123
1190
  ]
1124
1191
  }
1125
1192
  ) }) : null