Vue-i18n: Support Composition API

Created on 27 Aug 2019  路  13Comments  路  Source: kazupon/vue-i18n

Hello and first of all, thank you for this awesome plugin.
Until today I was used to write this.$t('to_translate') in my script with the corresponding

<i18n>
  to_translate: "translated"
</i18n>

in my SFC file when, for example, I wanted to use notifications.
Soon, with the Composition Api RFC, a new syntax will be introduced to develop our components. In fact, it's already available with this plugin.
So I was wondering what is the correct way of doing it with the new syntax as this is not available in the setup function and even if $i18n.t is accessible through setup context root, it seems that the I18n tag is not yet processed.
I know that I could define translations inside the messages property at init or try to access the translation inside the onMounted() lifecycle hook but it feels kind of hacky.
Any thoughts?

High Feature

Most helpful comment

Hi!

I have plans to rewrite vue-i18n functionality from full scratch with composition API and TypeScript in the next major version.
I'll keep the current vue-i18n API as compatible like the Vue object style API.

I'm developing some vue-i18n related tools (e.g. eslint-plugin-vue-i18n, vue-cli-plugin-i18n, and etc) and libraries (vue-i18n-locale-mesasge, vue-i18n-loader).

when I take a break these developing, I'll try to start vue-i18n major version developing.

All 13 comments

Hi!

I have plans to rewrite vue-i18n functionality from full scratch with composition API and TypeScript in the next major version.
I'll keep the current vue-i18n API as compatible like the Vue object style API.

I'm developing some vue-i18n related tools (e.g. eslint-plugin-vue-i18n, vue-cli-plugin-i18n, and etc) and libraries (vue-i18n-locale-mesasge, vue-i18n-loader).

when I take a break these developing, I'll try to start vue-i18n major version developing.

Awesome news! Thank you @kazupon

Hey @kazupon :wave:

Thanks for the information and the hard work!

Any chance that there will be either:

  • a "workaround" for using the current vue-i18n major version with the @vue/composition-api plugin
  • a way to try out the next version

in the near future?

Best Regards,
Alex

If you're using @vue/composition-api then the instance with your local component messages is available using getCurrentInstance.

e.g.

<i18n>
  {
    "en": {
      "localMessage": "Blah"
    }
  }
</i18n>

<template>
  <div>{{ message }}</div>
</template>

<script>
import { createComponent, ref, getCurrentInstance } from "@vue/composition-api";
export default createComponent({
  setup() {
    let vm = getCurrentInstance();
    let message = ref(vm.$i18n.t("localMessage"))
    return {
      message
    }
  };
})
</script>

The other approach for getting to $i18n, is to access it through root:

<script>
import { createComponent, ref, getCurrentInstance } from "@vue/composition-api";
export default createComponent({
  setup(props, { root}) {
    let message = ref(root.$i18n.t("localMessage"))
    return {
      message
    }
  };
})
</script>

I have been doing this. But the problem I am having is that root.$i18n is undefined in unit testing.
So I have switched to:

import i18n from '@/plugins/i18n'

<script>
import { createComponent, ref, getCurrentInstance } from "@vue/composition-api";
export default createComponent({
  setup(props, { root}) {
    let message = ref(i18n.t("localMessage"))
    return {
      message
    }
  };
})
</script>

Then I can mock out i18n in unit tests.

銇撱倱銇礌鏅淬倝銇椼亜銉戙儍銈便兗銈搞倰浣溿仯銇︺亸銈屻仸銇傘倞銇屻仺銇嗐仈銇炪伨銇欙紒

I am testing vue-i18n-next (v9.0.0-alpha.2) with Vue 3 (v3.0.0-beta.13). I have been successful at getting both the composition and legacy styles to work. 馃帀

My question: What are the benefits of using the composition style for this plugin? I understand the benefits for code organization, reuse and testability for other parts of my app. But for i18n it seems like having to useI18n() in every component just means using extra code. Is the legacy style going away at some point? I'm comfortable using either one, but the new style seems like extra unnecessary code. Is there a performance benefit to using the new style?

I should add that this is an app created with @vue/cli (i.e. vue create myapp) and will be built before deploying instead of the style used in the examples.

Files used in Both Styles

// src/main.js
import { createApp } from 'vue'
import i18n from '@/i18n'
import App from '@/App.vue'

createApp(App).use(i18n).mount('#app')
// src/i18n/locales/en.json
{
  "hello": "Hello!"
}

Composition Style Usage

// src/i18n/index.js
import { createI18n } from 'vue-i18n'

const loadMessages = () => {
  const locales = require.context('@/i18n/locales', true, /[\w-]+\.json$/i)
  return locales.keys().reduce((locs, loc) => ({ ...locs, [loc.replace(/\.|\/|json/g, '')]: locales(loc) }), {})
}

export default createI18n({
  locale: 'en',
  fallbackLocale: 'en',
  messages: loadMessages()
})
<!-- src/App.vue -->
<template>
  <div>{{ t('hello') }}</div>
</template>

<script>
import { useI18n } from 'vue-i18n'
export default {
  setup: () => useI18n()
}
</script>

Legacy Style

// src/i18n/index.js
import { createI18n } from 'vue-i18n'

const loadMessages = () => {
  const locales = require.context('@/i18n/locales', true, /[\w-]+\.json$/i)
  return locales.keys().reduce((locs, loc) => ({ ...locs, [loc.replace(/\.|\/|json/g, '')]: locales(loc) }), {})
}

export default createI18n({
  legacy: true, // LEGACY VERSION
  locale: 'en',
  fallbackLocale: 'en',
  messages: loadMessages()
})
<!-- src/App.vue LEGACY VERSION -->
<template>
  <div>{{ $t('hello') }}</div>
</template>

@morphatic
Thank you for your feedback!
I'm very happy to get feedback about vue-i18n-next. :)

What are the benefits of using the composition style for this plugin? I understand the benefits for code organization, reuse and testability for other parts of my app. But for i18n it seems like having to useI18n() in every component just means using extra code.

The benefit of using a new style API called composable API is composition API RFCs and as you mentioned.
you can get the benefits for code organization, reuse and testability.

The Composer object returned with useI18n() offers an APIs that is similar to the API offered by the VueI18n instance (via $i18n) provided so far in v8.x.

These APIs are implemented using the Composition API, so your application can be customized as the below.

import watch from 'vue'
import { useI18n } from 'vue-i18n'

const App = {
  setup() {
    const { t, locale } = useI18n({ ... })

    // watch `locale`
    watch(locale, (val, old) => {
      // something to do ...
    })

    return { ... , t }
  }
}

Is the legacy style going away at some point?

In about legacy style API supporting, we plan to go away at the same time Vue.js drop the option style API.

I'm comfortable using either one, but the new style seems like extra unnecessary code. Is there a performance benefit to using the new style?

The new style API optimized for the Composition API, so you'll have to write a little extra codes as Drawback, but it's the trade-off.

The legacy style API offer an API compatible with VueI18n instances.
it offers the legacy API with wrapping the Composer object.

For this reason, the legacy style have a bit call overhead.
The first reason to offer the legacy style API is to support Vue application built using existing API styles work in Vue 3 without any code changes.
Offering the legacy style API makes it easier to migrate your applications to Vue 3.

@morphatic I can give you my point of view. I use typescript and I separate all the logic from the component and put all the logic into a view model for the component. This allows my component to remain small and it makes it so much easier to do unit testing on the logic of my component. And now with vue 3's reactivity functions, I can get all the benefits in my typescript view model that I would have had from computed and data in the options approach.

So having i18n as something I can include in my view model through useI18n() is great and way better than trying to find a global instance of i18n. This makes it so much easier to mock out in my unit testing.

BTW, in my unit testing, I have my mock instance do this: t: (key: string) => 'i18n:' + key.
Then i can test that I am setting the right key on my elements or returning the right key from the view model and I can ensure that it has gone through translation because the matched string will be 'i18n:key' rather than just key.

So from my point of view, as somebody completely dedicated to unit testing and typescript, this approach of useI18n() rather than fetching a global instance and the vue 3 changes are fantastic and well worth any "extra" code that might need to be written to accomplish it.

@kazupon This is looking great. One thing I noticed from your example and from @morphatic is that you are returning t from Setup. Does that mean that $t won't be available in the template?
I use a combination of the v-t directive and {{ $t() }}. I prefer the v-t directive but there are some times when only the $t substitution will work.

I am using composition api 0.5, I am having a reactivity problem when I change the language hot on the page.

If I change the language like this.$root.i18n.locale = keyLang everything that is in template works fine {{$ t ()}} but what I use from the setup method does not make the change

I tried this:

 const vm = getCurrentInstance();
 const pricing = ref(vm.$t("c.pricing"));

And that:

import i18n from "@/plugins/lang";
....
  setup(props, context) {
    const pricing = ref(i18n.t("c.pricing"));
...

I have in no way managed to get it to work.
any ideas?

Thank you!

I answer myself.

My solution has been to store the translation key, it is basic but it had not fallen.

<span> {{ $t( title)}}</span>
const title= ref("c.pricing");

Thank you!

@vallemar That is not how ref is supposed to work. If you need your language strings to change, then you need to watch something that is going to change when the language changes using a watch statement.

There are two ways I can see this happening. I haven't tried either BTW.

  1. You can use watch in your setup and when the watch triggers, it will set the values of your ref variables using the new strings.

  2. you might try making your Ref variables computeds instead. So instead of creating them with ref(), you create them with computed(). I just don't know if vm.$t will be enough to trigger re-running the computed.

ref() doesn't mean that the variable will get recreated if something changes, it means that this is a variable that will change from time to time and that users of that variable should "watch" it for changes.

a computed() means that whenever something this variable depends on changes, then this variable will be recomputed. But you have to make sure that everything you depend on is a ref. I am not sure that vm.$t is a ref.

we supported in Vue I18n v9.
please try it!

Was this page helpful?
0 / 5 - 0 ratings