@form-engine-ts/react 2.9.6 → 3.0.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/README.md CHANGED
@@ -29,7 +29,7 @@ export function ContactForm() {
29
29
  schema={schema}
30
30
  locale="en"
31
31
  translator={mockTranslator}
32
- onSubmit={async (values) => console.log(values)}
32
+ onSubmit={async (values, context) => console.log(values, context.attemptId)}
33
33
  >
34
34
  <FormRenderer />
35
35
  </FormProvider>
@@ -149,14 +149,15 @@ and `useSubmissionReceipts` loads multiple form/version receipts for list and da
149
149
 
150
150
  Receipt persistence is best-effort: `onReceiptError` observes storage failures while the successful completion screen is
151
151
  preserved. Pass an SSR-safe `createLocalStorageSubmissionAttemptStore()` as `attemptStore` to reserve an ID immediately
152
- before submission. Renderer injects it as `attemptId` and `submissionId`, retains it after a failed request, promotes it
153
- to the receipt after success, and then clears the attempt. Custom receipt stores may omit `getBatch`; the hook falls back
154
- to concurrent `get` calls.
155
-
156
- After success, completion rendering receives a snapshot of `answers`, `schema`, the optional response, and
157
- `submittedItems`. Each summary item includes the field title, raw value, formatted display value, visibility, and field
158
- metadata. Use `renderSubmittedValues` for a typed summary slot; hidden fields are omitted by default and can be included
159
- with `showHiddenFieldsInSummary`. Server validation can be returned by throwing `FormSubmissionError` with `fieldErrors`
160
- and `formError`; field messages are mapped back to the form and the first invalid control is focused. Use
161
- `submissionConfirmationRenderMode="replace"` or `"dialog"` for alternate confirmation presentations, and
162
- `fieldsClassName` or `renderFields` to control the fields wrapper.
152
+ before submission. `onSubmit(answers, context)` keeps `attemptId`, `formId`, `formVersion`, `locale`, and `submittedAt`
153
+ outside the answers object, retains the same attempt after a failed request, promotes it to the receipt after success,
154
+ and then clears the attempt. Custom receipt stores may omit `getBatch`; the hook falls back to concurrent `get` calls.
155
+
156
+ After success, completion rendering receives a typed `FormCompletionSlotProps` snapshot of `answers`, `schema`, the
157
+ optional response, and `submittedItems`. Each summary item includes the field title, raw value, formatted display value,
158
+ visibility, and field metadata. Use `renderSubmittedValues` for a typed summary slot; hidden fields are omitted by default
159
+ and can be included with `showHiddenFieldsInSummary`. Supply `messages` or `messageResolver` to localize standard buttons,
160
+ validation, retry, already-submitted, server-error, and sensitive-data confirmation UI. Server validation can be returned
161
+ by throwing `FormSubmissionError` or a payload with `fieldErrors` and `formError`; field messages are mapped back to the
162
+ form, scrolled into view, and focused. Use `submissionConfirmationRenderMode="replace"` or `"dialog"` for alternate
163
+ confirmation presentations, and `fieldsClassName` or `renderFields` to control the fields wrapper.
package/dist/index.cjs CHANGED
@@ -1245,6 +1245,11 @@ var BUILDER_DEFAULTS = {
1245
1245
  "builder.translating": "Translating\u2026",
1246
1246
  "builder.translationLocale": "Translation locale",
1247
1247
  "builder.selectLocale": "Select a locale to edit translations.",
1248
+ "builder.localization.selectLocaleToAdd": "Select a locale to add",
1249
+ "builder.localization.noLocalesConfigured": "Translations not configured",
1250
+ "builder.localization.localesConfiguredSummary": "{{count}} locales configured",
1251
+ "builder.localization.allLocalesAdded": "\u3059\u3079\u3066\u306E\u5019\u88DC\u8A00\u8A9E\u304C\u8FFD\u52A0\u6E08\u307F\u3067\u3059",
1252
+ "builder.localization.maxLocalesReached": "\u767B\u9332\u53EF\u80FD\u306A\u6700\u5927\u8A00\u8A9E\u6570\uFF08{{max}}\uFF09\u306B\u9054\u3057\u307E\u3057\u305F",
1248
1253
  "builder.translation": "{{locale}} translation",
1249
1254
  "builder.translatedFormTitle": "Translated form title",
1250
1255
  "builder.translatedFormDescription": "Translated form description",
@@ -2608,6 +2613,18 @@ function FormBuilder({
2608
2613
  // src/context.tsx
2609
2614
  var import_core3 = require("@form-engine-ts/core");
2610
2615
  var import_react3 = require("react");
2616
+
2617
+ // src/types.ts
2618
+ var FormSubmissionError = class extends Error {
2619
+ payload;
2620
+ constructor(message, payload) {
2621
+ super(message);
2622
+ this.name = "FormSubmissionError";
2623
+ this.payload = payload ?? { formError: message };
2624
+ }
2625
+ };
2626
+
2627
+ // src/context.tsx
2611
2628
  var import_jsx_runtime2 = require("react/jsx-runtime");
2612
2629
  var FormContext = (0, import_react3.createContext)(null);
2613
2630
  function issuesByField(issues) {
@@ -2615,6 +2632,18 @@ function issuesByField(issues) {
2615
2632
  for (const issue of issues) result[issue.fieldId] ??= issue;
2616
2633
  return result;
2617
2634
  }
2635
+ function defaultAttemptId2() {
2636
+ const randomUuid = globalThis.crypto?.randomUUID;
2637
+ if (typeof randomUuid === "function") return randomUuid.call(globalThis.crypto);
2638
+ return `attempt-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
2639
+ }
2640
+ function isServerErrorPayload(value) {
2641
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
2642
+ const record = value;
2643
+ const fieldErrors = record.fieldErrors;
2644
+ const formError = record.formError;
2645
+ return (Object.hasOwn(record, "fieldErrors") || Object.hasOwn(record, "formError")) && (fieldErrors === void 0 || typeof fieldErrors === "object" && fieldErrors !== null && !Array.isArray(fieldErrors) && Object.values(fieldErrors).every((message) => typeof message === "string")) && (formError === void 0 || typeof formError === "string");
2646
+ }
2618
2647
  function FormProvider({
2619
2648
  schema,
2620
2649
  locale,
@@ -2699,7 +2728,7 @@ function FormProvider({
2699
2728
  setSubmitError(null);
2700
2729
  }, [initialValues]);
2701
2730
  const submit = (0, import_react3.useCallback)(
2702
- async (beforeSubmit, prepareSubmission) => {
2731
+ async (beforeSubmit, submitContext) => {
2703
2732
  if (submissionInFlight.current) return { status: "cancelled" };
2704
2733
  const validation = (0, import_core3.validateAnswers)(validSchema, values);
2705
2734
  if (!validation.valid) {
@@ -2720,13 +2749,22 @@ function FormProvider({
2720
2749
  setSubmitStatus("idle");
2721
2750
  return { status: "cancelled" };
2722
2751
  }
2723
- const submissionValues = prepareSubmission === void 0 ? visibleValues : await prepareSubmission(visibleValues);
2724
- const response = await onSubmit(submissionValues);
2752
+ const context = submitContext ?? {
2753
+ attemptId: defaultAttemptId2(),
2754
+ formId: validSchema.id,
2755
+ formVersion: validSchema.version,
2756
+ locale,
2757
+ submittedAt: (/* @__PURE__ */ new Date()).toISOString()
2758
+ };
2759
+ const response = await onSubmit({ ...visibleValues }, context);
2725
2760
  if (resetOnSuccess) setValues({ ...initialValues });
2726
2761
  setSubmitStatus("success");
2727
2762
  return response === void 0 ? { status: "success" } : { status: "success", response };
2728
2763
  } catch (cause) {
2729
- const error = cause instanceof Error ? cause : new Error(String(cause));
2764
+ const error = isServerErrorPayload(cause) ? new FormSubmissionError(
2765
+ cause.formError ?? (cause instanceof Error ? cause.message : "Form submission failed."),
2766
+ cause
2767
+ ) : cause instanceof Error ? cause : new Error(String(cause));
2730
2768
  setSubmitError(error);
2731
2769
  setSubmitStatus("error");
2732
2770
  return { status: "error", error };
@@ -2734,7 +2772,7 @@ function FormProvider({
2734
2772
  submissionInFlight.current = false;
2735
2773
  }
2736
2774
  },
2737
- [initialValues, onSubmit, resetOnSuccess, validSchema, values]
2775
+ [initialValues, locale, onSubmit, resetOnSuccess, validSchema, values]
2738
2776
  );
2739
2777
  const translate = (0, import_react3.useCallback)(
2740
2778
  (key, params) => translator.translate(key, locale, params),
@@ -2914,18 +2952,6 @@ function useSubmissionReceipts(store, queries) {
2914
2952
  // src/renderer.tsx
2915
2953
  var import_core4 = require("@form-engine-ts/core");
2916
2954
  var import_react5 = require("react");
2917
-
2918
- // src/types.ts
2919
- var FormSubmissionError = class extends Error {
2920
- payload;
2921
- constructor(message, payload) {
2922
- super(message);
2923
- this.name = "FormSubmissionError";
2924
- this.payload = payload ?? { formError: message };
2925
- }
2926
- };
2927
-
2928
- // src/renderer.tsx
2929
2955
  var import_jsx_runtime3 = require("react/jsx-runtime");
2930
2956
  function describedBy(field, error, helpId, errorId) {
2931
2957
  const ids = [field.description === void 0 ? void 0 : helpId, error === void 0 ? void 0 : errorId].filter(
@@ -3112,7 +3138,11 @@ function DefaultField(props) {
3112
3138
  fieldId: field.id,
3113
3139
  current: typeof value === "string" ? value.length : 0,
3114
3140
  max: field.maxLength
3115
- }) : null,
3141
+ }) ?? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "fe-character-count", "aria-live": "polite", children: [
3142
+ typeof value === "string" ? value.length : 0,
3143
+ " / ",
3144
+ field.maxLength
3145
+ ] }) : null,
3116
3146
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(FieldMessage, { props })
3117
3147
  ] });
3118
3148
  }
@@ -3164,11 +3194,58 @@ function parseDraft(serialized) {
3164
3194
  return null;
3165
3195
  }
3166
3196
  }
3197
+ var DEFAULT_RENDERER_MESSAGES = {
3198
+ en: {
3199
+ submitButton: "Submit",
3200
+ submittingButton: "Submitting...",
3201
+ retryButton: "Retry",
3202
+ requiredField: "This field is required.",
3203
+ alreadySubmittedTitle: "Already Submitted",
3204
+ alreadySubmittedMessage: "Already submitted.",
3205
+ serverErrorSummary: "Submission failed. Please check your answers and try again.",
3206
+ confirmSensitiveDataTitle: "Sensitive data may be included",
3207
+ confirmSensitiveDataMessage: "The following answers may contain personal information. Continue submitting?",
3208
+ confirmButton: "Proceed",
3209
+ cancelButton: "Cancel"
3210
+ },
3211
+ ja: {
3212
+ submitButton: "\u9001\u4FE1\u3059\u308B",
3213
+ submittingButton: "\u9001\u4FE1\u4E2D...",
3214
+ retryButton: "\u518D\u9001\u4FE1\u3059\u308B",
3215
+ requiredField: "\u3053\u306E\u9805\u76EE\u306F\u5FC5\u9808\u3067\u3059",
3216
+ alreadySubmittedTitle: "\u56DE\u7B54\u6E08\u307F\u3067\u3059",
3217
+ alreadySubmittedMessage: "\u3053\u306E\u30A2\u30F3\u30B1\u30FC\u30C8\u306B\u306F\u3059\u3067\u306B\u56DE\u7B54\u3057\u3066\u3044\u307E\u3059\u3002",
3218
+ serverErrorSummary: "\u9001\u4FE1\u306B\u5931\u6557\u3057\u307E\u3057\u305F\u3002\u5185\u5BB9\u3092\u3054\u78BA\u8A8D\u306E\u4E0A\u3001\u518D\u5EA6\u304A\u8A66\u3057\u304F\u3060\u3055\u3044\u3002",
3219
+ confirmSensitiveDataTitle: "\u500B\u4EBA\u60C5\u5831\u304C\u542B\u307E\u308C\u3066\u3044\u308B\u53EF\u80FD\u6027\u304C\u3042\u308A\u307E\u3059",
3220
+ confirmSensitiveDataMessage: "\u4EE5\u4E0B\u306E\u9805\u76EE\u306B\u500B\u4EBA\u60C5\u5831\u3068\u307F\u3089\u308C\u308B\u8A18\u8FF0\u304C\u3042\u308A\u307E\u3059\u3002\u3053\u306E\u307E\u307E\u9001\u4FE1\u3057\u3066\u3082\u3088\u308D\u3057\u3044\u3067\u3059\u304B\uFF1F",
3221
+ confirmButton: "\u3053\u306E\u307E\u307E\u9001\u4FE1",
3222
+ cancelButton: "\u4FEE\u6B63\u3059\u308B"
3223
+ }
3224
+ };
3225
+ function createRendererAttemptId() {
3226
+ const randomUuid = globalThis.crypto?.randomUUID;
3227
+ if (typeof randomUuid === "function") return randomUuid.call(globalThis.crypto);
3228
+ return `attempt-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
3229
+ }
3230
+ function maskSensitiveValue(finding) {
3231
+ if (finding.maskedText !== void 0) return finding.maskedText;
3232
+ const value = finding.matchedText;
3233
+ if (value === void 0) return void 0;
3234
+ if (finding.type === "email") {
3235
+ const separator = value.indexOf("@");
3236
+ if (separator > 0) return `${value.slice(0, Math.min(2, separator))}***${value.slice(separator)}`;
3237
+ }
3238
+ if (finding.type === "phone" || finding.type === "postal_code") return "***";
3239
+ return value.length <= 2 ? "***" : `${value.slice(0, 2)}***`;
3240
+ }
3167
3241
  function ContextFormRenderer({
3168
3242
  components = {},
3169
3243
  className = "",
3170
3244
  successMessageKey,
3171
3245
  errorMessageKey,
3246
+ attemptIdFactory,
3247
+ messages = {},
3248
+ messageResolver,
3172
3249
  autoSaveKey,
3173
3250
  beforeSubmit,
3174
3251
  onDraftSave,
@@ -3197,6 +3274,7 @@ function ContextFormRenderer({
3197
3274
  const [completionData, setCompletionData] = (0, import_react5.useState)(null);
3198
3275
  const [receiptLoaded, setReceiptLoaded] = (0, import_react5.useState)(receiptStore === void 0);
3199
3276
  const rendererSubmissionInFlight = (0, import_react5.useRef)(false);
3277
+ const fallbackAttemptId = (0, import_react5.useRef)(null);
3200
3278
  const completionRef = (0, import_react5.useRef)(null);
3201
3279
  const confirmationRef = (0, import_react5.useRef)(null);
3202
3280
  const pages = form.schema.pages;
@@ -3211,6 +3289,18 @@ function ContextFormRenderer({
3211
3289
  const submitState = confirmation === null && !guardsPending ? form.submitStatus : "confirming";
3212
3290
  const interactionLocked = submitState === "confirming" || submitState === "submitting";
3213
3291
  const isReplaceMode = successRenderMode === "replace" || hideFormOnSuccess;
3292
+ const resolveMessage = (0, import_react5.useCallback)(
3293
+ (key, fallback) => {
3294
+ const defaultText = fallback ?? DEFAULT_RENDERER_MESSAGES[form.locale.toLowerCase().startsWith("ja") ? "ja" : "en"][key] ?? key;
3295
+ const configured = messages[key];
3296
+ return messageResolver?.(key, configured ?? defaultText) ?? configured ?? defaultText;
3297
+ },
3298
+ [form.locale, messageResolver, messages]
3299
+ );
3300
+ const fieldTranslate = (0, import_react5.useCallback)(
3301
+ (key, params) => key === "validation.required" && (messages.requiredField !== void 0 || messageResolver !== void 0) ? resolveMessage("requiredField") : form.translate(key, params),
3302
+ [form.translate, messageResolver, messages.requiredField, resolveMessage]
3303
+ );
3214
3304
  const focusSubmitButton = (0, import_react5.useCallback)(() => {
3215
3305
  const button = formRef.current?.querySelector(".fe-submit, button[type='submit'], button");
3216
3306
  button?.focus();
@@ -3250,6 +3340,7 @@ function ContextFormRenderer({
3250
3340
  );
3251
3341
  const control = fieldContainer?.querySelector("input, select, textarea");
3252
3342
  if (control !== void 0 && control !== null) {
3343
+ control.scrollIntoView?.({ behavior: "smooth", block: "center" });
3253
3344
  control.focus();
3254
3345
  setFocusFieldId(null);
3255
3346
  }
@@ -3268,6 +3359,23 @@ function ContextFormRenderer({
3268
3359
  event.preventDefault();
3269
3360
  setConfirmation(null);
3270
3361
  globalThis.setTimeout(focusSubmitButton, 0);
3362
+ return;
3363
+ }
3364
+ if (event.key !== "Tab") return;
3365
+ const focusable = [
3366
+ ...confirmationRef.current?.querySelectorAll(
3367
+ "button, [href], input, select, textarea, [tabindex]:not([tabindex='-1'])"
3368
+ ) ?? []
3369
+ ].filter((element) => !element.hasAttribute("disabled"));
3370
+ if (focusable.length === 0) return;
3371
+ const first = focusable[0];
3372
+ const last = focusable[focusable.length - 1];
3373
+ if (event.shiftKey && document.activeElement === first) {
3374
+ event.preventDefault();
3375
+ last?.focus();
3376
+ } else if (!event.shiftKey && document.activeElement === last) {
3377
+ event.preventDefault();
3378
+ first?.focus();
3271
3379
  }
3272
3380
  };
3273
3381
  globalThis.addEventListener("keydown", onKeyDown);
@@ -3369,17 +3477,24 @@ function ContextFormRenderer({
3369
3477
  rendererSubmissionInFlight.current = true;
3370
3478
  try {
3371
3479
  let submissionAttempt;
3372
- const result = await form.submit(
3373
- beforeSubmit,
3374
- attemptStore === void 0 ? void 0 : async (values) => {
3375
- submissionAttempt = await attemptStore.getOrCreate(form.schema.id, form.schema.version);
3376
- return {
3377
- ...values,
3378
- attemptId: submissionAttempt.attemptId,
3379
- submissionId: submissionAttempt.attemptId
3380
- };
3381
- }
3382
- );
3480
+ let attemptId = fallbackAttemptId.current;
3481
+ if (attemptStore !== void 0) {
3482
+ submissionAttempt = await attemptStore.getOrCreate(form.schema.id, form.schema.version, attemptIdFactory);
3483
+ attemptId = submissionAttempt.attemptId;
3484
+ } else if (attemptId === null) {
3485
+ attemptId = attemptIdFactory?.() ?? createRendererAttemptId();
3486
+ fallbackAttemptId.current = attemptId;
3487
+ }
3488
+ if (attemptId === null) throw new Error("Unable to create a submission attempt id.");
3489
+ const submittedAt = (/* @__PURE__ */ new Date()).toISOString();
3490
+ const submitContext = {
3491
+ attemptId,
3492
+ formId: form.schema.id,
3493
+ formVersion: form.schema.version,
3494
+ locale: form.locale,
3495
+ submittedAt
3496
+ };
3497
+ const result = await form.submit(beforeSubmit, submitContext);
3383
3498
  if (result.status === "invalid") {
3384
3499
  const invalidPageIndex = pages?.findIndex(
3385
3500
  (page) => firstInvalidFieldId === void 0 ? false : page.questionIds.includes(firstInvalidFieldId)
@@ -3392,7 +3507,7 @@ function ContextFormRenderer({
3392
3507
  if (result.error instanceof FormSubmissionError) {
3393
3508
  const fieldErrors = result.error.payload.fieldErrors ?? {};
3394
3509
  form.setServerErrors?.(fieldErrors);
3395
- const firstServerFieldId = Object.keys(fieldErrors)[0];
3510
+ const firstServerFieldId = form.schema.fields.find((field) => Object.hasOwn(fieldErrors, field.id))?.id ?? Object.keys(fieldErrors)[0];
3396
3511
  if (firstServerFieldId !== void 0) {
3397
3512
  const invalidPageIndex = pages?.findIndex((page) => page.questionIds.includes(firstServerFieldId));
3398
3513
  if (invalidPageIndex !== void 0 && invalidPageIndex >= 0) setCurrentPageIndex(invalidPageIndex);
@@ -3417,11 +3532,11 @@ function ContextFormRenderer({
3417
3532
  });
3418
3533
  if (receiptStore !== void 0) {
3419
3534
  const response = result.response;
3420
- const submissionId = response?.submissionId ?? submissionAttempt?.attemptId;
3535
+ const submissionId = response?.submissionId ?? submissionAttempt?.attemptId ?? attemptId;
3421
3536
  const storedReceipt = {
3422
3537
  formId: form.schema.id,
3423
3538
  formVersion: form.schema.version,
3424
- submittedAt: response?.submittedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
3539
+ submittedAt: response?.submittedAt ?? submittedAt,
3425
3540
  ...submissionId === void 0 ? {} : { submissionId }
3426
3541
  };
3427
3542
  try {
@@ -3440,6 +3555,7 @@ function ContextFormRenderer({
3440
3555
  } catch {
3441
3556
  }
3442
3557
  }
3558
+ if (attemptStore === void 0) fallbackAttemptId.current = null;
3443
3559
  if (autoSaveKey !== void 0 && typeof globalThis.localStorage !== "undefined") {
3444
3560
  globalThis.localStorage.removeItem(autoSaveKey);
3445
3561
  setDraftRestored(false);
@@ -3479,7 +3595,10 @@ function ContextFormRenderer({
3479
3595
  disabled: interactionLocked || submitState === "success",
3480
3596
  onSubmit: () => void submitValues()
3481
3597
  };
3482
- return slots.renderSubmitButton?.(submitButtonProps) ?? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { className: "fe-submit", type: "submit", disabled: submitButtonProps.disabled, children: form.translate(form.schema.submitLabelKey ?? "form.submit") });
3598
+ return slots.renderSubmitButton?.(submitButtonProps) ?? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("button", { className: "fe-submit", type: "submit", disabled: submitButtonProps.disabled, children: [
3599
+ submitState === "submitting" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "fe-spinner", "aria-hidden": "true" }) : null,
3600
+ submitState === "submitting" ? resolveMessage("submittingButton") : resolveMessage("submitButton", form.translate(form.schema.submitLabelKey ?? "form.submit"))
3601
+ ] });
3483
3602
  };
3484
3603
  const completionMessage = form.schema.completionMessage ?? (successMessageKey === void 0 ? "Submitted." : form.translate(successMessageKey));
3485
3604
  const activeCompletionData = completionData ?? {
@@ -3512,15 +3631,31 @@ function ContextFormRenderer({
3512
3631
  role: submissionConfirmationRenderMode === "dialog" ? void 0 : "dialog",
3513
3632
  children: slots.renderSubmissionConfirmation?.({
3514
3633
  findings: confirmation?.findings ?? [],
3515
- message: confirmation?.message ?? form.translate("form.confirmSensitiveData"),
3634
+ message: confirmation?.message ?? resolveMessage("confirmSensitiveDataMessage", form.translate("form.confirmSensitiveData")),
3516
3635
  schema: form.schema,
3517
3636
  visibleValues,
3518
3637
  onConfirm: confirmSubmission,
3519
3638
  onCancel: cancelSubmission
3520
3639
  }) ?? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
3521
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { children: confirmation?.message ?? form.translate("form.confirmSensitiveData") }),
3522
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", "data-fe-confirm": "true", onClick: confirmSubmission, children: form.translate("form.confirmSubmission") }),
3523
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", onClick: cancelSubmission, children: form.translate("form.cancelSubmission") })
3640
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h2", { children: resolveMessage("confirmSensitiveDataTitle") }),
3641
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { children: confirmation?.message ?? resolveMessage("confirmSensitiveDataMessage", form.translate("form.confirmSensitiveData")) }),
3642
+ (confirmation?.findings ?? []).length === 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("ul", { children: (confirmation?.findings ?? []).map((finding, index) => {
3643
+ const field = form.schema.fields.find((candidate) => candidate.id === finding.fieldId);
3644
+ const typeLabels = form.locale.toLowerCase().startsWith("ja") ? { email: "\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9", phone: "\u96FB\u8A71\u756A\u53F7", url: "URL", postal_code: "\u90F5\u4FBF\u756A\u53F7" } : { email: "Email address", phone: "Phone number", url: "URL", postal_code: "Postal code" };
3645
+ const typeLabel = finding.typeLabel ?? typeLabels[finding.type] ?? finding.type;
3646
+ const value = maskSensitiveValue(finding);
3647
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("li", { children: [
3648
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: finding.fieldTitle ?? field?.title ?? finding.fieldId }),
3649
+ " ",
3650
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "fe-sensitive-type", children: typeLabel }),
3651
+ value === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "fe-sensitive-value", children: [
3652
+ " ",
3653
+ value
3654
+ ] })
3655
+ ] }, `${finding.fieldId}-${finding.type}-${finding.start ?? index}`);
3656
+ }) }),
3657
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", "data-fe-confirm": "true", onClick: confirmSubmission, children: resolveMessage("confirmButton", form.translate("form.confirmSubmission")) }),
3658
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", onClick: cancelSubmission, children: resolveMessage("cancelButton", form.translate("form.cancelSubmission")) })
3524
3659
  ] })
3525
3660
  }
3526
3661
  );
@@ -3530,8 +3665,9 @@ function ContextFormRenderer({
3530
3665
  receipt,
3531
3666
  ...receiptStore === void 0 ? {} : { onReset: () => void resetReceipt() }
3532
3667
  }) ?? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { role: "status", children: [
3533
- form.translate("form.alreadySubmitted"),
3534
- receiptStore === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", onClick: () => void resetReceipt(), children: form.translate("form.submitAnother") })
3668
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h2", { children: resolveMessage("alreadySubmittedTitle") }),
3669
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { children: resolveMessage("alreadySubmittedMessage", form.translate("form.alreadySubmitted")) }),
3670
+ receiptStore === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", onClick: () => void resetReceipt(), children: resolveMessage("submitButton", form.translate("form.submitAnother")) })
3535
3671
  ] }) });
3536
3672
  }
3537
3673
  if (form.submitStatus === "success" && isReplaceMode) {
@@ -3594,7 +3730,7 @@ function ContextFormRenderer({
3594
3730
  value: form.values[field.id],
3595
3731
  error,
3596
3732
  setValue: (value) => form.setValue(field.id, value),
3597
- translate: form.translate,
3733
+ translate: fieldTranslate,
3598
3734
  inputId: `${prefix}-${field.id}`,
3599
3735
  errorId: `${prefix}-${field.id}-error`,
3600
3736
  helpId: `${prefix}-${field.id}-help`,
@@ -3661,7 +3797,13 @@ function ContextFormRenderer({
3661
3797
  ] }),
3662
3798
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "fe-status", "aria-live": "polite", children: [
3663
3799
  form.submitStatus === "success" ? completionRegion : null,
3664
- form.submitStatus === "error" && form.submitError !== null ? slots.renderSubmitError?.({ error: form.submitError, onRetry: () => void submitValues() }) ?? (form.submitError instanceof FormSubmissionError && form.submitError.payload.formError !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { role: "alert", children: form.submitError.payload.formError }) : errorMessageKey === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { role: "alert", children: form.translate(errorMessageKey) })) : null
3800
+ form.submitStatus === "error" && form.submitError !== null ? slots.renderSubmitError?.({ error: form.submitError, onRetry: () => void submitValues() }) ?? (form.submitError instanceof FormSubmissionError && form.submitError.payload.formError !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { role: "alert", children: [
3801
+ form.submitError.payload.formError,
3802
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", onClick: () => void submitValues(), children: resolveMessage("retryButton") })
3803
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { role: "alert", children: [
3804
+ errorMessageKey === void 0 ? resolveMessage("serverErrorSummary") : form.translate(errorMessageKey),
3805
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", onClick: () => void submitValues(), children: resolveMessage("retryButton") })
3806
+ ] })) : null
3665
3807
  ] })
3666
3808
  ]
3667
3809
  }
@@ -3677,7 +3819,7 @@ var RENDERER_MESSAGES = {
3677
3819
  "form.draftRestored": "Draft restored",
3678
3820
  "form.submissionBlocked": "Submission blocked because sensitive data was detected.",
3679
3821
  "form.confirmSensitiveData": "Sensitive data may be included. Confirm before submitting.",
3680
- "form.confirmSubmission": "Confirm submission",
3822
+ "form.confirmSubmission": "Proceed",
3681
3823
  "form.cancelSubmission": "Cancel",
3682
3824
  "form.yes": "Yes",
3683
3825
  "form.no": "No",
@@ -3685,9 +3827,26 @@ var RENDERER_MESSAGES = {
3685
3827
  "form.submitAnother": "Submit another response",
3686
3828
  "validation.required": "This field is required."
3687
3829
  };
3830
+ var RENDERER_MESSAGES_JA = {
3831
+ "form.submit": "\u9001\u4FE1\u3059\u308B",
3832
+ "form.back": "\u623B\u308B",
3833
+ "form.next": "\u6B21\u3078",
3834
+ "form.step": "{{current}} / {{total}}",
3835
+ "form.draftRestored": "\u4E0B\u66F8\u304D\u3092\u5FA9\u5143\u3057\u307E\u3057\u305F",
3836
+ "form.submissionBlocked": "\u500B\u4EBA\u60C5\u5831\u304C\u691C\u51FA\u3055\u308C\u305F\u305F\u3081\u9001\u4FE1\u3067\u304D\u307E\u305B\u3093\u3002",
3837
+ "form.confirmSensitiveData": "\u500B\u4EBA\u60C5\u5831\u304C\u542B\u307E\u308C\u3066\u3044\u308B\u53EF\u80FD\u6027\u304C\u3042\u308A\u307E\u3059\u3002\u9001\u4FE1\u524D\u306B\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
3838
+ "form.confirmSubmission": "\u3053\u306E\u307E\u307E\u9001\u4FE1",
3839
+ "form.cancelSubmission": "\u4FEE\u6B63\u3059\u308B",
3840
+ "form.yes": "\u306F\u3044",
3841
+ "form.no": "\u3044\u3044\u3048",
3842
+ "form.alreadySubmitted": "\u56DE\u7B54\u6E08\u307F\u3067\u3059",
3843
+ "form.submitAnother": "\u5225\u306E\u56DE\u7B54\u3092\u9001\u4FE1",
3844
+ "validation.required": "\u3053\u306E\u9805\u76EE\u306F\u5FC5\u9808\u3067\u3059"
3845
+ };
3688
3846
  var defaultRendererTranslator = {
3689
- translate(key, _locale, params = {}) {
3690
- return (RENDERER_MESSAGES[key] ?? key).replace(
3847
+ translate(key, locale, params = {}) {
3848
+ const localizedMessages = locale.toLowerCase().startsWith("ja") ? RENDERER_MESSAGES_JA : RENDERER_MESSAGES;
3849
+ return (localizedMessages[key] ?? key).replace(
3691
3850
  /\{\{(\w+)\}\}/g,
3692
3851
  (token, name) => Object.hasOwn(params, name) ? String(params[name]) : token
3693
3852
  );
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as react from 'react';
2
- import { ReactNode, ComponentType, MouseEvent } from 'react';
2
+ import { ReactNode, ComponentType, KeyboardEvent, MouseEvent } from 'react';
3
3
  import { QuestionType, FormField, ChoiceOption, FormPage, FormSchema, DisplayCondition, JsonValue, SchemaIssue, FormPolicy, FieldOption, TranslationReport, ValidationError, FormValues, TranslationAdapter, AsyncTranslationAdapter, PopulateTranslationOptions, FormValue, ValidationIssue, AnswerValidationResult, FieldType } from '@form-engine-ts/core';
4
4
  import { SensitiveDataFinding } from '@form-engine-ts/privacy';
5
5
 
@@ -139,6 +139,7 @@ interface ComponentBaseProps {
139
139
  type BuilderActionIconType = "moveUp" | "moveDown" | "delete" | "add" | "edit" | "settings" | "translate" | "close" | "dragHandle";
140
140
  interface BuilderButtonProps extends ComponentBaseProps {
141
141
  readonly onClick?: () => void;
142
+ readonly noWrap?: boolean;
142
143
  readonly variant?: "primary" | "secondary" | "danger";
143
144
  readonly children: ReactNode;
144
145
  readonly title?: string;
@@ -165,6 +166,7 @@ interface InputComponentProps extends ComponentBaseProps {
165
166
  readonly helperText?: string;
166
167
  readonly value: string;
167
168
  readonly onChange: (value: string) => void;
169
+ readonly onKeyDown?: (event: KeyboardEvent<HTMLElement>) => void;
168
170
  readonly placeholder?: string;
169
171
  readonly maxLength?: number;
170
172
  }
@@ -289,6 +291,11 @@ interface BuilderLocalizationSlotProps extends BuilderSlotBaseProps {
289
291
  readonly policy?: FormPolicy;
290
292
  readonly translationAdapterAvailable?: boolean;
291
293
  }
294
+ interface LocalizationSummaryContext {
295
+ readonly defaultLocale: string;
296
+ readonly supportedLocales: readonly string[];
297
+ readonly totalLocales: number;
298
+ }
292
299
  interface BuilderTranslationActionsSlotProps extends BuilderSlotBaseProps {
293
300
  readonly currentLocale: string;
294
301
  readonly onAutoTranslate: () => void;
@@ -338,6 +345,26 @@ interface SubmitResponse {
338
345
  readonly submissionId?: string;
339
346
  readonly submittedAt?: string;
340
347
  }
348
+ interface SubmitContext {
349
+ readonly attemptId: string;
350
+ readonly formId: string;
351
+ readonly formVersion: number;
352
+ readonly locale?: string;
353
+ readonly submittedAt: string;
354
+ }
355
+ interface FormRendererMessages {
356
+ readonly submitButton?: string;
357
+ readonly submittingButton?: string;
358
+ readonly retryButton?: string;
359
+ readonly requiredField?: string;
360
+ readonly alreadySubmittedTitle?: string;
361
+ readonly alreadySubmittedMessage?: string;
362
+ readonly serverErrorSummary?: string;
363
+ readonly confirmSensitiveDataTitle?: string;
364
+ readonly confirmSensitiveDataMessage?: string;
365
+ readonly confirmButton?: string;
366
+ readonly cancelButton?: string;
367
+ }
341
368
  interface FormSubmittedAnswerItem {
342
369
  readonly fieldId: string;
343
370
  readonly title: string;
@@ -350,7 +377,7 @@ interface FormSubmittedAnswerItem {
350
377
  interface FormCompletionSlotProps {
351
378
  readonly message?: string;
352
379
  readonly schema: FormSchema;
353
- readonly answers: Record<string, unknown>;
380
+ readonly answers: Readonly<Record<string, unknown>>;
354
381
  readonly submittedItems: readonly FormSubmittedAnswerItem[];
355
382
  readonly response?: SubmitResponse;
356
383
  readonly onReset?: () => void;
@@ -363,7 +390,7 @@ declare class FormSubmissionError extends Error {
363
390
  readonly payload: FormServerErrorPayload;
364
391
  constructor(message: string, payload?: FormServerErrorPayload);
365
392
  }
366
- type FormSubmitHandler = (answers: FormValues) => SubmitResponse | void | Promise<SubmitResponse | undefined> | Promise<void>;
393
+ type FormSubmitHandler = (answers: FormValues, context: SubmitContext) => SubmitResponse | void | Promise<SubmitResponse | undefined> | Promise<void>;
367
394
  type SubmissionGuardResult = {
368
395
  readonly status: "allow";
369
396
  } | {
@@ -427,8 +454,7 @@ interface FormRendererSlots {
427
454
  readonly renderValidationSummary?: (props: {
428
455
  readonly issues: readonly ValidationError[];
429
456
  }) => ReactNode;
430
- /** Additional completion data is supplied at runtime; use renderSubmittedValues for typed summary rendering. */
431
- readonly renderCompletion?: (props: {
457
+ readonly renderCompletion?: (props: FormCompletionSlotProps & {
432
458
  readonly message: string;
433
459
  }) => ReactNode;
434
460
  readonly renderSubmittedValues?: (props: {
@@ -507,7 +533,7 @@ interface FormContextValue {
507
533
  readonly restoreValues: (values: FormValues) => void;
508
534
  readonly validatePage: (pageIndex: number) => AnswerValidationResult;
509
535
  readonly reset: () => void;
510
- readonly submit: (beforeSubmit?: BeforeSubmit, prepareSubmission?: (values: FormValues) => FormValues | Promise<FormValues>) => Promise<SubmitResult>;
536
+ readonly submit: (beforeSubmit?: BeforeSubmit, submitContext?: SubmitContext) => Promise<SubmitResult>;
511
537
  readonly translate: (key: string, params?: Readonly<Record<string, string | number>>) => string;
512
538
  }
513
539
  interface FormProviderProps {
@@ -556,6 +582,9 @@ interface FormRendererPresentationProps extends SubmissionProtectionProps {
556
582
  readonly hideFormOnSuccess?: boolean;
557
583
  readonly successMessageKey?: string;
558
584
  readonly errorMessageKey?: string;
585
+ readonly attemptIdFactory?: () => string;
586
+ readonly messages?: Partial<FormRendererMessages>;
587
+ readonly messageResolver?: (key: keyof FormRendererMessages, defaultText: string) => string;
559
588
  readonly autoSaveKey?: string;
560
589
  readonly beforeSubmit?: BeforeSubmit;
561
590
  readonly onDraftSave?: (draft: FormValues) => void;
@@ -572,4 +601,4 @@ interface StandaloneFormRendererProps extends FormRendererPresentationProps {
572
601
  type FormRendererProps = FormRendererPresentationProps | StandaloneFormRendererProps;
573
602
  declare function FormRenderer(props: FormRendererProps): react.JSX.Element;
574
603
 
575
- export { type BeforeSubmit, type BuilderActionContext, type BuilderActionError, type BuilderActionIconType, type BuilderActionResult, type BuilderButtonProps, type BuilderCheckboxProps, type BuilderErrorMessageProps, type BuilderFactories, type BuilderFieldEditorSlotProps, type BuilderFieldsetProps, type BuilderIconButtonProps, type BuilderIdKind, type BuilderLocalizationSlotProps, type BuilderOptionEditorSlotProps, type BuilderPagesSlotProps, type BuilderPolicy, type BuilderSectionProps, type BuilderSelectOption, type BuilderSelectProps, type BuilderSlotActions, type BuilderTextAreaProps, type BuilderTextInputProps, type BuilderTextTarget, type BuilderToolbarSlotProps, type BuilderTranslationActionsSlotProps, type ComponentBaseProps, type FieldComponentProps, type FieldComponents, type FieldState, FormBuilder, type FormBuilderActions, type FormBuilderComponents, type FormBuilderFeatures, type FormBuilderOptions, type FormBuilderProps, type FormBuilderResult, type FormBuilderSectionName, type FormBuilderSlots, type FormCompletionSlotProps, type FormContextValue, type FormFieldsSlotProps, FormProvider, type FormProviderProps, FormRenderer, type FormRendererPresentationProps, type FormRendererProps, type FormRendererSlots, type FormServerErrorPayload, FormSubmissionError, type FormSubmitHandler, type FormSubmitState, type FormSubmitStatus, type FormSubmittedAnswerItem, type FormSuccessRenderMode, type IconButtonProps, type InputComponentProps, type ManualTranslationContext, type ManualTranslationTarget, type RenderSubmitButtonProps, type StandaloneFormRendererProps, type SubmissionAttempt, type SubmissionAttemptStore, type SubmissionConfirmationRenderMode, type SubmissionConfirmationSlotProps, type SubmissionGuard, type SubmissionGuardResult, type SubmissionProtectionProps, type SubmissionReceipt, type SubmissionReceiptQuery, type SubmissionReceiptStore, type SubmitResponse, type SubmitResult, type SubmitStatus, type UseSubmissionReceiptsResult, createLocalStorageSubmissionAttemptStore, createLocalStorageSubmissionReceiptStore, resolveInitialFieldType, submissionReceiptQueryKey, useField, useForm, useFormBuilder, useSubmissionReceipts };
604
+ export { type BeforeSubmit, type BuilderActionContext, type BuilderActionError, type BuilderActionIconType, type BuilderActionResult, type BuilderButtonProps, type BuilderCheckboxProps, type BuilderErrorMessageProps, type BuilderFactories, type BuilderFieldEditorSlotProps, type BuilderFieldsetProps, type BuilderIconButtonProps, type BuilderIdKind, type BuilderLocalizationSlotProps, type BuilderOptionEditorSlotProps, type BuilderPagesSlotProps, type BuilderPolicy, type BuilderSectionProps, type BuilderSelectOption, type BuilderSelectProps, type BuilderSlotActions, type BuilderTextAreaProps, type BuilderTextInputProps, type BuilderTextTarget, type BuilderToolbarSlotProps, type BuilderTranslationActionsSlotProps, type ComponentBaseProps, type FieldComponentProps, type FieldComponents, type FieldState, FormBuilder, type FormBuilderActions, type FormBuilderComponents, type FormBuilderFeatures, type FormBuilderOptions, type FormBuilderProps, type FormBuilderResult, type FormBuilderSectionName, type FormBuilderSlots, type FormCompletionSlotProps, type FormContextValue, type FormFieldsSlotProps, FormProvider, type FormProviderProps, FormRenderer, type FormRendererMessages, type FormRendererPresentationProps, type FormRendererProps, type FormRendererSlots, type FormServerErrorPayload, FormSubmissionError, type FormSubmitHandler, type FormSubmitState, type FormSubmitStatus, type FormSubmittedAnswerItem, type FormSuccessRenderMode, type IconButtonProps, type InputComponentProps, type LocalizationSummaryContext, type ManualTranslationContext, type ManualTranslationTarget, type RenderSubmitButtonProps, type StandaloneFormRendererProps, type SubmissionAttempt, type SubmissionAttemptStore, type SubmissionConfirmationRenderMode, type SubmissionConfirmationSlotProps, type SubmissionGuard, type SubmissionGuardResult, type SubmissionProtectionProps, type SubmissionReceipt, type SubmissionReceiptQuery, type SubmissionReceiptStore, type SubmitContext, type SubmitResponse, type SubmitResult, type SubmitStatus, type UseSubmissionReceiptsResult, createLocalStorageSubmissionAttemptStore, createLocalStorageSubmissionReceiptStore, resolveInitialFieldType, submissionReceiptQueryKey, useField, useForm, useFormBuilder, useSubmissionReceipts };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as react from 'react';
2
- import { ReactNode, ComponentType, MouseEvent } from 'react';
2
+ import { ReactNode, ComponentType, KeyboardEvent, MouseEvent } from 'react';
3
3
  import { QuestionType, FormField, ChoiceOption, FormPage, FormSchema, DisplayCondition, JsonValue, SchemaIssue, FormPolicy, FieldOption, TranslationReport, ValidationError, FormValues, TranslationAdapter, AsyncTranslationAdapter, PopulateTranslationOptions, FormValue, ValidationIssue, AnswerValidationResult, FieldType } from '@form-engine-ts/core';
4
4
  import { SensitiveDataFinding } from '@form-engine-ts/privacy';
5
5
 
@@ -139,6 +139,7 @@ interface ComponentBaseProps {
139
139
  type BuilderActionIconType = "moveUp" | "moveDown" | "delete" | "add" | "edit" | "settings" | "translate" | "close" | "dragHandle";
140
140
  interface BuilderButtonProps extends ComponentBaseProps {
141
141
  readonly onClick?: () => void;
142
+ readonly noWrap?: boolean;
142
143
  readonly variant?: "primary" | "secondary" | "danger";
143
144
  readonly children: ReactNode;
144
145
  readonly title?: string;
@@ -165,6 +166,7 @@ interface InputComponentProps extends ComponentBaseProps {
165
166
  readonly helperText?: string;
166
167
  readonly value: string;
167
168
  readonly onChange: (value: string) => void;
169
+ readonly onKeyDown?: (event: KeyboardEvent<HTMLElement>) => void;
168
170
  readonly placeholder?: string;
169
171
  readonly maxLength?: number;
170
172
  }
@@ -289,6 +291,11 @@ interface BuilderLocalizationSlotProps extends BuilderSlotBaseProps {
289
291
  readonly policy?: FormPolicy;
290
292
  readonly translationAdapterAvailable?: boolean;
291
293
  }
294
+ interface LocalizationSummaryContext {
295
+ readonly defaultLocale: string;
296
+ readonly supportedLocales: readonly string[];
297
+ readonly totalLocales: number;
298
+ }
292
299
  interface BuilderTranslationActionsSlotProps extends BuilderSlotBaseProps {
293
300
  readonly currentLocale: string;
294
301
  readonly onAutoTranslate: () => void;
@@ -338,6 +345,26 @@ interface SubmitResponse {
338
345
  readonly submissionId?: string;
339
346
  readonly submittedAt?: string;
340
347
  }
348
+ interface SubmitContext {
349
+ readonly attemptId: string;
350
+ readonly formId: string;
351
+ readonly formVersion: number;
352
+ readonly locale?: string;
353
+ readonly submittedAt: string;
354
+ }
355
+ interface FormRendererMessages {
356
+ readonly submitButton?: string;
357
+ readonly submittingButton?: string;
358
+ readonly retryButton?: string;
359
+ readonly requiredField?: string;
360
+ readonly alreadySubmittedTitle?: string;
361
+ readonly alreadySubmittedMessage?: string;
362
+ readonly serverErrorSummary?: string;
363
+ readonly confirmSensitiveDataTitle?: string;
364
+ readonly confirmSensitiveDataMessage?: string;
365
+ readonly confirmButton?: string;
366
+ readonly cancelButton?: string;
367
+ }
341
368
  interface FormSubmittedAnswerItem {
342
369
  readonly fieldId: string;
343
370
  readonly title: string;
@@ -350,7 +377,7 @@ interface FormSubmittedAnswerItem {
350
377
  interface FormCompletionSlotProps {
351
378
  readonly message?: string;
352
379
  readonly schema: FormSchema;
353
- readonly answers: Record<string, unknown>;
380
+ readonly answers: Readonly<Record<string, unknown>>;
354
381
  readonly submittedItems: readonly FormSubmittedAnswerItem[];
355
382
  readonly response?: SubmitResponse;
356
383
  readonly onReset?: () => void;
@@ -363,7 +390,7 @@ declare class FormSubmissionError extends Error {
363
390
  readonly payload: FormServerErrorPayload;
364
391
  constructor(message: string, payload?: FormServerErrorPayload);
365
392
  }
366
- type FormSubmitHandler = (answers: FormValues) => SubmitResponse | void | Promise<SubmitResponse | undefined> | Promise<void>;
393
+ type FormSubmitHandler = (answers: FormValues, context: SubmitContext) => SubmitResponse | void | Promise<SubmitResponse | undefined> | Promise<void>;
367
394
  type SubmissionGuardResult = {
368
395
  readonly status: "allow";
369
396
  } | {
@@ -427,8 +454,7 @@ interface FormRendererSlots {
427
454
  readonly renderValidationSummary?: (props: {
428
455
  readonly issues: readonly ValidationError[];
429
456
  }) => ReactNode;
430
- /** Additional completion data is supplied at runtime; use renderSubmittedValues for typed summary rendering. */
431
- readonly renderCompletion?: (props: {
457
+ readonly renderCompletion?: (props: FormCompletionSlotProps & {
432
458
  readonly message: string;
433
459
  }) => ReactNode;
434
460
  readonly renderSubmittedValues?: (props: {
@@ -507,7 +533,7 @@ interface FormContextValue {
507
533
  readonly restoreValues: (values: FormValues) => void;
508
534
  readonly validatePage: (pageIndex: number) => AnswerValidationResult;
509
535
  readonly reset: () => void;
510
- readonly submit: (beforeSubmit?: BeforeSubmit, prepareSubmission?: (values: FormValues) => FormValues | Promise<FormValues>) => Promise<SubmitResult>;
536
+ readonly submit: (beforeSubmit?: BeforeSubmit, submitContext?: SubmitContext) => Promise<SubmitResult>;
511
537
  readonly translate: (key: string, params?: Readonly<Record<string, string | number>>) => string;
512
538
  }
513
539
  interface FormProviderProps {
@@ -556,6 +582,9 @@ interface FormRendererPresentationProps extends SubmissionProtectionProps {
556
582
  readonly hideFormOnSuccess?: boolean;
557
583
  readonly successMessageKey?: string;
558
584
  readonly errorMessageKey?: string;
585
+ readonly attemptIdFactory?: () => string;
586
+ readonly messages?: Partial<FormRendererMessages>;
587
+ readonly messageResolver?: (key: keyof FormRendererMessages, defaultText: string) => string;
559
588
  readonly autoSaveKey?: string;
560
589
  readonly beforeSubmit?: BeforeSubmit;
561
590
  readonly onDraftSave?: (draft: FormValues) => void;
@@ -572,4 +601,4 @@ interface StandaloneFormRendererProps extends FormRendererPresentationProps {
572
601
  type FormRendererProps = FormRendererPresentationProps | StandaloneFormRendererProps;
573
602
  declare function FormRenderer(props: FormRendererProps): react.JSX.Element;
574
603
 
575
- export { type BeforeSubmit, type BuilderActionContext, type BuilderActionError, type BuilderActionIconType, type BuilderActionResult, type BuilderButtonProps, type BuilderCheckboxProps, type BuilderErrorMessageProps, type BuilderFactories, type BuilderFieldEditorSlotProps, type BuilderFieldsetProps, type BuilderIconButtonProps, type BuilderIdKind, type BuilderLocalizationSlotProps, type BuilderOptionEditorSlotProps, type BuilderPagesSlotProps, type BuilderPolicy, type BuilderSectionProps, type BuilderSelectOption, type BuilderSelectProps, type BuilderSlotActions, type BuilderTextAreaProps, type BuilderTextInputProps, type BuilderTextTarget, type BuilderToolbarSlotProps, type BuilderTranslationActionsSlotProps, type ComponentBaseProps, type FieldComponentProps, type FieldComponents, type FieldState, FormBuilder, type FormBuilderActions, type FormBuilderComponents, type FormBuilderFeatures, type FormBuilderOptions, type FormBuilderProps, type FormBuilderResult, type FormBuilderSectionName, type FormBuilderSlots, type FormCompletionSlotProps, type FormContextValue, type FormFieldsSlotProps, FormProvider, type FormProviderProps, FormRenderer, type FormRendererPresentationProps, type FormRendererProps, type FormRendererSlots, type FormServerErrorPayload, FormSubmissionError, type FormSubmitHandler, type FormSubmitState, type FormSubmitStatus, type FormSubmittedAnswerItem, type FormSuccessRenderMode, type IconButtonProps, type InputComponentProps, type ManualTranslationContext, type ManualTranslationTarget, type RenderSubmitButtonProps, type StandaloneFormRendererProps, type SubmissionAttempt, type SubmissionAttemptStore, type SubmissionConfirmationRenderMode, type SubmissionConfirmationSlotProps, type SubmissionGuard, type SubmissionGuardResult, type SubmissionProtectionProps, type SubmissionReceipt, type SubmissionReceiptQuery, type SubmissionReceiptStore, type SubmitResponse, type SubmitResult, type SubmitStatus, type UseSubmissionReceiptsResult, createLocalStorageSubmissionAttemptStore, createLocalStorageSubmissionReceiptStore, resolveInitialFieldType, submissionReceiptQueryKey, useField, useForm, useFormBuilder, useSubmissionReceipts };
604
+ export { type BeforeSubmit, type BuilderActionContext, type BuilderActionError, type BuilderActionIconType, type BuilderActionResult, type BuilderButtonProps, type BuilderCheckboxProps, type BuilderErrorMessageProps, type BuilderFactories, type BuilderFieldEditorSlotProps, type BuilderFieldsetProps, type BuilderIconButtonProps, type BuilderIdKind, type BuilderLocalizationSlotProps, type BuilderOptionEditorSlotProps, type BuilderPagesSlotProps, type BuilderPolicy, type BuilderSectionProps, type BuilderSelectOption, type BuilderSelectProps, type BuilderSlotActions, type BuilderTextAreaProps, type BuilderTextInputProps, type BuilderTextTarget, type BuilderToolbarSlotProps, type BuilderTranslationActionsSlotProps, type ComponentBaseProps, type FieldComponentProps, type FieldComponents, type FieldState, FormBuilder, type FormBuilderActions, type FormBuilderComponents, type FormBuilderFeatures, type FormBuilderOptions, type FormBuilderProps, type FormBuilderResult, type FormBuilderSectionName, type FormBuilderSlots, type FormCompletionSlotProps, type FormContextValue, type FormFieldsSlotProps, FormProvider, type FormProviderProps, FormRenderer, type FormRendererMessages, type FormRendererPresentationProps, type FormRendererProps, type FormRendererSlots, type FormServerErrorPayload, FormSubmissionError, type FormSubmitHandler, type FormSubmitState, type FormSubmitStatus, type FormSubmittedAnswerItem, type FormSuccessRenderMode, type IconButtonProps, type InputComponentProps, type LocalizationSummaryContext, type ManualTranslationContext, type ManualTranslationTarget, type RenderSubmitButtonProps, type StandaloneFormRendererProps, type SubmissionAttempt, type SubmissionAttemptStore, type SubmissionConfirmationRenderMode, type SubmissionConfirmationSlotProps, type SubmissionGuard, type SubmissionGuardResult, type SubmissionProtectionProps, type SubmissionReceipt, type SubmissionReceiptQuery, type SubmissionReceiptStore, type SubmitContext, type SubmitResponse, type SubmitResult, type SubmitStatus, type UseSubmissionReceiptsResult, createLocalStorageSubmissionAttemptStore, createLocalStorageSubmissionReceiptStore, resolveInitialFieldType, submissionReceiptQueryKey, useField, useForm, useFormBuilder, useSubmissionReceipts };
package/dist/index.js CHANGED
@@ -1214,6 +1214,11 @@ var BUILDER_DEFAULTS = {
1214
1214
  "builder.translating": "Translating\u2026",
1215
1215
  "builder.translationLocale": "Translation locale",
1216
1216
  "builder.selectLocale": "Select a locale to edit translations.",
1217
+ "builder.localization.selectLocaleToAdd": "Select a locale to add",
1218
+ "builder.localization.noLocalesConfigured": "Translations not configured",
1219
+ "builder.localization.localesConfiguredSummary": "{{count}} locales configured",
1220
+ "builder.localization.allLocalesAdded": "\u3059\u3079\u3066\u306E\u5019\u88DC\u8A00\u8A9E\u304C\u8FFD\u52A0\u6E08\u307F\u3067\u3059",
1221
+ "builder.localization.maxLocalesReached": "\u767B\u9332\u53EF\u80FD\u306A\u6700\u5927\u8A00\u8A9E\u6570\uFF08{{max}}\uFF09\u306B\u9054\u3057\u307E\u3057\u305F",
1217
1222
  "builder.translation": "{{locale}} translation",
1218
1223
  "builder.translatedFormTitle": "Translated form title",
1219
1224
  "builder.translatedFormDescription": "Translated form description",
@@ -2585,6 +2590,18 @@ import {
2585
2590
  validatePageAnswers
2586
2591
  } from "@form-engine-ts/core";
2587
2592
  import { createContext as createContext2, useCallback as useCallback2, useContext as useContext2, useEffect, useMemo as useMemo2, useRef, useState as useState2 } from "react";
2593
+
2594
+ // src/types.ts
2595
+ var FormSubmissionError = class extends Error {
2596
+ payload;
2597
+ constructor(message, payload) {
2598
+ super(message);
2599
+ this.name = "FormSubmissionError";
2600
+ this.payload = payload ?? { formError: message };
2601
+ }
2602
+ };
2603
+
2604
+ // src/context.tsx
2588
2605
  import { jsx as jsx2 } from "react/jsx-runtime";
2589
2606
  var FormContext = createContext2(null);
2590
2607
  function issuesByField(issues) {
@@ -2592,6 +2609,18 @@ function issuesByField(issues) {
2592
2609
  for (const issue of issues) result[issue.fieldId] ??= issue;
2593
2610
  return result;
2594
2611
  }
2612
+ function defaultAttemptId2() {
2613
+ const randomUuid = globalThis.crypto?.randomUUID;
2614
+ if (typeof randomUuid === "function") return randomUuid.call(globalThis.crypto);
2615
+ return `attempt-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
2616
+ }
2617
+ function isServerErrorPayload(value) {
2618
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
2619
+ const record = value;
2620
+ const fieldErrors = record.fieldErrors;
2621
+ const formError = record.formError;
2622
+ return (Object.hasOwn(record, "fieldErrors") || Object.hasOwn(record, "formError")) && (fieldErrors === void 0 || typeof fieldErrors === "object" && fieldErrors !== null && !Array.isArray(fieldErrors) && Object.values(fieldErrors).every((message) => typeof message === "string")) && (formError === void 0 || typeof formError === "string");
2623
+ }
2595
2624
  function FormProvider({
2596
2625
  schema,
2597
2626
  locale,
@@ -2676,7 +2705,7 @@ function FormProvider({
2676
2705
  setSubmitError(null);
2677
2706
  }, [initialValues]);
2678
2707
  const submit = useCallback2(
2679
- async (beforeSubmit, prepareSubmission) => {
2708
+ async (beforeSubmit, submitContext) => {
2680
2709
  if (submissionInFlight.current) return { status: "cancelled" };
2681
2710
  const validation = validateAnswers(validSchema, values);
2682
2711
  if (!validation.valid) {
@@ -2697,13 +2726,22 @@ function FormProvider({
2697
2726
  setSubmitStatus("idle");
2698
2727
  return { status: "cancelled" };
2699
2728
  }
2700
- const submissionValues = prepareSubmission === void 0 ? visibleValues : await prepareSubmission(visibleValues);
2701
- const response = await onSubmit(submissionValues);
2729
+ const context = submitContext ?? {
2730
+ attemptId: defaultAttemptId2(),
2731
+ formId: validSchema.id,
2732
+ formVersion: validSchema.version,
2733
+ locale,
2734
+ submittedAt: (/* @__PURE__ */ new Date()).toISOString()
2735
+ };
2736
+ const response = await onSubmit({ ...visibleValues }, context);
2702
2737
  if (resetOnSuccess) setValues({ ...initialValues });
2703
2738
  setSubmitStatus("success");
2704
2739
  return response === void 0 ? { status: "success" } : { status: "success", response };
2705
2740
  } catch (cause) {
2706
- const error = cause instanceof Error ? cause : new Error(String(cause));
2741
+ const error = isServerErrorPayload(cause) ? new FormSubmissionError(
2742
+ cause.formError ?? (cause instanceof Error ? cause.message : "Form submission failed."),
2743
+ cause
2744
+ ) : cause instanceof Error ? cause : new Error(String(cause));
2707
2745
  setSubmitError(error);
2708
2746
  setSubmitStatus("error");
2709
2747
  return { status: "error", error };
@@ -2711,7 +2749,7 @@ function FormProvider({
2711
2749
  submissionInFlight.current = false;
2712
2750
  }
2713
2751
  },
2714
- [initialValues, onSubmit, resetOnSuccess, validSchema, values]
2752
+ [initialValues, locale, onSubmit, resetOnSuccess, validSchema, values]
2715
2753
  );
2716
2754
  const translate = useCallback2(
2717
2755
  (key, params) => translator.translate(key, locale, params),
@@ -2902,18 +2940,6 @@ import {
2902
2940
  useRef as useRef2,
2903
2941
  useState as useState4
2904
2942
  } from "react";
2905
-
2906
- // src/types.ts
2907
- var FormSubmissionError = class extends Error {
2908
- payload;
2909
- constructor(message, payload) {
2910
- super(message);
2911
- this.name = "FormSubmissionError";
2912
- this.payload = payload ?? { formError: message };
2913
- }
2914
- };
2915
-
2916
- // src/renderer.tsx
2917
2943
  import { Fragment as Fragment3, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
2918
2944
  function describedBy(field, error, helpId, errorId) {
2919
2945
  const ids = [field.description === void 0 ? void 0 : helpId, error === void 0 ? void 0 : errorId].filter(
@@ -3100,7 +3126,11 @@ function DefaultField(props) {
3100
3126
  fieldId: field.id,
3101
3127
  current: typeof value === "string" ? value.length : 0,
3102
3128
  max: field.maxLength
3103
- }) : null,
3129
+ }) ?? /* @__PURE__ */ jsxs2("div", { className: "fe-character-count", "aria-live": "polite", children: [
3130
+ typeof value === "string" ? value.length : 0,
3131
+ " / ",
3132
+ field.maxLength
3133
+ ] }) : null,
3104
3134
  /* @__PURE__ */ jsx3(FieldMessage, { props })
3105
3135
  ] });
3106
3136
  }
@@ -3152,11 +3182,58 @@ function parseDraft(serialized) {
3152
3182
  return null;
3153
3183
  }
3154
3184
  }
3185
+ var DEFAULT_RENDERER_MESSAGES = {
3186
+ en: {
3187
+ submitButton: "Submit",
3188
+ submittingButton: "Submitting...",
3189
+ retryButton: "Retry",
3190
+ requiredField: "This field is required.",
3191
+ alreadySubmittedTitle: "Already Submitted",
3192
+ alreadySubmittedMessage: "Already submitted.",
3193
+ serverErrorSummary: "Submission failed. Please check your answers and try again.",
3194
+ confirmSensitiveDataTitle: "Sensitive data may be included",
3195
+ confirmSensitiveDataMessage: "The following answers may contain personal information. Continue submitting?",
3196
+ confirmButton: "Proceed",
3197
+ cancelButton: "Cancel"
3198
+ },
3199
+ ja: {
3200
+ submitButton: "\u9001\u4FE1\u3059\u308B",
3201
+ submittingButton: "\u9001\u4FE1\u4E2D...",
3202
+ retryButton: "\u518D\u9001\u4FE1\u3059\u308B",
3203
+ requiredField: "\u3053\u306E\u9805\u76EE\u306F\u5FC5\u9808\u3067\u3059",
3204
+ alreadySubmittedTitle: "\u56DE\u7B54\u6E08\u307F\u3067\u3059",
3205
+ alreadySubmittedMessage: "\u3053\u306E\u30A2\u30F3\u30B1\u30FC\u30C8\u306B\u306F\u3059\u3067\u306B\u56DE\u7B54\u3057\u3066\u3044\u307E\u3059\u3002",
3206
+ serverErrorSummary: "\u9001\u4FE1\u306B\u5931\u6557\u3057\u307E\u3057\u305F\u3002\u5185\u5BB9\u3092\u3054\u78BA\u8A8D\u306E\u4E0A\u3001\u518D\u5EA6\u304A\u8A66\u3057\u304F\u3060\u3055\u3044\u3002",
3207
+ confirmSensitiveDataTitle: "\u500B\u4EBA\u60C5\u5831\u304C\u542B\u307E\u308C\u3066\u3044\u308B\u53EF\u80FD\u6027\u304C\u3042\u308A\u307E\u3059",
3208
+ confirmSensitiveDataMessage: "\u4EE5\u4E0B\u306E\u9805\u76EE\u306B\u500B\u4EBA\u60C5\u5831\u3068\u307F\u3089\u308C\u308B\u8A18\u8FF0\u304C\u3042\u308A\u307E\u3059\u3002\u3053\u306E\u307E\u307E\u9001\u4FE1\u3057\u3066\u3082\u3088\u308D\u3057\u3044\u3067\u3059\u304B\uFF1F",
3209
+ confirmButton: "\u3053\u306E\u307E\u307E\u9001\u4FE1",
3210
+ cancelButton: "\u4FEE\u6B63\u3059\u308B"
3211
+ }
3212
+ };
3213
+ function createRendererAttemptId() {
3214
+ const randomUuid = globalThis.crypto?.randomUUID;
3215
+ if (typeof randomUuid === "function") return randomUuid.call(globalThis.crypto);
3216
+ return `attempt-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
3217
+ }
3218
+ function maskSensitiveValue(finding) {
3219
+ if (finding.maskedText !== void 0) return finding.maskedText;
3220
+ const value = finding.matchedText;
3221
+ if (value === void 0) return void 0;
3222
+ if (finding.type === "email") {
3223
+ const separator = value.indexOf("@");
3224
+ if (separator > 0) return `${value.slice(0, Math.min(2, separator))}***${value.slice(separator)}`;
3225
+ }
3226
+ if (finding.type === "phone" || finding.type === "postal_code") return "***";
3227
+ return value.length <= 2 ? "***" : `${value.slice(0, 2)}***`;
3228
+ }
3155
3229
  function ContextFormRenderer({
3156
3230
  components = {},
3157
3231
  className = "",
3158
3232
  successMessageKey,
3159
3233
  errorMessageKey,
3234
+ attemptIdFactory,
3235
+ messages = {},
3236
+ messageResolver,
3160
3237
  autoSaveKey,
3161
3238
  beforeSubmit,
3162
3239
  onDraftSave,
@@ -3185,6 +3262,7 @@ function ContextFormRenderer({
3185
3262
  const [completionData, setCompletionData] = useState4(null);
3186
3263
  const [receiptLoaded, setReceiptLoaded] = useState4(receiptStore === void 0);
3187
3264
  const rendererSubmissionInFlight = useRef2(false);
3265
+ const fallbackAttemptId = useRef2(null);
3188
3266
  const completionRef = useRef2(null);
3189
3267
  const confirmationRef = useRef2(null);
3190
3268
  const pages = form.schema.pages;
@@ -3199,6 +3277,18 @@ function ContextFormRenderer({
3199
3277
  const submitState = confirmation === null && !guardsPending ? form.submitStatus : "confirming";
3200
3278
  const interactionLocked = submitState === "confirming" || submitState === "submitting";
3201
3279
  const isReplaceMode = successRenderMode === "replace" || hideFormOnSuccess;
3280
+ const resolveMessage = useCallback3(
3281
+ (key, fallback) => {
3282
+ const defaultText = fallback ?? DEFAULT_RENDERER_MESSAGES[form.locale.toLowerCase().startsWith("ja") ? "ja" : "en"][key] ?? key;
3283
+ const configured = messages[key];
3284
+ return messageResolver?.(key, configured ?? defaultText) ?? configured ?? defaultText;
3285
+ },
3286
+ [form.locale, messageResolver, messages]
3287
+ );
3288
+ const fieldTranslate = useCallback3(
3289
+ (key, params) => key === "validation.required" && (messages.requiredField !== void 0 || messageResolver !== void 0) ? resolveMessage("requiredField") : form.translate(key, params),
3290
+ [form.translate, messageResolver, messages.requiredField, resolveMessage]
3291
+ );
3202
3292
  const focusSubmitButton = useCallback3(() => {
3203
3293
  const button = formRef.current?.querySelector(".fe-submit, button[type='submit'], button");
3204
3294
  button?.focus();
@@ -3238,6 +3328,7 @@ function ContextFormRenderer({
3238
3328
  );
3239
3329
  const control = fieldContainer?.querySelector("input, select, textarea");
3240
3330
  if (control !== void 0 && control !== null) {
3331
+ control.scrollIntoView?.({ behavior: "smooth", block: "center" });
3241
3332
  control.focus();
3242
3333
  setFocusFieldId(null);
3243
3334
  }
@@ -3256,6 +3347,23 @@ function ContextFormRenderer({
3256
3347
  event.preventDefault();
3257
3348
  setConfirmation(null);
3258
3349
  globalThis.setTimeout(focusSubmitButton, 0);
3350
+ return;
3351
+ }
3352
+ if (event.key !== "Tab") return;
3353
+ const focusable = [
3354
+ ...confirmationRef.current?.querySelectorAll(
3355
+ "button, [href], input, select, textarea, [tabindex]:not([tabindex='-1'])"
3356
+ ) ?? []
3357
+ ].filter((element) => !element.hasAttribute("disabled"));
3358
+ if (focusable.length === 0) return;
3359
+ const first = focusable[0];
3360
+ const last = focusable[focusable.length - 1];
3361
+ if (event.shiftKey && document.activeElement === first) {
3362
+ event.preventDefault();
3363
+ last?.focus();
3364
+ } else if (!event.shiftKey && document.activeElement === last) {
3365
+ event.preventDefault();
3366
+ first?.focus();
3259
3367
  }
3260
3368
  };
3261
3369
  globalThis.addEventListener("keydown", onKeyDown);
@@ -3357,17 +3465,24 @@ function ContextFormRenderer({
3357
3465
  rendererSubmissionInFlight.current = true;
3358
3466
  try {
3359
3467
  let submissionAttempt;
3360
- const result = await form.submit(
3361
- beforeSubmit,
3362
- attemptStore === void 0 ? void 0 : async (values) => {
3363
- submissionAttempt = await attemptStore.getOrCreate(form.schema.id, form.schema.version);
3364
- return {
3365
- ...values,
3366
- attemptId: submissionAttempt.attemptId,
3367
- submissionId: submissionAttempt.attemptId
3368
- };
3369
- }
3370
- );
3468
+ let attemptId = fallbackAttemptId.current;
3469
+ if (attemptStore !== void 0) {
3470
+ submissionAttempt = await attemptStore.getOrCreate(form.schema.id, form.schema.version, attemptIdFactory);
3471
+ attemptId = submissionAttempt.attemptId;
3472
+ } else if (attemptId === null) {
3473
+ attemptId = attemptIdFactory?.() ?? createRendererAttemptId();
3474
+ fallbackAttemptId.current = attemptId;
3475
+ }
3476
+ if (attemptId === null) throw new Error("Unable to create a submission attempt id.");
3477
+ const submittedAt = (/* @__PURE__ */ new Date()).toISOString();
3478
+ const submitContext = {
3479
+ attemptId,
3480
+ formId: form.schema.id,
3481
+ formVersion: form.schema.version,
3482
+ locale: form.locale,
3483
+ submittedAt
3484
+ };
3485
+ const result = await form.submit(beforeSubmit, submitContext);
3371
3486
  if (result.status === "invalid") {
3372
3487
  const invalidPageIndex = pages?.findIndex(
3373
3488
  (page) => firstInvalidFieldId === void 0 ? false : page.questionIds.includes(firstInvalidFieldId)
@@ -3380,7 +3495,7 @@ function ContextFormRenderer({
3380
3495
  if (result.error instanceof FormSubmissionError) {
3381
3496
  const fieldErrors = result.error.payload.fieldErrors ?? {};
3382
3497
  form.setServerErrors?.(fieldErrors);
3383
- const firstServerFieldId = Object.keys(fieldErrors)[0];
3498
+ const firstServerFieldId = form.schema.fields.find((field) => Object.hasOwn(fieldErrors, field.id))?.id ?? Object.keys(fieldErrors)[0];
3384
3499
  if (firstServerFieldId !== void 0) {
3385
3500
  const invalidPageIndex = pages?.findIndex((page) => page.questionIds.includes(firstServerFieldId));
3386
3501
  if (invalidPageIndex !== void 0 && invalidPageIndex >= 0) setCurrentPageIndex(invalidPageIndex);
@@ -3405,11 +3520,11 @@ function ContextFormRenderer({
3405
3520
  });
3406
3521
  if (receiptStore !== void 0) {
3407
3522
  const response = result.response;
3408
- const submissionId = response?.submissionId ?? submissionAttempt?.attemptId;
3523
+ const submissionId = response?.submissionId ?? submissionAttempt?.attemptId ?? attemptId;
3409
3524
  const storedReceipt = {
3410
3525
  formId: form.schema.id,
3411
3526
  formVersion: form.schema.version,
3412
- submittedAt: response?.submittedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
3527
+ submittedAt: response?.submittedAt ?? submittedAt,
3413
3528
  ...submissionId === void 0 ? {} : { submissionId }
3414
3529
  };
3415
3530
  try {
@@ -3428,6 +3543,7 @@ function ContextFormRenderer({
3428
3543
  } catch {
3429
3544
  }
3430
3545
  }
3546
+ if (attemptStore === void 0) fallbackAttemptId.current = null;
3431
3547
  if (autoSaveKey !== void 0 && typeof globalThis.localStorage !== "undefined") {
3432
3548
  globalThis.localStorage.removeItem(autoSaveKey);
3433
3549
  setDraftRestored(false);
@@ -3467,7 +3583,10 @@ function ContextFormRenderer({
3467
3583
  disabled: interactionLocked || submitState === "success",
3468
3584
  onSubmit: () => void submitValues()
3469
3585
  };
3470
- return slots.renderSubmitButton?.(submitButtonProps) ?? /* @__PURE__ */ jsx3("button", { className: "fe-submit", type: "submit", disabled: submitButtonProps.disabled, children: form.translate(form.schema.submitLabelKey ?? "form.submit") });
3586
+ return slots.renderSubmitButton?.(submitButtonProps) ?? /* @__PURE__ */ jsxs2("button", { className: "fe-submit", type: "submit", disabled: submitButtonProps.disabled, children: [
3587
+ submitState === "submitting" ? /* @__PURE__ */ jsx3("span", { className: "fe-spinner", "aria-hidden": "true" }) : null,
3588
+ submitState === "submitting" ? resolveMessage("submittingButton") : resolveMessage("submitButton", form.translate(form.schema.submitLabelKey ?? "form.submit"))
3589
+ ] });
3471
3590
  };
3472
3591
  const completionMessage = form.schema.completionMessage ?? (successMessageKey === void 0 ? "Submitted." : form.translate(successMessageKey));
3473
3592
  const activeCompletionData = completionData ?? {
@@ -3500,15 +3619,31 @@ function ContextFormRenderer({
3500
3619
  role: submissionConfirmationRenderMode === "dialog" ? void 0 : "dialog",
3501
3620
  children: slots.renderSubmissionConfirmation?.({
3502
3621
  findings: confirmation?.findings ?? [],
3503
- message: confirmation?.message ?? form.translate("form.confirmSensitiveData"),
3622
+ message: confirmation?.message ?? resolveMessage("confirmSensitiveDataMessage", form.translate("form.confirmSensitiveData")),
3504
3623
  schema: form.schema,
3505
3624
  visibleValues,
3506
3625
  onConfirm: confirmSubmission,
3507
3626
  onCancel: cancelSubmission
3508
3627
  }) ?? /* @__PURE__ */ jsxs2(Fragment3, { children: [
3509
- /* @__PURE__ */ jsx3("p", { children: confirmation?.message ?? form.translate("form.confirmSensitiveData") }),
3510
- /* @__PURE__ */ jsx3("button", { type: "button", "data-fe-confirm": "true", onClick: confirmSubmission, children: form.translate("form.confirmSubmission") }),
3511
- /* @__PURE__ */ jsx3("button", { type: "button", onClick: cancelSubmission, children: form.translate("form.cancelSubmission") })
3628
+ /* @__PURE__ */ jsx3("h2", { children: resolveMessage("confirmSensitiveDataTitle") }),
3629
+ /* @__PURE__ */ jsx3("p", { children: confirmation?.message ?? resolveMessage("confirmSensitiveDataMessage", form.translate("form.confirmSensitiveData")) }),
3630
+ (confirmation?.findings ?? []).length === 0 ? null : /* @__PURE__ */ jsx3("ul", { children: (confirmation?.findings ?? []).map((finding, index) => {
3631
+ const field = form.schema.fields.find((candidate) => candidate.id === finding.fieldId);
3632
+ const typeLabels = form.locale.toLowerCase().startsWith("ja") ? { email: "\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9", phone: "\u96FB\u8A71\u756A\u53F7", url: "URL", postal_code: "\u90F5\u4FBF\u756A\u53F7" } : { email: "Email address", phone: "Phone number", url: "URL", postal_code: "Postal code" };
3633
+ const typeLabel = finding.typeLabel ?? typeLabels[finding.type] ?? finding.type;
3634
+ const value = maskSensitiveValue(finding);
3635
+ return /* @__PURE__ */ jsxs2("li", { children: [
3636
+ /* @__PURE__ */ jsx3("span", { children: finding.fieldTitle ?? field?.title ?? finding.fieldId }),
3637
+ " ",
3638
+ /* @__PURE__ */ jsx3("span", { className: "fe-sensitive-type", children: typeLabel }),
3639
+ value === void 0 ? null : /* @__PURE__ */ jsxs2("span", { className: "fe-sensitive-value", children: [
3640
+ " ",
3641
+ value
3642
+ ] })
3643
+ ] }, `${finding.fieldId}-${finding.type}-${finding.start ?? index}`);
3644
+ }) }),
3645
+ /* @__PURE__ */ jsx3("button", { type: "button", "data-fe-confirm": "true", onClick: confirmSubmission, children: resolveMessage("confirmButton", form.translate("form.confirmSubmission")) }),
3646
+ /* @__PURE__ */ jsx3("button", { type: "button", onClick: cancelSubmission, children: resolveMessage("cancelButton", form.translate("form.cancelSubmission")) })
3512
3647
  ] })
3513
3648
  }
3514
3649
  );
@@ -3518,8 +3653,9 @@ function ContextFormRenderer({
3518
3653
  receipt,
3519
3654
  ...receiptStore === void 0 ? {} : { onReset: () => void resetReceipt() }
3520
3655
  }) ?? /* @__PURE__ */ jsxs2("div", { role: "status", children: [
3521
- form.translate("form.alreadySubmitted"),
3522
- receiptStore === void 0 ? null : /* @__PURE__ */ jsx3("button", { type: "button", onClick: () => void resetReceipt(), children: form.translate("form.submitAnother") })
3656
+ /* @__PURE__ */ jsx3("h2", { children: resolveMessage("alreadySubmittedTitle") }),
3657
+ /* @__PURE__ */ jsx3("p", { children: resolveMessage("alreadySubmittedMessage", form.translate("form.alreadySubmitted")) }),
3658
+ receiptStore === void 0 ? null : /* @__PURE__ */ jsx3("button", { type: "button", onClick: () => void resetReceipt(), children: resolveMessage("submitButton", form.translate("form.submitAnother")) })
3523
3659
  ] }) });
3524
3660
  }
3525
3661
  if (form.submitStatus === "success" && isReplaceMode) {
@@ -3582,7 +3718,7 @@ function ContextFormRenderer({
3582
3718
  value: form.values[field.id],
3583
3719
  error,
3584
3720
  setValue: (value) => form.setValue(field.id, value),
3585
- translate: form.translate,
3721
+ translate: fieldTranslate,
3586
3722
  inputId: `${prefix}-${field.id}`,
3587
3723
  errorId: `${prefix}-${field.id}-error`,
3588
3724
  helpId: `${prefix}-${field.id}-help`,
@@ -3649,7 +3785,13 @@ function ContextFormRenderer({
3649
3785
  ] }),
3650
3786
  /* @__PURE__ */ jsxs2("div", { className: "fe-status", "aria-live": "polite", children: [
3651
3787
  form.submitStatus === "success" ? completionRegion : null,
3652
- form.submitStatus === "error" && form.submitError !== null ? slots.renderSubmitError?.({ error: form.submitError, onRetry: () => void submitValues() }) ?? (form.submitError instanceof FormSubmissionError && form.submitError.payload.formError !== void 0 ? /* @__PURE__ */ jsx3("div", { role: "alert", children: form.submitError.payload.formError }) : errorMessageKey === void 0 ? null : /* @__PURE__ */ jsx3("div", { role: "alert", children: form.translate(errorMessageKey) })) : null
3788
+ form.submitStatus === "error" && form.submitError !== null ? slots.renderSubmitError?.({ error: form.submitError, onRetry: () => void submitValues() }) ?? (form.submitError instanceof FormSubmissionError && form.submitError.payload.formError !== void 0 ? /* @__PURE__ */ jsxs2("div", { role: "alert", children: [
3789
+ form.submitError.payload.formError,
3790
+ /* @__PURE__ */ jsx3("button", { type: "button", onClick: () => void submitValues(), children: resolveMessage("retryButton") })
3791
+ ] }) : /* @__PURE__ */ jsxs2("div", { role: "alert", children: [
3792
+ errorMessageKey === void 0 ? resolveMessage("serverErrorSummary") : form.translate(errorMessageKey),
3793
+ /* @__PURE__ */ jsx3("button", { type: "button", onClick: () => void submitValues(), children: resolveMessage("retryButton") })
3794
+ ] })) : null
3653
3795
  ] })
3654
3796
  ]
3655
3797
  }
@@ -3665,7 +3807,7 @@ var RENDERER_MESSAGES = {
3665
3807
  "form.draftRestored": "Draft restored",
3666
3808
  "form.submissionBlocked": "Submission blocked because sensitive data was detected.",
3667
3809
  "form.confirmSensitiveData": "Sensitive data may be included. Confirm before submitting.",
3668
- "form.confirmSubmission": "Confirm submission",
3810
+ "form.confirmSubmission": "Proceed",
3669
3811
  "form.cancelSubmission": "Cancel",
3670
3812
  "form.yes": "Yes",
3671
3813
  "form.no": "No",
@@ -3673,9 +3815,26 @@ var RENDERER_MESSAGES = {
3673
3815
  "form.submitAnother": "Submit another response",
3674
3816
  "validation.required": "This field is required."
3675
3817
  };
3818
+ var RENDERER_MESSAGES_JA = {
3819
+ "form.submit": "\u9001\u4FE1\u3059\u308B",
3820
+ "form.back": "\u623B\u308B",
3821
+ "form.next": "\u6B21\u3078",
3822
+ "form.step": "{{current}} / {{total}}",
3823
+ "form.draftRestored": "\u4E0B\u66F8\u304D\u3092\u5FA9\u5143\u3057\u307E\u3057\u305F",
3824
+ "form.submissionBlocked": "\u500B\u4EBA\u60C5\u5831\u304C\u691C\u51FA\u3055\u308C\u305F\u305F\u3081\u9001\u4FE1\u3067\u304D\u307E\u305B\u3093\u3002",
3825
+ "form.confirmSensitiveData": "\u500B\u4EBA\u60C5\u5831\u304C\u542B\u307E\u308C\u3066\u3044\u308B\u53EF\u80FD\u6027\u304C\u3042\u308A\u307E\u3059\u3002\u9001\u4FE1\u524D\u306B\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
3826
+ "form.confirmSubmission": "\u3053\u306E\u307E\u307E\u9001\u4FE1",
3827
+ "form.cancelSubmission": "\u4FEE\u6B63\u3059\u308B",
3828
+ "form.yes": "\u306F\u3044",
3829
+ "form.no": "\u3044\u3044\u3048",
3830
+ "form.alreadySubmitted": "\u56DE\u7B54\u6E08\u307F\u3067\u3059",
3831
+ "form.submitAnother": "\u5225\u306E\u56DE\u7B54\u3092\u9001\u4FE1",
3832
+ "validation.required": "\u3053\u306E\u9805\u76EE\u306F\u5FC5\u9808\u3067\u3059"
3833
+ };
3676
3834
  var defaultRendererTranslator = {
3677
- translate(key, _locale, params = {}) {
3678
- return (RENDERER_MESSAGES[key] ?? key).replace(
3835
+ translate(key, locale, params = {}) {
3836
+ const localizedMessages = locale.toLowerCase().startsWith("ja") ? RENDERER_MESSAGES_JA : RENDERER_MESSAGES;
3837
+ return (localizedMessages[key] ?? key).replace(
3679
3838
  /\{\{(\w+)\}\}/g,
3680
3839
  (token, name) => Object.hasOwn(params, name) ? String(params[name]) : token
3681
3840
  );
package/dist/styles.css CHANGED
@@ -93,6 +93,27 @@
93
93
  font-size: 0.875rem;
94
94
  font-weight: 600;
95
95
  }
96
+ .fe-character-count {
97
+ color: #667085;
98
+ font-size: 0.8rem;
99
+ text-align: right;
100
+ }
101
+ .fe-spinner {
102
+ display: inline-block;
103
+ width: 0.85em;
104
+ height: 0.85em;
105
+ margin-right: 0.4em;
106
+ border: 0.12em solid currentColor;
107
+ border-right-color: transparent;
108
+ border-radius: 50%;
109
+ animation: fe-spin 0.8s linear infinite;
110
+ vertical-align: -0.1em;
111
+ }
112
+ @keyframes fe-spin {
113
+ to {
114
+ transform: rotate(360deg);
115
+ }
116
+ }
96
117
  .fe-check-label {
97
118
  align-items: flex-start;
98
119
  display: flex;
@@ -298,3 +319,16 @@
298
319
  border-radius: 0.5rem;
299
320
  box-shadow: 0 1rem 3rem rgb(0 0 0 / 25%);
300
321
  }
322
+ .fe-sensitive-type {
323
+ border-radius: 999px;
324
+ background: #f2f4f7;
325
+ padding: 0.15rem 0.45rem;
326
+ font-size: 0.8rem;
327
+ }
328
+ .fe-sensitive-value {
329
+ font-family:
330
+ ui-monospace,
331
+ SFMono-Regular,
332
+ Menlo,
333
+ monospace;
334
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@form-engine-ts/react",
3
- "version": "2.9.6",
3
+ "version": "3.0.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -42,8 +42,8 @@
42
42
  "typescript"
43
43
  ],
44
44
  "dependencies": {
45
- "@form-engine-ts/core": "2.9.6",
46
- "@form-engine-ts/privacy": "2.9.6"
45
+ "@form-engine-ts/core": "3.0.0",
46
+ "@form-engine-ts/privacy": "3.0.0"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "react": ">=18.2 <20",