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

Custom Elements: Handle child component

Open gnuletik opened this issue 4 years ago • 6 comments
trafficstars

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 :

  • Check recursively parent's property for isCE ?
  • Call inject even if !isCE and check for its result ?

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 avatar Aug 26 '21 17:08 gnuletik

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)

gnuletik avatar Aug 27 '21 11:08 gnuletik

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.

kazupon avatar Aug 27 '21 16:08 kazupon

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!

gnuletik avatar Aug 30 '21 16:08 gnuletik