i18next / react-i18next

Internationalization for react done right. Using the i18next i18n ecosystem.
https://react.i18next.com
MIT License
9.3k stars 1.03k forks source link

Support raw HTML in translation values #189

Closed KostyaEsmukov closed 8 years ago

KostyaEsmukov commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

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

jamuhl commented 8 years ago

no markdown built in...nope

KostyaEsmukov commented 8 years ago

Yeah, that's clear.

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

But they both are weird :-)

jamuhl commented 8 years ago

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

KostyaEsmukov commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

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

KostyaEsmukov commented 8 years ago

Yes I will, just too busy as for now.

01Kuzma commented 6 years ago

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 commented 6 years ago

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

all options to solve this...

jkiss commented 4 years ago

trans component will NOT trigger render when language changed 😢

jamuhl commented 4 years ago

@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 commented 4 years ago

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 commented 4 years ago

@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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

@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 commented 4 years ago

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

adrai commented 4 years ago

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

mwaeckerlin commented 4 years ago

@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 commented 4 years ago

@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 commented 4 years ago

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 commented 4 years ago

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

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

mwaeckerlin commented 4 years ago

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 commented 4 years ago

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

mwaeckerlin commented 4 years ago

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

jamuhl commented 4 years ago

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

mwaeckerlin commented 4 years ago

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 commented 4 years ago

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",
    …
  });