Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
232 lines
8.0 KiB
Plaintext
232 lines
8.0 KiB
Plaintext
---
|
|
title: Internationalization
|
|
created_at: 12/29/2025
|
|
updated_at: 12/29/2025
|
|
keywords:
|
|
- i18n
|
|
- internationalization
|
|
- localization
|
|
- translations
|
|
- language
|
|
- locale
|
|
- rtl
|
|
status: published
|
|
date: 12/29/2025
|
|
readability: 9
|
|
voice: 9
|
|
completeness: 9
|
|
accuracy: 9
|
|
notes: Consider linking to an i18n example if one exists.
|
|
---
|
|
|
|
Tldraw's UI supports 49 languages out of the box, including right-to-left languages like Arabic, Hebrew, Farsi, and Urdu. The translation system loads language files on demand, detects the user's browser language, and lets you override any translation string or add custom ones.
|
|
|
|
## Setting the locale
|
|
|
|
The user's locale is stored in [user preferences](/sdk-features/user-preferences). By default, tldraw detects the browser's language and selects the closest match from supported languages. The default main menu also includes a language submenu where users can change it.
|
|
|
|
To set the locale from your application, update the user's locale preference. The UI reads its language from `editor.user.getLocale()`, so this takes effect immediately:
|
|
|
|
```tsx
|
|
import { Tldraw } from 'tldraw'
|
|
import 'tldraw/tldraw.css'
|
|
|
|
export default function App() {
|
|
return (
|
|
<div style={{ position: 'fixed', inset: 0 }}>
|
|
<Tldraw
|
|
onMount={(editor) => {
|
|
// Change the locale to French
|
|
editor.user.updateUserPreferences({ locale: 'fr' })
|
|
|
|
// Get the current locale
|
|
const locale = editor.user.getLocale() // "fr"
|
|
}}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
```
|
|
|
|
The `locale` value uses lowercase language codes: `'en'`, `'fr'`, `'de'`, `'ja'`, `'zh-cn'`, `'ar'`, and so on.
|
|
|
|
The `Tldraw` component also has a `locale` prop. Today it only sets the language for the outer translation provider (used by the loading screen); the editor UI itself keeps reading the user preference, so prefer `updateUserPreferences` to change the visible language.
|
|
|
|
### Automatic detection
|
|
|
|
When no locale is set, tldraw uses [getDefaultTranslationLocale](?) to detect the user's preferred language from the browser:
|
|
|
|
```ts
|
|
import { getDefaultTranslationLocale } from 'tldraw'
|
|
|
|
// Returns 'fr', 'en', 'zh-cn', etc. based on browser settings
|
|
const locale = getDefaultTranslationLocale()
|
|
```
|
|
|
|
For each entry in the browser's `navigator.languages` array, in order, it:
|
|
|
|
1. Tries an exact match against supported languages
|
|
2. Falls back to a language-only match (e.g., `'fr-CA'` → `'fr'`)
|
|
3. Applies region defaults for Chinese (`'zh'` → `'zh-cn'`), Portuguese (`'pt'` → `'pt-br'`), Korean (`'ko'` → `'ko-kr'`), and Hindi (`'hi'` → `'hi-in'`)
|
|
|
|
The first entry that matches wins. If none match, it defaults to `'en'`.
|
|
|
|
## Using translations in components
|
|
|
|
The [useTranslation](?) hook returns a function for looking up translation strings. Unknown keys are returned as-is, so you can pass plain text or your own keys:
|
|
|
|
```tsx
|
|
import { useTranslation } from 'tldraw'
|
|
|
|
function CopyButton() {
|
|
const msg = useTranslation()
|
|
return <button>{msg('action.copy')}</button>
|
|
}
|
|
```
|
|
|
|
Use [useCurrentTranslation](?) for the full translation object, including locale and text direction:
|
|
|
|
```tsx
|
|
import { useCurrentTranslation } from 'tldraw'
|
|
|
|
function LocaleInfo() {
|
|
const translation = useCurrentTranslation()
|
|
return (
|
|
<div dir={translation.dir}>
|
|
<p>Locale: {translation.locale}</p>
|
|
<p>Label: {translation.label}</p>
|
|
</div>
|
|
)
|
|
}
|
|
```
|
|
|
|
The [TLUiTranslation](?) object contains a `locale` code (e.g., `'fr'`), a `label` in the native script (e.g., `'Français'`), a `messages` record with all translation strings, and a `dir` indicating text direction (`'ltr'` or `'rtl'`).
|
|
|
|
## Overriding translations
|
|
|
|
Pass translation overrides through the `overrides` prop on `Tldraw`:
|
|
|
|
```tsx
|
|
import { Tldraw } from 'tldraw'
|
|
|
|
function App() {
|
|
return (
|
|
<Tldraw
|
|
overrides={{
|
|
translations: {
|
|
en: {
|
|
'action.copy': 'Copy to clipboard',
|
|
'action.paste': 'Paste from clipboard',
|
|
},
|
|
fr: {
|
|
'action.copy': 'Copier dans le presse-papiers',
|
|
},
|
|
},
|
|
}}
|
|
/>
|
|
)
|
|
}
|
|
```
|
|
|
|
Overrides are merged with the base translations for each language. English is the fallback: any key missing from the target language uses the English string. If you add your own keys (for example a custom tool label), provide at least an `en` override for them.
|
|
|
|
### Translation keys
|
|
|
|
Translation keys follow a hierarchical naming convention. Common prefixes include `action.*` for user actions like copy and paste, `tool.*` for tool names, `menu.*` for menu labels, `style-panel.*` for style panel UI, and `a11y.*` for accessibility announcements.
|
|
|
|
The [TLUiTranslationKey](?) type provides autocomplete for all available keys:
|
|
|
|
```ts
|
|
import type { TLUiTranslationKey } from 'tldraw'
|
|
|
|
const key: TLUiTranslationKey = 'action.copy'
|
|
```
|
|
|
|
## Supported languages
|
|
|
|
Import [LANGUAGES](?) from `tldraw` for the complete list of supported languages:
|
|
|
|
```tsx
|
|
import { LANGUAGES } from 'tldraw'
|
|
|
|
function LanguageSelector() {
|
|
return (
|
|
<select>
|
|
{LANGUAGES.map(({ locale, label }) => (
|
|
<option key={locale} value={locale}>
|
|
{label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
)
|
|
}
|
|
```
|
|
|
|
Each entry in `LANGUAGES` is a [TLLanguage](?) with a `locale` code and a `label` in that language's native script. Translation files are only loaded for locales in this list, so you can't add a new language by supplying a translation file alone.
|
|
|
|
The supported languages include: English, Spanish, French, German, Italian, Portuguese (Brazilian and European), Dutch, Russian, Polish, Czech, Danish, Finnish, Swedish, Hungarian, Norwegian, Romanian, Turkish, Ukrainian, Greek, Croatian, Slovenian, Arabic, Hebrew, Farsi, Urdu, Hindi, Tamil, Telugu, Malayalam, Kannada, Bengali, Gujarati, Nepali, Marathi, Punjabi, Thai, Khmer, Vietnamese, Indonesian, Malay, Filipino, Somali, Japanese, Korean, Simplified Chinese, Traditional Chinese (Taiwan), Catalan, and Galician.
|
|
|
|
## Right-to-left support
|
|
|
|
Languages like Arabic, Hebrew, Farsi, and Urdu automatically set `dir: 'rtl'` in the translation object. The tldraw UI respects this direction. Layout and text alignment mirror automatically. The `dir` attribute is set on the editor's root container, and the built-in components use CSS logical properties (`margin-inline-start`, `inset-inline-end`, and so on) so they flip without per-component code.
|
|
|
|
When building custom UI components, use the [useDirection](?) hook to get the current text direction. It returns `'ltr'` or `'rtl'` from the active translation context, the same value as `useCurrentTranslation().dir`:
|
|
|
|
```tsx
|
|
import { useDirection } from 'tldraw'
|
|
|
|
function CustomPanel() {
|
|
const dir = useDirection()
|
|
return <aside dir={dir}>{/* Panel content */}</aside>
|
|
}
|
|
```
|
|
|
|
Use `useCurrentTranslation` instead when you also need the locale, label, or `messages` from the same render.
|
|
|
|
## Building a language picker
|
|
|
|
Here's a language picker that updates the user's locale preference:
|
|
|
|
```tsx
|
|
import { LANGUAGES, useEditor, useValue } from 'tldraw'
|
|
|
|
function LanguagePicker() {
|
|
const editor = useEditor()
|
|
const currentLocale = useValue('locale', () => editor.user.getLocale(), [editor])
|
|
|
|
return (
|
|
<select
|
|
value={currentLocale}
|
|
onChange={(e) => {
|
|
editor.user.updateUserPreferences({ locale: e.target.value })
|
|
}}
|
|
>
|
|
{LANGUAGES.map(({ locale, label }) => (
|
|
<option key={locale} value={locale}>
|
|
{label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
)
|
|
}
|
|
```
|
|
|
|
The language change applies immediately without a page reload. The preference persists to localStorage and synchronizes across browser tabs.
|
|
|
|
## Translation loading
|
|
|
|
Translations load asynchronously when the locale changes. The system:
|
|
|
|
1. Fetches the translation JSON file for the selected locale
|
|
2. Merges it with English translations (ensuring all keys have values)
|
|
3. Applies any custom overrides
|
|
4. Updates the translation context
|
|
|
|
During loading, the UI uses the previous translations to avoid flicker. If loading fails, English remains as the fallback.
|
|
|
|
To use translations outside the full tldraw UI, wrap your components in [TldrawUiTranslationProvider](?) (inside an `AssetUrlsProvider`).
|
|
|
|
## Related examples
|
|
|
|
- [Custom translations and overrides](/examples/ui/custom-language-translations) - Override translation strings and use them in custom UI.
|