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

Support raw HTML in translation values #189

Closed
KostyaEsmukov opened this issue Jul 24, 2016 · 34 comments
Closed

Support raw HTML in translation values #189

KostyaEsmukov opened this issue Jul 24, 2016 · 34 comments

Comments

@KostyaEsmukov
Copy link
Contributor

Say we have the next translation resource:

{foo: "<b>Bar!</b>"}

Then <div><Interpolate i18nKey='foo' /></div> will result in the next output:
<div>&lt;b&gt;Bar!&lt;/b&gt;</div> , while I expect to get this: <div><b>Bar!</b></div> , so a raw HTML in translation strings should not be escaped.

Is this an intended behavior, or is a bug?

I guess replacing this line with something like
child = React.createElement(parent, {dangerouslySetInnerHTML: {__html: match}}); will work this out.

@jamuhl
Copy link
Member

jamuhl commented Jul 24, 2016

right changing that line would work it out - but lead to dangerous insert of pure html - depending on interpolating of user inputs would lead to xss vulnerability.

i won't open this door - but you can create a own component doing so - but i would suggest you don't use html in your translations - better use a markdown component and use markdown...

@KostyaEsmukov
Copy link
Contributor Author

Yeah, I share your concern about allowing use of raw HTML.

However, user input can not be in translation values. So it's only a translator who can put harmful HTML code, but we are supposed to trust our translators and verify their work, aren't we? :)

What do you think about adding a prop like escapeHTMLinValues set to true by default? I can submit a PR if you find this appropriate.

@jamuhl
Copy link
Member

jamuhl commented Jul 25, 2016

why not just doing it like in the sample: https://github.com/i18next/react-i18next/blob/master/example/app/components/View.js#L19 interpolating a strong component?

@KostyaEsmukov
Copy link
Contributor Author

KostyaEsmukov commented Jul 25, 2016

The a interpolated component string is not translated here actually. I need to put some bold parts to a value. Splitting it to multiple keys looks awkward, especially if it is just 3 bold words for a long text.

BTW There's no way to use markdown out of the box, is there?

@jamuhl
Copy link
Member

jamuhl commented Jul 25, 2016

<Interpolate i18nKey='common:interpolateSample' component={<strong>{t('key'}</strong>} />

@jamuhl
Copy link
Member

jamuhl commented Jul 25, 2016

no markdown built in...nope

@KostyaEsmukov
Copy link
Contributor Author

Yeah, that's clear.

Another option as for now is
<div dangerouslySetInnerHTML={{__html: t('foo')}} />

But they both are weird :-)

@jamuhl
Copy link
Member

jamuhl commented Jul 25, 2016

yes...why not using eg. react-remarkable and pass markdown from t function to that?

@KostyaEsmukov
Copy link
Contributor Author

Yes it's possible, but it's no better than the dangerouslySetInnerHTML approach, as it would process markdown from user inputs as well.

I sure can write my own components which do whatever I want (in fact I already did), but I guess it's not just me having this problem #80 , so it would be great if this package had a solution for it out of the box. And I'm ready to contribute, as soon as we come up with a consensus.

Maybe this should be a new component, extended from the Interpolate?

@jamuhl
Copy link
Member

jamuhl commented Jul 26, 2016

it will process user input - but you can configure remarkable to not allow html tags or just defined tags - so no risk there.

So i think the solution with markdown or <div dangerouslySetInnerHTML={{__html: t('foo')}} /> is the way to go...might be ugly - but the uglyness remembers what is going on.

you might do a PR adding a prop dangerouslySetInnerHTML=true - but it need to be visible that people are doing something dangerous

@jamuhl jamuhl closed this as completed Jul 28, 2016
@jamuhl
Copy link
Member

jamuhl commented Jul 28, 2016

closing for now...if you still want to provide a PR go for it...

@KostyaEsmukov
Copy link
Contributor Author

Yes I will, just too busy as for now.

@01Kuzma
Copy link

01Kuzma commented Feb 11, 2018

Hi!
Probably it's not necessarily to create new issue. My question is very close to it.
How the new line tag - <br /> could be added in translation string of i18n.js file ?

translations: {
    "key":"long translation <br /> needed"
}

Thank you!

@jamuhl
Copy link
Member

jamuhl commented Feb 11, 2018

@01Kuzma it's the same question...like said...using a component setting inner html #189 (comment) or using markdown or using https://react.i18next.com/components/trans-component.html

all options to solve this...

@jkiss
Copy link

jkiss commented Apr 10, 2020

trans component will NOT trigger render when language changed 😢

@jamuhl
Copy link
Member

jamuhl commented Apr 10, 2020

@jkiss not the concern of that Component...it's used too often in one Component -> to much bindings to much noisy updates...languageChange is a "pagelevel" concern

@mwaeckerlin
Copy link

mwaeckerlin commented May 18, 2020

First of all, what HTML formatting has to be used, except fot technical structure, should be up the translator, not up to the developer, so all markup belongs to the translation files, not to the code. E.g. how many paragraphs there should be, depends on the content, e.g. the marketing wants to publish.

I don't think it is a good idea to have HTML-tags within a string, but it should be possible to add norml JSX in ttanslations.

I suggest a syntax like this:

i18n.use(LanguageDetector).init({
  resources: {
    en: {
        translation: {
             marketing: (
                 <>
                     <h1>Coolest Product in the world</h>
                     <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
                     eiusmod tempor incididunt ut labore et dolore magna aliqua. Dictum
                     non consectetur a erat nam.</p>
                     <p>Arcu cursus euismod quis viverra nibh cras pulvinar. Commodo nulla
                     facilisi nullam vehicula. Eget magna fermentum iaculis eu non diam
                     phasellus vestibulum lorem.<p>
                     <p>Est ultricies integer quis auctor. Ornare suspendisse sed nisi lacus.
                     Nisl vel pretium lectus quam. Nisl rhoncus mattis rhoncus urna neque
                     viverra. Nulla facilisi cras fermentum odio eu feugiat pretium nibh
                     ipsum.</p>
                 </>
              ),
             about: (
                 <>
                     <h1>About Our Product</h1>
                     <p>Bibendum enim facilisis gravida neque convallis a cras semper
                     auctor. Mattis rhoncus urna neque viverra justo nec. Tellus pellentesque
                     eu tincidunt tortor aliquam nulla facilisi cras. Montes nascetur ridiculus
                     mus mauris.</p>
                     <ul>
                         <li>Volutpat consequat mauris nunc congue nisi vitae suscipit tellus.</li>
                         <li>Sagittis id consectetur purus ut faucibus pulvinar elementum.</li>
                     </ul>
                     <p>In hendrerit gravida rutrum quisque non tellus orci ac auctor. Morbi
                     non arcu risus quis varius quam quisque. Urna cursus eget nunc
                     scelerisque.</p>
                 </>
             )
        }
    },
  },
…
});

@jamuhl
Copy link
Member

jamuhl commented May 18, 2020

@mwaeckerlin that's completely a different usecase from what the Trans component does -> Trans component is for embedding rich react elements into a translation - not for rendering prosa

for the prosa case (long text with formatting):

@mwaeckerlin
Copy link

Normally in your code, you have both to be translated: simple tags and texts, such as form labels etc., but often prosa, i.e. help texts and explanations. And in my experience, it is very bad, if the programmer defines the structure, e.g. how many paragraphs there should be, not the translator. Because then, sales come in, support and marketing, and ask for completely different structures. Then, if that need code change, the result is often a hack and a mess.

Previously. I've been using react-markup for specific messages. As you suggest, react-render-html is also an option, but both for me still looks like a dirty hack. The programmer, not the translator needs to decide which texts will be parsed. Also, having markup or HTML in one huge string is not readable and so really maintainable.

IMHO having the ability to add JSX in the tanslation files, as alternative to simple strings, would be best solution. Then the translator (and communications department) has full control over the content, while the programmer just cares about the logic.

@jamuhl
Copy link
Member

jamuhl commented May 18, 2020

There is already one option: https://react.i18next.com/latest/trans-component#using-for-less-than-br-greater-than-and-other-simple-html-elements-in-translations-v-10-4-0 but it has it's limitations...but might ok for your case

@jamuhl
Copy link
Member

jamuhl commented May 18, 2020

IMHO having the ability to add JSX in the tanslation files, as alternative to simple strings, would be best solution. Then the translator (and communications department) has full control over the content, while the programmer just cares about the logic.

Just not the way it works...again JSX is not a string...you do not have JSX on the client during runtime...you have functions

@mwaeckerlin
Copy link

mwaeckerlin commented May 18, 2020

@jamuhl, thank you for the link. I have seen this, but it does not fit my case, because the structure is given in the code, not in the translation file.

To better understand my point, in the past, I've seen code like this (not really in JavaScript/React, but in PHP/Joomla, but basically it's the same problem):

<h2>{t('maintitle')}</h2>
<h3>{t('subtitle')}</h3>
<p>{t('p1')}</p>
<p>{t('p2')}</p>

Then the product owner asked for having only the main title and one paragraph. With this construct, that requires a code change. Changing texts was easy on our system, just edit the new text in an online form. But changing the code means full build, full test scenarios, approvement by the change board and a maintenance window. That's why I say, text and it's structure belong together and should be separated from code.

So my current solution is this:

i18n.use(initReactI18next).init({
  resources: {
    en: {
      translations: {
        mytext: `
          <h2>Main Something</h2>
          <h3>Some Subtitle</h3>
          <p>Here some text.</p>
          <p>Here some more text.</p>
        `,
        },
      },
    },
  },
[…]
});

export default i18n;

And then, where I use it, I just add:

            {renderHTML(this.props.t("mytext"))}

So, as a programmer, I just define the area, where some text can be inserted, but the translators are then completely free on what and how they want to place there.

But I wish, there were a better, native solution. I I would write a translation library, something like embedded JSX would be one of the key-features.

How do others solve this flexibility for the ttranslators / texters issue? Is the above workaround often used?

@jamuhl
Copy link
Member

jamuhl commented May 18, 2020

@mwaeckerlin fully agree...I like the idea...providing a PR I will immediately merge it and publish an update

@adrai
Copy link
Member

adrai commented May 18, 2020

there’s also the option to postprocess, like: https://www.npmjs.com/package/i18next-markdown-jsx-plugin

@mwaeckerlin
Copy link

@jamuhl, than you for your motivation, @adrai, than you for your Idea. I started to prepare a patch, then analyzing the code brought me to this already existing solution:

How to Use JSX in the Translation File

There is a option returnObjects that allows objects to be returned instead of strings only. Unfortunately, that object is not directly a JSX that can be rendered. But there is another option, returnedObjectHandler that is called, when returnObjectsis false. It takes three parameters, the middle of it is the JSX I am looking for.

So the solution is:

import i18n from "i18next";
import { initReactI18next } from "react-i18next";

i18n.use(initReactI18next).init({
  resources: {
    en: {
      translations: {
        login: {
          test: (
            <>
              <h1>This is my Test</h1>
              <p>Hello World</p>
            </>
          ),
        },
      },
    },
  },
  fallbackLng: "en",
  debug: true,
  returnObjects: false,
  returnedObjectHandler: (key, value, options) => value,
  ns: ["translations"],
  defaultNS: "translations",
  interpolation: {
    escapeValue: false, // not needed for react!!
    formatSeparator: ".",
  },
  react: {
    wait: true,
  },
});

export default i18n;

Then just add in your JSX Code:

            {this.props.t("login.test")}

No more need for renderHTML or any other dirty trick!

Please add this to the documentation, if it is not already there (and I missed it).

@jamuhl
Copy link
Member

jamuhl commented May 19, 2020

@mwaeckerlin this solution has one big drawback...it only works as long your translations are located in the source code and transpiled. As the content of login.test is a function that content is nearly impossible for a translator to work with (you can't import/export it to any translation management system)...therefore I won't recommend this as a best practice.

@mwaeckerlin
Copy link

When you want to enter JSX directly, then I see no other solution. JSX is a JavaScript object that does not exist in this format in plain JSON.

What you could do is something like:

        login: {
          test: {
              h1: This is my Test
              p: Hello World
            }
          ),
        },

Then in returnedObjectHandler write a mapper. But this way, you have less flexibility (no attributes, so no links, unless you invent some special syntax) and you have to explicitely map all supported html tags. But if you must work with a translation management system, it's probably an option.

Also my solution above is not that bad:

Even though the file ending is *.js, you still can put the translations into a separate file, so the differences are very low and a translator who knows html could work with the translation file manually. The translation file would then look like:

import React from "react";

export default {
  login: {
          test: (
            <>
              <h1>This is my Test</h1>
              <p>Hello World</p>
            </>
          ),
         api: {
           error: {
             infos: "Failed to Load Info",
             load: "Failed to Load API",
           },
           request: {
             send: "sending",
             retrieve: "retrieving",
           },
         },
  },
};

Or you can even split this in two files, one *.js for the complex texts and a normal *.json file for all the simple texts that can be handled by a translation tool.

What do you think is the best way to go? What is your suggestion, @jamuhl? What could be a good practice?

@jamuhl
Copy link
Member

jamuhl commented May 19, 2020

@mwaeckerlin like said...personally I would use:

  • markdown
  • HTML and use the react-render-html
  • or Trans with the "simple HTML"

But if it works for your case and team...it's ok 👍

@mwaeckerlin
Copy link

Translator's Choice

Everything at Once: JSX, HTML-Text, Markup

To give your translator the transparent choice between: simple text, JSX, html string, markup string:

file: translations/en.js (except JSX, this could be a .json):

import React from "react";

export default {
  login: {
    test0: (
      <>
        <h1>This is my Test0</h1>
        <p>Hello World</p>
      </>
    ),
    test1: {
      html: `
        <h1>This is my Test1</h1>
        <p>Hello World</p>
        `,
    },
    test2: {
      markdown: `# This is my Test2
      
Hello World
  - item 1
  - item 2`,
    },
    test: "This is a simple text",
  },
};

Then in returnedObjectHandler, if it is an object with element html, render html. if it has element markdown, render markdown:

import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import LngDetector from "i18next-browser-languagedetector";
import en from "translations/en";
import de from "translations/de";
import renderHTML from "react-render-html";
import ReactMarkdown from "react-markdown";

i18n
  .use(initReactI18next)
  .use(LngDetector)
  .init({
    resources: {
      en: {
        translations: en,
      },
      de: {
        translations: de,
      },
    },
    fallbackLng: "en",
    debug: true,
    returnedObjectHandler: (key, value, options) => {
      if ("html" in value) return renderHTML(value.html);
      if ("markdown" in value) return <ReactMarkdown source={value.markdown} />;
      return value;
    },
    // have a common namespace used around the full app
    ns: ["translations"],
    defaultNS: "translations",
    interpolation: {
      escapeValue: false, // not needed for react!!
      formatSeparator: ".",
    },
    react: {
      wait: true,
    },
  });

export default i18n;

What do you think about this solution, @jamuhl?

@jamuhl
Copy link
Member

jamuhl commented May 19, 2020

@mwaeckerlin like said...if it works for you - go for it...

@mwaeckerlin
Copy link

@jamuhl, isn't that what you suggested, or do I misunderstand your point?

@jamuhl
Copy link
Member

jamuhl commented May 19, 2020

yes...I personally just don't use the handler for that but use <ReactMarkdown source={t('markdownKey')} /> inside my render...

@mwaeckerlin
Copy link

It's what I used to use before, but less flexible for the translator. I'll now give my suggestion a try and see, how it works together with other tools. I'll keep you updated.

@mwaeckerlin
Copy link

mwaeckerlin commented May 22, 2020

Ok, @jamuhl, you were completely right: Scanners just ignore (and therefore overwrite) objects instead of strings. So I have a new solution, that works together with i18next-parser: Instead of allowing an object in the translation, I add keywords to the texts: If the string starts with html:, it will be converted to html, if it starts with markdown, it will be converted to markdown.

Moreover I decided to use yaml for my translation files, since yaml allows multi line strings. So my translation file looks e.g. like this:

login:
  help: >-
    html:
    <p>Some help text</p>
    <p>Another paragraph of help text.</p>

Unfortunately, after running i18next-parser, the nice formatting is merged into one line and looks like:

  help: "html: \n<p>Some help text</p>\n<p>Another paragraph of help text.</p>"

But at least entering multiline mode handy when I edit it.

Then I add a post processor before init, instead of the returnedObjectHandler:

i18n
  .use({
    type: "postProcessor",
    name: "formatted-text",
    process: (value, key, options, translator) => {
      if (value.match(/^ *html: */))
        return renderHTML(value.replace(/^ *html: */, ""));
      if (value.match(/^ *markdown: */))
        return <ReactMarkdown source={value.replace(/^ *markdown: */, "")} />;
      return value;
    },
  })
  .init({
    postProcess: "formatted-text",
    …
  });

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

6 participants