@arqel-dev/ai 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/register.js CHANGED
@@ -1,12 +1,18 @@
1
+ import { useArqelTranslations, useArqelLocale } from '@arqel-dev/react/utils';
1
2
  import { Textarea, Button, Alert, AlertDescription, Badge, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, Label, Card, CardContent } from '@arqel-dev/ui';
2
- import { lazy, useId, useState, useCallback, useMemo } from 'react';
3
+ import { lazy, useMemo, useId, useState, useCallback } from 'react';
3
4
  import { jsx, jsxs } from 'react/jsx-runtime';
4
5
  import { registerField } from '@arqel-dev/ui/form';
5
6
 
6
7
  var __defProp = Object.defineProperty;
7
8
  var __getOwnPropNames = Object.getOwnPropertyNames;
8
- var __esm = (fn, res) => function __init() {
9
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
9
+ var __esm = (fn, res, err) => function __init() {
10
+ if (err) throw err[0];
11
+ try {
12
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
13
+ } catch (e) {
14
+ throw err = [e], e;
15
+ }
10
16
  };
11
17
  var __export = (target, all) => {
12
18
  for (var name in all)
@@ -66,7 +72,11 @@ function AiTextInput(props) {
66
72
  generateUrl,
67
73
  csrfToken
68
74
  } = props;
69
- const buttonLabel = fieldProps?.buttonLabel ?? DEFAULT_BUTTON_LABEL;
75
+ const t = useArqelTranslations();
76
+ const locale = useArqelLocale();
77
+ const numberFormatter = useMemo(() => new Intl.NumberFormat(locale), [locale]);
78
+ const serverLabel = fieldProps?.buttonLabel;
79
+ const buttonLabel = serverLabel !== void 0 && serverLabel !== "" ? serverLabel : t("arqel.ai.generate", DEFAULT_BUTTON_LABEL);
70
80
  const maxLength = fieldProps?.maxLength ?? null;
71
81
  const reactId = useId();
72
82
  const textareaId = `arqel-ai-text-${reactId}`;
@@ -100,7 +110,12 @@ function AiTextInput(props) {
100
110
  const handleGenerate = useCallback(async () => {
101
111
  const url = buildGenerateUrl(generateUrl, resource, field);
102
112
  if (url === null) {
103
- setError("Missing generate URL: provide `generateUrl` or both `resource` and `field`.");
113
+ setError(
114
+ t(
115
+ "arqel.ai.missing_generate_url",
116
+ "Missing generate URL: provide `generateUrl` or both `resource` and `field`."
117
+ )
118
+ );
104
119
  return;
105
120
  }
106
121
  setIsLoading(true);
@@ -117,7 +132,11 @@ function AiTextInput(props) {
117
132
  body: JSON.stringify({ formData: formData ?? {} })
118
133
  });
119
134
  if (!response.ok) {
120
- setError(`Generation failed (HTTP ${String(response.status)}).`);
135
+ setError(
136
+ t("arqel.ai.error_http", "Generation failed (HTTP :status).", {
137
+ status: String(response.status)
138
+ })
139
+ );
121
140
  return;
122
141
  }
123
142
  const body = await response.json();
@@ -125,12 +144,12 @@ function AiTextInput(props) {
125
144
  applyGeneratedText(text);
126
145
  setHasGenerated(true);
127
146
  } catch {
128
- setError("Generation failed: network error.");
147
+ setError(t("arqel.ai.error_network", "Generation failed: network error."));
129
148
  } finally {
130
149
  setIsLoading(false);
131
150
  }
132
- }, [applyGeneratedText, csrfToken, field, formData, generateUrl, resource]);
133
- const triggerLabel = hasGenerated ? REGENERATE_LABEL : buttonLabel;
151
+ }, [applyGeneratedText, csrfToken, field, formData, generateUrl, resource, t]);
152
+ const triggerLabel = hasGenerated ? t("arqel.ai.regenerate", REGENERATE_LABEL) : buttonLabel;
134
153
  const charCount = currentValue.length;
135
154
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", "data-arqel-field": "aiText", "data-field-name": name, children: [
136
155
  /* @__PURE__ */ jsx(
@@ -145,9 +164,9 @@ function AiTextInput(props) {
145
164
  }
146
165
  ),
147
166
  maxLength !== null ? /* @__PURE__ */ jsxs("div", { className: "text-xs text-muted-foreground self-end", "aria-live": "polite", children: [
148
- charCount,
167
+ numberFormatter.format(charCount),
149
168
  " / ",
150
- maxLength
169
+ numberFormatter.format(maxLength)
151
170
  ] }) : null,
152
171
  /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2", children: /* @__PURE__ */ jsxs(
153
172
  Button,
@@ -160,7 +179,7 @@ function AiTextInput(props) {
160
179
  disabled: isLoading,
161
180
  "aria-label": triggerLabel,
162
181
  children: [
163
- isLoading ? /* @__PURE__ */ jsx("span", { role: "status", "aria-label": "Generating", children: /* @__PURE__ */ jsx(Spinner, {}) }) : null,
182
+ isLoading ? /* @__PURE__ */ jsx("span", { role: "status", "aria-label": t("arqel.ai.status_generating", "Generating"), children: /* @__PURE__ */ jsx(Spinner, {}) }) : null,
164
183
  /* @__PURE__ */ jsx("span", { children: triggerLabel })
165
184
  ]
166
185
  }
@@ -244,6 +263,7 @@ function AiTranslateInput(props) {
244
263
  translateUrl,
245
264
  csrfToken
246
265
  } = props;
266
+ const t = useArqelTranslations();
247
267
  const languages = useMemo(() => fieldProps?.languages ?? [], [fieldProps?.languages]);
248
268
  const defaultLanguage = fieldProps?.defaultLanguage ?? languages[0] ?? "";
249
269
  const reactId = useId();
@@ -292,7 +312,12 @@ function AiTranslateInput(props) {
292
312
  }
293
313
  const url = buildTranslateUrl(translateUrl, resource, field);
294
314
  if (url === null) {
295
- setError("Missing translate URL: provide `translateUrl` or both `resource` and `field`.");
315
+ setError(
316
+ t(
317
+ "arqel.ai.missing_translate_url",
318
+ "Missing translate URL: provide `translateUrl` or both `resource` and `field`."
319
+ )
320
+ );
296
321
  return;
297
322
  }
298
323
  setError(null);
@@ -313,19 +338,25 @@ function AiTranslateInput(props) {
313
338
  })
314
339
  });
315
340
  if (!response.ok) {
316
- setError(`Translation failed (HTTP ${String(response.status)}).`);
341
+ setError(
342
+ t("arqel.ai.translate_error_http", "Translation failed (HTTP :status).", {
343
+ status: String(response.status)
344
+ })
345
+ );
317
346
  return;
318
347
  }
319
348
  const body = await response.json();
320
349
  const incoming = body.translations;
321
350
  if (!isStringRecord(incoming)) {
322
- setError("Translation failed: invalid response body.");
351
+ setError(
352
+ t("arqel.ai.translate_error_invalid", "Translation failed: invalid response body.")
353
+ );
323
354
  return;
324
355
  }
325
356
  const merged = { ...currentValue, ...incoming };
326
357
  applyValue(merged);
327
358
  } catch {
328
- setError("Translation failed: network error.");
359
+ setError(t("arqel.ai.translate_error_network", "Translation failed: network error."));
329
360
  } finally {
330
361
  setLoadingFor(targetLanguages, false);
331
362
  }
@@ -338,6 +369,7 @@ function AiTranslateInput(props) {
338
369
  field,
339
370
  resource,
340
371
  setLoadingFor,
372
+ t,
341
373
  translateUrl
342
374
  ]
343
375
  );
@@ -366,7 +398,7 @@ function AiTranslateInput(props) {
366
398
  size: "sm",
367
399
  onClick: handleTranslateAllMissing,
368
400
  disabled: missingCount === 0 || loadingLangs.size > 0,
369
- children: "Translate all missing"
401
+ children: t("arqel.ai.translate_all_missing", "Translate all missing")
370
402
  }
371
403
  ) }),
372
404
  /* @__PURE__ */ jsx("div", { role: "tablist", id: tablistId, className: "flex items-center gap-1 border-b border-border", children: languages.map((lang) => {
@@ -389,7 +421,7 @@ function AiTranslateInput(props) {
389
421
  {
390
422
  variant: "secondary",
391
423
  role: "img",
392
- "aria-label": "Missing translation",
424
+ "aria-label": t("arqel.ai.missing_translation", "Missing translation"),
393
425
  "data-testid": `missing-dot-${lang}`,
394
426
  className: "px-1 py-0",
395
427
  children: "\u2022"
@@ -408,7 +440,9 @@ function AiTranslateInput(props) {
408
440
  value: activeText,
409
441
  onChange: handleTextareaChange,
410
442
  rows: 6,
411
- "aria-label": `Translation in ${activeLang}`
443
+ "aria-label": t("arqel.ai.translate_textarea_aria", "Translation in :language", {
444
+ language: activeLang
445
+ })
412
446
  }
413
447
  ),
414
448
  activeLang !== defaultLanguage ? /* @__PURE__ */ jsx("div", { className: "flex items-center", children: /* @__PURE__ */ jsxs(
@@ -419,11 +453,10 @@ function AiTranslateInput(props) {
419
453
  onClick: () => handleTranslateOne(activeLang),
420
454
  disabled: activeIsLoading,
421
455
  children: [
422
- activeIsLoading ? /* @__PURE__ */ jsx("span", { role: "status", "aria-label": "Translating", children: /* @__PURE__ */ jsx(Spinner2, {}) }) : null,
423
- /* @__PURE__ */ jsxs("span", { children: [
424
- "Translate from ",
425
- defaultLanguage
426
- ] })
456
+ activeIsLoading ? /* @__PURE__ */ jsx("span", { role: "status", "aria-label": t("arqel.ai.status_translating", "Translating"), children: /* @__PURE__ */ jsx(Spinner2, {}) }) : null,
457
+ /* @__PURE__ */ jsx("span", { children: t("arqel.ai.translate_from", "Translate from :language", {
458
+ language: defaultLanguage
459
+ }) })
427
460
  ]
428
461
  }
429
462
  ) }) : null
@@ -491,9 +524,11 @@ function AiSelectInput(props) {
491
524
  classifyUrl,
492
525
  csrfToken
493
526
  } = props;
527
+ const t = useArqelTranslations();
494
528
  const options = fieldProps?.options ?? {};
495
529
  const fallbackOption = fieldProps?.fallbackOption ?? null;
496
530
  const hasContextFields = fieldProps?.hasContextFields ?? false;
531
+ const buttonLabel = t("arqel.ai.classify", DEFAULT_BUTTON_LABEL2);
497
532
  const reactId = useId();
498
533
  const selectId = `arqel-ai-select-${reactId}`;
499
534
  const [internalValue, setInternalValue] = useState(value);
@@ -522,7 +557,12 @@ function AiSelectInput(props) {
522
557
  const handleClassify = useCallback(async () => {
523
558
  const url = buildClassifyUrl(classifyUrl, resource, field);
524
559
  if (url === null) {
525
- setError("Missing classify URL: provide `classifyUrl` or both `resource` and `field`.");
560
+ setError(
561
+ t(
562
+ "arqel.ai.missing_classify_url",
563
+ "Missing classify URL: provide `classifyUrl` or both `resource` and `field`."
564
+ )
565
+ );
526
566
  return;
527
567
  }
528
568
  setIsLoading(true);
@@ -540,7 +580,11 @@ function AiSelectInput(props) {
540
580
  body: JSON.stringify({ formData: formData ?? {} })
541
581
  });
542
582
  if (!response.ok) {
543
- setError(`Classification failed (HTTP ${String(response.status)}).`);
583
+ setError(
584
+ t("arqel.ai.classify_error_http", "Classification failed (HTTP :status).", {
585
+ status: String(response.status)
586
+ })
587
+ );
544
588
  return;
545
589
  }
546
590
  const body = await response.json();
@@ -555,19 +599,19 @@ function AiSelectInput(props) {
555
599
  setSuggestion("fallback");
556
600
  return;
557
601
  }
558
- setError("Could not classify.");
602
+ setError(t("arqel.ai.classify_error_none", "Could not classify."));
559
603
  } catch {
560
- setError("Classification failed: network error.");
604
+ setError(t("arqel.ai.classify_error_network", "Classification failed: network error."));
561
605
  } finally {
562
606
  setIsLoading(false);
563
607
  }
564
- }, [applyValue, classifyUrl, csrfToken, fallbackOption, field, formData, resource]);
608
+ }, [applyValue, classifyUrl, csrfToken, fallbackOption, field, formData, resource, t]);
565
609
  const buttonDisabled = isLoading || !hasContextFields;
566
- const buttonTitle = !hasContextFields ? NO_CONTEXT_TOOLTIP : void 0;
610
+ const buttonTitle = !hasContextFields ? t("arqel.ai.classify_no_context_tooltip", NO_CONTEXT_TOOLTIP_FALLBACK) : void 0;
567
611
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", "data-arqel-field": "aiSelect", "data-field-name": name, children: [
568
612
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
569
613
  /* @__PURE__ */ jsxs(Select, { value: currentValue ?? "", onValueChange: handleSelectChange, name, children: [
570
- /* @__PURE__ */ jsx(SelectTrigger, { id: selectId, className: "flex-1", children: /* @__PURE__ */ jsx(SelectValue, { placeholder: "Select..." }) }),
614
+ /* @__PURE__ */ jsx(SelectTrigger, { id: selectId, className: "flex-1", children: /* @__PURE__ */ jsx(SelectValue, { placeholder: t("arqel.ai.select_placeholder", "Select...") }) }),
571
615
  /* @__PURE__ */ jsx(SelectContent, { children: Object.entries(options).map(([key, label]) => /* @__PURE__ */ jsx(SelectItem, { value: key, children: label }, key)) })
572
616
  ] }),
573
617
  /* @__PURE__ */ jsxs(
@@ -579,11 +623,11 @@ function AiSelectInput(props) {
579
623
  void handleClassify();
580
624
  },
581
625
  disabled: buttonDisabled,
582
- "aria-label": DEFAULT_BUTTON_LABEL2,
626
+ "aria-label": buttonLabel,
583
627
  ...buttonTitle !== void 0 ? { title: buttonTitle } : {},
584
628
  children: [
585
- isLoading ? /* @__PURE__ */ jsx("span", { role: "status", "aria-label": "Classifying", children: /* @__PURE__ */ jsx(Spinner3, {}) }) : null,
586
- /* @__PURE__ */ jsx("span", { children: DEFAULT_BUTTON_LABEL2 })
629
+ isLoading ? /* @__PURE__ */ jsx("span", { role: "status", "aria-label": t("arqel.ai.status_classifying", "Classifying"), children: /* @__PURE__ */ jsx(Spinner3, {}) }) : null,
630
+ /* @__PURE__ */ jsx("span", { children: buttonLabel })
587
631
  ]
588
632
  }
589
633
  )
@@ -594,7 +638,7 @@ function AiSelectInput(props) {
594
638
  {
595
639
  variant: "secondary",
596
640
  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",
597
- children: suggestion === "ai" ? "Suggested by AI" : "Used fallback"
641
+ children: suggestion === "ai" ? t("arqel.ai.suggestion_ai", "Suggested by AI") : t("arqel.ai.suggestion_fallback", "Used fallback")
598
642
  }
599
643
  ),
600
644
  /* @__PURE__ */ jsx(
@@ -605,7 +649,7 @@ function AiSelectInput(props) {
605
649
  onClick: () => {
606
650
  setSuggestion(null);
607
651
  },
608
- children: "Accept"
652
+ children: t("arqel.ai.suggestion_accept", "Accept")
609
653
  }
610
654
  ),
611
655
  /* @__PURE__ */ jsx(
@@ -616,18 +660,18 @@ function AiSelectInput(props) {
616
660
  onClick: () => {
617
661
  setSuggestion(null);
618
662
  },
619
- children: "Pick another"
663
+ children: t("arqel.ai.suggestion_pick_another", "Pick another")
620
664
  }
621
665
  )
622
666
  ] }) : null,
623
667
  error !== null ? /* @__PURE__ */ jsx(Alert, { variant: "destructive", children: /* @__PURE__ */ jsx(AlertDescription, { children: error }) }) : null
624
668
  ] });
625
669
  }
626
- var DEFAULT_BUTTON_LABEL2, NO_CONTEXT_TOOLTIP, AiSelectInput_default;
670
+ var DEFAULT_BUTTON_LABEL2, NO_CONTEXT_TOOLTIP_FALLBACK, AiSelectInput_default;
627
671
  var init_AiSelectInput = __esm({
628
672
  "src/AiSelectInput.tsx"() {
629
673
  DEFAULT_BUTTON_LABEL2 = "Classify with AI";
630
- NO_CONTEXT_TOOLTIP = "No context fields configured. Add `classifyFromFields` to enable AI classification.";
674
+ NO_CONTEXT_TOOLTIP_FALLBACK = "No context fields configured. Add `classifyFromFields` to enable AI classification.";
631
675
  AiSelectInput_default = AiSelectInput;
632
676
  }
633
677
  });
@@ -717,9 +761,11 @@ function AiExtractInput(props) {
717
761
  csrfToken,
718
762
  onPopulateField
719
763
  } = props;
764
+ const t = useArqelTranslations();
720
765
  const sourceField = fieldProps?.sourceField ?? "";
721
766
  const targetFields = fieldProps?.targetFields ?? [];
722
- const buttonLabel = fieldProps?.buttonLabel ?? "Extract with AI";
767
+ const serverLabel = fieldProps?.buttonLabel;
768
+ const buttonLabel = serverLabel !== void 0 && serverLabel !== "" ? serverLabel : t("arqel.ai.extract", "Extract with AI");
723
769
  const reactId = useId();
724
770
  const previewId = `arqel-ai-extract-${reactId}`;
725
771
  const [extracted, setExtracted] = useState(value);
@@ -750,7 +796,12 @@ function AiExtractInput(props) {
750
796
  const handleExtract = useCallback(async () => {
751
797
  const url = buildExtractUrl(extractUrl, resource, field);
752
798
  if (url === null) {
753
- setError("Missing extract URL: provide `extractUrl` or both `resource` and `field`.");
799
+ setError(
800
+ t(
801
+ "arqel.ai.missing_extract_url",
802
+ "Missing extract URL: provide `extractUrl` or both `resource` and `field`."
803
+ )
804
+ );
754
805
  return;
755
806
  }
756
807
  const sourceText = coerceSourceText(
@@ -779,27 +830,28 @@ function AiExtractInput(props) {
779
830
  } catch {
780
831
  message = null;
781
832
  }
782
- setError(message ?? `Extraction failed (HTTP ${String(response.status)}).`);
833
+ setError(
834
+ message ?? t("arqel.ai.extract_error_http", "Extraction failed (HTTP :status).", {
835
+ status: String(response.status)
836
+ })
837
+ );
783
838
  return;
784
839
  }
785
840
  const body = await response.json();
786
841
  if (!isPlainRecord(body.extracted)) {
787
- setError("Extraction failed: invalid response body.");
842
+ setError(t("arqel.ai.extract_error_invalid", "Extraction failed: invalid response body."));
788
843
  return;
789
844
  }
790
845
  setExtracted(body.extracted);
791
846
  } catch {
792
- setError("Extraction failed: network error.");
847
+ setError(t("arqel.ai.extract_error_network", "Extraction failed: network error."));
793
848
  } finally {
794
849
  setIsLoading(false);
795
850
  }
796
- }, [csrfToken, extractUrl, field, formData, resource, sourceField]);
851
+ }, [csrfToken, extractUrl, field, formData, resource, sourceField, t]);
797
852
  const hasExtraction = extracted !== null && Object.keys(extracted).length > 0;
798
853
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", "data-arqel-field": "aiExtract", "data-field-name": name, children: [
799
- /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsxs(Label, { className: "text-muted-foreground", children: [
800
- "Source: ",
801
- sourceField
802
- ] }) }),
854
+ /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(Label, { className: "text-muted-foreground", children: t("arqel.ai.source", "Source: :field", { field: sourceField }) }) }),
803
855
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
804
856
  /* @__PURE__ */ jsxs(
805
857
  Button,
@@ -812,7 +864,7 @@ function AiExtractInput(props) {
812
864
  disabled: isLoading,
813
865
  "aria-label": buttonLabel,
814
866
  children: [
815
- isLoading ? /* @__PURE__ */ jsx("span", { role: "status", "aria-label": "Extracting", children: /* @__PURE__ */ jsx(Spinner4, {}) }) : null,
867
+ isLoading ? /* @__PURE__ */ jsx("span", { role: "status", "aria-label": t("arqel.ai.status_extracting", "Extracting"), children: /* @__PURE__ */ jsx(Spinner4, {}) }) : null,
816
868
  /* @__PURE__ */ jsx("span", { children: buttonLabel })
817
869
  ]
818
870
  }
@@ -825,7 +877,7 @@ function AiExtractInput(props) {
825
877
  onClick: () => {
826
878
  applyAll(extracted);
827
879
  },
828
- children: "Apply all"
880
+ children: t("arqel.ai.apply_all", "Apply all")
829
881
  }
830
882
  ) : null
831
883
  ] }),
@@ -854,8 +906,8 @@ function AiExtractInput(props) {
854
906
  onClick: () => {
855
907
  applyOne(target, val);
856
908
  },
857
- "aria-label": `Apply ${target}`,
858
- children: "Apply"
909
+ "aria-label": t("arqel.ai.apply_field", "Apply :field", { field: target }),
910
+ children: t("arqel.ai.apply", "Apply")
859
911
  }
860
912
  )
861
913
  ] })
@@ -863,7 +915,7 @@ function AiExtractInput(props) {
863
915
  },
864
916
  target
865
917
  );
866
- }) }) }) }) : /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground italic", children: "No extraction yet \u2014 click button to start." }),
918
+ }) }) }) }) : /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground italic", children: t("arqel.ai.extract_empty", "No extraction yet \u2014 click button to start.") }),
867
919
  error !== null ? /* @__PURE__ */ jsx(Alert, { variant: "destructive", children: /* @__PURE__ */ jsx(AlertDescription, { children: error }) }) : null
868
920
  ] });
869
921
  }
@@ -917,14 +969,15 @@ function readFileAsDataUrl(file) {
917
969
  reader.readAsDataURL(file);
918
970
  });
919
971
  }
920
- function formatBytes(bytes) {
972
+ function formatBytes(bytes, locale) {
973
+ const round1 = (value) => new Intl.NumberFormat(locale, { maximumFractionDigits: 1 }).format(value);
921
974
  if (bytes >= 1048576) {
922
- return `${(bytes / 1048576).toFixed(1)} MB`;
975
+ return `${round1(bytes / 1048576)} MB`;
923
976
  }
924
977
  if (bytes >= 1024) {
925
- return `${(bytes / 1024).toFixed(1)} KB`;
978
+ return `${round1(bytes / 1024)} KB`;
926
979
  }
927
- return `${String(bytes)} B`;
980
+ return `${round1(bytes)} B`;
928
981
  }
929
982
  function Spinner5() {
930
983
  return /* @__PURE__ */ jsxs(
@@ -968,7 +1021,10 @@ function AiImageInput(props) {
968
1021
  const populateFields = fieldProps?.populateFields ?? {};
969
1022
  const acceptedMimes = fieldProps?.acceptedMimes ?? [];
970
1023
  const maxFileSize = fieldProps?.maxFileSize ?? 0;
971
- const buttonLabel = fieldProps?.buttonLabel ?? "Analyze with AI";
1024
+ const t = useArqelTranslations();
1025
+ const locale = useArqelLocale();
1026
+ const serverLabel = fieldProps?.buttonLabel;
1027
+ const buttonLabel = serverLabel !== void 0 && serverLabel !== "" ? serverLabel : t("arqel.ai.analyze", "Analyze with AI");
972
1028
  const reactId = useId();
973
1029
  const inputId = `arqel-ai-image-${reactId}`;
974
1030
  const [file, setFile] = useState(null);
@@ -988,7 +1044,12 @@ function AiImageInput(props) {
988
1044
  return;
989
1045
  }
990
1046
  if (maxFileSize > 0 && next.size > maxFileSize) {
991
- setError(`File too large: ${formatBytes(next.size)} (max ${formatBytes(maxFileSize)}).`);
1047
+ setError(
1048
+ t("arqel.ai.file_too_large", "File too large: :size (max :max).", {
1049
+ size: formatBytes(next.size, locale),
1050
+ max: formatBytes(maxFileSize, locale)
1051
+ })
1052
+ );
992
1053
  setFile(null);
993
1054
  setPreviewUrl(null);
994
1055
  return;
@@ -1000,7 +1061,7 @@ function AiImageInput(props) {
1000
1061
  setPreviewUrl(null);
1001
1062
  }
1002
1063
  },
1003
- [maxFileSize]
1064
+ [locale, maxFileSize, t]
1004
1065
  );
1005
1066
  const handleAnalyze = useCallback(async () => {
1006
1067
  if (file === null) {
@@ -1008,7 +1069,12 @@ function AiImageInput(props) {
1008
1069
  }
1009
1070
  const url = buildAnalyzeUrl(analyzeUrl, resource, field);
1010
1071
  if (url === null) {
1011
- setError("Missing analyze URL: provide `analyzeUrl` or both `resource` and `field`.");
1072
+ setError(
1073
+ t(
1074
+ "arqel.ai.missing_analyze_url",
1075
+ "Missing analyze URL: provide `analyzeUrl` or both `resource` and `field`."
1076
+ )
1077
+ );
1012
1078
  return;
1013
1079
  }
1014
1080
  setIsLoading(true);
@@ -1035,12 +1101,16 @@ function AiImageInput(props) {
1035
1101
  } catch {
1036
1102
  message = null;
1037
1103
  }
1038
- setError(message ?? `Analysis failed (HTTP ${String(response.status)}).`);
1104
+ setError(
1105
+ message ?? t("arqel.ai.analyze_error_http", "Analysis failed (HTTP :status).", {
1106
+ status: String(response.status)
1107
+ })
1108
+ );
1039
1109
  return;
1040
1110
  }
1041
1111
  const body = await response.json();
1042
1112
  if (!isStringRecord2(body.analyses)) {
1043
- setError("Analysis failed: invalid response body.");
1113
+ setError(t("arqel.ai.analyze_error_invalid", "Analysis failed: invalid response body."));
1044
1114
  return;
1045
1115
  }
1046
1116
  setResults(body.analyses);
@@ -1051,11 +1121,11 @@ function AiImageInput(props) {
1051
1121
  onChange(imageBase64);
1052
1122
  }
1053
1123
  } catch {
1054
- setError("Analysis failed: network error.");
1124
+ setError(t("arqel.ai.analyze_error_network", "Analysis failed: network error."));
1055
1125
  } finally {
1056
1126
  setIsLoading(false);
1057
1127
  }
1058
- }, [analyzeUrl, csrfToken, field, file, onChange, resource]);
1128
+ }, [analyzeUrl, csrfToken, field, file, onChange, resource, t]);
1059
1129
  const applyOne = useCallback(
1060
1130
  (analysisKey, val) => {
1061
1131
  const target = populateMapping[analysisKey];
@@ -1096,7 +1166,9 @@ function AiImageInput(props) {
1096
1166
  htmlFor: inputId,
1097
1167
  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",
1098
1168
  children: [
1099
- /* @__PURE__ */ jsx("span", { children: file !== null ? file.name : `Click or drop image (${accept !== "" ? accept : "any"})` }),
1169
+ /* @__PURE__ */ jsx("span", { children: file !== null ? file.name : t("arqel.ai.image_dropzone", "Click or drop image (:accept)", {
1170
+ accept: accept !== "" ? accept : t("arqel.ai.image_accept_any", "any")
1171
+ }) }),
1100
1172
  /* @__PURE__ */ jsx(
1101
1173
  "input",
1102
1174
  {
@@ -1105,7 +1177,7 @@ function AiImageInput(props) {
1105
1177
  accept,
1106
1178
  onChange: handleFileChange,
1107
1179
  className: "sr-only",
1108
- "aria-label": "Image file"
1180
+ "aria-label": t("arqel.ai.image_file", "Image file")
1109
1181
  }
1110
1182
  )
1111
1183
  ]
@@ -1115,7 +1187,7 @@ function AiImageInput(props) {
1115
1187
  "img",
1116
1188
  {
1117
1189
  src: previewUrl,
1118
- alt: "Selected preview",
1190
+ alt: t("arqel.ai.selected_preview_alt", "Selected preview"),
1119
1191
  "data-testid": "image-preview",
1120
1192
  className: "max-h-64 max-w-full rounded-sm"
1121
1193
  }
@@ -1132,7 +1204,7 @@ function AiImageInput(props) {
1132
1204
  disabled: !canAnalyze,
1133
1205
  "aria-label": buttonLabel,
1134
1206
  children: [
1135
- isLoading ? /* @__PURE__ */ jsx("span", { role: "status", "aria-label": "Analyzing", children: /* @__PURE__ */ jsx(Spinner5, {}) }) : null,
1207
+ isLoading ? /* @__PURE__ */ jsx("span", { role: "status", "aria-label": t("arqel.ai.status_analyzing", "Analyzing"), children: /* @__PURE__ */ jsx(Spinner5, {}) }) : null,
1136
1208
  /* @__PURE__ */ jsx("span", { children: buttonLabel })
1137
1209
  ]
1138
1210
  }
@@ -1145,7 +1217,7 @@ function AiImageInput(props) {
1145
1217
  onClick: () => {
1146
1218
  applyAll(results);
1147
1219
  },
1148
- children: "Apply all"
1220
+ children: t("arqel.ai.apply_all", "Apply all")
1149
1221
  }
1150
1222
  ) : null
1151
1223
  ] }),
@@ -1179,8 +1251,8 @@ function AiImageInput(props) {
1179
1251
  onClick: () => {
1180
1252
  applyOne(key, val);
1181
1253
  },
1182
- "aria-label": `Apply ${key}`,
1183
- children: "Apply"
1254
+ "aria-label": t("arqel.ai.apply_field", "Apply :field", { field: key }),
1255
+ children: t("arqel.ai.apply", "Apply")
1184
1256
  }
1185
1257
  ) : null
1186
1258
  ] })