Files
TREK/client/src/i18n/TransHtml.tsx
T
Maurice fc7d8b5d12 Migrate TREK 3 to NestJS + React 19 with a shared Zod contract layer
Brownfield strangler migration of the backend onto NestJS modules
(auth, trips, days, places, assignments, packing, todo, budget,
reservations, collab, files, photos, journey, share, settings, backup,
oidc, oauth, admin, atlas, vacay, weather, airports, maps, categories,
tags, notifications, system-notices) served through a per-prefix
dispatcher, keeping the existing SQLite/better-sqlite3 DB and JWT
httpOnly cookie auth, with behavioural parity for every route.

Client: React 19 upgrade, "page = wiring container + data hook"
pattern across all pages, per-domain Zustand stores bound to
@trek/shared contracts, and decomposition of the large components
(DayPlanSidebar, PackingListPanel, CollabNotes, FileManager,
MemoriesPanel, PlacesSidebar, CollabChat, SystemNoticeModal,
BudgetPanel, PlaceFormModal, ...) into focused render units backed by
in-file hooks.

Apply the shared global request pipeline (helmet/CSP, CORS, HSTS,
forced HTTPS, the global MFA policy and request logging) to the NestJS
instance as well, so a migrated route is protected identically to the
legacy fallback rather than bypassing it.
2026-05-30 02:39:26 +02:00

59 lines
2.1 KiB
TypeScript

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) }} />
)
}