Skip to content

Commit

Permalink
feature(core): pass more parameters to $message to help with i18n s…
Browse files Browse the repository at this point in the history
…upport (vuelidate#881)

* refactor: improve $message to work easier with i18n

* chore: add tests

* docs: add docs about i18n
  • Loading branch information
dobromir-hristov authored Jun 30, 2021
1 parent b259587 commit ca6bb32
Show file tree
Hide file tree
Showing 10 changed files with 345 additions and 14 deletions.
62 changes: 61 additions & 1 deletion packages/docs/src/advanced_usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -411,9 +411,69 @@ export default {
To clear out the external results, you can again, use the `$clearExternalResults()` method

```js
async function validate() {
async function validate () {
this.$v.$clearExternalResults()
// perform validations
const result = await this.runAsyncValidators()
}
```

## i18n support

Validator messages are very flexible. You can wrap each validator with your own helper, that returns a translated error message, based on the
validator name. Let's define a few validators:

```js
// @/utils/validators.js
import { withI18nMessage } from '@/utils/withI18nMessage'
import * as validators from '@vuelidate/validators'

export const required = withI18nMessage(validators.required)
export const minLength = withI18nMessage(validators.minLength)
```

Now lets see how that `withI18nMessage` helpers would look like:

```js
// @/utils/withI18nMessage.js
import { i18n } from "@/i18n"

const { t } = i18n.global || i18n // this should work for both Vue 2 and Vue 3 versions of vue-i18n

export const withI18nMessage = (validator) => helpers.withMessage((props) => t(`messages.${props.$validator}`, {
model: props.$model,
property: props.$property,
...props.$params
}), validator)
```

We can now use the validators as we normally do

```vue
<script>
import { required } from '@/utils/validators'
export default {
validations () {
return {
name: { required }
}
}
}
</script>
```

One drawback is that Vuelidate params passed to `$message` are prefixed with `$`, which Vue-i18n does not allow. So we would have to manually map any
parameter we need, to a new name parameter without `$`.

We can now define our validator messages, with optional data inside each message.

```json
{
"messages": {
"required": "The field {property} is required.",
"minLength": "The {property} field has a value of '{model}', but it must have a min length of {min}."
}
}
```
2 changes: 2 additions & 0 deletions packages/test-project/main.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { createApp } from 'vue'
import App from './src/App.vue'
import { router } from './src/router.js'
import { i18n } from './src/i18n'

const app = createApp(App)

app.use(router)
app.use(i18n)
app.mount('#app')
1 change: 1 addition & 0 deletions packages/test-project/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
},
"dependencies": {
"vue": "^3.0.1",
"vue-i18n": "^9.1.6",
"vue-router": "^4.0.0-beta.6"
},
"devDependencies": {
Expand Down
1 change: 1 addition & 0 deletions packages/test-project/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<div class="navigation">
<ul>
<li><router-link to="/">Simple Form</router-link></li>
<li><router-link to="/i18n-simple">i18n Simple Form</router-link></li>
<li><router-link to="/old-api">Old api</router-link></li>
<li><router-link to="/nested-validations">Nested Validations</router-link></li>
<li><router-link to="/nested-ref">Nested Ref</router-link></li>
Expand Down
117 changes: 117 additions & 0 deletions packages/test-project/src/components/I18nSimpleForm.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<template>
<div class="SimpleForm">
<div class="lang-switcher">
Change Language to:
<a
href="#"
@click.prevent="$i18n.locale = 'bg'"
>BG</a> |
<a
href="#"
@click.prevent="$i18n.locale = 'en'"
>EN</a>
</div>
<div>
<label>name</label>
<input
v-model="name"
type="text"
>
</div>
<div>
<label>twitter</label>
<input
v-model="social.twitter"
type="text"
>
</div>
<div>
<label>github</label>
<input
v-model="social.github"
type="text"
>
</div>
<button @click="validate">
Validate
</button>
<button @click="v$.$touch">
$touch
</button>
<button @click="v$.$reset">
$reset
</button>
<div style="background: rgba(219, 53, 53, 0.62); color: #ff9090; padding: 10px 15px">
<p
v-for="(error, index) of v$.$errors"
:key="index"
style="padding: 0; margin: 5px 0"
>
{{ error.$message }}
</p>
</div>
<pre>{{ v$ }}</pre>
</div>
</template>

<script>
import { ref, reactive, computed } from 'vue'
import useVuelidate from '@vuelidate/core'
import { required, helpers, minLength } from '@vuelidate/validators'
import { i18n } from '../i18n'
const { global: { t } } = i18n
const { withAsync } = helpers
const asyncValidator = withAsync((v) => {
return new Promise(resolve => {
setTimeout(() => {
resolve(v === 'aaaa')
}, 2000)
})
})
const withI18nMessage = (validator) => helpers.withMessage((props) => t(`messages.${props.$validator}`, {
model: props.$model,
property: props.$property,
...props.$params
}), validator)
export default {
name: 'I18nForm',
setup () {
const name = ref('given name')
const social = reactive({
github: 'hi',
twitter: 'hey'
})
let v$ = useVuelidate(
{
name: {
required: withI18nMessage(required),
asyncValidator: withI18nMessage(asyncValidator)
},
social: {
github: { minLength: withI18nMessage(minLength(computed(() => social.twitter.length))) },
twitter: { minLength: withI18nMessage(minLength(computed(() => name.value.length))) }
}
},
{ name, social },
{ $autoDirty: true }
)
return { name, v$, social }
},
methods: {
async validate () {
const result = await this.v$.$validate()
console.log('Result is', result)
}
}
}
</script>
<style>
.lang-switcher a {
color: white;
}
</style>
23 changes: 23 additions & 0 deletions packages/test-project/src/i18n.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { createI18n } from 'vue-i18n'

const messages = {
en: {
messages: {
required: '{property} is required',
minLength: 'The {property} field has a value of "{model}", but must have a min length of {min}.',
asyncValidator: '{property} should equal "aaaa", but it is "{model}".'
}
},
bg: {
messages: {
required: '{property} e задължително',
minLength: 'Полето {property} има стойност "{model}", но трябва да е дълго поне {min} символа.',
asyncValidator: '{property} трябва да е "aaaa", но е "{model}".'
}
}
}

export const i18n = createI18n({
locale: 'en',
messages
})
5 changes: 5 additions & 0 deletions packages/test-project/src/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@ import SimpleForm from './components/SimpleForm.vue'
import NestedValidations from './components/NestedValidations.vue'
import OldApiExample from './components/OldApiExample.vue'
import ChainOfRefs from './components/ChainOfRefs.vue'
import I18nSimpleForm from './components/I18nSimpleForm.vue'

export const routes = [
{
path: '/',
component: SimpleForm
},
{
path: '/i18n-simple',
component: I18nSimpleForm
},
{
path: '/nested-validations',
component: NestedValidations
Expand Down
19 changes: 14 additions & 5 deletions packages/vuelidate/src/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -186,11 +186,14 @@ function createSyncResult (rule, model, $dirty, { $lazy }, $response, instance)
* @param {NormalizedValidator} rule
* @param {Ref<*>} model
* @param {Ref<boolean>} $dirty
* @param {GlobalConfig} config
* @param {VueInstance} instance
* @param {GlobalConfig} config - Vuelidate config
* @param {VueInstance} instance - component instance
* @param {string} validatorName - name of the current validator
* @param {string} propertyKey - the current property we are validating
* @param {string} propertyPath - the deep path to the validated property
* @return {{ $params: *, $message: Ref<String>, $pending: Ref<Boolean>, $invalid: Ref<Boolean>, $response: Ref<*>, $unwatch: WatchStopHandle }}
*/
function createValidatorResult (rule, model, $dirty, config, instance) {
function createValidatorResult (rule, model, $dirty, config, instance, validatorName, propertyKey, propertyPath) {
const $pending = ref(false)
const $params = rule.$params || {}
const $response = ref(null)
Expand Down Expand Up @@ -228,7 +231,10 @@ function createValidatorResult (rule, model, $dirty, config, instance) {
$invalid,
$params: unwrapObj($params), // $params can hold refs, so we unwrap them for easy access
$model: model,
$response
$response,
$validator: validatorName,
$propertyPath: propertyPath,
$property: propertyKey
})
))
: message || ''
Expand Down Expand Up @@ -321,7 +327,10 @@ function createValidationResults (rules, model, key, resultsCache, path, config,
model,
result.$dirty,
config,
instance
instance,
ruleKey,
key,
path
)
})

Expand Down
48 changes: 45 additions & 3 deletions packages/vuelidate/test/unit/specs/validation.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -716,12 +716,54 @@ describe('useVuelidate', () => {
expect(vm.v.$errors[0]).toHaveProperty('$message', 'Field is not Even')
})

it('allows `$message` to be a function', async () => {
const isEvenMessage = withMessage(() => `Field is not Even`, isEven)
it('allows `$message` to be constructed from a function', async () => {
const message = `Field is not Even`
const isEvenMessage = withMessage(() => message, isEven)
const value = ref(1)
const { vm } = await createSimpleWrapper({ value: { isEvenMessage } }, { value })
vm.v.$touch()
expect(vm.v.$errors[0]).toHaveProperty('$message', 'Field is not Even')
expect(vm.v.$errors[0]).toHaveProperty('$message', message)
})

it('passes extra parameters to the `$message` function', async () => {
const messageFunc = jest.fn().mockReturnValue('Message')
const nestedMessage = jest.fn().mockReturnValue('Nested Message')
const isEvenMessage = withMessage(messageFunc, isEven)

const value = ref(1)
const foo = ref('')

const { vm } = await createSimpleWrapper(
{
value: { isEvenMessage },
child: {
foo: { validator: withMessage(nestedMessage, isEven) }
}
},
{ value, child: { foo } }
)

vm.v.$touch()
expect(messageFunc).toHaveBeenCalledWith({
$invalid: true,
$model: 1,
$params: {},
$pending: false,
$property: 'value',
$propertyPath: 'value',
$response: false,
$validator: 'isEvenMessage'
})
expect(nestedMessage).toHaveBeenCalledWith({
$invalid: false,
$model: '',
$params: {},
$pending: false,
$property: 'foo',
$propertyPath: 'child.foo',
$response: true,
$validator: 'validator'
})
})

it('keeps the `$message` reactive', async () => {
Expand Down
Loading

0 comments on commit ca6bb32

Please sign in to comment.