vue-i18n icon indicating copy to clipboard operation
vue-i18n copied to clipboard

No translations when vue-i18n used inside library

Open saawsan opened this issue 5 years ago • 10 comments

Related to https://github.com/kazupon/vue-i18n/issues/505 and https://github.com/kazupon/vue-i18n/issues/670

Context

I'm creating a package to share some vue components to my app. Both the package and the app are in vuejs and use vue-i18n to handle translations.

I build the package with the --target option.

Issue

When working on the package as a stand-alone (ie. local mode or building the package without the --target option), all translations displays correctly.

When building the package and using it inside the parent app, the translations does not show. The translation key is visible instead.

Code extract

The package (my-package)

I'm using the following command to build the package: $vue-cli-service build --target lib --name my-package ./src/all-components.js

// src/main.js (used in dev mode / stand-alone version of the package)
import Vue from 'vue';

import App from './App.vue';
import { i18n, loadLanguage } from './plugin/i18n';

new Vue({
  i18n,
  beforeCreate() {
    loadLanguage('fr');
  },
  render: (h) => h(App),
}).$mount('#app');

// src/all-components.js (used to build and share the package)
import MySharedComponent1 from '@/components/my-shared-component-1.vue';
import MySharedComponent2 from '@/components/my-shared-component-2.vue';

export default { MySharedComponent1, MySharedComponent2 };
// src/components/my-shared-component-1.vue
<template name="my-shared-component-1">
  <main>
    <h1 v-t="'article.title'" />
    <p>
      {{ $t('article.title') }}
      <span v-text="$tc('article.comments', count, { x: count })" />
    </p>
  </main>
</template>

<script>
import { loadLanguage } from '@/plugin/i18n';

export default {
  name: 'MySharedComponent1',
  props: {
    language: {
      type: String,
      default: 'fr',
    },
  },
  data() {
    return {
      count: 4,
    };
  },
  created() {
    loadLanguage(this.language);
  },
};
</script>

// src/plugins/i18n.js
import Vue from 'vue';
import VueI18n from 'vue-i18n';

import de from '@/locales/de.json';
import en from '@/locales/en.json';
import fr from '@/locales/fr.json';

const languages = { de, en, fr };

Vue.use(VueI18n);

const i18n = new VueI18n({
  locale: 'fr',
  fallbackLocale: 'fr',
  messages: { fr: languages.fr },
  silentTranslationWarn: process.env.NODE_ENV === 'production',
});

function loadLanguage(lang) {
  if (i18n.locale === lang) {
    return;
  }
  const currentLang = languages[lang] ? lang : 'fr';
  i18n.setLocaleMessage(currentLang, languages[currentLang]);
  i18n.locale = currentLang;
  document.querySelector('html').setAttribute('lang', currentLang);
}

export {
  i18n,
  loadLanguage,
};

The app (my-app)

// src/pages/home.vue
<i18n>
{
  "fr": {
    "title": "Accueil"
  },
  "de": {
    "title": "Startseite"
  },
  "en": {
    "title": "Homepage"
  }
}
</i18n>

<template>
  <div>
    <my-shared-component-1 language="en" />
    ...
  </div>
</template>

<script>
import { mapGetters } from 'vuex';
import { MySharedComponent1 } from '@namespace/my-package';

export default {
  name: 'Home',
  components: {
    MySharedComponent1,
  },
  head() {
    return {
      title: this.$t('title'),
    };
  },
};
</script>

saawsan avatar Nov 29 '19 16:11 saawsan

So, I found something interesting.

In the generated package bundle, it seems that there is some conflicts between the vue-i18n of the package and the vue-i18n of the parent app.

Extract of the package @namespace/my-package/dist/my-package.common.js:

var i18n = new vue_i18n_esm({
  locale: 'fr',
  fallbackLocale: 'fr',
  messages: {
    fr: langages.fr
  },
  silentTranslationWarn: "production" === 'production'
});

// ...

Vue.prototype.$t = function (key) {
    var values = [], len = arguments.length - 1;
    while ( len-- > 0 ) values[ len ] = arguments[ len + 1 ];

    var i18n = this.$i18n; // Try to use the parent $i18n
    return i18n._t.apply(i18n, [ key, i18n.locale, i18n._getMessages(), this ].concat( values ))
  };

  Vue.prototype.$tc = function (key, choice) {
    var values = [], len = arguments.length - 2;
    while ( len-- > 0 ) values[ len ] = arguments[ len + 2 ];

    var i18n = this.$i18n; // here too
    return i18n._tc.apply(i18n, [ key, i18n.locale, i18n._getMessages(), this, choice ].concat( values ))
  };

  Vue.prototype.$te = function (key, locale) {
    var i18n = this.$i18n; // here again
    return i18n._te(key, i18n.locale, i18n._getMessages(), locale)
  };

When I comment the indicated lines, the translations of my-package appear. However, the translations of my-app are lost instead.

There are many more similar cases in the generated package. It feels that the scope of the package's vue-i18n is not properly closed.

saawsan avatar Nov 29 '19 16:11 saawsan

@saawsan I have exactly similar problem, and I think support of library use-case is very important think.

All the conflict happens because of Vue.use(VueI18n), but without it nothing will work, except you do some monkey-patching.

@kazupon, any chances to land support of localization for libraries? What do you think about it, in general? I don't want to use second translation library, and I love vue-i18n, but I need this feature too.

georgyfarniev avatar Dec 06 '19 08:12 georgyfarniev

In your Shared Library project, You need to external vue-i18n with vue.config.js.

e.g. vue.config.js configration:

module.exports = {
  // ...
  configureWebpack: config => {
    config.externals = ['vue-i18n']
  }
}

kazupon avatar Dec 07 '19 14:12 kazupon

Hello @kazupon, thank you for quick reply. Can you explain how externalizing of vue-i18n can help for this case? I don’t want to store my library localization files in my application side (where I will actually load vue-i18n).

UPD: @kazupon, I tried your example and it doesn't work for me. And as I understand how webpack works, it will not help to resolve current issue :(

I can use another library, but I like your, and would be happy to use it in library too. Thank you for working on it. I just need little help to make it work with a library, in other words, make instance of VueI18n work without Vue.use. I studied it's code, and as I understand, it shouldn't be difficult. But before I do PR, I would like to know your opinion, will you consider accepting such change?

georgyfarniev avatar Dec 08 '19 01:12 georgyfarniev

I ran into the same problem though fixed this by specifying vue-i18n as a peerDependency. This will put the responsibility to supply the vue-i18n instance with the application package.

Though I haven't tried, adding vue-i18n too externals as @kazupon mentioned should also do the trick in case you have listed vue-i18n as a "regular" npm dependency.

Both of these will make sure you don't bundle vue-i18n with the library bundle

Robin-Hoodie avatar Jul 12 '20 20:07 Robin-Hoodie

I also get a problem when importing my lib with vue-i18n in it I tried adding vue-i18n to: transpileDependencies, configureWebpack as @kazupon offered, peerDependencies in package.json file My parent app still throws an error saying [Vue warn]: Error in render: "TypeError: _vm.$t is not a function"

UPD: So, after I imported i18n from i18n.js file that creates after vue-i18n installation instead of from vue-i18n itself my console error went from [Vue warn]: Error in render: "TypeError: _vm.$t is not a function" to [Vue warn]: Error in render: "TypeError: Cannot read property '_t' of undefined"

redisotschek avatar Jul 24 '20 15:07 redisotschek

Please see https://github.com/kazupon/vue-i18n/issues/505#issuecomment-666750256 for a related solution

gangsthub avatar Jul 30 '20 22:07 gangsthub

I got this to work by using component based localization and adding vue-18n to externals.

mkampmey avatar Sep 20 '20 15:09 mkampmey

I got this to work by using component based localization and adding vue-18n to externals.

@mkampmey hi, could you link some examples, please?

TorusHelm avatar Sep 29 '20 22:09 TorusHelm

@TorusHelm Sure! So I have a single file component which looks somehow like this:

<template>
 <div></div>
</template>
<script>
export default {
  name: 'MyComponent',
  i18n: {
    messages: {
      en: {
        "foo": "bar"
      }
    }
  },
  data() {
    return {}
  },
  ...
}
</script>

And I am using webpack externals to exclude vue-i18n from the bundle. In my case I am using webpack-node-externals to exclude everything (like this: config.externals(nodeExternals())), but just excluding vue-i18n should also work: config.externals(['vue-i18n']). And thats everything. In the bundle where I am using this library I am just importing the component and then translation is working.

mkampmey avatar Oct 01 '20 15:10 mkampmey