Localisation adapters
Keep validation rules in the schema and translations in your locale catalogue. Verific describes recognised Standard Schema issues with stable identifiers such as required, invalidEmail and minLength; an adapter turns that structured description into text only when the component reads an error.
Choose the adapter for the locale library your application already uses:
| Application locale library | Adapter | Guide |
|---|---|---|
Vue I18n or @nuxtjs/i18n | @verific/vue-i18n | Vue I18n |
| i18next or i18next-vue | @verific/i18next | i18next |
| Paraglide JS | @verific/paraglide | Paraglide |
| Another catalogue | @verific/i18n | Custom adapters |
Each adapter is optional and independently installed. Core does not depend on a locale library.
The shared message contract
Suppose an invalidEmail issue occurs at email and the form uses messagePrefix: 'forms.signup'. With fallbackPrefix: 'errors', every catalogue adapter uses this key-first order:
forms.signup.email.invalidEmailin every configured locale, in locale order;errors.invalidEmailin every configured locale, in locale order;- the original Standard Schema message when every configured resolver misses.
Key-first means a form-specific translation in a fallback locale wins over a shared translation in the active locale. Nested string and number paths are included automatically, for example forms.checkout.contacts.0.email.invalidEmail.
The schema remains locale-independent:
const { errorsFor, hasError, validate, validateFor } = useValidation(schema, form, {
messagePrefix: 'forms.signup',
})Adapters receive semantic interpolation values such as minimum, maximum and expected. Length issues also receive count for plural selection. Verific does not copy model values into translation parameters or require translated prose in the schema.
Missing messages
All catalogue adapters support the same policy:
missing | Behaviour after the complete resolver chain misses |
|---|---|
| omitted | Warn in development; remain silent in production |
'silent' | Return the schema message without reporting |
'warn' | Emit an actionable development warning |
'throw' | Throw, so an exercised test or build-render path fails |
| callback | Receive the structured final diagnostic |
Use strict mode in tests:
const messages = vueI18nMessages(i18n.global, {
fallbackPrefix: 'errors',
missing: 'throw',
})
await validate()
expect(() => errorsFor('email')).not.toThrow()Resolution is lazy, so the test must read errors, errorsFor() or errorFor() after validation. A diagnostic contains the complete ordered key-and-locale attempts from every adapter in the resolver chain. A later resolver success suppresses the diagnostic. Warnings are deduplicated with a finite per-adapter cache to prevent noise; that cache is not proof that a catalogue is complete.
Static analysis cannot discover every runtime schema outcome, prefix and dynamic path. Combine missing: 'throw' in exercised form tests with your locale library's catalogue typing or locale-parity check.
Locale changes do not revalidate
Error strings are derived lazily from stored structured issues. Adapters read their caller-owned locale source while Vue evaluates those strings, so changing locale updates rendered text without rerunning the schema.
- Select Validate email once.
- Change Message language.
- Notice that the message changes while Validation runs does not.
- Select Demonstrate missing-key fallback to see the raw schema fallback and exact missing key and locale without another validation run. Select it again to restore the translated message.
View the source used by this example
<script setup lang="ts">
import type { CatalogueMissingMessageDiagnostic } from '@verific/i18n'
import { useValidation } from '@verific/core'
import { vueI18nMessages } from '@verific/vue-i18n'
import { nextTick, ref } from 'vue'
import { createI18n } from 'vue-i18n'
import { z } from 'zod'
const i18n = createI18n({
legacy: false,
locale: 'en',
fallbackLocale: false,
missingWarn: false,
fallbackWarn: false,
messages: {
en: { forms: { newsletter: { email: {
minLength: 'Enter your email address',
invalidEmail: 'Enter a valid email address',
} } } },
es: { forms: { newsletter: { email: {
minLength: 'Introduce tu dirección de correo electrónico',
invalidEmail: 'Introduce una dirección de correo electrónico válida',
} } } },
},
})
const composer = i18n.global
const locale = composer.locale
const email = ref('not-an-email')
const runCount = ref(0)
const demonstrateMissing = ref(false)
const missingSequence = ref(0)
const missingDiagnostic = ref('')
const schema = z.object({
email: z.string().min(1).email('Schema fallback: enter a valid email address'),
}).superRefine(() => {
runCount.value += 1
})
const outcome = ref('Validate once, then change the locale.')
const { errorsFor, isValidating, validate } = useValidation(
schema,
{ email },
{
messagePrefix: 'forms.newsletter',
messages: vueI18nMessages(composer, {
key: ({ defaultKeys }) => demonstrateMissing.value
? [`demo.missing.${missingSequence.value}`]
: defaultKeys,
missing: reportMissing,
}),
},
)
async function onSubmit() {
const result = await validate()
if (!result.success) {
outcome.value = 'The committed error is translated when the locale changes.'
await nextTick()
document.getElementById('localised-email')?.focus()
return
}
outcome.value = 'The email address is valid.'
}
function reportMissing(diagnostic: CatalogueMissingMessageDiagnostic) {
const attempt = diagnostic.attempts[0]
const key = attempt?.keys[0] ?? 'unknown key'
const attemptedLocale = attempt?.locale ?? 'unknown locale'
missingDiagnostic.value = `Missing catalogue message. Add "${key}" for locale "${attemptedLocale}".`
}
function toggleMissingDemonstration() {
missingDiagnostic.value = ''
demonstrateMissing.value = !demonstrateMissing.value
if (demonstrateMissing.value) {
missingSequence.value += 1
}
}
</script>
<template>
<div class="verific-example">
<form novalidate @submit.prevent="onSubmit">
<div class="verific-example__toolbar">
<div class="verific-example__field">
<label for="localised-locale">Message language</label>
<select id="localised-locale" v-model="locale" data-validation-skip>
<option value="en">
English
</option>
<option value="es">
Español
</option>
</select>
</div>
<p class="verific-example__counter" aria-live="polite">
Validation runs: <strong>{{ runCount }}</strong>
</p>
</div>
<div class="verific-example__field">
<label for="localised-email">Email address</label>
<input
id="localised-email"
v-model="email"
type="email"
autocomplete="email"
:aria-invalid="errorsFor('email').length > 0"
aria-describedby="localised-email-errors"
>
<ul id="localised-email-errors" class="verific-example__errors" aria-live="polite" aria-atomic="true" :lang="locale">
<li v-for="(error, index) in errorsFor('email')" :key="`${index}:${error}`">
{{ error }}
</li>
</ul>
</div>
<div class="verific-example__actions">
<button type="submit" :disabled="isValidating">
{{ isValidating ? 'Validating…' : 'Validate email' }}
</button>
<button
type="button"
:aria-pressed="demonstrateMissing"
aria-controls="localised-missing-diagnostic"
@click="toggleMissingDemonstration"
>
Demonstrate missing-key fallback
</button>
</div>
<p
v-show="missingDiagnostic"
id="localised-missing-diagnostic"
class="verific-example__outcome"
role="status"
aria-live="polite"
>
{{ missingDiagnostic }}
</p>
<p class="verific-example__outcome" role="status" aria-live="polite">
{{ outcome }}
</p>
</form>
</div>
</template>For server rendering, create or obtain locale state inside the request or application boundary. Do not store a mutable Composer, i18next instance, locale ref or Verific adapter in a process-global singleton.
Compatibility
The tested baselines match each adapter package's peer dependencies.
| Adapter | Direct locale runtime | Supported range | Tested baseline |
|---|---|---|---|
@verific/vue-i18n | vue-i18n | >=11.4 <12 | 11.4.10 |
@verific/i18next | i18next | >=26 <27 | 26.4.0 |
@verific/i18next | vue (reactivity) | ^3.4.26 | ^3.5.42 |
@verific/paraglide | @inlang/paraglide-js | >=2 <3 | 2.25.0 |
Vue I18n, i18next and Paraglide are not transitive requirements of one another. See Nuxt for automatic Vue I18n integration and request-safe manual setup for the other adapters, or read the message-resolution reference for the core contracts.
