Repository: logaretm/vee-validate
Pull request #4747 (2024-05-16 08:54:25)

docs: fix confused between validateOnModelUpdate
🔎 __Overview__

<!-- Explain the why behind adding this PR, here is a couple of examples -->
<!--
  This PR {adds/fixes/improves} the {feature/bug/something}.
  This PR changes the {locale} messages style because {reason}
-->

🤓 __Code snippets/examples (if applicable)__

```js
// some code
```

✔ __Issues affected__

<!-- list of issues formatted like this
closes #{issue id}
 -->
 



Patch:
diff --git a/.changeset/sharp-glasses-wait.md b/.changeset/sharp-glasses-wait.md
new file mode 100644
index 00000000..cbdf760f
--- /dev/null
+++ b/.changeset/sharp-glasses-wait.md
@@ -0,0 +1,5 @@
+---
+"vee-validate": minor
+---
+
+fix: force resetForm should not merge values closes #4680 closes #4729
diff --git a/.changeset/short-frogs-shake.md b/.changeset/short-frogs-shake.md
new file mode 100644
index 00000000..528e79a8
--- /dev/null
+++ b/.changeset/short-frogs-shake.md
@@ -0,0 +1,5 @@
+---
+"vee-validate": patch
+---
+
+feat: expose setValue on Field instance and slot props closes #4755
diff --git a/.changeset/spotty-cobras-sparkle.md b/.changeset/spotty-cobras-sparkle.md
new file mode 100644
index 00000000..a3ca05ec
--- /dev/null
+++ b/.changeset/spotty-cobras-sparkle.md
@@ -0,0 +1,5 @@
+---
+"vee-validate": minor
+---
+
+feat: provide form values as context for yup closes #4753
diff --git a/.changeset/wise-schools-hammer.md b/.changeset/wise-schools-hammer.md
new file mode 100644
index 00000000..dfb2bc91
--- /dev/null
+++ b/.changeset/wise-schools-hammer.md
@@ -0,0 +1,5 @@
+---
+"vee-validate": patch
+---
+
+feat: added ResetFormOpts arg to useResetForm closes #4707
diff --git a/docs/src/pages/api/form.mdx b/docs/src/pages/api/form.mdx
index 2b1c15d9..6152cfa2 100644
--- a/docs/src/pages/api/form.mdx
+++ b/docs/src/pages/api/form.mdx
@@ -136,7 +136,7 @@ Here is an example of its shape:
 }
 ```
 
-Any fields without error messages will not be included in the object. So you can safety iterate over it with `Object.keys()` knowing all the included fields are invalid.
+Any fields without error messages will not be included in the object. So you can safely iterate over it with `Object.keys()` knowing all the included fields are invalid.
 
 <CodeTitle level="4">
 
diff --git a/docs/src/pages/api/use-field.mdx b/docs/src/pages/api/use-field.mdx
index a3d12268..9a737e66 100644
--- a/docs/src/pages/api/use-field.mdx
+++ b/docs/src/pages/api/use-field.mdx
@@ -242,7 +242,7 @@ Sets then field errors, you can pass a single message string or an array of erro
 ```js
 const { setErrors } = useField('field', value => !!value);
 
-// Sets a the errors to a single error message
+// Sets the errors to a single error message
 setErrors('field is required');
 
 // sets the errors to multiple error messages
diff --git a/docs/src/pages/examples/ui-libraries.mdx b/docs/src/pages/examples/ui-libraries.mdx
index 684708e0..4b771460 100644
--- a/docs/src/pages/examples/ui-libraries.mdx
+++ b/docs/src/pages/examples/ui-libraries.mdx
@@ -35,7 +35,7 @@ Check their [official example here](https://primevue.org/inputtext/#veevalidate)
 
 ## Quasar
 
-[Quasar framework](https://next.quasar.dev/) aims to:
+[Quasar framework](https://quasar.dev/) aims to:
 
 > Effortlessly build high-performance & high-quality Vue 3 user interfaces in record time
 
diff --git a/docs/src/pages/guide/composition-api/custom-inputs.mdx b/docs/src/pages/guide/composition-api/custom-inputs.mdx
index efff16ab..823eb75c 100644
--- a/docs/src/pages/guide/composition-api/custom-inputs.mdx
+++ b/docs/src/pages/guide/composition-api/custom-inputs.mdx
@@ -343,12 +343,12 @@ const schema = Yup.object({
 
 #### Custom Labels with Zod
 
-With Zod it is similar, you call the `label` function and give it the name you want to use in the generated message.
+Zod does not have a built-in `label` method, but you can override the default error messages by passing a custom message to the chained validator.
 
 ```js
 const schema = z.object({
-  email_addr: z.string().email().label('Email Address'),
-  acc_password: z.string().min(5).label('Your Password'),
+  email_addr: z.string().email({ message: 'Email Address be a valid email address' });
+  acc_password: z.string().min(5, { message: 'Password be at least 5 characters long' });
 });
 ```
 
diff --git a/docs/src/pages/guide/composition-api/getting-started.mdx b/docs/src/pages/guide/composition-api/getting-started.mdx
index fb280881..c41a513b 100644
--- a/docs/src/pages/guide/composition-api/getting-started.mdx
+++ b/docs/src/pages/guide/composition-api/getting-started.mdx
@@ -120,7 +120,7 @@ You can change that behavior. For example, you can make it "lazy" by passing a c
 
 <Repl files={{ 'App.vue': 'CompositionInputBindsBasic03' }} client:visible />
 
-Now as you type in the field, the input is immediately validated. You can do more with dynamic configurations.
+Now as you type in the field, the input is not immediately validated. You can do more with dynamic configurations.
 
 ### Components
 
diff --git a/docs/src/pages/guide/i18n.mdx b/docs/src/pages/guide/i18n.mdx
index d5b02920..6dc9619b 100644
--- a/docs/src/pages/guide/i18n.mdx
+++ b/docs/src/pages/guide/i18n.mdx
@@ -234,6 +234,38 @@ Here is an example that show cases interpolation for different cases:
 
 <LiveExample client:visible id="vee-validate-v4-i18n-interpolation" />
 
+#### Custom Interpolation Prefix and Suffix
+
+You also have the ability to customize the `prefix` and `suffix` of the interpolated values, by default they are `{` and `}` respectively.
+
+Configure them by passing a third parameter of `InterpolateOptions` to the `localize` function:
+
+```js
+import { defineRule, configure } from 'vee-validate';
+import { between } from '@vee-validate/rules';
+import { localize } from '@vee-validate/i18n';
+
+// Define the rule globally
+defineRule('between', between);
+
+configure({
+  // Generates an English message locale generator
+  generateMessage: localize(
+    'en',
+    {
+      messages: {
+        // use double `{{` and `}}` i18next-like curly braces for the interpolated values, instead of the default `{` and `}`
+        between: `The {{field}} field must be between 0:{{min}} and 1:{{max}}`,
+      },
+    },
+    {
+      prefix: '{{',
+      suffix: '}}',
+    },
+  ),
+});
+```
+
 ### Loading Locale From CDN
 
 If you are using Vue.js over CDN, you can still load the locale files without having to copy them to your code, the `@vee-validate/i18n` library exposes a `loadLocaleFromURL` function that accepts a URL to a locale file.
diff --git a/packages/i18n/src/index.ts b/packages/i18n/src/index.ts
index 3c532fd1..43154489 100644
--- a/packages/i18n/src/index.ts
+++ b/packages/i18n/src/index.ts
@@ -1,4 +1,5 @@
 import { isCallable, FieldValidationMetaInfo, ValidationMessageGenerator, merge } from '../../shared';
+import { InterpolateOptions } from '../../shared/types';
 import { interpolate } from './utils';
 
 export { FieldValidationMetaInfo };
@@ -18,15 +19,21 @@ class Dictionary {
   public locale: string;
 
   private container: RootI18nDictionary;
+  private interpolateOptions: InterpolateOptions;
 
-  public constructor(locale: string, dictionary: RootI18nDictionary) {
+  public constructor(
+    locale: string,
+    dictionary: RootI18nDictionary,
+    interpolateOptions: InterpolateOptions = { prefix: '{', suffix: '}' },
+  ) {
     this.container = {};
     this.locale = locale;
+    this.interpolateOptions = interpolateOptions;
     this.merge(dictionary);
   }
 
-  public resolve(ctx: FieldValidationMetaInfo) {
-    return this.format(this.locale, ctx);
+  public resolve(ctx: FieldValidationMetaInfo, interpolateOptions?: InterpolateOptions) {
+    return this.format(this.locale, ctx, interpolateOptions);
   }
 
   public getLocaleDefault(locale: string, field: string): string | ValidationMessageGenerator | undefined {
@@ -41,14 +48,16 @@ class Dictionary {
     return this.container[locale]?.names?.[name] || name;
   }
 
-  public format(locale: string, ctx: FieldValidationMetaInfo) {
+  public format(locale: string, ctx: FieldValidationMetaInfo, interpolateOptions?: InterpolateOptions) {
     let message!: ValidationMessageTemplate | undefined;
     const { rule, form, label, name } = ctx;
     const fieldName = this.resolveLabel(locale, name, label);
 
     if (!rule) {
       message = this.getLocaleDefault(locale, name) || `${fieldName} is not valid`;
-      return isCallable(message) ? message(ctx) : interpolate(message, { ...form, field: fieldName });
+      return isCallable(message)
+        ? message(ctx)
+        : interpolate(message, { ...form, field: fieldName }, interpolateOptions ?? this.interpolateOptions);
     }
 
     // find if specific message for that field was specified.
@@ -59,7 +68,11 @@ class Dictionary {
 
     return isCallable(message)
       ? message(ctx)
-      : interpolate(message, { ...form, field: fieldName, params: rule.params });
+      : interpolate(
+          message,
+          { ...form, field: fieldName, params: rule.params },
+          interpolateOptions ?? this.interpolateOptions,
+        );
   }
 
   public merge(dictionary: RootI18nDictionary) {
@@ -71,10 +84,19 @@ const DICTIONARY: Dictionary = new Dictionary('en', {});
 
 function localize(dictionary: RootI18nDictionary): ValidationMessageGenerator;
 function localize(locale: string, dictionary?: PartialI18nDictionary): ValidationMessageGenerator;
-
-function localize(locale: string | RootI18nDictionary, dictionary?: PartialI18nDictionary) {
+function localize(
+  locale: string,
+  dictionary?: PartialI18nDictionary,
+  interpolateOptions?: InterpolateOptions,
+): ValidationMessageGenerator;
+
+function localize(
+  locale: string | RootI18nDictionary,
+  dictionary?: PartialI18nDictionary,
+  interpolateOptions?: InterpolateOptions,
+) {
   const generateMessage: ValidationMessageGenerator = ctx => {
-    return DICTIONARY.resolve(ctx);
+    return DICTIONARY.resolve(ctx, interpolateOptions);
   };
 
   if (typeof locale === 'string') {
diff --git a/packages/i18n/src/utils.ts b/packages/i18n/src/utils.ts
index efd3b7bd..65bc5ebd 100644
--- a/packages/i18n/src/utils.ts
+++ b/packages/i18n/src/utils.ts
@@ -1,24 +1,30 @@
+import { InterpolateOptions } from '../../shared/types';
+
 /**
  * Replaces placeholder values in a string with their actual values
  */
-export function interpolate(template: string, values: Record<string, any>): string {
-  return template.replace(/(\d:)?{([^}]+)}/g, function (_, param, placeholder): string {
+export function interpolate(template: string, values: Record<string, any>, options: InterpolateOptions): string {
+  const { prefix, suffix } = options;
+
+  const regExp = new RegExp(`([0-9]:)?${prefix}([^${suffix}]+)${suffix}`, 'g');
+
+  return template.replace(regExp, function (_, param, placeholder): string {
     if (!param || !values.params) {
       return placeholder in values
         ? values[placeholder]
         : values.params && placeholder in values.params
           ? values.params[placeholder]
-          : `{${placeholder}}`;
+          : `${prefix}${placeholder}${suffix}`;
     }
 
     // Handles extended object params format
     if (!Array.isArray(values.params)) {
-      return placeholder in values.params ? values.params[placeholder] : `{${placeholder}}`;
+      return placeholder in values.params ? values.params[placeholder] : `${prefix}${placeholder}${suffix}`;
     }
 
     // Extended Params exit in the format of `paramIndex:{paramName}` where the index is optional
     const paramIndex = Number(param.replace(':', ''));
 
-    return paramIndex in values.params ? values.params[paramIndex] : `${param}{${placeholder}}`;
+    return paramIndex in values.params ? values.params[paramIndex] : `${param}${prefix}${placeholder}${suffix}`;
   });
 }
diff --git a/packages/rules/src/toTypedSchema.ts b/packages/rules/src/toTypedSchema.ts
index a5b47d88..d5ef2486 100644
--- a/packages/rules/src/toTypedSchema.ts
+++ b/packages/rules/src/toTypedSchema.ts
@@ -1,5 +1,5 @@
 import { keysOf } from '../../vee-validate/src/utils';
-import { TypedSchema, RawFormSchema, validateObject, TypedSchemaError, validate } from 'vee-validate';
+import { TypedSchema, RawFormSchema, validateObject, TypedSchemaError, validate, GenericObject } from 'vee-validate';
 import { Optional, isObject } from '../../shared';
 
 export function toTypedSchema<TOutput = any, TInput extends Optional<TOutput> = Optional<TOutput>>(
@@ -21,7 +21,7 @@ export function toTypedSchema<TOutput = any, TInput extends Optional<TOutput> =
         };
       }
 
-      const result = await validateObject<TInput, TOutput>(rawSchema, values);
+      const result = await validateObject(rawSchema, values as GenericObject | undefined);
 
       return {
         errors: keysOf(result.errors).map(path => {
diff --git a/packages/shared/types.ts b/packages/shared/types.ts
index ef8fe22a..ec74600c 100644
--- a/packages/shared/types.ts
+++ b/packages/shared/types.ts
@@ -24,3 +24,8 @@ export type SimpleValidationRuleFunction<TValue = unknown, TParams = unknown[] |
 export type ValidationMessageGenerator = (ctx: FieldValidationMetaInfo) => string;
 
 export type Optional<T> = T extends Record<string, any> ? Partial<T> : T | undefined;
+
+export type InterpolateOptions = {
+  prefix: string;
+  suffix: string;
+};
diff --git a/packages/vee-validate/src/Field.ts b/packages/vee-validate/src/Field.ts
index 6e442668..f8079742 100644
--- a/packages/vee-validate/src/Field.ts
+++ b/packages/vee-validate/src/Field.ts
@@ -45,7 +45,7 @@ export interface ComponentFieldBindingObject<TValue = any> extends SharedBinding
 interface FieldSlotProps<TValue = unknown>
   extends Pick<
     FieldContext,
-    'validate' | 'resetField' | 'handleChange' | 'handleReset' | 'handleBlur' | 'setTouched' | 'setErrors'
+    'validate' | 'resetField' | 'handleChange' | 'handleReset' | 'handleBlur' | 'setTouched' | 'setErrors' | 'setValue'
   > {
   field: FieldBindingObject<TValue>;
   componentField: ComponentFieldBindingObject<TValue>;
@@ -145,6 +145,7 @@ const FieldImpl = /** #__PURE__ */ defineComponent({
       meta,
       checked,
       setErrors,
+      setValue,
     } = useField(name, rules, {
       validateOnMount: props.validateOnMount,
       bails: props.bails,
@@ -242,6 +243,7 @@ const FieldImpl = /** #__PURE__ */ defineComponent({
         handleBlur: sharedProps.value.onBlur,
         setTouched,
         setErrors,
+        setValue,
       };
     }
 
@@ -252,6 +254,7 @@ const FieldImpl = /** #__PURE__ */ defineComponent({
       errorMessage,
       setErrors,
       setTouched,
+      setValue,
       reset: resetField,
       validate: validateField,
       handleChange,
@@ -318,6 +321,7 @@ export const Field = FieldImpl as typeof FieldImpl & {
     setTouched: FieldContext['setTouched'];
     reset: FieldContext['resetField'];
     validate: FieldContext['validate'];
+    setValue: FieldContext['setValue'];
     handleChange: FieldContext['handleChange'];
     $slots: {
       default: (arg: FieldSlotProps<any>) => VNode[];
diff --git a/packages/vee-validate/src/types/forms.ts b/packages/vee-validate/src/types/forms.ts
index 57b3f0d1..07287629 100644
--- a/packages/vee-validate/src/types/forms.ts
+++ b/packages/vee-validate/src/types/forms.ts
@@ -4,11 +4,16 @@ import { FieldValidationMetaInfo } from '../../../shared';
 import { Path, PathValue } from './paths';
 import { PartialDeep } from 'type-fest';
 
-export interface ValidationResult {
+export interface ValidationResult<TValue = unknown> {
   errors: string[];
   valid: boolean;
+  value?: TValue;
 }
 
+export type FlattenAndMapPathsValidationResult<TInput extends GenericObject, TOutput extends GenericObject> = {
+  [K in Path<TInput>]: ValidationResult<TOutput[K]>;
+};
+
 export interface TypedSchemaError {
   path?: string;
   errors: string[];
@@ -18,9 +23,13 @@ export interface TypedSchemaPathDescription {
   exists: boolean;
 }
 
+export interface TypedSchemaContext {
+  formData: GenericObject;
+}
+
 export interface TypedSchema<TInput = any, TOutput = TInput> {
   __type: 'VVTypedSchema';
-  parse(values: TInput): Promise<{ value?: TOutput; errors: TypedSchemaError[] }>;
+  parse(values: TInput, context?: TypedSchemaContext): Promise<{ value?: TOutput; errors: TypedSchemaError[] }>;
   cast?(values: Partial<TInput>): TInput;
   describe?(path?: Path<TInput>): Partial<TypedSchemaPathDescription>;
 }
@@ -77,17 +86,17 @@ export interface ValidationOptions {
   warn: boolean;
 }
 
-export type FieldValidator = (opts?: Partial<ValidationOptions>) => Promise<ValidationResult>;
+export type FieldValidator<TOutput> = (opts?: Partial<ValidationOptions>) => Promise<ValidationResult<TOutput>>;
 
-export interface PathStateConfig {
+export interface PathStateConfig<TOutput> {
   bails: boolean;
   label: MaybeRefOrGetter<string | undefined>;
   type: InputType;
-  validate: FieldValidator;
+  validate: FieldValidator<TOutput>;
   schema?: MaybeRefOrGetter<TypedSchema | undefined>;
 }
 
-export interface PathState<TValue = unknown> {
+export interface PathState<TInput = unknown, TOutput = TInput> {
   id: number | number[];
   path: string;
   touched: boolean;
@@ -96,8 +105,8 @@ export interface PathState<TValue = unknown> {
   required: boolean;
   validated: boolean;
   pending: boolean;
-  initialValue: TValue | undefined;
-  value: TValue | undefined;
+  initialValue: TInput | undefined;
+  value: TInput | undefined;
   errors: string[];
   bails: boolean;
   label: string | undefined;
@@ -108,7 +117,7 @@ export interface PathState<TValue = unknown> {
     pendingUnmount: Record<string, boolean>;
     pendingReset: boolean;
   };
-  validate?: FieldValidator;
+  validate?: FieldValidator<TOutput>;
 }
 
 export interface FieldEntry<TValue = unknown> {
@@ -135,29 +144,29 @@ export interface PrivateFieldArrayContext<TValue = unknown> extends FieldArrayCo
   path: MaybeRefOrGetter<string>;
 }
 
-export interface PrivateFieldContext<TValue = unknown> {
+export interface PrivateFieldContext<TInput = unknown, TOutput = TInput> {
   id: number;
   name: MaybeRef<string>;
-  value: Ref<TValue>;
-  meta: FieldMeta<TValue>;
+  value: Ref<TInput>;
+  meta: FieldMeta<TInput>;
   errors: Ref<string[]>;
   errorMessage: Ref<string | undefined>;
   label?: MaybeRefOrGetter<string | undefined>;
   type?: string;
   bails?: boolean;
   keepValueOnUnmount?: MaybeRefOrGetter<boolean | undefined>;
-  checkedValue?: MaybeRefOrGetter<TValue>;
-  uncheckedValue?: MaybeRefOrGetter<TValue>;
+  checkedValue?: MaybeRefOrGetter<TInput>;
+  uncheckedValue?: MaybeRefOrGetter<TInput>;
   checked?: Ref<boolean>;
-  resetField(state?: Partial<FieldState<TValue>>): void;
+  resetField(state?: Partial<FieldState<TInput>>): void;
   handleReset(): void;
-  validate: FieldValidator;
+  validate: FieldValidator<TOutput>;
   handleChange(e: Event | unknown, shouldValidate?: boolean): void;
   handleBlur(e?: Event, shouldValidate?: boolean): void;
-  setState(state: Partial<FieldState<TValue>>): void;
+  setState(state: Partial<FieldState<TInput>>): void;
   setTouched(isTouched: boolean): void;
   setErrors(message: string | string[]): void;
-  setValue(value: TValue, shouldValidate?: boolean): void;
+  setValue(value: TInput, shouldValidate?: boolean): void;
 }
 
 export type FieldContext<TValue = unknown> = Omit<PrivateFieldContext<TValue>, 'id' | 'instances'>;
@@ -193,33 +202,38 @@ export interface FormActions<TValues extends GenericObject, TOutput = TValues> {
   resetField(field: Path<TValues>, state?: Partial<FieldState>): void;
 }
 
-export interface FormValidationResult<TValues, TOutput = TValues> {
+export interface FormValidationResult<TInput extends GenericObject, TOutput extends GenericObject = TInput> {
   valid: boolean;
-  results: Partial<Record<Path<TValues>, ValidationResult>>;
-  errors: Partial<Record<Path<TValues>, string>>;
-  values?: TOutput;
+  results: Partial<FlattenAndMapPathsValidationResult<TInput, TOutput>>;
+  errors: Partial<Record<Path<TInput>, string>>;
+  values?: Partial<TOutput>;
+  source: 'schema' | 'fields' | 'none';
 }
 
-export interface SubmissionContext<TValues extends GenericObject = GenericObject> extends FormActions<TValues> {
+export interface SubmissionContext<TInput extends GenericObject = GenericObject> extends FormActions<TInput> {
   evt?: Event;
-  controlledValues: Partial<TValues>;
+  controlledValues: Partial<TInput>;
 }
 
-export type SubmissionHandler<TValues extends GenericObject = GenericObject, TOutput = TValues, TReturn = unknown> = (
+export type SubmissionHandler<TInput extends GenericObject = GenericObject, TOutput = TInput, TReturn = unknown> = (
   values: TOutput,
-  ctx: SubmissionContext<TValues>,
+  ctx: SubmissionContext<TInput>,
 ) => TReturn;
 
-export interface InvalidSubmissionContext<TValues extends GenericObject = GenericObject> {
-  values: TValues;
+export interface InvalidSubmissionContext<
+  TInput extends GenericObject = GenericObject,
+  TOutput extends GenericObject = TInput,
+> {
+  values: TInput;
   evt?: Event;
-  errors: Partial<Record<Path<TValues>, string>>;
-  results: Partial<Record<Path<TValues>, ValidationResult>>;
+  errors: Partial<Record<Path<TInput>, string>>;
+  results: FormValidationResult<TInput, TOutput>['results'];
 }
 
-export type InvalidSubmissionHandler<TValues extends GenericObject = GenericObject> = (
-  ctx: InvalidSubmissionContext<TValues>,
-) => void;
+export type InvalidSubmissionHandler<
+  TInput extends GenericObject = GenericObject,
+  TOutput extends GenericObject = TInput,
+> = (ctx: InvalidSubmissionContext<TInput, TOutput>) => void;
 
 export type RawFormSchema<TValues> = Record<Path<TValues>, string | GenericValidateFunction | GenericObject>;
 
@@ -227,9 +241,9 @@ export type FieldPathLookup<TValues extends GenericObject = GenericObject> = Par
   Record<Path<TValues>, PrivateFieldContext | PrivateFieldContext[]>
 >;
 
-type HandleSubmitFactory<TValues extends GenericObject, TOutput = TValues> = <TReturn = unknown>(
+type HandleSubmitFactory<TValues extends GenericObject, TOutput extends GenericObject = TValues> = <TReturn = unknown>(
   cb: SubmissionHandler<TValues, TOutput, TReturn>,
-  onSubmitValidationErrorCb?: InvalidSubmissionHandler<TValues>,
+  onSubmitValidationErrorCb?: InvalidSubmissionHandler<TValues, TOutput>,
 ) => (e?: Event) => Promise<TReturn | undefined>;
 
 export type PublicPathState<TValue = unknown> = Omit<
@@ -306,8 +320,10 @@ export interface BaseInputBinds<TValue = unknown> {
   onInput: (e: Event) => void;
 }
 
-export interface PrivateFormContext<TValues extends GenericObject = GenericObject, TOutput = TValues>
-  extends FormActions<TValues> {
+export interface PrivateFormContext<
+  TValues extends GenericObject = GenericObject,
+  TOutput extends GenericObject = TValues,
+> extends FormActions<TValues> {
   formId: number;
   values: TValues;
   initialValues: Ref<Partial<TValues>>;
@@ -323,14 +339,17 @@ export interface PrivateFormContext<TValues extends GenericObject = GenericObjec
   keepValuesOnUnmount: MaybeRef<boolean>;
   validateSchema?: (mode: SchemaValidationMode) => Promise<FormValidationResult<TValues, TOutput>>;
   validate(opts?: Partial<ValidationOptions>): Promise<FormValidationResult<TValues, TOutput>>;
-  validateField(field: Path<TValues>, opts?: Partial<ValidationOptions>): Promise<ValidationResult>;
+  validateField<TPath extends Path<TValues>>(
+    field: TPath,
+    opts?: Partial<ValidationOptions>,
+  ): Promise<ValidationResult<TOutput[TPath]>>;
   stageInitialValue(path: string, value: unknown, updateOriginal?: boolean): void;
   unsetInitialValue(path: string): void;
   handleSubmit: HandleSubmitFactory<TValues, TOutput> & { withControlled: HandleSubmitFactory<TValues, TOutput> };
   setFieldInitialValue(path: string, value: unknown, updateOriginal?: boolean): void;
   createPathState<TPath extends Path<TValues>>(
     path: MaybeRef<TPath>,
-    config?: Partial<PathStateConfig>,
+    config?: Partial<PathStateConfig<TOutput[TPath]>>,
   ): PathState<PathValue<TValues, TPath>>;
   getPathState<TPath extends Path<TValues>>(path: TPath): PathState<PathValue<TValues, TPath>> | undefined;
   getAllPathStates(): PathState[];
@@ -383,7 +402,7 @@ export interface PrivateFormContext<TValues extends GenericObject = GenericObjec
   ): Ref<BaseInputBinds<TValue> & TExtras>;
 }
 
-export interface FormContext<TValues extends GenericObject = GenericObject, TOutput = TValues>
+export interface FormContext<TValues extends GenericObject = GenericObject, TOutput extends GenericObject = TValues>
   extends Omit<
     PrivateFormContext<TValues, TOutput>,
     | 'formId'
diff --git a/packages/vee-validate/src/useFieldState.ts b/packages/vee-validate/src/useFieldState.ts
index bf9dac06..b31de274 100644
--- a/packages/vee-validate/src/useFieldState.ts
+++ b/packages/vee-validate/src/useFieldState.ts
@@ -18,14 +18,14 @@ export interface FieldStateComposable<TValue = unknown> {
   setState(state: Partial<StateSetterInit<TValue>>): void;
 }
 
-export interface StateInit<TValue = unknown> {
-  modelValue: MaybeRef<TValue>;
+export interface StateInit<TInput = unknown, TOutput = TInput> {
+  modelValue: MaybeRef<TInput>;
   form?: PrivateFormContext;
   bails: boolean;
   label?: MaybeRefOrGetter<string | undefined>;
   type?: InputType;
-  validate?: FieldValidator;
-  schema?: MaybeRefOrGetter<TypedSchema<TValue> | undefined>;
+  validate?: FieldValidator<TOutput>;
+  schema?: MaybeRefOrGetter<TypedSchema<TInput> | undefined>;
 }
 
 let ID_COUNTER = 0;
diff --git a/packages/vee-validate/src/useForm.ts b/packages/vee-validate/src/useForm.ts
index a6265b7b..9d1c18ce 100644
--- a/packages/vee-validate/src/useForm.ts
+++ b/packages/vee-validate/src/useForm.ts
@@ -26,6 +26,7 @@ import {
   ValidationResult,
   FormState,
   FormValidationResult,
+  FlattenAndMapPathsValidationResult,
   PrivateFormContext,
   FormContext,
   FormErrors,
@@ -109,7 +110,7 @@ function resolveInitialValues<TValues extends GenericObject = GenericObject>(opt
 
 export function useForm<
   TValues extends GenericObject = GenericObject,
-  TOutput = TValues,
+  TOutput extends GenericObject = TValues,
   TSchema extends FormSchema<TValues> | TypedSchema<TValues, TOutput> =
     | FormSchema<TValues>
     | TypedSchema<TValues, TOutput>,
@@ -266,10 +267,10 @@ export function useForm<
 
   const schema = opts?.validationSchema;
 
-  function createPathState<TValue>(
-    path: MaybeRefOrGetter<Path<TValues>>,
-    config?: Partial<PathStateConfig>,
-  ): PathState<TValue> {
+  function createPathState<TPath extends Path<TValues>>(
+    path: MaybeRefOrGetter<TPath>,
+    config?: Partial<PathStateConfig<TOutput[TPath]>>,
+  ): PathState<TValues[TPath], TOutput[TPath]> {
     const initialValue = computed(() => getFromPath(initialValues.value, toValue(path)));
     const pathStateExists = pathStateLookup.value[toValue(path)];
     const isCheckboxOrRadio = config?.type === 'checkbox' || config?.type === 'radio';
@@ -285,7 +286,7 @@ export function useForm<
       pathStateExists.fieldsCount++;
       pathStateExists.__flags.pendingUnmount[id] = false;
 
-      return pathStateExists as PathState<TValue>;
+      return pathStateExists as PathState<TValues[TPath], TOutput[TPath]>;
     }
 
     const currentValue = computed(() => getFromPath(formValues, toValue(path)));
@@ -336,7 +337,7 @@ export function useForm<
       dirty: computed(() => {
         return !isEqual(unref(currentValue), unref(initialValue));
       }),
-    }) as PathState<TValue>;
+    }) as PathState<TValues[TPath], TOutput[TPath]>;
 
     pathStates.value.push(state);
     pathStateLookup.value[pathValue] = state;
@@ -432,11 +433,17 @@ export function useForm<
 
           return validation;
         },
-        { valid: formResult.valid, results: {}, errors: {} } as FormValidationResult<TValues>,
+        {
+          valid: formResult.valid,
+          results: {},
+          errors: {},
+          source: formResult.source,
+        } as FormValidationResult<TValues>,
       );
 
       if (formResult.values) {
         results.values = formResult.values;
+        results.source = formResult.source;
       }
 
       keysOf(results.results).forEach(path => {
@@ -510,7 +517,7 @@ export function useForm<
   function makeSubmissionFactory(onlyControlled: boolean) {
     return function submitHandlerFactory<TReturn = unknown>(
       fn?: SubmissionHandler<TValues, TOutput, TReturn>,
-      onValidationError?: InvalidSubmissionHandler<TValues>,
+      onValidationError?: InvalidSubmissionHandler<TValues, TOutput>,
     ) {
       return function submissionHandler(e: unknown) {
         if (e instanceof Event) {
@@ -530,8 +537,12 @@ export function useForm<
             if (result.valid && typeof fn === 'function') {
               const controlled = deepCopy(controlledValues.value);
               let submittedValues = (onlyControlled ? controlled : values) as unknown as TOutput;
+
               if (result.values) {
-                submittedValues = result.values;
+                submittedValues =
+                  result.source === 'schema'
+                    ? (result.values as TOutput)
+                    : Object.assign({}, submittedValues, result.values);
               }
 
               return fn(submittedValues, {
@@ -817,7 +828,7 @@ export function useForm<
     let newValues = deepCopy(resetState?.values ? resetState.values : originalInitialValues.value);
     newValues = opts?.force ? newValues : merge(originalInitialValues.value, newValues);
     newValues = isTypedSchema(schema) && isCallable(schema.cast) ? schema.cast(newValues) : newValues;
-    setInitialValues(newValues);
+    setInitialValues(newValues, { force: opts?.force });
     mutateAllPathState(state => {
       state.__flags.pendingReset = true;
       state.validated = false;
@@ -859,14 +870,16 @@ export function useForm<
             key: state.path,
             valid: true,
             errors: [],
+            value: undefined,
           });
         }
 
-        return state.validate(opts).then((result: ValidationResult) => {
+        return state.validate(opts).then(result => {
           return {
             key: state.path,
             valid: result.valid,
             errors: result.errors,
+            value: result.value,
           };
         });
       }),
@@ -874,14 +887,20 @@ export function useForm<
 
     isValidating.value = false;
 
-    const results: Partial<FlattenAndSetPathsType<TValues, ValidationResult>> = {};
+    const results: Partial<FlattenAndMapPathsValidationResult<TValues, TOutput>> = {};
     const errors: Partial<FlattenAndSetPathsType<TValues, string>> = {};
+    const values: Partial<TOutput> = {};
+
     for (const validation of validations) {
       results[validation.key as Path<TValues>] = {
         valid: validation.valid,
         errors: validation.errors,
       };
 
+      if (validation.value) {
+        setInPath(values, validation.key, validation.value);
+      }
+
       if (validation.errors.length) {
         errors[validation.key as Path<TValues>] = validation.errors[0];
       }
@@ -891,10 +910,15 @@ export function useForm<
       valid: validations.every(r => r.valid),
       results,
       errors,
+      values,
+      source: 'fields',
     };
   }
 
-  async function validateField(path: Path<TValues>, opts?: Partial<ValidationOptions>): Promise<ValidationResult> {
+  async function validateField<TPath extends Path<TValues>>(
+    path: TPath,
+    opts?: Partial<ValidationOptions>,
+  ): Promise<ValidationResult<TOutput[TPath]>> {
     const state = findPathState(path);
     if (state && opts?.mode !== 'silent') {
       state.validated = true;
@@ -945,7 +969,7 @@ export function useForm<
   async function _validateSchema(): Promise<FormValidationResult<TValues, TOutput>> {
     const schemaValue = unref(schema);
     if (!schemaValue) {
-      return { valid: true, results: {}, errors: {} };
+      return { valid: true, results: {}, errors: {}, source: 'none' };
     }
 
     isValidating.value = true;
@@ -1233,6 +1257,11 @@ function useFormMeta<TValues extends Record<string, unknown>>(
   });
 }
 
+interface SetFormInitialValuesOpts {
+  updateFields?: boolean;
+  force?: boolean;
+}
+
 /**
  * Manages the initial values prop
  */
@@ -1251,11 +1280,16 @@ function useFormInitialValues<TValues extends GenericObject>(
   // these only change when the user explicitly changes the initial values or when the user resets them with new values.
   const originalInitialValues = ref<PartialDeep<TValues>>(deepCopy(values)) as Ref<PartialDeep<TValues>>;
 
-  function setInitialValues(values: PartialDeep<TValues>, updateFields = false) {
-    initialValues.value = merge(deepCopy(initialValues.value) || {}, deepCopy(values));
-    originalInitialValues.value = merge(deepCopy(originalInitialValues.value) || {}, deepCopy(values));
+  function setInitialValues(values: PartialDeep<TValues>, opts?: SetFormInitialValuesOpts) {
+    if (opts?.force) {
+      initialValues.value = deepCopy(values);
+      originalInitialValues.value = deepCopy(values);
+    } else {
+      initialValues.value = merge(deepCopy(initialValues.value) || {}, deepCopy(values));
+      originalInitialValues.value = merge(deepCopy(originalInitialValues.value) || {}, deepCopy(values));
+    }
 
-    if (!updateFields) {
+    if (!opts?.updateFields) {
       return;
     }
 
@@ -1281,7 +1315,10 @@ function useFormInitialValues<TValues extends GenericObject>(
   };
 }
 
-function mergeValidationResults(a: ValidationResult, b?: ValidationResult): ValidationResult {
+function mergeValidationResults<TValue extends GenericObject>(
+  a: ValidationResult<TValue>,
+  b?: ValidationResult<TValue>,
+): ValidationResult<TValue> {
   if (!b) {
     return a;
   }
diff --git a/packages/vee-validate/src/useResetForm.ts b/packages/vee-validate/src/useResetForm.ts
index 9043a8fa..4bd1857a 100644
--- a/packages/vee-validate/src/useResetForm.ts
+++ b/packages/vee-validate/src/useResetForm.ts
@@ -1,5 +1,5 @@
 import { FormContextKey } from './symbols';
-import { FormState } from './types';
+import { FormState, ResetFormOpts } from './types';
 import { injectWithSelf, warn } from './utils';
 
 export function useResetForm<TValues extends Record<string, unknown> = Record<string, unknown>>() {
@@ -10,11 +10,11 @@ export function useResetForm<TValues extends Record<string, unknown> = Record<st
     }
   }
 
-  return function resetForm(state?: Partial<FormState<TValues>>) {
+  return function resetForm(state?: Partial<FormState<TValues>>, opts?: ResetFormOpts) {
     if (!form) {
       return;
     }
 
-    return form.resetForm(state);
+    return form.resetForm(state, opts);
   };
 }
diff --git a/packages/vee-validate/src/useValidateField.ts b/packages/vee-validate/src/useValidateField.ts
index f6a79992..7763516e 100644
--- a/packages/vee-validate/src/useValidateField.ts
+++ b/packages/vee-validate/src/useValidateField.ts
@@ -6,13 +6,13 @@ import { injectWithSelf, warn } from './utils';
 /**
  * Validates a single field
  */
-export function useValidateField(path?: MaybeRefOrGetter<string>) {
+export function useValidateField<TOutput>(path?: MaybeRefOrGetter<string>) {
   const form = injectWithSelf(FormContextKey);
   const field = path ? undefined : inject(FieldContextKey);
 
-  return function validateField(): Promise<ValidationResult> {
+  return function validateField(): Promise<ValidationResult<TOutput>> {
     if (field) {
-      return field.validate();
+      return field.validate() as Promise<ValidationResult<TOutput>>;
     }
 
     if (form && path) {
diff --git a/packages/vee-validate/src/useValidateForm.ts b/packages/vee-validate/src/useValidateForm.ts
index eb532ffe..32410330 100644
--- a/packages/vee-validate/src/useValidateForm.ts
+++ b/packages/vee-validate/src/useValidateForm.ts
@@ -15,7 +15,7 @@ export function useValidateForm<TValues extends Record<string, unknown> = Record
 
   return function validateField(): Promise<FormValidationResult<TValues>> {
     if (!form) {
-      return Promise.resolve({ results: {}, errors: {}, valid: true });
+      return Promise.resolve({ results: {}, errors: {}, valid: true, source: 'none' });
     }
 
     return form.validate();
diff --git a/packages/vee-validate/src/validate.ts b/packages/vee-validate/src/validate.ts
index aa731575..4bda8976 100644
--- a/packages/vee-validate/src/validate.ts
+++ b/packages/vee-validate/src/validate.ts
@@ -6,24 +6,27 @@ import {
   ValidationResult,
   GenericValidateFunction,
   TypedSchema,
+  FlattenAndMapPathsValidationResult,
   FormValidationResult,
   RawFormSchema,
   YupSchema,
+  GenericObject,
   TypedSchemaError,
   Path,
+  TypedSchemaContext,
 } from './types';
 import { isCallable, FieldValidationMetaInfo } from '../../shared';
 
 /**
  * Used internally
  */
-interface FieldValidationContext<TValue = unknown> {
+interface FieldValidationContext<TInput = unknown, TOutput = TInput> {
   name: string;
   label?: string;
   rules:
-    | GenericValidateFunction<TValue>
-    | GenericValidateFunction<TValue>[]
-    | TypedSchema<TValue>
+    | GenericValidateFunction<TInput>
+    | GenericValidateFunction<TInput>[]
+    | TypedSchema<TInput, TOutput>
     | string
     | Record<string, unknown>;
   bails: boolean;
@@ -40,18 +43,18 @@ interface ValidationOptions {
 /**
  * Validates a value against the rules.
  */
-export async function validate<TValue = unknown>(
-  value: TValue,
+export async function validate<TInput, TOutput>(
+  value: TInput,
   rules:
     | string
     | Record<string, unknown | unknown[]>
-    | GenericValidateFunction<TValue>
-    | GenericValidateFunction<TValue>[]
-    | TypedSchema<TValue>,
+    | GenericValidateFunction<TInput>
+    | GenericValidateFunction<TInput>[]
+    | TypedSchema<TInput, TOutput>,
   options: ValidationOptions = {},
-): Promise<ValidationResult> {
+): Promise<ValidationResult<TOutput>> {
   const shouldBail = options?.bails;
-  const field: FieldValidationContext<TValue> = {
+  const field: FieldValidationContext<TInput, TOutput> = {
     name: options?.name || '{field}',
     rules,
     label: options?.label,
@@ -60,24 +63,27 @@ export async function validate<TValue = unknown>(
   };
 
   const result = await _validate(field, value);
-  const errors = result.errors;
 
   return {
-    errors,
-    valid: !errors.length,
+    ...result,
+    valid: !result.errors.length,
   };
 }
 
 /**
  * Starts the validation process.
  */
-async function _validate<TValue = unknown>(field: FieldValidationContext<TValue>, value: TValue) {
-  if (isTypedSchema(field.rules) || isYupValidator(field.rules)) {
-    return validateFieldWithTypedSchema(value, field.rules);
+async function _validate<TInput = unknown, TOutput = TInput>(
+  field: FieldValidationContext<TInput, TOutput>,
+  value: TInput,
+) {
+  const rules = field.rules;
+  if (isTypedSchema(rules) || isYupValidator(rules)) {
+    return validateFieldWithTypedSchema(value, { ...field, rules });
   }
 
   // if a generic function or chain of generic functions
-  if (isCallable(field.rules) || Array.isArray(field.rules)) {
+  if (isCallable(rules) || Array.isArray(rules)) {
     const ctx = {
       field: field.label || field.name,
       name: field.name,
@@ -87,7 +93,7 @@ async function _validate<TValue = unknown>(field: FieldValidationContext<TValue>
     };
 
     // Normalize the pipeline
-    const pipeline = Array.isArray(field.rules) ? field.rules : [field.rules];
+    const pipeline = Array.isArray(rules) ? rules : [rules];
     const length = pipeline.length;
     const errors: ReturnType<typeof _generateFieldError>[] = [];
 
@@ -120,7 +126,7 @@ async function _validate<TValue = unknown>(field: FieldValidationContext<TValue>
 
   const normalizedContext = {
     ...field,
-    rules: normalizeRules(field.rules),
+    rules: normalizeRules(rules),
   };
   const errors: ReturnType<typeof _generateFieldError>[] = [];
   const rulesKeys = Object.keys(normalizedContext.rules);
@@ -161,9 +167,9 @@ function isYupError(err: unknown): err is YupError {
 function yupToTypedSchema(yupSchema: YupSchema): TypedSchema {
   const schema: TypedSchema = {
     __type: 'VVTypedSchema',
-    async parse(values: any) {
+    async parse(values: any, context?: TypedSchemaContext) {
       try {
-        const output = await yupSchema.validate(values, { abortEarly: false });
+        const output = await yupSchema.validate(values, { abortEarly: false, context: context?.formData || {} });
 
         return {
           output,
@@ -205,9 +211,12 @@ function yupToTypedSchema(yupSchema: YupSchema): TypedSchema {
 /**
  * Handles yup validation
  */
-async function validateFieldWithTypedSchema(value: unknown, schema: TypedSchema | YupSchema) {
-  const typedSchema = isTypedSchema(schema) ? schema : yupToTypedSchema(schema);
-  const result = await typedSchema.parse(value);
+async function validateFieldWithTypedSchema(
+  value: unknown,
+  context: FieldValidationContext<any> & { rules: TypedSchema | YupSchema },
+) {
+  const typedSchema = isTypedSchema(context.rules) ? context.rules : yupToTypedSchema(context.rules);
+  const result = await typedSchema.parse(value, { formData: context.formData });
 
   const messages: string[] = [];
   for (const error of result.errors) {
@@ -217,6 +226,7 @@ async function validateFieldWithTypedSchema(value: unknown, schema: TypedSchema
   }
 
   return {
+    value: result.value,
     errors: messages,
   };
 }
@@ -295,14 +305,14 @@ function fillTargetValues(params: unknown[] | Record<string, unknown>, crossTabl
   );
 }
 
-export async function validateTypedSchema<TValues, TOutput = TValues>(
+export async function validateTypedSchema<TValues extends GenericObject, TOutput extends GenericObject = TValues>(
   schema: TypedSchema<TValues> | YupSchema<TValues>,
   values: TValues,
 ): Promise<FormValidationResult<TValues, TOutput>> {
   const typedSchema = isTypedSchema(schema) ? schema : yupToTypedSchema(schema);
   const validationResult = await typedSchema.parse(deepCopy(values));
 
-  const results: Partial<Record<Path<TValues>, ValidationResult>> = {};
+  const results: Partial<FlattenAndMapPathsValidationResult<TValues, TOutput>> = {};
   const errors: Partial<Record<Path<TValues>, string>> = {};
   for (const error of validationResult.errors) {
     const messages = error.errors;
@@ -322,12 +332,13 @@ export async function validateTypedSchema<TValues, TOutput = TValues>(
     results,
     errors,
     values: validationResult.value,
+    source: 'schema',
   };
 }
 
-export async function validateObjectSchema<TValues, TOutput>(
+export async function validateObjectSchema<TValues extends GenericObject, TOutput extends GenericObject>(
   schema: RawFormSchema<TValues>,
-  values: TValues,
+  values: TValues | undefined,
   opts?: Partial<{ names: Record<string, { name: string; label: string }>; bailsMap: Record<string, boolean> }>,
 ): Promise<FormValidationResult<TValues, TOutput>> {
   const paths = keysOf(schema) as Path<TValues>[];
@@ -349,7 +360,7 @@ export async function validateObjectSchema<TValues, TOutput>(
   let isAllValid = true;
   const validationResults = await Promise.all(validations);
 
-  const results: Partial<Record<Path<TValues>, ValidationResult>> = {};
+  const results: Partial<FlattenAndMapPathsValidationResult<TValues, TOutput>> = {};
   const errors: Partial<Record<Path<TValues>, string>> = {};
   for (const result of validationResults) {
     results[result.path] = {
@@ -367,5 +378,6 @@ export async function validateObjectSchema<TValues, TOutput>(
     valid: isAllValid,
     results,
     errors,
+    source: 'schema',
   };
 }


Test patch:
diff --git a/packages/i18n/tests/index.spec.ts b/packages/i18n/tests/index.spec.ts
index 7ce69975..4922fab4 100644
--- a/packages/i18n/tests/index.spec.ts
+++ b/packages/i18n/tests/index.spec.ts
@@ -549,3 +549,252 @@ describe('interpolation preserves placeholders if not found', () => {
     expect(error.textContent).toContain('The name field must be between 0 and {max}');
   });
 });
+
+// begin - #4726 - custom interpolation options
+test('custom interpolation options - interpolates object params with short format', async () => {
+  configure({
+    generateMessage: localize(
+      'en',
+      {
+        messages: {
+          between: `The {{field}} field must be between {{min}} and {{max}}`,
+        },
+      },
+      {
+        prefix: '{{',
+        suffix: '}}',
+      },
+    ),
+  });
+
+  const wrapper = mountWithHoc({
+    template: `
+        <div>
+          <Field name="name" value="-1" :validateOnMount="true" :rules="{ between: { min: 1, max: 10 } }" v-slot="{ field, errors }">
+            <input v-bind="field" type="text">
+            <span id="error">{{ errors[0] }}</span>
+          </Field>
+        </div>
+      `,
+  });
+
+  const error = wrapper.$el.querySelector('#error');
+  // flush the pending validation.
+  await flushPromises();
+
+  // locale wasn't set.
+  expect(error.textContent).toContain('The name field must be between 1 and 10');
+});
+
+test('custom interpolation options - interpolates object params with short format and handlebars style interpolation', async () => {
+  configure({
+    generateMessage: localize(
+      'en',
+      {
+        messages: {
+          between: `The <%field%> field must be between <%min%> and <%max%>`,
+        },
+      },
+      {
+        prefix: '<%',
+        suffix: '%>',
+      },
+    ),
+  });
+
+  const wrapper = mountWithHoc({
+    template: `
+        <div>
+          <Field name="name" value="-1" :validateOnMount="true" :rules="{ between: { min: 1, max: 10 } }" v-slot="{ field, errors }">
+            <input v-bind="field" type="text">
+            <span id="error">{{ errors[0] }}</span>
+          </Field>
+        </div>
+      `,
+  });
+
+  const error = wrapper.$el.querySelector('#error');
+  // flush the pending validation.
+  await flushPromises();
+
+  // locale wasn't set.
+  expect(error.textContent).toContain('The name field must be between 1 and 10');
+});
+
+test('custom interpolation options - interpolates object params with extended format', async () => {
+  configure({
+    generateMessage: localize(
+      'en',
+      {
+        messages: {
+          between: `The {{field}} field must be between 0:{{min}} and 1:{{max}}`,
+        },
+      },
+      {
+        prefix: '{{',
+        suffix: '}}',
+      },
+    ),
+  });
+
+  const wrapper = mountWithHoc({
+    template: `
+        <div>
+          <Field name="name" value="-1" :validateOnMount="true" :rules="{ between: { min: 1, max: 10 } }" v-slot="{ field, errors }">
+            <input v-bind="field" type="text">
+            <span id="error">{{ errors[0] }}</span>
+          </Field>
+        </div>
+      `,
+  });
+
+  const error = wrapper.$el.querySelector('#error');
+  // flush the pending validation.
+  await flushPromises();
+
+  // locale wasn't set.
+  expect(error.textContent).toContain('The name field must be between 1 and 10');
+});
+
+test('custom interpolation options - interpolates array params', async () => {
+  configure({
+    generateMessage: localize(
+      'en',
+      {
+        messages: {
+          between: 'The {{field}} field must be between 0:{{min}} and 1:{{max}}',
+        },
+      },
+      {
+        prefix: '{{',
+        suffix: '}}',
+      },
+    ),
+  });
+
+  const wrapper = mountWithHoc({
+    template: `
+        <div>
+          <Field name="name" value="-1" :validateOnMount="true" :rules="{ between: [1, 10] }" v-slot="{ field, errors }">
+            <input v-bind="field" type="text">
+            <span id="error">{{ errors[0] }}</span>
+          </Field>
+        </div>
+      `,
+  });
+
+  const error = wrapper.$el.querySelector('#error');
+  // flush the pending validation.
+  await flushPromises();
+
+  // locale wasn't set.
+  expect(error.textContent).toContain('The name field must be between 1 and 10');
+});
+
+test('custom interpolation options - interpolates string params', async () => {
+  configure({
+    generateMessage: localize(
+      'en',
+      {
+        messages: {
+          between: 'The {{field}} field must be between 0:{{min}} and 1:{{max}}',
+        },
+      },
+      {
+        prefix: '{{',
+        suffix: '}}',
+      },
+    ),
+  });
+
+  const wrapper = mountWithHoc({
+    template: `
+        <div>
+          <Field name="name" value="-1" :validateOnMount="true" rules="between:1,10" v-slot="{ field, errors }">
+            <input v-bind="field" type="text">
+            <span id="error">{{ errors[0] }}</span>
+          </Field>
+        </div>
+      `,
+  });
+
+  const error = wrapper.$el.querySelector('#error');
+  // flush the pending validation.
+  await flushPromises();
+
+  // locale wasn't set.
+  expect(error.textContent).toContain('The name field must be between 1 and 10');
+});
+
+describe('interpolation preserves placeholders if not found', () => {
+  test('custom interpolation options - array format', async () => {
+    configure({
+      generateMessage: localize(
+        'en',
+        {
+          messages: {
+            between: 'The {{field}} field must be between 0:{{min}} and 1:{{max}}',
+          },
+        },
+        {
+          prefix: '{{',
+          suffix: '}}',
+        },
+      ),
+    });
+
+    const wrapper = mountWithHoc({
+      template: `
+        <div>
+          <Field name="name" value="-1" :validateOnMount="true" :rules="{ between: [1] }" v-slot="{ field, errors }">
+            <input v-bind="field" type="text">
+            <span id="error">{{ errors[0] }}</span>
+          </Field>
+        </div>
+      `,
+    });
+
+    const error = wrapper.$el.querySelector('#error');
+    // flush the pending validation.
+    await flushPromises();
+
+    // locale wasn't set.
+    expect(error.textContent).toContain('The name field must be between 1 and 1:{{max}}');
+  });
+
+  test('custom interpolation options - object format', async () => {
+    configure({
+      generateMessage: localize(
+        'en',
+        {
+          messages: {
+            between: 'The {{field}} field must be between 0:{{min}} and 1:{{max}}',
+          },
+        },
+        {
+          prefix: '{{',
+          suffix: '}}',
+        },
+      ),
+    });
+
+    const wrapper = mountWithHoc({
+      template: `
+        <div>
+          <Field name="name" value="-1" :validateOnMount="true" :rules="{ between: { min: 0 } }" v-slot="{ field, errors }">
+            <input v-bind="field" type="text">
+            <span id="error">{{ errors[0] }}</span>
+          </Field>
+        </div>
+      `,
+    });
+
+    const error = wrapper.$el.querySelector('#error');
+    // flush the pending validation.
+    await flushPromises();
+
+    // locale wasn't set.
+    expect(error.textContent).toContain('The name field must be between 0 and {{max}}');
+  });
+});
+// end - #4726 - custom interpolation options tests
diff --git a/packages/vee-validate/tests/Form.spec.ts b/packages/vee-validate/tests/Form.spec.ts
index 792f9734..607e7e9c 100644
--- a/packages/vee-validate/tests/Form.spec.ts
+++ b/packages/vee-validate/tests/Form.spec.ts
@@ -3178,3 +3178,47 @@ test('removes proper pathState when field is unmounting', async () => {
   expect(form.meta.value.valid).toBe(true);
   expect(form.getAllPathStates()).toMatchObject([{ id: 0, path: 'foo' }]);
 });
+
+test('provides form values as yup context refs', async () => {
+  mountWithHoc({
+    setup() {
+      const pw = yup.string().required().min(3).label('Password');
+      const cpw = yup
+        .string()
+        .required()
+        .oneOf([yup.ref('$password')])
+        .label('Confirm Password');
+
+      return {
+        pw,
+        cpw,
+      };
+    },
+    template: `
+      <VForm  v-slot="{ errors }">
+        <Field id="password" name="password" type="password" :rules="pw" />
+        <span id="passwordErr">{{ errors.password }}</span>
+
+        <Field id="confirmPassword" name="confirmPassword" type="password" :rules="cpw" />
+        <span id="confirmPasswordErr">{{ errors.confirmPassword }}</span>
+
+        <button>Validate</button>
+      </VForm>
+    `,
+  });
+
+  const pwError = document.querySelector('#passwordErr');
+  const cpwError = document.querySelector('#confirmPasswordErr');
+
+  await flushPromises();
+  setValue(document.querySelector('#password') as HTMLInputElement, '123');
+  setValue(document.querySelector('#confirmPassword') as HTMLInputElement, '12');
+  await flushPromises();
+
+  expect(pwError?.textContent).toBeFalsy();
+  expect(cpwError?.textContent).toBeTruthy();
+  setValue(document.querySelector('#confirmPassword') as HTMLInputElement, '123');
+  await flushPromises();
+  expect(pwError?.textContent).toBeFalsy();
+  expect(cpwError?.textContent).toBeFalsy();
+});
diff --git a/packages/vee-validate/tests/useForm.spec.ts b/packages/vee-validate/tests/useForm.spec.ts
index e99cfdf7..f1a2ce4f 100644
--- a/packages/vee-validate/tests/useForm.spec.ts
+++ b/packages/vee-validate/tests/useForm.spec.ts
@@ -226,6 +226,7 @@ describe('useForm()', () => {
     const result = await validate();
     expect(result).toEqual({
       valid: false,
+      source: 'fields',
       errors: {
         field1: REQUIRED_MESSAGE,
         field2: REQUIRED_MESSAGE,
@@ -240,6 +241,7 @@ describe('useForm()', () => {
           errors: [REQUIRED_MESSAGE],
         },
       },
+      values: {},
     });
   });
 
@@ -269,6 +271,7 @@ describe('useForm()', () => {
     const result = await pending;
     expect(result).toEqual({
       valid: false,
+      source: 'schema',
       errors: {
         field1: REQUIRED_MESSAGE,
         field2: REQUIRED_MESSAGE,
@@ -1316,6 +1319,37 @@ describe('useForm()', () => {
     expect(form.values.fname).toBe('test');
   });
 
+  test('reset should be able to set initial values to undefined with force: true', async () => {
+    let form!: FormContext<{ lname: string; fname: string }>;
+
+    mountWithHoc({
+      setup() {
+        form = useForm({
+          initialValues: { fname: '123', lname: '456' },
+        });
+        return {};
+      },
+      template: `<div></div>`,
+    });
+
+    await flushPromises();
+
+    // FAIL
+    form.resetForm({ values: { lname: '789' } }, { force: true });
+    expect(form.meta.value.initialValues?.fname).toBeUndefined(); // This is not undefined. It stayed at value '123'
+    expect(form.meta.value.initialValues?.lname).toBe('789');
+
+    // FAIL
+    form.resetForm({ values: {} }, { force: true });
+    expect(form.meta.value.initialValues?.fname).toBeUndefined();
+    expect(form.meta.value.initialValues?.lname).toBeUndefined();
+
+    // PASS
+    form.resetForm({ values: { lname: undefined, fname: undefined } }, { force: true });
+    expect(form.meta.value.initialValues?.fname).toBeUndefined();
+    expect(form.meta.value.initialValues?.lname).toBeUndefined();
+  });
+
   test('reset should not make unspecified values undefined', async () => {
     let form!: FormContext<{ fname: string; lname: string }>;
 
diff --git a/packages/vee-validate/tests/useResetForm.spec.ts b/packages/vee-validate/tests/useResetForm.spec.ts
index 1d96584c..fba1973e 100644
--- a/packages/vee-validate/tests/useResetForm.spec.ts
+++ b/packages/vee-validate/tests/useResetForm.spec.ts
@@ -1,4 +1,4 @@
-import { useField, useForm, useResetForm } from '@/vee-validate';
+import { FormContext, useField, useForm, useResetForm } from '@/vee-validate';
 import { mountWithHoc, setValue, flushPromises } from './helpers';
 
 describe('useResetForm()', () => {
@@ -73,4 +73,30 @@ describe('useResetForm()', () => {
     expect(spy).toHaveBeenCalled();
     spy.mockRestore();
   });
+
+  test('resets a form with passed options', async () => {
+    let resetForm: any;
+    let form!: FormContext<{ fname: string; lname: string }>;
+
+    mountWithHoc({
+      setup() {
+        form = useForm({
+          initialValues: { fname: '123', lname: '456' },
+        });
+
+        resetForm = useResetForm();
+
+        return {};
+      },
+      template: `
+        <div></div>
+      `,
+    });
+
+    await flushPromises();
+
+    resetForm({ values: { fname: 'test' } }, { force: true });
+    expect(form.values.lname).toBeUndefined();
+    expect(form.values.fname).toBe('test');
+  });
 });
diff --git a/packages/yup/tests/yup.spec.ts b/packages/yup/tests/yup.spec.ts
index 85612598..4446d615 100644
--- a/packages/yup/tests/yup.spec.ts
+++ b/packages/yup/tests/yup.spec.ts
@@ -545,3 +545,94 @@ test('reports required state for field-level schemas without a form context', as
     }),
   );
 });
+
+test('uses transformed value as submitted value', async () => {
+  const onSubmitSpy = vi.fn();
+  let onSubmit!: () => void;
+
+  const wrapper = mountWithHoc({
+    setup() {
+      const { handleSubmit } = useForm<{
+        req: string;
+      }>();
+
+      const { value } = useField('test', toTypedSchema(yup.string().transform(val => `modified: ${val}`)));
+
+      // submit now
+      onSubmit = handleSubmit(onSubmitSpy);
+
+      return {
+        value,
+      };
+    },
+    template: `
+      <div>
+        <input v-model="value" type="text">
+      </div>
+    `,
+  });
+
+  const input = wrapper.$el.querySelector('input');
+
+  setValue(input, '12345678');
+  await flushPromises();
+  onSubmit();
+  await flushPromises();
+  await expect(onSubmitSpy).toHaveBeenCalledTimes(1);
+  await expect(onSubmitSpy).toHaveBeenLastCalledWith(
+    expect.objectContaining({
+      test: 'modified: 12345678',
+    }),
+    expect.anything(),
+  );
+});
+
+test('supports yup.strip', async () => {
+  const onSubmitSpy = vi.fn();
+  let onSubmit!: () => void;
+
+  const wrapper = mountWithHoc({
+    setup() {
+      const { handleSubmit } = useForm({
+        validationSchema: toTypedSchema(
+          yup.object({
+            test: yup.string().strip(),
+            name: yup.string(),
+          }),
+        ),
+      });
+
+      const { value: test } = useField('test');
+      const { value: name } = useField('name');
+
+      // submit now
+      onSubmit = handleSubmit(onSubmitSpy);
+
+      return { test, name };
+    },
+    template: `
+      <div>
+        <input id="test" v-model="test" type="text">
+        <input id="name" v-model="name" type="text">
+      </div>
+    `,
+  });
+
+  await flushPromises();
+
+  const test = wrapper.$el.querySelector('#test');
+  const name = wrapper.$el.querySelector('#name');
+
+  setValue(test, '12345678');
+  setValue(name, '12345678');
+  await flushPromises();
+  onSubmit();
+  await flushPromises();
+  await expect(onSubmitSpy).toHaveBeenCalledTimes(1);
+  await expect(onSubmitSpy).toHaveBeenLastCalledWith(
+    expect.not.objectContaining({
+      test: expect.anything(),
+    }),
+    expect.anything(),
+  );
+});
diff --git a/packages/zod/tests/zod.spec.ts b/packages/zod/tests/zod.spec.ts
index a25342dd..b6d23888 100644
--- a/packages/zod/tests/zod.spec.ts
+++ b/packages/zod/tests/zod.spec.ts
@@ -660,3 +660,45 @@ test('reports required state for field-level schemas without a form context', as
     }),
   );
 });
+
+test('uses transformed value as submitted value', async () => {
+  const onSubmitSpy = vi.fn();
+  let onSubmit!: () => void;
+
+  const wrapper = mountWithHoc({
+    setup() {
+      const { handleSubmit } = useForm<{
+        test: string;
+      }>();
+
+      const testRules = toTypedSchema(z.string().transform(value => `modified: ${value}`));
+      const { value } = useField('test', testRules);
+
+      // submit now
+      onSubmit = handleSubmit(onSubmitSpy);
+
+      return {
+        value,
+      };
+    },
+    template: `
+      <div>
+          <input v-model="value" type="text">
+      </div>
+    `,
+  });
+
+  const input = wrapper.$el.querySelector('input');
+
+  setValue(input, '12345678');
+  await flushPromises();
+  onSubmit();
+  await flushPromises();
+  await expect(onSubmitSpy).toHaveBeenCalledTimes(1);
+  await expect(onSubmitSpy).toHaveBeenLastCalledWith(
+    expect.objectContaining({
+      test: 'modified: 12345678',
+    }),
+    expect.anything(),
+  );
+});