dkfbasel / vuex-i18n

Localization plugin for vue.js 2.0 using vuex as store
MIT License
665 stars 56 forks source link

Translation of HTML context (e.g. paragraph with links inside) #48

Open infinite-dao opened 6 years ago

infinite-dao commented 6 years ago

Hi,

by default all HTML in a translation key is rendered as string and no possibility is given to render also translated HTML within a translation key. Right? How is it possible to capacitate the package and having something like the component interpolation of <i18n> in vue-i18n? So the code would look like:

<i18n path="term" tag="label" for="tos">
  <a :href="url" target="_blank">{{ $t('tos') }}</a>
</i18n>

… that renders to something like this:

<label for="tos">
  I accept xxx <a href="/term" target="_blank">Term of Service</a>.
</label>

Or more advanced to put HTML to named target-“places” in the parent translation context:

en: {
  info: 'You can {action} until {limit} minutes from departure.',
  change: 'change your flight'
}

… coded as:

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

… renders to:

<p>
  You can <a href="/change">change your flight</a> until <span>15</span> minutes from departure.
</p>

What can I code to get this functionality? Or how can one implement this?

Or is there another work around to come to this?

tikiatua commented 6 years ago

Hi @infinite-dao,

I have seen this functionality in vue-i18n and think it is an important use case. Unfortunately, vuex-i18n does currently not support this. In my opinion, the solution of vue-i18n might also have some room for improvement.

For now, we are using the locale detection with $i18n.locale() and render different components based on the respective locale.

infinite-dao commented 6 years ago

One quick work around I could find was to use v-html directive of Vue.js but it is not safe, messages like this:

en: {
  info: 'You can {action} until {limit} minutes from departure.',
  change: 'change your flight'
}

… can be coded as:


<p v-html="$t(
  'info', {
    'action': 'html composed string',
    'limit': 'html composed string'
  })">
</p>
tqwewe commented 6 years ago

Bump! This is a must have feature for us.

tikiatua commented 6 years ago

I will try to look into this sometime during the next two weeks.

mnedoszytko commented 6 years ago

bump on that. I need to translate a text with an inside, like this

<li>{{ $t('schedules.set_interesting_options', { icon: 'fa-gear'}) }}</li> translate source:

{ 'en: { 'set_interesting_options' => 'Set some interesting options <i class="fa fa-icon :icon"></i>, ... more text' }}

currently it doesnt even render the translate output and outs $t( in the html

tikiatua commented 6 years ago

Hi everyone,

I will look into this again as I am preparing a presentation of the vuex-i18n library for a vue meet up in Switzerland in November. It will probably boil down to using a custom component that will take component functions as props.

awakash commented 6 years ago

Bump, same here, thanks!

dcshiman commented 5 years ago

You can use v-html to html elements as follows

<span v-html="$t('term', {
    'link': `<a href='/link.html'>${$t('link')}</a>`
  })" 
/>
eliranmal commented 5 years ago

an issue on the i18next-node repository points at a similar problem (albeit from another point of view, and at a lower level). Jan Mühlemann, a core member, offered to use post-processing of markdown content, which IMHO suits here pretty well.

the idea is to add a markdown plugin (though you can also write your own), so you wouldn't have to parse HTML (it's not a regular language!), but only markdown. the template output would still be HTML, after the i18next post-processor had done its job.

any thoughts?

@tikiatua @infinite-dao @Acidic9 @mnedoszytko @awakash @dcshiman

eliranmal commented 5 years ago

@dcshiman, note that the v-html solution may shadow issues with input validation, increasing the chances of missing potential XSS vulnerabilities in the code (as hinted by @infinite-dao in the above comment).

tikiatua commented 5 years ago

Hi @eliranmal

Thank you for your input. I am finally getting around to tackle some of the harder problems with this library. Interpolation of html and components is definitely on the list.

I was thinking that interpolation could be achieved on an opt-in basis. Where the developer defines, which tags are allowed to be used for interpolation and all others are automatically stripped (with a warning in debug mode).

This could probably look something like this

<div>
   {{ $t(
      'lookupIdentifierForMessage', {
       // components to make available in the translation
       components: {
         //  strings can be used to interpolate html elements
         title: 'h1',
         // custom components can be used with a different tag
         bold: MyBoldComponent,
         // or with the default tag, i.e. <blinking>..</blinking> 
         Blinking
       },
      // data to make available in the translation
       data: {
            what: 'something like this'
       }
    ) }}
</div>

And in the translation, one would be allowed to use only the specified tags.

const translations = {
  'lookupIdentifierForMessage': '<title>this is important</title>\nWe should <bold>not</bold>forget the security implications of <blinking>{what}</blinking>!'
}
eliranmal commented 5 years ago

@tikiatua, thanx, it's great to hear that you're on it!

the imperative API is well done - it's both very expressive and flexible.
i agree that the white-list approach is preferable when mitigating XSS-related issues, however, choosing which tags to render is not the whole solution; should we now check for element attributes as well?

IMHO, introducing markup inside translation values would unnecessarily complicate the code, as it will include new responsibilities to bare (sanitizing the HTML). we can solve it by delegating the job of rendering tags to the API consumer (e.g. the UI framework layer). my suggestion is to leave hook functions in the chain for that purpose, something like:

$t(
  'lookupIdentifierForMessage', {
    plugins: {
      beforeRender(translation) {
        // a chance to use an external library for keeping all the XSS shenanigans out
        return HtmlCleaner.sanitize(translation);
      }
    },
    // ...
  }
)

or maybe just incorporate the above implementation (a general sanitizing library) into vuex-i18n, as long as only placeholders are used, not tags.

i hope that makes sense in the context of this issue; i'm not very familiar with vuex-i18n, and i only swept through the code.

tikiatua commented 5 years ago

Good point. My idea was, that element attributes would be limited to props of the respective components. In addition, we would not actually evaluate the html, but parse the content and build a custom render function to create the respective virtual dom.

Something like this:


message: '<h1>This is my <blinking cadence="10">title</blinking></h1>'

// is complied to
render: function(createElement){
    return createElement('h1', [
       "This is my",
       createElement(MyBlinkingComponent, {
           props: { cadence: 10 }
       }, "title" )
    ])
}
eliranmal commented 5 years ago

@tikiatua, that sounds like a great idea - if we don't evaluate any markup, we lower the risk of injections.

also, if the code eventually prepares a render function, we do delegate the job to a higher level, and we can still leave hooks for user intervention (enabling points of integration to actually evaluate the markup).

the added complexity is presumably still there - we'd have to keep so the parsing and keep a whitelist of tags, but if all attributes are just derived from the component's props, the task becomes a lot easier; i love that idea.

by the way - it's worth investigating how does vue itself interprets templates to generate imperative instructions for building the virtual dom - you may be able reuse that code for your purposes (will probably be found in some auxilary code, like vue-template-loader?).

Gvade commented 5 years ago

I've made a quick solution to this problem. It's forked here. The example: doc. It assumes that you're using { and } as the identifiers (I didn't manage to access plugin config value from the external file in order to pick the actual ones).

BlakeHancock commented 4 years ago

I liked how @Gvade's solution worked, but it wasn't very practical for doing simple things like bolding a word in a sentence.

I expanded on it by having it check if the translation key + '__html' exists in the JSON translation file and then compiling it as HTML instead of text, but otherwise working the same way. The only caveat is you can't use slot arguments in attributes, but regular arguments work.

Gist of the component can be found here

Note that I have my translation files grouped by modules so there are a few extra component props to handle that.

There is also a whitelist of elements and attributes at the top as well as a way to specify what vuex-i18n identifiers you want to use. (Currently '{{' and '}}')