Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature Request: Use Vue's scoped slots for component interpolation #668

Closed
sirlancelot opened this issue Jul 24, 2019 · 5 comments
Closed

Comments

@sirlancelot
Copy link

sirlancelot commented Jul 24, 2019

One of my biggest struggles as I was learning vue-i18n's usages was its peculiar syntax for component interpolation. I see a lot of overlap between the place attribute and Vue's own slots mechanism. This suggestion is a breaking change, but I would like to see this library adopt Vue's own Scoped Slots in its next major version rather than continue forward with the homegrown place solution.


Basic usage example (single key replacement):

<!-- Current -->
<i18n path="term" tag="span">
  <a :href="url" target="_blank">{{ $t('tos') }}</a>
</i18n>

<!-- Proposed -->
<i18n path="term" tag="span" v-slot>
  <a :href="url" target="_blank">{{ $t('tos') }}</a>
</i18n>

The library would need to replace {0} in messages with the default scoped slot.


Advanced usage example (multiple key replacement):

<!-- Current -->
<i18n path="info" tag="p">
  <span place="limit">{{ changeLimit }}</span>
  <a place="action" :href="changeUrl">{{ $t('change') }}</a>
</i18n>

<!-- Proposed -->
<i18n path="info" tag="p">
  <template #limit>
    <span>{{ changeLimit }}</span>
  </template>
  <template #action>
    <a :href="changeUrl">{{ $t('change') }}</a>
  </template>
</i18n>

The library would need to replace {limit} & {action} in messages with the respectively named scoped slot.


Replace usage of the places prop:

<!-- Current -->
<i18n path="info" tag="p" :places="{ limit: refundLimit }">
  <a place="action" :href="refundUrl">{{ $t('refund') }}</a>
</i18n>

<!-- Proposed -->
<i18n path="info" tag="p">
  <template #action><a :href="refundUrl">{{ $t('refund') }}</a></template>
  <template #limit>{{ refundLimit }}</template>
</i18n>

I propose removing the places="{}" prop but strictly speaking the two can live and work together.


The proposed syntax is admittedly more verbose than the current syntax, but I believe it will feel more natural. It is more consistent with Vue's own syntax so developers will not need to learn or remember a special syntax for this library.

@sirlancelot
Copy link
Author

sirlancelot commented Jul 26, 2019

Here's the simplest prototype I could come up with:

export default {
  functional: true,
  props: {
    tag: { type: String, default: undefined },
    path: { type: String, required: true },
    locale: { type: String, default: undefined },
  },
  render(h, { data, parent, props, slots }) {
    const { $i18n } = parent
    if (!$i18n) throw new Error("Cannot find `vue-i18n` instance.")

    const params = slots()
    const children = $i18n.i(
      props.path,
      props.locale,
      onlyHasDefaultPlace(params) ? params.default : params
    )

    const { tag } = props
    return tag ? h(tag, data, children) : children
  },
}

function onlyHasDefaultPlace(params) {
  const places = Object.keys(params)
  return places.length === 1 && places[0] === "default"
}

Turned out to be much simpler than I thought thanks to $i18n.i(). This implementation supports named places as well as array places in much the same way as the existing version. I made tag optional since Vue Functional Components don't need to adhere to the single node rule.

Feel free to use and modify as desired :)

@kazupon
Copy link
Owner

kazupon commented Jul 28, 2019

@sirlancelot
Thank you for your suggestion!

As you say, Using a slot is a similar syntax of Vue and is easier to learn.
I would like to support it, but it is likely that we need supports backward compatibility to support the case with the place attribute.

@sirlancelot
Copy link
Author

Makes sense, I'll see if I can prototype a backwards-compatible version.

@sirlancelot
Copy link
Author

I made an improved version below. It is also backwards-compatible with both place & places and emits deprecation warnings. The warnings could link to an upgrade guide showing how to uplift existing codebases.

import { warn } from "../util"

export default {
  name: "i18n",
  functional: true,
  props: {
    locale: { type: String, default: undefined },
    path: { type: String, required: true },
    places: { type: [Array, Object], default: undefined },
    tag: { type: String, default: undefined },
  },
  render(h, { data, parent, props, slots }) {
    const { $i18n } = parent
    if (!$i18n) throw new Error("Cannot find `vue-i18n` instance.")

    const params = slots()
    const children = $i18n.i(
      props.path,
      props.locale,
      onlyHasDefaultPlace(params)
        ? useLegacyPlaces(params.default, props.places)
        : params
    )

    const { tag } = props
    return tag ? h(tag, data, children) : children
  },
}

function onlyHasDefaultPlace(params) {
  var prop
  for (prop in params) if (prop != "default") return false
  return Boolean(prop)
}

function useLegacyPlaces(children, places) {
  const params = places ? createParamsFromPlaces(places) : {}
  const everyPlace = children.every(vnodeHasPlaceAttribute)

  if (process.env.NODE_ENV !== "production" && everyPlace)
    warn("`place` attribute is deprecated. Please switch to Vue slots.")

  return children.reduce(
    everyPlace ? assignChildPlace : assignChildIndex,
    params
  )
}

function createParamsFromPlaces(places) {
  if (process.env.NODE_ENV !== "production")
    warn("`places` prop is deprecated. Please switch to Vue slots.")

  return Array.isArray(places)
    ? places.reduce(assignChildIndex, {})
    : Object.assign({}, places)
}

function assignChildPlace(params, child) {
  params[child.data.attrs.place] = child
  return params
}

function assignChildIndex(params, child, key) {
  params[key] = child
  return params
}

function vnodeHasPlaceAttribute(vnode) {
  return Boolean(vnode.data && vnode.data.attrs && vnode.data.attrs.place)
}

As always, feel free to use and modify as desired :)

@kazupon
Copy link
Owner

kazupon commented Aug 12, 2019

close due to merge #685
Thank you very much!

@kazupon kazupon closed this as completed Aug 12, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants