import { TLI18n, TLI18nAdapter, TLI18nMessageInput } from '@tldraw/editor' import { ReactNode, useCallback, useRef } from 'react' import { FormattedMessage, useIntl } from 'react-intl' import { TLUiTranslationValues, useCurrentTranslation, } from '../hooks/useTranslation/useTranslation' /** * Values for a message rendered as an element. Alongside the primitives a string message takes, a * value may be an element, and a function may be given for a tag that appears in the message, which * receives the text that tag encloses. Either way the markup stays out of the string, so a * translator gets one whole sentence to reorder rather than fragments that can only be concatenated. * * @example * * ```tsx * {chunks}, * help: Need help?, * }} * /> * ``` * * @public */ export type TLUiMessageValues = Record< string, TLUiTranslationValues[string] | ReactNode | ((chunks: ReactNode) => ReactNode) > /** @public */ export interface TLUiMessageProps extends TLI18nMessageInput { values?: TLUiMessageValues } /** * Renders a translated message. Use this wherever the message is text in the document; where a * string is needed instead, translate the message's id through `useTranslation`. * * The `id` is optional, so a message whose id a bundler plugin injects can be rendered from source * that carries none. The output is wrapped in a ``, which lets a tool * tell text that came through here from text that never did. * * @example * * ```tsx * * ``` * * @public @react */ export function F({ values, ...descriptor }: TLUiMessageProps) { // Without a plugin to inject one, react-intl throws on a missing id. Same default as // `defineMessages`, so the id an untransformed `` looks up is the one it was declared under. const id = descriptor.id ?? descriptor.defaultMessage return ( // Inline, so the marker span doesn't disturb the layout around it. Why it's here at all is // on `F` above. {/* The rule can't see a spread; `F`'s own callers are what it needs to check. */} {/* eslint-disable-next-line tldraw/enforce-default-message */} ) } /** * Builds the `TLI18nAdapter` the editor hands to code that has no access to React context, * such as a `ShapeUtil`. `` wires this up for you; call it yourself only when composing * `` by hand, and from inside a ``. * * @example * * ```tsx * function MyEditor(props: TldrawEditorProps) { * const i18n = useTldrawI18n() * return * } * ``` * * @public */ export function useTldrawI18n(): TLI18nAdapter { const translation = useCurrentTranslation() const intl = useIntl() // A stable function that reads the current translation, so whatever holds the adapter — the // editor, or an app that stores it — never sees it change identity on a locale switch. const i18nRef = useRef(null) i18nRef.current = { // The requested locale, as the `IntlProvider` has it, not the catalog that loaded. They // differ while a catalog is in flight and for a locale the SDK ships none for, and either // way the loaded one is English — so a French user's dates formatted as English. locale: intl.locale, // The loaded catalog's, deliberately: it has to agree with `useDirection`, which lays out // the rest of the UI. dir: translation.dir, // The whole descriptor, so react-intl formats `defaultMessage` itself for an id the // catalog doesn't have. Given only the id it returns the id, and the English then had to // be substituted in verbatim — which showed a plural message's own ICU source in a toast. // eslint-disable-next-line tldraw/enforce-default-message translate: (message, values) => intl.formatMessage(message, values), } return useCallback(() => i18nRef.current!, []) }