import { warnOnce } from '@tldraw/editor'
import * as React from 'react'
import { useAssetUrls } from '../../context/asset-urls'
import { DEFAULT_TRANSLATION } from './defaultTranslation'
import { TLUiTranslationKey } from './TLUiTranslationKey'
import { TLUiTranslation, fetchTranslation } from './translations'
/** @public */
export interface TLUiTranslationProviderProps {
children: React.ReactNode
locale: string
/**
* A collection of overrides different locales.
*
* @example
*
* ```ts
*
* ```
*/
overrides?: Record>
}
/** @public */
export type TLUiTranslationContextType = TLUiTranslation
/** @internal */
export const TranslationsContext = React.createContext(null)
/** @public */
export function useCurrentTranslation() {
const translations = React.useContext(TranslationsContext)
if (!translations) {
throw new Error('useCurrentTranslation must be used inside of ')
}
return translations
}
/**
* Provides a translation context to the editor. Wrap this around components that use
* `useTranslation` (such as `TldrawSelectionForeground`) when you don't want to use the
* full `TldrawUiContextProvider`. Must be rendered inside an `AssetUrlsProvider`.
*
* @public @react
*/
export function TldrawUiTranslationProvider({
overrides,
locale,
children,
}: TLUiTranslationProviderProps) {
const getAssetUrl = useAssetUrls()
const [currentTranslation, setCurrentTranslation] = React.useState(() => {
if (overrides && overrides['en']) {
return {
locale: 'en',
label: 'English',
dir: 'ltr',
messages: { ...DEFAULT_TRANSLATION, ...overrides['en'] },
}
}
return {
locale: 'en',
label: 'English',
dir: 'ltr',
messages: DEFAULT_TRANSLATION,
}
})
React.useEffect(() => {
let isCancelled = false
async function loadTranslation() {
const translation = await fetchTranslation(locale, getAssetUrl)
if (translation && !isCancelled) {
if (overrides && overrides[locale]) {
setCurrentTranslation({
...translation,
messages: { ...translation.messages, ...overrides[locale] },
})
} else {
setCurrentTranslation(translation)
}
}
}
loadTranslation()
return () => {
isCancelled = true
}
}, [getAssetUrl, locale, overrides])
return (
{children}
)
}
/**
* Returns a function to translate a translation key into a string based on the current translation.
*
* @example
*
* ```ts
* const msg = useTranslation()
* const label = msg('style-panel.styles')
* ```
*
* @public
*/
export function useTranslation() {
const translation = React.useContext(TranslationsContext)
const messages = translation?.messages ?? DEFAULT_TRANSLATION
React.useEffect(() => {
if (!translation?.messages) {
warnOnce(
'No translation messages found, falling back to default translation. Wrap your app in , or in both and , to provide translations.'
)
}
}, [translation?.messages])
return React.useCallback(
function msg(id?: Exclude | string) {
return messages[id as TLUiTranslationKey] ?? id
},
[messages]
)
}
/**
* Returns the current text direction ('ltr' or 'rtl') based on the current translation.
*
* @public
*/
export function useDirection() {
const translation = useCurrentTranslation()
return translation.dir
}
export function untranslated(string: string) {
return string as TLUiTranslationKey
}