Skip to content

Internationalization (i18n)

Vorm has built-in support for internationalization. All text in your forms — labels, placeholders, help text, and validation messages — can be reactive and automatically update when the locale changes.

Key Features

  • Reactive text — Labels, placeholders, and helpText update when locale changes
  • No re-validation — Language changes update messages without re-running validation
  • No computed() needed — Plain functions work directly
  • FormContext access — Dynamic text based on form state
  • Built-in translations — English and German included for validators

ReactiveString

All text properties in Vorm accept a ReactiveString:

ts
type ReactiveString =
  | string                              // Static: "Username"
  | Ref<string>                         // Vue Ref: ref('Username')
  | ComputedRef<string>                 // Vue Computed
  | (() => string)                      // Function
  | ((ctx: FormContext) => string);     // Function with form context

No computed() wrapper needed:

ts
const schema: VormSchema = [
  {
    name: 'username',
    label: () => locale.value === 'en' ? 'Username' : 'Benutzername',
    placeholder: () => locale.value === 'en' ? 'Enter username' : 'Benutzername eingeben',
  }
];

With Vue I18n

ts
import { useI18n } from 'vue-i18n';

const { t } = useI18n();

const schema: VormSchema = [
  {
    name: 'username',
    label: () => t('form.username'),
    placeholder: () => t('form.username.placeholder'),
  }
];

With Nuxt I18n

ts
const { t } = useI18n();

const schema: VormSchema = [
  {
    name: 'email',
    label: () => t('form.email'),
    validation: [
      { rule: 'required' },
      { rule: 'email' }
    ]
  }
];

FormContext

Functions can receive a FormContext parameter for dynamic text based on form state:

ts
interface FormContext {
  formData: Record<string, any>;
  readonly errors: Record<string, string | null>;
  readonly isValid: boolean;
  readonly isDirty: boolean;
  readonly isTouched: boolean;
  readonly touched: Record<string, boolean>;
  readonly dirty: Record<string, boolean>;
}

Dynamic Placeholder

ts
{
  name: 'email',
  placeholder: (ctx) => ctx.formData.username
    ? `${ctx.formData.username}@example.com`
    : 'your@email.com',
}

Dynamic Help Text

ts
{
  name: 'password',
  helpText: (ctx) => ctx.formData.email
    ? `Password for ${ctx.formData.email}`
    : 'At least 8 characters',
}

Dynamic Validation Message

ts
{
  name: 'age',
  validation: [{
    rule: between(18, 100),
    message: (ctx) => `Age must be 18-100 (you entered: ${ctx.formData.age || 'nothing'})`
  }]
}

Validation Messages

Built-in i18n Keys

All built-in validators return message keys that are automatically resolved:

ValidatorKeyParams
requiredvorm.validation.required-
emailvorm.validation.email-
minLengthvorm.validation.minLength[min]
maxLengthvorm.validation.maxLength[max]
minvorm.validation.min[min]
maxvorm.validation.max[max]
betweenvorm.validation.between[min, max]
stepvorm.validation.step[step]
patternvorm.validation.pattern-
matchFieldvorm.validation.matchField[fieldName]
urlvorm.validation.url-
integervorm.validation.integer-
alphavorm.validation.alpha-

Default Messages

Vorm includes default translations:

English:

This field is required.
Please enter a valid email address.
This field must be at least {0} characters.
...

German:

Dieses Feld ist erforderlich.
Bitte geben Sie eine gültige E-Mail-Adresse ein.
Dieses Feld muss mindestens {0} Zeichen lang sein.
...

Custom i18n Messages

Override or add translations:

ts
const vorm = useVorm(schema, {
  i18n: {
    'vorm.validation.required': 'Campo obligatorio',
    'vorm.validation.minLength': 'Mínimo {0} caracteres',
    'my.custom.key': 'Custom message',
  }
});

Message Interpolation

Messages use {0}, {1}, etc. for parameter interpolation:

"This field must be at least {0} characters."
// With minLength(5) → "This field must be at least 5 characters."

"Value must be between {0} and {1}."
// With between(18, 65) → "Value must be between 18 and 65."

How It Works

Two-Layer Error System

Vorm uses a clever two-layer system to update messages without re-validation:

  1. Storage Layer — Stores error metadata (message reference + params)
  2. Display Layer — Resolves messages reactively to strings

When locale changes:

  1. Display layer re-computes
  2. Messages update with new locale
  3. Validation does NOT re-run

This means users can switch languages without losing their validation state or triggering unnecessary API calls.

Complete Example

vue
<script setup lang="ts">
import { ref } from 'vue';
import { useVorm, type VormSchema, minLength, between } from 'vorm-vue';

const locale = ref('en');

const schema: VormSchema = [
  {
    name: 'username',
    type: 'text',
    label: () => locale.value === 'en' ? 'Username' : 'Benutzername',
    placeholder: () => locale.value === 'en' ? 'Enter username' : 'Benutzername eingeben',
    validation: [
      { rule: 'required' },
      { rule: minLength(3) }
    ]
  },
  {
    name: 'email',
    type: 'email',
    label: () => locale.value === 'en' ? 'Email' : 'E-Mail',
    placeholder: (ctx) => ctx.formData.username
      ? `${ctx.formData.username}@example.com`
      : (locale.value === 'en' ? 'your@email.com' : 'ihre@email.de'),
    validation: [
      { rule: 'required' },
      { rule: 'email' }
    ]
  },
  {
    name: 'age',
    type: 'number',
    label: () => locale.value === 'en' ? 'Age' : 'Alter',
    helpText: (ctx) => ctx.formData.age
      ? (locale.value === 'en' ? `You are ${ctx.formData.age} years old` : `Sie sind ${ctx.formData.age} Jahre alt`)
      : (locale.value === 'en' ? 'Enter your age' : 'Geben Sie Ihr Alter ein'),
    validation: [
      { rule: between(18, 120) }
    ]
  }
];

const vorm = useVorm(schema);
</script>

<template>
  <div>
    <button @click="locale = 'en'">English</button>
    <button @click="locale = 'de'">Deutsch</button>

    <VormProvider :vorm="vorm">
      <AutoVorm />
    </VormProvider>
  </div>
</template>

MIT Licensed