Localization
The chat UI can render in any language, with automatic right-to-left layout. English ships built in and is always the fallback: any string you don't translate shows its English default, so partial translations are safe.
See the full list of translation keys for every translatable string and its English default.
Quick start
Choose a language with the locale attribute and point translations-url at a hosted catalog. No JavaScript required:
<saytv-chat
app-id="your-app-id"
locale="es"
translations-url="https://sdk.saytv.net/@saytv/chat-sdk@latest/locales/es.json"
></saytv-chat>SayTV hosts ready-made catalogs for the languages below, and right-to-left languages (like Arabic) lay out automatically.
Available languages
Catalogs are hosted, cached and CORS-enabled on the SDK CDN, so translations-url can point straight at them. translations-url is fetched by the browser at runtime, so if nothing translates see When the UI stays in English.
| Language | locale | Catalog URL |
|---|---|---|
| English (template) | en | …/locales/en.json |
| Arabic (RTL) | ar | …/locales/ar.json |
| Spanish | es | …/locales/es.json |
| French | fr | …/locales/fr.json |
Pin a version for production
@latest always serves the newest catalogs (short cache). For production, pin a version (e.g. @saytv/chat-sdk@2.2.2/locales/es.json) so the catalog matches the SDK you ship (immutable, cached for a year). The current version is in the top navigation and on the Changelog.
Review machine-generated translations
ar, es, and fr are ready to use but machine-translated, have a native speaker review them before production. en.json is the authoritative key set and is always safe as a template.
Supplying your own translations
Want a language we don't host, or to tweak the wording? Supply your own catalog, a flat map of keys to strings, e.g. { "auth.login.title": "…" }. Provide it one of two ways; either way, untranslated keys fall back to English, so you only translate what you need.
From a URL (no JavaScript)
Host a catalog and point translations-url at it:
<saytv-chat locale="fr" translations-url="https://your-cdn.com/fr.json"></saytv-chat>The file must be a flat, single-locale catalog matching the locale. Serve it with an Access-Control-Allow-Origin header that permits your page's origin. If the fetch fails the widget stays on English and logs an error to the console — it never breaks the page.
Don't hotlink the docs site
The /locales/*.json links on this site are for downloading and inspection. For runtime use, host your own copy (or use the SDK CDN), not the docs domain.
From an object (programmatic)
Assign the translations property, keyed by locale, handy when you already have the catalog in JS or switch languages dynamically:
const el = document.querySelector('saytv-chat');
el.translations = {
ar: { 'auth.login.title': 'مرحبا بعودتك', 'nav.chat': 'الدردشة' },
};
el.setAttribute('locale', 'ar'); // set translations first, then the localeInterpolation: some values contain {placeholder} tokens the widget fills at runtime. Keep them intact in your translation:
el.translations = { fr: { 'time.minutesAgo': 'il y a {count} min' } };Customizing & adding a language
Adding a language needs no SDK code, just a catalog:
- Download the English template,
en.json, which lists every key. - Translate the values, keeping any
{placeholder}tokens as-is. - Host it and point
translations-urlat it (or pass it viatranslations), then setlocale.
Keys are the contract
The translation keys are the stable interface. New SDK versions may add keys; until you translate them they fall back to English, so existing languages keep working.
Switching at runtime & RTL
Changing the locale attribute switches the language live, the widget re-renders, no reload:
document.querySelector('saytv-chat').setAttribute('locale', 'fr');Right-to-left languages (ar, he, fa, ur, …) set the text direction automatically.
One language per page
Language is shared by every widget on a page. Two <saytv-chat> elements (or a widget plus a bubble) cannot show different locales — the last one to set locale wins, and both re-render in it. The widget logs a warning when it detects this.
Quizzes and polls in their own language
A quiz or poll carries the language it was written in. That comes either from a client-wide default — set once in the client panel, for a client that runs a single language — or from the language a moderator picked on that specific interaction, which wins over the default. The widget renders that interaction's own labels — the submit button, the panel heading, the close button — in that language, whatever language the rest of the chat is in. So a French poll running in an English chat reads "Envoyer la réponse", not "Submit Answer".
Nothing to configure in the embed. Two things are worth knowing:
The catalog still has to be registered. A poll marked
frfalls back to English labels unless a French catalog has been supplied, exactly as the widget itself would. If you run interactions in several languages, register a catalog for each one up front:jsconst el = document.querySelector('saytv-chat'); el.translations = { fr: frCatalog, es: esCatalog };A catalog that arrives late — a
translations-urlfetch resolving after the poll opens — is picked up as soon as it lands; the panel re-renders.Right-to-left interactions lay themselves out. An Arabic poll inside an English chat gets
dir="rtl"on its own panel and nothing else moves.
With neither set, the interaction follows whatever language the widget is in — the behaviour before this existed.
Messages outside the panel stay in the widget's language. An error such as "Failed to submit vote" speaks for the app rather than for the poll, so it follows locale.
When the UI stays in English
The symptom: your content is in the right language — questions, options, messages, all of which come from your admin — but the widget's own labels ("Submit Answer", "Type a message…", the tab names) are English.
That means no catalog was registered for the active locale. locale on its own only selects a language; the catalog has to be supplied too. Check, in order:
- Is a catalog supplied at all?
locale="fr"with notranslations-urland notranslationsproperty renders English. Add one of the two. - Open the browser console. A failed catalog fetch logs
[saytv] Could not load the "…" translations from "…". The widget also fires anstv:errorevent, so you can surface it in your own monitoring. - Is it a CORS error?
Access to fetch at '…' has been blocked by CORS policymeans the host serving the catalog is not sendingAccess-Control-Allow-Origin. The request succeeds incurl(which does not enforce CORS) and fails in the browser, so test it from a page, not a terminal. Fix the header on that host, or embed the catalog with thetranslationsproperty, which needs no request at all. - Do the locale and the catalog match? A catalog is registered under the locale you name, not the language inside the file —
locale="de"withtranslations-urlpointing atfr.jsonregisters French as German. Region subtags are safe:fr-CAresolves to thefrcatalog.
The Playground's Code tab sidesteps all of this: pick a language and the snippet it generates embeds the whole catalog inline.
Dates & numbers
Dates and numbers format with the browser's native Intl APIs for the active locale, no setup needed. Relative-time words ("Just now", "Yesterday", "5m ago") come from the catalog (time.* keys), so they translate like any other string.
Framework wrappers
React and Vue expose locale, translations, and translationsUrl props.
// React
<SayTVChat
appId="your-app-id"
locale="es"
translationsUrl="https://sdk.saytv.net/@saytv/chat-sdk@latest/locales/es.json"
/><!-- Vue -->
<SayTVChat
app-id="your-app-id"
locale="es"
translations-url="https://sdk.saytv.net/@saytv/chat-sdk@latest/locales/es.json"
/>Both also accept a translations object. See the React and Vue guides.
Programmatic API
For advanced use, the SDK exports its i18n primitives directly:
import { registerTranslations, setLocale, t, locale, direction } from '@saytv/chat-sdk';
registerTranslations('ar', { 'auth.login.title': 'مرحبا بعودتك' });
setLocale('ar');
t('auth.login.title'); // 'مرحبا بعودتك'
t('time.minutesAgo', { count: 5 }); // interpolated
locale.value; // 'ar' (signal)
direction.value; // 'rtl' (signal)| Export | Description |
|---|---|
setLocale(tag) | Switch the active locale (unknown/empty → en). |
registerTranslations(tag, catalog) | Register or extend one locale's catalog. |
registerAllTranslations(map) | Register several catalogs at once, keyed by locale. |
loadTranslations(tag, url) | Fetch a hosted catalog and register it for tag. The imperative form of translations-url; resolves once registered. A failed fetch never throws — it logs an error, fires stv:error and leaves the UI on English. |
t(key, params?) | Resolve a key for the active locale, interpolating {tokens}. |
tIn(tag, key, params?) | Resolve a key in an explicit locale, ignoring the active one. Pass null to fall back to the active locale. This is what renders a quiz or poll in its own language. |
locale / direction | Signals holding the active tag and 'ltr'/'rtl'. |
directionFor(tag) | 'ltr'/'rtl' for an explicit locale. The counterpart to tIn. |