kaisermann / svelte-i18n

Internationalization library for Svelte
MIT License
1.25k stars 80 forks source link

Add ability to easily internationalize text containing links #118

Open marekdedic opened 3 years ago

marekdedic commented 3 years ago

Hi, I have a text to internationalize which contains a link. Currently, there is no easy way to do this.

Describe alternatives you've considered I have tried:

$_("text", {values: {link: '<a href="...">' + $_("linkText") + "</a>"}})

with

{
    "text": "Go to {link} to find out more!",
    "linkText": "our awesome website"
}

but that does not work as the HTML is escaped (unless I do {@html, but then there would be no protection from XSS in localizations - don't know if that's part of your threat model). I can always do

$_("textBefore") + '<a href="...">' + $_("linkText") + "</a>" + $_("textAfter"))

with

{
    "textBefore": "Go to ",
    "linkText": "our awesome website",
    "textAfter": " to find out more!",
}

but that is really ugly and with multiple links would get messy quickly...

How important is this feature to you? It would be nice to be able to do this, but it's not a deal breaker

Additional context Here is the solution vue-i18n uses: https://kazupon.github.io/vue-i18n/guide/interpolation.html#slots-syntax-usage - not great, not terrible.

Thanks!

PiotrBaczkowski commented 3 years ago

would be amazing to see it

buttjer commented 3 years ago

try

{@html $_('textwithlink')}

marekdedic commented 3 years ago

Yeah, I thought about that (see the mention of @html in the original comment), but that removes all the XSS protections - which is not necessary.

ghost commented 3 years ago

Hey,

Is using markdown inside messages an option?

I used a translate wrapper store that parses inline markdown after translating, using markedjs. You could use purgeHTML to sanitize that?

export const translateMarkdown = derived([t], ([$t]) => {
  return (key, options) => {
    const translated = $t(key, options);

    return markdown.parseInline(translated);
  };
});

Or create your own wrapper for just links?

I also struggled with this and tried using the format option from the MessageObject directly, but I couldnt figure that out. (formatting)

Hope any of these suggestions help.

kaisermann commented 3 years ago

Hm, I can't think of a way of doing this without the @html modifier. We can't pass a "SvelteElement" around as we do with JSX (SvelteElement i.e). The approach proposed by @rvantonisse-accessibility, with the addition of a sanitiser, could help to prevent XSS attacks.

brunnerh commented 2 years ago

I needed a function like this as well and decided to mark up the links with square brackets and post-process that with a function which adds the link elements.

e.g.

{ "link-text": "This text has [a link] or [two] embedded." }
<p>{@html links($_('link-text'), link1, link2)}</p>

This approach works well enough in most cases, but there are some things to consider:

There might be even more. My implementation is rather shoddy (e.g. uses regex for "parsing") and just fits my specific requirements. If this were to be added as part of the library, it probably should be flexible while still maintaining ergonomics as much as possible, which may be difficult...

ZerdoX-x commented 2 years ago

@kaisermann maybe we could build an integration with svelte-markdown? It could handle issue partially. But ability to include some custom attributes will disappear (see svelte-markdown/Link.svelte, it only accepts title and href). But all other markdown elements will now be available too.

So library doesn't use {@html ...} (see https://github.com/pablo-abc/svelte-markdown/issues/42). Which will save from XSS as @marekdedic mentioned

TiagoVentosa commented 1 year ago

Hey there! Recently in my project I ended up having the same problem, and we ended up with a simple solution.

I had ideas to expand that solution to make it more general, and possibly do a pull request here, but would like to know your opinions first. In terms of use it would be something like this:

Strings would contain a specific marker and (possibly optional) tags, something like this

{"text.welcome": "Welcome to home. To ask for help click [[linkHelp|here]]"}

A new component in the library could be used to add text with your own components (maybe with a better name):

<MessageFormatter textId="..." options="...">
  <a href="/help" slot="linkHelp" let:text={t}>
    {t}
  </a>
</MessageFormatter>

Main problem I see (there might be others, not sure until I start trying) is that I would probably require to add a new library to parse the string (probably parser, since that one is already an indirect dependency of this library)

Opinions?

Edit: intl-messageformat already supports rich text with simple XML tags, so probably better to use that. The text above would become:

{"text.welcome": "Welcome to home. To ask for help click <linkHelp>here</linkHelp>"}
marekdedic commented 1 year ago

I think what you're describing is in principle the same thing vue-i18n does, right?

TiagoVentosa commented 1 year ago

So using slots won't work, since it is not possible to use dynamic slot names. The other solution (passing the svelte components and its props as a prop) is what I had in my project, but feels very hack-y. Until dynamic slots are possible I guess this won't be solved.

felixgirault commented 1 year ago

As @TiagoVentosa said, using intl-messageformat's mechanism for rich text is probably the best solution (https://formatjs.io/docs/intl-messageformat/#rich-text-support)

For example, rendering a link could look like that :

{"richText": "This text has a <homeLink>link to the home page</homeLink>"}
$_({
  id: 'richText',
  values: {
    homeLink: (chunks) => `<a href="/">${chunks}</a>`
  }
})

// This text has a <a href="/">link to the home page</a>

This works out of the box with intl-messageformat, but not with svelte-i18n. It took me ages to realize that this was because of the ignoreTags option (https://github.com/kaisermann/svelte-i18n/pull/121).

So, to get the above example to work, you should initialize svelte-i18n like that:

init({
  // …
  ignoreTag: false
});

Sadly, this is a global option, so beware if you already use HTML tags in some translations, this could probably mess them up :thinking:

SavageCore commented 1 year ago

@felixgirault using your example the text This text has a <a href="/">link to the home page</a> is displayed. Am I missing something? We'd still need @html to render it?

"text": "This text has a <homeLink>link to the home page</homeLink>"
<div>
  {$_('text', {
    values: { homeLink: (chunks) => `<a href="/">${chunks}</a>` },
  })}
</div>