Vuelidate with i18n: Key not found in locale messages - vue.js

So in my i18n-validators.js file I want to export validators with translated messages to my language of choice and use them in my vue component to validate a form.
My code:
// import * as VuelidateValidators from 'https://cdn.jsdelivr.net/npm/#vuelidate/validators';
// import * as VueI18n from 'https://unpkg.com/vue-i18n#9';
const messages = {
en: {
validations: {
required: 'The field {property} is required.',
}
},
cs: {
validations: {
required: 'Toto pole {property} je povinné',
}
},
}
const i18n = VueI18n.createI18n({
locale: 'cz',
fallbackLocale: 'en',
messages
})
const withI18nMessage = VuelidateValidators.createI18nMessage({
t: VueI18n.createI18n().global.t.bind(i18n)
})
export const required = withI18nMessage(VuelidateValidators.required)
Console:
Not found 'validations.required' key in 'en-US' locale messages. vue-i18n#9
Fall back to translate 'validations.required' key with 'en' locale. vue-i18n#9
Not found 'validations.required' key in 'en' locale messages.
And I want the validator to throw me the specified message instead of the "validations.required" message

First make sure you have installed vuelidade and vue-i18n
Following your example, you can change the file above to:
import * as validators from "#vuelidate/validators";
import { createI18n } from "vue-i18n";
const { createI18nMessage } = validators;
const messages = {
en: {
validations: {
required: "The field {property} is required.",
},
},
cs: {
validations: {
required: "Toto pole {property} je povinné",
},
},
};
const i18n = createI18n({
locale: "cs",
fallbackLocale: "en",
messages,
});
const withI18nMessage = createI18nMessage({ t: i18n.global.t.bind(i18n) });
export const required = withI18nMessage(validators.required);
as a component you can follow this one as example:
<template>
...
<div class="mb-3">
<input
v-model="formData.name"
className="form-control"
placeholder="Insert your name.."
/>
</div>
<span v-for="error in v$.name.$errors" :key="String(error.$uid)">
<span class="text-danger">{{ error.$message }}</span>
</span>
<div class="mt-5 submit">
<button class="btn btn-primary btn-sm" type="button" #click="submitForm">
Next
</button>
</div>
...
</template>
<script lang="ts">
import { defineComponent, reactive } from "vue";
import useVuelidate from "#vuelidate/core";
import { required } from "#/utils/validators/i18n-validators";
export default defineComponent({
name: "InitialDataForm",
setup() {
const formData = reactive({
name: "",
});
const rules = {
name: { required },
};
const v$ = useVuelidate(rules, formData);
return {
formData,
v$,
};
},
methods: {
async submitForm() {
const result = await this.v$.$validate();
if (result) {
alert("validation passed");
}
},
},
});
</script>
and now you should be able to see the translated message:

Related

How to use pinia store with v-for directive whilst keeping it reactive?

I have a pinia data store similar to the following code snippet that stores user information and a list of individual orders he is placing:
order.js
import { defineStore } from 'pinia'
import { reactive } from 'vue'
export const useOrderStore = defineStore('order', {
state: () => ({
username: '',
orders: reactive([
{
id: '',
item: '',
price: ''
}
])
}),
})
Also I am using the v-for directive to render the components that should display the individual orders
OrdersComp.vue
<template>
<div class="orders_container">
<div v-for="(order, index) in orders" :key="order.id">
<OrderComp />
</div>
</div>
</template>
<script>
import { storeToRefs } from 'pinia'
import { useOrderStore } from "#/store/order";
setup() {
const { orders } = storeToRefs(useOrderStore())
return { orders };
},
</script>
How can I access the store data for the individual orders in the child component OrderComp
Basically I want something like this:
OrderComp.vue
<div>
<p>{{ orders.id }}</p>
<input v-model="orders.item" />
<input v-model="orders.price" />
</div>
<script>
import { storeToRefs } from 'pinia'
import { useOrderStore } from "#/store/order";
setup() {
const { orders } = storeToRefs(useOrderStore())
return { orders };
},
</script>
and still keep its reactive state? How does the child component know which order of the orders array to modify? Can/Should I combine the pinia data store with props that pass the data from parent to child? (Though this seems somewhat wrong for me, as pinia is probably able to replace all data passing between components) And furthermore as item and price are bound to input fields, they should of course dynamically change based on a user input.
Based on Estus Flasks comments I got it working by emitting events from the child OrderComp to the parent OrdersComp and on each change it invoked a function that modified my orders array at the correct index in the datastore.
So following the example above I did something like this:
order.js
import { defineStore } from 'pinia'
import { reactive } from 'vue'
export const useOrderStore = defineStore('order', {
state: () => ({
username: '',
orders: reactive([
{
id: '',
item: '',
price: ''
}
])
}),
actions: {
modifyOrder (id, order) {
var foundIndex = this.orders.findIndex(elem => elem.id == order.id)
this.orders[foundIndex] = order
}
}
})
OrdersComp.vue
<template>
<div class="orders_container">
<div v-for="(order, index) in order_store.orders" :key="order.id">
<OrderComp #change="order_store.modifyOrder(index, order)"
v-model:itemProp="order.item"
v-model:priceProp="order.price"
/>
</div>
</div>
</template>
<script>
import { useOrderStore } from "#/store/order";
export default {
setup() {
const order_store = useOrderStore()
return { order_store };
},
}
</script>
Note: I use a wrapper function here to emit the inputs, however you can of course emit it directly e.g. via #input/#change
OrderComp.vue
<div>
<input v-model="item" />
<input v-model="price" />
</div>
<script>
import { useModelWrapper } from "#/modelWrapper";
export default {
name: "OrderComp",´
props: {
itemProp: { type: String, default: "" },
priceProp: { type: String, default: "" },
},
emits: [
"update:itemProp",
"update:priceProp",
],
setup(props, { emit }) {
return {
item: useModelWrapper(props, emit, "itemProp"),
price: useModelWrapper(props, emit, "priceProp"),
};
},
}
modelWrapper.js
import { computed } from "vue";
export function useModelWrapper(props, emit, name = "modelValue") {
return computed({
get: () => props[name],
set: (value) => emit(`update:${name}`, value),
});
}

module namespace not found in mapGetters() Vue Jest Testing Error

Currently I am writing a jest testing, but running into the following problem which pops up in my terminal. How can I fix that issue here. According to what the community answered on different forums, I added 'namespaced: true' but without any success. So was wondering what I am doing wrong in this case.
import { shallowMount, createLocalVue } from '#vue/test-utils';
import Vuex from 'vuex';
import Onboarding from '../Onboarding.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('Test onboarding', () => {
let getters;
let store;
const mockStore = { dispatch: jest.fn() };
beforeEach(() => {
getters = {
isReturningUser: () => true,
};
// eslint-disable-next-line import/no-named-as-default-member
store = new Vuex.Store({
namespaced: true,
modules: {
requests: {
getters,
mocks: {
$mockStore: mockStore,
},
},
},
});
});
it('check design with snapshot', () => {
const wrapper = shallowMount(Onboarding, {
store,
localVue,
});
expect(wrapper.findAll('[data-test="onboarding-container"]')).toHaveLength(
1,
);
});
});
<template>
<div
v-if="isReturningUser"
class="popup-container"
data-test="onboarding-container"
>
<div class="popup">
<div class="step">
<img :src="activeStep.image" />
<h2>{{ activeStep.title }}</h2>
<p>{{ activeStep.text }}</p>
</div>
<button
v-if="activeStepIndex <= 2"
class="base-button-primary"
#click="nextStep"
>
Volgende
</button>
<button v-else class="base-button-primary" #click="nextStep">
Ik snap het
</button>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
export default {
name: 'Onboarding',
data() {
return {
activeStepIndex: 0,
steps: [
{
title: 'Bekijk de drukte',
text: 'Bekijk hoe druk het nu is in de stad.',
image: require('#/assets/images/onboarding/step-1.png'),
},
{
title: 'Pas het scherm aan',
text: "Bezoekers, auto's, parkeerplaatsen, hostspots, routes.",
image: require('#/assets/images/onboarding/step-2.png'),
},
],
};
},
computed: {
...mapGetters('onboarding', ['isReturningUser']),
activeStep() {
return this.steps[this.activeStepIndex];
},
iconUrl() {
return require(`~/assets/icons/checkmark.svg`);
},
},
methods: {
nextStep() {
if (this.activeStepIndex < this.steps.length - 1) {
this.activeStepIndex += 1;
} else {
this.$store.commit('onboarding/isReturningUser', true);
this.activeStepIndex = 0;
}
},
},
};
</script>

Vue-i18n not translating inside component script tags

Building a language switcher, all works fine but when I use the $t() inside the data object it will not be dynamic when I switch between a language.
Component.vue
<template>
// loop menu here
<div v-for="item in menu">
{{ item.label }}
</div>
</template>
<script>
const mainMenu = [
{
label: $t('dashboard'),
},
{
label: $t('users'),
},
{
label: $t('settings'),
},
}
export default {
data () {
return {
menu = MainMenu
}
}
}
</script>
i18n.js
// https://vue-i18n.intlify.dev/
import { createI18n } from 'vue-i18n'
export function loadLocalMessages () {
const locales = require.context('../locales', true, /[A-Za-z0-9-_,\s]+\.json$/i)
const messages = {}
locales.keys().forEach(key => {
const matched = key.match(/([A-Za-z0-9-_]+)\./i)
if (matched && matched.length > 1) {
const locale = matched[1]
messages[locale] = locales(key)
}
})
return messages;
}
const i18n = createI18n({
locale: 'en',// .env not working
fallbackLocale: 'en',// .env not working
messages: loadLocalMessages(),
});
export default i18n
<template>
<div v-for="item in menu">
{{ item.label }}
</div>
</template>
<script>
export default {
computed: {
menu() {
return [{
label: this.$t('dashboard'),
}, {
label: this.$t('users'),
}, {
label: this.$t('settings'),
}]
}
}
}
</script>
data is only ever called once when creating the component, and it's not intended to be reactive.
To make a property reactive on $t(), it should be computed:
export default {
computed: {
hello() {
return this.$t('hello')
}
}
}
demo

Default value for props like an object didn't work

I have a very strange situation :
My HomeComponent.vue
<template>
<div class="home">
<img alt="Vue logo" src="../assets/logo.png" />
<HelloWorld :msg="msg" #update="inputUpdated" />
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import HelloWorld from "#/components/HelloWorld.vue"; // # is an alias to /src
import { Message } from "#/interfaces/message";
export default defineComponent({
name: "Home",
components: {
HelloWorld,
},
setup(props, { emit }) {
const msg: Message = {
year: 2020,
};
function inputUpdated(value): void {
console.log("Get Event : " + value);
}
return {
msg,
inputUpdated,
};
},
});
</script>
My HelloWorld.vue
<template>
<div class="hello">
<h1>{{ msg.title }} in the year : {{ msg.year }}</h1>
<button #click="increment">count is : {{ count }}</button>
<div>{{ pow(2, 3) }}</div>
<p>Edit <code>component</code> to test state is {{ state.year }}</p>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, reactive, PropType } from "vue";
import { State } from "#/interfaces/state";
import { Message } from "#/interfaces/message";
export default defineComponent({
name: "HelloWorld",
props: {
msg: {
type: Object as PropType<Message>,
required: true,
default: function () {
return {
title: "Arrow Function Expression",
year: 2020,
};
},
},
},
setup(props, { emit }) {
console.log(props);
const count = ref(0);
const state: State = reactive({
title: "",
year: 2020,
});
function pow(x: number, y: number): number {
return Math.pow(x, y);
}
function increment(): void {
console.log("Emit : Hello World");
emit("update", "Hello World");
count.value++;
}
return {
count,
increment,
pow,
state,
};
},
});
</script>
The interface Message :
export interface Message {
title?: string;
year?: number;
}
The problem what I have is even if I created a default for msg props didn't work, is not taken into account. I have the message in the year :. What I'm doing wrong with default values for props values ? Thx in advance and sorry for my english.
You're expecting the default prop's properties to be merged with the passed prop. That won't work and would mean the prop was modified, and props should not be modified. The default prop is used only when no prop has been passed (so you don't need the required attribute.)
You'll have to decide between splitting up the object into individual props:
props: {
title: ...,
year: ...
}
Or use a computed to merge:
setup(props) {
const msgDefault = {
title: "Arrow Function Expression",
year: 2020,
};
const merged = computed(() => {
return Object.assign({}, msgDefault, props.msg); // shallow merge
})
return { merged }
}

Vue unit tests failing because component method calls this.$route.query - TypeError: Cannot read property 'query' of undefined

Long time user of the wisdom of StackOverflow on and off the job, but first time I'm posting a question. What a milestone!
I'm writing unit tests for a large Vue application, and one of my components uses a method that references $route in order to determine if a query param is being passed in / pass in the param if it is being used. The method calling this.$route.query.article_id works great, however now that I am in testing, the tests don't recognize this.$route.query
I've tried to mock the $route object when using shallowMount to mount my localVue, as described in the doc, but it doesn't work, and I continue to get the same error.
Here is my component:
<template>
<b-container fluid>
<div class="content-page-header"></div>
<b-row>
<b-col cols="3" class="outer-columns text-center" style="color:grey">
<font-awesome-icon
:icon="['fas', 'newspaper']"
class="fa-9x content-page-photo mb-3 circle-icon"
/>
<br />
<br />Get practical tips and helpful
<br />advice in clear articles written
<br />by our staff's experts.
</b-col>
<b-col cols="6" v-if="articlesExist">
<h1 class="header-text">
<b>Articles</b>
</h1>
<div v-if="!selectedArticle">
<div v-for="article in articles">
<article-card :article="article" #clicked="onClickRead" />
<br />
</div>
</div>
<div v-else>
<router-link to="articles" v-on:click.native="setSelectedArticle(null)">
<font-awesome-icon icon="chevron-circle-left" />&nbsp
<b>Back to All Articles</b>
</router-link>
<article-header :article="selectedArticle" />
<br />
<span v-html="selectedArticle.text"></span>
</div>
</b-col>
<b-col cols="6" v-else>
<h1 class="header-text">
<b>Articles</b>
</h1>
<div class="text-center">Stay tuned for more Articles</div>
</b-col>
<b-col class="outer-columns">
<b class="text-color" style="font-size:14pt">Saved Articles</b>
<div v-for="article in userArticles">
<router-link
:to="{path:'articles', query: {article_id: article.article.id}}"
v-on:click.native="setSelectedArticle(article.article)"
>
<user-article :article="article.article" />
</router-link>
<br />
</div>
</b-col>
</b-row>
</b-container>
</template>
<script>
import ArticleCard from "./ArticleCard";
import UserArticle from "./UserArticle";
import ArticleHeader from "./ArticleHeader";
import { library } from "#fortawesome/fontawesome-svg-core";
import {
faNewspaper,
faChevronCircleLeft
} from "#fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "#fortawesome/vue-fontawesome";
library.add(faNewspaper, faChevronCircleLeft);
export default {
name: "Articles",
props: [],
components: {
ArticleCard,
ArticleHeader,
UserArticle,
library,
FontAwesomeIcon,
faNewspaper,
faChevronCircleLeft
},
mixins: [],
data() {
return {
selectedArticle: null
};
},
computed: {
articles() {
return this.$store.getters.articles.filter(article => article.text);
},
articlesExist() {
return Array.isArray(this.articles) && this.articles.length;
},
userArticles() {
return this.$store.getters.userArticles;
},
articleParam() {
return parseInt(this.$route.query.article_id);
}
},
methods: {
setSelectedArticle(article) {
this.selectedArticle = article;
},
onClickRead(article) {
this.selectedArticle = article;
}
},
mounted() {
if (this.articleParam) {
this.setSelectedArticle(
this.articles.filter(article => article.id === this.articleParam)[0]
);
}
}
};
</script>
<style lang="stylus" scoped>
.text-color {
color: #549DB0;
}
.header-text {
color: white;
margin-top: -50px;
margin-bottom: 20px;
}
.outer-columns {
background-color: #F2FBFD;
padding-top: 20px;
}
.nav-back {
color: #549DB0;
background-color: #F0FBFD;
padding: 5px;
}
</style>
And here is my test:
import { shallowMount, createLocalVue } from '#vue/test-utils'
import VueRouter from 'vue-router'
import Articles from '../../../app/javascript/components/member-dashboard/Articles.vue'
const localVue = createLocalVue()
localVue.use(Vuex)
localVue.use(VueRouter)
localVue.use(BootstrapVue)
describe('Articles', () => {
let store
let getters
let state = {
articles: [
{
title: "Testing Vue Components"
},
{
title: "This One shows",
text: "<p>You can see me!</p>"
},
{
title: "Another One",
text: "<p>See me too!</p>"
}
],
userArticles: [
{article: {
title: "This One shows",
text: "<p>You can see me!</p>"
}},
{article: {
title: "Another One",
text: "<p>See me too!</p>"
}}
]
}
beforeEach(() => {
getters = {
articles: () => {
return state.articles
},
userArticles: () => {
return state.userArticles
}
}
store = new Vuex.Store({ getters })
})
it('only displays article with body text', () => {
const wrapper = shallowMount(Articles, {
store,
localVue
})
expect(wrapper.vm.articles.length).to.deep.equal(2)
})
})
As I mentioned, in the shallow mount, I've tried doing this:
const wrapper = shallowMount(Articles, {
store,
localVue,
mocks: {
$route: {
query: null
}
}
})
But I continue to get this error:
TypeError: Cannot read property 'query' of undefined
at VueComponent.articleParam (webpack-internal:///1:107:35)
When I remove the line return parseInt(this.$route.query.article_id); from the articleParam method, my test passes.
How do I get around this call to this.$route.query in the component? It's not necessary to my test, but is causing my test to fail when mounting the component.
import import VueRouter from 'vue-router'; in your unite test file and create a new object of the router like const router = new VueRouter(); and use it in your test case.
I have updated code here:
import { shallowMount, createLocalVue } from '#vue/test-utils'
import VueRouter from 'vue-router'
import Articles from '../../../app/javascript/components/member-dashboard/Articles.vue'
const localVue = createLocalVue()
localVue.use(Vuex)
localVue.use(VueRouter)
localVue.use(BootstrapVue);
const router = new VueRouter();
describe('Articles', () => {
let store
let getters
let state = {
articles: [
{
title: "Testing Vue Components"
},
{
title: "This One shows",
text: "<p>You can see me!</p>"
},
{
title: "Another One",
text: "<p>See me too!</p>"
}
],
userArticles: [
{article: {
title: "This One shows",
text: "<p>You can see me!</p>"
}},
{article: {
title: "Another One",
text: "<p>See me too!</p>"
}}
]
}
beforeEach(() => {
getters = {
articles: () => {
return state.articles
},
userArticles: () => {
return state.userArticles
}
}
store = new Vuex.Store({ getters })
})
it('only displays article with body text', () => {
const wrapper = shallowMount(Articles, {
store,
router,
localVue
})
expect(wrapper.vm.articles.length).to.deep.equal(2)
})
})