@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/register.js CHANGED
@@ -1,5 +1,6 @@
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
 
@@ -66,7 +67,11 @@ function AiTextInput(props) {
66
67
  generateUrl,
67
68
  csrfToken
68
69
  } = props;
69
- const buttonLabel = fieldProps?.buttonLabel ?? DEFAULT_BUTTON_LABEL;
70
+ const t = useArqelTranslations();
71
+ const locale = useArqelLocale();
72
+ const numberFormatter = useMemo(() => new Intl.NumberFormat(locale), [locale]);
73
+ const serverLabel = fieldProps?.buttonLabel;
74
+ const buttonLabel = serverLabel !== void 0 && serverLabel !== "" ? serverLabel : t("arqel.ai.generate", DEFAULT_BUTTON_LABEL);
70
75
  const maxLength = fieldProps?.maxLength ?? null;
71
76
  const reactId = useId();
72
77
  const textareaId = `arqel-ai-text-${reactId}`;
@@ -100,7 +105,12 @@ function AiTextInput(props) {
100
105
  const handleGenerate = useCallback(async () => {
101
106
  const url = buildGenerateUrl(generateUrl, resource, field);
102
107
  if (url === null) {
103
- setError("Missing generate URL: provide `generateUrl` or both `resource` and `field`.");
108
+ setError(
109
+ t(
110
+ "arqel.ai.missing_generate_url",
111
+ "Missing generate URL: provide `generateUrl` or both `resource` and `field`."
112
+ )
113
+ );
104
114
  return;
105
115
  }
106
116
  setIsLoading(true);
@@ -117,7 +127,11 @@ function AiTextInput(props) {
117
127
  body: JSON.stringify({ formData: formData ?? {} })
118
128
  });
119
129
  if (!response.ok) {
120
- setError(`Generation failed (HTTP ${String(response.status)}).`);
130
+ setError(
131
+ t("arqel.ai.error_http", "Generation failed (HTTP :status).", {
132
+ status: String(response.status)
133
+ })
134
+ );
121
135
  return;
122
136
  }
123
137
  const body = await response.json();
@@ -125,12 +139,12 @@ function AiTextInput(props) {
125
139
  applyGeneratedText(text);
126
140
  setHasGenerated(true);
127
141
  } catch {
128
- setError("Generation failed: network error.");
142
+ setError(t("arqel.ai.error_network", "Generation failed: network error."));
129
143
  } finally {
130
144
  setIsLoading(false);
131
145
  }
132
- }, [applyGeneratedText, csrfToken, field, formData, generateUrl, resource]);
133
- const triggerLabel = hasGenerated ? REGENERATE_LABEL : buttonLabel;
146
+ }, [applyGeneratedText, csrfToken, field, formData, generateUrl, resource, t]);
147
+ const triggerLabel = hasGenerated ? t("arqel.ai.regenerate", REGENERATE_LABEL) : buttonLabel;
134
148
  const charCount = currentValue.length;
135
149
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", "data-arqel-field": "aiText", "data-field-name": name, children: [
136
150
  /* @__PURE__ */ jsx(
@@ -145,9 +159,9 @@ function AiTextInput(props) {
145
159
  }
146
160
  ),
147
161
  maxLength !== null ? /* @__PURE__ */ jsxs("div", { className: "text-xs text-muted-foreground self-end", "aria-live": "polite", children: [
148
- charCount,
162
+ numberFormatter.format(charCount),
149
163
  " / ",
150
- maxLength
164
+ numberFormatter.format(maxLength)
151
165
  ] }) : null,
152
166
  /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2", children: /* @__PURE__ */ jsxs(
153
167
  Button,
@@ -160,7 +174,7 @@ function AiTextInput(props) {
160
174
  disabled: isLoading,
161
175
  "aria-label": triggerLabel,
162
176
  children: [
163
- isLoading ? /* @__PURE__ */ jsx("span", { role: "status", "aria-label": "Generating", children: /* @__PURE__ */ jsx(Spinner, {}) }) : null,
177
+ isLoading ? /* @__PURE__ */ jsx("span", { role: "status", "aria-label": t("arqel.ai.status_generating", "Generating"), children: /* @__PURE__ */ jsx(Spinner, {}) }) : null,
164
178
  /* @__PURE__ */ jsx("span", { children: triggerLabel })
165
179
  ]
166
180
  }
@@ -244,6 +258,7 @@ function AiTranslateInput(props) {
244
258
  translateUrl,
245
259
  csrfToken
246
260
  } = props;
261
+ const t = useArqelTranslations();
247
262
  const languages = useMemo(() => fieldProps?.languages ?? [], [fieldProps?.languages]);
248
263
  const defaultLanguage = fieldProps?.defaultLanguage ?? languages[0] ?? "";
249
264
  const reactId = useId();
@@ -292,7 +307,12 @@ function AiTranslateInput(props) {
292
307
  }
293
308
  const url = buildTranslateUrl(translateUrl, resource, field);
294
309
  if (url === null) {
295
- setError("Missing translate URL: provide `translateUrl` or both `resource` and `field`.");
310
+ setError(
311
+ t(
312
+ "arqel.ai.missing_translate_url",
313
+ "Missing translate URL: provide `translateUrl` or both `resource` and `field`."
314
+ )
315
+ );
296
316
  return;
297
317
  }
298
318
  setError(null);
@@ -313,19 +333,25 @@ function AiTranslateInput(props) {
313
333
  })
314
334
  });
315
335
  if (!response.ok) {
316
- setError(`Translation failed (HTTP ${String(response.status)}).`);
336
+ setError(
337
+ t("arqel.ai.translate_error_http", "Translation failed (HTTP :status).", {
338
+ status: String(response.status)
339
+ })
340
+ );
317
341
  return;
318
342
  }
319
343
  const body = await response.json();
320
344
  const incoming = body.translations;
321
345
  if (!isStringRecord(incoming)) {
322
- setError("Translation failed: invalid response body.");
346
+ setError(
347
+ t("arqel.ai.translate_error_invalid", "Translation failed: invalid response body.")
348
+ );
323
349
  return;
324
350
  }
325
351
  const merged = { ...currentValue, ...incoming };
326
352
  applyValue(merged);
327
353
  } catch {
328
- setError("Translation failed: network error.");
354
+ setError(t("arqel.ai.translate_error_network", "Translation failed: network error."));
329
355
  } finally {
330
356
  setLoadingFor(targetLanguages, false);
331
357
  }
@@ -338,6 +364,7 @@ function AiTranslateInput(props) {
338
364
  field,
339
365
  resource,
340
366
  setLoadingFor,
367
+ t,
341
368
  translateUrl
342
369
  ]
343
370
  );
@@ -366,7 +393,7 @@ function AiTranslateInput(props) {
366
393
  size: "sm",
367
394
  onClick: handleTranslateAllMissing,
368
395
  disabled: missingCount === 0 || loadingLangs.size > 0,
369
- children: "Translate all missing"
396
+ children: t("arqel.ai.translate_all_missing", "Translate all missing")
370
397
  }
371
398
  ) }),
372
399
  /* @__PURE__ */ jsx("div", { role: "tablist", id: tablistId, className: "flex items-center gap-1 border-b border-border", children: languages.map((lang) => {
@@ -389,7 +416,7 @@ function AiTranslateInput(props) {
389
416
  {
390
417
  variant: "secondary",
391
418
  role: "img",
392
- "aria-label": "Missing translation",
419
+ "aria-label": t("arqel.ai.missing_translation", "Missing translation"),
393
420
  "data-testid": `missing-dot-${lang}`,
394
421
  className: "px-1 py-0",
395
422
  children: "\u2022"
@@ -408,7 +435,9 @@ function AiTranslateInput(props) {
408
435
  value: activeText,
409
436
  onChange: handleTextareaChange,
410
437
  rows: 6,
411
- "aria-label": `Translation in ${activeLang}`
438
+ "aria-label": t("arqel.ai.translate_textarea_aria", "Translation in :language", {
439
+ language: activeLang
440
+ })
412
441
  }
413
442
  ),
414
443
  activeLang !== defaultLanguage ? /* @__PURE__ */ jsx("div", { className: "flex items-center", children: /* @__PURE__ */ jsxs(
@@ -419,11 +448,10 @@ function AiTranslateInput(props) {
419
448
  onClick: () => handleTranslateOne(activeLang),
420
449
  disabled: activeIsLoading,
421
450
  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
- ] })
451
+ activeIsLoading ? /* @__PURE__ */ jsx("span", { role: "status", "aria-label": t("arqel.ai.status_translating", "Translating"), children: /* @__PURE__ */ jsx(Spinner2, {}) }) : null,
452
+ /* @__PURE__ */ jsx("span", { children: t("arqel.ai.translate_from", "Translate from :language", {
453
+ language: defaultLanguage
454
+ }) })
427
455
  ]
428
456
  }
429
457
  ) }) : null
@@ -491,9 +519,11 @@ function AiSelectInput(props) {
491
519
  classifyUrl,
492
520
  csrfToken
493
521
  } = props;
522
+ const t = useArqelTranslations();
494
523
  const options = fieldProps?.options ?? {};
495
524
  const fallbackOption = fieldProps?.fallbackOption ?? null;
496
525
  const hasContextFields = fieldProps?.hasContextFields ?? false;
526
+ const buttonLabel = t("arqel.ai.classify", DEFAULT_BUTTON_LABEL2);
497
527
  const reactId = useId();
498
528
  const selectId = `arqel-ai-select-${reactId}`;
499
529
  const [internalValue, setInternalValue] = useState(value);
@@ -522,7 +552,12 @@ function AiSelectInput(props) {
522
552
  const handleClassify = useCallback(async () => {
523
553
  const url = buildClassifyUrl(classifyUrl, resource, field);
524
554
  if (url === null) {
525
- setError("Missing classify URL: provide `classifyUrl` or both `resource` and `field`.");
555
+ setError(
556
+ t(
557
+ "arqel.ai.missing_classify_url",
558
+ "Missing classify URL: provide `classifyUrl` or both `resource` and `field`."
559
+ )
560
+ );
526
561
  return;
527
562
  }
528
563
  setIsLoading(true);
@@ -540,7 +575,11 @@ function AiSelectInput(props) {
540
575
  body: JSON.stringify({ formData: formData ?? {} })
541
576
  });
542
577
  if (!response.ok) {
543
- setError(`Classification failed (HTTP ${String(response.status)}).`);
578
+ setError(
579
+ t("arqel.ai.classify_error_http", "Classification failed (HTTP :status).", {
580
+ status: String(response.status)
581
+ })
582
+ );
544
583
  return;
545
584
  }
546
585
  const body = await response.json();
@@ -555,19 +594,19 @@ function AiSelectInput(props) {
555
594
  setSuggestion("fallback");
556
595
  return;
557
596
  }
558
- setError("Could not classify.");
597
+ setError(t("arqel.ai.classify_error_none", "Could not classify."));
559
598
  } catch {
560
- setError("Classification failed: network error.");
599
+ setError(t("arqel.ai.classify_error_network", "Classification failed: network error."));
561
600
  } finally {
562
601
  setIsLoading(false);
563
602
  }
564
- }, [applyValue, classifyUrl, csrfToken, fallbackOption, field, formData, resource]);
603
+ }, [applyValue, classifyUrl, csrfToken, fallbackOption, field, formData, resource, t]);
565
604
  const buttonDisabled = isLoading || !hasContextFields;
566
- const buttonTitle = !hasContextFields ? NO_CONTEXT_TOOLTIP : void 0;
605
+ const buttonTitle = !hasContextFields ? t("arqel.ai.classify_no_context_tooltip", NO_CONTEXT_TOOLTIP_FALLBACK) : void 0;
567
606
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", "data-arqel-field": "aiSelect", "data-field-name": name, children: [
568
607
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
569
608
  /* @__PURE__ */ jsxs(Select, { value: currentValue ?? "", onValueChange: handleSelectChange, name, children: [
570
- /* @__PURE__ */ jsx(SelectTrigger, { id: selectId, className: "flex-1", children: /* @__PURE__ */ jsx(SelectValue, { placeholder: "Select..." }) }),
609
+ /* @__PURE__ */ jsx(SelectTrigger, { id: selectId, className: "flex-1", children: /* @__PURE__ */ jsx(SelectValue, { placeholder: t("arqel.ai.select_placeholder", "Select...") }) }),
571
610
  /* @__PURE__ */ jsx(SelectContent, { children: Object.entries(options).map(([key, label]) => /* @__PURE__ */ jsx(SelectItem, { value: key, children: label }, key)) })
572
611
  ] }),
573
612
  /* @__PURE__ */ jsxs(
@@ -579,11 +618,11 @@ function AiSelectInput(props) {
579
618
  void handleClassify();
580
619
  },
581
620
  disabled: buttonDisabled,
582
- "aria-label": DEFAULT_BUTTON_LABEL2,
621
+ "aria-label": buttonLabel,
583
622
  ...buttonTitle !== void 0 ? { title: buttonTitle } : {},
584
623
  children: [
585
- isLoading ? /* @__PURE__ */ jsx("span", { role: "status", "aria-label": "Classifying", children: /* @__PURE__ */ jsx(Spinner3, {}) }) : null,
586
- /* @__PURE__ */ jsx("span", { children: DEFAULT_BUTTON_LABEL2 })
624
+ isLoading ? /* @__PURE__ */ jsx("span", { role: "status", "aria-label": t("arqel.ai.status_classifying", "Classifying"), children: /* @__PURE__ */ jsx(Spinner3, {}) }) : null,
625
+ /* @__PURE__ */ jsx("span", { children: buttonLabel })
587
626
  ]
588
627
  }
589
628
  )
@@ -594,7 +633,7 @@ function AiSelectInput(props) {
594
633
  {
595
634
  variant: "secondary",
596
635
  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"
636
+ children: suggestion === "ai" ? t("arqel.ai.suggestion_ai", "Suggested by AI") : t("arqel.ai.suggestion_fallback", "Used fallback")
598
637
  }
599
638
  ),
600
639
  /* @__PURE__ */ jsx(
@@ -605,7 +644,7 @@ function AiSelectInput(props) {
605
644
  onClick: () => {
606
645
  setSuggestion(null);
607
646
  },
608
- children: "Accept"
647
+ children: t("arqel.ai.suggestion_accept", "Accept")
609
648
  }
610
649
  ),
611
650
  /* @__PURE__ */ jsx(
@@ -616,18 +655,18 @@ function AiSelectInput(props) {
616
655
  onClick: () => {
617
656
  setSuggestion(null);
618
657
  },
619
- children: "Pick another"
658
+ children: t("arqel.ai.suggestion_pick_another", "Pick another")
620
659
  }
621
660
  )
622
661
  ] }) : null,
623
662
  error !== null ? /* @__PURE__ */ jsx(Alert, { variant: "destructive", children: /* @__PURE__ */ jsx(AlertDescription, { children: error }) }) : null
624
663
  ] });
625
664
  }
626
- var DEFAULT_BUTTON_LABEL2, NO_CONTEXT_TOOLTIP, AiSelectInput_default;
665
+ var DEFAULT_BUTTON_LABEL2, NO_CONTEXT_TOOLTIP_FALLBACK, AiSelectInput_default;
627
666
  var init_AiSelectInput = __esm({
628
667
  "src/AiSelectInput.tsx"() {
629
668
  DEFAULT_BUTTON_LABEL2 = "Classify with AI";
630
- NO_CONTEXT_TOOLTIP = "No context fields configured. Add `classifyFromFields` to enable AI classification.";
669
+ NO_CONTEXT_TOOLTIP_FALLBACK = "No context fields configured. Add `classifyFromFields` to enable AI classification.";
631
670
  AiSelectInput_default = AiSelectInput;
632
671
  }
633
672
  });
@@ -717,9 +756,11 @@ function AiExtractInput(props) {
717
756
  csrfToken,
718
757
  onPopulateField
719
758
  } = props;
759
+ const t = useArqelTranslations();
720
760
  const sourceField = fieldProps?.sourceField ?? "";
721
761
  const targetFields = fieldProps?.targetFields ?? [];
722
- const buttonLabel = fieldProps?.buttonLabel ?? "Extract with AI";
762
+ const serverLabel = fieldProps?.buttonLabel;
763
+ const buttonLabel = serverLabel !== void 0 && serverLabel !== "" ? serverLabel : t("arqel.ai.extract", "Extract with AI");
723
764
  const reactId = useId();
724
765
  const previewId = `arqel-ai-extract-${reactId}`;
725
766
  const [extracted, setExtracted] = useState(value);
@@ -750,7 +791,12 @@ function AiExtractInput(props) {
750
791
  const handleExtract = useCallback(async () => {
751
792
  const url = buildExtractUrl(extractUrl, resource, field);
752
793
  if (url === null) {
753
- setError("Missing extract URL: provide `extractUrl` or both `resource` and `field`.");
794
+ setError(
795
+ t(
796
+ "arqel.ai.missing_extract_url",
797
+ "Missing extract URL: provide `extractUrl` or both `resource` and `field`."
798
+ )
799
+ );
754
800
  return;
755
801
  }
756
802
  const sourceText = coerceSourceText(
@@ -779,27 +825,28 @@ function AiExtractInput(props) {
779
825
  } catch {
780
826
  message = null;
781
827
  }
782
- setError(message ?? `Extraction failed (HTTP ${String(response.status)}).`);
828
+ setError(
829
+ message ?? t("arqel.ai.extract_error_http", "Extraction failed (HTTP :status).", {
830
+ status: String(response.status)
831
+ })
832
+ );
783
833
  return;
784
834
  }
785
835
  const body = await response.json();
786
836
  if (!isPlainRecord(body.extracted)) {
787
- setError("Extraction failed: invalid response body.");
837
+ setError(t("arqel.ai.extract_error_invalid", "Extraction failed: invalid response body."));
788
838
  return;
789
839
  }
790
840
  setExtracted(body.extracted);
791
841
  } catch {
792
- setError("Extraction failed: network error.");
842
+ setError(t("arqel.ai.extract_error_network", "Extraction failed: network error."));
793
843
  } finally {
794
844
  setIsLoading(false);
795
845
  }
796
- }, [csrfToken, extractUrl, field, formData, resource, sourceField]);
846
+ }, [csrfToken, extractUrl, field, formData, resource, sourceField, t]);
797
847
  const hasExtraction = extracted !== null && Object.keys(extracted).length > 0;
798
848
  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
- ] }) }),
849
+ /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(Label, { className: "text-muted-foreground", children: t("arqel.ai.source", "Source: :field", { field: sourceField }) }) }),
803
850
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
804
851
  /* @__PURE__ */ jsxs(
805
852
  Button,
@@ -812,7 +859,7 @@ function AiExtractInput(props) {
812
859
  disabled: isLoading,
813
860
  "aria-label": buttonLabel,
814
861
  children: [
815
- isLoading ? /* @__PURE__ */ jsx("span", { role: "status", "aria-label": "Extracting", children: /* @__PURE__ */ jsx(Spinner4, {}) }) : null,
862
+ isLoading ? /* @__PURE__ */ jsx("span", { role: "status", "aria-label": t("arqel.ai.status_extracting", "Extracting"), children: /* @__PURE__ */ jsx(Spinner4, {}) }) : null,
816
863
  /* @__PURE__ */ jsx("span", { children: buttonLabel })
817
864
  ]
818
865
  }
@@ -825,7 +872,7 @@ function AiExtractInput(props) {
825
872
  onClick: () => {
826
873
  applyAll(extracted);
827
874
  },
828
- children: "Apply all"
875
+ children: t("arqel.ai.apply_all", "Apply all")
829
876
  }
830
877
  ) : null
831
878
  ] }),
@@ -854,8 +901,8 @@ function AiExtractInput(props) {
854
901
  onClick: () => {
855
902
  applyOne(target, val);
856
903
  },
857
- "aria-label": `Apply ${target}`,
858
- children: "Apply"
904
+ "aria-label": t("arqel.ai.apply_field", "Apply :field", { field: target }),
905
+ children: t("arqel.ai.apply", "Apply")
859
906
  }
860
907
  )
861
908
  ] })
@@ -863,7 +910,7 @@ function AiExtractInput(props) {
863
910
  },
864
911
  target
865
912
  );
866
- }) }) }) }) : /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground italic", children: "No extraction yet \u2014 click button to start." }),
913
+ }) }) }) }) : /* @__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
914
  error !== null ? /* @__PURE__ */ jsx(Alert, { variant: "destructive", children: /* @__PURE__ */ jsx(AlertDescription, { children: error }) }) : null
868
915
  ] });
869
916
  }
@@ -917,14 +964,15 @@ function readFileAsDataUrl(file) {
917
964
  reader.readAsDataURL(file);
918
965
  });
919
966
  }
920
- function formatBytes(bytes) {
967
+ function formatBytes(bytes, locale) {
968
+ const round1 = (value) => new Intl.NumberFormat(locale, { maximumFractionDigits: 1 }).format(value);
921
969
  if (bytes >= 1048576) {
922
- return `${(bytes / 1048576).toFixed(1)} MB`;
970
+ return `${round1(bytes / 1048576)} MB`;
923
971
  }
924
972
  if (bytes >= 1024) {
925
- return `${(bytes / 1024).toFixed(1)} KB`;
973
+ return `${round1(bytes / 1024)} KB`;
926
974
  }
927
- return `${String(bytes)} B`;
975
+ return `${round1(bytes)} B`;
928
976
  }
929
977
  function Spinner5() {
930
978
  return /* @__PURE__ */ jsxs(
@@ -968,7 +1016,10 @@ function AiImageInput(props) {
968
1016
  const populateFields = fieldProps?.populateFields ?? {};
969
1017
  const acceptedMimes = fieldProps?.acceptedMimes ?? [];
970
1018
  const maxFileSize = fieldProps?.maxFileSize ?? 0;
971
- const buttonLabel = fieldProps?.buttonLabel ?? "Analyze with AI";
1019
+ const t = useArqelTranslations();
1020
+ const locale = useArqelLocale();
1021
+ const serverLabel = fieldProps?.buttonLabel;
1022
+ const buttonLabel = serverLabel !== void 0 && serverLabel !== "" ? serverLabel : t("arqel.ai.analyze", "Analyze with AI");
972
1023
  const reactId = useId();
973
1024
  const inputId = `arqel-ai-image-${reactId}`;
974
1025
  const [file, setFile] = useState(null);
@@ -988,7 +1039,12 @@ function AiImageInput(props) {
988
1039
  return;
989
1040
  }
990
1041
  if (maxFileSize > 0 && next.size > maxFileSize) {
991
- setError(`File too large: ${formatBytes(next.size)} (max ${formatBytes(maxFileSize)}).`);
1042
+ setError(
1043
+ t("arqel.ai.file_too_large", "File too large: :size (max :max).", {
1044
+ size: formatBytes(next.size, locale),
1045
+ max: formatBytes(maxFileSize, locale)
1046
+ })
1047
+ );
992
1048
  setFile(null);
993
1049
  setPreviewUrl(null);
994
1050
  return;
@@ -1000,7 +1056,7 @@ function AiImageInput(props) {
1000
1056
  setPreviewUrl(null);
1001
1057
  }
1002
1058
  },
1003
- [maxFileSize]
1059
+ [locale, maxFileSize, t]
1004
1060
  );
1005
1061
  const handleAnalyze = useCallback(async () => {
1006
1062
  if (file === null) {
@@ -1008,7 +1064,12 @@ function AiImageInput(props) {
1008
1064
  }
1009
1065
  const url = buildAnalyzeUrl(analyzeUrl, resource, field);
1010
1066
  if (url === null) {
1011
- setError("Missing analyze URL: provide `analyzeUrl` or both `resource` and `field`.");
1067
+ setError(
1068
+ t(
1069
+ "arqel.ai.missing_analyze_url",
1070
+ "Missing analyze URL: provide `analyzeUrl` or both `resource` and `field`."
1071
+ )
1072
+ );
1012
1073
  return;
1013
1074
  }
1014
1075
  setIsLoading(true);
@@ -1035,12 +1096,16 @@ function AiImageInput(props) {
1035
1096
  } catch {
1036
1097
  message = null;
1037
1098
  }
1038
- setError(message ?? `Analysis failed (HTTP ${String(response.status)}).`);
1099
+ setError(
1100
+ message ?? t("arqel.ai.analyze_error_http", "Analysis failed (HTTP :status).", {
1101
+ status: String(response.status)
1102
+ })
1103
+ );
1039
1104
  return;
1040
1105
  }
1041
1106
  const body = await response.json();
1042
1107
  if (!isStringRecord2(body.analyses)) {
1043
- setError("Analysis failed: invalid response body.");
1108
+ setError(t("arqel.ai.analyze_error_invalid", "Analysis failed: invalid response body."));
1044
1109
  return;
1045
1110
  }
1046
1111
  setResults(body.analyses);
@@ -1051,11 +1116,11 @@ function AiImageInput(props) {
1051
1116
  onChange(imageBase64);
1052
1117
  }
1053
1118
  } catch {
1054
- setError("Analysis failed: network error.");
1119
+ setError(t("arqel.ai.analyze_error_network", "Analysis failed: network error."));
1055
1120
  } finally {
1056
1121
  setIsLoading(false);
1057
1122
  }
1058
- }, [analyzeUrl, csrfToken, field, file, onChange, resource]);
1123
+ }, [analyzeUrl, csrfToken, field, file, onChange, resource, t]);
1059
1124
  const applyOne = useCallback(
1060
1125
  (analysisKey, val) => {
1061
1126
  const target = populateMapping[analysisKey];
@@ -1096,7 +1161,9 @@ function AiImageInput(props) {
1096
1161
  htmlFor: inputId,
1097
1162
  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
1163
  children: [
1099
- /* @__PURE__ */ jsx("span", { children: file !== null ? file.name : `Click or drop image (${accept !== "" ? accept : "any"})` }),
1164
+ /* @__PURE__ */ jsx("span", { children: file !== null ? file.name : t("arqel.ai.image_dropzone", "Click or drop image (:accept)", {
1165
+ accept: accept !== "" ? accept : t("arqel.ai.image_accept_any", "any")
1166
+ }) }),
1100
1167
  /* @__PURE__ */ jsx(
1101
1168
  "input",
1102
1169
  {
@@ -1105,7 +1172,7 @@ function AiImageInput(props) {
1105
1172
  accept,
1106
1173
  onChange: handleFileChange,
1107
1174
  className: "sr-only",
1108
- "aria-label": "Image file"
1175
+ "aria-label": t("arqel.ai.image_file", "Image file")
1109
1176
  }
1110
1177
  )
1111
1178
  ]
@@ -1115,7 +1182,7 @@ function AiImageInput(props) {
1115
1182
  "img",
1116
1183
  {
1117
1184
  src: previewUrl,
1118
- alt: "Selected preview",
1185
+ alt: t("arqel.ai.selected_preview_alt", "Selected preview"),
1119
1186
  "data-testid": "image-preview",
1120
1187
  className: "max-h-64 max-w-full rounded-sm"
1121
1188
  }
@@ -1132,7 +1199,7 @@ function AiImageInput(props) {
1132
1199
  disabled: !canAnalyze,
1133
1200
  "aria-label": buttonLabel,
1134
1201
  children: [
1135
- isLoading ? /* @__PURE__ */ jsx("span", { role: "status", "aria-label": "Analyzing", children: /* @__PURE__ */ jsx(Spinner5, {}) }) : null,
1202
+ isLoading ? /* @__PURE__ */ jsx("span", { role: "status", "aria-label": t("arqel.ai.status_analyzing", "Analyzing"), children: /* @__PURE__ */ jsx(Spinner5, {}) }) : null,
1136
1203
  /* @__PURE__ */ jsx("span", { children: buttonLabel })
1137
1204
  ]
1138
1205
  }
@@ -1145,7 +1212,7 @@ function AiImageInput(props) {
1145
1212
  onClick: () => {
1146
1213
  applyAll(results);
1147
1214
  },
1148
- children: "Apply all"
1215
+ children: t("arqel.ai.apply_all", "Apply all")
1149
1216
  }
1150
1217
  ) : null
1151
1218
  ] }),
@@ -1179,8 +1246,8 @@ function AiImageInput(props) {
1179
1246
  onClick: () => {
1180
1247
  applyOne(key, val);
1181
1248
  },
1182
- "aria-label": `Apply ${key}`,
1183
- children: "Apply"
1249
+ "aria-label": t("arqel.ai.apply_field", "Apply :field", { field: key }),
1250
+ children: t("arqel.ai.apply", "Apply")
1184
1251
  }
1185
1252
  ) : null
1186
1253
  ] })