File size: 2,147 Bytes
cd99321
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import type { JSX } from 'react'
import { useTranslation } from './TranslationContext'

interface TransHtmlProps<T extends keyof JSX.IntrinsicElements = 'span'> {
  /**
   * Translation key whose template legitimately contains markup (e.g.
   * `'Turn <strong>{title}</strong> into a Journey'`).
   */
  html: string
  /**
   * Values to interpolate into `{paramName}` placeholders. Every value is
   * HTML-escaped before substitution, so passing user-controlled data is safe.
   */
  params?: Record<string, string | number>
  /**
   * Element to render. Defaults to `<span>`. Use the tag that fits the
   * surrounding flow — block, inline, list item, etc.
   */
  as?: T
  className?: string
  /**
   * `id` is forwarded so the component can be the target of `aria-labelledby`
   * or `htmlFor`. Other ARIA attributes can be added if needed; we intentionally
   * keep the surface small to discourage overloading this with arbitrary props.
   */
  id?: string
}

/**
 * Renders a translation that contains markup (e.g. `<strong>`) safely.
 *
 * Replaces the pattern that bit us in the Journey suggestion banner:
 *   <span dangerouslySetInnerHTML={{ __html: t('...', { user_input }) }} />
 *
 * That pattern interpolates `user_input` into the template *before* React
 * ever sees it, so a trip title like `<script>alert(1)</script>` would inject
 * a script tag. `TransHtml` runs `tHtml()` which:
 *
 *   1. HTML-escapes every interpolated value, neutralising it.
 *   2. Sanitises the resulting string against an inline tag allow-list.
 *
 * Use this for any user-controlled value that lands in a markup template.
 * Plain text-only templates can continue to use `<>{t('key', params)}</>`.
 */
export function TransHtml<T extends keyof JSX.IntrinsicElements = 'span'>({
  html,
  params,
  as,
  className,
  id,
}: TransHtmlProps<T>) {
  const { tHtml } = useTranslation()
  const Tag = (as ?? 'span') as keyof JSX.IntrinsicElements
  return (
    // eslint-disable-next-line react/no-danger -- sanitised by tHtml (defence in depth)
    <Tag className={className} id={id} dangerouslySetInnerHTML={{ __html: tHtml(html, params) }} />
  )
}