Cette page est publiée en anglais et en ukrainien ; la version anglaise figure ci-dessous.
Widget languages and right-to-left scripts
For developers, in code: which language the widget's chrome, the greeting and the answers come in, how the bundled dictionary and the translator share the work, and what mirrors for Arabic and Hebrew.
Three languages are in play on any page, and they are decided separately.
| What | Follows | Decided by |
|---|---|---|
| the widget’s own text — buttons, labels, the composer, the contact form | the page: <html lang> (pageLang) |
served localized in GET /widget/config?lang= |
| the greeting and the FAQ chips | the page’s language too | the owner’s text for that language, else translated from the owner’s base language |
| the answer | the visitor’s message | detected per message; a Cyrillic message is told apart from its neighbours by its letters and words; a short reply inside a consent gate keeps the conversation’s language |
The reply language is never the page’s. A Ukrainian question on an English page gets Ukrainian, and a language the owner never listed still gets an answer in it.
Where the chrome strings come from
GET /widget/config serves strings, a { key: text } map of the widget’s fixed text, localized
for lang. Three layers, cheapest first: a bundled dictionary for 17 languages (no model call), a
cache of earlier translations, and a translation on demand for any other language. A page may
override single keys in ShukaiConfig.strings — but that replaces the whole served map, so it is
for changing a word on purpose, not for localizing.
The keys: composer.placeholder, composer.label, send, launcher.open, teaser.dismiss;
voice.input.start / .stop / .listening / .denied / .error; language.label,
language.search, language.group.browser / .site / .common; conversation.listening,
.idle_hint, .muted_talking, .held; confirm.accept, confirm.decline, confirm.press,
confirm.dontPress; handoff.label (+ .whatsapp / .telegram / .viber / .email),
handoff.menu, .contact, .comment, .send, .disclaimer, .invalid, .sent, .sent_to,
.sending, .error, .form_title, .or, .dismiss; companion.ai_label, .back, .close,
.conversation, .delete_conversation, .dismiss, .history, .mic_paused, .more,
.new_conversation, .stop, .talk; error.limit, error.generic, error.unavailable.
The consent chips (confirm.*) are the exception to «chrome follows the page»: they are a
question inside the conversation, so they come in the answer’s language, sent with the done
frame as confirm_strings only when a gate is held.
Right to left
When <html lang> is an RTL language — ar, he, fa, ur, ps, sd, ug, yi, dv,
ckb — the widget writes data-dir="rtl" on its root and its text reads right to left. The page’s
own dir attribute is not consulted: an English widget on a mirrored page stays left to right.
| In an RTL language | |
|---|---|
| bubbles, the card, the teaser, the contact form, FAQ chips | text starts at the right; the ✕ moves to the left |
| the composer | the field starts at the right, Send ends at the left |
| where the widget sits | unchanged — the owner’s corner, a visitor’s drag, the side the bubbles open towards |
| a message | reads in the direction of its own text, in every widget (dir="auto") |
| code inside a message | left to right, in every widget — brackets and slashes keep their order |
Nothing is added to a left-to-right widget: every mirroring rule is scoped to
.root[data-dir="rtl"]. Two notes for anyone styling the widget themselves: prefer logical
properties (padding-inline-start, text-align: start) in ::part() rules and custom CSS so they
follow the direction; and the RTL padding rules are more specific than a plain selector, so a
custom_css rule like .sa-bubble { padding: … } applies to LTR visitors only — write the RTL
value as .root[data-dir="rtl"] .sa-bubble { … }.
Not mirrored yet: the launcher’s label side, the language picker’s list order, the contact form’s field order.
The spoken side
Which language the microphone listens for is the spoken-language chip’s business
(Voice); which voice reads the answer is chosen server-side per language,
with the owner’s overrides first. 49 languages have a voice today; the exact list is
GET /stt/languages.