intlify / vue-i18n

Vue I18n for Vue 3
https://vue-i18n.intlify.dev/
MIT License
2k stars 319 forks source link

Custom Elements: Handle child component #646

Open gnuletik opened 2 years ago

gnuletik commented 2 years ago

Reporting a bug?

When calling useI18n from a component which is a children from a Custom Element, it throws :

Uncaught TypeError: Cannot read property '__VUE_I18N_SYMBOL__' of null

here : https://github.com/intlify/vue-i18n-next/blob/24b6d60a711ba591b1abdae937930c1615c98d81/packages/vue-i18n-core/src/i18n.ts#L604

This is because the child components do not have instance.isCE.

Would it be possible to :

Expected behavior

Should not throw

Reproduction

NA

System Info

Binaries:
    Node: 16.7.0 - /usr/bin/node
    Yarn: 1.22.11 - /usr/bin/yarn
    npm: 7.21.0 - /usr/bin/npm
  Browsers:
    Chromium: 92.0.4515.159
    Firefox: 91.0.1
  npmPackages:
    vue: ^3.2.6 => 3.2.6 
    vue-i18n: ^9.2.0-beta.3 => 9.2.0-beta.3

Screenshot

No response

Additional context

No response

Validations

gnuletik commented 2 years ago

The same error occurs when using async custom element with :

customElements.define(
  'my-element',
  defineCustomElement(defineAsyncComponent(() => import('MyComponent.vue')))
)

(but it makes sense because MyComponent is a child of the async component)

kazupon commented 2 years ago

Thank you for feedback!

Unfortunately, this is a limitation of Vue Provide / Inject. :disappointed: https://vue-i18n.intlify.dev/guide/advanced/wc.html#limitations

Vue docs says:

note that this works only between custom elements

https://v3.vuejs.org/guide/web-components.html#definecustomelement

This means that if you use a Vue component from a custom element that is a web component, the inject will not work.

gnuletik commented 2 years ago

Thanks for your answer ! I missed that important info !

I created an issue in the vue-next repo : https://github.com/vuejs/vue-next/issues/4476

And will use a custom implementation of useI18n:

import { getCurrentInstance, InjectionKey } from 'vue'
import { I18nInjectionKey } from 'vue-i18n'

export function deepInject<T> (key: InjectionKey<T> | string): T | undefined {
  let inst = getCurrentInstance()
  if (inst === null) {
    throw new Error('getCurrentInstance returned null')
  }
  inst = inst.parent
  if (inst === null) {
    return
  }
  while (inst !== null) {
    // @ts-expect-error
    if (key in inst.provides) {
      // @ts-expect-error
      return inst.provides[key]
    }
    inst = inst.parent
  }
}

interface Options {
  messages?: Record<string, Record<string, string>>
}

// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function useI18n (options?: Options) {
  const instance = deepInject(I18nInjectionKey)
  if (instance === undefined) {
    throw new Error('i18n not found in context')
  }
  const { global } = instance

  // merge locale messages
  const messages = options?.messages ?? {}
  const locales = Object.keys(messages)
  if (locales.length > 0) {
    locales.forEach(locale => {
      global.mergeLocaleMessage(locale, messages[locale])
    })
  }

  return global
}

It does not cover all use-cases of the original implementation but it covers my use-case.

Thanks!

ploca14 commented 10 months ago

I think the provide/inject limitation does not effect this. I tried providing the i18n instance from a vue web component and than inject this in a child vue component and it worked fine.

PluginProvider.ce.vue

<template>
  <Suspense>
    <slot />
  </Suspense>
</template>

<script lang="ts">
import { defineComponent, provide } from 'vue';
import { createI18n, I18nInjectionKey } from 'vue-i18n';
import i18nConfig from '~/i18n.config';

const i18n = createI18n<false>(i18nConfig.apply());

export default defineComponent({
  setup(props) {
    provide(I18nInjectionKey, i18n);

    return {};
  },
});
</script>

CustomElement.ce.vue

<template>
  <div>
    {{ t('login') }}
    <ChildElement />  
  </div>
</template>

<script setup lang="ts">
const { t, locale } = useI18n({
  useScope: 'local',
  messages: {
    cs: {
      login: 'Přihlásit',
    },
    en: {
      login: 'Login',
    },
  },
});
</script>

ChildElement.vue

<template>
  <div>
    ChildElement
  </div>
</template>

<script lang="ts" setup>
import { I18nInjectionKey } from 'vue-i18n';

console.log(inject(I18nInjectionKey)); // This properly injects
</script>
AleksejDix commented 7 months ago

We also run into this issue Please checkout this implementation which just create a wrapper component which doesn't need to be mounted on the application but can be mounted on any vue component which will be converted to web component

It would be nice to have this feature inside native i18n So it can be used to create web components without App context

https://github.com/padcom/vue-i18n

kazupon commented 7 months ago

https://github.com/padcom/vue-i18n

That's interesting! I'll check about it. :)