react-i18next icon indicating copy to clipboard operation
react-i18next copied to clipboard

Double-escaping of interpolated values in component props

Open David0036 opened this issue 2 months ago • 2 comments

🐛 Bug Report

When using Trans component with escapeValue: true and passing interpolated user input as component props (e.g., title attribute), the values are double-escaped. HTML entities like " and ' render as " and ' instead of their actual characters, making the output unreadable.

Additionally, without explicit escapeValue: true in tOptions, the Trans component fails to parse and replace componenttags entirely, rendering them as literal text.

To Reproduce

minimal reproducible example: stackblitz

 {
   "hello": "Hello <Item title=\"{{ name }}\" />!"
 }

Code:

import { Trans } from 'react-i18next';
import type { FC } from 'react';

function App() {
  const value1 = 'World " \'" Test';
  const value2 = "World \" '\" Test";

  return (
    <>
      <div>
        case 1:{' '}
        <Trans
          i18nKey="hello"
          values={{ name: value1 }}
          components={{ Item: <Item /> }}
          shouldUnescape
        />
      </div>

      <div>
        case 2:{' '}
        <Trans
          i18nKey="hello"
          values={{ name: value2 }}
          components={{ Item: <Item /> }}
          tOptions={{
            interpolation: { escapeValue: true },
          }}
          shouldUnescape
        />
      </div>
    </>
  );
}

const Item: FC<{ title?: string }> = ({ title }) => (
  <span id="span15" title={title}>
    {title}
  </span>
);

export default App;

i18n initialization:

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

await i18n.use(initReactI18next).init({
  lng: 'en',
  fallbackLng: 'en',
  interpolation: {
    escapeValue: false, // Global setting
  },
  react: {
    useSuspense: false,
    transSupportBasicHtmlNodes: false,
    transKeepBasicHtmlNodesFor: [],
  },
})

Actual output:

Case 1: Hello <Item title="World " '" Test" />!

  • Component tag not parsed - rendered as literal text

Case 2: Hello <span id="span15" title="World &amp;quot; &amp;#39;&amp;quot; Test"> World &amp;quot; &amp;#39;&amp;quot; Test </span>

  • Component parsed but values are double-escaped

Expected behavior

Both cases should properly parse the component AND render values with correct escaping (once, not twice).

Hello <span id="span15" title="World " '" Test">World " '" Test</span>

The component should:

  1. Parse and replace <Item /> tags correctly
  2. Escape user input once for XSS protection
  3. NOT double-escape when rendering as HTML attributes
  4. Work consistently regardless of global escapeValue setting

Current Workarounds (all have drawbacks)

Manually decode in each component - Not scalable for large projects

import he from 'he';

const Item: FC<{ title?: string }> = ({ title }) => {
  const decoded = title ? he.decode(title) : undefined;
  return (
    <span id="span15" title={decoded}>
      {decoded}
    </span>
  );
};

Environment

  • runtime version: node v22.19.0
  • i18next version: 25.6.3
  • os: Windows
  • Package manager: pnpm

Thank you for maintaining this excellent library! Any guidance would be greatly appreciated.

David0036 avatar Dec 10 '25 11:12 David0036

as far as I know - interpolation.escapeValue can safely be set to false as react already does escape (-> therefore also the double escaping if set true -> once by i18next, once by react)

jamuhl avatar Dec 10 '25 11:12 jamuhl

v16.4.1 should fix this... Can you try and let me know?

adrai avatar Dec 10 '25 13:12 adrai

Thank you so much for fixing the issue I reported. I really appreciate your quick response and the effort you put into maintaining this library. We can close this issue

David0036 avatar Dec 12 '25 07:12 David0036