Open leandromatos-hotmart opened 4 years ago
I believe the purpose of component interpolation is precisely to avoid having markup inside the translation strings. Instead, you can do this:
<i18n path="text" tag="p">
<template #link>
<a href="https://kazupon.github.io/vue-i18n/guide/interpolation.html">
{{ $t('linkText') }}
</a>
</template>
</i18n>
const messages = {
en: {
text: 'Go to {link}',
linkText: 'vue-i18n: Component interpolation'
}
}
producing:
<p>
Go to,
<a href="https://kazupon.github.io/vue-i18n/guide/interpolation.html">
vue-i18n: Component interpolation
</a>
</p>
This has the big advantage of keeping the markup out of the translation content and in your template (where it belongs).
This has the big advantage of keeping the markup out of the translation content and in your template (where it belongs).
In general I agree with that statement. But I think there are still some use-cases where html in the translations makes sense, like if you want to highlight 1 word by using a <b>
tag, or like the use-case of @leandromatos-hotmart.
Would it make sense to add something like a isHtml
option to the i18n tag?
Excellent example @tho-masn
In many cases, in projects I work on, I need to make something bold in the middle of the text or even create simple paragraphs to break the text, and in the middle of them, I need to use an i18n tag to interpolate with a hyperlink component for example.
An prop isHtml
would be a amazing!
I have the same issue. I need to place a <strong>...</strong>
inside a localized text.
As the docs says, you can use v-html
(https://kazupon.github.io/vue-i18n/guide/formatting.html#html-formatting)
const messages = {
en: {
test: 'this is just a <strong>test</strong> message',
}
}
<div v-html="$t('test')" />
output: this is just a test message
But it can be dangerous (see the warning in the doc links)
Or take a look at component interpolation: https://kazupon.github.io/vue-i18n/guide/interpolation.html#basic-usage
@Pochwar The problema is the component interpolation, he not render html.
Do you saw my example?
I believe the purpose of component interpolation is precisely to avoid having markup inside the translation strings. Instead, you can do this:
<i18n path="text" tag="p"> <template #link> <a href="https://kazupon.github.io/vue-i18n/guide/interpolation.html"> {{ $t('linkText') }} </a> </template> </i18n>
const messages = { en: { text: 'Go to {link}', linkText: 'vue-i18n: Component interpolation' } }
producing:
<p> Go to, <a href="https://kazupon.github.io/vue-i18n/guide/interpolation.html"> vue-i18n: Component interpolation </a> </p>
This has the big advantage of keeping the markup out of the translation content and in your template (where it belongs).
That doesn't solve the bold text problem. I agree on the security point, I also agree on the separation of concern, but handling bold text with interpolation is a pain. All the exemples and use cases show short sentences translation. But When you have a full paragraph with 4, 5 or more bold texts, italic + some links, well, welcome in hell. You have to cut the paragraph into too many pieces that translators don't understand what they have to do anymore.
A feature is definitely missing here.
A quick fix will be to allow v-html
inside the <i18n>
component, through a props for exemple. Quick, but dirty.
Another solution is to design a feature to allow some basic text stylisation. What about interpreting markdown? It could be a very handy solution, but it's a more complex feature. Maybe not all markdown properties but some of them could be a really nice improvement.
I'd like also support the idea that adding things like a
, li
, p
, strong
, etc tags in translation files makes life much simpler. This content is going to be generated by internal team members, stored in source control, reviewed, etc, and from my perspective is trusted content. Content from translators that enter markdown is going to be sanitized when converting to html, and is then subject to the same review & QA process as an engineer adding the copy to the files.
I'd also like to offer the perspective that in trying to prevent malicious copy with component interpolation, the approach adds more security concerns than it resolves. This relies on allowing unescaped html to be injected into template placeholders. An engineer might look at a copy section and think that an anchor tag is safe, and they can use v-html
for this one specific copy. If that section also has something like company_name
and that supplied variable has say a script tag in it, it is not escaped, and causes a security issue.
For comparison, Rails allows html in translations, uses an explicit _html
suffix on keys containing such content, and escapes any arguments supplied to the template. This results in a safe translation process, and one that I think should be mirrored in vue-i18n, maybe with a $tSafe or something like that.
I agree with this this feature. Especially for line-breaks it feels very silly.
Also might make sense to elaborate on the danger of XSS inside of templates. From what i understand, the only risk is when an attacker can choose which content is to be translated and displayed inside of vhtml
. If this is truly the only issue, then vhtml is an adequate solution to putting html inside the translations. If there are bigger security risks associated to vhtml then that would be good to know!
Putting HTML into translations also places a burden on translators because now they, too, need to know what HTML is and what therefor must be left unaltered within a translation.
Putting HTML into translations also places a burden on translators because now they, too, need to know what HTML is and what therefor must be left unaltered within a translation.
requirements for translators are a different discussion. Its unfortunate to burden translators with additional requirements, but its critical in some situations to insert HTML elements to add context to the text.
Since most markup will stay the same even after translation, most translators could actually ignore HTML content in their translations. a markup solution would solve your issue as well though.
How would you write this Text with component interpolation?
I love <b>cake</b></br>
lets bake together <i>today</i>!
Like this?
<i18n path="cake_text" tag="p">
{{ $t("part1") }}<b>{{ $t("bold") }}</b
><br />
{{ $t("part2") }}<i>{{ $t("italic") }}</i>
</i18n>
const messages = {
en: {
cake_text: "{0} {1} {2} {3} {4}",
part1: "i love",
bold: "cake",
part2: "lets bake together",
italic: "today!"
}
}
that doesn't look very translator friendly to me and this is only a 2 sentence example.
I like to add my vote to enable HTML be output as such inside interpolation from <i18n-t>
. Translated languages can have different grammar and different phrase symbols (think Quotes, Ampersand) which requires multiple paragraphs in the translation or HTML escapes.
As such, for a translation utility, I think this is a 1st class requirement.
I also like to know what the security issue is exactly. Translations are static pre-delivered and not user-generated for other users on the fly. Sanitisation would have to occur on borders, ie. first after a backend received data, not "inbetween every code line".
I just want to add my point of view and provide one option.
I agree that complex markup in translations is mess, I won't allow to translators make complex html in translations.
Interpolation may not be suitable in many cases.
As example of @fogx they provided I just want to add that each locale might have differences of number, order and positions of formatted parts. Syntax and commons in different languages are versatile. Then there is always additional task for dev team to adapt code supporting changes in content.
Our translators are already handling pluralisation (one, few, ...), parametrisation (you have {count} of tickets), etc.. it is not rocket science and they easily get used to it. I don't think it is problem to add few more which are even simpler than those I mentioned.
It has its styling and security issue as was already mentioned here, and also documentation warns about (reference).
I would love to see some alternative which may be like v-html but allowing only restricted markup.
xss lib is library for sanitising markup, it works fine
example:
<div v-html="xss($t('my-paragraph'))">
But implementation is quite complex, it is ok for computed values which are cached and only recalculates once edependency is changed, but there are cases (listing of products, which might be filtered, sorted, etc), where performance matters and some lightweight version would be welcome.. escaping all but <b>, <i>, ... few more maybe
.
I hope I summarize this somehow and also provide some solution with xss lib so someone can use.
Happy coding :)
Workaround is a bit dirty, but worked for me:
// add in app.js
Vue.component('i18n').options.props = {
...Vue.component('i18n').options.props,
displayHtmlTags: {
type: Boolean,
required: false,
default: false,
},
}
Vue.component('i18n').options.render = function (h, ref) {
var data = ref.data
var parent = ref.parent
var props = ref.props
var slots = ref.slots
var $i18n = parent.$i18n
if (!$i18n) {
return
}
var path = props.path
var locale = props.locale
var params = slots()
var children = $i18n.i(path, locale, params)
var tag = (!!props.tag && props.tag !== true) || props.tag === false ? props.tag : 'span'
if (props.displayHtmlTags) {
let fn = (item) =>
typeof item === 'string'
? h('span', { domProps: { innerHTML: `<span>${item}</span>` } })
: item
children = Array.isArray(children) ? children.map(fn) : [children].map(fn)
}
return tag ? h(tag, data, children) : children
}
Any new developments on this?
Isn't it possible to do something similar to Trans
component in React i18next package?
https://react.i18next.com/latest/trans-component#using-with-react-components https://react.i18next.com/latest/trans-component#alternative-usage-which-lists-the-components-v11.6.0
They allow you to use both indexed or named tags, that are replaced by components that are passed as parameters.
I think this could be a viable solution that would be similar to how i18next handles components
prop with React:
const messages = {
en: {
text: 'Go to <link externalLink="https://example.com">this very important link</link>'
}
}
With following template. Note that content
can be a string or a node or group of nodes and in Vue they could be rendered differently, so it makes sense to do a helper.
<script setup>
import { ContentHelper, i18n } from 'vue-i18n'
</script>
<template>
<i18n path="text" tag="p">
<template #link="{ content, someAttr }">
<MyLinkComponent :href="externalLink">
<ContentHelper :content="content" />
</MyLinkComponent>
</template>
</i18n>
</template>
Result would be a string with rendered link that was customized. v-html
can be still prohibited and approach still will work just fine.
I'm currently thinking of how to add this behaviour without a lot of pain to existing solution.
The approach should be very similar to how component interpolation works right now with following differences:
I did this component. I've used the same html parser i18next-react uses
I've used netlify i18n lib
InterpolateComponents.vue
<script lang="ts">
import {
type Component,
computed,
defineComponent,
h,
type PropType,
type Slots,
type VNode,
} from 'vue';
import HTML, { type AstNode } from 'html-parse-stringify';
// These are type for any translation key and typed version of composable. You can replace it however you want.
import { type TranslationKey, useTypedI18n } from '@/i18n';
// Recursively turns HTML AST into Vue nodes based on provided slots.
const mapVNodes = (astNodes: AstNode[], slots: Slots) =>
astNodes.reduce(
(acc, node) => {
if (node.type === 'component') return acc;
if (node.type === 'text') {
acc.push(node.content);
return acc;
}
// Continue recursion
const content = mapVNodes(node.children, slots);
const slotFunc = slots[node.name];
if (slotFunc !== undefined) {
acc.push(...slotFunc({ ...node.attrs, content }));
} else if (node.voidElement) {
acc.push(HTML.stringify([node]));
} else {
acc.push(
`<${node.name} ${Object.entries(node.attrs)
.map((attr) => attr.join('='))
.join(' ')}>`,
);
acc.push(...content);
acc.push(`</${node.name}>`);
}
return acc.flat();
},
[] as Array<VNode | string>,
);
const InterpolateComponents = defineComponent({
props: {
tag: {
type: [String, Boolean, Object] as PropType<string | false | Component>,
default: 'span',
},
path: {
type: String as PropType<TranslationKey>,
required: true,
},
params: Object as PropType<Record<string | number, string | number>>,
},
setup: ({ tag, path, params }, { slots }) => {
const { t } = useTypedI18n();
const interpolatedString = computed(() => (params !== undefined ? t(path, params) : t(path)));
const nodes = computed(() => {
// Wrap with tags to preserve text around tags in translation. This is how i18next-react does that too.
const rootNode = HTML.parse('<0>' + interpolatedString.value + '</0>')[0];
// Extract real nodes from rootNode
return rootNode.type === 'tag' ? rootNode.children : [];
});
return () => {
const vNodes = mapVNodes(nodes.value, slots);
if (tag === false) return vNodes;
return h(tag, vNodes);
};
},
});
export default InterpolateComponents;
</script>
RenderContentHelper.vue
<script lang="ts" setup>
import { type VNode } from 'vue';
const props = defineProps<{
content: Array<VNode | string>;
}>();
</script>
<template>
<component :is="() => props.content" />
</template>
const messages = {
en: {
text: 'Go to <link externalLink="https://example.com">this very important link</link>'
}
}
<InterpolateComponents path="text">
<template #link="{ content }">
<MyLink>
<RenderContentHelper :content="content" />
</MyLink>
</template>
</InterpolateComponents>
It's possible to render html tags with component interpolation?
Example:
I expect two paragraphs, and in the second paragraph, one link with my custom link component, but, the paragraphs was rendering as strings and not as html tags.
Maybe it's is a bug? Or is impossible to render an html tags inside a component?